Skip to content

fix(recording): recover capture after dark wake#329

Open
serg-shcherbak wants to merge 2 commits into
JerryZLiu:mainfrom
serg-shcherbak:codex/fix-recorder-dark-wake
Open

fix(recording): recover capture after dark wake#329
serg-shcherbak wants to merge 2 commits into
JerryZLiu:mainfrom
serg-shcherbak:codex/fix-recorder-dark-wake

Conversation

@serg-shcherbak

Copy link
Copy Markdown

Summary

Dayflow could remain running in the menu bar and report recording as active while silently producing no screenshots after a macOS dark-wake cycle. This change makes recorder startup contingent on an actual ScreenCaptureKit display and adds a bounded, cancellable recovery path for wake-related failures.

Problem

This failure was observed on Dayflow v2.0.3 installed from the release DMG.

The Dayflow process remained alive, and macOS screen-recording permission remained granted, but screenshot files stopped appearing after the Mac entered clamshell sleep. Opening the application later coincided with a display-change event that allowed capture to recover.

System logs and the recording timeline showed the following sequence:

  1. Clamshell sleep paused the recorder normally.
  2. macOS performed a transient dark wake while the display was still unavailable.
  3. NSWorkspace.didWakeNotification scheduled recorder startup after a five-second delay.
  4. By the time startup ran, ScreenCaptureKit returned shareable content without the preferred display.
  5. The recorder still started its timer and transitioned to .capturing with cachedDisplay == nil.
  6. Every timer tick then returned early without producing a screenshot.
  7. The real user wake was ignored because the wake handler only restarted a recorder whose state was .paused; the recorder incorrectly claimed to be .capturing.

This left Dayflow alive but unable to record until a later screen-configuration event refreshed the display.

Root cause

The recorder state machine treated “the capture timer is running” as equivalent to “screen capture is healthy.”

That assumption was invalid when ScreenCaptureKit temporarily returned no matching display during dark wake. The recorder could enter .capturing without a usable SCDisplay, and there was no successful-capture timestamp or other health signal that could expose the stale state.

Delayed wake callbacks also had no cancellation mechanism. A resume scheduled by an earlier wake could still run after the system returned to sleep.

Solution

The recorder now enforces a stronger invariant: .capturing is entered only after a real SCDisplay has been resolved and committed on the recorder's serial queue.

The recovery flow now:

  • treats a missing preferred display as a recoverable startup failure instead of a valid capture state;
  • retries missing-display startup with bounded exponential backoff at 1, 2, and 4 seconds;
  • preserves the requested display ID instead of falling back to an unrelated display;
  • uses a recovery generation token to invalidate delayed callbacks after sleep, lock, screensaver activation, stop, or permission loss;
  • performs the generation check and the subsequent start on the same serial queue, preventing another sleep event from entering between them;
  • records lastSuccessfulCaptureAt after both normal and privacy-redacted screenshots;
  • verifies recorder health on wake using the state, resolved display, and most recent successful capture;
  • restarts a recorder that is not capturing, has no display, has never completed a capture, or has not produced a frame within the health threshold;
  • centralizes sleep, lock, and screensaver pausing so timer cancellation, cache clearing, and state transition happen together.

The asynchronous ScreenCaptureKit result is committed only after the recorder rechecks that it is still in .starting. A late result arriving after sleep or stop therefore cannot repopulate stale display state.

Compatibility and risk

The normal screenshot path is unchanged:

  • capture interval and timer behavior remain the same once recording is healthy;
  • image scaling and JPEG encoding are unchanged;
  • screenshot filenames and database writes are unchanged;
  • privacy filtering and redacted captures are unchanged;
  • screen-recording permission handling remains in place;
  • no new runtime dependencies are introduced.

The existing preferred-display behavior is preserved: when the requested external display is temporarily absent, Dayflow waits for that display rather than silently capturing another screen.

The main behavioral change is limited to recorder startup and recovery after sleep, wake, lock, screensaver, permission loss, or temporary display unavailability.

Related: #308

Tests added

RecorderRecoveryPolicyTests covers:

  • a recorder that claims to be capturing without a display;
  • a recorder whose last successful capture is stale;
  • a recorder that is not in the capturing state;
  • a recorder that has never completed a successful capture;
  • a healthy recorder with a recent capture;
  • bounded exponential backoff for missing-display retries.

Validation performed

The following checks passed without fetching dependencies:

  • swiftc -frontend -parse for ScreenRecorder.swift, RecorderRecoveryPolicy.swift, and RecorderRecoveryPolicyTests.swift;
  • standalone swiftc -typecheck for RecorderRecoveryPolicy.swift;
  • compilation and execution of a standalone smoke test covering the recovery-policy assertions;
  • git diff --check;
  • trailing-whitespace checks.

Validation not performed

A real Dayflow application build was not performed.

Specifically:

  • Xcode was not opened;
  • xcodebuild was not run;
  • the XCTest target was not executed;
  • the modified application was not launched;
  • the complete sleep → dark wake → sleep → user wake sequence was not manually reproduced with a built binary;
  • external-monitor and Zoom screen-sharing scenarios were not manually exercised after the change.

The local environment was intentionally limited to checks that did not require resolving or downloading the Xcode project dependencies. CI and a maintainer build should therefore confirm full compilation and execute the XCTest target before merge.

Suggested manual verification

  1. Start and pause recording from the menu bar.
  2. Lock and unlock the Mac.
  3. Start and stop the screensaver.
  4. Close the MacBook lid while connected to USB-C, allow a dark wake, and then open the lid.
  5. Confirm screenshots resume without opening the Dayflow window.
  6. Repeat the sleep/wake sequence with an external display and confirm Dayflow does not switch to the wrong screen.
  7. Revoke and restore screen-recording permission.
  8. Run Dayflow while Zoom is sharing or recording the screen.

Comment thread Dayflow/Dayflow/Core/Recording/ScreenRecorder.swift Outdated
- reject stale setup completions with a startup generation guard
- keep display intent changes inside the guarded recorder queue commit
- cover an old pre-sleep setup completing after a post-wake restart
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.

2 participants