goke 6.13.0 → 6.14.1
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/__test__/daemon.test.js +91 -0
- package/dist/daemon.d.ts +14 -0
- package/dist/daemon.d.ts.map +1 -1
- package/dist/daemon.js +39 -4
- package/package.json +1 -1
- package/src/__test__/daemon.test.ts +94 -0
- package/src/daemon.ts +47 -4
|
@@ -282,6 +282,97 @@ process.exit(0)
|
|
|
282
282
|
// Should return false because heartbeat is stale (even though PID is alive)
|
|
283
283
|
expect(await ctx.isRunning()).toBe(false);
|
|
284
284
|
});
|
|
285
|
+
test('start with attach: true pipes stdout/stderr and waits for exit', async () => {
|
|
286
|
+
const helperScript = path.join(os.tmpdir(), 'goke-daemon-test-attach.mjs');
|
|
287
|
+
// This helper writes to stdout/stderr and exits after a short delay.
|
|
288
|
+
// The PID file setup mirrors the standard daemon helper.
|
|
289
|
+
fs.writeFileSync(helperScript, `
|
|
290
|
+
import fs from 'node:fs'
|
|
291
|
+
import path from 'node:path'
|
|
292
|
+
import os from 'node:os'
|
|
293
|
+
import crypto from 'node:crypto'
|
|
294
|
+
|
|
295
|
+
const DAEMON_DIR = path.join(os.homedir(), '.config', 'goke', 'daemons')
|
|
296
|
+
const cliName = process.env.TEST_CLI_NAME || 'test-daemon-cli'
|
|
297
|
+
const cmdName = process.env.TEST_CMD_NAME || 'attach-test'
|
|
298
|
+
const safeName = cliName + '--' + cmdName
|
|
299
|
+
const pidFile = path.join(DAEMON_DIR, safeName + '.pid.json')
|
|
300
|
+
|
|
301
|
+
fs.mkdirSync(path.dirname(pidFile), { recursive: true })
|
|
302
|
+
|
|
303
|
+
const instanceId = crypto.randomBytes(8).toString('hex')
|
|
304
|
+
const pidData = { pid: process.pid, id: instanceId, startedAt: Date.now(), heartbeatAt: Date.now() }
|
|
305
|
+
fs.writeFileSync(pidFile, JSON.stringify(pidData), { mode: 0o600 })
|
|
306
|
+
|
|
307
|
+
// Write to stdout and stderr so the parent can see it
|
|
308
|
+
console.log('DAEMON_STDOUT_MESSAGE')
|
|
309
|
+
console.error('DAEMON_STDERR_MESSAGE')
|
|
310
|
+
|
|
311
|
+
// Exit after a short delay
|
|
312
|
+
setTimeout(() => {
|
|
313
|
+
try {
|
|
314
|
+
const current = JSON.parse(fs.readFileSync(pidFile, 'utf-8'))
|
|
315
|
+
if (current.id === instanceId) fs.unlinkSync(pidFile)
|
|
316
|
+
} catch {}
|
|
317
|
+
process.exit(0)
|
|
318
|
+
}, 500)
|
|
319
|
+
`);
|
|
320
|
+
testPidFiles.push(pidFilePath('test-daemon-cli', 'attach-test'));
|
|
321
|
+
const { DaemonContext } = await import('../daemon.js');
|
|
322
|
+
const ctx = new DaemonContext('test-daemon-cli', 'attach-test', [
|
|
323
|
+
process.execPath, helperScript, 'attach-test',
|
|
324
|
+
]);
|
|
325
|
+
// attach: true should wait for the daemon to finish
|
|
326
|
+
await ctx.start({ attach: true, timeoutMs: 10_000 });
|
|
327
|
+
// After start resolves, the daemon should have exited and cleaned up its PID file
|
|
328
|
+
expect(await ctx.isRunning()).toBe(false);
|
|
329
|
+
try {
|
|
330
|
+
fs.unlinkSync(helperScript);
|
|
331
|
+
}
|
|
332
|
+
catch { }
|
|
333
|
+
}, 15_000);
|
|
334
|
+
test('start with attach: true throws on non-zero exit', async () => {
|
|
335
|
+
const helperScript = path.join(os.tmpdir(), 'goke-daemon-test-attach-fail.mjs');
|
|
336
|
+
fs.writeFileSync(helperScript, `
|
|
337
|
+
import fs from 'node:fs'
|
|
338
|
+
import path from 'node:path'
|
|
339
|
+
import os from 'node:os'
|
|
340
|
+
import crypto from 'node:crypto'
|
|
341
|
+
|
|
342
|
+
const DAEMON_DIR = path.join(os.homedir(), '.config', 'goke', 'daemons')
|
|
343
|
+
const cliName = process.env.TEST_CLI_NAME || 'test-daemon-cli'
|
|
344
|
+
const cmdName = process.env.TEST_CMD_NAME || 'attach-fail'
|
|
345
|
+
const safeName = cliName + '--' + cmdName
|
|
346
|
+
const pidFile = path.join(DAEMON_DIR, safeName + '.pid.json')
|
|
347
|
+
|
|
348
|
+
fs.mkdirSync(path.dirname(pidFile), { recursive: true })
|
|
349
|
+
|
|
350
|
+
const instanceId = crypto.randomBytes(8).toString('hex')
|
|
351
|
+
const pidData = { pid: process.pid, id: instanceId, startedAt: Date.now(), heartbeatAt: Date.now() }
|
|
352
|
+
fs.writeFileSync(pidFile, JSON.stringify(pidData), { mode: 0o600 })
|
|
353
|
+
|
|
354
|
+
console.error('Something went wrong')
|
|
355
|
+
|
|
356
|
+
setTimeout(() => {
|
|
357
|
+
try {
|
|
358
|
+
const current = JSON.parse(fs.readFileSync(pidFile, 'utf-8'))
|
|
359
|
+
if (current.id === instanceId) fs.unlinkSync(pidFile)
|
|
360
|
+
} catch {}
|
|
361
|
+
process.exit(1)
|
|
362
|
+
}, 500)
|
|
363
|
+
`);
|
|
364
|
+
testPidFiles.push(pidFilePath('test-daemon-cli', 'attach-fail'));
|
|
365
|
+
const { DaemonContext } = await import('../daemon.js');
|
|
366
|
+
const ctx = new DaemonContext('test-daemon-cli', 'attach-fail', [
|
|
367
|
+
process.execPath, helperScript, 'attach-fail',
|
|
368
|
+
]);
|
|
369
|
+
await expect(ctx.start({ attach: true, timeoutMs: 10_000 }))
|
|
370
|
+
.rejects.toThrow('exited with code 1');
|
|
371
|
+
try {
|
|
372
|
+
fs.unlinkSync(helperScript);
|
|
373
|
+
}
|
|
374
|
+
catch { }
|
|
375
|
+
}, 15_000);
|
|
285
376
|
test('daemon context has correct command name from parsed cli', async () => {
|
|
286
377
|
const { default: goke } = await import('../index.js');
|
|
287
378
|
const cli = goke('my-app');
|
package/dist/daemon.d.ts
CHANGED
|
@@ -29,6 +29,15 @@ interface DaemonStartOptions {
|
|
|
29
29
|
timeoutMs?: number;
|
|
30
30
|
/** Extra environment variables passed to the daemon process. */
|
|
31
31
|
env?: Record<string, string>;
|
|
32
|
+
/**
|
|
33
|
+
* When true, pipe daemon stdout/stderr to the parent process and wait
|
|
34
|
+
* for the daemon to exit before resolving. This lets interactive users
|
|
35
|
+
* see real-time logs and error messages from the daemon.
|
|
36
|
+
*
|
|
37
|
+
* When false (default), the daemon runs fully detached with no stdio
|
|
38
|
+
* and start() returns as soon as the PID file is confirmed.
|
|
39
|
+
*/
|
|
40
|
+
attach?: boolean;
|
|
32
41
|
}
|
|
33
42
|
/**
|
|
34
43
|
* Daemon context available on every command's execution context.
|
|
@@ -57,6 +66,11 @@ declare class DaemonContext {
|
|
|
57
66
|
/**
|
|
58
67
|
* Spawn the current command as a detached background daemon process.
|
|
59
68
|
* Kills any existing daemon for this command first.
|
|
69
|
+
*
|
|
70
|
+
* When `attach: true`, pipes daemon stdout/stderr to the parent and
|
|
71
|
+
* waits for the daemon to exit. Throws if the daemon exits with a
|
|
72
|
+
* non-zero code. This is useful for interactive login flows where the
|
|
73
|
+
* user wants to see real-time logs and error messages.
|
|
60
74
|
*/
|
|
61
75
|
start(options?: DaemonStartOptions): Promise<void>;
|
|
62
76
|
/**
|
package/dist/daemon.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"daemon.d.ts","sourceRoot":"","sources":["../src/daemon.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;AA6IH,UAAU,kBAAkB;IAC1B,8DAA8D;IAC9D,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB,gEAAgE;IAChE,GAAG,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;
|
|
1
|
+
{"version":3,"file":"daemon.d.ts","sourceRoot":"","sources":["../src/daemon.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;AA6IH,UAAU,kBAAkB;IAC1B,8DAA8D;IAC9D,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB,gEAAgE;IAChE,GAAG,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;IAC5B;;;;;;;OAOG;IACH,MAAM,CAAC,EAAE,OAAO,CAAA;CACjB;AAED;;;;;;;;;;GAUG;AACH,cAAM,aAAa;;IACjB,uDAAuD;IACvD,QAAQ,CAAC,QAAQ,EAAE,OAAO,CAAA;gBAYxB,OAAO,EAAE,MAAM,EACf,WAAW,EAAE,MAAM,EACnB,IAAI,EAAE,MAAM,EAAE,EACd,GAAG,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,SAAS,CAAC;IAc1C;;;;;;OAMG;IACH,UAAU,CAAC,WAAW,EAAE,MAAM,GAAG,aAAa;IAkF9C;;;;;;;;OAQG;IACG,KAAK,CAAC,OAAO,CAAC,EAAE,kBAAkB,GAAG,OAAO,CAAC,IAAI,CAAC;IAoExD;;OAEG;IACG,IAAI,IAAI,OAAO,CAAC,IAAI,CAAC;IAkB3B;;;;OAIG;IACG,SAAS,IAAI,OAAO,CAAC,OAAO,CAAC;CAcpC;AAED;;;GAGG;AACH,iBAAS,mBAAmB,CAC1B,OAAO,EAAE,MAAM,EACf,WAAW,EAAE,MAAM,EACnB,IAAI,EAAE,MAAM,EAAE,EACd,GAAG,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,SAAS,CAAC,GACvC,aAAa,CAEf;AAED,OAAO,EAAE,aAAa,EAAE,mBAAmB,EAAE,CAAA;AAC7C,YAAY,EAAE,kBAAkB,EAAE,CAAA"}
|
package/dist/daemon.js
CHANGED
|
@@ -259,9 +259,15 @@ class DaemonContext {
|
|
|
259
259
|
/**
|
|
260
260
|
* Spawn the current command as a detached background daemon process.
|
|
261
261
|
* Kills any existing daemon for this command first.
|
|
262
|
+
*
|
|
263
|
+
* When `attach: true`, pipes daemon stdout/stderr to the parent and
|
|
264
|
+
* waits for the daemon to exit. Throws if the daemon exits with a
|
|
265
|
+
* non-zero code. This is useful for interactive login flows where the
|
|
266
|
+
* user wants to see real-time logs and error messages.
|
|
262
267
|
*/
|
|
263
268
|
async start(options) {
|
|
264
269
|
const timeoutMs = options?.timeoutMs ?? 10 * 60 * 1000;
|
|
270
|
+
const attach = options?.attach ?? false;
|
|
265
271
|
// Kill existing daemon if running
|
|
266
272
|
await this.stop();
|
|
267
273
|
const env = {
|
|
@@ -275,18 +281,47 @@ class DaemonContext {
|
|
|
275
281
|
const execPath = this.#argv[0];
|
|
276
282
|
const args = this.#argv.slice(1);
|
|
277
283
|
const child = spawn(execPath, args, {
|
|
278
|
-
|
|
279
|
-
|
|
284
|
+
// Detach only when running in background so the daemon outlives the
|
|
285
|
+
// parent. In attached mode, keep the child in the same process group
|
|
286
|
+
// so signals propagate naturally and the parent stays alive.
|
|
287
|
+
detached: !attach,
|
|
288
|
+
stdio: attach ? ['ignore', 'inherit', 'inherit'] : 'ignore',
|
|
280
289
|
env,
|
|
281
290
|
});
|
|
282
|
-
|
|
291
|
+
// Only unref when detached (non-attached) so the parent can exit
|
|
292
|
+
// immediately. In attached mode, the parent must stay alive to wait for
|
|
293
|
+
// the child's 'close' event — unref() would let the event loop drain
|
|
294
|
+
// and the parent would exit before the child finishes.
|
|
295
|
+
if (!attach) {
|
|
296
|
+
child.unref();
|
|
297
|
+
}
|
|
298
|
+
// Register close/error listeners immediately after spawn, before the
|
|
299
|
+
// PID file polling loop. If the child exits very quickly (writes PID
|
|
300
|
+
// file then exits), registering listeners after seeing the PID file
|
|
301
|
+
// could miss the 'close' event and hang the promise forever.
|
|
302
|
+
let childDone;
|
|
303
|
+
if (attach) {
|
|
304
|
+
childDone = new Promise((resolve, reject) => {
|
|
305
|
+
child.on('close', (code) => {
|
|
306
|
+
if (code && code !== 0) {
|
|
307
|
+
reject(new Error(`Daemon "${this.#cliName} ${this.#commandName}" exited with code ${code}`));
|
|
308
|
+
}
|
|
309
|
+
else {
|
|
310
|
+
resolve();
|
|
311
|
+
}
|
|
312
|
+
});
|
|
313
|
+
child.on('error', reject);
|
|
314
|
+
});
|
|
315
|
+
}
|
|
283
316
|
// Brief wait to confirm the daemon started and wrote its PID file
|
|
284
317
|
const startDeadline = Date.now() + 5000;
|
|
285
318
|
while (Date.now() < startDeadline) {
|
|
286
319
|
await new Promise((r) => setTimeout(r, 100));
|
|
287
320
|
const pidData = readPidFile(this.#pidFile);
|
|
288
321
|
if (pidData && isProcessAlive(pidData.pid)) {
|
|
289
|
-
|
|
322
|
+
if (!attach)
|
|
323
|
+
return;
|
|
324
|
+
return childDone;
|
|
290
325
|
}
|
|
291
326
|
}
|
|
292
327
|
throw new Error(`Failed to start daemon for "${this.#cliName} ${this.#commandName}"`);
|
package/package.json
CHANGED
|
@@ -318,6 +318,100 @@ process.exit(0)
|
|
|
318
318
|
expect(await ctx.isRunning()).toBe(false)
|
|
319
319
|
})
|
|
320
320
|
|
|
321
|
+
test('start with attach: true pipes stdout/stderr and waits for exit', async () => {
|
|
322
|
+
const helperScript = path.join(os.tmpdir(), 'goke-daemon-test-attach.mjs')
|
|
323
|
+
// This helper writes to stdout/stderr and exits after a short delay.
|
|
324
|
+
// The PID file setup mirrors the standard daemon helper.
|
|
325
|
+
fs.writeFileSync(helperScript, `
|
|
326
|
+
import fs from 'node:fs'
|
|
327
|
+
import path from 'node:path'
|
|
328
|
+
import os from 'node:os'
|
|
329
|
+
import crypto from 'node:crypto'
|
|
330
|
+
|
|
331
|
+
const DAEMON_DIR = path.join(os.homedir(), '.config', 'goke', 'daemons')
|
|
332
|
+
const cliName = process.env.TEST_CLI_NAME || 'test-daemon-cli'
|
|
333
|
+
const cmdName = process.env.TEST_CMD_NAME || 'attach-test'
|
|
334
|
+
const safeName = cliName + '--' + cmdName
|
|
335
|
+
const pidFile = path.join(DAEMON_DIR, safeName + '.pid.json')
|
|
336
|
+
|
|
337
|
+
fs.mkdirSync(path.dirname(pidFile), { recursive: true })
|
|
338
|
+
|
|
339
|
+
const instanceId = crypto.randomBytes(8).toString('hex')
|
|
340
|
+
const pidData = { pid: process.pid, id: instanceId, startedAt: Date.now(), heartbeatAt: Date.now() }
|
|
341
|
+
fs.writeFileSync(pidFile, JSON.stringify(pidData), { mode: 0o600 })
|
|
342
|
+
|
|
343
|
+
// Write to stdout and stderr so the parent can see it
|
|
344
|
+
console.log('DAEMON_STDOUT_MESSAGE')
|
|
345
|
+
console.error('DAEMON_STDERR_MESSAGE')
|
|
346
|
+
|
|
347
|
+
// Exit after a short delay
|
|
348
|
+
setTimeout(() => {
|
|
349
|
+
try {
|
|
350
|
+
const current = JSON.parse(fs.readFileSync(pidFile, 'utf-8'))
|
|
351
|
+
if (current.id === instanceId) fs.unlinkSync(pidFile)
|
|
352
|
+
} catch {}
|
|
353
|
+
process.exit(0)
|
|
354
|
+
}, 500)
|
|
355
|
+
`)
|
|
356
|
+
testPidFiles.push(pidFilePath('test-daemon-cli', 'attach-test'))
|
|
357
|
+
|
|
358
|
+
const { DaemonContext } = await import('../daemon.js')
|
|
359
|
+
const ctx = new DaemonContext('test-daemon-cli', 'attach-test', [
|
|
360
|
+
process.execPath, helperScript, 'attach-test',
|
|
361
|
+
])
|
|
362
|
+
|
|
363
|
+
// attach: true should wait for the daemon to finish
|
|
364
|
+
await ctx.start({ attach: true, timeoutMs: 10_000 })
|
|
365
|
+
|
|
366
|
+
// After start resolves, the daemon should have exited and cleaned up its PID file
|
|
367
|
+
expect(await ctx.isRunning()).toBe(false)
|
|
368
|
+
|
|
369
|
+
try { fs.unlinkSync(helperScript) } catch {}
|
|
370
|
+
}, 15_000)
|
|
371
|
+
|
|
372
|
+
test('start with attach: true throws on non-zero exit', async () => {
|
|
373
|
+
const helperScript = path.join(os.tmpdir(), 'goke-daemon-test-attach-fail.mjs')
|
|
374
|
+
fs.writeFileSync(helperScript, `
|
|
375
|
+
import fs from 'node:fs'
|
|
376
|
+
import path from 'node:path'
|
|
377
|
+
import os from 'node:os'
|
|
378
|
+
import crypto from 'node:crypto'
|
|
379
|
+
|
|
380
|
+
const DAEMON_DIR = path.join(os.homedir(), '.config', 'goke', 'daemons')
|
|
381
|
+
const cliName = process.env.TEST_CLI_NAME || 'test-daemon-cli'
|
|
382
|
+
const cmdName = process.env.TEST_CMD_NAME || 'attach-fail'
|
|
383
|
+
const safeName = cliName + '--' + cmdName
|
|
384
|
+
const pidFile = path.join(DAEMON_DIR, safeName + '.pid.json')
|
|
385
|
+
|
|
386
|
+
fs.mkdirSync(path.dirname(pidFile), { recursive: true })
|
|
387
|
+
|
|
388
|
+
const instanceId = crypto.randomBytes(8).toString('hex')
|
|
389
|
+
const pidData = { pid: process.pid, id: instanceId, startedAt: Date.now(), heartbeatAt: Date.now() }
|
|
390
|
+
fs.writeFileSync(pidFile, JSON.stringify(pidData), { mode: 0o600 })
|
|
391
|
+
|
|
392
|
+
console.error('Something went wrong')
|
|
393
|
+
|
|
394
|
+
setTimeout(() => {
|
|
395
|
+
try {
|
|
396
|
+
const current = JSON.parse(fs.readFileSync(pidFile, 'utf-8'))
|
|
397
|
+
if (current.id === instanceId) fs.unlinkSync(pidFile)
|
|
398
|
+
} catch {}
|
|
399
|
+
process.exit(1)
|
|
400
|
+
}, 500)
|
|
401
|
+
`)
|
|
402
|
+
testPidFiles.push(pidFilePath('test-daemon-cli', 'attach-fail'))
|
|
403
|
+
|
|
404
|
+
const { DaemonContext } = await import('../daemon.js')
|
|
405
|
+
const ctx = new DaemonContext('test-daemon-cli', 'attach-fail', [
|
|
406
|
+
process.execPath, helperScript, 'attach-fail',
|
|
407
|
+
])
|
|
408
|
+
|
|
409
|
+
await expect(ctx.start({ attach: true, timeoutMs: 10_000 }))
|
|
410
|
+
.rejects.toThrow('exited with code 1')
|
|
411
|
+
|
|
412
|
+
try { fs.unlinkSync(helperScript) } catch {}
|
|
413
|
+
}, 15_000)
|
|
414
|
+
|
|
321
415
|
test('daemon context has correct command name from parsed cli', async () => {
|
|
322
416
|
const { default: goke } = await import('../index.js')
|
|
323
417
|
const cli = goke('my-app')
|
package/src/daemon.ts
CHANGED
|
@@ -169,6 +169,15 @@ interface DaemonStartOptions {
|
|
|
169
169
|
timeoutMs?: number
|
|
170
170
|
/** Extra environment variables passed to the daemon process. */
|
|
171
171
|
env?: Record<string, string>
|
|
172
|
+
/**
|
|
173
|
+
* When true, pipe daemon stdout/stderr to the parent process and wait
|
|
174
|
+
* for the daemon to exit before resolving. This lets interactive users
|
|
175
|
+
* see real-time logs and error messages from the daemon.
|
|
176
|
+
*
|
|
177
|
+
* When false (default), the daemon runs fully detached with no stdio
|
|
178
|
+
* and start() returns as soon as the PID file is confirmed.
|
|
179
|
+
*/
|
|
180
|
+
attach?: boolean
|
|
172
181
|
}
|
|
173
182
|
|
|
174
183
|
/**
|
|
@@ -305,9 +314,15 @@ class DaemonContext {
|
|
|
305
314
|
/**
|
|
306
315
|
* Spawn the current command as a detached background daemon process.
|
|
307
316
|
* Kills any existing daemon for this command first.
|
|
317
|
+
*
|
|
318
|
+
* When `attach: true`, pipes daemon stdout/stderr to the parent and
|
|
319
|
+
* waits for the daemon to exit. Throws if the daemon exits with a
|
|
320
|
+
* non-zero code. This is useful for interactive login flows where the
|
|
321
|
+
* user wants to see real-time logs and error messages.
|
|
308
322
|
*/
|
|
309
323
|
async start(options?: DaemonStartOptions): Promise<void> {
|
|
310
324
|
const timeoutMs = options?.timeoutMs ?? 10 * 60 * 1000
|
|
325
|
+
const attach = options?.attach ?? false
|
|
311
326
|
|
|
312
327
|
// Kill existing daemon if running
|
|
313
328
|
await this.stop()
|
|
@@ -325,12 +340,39 @@ class DaemonContext {
|
|
|
325
340
|
const args = this.#argv.slice(1)
|
|
326
341
|
|
|
327
342
|
const child = spawn(execPath, args, {
|
|
328
|
-
|
|
329
|
-
|
|
343
|
+
// Detach only when running in background so the daemon outlives the
|
|
344
|
+
// parent. In attached mode, keep the child in the same process group
|
|
345
|
+
// so signals propagate naturally and the parent stays alive.
|
|
346
|
+
detached: !attach,
|
|
347
|
+
stdio: attach ? ['ignore', 'inherit', 'inherit'] : 'ignore',
|
|
330
348
|
env,
|
|
331
349
|
})
|
|
332
350
|
|
|
333
|
-
|
|
351
|
+
// Only unref when detached (non-attached) so the parent can exit
|
|
352
|
+
// immediately. In attached mode, the parent must stay alive to wait for
|
|
353
|
+
// the child's 'close' event — unref() would let the event loop drain
|
|
354
|
+
// and the parent would exit before the child finishes.
|
|
355
|
+
if (!attach) {
|
|
356
|
+
child.unref()
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
// Register close/error listeners immediately after spawn, before the
|
|
360
|
+
// PID file polling loop. If the child exits very quickly (writes PID
|
|
361
|
+
// file then exits), registering listeners after seeing the PID file
|
|
362
|
+
// could miss the 'close' event and hang the promise forever.
|
|
363
|
+
let childDone: Promise<void> | undefined
|
|
364
|
+
if (attach) {
|
|
365
|
+
childDone = new Promise<void>((resolve, reject) => {
|
|
366
|
+
child.on('close', (code) => {
|
|
367
|
+
if (code && code !== 0) {
|
|
368
|
+
reject(new Error(`Daemon "${this.#cliName} ${this.#commandName}" exited with code ${code}`))
|
|
369
|
+
} else {
|
|
370
|
+
resolve()
|
|
371
|
+
}
|
|
372
|
+
})
|
|
373
|
+
child.on('error', reject)
|
|
374
|
+
})
|
|
375
|
+
}
|
|
334
376
|
|
|
335
377
|
// Brief wait to confirm the daemon started and wrote its PID file
|
|
336
378
|
const startDeadline = Date.now() + 5000
|
|
@@ -338,7 +380,8 @@ class DaemonContext {
|
|
|
338
380
|
await new Promise((r) => setTimeout(r, 100))
|
|
339
381
|
const pidData = readPidFile(this.#pidFile)
|
|
340
382
|
if (pidData && isProcessAlive(pidData.pid)) {
|
|
341
|
-
return
|
|
383
|
+
if (!attach) return
|
|
384
|
+
return childDone
|
|
342
385
|
}
|
|
343
386
|
}
|
|
344
387
|
|