Skip to content

Ask before closing while recording or streaming, and finish the files first (T1.8) - #467

Merged
iamfatness merged 3 commits into
mainfrom
codex/t1-8-close-guard
Sep 11, 2026
Merged

iamfatness merged 3 commits into
mainfrom
codex/t1-8-close-guard

Conversation

@iamfatness

Copy link
Copy Markdown
Owner

Fixes #461 (T1.8). Owner decision: ask first.

Before: closing the app killed the core's whole process tree. No stop-recording was sent, so the Program MP4, every ISO and any RTMP/SRT stream were cut off mid-write. An operator who closed the app after a show lost the recording.

Now

  • The dialog. Closing while anything is live asks "Stop outputs and close?", with Stop and close / Keep running. Keep running is the default. Recording, streaming, and a recording still finalizing count as live. The virtual camera alone doesn't prompt.
  • Stop and close. Stops streams first, then the recording, through the existing transport path. The order matters: see Stopping Record while a stream stays up leaves recording status stuck at "stopping" (encoder restart erases the finalize result) #466 and the comment at the site. It then waits, up to 15 s, for the core's own evidence that this session finished. Evidence counts only if it arrived after the stop, for the session that was live at the close request, and from the same core generation. A stale completed, a respawned core's idle, or a missing node never counts. A dead or restarted core ends the wait immediately, logged loudly as the recording being interrupted with nothing more to save.
  • If the dialog fails while outputs are live, the app still takes the Stop and close path. It never kills the files.
  • Grace before the kill (app exit only). The core exits on its own when its stdin closes (JsonRpcServer.cpp EOF → loop break → joins → main returns). So app exit now closes stdin, waits 2 s, and only then kill-trees. The supervisor keeps draining the retiring core's stdout, or a full pipe would wedge its clean exit. Leftover child processes are swept after the exit. Leave-meeting, respawn and failure paths still stop immediately. The whole close fits inside the 5 s / 6 s shutdown budget, and a test pins that.
  • Code. New focused types: CloseGuardPolicy (pure), CloseGuardFlow, and OutputShutdownCoordinator. StudioViewModel and MainWindow only forward.

Review. One task review (4 Important, 9 Minor) and two fix rounds. Scoped re-review 1 found N1: Record and Stream together always hit the 15 s timeout. Scoped re-review 2 approved it with no findings.

Tests. WinUI 1409/1409. MediaCore 2171/2171, 3 back-to-back runs. The app-exit tests are structural, with no wall-clock assertions, and passed 5/5 with every core pinned busy.

Known gaps (tracked)

Required before merge: live checklist (owner, test build stopped, quiet machine)

  1. Idle close: no dialog. launch.log shows media core exited on its own or …killed. Note which, and the time.
  2. Recording, close, Keep running (try mouse, then Esc, then Enter): the app stays up and the recording continues.
  3. Recording, close, Stop and close: the log shows outputs finished after …ms. The Program MP4 and each ISO play, and ffprobe reads a duration.
  4. Record AND stream, close, Stop and close: the log shows outputs finished after …ms, not the 15 s timeout.
  5. Close pressed 3× while the dialog is open, and again during the wait: each is logged as ignored.
  6. Close from the taskbar while minimized: the window is restored, then asks.
  7. Streaming only to one RTMP/SRT destination, then Stop and close.
  8. Crash proof (the T1.7 check): zero new CoreVideoPro.WinUI.exe.*.dmp files and zero Application Error 1000 events across all runs.

🤖 Generated with Claude Code

https://claude.ai/code/session_01SAB8v62BEV8ihmaeGhhtY9

