feat: disconnect Bluetooth devices, flush disks on shutdown/reboot, display progress in login.elf window

This commit is contained in:
2026-06-06 18:27:19 +02:00
parent b51ab42eb9
commit cee21738ee
24 changed files with 418 additions and 12 deletions
+15
View File
@@ -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;
+14
View File
@@ -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);
}
@@ -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);
+4 -4
View File
@@ -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++) {
+2 -2
View File
@@ -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') {
+1
View File
@@ -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))
+7
View File
@@ -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);
+12 -2
View File
@@ -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;
}
}
+189
View File
@@ -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 <montauk/thread.h>
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();
}
+3
View File
@@ -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();
}