daemon: replace exit(1) callbacks with recoverable state machine - #62
Open
silentone12725 wants to merge 2 commits into
Open
daemon: replace exit(1) callbacks with recoverable state machine#62silentone12725 wants to merge 2 commits into
silentone12725 wants to merge 2 commits into
Conversation
Previously, both SVPlaybackLeaseManager callbacks (endLeaseCb, pbErrCb) called exit(1) unconditionally, making every Apple lease termination a fatal process death. wrapper-rootless was behaving like a short-lived utility rather than a persistent background service. This commit introduces a proper recovery lifecycle: Recovery state machine (main.cpp) - RecoveryState enum: Running / Scheduled / Refreshing / Failed - get_recovery_state() exported as extern C int for status endpoints and Electron IPC (0=Running, 1=Scheduled, 2=Refreshing, 3=Failed) - is_recovery_active() derived from state, used to gate HTTP requests Non-blocking callbacks - endLeaseCb / pbErrCb now push a code onto a queue and return immediately — no library calls from within the lease manager thread - Eliminates reentrancy and deadlock risk from callback context Dedicated recovery worker thread - Drains the entire queue on each wake (coalescing): a burst of 3084+3084+PLAYBACK_ERR produces one refresh cycle, not three - Exponential backoff: 1s -> 2s -> 5s -> 10s -> 30s (clamped, never stops) - Calls refresh_decrypt_ctx() as the sole owner of reacquisition logic - Verifies success via is_preshare_ctx_ready() — resets consec_fails only when preshareCtx is non-null after the refresh - Self-schedules retry on failure (kRetryInternal) so the daemon keeps retrying even when Apple sends no further lease-end event (e.g. transient network failure during refresh) Thread safety (main.c) - g_ctx_mutex (PTHREAD_MUTEX_INITIALIZER) protects all preshareCtx reads and writes against concurrent access from the decrypt thread and the recovery worker - Lock is released before FairPlay network calls to avoid blocking decryption during reacquisition - is_preshare_ctx_ready() reads preshareCtx under g_ctx_mutex Client-visible state gating (main.c) - handle() and handle_m3u8() check is_recovery_active() per request - Decrypt server: returns immediately (EOF) during recovery - M3U8 server: writes empty line, continues loop — no hanging requests - Prevents FairPlay key-delivery calls from racing the recovery worker HTTP servers (decrypt, m3u8, account) remain alive across all lease events. The Electron supervisor continues to provide the outer safety net for true process deaths (segfault, OOM, etc.).
silentone12725
marked this pull request as ready for review
August 7, 2026 04:44
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
This PR changes the wrapper from a process that terminates on Apple playback lease errors into a long-running, self-recovering daemon.
Previously, both
SVPlaybackLeaseManagercallbacks (endLeaseCbandpbErrCb) calledexit(1)unconditionally. A lease termination therefore killed the entire wrapper, requiring an external supervisor to restart the process.The new implementation keeps the wrapper and its HTTP services alive, moves FairPlay context reacquisition into a dedicated recovery worker, makes access to the shared decryption context thread-safe, exposes lifecycle state to external consumers, and brings the rootless wrapper in line with the same recovery model.
It also adds build/drift tooling to help keep the C and rootless wrapper implementations synchronized.
What changed
Recovery state machine
main.cppnow maintains an explicit recovery lifecycle:RunningScheduledRefreshingFailedget_recovery_state()is exported as anextern "C"API so external consumers can observe the daemon state:0123is_recovery_active()provides a simple recovery gate for request handlers.Non-blocking lease callbacks
endLeaseCbandpbErrCbno longer perform recovery work or terminate the process directly.Instead, callbacks enqueue a recovery event and return immediately.
This keeps FairPlay/library calls out of the lease-manager callback context and reduces the risk of callback reentrancy or deadlocks.
Dedicated recovery worker
Recovery is handled by a persistent worker thread that owns context reacquisition.
The worker:
refresh_decrypt_ctx()as the single owner of reacquisition;is_preshare_ctx_ready();preshareCtxexists; andRetry delay uses bounded exponential backoff:
1s -> 2s -> 5s -> 10s -> 30sThe 30-second delay is clamped rather than terminating recovery, allowing the daemon to survive prolonged transient failures.
Thread-safe FairPlay context access
main.cnow protectspreshareCtxwith aPTHREAD_MUTEX_INITIALIZER-backed mutex.All relevant reads and writes are synchronized between the decrypt path and the recovery worker.
The mutex is deliberately released before FairPlay/network reacquisition work so long-running recovery operations do not hold the context lock.
is_preshare_ctx_ready()also checks the context while holding the mutex.Request gating during recovery
Decrypt and M3U8 request handling now checks the recovery state before using the FairPlay context.
During recovery:
The decrypt, M3U8, and account servers themselves remain running throughout lease recovery.
External supervision is therefore reserved for actual process failures such as crashes, OOM termination, or other unrecoverable failures rather than normal lease lifecycle events.
DRM lifecycle state
The wrapper now publishes its lifecycle through:
<base-dir>/drm-stateThe state file allows the surrounding Go/Electron layer to observe wrapper lifecycle changes without inferring state solely from process existence.
Lifecycle states include:
STARTINGLOGINWAITING_2FAINITIALIZING_FAIRPLAYRUNNINGRECOVERYFAILEDSTOPPEDThis provides a lightweight interface for status reporting and supervisor integration while keeping recovery ownership inside the wrapper.
Rootless wrapper
wrapper-rootless.chas been updated to follow the persistent-daemon recovery model rather than treating lease termination as an unconditional process-exit condition.This keeps rootless operation aligned with the recovery behavior introduced in the main wrapper implementation.
Build and drift protection
This PR also adds tooling around the wrapper build:
Dockerfile.buildfor a reproducible wrapper build environment;scripts/check-drift.py;The drift check is intended to catch unintended divergence between related wrapper implementations as recovery/lifecycle behavior evolves.
Result
Before
After
The wrapper therefore behaves as a persistent service: lease expiration and transient FairPlay recovery failures are handled internally, while the process and HTTP servers remain alive.
Commits
This PR consists of two logical changes:
daemon: replace exit(1) callbacks with recoverable state machineAdd DRM state tracking and wrapper drift check