@@ -23,8 +23,8 @@ import { RunDir } from "../src/services";
2323const execFileAsync = promisify ( execFile ) ;
2424const SERVICE_LABEL = "sh.executor.daemon" ;
2525const SCENARIO_NAME =
26- "Desktop packaged supervised attach: stale launchd registration pins the 15s dead boot wait" ;
27- const RECORDING_FILE = resolve ( "recordings/stale-launchd-slow -boot.mp4" ) ;
26+ "Desktop packaged supervised attach: stale launchd registration self-heals without a dead boot wait" ;
27+ const RECORDING_FILE = resolve ( "recordings/stale-launchd-fast -boot.mp4" ) ;
2828const RECORDING_WIDTH = 1280 ;
2929const RECORDING_HEIGHT = 720 ;
3030const STALE_PLIST_PORT = 58_251 ;
@@ -72,12 +72,15 @@ interface LaunchTiming {
7272
7373interface BootResult {
7474 readonly pageCdpMs : number ;
75+ readonly startupWindowMs : number ;
7576 readonly sidecarReadyMs : number ;
7677 readonly connectionKind : string ;
7778 readonly origin : string ;
7879 readonly manifestKind : string ;
7980 readonly manifestPid : number ;
80- readonly matchedLogLine : string | null ;
81+ readonly kickstartFailureLine : string | null ;
82+ readonly recoveryLogLine : string | null ;
83+ readonly launchdPrintSucceeded : boolean ;
8184}
8285
8386declare global {
@@ -273,6 +276,17 @@ const launchctl = async (args: ReadonlyArray<string>): Promise<boolean> => {
273276 }
274277} ;
275278
279+ const launchctlPrintService = async ( ) : Promise < string | null > => {
280+ try {
281+ const { stdout, stderr } = await execFileAsync ( "launchctl" , [ "print" , serviceTarget ( ) ] , {
282+ encoding : "utf8" ,
283+ } ) ;
284+ return `${ stdout } ${ stderr } ` . trim ( ) ;
285+ } catch {
286+ return null ;
287+ }
288+ } ;
289+
276290const captureLaunchdService = ( ) : LaunchdServiceSnapshot | null => {
277291 if ( process . platform !== "darwin" ) return null ;
278292 const path = launchAgentPath ( ) ;
@@ -482,31 +496,51 @@ const readServerManifest = (home: string): ServerManifest => {
482496 return JSON . parse ( readFileSync ( manifestPath , "utf8" ) ) as ServerManifest ;
483497} ;
484498
485- const readKickstartFailureLine = ( home : string ) : string | null => {
499+ interface StaleRecoveryLog {
500+ readonly kickstartFailureLine : string | null ;
501+ readonly recoveryLogLine : string | null ;
502+ }
503+
504+ // The desktop's own classification line for a stale registration; electron-log
505+ // appends the launchctl error on the same line. Production launchctl fails the
506+ // kickstart with exit 113 ("Could not find service"); a tart guest's
507+ // `launchctl asuser` session fails it with exit 125 ("Domain does not support
508+ // specified action"). Same stale state, same self-heal branch, so match the
509+ // kickstart failure without pinning the environment-specific exit code.
510+ const isKickstartFailureLine = ( line : string ) : boolean =>
511+ line . includes ( "Failed to restart registered supervised service; reinstalling" ) &&
512+ line . includes ( "launchctl kickstart failed" ) ;
513+
514+ const isRecoveryLogLine = ( line : string ) : boolean =>
515+ line . includes ( "installed supervised service via bundled executor" ) ||
516+ line . includes ( "Failed to install supervised service after registered service restart failure" ) ||
517+ line . includes ( "using managed sidecar" ) ;
518+
519+ const readStaleRecoveryLog = ( home : string ) : StaleRecoveryLog => {
486520 const logPath = join ( home , "Library" , "Logs" , "Executor" , "main.log" ) ;
487- if ( ! existsSync ( logPath ) ) return null ;
488- return (
489- readFileSync ( logPath , "utf8" )
490- . split ( "\n" )
491- . find (
492- ( line ) =>
493- line . includes ( "launchctl kickstart failed (exit 113)" ) &&
494- line . includes ( 'Could not find service "sh.executor.daemon"' ) ,
495- ) ?? null
496- ) ;
521+ if ( ! existsSync ( logPath ) ) return { kickstartFailureLine : null , recoveryLogLine : null } ;
522+ const lines = readFileSync ( logPath , "utf8" ) . split ( "\n" ) ;
523+ const kickstartIndex = lines . findIndex ( isKickstartFailureLine ) ;
524+ if ( kickstartIndex < 0 ) return { kickstartFailureLine : null , recoveryLogLine : null } ;
525+ const recoveryLogLine =
526+ lines . slice ( kickstartIndex + 1 ) . find ( ( line ) => isRecoveryLogLine ( line ) ) ?? null ;
527+ return {
528+ kickstartFailureLine : lines [ kickstartIndex ] ?? null ,
529+ recoveryLogLine ,
530+ } ;
497531} ;
498532
499- const waitForKickstartFailureLine = async ( home : string ) : Promise < string > => {
533+ const waitForStaleRecoveryLog = async ( home : string ) : Promise < StaleRecoveryLog > => {
500534 const deadline = Date . now ( ) + 30_000 ;
501535 for ( ; ; ) {
502- const line = readKickstartFailureLine ( home ) ;
503- if ( line ) return line ;
536+ const lines = readStaleRecoveryLog ( home ) ;
537+ if ( lines . kickstartFailureLine && lines . recoveryLogLine ) return lines ;
504538 if ( Date . now ( ) >= deadline ) {
505539 const logPath = join ( home , "Library" , "Logs" , "Executor" , "main.log" ) ;
506540 const tail = existsSync ( logPath )
507541 ? readFileSync ( logPath , "utf8" ) . split ( "\n" ) . slice ( - 20 ) . join ( "\n" )
508542 : "<main.log missing>" ;
509- throw new Error ( `Timed out waiting for kickstart failure log line \n${ tail } ` ) ;
543+ throw new Error ( `Timed out waiting for stale launchd recovery log lines \n${ tail } ` ) ;
510544 }
511545 await new Promise ( ( resolveWait ) => setTimeout ( resolveWait , 250 ) ) ;
512546 }
@@ -516,14 +550,24 @@ const bootPackagedApp = async (input: {
516550 readonly home : string ;
517551 readonly env : NodeJS . ProcessEnv ;
518552 readonly recorder : BootRecorder | null ;
519- readonly expectKickstartFailure : boolean ;
553+ readonly expectStartupWindow : boolean ;
554+ readonly expectStaleRecoveryLog : boolean ;
520555} ) : Promise < BootResult > => {
521556 let app : PackagedApp | undefined ;
522557 const startedAt = Date . now ( ) ;
523558 try {
524559 const launched = await launchPackaged ( input . env , input . recorder ) ;
525560 app = launched . app ;
526561 const page = app . cdp ;
562+ const startupWindowVisible = await page
563+ . waitForText ( "Starting Executor" , 5_000 )
564+ . then ( ( ) => true )
565+ . catch ( ( ) => false ) ;
566+ const startupWindowMs = startupWindowVisible ? Date . now ( ) - startedAt : launched . pageCdpMs ;
567+ expect (
568+ ! input . expectStartupWindow || startupWindowVisible ,
569+ "startup window shows before service recovery finishes when required" ,
570+ ) . toBe ( true ) ;
527571 await page . waitForText ( "Settings" , 120_000 ) ;
528572 const sidecarReadyMs = Date . now ( ) - startedAt ;
529573 await input . recorder ?. captureReadyFrame ( ) ;
@@ -534,17 +578,21 @@ const bootPackagedApp = async (input: {
534578 } | null > ( "window.executor.getServerConnection()" ) ;
535579 expect ( connection , "the app eventually exposes a server connection" ) . not . toBeNull ( ) ;
536580 const manifest = readServerManifest ( input . home ) ;
537- const matchedLogLine = input . expectKickstartFailure
538- ? await waitForKickstartFailureLine ( input . home )
539- : readKickstartFailureLine ( input . home ) ;
581+ const recoveryLog = input . expectStaleRecoveryLog
582+ ? await waitForStaleRecoveryLog ( input . home )
583+ : readStaleRecoveryLog ( input . home ) ;
584+ const launchdPrintSucceeded = ( await launchctlPrintService ( ) ) !== null ;
540585 return {
541586 pageCdpMs : launched . pageCdpMs ,
587+ startupWindowMs,
542588 sidecarReadyMs,
543589 connectionKind : connection ! . kind ,
544590 origin : connection ! . origin ,
545591 manifestKind : manifest . kind ,
546592 manifestPid : manifest . pid ,
547- matchedLogLine,
593+ kickstartFailureLine : recoveryLog . kickstartFailureLine ,
594+ recoveryLogLine : recoveryLog . recoveryLogLine ,
595+ launchdPrintSucceeded,
548596 } ;
549597 } finally {
550598 await input . recorder ?. stop ( ) ;
@@ -680,7 +728,7 @@ class BootRecorder {
680728
681729 static create = ( runDir : string ) : BootRecorder | null => {
682730 if ( process . env . E2E_RECORD !== "1" ) return null ;
683- const frameDir = join ( runDir , "stale-launchd-slow -boot-frames" ) ;
731+ const frameDir = join ( runDir , "stale-launchd-fast -boot-frames" ) ;
684732 rmSync ( frameDir , { recursive : true , force : true } ) ;
685733 mkdirSync ( frameDir , { recursive : true } ) ;
686734 mkdirSync ( dirname ( RECORDING_FILE ) , { recursive : true } ) ;
@@ -707,7 +755,7 @@ class BootRecorder {
707755
708756 captureReadyFrame = async ( ) : Promise < void > => {
709757 if ( ! this . enabled ) return ;
710- this . enqueueFrame ( "managed sidecar ready" , this . page ) ;
758+ this . enqueueFrame ( "executor ready" , this . page ) ;
711759 await this . queue ;
712760 } ;
713761
@@ -802,7 +850,8 @@ const run = async (runDir: string): Promise<void> => {
802850 home : cleanHome ,
803851 env : cleanBootEnv ( cleanHome ) ,
804852 recorder : null ,
805- expectKickstartFailure : false ,
853+ expectStartupWindow : false ,
854+ expectStaleRecoveryLog : false ,
806855 } ) ;
807856
808857 writeStaleLaunchAgentPlist ( staleHome ) ;
@@ -811,53 +860,64 @@ const run = async (runDir: string): Promise<void> => {
811860 home : staleHome ,
812861 env : packagedAppEnv ( staleHome ) ,
813862 recorder : staleRecorder ,
814- expectKickstartFailure : true ,
863+ expectStartupWindow : true ,
864+ expectStaleRecoveryLog : true ,
815865 } ) ;
816866
817867 /*
818- * Regression pin for the current broken behavior:
819- * a stale plist makes `service status` report registered even though launchd
820- * cannot find the label. The packaged app logs the kickstart failure, then
821- * still pays the full 15s wait before falling back to managed-spawn.
822- *
823- * When the fix lands, invert the >=14s stale assertions. The desired
824- * behavior is a fast-fail to managed-spawn, close to the clean control.
868+ * Regression guard for the production incident:
869+ * a stale plist makes `service status` report registered while launchd cannot
870+ * find the label. The app must show a window immediately, then repair or
871+ * bypass that stale service without paying the old dead 15s wait.
825872 */
826- expect ( stale . matchedLogLine , "stale boot logs the launchd kickstart failure" ) . toContain (
827- "launchctl kickstart failed (exit 113)" ,
828- ) ;
829- expect ( stale . matchedLogLine , "stale boot says launchd cannot find the label" ) . toContain (
830- 'Could not find service "sh.executor.daemon"' ,
873+ expect ( stale . kickstartFailureLine , "stale boot logs the launchd kickstart failure" ) . toContain (
874+ "launchctl kickstart failed" ,
831875 ) ;
876+ expect (
877+ stale . kickstartFailureLine ,
878+ "the failed kickstart is classified as a stale registration to repair" ,
879+ ) . toContain ( "Failed to restart registered supervised service; reinstalling" ) ;
880+ expect (
881+ stale . recoveryLogLine ,
882+ "kickstart failure is followed by self-heal or fallback logging" ,
883+ ) . not . toBeNull ( ) ;
832884 expect (
833885 stale . pageCdpMs ,
834- "window/CDP is not reachable until the dead wait is paid " ,
835- ) . toBeGreaterThanOrEqual ( 14_000 ) ;
886+ "the Electron page target is reachable before service recovery finishes " ,
887+ ) . toBeLessThan ( 3_000 ) ;
836888 expect (
837- stale . sidecarReadyMs ,
838- "managed sidecar readiness is delayed by the dead wait " ,
839- ) . toBeGreaterThanOrEqual ( 14_000 ) ;
840- expect ( stale . connectionKind , "the app eventually recovers through managed-spawn" ) . toBe (
841- "desktop-sidecar" ,
889+ stale . startupWindowMs ,
890+ "the startup window appears promptly in the stale launchd case " ,
891+ ) . toBeLessThan ( 3_000 ) ;
892+ expect ( new URL ( stale . origin ) . port , "recovery must not attach to the stale plist port" ) . not . toBe (
893+ String ( STALE_PLIST_PORT ) ,
842894 ) ;
843- expect (
844- new URL ( stale . origin ) . port ,
845- "managed recovery must not be a launchd attach to the stale plist port" ,
846- ) . not . toBe ( String ( STALE_PLIST_PORT ) ) ;
847895 expect (
848896 clean . sidecarReadyMs ,
849897 "the clean control skips supervision and boots quickly" ,
850898 ) . toBeLessThan ( 10_000 ) ;
851899 expect (
852900 stale . sidecarReadyMs ,
853- "the stale launchd path is dramatically slower than the clean control" ,
854- ) . toBeGreaterThan ( clean . sidecarReadyMs + 5_000 ) ;
901+ "stale launchd recovery stays within the fast boot budget" ,
902+ ) . toBeLessThan ( 10_000 ) ;
903+ expect (
904+ stale . sidecarReadyMs ,
905+ "the stale launchd path stays close to the clean control" ,
906+ ) . toBeLessThan ( clean . sidecarReadyMs + 7_500 ) ;
907+ const repairedLaunchd = stale . launchdPrintSucceeded && stale . manifestKind === "cli-daemon" ;
908+ const managedFallback =
909+ ! stale . launchdPrintSucceeded && stale . manifestKind === "desktop-sidecar" ;
910+ expect (
911+ repairedLaunchd || managedFallback ,
912+ "stale state is either repaired into launchd or cleanly falls back to managed spawn" ,
913+ ) . toBe ( true ) ;
855914
856915 const recording = staleRecorder ?. recordingStats ( ) ;
857916 emitEvidence (
858- `[stale-launchd-repro] clean=${ clean . sidecarReadyMs } ms stale=${ stale . sidecarReadyMs } ms stalePageCdp=${ stale . pageCdpMs } ms cleanOrigin=${ clean . origin } staleOrigin=${ stale . origin } staleConnectionKind=${ stale . connectionKind } staleManifestKind=${ stale . manifestKind } stalePid=${ stale . manifestPid } ` ,
917+ `[stale-launchd-repro] clean=${ clean . sidecarReadyMs } ms stale=${ stale . sidecarReadyMs } ms staleStartup= ${ stale . startupWindowMs } ms stalePageCdp=${ stale . pageCdpMs } ms cleanOrigin=${ clean . origin } staleOrigin=${ stale . origin } staleConnectionKind=${ stale . connectionKind } staleManifestKind=${ stale . manifestKind } stalePid=${ stale . manifestPid } launchdPrintAfterBoot= ${ stale . launchdPrintSucceeded } ` ,
859918 ) ;
860- emitEvidence ( `[stale-launchd-repro] matchedLogLine=${ stale . matchedLogLine } ` ) ;
919+ emitEvidence ( `[stale-launchd-repro] kickstartFailureLine=${ stale . kickstartFailureLine } ` ) ;
920+ emitEvidence ( `[stale-launchd-repro] recoveryLogLine=${ stale . recoveryLogLine } ` ) ;
861921 if ( recording ) {
862922 emitEvidence (
863923 `[stale-launchd-repro] recording=${ recording . path } frames=${ recording . frames } size=${ recording . size } ` ,
0 commit comments