@theford/runly 0.0.0-stage → 0.20.0
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/LICENSE +202 -0
- package/README.md +157 -2
- package/dist/cli/client.d.ts +80 -0
- package/dist/cli/client.js +294 -0
- package/dist/cli/client.js.map +1 -0
- package/dist/cli/daemon-unit.d.ts +58 -0
- package/dist/cli/daemon-unit.js +289 -0
- package/dist/cli/daemon-unit.js.map +1 -0
- package/dist/cli/index.d.ts +2 -0
- package/dist/cli/index.js +1482 -0
- package/dist/cli/index.js.map +1 -0
- package/dist/cli/install.d.ts +9 -0
- package/dist/cli/install.js +53 -0
- package/dist/cli/install.js.map +1 -0
- package/dist/cli/logs.d.ts +43 -0
- package/dist/cli/logs.js +232 -0
- package/dist/cli/logs.js.map +1 -0
- package/dist/cli/tether.d.ts +3 -0
- package/dist/cli/tether.js +65 -0
- package/dist/cli/tether.js.map +1 -0
- package/dist/cli/watchdog.d.ts +1 -0
- package/dist/cli/watchdog.js +81 -0
- package/dist/cli/watchdog.js.map +1 -0
- package/dist/core/builds.d.ts +20 -0
- package/dist/core/builds.js +93 -0
- package/dist/core/builds.js.map +1 -0
- package/dist/core/caller-env.d.ts +17 -0
- package/dist/core/caller-env.js +77 -0
- package/dist/core/caller-env.js.map +1 -0
- package/dist/core/diagnostics.d.ts +68 -0
- package/dist/core/diagnostics.js +34 -0
- package/dist/core/diagnostics.js.map +1 -0
- package/dist/core/env-vars.d.ts +35 -0
- package/dist/core/env-vars.js +32 -0
- package/dist/core/env-vars.js.map +1 -0
- package/dist/core/events.d.ts +10 -0
- package/dist/core/events.js +42 -0
- package/dist/core/events.js.map +1 -0
- package/dist/core/exec.d.ts +22 -0
- package/dist/core/exec.js +84 -0
- package/dist/core/exec.js.map +1 -0
- package/dist/core/journal.d.ts +227 -0
- package/dist/core/journal.js +395 -0
- package/dist/core/journal.js.map +1 -0
- package/dist/core/logs.d.ts +56 -0
- package/dist/core/logs.js +259 -0
- package/dist/core/logs.js.map +1 -0
- package/dist/core/manifest.d.ts +292 -0
- package/dist/core/manifest.js +179 -0
- package/dist/core/manifest.js.map +1 -0
- package/dist/core/namespaces.d.ts +7 -0
- package/dist/core/namespaces.js +59 -0
- package/dist/core/namespaces.js.map +1 -0
- package/dist/core/paths.d.ts +31 -0
- package/dist/core/paths.js +65 -0
- package/dist/core/paths.js.map +1 -0
- package/dist/core/policy.d.ts +94 -0
- package/dist/core/policy.js +122 -0
- package/dist/core/policy.js.map +1 -0
- package/dist/core/ports.d.ts +52 -0
- package/dist/core/ports.js +94 -0
- package/dist/core/ports.js.map +1 -0
- package/dist/core/presets.d.ts +25 -0
- package/dist/core/presets.js +86 -0
- package/dist/core/presets.js.map +1 -0
- package/dist/core/procscan.d.ts +111 -0
- package/dist/core/procscan.js +342 -0
- package/dist/core/procscan.js.map +1 -0
- package/dist/core/retention.d.ts +97 -0
- package/dist/core/retention.js +322 -0
- package/dist/core/retention.js.map +1 -0
- package/dist/core/sleep.d.ts +51 -0
- package/dist/core/sleep.js +99 -0
- package/dist/core/sleep.js.map +1 -0
- package/dist/core/tree-ledger.d.ts +39 -0
- package/dist/core/tree-ledger.js +92 -0
- package/dist/core/tree-ledger.js.map +1 -0
- package/dist/core/types.d.ts +21 -0
- package/dist/core/types.js +6 -0
- package/dist/core/types.js.map +1 -0
- package/dist/core/units.d.ts +20 -0
- package/dist/core/units.js +69 -0
- package/dist/core/units.js.map +1 -0
- package/dist/core/upkeep.d.ts +90 -0
- package/dist/core/upkeep.js +176 -0
- package/dist/core/upkeep.js.map +1 -0
- package/dist/core/util.d.ts +46 -0
- package/dist/core/util.js +168 -0
- package/dist/core/util.js.map +1 -0
- package/dist/core/version.d.ts +39 -0
- package/dist/core/version.js +144 -0
- package/dist/core/version.js.map +1 -0
- package/dist/core/worktree.d.ts +51 -0
- package/dist/core/worktree.js +309 -0
- package/dist/core/worktree.js.map +1 -0
- package/dist/daemon/budget.d.ts +155 -0
- package/dist/daemon/budget.js +288 -0
- package/dist/daemon/budget.js.map +1 -0
- package/dist/daemon/election.d.ts +9 -0
- package/dist/daemon/election.js +140 -0
- package/dist/daemon/election.js.map +1 -0
- package/dist/daemon/engine.d.ts +1703 -0
- package/dist/daemon/engine.js +5305 -0
- package/dist/daemon/engine.js.map +1 -0
- package/dist/daemon/index.d.ts +2 -0
- package/dist/daemon/index.js +412 -0
- package/dist/daemon/index.js.map +1 -0
- package/dist/daemon/proxy.d.ts +108 -0
- package/dist/daemon/proxy.js +615 -0
- package/dist/daemon/proxy.js.map +1 -0
- package/dist/daemon/supervisor.d.ts +101 -0
- package/dist/daemon/supervisor.js +493 -0
- package/dist/daemon/supervisor.js.map +1 -0
- package/dist/drivers/appliances.d.ts +11 -0
- package/dist/drivers/appliances.js +174 -0
- package/dist/drivers/appliances.js.map +1 -0
- package/dist/drivers/datastores.d.ts +144 -0
- package/dist/drivers/datastores.js +775 -0
- package/dist/drivers/datastores.js.map +1 -0
- package/dist/drivers/preview.d.ts +69 -0
- package/dist/drivers/preview.js +605 -0
- package/dist/drivers/preview.js.map +1 -0
- package/dist/drivers/types.d.ts +70 -0
- package/dist/drivers/types.js +13 -0
- package/dist/drivers/types.js.map +1 -0
- package/package.json +63 -4
- package/schema/backlot.schema.json +549 -0
- package/schema/runly.schema.json +556 -0
|
@@ -0,0 +1,412 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* The runly daemon: HTTP over a unix socket, auto-spawned by the CLI
|
|
4
|
+
* (decision 0009). One RPC endpoint; the CLI is a thin client. Serializes
|
|
5
|
+
* requests through a simple queue — policy code stays race-free.
|
|
6
|
+
*/
|
|
7
|
+
import { createServer, request } from 'node:http';
|
|
8
|
+
import { existsSync, rmSync, writeFileSync, chmodSync } from 'node:fs';
|
|
9
|
+
import { socketPath, pidPath, stateRoot } from '../core/paths.js';
|
|
10
|
+
import { electSelf, releaseSelf } from './election.js';
|
|
11
|
+
import { BrokerError } from '../core/util.js';
|
|
12
|
+
import { BUILD, VERSION } from '../core/version.js';
|
|
13
|
+
import { JOURNAL_SCHEMA_VERSION } from '../core/journal.js';
|
|
14
|
+
import { Engine } from './engine.js';
|
|
15
|
+
import { logEvent } from '../core/events.js';
|
|
16
|
+
// Singleton guard: recover() and listen happen only AFTER we win the socket,
|
|
17
|
+
// so a losing daemon never reaps the winner's children or mutates the journal.
|
|
18
|
+
const engine = new Engine();
|
|
19
|
+
// Concurrency lives in the engine: a short pool lock for claim bookkeeping and
|
|
20
|
+
// one lock per environment. Requests for different environments overlap; two
|
|
21
|
+
// operations on one environment never do.
|
|
22
|
+
async function dispatch(verb, args, emit, signal) {
|
|
23
|
+
const cwd = String(args.cwd ?? process.cwd());
|
|
24
|
+
const holder = args.holder ? String(args.holder) : undefined;
|
|
25
|
+
const holderPid = args.holderPid !== undefined ? Number(args.holderPid) : undefined;
|
|
26
|
+
switch (verb) {
|
|
27
|
+
case 'ping':
|
|
28
|
+
// The version rides on ping because the CLI already pings on EVERY
|
|
29
|
+
// invocation (ensureDaemon), so skew detection costs no extra round trip.
|
|
30
|
+
return { pid: process.pid, version: VERSION, build: BUILD, journalSchema: JOURNAL_SCHEMA_VERSION };
|
|
31
|
+
case 'up':
|
|
32
|
+
return engine.up({
|
|
33
|
+
cwd, holder, holderPid, callerEnv: args.callerEnv, presets: args.presets,
|
|
34
|
+
hygiene: args.hygiene ?? undefined,
|
|
35
|
+
ttlMs: args.ttlMs ? Number(args.ttlMs) : undefined,
|
|
36
|
+
// [] (not undefined) so a bare `up` adds the default set — every
|
|
37
|
+
// service; undefined is reserved for the internal reset-data rebind,
|
|
38
|
+
// which adds nothing and never comes through this RPC.
|
|
39
|
+
services: Array.isArray(args.services) ? args.services.map(String) : [],
|
|
40
|
+
// Removed by decision 0034; refused by the engine if a client sends it.
|
|
41
|
+
dataOnly: Boolean(args.dataOnly),
|
|
42
|
+
rebuild: Boolean(args.rebuild),
|
|
43
|
+
onProgress: emit,
|
|
44
|
+
signal,
|
|
45
|
+
});
|
|
46
|
+
case 'down':
|
|
47
|
+
return engine.down({
|
|
48
|
+
cwd, holder,
|
|
49
|
+
services: Array.isArray(args.services) ? args.services.map(String) : [],
|
|
50
|
+
onProgress: emit,
|
|
51
|
+
});
|
|
52
|
+
case 'db-new':
|
|
53
|
+
return engine.dbNew({
|
|
54
|
+
cwd, holder, holderPid,
|
|
55
|
+
datastore: String(args.datastore ?? ''),
|
|
56
|
+
preset: args.preset === undefined ? undefined : String(args.preset),
|
|
57
|
+
onProgress: emit,
|
|
58
|
+
});
|
|
59
|
+
case 'db-drop':
|
|
60
|
+
return engine.dbDrop(String(args.name ?? ''));
|
|
61
|
+
case 'db-attach':
|
|
62
|
+
return engine.dbAttach(String(args.name ?? ''), {
|
|
63
|
+
pid: Number(args.pid),
|
|
64
|
+
start: args.start === undefined || args.start === null ? undefined : Number(args.start),
|
|
65
|
+
pgid: args.pgid === undefined || args.pgid === null ? undefined : Number(args.pgid),
|
|
66
|
+
});
|
|
67
|
+
case 'db-ls':
|
|
68
|
+
return engine.dbLs(cwd, Boolean(args.all));
|
|
69
|
+
case 'ps':
|
|
70
|
+
return engine.ps(cwd, Boolean(args.all));
|
|
71
|
+
case 'ctx':
|
|
72
|
+
return engine.ctx(cwd, holder);
|
|
73
|
+
case 'reset-data':
|
|
74
|
+
return engine.resetData(cwd, holder, emit, args.presets);
|
|
75
|
+
case 'exec-env':
|
|
76
|
+
return engine.execEnv(cwd, holder);
|
|
77
|
+
case 'exec-touch':
|
|
78
|
+
return engine.execTouch(String(args.envId ?? ''));
|
|
79
|
+
case 'logs':
|
|
80
|
+
return engine.logs(cwd, String(args.service), Number(args.lines ?? 40), holder);
|
|
81
|
+
case 'logs-spec':
|
|
82
|
+
return engine.logsSpec(cwd, Array.isArray(args.services) ? args.services.map(String) : [], Boolean(args.build), holder);
|
|
83
|
+
case 'plan':
|
|
84
|
+
return engine.plan(cwd, Array.isArray(args.services) ? args.services.map(String) : [], Boolean(args.rebuild), holder);
|
|
85
|
+
case 'destroy':
|
|
86
|
+
return engine.destroy(cwd);
|
|
87
|
+
case 'pool-doctor':
|
|
88
|
+
return engine.poolDoctor(cwd, Boolean(args.fix));
|
|
89
|
+
case 'token':
|
|
90
|
+
return engine.token(cwd, String(args.role ?? 'admin'), holder);
|
|
91
|
+
case 'warm':
|
|
92
|
+
return engine.warm(cwd, emit);
|
|
93
|
+
case 'release':
|
|
94
|
+
return engine.release(cwd, holder);
|
|
95
|
+
case 'preview':
|
|
96
|
+
return engine.previewStart(cwd, String(args.service), holder, args.ttlMs ? Number(args.ttlMs) : undefined, args.httpsPort !== undefined ? Number(args.httpsPort) : undefined);
|
|
97
|
+
case 'preview-stop':
|
|
98
|
+
return engine.previewStop(cwd, holder);
|
|
99
|
+
case 'appliance-ls':
|
|
100
|
+
return engine.applianceLs(cwd);
|
|
101
|
+
case 'appliance-start':
|
|
102
|
+
return engine.applianceStart(cwd, args.name ? String(args.name) : undefined);
|
|
103
|
+
case 'appliance-stop':
|
|
104
|
+
return engine.applianceStop(cwd, String(args.name));
|
|
105
|
+
case 'status':
|
|
106
|
+
return engine.status();
|
|
107
|
+
case 'doctor':
|
|
108
|
+
// The CLI's own version is passed in because only the CALLER knows it,
|
|
109
|
+
// and skew belongs in the diagnosis every RPC client reads — not just in
|
|
110
|
+
// the CLI that happens to have the comparison in front of it.
|
|
111
|
+
return engine.doctor(args.cliVersion === undefined ? undefined : String(args.cliVersion));
|
|
112
|
+
case 'update-plan':
|
|
113
|
+
return engine.updatePlan(args.cliVersion === undefined ? undefined : String(args.cliVersion));
|
|
114
|
+
case 'daemon-restart':
|
|
115
|
+
// The refusal lives in the ENGINE, not the CLI, so any
|
|
116
|
+
// other RPC caller cannot restart past an in-flight operation just by
|
|
117
|
+
// not implementing the check (the same reason the old --data-only
|
|
118
|
+
// contradictions were moved out of the CLI in 0.8.0).
|
|
119
|
+
engine.assertRestartable({
|
|
120
|
+
force: Boolean(args.force),
|
|
121
|
+
cliVersion: args.cliVersion === undefined ? undefined : String(args.cliVersion),
|
|
122
|
+
});
|
|
123
|
+
stopDaemon();
|
|
124
|
+
return { stopping: true, was: VERSION, holders: engine.leaseHolders() };
|
|
125
|
+
case 'pool-recycle':
|
|
126
|
+
return engine.poolRecycle({
|
|
127
|
+
envId: args.envId ? String(args.envId) : undefined,
|
|
128
|
+
force: Boolean(args.force ?? args.all),
|
|
129
|
+
});
|
|
130
|
+
case 'pool-reconcile':
|
|
131
|
+
// Local substrate: reconcile = doctor + reap anything degraded/stuck.
|
|
132
|
+
return engine.poolReconcile();
|
|
133
|
+
case 'pool-gc':
|
|
134
|
+
return engine.poolGc();
|
|
135
|
+
case 'shutdown':
|
|
136
|
+
stopDaemon();
|
|
137
|
+
return { stopping: true };
|
|
138
|
+
default:
|
|
139
|
+
throw new BrokerError('env-error', `daemon does not know verb '${verb}'`, 'rpc');
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
const sock = socketPath();
|
|
143
|
+
let ownsSocket = false;
|
|
144
|
+
let ownsLock = false;
|
|
145
|
+
/**
|
|
146
|
+
* Stop this daemon after answering the caller.
|
|
147
|
+
*
|
|
148
|
+
* The 50ms delay is what lets the result frame reach the client before the
|
|
149
|
+
* process goes: exiting inside the handler leaves the CLI reading a socket that
|
|
150
|
+
* closed without a result. `runly update` and `daemon stop` share this path
|
|
151
|
+
* on purpose — a restart is a stop plus the next verb's autospawn (decision
|
|
152
|
+
* 0009), so there is no second teardown implementation to keep in step.
|
|
153
|
+
*/
|
|
154
|
+
function stopDaemon() {
|
|
155
|
+
stopSweeper();
|
|
156
|
+
setTimeout(async () => {
|
|
157
|
+
await engine.shutdown();
|
|
158
|
+
// Clean up what we own, exactly as the signal path does — a stale lock
|
|
159
|
+
// is recoverable (the next daemon sees a dead holder) but leaving one
|
|
160
|
+
// behind makes every restart pay a lock-break round.
|
|
161
|
+
if (ownsSocket)
|
|
162
|
+
rmSync(sock, { force: true });
|
|
163
|
+
if (ownsLock)
|
|
164
|
+
releaseSelf();
|
|
165
|
+
process.exit(0);
|
|
166
|
+
}, 50);
|
|
167
|
+
}
|
|
168
|
+
/** Is a LIVE daemon already answering on the socket? (vs. a stale socket file) */
|
|
169
|
+
function pingExisting() {
|
|
170
|
+
return new Promise((resolve) => {
|
|
171
|
+
if (!existsSync(sock))
|
|
172
|
+
return resolve(false);
|
|
173
|
+
const req = request({ socketPath: sock, path: '/', method: 'POST', timeout: 1500 }, (res) => {
|
|
174
|
+
res.resume();
|
|
175
|
+
resolve(res.statusCode === 200);
|
|
176
|
+
});
|
|
177
|
+
req.on('error', () => resolve(false)); // ECONNREFUSED = stale socket
|
|
178
|
+
req.on('timeout', () => {
|
|
179
|
+
req.destroy();
|
|
180
|
+
resolve(false);
|
|
181
|
+
});
|
|
182
|
+
req.end(JSON.stringify({ verb: 'ping', args: {} }));
|
|
183
|
+
});
|
|
184
|
+
}
|
|
185
|
+
/**
|
|
186
|
+
* Recovery reconciles prior daemon state, and now genuinely waits on kills
|
|
187
|
+
* (verified reaps, orphan sweep) instead of firing signals blind. Requests are
|
|
188
|
+
* therefore held until it completes: serving `status` mid-reconcile would show
|
|
189
|
+
* a caller envs that are about to change state, which is the old synchronous
|
|
190
|
+
* contract silently broken.
|
|
191
|
+
*
|
|
192
|
+
* `ping` is exempt — the singleton election must be answerable immediately, or
|
|
193
|
+
* a second daemon would conclude the socket is stale and take over.
|
|
194
|
+
*/
|
|
195
|
+
let recovered = Promise.resolve();
|
|
196
|
+
/** The sweeper's timer; cleared the moment a shutdown begins. */
|
|
197
|
+
let sweepTimer;
|
|
198
|
+
function stopSweeper() {
|
|
199
|
+
if (sweepTimer)
|
|
200
|
+
clearInterval(sweepTimer);
|
|
201
|
+
sweepTimer = undefined;
|
|
202
|
+
}
|
|
203
|
+
const server = createServer((req, res) => {
|
|
204
|
+
let body = '';
|
|
205
|
+
req.on('data', (d) => (body += d));
|
|
206
|
+
// A client that hangs up (Ctrl-C, a killed agent) aborts what it asked for
|
|
207
|
+
// where that is still possible: a queued `up` leaves the queue instead of
|
|
208
|
+
// binding later for nobody.
|
|
209
|
+
const aborter = new AbortController();
|
|
210
|
+
res.on('close', () => {
|
|
211
|
+
if (!res.writableFinished)
|
|
212
|
+
aborter.abort();
|
|
213
|
+
});
|
|
214
|
+
req.on('end', () => {
|
|
215
|
+
void (async () => {
|
|
216
|
+
// Response is newline-delimited JSON: zero or more {type:'progress'}
|
|
217
|
+
// frames, then exactly one {type:'result'} frame. The CLI renders
|
|
218
|
+
// progress to stderr (TTY only) and the result to stdout.
|
|
219
|
+
res.writeHead(200, { 'content-type': 'application/x-ndjson' });
|
|
220
|
+
const emit = (phase) => {
|
|
221
|
+
try {
|
|
222
|
+
res.write(JSON.stringify({ type: 'progress', phase }) + '\n');
|
|
223
|
+
}
|
|
224
|
+
catch {
|
|
225
|
+
/* client hung up mid-stream */
|
|
226
|
+
}
|
|
227
|
+
};
|
|
228
|
+
let verbForLog = '?';
|
|
229
|
+
try {
|
|
230
|
+
const { verb, args } = JSON.parse(body || '{}');
|
|
231
|
+
verbForLog = String(verb ?? '?');
|
|
232
|
+
if (verb !== 'ping')
|
|
233
|
+
await recovered;
|
|
234
|
+
const data = await dispatch(verb, args ?? {}, emit, aborter.signal);
|
|
235
|
+
res.end(JSON.stringify({ type: 'result', ok: true, data }) + '\n');
|
|
236
|
+
}
|
|
237
|
+
catch (err) {
|
|
238
|
+
// An unclassified throw is a DAEMON bug, not a bad environment.
|
|
239
|
+
// Labelling it env-error told the agent to recycle an environment,
|
|
240
|
+
// which cannot fix a TypeError in the broker — and buried the defect.
|
|
241
|
+
const e = err instanceof BrokerError
|
|
242
|
+
? err.toJSON()
|
|
243
|
+
: { class: 'infra-error', message: `internal daemon error: ${String(err.message ?? err)}` };
|
|
244
|
+
if (!(err instanceof BrokerError)) {
|
|
245
|
+
logEvent({ level: 'error', kind: 'internal', detail: `${verbForLog}: ${String(err.stack ?? err)}`.slice(0, 1000) });
|
|
246
|
+
}
|
|
247
|
+
res.end(JSON.stringify({ type: 'result', ok: false, error: e }) + '\n');
|
|
248
|
+
}
|
|
249
|
+
})();
|
|
250
|
+
});
|
|
251
|
+
});
|
|
252
|
+
// EADDRINUSE means another daemon won the listen race between our ping and our
|
|
253
|
+
// bind — concede cleanly rather than clobber it.
|
|
254
|
+
server.on('error', (err) => {
|
|
255
|
+
if (err.code === 'EADDRINUSE') {
|
|
256
|
+
if (process.send)
|
|
257
|
+
process.send('ready'); // a live daemon exists; the client can proceed
|
|
258
|
+
process.exit(0);
|
|
259
|
+
}
|
|
260
|
+
throw err;
|
|
261
|
+
});
|
|
262
|
+
async function start() {
|
|
263
|
+
if (await pingExisting()) {
|
|
264
|
+
// A healthy daemon already owns the socket — do NOT recover() or listen.
|
|
265
|
+
if (process.send)
|
|
266
|
+
process.send('ready');
|
|
267
|
+
process.exit(0);
|
|
268
|
+
}
|
|
269
|
+
// Win the lock BEFORE touching the socket. Without this, two daemons that
|
|
270
|
+
// both saw no answer to their ping would each unlink the socket — the second
|
|
271
|
+
// deleting the first's LIVE one — and both would then bind successfully,
|
|
272
|
+
// sweeping the same journal and reaping each other's processes.
|
|
273
|
+
if (!electSelf()) {
|
|
274
|
+
if (process.send)
|
|
275
|
+
process.send('ready'); // someone else owns it; the client can proceed
|
|
276
|
+
process.exit(0);
|
|
277
|
+
}
|
|
278
|
+
ownsLock = true;
|
|
279
|
+
// The daemon inherited its cwd from whichever CLI spawned it — usually a
|
|
280
|
+
// WORKTREE, which the consumer may delete while the daemon lives on. A
|
|
281
|
+
// deleted cwd makes anything that reads it (worker_threads spawn calls
|
|
282
|
+
// uv_cwd) fail with ENOENT. Pin cwd to the state root, which the daemon
|
|
283
|
+
// owns for exactly its own lifetime; nothing daemon-side resolves against
|
|
284
|
+
// cwd (every RPC carries the client's cwd explicitly).
|
|
285
|
+
process.chdir(stateRoot());
|
|
286
|
+
// Only the election winner may remove a socket, so this can no longer be a
|
|
287
|
+
// live one: any daemon that could have been listening lost the lock.
|
|
288
|
+
if (existsSync(sock))
|
|
289
|
+
rmSync(sock, { force: true }); // stale socket from a dead daemon
|
|
290
|
+
// Create the socket ALREADY private. The chmod below runs inside the listen
|
|
291
|
+
// callback, by which point the socket exists and can accept connections, so
|
|
292
|
+
// a permissive umask left a window where it was world-reachable. The socket
|
|
293
|
+
// has no RPC auth and exposes arbitrary-shell verbs, so the window matters.
|
|
294
|
+
const callerUmask = process.umask(0o077);
|
|
295
|
+
server.listen(sock, () => {
|
|
296
|
+
// …and only the socket. Everything the daemon spawns — services, builds,
|
|
297
|
+
// upkeep, exec — inherited 077 and wrote owner-only files into the
|
|
298
|
+
// caller's worktree (a build output nobody else could read). The state
|
|
299
|
+
// root is 0700 itself, so restoring the caller's umask keeps it private.
|
|
300
|
+
process.umask(callerUmask);
|
|
301
|
+
ownsSocket = true;
|
|
302
|
+
// The socket has no RPC auth and exposes arbitrary-shell verbs (exec/token),
|
|
303
|
+
// so it must be owner-only. On macOS the socket-file mode is what actually
|
|
304
|
+
// gates connect() (the dir traversal isn't enough), so this chmod is the
|
|
305
|
+
// real boundary, not just belt-and-suspenders.
|
|
306
|
+
try {
|
|
307
|
+
chmodSync(sock, 0o600);
|
|
308
|
+
}
|
|
309
|
+
catch {
|
|
310
|
+
/* best-effort; the 0700 state dir is the backstop */
|
|
311
|
+
}
|
|
312
|
+
writeFileSync(pidPath(), String(process.pid));
|
|
313
|
+
// Only NOW, as the sole owner, reconcile prior daemon state. Recovery
|
|
314
|
+
// verifies each reap and sweeps for orphans, so it awaits real work; every
|
|
315
|
+
// non-ping request queues behind it.
|
|
316
|
+
recovered = engine.recover().catch((err) => {
|
|
317
|
+
// A failed reconcile must not wedge the daemon forever — surface it and
|
|
318
|
+
// let requests through rather than hanging every client.
|
|
319
|
+
logEvent({ level: 'error', kind: 'recover', detail: `recovery failed: ${String(err.message ?? err)}` });
|
|
320
|
+
});
|
|
321
|
+
const sweepMs = Number(process.env.BACKLOT_SWEEP_MS ?? 15_000);
|
|
322
|
+
// The sweeper must not run DURING recovery: it would act on rows recovery
|
|
323
|
+
// is still reconciling, and a stale-snapshot save could resurrect an env
|
|
324
|
+
// recovery had just deleted. Each tick also swallows its own rejection —
|
|
325
|
+
// a fire-and-forget async call whose promise rejects is an unhandled
|
|
326
|
+
// rejection, which takes the daemon down on any transient journal or FS
|
|
327
|
+
// write failure.
|
|
328
|
+
void recovered.then(() => {
|
|
329
|
+
if (engine.stopping)
|
|
330
|
+
return;
|
|
331
|
+
sweepTimer = setInterval(() => {
|
|
332
|
+
void engine.sweep().catch((err) => {
|
|
333
|
+
logEvent({ level: 'error', kind: 'sweep', detail: `sweep failed: ${String(err.message ?? err)}` });
|
|
334
|
+
});
|
|
335
|
+
}, sweepMs);
|
|
336
|
+
sweepTimer.unref();
|
|
337
|
+
}).catch((err) => logEvent({ level: 'error', kind: 'recover', detail: `sweeper never armed: ${String(err.message ?? err)}` }));
|
|
338
|
+
// Detach from the spawning CLI's lifetime.
|
|
339
|
+
if (process.send)
|
|
340
|
+
process.send('ready');
|
|
341
|
+
});
|
|
342
|
+
}
|
|
343
|
+
void start().catch((err) => {
|
|
344
|
+
// Nothing has been established yet, so there is no graceful path — but dying
|
|
345
|
+
// silently left the client waiting on a daemon that would never answer.
|
|
346
|
+
console.error(`runly daemon failed to start: ${String(err.stack ?? err)}`);
|
|
347
|
+
process.exit(1);
|
|
348
|
+
});
|
|
349
|
+
/**
|
|
350
|
+
* Runtime-level guards.
|
|
351
|
+
*
|
|
352
|
+
* Node's default for an unhandled rejection or an uncaught exception is to kill
|
|
353
|
+
* the process — which for this daemon means every environment loses its
|
|
354
|
+
* supervisor over one missed `.catch`. Four confirmed defects in the 2026-07-18
|
|
355
|
+
* review were exactly that shape (a socket 'error', an fs.watch 'error', a
|
|
356
|
+
* spawn 'error' with no 'exit', a fire-and-forget sweep).
|
|
357
|
+
*
|
|
358
|
+
* The two are handled ASYMMETRICALLY on purpose:
|
|
359
|
+
*
|
|
360
|
+
* - An unhandled REJECTION is almost always a missing `.catch` on an otherwise
|
|
361
|
+
* contained operation. Logging and staying alive is right: the daemon keeps
|
|
362
|
+
* supervising, and the event log names the gap.
|
|
363
|
+
* - An uncaught EXCEPTION leaves the process in an undefined state, and this
|
|
364
|
+
* daemon owns a journal that is the machine's source of truth. Continuing
|
|
365
|
+
* risks corrupting it. Exiting is safe BECAUSE of the crash-recovery contract
|
|
366
|
+
* (decision 0009): services survive, and the next daemon reconciles them. So
|
|
367
|
+
* log loudly, then die deliberately rather than limp.
|
|
368
|
+
*/
|
|
369
|
+
process.on('unhandledRejection', (reason) => {
|
|
370
|
+
logEvent({
|
|
371
|
+
level: 'error',
|
|
372
|
+
kind: 'internal',
|
|
373
|
+
detail: `unhandled rejection (daemon continuing): ${String(reason?.stack ?? reason)}`.slice(0, 2000),
|
|
374
|
+
});
|
|
375
|
+
});
|
|
376
|
+
process.on('uncaughtException', (err) => {
|
|
377
|
+
logEvent({
|
|
378
|
+
level: 'error',
|
|
379
|
+
kind: 'internal',
|
|
380
|
+
detail: `uncaught exception — exiting so a clean daemon can reconcile: ${String(err?.stack ?? err)}`.slice(0, 2000),
|
|
381
|
+
});
|
|
382
|
+
try {
|
|
383
|
+
if (ownsSocket)
|
|
384
|
+
rmSync(sock, { force: true });
|
|
385
|
+
if (ownsLock)
|
|
386
|
+
releaseSelf();
|
|
387
|
+
}
|
|
388
|
+
catch {
|
|
389
|
+
/* best-effort */
|
|
390
|
+
}
|
|
391
|
+
process.exit(1);
|
|
392
|
+
});
|
|
393
|
+
for (const sig of ['SIGINT', 'SIGTERM']) {
|
|
394
|
+
process.on(sig, () => {
|
|
395
|
+
// A daemon that LOST the election must exit without touching shared state:
|
|
396
|
+
// engine.shutdown() stops services and rewrites env rows, which would let a
|
|
397
|
+
// conceding process tear down the winner's environments.
|
|
398
|
+
if (!ownsLock && !ownsSocket)
|
|
399
|
+
process.exit(0);
|
|
400
|
+
stopSweeper();
|
|
401
|
+
void engine.shutdown().then(() => {
|
|
402
|
+
// Only remove the socket if WE own it — a losing/duplicate daemon must
|
|
403
|
+
// never delete the healthy daemon's socket.
|
|
404
|
+
if (ownsSocket)
|
|
405
|
+
rmSync(sock, { force: true });
|
|
406
|
+
if (ownsLock)
|
|
407
|
+
releaseSelf();
|
|
408
|
+
process.exit(0);
|
|
409
|
+
});
|
|
410
|
+
});
|
|
411
|
+
}
|
|
412
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/daemon/index.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
export type TargetState = 'down' | 'starting' | 'up';
|
|
2
|
+
export interface ProxyStats {
|
|
3
|
+
/** The public port (what ctx reports). */
|
|
4
|
+
port: number;
|
|
5
|
+
/** The service currently behind it, when known. */
|
|
6
|
+
service?: string;
|
|
7
|
+
state: TargetState;
|
|
8
|
+
/** Where the service listens right now (diagnostics only — never hand it out). */
|
|
9
|
+
internalPort?: number;
|
|
10
|
+
/** Client→server bytes since the daemon started proxying this port. */
|
|
11
|
+
clientBytes: number;
|
|
12
|
+
/** Epoch ms of the last client→server byte; null when none arrived yet. */
|
|
13
|
+
lastActivityAt: number | null;
|
|
14
|
+
/** Connections currently piped. */
|
|
15
|
+
open: number;
|
|
16
|
+
/** Connections currently held while the service starts. */
|
|
17
|
+
held: number;
|
|
18
|
+
/** Connections accepted in total. */
|
|
19
|
+
accepted: number;
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* Called when a connection arrives for a target that is DOWN. Returning true
|
|
23
|
+
* means "a start has been requested" — the connection is held (bounded by
|
|
24
|
+
* BACKLOT_PROXY_HOLD_MS) until the engine calls `up` (forwarded) or `down`
|
|
25
|
+
* (closed). Returning false closes it at once, which is what a client saw
|
|
26
|
+
* before the proxy existed: nothing listening.
|
|
27
|
+
*/
|
|
28
|
+
export type WakeHook = (envId: string, key: string) => boolean;
|
|
29
|
+
/** How long a connection may wait for its service to become ready. */
|
|
30
|
+
export declare const HOLD_MS: () => number;
|
|
31
|
+
/** What every connection carries for a replay right now (tests, `status`). */
|
|
32
|
+
export declare const replayCarriedBytes: () => number;
|
|
33
|
+
/**
|
|
34
|
+
* May bytes a process already received be sent to the next one? Only when the
|
|
35
|
+
* connection's first bytes are an HTTP request with a method that is safe to
|
|
36
|
+
* repeat: the dead process may have acted on what it read (charged a card,
|
|
37
|
+
* inserted a row) before it died, and a replay would act a second time. Any
|
|
38
|
+
* other request — POST, PUT, PATCH, DELETE, a database or TLS wire protocol,
|
|
39
|
+
* a first chunk too short to tell — is not replayed once delivered.
|
|
40
|
+
*/
|
|
41
|
+
export declare function replayable(chunks: readonly Buffer[]): boolean;
|
|
42
|
+
export declare class PortInUse extends Error {
|
|
43
|
+
readonly port: number;
|
|
44
|
+
readonly host: string;
|
|
45
|
+
constructor(port: number, host: string);
|
|
46
|
+
}
|
|
47
|
+
export declare class ProxyHub {
|
|
48
|
+
private targets;
|
|
49
|
+
/**
|
|
50
|
+
* Internal ports handed to a start that is not `up` yet, by env/key. A port
|
|
51
|
+
* is probed free when it is allocated, and the service binds it seconds
|
|
52
|
+
* later; without this, two starts in that window were given the same port
|
|
53
|
+
* and one environment's public URL served the other's process.
|
|
54
|
+
*/
|
|
55
|
+
private reservedInternal;
|
|
56
|
+
private wakeHook?;
|
|
57
|
+
private activityHook?;
|
|
58
|
+
private id;
|
|
59
|
+
/** The engine installs this (decision 0035): start a stopped service on its first connection. */
|
|
60
|
+
setWakeHook(hook: WakeHook | undefined): void;
|
|
61
|
+
/** Optional observer of client bytes (idle bookkeeping lives with the caller). */
|
|
62
|
+
setActivityHook(hook: ((envId: string, key: string) => void) | undefined): void;
|
|
63
|
+
wake(envId: string, key: string): boolean;
|
|
64
|
+
noteActivity(envId: string, key: string): void;
|
|
65
|
+
/** Where the service behind env/key listens, also while it restarts (decision 0035). */
|
|
66
|
+
internalPortOf(envId: string, key: string): number | undefined;
|
|
67
|
+
/** Start a target's activity clock from the journal, so a daemon restart keeps it (decision 0035). */
|
|
68
|
+
seedActivity(envId: string, key: string, at: number): void;
|
|
69
|
+
/** The last client byte per port key of an environment (null = none yet). */
|
|
70
|
+
activity(envId: string): Record<string, number | null>;
|
|
71
|
+
/** Every env id with a listener (doctor's stale-listener check, decision 0037). */
|
|
72
|
+
envIds(): Set<string>;
|
|
73
|
+
/** Is the public listener for this env/key currently held, on this port? */
|
|
74
|
+
holds(envId: string, key: string, port: number): boolean;
|
|
75
|
+
/** Every public port this daemon is listening on. */
|
|
76
|
+
heldPorts(): Set<number>;
|
|
77
|
+
/** Every internal port currently assigned to a target (up or not) or reserved for a start, so none is handed out twice. */
|
|
78
|
+
assignedInternalPorts(): Set<number>;
|
|
79
|
+
/** Hold `port` for the start of env/key until it is `up` (then the target owns it) or goes down. */
|
|
80
|
+
reserveInternal(envId: string, key: string, port: number): void;
|
|
81
|
+
private release;
|
|
82
|
+
/**
|
|
83
|
+
* Listen on `port` for env/key (idempotent). Throws PortInUse when another
|
|
84
|
+
* process holds it; any previous listener for this env/key on a different
|
|
85
|
+
* port is closed first, and its counters carried over.
|
|
86
|
+
*/
|
|
87
|
+
listen(envId: string, key: string, port: number): Promise<void>;
|
|
88
|
+
/** New connections are held until `up` (or dropped by `down`). */
|
|
89
|
+
starting(envId: string, key: string, service?: string): void;
|
|
90
|
+
/** The service behind env/key is ready on `internalPort`; held connections go through. */
|
|
91
|
+
up(envId: string, key: string, internalPort: number, service?: string): void;
|
|
92
|
+
/** Nothing is (or will soon be) behind env/key: held connections are closed. */
|
|
93
|
+
down(envId: string, key: string): void;
|
|
94
|
+
/** A service was stopped: its targets go down unless a restart already marked them starting. */
|
|
95
|
+
serviceStopped(envId: string, service: string): void;
|
|
96
|
+
/** Every `starting` target of an env that never reached `up` goes down (a failed bind). */
|
|
97
|
+
settle(envId: string): void;
|
|
98
|
+
state(envId: string, key: string): TargetState | undefined;
|
|
99
|
+
/** Close one env/key listener (a port key the manifest dropped). */
|
|
100
|
+
closeKey(envId: string, key: string): void;
|
|
101
|
+
/** The port keys this daemon holds a listener for, for one environment. */
|
|
102
|
+
keysOf(envId: string): string[];
|
|
103
|
+
/** Release every listener of an environment (teardown). */
|
|
104
|
+
closeEnv(envId: string): void;
|
|
105
|
+
closeAll(): void;
|
|
106
|
+
/** Stats by port key for one environment. */
|
|
107
|
+
stats(envId: string): Record<string, ProxyStats>;
|
|
108
|
+
}
|