@particle-academy/fancy-term-host 0.1.2 → 0.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/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.cjs +18 -1
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +24 -9
- 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/pty-host.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/pty-host.ts"],"names":["socketPath"],"mappings":"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","file":"pty-host.js","sourcesContent":["/**\n * Genie detached pty-host (Tier 3).\n *\n * A HEADLESS Node process — NO electron import — that owns the real node-pty\n * instances so they survive a full quit of the Electron app. The in-app\n * HostClient connects over a local socket (named pipe on Windows, unix domain\n * socket on POSIX) and proxies create/write/resize/kill; the host pushes back\n * `data`/`exit`. The host keeps its OWN scrollback ring buffer per pty so a\n * reattach AFTER a full quit can replay history.\n *\n * Launched detached by background.ts:\n * spawn(process.execPath, [hostScript], {\n * detached: true, stdio: 'ignore',\n * env: { ELECTRON_RUN_AS_NODE: '1', GENIE_USERDATA: <userData>, … }\n * }).unref()\n *\n * ELECTRON_RUN_AS_NODE makes Electron's binary run as plain Node so node-pty's\n * native ABI matches the one the app was built against (critical — a system Node\n * with a different ABI would fail to load the .node).\n *\n * Self-terminates after an idle period with zero live ptys AND no connected\n * client, so a host can never become a forever-orphan.\n */\n\nimport net from 'node:net';\nimport fs from 'node:fs';\nimport path from 'node:path';\nimport { spawn, IPty } from 'node-pty';\nimport {\n encodeFrame,\n FrameDecoder,\n PROTOCOL_VERSION,\n type ClientMessage,\n type HostMessage,\n} from './host-protocol';\nimport { socketPathFor, pidfilePath } from './host-locate';\nimport { resolveSpawnCwd } from './cwd';\n\nconst SCROLLBACK_MAX = 1_000_000;\n/** Self-exit after this long with no ptys AND no connected client. */\nconst IDLE_TIMEOUT_MS = 10 * 60 * 1000;\nconst IDLE_CHECK_MS = 60 * 1000;\n\nconst userData = process.env.GENIE_USERDATA;\nif (!userData) {\n // Without a userData path we can't write a pidfile the client can find.\n process.exit(2);\n}\n\ninterface HostPty {\n pty: IPty;\n shell: string;\n scrollback: string;\n}\n\nconst ptys = new Map<string, HostPty>();\nconst clients = new Set<net.Socket>();\nlet lastActivity = Date.now();\n/** The listening server, set once startServer binds — used by graceful shutdown. */\nlet activeServer: net.Server | null = null;\n\nfunction broadcast(msg: HostMessage): void {\n const frame = encodeFrame(msg);\n for (const sock of clients) {\n try {\n sock.write(frame);\n } catch {\n /* dropped client — close handler cleans it up */\n }\n }\n}\n\nfunction createPty(opts: {\n id: string;\n cwd: string;\n shell?: string;\n args?: string[];\n cols?: number;\n rows?: number;\n env?: Record<string, string>;\n}): { pid: number; shell: string; existing: boolean; scrollback: string } {\n const existing = ptys.get(opts.id);\n if (existing) {\n return {\n pid: existing.pty.pid,\n shell: existing.shell,\n existing: true,\n scrollback: existing.scrollback,\n };\n }\n const shell = opts.shell ?? defaultShell();\n const env = { ...process.env, ...(opts.env ?? {}) } as Record<string, string>;\n env.TERM = env.TERM || 'xterm-256color';\n\n const pty = spawn(shell, opts.args ?? [], {\n name: 'xterm-color',\n // Native-convert + validate the requested dir; a stale/foreign/MSYS cwd\n // (e.g. Git Bash's /c/Users/me) would otherwise crash spawn with Windows\n // ERROR_DIRECTORY (267). Falls back to home if unusable.\n cwd: resolveSpawnCwd(opts.cwd),\n cols: opts.cols ?? 80,\n rows: opts.rows ?? 24,\n env,\n });\n\n const entry: HostPty = { pty, shell, scrollback: '' };\n ptys.set(opts.id, entry);\n\n pty.onData((data) => {\n const next = entry.scrollback + data;\n entry.scrollback =\n next.length > SCROLLBACK_MAX ? next.slice(-SCROLLBACK_MAX) : next;\n broadcast({ kind: 'data', id: opts.id, data });\n });\n pty.onExit(({ exitCode, signal }) => {\n ptys.delete(opts.id);\n broadcast({ kind: 'exit', id: opts.id, exitCode, signal });\n lastActivity = Date.now();\n });\n\n return { pid: pty.pid, shell, existing: false, scrollback: '' };\n}\n\nfunction defaultShell(): string {\n if (process.platform === 'win32') return process.env.COMSPEC ?? 'cmd.exe';\n return process.env.SHELL ?? '/bin/bash';\n}\n\nfunction handleClientMessage(sock: net.Socket, msg: ClientMessage): void {\n lastActivity = Date.now();\n switch (msg.kind) {\n case 'hello':\n reply(sock, {\n kind: 'hello-ok',\n seq: msg.seq,\n protocolVersion: PROTOCOL_VERSION,\n pid: process.pid,\n });\n break;\n case 'create': {\n const r = createPty(msg.opts);\n reply(sock, {\n kind: 'created',\n seq: msg.seq,\n result: {\n id: msg.opts.id,\n pid: r.pid,\n shell: r.shell,\n existing: r.existing,\n scrollback: r.scrollback,\n },\n });\n break;\n }\n case 'write': {\n const e = ptys.get(msg.id);\n if (e) e.pty.write(msg.data);\n break;\n }\n case 'resize': {\n const e = ptys.get(msg.id);\n if (e) {\n try {\n e.pty.resize(Math.max(1, msg.cols | 0), Math.max(1, msg.rows | 0));\n } catch {\n /* transient 0×0 during layout */\n }\n }\n break;\n }\n case 'kill': {\n const e = ptys.get(msg.id);\n if (e) {\n try {\n e.pty.kill();\n } catch {\n /* already exited */\n }\n ptys.delete(msg.id);\n }\n break;\n }\n case 'list':\n reply(sock, {\n kind: 'list-result',\n seq: msg.seq,\n terminals: Array.from(ptys.entries()).map(([id, e]) => ({\n id,\n pid: e.pty.pid,\n shell: e.shell,\n })),\n });\n break;\n case 'set-retained':\n // The host keeps EVERYTHING alive across quit regardless; the\n // retained flag is meaningful to the client (fallback/UX). The host\n // only needs to not-die, which it doesn't. Acknowledge by no-op.\n break;\n case 'get-scrollback':\n reply(sock, {\n kind: 'scrollback-result',\n seq: msg.seq,\n scrollback: ptys.get(msg.id)?.scrollback ?? null,\n });\n break;\n case 'ping':\n reply(sock, { kind: 'pong', seq: msg.seq });\n break;\n case 'shutdown':\n // Graceful teardown: ack first so the client can finalize (it has\n // already snapshotted from its mirror), then kill every pty, remove\n // the pidfile/socket, and exit promptly. This is the clean path the\n // SIGKILL-by-pidfile interim fix can't take — it lets a consumer\n // bring the host down deterministically (e.g. before an auto-update)\n // instead of waiting on the 10-min idle timeout.\n reply(sock, { kind: 'shutdown-ok', seq: msg.seq });\n shutdown();\n break;\n }\n}\n\nfunction reply(sock: net.Socket, msg: HostMessage): void {\n try {\n sock.write(encodeFrame(msg));\n } catch {\n /* client gone */\n }\n}\n\nfunction startServer(socketPath: string): void {\n // On POSIX a stale socket file blocks bind; remove it first. (On Windows the\n // pipe namespace handles this.)\n if (process.platform !== 'win32') {\n try {\n fs.rmSync(socketPath, { force: true });\n } catch {\n /* ignore */\n }\n try {\n fs.mkdirSync(path.dirname(socketPath), { recursive: true });\n } catch {\n /* ignore */\n }\n }\n\n const server = net.createServer((sock) => {\n clients.add(sock);\n lastActivity = Date.now();\n const decoder = new FrameDecoder();\n sock.on('data', (chunk: Buffer) => {\n const frames = decoder.push(chunk);\n if (decoder.desynced) {\n try {\n sock.destroy();\n } catch {\n /* ignore */\n }\n return;\n }\n for (const f of frames) handleClientMessage(sock, f as ClientMessage);\n });\n const drop = () => {\n clients.delete(sock);\n lastActivity = Date.now();\n };\n sock.on('close', drop);\n sock.on('error', drop);\n });\n activeServer = server;\n\n server.on('error', (err) => {\n // EADDRINUSE: another host beat us to it. Exit quietly — the client will\n // connect to the winner.\n // eslint-disable-next-line no-console\n console.error('[pty-host] server error:', (err as Error).message);\n process.exit(3);\n });\n\n server.listen(socketPath, () => {\n try {\n writePidfileLocal(socketPath);\n } catch (err) {\n // eslint-disable-next-line no-console\n console.error('[pty-host] pidfile write failed:', (err as Error).message);\n }\n });\n\n // Idle watchdog: exit when nothing is running and nobody is connected.\n const idle = setInterval(() => {\n if (ptys.size === 0 && clients.size === 0 && Date.now() - lastActivity > IDLE_TIMEOUT_MS) {\n cleanupAndExit(socketPath, server);\n }\n }, IDLE_CHECK_MS);\n if (typeof idle.unref === 'function') idle.unref();\n}\n\nfunction writePidfileLocal(socketPath: string): void {\n const target = pidfilePath(userData!);\n const tmp = `${target}.tmp`;\n fs.writeFileSync(\n tmp,\n JSON.stringify({\n pid: process.pid,\n socketPath,\n protocolVersion: PROTOCOL_VERSION,\n startedAt: Date.now(),\n }),\n );\n fs.renameSync(tmp, target);\n}\n\nfunction cleanupAndExit(socketPath: string, server: net.Server): void {\n try {\n // Only remove the pidfile if it still points at US (avoid clobbering a\n // successor host that took over the socket).\n const pf = JSON.parse(fs.readFileSync(pidfilePath(userData!), 'utf8'));\n if (pf?.pid === process.pid) fs.rmSync(pidfilePath(userData!), { force: true });\n } catch {\n /* ignore */\n }\n if (process.platform !== 'win32') {\n try {\n fs.rmSync(socketPath, { force: true });\n } catch {\n /* ignore */\n }\n }\n try {\n server.close();\n } catch {\n /* ignore */\n }\n process.exit(0);\n}\n\n/**\n * Graceful host shutdown — the clean counterpart to the idle watchdog and to a\n * SIGKILL-by-pidfile. Kills every live pty (so no orphaned shells linger), then\n * removes the pidfile/socket, closes the server, and exits. Triggered by a\n * `shutdown` client request; the client has already snapshotted its mirror, so\n * tearing down the host here loses nothing.\n */\nfunction shutdown(): void {\n for (const [, e] of ptys) {\n try {\n e.pty.kill();\n } catch {\n /* already exited */\n }\n }\n ptys.clear();\n if (activeServer) {\n cleanupAndExit(socketPath, activeServer);\n } else {\n process.exit(0);\n }\n}\n\n// --- main ------------------------------------------------------------------\n\nconst socketPath = socketPathFor(userData);\n\n// A dead-mans-switch so we don't keep a host with no shells AND no client when\n// the parent vanished without a clean disconnect: covered by the idle watchdog.\nprocess.on('uncaughtException', (err) => {\n // eslint-disable-next-line no-console\n console.error('[pty-host] uncaught:', err);\n});\n\nstartServer(socketPath);\n"]}
|
|
1
|
+
{"version":3,"sources":["../src/pty-host.ts"],"names":["socketPath"],"mappings":"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","file":"pty-host.js","sourcesContent":["/**\n * Genie detached pty-host (Tier 3).\n *\n * A HEADLESS Node process — NO electron import — that owns the real node-pty\n * instances so they survive a full quit of the Electron app. The in-app\n * HostClient connects over a local socket (named pipe on Windows, unix domain\n * socket on POSIX) and proxies create/write/resize/kill; the host pushes back\n * `data`/`exit`. The host keeps its OWN scrollback ring buffer per pty so a\n * reattach AFTER a full quit can replay history.\n *\n * Launched detached by background.ts:\n * spawn(process.execPath, [hostScript], {\n * detached: true, stdio: 'ignore',\n * env: { ELECTRON_RUN_AS_NODE: '1', GENIE_USERDATA: <userData>, … }\n * }).unref()\n *\n * ELECTRON_RUN_AS_NODE makes Electron's binary run as plain Node so node-pty's\n * native ABI matches the one the app was built against (critical — a system Node\n * with a different ABI would fail to load the .node).\n *\n * Self-terminates after an idle period with zero live ptys AND no connected\n * client, so a host can never become a forever-orphan.\n */\n\nimport net from 'node:net';\nimport fs from 'node:fs';\nimport path from 'node:path';\nimport { spawn, IPty } from 'node-pty';\nimport {\n encodeFrame,\n FrameDecoder,\n PROTOCOL_VERSION,\n type ClientMessage,\n type HostMessage,\n} from './host-protocol';\nimport { socketPathFor, pidfilePath } from './host-locate';\nimport { resolveSpawnCwd } from './cwd';\n\nconst SCROLLBACK_MAX = 1_000_000;\n/** Self-exit after this long with no ptys AND no connected client. */\nconst IDLE_TIMEOUT_MS = 10 * 60 * 1000;\nconst IDLE_CHECK_MS = 60 * 1000;\n\nconst userData = process.env.GENIE_USERDATA;\nif (!userData) {\n // Without a userData path we can't write a pidfile the client can find.\n process.exit(2);\n}\n\ninterface HostPty {\n pty: IPty;\n shell: string;\n scrollback: string;\n}\n\nconst ptys = new Map<string, HostPty>();\nconst clients = new Set<net.Socket>();\nlet lastActivity = Date.now();\n/** The listening server, set once startServer binds — used by graceful shutdown. */\nlet activeServer: net.Server | null = null;\n\nfunction broadcast(msg: HostMessage): void {\n const frame = encodeFrame(msg);\n for (const sock of clients) {\n try {\n sock.write(frame);\n } catch {\n /* dropped client — close handler cleans it up */\n }\n }\n}\n\nfunction createPty(opts: {\n id: string;\n cwd: string;\n shell?: string;\n args?: string[];\n cols?: number;\n rows?: number;\n env?: Record<string, string>;\n}): { pid: number; shell: string; existing: boolean; scrollback: string } {\n const existing = ptys.get(opts.id);\n if (existing) {\n return {\n pid: existing.pty.pid,\n shell: existing.shell,\n existing: true,\n scrollback: existing.scrollback,\n };\n }\n const shell = opts.shell ?? defaultShell();\n const env = { ...process.env, ...(opts.env ?? {}) } as Record<string, string>;\n env.TERM = env.TERM || 'xterm-256color';\n\n const pty = spawn(shell, opts.args ?? [], {\n name: 'xterm-color',\n // Native-convert + validate the requested dir; a stale/foreign/MSYS cwd\n // (e.g. Git Bash's /c/Users/me) would otherwise crash spawn with Windows\n // ERROR_DIRECTORY (267). Falls back to home if unusable.\n cwd: resolveSpawnCwd(opts.cwd),\n cols: opts.cols ?? 80,\n rows: opts.rows ?? 24,\n env,\n });\n\n const entry: HostPty = { pty, shell, scrollback: '' };\n ptys.set(opts.id, entry);\n\n pty.onData((data) => {\n const next = entry.scrollback + data;\n entry.scrollback =\n next.length > SCROLLBACK_MAX ? next.slice(-SCROLLBACK_MAX) : next;\n broadcast({ kind: 'data', id: opts.id, data });\n });\n pty.onExit(({ exitCode, signal }) => {\n ptys.delete(opts.id);\n broadcast({ kind: 'exit', id: opts.id, exitCode, signal });\n lastActivity = Date.now();\n });\n\n return { pid: pty.pid, shell, existing: false, scrollback: '' };\n}\n\nfunction defaultShell(): string {\n if (process.platform === 'win32') return process.env.COMSPEC ?? 'cmd.exe';\n return process.env.SHELL ?? '/bin/bash';\n}\n\nfunction handleClientMessage(sock: net.Socket, msg: ClientMessage): void {\n lastActivity = Date.now();\n switch (msg.kind) {\n case 'hello':\n reply(sock, {\n kind: 'hello-ok',\n seq: msg.seq,\n protocolVersion: PROTOCOL_VERSION,\n pid: process.pid,\n });\n break;\n case 'create': {\n const r = createPty(msg.opts);\n reply(sock, {\n kind: 'created',\n seq: msg.seq,\n result: {\n id: msg.opts.id,\n pid: r.pid,\n shell: r.shell,\n existing: r.existing,\n scrollback: r.scrollback,\n },\n });\n break;\n }\n case 'write': {\n const e = ptys.get(msg.id);\n if (e) e.pty.write(msg.data);\n break;\n }\n case 'resize': {\n const e = ptys.get(msg.id);\n if (e) {\n try {\n e.pty.resize(Math.max(1, msg.cols | 0), Math.max(1, msg.rows | 0));\n } catch {\n /* transient 0×0 during layout */\n }\n }\n break;\n }\n case 'kill': {\n const e = ptys.get(msg.id);\n if (e) {\n try {\n e.pty.kill();\n } catch {\n /* already exited */\n }\n ptys.delete(msg.id);\n }\n break;\n }\n case 'list':\n reply(sock, {\n kind: 'list-result',\n seq: msg.seq,\n terminals: Array.from(ptys.entries()).map(([id, e]) => ({\n id,\n pid: e.pty.pid,\n shell: e.shell,\n })),\n });\n break;\n case 'set-retained':\n // The host keeps EVERYTHING alive across quit regardless; the\n // retained flag is meaningful to the client (fallback/UX). The host\n // only needs to not-die, which it doesn't. Acknowledge by no-op.\n break;\n case 'get-scrollback':\n reply(sock, {\n kind: 'scrollback-result',\n seq: msg.seq,\n scrollback: ptys.get(msg.id)?.scrollback ?? null,\n });\n break;\n case 'ping':\n reply(sock, { kind: 'pong', seq: msg.seq });\n break;\n case 'shutdown':\n // Graceful teardown: ack first so the client can finalize (it has\n // already snapshotted from its mirror), then kill every pty, remove\n // the pidfile/socket, and exit promptly. This is the clean path the\n // SIGKILL-by-pidfile interim fix can't take — it lets a consumer\n // bring the host down deterministically (e.g. before an auto-update)\n // instead of waiting on the 10-min idle timeout.\n reply(sock, { kind: 'shutdown-ok', seq: msg.seq });\n shutdown();\n break;\n }\n}\n\nfunction reply(sock: net.Socket, msg: HostMessage): void {\n try {\n sock.write(encodeFrame(msg));\n } catch {\n /* client gone */\n }\n}\n\nfunction startServer(socketPath: string): void {\n // On POSIX a stale socket file blocks bind; remove it first. (On Windows the\n // pipe namespace handles this.)\n if (process.platform !== 'win32') {\n try {\n fs.rmSync(socketPath, { force: true });\n } catch {\n /* ignore */\n }\n try {\n fs.mkdirSync(path.dirname(socketPath), { recursive: true });\n } catch {\n /* ignore */\n }\n }\n\n const server = net.createServer((sock) => {\n clients.add(sock);\n lastActivity = Date.now();\n const decoder = new FrameDecoder();\n sock.on('data', (chunk: Buffer) => {\n const frames = decoder.push(chunk);\n if (decoder.desynced) {\n try {\n sock.destroy();\n } catch {\n /* ignore */\n }\n return;\n }\n for (const f of frames) handleClientMessage(sock, f as ClientMessage);\n });\n const drop = () => {\n clients.delete(sock);\n lastActivity = Date.now();\n };\n sock.on('close', drop);\n sock.on('error', drop);\n });\n activeServer = server;\n\n server.on('error', (err) => {\n // EADDRINUSE: another host beat us to it. Exit quietly — the client will\n // connect to the winner.\n // eslint-disable-next-line no-console\n console.error('[pty-host] server error:', (err as Error).message);\n process.exit(3);\n });\n\n server.listen(socketPath, () => {\n try {\n writePidfileLocal(socketPath);\n } catch (err) {\n // eslint-disable-next-line no-console\n console.error('[pty-host] pidfile write failed:', (err as Error).message);\n }\n });\n\n // Idle watchdog: exit when nothing is running and nobody is connected.\n const idle = setInterval(() => {\n if (ptys.size === 0 && clients.size === 0 && Date.now() - lastActivity > IDLE_TIMEOUT_MS) {\n cleanupAndExit(socketPath, server);\n }\n }, IDLE_CHECK_MS);\n if (typeof idle.unref === 'function') idle.unref();\n}\n\nfunction writePidfileLocal(socketPath: string): void {\n const target = pidfilePath(userData!);\n const tmp = `${target}.tmp`;\n fs.writeFileSync(\n tmp,\n JSON.stringify({\n pid: process.pid,\n socketPath,\n protocolVersion: PROTOCOL_VERSION,\n startedAt: Date.now(),\n }),\n );\n fs.renameSync(tmp, target);\n}\n\nfunction cleanupAndExit(socketPath: string, server: net.Server): void {\n try {\n // Only remove the pidfile if it still points at US (avoid clobbering a\n // successor host that took over the socket).\n const pf = JSON.parse(fs.readFileSync(pidfilePath(userData!), 'utf8'));\n if (pf?.pid === process.pid) fs.rmSync(pidfilePath(userData!), { force: true });\n } catch {\n /* ignore */\n }\n if (process.platform !== 'win32') {\n try {\n fs.rmSync(socketPath, { force: true });\n } catch {\n /* ignore */\n }\n }\n try {\n server.close();\n } catch {\n /* ignore */\n }\n process.exit(0);\n}\n\n/**\n * Graceful host shutdown — the clean counterpart to the idle watchdog and to a\n * SIGKILL-by-pidfile. Kills every live pty (so no orphaned shells linger), then\n * removes the pidfile/socket, closes the server, and exits. Triggered by a\n * `shutdown` client request; the client has already snapshotted its mirror, so\n * tearing down the host here loses nothing.\n */\nfunction shutdown(): void {\n for (const [, e] of ptys) {\n try {\n e.pty.kill();\n } catch {\n /* already exited */\n }\n }\n ptys.clear();\n if (activeServer) {\n cleanupAndExit(socketPath, activeServer);\n } else {\n process.exit(0);\n }\n}\n\n// --- main ------------------------------------------------------------------\n\nconst socketPath = socketPathFor(userData);\n\n// A dead-mans-switch so we don't keep a host with no shells AND no client when\n// the parent vanished without a clean disconnect: covered by the idle watchdog.\nprocess.on('uncaughtException', (err) => {\n // eslint-disable-next-line no-console\n console.error('[pty-host] uncaught:', err);\n});\n\nstartServer(socketPath);\n"]}
|
package/dist/service.cjs
ADDED
|
@@ -0,0 +1,508 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
var path3 = require('path');
|
|
4
|
+
var url = require('url');
|
|
5
|
+
var os2 = require('os');
|
|
6
|
+
var fs = require('fs');
|
|
7
|
+
require('crypto');
|
|
8
|
+
var child_process = require('child_process');
|
|
9
|
+
var fs3 = require('fs/promises');
|
|
10
|
+
|
|
11
|
+
var _documentCurrentScript = typeof document !== 'undefined' ? document.currentScript : null;
|
|
12
|
+
function _interopDefault (e) { return e && e.__esModule ? e : { default: e }; }
|
|
13
|
+
|
|
14
|
+
var path3__default = /*#__PURE__*/_interopDefault(path3);
|
|
15
|
+
var os2__default = /*#__PURE__*/_interopDefault(os2);
|
|
16
|
+
var fs__default = /*#__PURE__*/_interopDefault(fs);
|
|
17
|
+
var fs3__default = /*#__PURE__*/_interopDefault(fs3);
|
|
18
|
+
|
|
19
|
+
// src/host-script.ts
|
|
20
|
+
|
|
21
|
+
// src/host-protocol.ts
|
|
22
|
+
var PROTOCOL_VERSION = 2;
|
|
23
|
+
|
|
24
|
+
// src/host-locate.ts
|
|
25
|
+
function resolveHostScript(dirname) {
|
|
26
|
+
const candidates = [
|
|
27
|
+
// Packaged: node-pty must be unpacked, so run the host from the unpacked
|
|
28
|
+
// tree too (its require('node-pty') resolves to the unpacked .node).
|
|
29
|
+
dirname.includes(`app.asar${path3__default.default.sep}`) || dirname.includes("app.asar/") ? dirname.replace(
|
|
30
|
+
/app\.asar([\\/])/,
|
|
31
|
+
`app.asar.unpacked$1`
|
|
32
|
+
) + path3__default.default.sep + "pty-host.js" : "",
|
|
33
|
+
// Same dir as the compiled main bundle (dev: app/pty-host.js).
|
|
34
|
+
path3__default.default.join(dirname, "pty-host.js"),
|
|
35
|
+
// Defensive: a sibling unpacked dir computed from the asar path.
|
|
36
|
+
path3__default.default.join(dirname.replace("app.asar", "app.asar.unpacked"), "pty-host.js")
|
|
37
|
+
].filter(Boolean);
|
|
38
|
+
for (const c of candidates) {
|
|
39
|
+
try {
|
|
40
|
+
if (fs__default.default.existsSync(c)) return c;
|
|
41
|
+
} catch {
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
return null;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// src/host-script.ts
|
|
48
|
+
function ptyHostScriptPath() {
|
|
49
|
+
const here = typeof __dirname !== "undefined" ? __dirname : path3__default.default.dirname(url.fileURLToPath((typeof document === 'undefined' ? require('u' + 'rl').pathToFileURL(__filename).href : (_documentCurrentScript && _documentCurrentScript.tagName.toUpperCase() === 'SCRIPT' && _documentCurrentScript.src || new URL('service.cjs', document.baseURI).href))));
|
|
50
|
+
return resolveHostScript(here) ?? path3__default.default.join(here, "pty-host.js");
|
|
51
|
+
}
|
|
52
|
+
var SERVICE_REVISION = `svc1+proto${PROTOCOL_VERSION}`;
|
|
53
|
+
var REVISION_MARKER = "fancy-term-service-revision";
|
|
54
|
+
function servicePlatformFor(platform = process.platform) {
|
|
55
|
+
switch (platform) {
|
|
56
|
+
case "darwin":
|
|
57
|
+
return "launchd";
|
|
58
|
+
case "linux":
|
|
59
|
+
return "systemd";
|
|
60
|
+
case "win32":
|
|
61
|
+
return "windows-task";
|
|
62
|
+
default:
|
|
63
|
+
return null;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
function buildServiceDescriptor(config, ctx = {}) {
|
|
67
|
+
const platform = ctx.platform ?? servicePlatformFor() ?? unsupported(config);
|
|
68
|
+
const home = ctx.home ?? os2__default.default.homedir();
|
|
69
|
+
switch (platform) {
|
|
70
|
+
case "launchd":
|
|
71
|
+
return launchd(config, home, ctx.uid ?? safeUid());
|
|
72
|
+
case "systemd":
|
|
73
|
+
return systemd(config, home);
|
|
74
|
+
case "windows-task":
|
|
75
|
+
return windowsTask(config);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
function unsupported(config) {
|
|
79
|
+
throw new Error(
|
|
80
|
+
`fancy-term-host service: unsupported platform for "${config.label}"`
|
|
81
|
+
);
|
|
82
|
+
}
|
|
83
|
+
function safeUid() {
|
|
84
|
+
const getuid = process.getuid;
|
|
85
|
+
return typeof getuid === "function" ? getuid() : 0;
|
|
86
|
+
}
|
|
87
|
+
function serviceEnv(config) {
|
|
88
|
+
const env = {
|
|
89
|
+
...config.env,
|
|
90
|
+
// GENIE_USERDATA is the var the existing detached-spawn path already uses
|
|
91
|
+
// (see host-lifecycle.spawnDetached); keep it so the host finds its data.
|
|
92
|
+
GENIE_USERDATA: config.userDataDir,
|
|
93
|
+
FANCY_TERM_SERVICE_REVISION: config.revision
|
|
94
|
+
};
|
|
95
|
+
if (config.runtime.nodePtyDir) {
|
|
96
|
+
env.NODE_PATH = config.runtime.nodePtyDir;
|
|
97
|
+
}
|
|
98
|
+
return env;
|
|
99
|
+
}
|
|
100
|
+
function launchd(config, home, uid) {
|
|
101
|
+
const label = config.label;
|
|
102
|
+
const plistPath = path3__default.default.posix.join(home, "Library", "LaunchAgents", `${label}.plist`);
|
|
103
|
+
const env = serviceEnv(config);
|
|
104
|
+
const outLog = path3__default.default.posix.join(config.logDir, "ptyhost.out.log");
|
|
105
|
+
const errLog = path3__default.default.posix.join(config.logDir, "ptyhost.err.log");
|
|
106
|
+
const envXml = Object.entries(env).map(
|
|
107
|
+
([k, v]) => ` <key>${xml(k)}</key>
|
|
108
|
+
<string>${xml(v)}</string>`
|
|
109
|
+
).join("\n");
|
|
110
|
+
const contents = `<?xml version="1.0" encoding="UTF-8"?>
|
|
111
|
+
<!-- ${REVISION_MARKER}: ${config.revision} -->
|
|
112
|
+
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
|
113
|
+
<plist version="1.0">
|
|
114
|
+
<dict>
|
|
115
|
+
<key>Label</key>
|
|
116
|
+
<string>${xml(label)}</string>
|
|
117
|
+
<key>ProgramArguments</key>
|
|
118
|
+
<array>
|
|
119
|
+
<string>${xml(config.runtime.nodePath)}</string>
|
|
120
|
+
<string>${xml(config.hostScript)}</string>
|
|
121
|
+
</array>
|
|
122
|
+
<key>EnvironmentVariables</key>
|
|
123
|
+
<dict>
|
|
124
|
+
${envXml}
|
|
125
|
+
</dict>
|
|
126
|
+
<key>RunAtLoad</key>
|
|
127
|
+
<true/>
|
|
128
|
+
<key>KeepAlive</key>
|
|
129
|
+
<false/>
|
|
130
|
+
<key>ProcessType</key>
|
|
131
|
+
<string>Background</string>
|
|
132
|
+
<key>StandardOutPath</key>
|
|
133
|
+
<string>${xml(outLog)}</string>
|
|
134
|
+
<key>StandardErrorPath</key>
|
|
135
|
+
<string>${xml(errLog)}</string>
|
|
136
|
+
</dict>
|
|
137
|
+
</plist>
|
|
138
|
+
`;
|
|
139
|
+
const domain = `gui/${uid}`;
|
|
140
|
+
const target = `${domain}/${label}`;
|
|
141
|
+
return {
|
|
142
|
+
platform: "launchd",
|
|
143
|
+
label,
|
|
144
|
+
revision: config.revision,
|
|
145
|
+
unitPath: plistPath,
|
|
146
|
+
unitContents: contents,
|
|
147
|
+
unitMode: 384,
|
|
148
|
+
// bootstrap loads + (RunAtLoad) starts it.
|
|
149
|
+
installArgv: [["launchctl", "bootstrap", domain, plistPath]],
|
|
150
|
+
uninstallArgv: [["launchctl", "bootout", target]],
|
|
151
|
+
startArgv: [["launchctl", "kickstart", "-k", target]],
|
|
152
|
+
stopArgv: [["launchctl", "kill", "SIGTERM", target]],
|
|
153
|
+
statusArgv: ["launchctl", "print", target],
|
|
154
|
+
removePaths: [plistPath]
|
|
155
|
+
};
|
|
156
|
+
}
|
|
157
|
+
function systemd(config, home) {
|
|
158
|
+
const unit = `${config.label}.service`;
|
|
159
|
+
const unitPath = path3__default.default.posix.join(home, ".config", "systemd", "user", unit);
|
|
160
|
+
const env = serviceEnv(config);
|
|
161
|
+
const envLines = Object.entries(env).map(([k, v]) => `Environment=${k}=${systemdEnvValue(v)}`).join("\n");
|
|
162
|
+
const contents = `# ${REVISION_MARKER}: ${config.revision}
|
|
163
|
+
[Unit]
|
|
164
|
+
Description=fancy-term pty-host (${config.label})
|
|
165
|
+
After=default.target
|
|
166
|
+
|
|
167
|
+
[Service]
|
|
168
|
+
Type=simple
|
|
169
|
+
ExecStart=${quoteForExec(config.runtime.nodePath)} ${quoteForExec(config.hostScript)}
|
|
170
|
+
${envLines}
|
|
171
|
+
Restart=no
|
|
172
|
+
|
|
173
|
+
[Install]
|
|
174
|
+
WantedBy=default.target
|
|
175
|
+
`;
|
|
176
|
+
return {
|
|
177
|
+
platform: "systemd",
|
|
178
|
+
label: config.label,
|
|
179
|
+
revision: config.revision,
|
|
180
|
+
unitPath,
|
|
181
|
+
unitContents: contents,
|
|
182
|
+
unitMode: 420,
|
|
183
|
+
installArgv: [
|
|
184
|
+
["systemctl", "--user", "daemon-reload"],
|
|
185
|
+
["systemctl", "--user", "enable", "--now", unit]
|
|
186
|
+
],
|
|
187
|
+
uninstallArgv: [["systemctl", "--user", "disable", "--now", unit]],
|
|
188
|
+
startArgv: [["systemctl", "--user", "start", unit]],
|
|
189
|
+
stopArgv: [["systemctl", "--user", "stop", unit]],
|
|
190
|
+
statusArgv: ["systemctl", "--user", "is-active", unit],
|
|
191
|
+
removePaths: [unitPath]
|
|
192
|
+
};
|
|
193
|
+
}
|
|
194
|
+
function windowsTask(config) {
|
|
195
|
+
const env = serviceEnv(config);
|
|
196
|
+
const cmdPath = path3__default.default.win32.join(config.userDataDir, `${sanitizeFileName(config.label)}.cmd`);
|
|
197
|
+
const setLines = Object.entries(env).map(([k, v]) => `set "${k}=${v}"`).join("\r\n");
|
|
198
|
+
const contents = [
|
|
199
|
+
"@echo off",
|
|
200
|
+
`rem ${REVISION_MARKER}: ${config.revision}`,
|
|
201
|
+
setLines,
|
|
202
|
+
`"${config.runtime.nodePath}" "${config.hostScript}"`,
|
|
203
|
+
""
|
|
204
|
+
].join("\r\n");
|
|
205
|
+
const taskName = config.label;
|
|
206
|
+
const tr = `cmd /c "${cmdPath}"`;
|
|
207
|
+
return {
|
|
208
|
+
platform: "windows-task",
|
|
209
|
+
label: config.label,
|
|
210
|
+
revision: config.revision,
|
|
211
|
+
unitPath: cmdPath,
|
|
212
|
+
unitContents: contents,
|
|
213
|
+
unitMode: 448,
|
|
214
|
+
installArgv: [
|
|
215
|
+
["schtasks", "/Create", "/TN", taskName, "/TR", tr, "/SC", "ONLOGON", "/RL", "LIMITED", "/F"]
|
|
216
|
+
],
|
|
217
|
+
uninstallArgv: [["schtasks", "/Delete", "/TN", taskName, "/F"]],
|
|
218
|
+
startArgv: [["schtasks", "/Run", "/TN", taskName]],
|
|
219
|
+
stopArgv: [["schtasks", "/End", "/TN", taskName]],
|
|
220
|
+
statusArgv: ["schtasks", "/Query", "/TN", taskName, "/FO", "LIST"],
|
|
221
|
+
removePaths: [cmdPath]
|
|
222
|
+
};
|
|
223
|
+
}
|
|
224
|
+
function xml(s) {
|
|
225
|
+
return s.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """);
|
|
226
|
+
}
|
|
227
|
+
function systemdEnvValue(v) {
|
|
228
|
+
return /\s/.test(v) ? `"${v.replace(/"/g, '\\"')}"` : v;
|
|
229
|
+
}
|
|
230
|
+
function quoteForExec(p) {
|
|
231
|
+
return /\s/.test(p) ? `"${p}"` : p;
|
|
232
|
+
}
|
|
233
|
+
function sanitizeFileName(label) {
|
|
234
|
+
return label.replace(/[^A-Za-z0-9._-]+/g, "_");
|
|
235
|
+
}
|
|
236
|
+
function parseInstalledRevision(unitContents) {
|
|
237
|
+
const m = unitContents.match(
|
|
238
|
+
new RegExp(`${REVISION_MARKER}:\\s*(\\S+)`)
|
|
239
|
+
);
|
|
240
|
+
return m ? m[1] : null;
|
|
241
|
+
}
|
|
242
|
+
function resolveServiceRuntime(opts = {}) {
|
|
243
|
+
const env = opts.env ?? process.env;
|
|
244
|
+
const execPath = opts.execPath ?? process.execPath;
|
|
245
|
+
const isElectron = opts.isElectron ?? Boolean(process.versions?.electron);
|
|
246
|
+
const nodePtyDir = opts.nodePtyDir ?? env.FANCY_TERM_NODE_PTY ?? void 0;
|
|
247
|
+
const probe = opts.pathProbe ?? defaultPathProbe;
|
|
248
|
+
const finish = (nodePath, source) => ({
|
|
249
|
+
nodePath,
|
|
250
|
+
...nodePtyDir ? { nodePtyDir } : {},
|
|
251
|
+
source
|
|
252
|
+
});
|
|
253
|
+
if (opts.nodePath) return finish(opts.nodePath, "explicit");
|
|
254
|
+
if (env.FANCY_TERM_NODE) return finish(env.FANCY_TERM_NODE, "env:FANCY_TERM_NODE");
|
|
255
|
+
if (!isElectron && looksLikeNode(execPath)) {
|
|
256
|
+
return finish(execPath, "process.execPath");
|
|
257
|
+
}
|
|
258
|
+
const onPath = probe(nodeBinNames(), env);
|
|
259
|
+
if (onPath) return finish(onPath, "PATH");
|
|
260
|
+
return null;
|
|
261
|
+
}
|
|
262
|
+
function nodeBinNames() {
|
|
263
|
+
return process.platform === "win32" ? ["node.exe", "node"] : ["node"];
|
|
264
|
+
}
|
|
265
|
+
function looksLikeNode(execPath) {
|
|
266
|
+
const base = path3__default.default.basename(execPath).toLowerCase();
|
|
267
|
+
return base === "node" || base === "node.exe";
|
|
268
|
+
}
|
|
269
|
+
function defaultPathProbe(binNames, env) {
|
|
270
|
+
const PATH = env.PATH ?? env.Path ?? "";
|
|
271
|
+
const dirs = PATH.split(path3__default.default.delimiter).filter(Boolean);
|
|
272
|
+
for (const dir of dirs) {
|
|
273
|
+
for (const bin of binNames) {
|
|
274
|
+
const candidate = path3__default.default.join(dir, bin);
|
|
275
|
+
try {
|
|
276
|
+
if (fs__default.default.existsSync(candidate)) return candidate;
|
|
277
|
+
} catch {
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
return null;
|
|
282
|
+
}
|
|
283
|
+
function nodeServiceIo() {
|
|
284
|
+
return {
|
|
285
|
+
run(argv) {
|
|
286
|
+
const [cmd, ...args] = argv;
|
|
287
|
+
return new Promise((resolve) => {
|
|
288
|
+
let stdout = "";
|
|
289
|
+
let stderr = "";
|
|
290
|
+
const child = child_process.spawn(cmd, args, {
|
|
291
|
+
// schtasks/launchctl/systemctl resolve via the shell on win32.
|
|
292
|
+
shell: process.platform === "win32",
|
|
293
|
+
windowsHide: true
|
|
294
|
+
});
|
|
295
|
+
child.stdout?.on("data", (d) => stdout += d.toString());
|
|
296
|
+
child.stderr?.on("data", (d) => stderr += d.toString());
|
|
297
|
+
child.on(
|
|
298
|
+
"error",
|
|
299
|
+
(err) => resolve({ code: -1, stdout, stderr: stderr + String(err) })
|
|
300
|
+
);
|
|
301
|
+
child.on(
|
|
302
|
+
"close",
|
|
303
|
+
(code) => resolve({ code: code ?? -1, stdout, stderr })
|
|
304
|
+
);
|
|
305
|
+
});
|
|
306
|
+
},
|
|
307
|
+
async writeFile(p, contents, opts) {
|
|
308
|
+
await fs3__default.default.mkdir(path3__default.default.dirname(p), { recursive: true });
|
|
309
|
+
await fs3__default.default.writeFile(p, contents, { mode: opts?.mode ?? 384 });
|
|
310
|
+
},
|
|
311
|
+
async readFile(p) {
|
|
312
|
+
try {
|
|
313
|
+
return await fs3__default.default.readFile(p, "utf8");
|
|
314
|
+
} catch {
|
|
315
|
+
return null;
|
|
316
|
+
}
|
|
317
|
+
},
|
|
318
|
+
async mkdirp(dir) {
|
|
319
|
+
await fs3__default.default.mkdir(dir, { recursive: true });
|
|
320
|
+
},
|
|
321
|
+
async rm(p) {
|
|
322
|
+
await fs3__default.default.rm(p, { force: true }).catch(() => {
|
|
323
|
+
});
|
|
324
|
+
},
|
|
325
|
+
async exists(p) {
|
|
326
|
+
try {
|
|
327
|
+
await fs3__default.default.access(p);
|
|
328
|
+
return true;
|
|
329
|
+
} catch {
|
|
330
|
+
return false;
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
};
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
// src/service/index.ts
|
|
337
|
+
function isServiceSupported() {
|
|
338
|
+
return servicePlatformFor() !== null;
|
|
339
|
+
}
|
|
340
|
+
function resolveServiceConfig(config) {
|
|
341
|
+
const runtime = config.runtime ?? resolveServiceRuntime();
|
|
342
|
+
if (!runtime) {
|
|
343
|
+
throw new Error(
|
|
344
|
+
"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."
|
|
345
|
+
);
|
|
346
|
+
}
|
|
347
|
+
return {
|
|
348
|
+
label: config.label,
|
|
349
|
+
userDataDir: config.userDataDir,
|
|
350
|
+
hostScript: config.hostScript ?? ptyHostScriptPath(),
|
|
351
|
+
runtime,
|
|
352
|
+
env: config.env ?? {},
|
|
353
|
+
revision: config.revision ?? SERVICE_REVISION,
|
|
354
|
+
logDir: config.logDir ?? config.userDataDir
|
|
355
|
+
};
|
|
356
|
+
}
|
|
357
|
+
function descriptorFor(config) {
|
|
358
|
+
return buildServiceDescriptor(resolveServiceConfig(config));
|
|
359
|
+
}
|
|
360
|
+
async function runAll(io, argvList) {
|
|
361
|
+
for (const argv of argvList) {
|
|
362
|
+
const { code, stderr } = await io.run(argv);
|
|
363
|
+
if (code !== 0) {
|
|
364
|
+
throw new Error(
|
|
365
|
+
`command failed (${code}): ${argv.join(" ")}${stderr ? ` \u2014 ${stderr.trim()}` : ""}`
|
|
366
|
+
);
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
async function installHostService(config, io = nodeServiceIo()) {
|
|
371
|
+
const desc = descriptorFor(config);
|
|
372
|
+
await io.mkdirp(config.userDataDir);
|
|
373
|
+
await io.writeFile(desc.unitPath, desc.unitContents, { mode: desc.unitMode });
|
|
374
|
+
await runAll(io, desc.installArgv);
|
|
375
|
+
return serviceStatusFor(desc, io);
|
|
376
|
+
}
|
|
377
|
+
async function uninstallHostService(config, io = nodeServiceIo()) {
|
|
378
|
+
const desc = descriptorFor(config);
|
|
379
|
+
for (const argv of desc.uninstallArgv) {
|
|
380
|
+
await io.run(argv);
|
|
381
|
+
}
|
|
382
|
+
for (const p of desc.removePaths) {
|
|
383
|
+
await io.rm(p);
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
async function startHostService(config, io = nodeServiceIo()) {
|
|
387
|
+
await runAll(io, descriptorFor(config).startArgv);
|
|
388
|
+
}
|
|
389
|
+
async function stopHostService(config, io = nodeServiceIo()) {
|
|
390
|
+
await runAll(io, descriptorFor(config).stopArgv);
|
|
391
|
+
}
|
|
392
|
+
async function isServiceInstalled(config, io = nodeServiceIo()) {
|
|
393
|
+
return io.exists(descriptorFor(config).unitPath);
|
|
394
|
+
}
|
|
395
|
+
async function serviceStatus(config, io = nodeServiceIo()) {
|
|
396
|
+
return serviceStatusFor(descriptorFor(config), io);
|
|
397
|
+
}
|
|
398
|
+
async function serviceStatusFor(desc, io) {
|
|
399
|
+
const unit = await io.readFile(desc.unitPath);
|
|
400
|
+
const installed = unit !== null;
|
|
401
|
+
const installedRevision = unit ? parseInstalledRevision(unit) ?? void 0 : void 0;
|
|
402
|
+
let running = false;
|
|
403
|
+
let detail;
|
|
404
|
+
if (installed) {
|
|
405
|
+
const { code, stdout, stderr } = await io.run(desc.statusArgv);
|
|
406
|
+
detail = (stdout || stderr).trim() || void 0;
|
|
407
|
+
running = isRunningOutput(desc, code, stdout);
|
|
408
|
+
}
|
|
409
|
+
return {
|
|
410
|
+
platform: desc.platform,
|
|
411
|
+
state: !installed ? "not-installed" : running ? "running" : "installed",
|
|
412
|
+
installed,
|
|
413
|
+
running,
|
|
414
|
+
label: desc.label,
|
|
415
|
+
unitPath: desc.unitPath,
|
|
416
|
+
installedRevision,
|
|
417
|
+
detail
|
|
418
|
+
};
|
|
419
|
+
}
|
|
420
|
+
function isRunningOutput(desc, code, stdout) {
|
|
421
|
+
const out = stdout.toLowerCase();
|
|
422
|
+
switch (desc.platform) {
|
|
423
|
+
case "systemd":
|
|
424
|
+
return code === 0 && out.includes("active") && !out.includes("inactive");
|
|
425
|
+
case "launchd":
|
|
426
|
+
return code === 0 && (out.includes("state = running") || out.includes("pid ="));
|
|
427
|
+
case "windows-task":
|
|
428
|
+
return code === 0 && out.includes("running");
|
|
429
|
+
}
|
|
430
|
+
}
|
|
431
|
+
async function ensureHostService(config, io = nodeServiceIo()) {
|
|
432
|
+
if (!isServiceSupported()) {
|
|
433
|
+
return {
|
|
434
|
+
ok: false,
|
|
435
|
+
installed: false,
|
|
436
|
+
running: false,
|
|
437
|
+
action: "unsupported",
|
|
438
|
+
error: `unsupported platform: ${process.platform}`
|
|
439
|
+
};
|
|
440
|
+
}
|
|
441
|
+
let resolved;
|
|
442
|
+
try {
|
|
443
|
+
resolved = resolveServiceConfig(config);
|
|
444
|
+
} catch (err) {
|
|
445
|
+
return {
|
|
446
|
+
ok: false,
|
|
447
|
+
installed: false,
|
|
448
|
+
running: false,
|
|
449
|
+
action: "failed",
|
|
450
|
+
error: err.message
|
|
451
|
+
};
|
|
452
|
+
}
|
|
453
|
+
const desc = buildServiceDescriptor(resolved);
|
|
454
|
+
const runtime = resolved.runtime;
|
|
455
|
+
try {
|
|
456
|
+
const status = await serviceStatusFor(desc, io);
|
|
457
|
+
if (status.installed && status.installedRevision === resolved.revision) {
|
|
458
|
+
if (status.running) {
|
|
459
|
+
return { ok: true, installed: true, running: true, action: "already-running", runtime };
|
|
460
|
+
}
|
|
461
|
+
await runAll(io, desc.startArgv);
|
|
462
|
+
return { ok: true, installed: true, running: true, action: "started", runtime };
|
|
463
|
+
}
|
|
464
|
+
if (status.installed) {
|
|
465
|
+
for (const argv of desc.uninstallArgv) await io.run(argv);
|
|
466
|
+
for (const p of desc.removePaths) await io.rm(p);
|
|
467
|
+
}
|
|
468
|
+
await io.mkdirp(resolved.userDataDir);
|
|
469
|
+
await io.writeFile(desc.unitPath, desc.unitContents, { mode: desc.unitMode });
|
|
470
|
+
await runAll(io, desc.installArgv);
|
|
471
|
+
await runAll(io, desc.startArgv).catch(() => {
|
|
472
|
+
});
|
|
473
|
+
return {
|
|
474
|
+
ok: true,
|
|
475
|
+
installed: true,
|
|
476
|
+
running: true,
|
|
477
|
+
action: status.installed ? "reinstalled" : "installed-and-started",
|
|
478
|
+
runtime
|
|
479
|
+
};
|
|
480
|
+
} catch (err) {
|
|
481
|
+
return {
|
|
482
|
+
ok: false,
|
|
483
|
+
installed: false,
|
|
484
|
+
running: false,
|
|
485
|
+
action: "failed",
|
|
486
|
+
error: err.message,
|
|
487
|
+
runtime
|
|
488
|
+
};
|
|
489
|
+
}
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
exports.SERVICE_REVISION = SERVICE_REVISION;
|
|
493
|
+
exports.buildServiceDescriptor = buildServiceDescriptor;
|
|
494
|
+
exports.ensureHostService = ensureHostService;
|
|
495
|
+
exports.installHostService = installHostService;
|
|
496
|
+
exports.isServiceInstalled = isServiceInstalled;
|
|
497
|
+
exports.isServiceSupported = isServiceSupported;
|
|
498
|
+
exports.nodeServiceIo = nodeServiceIo;
|
|
499
|
+
exports.parseInstalledRevision = parseInstalledRevision;
|
|
500
|
+
exports.resolveServiceConfig = resolveServiceConfig;
|
|
501
|
+
exports.resolveServiceRuntime = resolveServiceRuntime;
|
|
502
|
+
exports.servicePlatformFor = servicePlatformFor;
|
|
503
|
+
exports.serviceStatus = serviceStatus;
|
|
504
|
+
exports.startHostService = startHostService;
|
|
505
|
+
exports.stopHostService = stopHostService;
|
|
506
|
+
exports.uninstallHostService = uninstallHostService;
|
|
507
|
+
//# sourceMappingURL=service.cjs.map
|
|
508
|
+
//# sourceMappingURL=service.cjs.map
|