Skip to content

shell: a hosted module may refuse a close and ask first (makepad#65) - #186

Merged
ymote merged 1 commit into
mainfrom
feat/hosted-close-confirm
Sep 30, 2026
Merged

ymote merged 1 commit into
mainfrom
feat/hosted-close-confirm

Conversation

@ymote

@ymote ymote commented Sep 30, 2026

Copy link
Copy Markdown
Contributor

Stacked on #185, the makepad bcb0583b repin. Review and merge that first, then retarget this PR to main. Part of #179, the Phase 1 item "hosted close path skips the running-job confirmation".

Problem

makepad#65 gave AppModule a close-veto hook: close_requested(cx, root) -> CloseDecision and ModuleCloseAction::Confirmed. The shell still dropped a module instance at once. So closing an in-process Terminal tile, or quitting OctoSense, sent SIGHUP to its running jobs without asking.

Design

  • module_host::CloseGate (owned by ModuleHost) tracks each pending close as client → root widget_uid, plus whether a quit is waiting.
    • ModuleHost::ask_close calls close_requested outside the isolate under contain_outside, as the executor calls are. A panic there is contained and treated as Allow.
    • A failed instance, or one that is not a module, is always Allow.
    • teardown and release_failed forget the instance's pending mark.
  • App::request_close, which the tile close button, Super+W, the bar, Alt+F4, the assistant's close tool and a Recents swipe all go through:
    • Allow: tears the instance down as before.
    • Veto: keeps the tile and the instance and calls activate_client so the tile comes to the front with its question. It logs asks before closing.
  • handle_actions: a ModuleCloseAction::Confirmed from a pending root's uid calls remove_client. A Confirmed from any other uid is ignored. A cancel emits nothing, so nothing changes.
  • Quit: the shell menu's Quit, Event::QuitRequested (Cmd+Q, /quit) and Event::WindowCloseRequested (the window's close button, /close) call ask_before_quit.
    • It asks every instance. If any refuses, the quit waits, and the first instance that refused comes to the front.
    • shell_handle_event calls cx.quit() once the last refusing instance confirms, or fails.
    • QuitReason::Signal, a crash and a module fault never ask.
    • If the person closes one app while a quit is waiting, the waiting quit is dropped. Saying yes later then closes only that app, not the shell.
  • The assistant's close tool answers "asks the person before closing". A Recents swipe on an app that refuses opens that app instead of staying on Recents.

Tests

crates/shell/src/module_close_tests.rs, 11 new tests:

  • CloseGate:
    • Allow tears down.
    • Veto keeps the instance and records it as pending.
    • Only the pending root's uid confirms.
    • A second or stale Confirmed does nothing.
    • A quit nobody refuses goes ahead.
    • A quit waits for every refusal.
    • A failed instance does not hold a quit.
    • Closing one app drops a waiting quit.
  • ModuleHost, with real probe instances:
    • Allow versus Veto.
    • A Confirmed from another instance's root is ignored.
    • A module that panics in close_requested is let go.
    • The shell quit waits for two refusing instances and ignores one that allows.

Results:

  • cd phone && cargo test --locked --features mobile-apps -p octosense-shell: 595 passed (584 + 11).
  • ... --features mobile-apps,app-terminal -p octosense-shell terminal: 14 passed.
  • cargo check --locked -p octosense: ok.

Instrument evidence (macOS)

The run was MAKEPAD_HIDE_WINDOWS=1 MAKEPAD_REMOTE=8471 octosense --module terminal --test-action launch-terminal, release build, with a temp OCTOSENSE_HOME, OCTOSENSE_APP_DATA and OCTOS_APP_CORE_DIR.

Step Result
sleep 300, then click the tile's close button The tile stays. Its bar reads Closing the terminal ends "sleep". Close anyway? with Close (Enter) and Cancel (Esc). Log: terminal (client 1) asks before closing
Esc The prompt goes away. The tile and sleep are still there
/quit Same prompt. The shell is still up. Log: quit waits: clients [1]
Esc, then /close?w=0 {"ok":1,"refused":1} and the same prompt
Esc, click close again, then Enter client 1 confirmed its close, then terminal.1 torn down. The tile is gone, sleep has ended and the shell stays up, because the earlier waiting quit was dropped
A new Terminal with sleep 301, /quit, then Enter client 2 confirmed its close, then every instance confirmed; quitting. The shell exits and sleep has ended

