From 1085b20d438f9b279c820f7fcc69722e4469950e Mon Sep 17 00:00:00 2001 From: luvs01 Date: Thu, 13 Aug 2026 09:38:22 +0900 Subject: [PATCH] fix(windows): remove native service in fresh scheduler install --- src/service.ts | 11 +++++++++-- tests/service.test.ts | 29 +++++++++++++++++++++++++++++ 2 files changed, 38 insertions(+), 2 deletions(-) diff --git a/src/service.ts b/src/service.ts index 668f165202..31a3021789 100644 --- a/src/service.ts +++ b/src/service.ts @@ -1978,8 +1978,7 @@ export async function registerFreshWindowsSchedulerTask( } } -function installWindows(): void { - recordOwnedConfigPath(getConfigDir(), serviceStatePath()); +function removeNativeWindowsServiceForScheduler(): void { // Transactional backend switch: installing the scheduler backend removes a native // service first — two live managers would both respawn the proxy (conflict). if (statusWinswRaw() !== "nonexistent") { @@ -1993,6 +1992,11 @@ function installWindows(): void { throw new Error(`Native service registration could not be re-verified after the removal attempt — aborting switch. Check 'sc.exe query ${WINSW_SERVICE_ID}' and remove it manually if present.`); } } +} + +function installWindows(): void { + recordOwnedConfigPath(getConfigDir(), serviceStatePath()); + removeNativeWindowsServiceForScheduler(); // End a running task BEFORE rewriting the assets it is executing — cmd.exe reading the // script mid-rewrite runs a torn batch file, and its open handle can fail the write. try { stopWindows(); } catch { /* not running */ } @@ -2596,6 +2600,7 @@ export interface FreshWindowsSchedulerInstallDeps { stageRegistrationXml?: (attemptNonce: string) => string; register?: (xmlPath: string, attemptNonce: string) => Promise; prepare?: () => Promise; + removeNativeService?: () => void; publishAssets?: () => void; runTask?: () => void; writeState?: () => void; @@ -2617,6 +2622,7 @@ export async function installFreshWindowsSchedulerSafely( const stage = deps.stageRegistrationXml ?? stageWindowsSchedulerRegistrationXml; const register = deps.register ?? registerFreshWindowsSchedulerTask; const prepare = deps.prepare ?? (() => prepareServiceInstall("scheduler")); + const removeNativeService = deps.removeNativeService ?? removeNativeWindowsServiceForScheduler; const publishAssets = deps.publishAssets ?? writeWindowsSchedulerAssets; const runTask = deps.runTask ?? startWindows; const writeState = deps.writeState ?? (() => writeServiceInstallState("scheduler")); @@ -2638,6 +2644,7 @@ export async function installFreshWindowsSchedulerSafely( // The destructive boundary begins only after Task Scheduler accepted the definition. await prepare(); + removeNativeService(); publishAssets(); runTask(); started = true; diff --git a/tests/service.test.ts b/tests/service.test.ts index 1613042448..daf6c317ab 100644 --- a/tests/service.test.ts +++ b/tests/service.test.ts @@ -800,6 +800,7 @@ describe("service lifecycle cleanup ordering", () => { calls.push(`register:${path}`); }, prepare: async () => { calls.push("prepare:stop-managers-and-proxy"); }, + removeNativeService: () => { calls.push("remove-native-service"); }, publishAssets: () => { calls.push("publish-assets"); }, runTask: () => { calls.push("run-task"); }, writeState: () => { calls.push("write-state"); }, @@ -811,6 +812,7 @@ describe("service lifecycle cleanup ordering", () => { "stage", "register:attempt.xml", "prepare:stop-managers-and-proxy", + "remove-native-service", "publish-assets", "run-task", "write-state", @@ -832,6 +834,7 @@ describe("service lifecycle cleanup ordering", () => { throw new Error("UAC prompt was cancelled"); }, prepare: async () => { calls.push("prepare"); }, + removeNativeService: () => { calls.push("remove-native-service"); }, publishAssets: () => { calls.push("publish-assets"); }, runTask: () => { calls.push("run-task"); }, writeState: () => { calls.push("write-state"); }, @@ -853,6 +856,7 @@ describe("service lifecycle cleanup ordering", () => { stageRegistrationXml: () => "attempt.xml", register: async () => { calls.push("register"); }, prepare: async () => { calls.push("prepare"); throw new Error("standalone stop failed"); }, + removeNativeService: () => { calls.push("remove-native-service"); }, publishAssets: () => { calls.push("publish-assets"); }, runTask: () => { calls.push("run-task"); }, writeState: () => { calls.push("write-state"); }, @@ -869,6 +873,7 @@ describe("service lifecycle cleanup ordering", () => { stageRegistrationXml: () => "attempt.xml", register: async () => { calls.push("register"); }, prepare: async () => { calls.push("prepare"); }, + removeNativeService: () => { calls.push("remove-native-service"); }, publishAssets: () => { calls.push("publish-assets"); }, runTask: () => { calls.push("run-task"); }, writeState: () => { calls.push("write-state"); throw new Error("state write failed"); }, @@ -879,6 +884,7 @@ describe("service lifecycle cleanup ordering", () => { expect(calls).toEqual([ "register", "prepare", + "remove-native-service", "publish-assets", "run-task", "write-state", @@ -886,6 +892,29 @@ describe("service lifecycle cleanup ordering", () => { ]); }); + test("fresh scheduler install rolls back before publication when native service removal fails", async () => { + const calls: string[] = []; + await expect(installFreshWindowsSchedulerSafely({ + stageRegistrationXml: () => "attempt.xml", + register: async () => { calls.push("register"); }, + prepare: async () => { calls.push("prepare"); }, + removeNativeService: () => { calls.push("remove-native-service"); throw new Error("native service remains"); }, + publishAssets: () => { calls.push("publish-assets"); }, + runTask: () => { calls.push("run-task"); }, + writeState: () => { calls.push("write-state"); }, + rollbackTask: async () => { calls.push("rollback-task"); return null; }, + removeStagedXml: () => { calls.push("remove-stage"); }, + })).rejects.toThrow("native service remains"); + + expect(calls).toEqual([ + "register", + "prepare", + "remove-native-service", + "rollback-task", + "remove-stage", + ]); + }); + test("service install stops the recorded backend, requested backend, and standalone before loading assets", async () => { const calls: string[] = []; const managerOps = (backend: "scheduler" | "native") => ({