Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 9 additions & 2 deletions src/service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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") {
Expand All @@ -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 */ }
Expand Down Expand Up @@ -2596,6 +2600,7 @@ export interface FreshWindowsSchedulerInstallDeps {
stageRegistrationXml?: (attemptNonce: string) => string;
register?: (xmlPath: string, attemptNonce: string) => Promise<void>;
prepare?: () => Promise<void>;
removeNativeService?: () => void;
publishAssets?: () => void;
runTask?: () => void;
writeState?: () => void;
Expand All @@ -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"));
Expand All @@ -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;
Expand Down
29 changes: 29 additions & 0 deletions tests/service.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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"); },
Expand All @@ -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",
Expand All @@ -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"); },
Expand All @@ -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"); },
Expand All @@ -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"); },
Expand All @@ -879,13 +884,37 @@ describe("service lifecycle cleanup ordering", () => {
expect(calls).toEqual([
"register",
"prepare",
"remove-native-service",
"publish-assets",
"run-task",
"write-state",
"remove-stage",
]);
});

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") => ({
Expand Down
Loading