ghostrail 0.8.0 → 0.10.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/dist/cli/commands.d.ts +32 -1
- package/dist/cli/commands.d.ts.map +1 -1
- package/dist/cli/commands.js +409 -5
- package/dist/cli/commands.js.map +1 -1
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +11 -1
- package/dist/cli.js.map +1 -1
- package/dist/commands/help.d.ts.map +1 -1
- package/dist/commands/help.js +49 -0
- package/dist/commands/help.js.map +1 -1
- package/dist/dashboard/index.d.ts +4 -0
- package/dist/dashboard/index.d.ts.map +1 -0
- package/dist/dashboard/index.js +4 -0
- package/dist/dashboard/index.js.map +1 -0
- package/dist/dashboard/page.d.ts +10 -0
- package/dist/dashboard/page.d.ts.map +1 -0
- package/dist/dashboard/page.js +94 -0
- package/dist/dashboard/page.js.map +1 -0
- package/dist/dashboard/routes.d.ts +48 -0
- package/dist/dashboard/routes.d.ts.map +1 -0
- package/dist/dashboard/routes.js +102 -0
- package/dist/dashboard/routes.js.map +1 -0
- package/dist/dashboard/server.d.ts +43 -0
- package/dist/dashboard/server.d.ts.map +1 -0
- package/dist/dashboard/server.js +110 -0
- package/dist/dashboard/server.js.map +1 -0
- package/dist/global/index.d.ts +1 -0
- package/dist/global/index.d.ts.map +1 -1
- package/dist/global/index.js +1 -0
- package/dist/global/index.js.map +1 -1
- package/dist/global/registry.d.ts +63 -0
- package/dist/global/registry.d.ts.map +1 -0
- package/dist/global/registry.js +120 -0
- package/dist/global/registry.js.map +1 -0
- package/dist/global/store.d.ts +10 -0
- package/dist/global/store.d.ts.map +1 -1
- package/dist/global/store.js +25 -1
- package/dist/global/store.js.map +1 -1
- package/dist/service/index.d.ts +3 -0
- package/dist/service/index.d.ts.map +1 -0
- package/dist/service/index.js +3 -0
- package/dist/service/index.js.map +1 -0
- package/dist/service/manager.d.ts +71 -0
- package/dist/service/manager.d.ts.map +1 -0
- package/dist/service/manager.js +125 -0
- package/dist/service/manager.js.map +1 -0
- package/dist/service/unit.d.ts +71 -0
- package/dist/service/unit.d.ts.map +1 -0
- package/dist/service/unit.js +138 -0
- package/dist/service/unit.js.map +1 -0
- package/package.json +1 -1
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import { type UnitSpec } from "./unit.js";
|
|
2
|
+
/**
|
|
3
|
+
* Talking to the platform's user service manager. Detection is deliberate:
|
|
4
|
+
* "no service manager" and "a service manager this does not support yet" are
|
|
5
|
+
* different answers and deserve different messages.
|
|
6
|
+
*/
|
|
7
|
+
export type ManagerKind = "systemd" | "launchd" | "none";
|
|
8
|
+
/** Which manager this platform offers, whether or not it is supported. */
|
|
9
|
+
export declare function detectManager(): Promise<ManagerKind>;
|
|
10
|
+
/** Where systemd looks for this user's units. */
|
|
11
|
+
export declare function systemdUnitDir(env?: NodeJS.ProcessEnv): string;
|
|
12
|
+
/** What `install` did with one unit, so the caller can report it honestly. */
|
|
13
|
+
export type InstallOutcome = "written" | "unchanged" | "hand-edited";
|
|
14
|
+
/**
|
|
15
|
+
* Write a unit unless it is already right, or has been edited.
|
|
16
|
+
*
|
|
17
|
+
* An edited unit is never overwritten. Same instinct as `init --update`: a
|
|
18
|
+
* customization is deliberate until proven otherwise, and silently reverting
|
|
19
|
+
* one is the kind of thing that makes a tool untrustworthy.
|
|
20
|
+
*/
|
|
21
|
+
export declare function installUnit(spec: UnitSpec, env?: NodeJS.ProcessEnv): Promise<{
|
|
22
|
+
outcome: InstallOutcome;
|
|
23
|
+
unit: string;
|
|
24
|
+
file: string;
|
|
25
|
+
}>;
|
|
26
|
+
/** Reload the manager so a newly written unit is visible. */
|
|
27
|
+
export declare function reload(): Promise<void>;
|
|
28
|
+
/** Enable and start a unit. Returns an error message, or undefined on success. */
|
|
29
|
+
export declare function enableAndStart(unit: string): Promise<string | undefined>;
|
|
30
|
+
/**
|
|
31
|
+
* Stop and disable a unit.
|
|
32
|
+
*
|
|
33
|
+
* This can genuinely take minutes: an in-flight tick is not interrupted, so the
|
|
34
|
+
* manager waits out the current agent run. Callers should say so before this
|
|
35
|
+
* starts rather than letting it look hung.
|
|
36
|
+
*/
|
|
37
|
+
export declare function disableAndStop(unit: string): Promise<string | undefined>;
|
|
38
|
+
/** Remove a unit file. Absent is success: the goal is that it is not there. */
|
|
39
|
+
export declare function removeUnit(unit: string, env?: NodeJS.ProcessEnv): Promise<void>;
|
|
40
|
+
/** Active state per unit, for the units asked about. */
|
|
41
|
+
export declare function unitStates(units: readonly string[]): Promise<Map<string, string>>;
|
|
42
|
+
/**
|
|
43
|
+
* The service manager, behind a seam.
|
|
44
|
+
*
|
|
45
|
+
* This is injectable for one blunt reason: without it, a test that calls
|
|
46
|
+
* `ghostrail start` installs and enables real user services on the developer's
|
|
47
|
+
* machine. That is not hypothetical. Writing these tests did exactly that,
|
|
48
|
+
* leaving four enabled units crash-looping against deleted temp directories,
|
|
49
|
+
* because the default unit directory falls back to the real `~/.config` and
|
|
50
|
+
* `systemctl enable --now` does what it is told.
|
|
51
|
+
*
|
|
52
|
+
* A command must be drivable without touching the machine it runs on.
|
|
53
|
+
*/
|
|
54
|
+
export interface ServiceManager {
|
|
55
|
+
kind(): Promise<ManagerKind>;
|
|
56
|
+
/** Write a unit file verbatim, for units with no UnitSpec (the dashboard). */
|
|
57
|
+
installRaw(unit: string, contents: string): Promise<InstallOutcome>;
|
|
58
|
+
install(spec: UnitSpec): Promise<{
|
|
59
|
+
outcome: InstallOutcome;
|
|
60
|
+
unit: string;
|
|
61
|
+
file: string;
|
|
62
|
+
}>;
|
|
63
|
+
/** Enable and start. Returns an error message, or undefined on success. */
|
|
64
|
+
enable(unit: string): Promise<string | undefined>;
|
|
65
|
+
/** Stop and disable. Returns an error message, or undefined on success. */
|
|
66
|
+
disable(unit: string): Promise<string | undefined>;
|
|
67
|
+
states(units: readonly string[]): Promise<Map<string, string>>;
|
|
68
|
+
}
|
|
69
|
+
/** The real one: systemd through `systemctl --user`. */
|
|
70
|
+
export declare function systemdManager(env?: NodeJS.ProcessEnv): ServiceManager;
|
|
71
|
+
//# sourceMappingURL=manager.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"manager.d.ts","sourceRoot":"","sources":["../../src/service/manager.ts"],"names":[],"mappings":"AAKA,OAAO,EAAE,KAAK,QAAQ,EAAgD,MAAM,WAAW,CAAC;AAExF;;;;GAIG;AACH,MAAM,MAAM,WAAW,GAAG,SAAS,GAAG,SAAS,GAAG,MAAM,CAAC;AAEzD,0EAA0E;AAC1E,wBAAsB,aAAa,IAAI,OAAO,CAAC,WAAW,CAAC,CAU1D;AAED,iDAAiD;AACjD,wBAAgB,cAAc,CAAC,GAAG,GAAE,MAAM,CAAC,UAAwB,GAAG,MAAM,CAG3E;AAED,8EAA8E;AAC9E,MAAM,MAAM,cAAc,GAAG,SAAS,GAAG,WAAW,GAAG,aAAa,CAAC;AAErE;;;;;;GAMG;AACH,wBAAsB,WAAW,CAC/B,IAAI,EAAE,QAAQ,EACd,GAAG,GAAE,MAAM,CAAC,UAAwB,GACnC,OAAO,CAAC;IAAE,OAAO,EAAE,cAAc,CAAC;IAAC,IAAI,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,CAAA;CAAE,CAAC,CAkBlE;AAQD,6DAA6D;AAC7D,wBAAsB,MAAM,IAAI,OAAO,CAAC,IAAI,CAAC,CAE5C;AAED,kFAAkF;AAClF,wBAAsB,cAAc,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,GAAG,SAAS,CAAC,CAG9E;AAED;;;;;;GAMG;AACH,wBAAsB,cAAc,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,GAAG,SAAS,CAAC,CAQ9E;AAED,+EAA+E;AAC/E,wBAAsB,UAAU,CAC9B,IAAI,EAAE,MAAM,EACZ,GAAG,GAAE,MAAM,CAAC,UAAwB,GACnC,OAAO,CAAC,IAAI,CAAC,CAEf;AAED,wDAAwD;AACxD,wBAAsB,UAAU,CAAC,KAAK,EAAE,SAAS,MAAM,EAAE,GAAG,OAAO,CAAC,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,CAIvF;AAED;;;;;;;;;;;GAWG;AACH,MAAM,WAAW,cAAc;IAC7B,IAAI,IAAI,OAAO,CAAC,WAAW,CAAC,CAAC;IAC7B,8EAA8E;IAC9E,UAAU,CAAC,IAAI,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,cAAc,CAAC,CAAC;IACpE,OAAO,CAAC,IAAI,EAAE,QAAQ,GAAG,OAAO,CAAC;QAAE,OAAO,EAAE,cAAc,CAAC;QAAC,IAAI,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;IAC1F,2EAA2E;IAC3E,MAAM,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,GAAG,SAAS,CAAC,CAAC;IAClD,2EAA2E;IAC3E,OAAO,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,GAAG,SAAS,CAAC,CAAC;IACnD,MAAM,CAAC,KAAK,EAAE,SAAS,MAAM,EAAE,GAAG,OAAO,CAAC,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,CAAC;CAChE;AAED,wDAAwD;AACxD,wBAAgB,cAAc,CAAC,GAAG,GAAE,MAAM,CAAC,UAAwB,GAAG,cAAc,CA0BnF"}
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
import { mkdir, readFile, rm, writeFile } from "node:fs/promises";
|
|
2
|
+
import { homedir } from "node:os";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
import process from "node:process";
|
|
5
|
+
import { spawnCollect } from "../backend/proc.js";
|
|
6
|
+
import { parseUnitStates, renderSystemdUnit, unitName } from "./unit.js";
|
|
7
|
+
/** Which manager this platform offers, whether or not it is supported. */
|
|
8
|
+
export async function detectManager() {
|
|
9
|
+
if (process.platform === "darwin") {
|
|
10
|
+
const launchctl = await spawnCollect(["launchctl", "help"], { timeoutMs: 5_000 });
|
|
11
|
+
// launchctl exits nonzero for `help`, so presence is what is being tested.
|
|
12
|
+
return launchctl.exitCode === 127 ? "none" : "launchd";
|
|
13
|
+
}
|
|
14
|
+
const systemctl = await spawnCollect(["systemctl", "--user", "--version"], {
|
|
15
|
+
timeoutMs: 5_000,
|
|
16
|
+
});
|
|
17
|
+
return systemctl.exitCode === 0 ? "systemd" : "none";
|
|
18
|
+
}
|
|
19
|
+
/** Where systemd looks for this user's units. */
|
|
20
|
+
export function systemdUnitDir(env = process.env) {
|
|
21
|
+
const base = env.XDG_CONFIG_HOME ?? join(homedir(), ".config");
|
|
22
|
+
return join(base, "systemd", "user");
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* Write a unit unless it is already right, or has been edited.
|
|
26
|
+
*
|
|
27
|
+
* An edited unit is never overwritten. Same instinct as `init --update`: a
|
|
28
|
+
* customization is deliberate until proven otherwise, and silently reverting
|
|
29
|
+
* one is the kind of thing that makes a tool untrustworthy.
|
|
30
|
+
*/
|
|
31
|
+
export async function installUnit(spec, env = process.env) {
|
|
32
|
+
const unit = unitName(spec.name, spec.path);
|
|
33
|
+
const dir = systemdUnitDir(env);
|
|
34
|
+
const file = join(dir, unit);
|
|
35
|
+
const wanted = renderSystemdUnit(spec);
|
|
36
|
+
let current;
|
|
37
|
+
try {
|
|
38
|
+
current = await readFile(file, "utf8");
|
|
39
|
+
}
|
|
40
|
+
catch {
|
|
41
|
+
current = undefined;
|
|
42
|
+
}
|
|
43
|
+
if (current === wanted)
|
|
44
|
+
return { outcome: "unchanged", unit, file };
|
|
45
|
+
if (current !== undefined)
|
|
46
|
+
return { outcome: "hand-edited", unit, file };
|
|
47
|
+
await mkdir(dir, { recursive: true });
|
|
48
|
+
await writeFile(file, wanted, "utf8");
|
|
49
|
+
return { outcome: "written", unit, file };
|
|
50
|
+
}
|
|
51
|
+
/** Run a systemctl user subcommand, returning its exit code and output. */
|
|
52
|
+
async function systemctl(args) {
|
|
53
|
+
const result = await spawnCollect(["systemctl", "--user", ...args], { timeoutMs: 30_000 });
|
|
54
|
+
return { code: result.exitCode, out: `${result.stdout}${result.stderr}` };
|
|
55
|
+
}
|
|
56
|
+
/** Reload the manager so a newly written unit is visible. */
|
|
57
|
+
export async function reload() {
|
|
58
|
+
await systemctl(["daemon-reload"]);
|
|
59
|
+
}
|
|
60
|
+
/** Enable and start a unit. Returns an error message, or undefined on success. */
|
|
61
|
+
export async function enableAndStart(unit) {
|
|
62
|
+
const result = await systemctl(["enable", "--now", unit]);
|
|
63
|
+
return result.code === 0 ? undefined : result.out.trim();
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* Stop and disable a unit.
|
|
67
|
+
*
|
|
68
|
+
* This can genuinely take minutes: an in-flight tick is not interrupted, so the
|
|
69
|
+
* manager waits out the current agent run. Callers should say so before this
|
|
70
|
+
* starts rather than letting it look hung.
|
|
71
|
+
*/
|
|
72
|
+
export async function disableAndStop(unit) {
|
|
73
|
+
const result = await systemctl(["disable", "--now", unit]);
|
|
74
|
+
if (result.code === 0)
|
|
75
|
+
return undefined;
|
|
76
|
+
// A unit that was never installed is not a failure to stop. The goal is that
|
|
77
|
+
// it is not running, and it is not. Reporting this as an error made `stop` on
|
|
78
|
+
// a never-started ghostrail look broken.
|
|
79
|
+
if (/does not exist|not loaded|no such file/i.test(result.out))
|
|
80
|
+
return undefined;
|
|
81
|
+
return result.out.trim();
|
|
82
|
+
}
|
|
83
|
+
/** Remove a unit file. Absent is success: the goal is that it is not there. */
|
|
84
|
+
export async function removeUnit(unit, env = process.env) {
|
|
85
|
+
await rm(join(systemdUnitDir(env), unit), { force: true });
|
|
86
|
+
}
|
|
87
|
+
/** Active state per unit, for the units asked about. */
|
|
88
|
+
export async function unitStates(units) {
|
|
89
|
+
if (units.length === 0)
|
|
90
|
+
return new Map();
|
|
91
|
+
const result = await systemctl(["show", "--property=Id", "--property=ActiveState", ...units]);
|
|
92
|
+
return parseUnitStates(result.out);
|
|
93
|
+
}
|
|
94
|
+
/** The real one: systemd through `systemctl --user`. */
|
|
95
|
+
export function systemdManager(env = process.env) {
|
|
96
|
+
return {
|
|
97
|
+
kind: detectManager,
|
|
98
|
+
install: (spec) => installUnit(spec, env),
|
|
99
|
+
installRaw: async (unit, contents) => {
|
|
100
|
+
const dir = systemdUnitDir(env);
|
|
101
|
+
const file = join(dir, unit);
|
|
102
|
+
let current;
|
|
103
|
+
try {
|
|
104
|
+
current = await readFile(file, "utf8");
|
|
105
|
+
}
|
|
106
|
+
catch {
|
|
107
|
+
current = undefined;
|
|
108
|
+
}
|
|
109
|
+
if (current === contents)
|
|
110
|
+
return "unchanged";
|
|
111
|
+
if (current !== undefined)
|
|
112
|
+
return "hand-edited";
|
|
113
|
+
await mkdir(dir, { recursive: true });
|
|
114
|
+
await writeFile(file, contents, "utf8");
|
|
115
|
+
return "written";
|
|
116
|
+
},
|
|
117
|
+
enable: async (unit) => {
|
|
118
|
+
await reload();
|
|
119
|
+
return enableAndStart(unit);
|
|
120
|
+
},
|
|
121
|
+
disable: disableAndStop,
|
|
122
|
+
states: unitStates,
|
|
123
|
+
};
|
|
124
|
+
}
|
|
125
|
+
//# sourceMappingURL=manager.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"manager.js","sourceRoot":"","sources":["../../src/service/manager.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Rendering and naming for the user service units that keep a ghostrail
|
|
3
|
+
* running. Pure: nothing here touches the disk or shells out, so the awkward
|
|
4
|
+
* parts (unit naming, the stop timeout) are testable without a service manager.
|
|
5
|
+
*/
|
|
6
|
+
/** Everything a unit file needs. */
|
|
7
|
+
export interface UnitSpec {
|
|
8
|
+
/** The ghostrail's display name, for the unit description. */
|
|
9
|
+
name: string;
|
|
10
|
+
/** Absolute path to the repo. Becomes the working directory. */
|
|
11
|
+
path: string;
|
|
12
|
+
/** Absolute path to the ghostrail binary. */
|
|
13
|
+
exec: string;
|
|
14
|
+
/** Poll interval, as a duration string like `5m`. */
|
|
15
|
+
interval: string;
|
|
16
|
+
/** How long the manager waits for a clean stop, in seconds. */
|
|
17
|
+
stopTimeoutSec: number;
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* The unit file name for a ghostrail: `ghostrail-<slug>-<hash>.service`.
|
|
21
|
+
*
|
|
22
|
+
* The hash of the path is not decoration. `name` is display-only and duplicates
|
|
23
|
+
* are entirely legal: two repos scaffolded from the same template are both
|
|
24
|
+
* called `code-local`. Keying the unit on the name alone would give them one
|
|
25
|
+
* unit between them, so starting the second would silently hijack the first.
|
|
26
|
+
* The identity is the path, so the path is what disambiguates.
|
|
27
|
+
*/
|
|
28
|
+
export declare function unitName(name: string, path: string): string;
|
|
29
|
+
/**
|
|
30
|
+
* How long the service manager must wait for a clean stop.
|
|
31
|
+
*
|
|
32
|
+
* `watchLoop` checks its abort signal *before* each tick and each sleep, so a
|
|
33
|
+
* SIGTERM arriving mid-tick does not interrupt the running agent: the tick
|
|
34
|
+
* finishes first. A manager that gives up sooner SIGKILLs a live agent run,
|
|
35
|
+
* orphaning its container and leaving the tracker issue claimed by a bot that no
|
|
36
|
+
* longer exists.
|
|
37
|
+
*
|
|
38
|
+
* The headroom is there because the agent timeout bounds the agent, not the
|
|
39
|
+
* tick: provisioning, the gate, and publishing all happen outside it.
|
|
40
|
+
*/
|
|
41
|
+
export declare function stopTimeoutSeconds(timeoutMs: number): number;
|
|
42
|
+
/**
|
|
43
|
+
* A systemd user unit. Deterministic, so an unchanged unit can be recognized
|
|
44
|
+
* and an edited one left alone.
|
|
45
|
+
*
|
|
46
|
+
* Paths are written as-is: a unit file is not shell, and a path containing a
|
|
47
|
+
* space is legal in `WorkingDirectory=`.
|
|
48
|
+
*/
|
|
49
|
+
export declare function renderSystemdUnit(spec: UnitSpec): string;
|
|
50
|
+
/**
|
|
51
|
+
* Parse `systemctl --user show --property=Id --property=ActiveState` for
|
|
52
|
+
* several units into unit name -> active state.
|
|
53
|
+
*
|
|
54
|
+
* Tolerates what the real command emits: records separated by blank lines,
|
|
55
|
+
* properties in any order, extra properties, and a unit that does not exist
|
|
56
|
+
* (reported as `inactive` rather than as a failure). A record with no `Id` is
|
|
57
|
+
* skipped rather than fatal.
|
|
58
|
+
*/
|
|
59
|
+
export declare function parseUnitStates(stdout: string): Map<string, string>;
|
|
60
|
+
/** The unit that runs the cross-ghostrail dashboard. Only ever one. */
|
|
61
|
+
export declare const DASHBOARD_UNIT = "ghostrail-dashboard.service";
|
|
62
|
+
/**
|
|
63
|
+
* The dashboard's unit. Unlike a ghostrail's, it has no `WorkingDirectory`
|
|
64
|
+
* tied to a repo: it serves all of them and reads the registry from the global
|
|
65
|
+
* config dir.
|
|
66
|
+
*
|
|
67
|
+
* Its stop timeout is short on purpose. A ghostrail needs minutes because an
|
|
68
|
+
* in-flight tick is not interrupted; an HTTP server has nothing to protect.
|
|
69
|
+
*/
|
|
70
|
+
export declare function renderDashboardUnit(exec: string, port: number, bind: string): string;
|
|
71
|
+
//# sourceMappingURL=unit.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"unit.d.ts","sourceRoot":"","sources":["../../src/service/unit.ts"],"names":[],"mappings":"AAEA;;;;GAIG;AAEH,oCAAoC;AACpC,MAAM,WAAW,QAAQ;IACvB,8DAA8D;IAC9D,IAAI,EAAE,MAAM,CAAC;IACb,gEAAgE;IAChE,IAAI,EAAE,MAAM,CAAC;IACb,6CAA6C;IAC7C,IAAI,EAAE,MAAM,CAAC;IACb,qDAAqD;IACrD,QAAQ,EAAE,MAAM,CAAC;IACjB,+DAA+D;IAC/D,cAAc,EAAE,MAAM,CAAC;CACxB;AAWD;;;;;;;;GAQG;AACH,wBAAgB,QAAQ,CAAC,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,MAAM,CAG3D;AAQD;;;;;;;;;;;GAWG;AACH,wBAAgB,kBAAkB,CAAC,SAAS,EAAE,MAAM,GAAG,MAAM,CAQ5D;AAED;;;;;;GAMG;AACH,wBAAgB,iBAAiB,CAAC,IAAI,EAAE,QAAQ,GAAG,MAAM,CAoBxD;AAED;;;;;;;;GAQG;AACH,wBAAgB,eAAe,CAAC,MAAM,EAAE,MAAM,GAAG,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,CAoBnE;AAED,uEAAuE;AACvE,eAAO,MAAM,cAAc,gCAAgC,CAAC;AAE5D;;;;;;;GAOG;AACH,wBAAgB,mBAAmB,CAAC,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,MAAM,CAgBpF"}
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
/** Lowercase, non-alphanumeric runs collapsed to `-`, trimmed. */
|
|
3
|
+
function slug(name) {
|
|
4
|
+
const out = name
|
|
5
|
+
.toLowerCase()
|
|
6
|
+
.replace(/[^a-z0-9]+/g, "-")
|
|
7
|
+
.replace(/^-+|-+$/g, "");
|
|
8
|
+
return out.length > 0 ? out : "ghostrail";
|
|
9
|
+
}
|
|
10
|
+
/**
|
|
11
|
+
* The unit file name for a ghostrail: `ghostrail-<slug>-<hash>.service`.
|
|
12
|
+
*
|
|
13
|
+
* The hash of the path is not decoration. `name` is display-only and duplicates
|
|
14
|
+
* are entirely legal: two repos scaffolded from the same template are both
|
|
15
|
+
* called `code-local`. Keying the unit on the name alone would give them one
|
|
16
|
+
* unit between them, so starting the second would silently hijack the first.
|
|
17
|
+
* The identity is the path, so the path is what disambiguates.
|
|
18
|
+
*/
|
|
19
|
+
export function unitName(name, path) {
|
|
20
|
+
const hash = createHash("sha256").update(path, "utf8").digest("hex").slice(0, 8);
|
|
21
|
+
return `ghostrail-${slug(name)}-${hash}.service`;
|
|
22
|
+
}
|
|
23
|
+
/** Headroom over the agent timeout, in seconds. */
|
|
24
|
+
const STOP_HEADROOM_SEC = 300;
|
|
25
|
+
/** Never wait less than this for a stop, however short the configured timeout. */
|
|
26
|
+
const MIN_STOP_SEC = 600;
|
|
27
|
+
/**
|
|
28
|
+
* How long the service manager must wait for a clean stop.
|
|
29
|
+
*
|
|
30
|
+
* `watchLoop` checks its abort signal *before* each tick and each sleep, so a
|
|
31
|
+
* SIGTERM arriving mid-tick does not interrupt the running agent: the tick
|
|
32
|
+
* finishes first. A manager that gives up sooner SIGKILLs a live agent run,
|
|
33
|
+
* orphaning its container and leaving the tracker issue claimed by a bot that no
|
|
34
|
+
* longer exists.
|
|
35
|
+
*
|
|
36
|
+
* The headroom is there because the agent timeout bounds the agent, not the
|
|
37
|
+
* tick: provisioning, the gate, and publishing all happen outside it.
|
|
38
|
+
*/
|
|
39
|
+
export function stopTimeoutSeconds(timeoutMs) {
|
|
40
|
+
// Total by construction. The caller reads this from a validated config, so a
|
|
41
|
+
// negative or non-finite value should be unreachable, but this number is the
|
|
42
|
+
// one thing standing between a stop and a SIGKILL through a live agent run.
|
|
43
|
+
// Rendering `TimeoutStopSec=NaN` would make systemd reject the whole unit.
|
|
44
|
+
if (!Number.isFinite(timeoutMs) || timeoutMs < 0)
|
|
45
|
+
return MIN_STOP_SEC;
|
|
46
|
+
const seconds = Math.ceil(timeoutMs / 1000) + STOP_HEADROOM_SEC;
|
|
47
|
+
return Math.max(seconds, MIN_STOP_SEC);
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* A systemd user unit. Deterministic, so an unchanged unit can be recognized
|
|
51
|
+
* and an edited one left alone.
|
|
52
|
+
*
|
|
53
|
+
* Paths are written as-is: a unit file is not shell, and a path containing a
|
|
54
|
+
* space is legal in `WorkingDirectory=`.
|
|
55
|
+
*/
|
|
56
|
+
export function renderSystemdUnit(spec) {
|
|
57
|
+
return `${[
|
|
58
|
+
"[Unit]",
|
|
59
|
+
`Description=ghostrail factory (${spec.name})`,
|
|
60
|
+
"Documentation=https://github.com/timothyjordan/ghostrail",
|
|
61
|
+
"",
|
|
62
|
+
"[Service]",
|
|
63
|
+
"Type=simple",
|
|
64
|
+
`WorkingDirectory=${spec.path}`,
|
|
65
|
+
// Absolute: a bare `ghostrail` is not on a service manager's PATH, and on
|
|
66
|
+
// an nvm install the real path is version-specific.
|
|
67
|
+
`ExecStart=${spec.exec} watch --interval ${spec.interval}`,
|
|
68
|
+
"Restart=on-failure",
|
|
69
|
+
"RestartSec=30",
|
|
70
|
+
// Long enough for an in-flight tick to finish. See stopTimeoutSeconds.
|
|
71
|
+
`TimeoutStopSec=${spec.stopTimeoutSec}`,
|
|
72
|
+
"",
|
|
73
|
+
"[Install]",
|
|
74
|
+
"WantedBy=default.target",
|
|
75
|
+
].join("\n")}\n`;
|
|
76
|
+
}
|
|
77
|
+
/**
|
|
78
|
+
* Parse `systemctl --user show --property=Id --property=ActiveState` for
|
|
79
|
+
* several units into unit name -> active state.
|
|
80
|
+
*
|
|
81
|
+
* Tolerates what the real command emits: records separated by blank lines,
|
|
82
|
+
* properties in any order, extra properties, and a unit that does not exist
|
|
83
|
+
* (reported as `inactive` rather than as a failure). A record with no `Id` is
|
|
84
|
+
* skipped rather than fatal.
|
|
85
|
+
*/
|
|
86
|
+
export function parseUnitStates(stdout) {
|
|
87
|
+
const states = new Map();
|
|
88
|
+
for (const record of stdout.split(/\n\s*\n/)) {
|
|
89
|
+
let id;
|
|
90
|
+
let active;
|
|
91
|
+
for (const line of record.split("\n")) {
|
|
92
|
+
const eq = line.indexOf("=");
|
|
93
|
+
if (eq === -1)
|
|
94
|
+
continue;
|
|
95
|
+
const key = line.slice(0, eq).trim();
|
|
96
|
+
const value = line.slice(eq + 1).trim();
|
|
97
|
+
if (key === "Id")
|
|
98
|
+
id = value;
|
|
99
|
+
else if (key === "ActiveState")
|
|
100
|
+
active = value;
|
|
101
|
+
}
|
|
102
|
+
// A record carrying an Id but no ActiveState becomes "unknown" rather than
|
|
103
|
+
// being dropped: the unit exists, and saying so beats pretending it does
|
|
104
|
+
// not. A repeated Id keeps the last record, matching how systemd's own
|
|
105
|
+
// later output would supersede earlier output.
|
|
106
|
+
if (id !== undefined && id.length > 0)
|
|
107
|
+
states.set(id, active ?? "unknown");
|
|
108
|
+
}
|
|
109
|
+
return states;
|
|
110
|
+
}
|
|
111
|
+
/** The unit that runs the cross-ghostrail dashboard. Only ever one. */
|
|
112
|
+
export const DASHBOARD_UNIT = "ghostrail-dashboard.service";
|
|
113
|
+
/**
|
|
114
|
+
* The dashboard's unit. Unlike a ghostrail's, it has no `WorkingDirectory`
|
|
115
|
+
* tied to a repo: it serves all of them and reads the registry from the global
|
|
116
|
+
* config dir.
|
|
117
|
+
*
|
|
118
|
+
* Its stop timeout is short on purpose. A ghostrail needs minutes because an
|
|
119
|
+
* in-flight tick is not interrupted; an HTTP server has nothing to protect.
|
|
120
|
+
*/
|
|
121
|
+
export function renderDashboardUnit(exec, port, bind) {
|
|
122
|
+
return `${[
|
|
123
|
+
"[Unit]",
|
|
124
|
+
"Description=ghostrail dashboard (all ghostrails)",
|
|
125
|
+
"Documentation=https://github.com/timothyjordan/ghostrail",
|
|
126
|
+
"",
|
|
127
|
+
"[Service]",
|
|
128
|
+
"Type=simple",
|
|
129
|
+
`ExecStart=${exec} dashboard --port ${port} --bind ${bind}`,
|
|
130
|
+
"Restart=on-failure",
|
|
131
|
+
"RestartSec=10",
|
|
132
|
+
"TimeoutStopSec=30",
|
|
133
|
+
"",
|
|
134
|
+
"[Install]",
|
|
135
|
+
"WantedBy=default.target",
|
|
136
|
+
].join("\n")}\n`;
|
|
137
|
+
}
|
|
138
|
+
//# sourceMappingURL=unit.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"unit.js","sourceRoot":"","sources":["../../src/service/unit.ts"],"names":[],"mappings":"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"}
|