@zgeoff/atc 3.6.1 → 3.6.2
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/package.json
CHANGED
|
@@ -12,6 +12,14 @@ interface StubExecutionProviderConfig {
|
|
|
12
12
|
// Capabilities that differ from a local pseudo-terminal's.
|
|
13
13
|
readonly capabilities?: Partial<ExecutionCapabilities>;
|
|
14
14
|
|
|
15
|
+
// Whether the daemon treats the host as another machine; false when left
|
|
16
|
+
// out.
|
|
17
|
+
readonly remote?: boolean;
|
|
18
|
+
|
|
19
|
+
// The error every command rejects with instead of running; left out,
|
|
20
|
+
// commands run on this machine.
|
|
21
|
+
readonly commandFailure?: Readonly<Error>;
|
|
22
|
+
|
|
15
23
|
/**
|
|
16
24
|
* Called with each harness spec before the harness starts; a throw from
|
|
17
25
|
* it aborts the spawn.
|
|
@@ -42,9 +50,10 @@ interface StubExecutionProvider extends ExecutionProvider {
|
|
|
42
50
|
/**
|
|
43
51
|
* An execution provider for daemon tests that runs harnesses, transfers, and
|
|
44
52
|
* commands on this machine as the local pseudo-terminal provider does, with
|
|
45
|
-
* the capabilities the config changes on top of its own.
|
|
46
|
-
*
|
|
47
|
-
*
|
|
53
|
+
* the capabilities the config changes on top of its own. The config can
|
|
54
|
+
* mark the host remote, and can make every command reject with an error
|
|
55
|
+
* instead of running. It reports each harness spec to the config before
|
|
56
|
+
* starting it. A host suspend or destroy does nothing to the machine: it records the host in `suspended` or
|
|
48
57
|
* `destroyed` and resolves, or rejects with the error a failure setter gave.
|
|
49
58
|
* The daemon calls them only when the capabilities declare `suspend` or
|
|
50
59
|
* `destroy`.
|
|
@@ -55,6 +64,7 @@ export function buildStubExecutionProvider(
|
|
|
55
64
|
const local = new LocalPTYProvider();
|
|
56
65
|
|
|
57
66
|
const onSpawn = config.onSpawn ?? (() => {});
|
|
67
|
+
const commandFailure = config.commandFailure;
|
|
58
68
|
const suspended: string[] = [];
|
|
59
69
|
const destroyed: string[] = [];
|
|
60
70
|
|
|
@@ -67,7 +77,7 @@ export function buildStubExecutionProvider(
|
|
|
67
77
|
|
|
68
78
|
return {
|
|
69
79
|
kind: config.kind ?? 'stub',
|
|
70
|
-
remote: false,
|
|
80
|
+
remote: config.remote ?? false,
|
|
71
81
|
capabilities: { ...local.capabilities, ...config.capabilities },
|
|
72
82
|
prepareHost: local.prepareHost,
|
|
73
83
|
spawnHarness: (spec) => {
|
|
@@ -76,7 +86,8 @@ export function buildStubExecutionProvider(
|
|
|
76
86
|
return local.spawnHarness(spec);
|
|
77
87
|
},
|
|
78
88
|
transferArchive: local.transferArchive,
|
|
79
|
-
runCommand:
|
|
89
|
+
runCommand:
|
|
90
|
+
commandFailure === undefined ? local.runCommand : () => Promise.reject(commandFailure),
|
|
80
91
|
suspendHost: (host) => {
|
|
81
92
|
const failure = failures.suspend;
|
|
82
93
|
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { randomBytes, randomUUID } from 'node:crypto';
|
|
2
2
|
import { rmSync } from 'node:fs';
|
|
3
3
|
import { posix } from 'node:path';
|
|
4
|
+
import type { Subprocess } from 'bun';
|
|
4
5
|
import { spawn } from 'bun-pty';
|
|
5
6
|
import type { IPty } from 'bun-pty';
|
|
6
7
|
import type {
|
|
@@ -52,16 +53,14 @@ import { registerTestCleanup } from './register-test-cleanup';
|
|
|
52
53
|
* broker variable, and when it would join a process that started without
|
|
53
54
|
* the broker required. Once the current test finishes, the stand-in kills
|
|
54
55
|
* every process and stops every forward it holds, so it must be created inside
|
|
55
|
-
* a test; `stop` does so sooner, and a second stop does nothing.
|
|
56
|
+
* a test; `stop` does so sooner, and a second stop does nothing. Leases
|
|
57
|
+
* expire, and cold boots are stamped, by `now`, the wall clock when absent.
|
|
56
58
|
*/
|
|
57
|
-
export function createStubImpPort(
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
readonly principal: string;
|
|
63
|
-
readonly label: string;
|
|
64
|
-
readonly until: number;
|
|
59
|
+
export function createStubImpPort(
|
|
60
|
+
principal = 'token:atc',
|
|
61
|
+
now: () => number = Date.now,
|
|
62
|
+
): StubImpPort {
|
|
63
|
+
return new StubImpPort(principal, now);
|
|
65
64
|
}
|
|
66
65
|
|
|
67
66
|
interface StubConnection {
|
|
@@ -88,6 +87,12 @@ interface StubProcess {
|
|
|
88
87
|
readonly requireBroker: boolean;
|
|
89
88
|
}
|
|
90
89
|
|
|
90
|
+
interface StubLease {
|
|
91
|
+
readonly principal: string;
|
|
92
|
+
readonly label: string;
|
|
93
|
+
readonly until: number;
|
|
94
|
+
}
|
|
95
|
+
|
|
91
96
|
interface StubImp {
|
|
92
97
|
readonly id: string;
|
|
93
98
|
readonly name: string;
|
|
@@ -125,6 +130,9 @@ interface StubCommandHold {
|
|
|
125
130
|
readonly stop: () => void;
|
|
126
131
|
}
|
|
127
132
|
|
|
133
|
+
// The exit code of a guest command killed by SIGKILL.
|
|
134
|
+
const KILLED_CODE = 137;
|
|
135
|
+
|
|
128
136
|
// impd keeps exactly this many bytes of each generation's output.
|
|
129
137
|
const RING_BYTES = 262_144;
|
|
130
138
|
|
|
@@ -243,6 +251,12 @@ class StubImpPort implements ImpPort {
|
|
|
243
251
|
// How many commands wait on a command hold.
|
|
244
252
|
private heldCommands = 0;
|
|
245
253
|
|
|
254
|
+
// The guest commands running now, which a stop kills.
|
|
255
|
+
private readonly commands = new Set<Subprocess>();
|
|
256
|
+
|
|
257
|
+
// Set once the stand-in stops, so a command a hold released never runs.
|
|
258
|
+
private stopped = false;
|
|
259
|
+
|
|
246
260
|
// Commands whose argv holds this text exit 1 without running, or null.
|
|
247
261
|
private commandFailure: string | null = null;
|
|
248
262
|
|
|
@@ -268,12 +282,16 @@ class StubImpPort implements ImpPort {
|
|
|
268
282
|
|
|
269
283
|
private readonly principal: string;
|
|
270
284
|
|
|
285
|
+
// The time in epoch milliseconds that leases expire against.
|
|
286
|
+
private readonly now: () => number;
|
|
287
|
+
|
|
271
288
|
private readonly imps = new Map<string, StubImp>();
|
|
272
289
|
|
|
273
290
|
private readonly forwards = new Set<{ stop: () => void; stopRelays: () => void }>();
|
|
274
291
|
|
|
275
|
-
constructor(principal
|
|
292
|
+
constructor(principal: string, now: () => number) {
|
|
276
293
|
this.principal = principal;
|
|
294
|
+
this.now = now;
|
|
277
295
|
}
|
|
278
296
|
|
|
279
297
|
readFeatures(): Promise<ImpFeatures> {
|
|
@@ -456,7 +474,7 @@ class StubImpPort implements ImpPort {
|
|
|
456
474
|
|
|
457
475
|
this.updateAwake(imp);
|
|
458
476
|
|
|
459
|
-
const until =
|
|
477
|
+
const until = this.now() + ttlSeconds * 1000;
|
|
460
478
|
|
|
461
479
|
imp.leases.set(`${this.principal}\u0000${label}`, { principal: this.principal, label, until });
|
|
462
480
|
|
|
@@ -470,11 +488,11 @@ class StubImpPort implements ImpPort {
|
|
|
470
488
|
const key = `${this.principal}\u0000${label}`;
|
|
471
489
|
const lease = imp?.leases.get(key);
|
|
472
490
|
|
|
473
|
-
if (imp === undefined || lease === undefined || lease.until <=
|
|
491
|
+
if (imp === undefined || lease === undefined || lease.until <= this.now()) {
|
|
474
492
|
return Promise.reject(new ImpPortError('LEASE_NOT_HELD', 'the caller holds no such lease'));
|
|
475
493
|
}
|
|
476
494
|
|
|
477
|
-
const until =
|
|
495
|
+
const until = this.now() + ttlSeconds * 1000;
|
|
478
496
|
|
|
479
497
|
imp.leases.set(key, { ...lease, until });
|
|
480
498
|
|
|
@@ -518,7 +536,7 @@ class StubImpPort implements ImpPort {
|
|
|
518
536
|
return Promise.reject(buildNotFound(name));
|
|
519
537
|
}
|
|
520
538
|
|
|
521
|
-
const live = [...imp.leases.values()].filter((lease) => lease.until >
|
|
539
|
+
const live = [...imp.leases.values()].filter((lease) => lease.until > this.now());
|
|
522
540
|
|
|
523
541
|
if (imp.state === 'running' && live.length > 0) {
|
|
524
542
|
const own = live.filter((lease) => lease.principal === this.principal);
|
|
@@ -690,6 +708,12 @@ class StubImpPort implements ImpPort {
|
|
|
690
708
|
this.heldCommands -= 1;
|
|
691
709
|
}
|
|
692
710
|
|
|
711
|
+
// A command that gets past the hold after a stop, held or not, ends as a
|
|
712
|
+
// killed one does, so no process outlives the stop.
|
|
713
|
+
if (this.stopped) {
|
|
714
|
+
return { code: KILLED_CODE, stdout: new Uint8Array(0), stderr: new Uint8Array(0) };
|
|
715
|
+
}
|
|
716
|
+
|
|
693
717
|
if (this.commandFailure !== null && line.includes(this.commandFailure)) {
|
|
694
718
|
const encoder = new TextEncoder();
|
|
695
719
|
|
|
@@ -706,12 +730,16 @@ class StubImpPort implements ImpPort {
|
|
|
706
730
|
stderr: 'pipe',
|
|
707
731
|
});
|
|
708
732
|
|
|
733
|
+
this.commands.add(proc);
|
|
734
|
+
|
|
709
735
|
const [stdout, stderr, code] = await Promise.all([
|
|
710
736
|
new Response(proc.stdout).bytes(),
|
|
711
737
|
new Response(proc.stderr).bytes(),
|
|
712
738
|
proc.exited,
|
|
713
739
|
]);
|
|
714
740
|
|
|
741
|
+
this.commands.delete(proc);
|
|
742
|
+
|
|
715
743
|
return { code, stdout, stderr };
|
|
716
744
|
}
|
|
717
745
|
|
|
@@ -902,7 +930,7 @@ class StubImpPort implements ImpPort {
|
|
|
902
930
|
imp.leases.set(`${principal}\u0000${label}`, {
|
|
903
931
|
principal,
|
|
904
932
|
label,
|
|
905
|
-
until:
|
|
933
|
+
until: this.now() + ttlSeconds * 1000,
|
|
906
934
|
});
|
|
907
935
|
}
|
|
908
936
|
|
|
@@ -1252,12 +1280,22 @@ class StubImpPort implements ImpPort {
|
|
|
1252
1280
|
return proc.end;
|
|
1253
1281
|
}
|
|
1254
1282
|
|
|
1255
|
-
// Kills every process and stops every forward the stand-in holds
|
|
1283
|
+
// Kills every process and stops every forward the stand-in holds, and
|
|
1284
|
+
// resolves once every guest command it killed exits. A command a hold
|
|
1285
|
+
// still holds then exits as a killed one does, without running. It runs
|
|
1256
1286
|
// once the current test finishes; calling it sooner runs it then, and a
|
|
1257
1287
|
// second call does nothing.
|
|
1258
|
-
readonly stop: () => void = registerTestCleanup(() => {
|
|
1288
|
+
readonly stop: () => Promise<void> = registerTestCleanup(async () => {
|
|
1289
|
+
this.stopped = true;
|
|
1290
|
+
|
|
1259
1291
|
this.stopCommandHold();
|
|
1260
1292
|
|
|
1293
|
+
const commands = [...this.commands];
|
|
1294
|
+
|
|
1295
|
+
for (const command of commands) {
|
|
1296
|
+
command.kill('SIGKILL');
|
|
1297
|
+
}
|
|
1298
|
+
|
|
1261
1299
|
for (const imp of this.imps.values()) {
|
|
1262
1300
|
for (const proc of imp.sessions.values()) {
|
|
1263
1301
|
tryKill(proc.pty, 'SIGCONT');
|
|
@@ -1270,6 +1308,8 @@ class StubImpPort implements ImpPort {
|
|
|
1270
1308
|
}
|
|
1271
1309
|
|
|
1272
1310
|
this.imps.clear();
|
|
1311
|
+
|
|
1312
|
+
await Promise.all(commands.map((command) => command.exited));
|
|
1273
1313
|
});
|
|
1274
1314
|
|
|
1275
1315
|
// impd's refusal of a grant or revoke: the scope and pattern checks run
|
|
@@ -1325,7 +1365,7 @@ class StubImpPort implements ImpPort {
|
|
|
1325
1365
|
}
|
|
1326
1366
|
|
|
1327
1367
|
private buildView(imp: StubImp): ImpView {
|
|
1328
|
-
const live = [...imp.leases.values()].filter((lease) => lease.until >
|
|
1368
|
+
const live = [...imp.leases.values()].filter((lease) => lease.until > this.now());
|
|
1329
1369
|
const own = live.filter((lease) => lease.principal === this.principal);
|
|
1330
1370
|
const others = live.filter((lease) => lease.principal !== this.principal);
|
|
1331
1371
|
|
|
@@ -1381,7 +1421,7 @@ class StubImpPort implements ImpPort {
|
|
|
1381
1421
|
imp.bootId = randomUUID();
|
|
1382
1422
|
|
|
1383
1423
|
imp.coldBoots = [
|
|
1384
|
-
{ bootId: imp.bootId, cause, at: new Date().toISOString() },
|
|
1424
|
+
{ bootId: imp.bootId, cause, at: new Date(this.now()).toISOString() },
|
|
1385
1425
|
...imp.coldBoots,
|
|
1386
1426
|
].slice(0, 4);
|
|
1387
1427
|
}
|
|
@@ -1725,6 +1765,25 @@ function buildLease(name: string, principal: string, label: string, until: numbe
|
|
|
1725
1765
|
return { name, owner: { principal, display: principal, label }, until };
|
|
1726
1766
|
}
|
|
1727
1767
|
|
|
1768
|
+
function tryKill(pty: IPty, signal: NodeJS.Signals): void {
|
|
1769
|
+
try {
|
|
1770
|
+
process.kill(pty.pid, signal);
|
|
1771
|
+
} catch {}
|
|
1772
|
+
}
|
|
1773
|
+
|
|
1774
|
+
// A gate that throws keeps the request from going out, as a closed one does.
|
|
1775
|
+
function tryPassGate(gate: () => boolean): boolean {
|
|
1776
|
+
try {
|
|
1777
|
+
return gate();
|
|
1778
|
+
} catch {
|
|
1779
|
+
return false;
|
|
1780
|
+
}
|
|
1781
|
+
}
|
|
1782
|
+
|
|
1783
|
+
function toSignal(name: string): NodeJS.Signals {
|
|
1784
|
+
return name === 'SIGKILL' || name === 'SIGTERM' || name === 'SIGINT' ? name : 'SIGHUP';
|
|
1785
|
+
}
|
|
1786
|
+
|
|
1728
1787
|
// oxlint-disable-next-line prefer-readonly-parameter-types -- byte arrays have no readonly form
|
|
1729
1788
|
function mergeTail(ring: Uint8Array, data: Uint8Array, limit: number): Uint8Array {
|
|
1730
1789
|
const joined = new Uint8Array(ring.length + data.length);
|
|
@@ -1761,22 +1820,3 @@ function findSessionName(imp: StubImp, proc: StubProcess): string | undefined {
|
|
|
1761
1820
|
|
|
1762
1821
|
return undefined;
|
|
1763
1822
|
}
|
|
1764
|
-
|
|
1765
|
-
function toSignal(name: string): NodeJS.Signals {
|
|
1766
|
-
return name === 'SIGKILL' || name === 'SIGTERM' || name === 'SIGINT' ? name : 'SIGHUP';
|
|
1767
|
-
}
|
|
1768
|
-
|
|
1769
|
-
function tryKill(pty: IPty, signal: NodeJS.Signals): void {
|
|
1770
|
-
try {
|
|
1771
|
-
process.kill(pty.pid, signal);
|
|
1772
|
-
} catch {}
|
|
1773
|
-
}
|
|
1774
|
-
|
|
1775
|
-
// A gate that throws keeps the request from going out, as a closed one does.
|
|
1776
|
-
function tryPassGate(gate: () => boolean): boolean {
|
|
1777
|
-
try {
|
|
1778
|
-
return gate();
|
|
1779
|
-
} catch {
|
|
1780
|
-
return false;
|
|
1781
|
-
}
|
|
1782
|
-
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { readFileSync } from 'node:fs';
|
|
1
|
+
import { mkdtempSync, readFileSync, rmSync } from 'node:fs';
|
|
2
2
|
import { join } from 'node:path';
|
|
3
3
|
import type { Subprocess } from 'bun';
|
|
4
4
|
import { DaemonClient } from '../client/daemon-client';
|
|
@@ -39,17 +39,30 @@ interface DaemonBoot {
|
|
|
39
39
|
* sending no handshake, and fails at once with the daemon's stderr when the
|
|
40
40
|
* daemon exits before it listens. `restart` stops the daemon with the given
|
|
41
41
|
* signal, waits for it to exit, and starts another on the same home with the
|
|
42
|
-
* same config
|
|
43
|
-
*
|
|
44
|
-
*
|
|
45
|
-
*
|
|
46
|
-
*
|
|
47
|
-
*
|
|
42
|
+
* same config, and throws once the daemon is stopped. `readStderr` reads
|
|
43
|
+
* what the current boot printed, from a file in `stderrDir`, a directory
|
|
44
|
+
* under the home that no other daemon on the home writes. `stop` closes
|
|
45
|
+
* every client it opened, kills the daemon and waits for it to exit, then
|
|
46
|
+
* kills the daemon the home's state directory records, which a restart the
|
|
47
|
+
* test asked atc for may have started. That stop runs once the current test
|
|
48
|
+
* finishes, so it must run inside a test; calling `stop` sooner runs it
|
|
49
|
+
* then, and a second stop does nothing. `stderrDir` is removed once the
|
|
50
|
+
* current test finishes, after the stop, so a test can still read the
|
|
51
|
+
* stderr after an early stop.
|
|
48
52
|
*/
|
|
49
53
|
export function startDaemonProcess(config: Readonly<DaemonProcessConfig>) {
|
|
50
54
|
const socketPath = join(config.home, 'atc-daemon.sock');
|
|
51
55
|
const stateDir = join(config.home, '.local', 'state', 'atc');
|
|
52
56
|
|
|
57
|
+
// A directory of this helper's own under the home, so two daemons started
|
|
58
|
+
// on one home never write into one stderr file.
|
|
59
|
+
const stderrDir = mkdtempSync(join(config.home, 'daemon-stderr-'));
|
|
60
|
+
|
|
61
|
+
// Registered before the stop, so it runs after the daemon is gone.
|
|
62
|
+
registerTestCleanup(() => {
|
|
63
|
+
rmSync(stderrDir, { recursive: true, force: true });
|
|
64
|
+
});
|
|
65
|
+
|
|
53
66
|
const clients = new Set<DaemonClient>();
|
|
54
67
|
|
|
55
68
|
let boots = 0;
|
|
@@ -58,7 +71,7 @@ export function startDaemonProcess(config: Readonly<DaemonProcessConfig>) {
|
|
|
58
71
|
const boot = (): DaemonBoot => {
|
|
59
72
|
boots++;
|
|
60
73
|
|
|
61
|
-
const stderrPath = join(
|
|
74
|
+
const stderrPath = join(stderrDir, `boot-${boots}.stderr`);
|
|
62
75
|
|
|
63
76
|
const proc = Bun.spawn([...config.command, 'daemon', ...(config.args ?? [])], {
|
|
64
77
|
env: Object.fromEntries(
|
|
@@ -126,6 +139,11 @@ export function startDaemonProcess(config: Readonly<DaemonProcessConfig>) {
|
|
|
126
139
|
const watched = current.proc;
|
|
127
140
|
let settled = false;
|
|
128
141
|
const isSettled = () => settled;
|
|
142
|
+
const isWatchedExited = () => watched.exitCode !== null || watched.signalCode !== null;
|
|
143
|
+
|
|
144
|
+
// The poll's latest connect, so the wait after an exit can reuse one
|
|
145
|
+
// that was still in flight instead of dialing a second time.
|
|
146
|
+
let polled: Promise<DaemonClient> | null = null;
|
|
129
147
|
|
|
130
148
|
// A daemon that has exited either refused to start or handed its socket
|
|
131
149
|
// to a replacement, so one more connect tells the two apart.
|
|
@@ -136,6 +154,12 @@ export function startDaemonProcess(config: Readonly<DaemonProcessConfig>) {
|
|
|
136
154
|
return null;
|
|
137
155
|
}
|
|
138
156
|
|
|
157
|
+
const inFlight = await polled?.catch(() => null);
|
|
158
|
+
|
|
159
|
+
if (inFlight !== undefined && inFlight !== null) {
|
|
160
|
+
return inFlight;
|
|
161
|
+
}
|
|
162
|
+
|
|
139
163
|
try {
|
|
140
164
|
return await openTrackedClient(isSettled);
|
|
141
165
|
} catch {
|
|
@@ -145,16 +169,29 @@ export function startDaemonProcess(config: Readonly<DaemonProcessConfig>) {
|
|
|
145
169
|
}
|
|
146
170
|
};
|
|
147
171
|
|
|
148
|
-
// Polls the socket while the daemon runs. Once the
|
|
149
|
-
// the
|
|
150
|
-
//
|
|
172
|
+
// Polls the socket while the daemon runs. Once the daemon exits, it dials
|
|
173
|
+
// no more and leaves the connect to the wait after the exit. Once the
|
|
174
|
+
// other wait settles or the daemon stops, each attempt resolves empty,
|
|
175
|
+
// which ends the polling.
|
|
151
176
|
const openWhenListening = () =>
|
|
152
|
-
waitFor(
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
177
|
+
waitFor(
|
|
178
|
+
() => {
|
|
179
|
+
if (settled || stopped) {
|
|
180
|
+
return null;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
if (isWatchedExited()) {
|
|
184
|
+
throw new Error('the daemon exited');
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
polled = openTrackedClient(isSettled);
|
|
156
188
|
|
|
157
|
-
|
|
189
|
+
return polled;
|
|
190
|
+
},
|
|
191
|
+
{ timeoutMs: 15_000, intervalMs: 50 },
|
|
192
|
+
);
|
|
193
|
+
|
|
194
|
+
const hasExited = isWatchedExited();
|
|
158
195
|
|
|
159
196
|
try {
|
|
160
197
|
const client = await (hasExited
|
|
@@ -176,12 +213,17 @@ export function startDaemonProcess(config: Readonly<DaemonProcessConfig>) {
|
|
|
176
213
|
socketPath,
|
|
177
214
|
reporterSocketPath: join(config.home, 'atc.sock'),
|
|
178
215
|
stateDir,
|
|
216
|
+
stderrDir,
|
|
179
217
|
get proc(): Subprocess {
|
|
180
218
|
return current.proc;
|
|
181
219
|
},
|
|
182
220
|
readStderr,
|
|
183
221
|
openClient,
|
|
184
222
|
async restart(signal: NodeJS.Signals): Promise<void> {
|
|
223
|
+
if (stopped) {
|
|
224
|
+
throw new Error('a stopped daemon process cannot restart');
|
|
225
|
+
}
|
|
226
|
+
|
|
185
227
|
current.proc.kill(signal);
|
|
186
228
|
|
|
187
229
|
await current.proc.exited;
|