@particle-academy/fancy-term-host 0.1.2 → 0.2.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/README.md +6 -0
- package/dist/{chunk-M5TFZDJA.js → chunk-7NXYRMNA.js} +7 -27
- package/dist/chunk-7NXYRMNA.js.map +1 -0
- package/dist/chunk-DB3SIE53.js +12 -0
- package/dist/chunk-DB3SIE53.js.map +1 -0
- package/dist/chunk-V3MW5UTX.js +26 -0
- package/dist/chunk-V3MW5UTX.js.map +1 -0
- package/dist/index.js +6 -8
- package/dist/index.js.map +1 -1
- package/dist/pty-host.js +2 -1
- package/dist/pty-host.js.map +1 -1
- package/dist/service.cjs +508 -0
- package/dist/service.cjs.map +1 -0
- package/dist/service.d.cts +248 -0
- package/dist/service.d.ts +248 -0
- package/dist/service.js +451 -0
- package/dist/service.js.map +1 -0
- package/docs/persistence.md +48 -0
- package/docs/service.md +150 -0
- package/package.json +11 -1
- package/dist/chunk-M5TFZDJA.js.map +0 -1
package/dist/service.js
ADDED
|
@@ -0,0 +1,451 @@
|
|
|
1
|
+
import { ptyHostScriptPath } from './chunk-DB3SIE53.js';
|
|
2
|
+
import { PROTOCOL_VERSION } from './chunk-7NXYRMNA.js';
|
|
3
|
+
import path from 'path';
|
|
4
|
+
import os from 'os';
|
|
5
|
+
import fs from 'fs';
|
|
6
|
+
import { spawn } from 'child_process';
|
|
7
|
+
import fs2 from 'fs/promises';
|
|
8
|
+
|
|
9
|
+
var SERVICE_REVISION = `svc1+proto${PROTOCOL_VERSION}`;
|
|
10
|
+
var REVISION_MARKER = "fancy-term-service-revision";
|
|
11
|
+
function servicePlatformFor(platform = process.platform) {
|
|
12
|
+
switch (platform) {
|
|
13
|
+
case "darwin":
|
|
14
|
+
return "launchd";
|
|
15
|
+
case "linux":
|
|
16
|
+
return "systemd";
|
|
17
|
+
case "win32":
|
|
18
|
+
return "windows-task";
|
|
19
|
+
default:
|
|
20
|
+
return null;
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
function buildServiceDescriptor(config, ctx = {}) {
|
|
24
|
+
const platform = ctx.platform ?? servicePlatformFor() ?? unsupported(config);
|
|
25
|
+
const home = ctx.home ?? os.homedir();
|
|
26
|
+
switch (platform) {
|
|
27
|
+
case "launchd":
|
|
28
|
+
return launchd(config, home, ctx.uid ?? safeUid());
|
|
29
|
+
case "systemd":
|
|
30
|
+
return systemd(config, home);
|
|
31
|
+
case "windows-task":
|
|
32
|
+
return windowsTask(config);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
function unsupported(config) {
|
|
36
|
+
throw new Error(
|
|
37
|
+
`fancy-term-host service: unsupported platform for "${config.label}"`
|
|
38
|
+
);
|
|
39
|
+
}
|
|
40
|
+
function safeUid() {
|
|
41
|
+
const getuid = process.getuid;
|
|
42
|
+
return typeof getuid === "function" ? getuid() : 0;
|
|
43
|
+
}
|
|
44
|
+
function serviceEnv(config) {
|
|
45
|
+
const env = {
|
|
46
|
+
...config.env,
|
|
47
|
+
// GENIE_USERDATA is the var the existing detached-spawn path already uses
|
|
48
|
+
// (see host-lifecycle.spawnDetached); keep it so the host finds its data.
|
|
49
|
+
GENIE_USERDATA: config.userDataDir,
|
|
50
|
+
FANCY_TERM_SERVICE_REVISION: config.revision
|
|
51
|
+
};
|
|
52
|
+
if (config.runtime.nodePtyDir) {
|
|
53
|
+
env.NODE_PATH = config.runtime.nodePtyDir;
|
|
54
|
+
}
|
|
55
|
+
return env;
|
|
56
|
+
}
|
|
57
|
+
function launchd(config, home, uid) {
|
|
58
|
+
const label = config.label;
|
|
59
|
+
const plistPath = path.posix.join(home, "Library", "LaunchAgents", `${label}.plist`);
|
|
60
|
+
const env = serviceEnv(config);
|
|
61
|
+
const outLog = path.posix.join(config.logDir, "ptyhost.out.log");
|
|
62
|
+
const errLog = path.posix.join(config.logDir, "ptyhost.err.log");
|
|
63
|
+
const envXml = Object.entries(env).map(
|
|
64
|
+
([k, v]) => ` <key>${xml(k)}</key>
|
|
65
|
+
<string>${xml(v)}</string>`
|
|
66
|
+
).join("\n");
|
|
67
|
+
const contents = `<?xml version="1.0" encoding="UTF-8"?>
|
|
68
|
+
<!-- ${REVISION_MARKER}: ${config.revision} -->
|
|
69
|
+
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
|
70
|
+
<plist version="1.0">
|
|
71
|
+
<dict>
|
|
72
|
+
<key>Label</key>
|
|
73
|
+
<string>${xml(label)}</string>
|
|
74
|
+
<key>ProgramArguments</key>
|
|
75
|
+
<array>
|
|
76
|
+
<string>${xml(config.runtime.nodePath)}</string>
|
|
77
|
+
<string>${xml(config.hostScript)}</string>
|
|
78
|
+
</array>
|
|
79
|
+
<key>EnvironmentVariables</key>
|
|
80
|
+
<dict>
|
|
81
|
+
${envXml}
|
|
82
|
+
</dict>
|
|
83
|
+
<key>RunAtLoad</key>
|
|
84
|
+
<true/>
|
|
85
|
+
<key>KeepAlive</key>
|
|
86
|
+
<false/>
|
|
87
|
+
<key>ProcessType</key>
|
|
88
|
+
<string>Background</string>
|
|
89
|
+
<key>StandardOutPath</key>
|
|
90
|
+
<string>${xml(outLog)}</string>
|
|
91
|
+
<key>StandardErrorPath</key>
|
|
92
|
+
<string>${xml(errLog)}</string>
|
|
93
|
+
</dict>
|
|
94
|
+
</plist>
|
|
95
|
+
`;
|
|
96
|
+
const domain = `gui/${uid}`;
|
|
97
|
+
const target = `${domain}/${label}`;
|
|
98
|
+
return {
|
|
99
|
+
platform: "launchd",
|
|
100
|
+
label,
|
|
101
|
+
revision: config.revision,
|
|
102
|
+
unitPath: plistPath,
|
|
103
|
+
unitContents: contents,
|
|
104
|
+
unitMode: 384,
|
|
105
|
+
// bootstrap loads + (RunAtLoad) starts it.
|
|
106
|
+
installArgv: [["launchctl", "bootstrap", domain, plistPath]],
|
|
107
|
+
uninstallArgv: [["launchctl", "bootout", target]],
|
|
108
|
+
startArgv: [["launchctl", "kickstart", "-k", target]],
|
|
109
|
+
stopArgv: [["launchctl", "kill", "SIGTERM", target]],
|
|
110
|
+
statusArgv: ["launchctl", "print", target],
|
|
111
|
+
removePaths: [plistPath]
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
function systemd(config, home) {
|
|
115
|
+
const unit = `${config.label}.service`;
|
|
116
|
+
const unitPath = path.posix.join(home, ".config", "systemd", "user", unit);
|
|
117
|
+
const env = serviceEnv(config);
|
|
118
|
+
const envLines = Object.entries(env).map(([k, v]) => `Environment=${k}=${systemdEnvValue(v)}`).join("\n");
|
|
119
|
+
const contents = `# ${REVISION_MARKER}: ${config.revision}
|
|
120
|
+
[Unit]
|
|
121
|
+
Description=fancy-term pty-host (${config.label})
|
|
122
|
+
After=default.target
|
|
123
|
+
|
|
124
|
+
[Service]
|
|
125
|
+
Type=simple
|
|
126
|
+
ExecStart=${quoteForExec(config.runtime.nodePath)} ${quoteForExec(config.hostScript)}
|
|
127
|
+
${envLines}
|
|
128
|
+
Restart=no
|
|
129
|
+
|
|
130
|
+
[Install]
|
|
131
|
+
WantedBy=default.target
|
|
132
|
+
`;
|
|
133
|
+
return {
|
|
134
|
+
platform: "systemd",
|
|
135
|
+
label: config.label,
|
|
136
|
+
revision: config.revision,
|
|
137
|
+
unitPath,
|
|
138
|
+
unitContents: contents,
|
|
139
|
+
unitMode: 420,
|
|
140
|
+
installArgv: [
|
|
141
|
+
["systemctl", "--user", "daemon-reload"],
|
|
142
|
+
["systemctl", "--user", "enable", "--now", unit]
|
|
143
|
+
],
|
|
144
|
+
uninstallArgv: [["systemctl", "--user", "disable", "--now", unit]],
|
|
145
|
+
startArgv: [["systemctl", "--user", "start", unit]],
|
|
146
|
+
stopArgv: [["systemctl", "--user", "stop", unit]],
|
|
147
|
+
statusArgv: ["systemctl", "--user", "is-active", unit],
|
|
148
|
+
removePaths: [unitPath]
|
|
149
|
+
};
|
|
150
|
+
}
|
|
151
|
+
function windowsTask(config) {
|
|
152
|
+
const env = serviceEnv(config);
|
|
153
|
+
const cmdPath = path.win32.join(config.userDataDir, `${sanitizeFileName(config.label)}.cmd`);
|
|
154
|
+
const setLines = Object.entries(env).map(([k, v]) => `set "${k}=${v}"`).join("\r\n");
|
|
155
|
+
const contents = [
|
|
156
|
+
"@echo off",
|
|
157
|
+
`rem ${REVISION_MARKER}: ${config.revision}`,
|
|
158
|
+
setLines,
|
|
159
|
+
`"${config.runtime.nodePath}" "${config.hostScript}"`,
|
|
160
|
+
""
|
|
161
|
+
].join("\r\n");
|
|
162
|
+
const taskName = config.label;
|
|
163
|
+
const tr = `cmd /c "${cmdPath}"`;
|
|
164
|
+
return {
|
|
165
|
+
platform: "windows-task",
|
|
166
|
+
label: config.label,
|
|
167
|
+
revision: config.revision,
|
|
168
|
+
unitPath: cmdPath,
|
|
169
|
+
unitContents: contents,
|
|
170
|
+
unitMode: 448,
|
|
171
|
+
installArgv: [
|
|
172
|
+
["schtasks", "/Create", "/TN", taskName, "/TR", tr, "/SC", "ONLOGON", "/RL", "LIMITED", "/F"]
|
|
173
|
+
],
|
|
174
|
+
uninstallArgv: [["schtasks", "/Delete", "/TN", taskName, "/F"]],
|
|
175
|
+
startArgv: [["schtasks", "/Run", "/TN", taskName]],
|
|
176
|
+
stopArgv: [["schtasks", "/End", "/TN", taskName]],
|
|
177
|
+
statusArgv: ["schtasks", "/Query", "/TN", taskName, "/FO", "LIST"],
|
|
178
|
+
removePaths: [cmdPath]
|
|
179
|
+
};
|
|
180
|
+
}
|
|
181
|
+
function xml(s) {
|
|
182
|
+
return s.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """);
|
|
183
|
+
}
|
|
184
|
+
function systemdEnvValue(v) {
|
|
185
|
+
return /\s/.test(v) ? `"${v.replace(/"/g, '\\"')}"` : v;
|
|
186
|
+
}
|
|
187
|
+
function quoteForExec(p) {
|
|
188
|
+
return /\s/.test(p) ? `"${p}"` : p;
|
|
189
|
+
}
|
|
190
|
+
function sanitizeFileName(label) {
|
|
191
|
+
return label.replace(/[^A-Za-z0-9._-]+/g, "_");
|
|
192
|
+
}
|
|
193
|
+
function parseInstalledRevision(unitContents) {
|
|
194
|
+
const m = unitContents.match(
|
|
195
|
+
new RegExp(`${REVISION_MARKER}:\\s*(\\S+)`)
|
|
196
|
+
);
|
|
197
|
+
return m ? m[1] : null;
|
|
198
|
+
}
|
|
199
|
+
function resolveServiceRuntime(opts = {}) {
|
|
200
|
+
const env = opts.env ?? process.env;
|
|
201
|
+
const execPath = opts.execPath ?? process.execPath;
|
|
202
|
+
const isElectron = opts.isElectron ?? Boolean(process.versions?.electron);
|
|
203
|
+
const nodePtyDir = opts.nodePtyDir ?? env.FANCY_TERM_NODE_PTY ?? void 0;
|
|
204
|
+
const probe = opts.pathProbe ?? defaultPathProbe;
|
|
205
|
+
const finish = (nodePath, source) => ({
|
|
206
|
+
nodePath,
|
|
207
|
+
...nodePtyDir ? { nodePtyDir } : {},
|
|
208
|
+
source
|
|
209
|
+
});
|
|
210
|
+
if (opts.nodePath) return finish(opts.nodePath, "explicit");
|
|
211
|
+
if (env.FANCY_TERM_NODE) return finish(env.FANCY_TERM_NODE, "env:FANCY_TERM_NODE");
|
|
212
|
+
if (!isElectron && looksLikeNode(execPath)) {
|
|
213
|
+
return finish(execPath, "process.execPath");
|
|
214
|
+
}
|
|
215
|
+
const onPath = probe(nodeBinNames(), env);
|
|
216
|
+
if (onPath) return finish(onPath, "PATH");
|
|
217
|
+
return null;
|
|
218
|
+
}
|
|
219
|
+
function nodeBinNames() {
|
|
220
|
+
return process.platform === "win32" ? ["node.exe", "node"] : ["node"];
|
|
221
|
+
}
|
|
222
|
+
function looksLikeNode(execPath) {
|
|
223
|
+
const base = path.basename(execPath).toLowerCase();
|
|
224
|
+
return base === "node" || base === "node.exe";
|
|
225
|
+
}
|
|
226
|
+
function defaultPathProbe(binNames, env) {
|
|
227
|
+
const PATH = env.PATH ?? env.Path ?? "";
|
|
228
|
+
const dirs = PATH.split(path.delimiter).filter(Boolean);
|
|
229
|
+
for (const dir of dirs) {
|
|
230
|
+
for (const bin of binNames) {
|
|
231
|
+
const candidate = path.join(dir, bin);
|
|
232
|
+
try {
|
|
233
|
+
if (fs.existsSync(candidate)) return candidate;
|
|
234
|
+
} catch {
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
return null;
|
|
239
|
+
}
|
|
240
|
+
function nodeServiceIo() {
|
|
241
|
+
return {
|
|
242
|
+
run(argv) {
|
|
243
|
+
const [cmd, ...args] = argv;
|
|
244
|
+
return new Promise((resolve) => {
|
|
245
|
+
let stdout = "";
|
|
246
|
+
let stderr = "";
|
|
247
|
+
const child = spawn(cmd, args, {
|
|
248
|
+
// schtasks/launchctl/systemctl resolve via the shell on win32.
|
|
249
|
+
shell: process.platform === "win32",
|
|
250
|
+
windowsHide: true
|
|
251
|
+
});
|
|
252
|
+
child.stdout?.on("data", (d) => stdout += d.toString());
|
|
253
|
+
child.stderr?.on("data", (d) => stderr += d.toString());
|
|
254
|
+
child.on(
|
|
255
|
+
"error",
|
|
256
|
+
(err) => resolve({ code: -1, stdout, stderr: stderr + String(err) })
|
|
257
|
+
);
|
|
258
|
+
child.on(
|
|
259
|
+
"close",
|
|
260
|
+
(code) => resolve({ code: code ?? -1, stdout, stderr })
|
|
261
|
+
);
|
|
262
|
+
});
|
|
263
|
+
},
|
|
264
|
+
async writeFile(p, contents, opts) {
|
|
265
|
+
await fs2.mkdir(path.dirname(p), { recursive: true });
|
|
266
|
+
await fs2.writeFile(p, contents, { mode: opts?.mode ?? 384 });
|
|
267
|
+
},
|
|
268
|
+
async readFile(p) {
|
|
269
|
+
try {
|
|
270
|
+
return await fs2.readFile(p, "utf8");
|
|
271
|
+
} catch {
|
|
272
|
+
return null;
|
|
273
|
+
}
|
|
274
|
+
},
|
|
275
|
+
async mkdirp(dir) {
|
|
276
|
+
await fs2.mkdir(dir, { recursive: true });
|
|
277
|
+
},
|
|
278
|
+
async rm(p) {
|
|
279
|
+
await fs2.rm(p, { force: true }).catch(() => {
|
|
280
|
+
});
|
|
281
|
+
},
|
|
282
|
+
async exists(p) {
|
|
283
|
+
try {
|
|
284
|
+
await fs2.access(p);
|
|
285
|
+
return true;
|
|
286
|
+
} catch {
|
|
287
|
+
return false;
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
};
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
// src/service/index.ts
|
|
294
|
+
function isServiceSupported() {
|
|
295
|
+
return servicePlatformFor() !== null;
|
|
296
|
+
}
|
|
297
|
+
function resolveServiceConfig(config) {
|
|
298
|
+
const runtime = config.runtime ?? resolveServiceRuntime();
|
|
299
|
+
if (!runtime) {
|
|
300
|
+
throw new Error(
|
|
301
|
+
"fancy-term-host service: no standalone Node runtime found (set config.runtime or $FANCY_TERM_NODE). Refusing to pin the consumer binary by running on Electron."
|
|
302
|
+
);
|
|
303
|
+
}
|
|
304
|
+
return {
|
|
305
|
+
label: config.label,
|
|
306
|
+
userDataDir: config.userDataDir,
|
|
307
|
+
hostScript: config.hostScript ?? ptyHostScriptPath(),
|
|
308
|
+
runtime,
|
|
309
|
+
env: config.env ?? {},
|
|
310
|
+
revision: config.revision ?? SERVICE_REVISION,
|
|
311
|
+
logDir: config.logDir ?? config.userDataDir
|
|
312
|
+
};
|
|
313
|
+
}
|
|
314
|
+
function descriptorFor(config) {
|
|
315
|
+
return buildServiceDescriptor(resolveServiceConfig(config));
|
|
316
|
+
}
|
|
317
|
+
async function runAll(io, argvList) {
|
|
318
|
+
for (const argv of argvList) {
|
|
319
|
+
const { code, stderr } = await io.run(argv);
|
|
320
|
+
if (code !== 0) {
|
|
321
|
+
throw new Error(
|
|
322
|
+
`command failed (${code}): ${argv.join(" ")}${stderr ? ` \u2014 ${stderr.trim()}` : ""}`
|
|
323
|
+
);
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
async function installHostService(config, io = nodeServiceIo()) {
|
|
328
|
+
const desc = descriptorFor(config);
|
|
329
|
+
await io.mkdirp(config.userDataDir);
|
|
330
|
+
await io.writeFile(desc.unitPath, desc.unitContents, { mode: desc.unitMode });
|
|
331
|
+
await runAll(io, desc.installArgv);
|
|
332
|
+
return serviceStatusFor(desc, io);
|
|
333
|
+
}
|
|
334
|
+
async function uninstallHostService(config, io = nodeServiceIo()) {
|
|
335
|
+
const desc = descriptorFor(config);
|
|
336
|
+
for (const argv of desc.uninstallArgv) {
|
|
337
|
+
await io.run(argv);
|
|
338
|
+
}
|
|
339
|
+
for (const p of desc.removePaths) {
|
|
340
|
+
await io.rm(p);
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
async function startHostService(config, io = nodeServiceIo()) {
|
|
344
|
+
await runAll(io, descriptorFor(config).startArgv);
|
|
345
|
+
}
|
|
346
|
+
async function stopHostService(config, io = nodeServiceIo()) {
|
|
347
|
+
await runAll(io, descriptorFor(config).stopArgv);
|
|
348
|
+
}
|
|
349
|
+
async function isServiceInstalled(config, io = nodeServiceIo()) {
|
|
350
|
+
return io.exists(descriptorFor(config).unitPath);
|
|
351
|
+
}
|
|
352
|
+
async function serviceStatus(config, io = nodeServiceIo()) {
|
|
353
|
+
return serviceStatusFor(descriptorFor(config), io);
|
|
354
|
+
}
|
|
355
|
+
async function serviceStatusFor(desc, io) {
|
|
356
|
+
const unit = await io.readFile(desc.unitPath);
|
|
357
|
+
const installed = unit !== null;
|
|
358
|
+
const installedRevision = unit ? parseInstalledRevision(unit) ?? void 0 : void 0;
|
|
359
|
+
let running = false;
|
|
360
|
+
let detail;
|
|
361
|
+
if (installed) {
|
|
362
|
+
const { code, stdout, stderr } = await io.run(desc.statusArgv);
|
|
363
|
+
detail = (stdout || stderr).trim() || void 0;
|
|
364
|
+
running = isRunningOutput(desc, code, stdout);
|
|
365
|
+
}
|
|
366
|
+
return {
|
|
367
|
+
platform: desc.platform,
|
|
368
|
+
state: !installed ? "not-installed" : running ? "running" : "installed",
|
|
369
|
+
installed,
|
|
370
|
+
running,
|
|
371
|
+
label: desc.label,
|
|
372
|
+
unitPath: desc.unitPath,
|
|
373
|
+
installedRevision,
|
|
374
|
+
detail
|
|
375
|
+
};
|
|
376
|
+
}
|
|
377
|
+
function isRunningOutput(desc, code, stdout) {
|
|
378
|
+
const out = stdout.toLowerCase();
|
|
379
|
+
switch (desc.platform) {
|
|
380
|
+
case "systemd":
|
|
381
|
+
return code === 0 && out.includes("active") && !out.includes("inactive");
|
|
382
|
+
case "launchd":
|
|
383
|
+
return code === 0 && (out.includes("state = running") || out.includes("pid ="));
|
|
384
|
+
case "windows-task":
|
|
385
|
+
return code === 0 && out.includes("running");
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
async function ensureHostService(config, io = nodeServiceIo()) {
|
|
389
|
+
if (!isServiceSupported()) {
|
|
390
|
+
return {
|
|
391
|
+
ok: false,
|
|
392
|
+
installed: false,
|
|
393
|
+
running: false,
|
|
394
|
+
action: "unsupported",
|
|
395
|
+
error: `unsupported platform: ${process.platform}`
|
|
396
|
+
};
|
|
397
|
+
}
|
|
398
|
+
let resolved;
|
|
399
|
+
try {
|
|
400
|
+
resolved = resolveServiceConfig(config);
|
|
401
|
+
} catch (err) {
|
|
402
|
+
return {
|
|
403
|
+
ok: false,
|
|
404
|
+
installed: false,
|
|
405
|
+
running: false,
|
|
406
|
+
action: "failed",
|
|
407
|
+
error: err.message
|
|
408
|
+
};
|
|
409
|
+
}
|
|
410
|
+
const desc = buildServiceDescriptor(resolved);
|
|
411
|
+
const runtime = resolved.runtime;
|
|
412
|
+
try {
|
|
413
|
+
const status = await serviceStatusFor(desc, io);
|
|
414
|
+
if (status.installed && status.installedRevision === resolved.revision) {
|
|
415
|
+
if (status.running) {
|
|
416
|
+
return { ok: true, installed: true, running: true, action: "already-running", runtime };
|
|
417
|
+
}
|
|
418
|
+
await runAll(io, desc.startArgv);
|
|
419
|
+
return { ok: true, installed: true, running: true, action: "started", runtime };
|
|
420
|
+
}
|
|
421
|
+
if (status.installed) {
|
|
422
|
+
for (const argv of desc.uninstallArgv) await io.run(argv);
|
|
423
|
+
for (const p of desc.removePaths) await io.rm(p);
|
|
424
|
+
}
|
|
425
|
+
await io.mkdirp(resolved.userDataDir);
|
|
426
|
+
await io.writeFile(desc.unitPath, desc.unitContents, { mode: desc.unitMode });
|
|
427
|
+
await runAll(io, desc.installArgv);
|
|
428
|
+
await runAll(io, desc.startArgv).catch(() => {
|
|
429
|
+
});
|
|
430
|
+
return {
|
|
431
|
+
ok: true,
|
|
432
|
+
installed: true,
|
|
433
|
+
running: true,
|
|
434
|
+
action: status.installed ? "reinstalled" : "installed-and-started",
|
|
435
|
+
runtime
|
|
436
|
+
};
|
|
437
|
+
} catch (err) {
|
|
438
|
+
return {
|
|
439
|
+
ok: false,
|
|
440
|
+
installed: false,
|
|
441
|
+
running: false,
|
|
442
|
+
action: "failed",
|
|
443
|
+
error: err.message,
|
|
444
|
+
runtime
|
|
445
|
+
};
|
|
446
|
+
}
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
export { SERVICE_REVISION, buildServiceDescriptor, ensureHostService, installHostService, isServiceInstalled, isServiceSupported, nodeServiceIo, parseInstalledRevision, resolveServiceConfig, resolveServiceRuntime, servicePlatformFor, serviceStatus, startHostService, stopHostService, uninstallHostService };
|
|
450
|
+
//# sourceMappingURL=service.js.map
|
|
451
|
+
//# sourceMappingURL=service.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/service/descriptor.ts","../src/service/runtime.ts","../src/service/io.ts","../src/service/index.ts"],"names":["path","fs"],"mappings":"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","file":"service.js","sourcesContent":["import path from 'node:path';\nimport os from 'node:os';\nimport { PROTOCOL_VERSION } from '../host-protocol';\nimport type { HostServiceConfig, ServicePlatform, ServiceRuntime } from './types';\n\n/**\n * Default service revision. Encodes the host PROTOCOL_VERSION so that a protocol\n * bump (which makes an old host incompatible) forces a reinstall — the installed\n * unit carries this marker and `ensureHostService` reinstalls on a mismatch.\n */\nexport const SERVICE_REVISION = `svc1+proto${PROTOCOL_VERSION}`;\n\n/** The marker line embedded in every generated unit, parsed back on upgrade. */\nexport const REVISION_MARKER = 'fancy-term-service-revision';\n\n/** A fully-resolved, per-OS description of the service to install. */\nexport interface ServiceDescriptor {\n platform: ServicePlatform;\n label: string;\n revision: string;\n /** The file we write + own (plist / unit / launcher .cmd). */\n unitPath: string;\n unitContents: string;\n /** POSIX file mode for the unit (e.g. 0o600 plist, 0o700 launcher). */\n unitMode: number;\n /** Commands (after the file is written) to register + start the service. */\n installArgv: string[][];\n /** Commands to stop + deregister. */\n uninstallArgv: string[][];\n startArgv: string[][];\n stopArgv: string[][];\n /** Single command whose output tells us installed/running. */\n statusArgv: string[];\n /** Files to remove on uninstall (unit + any logs we created). */\n removePaths: string[];\n}\n\nexport interface DescriptorContext {\n /** Override the platform (tests). Defaults to the current OS. */\n platform?: ServicePlatform;\n /** Home dir (tests). Defaults to `os.homedir()`. */\n home?: string;\n /** Numeric uid for launchd domain targets (tests / non-posix). */\n uid?: number;\n}\n\n/** Map `process.platform` to a service mechanism, or null when unsupported. */\nexport function servicePlatformFor(\n platform: NodeJS.Platform = process.platform,\n): ServicePlatform | null {\n switch (platform) {\n case 'darwin':\n return 'launchd';\n case 'linux':\n return 'systemd';\n case 'win32':\n return 'windows-task';\n default:\n return null;\n }\n}\n\n/**\n * Build the per-OS descriptor for a service config. PURE — no fs, no spawning —\n * so the generated units + command argv are fully unit-testable.\n */\nexport function buildServiceDescriptor(\n config: ResolvedServiceConfig,\n ctx: DescriptorContext = {},\n): ServiceDescriptor {\n const platform =\n ctx.platform ?? servicePlatformFor() ?? unsupported(config);\n const home = ctx.home ?? os.homedir();\n switch (platform) {\n case 'launchd':\n return launchd(config, home, ctx.uid ?? safeUid());\n case 'systemd':\n return systemd(config, home);\n case 'windows-task':\n return windowsTask(config);\n }\n}\n\nfunction unsupported(config: ResolvedServiceConfig): never {\n throw new Error(\n `fancy-term-host service: unsupported platform for \"${config.label}\"`,\n );\n}\n\nfunction safeUid(): number {\n const getuid = (process as { getuid?: () => number }).getuid;\n return typeof getuid === 'function' ? getuid() : 0;\n}\n\n/**\n * A config with all defaults filled in — produced by `resolveServiceConfig`\n * (see index.ts) and consumed by the descriptor builder.\n */\nexport interface ResolvedServiceConfig {\n label: string;\n userDataDir: string;\n hostScript: string;\n runtime: ServiceRuntime;\n env: Record<string, string>;\n revision: string;\n logDir: string;\n}\n\n/** The environment every platform injects (user-data dir, NODE_PATH, revision). */\nfunction serviceEnv(config: ResolvedServiceConfig): Record<string, string> {\n const env: Record<string, string> = {\n ...config.env,\n // GENIE_USERDATA is the var the existing detached-spawn path already uses\n // (see host-lifecycle.spawnDetached); keep it so the host finds its data.\n GENIE_USERDATA: config.userDataDir,\n FANCY_TERM_SERVICE_REVISION: config.revision,\n };\n if (config.runtime.nodePtyDir) {\n env.NODE_PATH = config.runtime.nodePtyDir;\n }\n return env;\n}\n\n// ── macOS: launchd LaunchAgent ──────────────────────────────────────────────\n\nfunction launchd(\n config: ResolvedServiceConfig,\n home: string,\n uid: number,\n): ServiceDescriptor {\n const label = config.label;\n // launchd targets macOS — always POSIX paths, even if generated elsewhere.\n const plistPath = path.posix.join(home, 'Library', 'LaunchAgents', `${label}.plist`);\n const env = serviceEnv(config);\n const outLog = path.posix.join(config.logDir, 'ptyhost.out.log');\n const errLog = path.posix.join(config.logDir, 'ptyhost.err.log');\n\n const envXml = Object.entries(env)\n .map(\n ([k, v]) =>\n ` <key>${xml(k)}</key>\\n <string>${xml(v)}</string>`,\n )\n .join('\\n');\n\n const contents = `<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<!-- ${REVISION_MARKER}: ${config.revision} -->\n<!DOCTYPE plist PUBLIC \"-//Apple//DTD PLIST 1.0//EN\" \"http://www.apple.com/DTDs/PropertyList-1.0.dtd\">\n<plist version=\"1.0\">\n <dict>\n <key>Label</key>\n <string>${xml(label)}</string>\n <key>ProgramArguments</key>\n <array>\n <string>${xml(config.runtime.nodePath)}</string>\n <string>${xml(config.hostScript)}</string>\n </array>\n <key>EnvironmentVariables</key>\n <dict>\n${envXml}\n </dict>\n <key>RunAtLoad</key>\n <true/>\n <key>KeepAlive</key>\n <false/>\n <key>ProcessType</key>\n <string>Background</string>\n <key>StandardOutPath</key>\n <string>${xml(outLog)}</string>\n <key>StandardErrorPath</key>\n <string>${xml(errLog)}</string>\n </dict>\n</plist>\n`;\n\n const domain = `gui/${uid}`;\n const target = `${domain}/${label}`;\n return {\n platform: 'launchd',\n label,\n revision: config.revision,\n unitPath: plistPath,\n unitContents: contents,\n unitMode: 0o600,\n // bootstrap loads + (RunAtLoad) starts it.\n installArgv: [['launchctl', 'bootstrap', domain, plistPath]],\n uninstallArgv: [['launchctl', 'bootout', target]],\n startArgv: [['launchctl', 'kickstart', '-k', target]],\n stopArgv: [['launchctl', 'kill', 'SIGTERM', target]],\n statusArgv: ['launchctl', 'print', target],\n removePaths: [plistPath],\n };\n}\n\n// ── Linux: systemd --user unit ──────────────────────────────────────────────\n\nfunction systemd(config: ResolvedServiceConfig, home: string): ServiceDescriptor {\n const unit = `${config.label}.service`;\n // systemd --user targets Linux — always POSIX paths.\n const unitPath = path.posix.join(home, '.config', 'systemd', 'user', unit);\n const env = serviceEnv(config);\n const envLines = Object.entries(env)\n .map(([k, v]) => `Environment=${k}=${systemdEnvValue(v)}`)\n .join('\\n');\n\n const contents = `# ${REVISION_MARKER}: ${config.revision}\n[Unit]\nDescription=fancy-term pty-host (${config.label})\nAfter=default.target\n\n[Service]\nType=simple\nExecStart=${quoteForExec(config.runtime.nodePath)} ${quoteForExec(config.hostScript)}\n${envLines}\nRestart=no\n\n[Install]\nWantedBy=default.target\n`;\n\n return {\n platform: 'systemd',\n label: config.label,\n revision: config.revision,\n unitPath,\n unitContents: contents,\n unitMode: 0o644,\n installArgv: [\n ['systemctl', '--user', 'daemon-reload'],\n ['systemctl', '--user', 'enable', '--now', unit],\n ],\n uninstallArgv: [['systemctl', '--user', 'disable', '--now', unit]],\n startArgv: [['systemctl', '--user', 'start', unit]],\n stopArgv: [['systemctl', '--user', 'stop', unit]],\n statusArgv: ['systemctl', '--user', 'is-active', unit],\n removePaths: [unitPath],\n };\n}\n\n// ── Windows: per-user scheduled task (ONLOGON, no elevation) ─────────────────\n\nfunction windowsTask(config: ResolvedServiceConfig): ServiceDescriptor {\n const env = serviceEnv(config);\n // A launcher .cmd sets env then runs the host — schtasks can't carry rich\n // env cleanly, and the .cmd also records the revision marker.\n const cmdPath = path.win32.join(config.userDataDir, `${sanitizeFileName(config.label)}.cmd`);\n const setLines = Object.entries(env)\n .map(([k, v]) => `set \"${k}=${v}\"`)\n .join('\\r\\n');\n const contents = [\n '@echo off',\n `rem ${REVISION_MARKER}: ${config.revision}`,\n setLines,\n `\"${config.runtime.nodePath}\" \"${config.hostScript}\"`,\n '',\n ].join('\\r\\n');\n\n const taskName = config.label;\n // /RL LIMITED = run with the user's normal (non-elevated) rights.\n // /SC ONLOGON = start at this user's logon. /F = overwrite if present.\n const tr = `cmd /c \"${cmdPath}\"`;\n return {\n platform: 'windows-task',\n label: config.label,\n revision: config.revision,\n unitPath: cmdPath,\n unitContents: contents,\n unitMode: 0o700,\n installArgv: [\n ['schtasks', '/Create', '/TN', taskName, '/TR', tr, '/SC', 'ONLOGON', '/RL', 'LIMITED', '/F'],\n ],\n uninstallArgv: [['schtasks', '/Delete', '/TN', taskName, '/F']],\n startArgv: [['schtasks', '/Run', '/TN', taskName]],\n stopArgv: [['schtasks', '/End', '/TN', taskName]],\n statusArgv: ['schtasks', '/Query', '/TN', taskName, '/FO', 'LIST'],\n removePaths: [cmdPath],\n };\n}\n\n// ── helpers ─────────────────────────────────────────────────────────────────\n\nfunction xml(s: string): string {\n return s\n .replace(/&/g, '&')\n .replace(/</g, '<')\n .replace(/>/g, '>')\n .replace(/\"/g, '"');\n}\n\n/** systemd `Environment=` values: wrap in quotes if they contain whitespace. */\nfunction systemdEnvValue(v: string): string {\n return /\\s/.test(v) ? `\"${v.replace(/\"/g, '\\\\\"')}\"` : v;\n}\n\n/** Quote a path for a systemd ExecStart token. */\nfunction quoteForExec(p: string): string {\n return /\\s/.test(p) ? `\"${p}\"` : p;\n}\n\nfunction sanitizeFileName(label: string): string {\n return label.replace(/[^A-Za-z0-9._-]+/g, '_');\n}\n\n/** Read the revision marker out of an installed unit's contents, or null. */\nexport function parseInstalledRevision(unitContents: string): string | null {\n const m = unitContents.match(\n new RegExp(`${REVISION_MARKER}:\\\\s*(\\\\S+)`),\n );\n return m ? m[1] : null;\n}\n","import path from 'node:path';\nimport fs from 'node:fs';\nimport type { ServiceRuntime } from './types';\n\n/**\n * Locate a STANDALONE Node runtime to run the host service on — explicitly NOT\n * the consumer's Electron binary, whose whole problem is that running the host\n * on it pins the executable across auto-updates.\n *\n * Precedence:\n * 1. An explicit `nodePath` (the consumer ships/locates its own node).\n * 2. `$FANCY_TERM_NODE` (a deploy-time override).\n * 3. `process.execPath` — but ONLY if the current process is plain Node, not\n * Electron (`process.versions.electron`), and the binary is named `node`.\n * 4. A `node` / `node.exe` found on `$PATH`.\n *\n * Returns null when no safe standalone runtime is found — the caller should then\n * fall back to the existing detached-spawn (which works for a normal quit) or\n * in-process. Never throws.\n */\nexport interface ResolveRuntimeOptions {\n /** Explicit standalone node path (wins). */\n nodePath?: string;\n /** Directory with an ABI-matched node-pty for that runtime. */\n nodePtyDir?: string;\n /** Environment to read overrides from. Defaults to `process.env`. */\n env?: NodeJS.ProcessEnv;\n /** Current process binary. Defaults to `process.execPath`. */\n execPath?: string;\n /** Whether the current process is Electron. Defaults to detecting it. */\n isElectron?: boolean;\n /** PATH lookup probe (injectable for tests). Defaults to an fs probe. */\n pathProbe?: (binNames: string[], env: NodeJS.ProcessEnv) => string | null;\n}\n\nexport function resolveServiceRuntime(\n opts: ResolveRuntimeOptions = {},\n): ServiceRuntime | null {\n const env = opts.env ?? process.env;\n const execPath = opts.execPath ?? process.execPath;\n const isElectron =\n opts.isElectron ?? Boolean((process as { versions?: Record<string, string> }).versions?.electron);\n const nodePtyDir = opts.nodePtyDir ?? env.FANCY_TERM_NODE_PTY ?? undefined;\n const probe = opts.pathProbe ?? defaultPathProbe;\n\n const finish = (nodePath: string, source: string): ServiceRuntime => ({\n nodePath,\n ...(nodePtyDir ? { nodePtyDir } : {}),\n source,\n });\n\n // 1) Explicit.\n if (opts.nodePath) return finish(opts.nodePath, 'explicit');\n\n // 2) Env override.\n if (env.FANCY_TERM_NODE) return finish(env.FANCY_TERM_NODE, 'env:FANCY_TERM_NODE');\n\n // 3) The current process — only if it's a real standalone node.\n if (!isElectron && looksLikeNode(execPath)) {\n return finish(execPath, 'process.execPath');\n }\n\n // 4) PATH lookup.\n const onPath = probe(nodeBinNames(), env);\n if (onPath) return finish(onPath, 'PATH');\n\n return null;\n}\n\nfunction nodeBinNames(): string[] {\n return process.platform === 'win32' ? ['node.exe', 'node'] : ['node'];\n}\n\nfunction looksLikeNode(execPath: string): boolean {\n const base = path.basename(execPath).toLowerCase();\n return base === 'node' || base === 'node.exe';\n}\n\n/** Walk `$PATH`, returning the first existing `node`/`node.exe`. */\nfunction defaultPathProbe(\n binNames: string[],\n env: NodeJS.ProcessEnv,\n): string | null {\n const PATH = env.PATH ?? env.Path ?? '';\n const dirs = PATH.split(path.delimiter).filter(Boolean);\n for (const dir of dirs) {\n for (const bin of binNames) {\n const candidate = path.join(dir, bin);\n try {\n if (fs.existsSync(candidate)) return candidate;\n } catch {\n /* keep looking */\n }\n }\n }\n return null;\n}\n","import { spawn } from 'node:child_process';\nimport fs from 'node:fs/promises';\nimport path from 'node:path';\nimport type { ServiceIo } from './types';\n\n/**\n * Production {@link ServiceIo}: real fs + child_process. The orchestration in\n * index.ts takes a ServiceIo so tests can inject a fake and never touch the OS.\n */\nexport function nodeServiceIo(): ServiceIo {\n return {\n run(argv) {\n const [cmd, ...args] = argv;\n return new Promise((resolve) => {\n let stdout = '';\n let stderr = '';\n const child = spawn(cmd, args, {\n // schtasks/launchctl/systemctl resolve via the shell on win32.\n shell: process.platform === 'win32',\n windowsHide: true,\n });\n child.stdout?.on('data', (d) => (stdout += d.toString()));\n child.stderr?.on('data', (d) => (stderr += d.toString()));\n child.on('error', (err) =>\n resolve({ code: -1, stdout, stderr: stderr + String(err) }),\n );\n child.on('close', (code) =>\n resolve({ code: code ?? -1, stdout, stderr }),\n );\n });\n },\n async writeFile(p, contents, opts) {\n await fs.mkdir(path.dirname(p), { recursive: true });\n await fs.writeFile(p, contents, { mode: opts?.mode ?? 0o600 });\n },\n async readFile(p) {\n try {\n return await fs.readFile(p, 'utf8');\n } catch {\n return null;\n }\n },\n async mkdirp(dir) {\n await fs.mkdir(dir, { recursive: true });\n },\n async rm(p) {\n await fs.rm(p, { force: true }).catch(() => {});\n },\n async exists(p) {\n try {\n await fs.access(p);\n return true;\n } catch {\n return false;\n }\n },\n };\n}\n","/**\n * Per-user OS-service lifecycle for the pty-host. Install / start / stop /\n * status / uninstall + a single `ensureHostService()` that does the\n * install-if-missing-or-stale → start-if-stopped dance.\n *\n * Pairs with the existing detached-host path: the wire protocol, pidfile, and\n * `HostClient` are unchanged, so the consumer connects exactly as today. The\n * difference is WHO launches the host — a per-user service on a standalone Node\n * runtime, not a child of the consumer's Electron binary.\n *\n * Everything here is graceful: `ensureHostService` never throws (inspect `ok`),\n * so a consumer can try the service and, on failure, fall back to\n * `HostSpawner.spawnDetached` (normal-quit survival) → in-process.\n */\n\nimport { ptyHostScriptPath } from '../host-script';\nimport {\n buildServiceDescriptor,\n parseInstalledRevision,\n SERVICE_REVISION,\n servicePlatformFor,\n type ResolvedServiceConfig,\n type ServiceDescriptor,\n} from './descriptor';\nimport { resolveServiceRuntime } from './runtime';\nimport { nodeServiceIo } from './io';\nimport type {\n EnsureResult,\n HostServiceConfig,\n ServiceIo,\n ServiceRuntime,\n ServiceStatus,\n} from './types';\n\nexport { resolveServiceRuntime } from './runtime';\nexport { nodeServiceIo } from './io';\nexport {\n buildServiceDescriptor,\n servicePlatformFor,\n parseInstalledRevision,\n SERVICE_REVISION,\n} from './descriptor';\nexport type { ServiceDescriptor, ResolvedServiceConfig } from './descriptor';\nexport type {\n HostServiceConfig,\n ServiceRuntime,\n ServiceStatus,\n ServiceState,\n ServicePlatform,\n ServiceIo,\n EnsureResult,\n EnsureAction,\n} from './types';\n\n/** True when the current OS has a supported service mechanism. */\nexport function isServiceSupported(): boolean {\n return servicePlatformFor() !== null;\n}\n\n/**\n * Fill in every default (host script, runtime, revision, log dir) so the\n * descriptor builder has a complete config. Throws only if no standalone Node\n * runtime can be resolved — callers that want graceful behaviour should use\n * `ensureHostService`, which catches this.\n */\nexport function resolveServiceConfig(\n config: HostServiceConfig,\n): ResolvedServiceConfig {\n const runtime: ServiceRuntime | null =\n config.runtime ?? resolveServiceRuntime();\n if (!runtime) {\n throw new Error(\n 'fancy-term-host service: no standalone Node runtime found ' +\n '(set config.runtime or $FANCY_TERM_NODE). Refusing to pin the ' +\n 'consumer binary by running on Electron.',\n );\n }\n return {\n label: config.label,\n userDataDir: config.userDataDir,\n hostScript: config.hostScript ?? ptyHostScriptPath(),\n runtime,\n env: config.env ?? {},\n revision: config.revision ?? SERVICE_REVISION,\n logDir: config.logDir ?? config.userDataDir,\n };\n}\n\nfunction descriptorFor(config: HostServiceConfig): ServiceDescriptor {\n return buildServiceDescriptor(resolveServiceConfig(config));\n}\n\n/** Run a list of commands in order; throw on the first non-zero exit. */\nasync function runAll(io: ServiceIo, argvList: string[][]): Promise<void> {\n for (const argv of argvList) {\n const { code, stderr } = await io.run(argv);\n if (code !== 0) {\n throw new Error(\n `command failed (${code}): ${argv.join(' ')}${\n stderr ? ` — ${stderr.trim()}` : ''\n }`,\n );\n }\n }\n}\n\n/** Write the unit file(s) and register + start the service. */\nexport async function installHostService(\n config: HostServiceConfig,\n io: ServiceIo = nodeServiceIo(),\n): Promise<ServiceStatus> {\n const desc = descriptorFor(config);\n await io.mkdirp(config.userDataDir);\n await io.writeFile(desc.unitPath, desc.unitContents, { mode: desc.unitMode });\n await runAll(io, desc.installArgv);\n return serviceStatusFor(desc, io);\n}\n\n/** Stop + deregister the service and remove its unit file(s). */\nexport async function uninstallHostService(\n config: HostServiceConfig,\n io: ServiceIo = nodeServiceIo(),\n): Promise<void> {\n const desc = descriptorFor(config);\n // Best-effort: a not-installed service shouldn't make uninstall throw.\n for (const argv of desc.uninstallArgv) {\n await io.run(argv);\n }\n for (const p of desc.removePaths) {\n await io.rm(p);\n }\n}\n\nexport async function startHostService(\n config: HostServiceConfig,\n io: ServiceIo = nodeServiceIo(),\n): Promise<void> {\n await runAll(io, descriptorFor(config).startArgv);\n}\n\nexport async function stopHostService(\n config: HostServiceConfig,\n io: ServiceIo = nodeServiceIo(),\n): Promise<void> {\n await runAll(io, descriptorFor(config).stopArgv);\n}\n\nexport async function isServiceInstalled(\n config: HostServiceConfig,\n io: ServiceIo = nodeServiceIo(),\n): Promise<boolean> {\n return io.exists(descriptorFor(config).unitPath);\n}\n\nexport async function serviceStatus(\n config: HostServiceConfig,\n io: ServiceIo = nodeServiceIo(),\n): Promise<ServiceStatus> {\n return serviceStatusFor(descriptorFor(config), io);\n}\n\nasync function serviceStatusFor(\n desc: ServiceDescriptor,\n io: ServiceIo,\n): Promise<ServiceStatus> {\n const unit = await io.readFile(desc.unitPath);\n const installed = unit !== null;\n const installedRevision = unit ? parseInstalledRevision(unit) ?? undefined : undefined;\n\n let running = false;\n let detail: string | undefined;\n if (installed) {\n const { code, stdout, stderr } = await io.run(desc.statusArgv);\n detail = (stdout || stderr).trim() || undefined;\n running = isRunningOutput(desc, code, stdout);\n }\n\n return {\n platform: desc.platform,\n state: !installed ? 'not-installed' : running ? 'running' : 'installed',\n installed,\n running,\n label: desc.label,\n unitPath: desc.unitPath,\n installedRevision,\n detail,\n };\n}\n\n/** Interpret each platform's status command output. */\nfunction isRunningOutput(\n desc: ServiceDescriptor,\n code: number,\n stdout: string,\n): boolean {\n const out = stdout.toLowerCase();\n switch (desc.platform) {\n case 'systemd':\n // `systemctl --user is-active` → \"active\" (exit 0) when running.\n return code === 0 && out.includes('active') && !out.includes('inactive');\n case 'launchd':\n // `launchctl print` exits 0 when loaded; \"state = running\" when up.\n return code === 0 && (out.includes('state = running') || out.includes('pid ='));\n case 'windows-task':\n // `schtasks /Query /FO LIST` → a \"Status:\" line of \"Running\".\n return code === 0 && out.includes('running');\n }\n}\n\n/**\n * Ensure the service is installed at the current revision AND running.\n *\n * - already running at this revision → no-op\n * - installed (this revision), stopped → start\n * - installed at a DIFFERENT revision → uninstall + reinstall + start\n * - not installed → install (+ start)\n *\n * NEVER throws. On any failure (no runtime, unsupported OS, a command error) it\n * returns `{ ok: false, … }` with an `error` so the caller can fall back to the\n * detached-spawn path. Snapshot live sessions before a reinstall if you need\n * history to survive it (a reinstall restarts the host).\n */\nexport async function ensureHostService(\n config: HostServiceConfig,\n io: ServiceIo = nodeServiceIo(),\n): Promise<EnsureResult> {\n if (!isServiceSupported()) {\n return {\n ok: false,\n installed: false,\n running: false,\n action: 'unsupported',\n error: `unsupported platform: ${process.platform}`,\n };\n }\n\n let resolved: ResolvedServiceConfig;\n try {\n resolved = resolveServiceConfig(config);\n } catch (err) {\n return {\n ok: false,\n installed: false,\n running: false,\n action: 'failed',\n error: (err as Error).message,\n };\n }\n\n const desc = buildServiceDescriptor(resolved);\n const runtime = resolved.runtime;\n try {\n const status = await serviceStatusFor(desc, io);\n\n if (status.installed && status.installedRevision === resolved.revision) {\n if (status.running) {\n return { ok: true, installed: true, running: true, action: 'already-running', runtime };\n }\n await runAll(io, desc.startArgv);\n return { ok: true, installed: true, running: true, action: 'started', runtime };\n }\n\n if (status.installed) {\n // Stale revision → tear down then reinstall fresh.\n for (const argv of desc.uninstallArgv) await io.run(argv);\n for (const p of desc.removePaths) await io.rm(p);\n }\n\n await io.mkdirp(resolved.userDataDir);\n await io.writeFile(desc.unitPath, desc.unitContents, { mode: desc.unitMode });\n await runAll(io, desc.installArgv);\n // launchd RunAtLoad / systemd --now start on install; nudge to be sure.\n await runAll(io, desc.startArgv).catch(() => {});\n\n return {\n ok: true,\n installed: true,\n running: true,\n action: status.installed ? 'reinstalled' : 'installed-and-started',\n runtime,\n };\n } catch (err) {\n return {\n ok: false,\n installed: false,\n running: false,\n action: 'failed',\n error: (err as Error).message,\n runtime,\n };\n }\n}\n"]}
|
package/docs/persistence.md
CHANGED
|
@@ -67,6 +67,54 @@ their scrollback ring buffers. Because it's a different process, the shells
|
|
|
67
67
|
this is a clean teardown, **not** a SIGKILL-by-pidfile, so the host runs its own
|
|
68
68
|
cleanup. It never throws and no-ops when no host is active.
|
|
69
69
|
|
|
70
|
+
## T3+ — per-user OS service (`/service`)
|
|
71
|
+
|
|
72
|
+
The detached host above is launched with the consumer's binary (Electron-as-node,
|
|
73
|
+
for node-pty's ABI). That means the running host **pins the app executable**, so
|
|
74
|
+
an Electron auto-update can't overwrite it without first killing the host — and
|
|
75
|
+
killing it loses the live sessions T3 exists to protect.
|
|
76
|
+
|
|
77
|
+
`@particle-academy/fancy-term-host/service` fixes that by running the host as a
|
|
78
|
+
**per-user OS service on its own standalone Node runtime** — never the consumer's
|
|
79
|
+
binary, so an update never pins it. The wire protocol, pidfile, socket, and
|
|
80
|
+
`HostClient` are all unchanged; only *who launches the host* moves.
|
|
81
|
+
|
|
82
|
+
- **macOS** — a `launchd` LaunchAgent (`~/Library/LaunchAgents/<label>.plist`).
|
|
83
|
+
- **Linux** — a `systemd --user` unit (`~/.config/systemd/user/<label>.service`).
|
|
84
|
+
- **Windows** — a per-user **scheduled task** (`schtasks … /SC ONLOGON /RL LIMITED`,
|
|
85
|
+
no elevation) driving a small launcher `.cmd`.
|
|
86
|
+
|
|
87
|
+
```ts
|
|
88
|
+
import { ensureHostService } from "@particle-academy/fancy-term-host/service";
|
|
89
|
+
|
|
90
|
+
const result = await ensureHostService({
|
|
91
|
+
label: "academy.particle.genie.ptyhost",
|
|
92
|
+
userDataDir: app.getPath("userData"),
|
|
93
|
+
// A STANDALONE node — never your Electron binary. Auto-resolved from
|
|
94
|
+
// $FANCY_TERM_NODE / PATH if omitted; pass one explicitly to be sure.
|
|
95
|
+
runtime: { nodePath: "/opt/genie/runtime/node", nodePtyDir: "/opt/genie/native" },
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
if (result.ok) {
|
|
99
|
+
// service is installed + running; connect with the usual HostClient path.
|
|
100
|
+
} else {
|
|
101
|
+
// graceful fallback — never throws. Drop to the detached spawn (survives a
|
|
102
|
+
// normal quit) or in-process. result.error says why.
|
|
103
|
+
}
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
`ensureHostService` install-if-missing-or-stale → start-if-stopped, and is
|
|
107
|
+
**revision-stamped** (the default revision encodes the host `PROTOCOL_VERSION`),
|
|
108
|
+
so a protocol bump reinstalls the unit instead of leaving an incompatible host
|
|
109
|
+
running. Also exported: `installHostService` / `uninstallHostService` /
|
|
110
|
+
`startHostService` / `stopHostService` / `isServiceInstalled` / `serviceStatus`,
|
|
111
|
+
plus `resolveServiceRuntime` and the pure `buildServiceDescriptor`. Full guide +
|
|
112
|
+
the node-pty ABI notes: [`docs/service.md`](./service.md).
|
|
113
|
+
|
|
114
|
+
> Upgrading the service across a host-protocol bump restarts the host — snapshot
|
|
115
|
+
> live sessions (the T1 path) or call `shutdownHost()` first if you need history
|
|
116
|
+
> to survive the handoff.
|
|
117
|
+
|
|
70
118
|
## OSC-7 cwd tracking (Tier 1.5)
|
|
71
119
|
|
|
72
120
|
So a resumed shell starts in the *right directory*, the host learns each
|