From 5309bf43753531c8c90f26390cb63ccdc51522ab Mon Sep 17 00:00:00 2001 From: ventianima-lab <256240652+ventianima-lab@users.noreply.github.com> Date: Sun, 2 Aug 2026 03:13:26 +0900 Subject: [PATCH] fix(windows): settle scheduler registration verification --- src/service.ts | 39 ++++++- tests/windows-elevation-spawn.test.ts | 150 ++++++++++++++++++++++++++ 2 files changed, 188 insertions(+), 1 deletion(-) diff --git a/src/service.ts b/src/service.ts index bdf6140790..ee4722886e 100644 --- a/src/service.ts +++ b/src/service.ts @@ -664,6 +664,8 @@ type FinalizeHooks = { deleteArgs: string[], ) => Promise; verify?: () => WindowsSchedulerInstallVerification; + /** Test-only replacement for the bounded post-registration settle delay. */ + settleDelay?: (milliseconds: number) => Promise; writeInstallState?: () => void; /** Preferred tri-state probe for security-sensitive reconciliation. */ probeTask?: () => WindowsSchedulerTaskProbe; @@ -736,6 +738,36 @@ function attemptStillOwned(options: ApplyElevatedOptions): boolean { return !check || check(options.attemptId); } +const WINDOWS_SCHEDULER_VERIFICATION_SETTLE_DELAYS_MS = [50, 150, 300, 600] as const; + +function schedulerVerificationMaySettle( + verification: WindowsSchedulerInstallVerification, +): boolean { + return verification.assetsHealthy + && verification.nativeServiceAbsent + && !verification.nativeStatusUnknown + && !verification.conflict + && (!verification.taskInstalled || !verification.registrationHealthy); +} + +async function verifyWindowsSchedulerInstallAfterSettle( + options: ApplyElevatedOptions, +): Promise { + const verify = finalizeHooks?.verify ?? verifyWindowsSchedulerInstall; + const delay = finalizeHooks?.settleDelay + ?? ((milliseconds: number) => new Promise(resolve => setTimeout(resolve, milliseconds))); + if (!attemptStillOwned(options)) return null; + let verification = verify(); + for (const milliseconds of WINDOWS_SCHEDULER_VERIFICATION_SETTLE_DELAYS_MS) { + if (verification.ok || !schedulerVerificationMaySettle(verification)) break; + if (!attemptStillOwned(options)) return null; + await delay(milliseconds); + if (!attemptStillOwned(options)) return null; + verification = verify(); + } + return attemptStillOwned(options) ? verification : null; +} + async function applyElevatedSchedulerResult( result: ElevatedSchtasksCreateAndRunResult, options: ApplyElevatedOptions, @@ -765,7 +797,11 @@ async function applyElevatedSchedulerResult( await reconcileUnknownElevatedOutcome(result.exitCode); } - const verification = (finalizeHooks?.verify ?? verifyWindowsSchedulerInstall)(); + // Task Scheduler can acknowledge elevated creation before the non-elevated + // query/XML view is coherent. Settle only that narrow visibility window; + // conflicts, missing assets, and unknown SCM state still fail immediately. + const verification = await verifyWindowsSchedulerInstallAfterSettle(options); + if (!verification) return; if (!verification.ok) { // Preserve a healthy elevated task when WinSW absence cannot be proven (unknown SCM status). // Unknown is not a confirmed dual-backend conflict; install state is still withheld. @@ -782,6 +818,7 @@ async function applyElevatedSchedulerResult( "Installation state was not written.", ]); } + if (!attemptStillOwned(options)) return; const rollbackError = await rollbackElevatedSchedulerTask(); const parts = [ "Elevated Task Scheduler registration did not produce a conflict-free install.", diff --git a/tests/windows-elevation-spawn.test.ts b/tests/windows-elevation-spawn.test.ts index 411a3e1513..7fafce30f3 100644 --- a/tests/windows-elevation-spawn.test.ts +++ b/tests/windows-elevation-spawn.test.ts @@ -319,6 +319,156 @@ describe("finalizeWindowsSchedulerServiceRegistration", () => { expect(parentRollbackLaunches).toBe(0); }); + test("settles a transient post-create registration view before writing install state", async () => { + let verifies = 0; + const delays: number[] = []; + setFinalizeWindowsSchedulerHooksForTests({ + elevateCreateAndRun: async () => ({ + outcome: "success", + exitCode: OCX_ELEVATED_SUCCESS, + stdout: "", + stderr: "", + }), + verify: () => { + verifies += 1; + if (verifies < 3) { + return { + taskInstalled: verifies === 2, + registrationHealthy: false, + assetsHealthy: true, + nativeServiceAbsent: true, + nativeStatusUnknown: false, + conflict: false, + ok: false, + detail: verifies === 1 + ? "Task Scheduler task is not installed." + : "Task Scheduler registration is present but unhealthy.", + }; + } + return okVerify(); + }, + settleDelay: async milliseconds => { delays.push(milliseconds); }, + writeInstallState: () => { writeCount += 1; }, + }); + + await expect(finalizeWindowsSchedulerServiceRegistration()).resolves.toEqual({ kind: "done" }); + expect(verifies).toBe(3); + expect(delays).toEqual([50, 150]); + expect(writeCount).toBe(1); + expect(parentRollbackLaunches).toBe(0); + }); + + test("does not settle a confirmed scheduler conflict", async () => { + let verifies = 0; + let delays = 0; + mockParentRollbackSpawn(); + setFinalizeWindowsSchedulerHooksForTests({ + elevateCreateAndRun: async () => ({ + outcome: "success", + exitCode: OCX_ELEVATED_SUCCESS, + stdout: "", + stderr: "", + }), + verify: () => { + verifies += 1; + return { + taskInstalled: true, + registrationHealthy: true, + assetsHealthy: true, + nativeServiceAbsent: false, + nativeStatusUnknown: false, + conflict: true, + ok: false, + detail: "CONFLICT: Task Scheduler and native WinSW are both present.", + }; + }, + settleDelay: async () => { delays += 1; }, + writeInstallState: () => { writeCount += 1; }, + taskInstalled: () => false, + }); + + await expect(finalizeWindowsSchedulerServiceRegistration()).rejects.toThrow(/CONFLICT/); + expect(verifies).toBe(1); + expect(delays).toBe(0); + expect(writeCount).toBe(0); + expect(parentRollbackLaunches).toBe(1); + }); + + test("bounds settling when the registration remains unhealthy", async () => { + let verifies = 0; + const delays: number[] = []; + mockParentRollbackSpawn(); + setFinalizeWindowsSchedulerHooksForTests({ + elevateCreateAndRun: async () => ({ + outcome: "success", + exitCode: OCX_ELEVATED_SUCCESS, + stdout: "", + stderr: "", + }), + verify: () => { + verifies += 1; + return { + taskInstalled: true, + registrationHealthy: false, + assetsHealthy: true, + nativeServiceAbsent: true, + nativeStatusUnknown: false, + conflict: false, + ok: false, + detail: "Task Scheduler registration is present but unhealthy.", + }; + }, + settleDelay: async milliseconds => { delays.push(milliseconds); }, + writeInstallState: () => { writeCount += 1; }, + taskInstalled: () => false, + }); + + await expect(finalizeWindowsSchedulerServiceRegistration()).rejects.toThrow(/present but unhealthy/); + expect(verifies).toBe(5); + expect(delays).toEqual([50, 150, 300, 600]); + expect(writeCount).toBe(0); + expect(parentRollbackLaunches).toBe(1); + }); + + test("stops settling without rollback or state write after attempt ownership is lost", async () => { + let verifies = 0; + let owned = true; + const delays: number[] = []; + setFinalizeWindowsSchedulerHooksForTests({ + elevateCreateAndRun: async () => ({ + outcome: "success", + exitCode: OCX_ELEVATED_SUCCESS, + stdout: "", + stderr: "", + }), + verify: () => { + verifies += 1; + return { + taskInstalled: false, + registrationHealthy: false, + assetsHealthy: true, + nativeServiceAbsent: true, + nativeStatusUnknown: false, + conflict: false, + ok: false, + detail: "Task Scheduler task is not installed.", + }; + }, + settleDelay: async milliseconds => { + delays.push(milliseconds); + owned = false; + }, + stillOwnsAttempt: () => owned, + writeInstallState: () => { writeCount += 1; }, + }); + + await expect(finalizeWindowsSchedulerServiceRegistration()).resolves.toEqual({ kind: "done" }); + expect(verifies).toBe(1); + expect(delays).toEqual([50]); + expect(writeCount).toBe(0); + expect(parentRollbackLaunches).toBe(0); + }); + test("create failure does not write install state or parent-rollback", async () => { setFinalizeWindowsSchedulerHooksForTests({ elevateCreateAndRun: async () => {