No panics in the log.

Not covered here

  • The macOS desktop Terminal runs as a process by default (native-apps.json hosting). Only --module terminal, or a terminal: Module line, hosts it in-process, which is where this change applies.
  • For a process client, request_close still sends WmEvent::CloseRequested and Kill back to back. The process terminal's veto on CloseRequested is therefore overridden, and its jobs still end without asking. This is the same gap makepad#65 notes for the in-tree WM. Fixing it needs the shell to wait for the app's answer, or for its exit, before it kills. That should be a separate change.

🤖 Generated with Claude Code

makepad#65 gave AppModule a close-veto hook (close_requested ->
CloseDecision, ModuleCloseAction::Confirmed). The shell dropped a module
instance at once, so closing an in-process Terminal SIGHUPed its running
jobs without asking.

- request_close asks a live instance first. Allow: torn down as before.
  Veto: the tile and instance stay, the tile comes to the front so its
  question shows, and the root's uid is remembered (module_host CloseGate).
- A ModuleCloseAction::Confirmed from a pending root's uid tears that
  instance down; from any other uid it is ignored. A cancel emits nothing.
- Quit (the shell menu's Quit, Cmd+Q / QuitRequested, the window's close
  button) asks every instance and waits while any refuses; it quits when
  the last one confirms. A termination signal, a crash or a fault never
  asks. A later close of one app abandons a waiting quit.
- A module that panics while answering is contained and let go; a failed
  instance no longer holds a quit.
- The assistant's close tool says the app is asking; a Recents swipe on a
  refusing app opens it instead of staying on Recents.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@ymote
ymote changed the base branch from chore/repin-makepad-bcb0583b to main September 30, 2026 05:46
@ymote
ymote merged commit 996cb00 into main Sep 30, 2026
chrislearn pushed a commit to chrislearn/OctoSense that referenced this pull request Oct 8, 2026
)

A process client's close sent CloseRequested and Kill back to back, so
closing a process Terminal's tile (the macOS default hosting) still
ended its running jobs without asking. Stacked on OctoSense-org#186, which did the
same for in-process modules.

- process_close.rs: a pure close state machine per process client.
  An app that says WmRequest::AsksBeforeClose (makepad
  feat/wm-close-refused) is asked with CloseRequested alone; its tile
  and process stay. Its answer is its own WmRequest::Close (yes: closed
  at once, without asking back), CloseRefused (it is asking the person:
  no deadline, the tile comes to the front) or an exit.
- Fallback for an app that does not answer: a second close of the same
  tile while the first is unanswered ends it at once (a repeat within
  0.5 s is the same click); left alone it is ended after 5 s. A refusal
  is never a licence to hang: every close asks again, a live app
  re-answers at once, a frozen one falls to the fallback. So an app that
  is visibly asking is never killed, and a hung one is always closable.
- An app that never declared closes exactly as before (CloseRequested +
  Kill, the tile leaves at once), so apps without the new words, and a
  Terminal from an older makepad, lose nothing.
- Shell quit (menu, Cmd+Q, the window's close, /quit) asks declared
  apps the same way and waits while any module or app is deciding;
  it goes when the last says yes. A second quit forces unanswered apps.
  A termination signal or a crash still asks nobody.
- The words are read off the wire by their exact text, so this works
  with the current makepad pin; on_wm_request gains a catch-all arm so
  a repin to the new variants compiles.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
ymote added a commit that referenced this pull request Oct 9, 2026
App Hub #186 admits WebAssembly components when the manifest requires
wasm-components-v1, validates their imports (only the WASI packages a host
scopes to the app) and tells reviewers what each reaches. With the store
on it, a component shipped as a system app goes through admission, and the
new service test calls it as the app's script would: typed JSON, the
instance's state from one request to the next, no folder without storage.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
ymote added a commit that referenced this pull request Oct 10, 2026
App Hub #191 repins App Hub to the same runtime, and with the budget gate
gone a runaway app tool handler still fails at once (a captured-error
sink catches the per-evaluation limit); net alone hands an isolate its
network module. It also brings #186 (components under
wasm-components-v1), #188 (wasi:http and octosense:host admission) and

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant