Skip to content

feat: wire OS sleep/wake power events (suspend-edge session snapshot)#97

Open
pmaxhogan wants to merge 1 commit into
mainfrom
feat/os-sleep-wake-power-events
Open

feat: wire OS sleep/wake power events (suspend-edge session snapshot)#97
pmaxhogan wants to merge 1 commit into
mainfrom
feat/os-sleep-wake-power-events

Conversation

@pmaxhogan

Copy link
Copy Markdown
Owner

Closes #33.

What

Wires the OS suspend/resume power events that DESIGN s5.10 assumed but that were left as per-OS TODO seams. The orchestrator already had the on_power_event / complete_resume logic (and unit tests); nothing produced the edges in production, so the app relied on the 30s power poll (which never runs the resume sequence). Now the suspend/resume edge is detected at the edge and the s5.10.2 / s5.10.3 sequences run.

Per-platform backend choices

  • Windows (verified on-host): PowerRegisterSuspendResumeNotification with DEVICE_NOTIFY_CALLBACK. The system calls our callback directly - no hidden window or message pump. Maps PBT_APMSUSPEND -> Suspending, PBT_APMRESUMEAUTOMATIC/PBT_APMRESUMESUSPEND -> Resumed.
  • macOS (compile-only here): IOKit IORegisterForSystemPower on a dedicated CFRunLoop thread; kIOMessageSystemWillSleep -> Suspending, kIOMessageSystemHasPoweredOn -> Resumed, and we ack kIOMessageCanSystemSleep / will-sleep via IOAllowPowerChange so the system never blocks on us. Chosen over NSWorkspace because it is the documented C sleep/wake API, matches the existing IOKit power reader + the Windows callback shape, and needs no AppKit main-thread run loop (structurally identical to the tested Windows path).
  • Linux (compile-only here): logind org.freedesktop.login1.Manager.PrepareForSleep(bool) via a zbus #[proxy]-generated signal stream (start=true -> Suspending, false -> Resumed). zbus is pinned to the tokio reactor to match the app runtime.

Plumbing

  • driven-power: new SleepWakeEvent edge type, PowerSource::subscribe_sleep_wake (default inert receiver so no impl breaks), and a single uniform SleepWakeMonitor RAII handle whose per-OS teardown unregisters / stops-and-joins / aborts on drop or stop().
  • driven-core: the run loop subscribes to the edge stream, maps to PowerEvent, calls on_power_event, and fires the deferred complete_resume after the 30s defer via a fixed-instant timer arm (inline, so it respects the single-in-flight guard). A suspend supersedes a stale pending resume.
  • app-shell: assembly::build_account starts the monitor per account; AccountHandle holds it and tears it down on quit (no orphaned OS handle / thread / task), mirroring the existing power-poller bookkeeping.
  • FakePowerSource::push_sleep_wake lets tests drive edges deterministically.

Empirically tested vs compile-only

  • Empirically tested (this Windows host): the real Win32 registration + unregistration lifecycle; the exact PDEVICE_NOTIFY_CALLBACK_ROUTINE mapping suspend/resume codes to a broadcast a subscriber receives (a real machine-suspend cannot run in an automated test, so the callback is invoked directly with the OS codes); the pure PBT_* mapping; and a driven-core run-loop integration test that pushes Suspending -> Resumed through FakePowerSource and asserts the loop broadcasts the Power events and completes a from-scratch Wake cycle to Idle after the defer.
  • Compile-only here (implementer builds on Windows): the macOS and Linux OS backends. Both are cross-checked and clippy-clean from Windows via cargo check/cargo clippy --target x86_64-apple-darwin and --target x86_64-unknown-linux-gnu (type-check without linking); CI's 3-OS matrix is the runtime gate. Their pure mapping fns + the subscribe/broadcast plumbing are unit-tested on every OS.

Tests added

  • driven-power: Windows PBT mapping, callback->broadcast, real register/teardown lifecycle; macOS IOKit-message mapping + subscribe delivery; Linux PrepareForSleep mapping + subscribe delivery.
  • driven-test-fixtures: push_sleep_wake reaches a subscriber.
  • driven-core: run loop consumes sleep/wake edges and completes the deferred resume.

Notes / deferred

  • Resumable upload sessions are already checkpointed per-chunk into pending_ops.payload_json (PersistedResumable{session,acked_offset}) and stale sessions are discarded by the executor's existing 6-day age check, so the "session snapshot" requirement is satisfied by that continuous persistence; this PR adds the missing edge detection + resume sequence that drives it.
  • DESIGN s5.10.3 step 3 (pre-emptive token refresh on wake) is unchanged: RefreshingTokenSource refreshes on-demand on the first post-wake request. The "Suspending..." tray tooltip (s5.10.2 cosmetic) is not wired (the event bridge still ignores Power events) - deferred as a nicety.

🤖 Generated with Claude Code

https://claude.ai/code/session_014fLmkjpFkiuky72eLBijL4

Closes #33. Adds per-OS suspend/resume notifications and wires the
orchestrator to run the DESIGN s5.10.2 / s5.10.3 sequences at the suspend/
resume edge instead of waiting up to 30s for the power poll.

driven-power:
- SleepWakeEvent edge type + PowerSource::subscribe_sleep_wake (default
  inert receiver) + a uniform SleepWakeMonitor RAII teardown handle.
- Windows: PowerRegisterSuspendResumeNotification + DEVICE_NOTIFY_CALLBACK
  (no window / message pump); maps PBT_APMSUSPEND/RESUME* to edges.
- macOS: IOKit IORegisterForSystemPower on a dedicated CFRunLoop thread
  (kIOMessageSystemWillSleep / HasPoweredOn; acks CanSystemSleep).
- Linux: logind PrepareForSleep DBus signal via zbus #[proxy] stream
  (zbus on the tokio reactor).

driven-core: the run loop consumes the sleep/wake edge stream, calls
on_power_event, and fires the deferred complete_resume after the 30s defer.

app-shell: assembly starts the monitor per account; AccountHandle stores it
and tears it down on quit (no orphaned OS handle / thread / task).

Tested: Windows backend verified on-host (real registration + callback ->
broadcast path); a run-loop integration test drives suspend -> resume ->
Wake cycle via FakePowerSource. macOS/Linux backends are compile-only here
(implementer on Windows) but cross-checked + clippy-clean via
`cargo check/clippy --target {x86_64-apple-darwin,x86_64-unknown-linux-gnu}`.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014fLmkjpFkiuky72eLBijL4
@github-project-automation github-project-automation Bot moved this to Todo in Driven Jul 3, 2026
@pmaxhogan pmaxhogan added this to the v2.0.0 milestone Jul 3, 2026
@github-actions

github-actions Bot commented Jul 3, 2026

Copy link
Copy Markdown
Contributor

Coverage

Area main this PR delta
Rust (lib crates) 76.76% 76.70% -0.06 (OK)
UI (vue/ts) 88.17% 88.17% +0.00 (OK)

Gate: passed - no coverage regression (epsilon 0.1 pp).

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

Status: Todo

Development

Successfully merging this pull request may close these issues.

Wire OS sleep/wake power events (suspend-edge session snapshot)

1 participant