@agentteams/runner 0.0.102 → 0.0.105
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/autostart.d.ts +21 -6
- package/dist/autostart.js +262 -43
- package/dist/autostart.js.map +1 -1
- package/dist/autostart.test.js +155 -22
- package/dist/autostart.test.js.map +1 -1
- package/dist/commands/restart.d.ts +9 -1
- package/dist/commands/restart.js +16 -4
- package/dist/commands/restart.js.map +1 -1
- package/dist/commands/restart.test.d.ts +1 -0
- package/dist/commands/restart.test.js +31 -0
- package/dist/commands/restart.test.js.map +1 -0
- package/dist/commands/start.js +5 -0
- package/dist/commands/start.js.map +1 -1
- package/dist/daemon-control.d.ts +32 -6
- package/dist/daemon-control.js +198 -13
- package/dist/daemon-control.js.map +1 -1
- package/dist/daemon-control.test.js +260 -26
- package/dist/daemon-control.test.js.map +1 -1
- package/dist/executable.js +10 -0
- package/dist/executable.js.map +1 -1
- package/dist/executable.test.js +31 -9
- package/dist/executable.test.js.map +1 -1
- package/dist/poller.js +12 -11
- package/dist/poller.js.map +1 -1
- package/dist/poller.test.js +51 -7
- package/dist/poller.test.js.map +1 -1
- package/dist/restart-handoff.d.ts +71 -0
- package/dist/restart-handoff.js +169 -0
- package/dist/restart-handoff.js.map +1 -0
- package/dist/restart-handoff.test.d.ts +1 -0
- package/dist/restart-handoff.test.js +228 -0
- package/dist/restart-handoff.test.js.map +1 -0
- package/dist/runners/cursor-cli.test.js +3 -1
- package/dist/runners/cursor-cli.test.js.map +1 -1
- package/dist/utils/resolve-member-repo.test.js +1 -1
- package/dist/utils/resolve-member-repo.test.js.map +1 -1
- package/package.json +2 -2
package/dist/daemon-control.js
CHANGED
|
@@ -1,11 +1,32 @@
|
|
|
1
1
|
import { setTimeout as delay } from 'node:timers/promises';
|
|
2
|
+
import { randomUUID } from 'node:crypto';
|
|
2
3
|
import { platform as getPlatform } from 'node:os';
|
|
3
|
-
import { getAutostartStatus, launchWindowsHiddenDaemon, restartAutostartService, scheduleWindowsTaskRestart, } from './autostart.js';
|
|
4
|
+
import { getAutostartStatus, launchWindowsHiddenDaemon, registerWindowsTask, restartAutostartService, scheduleWindowsTaskRestart, } from './autostart.js';
|
|
4
5
|
import { logger } from './logger.js';
|
|
5
6
|
import { getDaemonStatus } from './pid.js';
|
|
6
7
|
import { spawnExecutable } from './executable.js';
|
|
8
|
+
import { acknowledgePreparedRestartHandoff, buildRestartHandoffEnv, getRestartHandoffPath, waitForPreparedRestartHandoff, } from './restart-handoff.js';
|
|
9
|
+
import { promises as fs } from 'node:fs';
|
|
7
10
|
const restartPollIntervalMs = 100;
|
|
8
11
|
const stopTimeoutMs = 10_000;
|
|
12
|
+
const startTimeoutMs = 15_000;
|
|
13
|
+
// A restart only *triggers* the replacement runner (a detached spawn, a
|
|
14
|
+
// Task Scheduler `/Run`, or a supervised respawn); the new process needs a
|
|
15
|
+
// moment to boot and write its PID file. Poll until it reports running so the
|
|
16
|
+
// command can report accurately instead of racing an async startup.
|
|
17
|
+
export const waitForDaemonToStart = async (deps = {}) => {
|
|
18
|
+
const resolvedGetDaemonStatus = deps.getDaemonStatus ?? getDaemonStatus;
|
|
19
|
+
const resolvedSleep = deps.sleep ?? ((milliseconds) => delay(milliseconds));
|
|
20
|
+
const resolvedNow = deps.now ?? (() => Date.now());
|
|
21
|
+
const timeoutMs = deps.timeoutMs ?? startTimeoutMs;
|
|
22
|
+
const deadline = resolvedNow() + timeoutMs;
|
|
23
|
+
let status = await resolvedGetDaemonStatus();
|
|
24
|
+
while (!status.running && resolvedNow() < deadline) {
|
|
25
|
+
await resolvedSleep(restartPollIntervalMs);
|
|
26
|
+
status = await resolvedGetDaemonStatus();
|
|
27
|
+
}
|
|
28
|
+
return status;
|
|
29
|
+
};
|
|
9
30
|
const waitForDaemonToStop = async (pid, deps) => {
|
|
10
31
|
deps.kill(pid, 'SIGTERM');
|
|
11
32
|
const deadline = Date.now() + stopTimeoutMs;
|
|
@@ -18,55 +39,219 @@ const waitForDaemonToStop = async (pid, deps) => {
|
|
|
18
39
|
}
|
|
19
40
|
throw new Error(`Timed out waiting for AgentRunner process ${pid} to stop.`);
|
|
20
41
|
};
|
|
21
|
-
export const spawnDetachedDaemon = () => {
|
|
42
|
+
export const spawnDetachedDaemon = (launch) => {
|
|
43
|
+
const env = launch ? buildRestartHandoffEnv(launch) : process.env;
|
|
22
44
|
// On Windows, use the dedicated hidden PowerShell launcher so the `.cmd`
|
|
23
45
|
// shim never creates a visible console host.
|
|
24
46
|
if (getPlatform() === 'win32') {
|
|
25
|
-
launchWindowsHiddenDaemon();
|
|
47
|
+
launchWindowsHiddenDaemon({ env });
|
|
26
48
|
return;
|
|
27
49
|
}
|
|
28
50
|
const child = spawnExecutable('agentrunner', ['start'], {
|
|
29
51
|
detached: true,
|
|
30
52
|
stdio: 'ignore',
|
|
31
|
-
env
|
|
53
|
+
env,
|
|
32
54
|
cwd: process.cwd(),
|
|
33
55
|
});
|
|
34
56
|
child.unref();
|
|
35
57
|
return child;
|
|
36
58
|
};
|
|
59
|
+
export const prepareDetachedRestartHandoff = async (deps = {}) => {
|
|
60
|
+
const handoffId = deps.handoffId ?? randomUUID();
|
|
61
|
+
const parentPid = deps.parentPid ?? process.pid;
|
|
62
|
+
const markerPath = deps.markerPath ?? getRestartHandoffPath(handoffId);
|
|
63
|
+
const launch = { handoffId, parentPid, markerPath };
|
|
64
|
+
try {
|
|
65
|
+
await (deps.unlink ?? fs.unlink)(markerPath);
|
|
66
|
+
}
|
|
67
|
+
catch {
|
|
68
|
+
// Missing or stale marker is fine.
|
|
69
|
+
}
|
|
70
|
+
let child;
|
|
71
|
+
try {
|
|
72
|
+
child = (deps.spawnDetachedDaemon ?? spawnDetachedDaemon)(launch);
|
|
73
|
+
}
|
|
74
|
+
catch (error) {
|
|
75
|
+
return {
|
|
76
|
+
status: 'retryable-failure',
|
|
77
|
+
handoffId,
|
|
78
|
+
replacementReady: false,
|
|
79
|
+
acknowledged: false,
|
|
80
|
+
retryableFailure: true,
|
|
81
|
+
reason: 'replacement-preparation-failed',
|
|
82
|
+
error: error instanceof Error ? error.message : String(error),
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
if (!child?.pid) {
|
|
86
|
+
return {
|
|
87
|
+
status: 'retryable-failure',
|
|
88
|
+
handoffId,
|
|
89
|
+
replacementReady: false,
|
|
90
|
+
acknowledged: false,
|
|
91
|
+
retryableFailure: true,
|
|
92
|
+
reason: 'replacement-preparation-failed',
|
|
93
|
+
error: 'Replacement runner process did not expose a PID.',
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
return (deps.waitForPreparedRestartHandoff ?? waitForPreparedRestartHandoff)(launch);
|
|
97
|
+
};
|
|
37
98
|
// Run from within the daemon itself when a web restart request is received.
|
|
38
99
|
// We can't call restartDaemon() here because that would SIGTERM our own PID
|
|
39
100
|
// before we get a chance to spawn the replacement; instead we either exit and
|
|
40
101
|
// let the OS supervisor restart us, or spawn a replacement and exit cleanly.
|
|
41
|
-
export const executeRestartRequest = (deps = {}) => {
|
|
102
|
+
export const executeRestartRequest = async (deps = {}) => {
|
|
42
103
|
const resolvedGetAutostartStatus = deps.getAutostartStatus ?? getAutostartStatus;
|
|
43
104
|
const resolvedScheduleWindowsTaskRestart = deps.scheduleWindowsTaskRestart ?? scheduleWindowsTaskRestart;
|
|
105
|
+
const resolvedRegisterWindowsTask = deps.registerWindowsTask ?? registerWindowsTask;
|
|
44
106
|
const resolvedSpawnDetachedDaemon = deps.spawnDetachedDaemon ?? spawnDetachedDaemon;
|
|
107
|
+
const acknowledgeRestart = deps.acknowledgeRestart ?? (async () => undefined);
|
|
108
|
+
const acknowledgePreparedHandoff = deps.acknowledgePreparedHandoff ?? acknowledgePreparedRestartHandoff;
|
|
45
109
|
const resolvedPlatform = (deps.platform ?? getPlatform)();
|
|
46
110
|
const exitProcess = deps.processExit ?? ((code) => process.exit(code));
|
|
47
111
|
const resolvedLogger = deps.logger ?? logger;
|
|
48
|
-
|
|
49
|
-
if (
|
|
50
|
-
|
|
51
|
-
|
|
112
|
+
let autostartStatus = resolvedGetAutostartStatus();
|
|
113
|
+
if (resolvedPlatform === 'win32' || autostartStatus.platform === 'task-scheduler') {
|
|
114
|
+
if (!autostartStatus.registered) {
|
|
115
|
+
if (!deps.config) {
|
|
116
|
+
return {
|
|
117
|
+
status: 'retryable-failure',
|
|
118
|
+
handoffId: randomUUID(),
|
|
119
|
+
replacementReady: false,
|
|
120
|
+
acknowledged: false,
|
|
121
|
+
retryableFailure: true,
|
|
122
|
+
reason: 'autostart-repair-failed',
|
|
123
|
+
error: 'Windows Task Scheduler autostart is missing and runtime configuration is unavailable.',
|
|
124
|
+
};
|
|
125
|
+
}
|
|
126
|
+
try {
|
|
127
|
+
resolvedLogger.info('Windows Task Scheduler autostart is missing — repairing it before restart');
|
|
128
|
+
await resolvedRegisterWindowsTask({ token: deps.config.daemonToken, apiUrl: deps.config.apiUrl }, { startImmediately: false });
|
|
129
|
+
autostartStatus = { registered: true, platform: 'task-scheduler' };
|
|
130
|
+
}
|
|
131
|
+
catch (error) {
|
|
132
|
+
return {
|
|
133
|
+
status: 'retryable-failure',
|
|
134
|
+
handoffId: randomUUID(),
|
|
135
|
+
replacementReady: false,
|
|
136
|
+
acknowledged: false,
|
|
137
|
+
retryableFailure: true,
|
|
138
|
+
reason: 'autostart-repair-failed',
|
|
139
|
+
error: error instanceof Error ? error.message : String(error),
|
|
140
|
+
};
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
resolvedLogger.info('Restart requested — preparing an out-of-job Task Scheduler handoff');
|
|
144
|
+
const preparation = await resolvedScheduleWindowsTaskRestart();
|
|
145
|
+
if (preparation.status === 'retryable-failure') {
|
|
146
|
+
resolvedLogger.info('Restart handoff preparation failed — keeping the current runner alive for retry');
|
|
147
|
+
return preparation;
|
|
148
|
+
}
|
|
149
|
+
try {
|
|
150
|
+
await acknowledgeRestart();
|
|
151
|
+
}
|
|
152
|
+
catch (error) {
|
|
153
|
+
return {
|
|
154
|
+
status: 'retryable-failure',
|
|
155
|
+
handoffId: preparation.handoffId,
|
|
156
|
+
replacementReady: false,
|
|
157
|
+
acknowledged: false,
|
|
158
|
+
retryableFailure: true,
|
|
159
|
+
reason: 'acknowledgement-failed',
|
|
160
|
+
error: error instanceof Error ? error.message : String(error),
|
|
161
|
+
};
|
|
162
|
+
}
|
|
163
|
+
if (!(await acknowledgePreparedHandoff(preparation))) {
|
|
164
|
+
return {
|
|
165
|
+
status: 'retryable-failure',
|
|
166
|
+
handoffId: preparation.handoffId,
|
|
167
|
+
replacementReady: false,
|
|
168
|
+
acknowledged: false,
|
|
169
|
+
retryableFailure: true,
|
|
170
|
+
reason: 'replacement-confirmation-failed',
|
|
171
|
+
error: 'The prepared Windows restart helper was no longer available after acknowledgement.',
|
|
172
|
+
};
|
|
173
|
+
}
|
|
52
174
|
exitProcess(0);
|
|
53
|
-
return
|
|
175
|
+
return {
|
|
176
|
+
status: 'acknowledged',
|
|
177
|
+
handoffId: preparation.handoffId,
|
|
178
|
+
replacementReady: true,
|
|
179
|
+
acknowledged: true,
|
|
180
|
+
retryableFailure: false,
|
|
181
|
+
};
|
|
54
182
|
}
|
|
55
183
|
const supervisedRespawn = autostartStatus.registered && (autostartStatus.platform === 'launchd' || autostartStatus.platform === 'systemd');
|
|
56
184
|
if (supervisedRespawn) {
|
|
185
|
+
const handoffId = randomUUID();
|
|
186
|
+
try {
|
|
187
|
+
await acknowledgeRestart();
|
|
188
|
+
}
|
|
189
|
+
catch (error) {
|
|
190
|
+
return {
|
|
191
|
+
status: 'retryable-failure',
|
|
192
|
+
handoffId,
|
|
193
|
+
replacementReady: false,
|
|
194
|
+
acknowledged: false,
|
|
195
|
+
retryableFailure: true,
|
|
196
|
+
reason: 'acknowledgement-failed',
|
|
197
|
+
error: error instanceof Error ? error.message : String(error),
|
|
198
|
+
};
|
|
199
|
+
}
|
|
57
200
|
resolvedLogger.info('Restart requested — exiting non-zero so the OS supervisor restarts the daemon', {
|
|
58
201
|
platform: autostartStatus.platform,
|
|
59
202
|
});
|
|
60
203
|
exitProcess(1);
|
|
61
|
-
return
|
|
204
|
+
return {
|
|
205
|
+
status: 'acknowledged',
|
|
206
|
+
handoffId,
|
|
207
|
+
replacementReady: true,
|
|
208
|
+
acknowledged: true,
|
|
209
|
+
retryableFailure: false,
|
|
210
|
+
};
|
|
62
211
|
}
|
|
63
|
-
resolvedLogger.info('Restart requested —
|
|
212
|
+
resolvedLogger.info('Restart requested — preparing a detached replacement runner', {
|
|
64
213
|
platform: autostartStatus.platform,
|
|
65
214
|
registered: autostartStatus.registered,
|
|
66
215
|
osPlatform: resolvedPlatform,
|
|
67
216
|
});
|
|
68
|
-
|
|
217
|
+
const preparation = await (deps.prepareDetachedDaemon ??
|
|
218
|
+
(() => prepareDetachedRestartHandoff({ spawnDetachedDaemon: resolvedSpawnDetachedDaemon })))();
|
|
219
|
+
if (preparation.status === 'retryable-failure') {
|
|
220
|
+
return preparation;
|
|
221
|
+
}
|
|
222
|
+
try {
|
|
223
|
+
await acknowledgeRestart();
|
|
224
|
+
}
|
|
225
|
+
catch (error) {
|
|
226
|
+
return {
|
|
227
|
+
status: 'retryable-failure',
|
|
228
|
+
handoffId: preparation.handoffId,
|
|
229
|
+
replacementReady: false,
|
|
230
|
+
acknowledged: false,
|
|
231
|
+
retryableFailure: true,
|
|
232
|
+
reason: 'acknowledgement-failed',
|
|
233
|
+
error: error instanceof Error ? error.message : String(error),
|
|
234
|
+
};
|
|
235
|
+
}
|
|
236
|
+
if (!(await acknowledgePreparedHandoff(preparation))) {
|
|
237
|
+
return {
|
|
238
|
+
status: 'retryable-failure',
|
|
239
|
+
handoffId: preparation.handoffId,
|
|
240
|
+
replacementReady: false,
|
|
241
|
+
acknowledged: false,
|
|
242
|
+
retryableFailure: true,
|
|
243
|
+
reason: 'replacement-confirmation-failed',
|
|
244
|
+
error: 'The prepared replacement runner was no longer available after acknowledgement.',
|
|
245
|
+
};
|
|
246
|
+
}
|
|
69
247
|
exitProcess(0);
|
|
248
|
+
return {
|
|
249
|
+
status: 'acknowledged',
|
|
250
|
+
handoffId: preparation.handoffId,
|
|
251
|
+
replacementReady: true,
|
|
252
|
+
acknowledged: true,
|
|
253
|
+
retryableFailure: false,
|
|
254
|
+
};
|
|
70
255
|
};
|
|
71
256
|
export const restartDaemon = async (deps = {}) => {
|
|
72
257
|
const resolvedGetDaemonStatus = deps.getDaemonStatus ?? getDaemonStatus;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"daemon-control.js","sourceRoot":"","sources":["../src/daemon-control.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,IAAI,KAAK,EAAE,MAAM,sBAAsB,CAAC;AAC3D,OAAO,EAAE,QAAQ,IAAI,WAAW,EAAE,MAAM,SAAS,CAAC;AAClD,OAAO,EACL,kBAAkB,EAClB,yBAAyB,EACzB,uBAAuB,EACvB,0BAA0B,GAC3B,MAAM,gBAAgB,CAAC;AACxB,OAAO,EAAE,MAAM,EAAE,MAAM,aAAa,CAAC;AACrC,OAAO,EAAE,eAAe,EAAE,MAAM,UAAU,CAAC;AAC3C,OAAO,EAAE,eAAe,EAAE,MAAM,iBAAiB,CAAC;
|
|
1
|
+
{"version":3,"file":"daemon-control.js","sourceRoot":"","sources":["../src/daemon-control.ts"],"names":[],"mappings":"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"}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import assert from 'node:assert/strict';
|
|
2
2
|
import test from 'node:test';
|
|
3
|
-
import { executeRestartRequest, restartDaemon } from './daemon-control.js';
|
|
3
|
+
import { executeRestartRequest, prepareDetachedRestartHandoff, restartDaemon, waitForDaemonToStart, } from './daemon-control.js';
|
|
4
4
|
test('restartDaemon stops running daemon and restarts via autostart when registered', async () => {
|
|
5
5
|
const signals = [];
|
|
6
6
|
let statusChecks = 0;
|
|
@@ -64,81 +64,315 @@ test('restartDaemon delegates Windows task restart without signaling the daemon
|
|
|
64
64
|
});
|
|
65
65
|
assert.deepEqual(events, ['task-restart']);
|
|
66
66
|
});
|
|
67
|
-
test('
|
|
67
|
+
test('waitForDaemonToStart returns once the runner reports running', async () => {
|
|
68
|
+
let checks = 0;
|
|
69
|
+
const sleeps = [];
|
|
70
|
+
const status = await waitForDaemonToStart({
|
|
71
|
+
getDaemonStatus: async () => {
|
|
72
|
+
checks += 1;
|
|
73
|
+
// Runner is still booting for the first two polls, then writes its PID.
|
|
74
|
+
return checks < 3 ? { running: false, pid: null } : { running: true, pid: 999 };
|
|
75
|
+
},
|
|
76
|
+
sleep: async (milliseconds) => {
|
|
77
|
+
sleeps.push(milliseconds);
|
|
78
|
+
},
|
|
79
|
+
now: () => 0,
|
|
80
|
+
});
|
|
81
|
+
assert.deepEqual(status, { running: true, pid: 999 });
|
|
82
|
+
assert.equal(checks, 3);
|
|
83
|
+
assert.equal(sleeps.length, 2);
|
|
84
|
+
});
|
|
85
|
+
test('waitForDaemonToStart gives up after the deadline and reports not running', async () => {
|
|
86
|
+
let checks = 0;
|
|
87
|
+
const now = [0, 5, 10, 20];
|
|
88
|
+
let tick = 0;
|
|
89
|
+
const status = await waitForDaemonToStart({
|
|
90
|
+
getDaemonStatus: async () => {
|
|
91
|
+
checks += 1;
|
|
92
|
+
return { running: false, pid: null };
|
|
93
|
+
},
|
|
94
|
+
sleep: async () => undefined,
|
|
95
|
+
now: () => now[Math.min(tick++, now.length - 1)] ?? 0,
|
|
96
|
+
timeoutMs: 15,
|
|
97
|
+
});
|
|
98
|
+
assert.equal(status.running, false);
|
|
99
|
+
// Stops polling once now() passes the deadline instead of looping forever.
|
|
100
|
+
assert.ok(checks >= 1);
|
|
101
|
+
});
|
|
102
|
+
test('executeRestartRequest exits non-zero when supervised by launchd', async () => {
|
|
68
103
|
const exitCodes = [];
|
|
69
104
|
let spawned = false;
|
|
70
|
-
executeRestartRequest({
|
|
105
|
+
await executeRestartRequest({
|
|
71
106
|
getAutostartStatus: () => ({ registered: true, platform: 'launchd' }),
|
|
72
107
|
spawnDetachedDaemon: () => {
|
|
73
108
|
spawned = true;
|
|
74
109
|
},
|
|
75
110
|
platform: () => 'darwin',
|
|
76
|
-
processExit: (
|
|
111
|
+
processExit: (code) => {
|
|
77
112
|
exitCodes.push(code);
|
|
78
|
-
|
|
79
|
-
}),
|
|
113
|
+
},
|
|
80
114
|
logger: { info: () => undefined },
|
|
81
115
|
});
|
|
82
116
|
assert.deepEqual(exitCodes, [1]);
|
|
83
117
|
assert.equal(spawned, false);
|
|
84
118
|
});
|
|
85
|
-
test('executeRestartRequest exits non-zero when supervised by systemd', () => {
|
|
119
|
+
test('executeRestartRequest exits non-zero when supervised by systemd', async () => {
|
|
86
120
|
const exitCodes = [];
|
|
87
121
|
let spawned = false;
|
|
88
|
-
executeRestartRequest({
|
|
122
|
+
await executeRestartRequest({
|
|
89
123
|
getAutostartStatus: () => ({ registered: true, platform: 'systemd' }),
|
|
90
124
|
spawnDetachedDaemon: () => {
|
|
91
125
|
spawned = true;
|
|
92
126
|
},
|
|
93
127
|
platform: () => 'linux',
|
|
94
|
-
processExit: (
|
|
128
|
+
processExit: (code) => {
|
|
95
129
|
exitCodes.push(code);
|
|
96
|
-
|
|
97
|
-
}),
|
|
130
|
+
},
|
|
98
131
|
logger: { info: () => undefined },
|
|
99
132
|
});
|
|
100
133
|
assert.deepEqual(exitCodes, [1]);
|
|
101
134
|
assert.equal(spawned, false);
|
|
102
135
|
});
|
|
103
|
-
test('executeRestartRequest schedules an explicit restart and exits cleanly for Windows Task Scheduler', () => {
|
|
136
|
+
test('executeRestartRequest schedules an explicit restart and exits cleanly for Windows Task Scheduler', async () => {
|
|
104
137
|
const exitCodes = [];
|
|
105
138
|
let spawned = false;
|
|
106
139
|
let scheduled = false;
|
|
107
|
-
executeRestartRequest({
|
|
140
|
+
await executeRestartRequest({
|
|
108
141
|
getAutostartStatus: () => ({ registered: true, platform: 'task-scheduler' }),
|
|
109
|
-
scheduleWindowsTaskRestart: () => {
|
|
142
|
+
scheduleWindowsTaskRestart: async () => {
|
|
110
143
|
scheduled = true;
|
|
144
|
+
return {
|
|
145
|
+
status: 'prepared',
|
|
146
|
+
handoffId: 'windows-success',
|
|
147
|
+
markerPath: '/tmp/restart-handoff-windows-success.json',
|
|
148
|
+
replacementPid: 9002,
|
|
149
|
+
replacementReady: true,
|
|
150
|
+
acknowledged: false,
|
|
151
|
+
retryableFailure: false,
|
|
152
|
+
};
|
|
111
153
|
},
|
|
112
154
|
spawnDetachedDaemon: () => {
|
|
113
155
|
spawned = true;
|
|
114
156
|
},
|
|
115
157
|
platform: () => 'win32',
|
|
116
|
-
|
|
158
|
+
acknowledgePreparedHandoff: async () => true,
|
|
159
|
+
processExit: (code) => {
|
|
117
160
|
exitCodes.push(code);
|
|
118
|
-
|
|
119
|
-
}),
|
|
161
|
+
},
|
|
120
162
|
logger: { info: () => undefined },
|
|
121
163
|
});
|
|
122
164
|
assert.equal(spawned, false);
|
|
123
165
|
assert.equal(scheduled, true);
|
|
124
166
|
assert.deepEqual(exitCodes, [0]);
|
|
125
167
|
});
|
|
126
|
-
test('executeRestartRequest
|
|
168
|
+
test('executeRestartRequest keeps the daemon alive when the Windows restart helper fails to schedule', async () => {
|
|
127
169
|
const exitCodes = [];
|
|
128
|
-
|
|
129
|
-
|
|
170
|
+
await executeRestartRequest({
|
|
171
|
+
getAutostartStatus: () => ({ registered: true, platform: 'task-scheduler' }),
|
|
172
|
+
// Helper creation failed — must NOT exit, or the runner dies with no replacement.
|
|
173
|
+
scheduleWindowsTaskRestart: async () => ({
|
|
174
|
+
status: 'retryable-failure',
|
|
175
|
+
handoffId: 'windows-failure',
|
|
176
|
+
replacementReady: false,
|
|
177
|
+
acknowledged: false,
|
|
178
|
+
retryableFailure: true,
|
|
179
|
+
reason: 'helper-preparation-failed',
|
|
180
|
+
error: 'helper creation failed',
|
|
181
|
+
}),
|
|
182
|
+
platform: () => 'win32',
|
|
183
|
+
processExit: (code) => {
|
|
184
|
+
exitCodes.push(code);
|
|
185
|
+
},
|
|
186
|
+
logger: { info: () => undefined },
|
|
187
|
+
});
|
|
188
|
+
assert.deepEqual(exitCodes, [], 'daemon must not exit when the restart helper could not be created');
|
|
189
|
+
});
|
|
190
|
+
test('executeRestartRequest acks and exits only after a manual replacement is prepared', async () => {
|
|
191
|
+
const events = [];
|
|
192
|
+
await executeRestartRequest({
|
|
130
193
|
getAutostartStatus: () => ({ registered: false, platform: 'launchd' }),
|
|
131
|
-
|
|
132
|
-
|
|
194
|
+
prepareDetachedDaemon: async () => {
|
|
195
|
+
events.push('prepared');
|
|
196
|
+
return {
|
|
197
|
+
status: 'prepared',
|
|
198
|
+
handoffId: 'manual-success',
|
|
199
|
+
markerPath: '/tmp/restart-handoff-manual-success.json',
|
|
200
|
+
replacementPid: 9002,
|
|
201
|
+
replacementReady: true,
|
|
202
|
+
acknowledged: false,
|
|
203
|
+
retryableFailure: false,
|
|
204
|
+
};
|
|
205
|
+
},
|
|
206
|
+
acknowledgeRestart: async () => {
|
|
207
|
+
events.push('ack');
|
|
208
|
+
},
|
|
209
|
+
acknowledgePreparedHandoff: async () => {
|
|
210
|
+
events.push('commit');
|
|
211
|
+
return true;
|
|
133
212
|
},
|
|
134
213
|
platform: () => 'darwin',
|
|
135
|
-
processExit: (
|
|
214
|
+
processExit: (code) => {
|
|
215
|
+
events.push(`exit:${code}`);
|
|
216
|
+
},
|
|
217
|
+
logger: { info: () => undefined },
|
|
218
|
+
});
|
|
219
|
+
assert.deepEqual(events, ['prepared', 'ack', 'commit', 'exit:0']);
|
|
220
|
+
});
|
|
221
|
+
test('executeRestartRequest keeps the daemon alive when the prepared standby disappears during acknowledgement', async () => {
|
|
222
|
+
const events = [];
|
|
223
|
+
const result = await executeRestartRequest({
|
|
224
|
+
getAutostartStatus: () => ({ registered: false, platform: 'manual' }),
|
|
225
|
+
prepareDetachedDaemon: async () => ({
|
|
226
|
+
status: 'prepared',
|
|
227
|
+
handoffId: 'manual-disappeared',
|
|
228
|
+
markerPath: '/tmp/restart-handoff-manual-disappeared.json',
|
|
229
|
+
replacementPid: 9002,
|
|
230
|
+
replacementReady: true,
|
|
231
|
+
acknowledged: false,
|
|
232
|
+
retryableFailure: false,
|
|
233
|
+
}),
|
|
234
|
+
acknowledgeRestart: async () => {
|
|
235
|
+
events.push('ack');
|
|
236
|
+
},
|
|
237
|
+
acknowledgePreparedHandoff: async () => {
|
|
238
|
+
events.push('commit-failed');
|
|
239
|
+
return false;
|
|
240
|
+
},
|
|
241
|
+
processExit: (code) => {
|
|
242
|
+
events.push(`exit:${code}`);
|
|
243
|
+
},
|
|
244
|
+
platform: () => 'darwin',
|
|
245
|
+
logger: { info: () => undefined },
|
|
246
|
+
});
|
|
247
|
+
assert.deepEqual(events, ['ack', 'commit-failed']);
|
|
248
|
+
assert.equal(result.status, 'retryable-failure');
|
|
249
|
+
});
|
|
250
|
+
test('prepareDetachedRestartHandoff reports spawn failure without waiting for readiness', async () => {
|
|
251
|
+
let waited = false;
|
|
252
|
+
const result = await prepareDetachedRestartHandoff({
|
|
253
|
+
handoffId: 'manual-spawn-failure',
|
|
254
|
+
parentPid: 4321,
|
|
255
|
+
markerPath: '/tmp/restart-handoff.json',
|
|
256
|
+
unlink: async () => undefined,
|
|
257
|
+
spawnDetachedDaemon: () => {
|
|
258
|
+
throw new Error('spawn failed');
|
|
259
|
+
},
|
|
260
|
+
waitForPreparedRestartHandoff: async () => {
|
|
261
|
+
waited = true;
|
|
262
|
+
throw new Error('should not wait');
|
|
263
|
+
},
|
|
264
|
+
});
|
|
265
|
+
assert.equal(result.status, 'retryable-failure');
|
|
266
|
+
assert.equal(result.reason, 'replacement-preparation-failed');
|
|
267
|
+
assert.equal(waited, false);
|
|
268
|
+
});
|
|
269
|
+
test('executeRestartRequest keeps supervised runners alive when acknowledgement fails', async () => {
|
|
270
|
+
const exitCodes = [];
|
|
271
|
+
const result = await executeRestartRequest({
|
|
272
|
+
getAutostartStatus: () => ({ registered: true, platform: 'systemd' }),
|
|
273
|
+
acknowledgeRestart: async () => {
|
|
274
|
+
throw new Error('network down');
|
|
275
|
+
},
|
|
276
|
+
platform: () => 'linux',
|
|
277
|
+
processExit: (code) => {
|
|
136
278
|
exitCodes.push(code);
|
|
137
|
-
|
|
279
|
+
},
|
|
280
|
+
logger: { info: () => undefined },
|
|
281
|
+
});
|
|
282
|
+
assert.equal(result.status, 'retryable-failure');
|
|
283
|
+
assert.equal(result.reason, 'acknowledgement-failed');
|
|
284
|
+
assert.deepEqual(exitCodes, []);
|
|
285
|
+
});
|
|
286
|
+
test('executeRestartRequest preserves the current runner and request when the Windows helper reports a post-create failure', async () => {
|
|
287
|
+
const events = [];
|
|
288
|
+
const result = await executeRestartRequest({
|
|
289
|
+
getAutostartStatus: () => ({ registered: true, platform: 'task-scheduler' }),
|
|
290
|
+
scheduleWindowsTaskRestart: async () => ({
|
|
291
|
+
status: 'retryable-failure',
|
|
292
|
+
handoffId: 'handoff-query-failure',
|
|
293
|
+
replacementReady: false,
|
|
294
|
+
acknowledged: false,
|
|
295
|
+
retryableFailure: true,
|
|
296
|
+
reason: 'helper-preparation-failed',
|
|
297
|
+
error: 'schtasks /Query failed with exit 1',
|
|
138
298
|
}),
|
|
299
|
+
acknowledgeRestart: async () => {
|
|
300
|
+
events.push('ack');
|
|
301
|
+
},
|
|
302
|
+
spawnDetachedDaemon: () => {
|
|
303
|
+
events.push('spawn');
|
|
304
|
+
},
|
|
305
|
+
processExit: (code) => {
|
|
306
|
+
events.push(`exit:${code}`);
|
|
307
|
+
},
|
|
308
|
+
platform: () => 'win32',
|
|
139
309
|
logger: { info: () => undefined },
|
|
140
310
|
});
|
|
141
|
-
assert.
|
|
142
|
-
assert.
|
|
311
|
+
assert.deepEqual(events, [], 'a post-create helper failure must not ack or terminate the current runner');
|
|
312
|
+
assert.equal(result.status, 'retryable-failure');
|
|
313
|
+
});
|
|
314
|
+
test('executeRestartRequest repairs a missing Windows scheduled task before preparing handoff', async () => {
|
|
315
|
+
const events = [];
|
|
316
|
+
const result = await executeRestartRequest({
|
|
317
|
+
getAutostartStatus: () => ({ registered: false, platform: 'task-scheduler' }),
|
|
318
|
+
registerWindowsTask: async () => {
|
|
319
|
+
events.push('register');
|
|
320
|
+
return { registered: true, servicePath: 'task.xml', platform: 'task-scheduler' };
|
|
321
|
+
},
|
|
322
|
+
scheduleWindowsTaskRestart: async () => {
|
|
323
|
+
events.push('prepare');
|
|
324
|
+
return {
|
|
325
|
+
status: 'retryable-failure',
|
|
326
|
+
handoffId: 'handoff-after-repair',
|
|
327
|
+
replacementReady: false,
|
|
328
|
+
acknowledged: false,
|
|
329
|
+
retryableFailure: true,
|
|
330
|
+
reason: 'helper-preparation-failed',
|
|
331
|
+
error: 'replacement did not become ready',
|
|
332
|
+
};
|
|
333
|
+
},
|
|
334
|
+
acknowledgeRestart: async () => {
|
|
335
|
+
events.push('ack');
|
|
336
|
+
},
|
|
337
|
+
spawnDetachedDaemon: () => {
|
|
338
|
+
events.push('spawn');
|
|
339
|
+
},
|
|
340
|
+
processExit: (code) => {
|
|
341
|
+
events.push(`exit:${code}`);
|
|
342
|
+
},
|
|
343
|
+
platform: () => 'win32',
|
|
344
|
+
config: { daemonToken: 'secret-token', apiUrl: 'https://api.example' },
|
|
345
|
+
logger: { info: () => undefined },
|
|
346
|
+
});
|
|
347
|
+
assert.deepEqual(events, ['register', 'prepare']);
|
|
348
|
+
assert.equal(result.status, 'retryable-failure');
|
|
349
|
+
});
|
|
350
|
+
test('executeRestartRequest does not ack or exit when a manual replacement never becomes ready', async () => {
|
|
351
|
+
const events = [];
|
|
352
|
+
const result = await executeRestartRequest({
|
|
353
|
+
getAutostartStatus: () => ({ registered: false, platform: 'manual' }),
|
|
354
|
+
prepareDetachedDaemon: async () => ({
|
|
355
|
+
status: 'retryable-failure',
|
|
356
|
+
handoffId: 'manual-timeout',
|
|
357
|
+
replacementReady: false,
|
|
358
|
+
acknowledged: false,
|
|
359
|
+
retryableFailure: true,
|
|
360
|
+
reason: 'replacement-preparation-failed',
|
|
361
|
+
error: 'replacement readiness timed out',
|
|
362
|
+
}),
|
|
363
|
+
acknowledgeRestart: async () => {
|
|
364
|
+
events.push('ack');
|
|
365
|
+
},
|
|
366
|
+
spawnDetachedDaemon: () => {
|
|
367
|
+
events.push('spawn');
|
|
368
|
+
},
|
|
369
|
+
processExit: (code) => {
|
|
370
|
+
events.push(`exit:${code}`);
|
|
371
|
+
},
|
|
372
|
+
platform: () => 'darwin',
|
|
373
|
+
logger: { info: () => undefined },
|
|
374
|
+
});
|
|
375
|
+
assert.deepEqual(events, [], 'spawn success alone must not ack or terminate the current runner');
|
|
376
|
+
assert.equal(result.status, 'retryable-failure');
|
|
143
377
|
});
|
|
144
378
|
//# sourceMappingURL=daemon-control.test.js.map
|