diff --git a/kernel/src/Api/BuildNo.hpp b/kernel/src/Api/BuildNo.hpp index a9ad340..5b60b68 100644 --- a/kernel/src/Api/BuildNo.hpp +++ b/kernel/src/Api/BuildNo.hpp @@ -12,4 +12,4 @@ #pragma once -#define MONTAUK_BUILD_NUMBER 57 +#define MONTAUK_BUILD_NUMBER 64 diff --git a/kernel/src/Api/Power.hpp b/kernel/src/Api/Power.hpp index 56dad0d..07dd2c0 100644 --- a/kernel/src/Api/Power.hpp +++ b/kernel/src/Api/Power.hpp @@ -13,8 +13,28 @@ #include #include +#include "Syscall.hpp" + namespace Montauk { + // Pending graceful power-off request, set by the desktop and consumed by + // login.elf. Kernel-global IPC channel (one outstanding request system-wide). + // `inline` so the single definition is shared across translation units. + inline int g_pendingPowerAction = POWER_REQ_QUERY; + + // SYS_POWER_REQUEST. action == POWER_REQ_QUERY reads and clears the pending + // action; any other value records it as the pending action. Returns the + // pending action for queries, or 0 when recording one. + static int64_t Sys_PowerRequest(int action) { + if (action == POWER_REQ_QUERY) { + int pending = g_pendingPowerAction; + g_pendingPowerAction = POWER_REQ_QUERY; + return (int64_t)pending; + } + g_pendingPowerAction = action; + return 0; + } + static void Sys_Reset() { if (Efi::g_ResetSystem) { /* Switch to kernel PML4 which has identity-mapped UEFI runtime regions */ diff --git a/kernel/src/Api/Storage.hpp b/kernel/src/Api/Storage.hpp index 5394306..c148577 100644 --- a/kernel/src/Api/Storage.hpp +++ b/kernel/src/Api/Storage.hpp @@ -109,6 +109,12 @@ namespace Montauk { return (int64_t)Fs::FsProbe::MountPartition(partIndex, driveNum); } + // Flush all block-device write caches and cleanly unmount disk-backed + // volumes ahead of power-off. Returns the number of volumes unmounted. + static int64_t Sys_FsSync() { + return (int64_t)Fs::FsProbe::SyncAndUnmountAll(); + } + // Format a partition with a filesystem. Returns 0 on success, -1 on error. static int64_t Sys_FsFormat(const FsFormatParams* params) { if (params == nullptr) return -1; diff --git a/kernel/src/Api/Syscall.cpp b/kernel/src/Api/Syscall.cpp index d1527cb..ed760f0 100644 --- a/kernel/src/Api/Syscall.cpp +++ b/kernel/src/Api/Syscall.cpp @@ -145,6 +145,8 @@ namespace Montauk { case SYS_SHUTDOWN: Sys_Shutdown(); return 0; + case SYS_POWER_REQUEST: + return Sys_PowerRequest((int)frame->arg1); case SYS_GETTIME: if (!UserMemory::Writable(frame->arg1)) return -1; Sys_GetTime((DateTime*)frame->arg1); @@ -322,6 +324,8 @@ namespace Montauk { return (int64_t)Sys_GptAdd((const GptAddParams*)frame->arg1); case SYS_FSMOUNT: return (int64_t)Sys_FsMount((int)frame->arg1, (int)frame->arg2); + case SYS_FS_SYNC: + return Sys_FsSync(); case SYS_FSFORMAT: if (!UserMemory::Readable(frame->arg1)) return -1; return (int64_t)Sys_FsFormat((const FsFormatParams*)frame->arg1); diff --git a/kernel/src/Api/Syscall.hpp b/kernel/src/Api/Syscall.hpp index 1bd1982..6afe04a 100644 --- a/kernel/src/Api/Syscall.hpp +++ b/kernel/src/Api/Syscall.hpp @@ -249,6 +249,21 @@ namespace Montauk { static constexpr uint64_t SYS_THREAD_JOIN = 132; static constexpr uint64_t SYS_THREAD_SELF = 133; + /* Storage.hpp -- flush + unmount persistent volumes for power-off */ + static constexpr uint64_t SYS_FS_SYNC = 134; + + /* Power.hpp -- cross-process graceful power-off request channel */ + static constexpr uint64_t SYS_POWER_REQUEST = 135; + + // Graceful power-off request actions (SYS_POWER_REQUEST). The desktop posts + // a pending action and exits; login.elf reads it, runs the shutdown stages, + // then issues the matching SYS_SHUTDOWN / SYS_RESET. + enum PowerRequestAction : int { + POWER_REQ_QUERY = 0, // read-and-clear the pending action + POWER_REQ_SHUTDOWN = 1, + POWER_REQ_REBOOT = 2, + }; + static constexpr uint32_t CLIPBOARD_MAX_TEXT_BYTES = 256 * 1024; static constexpr uint32_t IPC_SIGNAL_READABLE = 1u << 0; diff --git a/kernel/src/Drivers/Storage/Ahci.cpp b/kernel/src/Drivers/Storage/Ahci.cpp index 0f4ae98..6c0591d 100644 --- a/kernel/src/Drivers/Storage/Ahci.cpp +++ b/kernel/src/Drivers/Storage/Ahci.cpp @@ -745,6 +745,9 @@ namespace Drivers::Storage::Ahci { bdev.WriteSectors = [](void* ctx, uint64_t lba, uint32_t count, const void* buffer) -> bool { return WriteSectors((int)(uintptr_t)ctx, lba, count, buffer); }; + bdev.Flush = [](void* ctx) -> bool { + return FlushCache((int)(uintptr_t)ctx); + }; bdev.Ctx = (void*)(uintptr_t)i; bdev.SectorCount = g_ports[i].SectorCount; bdev.SectorSize = g_ports[i].SectorSizeLog; @@ -826,6 +829,37 @@ namespace Drivers::Storage::Ahci { return ok; } + bool FlushCache(int port) { + if (!g_initialized || port < 0 || port >= MAX_PORTS || !g_ports[port].Active) { + return false; + } + + int slot = FindFreeSlot(port); + if (slot < 0) { + KernelLogStream(ERROR, "AHCI") << "FlushCache: no free command slot"; + return false; + } + + CommandHeader* hdr = &g_ports[port].CmdList[slot]; + CommandTable* tbl = g_ports[port].CmdTables[slot]; + + // FLUSH CACHE EXT is a non-data command: no PRDT, no transfer direction. + memset(tbl, 0, sizeof(CommandTable) + sizeof(PrdtEntry) * MAX_PRDT_ENTRIES); + + FisRegH2D* fis = (FisRegH2D*)tbl->CommandFis; + fis->FisType = (uint8_t)FisType::RegH2D; + fis->CmdCtl = 1; // Command + fis->Command = ATA_CMD_FLUSH_CACHE_EX; + fis->Device = (1 << 6); // LBA mode + + hdr->CflPmpA = 5; // CFL = FIS length in dwords (FisRegH2D = 20 bytes) + hdr->Flags = 0; // read direction (no data either way) + hdr->PrdtLength = 0; + hdr->PrdByteCount = 0; + + return IssueCommand(port, slot); + } + bool WriteSectors(int port, uint64_t lba, uint32_t count, const void* buffer) { if (!g_initialized || port < 0 || port >= MAX_PORTS || !g_ports[port].Active) { return false; diff --git a/kernel/src/Drivers/Storage/Ahci.hpp b/kernel/src/Drivers/Storage/Ahci.hpp index 3859fc8..a147de9 100644 --- a/kernel/src/Drivers/Storage/Ahci.hpp +++ b/kernel/src/Drivers/Storage/Ahci.hpp @@ -167,9 +167,10 @@ namespace Drivers::Storage::Ahci { // ATA commands // ========================================================================= - constexpr uint8_t ATA_CMD_IDENTIFY = 0xEC; - constexpr uint8_t ATA_CMD_READ_DMA_EX = 0x25; // READ DMA EXT (48-bit LBA) - constexpr uint8_t ATA_CMD_WRITE_DMA_EX = 0x35; // WRITE DMA EXT (48-bit LBA) + constexpr uint8_t ATA_CMD_IDENTIFY = 0xEC; + constexpr uint8_t ATA_CMD_READ_DMA_EX = 0x25; // READ DMA EXT (48-bit LBA) + constexpr uint8_t ATA_CMD_WRITE_DMA_EX = 0x35; // WRITE DMA EXT (48-bit LBA) + constexpr uint8_t ATA_CMD_FLUSH_CACHE_EX = 0xEA; // FLUSH CACHE EXT (48-bit) // ========================================================================= // Constants @@ -252,6 +253,10 @@ namespace Drivers::Storage::Ahci { // Write sectors to a SATA device bool WriteSectors(int port, uint64_t lba, uint32_t count, const void* buffer); + // Flush the device's write cache to media (FLUSH CACHE EXT). Returns true on + // success. Used during graceful shutdown to make pending writes durable. + bool FlushCache(int port); + // Get info about a specific port const PortInfo* GetPortInfo(int port); diff --git a/kernel/src/Drivers/Storage/BlockDevice.hpp b/kernel/src/Drivers/Storage/BlockDevice.hpp index 24c7841..3972781 100644 --- a/kernel/src/Drivers/Storage/BlockDevice.hpp +++ b/kernel/src/Drivers/Storage/BlockDevice.hpp @@ -22,6 +22,10 @@ namespace Drivers::Storage { struct BlockDevice { bool (*ReadSectors)(void* ctx, uint64_t lba, uint32_t count, void* buffer); bool (*WriteSectors)(void* ctx, uint64_t lba, uint32_t count, const void* buffer); + // Flush the device's volatile write cache to non-volatile media. Optional: + // a null pointer means the device has no write-back cache to flush (so a + // completed WriteSectors is already durable). Returns true on success. + bool (*Flush)(void* ctx); void* Ctx; uint64_t SectorCount; uint16_t SectorSize; diff --git a/kernel/src/Drivers/Storage/Nvme.cpp b/kernel/src/Drivers/Storage/Nvme.cpp index 82f1404..1921f6a 100644 --- a/kernel/src/Drivers/Storage/Nvme.cpp +++ b/kernel/src/Drivers/Storage/Nvme.cpp @@ -665,6 +665,9 @@ namespace Drivers::Storage::Nvme { bdev.WriteSectors = [](void* ctx, uint64_t lba, uint32_t count, const void* buffer) -> bool { return WriteSectors((int)(uintptr_t)ctx, lba, count, buffer); }; + bdev.Flush = [](void* ctx) -> bool { + return Flush((int)(uintptr_t)ctx); + }; bdev.Ctx = (void*)(uintptr_t)i; bdev.SectorCount = g_namespaces[i].SectorCount; bdev.SectorSize = (uint16_t)g_namespaces[i].SectorSize; @@ -770,6 +773,22 @@ namespace Drivers::Storage::Nvme { return ok; } + bool Flush(int ns) { + if (!g_initialized || ns < 0 || ns >= g_nsCount || !g_namespaces[ns].Active) { + return false; + } + + // NVM Flush: commits the namespace's volatile write cache to media. + SqEntry cmd = {}; + cmd.Opcode = IO_CMD_FLUSH; + cmd.Nsid = g_namespaces[ns].Nsid; + + SubmitIoCommand(cmd); + + CqEntry cqe; + return WaitIoCompletion(cqe); + } + bool WriteSectors(int ns, uint64_t lba, uint32_t count, const void* buffer) { if (!g_initialized || ns < 0 || ns >= g_nsCount || !g_namespaces[ns].Active) { return false; diff --git a/kernel/src/Drivers/Storage/Nvme.hpp b/kernel/src/Drivers/Storage/Nvme.hpp index e2f87a5..102a9e3 100644 --- a/kernel/src/Drivers/Storage/Nvme.hpp +++ b/kernel/src/Drivers/Storage/Nvme.hpp @@ -120,6 +120,7 @@ namespace Drivers::Storage::Nvme { // NVM I/O opcodes // ========================================================================= + constexpr uint8_t IO_CMD_FLUSH = 0x00; constexpr uint8_t IO_CMD_READ = 0x02; constexpr uint8_t IO_CMD_WRITE = 0x01; @@ -187,6 +188,10 @@ namespace Drivers::Storage::Nvme { // Write sectors to an NVMe namespace bool WriteSectors(int ns, uint64_t lba, uint32_t count, const void* buffer); + // Flush the namespace's volatile write cache to media (NVM Flush command). + // Returns true on success. Used during graceful shutdown. + bool Flush(int ns); + // Get info about a specific namespace const NamespaceInfo* GetNamespaceInfo(int ns); diff --git a/kernel/src/Fs/FsProbe.cpp b/kernel/src/Fs/FsProbe.cpp index 96ca425..5ccfb2d 100644 --- a/kernel/src/Fs/FsProbe.cpp +++ b/kernel/src/Fs/FsProbe.cpp @@ -6,6 +6,7 @@ #include "FsProbe.hpp" #include +#include #include #include @@ -195,4 +196,43 @@ namespace Fs::FsProbe { return unmounted; } + int SyncAndUnmountAll() { + // 1. Flush every block device's volatile write cache to media. FAT32 and + // ext2 are write-through, so file data already reached the device; the + // flush makes the device commit it to non-volatile storage. + int devCount = Drivers::Storage::GetBlockDeviceCount(); + for (int i = 0; i < devCount; i++) { + const auto* dev = Drivers::Storage::GetBlockDevice(i); + if (dev && dev->Flush) { + dev->Flush(dev->Ctx); + } + } + + // 2. Cleanly unmount every disk-backed VFS drive. The ramdisk has no + // backing block device (GetBlockDeviceForDrive < 0) and is left + // mounted so the rest of userspace keeps running until power-off. + int drives[Vfs::MaxDrives]; + int n = Vfs::VfsDriveList(drives, Vfs::MaxDrives); + int unmounted = 0; + + for (int i = 0; i < n; i++) { + int driveNum = drives[i]; + if (GetBlockDeviceForDrive(driveNum) < 0) continue; + + if (Vfs::UnregisterDrive(driveNum) == 0) { + unmounted++; + for (int p = 0; p < Drivers::Storage::Gpt::MaxPartitions; p++) { + if (g_mounted[p] && g_driveForPart[p] == driveNum) { + g_mounted[p] = false; + g_driveForPart[p] = -1; + } + } + } + } + + Kt::KernelLogStream(Kt::OK, "FsProbe") << "Synced and unmounted " + << unmounted << " disk-backed volume(s) for power-off"; + return unmounted; + } + }; diff --git a/kernel/src/Fs/FsProbe.hpp b/kernel/src/Fs/FsProbe.hpp index aa836b8..89f579b 100644 --- a/kernel/src/Fs/FsProbe.hpp +++ b/kernel/src/Fs/FsProbe.hpp @@ -38,4 +38,10 @@ namespace Fs::FsProbe { // drive isn't backed by a partition (e.g. ramdisk). int GetBlockDeviceForDrive(int driveNum); + // Prepare persistent storage for power-off: flush every block device's + // write cache to media, then cleanly unmount all disk-backed VFS drives. + // The ramdisk (which has no backing block device) is left mounted. Returns + // the number of disk-backed volumes that were unmounted. + int SyncAndUnmountAll(); + }; diff --git a/programs/include/Api/Syscall.hpp b/programs/include/Api/Syscall.hpp index 30d4be1..591d38b 100644 --- a/programs/include/Api/Syscall.hpp +++ b/programs/include/Api/Syscall.hpp @@ -173,6 +173,21 @@ namespace Montauk { static constexpr uint64_t SYS_THREAD_JOIN = 132; static constexpr uint64_t SYS_THREAD_SELF = 133; + // Flush + unmount persistent volumes for power-off + static constexpr uint64_t SYS_FS_SYNC = 134; + + // Cross-process graceful power-off request channel + static constexpr uint64_t SYS_POWER_REQUEST = 135; + + // Graceful power-off request actions (SYS_POWER_REQUEST). The desktop posts + // a pending action and exits; login.elf reads it, runs the shutdown stages, + // then issues the matching SYS_SHUTDOWN / SYS_RESET. + enum PowerRequestAction : int { + POWER_REQ_QUERY = 0, // read-and-clear the pending action + POWER_REQ_SHUTDOWN = 1, + POWER_REQ_REBOOT = 2, + }; + static constexpr uint32_t CLIPBOARD_MAX_TEXT_BYTES = 256 * 1024; static constexpr uint32_t IPC_SIGNAL_READABLE = 1u << 0; diff --git a/programs/include/montauk/syscall.h b/programs/include/montauk/syscall.h index 8db7e69..15509a7 100644 --- a/programs/include/montauk/syscall.h +++ b/programs/include/montauk/syscall.h @@ -369,6 +369,15 @@ namespace montauk { return (int)syscall0(Montauk::SYS_SUSPEND); } + // Graceful power-off request channel. The desktop posts a pending action + // (Montauk::POWER_REQ_SHUTDOWN / POWER_REQ_REBOOT) then exits; login.elf + // reads it with POWER_REQ_QUERY (read-and-clear), runs the shutdown stages, + // and finally calls shutdown()/reset(). Returns the pending action for a + // query, or 0 when posting one. + inline int power_request(int action) { + return (int)syscall1(Montauk::SYS_POWER_REQUEST, (uint64_t)(int64_t)action); + } + // Mouse inline void mouse_state(Montauk::MouseState* out) { syscall1(Montauk::SYS_MOUSESTATE, (uint64_t)out); } inline void set_mouse_bounds(int32_t maxX, int32_t maxY) { @@ -436,6 +445,11 @@ namespace montauk { inline int fs_mount(int partIndex, int driveNum) { return (int)syscall2(Montauk::SYS_FSMOUNT, (uint64_t)partIndex, (uint64_t)driveNum); } + // Flush all block-device write caches and cleanly unmount disk-backed + // volumes ahead of power-off. Returns the number of volumes unmounted. + inline int fs_sync() { + return (int)syscall0(Montauk::SYS_FS_SYNC); + } inline int fs_format(const Montauk::FsFormatParams* params) { return (int)syscall1(Montauk::SYS_FSFORMAT, (uint64_t)params); } diff --git a/programs/libs/libprintersapplet/obj/src/libprintersapplet.o b/programs/libs/libprintersapplet/obj/src/libprintersapplet.o index bacff44..b876a53 100644 Binary files a/programs/libs/libprintersapplet/obj/src/libprintersapplet.o and b/programs/libs/libprintersapplet/obj/src/libprintersapplet.o differ diff --git a/programs/libs/libtimezonesapplet/obj/src/libtimezonesapplet.o b/programs/libs/libtimezonesapplet/obj/src/libtimezonesapplet.o index dd3f781..0160485 100644 Binary files a/programs/libs/libtimezonesapplet/obj/src/libtimezonesapplet.o and b/programs/libs/libtimezonesapplet/obj/src/libtimezonesapplet.o differ diff --git a/programs/src/desktop/apps/apps_common.hpp b/programs/src/desktop/apps/apps_common.hpp index 63bf256..bd77d9d 100644 --- a/programs/src/desktop/apps/apps_common.hpp +++ b/programs/src/desktop/apps/apps_common.hpp @@ -226,4 +226,13 @@ void open_reboot_dialog(DesktopState* ds); void open_wordprocessor(DesktopState* ds); void open_shutdown_dialog(DesktopState* ds); void open_sleep_dialog(DesktopState* ds); + +// Hand a graceful power-off request to login.elf and exit the desktop so the +// user returns to the login screen, where the shutdown stages run (Bluetooth +// teardown, filesystem flush) before the final ACPI power-off / reset. Pass +// Montauk::POWER_REQ_SHUTDOWN or Montauk::POWER_REQ_REBOOT. +[[noreturn]] inline void desktop_request_power(int action) { + montauk::power_request(action); + montauk::exit(0); +} bool desktop_poll_external_windows(DesktopState* ds); diff --git a/programs/src/desktop/dialogs.cpp b/programs/src/desktop/dialogs.cpp index 169c13f..65eba4f 100644 --- a/programs/src/desktop/dialogs.cpp +++ b/programs/src/desktop/dialogs.cpp @@ -69,7 +69,7 @@ static void reboot_dialog_on_mouse(Window* win, MouseEvent& ev) { rs->hover_cancel = cb.contains(lx, ly); if (ev.left_pressed()) { - if (rs->hover_reboot) montauk::reset(); + if (rs->hover_reboot) desktop_request_power(Montauk::POWER_REQ_REBOOT); if (rs->hover_cancel) { for (int i = 0; i < rs->ds->window_count; i++) { if (rs->ds->windows[i].app_data == rs) { @@ -86,7 +86,7 @@ static void reboot_dialog_on_key(Window* win, const Montauk::KeyEvent& key) { if (!rs || !key.pressed) return; if (key.ascii == '\n' || key.ascii == '\r') { - montauk::reset(); + desktop_request_power(Montauk::POWER_REQ_REBOOT); } if (key.scancode == 0x01) { // Escape for (int i = 0; i < rs->ds->window_count; i++) { @@ -181,7 +181,7 @@ static void shutdown_dialog_on_mouse(Window* win, MouseEvent& ev) { ss->hover_cancel = cb.contains(lx, ly); if (ev.left_pressed()) { - if (ss->hover_shutdown) montauk::shutdown(); + if (ss->hover_shutdown) desktop_request_power(Montauk::POWER_REQ_SHUTDOWN); if (ss->hover_cancel) { for (int i = 0; i < ss->ds->window_count; i++) { if (ss->ds->windows[i].app_data == ss) { @@ -198,7 +198,7 @@ static void shutdown_dialog_on_key(Window* win, const Montauk::KeyEvent& key) { if (!ss || !key.pressed) return; if (key.ascii == '\n' || key.ascii == '\r') { - montauk::shutdown(); + desktop_request_power(Montauk::POWER_REQ_SHUTDOWN); } if (key.scancode == 0x01) { // Escape for (int i = 0; i < ss->ds->window_count; i++) { diff --git a/programs/src/desktop/launcher.cpp b/programs/src/desktop/launcher.cpp index cfa9f8f..75369fe 100644 --- a/programs/src/desktop/launcher.cpp +++ b/programs/src/desktop/launcher.cpp @@ -339,11 +339,11 @@ static void launcher_activate_selected(DesktopState* ds) { break; case LAUNCHER_ITEM_COMMAND_REBOOT: desktop_close_launcher(ds); - montauk::reset(); + desktop_request_power(Montauk::POWER_REQ_REBOOT); return; case LAUNCHER_ITEM_COMMAND_SHUTDOWN: desktop_close_launcher(ds); - montauk::shutdown(); + desktop_request_power(Montauk::POWER_REQ_SHUTDOWN); return; case LAUNCHER_ITEM_SPECIAL_FOLDER: if (item->path[0] != '\0') { diff --git a/programs/src/login/Makefile b/programs/src/login/Makefile index 3e4c11d..a9b9f86 100644 --- a/programs/src/login/Makefile +++ b/programs/src/login/Makefile @@ -71,6 +71,7 @@ SRCS := \ login_wallpaper.cpp \ login_render.cpp \ login_input.cpp \ + login_shutdown.cpp \ stb_truetype_impl.cpp \ font_data.cpp OBJS := $(addprefix $(OBJDIR)/,$(SRCS:.cpp=.o)) diff --git a/programs/src/login/login.hpp b/programs/src/login/login.hpp index 0534346..c219cd6 100644 --- a/programs/src/login/login.hpp +++ b/programs/src/login/login.hpp @@ -146,3 +146,10 @@ bool try_first_boot_submit(LoginState* ls); bool try_login(LoginState* ls); void handle_key(LoginState* ls, const Montauk::KeyEvent& key); void handle_mouse(LoginState* ls); + +// Graceful shutdown view (login_shutdown.cpp). draw_shutdown_screen paints the +// status card; perform_graceful_shutdown runs the staged power-off (Bluetooth +// teardown, filesystem flush) and finally issues the ACPI power-off / reset, so +// it never returns. `action` is Montauk::POWER_REQ_SHUTDOWN or POWER_REQ_REBOOT. +void draw_shutdown_screen(LoginState* ls, const char* heading, const char* status); +[[noreturn]] void perform_graceful_shutdown(LoginState* ls, int action); diff --git a/programs/src/login/login_input.cpp b/programs/src/login/login_input.cpp index d584212..334c9f5 100644 --- a/programs/src/login/login_input.cpp +++ b/programs/src/login/login_input.cpp @@ -45,6 +45,16 @@ void launch_desktop_session(LoginState* ls) { montauk::setuser(pid, ls->username); montauk::waitpid(pid); } + + // The desktop hands off a graceful power-off by posting a request and + // exiting (see desktop_request_power). Pick it up now that waitpid has + // returned and run the shutdown stages; perform_graceful_shutdown never + // returns. A normal logout leaves no request, so we fall through. + int req = montauk::power_request(Montauk::POWER_REQ_QUERY); + if (req == Montauk::POWER_REQ_SHUTDOWN || req == Montauk::POWER_REQ_REBOOT) { + perform_graceful_shutdown(ls, req); + } + finish_desktop_session(ls); } @@ -210,10 +220,10 @@ void handle_mouse(LoginState* ls) { } return; } else if (lo.shutdown_button.contains(mx, my)) { - montauk::shutdown(); + perform_graceful_shutdown(ls, Montauk::POWER_REQ_SHUTDOWN); return; } else if (lo.reboot_button.contains(mx, my)) { - montauk::reset(); + perform_graceful_shutdown(ls, Montauk::POWER_REQ_REBOOT); return; } } diff --git a/programs/src/login/login_shutdown.cpp b/programs/src/login/login_shutdown.cpp new file mode 100644 index 0000000..dd1ed65 --- /dev/null +++ b/programs/src/login/login_shutdown.cpp @@ -0,0 +1,189 @@ +/* + * login_shutdown.cpp + * Graceful shutdown view and staged power-off for the MontaukOS login screen + * Copyright (c) 2026 Daniel Hammer +*/ + +#include "login.hpp" +#include + +using namespace gui; + +// Minimum time each shutdown stage stays on screen, so the status message is +// readable even when the underlying work completes near-instantly. +static constexpr uint64_t STAGE_MIN_VISIBLE_MS = 600; + +// Per-stage watchdog budgets. Each stage's blocking work runs on a disposable +// worker thread; if it overruns its budget we abandon the worker and move on, +// so an unresponsive device can never wedge the power-off. +// - Bluetooth: a disconnect is normally sub-second; 4 s tolerates a slow +// controller. Bailing here is safe -- it does not risk data. +// - Filesystem flush: kept generous (10 s) so a legitimately slow flush on +// real hardware is never cut short and writes are not lost; only a truly +// wedged storage controller hits this bound. +static constexpr uint64_t BT_STAGE_TIMEOUT_MS = 4000; +static constexpr uint64_t FS_STAGE_TIMEOUT_MS = 10000; + +void draw_shutdown_screen(LoginState* ls, const char* heading, const char* status) { + Framebuffer& fb = ls->fb; + int sw = ls->screen_w; + int sh = ls->screen_h; + + // Same packed/scratch handling as draw_login_screen: Canvas assumes a + // tightly packed buffer, so compose into the scratch buffer when the + // framebuffer pitch is not width*4. + bool packed = fb.pitch() == sw * (int)sizeof(uint32_t); + uint32_t* target = packed ? fb.buffer() : ls->compose; + if (!target) return; + Canvas c(target, sw, sh); + + const mtk::Theme& th = ls->theme; + int sfh = system_font_height(); + + // ==== Layout (mirrors the login/setup card chrome, minus the footer) ==== + // Titlebar holds the app title; the body holds a heading + a status line, + // matching the login/setup heading + subtitle. The card is sized to fit the + // content with symmetric padding -- no empty footer band. + const char* window_title = "MontaukOS"; + int content_x = CONTENT_PAD_X; + + int y = TITLEBAR_H + CONTENT_TOP_PAD; + int heading_y = y; + y += sfh + 6; + int status_y = y; + y += sfh; + + int card_h = y + CONTENT_TOP_PAD + 10; + int card_x = (sw - CARD_W) / 2; + int card_y = (sh - card_h) / 2; + + Rect card = {card_x, card_y, CARD_W, card_h}; + Rect titlebar = {card_x, card_y, CARD_W, TITLEBAR_H}; + + // ==== Background ==== + if (ls->has_wallpaper && ls->bg_wallpaper) { + montauk::memcpy(target, ls->bg_wallpaper, + (uint64_t)sw * sh * sizeof(uint32_t)); + } else { + c.fill(BG_COLOR); + } + + // ==== Card ==== + int so = 4; // drop-shadow offset + c.fill_rect_alpha(card.x + so, card.y + card.h, card.w, so, colors::SHADOW); + c.fill_rect_alpha(card.x + card.w, card.y + so, so, card.h, colors::SHADOW); + c.fill_rect_alpha(card.x + card.w, card.y + card.h, so, so, colors::SHADOW); + + c.fill_rect(card.x, card.y, card.w, card.h, CARD_BG); + c.fill_rect(titlebar.x, titlebar.y, titlebar.w, titlebar.h, TITLEBAR_BG); + c.rect(card.x, card.y, card.w, card.h, CARD_BORDER); + c.hline(titlebar.x, titlebar.y + titlebar.h - 1, titlebar.w, CARD_BORDER); + + // ==== Title + heading + status ==== + int window_tw = text_width(window_title); + c.text(titlebar.x + (titlebar.w - window_tw) / 2, + titlebar.y + (TITLEBAR_H - sfh) / 2, window_title, th.text); + c.text(card_x + content_x, card_y + heading_y, heading, th.text); + c.text(card_x + content_x, card_y + status_y, status, th.text_subtle); + + // ==== Present ==== + if (!packed) fb.copy_from(ls->compose, sw * (int)sizeof(uint32_t)); + fb.flip(); +} + +// Show a stage message, hold it on screen for a moment, then return so the +// caller can perform the stage's work. +static void show_stage(LoginState* ls, const char* heading, const char* status) { + draw_shutdown_screen(ls, heading, status); + montauk::sleep_ms(STAGE_MIN_VISIBLE_MS); +} + +namespace { + +// A unit of shutdown work plus a flag the worker sets when it finishes. `done` +// is polled across threads, so all access goes through atomics. +struct StageWork { + void (*fn)(); + volatile bool done; +}; + +int stage_worker(void* arg) { + StageWork* w = (StageWork*)arg; + w->fn(); + __atomic_store_n(&w->done, true, __ATOMIC_RELEASE); + return 0; +} + +// Run fn() on a worker thread, waiting up to timeout_ms for it to finish. If a +// device wedges, only the disposable worker blocks (in its kernel syscall); the +// shutdown flow keeps moving. Returns true if fn() completed in time, false if +// it was abandoned. A timed-out worker is intentionally left running and never +// joined (joining would re-block us) -- power-off reclaims it in moments. +bool run_stage(void (*fn)(), uint64_t timeout_ms) { + auto* w = (StageWork*)montauk::malloc(sizeof(StageWork)); + if (!w) { fn(); return true; } // no memory: best-effort inline + w->fn = fn; + w->done = false; + + int tid = montauk::thread_spawn(stage_worker, w); + if (tid < 0) { fn(); montauk::mfree(w); return true; } // can't isolate: inline + + const uint64_t step = 50; + uint64_t waited = 0; + while (!__atomic_load_n(&w->done, __ATOMIC_ACQUIRE) && waited < timeout_ms) { + montauk::sleep_ms(step); + waited += step; + } + + if (__atomic_load_n(&w->done, __ATOMIC_ACQUIRE)) { + montauk::thread_join(tid, nullptr); // reclaim the worker's stack + montauk::mfree(w); + return true; + } + return false; +} + +// Stage bodies -- no captured state, so they double as bare thread entries. +void stage_disconnect_bluetooth() { + Montauk::BtDevInfo devs[8]; + int n = montauk::bt_list(devs, 8); + for (int i = 0; i < n; i++) { + if (devs[i].connected) { + montauk::bt_disconnect(devs[i].bdAddr); + } + } +} + +void stage_flush_filesystems() { + montauk::fs_sync(); +} + +} // namespace + +void perform_graceful_shutdown(LoginState* ls, int action) { + const bool rebooting = (action == Montauk::POWER_REQ_REBOOT); + const char* heading = rebooting ? "Restarting" : "Shutting Down"; + + // ==== Stage 1: disconnect connected Bluetooth devices ==== + // Bounded so an unresponsive controller cannot block the (critical) + // filesystem flush that follows. + show_stage(ls, heading, "Disconnecting Bluetooth devices..."); + if (!run_stage(stage_disconnect_bluetooth, BT_STAGE_TIMEOUT_MS)) { + show_stage(ls, heading, "Bluetooth is unresponsive, continuing..."); + } + + // ==== Stage 2: flush writes and unmount filesystems ==== + show_stage(ls, heading, "Flushing file systems..."); + if (!run_stage(stage_flush_filesystems, FS_STAGE_TIMEOUT_MS)) { + show_stage(ls, heading, "Storage is unresponsive, continuing..."); + } + + // ==== Stage 3: dispatch the ACPI power-off / reset ==== + show_stage(ls, heading, rebooting ? "Restarting now..." : "Powering off..."); + if (rebooting) { + montauk::reset(); + } else { + montauk::shutdown(); + } + __builtin_unreachable(); +} diff --git a/programs/src/shutdown/main.cpp b/programs/src/shutdown/main.cpp index ade308a..cbcdce3 100644 --- a/programs/src/shutdown/main.cpp +++ b/programs/src/shutdown/main.cpp @@ -8,5 +8,8 @@ extern "C" void _start() { montauk::print("Shutting down...\n"); + // This low-level utility bypasses the login graceful-shutdown view, so flush + // pending writes and unmount disk-backed volumes here before powering off. + montauk::fs_sync(); montauk::shutdown(); }