@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.
@@ -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
- mainPid: number;
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 LinuxPm2GodOwnership = {
26
+ export type LinuxPm2GodInspection = {
28
27
  kind: "absent";
29
28
  } | {
30
- kind: "owned";
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 inspectLinuxPm2GodOwnership(home: string, deps?: LinuxPm2InspectionDeps): LinuxPm2GodOwnership;
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;
@@ -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=forking
88
- PIDFile=${systemdPath(pidFile)}
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
- assertRunningServiceOwnership(context);
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
- mainPid: Number.isSafeInteger(parsedMainPid) && parsedMainPid > 1
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 inspectLinuxPm2GodOwnership(home, deps = {}) {
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: "owned", processes }
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=MainPID",
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 !== "forking")
414
+ if (state.type !== "oneshot")
365
415
  errors.push(`Type=${state.type || "(empty)"}`);
366
- if (state.pidFile !== context.pidFile)
367
- errors.push(`PIDFile=${state.pidFile || "(empty)"}`);
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 assertRunningServiceOwnership(context) {
408
- const ownership = inspectLinuxPm2GodOwnership(context.pm2Home);
409
- assertSingleGod(ownership);
410
- if (ownership.kind !== "owned") {
411
- throw new Error(ownership.kind === "absent"
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 = ownership.processes[0];
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 !== "running" ||
420
- state.mainPid !== process.pid) {
421
- throw new Error(`${RYNX_SYSTEMD_SERVICE} does not own the PM2 supervisor: ` +
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(ownership) {
428
- if (ownership.kind !== "absent" && ownership.processes.length !== 1) {
429
- throw new Error(`multiple PM2 supervisors use this RYNX_HOME: ${describeOwnership(ownership)}`);
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 describeOwnership(ownership) {
433
- if (ownership.kind === "absent")
479
+ function describeInspection(inspection) {
480
+ if (inspection.kind === "absent")
434
481
  return "absent";
435
- return ownership.processes
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.40",
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/browser-cdp": "0.1.11-beta.40",
55
- "@rynx-ai/core": "0.1.11-beta.40",
56
- "@rynx-ai/emulator": "0.1.11-beta.40",
57
- "@rynx-ai/protocol": "0.1.11-beta.40",
58
- "@rynx-ai/tmux": "0.1.11-beta.40",
59
- "@rynx-ai/daemon": "0.1.11-beta.40"
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"