From cee21738eec44791a92e0e0e7d4df67e8bbe49e9 Mon Sep 17 00:00:00 2001 From: Daniel Hammer Date: Sat, 6 Jun 2026 18:27:19 +0200 Subject: [PATCH] feat: disconnect Bluetooth devices, flush disks on shutdown/reboot, display progress in login.elf window --- kernel/src/Api/BuildNo.hpp | 2 +- kernel/src/Api/Power.hpp | 20 ++ kernel/src/Api/Storage.hpp | 6 + kernel/src/Api/Syscall.cpp | 4 + kernel/src/Api/Syscall.hpp | 15 ++ kernel/src/Drivers/Storage/Ahci.cpp | 34 ++++ kernel/src/Drivers/Storage/Ahci.hpp | 11 +- kernel/src/Drivers/Storage/BlockDevice.hpp | 4 + kernel/src/Drivers/Storage/Nvme.cpp | 19 ++ kernel/src/Drivers/Storage/Nvme.hpp | 5 + kernel/src/Fs/FsProbe.cpp | 40 ++++ kernel/src/Fs/FsProbe.hpp | 6 + programs/include/Api/Syscall.hpp | 15 ++ programs/include/montauk/syscall.h | 14 ++ .../obj/src/libprintersapplet.o | Bin 18352 -> 18464 bytes .../obj/src/libtimezonesapplet.o | Bin 18488 -> 18600 bytes programs/src/desktop/apps/apps_common.hpp | 9 + programs/src/desktop/dialogs.cpp | 8 +- programs/src/desktop/launcher.cpp | 4 +- programs/src/login/Makefile | 1 + programs/src/login/login.hpp | 7 + programs/src/login/login_input.cpp | 14 +- programs/src/login/login_shutdown.cpp | 189 ++++++++++++++++++ programs/src/shutdown/main.cpp | 3 + 24 files changed, 418 insertions(+), 12 deletions(-) create mode 100644 programs/src/login/login_shutdown.cpp 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 bacff44c40fb491cb4cd21f4aff04720511af74d..b876a53078b763ca9bd2ce3aa34899ca25d9243d 100644 GIT binary patch delta 3479 zcmZ8jeQXp(6yLeF*Y35{yA+vjtCSY|{jqnw)?N$tINH1OsDPznsl=SMC?bDA0Y3-{ zrE0)pi8RmJD2buLL=X~a3P@;7t*E2{grf+w5V29ir-uF^h>+;a?AvUdEJlT3oNP>BOCnGA zX(En5#G;cJR${TOKgYJuVl7Qj6jBWrlUnShDu?`1$1h93HzgQqnW89}+!N8tI5vml zeAFU_vm`yudL@dq-iTT_?m%S9)Or6#QPzZH)0v`J9Z`!c&d*GXVmP5*WtS&7B9Bhr z40>`VIS=*8DdZ5mlI)E8n7O_KpC+fr9bzcA|7dcnYHw(22tT{9(Z49X$X~zM-vIfk zf5!(J!VOIeYWg43mL-t}XwRnVd{zZM{Lqy{;YhY!`v93HLr`pN=E-k^@oY-Ma4$PZ z3c!~`EonA*A*V#E;udTiGN#E5??Z16)vm@G^eIVoLnMcW6b$xw4QtcUey?QT4Vig5DTn$zs*Q=RP?VdJO$D?wo7K>l zM@a;R^Md3yIP$4>mh}i9b)x^2$@6XmA5+g@<}H$X30m`Mr~qSakkr^X!{-)>Z^Zh5 z#QPw^Vhy3XS5of*DWIAU&wfl&3&F+I-|!TdB=s;f7f^B>))(j@z^oEQoBw~(1l&s{ zz6bf45?=-rEMh$7GfPtUL1rNhd9iK{lB&fUl?hAyX{_W*i7$rsLaM!v-rkkePUvIm zljw7Yr2Y!yOm*O?`z7@xIErZ64fK3MQd6ujx2VM8NQ5;-0c{AgOTq5voA{ro8zM!N zbi?(cAUO)IVoCxKDh`s5psSc#rmFB|aS0iP(PCZOgh>uaF6tn>|Jh)FYCrVoO|+ z`1`P;L?`p%lM-Fqi3d!_*-UN?j5E*IkabIHYLamyyb_-Y`MU0U91q+gsj87BbJ$i@7~w@NJVXfv2$C>C%QSe7WRGyc^f%Yz7#_$Ff(l zzlDLHd5E8-D77dwJtQc~U93b8cD5PdFHH1v@KsQFzrwEmi93%#YX#L#OZG>xFUFvk z*{6E!DjFzPh5hX)dtNkCeE`Hm^TorDu{=8e<+2|iDt{=lVe&rc_0UT3y~)^VsPfuj zy4+aFh}y+tWeHYM3ZctGH9bn@eix$Zfgz^Omeeq+ewbitNK)TIbpd2nQms)^J5XH; z^-Nt6rE)liw|pJ6SJJjlNgc#`_DKBCco#mC_z}Fnhb6uf`L8AZdGzy(!52(+=ml)` zo6?32=!kvWRnd?Ozd?Bcx#2ndV|XOK5ATat;#09S0qCuw`ST_9J?@r`=OliA|9I7M zE7=Vp-yr!0)VYJ?A`H(Bk|uD}RH!qujcp#Nuc;uF&{5-MFM#11uVu+Bu+>&GDpc#W zIA_CFwpb6twO;ZzsD3YdT8G&6Hgx#C>h%=EiytEX3QNfhum!3u8CfuoO`a^+8mJ~m bU^w6<+d&O_$s7m;z3R1UW7C^Z@6Y%TW51e+ delta 3404 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 VAa7rUu+NorFP_oY8N7dg=D$P$kLLgY delta 3649 zcmZ`*Yiv|S6uz@NKmoq4O*^IrR?wKjNy_=S!cK+1 zQ8-!QZxyyC9#Gh&_!A1JDmsT+m|6l;gSE^uQ-k-S zucUtAa86IrG&UP1q%Uz+x&)jBUFpHFnXYL$(x2&765B0fZ`>e3vTTyac_fZ>o{Sr0 zoR1qMNpr`V$<9l0r1NV0;06u~rD?8w((X7*BObLV8roTf^~@y-O< z)rud$4tIId+d?@HzqltQfj~*o2ktf(gtKl(JM{K6y;{{l)2gJCXsP1o7C!?r3b?*B zQF5BL!0N`-H^3BXt27pEk$OJzHKDB_=t&cmlx?Hcjrl%s!H)%H>4LuJnuMo?_Fd}SpxF>8DV20z>$$V6rD40b-{W7lFPt?88UBvYjs4gVx z9ylx1BTg&(R-&c>o5uB9=(~=nb>I_f9#&vGQG1|8sH5n6fT(#85o$X6{y@|d5EJUh zsE!hK7u=j?uvL&#%=K$%!__`F&;WuI(f`re3UNP4{s5TR| z7e<6SKFz9OJ5is8j1sP|MBfjIngwPFZyLZexr3+=v3gO$A4L8r;lGFO67Kjr2@aN& zIsQzAYb62J1HMwOx8n>`(9=GrUC>e*)IY&`%qMCg#7cvn)mX(aQE#U}Zdn;S1fep6 zEryOVu9xE5J4j4dLa$J-;{m=x)JrfTB8c0;Fjyy;2G^g%biJ6Eo$e5{3H3gzexfEo zw_*4mVh&4)dKLLr!oL8l++df%SI%`eZqs&Rvld!}x)bw1K-7MSlm}S^hRcJ#YZzf1 z_IBor?So4A%@Fc&&sP}HAnGj_bov6mc}O=C`423_>x3VIdp?6Lf}#pTx681D*z`kN zg~95eyMnVu_*z(Y<4t#iSZ)PY$@MN&ld%Z)!k!0TC1+jGRN2ftu&dJ0&tX=}h-Car#3 z@g!)g;bAXPPh$zH2w#A_N%&!08p>7;BSNig0KNjJEdIr@)A2F}N|9NJW*@WlGH=nw6MjFQwTXlW+?dJWtLJ7u zQ77ZYQ%v|*@sL*#J`!DD-|l3e1Dk!6^+9a*L&uqXI6f!9YQWV{7Quz#9F6F&q7kyYrdT1)AIO$L8_VkA5KYpm;e9( 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(); }