@runeditrun/rer 0.1.0 → 0.1.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/README.md +4 -1
- package/cli/server-registry.mjs +9 -4
- package/global/README.md +4 -1
- package/global/launch/dev.mjs +5 -17
- package/global/launch/facts.mjs +3 -2
- package/global/launch/prepare.mjs +5 -3
- package/global/launch/state.mjs +10 -1
- package/global/launch/steps.mjs +14 -5
- package/global/launch/stop.mjs +89 -0
- package/global/launch/worker.mjs +30 -11
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -117,7 +117,10 @@ override the choice, and `--json` prints `progress` events and the ready event.
|
|
|
117
117
|
first launch can take minutes: it prints a progress line at least every 30
|
|
118
118
|
seconds, and if the calling command is killed the launch carries on; run
|
|
119
119
|
`rer dev` again to reattach. A second `rer dev` reprints the ready lines.
|
|
120
|
-
`rer dev --stop` stops the server
|
|
120
|
+
`rer dev --stop` stops the server, or a launch still preparing (which then
|
|
121
|
+
reports that it was stopped), waits until every process they started has
|
|
122
|
+
exited and says what it stopped; if any is still running it exits non-zero
|
|
123
|
+
naming their PIDs.
|
|
121
124
|
|
|
122
125
|
Servers started through `rer` (`rer dev`, `rer preview`) get a 10-server limit and a 12-hour idle timeout
|
|
123
126
|
unless you set `RER_SERVER_LIMIT`, `RER_SERVER_IDLE_SECONDS` or `--idle-timeout`.
|
package/cli/server-registry.mjs
CHANGED
|
@@ -167,11 +167,16 @@ export async function serverRegistry(root,{env=process.env,memory=availableMemor
|
|
|
167
167
|
throw refuse('The existing server is still starting. Wait for its ready output.');
|
|
168
168
|
}
|
|
169
169
|
return {checkout,existing,stop,reserve,serve,update,updateActivity,waitReady,custodyToken:()=>owned?.token,
|
|
170
|
+
// The sockets close even when the record cannot be removed (its registry
|
|
171
|
+
// deleted under it, say): an open listener would keep this process alive.
|
|
170
172
|
close(){return closing??=(async()=>{
|
|
171
|
-
if(owned)await change(state=>{state.running=state.running.filter(item=>item.generation!==owned.generation);});
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
173
|
+
try{if(owned)await change(state=>{state.running=state.running.filter(item=>item.generation!==owned.generation);});}
|
|
174
|
+
finally{
|
|
175
|
+
try{
|
|
176
|
+
if(control)await new Promise(resolve=>control.close(resolve));
|
|
177
|
+
if(owned)await unlink(owned.control).catch(error=>{if(error.code!=='ENOENT')throw error;});
|
|
178
|
+
}finally{await lifetime.close();}
|
|
179
|
+
}
|
|
175
180
|
})();}
|
|
176
181
|
};
|
|
177
182
|
}
|
package/global/README.md
CHANGED
|
@@ -117,7 +117,10 @@ override the choice, and `--json` prints `progress` events and the ready event.
|
|
|
117
117
|
first launch can take minutes: it prints a progress line at least every 30
|
|
118
118
|
seconds, and if the calling command is killed the launch carries on; run
|
|
119
119
|
`rer dev` again to reattach. A second `rer dev` reprints the ready lines.
|
|
120
|
-
`rer dev --stop` stops the server
|
|
120
|
+
`rer dev --stop` stops the server, or a launch still preparing (which then
|
|
121
|
+
reports that it was stopped), waits until every process they started has
|
|
122
|
+
exited and says what it stopped; if any is still running it exits non-zero
|
|
123
|
+
naming their PIDs.
|
|
121
124
|
|
|
122
125
|
Servers started through `rer` (`rer dev`, `rer preview`) get a 10-server limit and a 12-hour idle timeout
|
|
123
126
|
unless you set `RER_SERVER_LIMIT`, `RER_SERVER_IDLE_SECONDS` or `--idle-timeout`.
|
package/global/launch/dev.mjs
CHANGED
|
@@ -10,13 +10,13 @@ import { open, realpath, stat } from 'node:fs/promises';
|
|
|
10
10
|
import { join } from 'node:path';
|
|
11
11
|
import { setTimeout as delay } from 'node:timers/promises';
|
|
12
12
|
import { fileURLToPath, pathToFileURL } from 'node:url';
|
|
13
|
-
import { processInfo } from '../../modules/job-queue/processes.mjs';
|
|
14
13
|
import { dispatcher } from '../checkout.mjs';
|
|
15
14
|
import { delegate } from '../delegate.mjs';
|
|
16
15
|
import { ownRoot } from '../package-info.mjs';
|
|
17
16
|
import { LaunchFailure } from './failure.mjs';
|
|
18
17
|
import { prepare } from './prepare.mjs';
|
|
19
18
|
import { printer } from './render.mjs';
|
|
19
|
+
import { stopLaunch } from './stop.mjs';
|
|
20
20
|
import { launchDirectory, liveWorker } from './state.mjs';
|
|
21
21
|
|
|
22
22
|
const workerPath = fileURLToPath(new URL('./worker.mjs', import.meta.url));
|
|
@@ -30,8 +30,8 @@ export async function launchDev(target) {
|
|
|
30
30
|
if (!await accepted(target.root, devArgs)) return delegate(target, ['dev', ...devArgs]);
|
|
31
31
|
const checkout = await realpath(target.root), directory = await launchDirectory(checkout);
|
|
32
32
|
if (devArgs.includes('--stop')) {
|
|
33
|
-
try { await
|
|
34
|
-
|
|
33
|
+
try { return await stopLaunch(target, devArgs, { checkout, directory }); }
|
|
34
|
+
catch (failure) { if (!(failure instanceof LaunchFailure)) throw failure; process.stderr.write(failure.message + '\n'); return failure.exitCode; }
|
|
35
35
|
}
|
|
36
36
|
const here = await realpath(process.cwd()).catch(() => undefined);
|
|
37
37
|
const out = printer({ json: devArgs.includes('--json'), login: devArgs.includes('--login'), stopHint: `rer dev --stop${here === checkout ? '' : ` --cwd ${JSON.stringify(checkout)}`}` });
|
|
@@ -69,7 +69,8 @@ async function detached(target, devArgs, directory, out) {
|
|
|
69
69
|
if (holder) { token = holder.token; out.attaching(holder.startedAt); }
|
|
70
70
|
else {
|
|
71
71
|
token = randomUUID();
|
|
72
|
-
|
|
72
|
+
// The worker ends itself for a stop that ran after this command started.
|
|
73
|
+
worker = spawn(process.execPath, [workerPath, target.root, token, JSON.stringify(devArgs.filter(arg => arg !== '--json')), String(performance.timeOrigin)], { cwd: target.root, env: process.env, detached: true, stdio: 'ignore' });
|
|
73
74
|
worker.unref();
|
|
74
75
|
}
|
|
75
76
|
return follow(directory, token, out, { worker });
|
|
@@ -114,16 +115,3 @@ function missing(directory) {
|
|
|
114
115
|
process.stderr.write(`rer dev: the launch stopped before it finished. Run rer dev again; its log is ${join(directory, 'launch.log')}.\n`);
|
|
115
116
|
return 1;
|
|
116
117
|
}
|
|
117
|
-
|
|
118
|
-
// rer dev --stop also ends a preparation still running for this checkout.
|
|
119
|
-
async function stopWorker(directory) {
|
|
120
|
-
const holder = await liveWorker(directory);
|
|
121
|
-
if (!holder) return;
|
|
122
|
-
try { process.kill(holder.pid, 'SIGTERM'); } catch (error) { if (error.code === 'ESRCH') return; throw error; }
|
|
123
|
-
for (let waited = 0; waited < 60000; waited += 250) {
|
|
124
|
-
const current = await processInfo(holder.pid);
|
|
125
|
-
if (!current || current.zombie || current.start !== holder.start) return;
|
|
126
|
-
await delay(250);
|
|
127
|
-
}
|
|
128
|
-
throw new LaunchFailure(1, `The launch worker (PID ${holder.pid}) did not stop within a minute.`);
|
|
129
|
-
}
|
package/global/launch/facts.mjs
CHANGED
|
@@ -11,11 +11,12 @@ const run = promisify(execFile);
|
|
|
11
11
|
async function mtime(path) { try { return (await stat(path)).mtimeMs; } catch (error) { if (error.code === 'ENOENT') return undefined; throw error; } }
|
|
12
12
|
|
|
13
13
|
// The pinned dispatcher's own read-only description.
|
|
14
|
-
export async function describeCheckout(root, entry, env) {
|
|
14
|
+
export async function describeCheckout(root, entry, env, signal) {
|
|
15
15
|
try {
|
|
16
|
-
const { stdout } = await run(process.execPath, [entry, 'describe', '--json'], { cwd: root, env, maxBuffer: 16 * 1024 * 1024 });
|
|
16
|
+
const { stdout } = await run(process.execPath, [entry, 'describe', '--json'], { cwd: root, env, maxBuffer: 16 * 1024 * 1024, signal });
|
|
17
17
|
return JSON.parse(stdout);
|
|
18
18
|
} catch (error) {
|
|
19
|
+
if (signal?.aborted) throw signal.reason;
|
|
19
20
|
throw new LaunchFailure(error.code ?? 1, (error.stderr?.trim() || `rer describe failed in ${root}.`));
|
|
20
21
|
}
|
|
21
22
|
}
|
|
@@ -16,16 +16,18 @@ import { localDefaults } from './local-defaults.mjs';
|
|
|
16
16
|
// The value given for a flag in the user's dev arguments.
|
|
17
17
|
export const flagValue = (args, flag) => { const index = args.indexOf(flag); return index < 0 ? undefined : args[index + 1]; };
|
|
18
18
|
|
|
19
|
-
|
|
19
|
+
// A worker passes the signal a stop aborts: no step starts after it, and the
|
|
20
|
+
// step running then is signalled through `children`.
|
|
21
|
+
export async function prepare({ root, args, env, report, directory, log, children, signal }) {
|
|
20
22
|
const checkout = await realpath(root), { mode, entry } = dispatcher(root);
|
|
21
|
-
const described = await describeCheckout(root, entry, env);
|
|
23
|
+
const described = await describeCheckout(root, entry, env, signal);
|
|
22
24
|
const pin = await pinPolicy(root, mode, described);
|
|
23
25
|
const added = personEnvironment(env, args, pin), childEnv = { ...env, ...added };
|
|
24
26
|
report({ phase: 'check', message: 'checking the server limit' });
|
|
25
27
|
const capacity = await checkCapacity(checkout, childEnv, pin);
|
|
26
28
|
if (capacity.own) return { entry, added, own: capacity.own };
|
|
27
29
|
const note = message => report({ phase: 'note', message });
|
|
28
|
-
const step = (record, args, fields, env = childEnv) => recorded(directory, record, () => runVerb(entry, args, { root, env, note, log, children }), fields);
|
|
30
|
+
const step = (record, args, fields, env = childEnv) => recorded(directory, record, () => runVerb(entry, args, { root, env, note, log, children, signal }), fields);
|
|
29
31
|
// Only this setup fills the seed's declared local defaults.
|
|
30
32
|
const setupEnv = { ...childEnv, [localDefaults]: '1' };
|
|
31
33
|
if (await needsInstall(root, await installState.read(directory))) {
|
package/global/launch/state.mjs
CHANGED
|
@@ -6,7 +6,8 @@
|
|
|
6
6
|
// setup.json whether a setup this launcher started has finished
|
|
7
7
|
// launch.log the worker's private log (child output)
|
|
8
8
|
// server.log the private log of the server rer started
|
|
9
|
-
// server.json that server's PID
|
|
9
|
+
// server.json that server's PID and start time, and the process rer spawned
|
|
10
|
+
// stop.json when rer dev --stop last ran: a launch requested before it stops
|
|
10
11
|
import { createHash } from 'node:crypto';
|
|
11
12
|
import { appendFile, mkdir, readFile, readdir, rm, writeFile } from 'node:fs/promises';
|
|
12
13
|
import { basename, join } from 'node:path';
|
|
@@ -91,6 +92,14 @@ const stepRecord = name => ({
|
|
|
91
92
|
});
|
|
92
93
|
export const installState = stepRecord('install'), setupState = stepRecord('setup');
|
|
93
94
|
|
|
95
|
+
// A stop asks every launch started before it to end. A worker reads this right
|
|
96
|
+
// after it claims the launch; a stop that ran before the claim found no worker
|
|
97
|
+
// to signal, so the worker ends itself here instead of starting anything.
|
|
98
|
+
export const stopRequest = {
|
|
99
|
+
write: directory => writeFile(join(directory, 'stop.json'), JSON.stringify({ at: Date.now(), pid: process.pid }), { mode: 0o600 }),
|
|
100
|
+
covers: async (directory, requestedAt) => ((await readJson(join(directory, 'stop.json')))?.at ?? -Infinity) >= requestedAt,
|
|
101
|
+
};
|
|
102
|
+
|
|
94
103
|
export const serverRecord = {
|
|
95
104
|
read: directory => readJson(join(directory, 'server.json')),
|
|
96
105
|
write: (directory, record) => writeFile(join(directory, 'server.json'), JSON.stringify(record), { mode: 0o600 }),
|
package/global/launch/steps.mjs
CHANGED
|
@@ -21,10 +21,18 @@ export function failureFrom(code, error, output) {
|
|
|
21
21
|
return new LaunchFailure(code || 1, error?.message ?? (output.trim().split('\n').slice(-5).join('\n') || `The command stopped with exit ${code}.`), error);
|
|
22
22
|
}
|
|
23
23
|
|
|
24
|
+
// A child joins the set a stop signals; one spawned as the stop arrives is
|
|
25
|
+
// signalled at once rather than left to finish.
|
|
26
|
+
function own(children, child, signal) {
|
|
27
|
+
children.add(child);
|
|
28
|
+
if (signal?.aborted) child.kill('SIGTERM');
|
|
29
|
+
}
|
|
30
|
+
|
|
24
31
|
// One bounded verb, never prompting: no stdin, --non-interactive, --json.
|
|
25
|
-
export async function runVerb(entry, args, { root, env, note, log, children }) {
|
|
32
|
+
export async function runVerb(entry, args, { root, env, note, log, children, signal }) {
|
|
33
|
+
signal?.throwIfAborted();
|
|
26
34
|
const child = spawn(process.execPath, [entry, ...args, '--json', '--non-interactive'], { cwd: root, env, stdio: ['ignore', 'pipe', 'pipe'] });
|
|
27
|
-
children
|
|
35
|
+
own(children, child, signal);
|
|
28
36
|
let error, output = '';
|
|
29
37
|
const lines = createInterface({ input: child.stdout });
|
|
30
38
|
lines.on('line', line => { const event = events(line); if (event?.event === 'error') error = event; const text = event && waitingNote(event); if (text) note(text); log(line); });
|
|
@@ -55,14 +63,15 @@ async function follow(path, from, onLine, stopped) {
|
|
|
55
63
|
// Start `dev` detached into its own process group with its output in a private
|
|
56
64
|
// log, and return its ready event once it prints one. The server outlives this
|
|
57
65
|
// process and stops on rer dev --stop or its idle timeout.
|
|
58
|
-
export async function startServer(entry, args, { root, env, logPath, children }) {
|
|
66
|
+
export async function startServer(entry, args, { root, env, logPath, children, signal }) {
|
|
67
|
+
signal?.throwIfAborted();
|
|
59
68
|
const handle = await open(logPath, 'a', 0o600), from = (await handle.stat()).size;
|
|
60
69
|
let child;
|
|
61
70
|
try { child = spawn(process.execPath, [entry, 'dev', ...args, '--json'], { cwd: root, env, detached: true, stdio: ['ignore', handle.fd, handle.fd] }); }
|
|
62
71
|
finally { await handle.close(); }
|
|
63
|
-
children
|
|
72
|
+
own(children, child, signal);
|
|
64
73
|
let exit, ready, error, output = '';
|
|
65
|
-
child.once('exit', (code,
|
|
74
|
+
child.once('exit', (code, name) => { exit = code ?? (name ? 128 : 1); });
|
|
66
75
|
await follow(logPath, from, line => {
|
|
67
76
|
const event = events(line);
|
|
68
77
|
if (event?.event === 'ready') { ready = event; return true; }
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
// rer dev --stop for a checkout the launcher may be preparing or serving.
|
|
2
|
+
// It ends a launch still preparing (or one that has not yet claimed the
|
|
3
|
+
// checkout), has the pinned dispatcher stop the registered server, and then
|
|
4
|
+
// checks that every process it found -- the worker, the server rer started,
|
|
5
|
+
// the registered server and everything they spawned -- has exited. It says
|
|
6
|
+
// what it stopped, or exits non-zero naming what is still running.
|
|
7
|
+
import { setTimeout as delay } from 'node:timers/promises';
|
|
8
|
+
import { processInfo } from '../../modules/job-queue/processes.mjs';
|
|
9
|
+
import { readProcessTable, sameBirth } from '../../onboarding/process-table.mjs';
|
|
10
|
+
import { delegate } from '../delegate.mjs';
|
|
11
|
+
import { liveServers } from '../registry.mjs';
|
|
12
|
+
import { LaunchFailure } from './failure.mjs';
|
|
13
|
+
import { liveWorker, serverRecord, stopRequest } from './state.mjs';
|
|
14
|
+
|
|
15
|
+
export async function stopLaunch(target, devArgs, { checkout, directory }) {
|
|
16
|
+
// Written before looking for a worker: one that claims the launch after this
|
|
17
|
+
// look reads it and ends itself (worker.mjs).
|
|
18
|
+
await stopRequest.write(directory);
|
|
19
|
+
const holder = await liveWorker(directory);
|
|
20
|
+
const launch = holder ? await tree([holder]) : [];
|
|
21
|
+
if (holder) await endWorker(holder);
|
|
22
|
+
// Only now: a worker stopped while its server came up has registered it.
|
|
23
|
+
const record = (await liveServers()).find(item => item.checkout === checkout);
|
|
24
|
+
const started = await serverRecord.read(directory);
|
|
25
|
+
const server = await tree([record?.owner, record?.runtimeProcess, started, started?.spawned]);
|
|
26
|
+
await delegate(target, ['dev', ...devArgs]);
|
|
27
|
+
if (process.exitCode) return process.exitCode;
|
|
28
|
+
const left = await exited([...launch, ...server], 15000);
|
|
29
|
+
if (left.length) {
|
|
30
|
+
const pids = left.map(item => item.pid).join(' ');
|
|
31
|
+
process.stderr.write(`rer dev --stop: ${left.length === 1 ? 'a process' : `${left.length} processes`} of this checkout's dev server ${left.length === 1 ? 'is' : 'are'} still running 15 seconds after the stop (PID ${pids}). Stop ${left.length === 1 ? 'it' : 'them'} with kill -TERM ${pids}, then run rer dev --stop again.\n`);
|
|
32
|
+
return 1;
|
|
33
|
+
}
|
|
34
|
+
if (!devArgs.includes('--json')) process.stdout.write(said({ checkout, record, started, server, holder, launch }) + '\n');
|
|
35
|
+
return 0;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// SIGTERM the preparing worker and wait for it to exit.
|
|
39
|
+
async function endWorker(holder) {
|
|
40
|
+
try { process.kill(holder.pid, 'SIGTERM'); } catch (error) { if (error.code === 'ESRCH') return; throw error; }
|
|
41
|
+
for (let waited = 0; waited < 60000; waited += 250) {
|
|
42
|
+
const current = await processInfo(holder.pid);
|
|
43
|
+
if (!current || current.zombie || current.start !== holder.start) return;
|
|
44
|
+
await delay(250);
|
|
45
|
+
}
|
|
46
|
+
throw new LaunchFailure(1, `The launch worker (PID ${holder.pid}) did not stop within a minute.`);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// The live processes among these recorded identities ({pid, start}), with
|
|
50
|
+
// every process descended from them, as process-table entries.
|
|
51
|
+
async function tree(identities) {
|
|
52
|
+
const roots = new Set();
|
|
53
|
+
for (const identity of identities) {
|
|
54
|
+
if (!identity?.pid || !identity.start) continue;
|
|
55
|
+
const current = await processInfo(identity.pid);
|
|
56
|
+
if (current && !current.zombie && current.start === identity.start) roots.add(identity.pid);
|
|
57
|
+
}
|
|
58
|
+
if (!roots.size) return [];
|
|
59
|
+
const table = await readProcessTable(), found = table.filter(entry => roots.has(entry.pid));
|
|
60
|
+
for (let index = 0; index < found.length; index++) {
|
|
61
|
+
for (const entry of table) if (entry.ppid === found[index].pid && !found.includes(entry)) found.push(entry);
|
|
62
|
+
}
|
|
63
|
+
return found;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// Wait until none of these processes is running; return those still running.
|
|
67
|
+
async function exited(processes, within) {
|
|
68
|
+
const deadline = Date.now() + within;
|
|
69
|
+
for (;;) {
|
|
70
|
+
const table = processes.length ? await readProcessTable() : [];
|
|
71
|
+
const left = processes.filter(item => table.some(entry => sameBirth(item, entry) && !entry.state.startsWith('Z')));
|
|
72
|
+
if (!left.length || Date.now() >= deadline) return left;
|
|
73
|
+
await delay(250);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function said({ checkout, record, started, server, holder, launch }) {
|
|
78
|
+
const lines = [], more = n => n > 0 ? ` with ${n} more ${n === 1 ? 'process' : 'processes'}` : '';
|
|
79
|
+
if (server.length) {
|
|
80
|
+
const pid = record?.owner?.pid ?? started?.pid ?? server[0].pid;
|
|
81
|
+
lines.push(`Stopped the dev server${record?.url ? ` at ${record.url}` : ''} for ${checkout} (PID ${pid})${more(server.length - 1)}.`);
|
|
82
|
+
}
|
|
83
|
+
if (holder) {
|
|
84
|
+
const own = launch.filter(item => !server.some(entry => sameBirth(item, entry)));
|
|
85
|
+
lines.push(`Stopped the launch still running for ${checkout} (PID ${holder.pid})${more(own.length - 1)}.`);
|
|
86
|
+
}
|
|
87
|
+
if (!lines.length) lines.push(`No dev server was running for ${checkout}.`);
|
|
88
|
+
return lines.join('\n');
|
|
89
|
+
}
|
package/global/launch/worker.mjs
CHANGED
|
@@ -1,41 +1,60 @@
|
|
|
1
|
-
// The detached launch worker: node worker.mjs ROOT TOKEN ARGS_JSON
|
|
1
|
+
// The detached launch worker: node worker.mjs ROOT TOKEN ARGS_JSON REQUESTED_AT
|
|
2
|
+
// (REQUESTED_AT: when the `rer dev` that spawned it started, epoch ms).
|
|
2
3
|
// It runs in its own session, so killing the `rer dev` that started it (or that
|
|
3
4
|
// command's whole process group) leaves it running; a rerun attaches to it.
|
|
5
|
+
// rer dev --stop ends it: SIGTERM, or, for a stop that ran before this worker
|
|
6
|
+
// claimed the launch, the stop request it reads right after claiming. Either
|
|
7
|
+
// way no further step starts, the running one is signalled, and the launch
|
|
8
|
+
// reports that it was stopped instead of ready.
|
|
4
9
|
import { createWriteStream } from 'node:fs';
|
|
5
10
|
import { realpath } from 'node:fs/promises';
|
|
6
11
|
import { join } from 'node:path';
|
|
12
|
+
import { processInfo } from '../../modules/job-queue/processes.mjs';
|
|
7
13
|
import { liveServers } from '../registry.mjs';
|
|
14
|
+
import { LaunchFailure } from './failure.mjs';
|
|
8
15
|
import { prepare } from './prepare.mjs';
|
|
9
|
-
import { claimLaunch, eventLog, launchDirectory, liveWorker, releaseLaunch, serverRecord } from './state.mjs';
|
|
16
|
+
import { claimLaunch, eventLog, launchDirectory, liveWorker, releaseLaunch, serverRecord, stopRequest } from './state.mjs';
|
|
10
17
|
import { startServer } from './steps.mjs';
|
|
11
18
|
|
|
12
|
-
const [root, token, argsJSON] = process.argv.slice(2), args = JSON.parse(argsJSON);
|
|
19
|
+
const [root, token, argsJSON, requestedAtText] = process.argv.slice(2), args = JSON.parse(argsJSON), requestedAt = Number(requestedAtText);
|
|
13
20
|
const checkout = await realpath(root), directory = await launchDirectory(checkout), events = eventLog(directory, token);
|
|
21
|
+
// Listening before the claim: a stop may signal this worker as soon as it is the holder.
|
|
22
|
+
const children = new Set(), stopping = new AbortController();
|
|
23
|
+
const stop = () => { stopping.abort(); for (const child of children) child.kill('SIGTERM'); };
|
|
24
|
+
for (const signal of ['SIGTERM', 'SIGINT']) process.on(signal, stop);
|
|
25
|
+
process.on('SIGHUP', () => {});
|
|
14
26
|
if (!await claimLaunch(directory, token)) {
|
|
15
27
|
const holder = await liveWorker(directory);
|
|
16
28
|
await events.write({ event: 'redirect', token: holder?.token });
|
|
17
29
|
} else {
|
|
18
30
|
const logFile = createWriteStream(join(directory, 'launch.log'), { flags: 'a', mode: 0o600 });
|
|
19
31
|
const log = text => logFile.write(text + '\n');
|
|
20
|
-
const children = new Set();
|
|
21
|
-
for (const signal of ['SIGTERM', 'SIGINT']) process.on(signal, () => { for (const child of children) child.kill('SIGTERM'); });
|
|
22
|
-
process.on('SIGHUP', () => {});
|
|
23
32
|
try {
|
|
24
|
-
|
|
33
|
+
if (await stopRequest.covers(directory, requestedAt)) stop();
|
|
34
|
+
stopping.signal.throwIfAborted();
|
|
35
|
+
const prepared = await prepare({ root, args, env: process.env, report: fields => events.write({ event: 'phase', ...fields }), directory, log, children, signal: stopping.signal });
|
|
25
36
|
await events.write({ event: 'phase', phase: 'start', message: prepared.own ? 'the app is already running; checking it' : 'starting the app' });
|
|
26
37
|
// A server rer started logs to server.log; asking a running one for its
|
|
27
38
|
// ready lines goes to reuse.log, and names server.log only when rer started it.
|
|
28
39
|
const serverLog = join(directory, 'server.log');
|
|
29
|
-
|
|
40
|
+
let started;
|
|
41
|
+
try { started = await startServer(prepared.entry, args, { root, env: { ...process.env, ...prepared.added }, logPath: prepared.own ? join(directory, 'reuse.log') : serverLog, children, signal: stopping.signal }); }
|
|
42
|
+
finally { await prepared.release?.(); }
|
|
30
43
|
const record = (await liveServers(process.env)).find(item => item.checkout === checkout);
|
|
31
|
-
await prepared.release?.();
|
|
32
44
|
const pid = record?.owner?.pid ?? started.pid;
|
|
33
|
-
|
|
45
|
+
// The server's PID, and the process rer spawned for it (the same one unless
|
|
46
|
+
// the pin re-runs itself under its own Node), each with its start time.
|
|
47
|
+
const identity = async id => ({ pid: id, start: (await processInfo(id))?.start });
|
|
48
|
+
if (!prepared.own) await serverRecord.write(directory, { ...await identity(pid), spawned: await identity(started.pid) });
|
|
34
49
|
const ours = !prepared.own || (await serverRecord.read(directory))?.pid === pid;
|
|
50
|
+
// A stop that arrived while the server came up finds it registered and
|
|
51
|
+
// stops it; this launch reports the stop rather than a server about to end.
|
|
52
|
+
stopping.signal.throwIfAborted();
|
|
35
53
|
await events.write({ event: 'ready', ready: started.ready, pid, ...(ours ? { log: serverLog } : {}) });
|
|
36
54
|
} catch (error) {
|
|
37
55
|
log(String(error?.stack ?? error));
|
|
38
|
-
|
|
56
|
+
const failure = stopping.signal.aborted ? new LaunchFailure(1, 'rer dev --stop stopped this launch before the app was ready.') : error;
|
|
57
|
+
await events.write({ event: 'failed', exitCode: failure.exitCode ?? 1, message: failure.exitCode ? failure.message : `The launch failed unexpectedly; see ${join(directory, 'launch.log')}.`, ...(failure.event ? { error: failure.event } : {}) });
|
|
39
58
|
} finally {
|
|
40
59
|
await releaseLaunch(directory, token);
|
|
41
60
|
logFile.end();
|