iamfatness and others added 3 commits September 10, 2026 19:47
… first (T1.8, #461)

Closing the app kill-treed the media core with no stop sent, so the Program
MP4's moov atom, every ISO writer and any RTMP/SRT egress were cut off
mid-write: an operator who closed the app after a show lost the recording.

- CloseGuardPolicy (pure, tested): asks when recording or streaming is live
  per the shell flags OR the core lifecycle; stopping/finalizing still count;
  absent lifecycle trusts the shell flag; virtual camera alone never asks.
- MainWindow: "Stop outputs and close?" ContentDialog (Stop and close /
  Keep running, Keep running default). A second close while it is open or
  while outputs finish is ignored. Any exception logs and falls back to the
  existing shutdown path.
- OutputShutdownCoordinator: Stop and close sends the existing transport
  stops, then waits for terminal lifecycle evidence from core snapshots,
  re-sending a stop that did not land, bounded at 15 s (loud log on timeout),
  before the unchanged ShutdownAsync.
- App-exit core stop only: close stdin (the core exits on EOF), keep draining
  its stdout, give it 2 s (ShutdownBudget) to exit on its own, then the old
  kill-tree. Leave-meeting / respawn / failure fallback keep the immediate kill.
- CLAUDE.md (Engine teardown order) and docs/BACKLOG.md T1.8 updated.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SAB8v62BEV8ihmaeGhhtY9
…g failure

Review findings (t1-8-review.md) and binding rulings:

- Evidence is tied to THIS stop: the coordinator reads the bridge's
  LastSnapshot (no competing syncs) and accepts only a snapshot whose
  RawReceivedUtc is later than the last moment the stop was pending
  (intent set or a Record/Stream command in flight). CloseGuardPolicy
  captures an OutputStopBaseline at the close request (live recording
  session id, live senders, core generation); only their terminal states
  count. Absent nodes, stale pre-stop completions, other sessions and old
  failures never read as finished. A changed restart count ends the wait as
  CoreRestarted, a dead core as CoreUnavailable, both logged loudly. Stops
  are re-sent while intent is still set.
- CloseGuardFlow (tested) owns the close flow. A dialog failure while
  outputs are live takes the Stop-and-close path, never the old kill path.
  The window is restored/activated before the dialog, which opens after the
  Closing callback returns; ignored closes bring it forward.
- Starts are refused while Stop and close finishes the outputs.
- Legacy recording.active with no lifecycle now asks.
- Supervisor: a plain Stop during an app-exit grace kills the retiring core;
  descendants are snapshotted before stdin closes and survivors of a clean
  exit are killed; stderr is drained (<=250 ms) after a clean exit.
  Post-core-stop disposal time is logged.
- MediaCoreAppExitStopTests: structural assertions only (exit-handler
  trace), generous graces, hang guards; plus sweep and retiring-kill tests.
- CLAUDE.md (Engine teardown order) and docs/BACKLOG.md T1.8 updated.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SAB8v62BEV8ihmaeGhhtY9
…line at send

Scoped re-review (t1-8-rereview.md):

- N1: record+stream always timed out at 15 s. A recording stop sent while
  Streaming was still desired carried start-program-output, which restarts
  the encoder and erases the recording lifecycle. SendStops now stops
  streams first, then recording. Tests pin the order (first send and
  re-send) and model the record+stream case end to end; both fail with the
  order swapped. The core defect is left for a separate backlog item.
- N2: the core-generation baseline is re-armed when the stop is sent; a core
  respawned while the dialog was open is logged, and the new core's outputs
  are still stopped and waited for.
- N3: a baseline sender counts as finished only when its adapter reads
  stopped/failed (not a core idle or a re-served older terminal state).
- N4: a retiring core killed by a concurrent Stop() is reported Killed, and
  the clean-exit drain/sweep are skipped.
- N5: interrupted is no longer terminal (matches the core's isTerminal); a
  stalled recording is the live baseline and its later failure is reported.
- CLAUDE.md and BACKLOG updated.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SAB8v62BEV8ihmaeGhhtY9
@iamfatness

Copy link
Copy Markdown
Owner Author

Live checklist: all 8 checks passed (2026-09-10, 21:26-21:40)

Setup

  • Build: the T1.8 branch as a matched pair: shell and core built from the T1.8 worktree (core build:native-dev, D3D11 ON). Staged core hash verified.
  • Meeting: the test meeting, 12 participants, Engine on.
  • Driven by: the control API, window messages (WM_CLOSE / SC_CLOSE) and UI Automation Invoke.
  • Streaming target: RTMP to a LOCAL ffmpeg listener on 127.0.0.1. The owner's real YouTube destination was never used. Preferences were backed up, and restored byte-identical afterwards. This dev core has no srt-output capability, so RTMP was used.
# Check Result
1 Idle close ✅ No dialog. media core exited on its own 171 ms after stdin closed; whole close under 300 ms.
2 Recording → close → Keep running (mouse, Esc, Enter) ✅ All three kept the app up. Recording still producing after each.
3 Recording (Program + 7 ISOs) → Stop and close ✅ outputs finished after 806ms. All 8 MP4s finalized (ffprobe duration ≈208.6 s). The last 5 s of Program and an ISO decode with no errors. Program: 12,451 frames over 208.6 s (59.7 fps).
4 Record and stream → Stop and close ✅ outputs finished after 813ms, not the 15 s timeout, so the stop-order fix (N1) holds live. The core exited on its own in 1.26 s. All 8 files finalized, with clean tail decodes. The listener received 80 MB of stream.
5 Close pressed repeatedly while the guard is active ✅ 3 presses while the dialog was open and 1 mid-transition, each logged close requested while the close guard is active — ignored.
6 Close from the taskbar while minimized ✅ The window was iconic → SC_CLOSE → dialog shown → Keep running.
7 Stream only → Stop and close ✅ The dialog said "Streaming to 1 destination". outputs finished after 295ms, then the core exited on its own.
8 Crash proof ✅ 0 new CoreVideoPro.WinUI / core / engine dumps and 0 Application Error 1000 events since the start. No leftover processes after any close.

Observed, not blocking

  • Close during final cleanup. A close pressed during the final cleanup, after outputs had finished, takes the existing escape hatch (close requested while cleanup is in progress — forcing exit). That skips the core's 2 s self-exit. Files were already finalized; this behaviour predates this PR.
  • Phantom recording status. A fresh core running a stream-only output reports a phantom recording (status: stopping, writerStatus: finalizing, lifecycle: null). The close guard correctly ignores it. Filed on Stopping Record while a stream stays up leaves recording status stuck at "stopping" (encoder restart erases the finalize result) #466.
  • Stream health while unreachable. While the destination was unreachable, the sender kept status: live / destinationHealth: ok, even as its lifecycle cycled producing / interrupted and the supervisor restarted it. To be filed.
  • Relative recording folder. Recordings/CoreVideo Pro resolves inside the app's install/publish directory. To be filed.
  • Empty ISOs with Engine off. ISOs armed with Engine off write 0 frames and give no warning. To be filed.

@iamfatness
iamfatness merged commit 5a24225 into main Sep 11, 2026
18 of 19 checks passed
@iamfatness
iamfatness deleted the codex/t1-8-close-guard branch October 1, 2026 23:45
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.

[T1.8] Closing the app while recording kills the recording unfinalized (and skips the Zoom leave order)

1 participant