aibex 0.0.0-stage → 0.0.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/cli.js +777 -0
- package/dist/supervisor.js +1781 -0
- package/package.json +40 -4
- package/README.md +0 -3
package/dist/cli.js
ADDED
|
@@ -0,0 +1,777 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { parseArgs } from "node:util";
|
|
3
|
+
import { homedir } from "node:os";
|
|
4
|
+
import path from "node:path";
|
|
5
|
+
import { readFileSync, mkdirSync, writeFileSync, renameSync, rmSync, existsSync } from "node:fs";
|
|
6
|
+
import { randomBytes } from "node:crypto";
|
|
7
|
+
import { fork } from "node:child_process";
|
|
8
|
+
import { fileURLToPath } from "node:url";
|
|
9
|
+
import { setTimeout as setTimeout$1 } from "node:timers/promises";
|
|
10
|
+
import { createConnection } from "node:net";
|
|
11
|
+
function out(line) {
|
|
12
|
+
process.stdout.write(`${line}
|
|
13
|
+
`);
|
|
14
|
+
}
|
|
15
|
+
function error(line) {
|
|
16
|
+
process.stderr.write(`${line}
|
|
17
|
+
`);
|
|
18
|
+
}
|
|
19
|
+
function aibexHome(env = process.env) {
|
|
20
|
+
const override = env.AIBEX_HOME;
|
|
21
|
+
return override ? path.resolve(override) : path.join(homedir(), ".aibex");
|
|
22
|
+
}
|
|
23
|
+
function statePaths(home) {
|
|
24
|
+
return {
|
|
25
|
+
home,
|
|
26
|
+
pidFile: path.join(home, "aibexd.pid"),
|
|
27
|
+
configFile: path.join(home, "config.json"),
|
|
28
|
+
stateFile: path.join(home, "state.json"),
|
|
29
|
+
socketFile: path.join(home, "supervisor.sock"),
|
|
30
|
+
versionsDir: path.join(home, "versions"),
|
|
31
|
+
hostsDir: path.join(home, "hosts")
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
const DEFAULT_PORTS = {
|
|
35
|
+
"https:": "443",
|
|
36
|
+
"http:": "80"
|
|
37
|
+
};
|
|
38
|
+
function hostDirectoryName(host) {
|
|
39
|
+
const url = new URL(host);
|
|
40
|
+
const port = url.port || DEFAULT_PORTS[url.protocol] || "";
|
|
41
|
+
return `${url.hostname}_${port}`.replaceAll(/[^\w.-]/g, "_");
|
|
42
|
+
}
|
|
43
|
+
function hostDirectory(paths, host) {
|
|
44
|
+
return path.join(paths.hostsDir, hostDirectoryName(host));
|
|
45
|
+
}
|
|
46
|
+
function writeFileAtomicSync(file, content) {
|
|
47
|
+
mkdirSync(path.dirname(file), { recursive: true });
|
|
48
|
+
const temporary = `${file}.${process.pid}.${randomBytes(4).toString("hex")}.tmp`;
|
|
49
|
+
try {
|
|
50
|
+
writeFileSync(temporary, content);
|
|
51
|
+
renameSync(temporary, file);
|
|
52
|
+
} catch (error2) {
|
|
53
|
+
rmSync(temporary, { force: true });
|
|
54
|
+
throw error2;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
function writeJsonAtomicSync(file, value) {
|
|
58
|
+
writeFileAtomicSync(file, `${JSON.stringify(value, void 0, 2)}
|
|
59
|
+
`);
|
|
60
|
+
}
|
|
61
|
+
function readJsonSync(file) {
|
|
62
|
+
let text;
|
|
63
|
+
try {
|
|
64
|
+
text = readFileSync(file, "utf8");
|
|
65
|
+
} catch {
|
|
66
|
+
return void 0;
|
|
67
|
+
}
|
|
68
|
+
try {
|
|
69
|
+
return JSON.parse(text);
|
|
70
|
+
} catch {
|
|
71
|
+
return void 0;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
function isProcessAlive(pid) {
|
|
75
|
+
if (!Number.isInteger(pid) || pid <= 0) return false;
|
|
76
|
+
try {
|
|
77
|
+
process.kill(pid, 0);
|
|
78
|
+
return true;
|
|
79
|
+
} catch (error2) {
|
|
80
|
+
return error2.code === "EPERM";
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
function parseRecord(value) {
|
|
84
|
+
if (typeof value !== "object" || value === null) return void 0;
|
|
85
|
+
const { pid, host, startedAt } = value;
|
|
86
|
+
if (typeof pid !== "number" || typeof host !== "string") return void 0;
|
|
87
|
+
return {
|
|
88
|
+
pid,
|
|
89
|
+
host,
|
|
90
|
+
startedAt: typeof startedAt === "string" ? startedAt : ""
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
function readPidState(paths) {
|
|
94
|
+
const record = parseRecord(readJsonSync(paths.pidFile));
|
|
95
|
+
if (!record) return { state: "absent" };
|
|
96
|
+
return isProcessAlive(record.pid) ? { state: "running", record } : { state: "stale", record };
|
|
97
|
+
}
|
|
98
|
+
function writePidFile(paths, record) {
|
|
99
|
+
writeJsonAtomicSync(paths.pidFile, record);
|
|
100
|
+
}
|
|
101
|
+
function removePidFile(paths, pid) {
|
|
102
|
+
const record = parseRecord(readJsonSync(paths.pidFile));
|
|
103
|
+
if (record?.pid === pid) rmSync(paths.pidFile, { force: true });
|
|
104
|
+
}
|
|
105
|
+
function restart(env = process.env) {
|
|
106
|
+
const paths = statePaths(aibexHome(env));
|
|
107
|
+
const running = readPidState(paths);
|
|
108
|
+
if (running.state !== "running") {
|
|
109
|
+
error("aibexd is not running. Run `aibexd start` to start it.");
|
|
110
|
+
return 1;
|
|
111
|
+
}
|
|
112
|
+
const { pid, host } = running.record;
|
|
113
|
+
try {
|
|
114
|
+
process.kill(pid, "SIGHUP");
|
|
115
|
+
} catch {
|
|
116
|
+
error("aibexd is not running. Run `aibexd start` to start it.");
|
|
117
|
+
return 1;
|
|
118
|
+
}
|
|
119
|
+
out(
|
|
120
|
+
`restarting the supervisor of aibexd (pid ${pid}) against ${host} from the installed code`
|
|
121
|
+
);
|
|
122
|
+
return 0;
|
|
123
|
+
}
|
|
124
|
+
function log(source, message) {
|
|
125
|
+
process.stdout.write(`[${source}] ${message}
|
|
126
|
+
`);
|
|
127
|
+
}
|
|
128
|
+
function formatDuration(ms) {
|
|
129
|
+
if (ms < 1e3) return `${Math.round(ms)}ms`;
|
|
130
|
+
const seconds = Math.round(ms / 1e3);
|
|
131
|
+
if (seconds < 60) return `${seconds}s`;
|
|
132
|
+
const minutes = Math.floor(seconds / 60);
|
|
133
|
+
if (minutes >= 60) {
|
|
134
|
+
const hours = Math.floor(minutes / 60);
|
|
135
|
+
const restMinutes = minutes % 60;
|
|
136
|
+
return restMinutes === 0 ? `${hours}h` : `${hours}h${restMinutes}m`;
|
|
137
|
+
}
|
|
138
|
+
const rest = seconds % 60;
|
|
139
|
+
return rest === 0 ? `${minutes}m` : `${minutes}m${rest}s`;
|
|
140
|
+
}
|
|
141
|
+
function supervisorPath() {
|
|
142
|
+
return path.join(
|
|
143
|
+
path.dirname(fileURLToPath(import.meta.url)),
|
|
144
|
+
"supervisor.js"
|
|
145
|
+
);
|
|
146
|
+
}
|
|
147
|
+
function describeExit(code, signal) {
|
|
148
|
+
return signal ? `killed by ${signal}` : `exited with code ${code ?? 0}`;
|
|
149
|
+
}
|
|
150
|
+
function runOuterLoop(options) {
|
|
151
|
+
const { env, timings } = options;
|
|
152
|
+
let child;
|
|
153
|
+
let startedAt = 0;
|
|
154
|
+
let delay = timings.backoffInitialMs;
|
|
155
|
+
let restartTimer;
|
|
156
|
+
let killTimer;
|
|
157
|
+
let stopping = false;
|
|
158
|
+
let restartNow = false;
|
|
159
|
+
return new Promise((resolve) => {
|
|
160
|
+
const finish = () => {
|
|
161
|
+
clearTimeout(restartTimer);
|
|
162
|
+
clearTimeout(killTimer);
|
|
163
|
+
process.off("SIGHUP", onHangup);
|
|
164
|
+
process.off("SIGINT", onStop);
|
|
165
|
+
process.off("SIGTERM", onStop);
|
|
166
|
+
log("aibexd", "stopped");
|
|
167
|
+
options.onExit();
|
|
168
|
+
resolve(0);
|
|
169
|
+
};
|
|
170
|
+
const scheduleRestart = () => {
|
|
171
|
+
const wait = delay;
|
|
172
|
+
delay = Math.min(delay * 2, timings.backoffMaxMs);
|
|
173
|
+
restartTimer = setTimeout(start2, wait);
|
|
174
|
+
};
|
|
175
|
+
const start2 = () => {
|
|
176
|
+
restartTimer = void 0;
|
|
177
|
+
const entry = supervisorPath();
|
|
178
|
+
if (!existsSync(entry)) {
|
|
179
|
+
const wait = delay;
|
|
180
|
+
log(
|
|
181
|
+
"aibexd",
|
|
182
|
+
`no supervisor at ${entry}; aibexd was moved or uninstalled, run \`aibexd start\` again (retrying in ${formatDuration(wait)})`
|
|
183
|
+
);
|
|
184
|
+
scheduleRestart();
|
|
185
|
+
return;
|
|
186
|
+
}
|
|
187
|
+
startedAt = Date.now();
|
|
188
|
+
const started = fork(entry, [], { stdio: "inherit", env });
|
|
189
|
+
child = started;
|
|
190
|
+
started.on("error", (error2) => {
|
|
191
|
+
log("aibexd", `supervisor error: ${error2.message}`);
|
|
192
|
+
});
|
|
193
|
+
started.on("exit", (code, signal) => {
|
|
194
|
+
if (child !== started) return;
|
|
195
|
+
child = void 0;
|
|
196
|
+
clearTimeout(killTimer);
|
|
197
|
+
if (stopping) {
|
|
198
|
+
finish();
|
|
199
|
+
return;
|
|
200
|
+
}
|
|
201
|
+
if (restartNow) {
|
|
202
|
+
restartNow = false;
|
|
203
|
+
delay = timings.backoffInitialMs;
|
|
204
|
+
start2();
|
|
205
|
+
return;
|
|
206
|
+
}
|
|
207
|
+
if (Date.now() - startedAt >= timings.backoffResetMs) {
|
|
208
|
+
delay = timings.backoffInitialMs;
|
|
209
|
+
}
|
|
210
|
+
log(
|
|
211
|
+
"aibexd",
|
|
212
|
+
`supervisor ${describeExit(code, signal)}; restarting in ${formatDuration(delay)}`
|
|
213
|
+
);
|
|
214
|
+
scheduleRestart();
|
|
215
|
+
});
|
|
216
|
+
};
|
|
217
|
+
const onHangup = () => {
|
|
218
|
+
if (stopping) return;
|
|
219
|
+
log("aibexd", "restarting the supervisor");
|
|
220
|
+
if (child) {
|
|
221
|
+
restartNow = true;
|
|
222
|
+
child.kill("SIGTERM");
|
|
223
|
+
return;
|
|
224
|
+
}
|
|
225
|
+
clearTimeout(restartTimer);
|
|
226
|
+
delay = timings.backoffInitialMs;
|
|
227
|
+
start2();
|
|
228
|
+
};
|
|
229
|
+
const onStop = () => {
|
|
230
|
+
if (stopping) {
|
|
231
|
+
child?.kill("SIGKILL");
|
|
232
|
+
return;
|
|
233
|
+
}
|
|
234
|
+
stopping = true;
|
|
235
|
+
clearTimeout(restartTimer);
|
|
236
|
+
if (!child) {
|
|
237
|
+
finish();
|
|
238
|
+
return;
|
|
239
|
+
}
|
|
240
|
+
log("aibexd", "stopping");
|
|
241
|
+
child.kill("SIGTERM");
|
|
242
|
+
const stopped = child;
|
|
243
|
+
killTimer = setTimeout(() => {
|
|
244
|
+
stopped.kill("SIGKILL");
|
|
245
|
+
}, timings.supervisorStopMs);
|
|
246
|
+
};
|
|
247
|
+
process.on("SIGHUP", onHangup);
|
|
248
|
+
process.on("SIGINT", onStop);
|
|
249
|
+
process.on("SIGTERM", onStop);
|
|
250
|
+
start2();
|
|
251
|
+
});
|
|
252
|
+
}
|
|
253
|
+
const DEFAULT_HOST = "https://aibex-herd.aibex.workers.dev";
|
|
254
|
+
const HOST_FORMS = "an https:// URL, or an http:// URL on localhost or 127.0.0.1";
|
|
255
|
+
const LOCAL_HOSTNAMES = /* @__PURE__ */ new Set(["localhost", "127.0.0.1"]);
|
|
256
|
+
function parseHost(input) {
|
|
257
|
+
const invalid = (why) => ({
|
|
258
|
+
ok: false,
|
|
259
|
+
error: `Invalid host "${input}": ${why}. Use ${HOST_FORMS}.`
|
|
260
|
+
});
|
|
261
|
+
let url;
|
|
262
|
+
try {
|
|
263
|
+
url = new URL(input);
|
|
264
|
+
} catch {
|
|
265
|
+
return invalid("not a URL");
|
|
266
|
+
}
|
|
267
|
+
if (url.protocol === "http:") {
|
|
268
|
+
if (!LOCAL_HOSTNAMES.has(url.hostname)) {
|
|
269
|
+
return invalid("http is only accepted on localhost or 127.0.0.1");
|
|
270
|
+
}
|
|
271
|
+
} else if (url.protocol !== "https:") {
|
|
272
|
+
return invalid(`${url.protocol} is not http or https`);
|
|
273
|
+
}
|
|
274
|
+
if (url.username || url.password) return invalid("it has credentials");
|
|
275
|
+
if (url.pathname !== "/" || url.search || url.hash) {
|
|
276
|
+
return invalid("it has a path, query or fragment");
|
|
277
|
+
}
|
|
278
|
+
return { ok: true, host: url.origin };
|
|
279
|
+
}
|
|
280
|
+
function chooseHost(flag, remembered) {
|
|
281
|
+
if (flag !== void 0) return parseHost(flag);
|
|
282
|
+
if (remembered !== void 0) return parseHost(remembered);
|
|
283
|
+
return { ok: true, host: DEFAULT_HOST };
|
|
284
|
+
}
|
|
285
|
+
const PACKAGE_ROOT = path.resolve(
|
|
286
|
+
path.dirname(fileURLToPath(import.meta.url)),
|
|
287
|
+
".."
|
|
288
|
+
);
|
|
289
|
+
function readInstalledVersion(root = PACKAGE_ROOT) {
|
|
290
|
+
const manifest = JSON.parse(
|
|
291
|
+
readFileSync(path.join(root, "package.json"), "utf8")
|
|
292
|
+
);
|
|
293
|
+
if (typeof manifest.version !== "string") {
|
|
294
|
+
throw new TypeError(`${root}/package.json has no version`);
|
|
295
|
+
}
|
|
296
|
+
return manifest.version;
|
|
297
|
+
}
|
|
298
|
+
function readConfig(paths) {
|
|
299
|
+
const value = readJsonSync(paths.configFile);
|
|
300
|
+
if (typeof value !== "object" || value === null) return {};
|
|
301
|
+
const host = value.host;
|
|
302
|
+
return typeof host === "string" ? { host } : {};
|
|
303
|
+
}
|
|
304
|
+
function writeConfig(paths, config) {
|
|
305
|
+
writeJsonAtomicSync(paths.configFile, config);
|
|
306
|
+
}
|
|
307
|
+
const DEFAULT_TIMINGS = {
|
|
308
|
+
backoffInitialMs: 1e3,
|
|
309
|
+
backoffMaxMs: 5 * 6e4,
|
|
310
|
+
backoffResetMs: 10 * 6e4,
|
|
311
|
+
hungMs: 9e4,
|
|
312
|
+
linkStopMs: 1e4,
|
|
313
|
+
supervisorStopMs: 12e3,
|
|
314
|
+
versionCheckMs: 5 * 6e4,
|
|
315
|
+
stopWaitMs: 15e3,
|
|
316
|
+
updateCheckMs: 60 * 6e4,
|
|
317
|
+
attemptInitialMs: 15 * 6e4,
|
|
318
|
+
attemptMaxMs: 6 * 60 * 6e4,
|
|
319
|
+
probationMs: 2 * 6e4,
|
|
320
|
+
readyTimeoutMs: 3e4,
|
|
321
|
+
selfTestMs: 3e4,
|
|
322
|
+
npmViewMs: 6e4,
|
|
323
|
+
npmInstallMs: 10 * 6e4,
|
|
324
|
+
aibexdCheckMs: 24 * 60 * 6e4,
|
|
325
|
+
updateWaitMs: 15 * 6e4
|
|
326
|
+
};
|
|
327
|
+
function readTimings(env = process.env) {
|
|
328
|
+
const timings = { ...DEFAULT_TIMINGS };
|
|
329
|
+
const raw = env.AIBEX_TIMINGS;
|
|
330
|
+
if (!raw) return timings;
|
|
331
|
+
let overrides;
|
|
332
|
+
try {
|
|
333
|
+
overrides = JSON.parse(raw);
|
|
334
|
+
} catch {
|
|
335
|
+
return timings;
|
|
336
|
+
}
|
|
337
|
+
if (typeof overrides !== "object" || overrides === null) return timings;
|
|
338
|
+
for (const key of Object.keys(timings)) {
|
|
339
|
+
const value = overrides[key];
|
|
340
|
+
if (typeof value === "number" && Number.isFinite(value) && value > 0) {
|
|
341
|
+
timings[key] = value;
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
return timings;
|
|
345
|
+
}
|
|
346
|
+
async function start(hostFlag, env = process.env) {
|
|
347
|
+
const paths = statePaths(aibexHome(env));
|
|
348
|
+
let requested;
|
|
349
|
+
if (hostFlag !== void 0) {
|
|
350
|
+
const parsed = parseHost(hostFlag);
|
|
351
|
+
if (!parsed.ok) {
|
|
352
|
+
error(parsed.error);
|
|
353
|
+
return 1;
|
|
354
|
+
}
|
|
355
|
+
requested = parsed.host;
|
|
356
|
+
}
|
|
357
|
+
const running = readPidState(paths);
|
|
358
|
+
if (running.state === "running") {
|
|
359
|
+
const { pid, host: host2 } = running.record;
|
|
360
|
+
if (requested !== void 0 && requested !== host2) {
|
|
361
|
+
error(
|
|
362
|
+
`aibexd is already running against ${host2} (pid ${pid}). Run \`aibexd stop\` first, then \`aibexd start --host ${requested}\`.`
|
|
363
|
+
);
|
|
364
|
+
return 1;
|
|
365
|
+
}
|
|
366
|
+
out(`aibexd is already running against ${host2} (pid ${pid})`);
|
|
367
|
+
return 0;
|
|
368
|
+
}
|
|
369
|
+
const chosen = chooseHost(requested, readConfig(paths).host);
|
|
370
|
+
if (!chosen.ok) {
|
|
371
|
+
error(`${chosen.error} Pass --host to replace the remembered host.`);
|
|
372
|
+
return 1;
|
|
373
|
+
}
|
|
374
|
+
const host = chosen.host;
|
|
375
|
+
writeConfig(paths, { host });
|
|
376
|
+
const hostDirectoryPath = hostDirectory(paths, host);
|
|
377
|
+
mkdirSync(hostDirectoryPath, { recursive: true });
|
|
378
|
+
writePidFile(paths, {
|
|
379
|
+
pid: process.pid,
|
|
380
|
+
host,
|
|
381
|
+
startedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
382
|
+
});
|
|
383
|
+
const removeOwnPidFile = () => removePidFile(paths, process.pid);
|
|
384
|
+
process.on("exit", removeOwnPidFile);
|
|
385
|
+
out(
|
|
386
|
+
`aibexd ${readInstalledVersion()} starting against ${host} (pid ${process.pid}, state in ${paths.home})`
|
|
387
|
+
);
|
|
388
|
+
const supervisorEnvironment = {
|
|
389
|
+
...env,
|
|
390
|
+
AIBEX_HOME: paths.home,
|
|
391
|
+
AIBEX_HOST: host,
|
|
392
|
+
AIBEX_HOST_DIR: hostDirectoryPath
|
|
393
|
+
};
|
|
394
|
+
if (env.AIBEX_LINK_PATH) {
|
|
395
|
+
supervisorEnvironment.AIBEX_LINK_PATH = path.resolve(env.AIBEX_LINK_PATH);
|
|
396
|
+
out(`using the local link at ${supervisorEnvironment.AIBEX_LINK_PATH}`);
|
|
397
|
+
}
|
|
398
|
+
return runOuterLoop({
|
|
399
|
+
env: supervisorEnvironment,
|
|
400
|
+
timings: readTimings(env),
|
|
401
|
+
onExit: removeOwnPidFile
|
|
402
|
+
});
|
|
403
|
+
}
|
|
404
|
+
function readSupervisorState(paths) {
|
|
405
|
+
const value = readJsonSync(paths.stateFile);
|
|
406
|
+
if (typeof value !== "object" || value === null) return void 0;
|
|
407
|
+
const state = value;
|
|
408
|
+
if (typeof state.supervisor?.pid !== "number" || typeof state.link !== "object" || !Array.isArray(state.warnings)) {
|
|
409
|
+
return void 0;
|
|
410
|
+
}
|
|
411
|
+
return state;
|
|
412
|
+
}
|
|
413
|
+
function collectStatus(env = process.env) {
|
|
414
|
+
const paths = statePaths(aibexHome(env));
|
|
415
|
+
const installed = readInstalledVersion();
|
|
416
|
+
const pidState = readPidState(paths);
|
|
417
|
+
if (pidState.state !== "running") {
|
|
418
|
+
return {
|
|
419
|
+
running: false,
|
|
420
|
+
pid: null,
|
|
421
|
+
host: readConfig(paths).host ?? null,
|
|
422
|
+
home: paths.home,
|
|
423
|
+
aibexd: { installed, running: null },
|
|
424
|
+
supervisor: { running: false, pid: null, version: null },
|
|
425
|
+
link: {
|
|
426
|
+
running: false,
|
|
427
|
+
pid: null,
|
|
428
|
+
version: null,
|
|
429
|
+
source: null,
|
|
430
|
+
path: null,
|
|
431
|
+
readyAt: null,
|
|
432
|
+
lastAliveAt: null,
|
|
433
|
+
reason: "aibexd is not running"
|
|
434
|
+
},
|
|
435
|
+
versions: null,
|
|
436
|
+
warnings: []
|
|
437
|
+
};
|
|
438
|
+
}
|
|
439
|
+
const state = readSupervisorState(paths);
|
|
440
|
+
const supervisorRunning = state !== void 0 && isProcessAlive(state.supervisor.pid);
|
|
441
|
+
const link = state?.link;
|
|
442
|
+
const linkRunning = supervisorRunning && link?.status === "running" && link.pid !== null && isProcessAlive(link.pid);
|
|
443
|
+
let reason = null;
|
|
444
|
+
if (!supervisorRunning) {
|
|
445
|
+
reason = "the supervisor is not running";
|
|
446
|
+
} else if (!linkRunning) {
|
|
447
|
+
reason = link?.reason ?? "the link is not running";
|
|
448
|
+
}
|
|
449
|
+
return {
|
|
450
|
+
running: true,
|
|
451
|
+
pid: pidState.record.pid,
|
|
452
|
+
host: pidState.record.host,
|
|
453
|
+
home: paths.home,
|
|
454
|
+
aibexd: {
|
|
455
|
+
installed,
|
|
456
|
+
running: supervisorRunning ? state?.supervisor.version ?? null : null
|
|
457
|
+
},
|
|
458
|
+
supervisor: {
|
|
459
|
+
running: supervisorRunning,
|
|
460
|
+
pid: supervisorRunning ? state?.supervisor.pid ?? null : null,
|
|
461
|
+
version: supervisorRunning ? state?.supervisor.version ?? null : null
|
|
462
|
+
},
|
|
463
|
+
link: {
|
|
464
|
+
running: linkRunning,
|
|
465
|
+
pid: linkRunning ? link?.pid ?? null : null,
|
|
466
|
+
version: link?.version ?? null,
|
|
467
|
+
source: link?.source ?? null,
|
|
468
|
+
path: link?.path ?? null,
|
|
469
|
+
readyAt: linkRunning ? link?.readyAt ?? null : null,
|
|
470
|
+
lastAliveAt: linkRunning ? link?.lastAliveAt ?? null : null,
|
|
471
|
+
reason
|
|
472
|
+
},
|
|
473
|
+
versions: supervisorRunning && state?.versions ? {
|
|
474
|
+
current: state.versions.current,
|
|
475
|
+
previous: state.versions.previous,
|
|
476
|
+
bad: state.versions.bad
|
|
477
|
+
} : null,
|
|
478
|
+
warnings: supervisorRunning ? state?.warnings ?? [] : []
|
|
479
|
+
};
|
|
480
|
+
}
|
|
481
|
+
function formatStatus(status2) {
|
|
482
|
+
if (!status2.running) {
|
|
483
|
+
return [
|
|
484
|
+
"aibexd: stopped",
|
|
485
|
+
`host: ${status2.host ?? "none recorded"} (last used)`,
|
|
486
|
+
`aibexd version: ${status2.aibexd.installed} installed`,
|
|
487
|
+
`state: ${status2.home}`
|
|
488
|
+
];
|
|
489
|
+
}
|
|
490
|
+
const lines = [
|
|
491
|
+
`aibexd: running (pid ${status2.pid})`,
|
|
492
|
+
`host: ${status2.host}`,
|
|
493
|
+
`aibexd version: ${status2.aibexd.installed} installed, ${status2.aibexd.running ?? "none"} running`,
|
|
494
|
+
status2.supervisor.running ? `supervisor: running (pid ${status2.supervisor.pid})` : "supervisor: not running (aibexd is restarting it)"
|
|
495
|
+
];
|
|
496
|
+
const { link } = status2;
|
|
497
|
+
let from = "";
|
|
498
|
+
if (link.source === "local" && link.path) {
|
|
499
|
+
from = `, local build at ${link.path}`;
|
|
500
|
+
} else if (link.source === "installed" && link.path) {
|
|
501
|
+
from = `, installed at ${link.path}`;
|
|
502
|
+
}
|
|
503
|
+
lines.push(
|
|
504
|
+
link.running ? `link: aibex-link ${link.version} running (pid ${link.pid}${from})` : `link: not running: ${link.reason}`,
|
|
505
|
+
`state: ${status2.home}`
|
|
506
|
+
);
|
|
507
|
+
if (status2.versions) {
|
|
508
|
+
const { current, previous, bad } = status2.versions;
|
|
509
|
+
const badVersions = Object.keys(bad);
|
|
510
|
+
lines.push(
|
|
511
|
+
`link versions: ${current ?? "none"} current${previous ? `, ${previous} previous` : ""}${badVersions.length > 0 ? `, ${badVersions.join(", ")} bad` : ""}`
|
|
512
|
+
);
|
|
513
|
+
}
|
|
514
|
+
if (status2.warnings.length === 0) {
|
|
515
|
+
lines.push("warnings: none");
|
|
516
|
+
} else {
|
|
517
|
+
lines.push("warnings:");
|
|
518
|
+
for (const warning of status2.warnings) lines.push(` - ${warning.message}`);
|
|
519
|
+
}
|
|
520
|
+
return lines;
|
|
521
|
+
}
|
|
522
|
+
function status(json, env = process.env) {
|
|
523
|
+
const current = collectStatus(env);
|
|
524
|
+
if (json) {
|
|
525
|
+
out(JSON.stringify(current));
|
|
526
|
+
} else {
|
|
527
|
+
for (const line of formatStatus(current)) out(line);
|
|
528
|
+
}
|
|
529
|
+
return 0;
|
|
530
|
+
}
|
|
531
|
+
const POLL_MS = 100;
|
|
532
|
+
async function stop(env = process.env) {
|
|
533
|
+
const paths = statePaths(aibexHome(env));
|
|
534
|
+
const running = readPidState(paths);
|
|
535
|
+
if (running.state !== "running") {
|
|
536
|
+
out("aibexd was not running");
|
|
537
|
+
return 0;
|
|
538
|
+
}
|
|
539
|
+
const { pid } = running.record;
|
|
540
|
+
const linkPid = readSupervisorState(paths)?.link.pid ?? void 0;
|
|
541
|
+
try {
|
|
542
|
+
process.kill(pid, "SIGTERM");
|
|
543
|
+
} catch {
|
|
544
|
+
out("aibexd was not running");
|
|
545
|
+
return 0;
|
|
546
|
+
}
|
|
547
|
+
const deadline = Date.now() + readTimings(env).stopWaitMs;
|
|
548
|
+
const pending = () => [pid, linkPid].find(
|
|
549
|
+
(candidate) => candidate !== void 0 && isProcessAlive(candidate)
|
|
550
|
+
);
|
|
551
|
+
while (pending() !== void 0 && Date.now() < deadline) {
|
|
552
|
+
await setTimeout$1(POLL_MS);
|
|
553
|
+
}
|
|
554
|
+
const still = pending();
|
|
555
|
+
if (still !== void 0) {
|
|
556
|
+
error(
|
|
557
|
+
`aibexd did not stop in time: pid ${still} is still running${still === pid ? "" : " (the link)"}`
|
|
558
|
+
);
|
|
559
|
+
return 1;
|
|
560
|
+
}
|
|
561
|
+
out(`aibexd stopped (was pid ${pid})`);
|
|
562
|
+
return 0;
|
|
563
|
+
}
|
|
564
|
+
const MAX_SOCKET_PATH_BYTES = 100;
|
|
565
|
+
function socketAddress(socketFile) {
|
|
566
|
+
if (Buffer.byteLength(socketFile) <= MAX_SOCKET_PATH_BYTES) return socketFile;
|
|
567
|
+
process.chdir(path.dirname(socketFile));
|
|
568
|
+
return path.basename(socketFile);
|
|
569
|
+
}
|
|
570
|
+
function requestSupervisor(socketFile, request, timeoutMs) {
|
|
571
|
+
return new Promise((resolve) => {
|
|
572
|
+
let buffer = "";
|
|
573
|
+
let settled = false;
|
|
574
|
+
const finish = (result) => {
|
|
575
|
+
if (settled) return;
|
|
576
|
+
settled = true;
|
|
577
|
+
clearTimeout(timer);
|
|
578
|
+
connection.destroy();
|
|
579
|
+
resolve(result);
|
|
580
|
+
};
|
|
581
|
+
let address;
|
|
582
|
+
try {
|
|
583
|
+
address = socketAddress(socketFile);
|
|
584
|
+
} catch {
|
|
585
|
+
resolve({ ok: false, reason: "not-listening" });
|
|
586
|
+
return;
|
|
587
|
+
}
|
|
588
|
+
const connection = createConnection(address);
|
|
589
|
+
const timer = setTimeout(
|
|
590
|
+
() => finish({ ok: false, reason: "timeout" }),
|
|
591
|
+
timeoutMs
|
|
592
|
+
);
|
|
593
|
+
connection.setEncoding("utf8");
|
|
594
|
+
connection.on("connect", () => {
|
|
595
|
+
connection.write(`${JSON.stringify(request)}
|
|
596
|
+
`);
|
|
597
|
+
});
|
|
598
|
+
connection.on("data", (chunk) => {
|
|
599
|
+
buffer += chunk;
|
|
600
|
+
});
|
|
601
|
+
connection.on("end", () => {
|
|
602
|
+
try {
|
|
603
|
+
finish({
|
|
604
|
+
ok: true,
|
|
605
|
+
response: JSON.parse(buffer.trim())
|
|
606
|
+
});
|
|
607
|
+
} catch {
|
|
608
|
+
finish({ ok: false, reason: "bad-response" });
|
|
609
|
+
}
|
|
610
|
+
});
|
|
611
|
+
connection.on(
|
|
612
|
+
"error",
|
|
613
|
+
() => finish({ ok: false, reason: "not-listening" })
|
|
614
|
+
);
|
|
615
|
+
});
|
|
616
|
+
}
|
|
617
|
+
function describeReport(report) {
|
|
618
|
+
switch (report.kind) {
|
|
619
|
+
case "switched": {
|
|
620
|
+
return {
|
|
621
|
+
ok: true,
|
|
622
|
+
message: report.from ? `aibex-link updated from ${report.from} to ${report.to}` : `aibex-link ${report.to} installed and running`
|
|
623
|
+
};
|
|
624
|
+
}
|
|
625
|
+
case "up-to-date": {
|
|
626
|
+
return {
|
|
627
|
+
ok: true,
|
|
628
|
+
message: `aibex-link ${report.version} is up to date`
|
|
629
|
+
};
|
|
630
|
+
}
|
|
631
|
+
case "bad": {
|
|
632
|
+
return {
|
|
633
|
+
ok: false,
|
|
634
|
+
message: `aibex-link ${report.version} is the latest, but it will not be run: ${report.reason}`
|
|
635
|
+
};
|
|
636
|
+
}
|
|
637
|
+
case "failed": {
|
|
638
|
+
return {
|
|
639
|
+
ok: false,
|
|
640
|
+
message: `could not update aibex-link${report.version ? ` to ${report.version}` : ""}: ${report.reason}`
|
|
641
|
+
};
|
|
642
|
+
}
|
|
643
|
+
case "rolled-back": {
|
|
644
|
+
return {
|
|
645
|
+
ok: false,
|
|
646
|
+
message: `aibex-link ${report.from} was rolled back to ${report.to}: ${report.reason}`
|
|
647
|
+
};
|
|
648
|
+
}
|
|
649
|
+
}
|
|
650
|
+
}
|
|
651
|
+
async function update(env = process.env) {
|
|
652
|
+
const paths = statePaths(aibexHome(env));
|
|
653
|
+
if (readPidState(paths).state !== "running") {
|
|
654
|
+
error("aibexd is not running. Run `aibexd start` first.");
|
|
655
|
+
return 1;
|
|
656
|
+
}
|
|
657
|
+
const timings = readTimings(env);
|
|
658
|
+
out("checking npm for a newer aibex-link...");
|
|
659
|
+
const result = await requestSupervisor(
|
|
660
|
+
paths.socketFile,
|
|
661
|
+
{ type: "update" },
|
|
662
|
+
timings.updateWaitMs
|
|
663
|
+
);
|
|
664
|
+
if (!result.ok) {
|
|
665
|
+
switch (result.reason) {
|
|
666
|
+
case "not-listening": {
|
|
667
|
+
error(
|
|
668
|
+
"the aibexd supervisor is not answering (it may be restarting). Try again in a moment, or see `aibexd status`."
|
|
669
|
+
);
|
|
670
|
+
break;
|
|
671
|
+
}
|
|
672
|
+
case "timeout": {
|
|
673
|
+
error(
|
|
674
|
+
`no answer from the aibexd supervisor in ${formatDuration(timings.updateWaitMs)}; the update may still finish. See \`aibexd status\`.`
|
|
675
|
+
);
|
|
676
|
+
break;
|
|
677
|
+
}
|
|
678
|
+
case "bad-response": {
|
|
679
|
+
error("the aibexd supervisor gave an answer this aibexd cannot read.");
|
|
680
|
+
break;
|
|
681
|
+
}
|
|
682
|
+
}
|
|
683
|
+
return 1;
|
|
684
|
+
}
|
|
685
|
+
const { response } = result;
|
|
686
|
+
if (!response.ok) {
|
|
687
|
+
error(response.error);
|
|
688
|
+
return 1;
|
|
689
|
+
}
|
|
690
|
+
const described = describeReport(response.report);
|
|
691
|
+
if (described.ok) {
|
|
692
|
+
out(described.message);
|
|
693
|
+
return 0;
|
|
694
|
+
}
|
|
695
|
+
error(described.message);
|
|
696
|
+
return 1;
|
|
697
|
+
}
|
|
698
|
+
const USAGE = `Usage: aibexd <command> [options]
|
|
699
|
+
|
|
700
|
+
Commands:
|
|
701
|
+
start [--host <url>] Run aibexd in the foreground (Ctrl-C to stop)
|
|
702
|
+
stop Stop the running aibexd
|
|
703
|
+
restart Restart the supervisor and link from the installed code
|
|
704
|
+
status [--json] Show whether aibexd and its link are running
|
|
705
|
+
update Check npm for a newer aibex-link now and switch to it
|
|
706
|
+
|
|
707
|
+
Options:
|
|
708
|
+
--version Print the installed aibexd version
|
|
709
|
+
-h, --help Show this help`;
|
|
710
|
+
async function main(argv) {
|
|
711
|
+
let parsed;
|
|
712
|
+
try {
|
|
713
|
+
parsed = parseArgs({
|
|
714
|
+
args: argv,
|
|
715
|
+
allowPositionals: true,
|
|
716
|
+
options: {
|
|
717
|
+
version: { type: "boolean" },
|
|
718
|
+
help: { type: "boolean", short: "h" },
|
|
719
|
+
host: { type: "string" },
|
|
720
|
+
json: { type: "boolean" }
|
|
721
|
+
}
|
|
722
|
+
});
|
|
723
|
+
} catch (error_) {
|
|
724
|
+
error(error_.message);
|
|
725
|
+
error(USAGE);
|
|
726
|
+
return 2;
|
|
727
|
+
}
|
|
728
|
+
const { values, positionals } = parsed;
|
|
729
|
+
if (values.version) {
|
|
730
|
+
out(readInstalledVersion());
|
|
731
|
+
return 0;
|
|
732
|
+
}
|
|
733
|
+
if (values.help) {
|
|
734
|
+
out(USAGE);
|
|
735
|
+
return 0;
|
|
736
|
+
}
|
|
737
|
+
const [command, ...rest] = positionals;
|
|
738
|
+
if (rest.length > 0) {
|
|
739
|
+
error(`Unexpected arguments: ${rest.join(" ")}`);
|
|
740
|
+
return 2;
|
|
741
|
+
}
|
|
742
|
+
if (values.host !== void 0 && command !== "start") {
|
|
743
|
+
error("--host is only accepted by `aibexd start`");
|
|
744
|
+
return 2;
|
|
745
|
+
}
|
|
746
|
+
if (values.json && command !== "status") {
|
|
747
|
+
error("--json is only accepted by `aibexd status`");
|
|
748
|
+
return 2;
|
|
749
|
+
}
|
|
750
|
+
switch (command) {
|
|
751
|
+
case "start": {
|
|
752
|
+
return start(values.host);
|
|
753
|
+
}
|
|
754
|
+
case "stop": {
|
|
755
|
+
return stop();
|
|
756
|
+
}
|
|
757
|
+
case "restart": {
|
|
758
|
+
return restart();
|
|
759
|
+
}
|
|
760
|
+
case "status": {
|
|
761
|
+
return status(values.json ?? false);
|
|
762
|
+
}
|
|
763
|
+
case "update": {
|
|
764
|
+
return update();
|
|
765
|
+
}
|
|
766
|
+
case void 0: {
|
|
767
|
+
error(USAGE);
|
|
768
|
+
return 2;
|
|
769
|
+
}
|
|
770
|
+
default: {
|
|
771
|
+
error(`Unknown command: ${command}`);
|
|
772
|
+
error(USAGE);
|
|
773
|
+
return 2;
|
|
774
|
+
}
|
|
775
|
+
}
|
|
776
|
+
}
|
|
777
|
+
process.exitCode = await main(process.argv.slice(2));
|