@edspencer/paddock 0.82.0 → 0.83.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/package.json +1 -1
- package/packages/server/dist/cli/args.d.ts +29 -4
- package/packages/server/dist/cli/args.js +145 -7
- package/packages/server/dist/cli/busy-command.js +1 -1
- package/packages/server/dist/cli/busy.js +10 -12
- package/packages/server/dist/cli/doctor.d.ts +60 -0
- package/packages/server/dist/cli/doctor.js +580 -0
- package/packages/server/dist/cli/exit-codes.d.ts +21 -45
- package/packages/server/dist/cli/exit-codes.js +42 -45
- package/packages/server/dist/cli/output.js +5 -6
- package/packages/server/dist/cli/paddock.js +37 -12
- package/packages/server/dist/cli/service/index.d.ts +30 -2
- package/packages/server/dist/cli/service/index.js +163 -31
- package/packages/server/dist/cli/service/launchd.d.ts +31 -84
- package/packages/server/dist/cli/service/launchd.js +114 -304
- package/packages/server/dist/cli/service/logs.d.ts +9 -28
- package/packages/server/dist/cli/service/logs.js +33 -75
- package/packages/server/dist/cli/service/manager.d.ts +93 -0
- package/packages/server/dist/cli/service/manager.js +21 -0
- package/packages/server/dist/cli/service/path-env.d.ts +7 -18
- package/packages/server/dist/cli/service/path-env.js +30 -81
- package/packages/server/dist/cli/service/ready.d.ts +16 -23
- package/packages/server/dist/cli/service/ready.js +42 -0
- package/packages/server/dist/cli/service/spec.d.ts +4 -31
- package/packages/server/dist/cli/service/spec.js +9 -0
- package/packages/server/dist/cli/service/systemd.d.ts +27 -101
- package/packages/server/dist/cli/service/systemd.js +118 -261
- package/packages/server/dist/cli/update/health.d.ts +48 -0
- package/packages/server/dist/cli/update/health.js +64 -0
- package/packages/server/dist/cli/update/install-method.d.ts +29 -76
- package/packages/server/dist/cli/update/install-method.js +103 -178
- package/packages/server/dist/cli/update/lock.d.ts +7 -12
- package/packages/server/dist/cli/update/lock.js +28 -62
- package/packages/server/dist/cli/update/package-manager.d.ts +39 -74
- package/packages/server/dist/cli/update/package-manager.js +95 -160
- package/packages/server/dist/cli/update/registry.d.ts +17 -54
- package/packages/server/dist/cli/update/registry.js +46 -89
- package/packages/server/dist/cli/update/semver.d.ts +18 -41
- package/packages/server/dist/cli/update/semver.js +74 -124
- package/packages/server/dist/cli/update/update-check.d.ts +15 -26
- package/packages/server/dist/cli/update/update-check.js +30 -60
- package/packages/server/dist/cli/update/update.d.ts +57 -91
- package/packages/server/dist/cli/update/update.js +230 -431
- package/packages/server/dist/cli/update-command.js +3 -5
- package/packages/server/dist/runtime-file.d.ts +1 -1
- package/packages/server/dist/runtime-file.js +9 -13
- package/packages/server/package.json +1 -1
- package/packages/web/dist/assets/{DeleteProjectDialog-CphdGB3R.js → DeleteProjectDialog-B07MKgv4.js} +1 -1
- package/packages/web/dist/assets/DiscoverPage-D5C2fkeQ.js +2 -0
- package/packages/web/dist/assets/{DiscoverView-B_kcF4sm.js → DiscoverView-DpAAyBEf.js} +1 -1
- package/packages/web/dist/assets/{InstanceConfigPage-CvUK5DKW.js → InstanceConfigPage-YX0I_jVA.js} +1 -1
- package/packages/web/dist/assets/{MarkdownRenderer-DvZ-j_Ao.js → MarkdownRenderer-C-CUuFhT.js} +1 -1
- package/packages/web/dist/assets/{ProjectRedirect-DayHvUH2.js → ProjectRedirect-qyXbTK56.js} +1 -1
- package/packages/web/dist/assets/{ProjectView-CCw83gUi.js → ProjectView-CguNOv8k.js} +3 -3
- package/packages/web/dist/assets/{ProjectsGrid-B3Klg2H4.js → ProjectsGrid-CH2Lz2Hf.js} +1 -1
- package/packages/web/dist/assets/{RootHome-BxcDvpgI.js → RootHome-CUGUc8za.js} +1 -1
- package/packages/web/dist/assets/{Surfaces-7UlhFBSG.js → Surfaces-11or1q_3.js} +1 -1
- package/packages/web/dist/assets/{arc-DNkh5o5Q.js → arc-CQHeCzrk.js} +1 -1
- package/packages/web/dist/assets/{architectureDiagram-3BPJPVTR-BrgN3oLF.js → architectureDiagram-3BPJPVTR-dGcuFxAa.js} +1 -1
- package/packages/web/dist/assets/{blockDiagram-GPEHLZMM-D05XOJSk.js → blockDiagram-GPEHLZMM-CGc9h97S.js} +1 -1
- package/packages/web/dist/assets/{c4Diagram-AAUBKEIU-CKaMBhMP.js → c4Diagram-AAUBKEIU-BpX0sqhH.js} +1 -1
- package/packages/web/dist/assets/channel-BAeygx-e.js +2 -0
- package/packages/web/dist/assets/{chunk-2J33WTMH-8hgadCQc.js → chunk-2J33WTMH-uORGdO9D.js} +1 -1
- package/packages/web/dist/assets/{chunk-4BX2VUAB-CBfEC-pu.js → chunk-4BX2VUAB-C8zI2PFy.js} +1 -1
- package/packages/web/dist/assets/{chunk-55IACEB6-DV1eMgHk.js → chunk-55IACEB6-D22Iv6j6.js} +1 -1
- package/packages/web/dist/assets/{chunk-727SXJPM-Cfgl5fxY.js → chunk-727SXJPM-DdbHXAtS.js} +1 -1
- package/packages/web/dist/assets/{chunk-AQP2D5EJ-BXhqD295.js → chunk-AQP2D5EJ-BeKbob3a.js} +1 -1
- package/packages/web/dist/assets/{chunk-FMBD7UC4-TFGOPtek.js → chunk-FMBD7UC4-DtLvFU_y.js} +1 -1
- package/packages/web/dist/assets/{chunk-ND2GUHAM-CHFM665R.js → chunk-ND2GUHAM-BWJFo6B1.js} +1 -1
- package/packages/web/dist/assets/{chunk-QZHKN3VN-CqEf6MMT.js → chunk-QZHKN3VN-DeFkNg3S.js} +1 -1
- package/packages/web/dist/assets/classDiagram-4FO5ZUOK-Cq2-sBMa.js +2 -0
- package/packages/web/dist/assets/classDiagram-v2-Q7XG4LA2-Cq2-sBMa.js +2 -0
- package/packages/web/dist/assets/{core-DmEaKxBb.js → core-CTNNIHx5.js} +1 -1
- package/packages/web/dist/assets/{cose-bilkent-S5V4N54A-BnGI6jkr.js → cose-bilkent-S5V4N54A-BmtlyA-v.js} +1 -1
- package/packages/web/dist/assets/{dagre-BM42HDAG-CKoPx61E.js → dagre-BM42HDAG-oS3rC9CI.js} +1 -1
- package/packages/web/dist/assets/{diagram-2AECGRRQ-mMmtlBl1.js → diagram-2AECGRRQ-BzIBqz6A.js} +1 -1
- package/packages/web/dist/assets/{diagram-5GNKFQAL-Cdz9Mtnt.js → diagram-5GNKFQAL-BgIT2g8R.js} +1 -1
- package/packages/web/dist/assets/{diagram-KO2AKTUF-DQxM3wYj.js → diagram-KO2AKTUF-DACT41Re.js} +1 -1
- package/packages/web/dist/assets/{diagram-LMA3HP47-BWrinPsq.js → diagram-LMA3HP47-BstA3UkI.js} +1 -1
- package/packages/web/dist/assets/{diagram-OG6HWLK6-U6MYUve2.js → diagram-OG6HWLK6-Dp3Cn5n_.js} +1 -1
- package/packages/web/dist/assets/{documentTitle-DGjRXuTP.js → documentTitle-DRSQmA8q.js} +1 -1
- package/packages/web/dist/assets/{erDiagram-TEJ5UH35-xqMCsXxY.js → erDiagram-TEJ5UH35-wf8PyGx_.js} +1 -1
- package/packages/web/dist/assets/{flowDiagram-I6XJVG4X-PMfy_czF.js → flowDiagram-I6XJVG4X-CJ4xx6EQ.js} +1 -1
- package/packages/web/dist/assets/{ganttDiagram-6RSMTGT7-whhYYE4N.js → ganttDiagram-6RSMTGT7-z9obPrPM.js} +1 -1
- package/packages/web/dist/assets/{gitGraphDiagram-PVQCEYII-C51JW4tf.js → gitGraphDiagram-PVQCEYII-Cf4Cu7Tk.js} +1 -1
- package/packages/web/dist/assets/{index-CHOtvlnO.js → index-Bsv-raz2.js} +2 -2
- package/packages/web/dist/assets/{infoDiagram-5YYISTIA-SH8cr905.js → infoDiagram-5YYISTIA-BIfuiYxX.js} +1 -1
- package/packages/web/dist/assets/{ishikawaDiagram-YF4QCWOH-BjqaoU3U.js → ishikawaDiagram-YF4QCWOH-1pACxMTX.js} +1 -1
- package/packages/web/dist/assets/{journeyDiagram-JHISSGLW-CBmdEVrd.js → journeyDiagram-JHISSGLW-XGbzfw-v.js} +1 -1
- package/packages/web/dist/assets/{kanban-definition-UN3LZRKU-U8njjMLZ.js → kanban-definition-UN3LZRKU-Bvl-nMak.js} +1 -1
- package/packages/web/dist/assets/{linear-DF66GPs1.js → linear-uNToO4bv.js} +1 -1
- package/packages/web/dist/assets/{mermaid.core-CouJz7Ph.js → mermaid.core-CrChvyj6.js} +4 -4
- package/packages/web/dist/assets/{mindmap-definition-RKZ34NQL-BYIf0oFt.js → mindmap-definition-RKZ34NQL-C6DaQ4Fm.js} +1 -1
- package/packages/web/dist/assets/{pieDiagram-4H26LBE5-Cm8rgg09.js → pieDiagram-4H26LBE5-CEh8lKyF.js} +1 -1
- package/packages/web/dist/assets/{quadrantDiagram-W4KKPZXB-C3Il5kOH.js → quadrantDiagram-W4KKPZXB-z4zvICSM.js} +1 -1
- package/packages/web/dist/assets/{requirementDiagram-4Y6WPE33-D3PE2nTe.js → requirementDiagram-4Y6WPE33-BBltkjsj.js} +1 -1
- package/packages/web/dist/assets/{sankeyDiagram-5OEKKPKP-DdmvBM18.js → sankeyDiagram-5OEKKPKP-D0s95wQ4.js} +1 -1
- package/packages/web/dist/assets/{sequenceDiagram-3UESZ5HK-B8M_GAIq.js → sequenceDiagram-3UESZ5HK-kQa65Nbt.js} +1 -1
- package/packages/web/dist/assets/{stateDiagram-AJRCARHV-DZkXnK1_.js → stateDiagram-AJRCARHV-BqVIoyED.js} +1 -1
- package/packages/web/dist/assets/stateDiagram-v2-BHNVJYJU-B6OXxEra.js +2 -0
- package/packages/web/dist/assets/{timeline-definition-PNZ67QCA-IHXq7rqi.js → timeline-definition-PNZ67QCA-Bo5L62WI.js} +1 -1
- package/packages/web/dist/assets/{vennDiagram-CIIHVFJN-Dga7tLtT.js → vennDiagram-CIIHVFJN-CN9rH4lT.js} +1 -1
- package/packages/web/dist/assets/{wardley-L42UT6IY-CmcwnEJ-.js → wardley-L42UT6IY-BKhnD_W_.js} +1 -1
- package/packages/web/dist/assets/{wardleyDiagram-YWT4CUSO-zHuSLrD4.js → wardleyDiagram-YWT4CUSO-XHTbZzXj.js} +1 -1
- package/packages/web/dist/assets/{xychartDiagram-2RQKCTM6-D93MU_ic.js → xychartDiagram-2RQKCTM6--t9XJLbR.js} +1 -1
- package/packages/web/dist/index.html +1 -1
- package/packages/web/dist/sw.js +1 -1
- package/packages/server/dist/cli/service/backend.d.ts +0 -115
- package/packages/server/dist/cli/service/backend.js +0 -19
- package/packages/web/dist/assets/DiscoverPage-C4OPLNO8.js +0 -2
- package/packages/web/dist/assets/channel-JrEedxB8.js +0 -2
- package/packages/web/dist/assets/classDiagram-4FO5ZUOK-CHJiQp-W.js +0 -2
- package/packages/web/dist/assets/classDiagram-v2-Q7XG4LA2-CHJiQp-W.js +0 -2
- package/packages/web/dist/assets/stateDiagram-v2-BHNVJYJU-CjCdT9Ls.js +0 -2
|
@@ -19,6 +19,15 @@ import { servicePath } from "./path-env.js";
|
|
|
19
19
|
*/
|
|
20
20
|
export const LAUNCHD_LABEL = "net.edspencer.paddock";
|
|
21
21
|
export const SYSTEMD_UNIT = "paddock.service";
|
|
22
|
+
/** Who the reference's launchd and systemd writers are writing for. */
|
|
23
|
+
export const PADDOCK_SERVICE = {
|
|
24
|
+
name: "Paddock",
|
|
25
|
+
tool: "paddock",
|
|
26
|
+
label: LAUNCHD_LABEL,
|
|
27
|
+
unit: SYSTEMD_UNIT,
|
|
28
|
+
description: "Paddock — persistent, resumable Claude Code sessions, by project",
|
|
29
|
+
documentation: "https://paddock.edspencer.net",
|
|
30
|
+
};
|
|
22
31
|
/** Default port, matching the server's own (#741). Kept here so the generated
|
|
23
32
|
* unit can name a port even when the user passed no flag — a unit that relies
|
|
24
33
|
* on a default is a unit whose port changes under it on upgrade. */
|
|
@@ -1,110 +1,36 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import type { RunResult, Runner, ServiceBackend } from "./backend.js";
|
|
1
|
+
import type { Runner, RunResult, ServiceIdentity, ServiceManager, ServiceSpec } from './manager.js';
|
|
3
2
|
/**
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
* ## An injected home outranks the ambient variable
|
|
7
|
-
*
|
|
8
|
-
* The precedence is: an explicit `xdgConfigHome` argument, then an explicitly
|
|
9
|
-
* **injected** `homeDir`, then — only when the caller named neither — the real
|
|
10
|
-
* environment. That middle rung is the whole point, and getting it wrong was a
|
|
11
|
-
* live bug rather than a style question.
|
|
12
|
-
*
|
|
13
|
-
* The first version read `process.env.XDG_CONFIG_HOME` before falling back to
|
|
14
|
-
* `homeDir`. On any machine with that variable set — CI, and plenty of Linux
|
|
15
|
-
* desktops — the `homeDir` parameter was therefore **a lie**: a caller passing a
|
|
16
|
-
* throwaway directory still resolved to the real config home. Two things
|
|
17
|
-
* followed. Test isolation quietly stopped working, so which assertion passed
|
|
18
|
-
* depended on test ORDER rather than test content. And the suite wrote a real
|
|
19
|
-
* `paddock.service` into the runner's actual config directory, outside every
|
|
20
|
-
* temp dir it thought it was confined to — which on a contributor's machine
|
|
21
|
-
* would have installed a unit they never asked for.
|
|
22
|
-
*
|
|
23
|
-
* So: passing a home is a statement about where home is, and it wins. The
|
|
24
|
-
* ambient read survives only on the branch where nobody said otherwise, which
|
|
25
|
-
* is the real-use branch and the one where honouring `XDG_CONFIG_HOME` is
|
|
26
|
-
* correct. (`paddock.ts` threads the variable in explicitly as well, so the
|
|
27
|
-
* production path does not depend on this fallback — it is here so that a
|
|
28
|
-
* caller with no opinion still gets the right answer.)
|
|
3
|
+
* `${XDG_CONFIG_HOME:-~/.config}/systemd/user/<unit>`. A home passed in beats the ambient `XDG_CONFIG_HOME`: a test
|
|
4
|
+
* that names a throwaway home must not write a unit into the real config folder because the variable was set.
|
|
29
5
|
*/
|
|
30
|
-
export declare function unitPath(homeDir?: string, xdgConfigHome?: string): string;
|
|
31
|
-
/** One `ExecStart` word: specifiers and variables escaped, quoted when it has to be. */
|
|
6
|
+
export declare function unitPath(id: Pick<ServiceIdentity, 'unit'>, homeDir?: string, xdgConfigHome?: string): string;
|
|
7
|
+
/** One `ExecStart` word: specifiers and variables escaped, and quoted when it has to be. */
|
|
32
8
|
export declare function execWord(a: string): string;
|
|
33
|
-
/**
|
|
34
|
-
* The other way: the words of an `ExecStart` line.
|
|
35
|
-
*
|
|
36
|
-
* Reads both shapes on purpose. A unit written before #796's quoting is a bare
|
|
37
|
-
* whitespace-separated line, and its owner is exactly the person whose `status`
|
|
38
|
-
* has to keep working — so unquoted words pass through unchanged, and quoted
|
|
39
|
-
* ones are unescaped.
|
|
40
|
-
*/
|
|
9
|
+
/** The words of an `ExecStart` line. Reads unquoted words too, so an older unit's `status` still works. */
|
|
41
10
|
export declare function execWords(line: string): string[];
|
|
42
11
|
/**
|
|
43
|
-
*
|
|
44
|
-
*
|
|
45
|
-
* `Restart=always`, matching launchd's `KeepAlive=<true/>` (#872). It was
|
|
46
|
-
* `on-failure`, which for a service is the wrong default for the same reason it
|
|
47
|
-
* was wrong there: `start.ts` exits `0` on SIGTERM, so a logout — or anything
|
|
48
|
-
* else that signals the user manager — was read as an intended stop and Paddock
|
|
49
|
-
* stayed down.
|
|
50
|
-
*
|
|
51
|
-
* `always` does not make the unit unstoppable. systemd never restarts a unit it
|
|
52
|
-
* was itself asked to stop, so `systemctl --user stop` (and therefore
|
|
53
|
-
* `uninstall`) remains the clean, explicit way to put it down. That is the
|
|
54
|
-
* difference between an exit code, which has to guess at intent, and a command,
|
|
55
|
-
* which carries it.
|
|
56
|
-
*
|
|
57
|
-
* `WantedBy=default.target` is what makes `systemctl --user enable` mean
|
|
58
|
-
* "start at login" for a user unit; `multi-user.target` is the system-unit
|
|
59
|
-
* spelling and does nothing here.
|
|
60
|
-
*
|
|
61
|
-
* ## Logs go to a file, not the journal (SPEC §7)
|
|
12
|
+
* The unit.
|
|
62
13
|
*
|
|
63
|
-
* `
|
|
64
|
-
*
|
|
65
|
-
* one
|
|
66
|
-
*
|
|
67
|
-
*
|
|
68
|
-
* answer on each platform, so `paddock service logs` would be two
|
|
69
|
-
* implementations; and it rules out Windows, which the contract requires
|
|
70
|
-
* nothing rule out.
|
|
71
|
-
*
|
|
72
|
-
* The directory has to exist first — systemd creates the file, not its parent —
|
|
73
|
-
* which `install` below does.
|
|
74
|
-
*
|
|
75
|
-
* ## Every ExecStart word is quoted
|
|
76
|
-
*
|
|
77
|
-
* `%` starts a specifier anywhere in a unit file and `$` starts a variable in
|
|
78
|
-
* `ExecStart`, so both are escaped; a word containing whitespace, a quote or a
|
|
79
|
-
* backslash is quoted. Paths with a space in them are the common case
|
|
80
|
-
* (`/Users/Ed Spencer/.paddock`), and the old unqualified `join(" ")` wrote a
|
|
81
|
-
* unit systemd then split into the wrong arguments — a service that fails at
|
|
82
|
-
* login having printed nothing.
|
|
14
|
+
* - `Restart=always`, matching launchd's `KeepAlive`: a server exits 0 on SIGTERM, so `on-failure` would leave it
|
|
15
|
+
* down after a logout signals the user's manager. systemd never restarts a unit it was asked to stop.
|
|
16
|
+
* - Both streams `append:` to one file rather than the journal, which is volatile on many distributions and would
|
|
17
|
+
* make `logs` a different program on each OS. systemd creates the file, not its folder, so `install` makes that.
|
|
18
|
+
* - Every `ExecStart` word is escaped and quoted where needed: paths with spaces are common (`/Users/Ed Spencer/…`).
|
|
83
19
|
*/
|
|
84
|
-
export declare function renderUnit(spec: ServiceSpec): string;
|
|
85
|
-
/**
|
|
20
|
+
export declare function renderUnit(id: ServiceIdentity, spec: ServiceSpec): string;
|
|
21
|
+
/** `ExecStart=`, read back as an argv. */
|
|
22
|
+
export declare function parseUnitArgv(text: string): string[];
|
|
23
|
+
/** The log the unit appends to, if it names one. */
|
|
86
24
|
export declare function parseUnitLogPath(text: string): string | undefined;
|
|
87
|
-
/**
|
|
88
|
-
* Does an installed unit carry `Restart=always` (#872)?
|
|
89
|
-
*
|
|
90
|
-
* The counterpart to `plistKeepsAlive`, and narrow for the same reason: the
|
|
91
|
-
* value it has to reject is the `on-failure` this file used to write, which
|
|
92
|
-
* leaves Paddock down after the graceful exit `start.ts` performs on SIGTERM.
|
|
93
|
-
* Anything unrecognised reads as "not the fixed shape", which at worst tells a
|
|
94
|
-
* hand-edited unit's owner to re-run install.
|
|
95
|
-
*/
|
|
25
|
+
/** Whether the unit has `Restart=always` (the older `on-failure` doesn't count). */
|
|
96
26
|
export declare function unitKeepsAlive(text: string): boolean;
|
|
97
|
-
/**
|
|
98
|
-
export declare function
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
* going to work on.
|
|
108
|
-
*/
|
|
109
|
-
export declare function systemctlFailure(args: string[], r: RunResult): string;
|
|
110
|
-
export declare function createSystemdBackend(run: Runner, homeDir?: string, xdgConfigHome?: string): ServiceBackend;
|
|
27
|
+
/** What a `systemctl` failure means, in the manager's words where it has any. */
|
|
28
|
+
export declare function systemctlFailure(id: Pick<ServiceIdentity, 'name' | 'tool'>, args: string[], r: RunResult): string;
|
|
29
|
+
export interface SystemdOptions {
|
|
30
|
+
run: Runner;
|
|
31
|
+
homeDir?: string;
|
|
32
|
+
xdgConfigHome?: string;
|
|
33
|
+
/** Injected for tests: whose linger to ask about. */
|
|
34
|
+
user?: string;
|
|
35
|
+
}
|
|
36
|
+
export declare function createSystemd(id: ServiceIdentity, o: SystemdOptions): ServiceManager;
|
|
@@ -1,152 +1,56 @@
|
|
|
1
|
+
// From edspencer/cli-contract reference/service/systemd.ts @ 0.2; local changes: `.ts` import specifiers → `.js`.
|
|
2
|
+
// Linux: a systemd user unit (`systemctl --user`, SPEC §7), running as the user. Without linger the user's manager,
|
|
3
|
+
// and the service with it, stops at their last logout, SSH sessions included; `linger()` asks, and the tool prints
|
|
4
|
+
// the `sudo loginctl enable-linger` line rather than running it. Node built-ins only.
|
|
5
|
+
import fs from 'node:fs';
|
|
6
|
+
import os from 'node:os';
|
|
7
|
+
import path from 'node:path';
|
|
1
8
|
/**
|
|
2
|
-
*
|
|
3
|
-
*
|
|
4
|
-
* ## Not the server story
|
|
5
|
-
*
|
|
6
|
-
* Paddock already documents itself under systemd — `guides/proxmox-lxc.md`
|
|
7
|
-
* Path B, provisioned by the `paddock-deploy` Ansible recipes. That is a
|
|
8
|
-
* SYSTEM unit, running as a service user, behind a reverse proxy, with a token
|
|
9
|
-
* in its environment. This is the other one: a user unit, running as you, on
|
|
10
|
-
* loopback, using the Claude login you already have. They should not be
|
|
11
|
-
* confused, and this module writes nothing outside `~/.config/systemd/user`.
|
|
12
|
-
*
|
|
13
|
-
* ## Lingering
|
|
14
|
-
*
|
|
15
|
-
* A `--user` manager is started at your first login and stopped at your last
|
|
16
|
-
* logout, so by default the unit dies when you log out — including when you log
|
|
17
|
-
* out of an SSH session, which is how most people meet this. `loginctl
|
|
18
|
-
* enable-linger <user>` keeps the manager (and therefore Paddock) alive.
|
|
19
|
-
*
|
|
20
|
-
* That is called out in the install output rather than being run for the user:
|
|
21
|
-
* lingering is a real change to how the machine behaves after they leave it,
|
|
22
|
-
* and it needs a privilege this command otherwise never asks for.
|
|
23
|
-
*/
|
|
24
|
-
import fs from "node:fs";
|
|
25
|
-
import os from "node:os";
|
|
26
|
-
import path from "node:path";
|
|
27
|
-
import { SYSTEMD_UNIT } from "./spec.js";
|
|
28
|
-
/**
|
|
29
|
-
* `~/.config/systemd/user/paddock.service`, honouring `XDG_CONFIG_HOME`.
|
|
30
|
-
*
|
|
31
|
-
* ## An injected home outranks the ambient variable
|
|
32
|
-
*
|
|
33
|
-
* The precedence is: an explicit `xdgConfigHome` argument, then an explicitly
|
|
34
|
-
* **injected** `homeDir`, then — only when the caller named neither — the real
|
|
35
|
-
* environment. That middle rung is the whole point, and getting it wrong was a
|
|
36
|
-
* live bug rather than a style question.
|
|
37
|
-
*
|
|
38
|
-
* The first version read `process.env.XDG_CONFIG_HOME` before falling back to
|
|
39
|
-
* `homeDir`. On any machine with that variable set — CI, and plenty of Linux
|
|
40
|
-
* desktops — the `homeDir` parameter was therefore **a lie**: a caller passing a
|
|
41
|
-
* throwaway directory still resolved to the real config home. Two things
|
|
42
|
-
* followed. Test isolation quietly stopped working, so which assertion passed
|
|
43
|
-
* depended on test ORDER rather than test content. And the suite wrote a real
|
|
44
|
-
* `paddock.service` into the runner's actual config directory, outside every
|
|
45
|
-
* temp dir it thought it was confined to — which on a contributor's machine
|
|
46
|
-
* would have installed a unit they never asked for.
|
|
47
|
-
*
|
|
48
|
-
* So: passing a home is a statement about where home is, and it wins. The
|
|
49
|
-
* ambient read survives only on the branch where nobody said otherwise, which
|
|
50
|
-
* is the real-use branch and the one where honouring `XDG_CONFIG_HOME` is
|
|
51
|
-
* correct. (`paddock.ts` threads the variable in explicitly as well, so the
|
|
52
|
-
* production path does not depend on this fallback — it is here so that a
|
|
53
|
-
* caller with no opinion still gets the right answer.)
|
|
9
|
+
* `${XDG_CONFIG_HOME:-~/.config}/systemd/user/<unit>`. A home passed in beats the ambient `XDG_CONFIG_HOME`: a test
|
|
10
|
+
* that names a throwaway home must not write a unit into the real config folder because the variable was set.
|
|
54
11
|
*/
|
|
55
|
-
export function unitPath(homeDir, xdgConfigHome) {
|
|
56
|
-
const base = xdgConfigHome ??
|
|
57
|
-
|
|
58
|
-
? path.join(homeDir, ".config")
|
|
59
|
-
: (process.env.XDG_CONFIG_HOME ?? path.join(os.homedir(), ".config")));
|
|
60
|
-
return path.join(base, "systemd", "user", SYSTEMD_UNIT);
|
|
12
|
+
export function unitPath(id, homeDir, xdgConfigHome) {
|
|
13
|
+
const base = xdgConfigHome ?? (homeDir !== undefined ? path.join(homeDir, '.config') : (process.env.XDG_CONFIG_HOME ?? path.join(os.homedir(), '.config')));
|
|
14
|
+
return path.join(base, 'systemd', 'user', id.unit);
|
|
61
15
|
}
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
* `%` starts a specifier EVERYWHERE in a unit file, so it is doubled; `$`
|
|
69
|
-
* starts a variable in `ExecStart`, so it is doubled there too.
|
|
70
|
-
*/
|
|
71
|
-
const pct = (s) => s.replace(/%/g, "%%");
|
|
72
|
-
const cEscape = (s) => s.replace(/\\/g, "\\\\").replace(/"/g, '\\"').replace(/\n/g, "\\n").replace(/\t/g, "\\t");
|
|
73
|
-
const cUnescape = (s) => s.replace(/\\(.)/g, (_, c) => (c === "n" ? "\n" : c === "t" ? "\t" : c));
|
|
74
|
-
/** One `ExecStart` word: specifiers and variables escaped, quoted when it has to be. */
|
|
16
|
+
// `%` starts a specifier anywhere in a unit file, so it's doubled; `$` starts a variable in `ExecStart`, so it's
|
|
17
|
+
// doubled there too.
|
|
18
|
+
const pct = (s) => s.replace(/%/g, '%%');
|
|
19
|
+
const cEscape = (s) => s.replace(/\\/g, '\\\\').replace(/"/g, '\\"').replace(/\n/g, '\\n').replace(/\t/g, '\\t');
|
|
20
|
+
const cUnescape = (s) => s.replace(/\\(.)/g, (_, c) => (c === 'n' ? '\n' : c === 't' ? '\t' : c));
|
|
21
|
+
/** One `ExecStart` word: specifiers and variables escaped, and quoted when it has to be. */
|
|
75
22
|
export function execWord(a) {
|
|
76
|
-
const s = pct(a).replace(/\$/g,
|
|
77
|
-
return s ===
|
|
23
|
+
const s = pct(a).replace(/\$/g, '$$$$');
|
|
24
|
+
return s === '' || /[\s"'\\;]/.test(s) ? `"${cEscape(s)}"` : s;
|
|
78
25
|
}
|
|
79
|
-
/**
|
|
80
|
-
* The other way: the words of an `ExecStart` line.
|
|
81
|
-
*
|
|
82
|
-
* Reads both shapes on purpose. A unit written before #796's quoting is a bare
|
|
83
|
-
* whitespace-separated line, and its owner is exactly the person whose `status`
|
|
84
|
-
* has to keep working — so unquoted words pass through unchanged, and quoted
|
|
85
|
-
* ones are unescaped.
|
|
86
|
-
*/
|
|
26
|
+
/** The words of an `ExecStart` line. Reads unquoted words too, so an older unit's `status` still works. */
|
|
87
27
|
export function execWords(line) {
|
|
88
|
-
|
|
89
|
-
for (const m of line.matchAll(/"((?:[^"\\]|\\.)*)"|(\S+)/g)) {
|
|
90
|
-
const w = m[1] !== undefined ? cUnescape(m[1]) : m[2];
|
|
91
|
-
out.push(w.replace(/\$\$/g, "$").replace(/%%/g, "%"));
|
|
92
|
-
}
|
|
93
|
-
return out;
|
|
28
|
+
return [...line.matchAll(/"((?:[^"\\]|\\.)*)"|(\S+)/g)].map((m) => (m[1] !== undefined ? cUnescape(m[1]) : m[2]).replace(/\$\$/g, '$').replace(/%%/g, '%'));
|
|
94
29
|
}
|
|
95
30
|
/**
|
|
96
|
-
*
|
|
97
|
-
*
|
|
98
|
-
* `Restart=always`, matching launchd's `KeepAlive=<true/>` (#872). It was
|
|
99
|
-
* `on-failure`, which for a service is the wrong default for the same reason it
|
|
100
|
-
* was wrong there: `start.ts` exits `0` on SIGTERM, so a logout — or anything
|
|
101
|
-
* else that signals the user manager — was read as an intended stop and Paddock
|
|
102
|
-
* stayed down.
|
|
103
|
-
*
|
|
104
|
-
* `always` does not make the unit unstoppable. systemd never restarts a unit it
|
|
105
|
-
* was itself asked to stop, so `systemctl --user stop` (and therefore
|
|
106
|
-
* `uninstall`) remains the clean, explicit way to put it down. That is the
|
|
107
|
-
* difference between an exit code, which has to guess at intent, and a command,
|
|
108
|
-
* which carries it.
|
|
109
|
-
*
|
|
110
|
-
* `WantedBy=default.target` is what makes `systemctl --user enable` mean
|
|
111
|
-
* "start at login" for a user unit; `multi-user.target` is the system-unit
|
|
112
|
-
* spelling and does nothing here.
|
|
113
|
-
*
|
|
114
|
-
* ## Logs go to a file, not the journal (SPEC §7)
|
|
115
|
-
*
|
|
116
|
-
* `StandardOutput=append:` / `StandardError=append:` name one file, the same
|
|
117
|
-
* one launchd's two keys name. The journal was the obvious choice and the wrong
|
|
118
|
-
* one for three reasons: `journalctl --user` shows nothing at all on a distro
|
|
119
|
-
* whose journal is volatile (the output is gone after a reboot, which is
|
|
120
|
-
* exactly when someone looks); it makes "where are the logs" have a different
|
|
121
|
-
* answer on each platform, so `paddock service logs` would be two
|
|
122
|
-
* implementations; and it rules out Windows, which the contract requires
|
|
123
|
-
* nothing rule out.
|
|
31
|
+
* The unit.
|
|
124
32
|
*
|
|
125
|
-
*
|
|
126
|
-
*
|
|
127
|
-
*
|
|
128
|
-
*
|
|
129
|
-
*
|
|
130
|
-
* `%` starts a specifier anywhere in a unit file and `$` starts a variable in
|
|
131
|
-
* `ExecStart`, so both are escaped; a word containing whitespace, a quote or a
|
|
132
|
-
* backslash is quoted. Paths with a space in them are the common case
|
|
133
|
-
* (`/Users/Ed Spencer/.paddock`), and the old unqualified `join(" ")` wrote a
|
|
134
|
-
* unit systemd then split into the wrong arguments — a service that fails at
|
|
135
|
-
* login having printed nothing.
|
|
33
|
+
* - `Restart=always`, matching launchd's `KeepAlive`: a server exits 0 on SIGTERM, so `on-failure` would leave it
|
|
34
|
+
* down after a logout signals the user's manager. systemd never restarts a unit it was asked to stop.
|
|
35
|
+
* - Both streams `append:` to one file rather than the journal, which is volatile on many distributions and would
|
|
36
|
+
* make `logs` a different program on each OS. systemd creates the file, not its folder, so `install` makes that.
|
|
37
|
+
* - Every `ExecStart` word is escaped and quoted where needed: paths with spaces are common (`/Users/Ed Spencer/…`).
|
|
136
38
|
*/
|
|
137
|
-
export function renderUnit(spec) {
|
|
138
|
-
const exec = [spec.nodePath, spec.scriptPath, ...spec.args].map(execWord).join(
|
|
139
|
-
|
|
39
|
+
export function renderUnit(id, spec) {
|
|
40
|
+
const exec = [spec.nodePath, spec.scriptPath, ...spec.args].map(execWord).join(' ');
|
|
41
|
+
const env = Object.entries({ PATH: spec.pathEnv, ...spec.env })
|
|
42
|
+
.map(([k, v]) => `Environment="${cEscape(pct(`${k}=${v}`))}"`)
|
|
43
|
+
.join('\n');
|
|
44
|
+
return `# Generated by ${id.name}: \`${id.tool} service install\` rewrites this file.
|
|
140
45
|
# Node: ${spec.nodePath}
|
|
141
46
|
[Unit]
|
|
142
|
-
Description
|
|
143
|
-
Documentation
|
|
144
|
-
|
|
47
|
+
Description=${id.description}
|
|
48
|
+
${id.documentation !== undefined ? `Documentation=${id.documentation}\n` : ''}
|
|
145
49
|
[Service]
|
|
146
50
|
Type=simple
|
|
147
51
|
ExecStart=${exec}
|
|
148
52
|
WorkingDirectory=${pct(spec.workingDirectory)}
|
|
149
|
-
|
|
53
|
+
${env}
|
|
150
54
|
StandardOutput=append:${pct(spec.logPath)}
|
|
151
55
|
StandardError=append:${pct(spec.logPath)}
|
|
152
56
|
Restart=always
|
|
@@ -156,160 +60,114 @@ RestartSec=10
|
|
|
156
60
|
WantedBy=default.target
|
|
157
61
|
`;
|
|
158
62
|
}
|
|
159
|
-
/**
|
|
63
|
+
/** `ExecStart=`, read back as an argv. */
|
|
64
|
+
export function parseUnitArgv(text) {
|
|
65
|
+
const m = /^ExecStart=(.*)$/m.exec(text);
|
|
66
|
+
return m === null ? [] : execWords(m[1].trim());
|
|
67
|
+
}
|
|
68
|
+
/** The log the unit appends to, if it names one. */
|
|
160
69
|
export function parseUnitLogPath(text) {
|
|
161
70
|
const m = /^StandardOutput=append:(.*)$/m.exec(text);
|
|
162
|
-
return m === null ? undefined : m[1].trim().replace(/%%/g,
|
|
71
|
+
return m === null ? undefined : m[1].trim().replace(/%%/g, '%');
|
|
163
72
|
}
|
|
164
|
-
/**
|
|
165
|
-
* Does an installed unit carry `Restart=always` (#872)?
|
|
166
|
-
*
|
|
167
|
-
* The counterpart to `plistKeepsAlive`, and narrow for the same reason: the
|
|
168
|
-
* value it has to reject is the `on-failure` this file used to write, which
|
|
169
|
-
* leaves Paddock down after the graceful exit `start.ts` performs on SIGTERM.
|
|
170
|
-
* Anything unrecognised reads as "not the fixed shape", which at worst tells a
|
|
171
|
-
* hand-edited unit's owner to re-run install.
|
|
172
|
-
*/
|
|
73
|
+
/** Whether the unit has `Restart=always` (the older `on-failure` doesn't count). */
|
|
173
74
|
export function unitKeepsAlive(text) {
|
|
174
75
|
return /^Restart=always\s*$/m.test(text);
|
|
175
76
|
}
|
|
176
|
-
/**
|
|
177
|
-
export function
|
|
178
|
-
|
|
179
|
-
if (m === null)
|
|
180
|
-
return [];
|
|
181
|
-
return execWords(m[1].trim());
|
|
182
|
-
}
|
|
183
|
-
/**
|
|
184
|
-
* What a `systemctl` failure means, in the manager's own words where it has any.
|
|
185
|
-
*
|
|
186
|
-
* The `status === null` branch is the one QA found: `spawnSync` reports a
|
|
187
|
-
* missing binary that way, with no output at all, so the message was
|
|
188
|
-
* `systemctl --user enable --now failed (exit null).` and then a blank line —
|
|
189
|
-
* which tells someone on a container with no systemd nothing whatsoever. That
|
|
190
|
-
* is also the single likeliest way this command fails on a machine it was never
|
|
191
|
-
* going to work on.
|
|
192
|
-
*/
|
|
193
|
-
export function systemctlFailure(args, r) {
|
|
77
|
+
/** What a `systemctl` failure means, in the manager's words where it has any. */
|
|
78
|
+
export function systemctlFailure(id, args, r) {
|
|
79
|
+
// spawnSync reports a missing binary as status null with no output, which would print nothing useful.
|
|
194
80
|
if (r.status === null || r.status === 127) {
|
|
195
|
-
return (
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
"Run `paddock serve` in a terminal, or keep it up with whatever this " +
|
|
199
|
-
"machine uses.");
|
|
81
|
+
return (`systemctl isn't available, so ${id.name} can't install a service here: it needs systemd's per-user instance ` +
|
|
82
|
+
`(\`systemctl --user\`), which a container or a distribution without systemd doesn't have. Run \`${id.tool} serve\` ` +
|
|
83
|
+
'in a terminal instead, or keep it running with whatever this machine uses.');
|
|
200
84
|
}
|
|
201
85
|
if (/Failed to connect to (user scope )?bus|DBUS_SESSION_BUS_ADDRESS|XDG_RUNTIME_DIR/.test(r.stderr)) {
|
|
202
|
-
return
|
|
203
|
-
"Run this from a login session, and check that $XDG_RUNTIME_DIR is set " +
|
|
204
|
-
"(usually /run/user/$(id -u)).");
|
|
86
|
+
return `can't reach systemd's per-user instance (${r.stderr.trim()}). Run this from a login session, with $XDG_RUNTIME_DIR set (usually /run/user/$(id -u)).`;
|
|
205
87
|
}
|
|
206
|
-
return
|
|
207
|
-
`${r.stderr || r.stdout}`.trim());
|
|
88
|
+
return `systemctl ${args.join(' ')} failed (exit ${String(r.status)}): ${`${r.stderr || r.stdout}`.trim()}`;
|
|
208
89
|
}
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
const
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
// defaulting `homeDir` to `os.homedir()` at this layer would collapse "the
|
|
219
|
-
// caller named a home" into "the caller named nothing" — the exact
|
|
220
|
-
// distinction that precedence turns on.
|
|
221
|
-
const file = unitPath(homeDir, xdgConfigHome);
|
|
90
|
+
export function createSystemd(id, o) {
|
|
91
|
+
const { run } = o;
|
|
92
|
+
const file = unitPath(id, o.homeDir, o.xdgConfigHome);
|
|
93
|
+
const systemctl = (...args) => {
|
|
94
|
+
const r = run('systemctl', ['--user', ...args]);
|
|
95
|
+
if (r.status !== 0)
|
|
96
|
+
throw new Error(systemctlFailure(id, ['--user', ...args], r));
|
|
97
|
+
};
|
|
98
|
+
const isActive = () => run('systemctl', ['--user', 'is-active', id.unit]).stdout.trim() === 'active';
|
|
222
99
|
return {
|
|
223
|
-
platform:
|
|
100
|
+
platform: 'linux',
|
|
224
101
|
unitPath: file,
|
|
225
|
-
label:
|
|
102
|
+
label: id.unit,
|
|
103
|
+
kind: 'a systemd --user service',
|
|
226
104
|
install(spec) {
|
|
227
|
-
// Asked
|
|
228
|
-
|
|
229
|
-
const wasActive =
|
|
105
|
+
// Asked before the unit is overwritten, while it still describes what's running.
|
|
106
|
+
const existed = fs.existsSync(file);
|
|
107
|
+
const wasActive = existed && isActive();
|
|
230
108
|
fs.mkdirSync(path.dirname(file), { recursive: true });
|
|
231
109
|
fs.mkdirSync(spec.workingDirectory, { recursive: true });
|
|
232
|
-
// systemd creates the log FILE for `append:` but not its directory, and a
|
|
233
|
-
// missing directory fails the unit at start with the server never having
|
|
234
|
-
// run — no output anywhere, because the thing that would have written it
|
|
235
|
-
// is what failed.
|
|
236
110
|
fs.mkdirSync(path.dirname(spec.logPath), { recursive: true });
|
|
237
|
-
fs.writeFileSync(file, renderUnit(spec),
|
|
238
|
-
// Without
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
const enabled = run("systemctl", args);
|
|
243
|
-
if (enabled.status !== 0)
|
|
244
|
-
throw new Error(systemctlFailure(args, enabled));
|
|
245
|
-
// And then RESTART, but only if it was ALREADY active, because `enable --now` does nothing
|
|
246
|
-
// to a unit that is already running — systemd starts it only if it is stopped. So a
|
|
247
|
-
// reinstall over a running service wrote a new unit, left the old process serving, and
|
|
248
|
-
// reported success.
|
|
249
|
-
//
|
|
250
|
-
// That is not a theoretical gap: it is the one `paddock update`'s ROLLBACK walks into. The
|
|
251
|
-
// forward path stops the service first (SPEC §5 step 9), so `enable --now` starts the new
|
|
252
|
-
// version cleanly; the rollback runs `service install --refresh` while the FAILED version
|
|
253
|
-
// is still up, and without this the "restored" service was the broken one — which turns a
|
|
254
|
-
// clean exit 6 into an exit 7 and a machine nobody has fixed.
|
|
255
|
-
//
|
|
256
|
-
// Conditional rather than unconditional, even though `restart` would converge both states
|
|
257
|
-
// in one call: on a stopped unit that would be TWO starts milliseconds apart, and systemd's
|
|
258
|
-
// default start limit is five in ten seconds. Halving the headroom of a unit whose whole
|
|
259
|
-
// job is to come back is not worth saving a question.
|
|
260
|
-
//
|
|
261
|
-
// launchd needs no equivalent — its install is a bootout followed by a bootstrap, which
|
|
262
|
-
// already replaces a running job.
|
|
263
|
-
if (wasActive) {
|
|
264
|
-
const restartArgs = ["--user", "restart", SYSTEMD_UNIT];
|
|
265
|
-
const restarted = run("systemctl", restartArgs);
|
|
266
|
-
if (restarted.status !== 0)
|
|
267
|
-
throw new Error(systemctlFailure(restartArgs, restarted));
|
|
111
|
+
fs.writeFileSync(file, renderUnit(id, spec), 'utf8');
|
|
112
|
+
// Without a reload the manager keeps serving its cached copy of the old unit.
|
|
113
|
+
run('systemctl', ['--user', 'daemon-reload']);
|
|
114
|
+
try {
|
|
115
|
+
systemctl('enable', '--now', id.unit);
|
|
268
116
|
}
|
|
117
|
+
catch (err) {
|
|
118
|
+
// A first install systemd refused (no systemd here, no user bus) mustn't leave a unit behind: `status` would
|
|
119
|
+
// then say it's installed. A reinstall keeps its unit, which is what's installed either way.
|
|
120
|
+
if (!existed) {
|
|
121
|
+
run('systemctl', ['--user', 'disable', id.unit]);
|
|
122
|
+
fs.rmSync(file, { force: true });
|
|
123
|
+
run('systemctl', ['--user', 'daemon-reload']);
|
|
124
|
+
}
|
|
125
|
+
throw err;
|
|
126
|
+
}
|
|
127
|
+
// `enable --now` doesn't touch a unit that's already active: a reinstall over a running service would leave the
|
|
128
|
+
// old process serving and report success. That's the state `update`'s rollback refreshes in, while the failed
|
|
129
|
+
// version is still up. So restart, but only then: on a stopped unit it would be two starts milliseconds apart,
|
|
130
|
+
// and systemd's default limit is five starts in ten seconds.
|
|
131
|
+
if (wasActive)
|
|
132
|
+
systemctl('restart', id.unit);
|
|
269
133
|
},
|
|
270
134
|
uninstall() {
|
|
271
135
|
const existed = fs.existsSync(file);
|
|
272
|
-
run(
|
|
136
|
+
run('systemctl', ['--user', 'disable', '--now', id.unit]);
|
|
273
137
|
if (existed)
|
|
274
138
|
fs.rmSync(file, { force: true });
|
|
275
|
-
run(
|
|
139
|
+
run('systemctl', ['--user', 'daemon-reload']);
|
|
276
140
|
return existed;
|
|
277
141
|
},
|
|
278
142
|
/**
|
|
279
|
-
*
|
|
280
|
-
*
|
|
281
|
-
*
|
|
282
|
-
* comes back at your NEXT login, which is a different question from whether
|
|
283
|
-
* it is running now, and quietly answering both would make `stop` a
|
|
284
|
-
* surprise the following morning. `uninstall` is where that link is cut.
|
|
285
|
-
*
|
|
286
|
-
* `restart` re-reads the unit itself, but a `daemon-reload` first is what
|
|
287
|
-
* makes an EDITED unit take effect rather than the manager's cached copy —
|
|
288
|
-
* the same reason `install` reloads before enabling.
|
|
143
|
+
* `start` clears a failed state first: a unit that hit the start limit stays failed, and won't start, until
|
|
144
|
+
* `reset-failed`. None of the three touches enable or disable: whether it starts at the next login is a
|
|
145
|
+
* different question from whether it's running now.
|
|
289
146
|
*/
|
|
290
147
|
start() {
|
|
291
|
-
|
|
148
|
+
run('systemctl', ['--user', 'reset-failed', id.unit]);
|
|
149
|
+
systemctl('start', id.unit);
|
|
292
150
|
},
|
|
293
151
|
stop() {
|
|
294
|
-
|
|
152
|
+
systemctl('stop', id.unit);
|
|
295
153
|
},
|
|
154
|
+
/** A reload first, so an edited unit takes effect. */
|
|
296
155
|
restart() {
|
|
297
|
-
run(
|
|
298
|
-
|
|
156
|
+
run('systemctl', ['--user', 'daemon-reload']);
|
|
157
|
+
run('systemctl', ['--user', 'reset-failed', id.unit]);
|
|
158
|
+
systemctl('restart', id.unit);
|
|
299
159
|
},
|
|
300
160
|
status() {
|
|
301
161
|
if (!fs.existsSync(file))
|
|
302
162
|
return { registered: false, running: false, argv: [], keepsAlive: true };
|
|
303
|
-
const text = fs.readFileSync(file,
|
|
163
|
+
const text = fs.readFileSync(file, 'utf8');
|
|
304
164
|
const argv = parseUnitArgv(text);
|
|
305
|
-
const
|
|
306
|
-
const pid = run("systemctl", ["--user", "show", "-p", "MainPID", "--value", SYSTEMD_UNIT]);
|
|
307
|
-
const mainPid = pid.stdout.trim();
|
|
165
|
+
const pid = run('systemctl', ['--user', 'show', '-p', 'MainPID', '--value', id.unit]).stdout.trim();
|
|
308
166
|
const logPath = parseUnitLogPath(text);
|
|
309
167
|
return {
|
|
310
168
|
registered: true,
|
|
311
|
-
running:
|
|
312
|
-
...(
|
|
169
|
+
running: isActive(),
|
|
170
|
+
...(pid !== '' && pid !== '0' ? { pid } : {}),
|
|
313
171
|
argv: argv.slice(2),
|
|
314
172
|
...(argv[0] !== undefined ? { nodePath: argv[0] } : {}),
|
|
315
173
|
...(argv[1] !== undefined ? { cliPath: argv[1] } : {}),
|
|
@@ -318,23 +176,22 @@ export function createSystemdBackend(run, homeDir, xdgConfigHome) {
|
|
|
318
176
|
};
|
|
319
177
|
},
|
|
320
178
|
/**
|
|
321
|
-
* `loginctl show-user <user> -p Linger` (
|
|
322
|
-
*
|
|
323
|
-
* Parsed rather than trusted to exit codes: `loginctl` is absent in plenty
|
|
324
|
-
* of containers and exits non-zero for a user logind has no record of, and
|
|
325
|
-
* neither of those is "linger is off". An unparseable answer is reported as
|
|
326
|
-
* unknown, which prints nothing — the alternative is handing a `sudo`
|
|
327
|
-
* command to someone whose machine has no `loginctl` to run it with.
|
|
179
|
+
* `loginctl show-user <user> -p Linger`, parsed rather than trusted to its exit code: no `loginctl` (a container)
|
|
180
|
+
* or no record of the user isn't "off". An answer it can't read is unknown, and the tool says nothing.
|
|
328
181
|
*/
|
|
329
182
|
linger() {
|
|
330
|
-
|
|
331
|
-
|
|
183
|
+
let user = o.user;
|
|
184
|
+
try {
|
|
185
|
+
user ??= os.userInfo().username;
|
|
186
|
+
}
|
|
187
|
+
catch {
|
|
188
|
+
user = process.env.USER;
|
|
189
|
+
}
|
|
190
|
+
if (user === undefined)
|
|
191
|
+
return { applies: true };
|
|
192
|
+
const r = run('loginctl', ['show-user', user, '-p', 'Linger']);
|
|
332
193
|
const m = /Linger=(yes|no)/i.exec(`${r.stdout}${r.stderr}`);
|
|
333
|
-
return {
|
|
334
|
-
applies: true,
|
|
335
|
-
user,
|
|
336
|
-
...(m !== null ? { enabled: m[1].toLowerCase() === "yes" } : {}),
|
|
337
|
-
};
|
|
194
|
+
return { applies: true, user, ...(m !== null ? { enabled: m[1].toLowerCase() === 'yes' } : {}) };
|
|
338
195
|
},
|
|
339
196
|
};
|
|
340
197
|
}
|