@rynx-ai/cli 0.1.11-beta.49 → 0.1.11-beta.50
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/autostart-environment.d.ts +5 -0
- package/dist/autostart-environment.js +64 -0
- package/dist/autostart-launcher.d.ts +1 -0
- package/dist/autostart-launcher.js +48 -0
- package/dist/autostart.d.ts +3 -1
- package/dist/autostart.js +50 -13
- package/dist/commands/lifecycle.js +8 -1
- package/dist/systemd-service.d.ts +2 -1
- package/dist/systemd-service.js +37 -107
- package/package.json +6 -6
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import type { AutostartRenderInput } from "./autostart.js";
|
|
2
|
+
export declare function renderAutostartEnvironmentFile(input: AutostartRenderInput): string;
|
|
3
|
+
export declare function serviceEnvironmentPath(input: AutostartRenderInput): string;
|
|
4
|
+
export declare function currentShell(): string;
|
|
5
|
+
export declare function autostartLauncherArguments(input: AutostartRenderInput, serviceFlag: string): string[];
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import { readlinkSync } from "node:fs";
|
|
2
|
+
import { userInfo } from "node:os";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
// These describe a particular service invocation or IPC connection. The new
|
|
5
|
+
// service/Node process supplies its own values; all other variables are captured.
|
|
6
|
+
const BLOCKED_SERVICE_ENVIRONMENT = new Set([
|
|
7
|
+
"INVOCATION_ID",
|
|
8
|
+
"JOURNAL_STREAM",
|
|
9
|
+
"LISTEN_FDS",
|
|
10
|
+
"LISTEN_FDNAMES",
|
|
11
|
+
"LISTEN_PID",
|
|
12
|
+
"MAINPID",
|
|
13
|
+
"MANAGERPID",
|
|
14
|
+
"NOTIFY_SOCKET",
|
|
15
|
+
"ELECTRON_RUN_AS_NODE",
|
|
16
|
+
"NODE_CHANNEL_FD",
|
|
17
|
+
"NODE_UNIQUE_ID",
|
|
18
|
+
"PM2_HOME",
|
|
19
|
+
"RYNX_DAEMON_LIFECYCLE",
|
|
20
|
+
"RYNX_HOME",
|
|
21
|
+
"RYNX_REFRESH_LOGIN_SHELL_PATH",
|
|
22
|
+
"RYNX_SYSTEMD_SERVICE",
|
|
23
|
+
"RYNX_LAUNCHD_SERVICE",
|
|
24
|
+
"XPC_SERVICE_NAME",
|
|
25
|
+
"SYSTEMD_EXEC_PID",
|
|
26
|
+
"SYSTEMD_INVOCATION_ID",
|
|
27
|
+
"WATCHDOG_PID",
|
|
28
|
+
"WATCHDOG_USEC",
|
|
29
|
+
]);
|
|
30
|
+
export function renderAutostartEnvironmentFile(input) {
|
|
31
|
+
const env = { ...input.environment, PATH: input.pathEnv };
|
|
32
|
+
return Object.entries(env)
|
|
33
|
+
.filter(([name, value]) => value !== undefined &&
|
|
34
|
+
/^[A-Za-z_][A-Za-z0-9_]*$/.test(name) &&
|
|
35
|
+
!BLOCKED_SERVICE_ENVIRONMENT.has(name))
|
|
36
|
+
.sort(([left], [right]) => left.localeCompare(right))
|
|
37
|
+
// Preserve literal newlines and quote metacharacters. The launcher parses
|
|
38
|
+
// this as data; it must not execute the assignments in a shell.
|
|
39
|
+
.map(([name, value]) => `${name}="${value.replace(/[\\"$`]/g, "\\$&")}"`)
|
|
40
|
+
.join("\n") + "\n";
|
|
41
|
+
}
|
|
42
|
+
export function serviceEnvironmentPath(input) {
|
|
43
|
+
return path.join(input.dataDir, "autostart.env");
|
|
44
|
+
}
|
|
45
|
+
export function currentShell() {
|
|
46
|
+
try {
|
|
47
|
+
const parent = readlinkSync(`/proc/${process.ppid}/exe`);
|
|
48
|
+
if (/^(?:ba|z|fi|da|k|mk|c|tc)?sh$/.test(path.basename(parent)))
|
|
49
|
+
return parent;
|
|
50
|
+
}
|
|
51
|
+
catch {
|
|
52
|
+
// The caller may be a launcher, or /proc may be unavailable.
|
|
53
|
+
}
|
|
54
|
+
return process.env.SHELL || userInfo().shell || "/bin/sh";
|
|
55
|
+
}
|
|
56
|
+
export function autostartLauncherArguments(input, serviceFlag) {
|
|
57
|
+
return [
|
|
58
|
+
path.join(path.dirname(input.cliPath), "autostart-launcher.js"),
|
|
59
|
+
input.shellPath ?? "/bin/sh",
|
|
60
|
+
input.cliPath,
|
|
61
|
+
input.dataDir,
|
|
62
|
+
serviceFlag,
|
|
63
|
+
];
|
|
64
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { spawnSync } from "node:child_process";
|
|
2
|
+
import { readFileSync } from "node:fs";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
// Keep this entry independent of CLI initialization: restore the environment
|
|
5
|
+
// and initialize the user's shell before loading any Rynx runtime code.
|
|
6
|
+
const [shellPath, cliPath, dataDir, serviceFlag] = process.argv.slice(2);
|
|
7
|
+
if (!shellPath || !cliPath || !dataDir ||
|
|
8
|
+
(serviceFlag !== "--systemd-service" && serviceFlag !== "--launchd-service")) {
|
|
9
|
+
throw new Error("invalid autostart launcher arguments");
|
|
10
|
+
}
|
|
11
|
+
const contents = readFileSync(path.join(dataDir, "autostart.env"), "utf8");
|
|
12
|
+
// Parse only the quoted format emitted by renderAutostartEnvironmentFile.
|
|
13
|
+
// Never source assignments: a saved SHELLOPTS/UID may be readonly in a shell.
|
|
14
|
+
const assignments = [...contents.matchAll(/([A-Za-z_][A-Za-z0-9_]*)="((?:\\[\\"$`]|[^"\\])*)"\n/g)];
|
|
15
|
+
if (assignments.map(([assignment]) => assignment).join("") !== contents) {
|
|
16
|
+
throw new Error("invalid autostart environment snapshot");
|
|
17
|
+
}
|
|
18
|
+
const snapshot = Object.fromEntries(assignments.map(([, name = "", value = ""]) => [
|
|
19
|
+
name,
|
|
20
|
+
value.replace(/\\([\\"$`])/g, "$1"),
|
|
21
|
+
]));
|
|
22
|
+
const isCShell = /^(?:t?csh)$/.test(path.basename(shellPath));
|
|
23
|
+
const command = "exec " + [
|
|
24
|
+
"/usr/bin/env",
|
|
25
|
+
`RYNX_HOME=${dataDir}`,
|
|
26
|
+
serviceFlag === "--systemd-service"
|
|
27
|
+
? "RYNX_SYSTEMD_SERVICE=rynx.service"
|
|
28
|
+
: "RYNX_LAUNCHD_SERVICE=ai.rynx.daemon",
|
|
29
|
+
process.execPath,
|
|
30
|
+
cliPath,
|
|
31
|
+
"start",
|
|
32
|
+
serviceFlag,
|
|
33
|
+
].map((value) => {
|
|
34
|
+
const quoted = `'${value.replaceAll("'", "'\\''")}'`;
|
|
35
|
+
return isCShell ? quoted.replaceAll("!", "\\!").replaceAll("\n", "\\\n") : quoted;
|
|
36
|
+
}).join(" ");
|
|
37
|
+
// csh/tcsh require -l on its own. Reading the command from stdin preserves
|
|
38
|
+
// their login AND interactive startup, including configuration guarded by prompt.
|
|
39
|
+
const result = spawnSync(shellPath, isCShell ? ["-l"] : ["-lic", command], {
|
|
40
|
+
env: { ...process.env, ...snapshot },
|
|
41
|
+
stdio: [isCShell ? "pipe" : "inherit", "inherit", "inherit"],
|
|
42
|
+
...(isCShell ? { input: `${command}\n` } : {}),
|
|
43
|
+
});
|
|
44
|
+
// The shell passes its final environment directly to Rynx. Do not merge the
|
|
45
|
+
// snapshot again: that would undo configuration overrides and unset operations.
|
|
46
|
+
if (result.error)
|
|
47
|
+
throw result.error;
|
|
48
|
+
process.exitCode = result.status ?? 1;
|
package/dist/autostart.d.ts
CHANGED
|
@@ -17,11 +17,13 @@ export interface AutostartRenderInput {
|
|
|
17
17
|
logDir: string;
|
|
18
18
|
nodePath: string;
|
|
19
19
|
pathEnv: string;
|
|
20
|
+
shellPath?: string;
|
|
20
21
|
}
|
|
21
22
|
export declare function autostartPlatformLabel(platform?: NodeJS.Platform): string | undefined;
|
|
22
23
|
export declare function inspectAutostart(): AutostartState;
|
|
23
24
|
export declare function enableAutostart(): AutostartState;
|
|
24
25
|
export declare function disableAutostart(): AutostartState;
|
|
25
|
-
/** Refresh
|
|
26
|
+
/** Refresh an existing registration and its environment without enabling a new one. */
|
|
26
27
|
export declare function refreshAutostart(): boolean;
|
|
27
28
|
export declare function renderLaunchAgent(input: AutostartRenderInput): string;
|
|
29
|
+
export declare function assertMacLaunchdInvocation(): void;
|
package/dist/autostart.js
CHANGED
|
@@ -4,9 +4,11 @@ import { homedir, userInfo } from "node:os";
|
|
|
4
4
|
import path from "node:path";
|
|
5
5
|
import { fileURLToPath } from "node:url";
|
|
6
6
|
import { rynxHome } from "@rynx-ai/core";
|
|
7
|
+
import { autostartLauncherArguments, currentShell, renderAutostartEnvironmentFile, serviceEnvironmentPath, } from "./autostart-environment.js";
|
|
7
8
|
import { disableLinuxAutostart, enableLinuxAutostart, inspectLinuxAutostart, refreshLinuxServiceIfPresent, } from "./systemd-service.js";
|
|
8
9
|
export { renderSystemdUnit } from "./systemd-service.js";
|
|
9
10
|
const LAUNCHD_LABEL = "ai.rynx.daemon";
|
|
11
|
+
const LAUNCHD_SERVICE_ENV = "RYNX_LAUNCHD_SERVICE";
|
|
10
12
|
const COMMAND_TIMEOUT_MS = 5_000;
|
|
11
13
|
export function autostartPlatformLabel(platform = process.platform) {
|
|
12
14
|
if (platform === "darwin")
|
|
@@ -44,7 +46,7 @@ export function disableAutostart() {
|
|
|
44
46
|
disableLinuxAutostart();
|
|
45
47
|
return inspectAutostart();
|
|
46
48
|
}
|
|
47
|
-
/** Refresh
|
|
49
|
+
/** Refresh an existing registration and its environment without enabling a new one. */
|
|
48
50
|
export function refreshAutostart() {
|
|
49
51
|
if (process.platform === "darwin") {
|
|
50
52
|
const context = currentContext();
|
|
@@ -68,6 +70,7 @@ function unsupportedState() {
|
|
|
68
70
|
export function renderLaunchAgent(input) {
|
|
69
71
|
const outLog = path.join(input.logDir, "autostart-out.log");
|
|
70
72
|
const errLog = path.join(input.logDir, "autostart-err.log");
|
|
73
|
+
const argv = [input.nodePath, ...autostartLauncherArguments(input, "--launchd-service")];
|
|
71
74
|
return `<?xml version="1.0" encoding="UTF-8"?>
|
|
72
75
|
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
|
73
76
|
<plist version="1.0">
|
|
@@ -76,9 +79,7 @@ export function renderLaunchAgent(input) {
|
|
|
76
79
|
<string>${escapeXml(LAUNCHD_LABEL)}</string>
|
|
77
80
|
<key>ProgramArguments</key>
|
|
78
81
|
<array>
|
|
79
|
-
<string>${escapeXml(
|
|
80
|
-
<string>${escapeXml(input.cliPath)}</string>
|
|
81
|
-
<string>start</string>
|
|
82
|
+
${argv.map((value) => ` <string>${escapeXml(value)}</string>`).join("\n")}
|
|
82
83
|
</array>
|
|
83
84
|
<key>RunAtLoad</key>
|
|
84
85
|
<true/>
|
|
@@ -92,6 +93,8 @@ export function renderLaunchAgent(input) {
|
|
|
92
93
|
<string>${escapeXml(input.pathEnv)}</string>
|
|
93
94
|
<key>RYNX_HOME</key>
|
|
94
95
|
<string>${escapeXml(input.dataDir)}</string>
|
|
96
|
+
<key>${LAUNCHD_SERVICE_ENV}</key>
|
|
97
|
+
<string>${LAUNCHD_LABEL}</string>
|
|
95
98
|
</dict>
|
|
96
99
|
<key>StandardOutPath</key>
|
|
97
100
|
<string>${escapeXml(outLog)}</string>
|
|
@@ -101,6 +104,14 @@ export function renderLaunchAgent(input) {
|
|
|
101
104
|
</plist>
|
|
102
105
|
`;
|
|
103
106
|
}
|
|
107
|
+
export function assertMacLaunchdInvocation() {
|
|
108
|
+
if (process.platform !== "darwin") {
|
|
109
|
+
throw new Error("--launchd-service is only valid on macOS");
|
|
110
|
+
}
|
|
111
|
+
if (process.env[LAUNCHD_SERVICE_ENV] !== LAUNCHD_LABEL) {
|
|
112
|
+
throw new Error("--launchd-service requires the Rynx launchd environment");
|
|
113
|
+
}
|
|
114
|
+
}
|
|
104
115
|
function currentContext() {
|
|
105
116
|
const dataDir = rynxHome();
|
|
106
117
|
const homeDir = homedir();
|
|
@@ -109,8 +120,10 @@ function currentContext() {
|
|
|
109
120
|
dataDir,
|
|
110
121
|
logDir: path.join(dataDir, "logs"),
|
|
111
122
|
cliPath: fileURLToPath(new URL("./cli.js", import.meta.url)),
|
|
123
|
+
environment: process.env,
|
|
112
124
|
nodePath: process.execPath,
|
|
113
125
|
pathEnv: process.env.PATH || defaultPath(),
|
|
126
|
+
shellPath: currentShell(),
|
|
114
127
|
};
|
|
115
128
|
}
|
|
116
129
|
function defaultPath() {
|
|
@@ -142,19 +155,42 @@ function syncMacRegistration(context) {
|
|
|
142
155
|
? readFileSync(registrationPath, "utf8")
|
|
143
156
|
: undefined;
|
|
144
157
|
const content = renderLaunchAgent(context);
|
|
145
|
-
|
|
158
|
+
const changed = previous !== content;
|
|
159
|
+
const environmentPath = serviceEnvironmentPath(context);
|
|
160
|
+
const environment = renderAutostartEnvironmentFile(context);
|
|
161
|
+
const previousEnvironment = existsSync(environmentPath)
|
|
162
|
+
? readFileSync(environmentPath, "utf8")
|
|
163
|
+
: undefined;
|
|
164
|
+
const environmentChanged = previousEnvironment !== environment;
|
|
165
|
+
if (!changed && !environmentChanged)
|
|
146
166
|
return false;
|
|
147
|
-
const wasLoaded = launchctlLoaded();
|
|
148
|
-
|
|
167
|
+
const wasLoaded = changed && launchctlLoaded();
|
|
168
|
+
const restore = () => {
|
|
169
|
+
if (environmentChanged)
|
|
170
|
+
restoreRegistrationFile(environmentPath, previousEnvironment, 0o600);
|
|
171
|
+
if (changed)
|
|
172
|
+
restoreRegistrationFile(registrationPath, previous);
|
|
173
|
+
};
|
|
174
|
+
try {
|
|
175
|
+
if (environmentChanged)
|
|
176
|
+
replaceFileAtomically(environmentPath, environment, 0o600);
|
|
177
|
+
if (changed)
|
|
178
|
+
replaceFileAtomically(registrationPath, content);
|
|
179
|
+
}
|
|
180
|
+
catch (error) {
|
|
181
|
+
restore();
|
|
182
|
+
throw error;
|
|
183
|
+
}
|
|
184
|
+
// An environment-only refresh needs no reload: the job reads it on every run.
|
|
149
185
|
if (!wasLoaded)
|
|
150
186
|
return true;
|
|
151
187
|
if (!launchctlBootout(registrationPath)) {
|
|
152
|
-
|
|
188
|
+
restore();
|
|
153
189
|
throw new Error(`launchctl could not unload ${LAUNCHD_LABEL}; the previous file was restored`);
|
|
154
190
|
}
|
|
155
191
|
if (launchctlBootstrap(registrationPath))
|
|
156
192
|
return true;
|
|
157
|
-
|
|
193
|
+
restore();
|
|
158
194
|
if (previous !== undefined && launchctlBootstrap(registrationPath)) {
|
|
159
195
|
throw new Error(`launchctl could not load the updated registration; the previous job was restored`);
|
|
160
196
|
}
|
|
@@ -166,6 +202,7 @@ function disableMac(context) {
|
|
|
166
202
|
throw new Error(`launchctl could not unload ${LAUNCHD_LABEL}`);
|
|
167
203
|
}
|
|
168
204
|
rmSync(registrationPath, { force: true });
|
|
205
|
+
rmSync(serviceEnvironmentPath(context), { force: true });
|
|
169
206
|
}
|
|
170
207
|
function launchctlLoaded() {
|
|
171
208
|
const uid = userInfo().uid;
|
|
@@ -206,14 +243,14 @@ function escapeXml(value) {
|
|
|
206
243
|
.replaceAll("<", "<")
|
|
207
244
|
.replaceAll(">", ">");
|
|
208
245
|
}
|
|
209
|
-
function replaceFileAtomically(file, content) {
|
|
246
|
+
function replaceFileAtomically(file, content, mode = 0o644) {
|
|
210
247
|
mkdirSync(path.dirname(file), { recursive: true });
|
|
211
248
|
const temporary = `${file}.${process.pid}.${Date.now()}.tmp`;
|
|
212
249
|
try {
|
|
213
250
|
writeFileSync(temporary, content, {
|
|
214
251
|
encoding: "utf8",
|
|
215
252
|
flag: "wx",
|
|
216
|
-
mode
|
|
253
|
+
mode,
|
|
217
254
|
});
|
|
218
255
|
renameSync(temporary, file);
|
|
219
256
|
}
|
|
@@ -221,9 +258,9 @@ function replaceFileAtomically(file, content) {
|
|
|
221
258
|
rmSync(temporary, { force: true });
|
|
222
259
|
}
|
|
223
260
|
}
|
|
224
|
-
function restoreRegistrationFile(file, previous) {
|
|
261
|
+
function restoreRegistrationFile(file, previous, mode = 0o644) {
|
|
225
262
|
if (previous === undefined)
|
|
226
263
|
rmSync(file, { force: true });
|
|
227
264
|
else
|
|
228
|
-
replaceFileAtomically(file, previous);
|
|
265
|
+
replaceFileAtomically(file, previous, mode);
|
|
229
266
|
}
|
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
import { startStandaloneDaemon, statusStandaloneDaemon, stopStandaloneDaemon, streamStandaloneDaemonLogs, } from "../standalone.js";
|
|
2
2
|
import { shutdownResidentDaemonIfIdle, waitForResidentDaemonExit, } from "../control-client.js";
|
|
3
3
|
import { resolveDaemonControlEndpoint } from "../control-endpoint.js";
|
|
4
|
-
import { refreshAutostart } from "../autostart.js";
|
|
4
|
+
import { assertMacLaunchdInvocation, refreshAutostart } from "../autostart.js";
|
|
5
5
|
import { assertLinuxSystemdInvocation } from "../systemd-service.js";
|
|
6
6
|
import { clearLegacyMaintenanceState } from "../legacy-maintenance.js";
|
|
7
7
|
import { fail } from "./errors.js";
|
|
8
8
|
const SYSTEMD_SERVICE_FLAG = "--systemd-service";
|
|
9
|
+
const LAUNCHD_SERVICE_FLAG = "--launchd-service";
|
|
9
10
|
const IF_IDLE_FLAG = "--if-idle";
|
|
10
11
|
export async function runLifecycleCommand(command, args, options = {}) {
|
|
11
12
|
if (args.length === 1 && args[0] === SYSTEMD_SERVICE_FLAG) {
|
|
@@ -16,6 +17,12 @@ export async function runLifecycleCommand(command, args, options = {}) {
|
|
|
16
17
|
return stopStandaloneDaemon();
|
|
17
18
|
fail(`${SYSTEMD_SERVICE_FLAG} is only valid for start and stop`);
|
|
18
19
|
}
|
|
20
|
+
if (args.length === 1 && args[0] === LAUNCHD_SERVICE_FLAG) {
|
|
21
|
+
assertMacLaunchdInvocation();
|
|
22
|
+
if (command === "start")
|
|
23
|
+
return startStandaloneDaemon();
|
|
24
|
+
fail(`${LAUNCHD_SERVICE_FLAG} is only valid for start`);
|
|
25
|
+
}
|
|
19
26
|
const stopIfIdle = command === "stop" && args.length === 1 && args[0] === IF_IDLE_FLAG;
|
|
20
27
|
const statusJson = command === "status" && args.length === 1 && args[0] === "--json";
|
|
21
28
|
if (args.length > 0 && !stopIfIdle && !statusJson) {
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import type { AutostartRenderInput, AutostartState } from "./autostart.js";
|
|
2
|
+
import { renderAutostartEnvironmentFile as renderSystemdEnvironmentFile } from "./autostart-environment.js";
|
|
3
|
+
export { renderSystemdEnvironmentFile };
|
|
2
4
|
export declare const RYNX_SYSTEMD_SERVICE = "rynx.service";
|
|
3
5
|
export declare const RYNX_SYSTEMD_SERVICE_ENV = "RYNX_SYSTEMD_SERVICE";
|
|
4
|
-
export declare const RYNX_SERVICE_ENV_KEYS = "RYNX_SERVICE_ENV_KEYS";
|
|
5
6
|
export interface LinuxSystemdServiceState {
|
|
6
7
|
loadState: string;
|
|
7
8
|
activeState: string;
|
package/dist/systemd-service.js
CHANGED
|
@@ -4,79 +4,13 @@ import { homedir, userInfo } from "node:os";
|
|
|
4
4
|
import path from "node:path";
|
|
5
5
|
import { fileURLToPath } from "node:url";
|
|
6
6
|
import { rynxHome } from "@rynx-ai/core";
|
|
7
|
+
import { autostartLauncherArguments, currentShell, renderAutostartEnvironmentFile as renderSystemdEnvironmentFile, serviceEnvironmentPath, } from "./autostart-environment.js";
|
|
8
|
+
export { renderSystemdEnvironmentFile };
|
|
7
9
|
export const RYNX_SYSTEMD_SERVICE = "rynx.service";
|
|
8
10
|
export const RYNX_SYSTEMD_SERVICE_ENV = "RYNX_SYSTEMD_SERVICE";
|
|
9
|
-
export const RYNX_SERVICE_ENV_KEYS = "RYNX_SERVICE_ENV_KEYS";
|
|
10
11
|
const COMMAND_TIMEOUT_MS = 5_000;
|
|
11
|
-
const AUTOMATIC_SERVICE_ENVIRONMENT = new Set([
|
|
12
|
-
"ALL_PROXY",
|
|
13
|
-
"AWS_CA_BUNDLE",
|
|
14
|
-
"CURL_CA_BUNDLE",
|
|
15
|
-
"GIT_SSL_CAINFO",
|
|
16
|
-
"HTTPS_PROXY",
|
|
17
|
-
"HTTP_PROXY",
|
|
18
|
-
"NODE_EXTRA_CA_CERTS",
|
|
19
|
-
"NO_PROXY",
|
|
20
|
-
"REQUESTS_CA_BUNDLE",
|
|
21
|
-
"SSL_CERT_DIR",
|
|
22
|
-
"SSL_CERT_FILE",
|
|
23
|
-
"all_proxy",
|
|
24
|
-
"https_proxy",
|
|
25
|
-
"http_proxy",
|
|
26
|
-
"no_proxy",
|
|
27
|
-
]);
|
|
28
|
-
const BLOCKED_SERVICE_ENVIRONMENT = new Set([
|
|
29
|
-
"PATH",
|
|
30
|
-
"INVOCATION_ID",
|
|
31
|
-
"JOURNAL_STREAM",
|
|
32
|
-
"LISTEN_FDS",
|
|
33
|
-
"LISTEN_FDNAMES",
|
|
34
|
-
"LISTEN_PID",
|
|
35
|
-
"MAINPID",
|
|
36
|
-
"MANAGERPID",
|
|
37
|
-
"NOTIFY_SOCKET",
|
|
38
|
-
"ELECTRON_RUN_AS_NODE",
|
|
39
|
-
"NODE_CHANNEL_FD",
|
|
40
|
-
"NODE_UNIQUE_ID",
|
|
41
|
-
"PM2_HOME",
|
|
42
|
-
"RYNX_DAEMON_LIFECYCLE",
|
|
43
|
-
"RYNX_HOME",
|
|
44
|
-
"RYNX_REFRESH_LOGIN_SHELL_PATH",
|
|
45
|
-
RYNX_SERVICE_ENV_KEYS,
|
|
46
|
-
RYNX_SYSTEMD_SERVICE_ENV,
|
|
47
|
-
"SYSTEMD_EXEC_PID",
|
|
48
|
-
"SYSTEMD_INVOCATION_ID",
|
|
49
|
-
"WATCHDOG_PID",
|
|
50
|
-
"WATCHDOG_USEC",
|
|
51
|
-
]);
|
|
52
|
-
const TRANSIENT_SERVICE_ENVIRONMENT = new Set([
|
|
53
|
-
"_",
|
|
54
|
-
"CI",
|
|
55
|
-
"CLICOLOR",
|
|
56
|
-
"CLICOLOR_FORCE",
|
|
57
|
-
"CODEX_CI",
|
|
58
|
-
"COLORTERM",
|
|
59
|
-
"FORCE_COLOR",
|
|
60
|
-
"GH_PAGER",
|
|
61
|
-
"GIT_PAGER",
|
|
62
|
-
"NO_COLOR",
|
|
63
|
-
"OLDPWD",
|
|
64
|
-
"PAGER",
|
|
65
|
-
"PWD",
|
|
66
|
-
"SHLVL",
|
|
67
|
-
"SSH_CLIENT",
|
|
68
|
-
"SSH_CONNECTION",
|
|
69
|
-
"SSH_TTY",
|
|
70
|
-
"TERM",
|
|
71
|
-
"TERM_PROGRAM",
|
|
72
|
-
"TERM_PROGRAM_VERSION",
|
|
73
|
-
"TERM_SESSION_ID",
|
|
74
|
-
"TERMINFO",
|
|
75
|
-
"TMUX",
|
|
76
|
-
"TMUX_PANE",
|
|
77
|
-
]);
|
|
78
12
|
export function renderSystemdUnit(input) {
|
|
79
|
-
const
|
|
13
|
+
const argv = [input.nodePath, ...autostartLauncherArguments(input, "--systemd-service")];
|
|
80
14
|
return `[Unit]
|
|
81
15
|
Description=Rynx local control plane
|
|
82
16
|
After=network-online.target
|
|
@@ -93,12 +27,11 @@ SendSIGKILL=no
|
|
|
93
27
|
TimeoutStartSec=60
|
|
94
28
|
TimeoutStopSec=45
|
|
95
29
|
WorkingDirectory=${systemdPath(input.dataDir)}
|
|
96
|
-
${inheritedEnvironment}
|
|
97
30
|
Environment=${systemdQuote(`PATH=${input.pathEnv}`)}
|
|
98
31
|
Environment=${systemdQuote(`RYNX_HOME=${input.dataDir}`)}
|
|
99
32
|
Environment=${systemdQuote(`${RYNX_SYSTEMD_SERVICE_ENV}=${RYNX_SYSTEMD_SERVICE}`)}
|
|
100
|
-
ExecStart
|
|
101
|
-
ExecStop
|
|
33
|
+
ExecStart=:${argv.map(systemdQuote).join(" ")}
|
|
34
|
+
ExecStop=:${systemdQuote(input.nodePath)} ${systemdQuote(input.cliPath)} stop --systemd-service
|
|
102
35
|
|
|
103
36
|
[Install]
|
|
104
37
|
WantedBy=default.target
|
|
@@ -161,16 +94,17 @@ export function disableLinuxAutostart() {
|
|
|
161
94
|
requireSuccess(disabled, "systemctl --user disable");
|
|
162
95
|
}
|
|
163
96
|
rmSync(context.unitPath, { force: true });
|
|
97
|
+
rmSync(serviceEnvironmentPath(context), { force: true });
|
|
164
98
|
requireSuccess(run("systemctl", ["--user", "daemon-reload"]), "systemctl --user daemon-reload");
|
|
165
99
|
}
|
|
166
100
|
/** Refresh an existing autostart unit without changing enablement. */
|
|
167
101
|
export function refreshLinuxServiceIfPresent() {
|
|
168
|
-
if (process.platform !== "linux"
|
|
102
|
+
if (process.platform !== "linux")
|
|
169
103
|
return false;
|
|
170
104
|
const context = currentContext();
|
|
171
105
|
if (!existsSync(context.unitPath))
|
|
172
106
|
return false;
|
|
173
|
-
return syncLinuxService(context);
|
|
107
|
+
return syncLinuxService(context, linuxUserSystemdAvailable());
|
|
174
108
|
}
|
|
175
109
|
export function assertLinuxSystemdInvocation() {
|
|
176
110
|
if (process.platform !== "linux") {
|
|
@@ -296,11 +230,12 @@ function currentContext() {
|
|
|
296
230
|
environment: process.env,
|
|
297
231
|
nodePath: process.execPath,
|
|
298
232
|
pathEnv: process.env.PATH || defaultLinuxPath(),
|
|
233
|
+
shellPath: currentShell(),
|
|
299
234
|
pm2Home,
|
|
300
235
|
unitPath: path.join(homeDir, ".config", "systemd", "user", RYNX_SYSTEMD_SERVICE),
|
|
301
236
|
};
|
|
302
237
|
}
|
|
303
|
-
function syncLinuxService(context) {
|
|
238
|
+
function syncLinuxService(context, reload = true) {
|
|
304
239
|
mkdirSync(context.dataDir, { recursive: true, mode: 0o700 });
|
|
305
240
|
mkdirSync(context.logDir, { recursive: true, mode: 0o700 });
|
|
306
241
|
const content = renderSystemdUnit(context);
|
|
@@ -308,14 +243,30 @@ function syncLinuxService(context) {
|
|
|
308
243
|
? readFileSync(context.unitPath, "utf8")
|
|
309
244
|
: undefined;
|
|
310
245
|
const changed = previous !== content;
|
|
311
|
-
|
|
312
|
-
|
|
246
|
+
const environmentPath = serviceEnvironmentPath(context);
|
|
247
|
+
const environment = renderSystemdEnvironmentFile(context);
|
|
248
|
+
const previousEnvironment = existsSync(environmentPath)
|
|
249
|
+
? readFileSync(environmentPath, "utf8")
|
|
250
|
+
: undefined;
|
|
251
|
+
const environmentChanged = previousEnvironment !== environment;
|
|
313
252
|
try {
|
|
253
|
+
if (environmentChanged)
|
|
254
|
+
replaceFileAtomically(environmentPath, environment);
|
|
255
|
+
if (changed)
|
|
256
|
+
replaceFileAtomically(context.unitPath, content);
|
|
257
|
+
if (!reload)
|
|
258
|
+
return changed || environmentChanged;
|
|
314
259
|
requireSuccess(run("systemctl", ["--user", "daemon-reload"]), "systemctl --user daemon-reload");
|
|
315
260
|
assertStaticServiceDefinition(context, inspectLinuxSystemdService());
|
|
316
|
-
return changed;
|
|
261
|
+
return changed || environmentChanged;
|
|
317
262
|
}
|
|
318
263
|
catch (error) {
|
|
264
|
+
if (environmentChanged) {
|
|
265
|
+
if (previousEnvironment === undefined)
|
|
266
|
+
rmSync(environmentPath, { force: true });
|
|
267
|
+
else
|
|
268
|
+
replaceFileAtomically(environmentPath, previousEnvironment);
|
|
269
|
+
}
|
|
319
270
|
if (changed) {
|
|
320
271
|
if (previous === undefined)
|
|
321
272
|
rmSync(context.unitPath, { force: true });
|
|
@@ -389,6 +340,7 @@ function assertStaticServiceDefinition(context, state) {
|
|
|
389
340
|
]) {
|
|
390
341
|
if (!value.includes(context.nodePath) ||
|
|
391
342
|
!value.includes(context.cliPath) ||
|
|
343
|
+
(action === "start" && !value.includes(autostartLauncherArguments(context, "--systemd-service")[0])) ||
|
|
392
344
|
!value.includes(action) ||
|
|
393
345
|
!value.includes("--systemd-service")) {
|
|
394
346
|
errors.push(`${name}=${value || "(empty)"}`);
|
|
@@ -501,8 +453,7 @@ function replaceFileAtomically(file, content) {
|
|
|
501
453
|
writeFileSync(temporary, content, {
|
|
502
454
|
encoding: "utf8",
|
|
503
455
|
flag: "wx",
|
|
504
|
-
//
|
|
505
|
-
// runtime credentials. Keep the snapshot private to the owning user.
|
|
456
|
+
// Environment snapshots may include runtime credentials.
|
|
506
457
|
mode: 0o600,
|
|
507
458
|
});
|
|
508
459
|
renameSync(temporary, file);
|
|
@@ -539,24 +490,6 @@ function signalMatches(value, number, name) {
|
|
|
539
490
|
function defaultLinuxPath() {
|
|
540
491
|
return "/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin";
|
|
541
492
|
}
|
|
542
|
-
function renderInheritedEnvironment(env) {
|
|
543
|
-
if (!env)
|
|
544
|
-
return "";
|
|
545
|
-
const requested = new Set((env[RYNX_SERVICE_ENV_KEYS] ?? "")
|
|
546
|
-
.split(/[\s,]+/)
|
|
547
|
-
.filter((name) => /^[A-Za-z_][A-Za-z0-9_]*$/.test(name)));
|
|
548
|
-
return Object.entries(env)
|
|
549
|
-
.filter(([name, value]) => value !== undefined &&
|
|
550
|
-
/^[A-Za-z_][A-Za-z0-9_]*$/.test(name) &&
|
|
551
|
-
(AUTOMATIC_SERVICE_ENVIRONMENT.has(name) || requested.has(name)) &&
|
|
552
|
-
!name.startsWith("npm_") &&
|
|
553
|
-
!name.startsWith("RYNX_RUNNER_") &&
|
|
554
|
-
!BLOCKED_SERVICE_ENVIRONMENT.has(name) &&
|
|
555
|
-
!TRANSIENT_SERVICE_ENVIRONMENT.has(name))
|
|
556
|
-
.sort(([left], [right]) => left.localeCompare(right))
|
|
557
|
-
.map(([name, value]) => `Environment=${systemdQuote(`${name}=${value}`)}`)
|
|
558
|
-
.join("\n");
|
|
559
|
-
}
|
|
560
493
|
function systemdQuote(value) {
|
|
561
494
|
return `"${value
|
|
562
495
|
.replaceAll("\\", "\\\\")
|
|
@@ -567,13 +500,10 @@ function systemdQuote(value) {
|
|
|
567
500
|
.replaceAll("\r", "\\r")}"`;
|
|
568
501
|
}
|
|
569
502
|
function systemdPath(value) {
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
.replaceAll("\r", "\\r")
|
|
577
|
-
.replaceAll('"', "\\x22")
|
|
578
|
-
.replaceAll("'", "\\x27");
|
|
503
|
+
// These scalar path directives expand specifiers, but do not unquote words
|
|
504
|
+
// or decode the C-style escapes accepted by ExecStart/Environment.
|
|
505
|
+
if (/[\r\n]/.test(value)) {
|
|
506
|
+
throw new Error("systemd autostart paths cannot contain line breaks");
|
|
507
|
+
}
|
|
508
|
+
return value.replaceAll("%", "%%");
|
|
579
509
|
}
|
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.50",
|
|
4
4
|
"repository": {
|
|
5
5
|
"type": "git",
|
|
6
6
|
"url": "git+https://github.com/rynx-ai/rynx.git",
|
|
@@ -51,11 +51,11 @@
|
|
|
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/tmux": "0.1.11-beta.
|
|
54
|
+
"@rynx-ai/emulator": "0.1.11-beta.50",
|
|
55
|
+
"@rynx-ai/protocol": "0.1.11-beta.50",
|
|
56
|
+
"@rynx-ai/core": "0.1.11-beta.50",
|
|
57
|
+
"@rynx-ai/daemon": "0.1.11-beta.50",
|
|
58
|
+
"@rynx-ai/tmux": "0.1.11-beta.50"
|
|
59
59
|
},
|
|
60
60
|
"devDependencies": {
|
|
61
61
|
"@types/ws": "^8.18.1"
|