@edspencer/paddock 0.71.1 → 0.71.2
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 +11 -3
- package/packages/server/dist/cli/args.js +20 -2
- package/packages/server/dist/cli/service/backend.d.ts +33 -0
- package/packages/server/dist/cli/service/index.d.ts +3 -3
- package/packages/server/dist/cli/service/index.js +124 -2
- package/packages/server/dist/cli/service/launchd.d.ts +27 -4
- package/packages/server/dist/cli/service/launchd.js +88 -14
- package/packages/server/dist/cli/service/systemd.d.ts +21 -2
- package/packages/server/dist/cli/service/systemd.js +58 -5
- package/packages/server/package.json +1 -1
- package/packages/web/dist/assets/{Chip-Db--PJFH.js → Chip-Cp32dDmz.js} +1 -1
- package/packages/web/dist/assets/DiscoverPage-Cs439wTf.js +2 -0
- package/packages/web/dist/assets/{DiscoverView-C4hOB3nr.js → DiscoverView-DoGAlUXT.js} +1 -1
- package/packages/web/dist/assets/{InstanceConfigPage-U78M95nW.js → InstanceConfigPage-BDypeo_r.js} +1 -1
- package/packages/web/dist/assets/{MarkdownRenderer-DdoNl0Ao.js → MarkdownRenderer-DA5CpdGI.js} +1 -1
- package/packages/web/dist/assets/{ProjectMenu-BxQu14ze.js → ProjectMenu-DXXXtXeh.js} +1 -1
- package/packages/web/dist/assets/{ProjectRedirect-QuGeN-qx.js → ProjectRedirect-k10uM_63.js} +1 -1
- package/packages/web/dist/assets/ProjectView-BxjcMlSO.js +13 -0
- package/packages/web/dist/assets/{ProjectsGrid-CUFNVTif.js → ProjectsGrid-DrzZ7R8M.js} +1 -1
- package/packages/web/dist/assets/{RootHome-SIxo-xX9.js → RootHome-rWodzOC8.js} +1 -1
- package/packages/web/dist/assets/{arc-C0DRwCCT.js → arc-BKAVFR26.js} +1 -1
- package/packages/web/dist/assets/{architectureDiagram-3BPJPVTR-Wwpf4cvW.js → architectureDiagram-3BPJPVTR-UzIlxaCQ.js} +1 -1
- package/packages/web/dist/assets/{blockDiagram-GPEHLZMM-DKmCbgWk.js → blockDiagram-GPEHLZMM-Eu4BV7Ea.js} +1 -1
- package/packages/web/dist/assets/{c4Diagram-AAUBKEIU-CJ8LiLsj.js → c4Diagram-AAUBKEIU-DR8PZaoa.js} +1 -1
- package/packages/web/dist/assets/channel-C-sodtn7.js +2 -0
- package/packages/web/dist/assets/{chunk-2J33WTMH-C3P-O4C3.js → chunk-2J33WTMH-CgvIIrI1.js} +1 -1
- package/packages/web/dist/assets/{chunk-4BX2VUAB-Cs9u7x61.js → chunk-4BX2VUAB-CyJWrZqN.js} +1 -1
- package/packages/web/dist/assets/{chunk-55IACEB6-C1st284U.js → chunk-55IACEB6-DXEnZN1d.js} +1 -1
- package/packages/web/dist/assets/{chunk-727SXJPM-BjCAWo7B.js → chunk-727SXJPM-Co89QTmp.js} +1 -1
- package/packages/web/dist/assets/{chunk-AQP2D5EJ-BP6S9w2Q.js → chunk-AQP2D5EJ-BRxTzfev.js} +1 -1
- package/packages/web/dist/assets/{chunk-FMBD7UC4-C3ySTaPs.js → chunk-FMBD7UC4-DUj18CvF.js} +1 -1
- package/packages/web/dist/assets/{chunk-ND2GUHAM-eNPD4vzc.js → chunk-ND2GUHAM-BQMMJobN.js} +1 -1
- package/packages/web/dist/assets/{chunk-QZHKN3VN-BbBpgHkO.js → chunk-QZHKN3VN-COx8LZCe.js} +1 -1
- package/packages/web/dist/assets/classDiagram-4FO5ZUOK-D2eBPVdq.js +2 -0
- package/packages/web/dist/assets/classDiagram-v2-Q7XG4LA2-D2eBPVdq.js +2 -0
- package/packages/web/dist/assets/{core-ZwDDec0y.js → core-DnpcT0s6.js} +1 -1
- package/packages/web/dist/assets/{cose-bilkent-S5V4N54A-DiM4eeF5.js → cose-bilkent-S5V4N54A-B-4NadKI.js} +1 -1
- package/packages/web/dist/assets/{dagre-BM42HDAG-DKIEUM4M.js → dagre-BM42HDAG-CuaYJexd.js} +1 -1
- package/packages/web/dist/assets/{diagram-2AECGRRQ-CqlX7j8t.js → diagram-2AECGRRQ-C9_anxrA.js} +1 -1
- package/packages/web/dist/assets/{diagram-5GNKFQAL-Dph5apqo.js → diagram-5GNKFQAL-Ckf3kFlY.js} +1 -1
- package/packages/web/dist/assets/{diagram-KO2AKTUF-TW6LyHGt.js → diagram-KO2AKTUF-BrpUuY5x.js} +1 -1
- package/packages/web/dist/assets/{diagram-LMA3HP47-BxxIIv5a.js → diagram-LMA3HP47-B9lyXZXW.js} +1 -1
- package/packages/web/dist/assets/{diagram-OG6HWLK6-B8M0DeV7.js → diagram-OG6HWLK6-DT7ro32m.js} +1 -1
- package/packages/web/dist/assets/{erDiagram-TEJ5UH35-Bfpy6UQa.js → erDiagram-TEJ5UH35-ZZn9bHQh.js} +1 -1
- package/packages/web/dist/assets/{flowDiagram-I6XJVG4X-DstRzxek.js → flowDiagram-I6XJVG4X-BxQ82PY_.js} +1 -1
- package/packages/web/dist/assets/{ganttDiagram-6RSMTGT7-Bg0oFbAl.js → ganttDiagram-6RSMTGT7-BxliloOH.js} +1 -1
- package/packages/web/dist/assets/{gitGraphDiagram-PVQCEYII-CX4IozE-.js → gitGraphDiagram-PVQCEYII-BlwODdH9.js} +1 -1
- package/packages/web/dist/assets/{index-ASW1Jl0Y.js → index-Cx9y6c3f.js} +2 -2
- package/packages/web/dist/assets/{infoDiagram-5YYISTIA-CG2qCk6u.js → infoDiagram-5YYISTIA-nanSBFyG.js} +1 -1
- package/packages/web/dist/assets/{ishikawaDiagram-YF4QCWOH-CPErvl3a.js → ishikawaDiagram-YF4QCWOH-yZHr_s28.js} +1 -1
- package/packages/web/dist/assets/{journeyDiagram-JHISSGLW-Bm6krUsf.js → journeyDiagram-JHISSGLW-BKjDqQEQ.js} +1 -1
- package/packages/web/dist/assets/{kanban-definition-UN3LZRKU-B4CC8CAx.js → kanban-definition-UN3LZRKU-QusvHqc6.js} +1 -1
- package/packages/web/dist/assets/{linear-CJ1DbrOX.js → linear-4xRoD38Z.js} +1 -1
- package/packages/web/dist/assets/{mermaid.core-Dk2kkhf9.js → mermaid.core-CeulYvDv.js} +4 -4
- package/packages/web/dist/assets/{mindmap-definition-RKZ34NQL-CThvf_YD.js → mindmap-definition-RKZ34NQL-DSjYt2Kp.js} +1 -1
- package/packages/web/dist/assets/{pieDiagram-4H26LBE5-DvkHcng6.js → pieDiagram-4H26LBE5-Ci0gO3CA.js} +1 -1
- package/packages/web/dist/assets/{quadrantDiagram-W4KKPZXB-CqhrTh9q.js → quadrantDiagram-W4KKPZXB-y2qHkaHP.js} +1 -1
- package/packages/web/dist/assets/{requirementDiagram-4Y6WPE33-CZykiLym.js → requirementDiagram-4Y6WPE33-D9hiWbad.js} +1 -1
- package/packages/web/dist/assets/{sankeyDiagram-5OEKKPKP-D-TzCKMx.js → sankeyDiagram-5OEKKPKP-DA3Mvoze.js} +1 -1
- package/packages/web/dist/assets/{sequenceDiagram-3UESZ5HK-C5pIItVu.js → sequenceDiagram-3UESZ5HK-DWACrljw.js} +1 -1
- package/packages/web/dist/assets/{stateDiagram-AJRCARHV-DE6G2JfB.js → stateDiagram-AJRCARHV-CFGEhoTb.js} +1 -1
- package/packages/web/dist/assets/stateDiagram-v2-BHNVJYJU-CLfbDXLX.js +2 -0
- package/packages/web/dist/assets/{timeline-definition-PNZ67QCA-C1VK2OfU.js → timeline-definition-PNZ67QCA-Cjygpd9e.js} +1 -1
- package/packages/web/dist/assets/{vennDiagram-CIIHVFJN-CtOozEwa.js → vennDiagram-CIIHVFJN-zQnLaxgv.js} +1 -1
- package/packages/web/dist/assets/{wardley-L42UT6IY-DvHGK-lb.js → wardley-L42UT6IY-DGmt-Lo2.js} +1 -1
- package/packages/web/dist/assets/{wardleyDiagram-YWT4CUSO-DxKwZ3-G.js → wardleyDiagram-YWT4CUSO-Dj3vDukN.js} +1 -1
- package/packages/web/dist/assets/{xychartDiagram-2RQKCTM6-HM4j3l6y.js → xychartDiagram-2RQKCTM6-C0bGbwbL.js} +1 -1
- package/packages/web/dist/index.html +1 -1
- package/packages/web/dist/sw.js +1 -1
- package/packages/web/dist/assets/DiscoverPage-BFo4FTzZ.js +0 -2
- package/packages/web/dist/assets/ProjectView-FUcyJMUf.js +0 -13
- package/packages/web/dist/assets/channel-DEmtNKeB.js +0 -2
- package/packages/web/dist/assets/classDiagram-4FO5ZUOK-dAGAuccL.js +0 -2
- package/packages/web/dist/assets/classDiagram-v2-Q7XG4LA2-dAGAuccL.js +0 -2
- package/packages/web/dist/assets/stateDiagram-v2-BHNVJYJU-BP6oowh1.js +0 -2
package/package.json
CHANGED
|
@@ -36,8 +36,16 @@ export interface CliOptions {
|
|
|
36
36
|
export declare class CliError extends Error {
|
|
37
37
|
}
|
|
38
38
|
export declare const MIN_NODE_MAJOR = 22;
|
|
39
|
-
/**
|
|
40
|
-
|
|
39
|
+
/**
|
|
40
|
+
* What `paddock service` can be asked to do.
|
|
41
|
+
*
|
|
42
|
+
* `start`/`stop`/`restart` join the original three in #873. Note that `start`
|
|
43
|
+
* is also a top-level VERB — `paddock start` runs a server in this terminal,
|
|
44
|
+
* `paddock service start` asks the supervisor to run one. The overlap is safe
|
|
45
|
+
* because `parseCommand` dispatches on `argv[0]` alone and only then reads an
|
|
46
|
+
* action, so the two never compete for the same token.
|
|
47
|
+
*/
|
|
48
|
+
export declare const SERVICE_ACTIONS: readonly ["install", "uninstall", "status", "start", "stop", "restart"];
|
|
41
49
|
export type ServiceAction = (typeof SERVICE_ACTIONS)[number];
|
|
42
50
|
/** The leading words {@link parseCommand} recognises. Anything else is a flag. */
|
|
43
51
|
export declare const VERBS: readonly ["start", "service"];
|
|
@@ -100,4 +108,4 @@ export declare function nodeVersionProblem(nodeVersion: string): string | undefi
|
|
|
100
108
|
*/
|
|
101
109
|
export declare function explainListenError(err: unknown, host: string, port: string): string;
|
|
102
110
|
export declare const USAGE = "paddock \u2014 run a Paddock instance locally\n\nUsage\n npx @edspencer/paddock [options] start the server (the default)\n paddock start [options] the same thing, said out loud\n paddock service <install|uninstall|status>\n run it in the background from login\n (`paddock service --help`)\n\nOptions\n -p, --port <port> HTTP/WS port (default 7233, or $PORT)\n --host <host> Bind address (default 127.0.0.1)\n -d, --data-dir <path> Projects + state (default ~/.paddock, or $PADDOCK_DATA_DIR)\n -o, --open Open the app in your browser once it is listening\n --verbose Show the server's own logs (quiet by default)\n -v, --version Print the Paddock version and exit\n -h, --help Show this help\n\nOpening your own directories\n Where you run this from does not matter \u2014 the instance is decided by its data\n dir alone. Directories are added inside the app, on the Discover screen: it\n reads your Claude Code history, lists the directories you have worked in, and\n links the ones you tick as projects. A new instance opens on it.\n\n Importing a directory does not write into it. No .paddock/, no .chats/, no\n .gitignore edits, no CLAUDE.md \u2014 the project record and the copied transcripts\n both live in the data dir, and the project just points at the path. Your own\n ~/.claude transcripts are copied, never moved or deleted, so your terminal\n `claude` keeps working exactly as before.\n\nCredentials\n Paddock drives Claude Code, so it needs Claude credentials \u2014 and if you\n already use Claude Code on this machine, it uses the login you already have.\n That is a macOS Keychain entry on a Mac, or your ~/.claude/.credentials.json\n elsewhere (symlinked in, never copied). Reading a login writes nothing.\n\n Otherwise: a CLAUDE_CODE_OAUTH_TOKEN or ANTHROPIC_API_KEY in the environment,\n or a one-off `CLAUDE_CONFIG_DIR=<data-dir>/claude-home claude login`. With no\n login at all anywhere, run `claude setup-token`. Paddock says at startup when\n it can find none.\n\nSharing your Claude Code state\n Apart from that login, Paddock writes nothing outside its data dir by\n default: transcripts go to each project's .chats/, and your ~/.claude is read\n for config only. Each thing it can share is one key in\n <data-dir>/paddock.config.yaml:\n\n claude:\n transcripts: host # own | host, default own\n credentials: host # own | host, default host\n instructions: host # own | host, default own\n hooks: host # own | host, default own\n mcpServers: host # own | host, default own\n\n transcripts: host makes a chat and a `claude --resume` in the same directory\n the same file, live in both directions; deleting such a chat in Paddock\n releases it rather than removing it, because it is your history rather than\n Paddock's copy. credentials: own is the opt-out from sharing the login above.\n\n instructions: host loads your ~/.claude CLAUDE.md, agents/, commands/ and\n plugins/. Off by default, which is a change: your curated CLAUDE.md does not\n reach Paddock's agents unless you say so. Each project's own CLAUDE.md always\n does.\n\n hooks: host runs the shell commands your ~/.claude/settings.json binds to\n tool use. Off by default \u2014 inheriting someone's hooks is not something to\n discover after the fact. Its other keys (permissions, model, statusline)\n apply either way: under hooks: own Paddock writes its own settings.json\n carrying them with hooks dropped, regenerated at startup.\n\n mcpServers: host attaches the MCP servers declared in your ~/.claude.json \u2014\n the top-level ones, plus a project's own when its directory matches. To give\n this instance a server your machine doesn't have, declare it instead in a\n sibling mcpServers: block of the same file; use env:VAR_NAME anywhere a\n string goes so tokens stay out of it. That keeps a token out of the file, not\n out of `ps`: leave driveMode on its default (session) for a server holding\n one, since batch passes the definition to claude as a command-line argument.\n\nYour data\n Everything lives in one directory \u2014 ~/.paddock unless you pass --data-dir.\n Projects, chat transcripts and settings all persist there between runs. Move\n that directory to move your instance; delete it to start over. Nothing is\n stored anywhere else.\n\nNotes\n Binds loopback with authentication disabled, which is safe for a laptop. To\n expose it on a network, set PADDOCK_AUTH_MODE first \u2014 Paddock refuses to bind\n a routable interface wide open. See AUTH.md.\n\nDocs: https://github.com/edspencer/paddock";
|
|
103
|
-
export declare const SERVICE_USAGE = "paddock service \u2014 keep Paddock running in the background\n\nUsage\n paddock service install [options] register it and start it now\n paddock service uninstall stop it and deregister it\n paddock service status is it registered, is it running, where are the logs\n\nOptions (install only \u2014 recorded in the generated unit)\n -p, --port <port> HTTP/WS port (default 7233)\n --host <host> Bind address (default 127.0.0.1)\n -d, --data-dir <path> Only if you want an instance SEPARATE from your\n terminal one. Left out of the unit when you omit it\n AND `PADDOCK_DATA_DIR` is unset in this shell, so\n `paddock service` and a bare `paddock` are the same\n ~/.paddock instance reached two ways. If\n `PADDOCK_DATA_DIR` IS set here, that path is recorded\n in the unit \u2014 the service would otherwise point\n somewhere your terminal does not.\n --verbose Record the server's own logs, not just warnings\n\nAt login, not at boot\n This registers a per-USER service \u2014 a launchd LaunchAgent on macOS, a\n `systemd --user` unit on Linux \u2014 so it runs as you, with your own Claude\n login. That is not incidental: on macOS your Claude login is a Keychain item\n that only a logged-in user session can read. A boot-time system daemon has no\n such session and could not use it.\n\n So Paddock starts when you LOG IN, not when the machine boots. After a\n restart that nobody logs into, Paddock is not running. That is the design, not\n a fault.\n\n On Linux, a user service is also stopped when you log out. To keep it up:\n\n loginctl enable-linger $USER\n\nWhere it lives\n macOS ~/Library/LaunchAgents/net.edspencer.paddock.plist\n logs in <data-dir>/service/\n Linux ~/.config/systemd/user/paddock.service\n logs via journalctl --user -u paddock.service -f\n\nInstalled from npx?\n `service install` refuses. An npx cache path is hash-keyed and npm may prune\n it, so the unit would work until it silently didn't, at some future login.\n Install properly first:\n\n npm i -g @edspencer/paddock && paddock service install\n\nA note on access\n Paddock binds loopback with authentication off, which is right for a laptop.\n A service is up for as long as you are logged in rather than as long as a\n terminal tab, so that window is longer \u2014 but it is not wider: any local\n process that could reach the port could already read the same Claude login as\n you. Set PADDOCK_AUTH_MODE if you want a credential on it anyway.\n\nDocs: https://github.com/edspencer/paddock";
|
|
111
|
+
export declare const SERVICE_USAGE = "paddock service \u2014 keep Paddock running in the background\n\nUsage\n paddock service install [options] register it and start it now\n paddock service uninstall stop it and deregister it\n paddock service status is it registered, is it running, where are the logs\n paddock service start start an installed service that is stopped\n paddock service stop stop it, and leave it installed\n paddock service restart stop it and start it again, re-reading config\n\nOptions (install only \u2014 recorded in the generated unit)\n -p, --port <port> HTTP/WS port (default 7233)\n --host <host> Bind address (default 127.0.0.1)\n -d, --data-dir <path> Only if you want an instance SEPARATE from your\n terminal one. Left out of the unit when you omit it\n AND `PADDOCK_DATA_DIR` is unset in this shell, so\n `paddock service` and a bare `paddock` are the same\n ~/.paddock instance reached two ways. If\n `PADDOCK_DATA_DIR` IS set here, that path is recorded\n in the unit \u2014 the service would otherwise point\n somewhere your terminal does not.\n --verbose Record the server's own logs, not just warnings\n\nAt login, not at boot\n This registers a per-USER service \u2014 a launchd LaunchAgent on macOS, a\n `systemd --user` unit on Linux \u2014 so it runs as you, with your own Claude\n login. That is not incidental: on macOS your Claude login is a Keychain item\n that only a logged-in user session can read. A boot-time system daemon has no\n such session and could not use it.\n\n So Paddock starts when you LOG IN, not when the machine boots. After a\n restart that nobody logs into, Paddock is not running. That is the design, not\n a fault.\n\n On Linux, a user service is also stopped when you log out. To keep it up:\n\n loginctl enable-linger $USER\n\nWhere it lives\n macOS ~/Library/LaunchAgents/net.edspencer.paddock.plist\n logs in <data-dir>/service/\n Linux ~/.config/systemd/user/paddock.service\n logs via journalctl --user -u paddock.service -f\n\nInstalled from npx?\n `service install` refuses. An npx cache path is hash-keyed and npm may prune\n it, so the unit would work until it silently didn't, at some future login.\n Install properly first:\n\n npm i -g @edspencer/paddock && paddock service install\n\nA note on access\n Paddock binds loopback with authentication off, which is right for a laptop.\n A service is up for as long as you are logged in rather than as long as a\n terminal tab, so that window is longer \u2014 but it is not wider: any local\n process that could reach the port could already read the same Claude login as\n you. Set PADDOCK_AUTH_MODE if you want a credential on it anyway.\n\nDocs: https://github.com/edspencer/paddock";
|
|
@@ -27,8 +27,23 @@
|
|
|
27
27
|
export class CliError extends Error {
|
|
28
28
|
}
|
|
29
29
|
export const MIN_NODE_MAJOR = 22;
|
|
30
|
-
/**
|
|
31
|
-
|
|
30
|
+
/**
|
|
31
|
+
* What `paddock service` can be asked to do.
|
|
32
|
+
*
|
|
33
|
+
* `start`/`stop`/`restart` join the original three in #873. Note that `start`
|
|
34
|
+
* is also a top-level VERB — `paddock start` runs a server in this terminal,
|
|
35
|
+
* `paddock service start` asks the supervisor to run one. The overlap is safe
|
|
36
|
+
* because `parseCommand` dispatches on `argv[0]` alone and only then reads an
|
|
37
|
+
* action, so the two never compete for the same token.
|
|
38
|
+
*/
|
|
39
|
+
export const SERVICE_ACTIONS = [
|
|
40
|
+
"install",
|
|
41
|
+
"uninstall",
|
|
42
|
+
"status",
|
|
43
|
+
"start",
|
|
44
|
+
"stop",
|
|
45
|
+
"restart",
|
|
46
|
+
];
|
|
32
47
|
/** The leading words {@link parseCommand} recognises. Anything else is a flag. */
|
|
33
48
|
export const VERBS = ["start", "service"];
|
|
34
49
|
function isServiceAction(token) {
|
|
@@ -278,6 +293,9 @@ Usage
|
|
|
278
293
|
paddock service install [options] register it and start it now
|
|
279
294
|
paddock service uninstall stop it and deregister it
|
|
280
295
|
paddock service status is it registered, is it running, where are the logs
|
|
296
|
+
paddock service start start an installed service that is stopped
|
|
297
|
+
paddock service stop stop it, and leave it installed
|
|
298
|
+
paddock service restart stop it and start it again, re-reading config
|
|
281
299
|
|
|
282
300
|
Options (install only — recorded in the generated unit)
|
|
283
301
|
-p, --port <port> HTTP/WS port (default 7233)
|
|
@@ -20,6 +20,21 @@ export interface ServiceState {
|
|
|
20
20
|
pid?: string;
|
|
21
21
|
/** The Paddock arguments recorded in the installed unit, interpreter stripped. */
|
|
22
22
|
argv: string[];
|
|
23
|
+
/**
|
|
24
|
+
* Does the unit ON DISK come back after a graceful stop (#872)?
|
|
25
|
+
*
|
|
26
|
+
* Read from the installed file rather than from what we would write today,
|
|
27
|
+
* because those differ for exactly the people who need to hear about it.
|
|
28
|
+
* Upgrading the package does not rewrite a unit, so anyone who installed
|
|
29
|
+
* before #872 still has `SuccessfulExit: false` / `Restart=on-failure` — the
|
|
30
|
+
* shape where one sleep or logout stops Paddock for good. They cannot see
|
|
31
|
+
* that from the outside: the service simply is not running one morning.
|
|
32
|
+
* `status` is where they look, so `status` is where it has to say so.
|
|
33
|
+
*
|
|
34
|
+
* Meaningless when `registered` is false, and `true` there — "nothing to warn
|
|
35
|
+
* about" — so no caller has to special-case a unit that does not exist.
|
|
36
|
+
*/
|
|
37
|
+
keepsAlive: boolean;
|
|
23
38
|
}
|
|
24
39
|
export interface ServiceBackend {
|
|
25
40
|
platform: "darwin" | "linux";
|
|
@@ -31,6 +46,24 @@ export interface ServiceBackend {
|
|
|
31
46
|
/** True if something was actually there to remove. */
|
|
32
47
|
uninstall(): boolean;
|
|
33
48
|
status(): ServiceState;
|
|
49
|
+
/**
|
|
50
|
+
* Start / stop / restart an already-installed unit (#873).
|
|
51
|
+
*
|
|
52
|
+
* These exist so an intended stop has a channel of its OWN. Before them the
|
|
53
|
+
* only stop was `uninstall`, which also deregisters, so "bounce it after
|
|
54
|
+
* editing config" meant either losing the unit or dropping to raw
|
|
55
|
+
* `launchctl kickstart -k gui/$(id -u)/net.edspencer.paddock` — undiscoverable,
|
|
56
|
+
* and different on each platform. The exit-code heuristic that #872 removed
|
|
57
|
+
* was carrying the same meaning by inference; a command carries it directly.
|
|
58
|
+
*
|
|
59
|
+
* Each throws with the supervisor's own stderr rather than returning a
|
|
60
|
+
* boolean: the interesting failures here (a unit that was booted out, a
|
|
61
|
+
* manager that is not running) have text worth showing, and swallowing it
|
|
62
|
+
* would leave `start` printing a URL for a server that never came up.
|
|
63
|
+
*/
|
|
64
|
+
start(): void;
|
|
65
|
+
stop(): void;
|
|
66
|
+
restart(): void;
|
|
34
67
|
/** How to read the logs, as printed lines. */
|
|
35
68
|
logsHint(spec: ServiceSpec): string;
|
|
36
69
|
/** The `enable-linger` warning, or undefined where it does not apply. */
|
|
@@ -21,9 +21,9 @@ export interface ServiceContext {
|
|
|
21
21
|
/**
|
|
22
22
|
* "Did the URL answer?", injected for the same reason `run` is.
|
|
23
23
|
*
|
|
24
|
-
* Without a seam here every install
|
|
25
|
-
* full timeout and then assert on the disappointed
|
|
26
|
-
* per case, and green for the wrong reason.
|
|
24
|
+
* Without a seam here every `install` / `start` / `restart` test would really
|
|
25
|
+
* poll 127.0.0.1 for the full timeout and then assert on the disappointed
|
|
26
|
+
* message — twenty seconds per case, and green for the wrong reason.
|
|
27
27
|
*/
|
|
28
28
|
checkReady?: (url: string) => Promise<Readiness>;
|
|
29
29
|
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* `paddock service install | uninstall | status`
|
|
2
|
+
* `paddock service install | uninstall | status | start | stop | restart`
|
|
3
|
+
* (#796; the lifecycle three added in #873).
|
|
3
4
|
*
|
|
4
5
|
* Registers Paddock as a **per-user** background service — a launchd
|
|
5
6
|
* LaunchAgent on macOS, a `systemd --user` unit on Linux — so it is up from
|
|
@@ -26,7 +27,7 @@ import os from "node:os";
|
|
|
26
27
|
import path from "node:path";
|
|
27
28
|
import { CliError } from "../args.js";
|
|
28
29
|
import { buildSpec, defaultDataDir, hostFromArgv, isNpxPath, NPX_REFUSAL, portFromArgv, serviceDir, } from "./spec.js";
|
|
29
|
-
import { spawnRunner } from "./backend.js";
|
|
30
|
+
import { spawnRunner, } from "./backend.js";
|
|
30
31
|
import { createLaunchdBackend } from "./launchd.js";
|
|
31
32
|
import { createSystemdBackend } from "./systemd.js";
|
|
32
33
|
import { waitForReady, READY_TIMEOUT_LABEL } from "./ready.js";
|
|
@@ -77,6 +78,25 @@ function specFor(opts, ctx) {
|
|
|
77
78
|
const AT_LOGIN = " Paddock starts when you LOG IN, not when the machine boots — it runs as\n" +
|
|
78
79
|
" you, so it can use the Claude login you already have. After a restart\n" +
|
|
79
80
|
" that nobody logs into, Paddock is not running.";
|
|
81
|
+
/**
|
|
82
|
+
* What `status` says about a unit installed before #872.
|
|
83
|
+
*
|
|
84
|
+
* Upgrading the package leaves the unit file alone, so the people still holding
|
|
85
|
+
* the broken shape are exactly the people who will never notice — the symptom
|
|
86
|
+
* is Paddock being absent one morning, with nothing in the logs, because it was
|
|
87
|
+
* signalled rather than crashed. Naming the fix here is the only path from that
|
|
88
|
+
* symptom back to a working service.
|
|
89
|
+
*/
|
|
90
|
+
function staleUnitNote(backend) {
|
|
91
|
+
const key = backend.platform === "darwin"
|
|
92
|
+
? "KeepAlive is conditional on a non-zero exit"
|
|
93
|
+
: "Restart=on-failure";
|
|
94
|
+
return (` ⚠ This unit predates a fix (#872): ${key}, so a graceful stop —\n` +
|
|
95
|
+
" the kind sleep, logout or a stray `kill` sends — puts Paddock down\n" +
|
|
96
|
+
" until you start it again by hand. Re-run to update the unit:\n" +
|
|
97
|
+
"\n" +
|
|
98
|
+
" paddock service install");
|
|
99
|
+
}
|
|
80
100
|
/**
|
|
81
101
|
* Say what is about to happen, before the part that looks like a hang (#861).
|
|
82
102
|
*
|
|
@@ -135,6 +155,7 @@ async function install(opts, ctx, backend) {
|
|
|
135
155
|
...(linger !== undefined ? ["", linger] : []),
|
|
136
156
|
"",
|
|
137
157
|
" paddock service status is it up, and where",
|
|
158
|
+
" paddock service restart bounce it after editing config",
|
|
138
159
|
" paddock service uninstall stop it and remove the unit",
|
|
139
160
|
"",
|
|
140
161
|
].join("\n"));
|
|
@@ -194,16 +215,117 @@ function status(opts, ctx, backend) {
|
|
|
194
215
|
backend.logsHint(spec),
|
|
195
216
|
"",
|
|
196
217
|
AT_LOGIN,
|
|
218
|
+
...(state.keepsAlive ? [] : ["", staleUnitNote(backend)]),
|
|
197
219
|
...(linger !== undefined && !state.running ? ["", linger] : []),
|
|
198
220
|
"",
|
|
199
221
|
].join("\n"));
|
|
200
222
|
}
|
|
223
|
+
/**
|
|
224
|
+
* The URL an installed unit is actually serving, or undefined if none is.
|
|
225
|
+
*
|
|
226
|
+
* Read out of the unit rather than recomputed from today's flags, for the same
|
|
227
|
+
* reason `status` does it: the port in the file is the port the server is on.
|
|
228
|
+
*/
|
|
229
|
+
function installedUrl(state) {
|
|
230
|
+
return `http://${hostFromArgv(state.argv)}:${portFromArgv(state.argv)}`;
|
|
231
|
+
}
|
|
232
|
+
/** "It isn't installed" — the same answer, wherever it is asked. */
|
|
233
|
+
function notInstalled(backend) {
|
|
234
|
+
return [
|
|
235
|
+
"",
|
|
236
|
+
" Paddock is not installed as a service.",
|
|
237
|
+
` Looked for: ${backend.unitPath}`,
|
|
238
|
+
"",
|
|
239
|
+
" paddock service install register it and start it now",
|
|
240
|
+
"",
|
|
241
|
+
].join("\n");
|
|
242
|
+
}
|
|
243
|
+
/**
|
|
244
|
+
* `start` / `restart` (#873), which differ only in what they tolerate.
|
|
245
|
+
*
|
|
246
|
+
* `start` on something already running is reported and not acted on — asking
|
|
247
|
+
* launchd to bootstrap a loaded job is an error, and "it is already up" is the
|
|
248
|
+
* outcome the user wanted anyway. `restart` has no such case: bouncing a
|
|
249
|
+
* stopped service is a legitimate way to start it, and the sequence each
|
|
250
|
+
* backend uses handles both states.
|
|
251
|
+
*
|
|
252
|
+
* Both then WAIT. Neither supervisor's "started" means the port answers — it
|
|
253
|
+
* means the fork succeeded — so printing a URL on the strength of it produces a
|
|
254
|
+
* success message that can sit over a crash-loop (a port clash restarts every
|
|
255
|
+
* `ThrottleInterval`, and `launchctl print` says `running` for most of that
|
|
256
|
+
* window). The wait is what makes the last line true.
|
|
257
|
+
*/
|
|
258
|
+
async function startOrRestart(action, ctx, backend) {
|
|
259
|
+
const before = backend.status();
|
|
260
|
+
if (!before.registered) {
|
|
261
|
+
console.log(notInstalled(backend));
|
|
262
|
+
return;
|
|
263
|
+
}
|
|
264
|
+
const url = installedUrl(before);
|
|
265
|
+
if (action === "start" && before.running) {
|
|
266
|
+
console.log(`\n Paddock is already running${pid(before)}.\n\n URL: ${url}\n`);
|
|
267
|
+
return;
|
|
268
|
+
}
|
|
269
|
+
console.log(`\n ${action === "start" ? "Starting" : "Restarting"} Paddock…`);
|
|
270
|
+
if (action === "start")
|
|
271
|
+
backend.start();
|
|
272
|
+
else
|
|
273
|
+
backend.restart();
|
|
274
|
+
const readiness = await (ctx.checkReady ?? ((u) => waitForReady({ url: u })))(url);
|
|
275
|
+
const after = backend.status();
|
|
276
|
+
console.log([
|
|
277
|
+
"",
|
|
278
|
+
readiness === "ready"
|
|
279
|
+
? ` ✓ Paddock is running${pid(after)} at ${url}`
|
|
280
|
+
: ` Paddock was ${action === "start" ? "started" : "restarted"}, but ${url} did not\n` +
|
|
281
|
+
` answer within ${READY_TIMEOUT_LABEL}. It may still be coming up — check:`,
|
|
282
|
+
...(readiness === "ready" ? [] : ["", " paddock service status", backend.logsHint(specForLogs(ctx, after))]),
|
|
283
|
+
"",
|
|
284
|
+
].join("\n"));
|
|
285
|
+
}
|
|
286
|
+
/** `stop` (#873): put it down, leave it installed. */
|
|
287
|
+
function stop(backend) {
|
|
288
|
+
const state = backend.status();
|
|
289
|
+
if (!state.registered) {
|
|
290
|
+
console.log(notInstalled(backend));
|
|
291
|
+
return;
|
|
292
|
+
}
|
|
293
|
+
if (!state.running) {
|
|
294
|
+
console.log(`\n Paddock is already stopped.\n Unit: ${backend.unitPath}\n`);
|
|
295
|
+
return;
|
|
296
|
+
}
|
|
297
|
+
backend.stop();
|
|
298
|
+
console.log([
|
|
299
|
+
"",
|
|
300
|
+
" Paddock is stopped. The unit is still installed.",
|
|
301
|
+
` Unit: ${backend.unitPath}`,
|
|
302
|
+
"",
|
|
303
|
+
" It comes back when you log in again, or now with:",
|
|
304
|
+
"",
|
|
305
|
+
" paddock service start",
|
|
306
|
+
"",
|
|
307
|
+
].join("\n"));
|
|
308
|
+
}
|
|
309
|
+
function pid(state) {
|
|
310
|
+
return state.pid !== undefined ? ` (pid ${state.pid})` : "";
|
|
311
|
+
}
|
|
312
|
+
/** The spec `logsHint` needs, built from the INSTALLED unit's data dir. */
|
|
313
|
+
function specForLogs(ctx, state) {
|
|
314
|
+
const i = state.argv.indexOf("--data-dir");
|
|
315
|
+
const dataDir = i >= 0 && state.argv[i + 1] !== undefined ? state.argv[i + 1] : undefined;
|
|
316
|
+
const { spec } = specFor({ open: false, verbose: false, help: false, version: false }, { ...ctx, ...(dataDir !== undefined ? { envDataDir: dataDir } : {}) });
|
|
317
|
+
return spec;
|
|
318
|
+
}
|
|
201
319
|
export async function runService(action, opts, ctx) {
|
|
202
320
|
const backend = backendFor(ctx);
|
|
203
321
|
if (action === "install")
|
|
204
322
|
return install(opts, ctx, backend);
|
|
205
323
|
if (action === "uninstall")
|
|
206
324
|
return uninstall(ctx, backend);
|
|
325
|
+
if (action === "start" || action === "restart")
|
|
326
|
+
return startOrRestart(action, ctx, backend);
|
|
327
|
+
if (action === "stop")
|
|
328
|
+
return stop(backend);
|
|
207
329
|
return status(opts, ctx, backend);
|
|
208
330
|
}
|
|
209
331
|
/** Exported for the entrypoint's context assembly, and for tests. */
|
|
@@ -5,10 +5,22 @@ export declare function plistPath(homeDir?: string): string;
|
|
|
5
5
|
/**
|
|
6
6
|
* Render the plist.
|
|
7
7
|
*
|
|
8
|
-
* `KeepAlive` is
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
8
|
+
* `KeepAlive` is unconditional `<true/>`, and that is a deliberate reversal
|
|
9
|
+
* (#872). It used to be `{ SuccessfulExit: false }` — "restart it if it exits
|
|
10
|
+
* nonzero, leave it alone if it exits cleanly" — which reads like the polite
|
|
11
|
+
* choice and is, for a service, a trap. `start.ts` handles SIGTERM by shutting
|
|
12
|
+
* down cleanly and exiting `0`, so **every** SIGTERM the OS routinely sends a
|
|
13
|
+
* LaunchAgent (sleep, logout, a stray `kill`) looked to launchd like "it meant
|
|
14
|
+
* to stop", and Paddock stayed down until the next login. A hard crash was the
|
|
15
|
+
* survivable case; the graceful stop was the terminal one.
|
|
16
|
+
*
|
|
17
|
+
* The worry that motivated the dict — that `<true/>` fights a deliberate stop —
|
|
18
|
+
* does not hold for the stop this CLI actually performs. `launchctl bootout`
|
|
19
|
+
* unloads the job from the domain, and KeepAlive only applies to a job that is
|
|
20
|
+
* still loaded, so `uninstall` still stops Paddock in one call. What `<true/>`
|
|
21
|
+
* does fight is `launchctl stop`, which is why nothing here uses it.
|
|
22
|
+
*
|
|
23
|
+
* Intent belongs in an explicit command, not in an exit code.
|
|
12
24
|
*
|
|
13
25
|
* `EnvironmentVariables` carries PATH and nothing else. It carries PATH because
|
|
14
26
|
* launchd hands a process a stub PATH that a version manager's `node`, and the
|
|
@@ -32,6 +44,17 @@ export declare function renderPlist(spec: ServiceSpec): string;
|
|
|
32
44
|
* would be the larger risk.
|
|
33
45
|
*/
|
|
34
46
|
export declare function parsePlistArgv(xml: string): string[];
|
|
47
|
+
/**
|
|
48
|
+
* Does an installed plist carry the unconditional `KeepAlive` from #872?
|
|
49
|
+
*
|
|
50
|
+
* Deliberately narrow: it matches `<key>KeepAlive</key>` followed by `<true/>`
|
|
51
|
+
* and nothing else. The shape it must NOT accept is the old
|
|
52
|
+
* `<dict><key>SuccessfulExit</key><false/></dict>`, which is the one that
|
|
53
|
+
* leaves a service dead after a graceful stop — so a `false` here is the signal
|
|
54
|
+
* `status` turns into "re-run install", and defaulting to `true` on anything
|
|
55
|
+
* unrecognised would silence exactly the case this exists to catch.
|
|
56
|
+
*/
|
|
57
|
+
export declare function plistKeepsAlive(xml: string): boolean;
|
|
35
58
|
/**
|
|
36
59
|
* Read liveness out of `launchctl print gui/<uid>/<label>`.
|
|
37
60
|
*
|
|
@@ -51,10 +51,22 @@ function xmlEscape(s) {
|
|
|
51
51
|
/**
|
|
52
52
|
* Render the plist.
|
|
53
53
|
*
|
|
54
|
-
* `KeepAlive` is
|
|
55
|
-
*
|
|
56
|
-
*
|
|
57
|
-
*
|
|
54
|
+
* `KeepAlive` is unconditional `<true/>`, and that is a deliberate reversal
|
|
55
|
+
* (#872). It used to be `{ SuccessfulExit: false }` — "restart it if it exits
|
|
56
|
+
* nonzero, leave it alone if it exits cleanly" — which reads like the polite
|
|
57
|
+
* choice and is, for a service, a trap. `start.ts` handles SIGTERM by shutting
|
|
58
|
+
* down cleanly and exiting `0`, so **every** SIGTERM the OS routinely sends a
|
|
59
|
+
* LaunchAgent (sleep, logout, a stray `kill`) looked to launchd like "it meant
|
|
60
|
+
* to stop", and Paddock stayed down until the next login. A hard crash was the
|
|
61
|
+
* survivable case; the graceful stop was the terminal one.
|
|
62
|
+
*
|
|
63
|
+
* The worry that motivated the dict — that `<true/>` fights a deliberate stop —
|
|
64
|
+
* does not hold for the stop this CLI actually performs. `launchctl bootout`
|
|
65
|
+
* unloads the job from the domain, and KeepAlive only applies to a job that is
|
|
66
|
+
* still loaded, so `uninstall` still stops Paddock in one call. What `<true/>`
|
|
67
|
+
* does fight is `launchctl stop`, which is why nothing here uses it.
|
|
68
|
+
*
|
|
69
|
+
* Intent belongs in an explicit command, not in an exit code.
|
|
58
70
|
*
|
|
59
71
|
* `EnvironmentVariables` carries PATH and nothing else. It carries PATH because
|
|
60
72
|
* launchd hands a process a stub PATH that a version manager's `node`, and the
|
|
@@ -82,10 +94,7 @@ ${strings}
|
|
|
82
94
|
<key>RunAtLoad</key>
|
|
83
95
|
<true/>
|
|
84
96
|
<key>KeepAlive</key>
|
|
85
|
-
<
|
|
86
|
-
<key>SuccessfulExit</key>
|
|
87
|
-
<false/>
|
|
88
|
-
</dict>
|
|
97
|
+
<true/>
|
|
89
98
|
<key>ThrottleInterval</key>
|
|
90
99
|
<integer>10</integer>
|
|
91
100
|
<key>WorkingDirectory</key>
|
|
@@ -123,6 +132,19 @@ export function parsePlistArgv(xml) {
|
|
|
123
132
|
.replace(/</g, "<")
|
|
124
133
|
.replace(/&/g, "&"));
|
|
125
134
|
}
|
|
135
|
+
/**
|
|
136
|
+
* Does an installed plist carry the unconditional `KeepAlive` from #872?
|
|
137
|
+
*
|
|
138
|
+
* Deliberately narrow: it matches `<key>KeepAlive</key>` followed by `<true/>`
|
|
139
|
+
* and nothing else. The shape it must NOT accept is the old
|
|
140
|
+
* `<dict><key>SuccessfulExit</key><false/></dict>`, which is the one that
|
|
141
|
+
* leaves a service dead after a graceful stop — so a `false` here is the signal
|
|
142
|
+
* `status` turns into "re-run install", and defaulting to `true` on anything
|
|
143
|
+
* unrecognised would silence exactly the case this exists to catch.
|
|
144
|
+
*/
|
|
145
|
+
export function plistKeepsAlive(xml) {
|
|
146
|
+
return /<key>KeepAlive<\/key>\s*<true\s*\/>/.test(xml);
|
|
147
|
+
}
|
|
126
148
|
/**
|
|
127
149
|
* Read liveness out of `launchctl print gui/<uid>/<label>`.
|
|
128
150
|
*
|
|
@@ -139,6 +161,10 @@ export function parseLaunchctlPrint(out) {
|
|
|
139
161
|
...(pid !== null ? { pid: pid[1] } : {}),
|
|
140
162
|
};
|
|
141
163
|
}
|
|
164
|
+
/** One phrasing for "launchctl said no", so every caller reports it the same way. */
|
|
165
|
+
function launchctlFailure(verb, r) {
|
|
166
|
+
return (`launchctl ${verb} failed (exit ${String(r.status)}).\n` + `${r.stderr || r.stdout}`.trim());
|
|
167
|
+
}
|
|
142
168
|
function target() {
|
|
143
169
|
return `gui/${process.getuid?.() ?? 0}/${LAUNCHD_LABEL}`;
|
|
144
170
|
}
|
|
@@ -164,10 +190,8 @@ export function createLaunchdBackend(run, homeDir = os.homedir()) {
|
|
|
164
190
|
// they are shims over this one that report success in cases where it
|
|
165
191
|
// reports the actual error.
|
|
166
192
|
const boot = run("launchctl", ["bootstrap", domain(), file]);
|
|
167
|
-
if (boot.status !== 0)
|
|
168
|
-
throw new Error(
|
|
169
|
-
`${boot.stderr || boot.stdout}`.trim());
|
|
170
|
-
}
|
|
193
|
+
if (boot.status !== 0)
|
|
194
|
+
throw new Error(launchctlFailure("bootstrap", boot));
|
|
171
195
|
// `RunAtLoad` should already have started it; `kickstart -k` makes that
|
|
172
196
|
// true rather than probable, and is the same command a later restart uses.
|
|
173
197
|
run("launchctl", ["kickstart", "-k", target()]);
|
|
@@ -179,10 +203,59 @@ export function createLaunchdBackend(run, homeDir = os.homedir()) {
|
|
|
179
203
|
fs.rmSync(file, { force: true });
|
|
180
204
|
return existed;
|
|
181
205
|
},
|
|
206
|
+
/**
|
|
207
|
+
* Load the job back into the GUI domain and make sure it is running (#873).
|
|
208
|
+
*
|
|
209
|
+
* `bootstrap`, not `launchctl start`: `stop` below BOOTS OUT, so a stopped
|
|
210
|
+
* Paddock is not merely idle, it is absent from the domain — and `start` on
|
|
211
|
+
* a label launchd has never heard of fails. Bootstrapping is also what a
|
|
212
|
+
* login does, so `service start` and the next login take the same path.
|
|
213
|
+
*/
|
|
214
|
+
start() {
|
|
215
|
+
const boot = run("launchctl", ["bootstrap", domain(), file]);
|
|
216
|
+
if (boot.status !== 0)
|
|
217
|
+
throw new Error(launchctlFailure("bootstrap", boot));
|
|
218
|
+
run("launchctl", ["kickstart", target()]);
|
|
219
|
+
},
|
|
220
|
+
/**
|
|
221
|
+
* `bootout`, not `launchctl stop`.
|
|
222
|
+
*
|
|
223
|
+
* With #872's unconditional `KeepAlive`, `stop` is a request launchd
|
|
224
|
+
* immediately overrules — it stops the process and then relaunches it,
|
|
225
|
+
* which is exactly the wrestling match the old conditional KeepAlive was
|
|
226
|
+
* meant to avoid. `bootout` unloads the job, so KeepAlive no longer applies
|
|
227
|
+
* and it stays down. The plist is untouched, so `status` still reports it
|
|
228
|
+
* registered, `start` puts it back, and the next LOGIN starts it again via
|
|
229
|
+
* `RunAtLoad` — a stop is for this session, not forever. Uninstall is the
|
|
230
|
+
* one that is forever.
|
|
231
|
+
*/
|
|
232
|
+
stop() {
|
|
233
|
+
const out = run("launchctl", ["bootout", target()]);
|
|
234
|
+
// Exit 3 / "No such process" is launchd for "it was not loaded", which is
|
|
235
|
+
// the state the caller asked for. Anything else is worth surfacing.
|
|
236
|
+
if (out.status !== 0 && !/no such process/i.test(`${out.stderr}${out.stdout}`)) {
|
|
237
|
+
throw new Error(launchctlFailure("bootout", out));
|
|
238
|
+
}
|
|
239
|
+
},
|
|
240
|
+
/**
|
|
241
|
+
* Boot out, bootstrap, kick. Not `kickstart -k` alone, which needs the job
|
|
242
|
+
* to be loaded and so fails on the case `restart` most needs to handle: a
|
|
243
|
+
* service that is stopped. This sequence is identical to `install`'s minus
|
|
244
|
+
* writing the plist, which also gives `restart` the property the issue asks
|
|
245
|
+
* for — the unit is re-read on the way up, so an edited plist takes effect.
|
|
246
|
+
*/
|
|
247
|
+
restart() {
|
|
248
|
+
run("launchctl", ["bootout", target()]);
|
|
249
|
+
const boot = run("launchctl", ["bootstrap", domain(), file]);
|
|
250
|
+
if (boot.status !== 0)
|
|
251
|
+
throw new Error(launchctlFailure("bootstrap", boot));
|
|
252
|
+
run("launchctl", ["kickstart", "-k", target()]);
|
|
253
|
+
},
|
|
182
254
|
status() {
|
|
183
255
|
if (!fs.existsSync(file))
|
|
184
|
-
return { registered: false, running: false, argv: [] };
|
|
185
|
-
const
|
|
256
|
+
return { registered: false, running: false, argv: [], keepsAlive: true };
|
|
257
|
+
const xml = fs.readFileSync(file, "utf8");
|
|
258
|
+
const argv = parsePlistArgv(xml);
|
|
186
259
|
const printed = run("launchctl", ["print", target()]);
|
|
187
260
|
const parsed = printed.status === 0
|
|
188
261
|
? parseLaunchctlPrint(printed.stdout)
|
|
@@ -194,6 +267,7 @@ export function createLaunchdBackend(run, homeDir = os.homedir()) {
|
|
|
194
267
|
// Strip the interpreter and script: what a reader wants from `status`
|
|
195
268
|
// is the Paddock invocation, not the absolute path to node twice.
|
|
196
269
|
argv: argv.slice(2),
|
|
270
|
+
keepsAlive: plistKeepsAlive(xml),
|
|
197
271
|
};
|
|
198
272
|
},
|
|
199
273
|
logsHint(spec) {
|
|
@@ -31,8 +31,17 @@ export declare function unitPath(homeDir?: string, xdgConfigHome?: string): stri
|
|
|
31
31
|
/**
|
|
32
32
|
* Render the unit.
|
|
33
33
|
*
|
|
34
|
-
* `Restart=
|
|
35
|
-
*
|
|
34
|
+
* `Restart=always`, matching launchd's `KeepAlive=<true/>` (#872). It was
|
|
35
|
+
* `on-failure`, which for a service is the wrong default for the same reason it
|
|
36
|
+
* was wrong there: `start.ts` exits `0` on SIGTERM, so a logout — or anything
|
|
37
|
+
* else that signals the user manager — was read as an intended stop and Paddock
|
|
38
|
+
* stayed down.
|
|
39
|
+
*
|
|
40
|
+
* `always` does not make the unit unstoppable. systemd never restarts a unit it
|
|
41
|
+
* was itself asked to stop, so `systemctl --user stop` (and therefore
|
|
42
|
+
* `uninstall`) remains the clean, explicit way to put it down. That is the
|
|
43
|
+
* difference between an exit code, which has to guess at intent, and a command,
|
|
44
|
+
* which carries it.
|
|
36
45
|
*
|
|
37
46
|
* `WantedBy=default.target` is what makes `systemctl --user enable` mean
|
|
38
47
|
* "start at login" for a user unit; `multi-user.target` is the system-unit
|
|
@@ -43,6 +52,16 @@ export declare function unitPath(homeDir?: string, xdgConfigHome?: string): stri
|
|
|
43
52
|
* answers to "where are the logs".
|
|
44
53
|
*/
|
|
45
54
|
export declare function renderUnit(spec: ServiceSpec): string;
|
|
55
|
+
/**
|
|
56
|
+
* Does an installed unit carry `Restart=always` (#872)?
|
|
57
|
+
*
|
|
58
|
+
* The counterpart to `plistKeepsAlive`, and narrow for the same reason: the
|
|
59
|
+
* value it has to reject is the `on-failure` this file used to write, which
|
|
60
|
+
* leaves Paddock down after the graceful exit `start.ts` performs on SIGTERM.
|
|
61
|
+
* Anything unrecognised reads as "not the fixed shape", which at worst tells a
|
|
62
|
+
* hand-edited unit's owner to re-run install.
|
|
63
|
+
*/
|
|
64
|
+
export declare function unitKeepsAlive(text: string): boolean;
|
|
46
65
|
/** Read `ExecStart=` back out of an installed unit, split into an argv. */
|
|
47
66
|
export declare function parseUnitArgv(text: string): string[];
|
|
48
67
|
export declare function createSystemdBackend(run: Runner, homeDir?: string, xdgConfigHome?: string): ServiceBackend;
|