@rynx-ai/cli 0.1.11-beta.40 → 0.1.11-beta.42
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/commands/lifecycle.js +3 -1
- package/dist/systemd-service.d.ts +10 -5
- package/dist/systemd-service.js +83 -36
- package/package.json +7 -7
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { startStandaloneDaemon, statusStandaloneDaemon, stopStandaloneDaemon, streamStandaloneDaemonLogs, } from "../standalone.js";
|
|
2
2
|
import { refreshAutostart } from "../autostart.js";
|
|
3
|
-
import { assertLinuxSystemdInvocation } from "../systemd-service.js";
|
|
3
|
+
import { assertLinuxSystemdInvocation, stopLinuxServiceIfActive, } from "../systemd-service.js";
|
|
4
4
|
import { fail } from "./errors.js";
|
|
5
5
|
const SYSTEMD_SERVICE_FLAG = "--systemd-service";
|
|
6
6
|
export async function runLifecycleCommand(command, args, options = {}) {
|
|
@@ -24,6 +24,8 @@ export async function runLifecycleCommand(command, args, options = {}) {
|
|
|
24
24
|
return startStandaloneDaemon({ restart: true, quiet: options.quiet });
|
|
25
25
|
}
|
|
26
26
|
case "stop":
|
|
27
|
+
if (stopLinuxServiceIfActive())
|
|
28
|
+
return 0;
|
|
27
29
|
return stopStandaloneDaemon();
|
|
28
30
|
case "status":
|
|
29
31
|
return statusStandaloneDaemon();
|
|
@@ -7,14 +7,13 @@ export interface LinuxSystemdServiceState {
|
|
|
7
7
|
activeState: string;
|
|
8
8
|
subState: string;
|
|
9
9
|
type: string;
|
|
10
|
-
|
|
10
|
+
remainAfterExit: string;
|
|
11
11
|
killMode: string;
|
|
12
12
|
killSignal: string;
|
|
13
13
|
restartKillSignal: string;
|
|
14
14
|
finalKillSignal: string;
|
|
15
15
|
sendSigkill: string;
|
|
16
16
|
workingDirectory: string;
|
|
17
|
-
pidFile: string;
|
|
18
17
|
environment: string;
|
|
19
18
|
execStart: string;
|
|
20
19
|
execStop: string;
|
|
@@ -24,10 +23,10 @@ export interface LinuxPm2GodProcess {
|
|
|
24
23
|
cgroup: string;
|
|
25
24
|
startIdentity: string;
|
|
26
25
|
}
|
|
27
|
-
export type
|
|
26
|
+
export type LinuxPm2GodInspection = {
|
|
28
27
|
kind: "absent";
|
|
29
28
|
} | {
|
|
30
|
-
kind: "
|
|
29
|
+
kind: "service-cgroup";
|
|
31
30
|
processes: LinuxPm2GodProcess[];
|
|
32
31
|
} | {
|
|
33
32
|
kind: "external";
|
|
@@ -47,7 +46,13 @@ export declare function disableLinuxAutostart(): void;
|
|
|
47
46
|
/** Refresh an existing autostart unit without changing enablement. */
|
|
48
47
|
export declare function refreshLinuxServiceIfPresent(): boolean;
|
|
49
48
|
export declare function assertLinuxSystemdInvocation(): void;
|
|
49
|
+
/**
|
|
50
|
+
* Stop through systemd while the oneshot unit owns the lifecycle state. PM2
|
|
51
|
+
* owns its own process tree; systemd only records whether its start hook has
|
|
52
|
+
* completed and provides the matching synchronous ExecStop boundary.
|
|
53
|
+
*/
|
|
54
|
+
export declare function stopLinuxServiceIfActive(): boolean;
|
|
50
55
|
export declare function parseLinuxSystemdShow(output: string): LinuxSystemdServiceState;
|
|
51
|
-
export declare function
|
|
56
|
+
export declare function inspectLinuxPm2GodProcesses(home: string, deps?: LinuxPm2InspectionDeps): LinuxPm2GodInspection;
|
|
52
57
|
export declare function scanLinuxPm2GodPids(home: string, deps?: LinuxPm2InspectionDeps): number[];
|
|
53
58
|
export declare function linuxSystemdCgroupForPid(pid: number, readText?: (file: string) => string): string;
|
package/dist/systemd-service.js
CHANGED
|
@@ -8,6 +8,7 @@ export const RYNX_SYSTEMD_SERVICE = "rynx.service";
|
|
|
8
8
|
export const RYNX_SYSTEMD_SERVICE_ENV = "RYNX_SYSTEMD_SERVICE";
|
|
9
9
|
export const RYNX_SERVICE_ENV_KEYS = "RYNX_SERVICE_ENV_KEYS";
|
|
10
10
|
const COMMAND_TIMEOUT_MS = 5_000;
|
|
11
|
+
const STOP_COMMAND_TIMEOUT_MS = 50_000;
|
|
11
12
|
const AUTOMATIC_SERVICE_ENVIRONMENT = new Set([
|
|
12
13
|
"ALL_PROXY",
|
|
13
14
|
"AWS_CA_BUNDLE",
|
|
@@ -76,7 +77,6 @@ const TRANSIENT_SERVICE_ENVIRONMENT = new Set([
|
|
|
76
77
|
"TMUX_PANE",
|
|
77
78
|
]);
|
|
78
79
|
export function renderSystemdUnit(input) {
|
|
79
|
-
const pidFile = path.join(input.dataDir, "pm2", "pm2.pid");
|
|
80
80
|
const inheritedEnvironment = renderInheritedEnvironment(input.environment);
|
|
81
81
|
return `[Unit]
|
|
82
82
|
Description=Rynx local control plane
|
|
@@ -84,8 +84,8 @@ After=network-online.target
|
|
|
84
84
|
Wants=network-online.target
|
|
85
85
|
|
|
86
86
|
[Service]
|
|
87
|
-
Type=
|
|
88
|
-
|
|
87
|
+
Type=oneshot
|
|
88
|
+
RemainAfterExit=yes
|
|
89
89
|
KillMode=process
|
|
90
90
|
KillSignal=SIGCONT
|
|
91
91
|
RestartKillSignal=SIGCONT
|
|
@@ -125,7 +125,7 @@ export function inspectLinuxAutostart() {
|
|
|
125
125
|
let detail;
|
|
126
126
|
if (active) {
|
|
127
127
|
try {
|
|
128
|
-
|
|
128
|
+
assertActiveServiceLifecycle(context);
|
|
129
129
|
}
|
|
130
130
|
catch (error) {
|
|
131
131
|
detail = errorMessage(error);
|
|
@@ -185,6 +185,62 @@ export function assertLinuxSystemdInvocation() {
|
|
|
185
185
|
throw new Error(`--systemd-service requires the ${RYNX_SYSTEMD_SERVICE} cgroup`);
|
|
186
186
|
}
|
|
187
187
|
}
|
|
188
|
+
/**
|
|
189
|
+
* Stop through systemd while the oneshot unit owns the lifecycle state. PM2
|
|
190
|
+
* owns its own process tree; systemd only records whether its start hook has
|
|
191
|
+
* completed and provides the matching synchronous ExecStop boundary.
|
|
192
|
+
*/
|
|
193
|
+
export function stopLinuxServiceIfActive() {
|
|
194
|
+
if (process.platform !== "linux")
|
|
195
|
+
return false;
|
|
196
|
+
const context = currentContext();
|
|
197
|
+
const inspection = inspectLinuxPm2GodProcesses(context.pm2Home);
|
|
198
|
+
assertSingleGod(inspection);
|
|
199
|
+
if (!linuxUserSystemdAvailable()) {
|
|
200
|
+
const unitMode = installedLinuxUnitMode(context);
|
|
201
|
+
if (unitMode === "legacy-or-unknown" ||
|
|
202
|
+
(unitMode === "absent" && (inspection.kind === "service-cgroup" ||
|
|
203
|
+
currentProcessHasLinuxServiceEvidence()))) {
|
|
204
|
+
throw new Error(`cannot safely stop a legacy or still-loaded ${RYNX_SYSTEMD_SERVICE} without user systemd; ` +
|
|
205
|
+
"retry from a login session where systemctl --user is available");
|
|
206
|
+
}
|
|
207
|
+
return false;
|
|
208
|
+
}
|
|
209
|
+
const state = inspectLinuxSystemdService();
|
|
210
|
+
if (linuxSystemdServiceIsSettledInactive(state))
|
|
211
|
+
return false;
|
|
212
|
+
stopLinuxSystemdServiceAndVerify(context);
|
|
213
|
+
return true;
|
|
214
|
+
}
|
|
215
|
+
function stopLinuxSystemdServiceAndVerify(context) {
|
|
216
|
+
requireSuccess(run("systemctl", ["--user", "stop", RYNX_SYSTEMD_SERVICE], STOP_COMMAND_TIMEOUT_MS), `systemctl --user stop ${RYNX_SYSTEMD_SERVICE}`);
|
|
217
|
+
const remaining = inspectLinuxPm2GodProcesses(context.pm2Home);
|
|
218
|
+
if (remaining.kind !== "absent") {
|
|
219
|
+
throw new Error(`${RYNX_SYSTEMD_SERVICE} stop completed but the PM2 supervisor is still running: ` +
|
|
220
|
+
describeInspection(remaining));
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
function linuxSystemdServiceIsSettledInactive(state) {
|
|
224
|
+
return state.activeState === "inactive" || state.activeState === "failed";
|
|
225
|
+
}
|
|
226
|
+
function installedLinuxUnitMode(context) {
|
|
227
|
+
if (!existsSync(context.unitPath))
|
|
228
|
+
return "absent";
|
|
229
|
+
const content = readFileSync(context.unitPath, "utf8");
|
|
230
|
+
const type = /^\s*Type\s*=\s*([^\s#]+)\s*$/mi.exec(content)?.[1]?.toLowerCase();
|
|
231
|
+
const remains = /^\s*RemainAfterExit\s*=\s*yes\s*$/mi.test(content);
|
|
232
|
+
return type === "oneshot" && remains ? "oneshot" : "legacy-or-unknown";
|
|
233
|
+
}
|
|
234
|
+
function currentProcessHasLinuxServiceEvidence() {
|
|
235
|
+
if (process.env[RYNX_SYSTEMD_SERVICE_ENV] === RYNX_SYSTEMD_SERVICE)
|
|
236
|
+
return true;
|
|
237
|
+
try {
|
|
238
|
+
return cgroupHasService(linuxSystemdCgroupForPid(process.pid));
|
|
239
|
+
}
|
|
240
|
+
catch {
|
|
241
|
+
return false;
|
|
242
|
+
}
|
|
243
|
+
}
|
|
188
244
|
export function parseLinuxSystemdShow(output) {
|
|
189
245
|
const values = new Map();
|
|
190
246
|
for (const line of output.split(/\r?\n/)) {
|
|
@@ -193,28 +249,24 @@ export function parseLinuxSystemdShow(output) {
|
|
|
193
249
|
continue;
|
|
194
250
|
values.set(line.slice(0, separator), line.slice(separator + 1));
|
|
195
251
|
}
|
|
196
|
-
const parsedMainPid = Number.parseInt(values.get("MainPID") ?? "0", 10);
|
|
197
252
|
return {
|
|
198
253
|
loadState: values.get("LoadState") ?? "",
|
|
199
254
|
activeState: values.get("ActiveState") ?? "",
|
|
200
255
|
subState: values.get("SubState") ?? "",
|
|
201
256
|
type: values.get("Type") ?? "",
|
|
202
|
-
|
|
203
|
-
? parsedMainPid
|
|
204
|
-
: 0,
|
|
257
|
+
remainAfterExit: values.get("RemainAfterExit") ?? "",
|
|
205
258
|
killMode: values.get("KillMode") ?? "",
|
|
206
259
|
killSignal: values.get("KillSignal") ?? "",
|
|
207
260
|
restartKillSignal: values.get("RestartKillSignal") ?? "",
|
|
208
261
|
finalKillSignal: values.get("FinalKillSignal") ?? "",
|
|
209
262
|
sendSigkill: values.get("SendSIGKILL") ?? "",
|
|
210
263
|
workingDirectory: values.get("WorkingDirectory") ?? "",
|
|
211
|
-
pidFile: values.get("PIDFile") ?? "",
|
|
212
264
|
environment: values.get("Environment") ?? "",
|
|
213
265
|
execStart: values.get("ExecStart") ?? "",
|
|
214
266
|
execStop: values.get("ExecStop") ?? "",
|
|
215
267
|
};
|
|
216
268
|
}
|
|
217
|
-
export function
|
|
269
|
+
export function inspectLinuxPm2GodProcesses(home, deps = {}) {
|
|
218
270
|
const readText = deps.readText ?? ((file) => readFileSync(file, "utf8"));
|
|
219
271
|
const entries = scanLinuxPm2GodPids(home, deps);
|
|
220
272
|
const processes = [];
|
|
@@ -240,7 +292,7 @@ export function inspectLinuxPm2GodOwnership(home, deps = {}) {
|
|
|
240
292
|
return { kind: "absent" };
|
|
241
293
|
const external = processes.filter((entry) => !cgroupHasService(entry.cgroup));
|
|
242
294
|
return external.length === 0
|
|
243
|
-
? { kind: "
|
|
295
|
+
? { kind: "service-cgroup", processes }
|
|
244
296
|
: { kind: "external", processes };
|
|
245
297
|
}
|
|
246
298
|
export function scanLinuxPm2GodPids(home, deps = {}) {
|
|
@@ -302,7 +354,6 @@ function currentContext() {
|
|
|
302
354
|
nodePath: process.execPath,
|
|
303
355
|
pathEnv: process.env.PATH || defaultLinuxPath(),
|
|
304
356
|
pm2Home,
|
|
305
|
-
pidFile: path.join(pm2Home, "pm2.pid"),
|
|
306
357
|
unitPath: path.join(homeDir, ".config", "systemd", "user", RYNX_SYSTEMD_SERVICE),
|
|
307
358
|
};
|
|
308
359
|
}
|
|
@@ -343,14 +394,13 @@ function inspectLinuxSystemdService() {
|
|
|
343
394
|
"--property=ActiveState",
|
|
344
395
|
"--property=SubState",
|
|
345
396
|
"--property=Type",
|
|
346
|
-
"--property=
|
|
397
|
+
"--property=RemainAfterExit",
|
|
347
398
|
"--property=KillMode",
|
|
348
399
|
"--property=KillSignal",
|
|
349
400
|
"--property=RestartKillSignal",
|
|
350
401
|
"--property=FinalKillSignal",
|
|
351
402
|
"--property=SendSIGKILL",
|
|
352
403
|
"--property=WorkingDirectory",
|
|
353
|
-
"--property=PIDFile",
|
|
354
404
|
"--property=Environment",
|
|
355
405
|
"--property=ExecStart",
|
|
356
406
|
"--property=ExecStop",
|
|
@@ -361,10 +411,11 @@ function assertStaticServiceDefinition(context, state) {
|
|
|
361
411
|
const errors = [];
|
|
362
412
|
if (state.loadState !== "loaded")
|
|
363
413
|
errors.push(`LoadState=${state.loadState || "(empty)"}`);
|
|
364
|
-
if (state.type !== "
|
|
414
|
+
if (state.type !== "oneshot")
|
|
365
415
|
errors.push(`Type=${state.type || "(empty)"}`);
|
|
366
|
-
if (state.
|
|
367
|
-
errors.push(`
|
|
416
|
+
if (state.remainAfterExit !== "yes") {
|
|
417
|
+
errors.push(`RemainAfterExit=${state.remainAfterExit || "(empty)"}`);
|
|
418
|
+
}
|
|
368
419
|
if (state.workingDirectory !== context.dataDir) {
|
|
369
420
|
errors.push(`WorkingDirectory=${state.workingDirectory || "(empty)"}`);
|
|
370
421
|
}
|
|
@@ -404,35 +455,31 @@ function assertStaticServiceDefinition(context, state) {
|
|
|
404
455
|
throw new Error(`${RYNX_SYSTEMD_SERVICE} effective configuration is invalid: ${errors.join("; ")}`);
|
|
405
456
|
}
|
|
406
457
|
}
|
|
407
|
-
function
|
|
408
|
-
const
|
|
409
|
-
assertSingleGod(
|
|
410
|
-
if (
|
|
411
|
-
throw new Error(
|
|
412
|
-
? "systemd started without a PM2 supervisor"
|
|
413
|
-
: `PM2 supervisor is outside ${RYNX_SYSTEMD_SERVICE}: ${describeOwnership(ownership)}`);
|
|
458
|
+
function assertActiveServiceLifecycle(context) {
|
|
459
|
+
const inspection = inspectLinuxPm2GodProcesses(context.pm2Home);
|
|
460
|
+
assertSingleGod(inspection);
|
|
461
|
+
if (inspection.kind === "absent") {
|
|
462
|
+
throw new Error("systemd started without a PM2 supervisor");
|
|
414
463
|
}
|
|
415
|
-
const process =
|
|
464
|
+
const process = inspection.processes[0];
|
|
416
465
|
const state = inspectLinuxSystemdService();
|
|
417
466
|
assertStaticServiceDefinition(context, state);
|
|
418
467
|
if (state.activeState !== "active" ||
|
|
419
|
-
state.subState !== "
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
`ActiveState=${state.activeState}, SubState=${state.subState}, ` +
|
|
423
|
-
`MainPID=${state.mainPid}, PM2=${process.pid}`);
|
|
468
|
+
state.subState !== "exited") {
|
|
469
|
+
throw new Error(`${RYNX_SYSTEMD_SERVICE} lifecycle is not active: ` +
|
|
470
|
+
`ActiveState=${state.activeState}, SubState=${state.subState}`);
|
|
424
471
|
}
|
|
425
472
|
return process;
|
|
426
473
|
}
|
|
427
|
-
function assertSingleGod(
|
|
428
|
-
if (
|
|
429
|
-
throw new Error(`multiple PM2 supervisors use this RYNX_HOME: ${
|
|
474
|
+
function assertSingleGod(inspection) {
|
|
475
|
+
if (inspection.kind !== "absent" && inspection.processes.length !== 1) {
|
|
476
|
+
throw new Error(`multiple PM2 supervisors use this RYNX_HOME: ${describeInspection(inspection)}`);
|
|
430
477
|
}
|
|
431
478
|
}
|
|
432
|
-
function
|
|
433
|
-
if (
|
|
479
|
+
function describeInspection(inspection) {
|
|
480
|
+
if (inspection.kind === "absent")
|
|
434
481
|
return "absent";
|
|
435
|
-
return
|
|
482
|
+
return inspection.processes
|
|
436
483
|
.map((entry) => `${entry.pid}@${entry.cgroup}`)
|
|
437
484
|
.join(", ");
|
|
438
485
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@rynx-ai/cli",
|
|
3
|
-
"version": "0.1.11-beta.
|
|
3
|
+
"version": "0.1.11-beta.42",
|
|
4
4
|
"repository": {
|
|
5
5
|
"type": "git",
|
|
6
6
|
"url": "git+https://github.com/rynx-ai/rynx.git",
|
|
@@ -51,12 +51,12 @@
|
|
|
51
51
|
"dependencies": {
|
|
52
52
|
"@clack/prompts": "^1.6.0",
|
|
53
53
|
"ws": "^8.21.0",
|
|
54
|
-
"@rynx-ai/
|
|
55
|
-
"@rynx-ai/
|
|
56
|
-
"@rynx-ai/
|
|
57
|
-
"@rynx-ai/
|
|
58
|
-
"@rynx-ai/
|
|
59
|
-
"@rynx-ai/
|
|
54
|
+
"@rynx-ai/daemon": "0.1.11-beta.42",
|
|
55
|
+
"@rynx-ai/emulator": "0.1.11-beta.42",
|
|
56
|
+
"@rynx-ai/core": "0.1.11-beta.42",
|
|
57
|
+
"@rynx-ai/browser-cdp": "0.1.11-beta.42",
|
|
58
|
+
"@rynx-ai/protocol": "0.1.11-beta.42",
|
|
59
|
+
"@rynx-ai/tmux": "0.1.11-beta.42"
|
|
60
60
|
},
|
|
61
61
|
"devDependencies": {
|
|
62
62
|
"@types/ws": "^8.18.1"
|