@edspencer/paddock 0.79.1 → 0.81.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/app.d.ts +7 -0
- package/packages/server/dist/app.js +19 -15
- package/packages/server/dist/auth.d.ts +3 -2
- package/packages/server/dist/auth.js +11 -0
- package/packages/server/dist/background-live.d.ts +17 -0
- package/packages/server/dist/background-live.js +24 -0
- package/packages/server/dist/busy.d.ts +96 -0
- package/packages/server/dist/busy.js +88 -0
- package/packages/server/dist/cli/args.d.ts +63 -7
- package/packages/server/dist/cli/args.js +227 -20
- package/packages/server/dist/cli/busy-command.d.ts +58 -0
- package/packages/server/dist/cli/busy-command.js +108 -0
- package/packages/server/dist/cli/busy.d.ts +69 -0
- package/packages/server/dist/cli/busy.js +150 -0
- package/packages/server/dist/cli/exit-codes.d.ts +49 -0
- package/packages/server/dist/cli/exit-codes.js +48 -0
- package/packages/server/dist/cli/output.d.ts +52 -0
- package/packages/server/dist/cli/output.js +84 -0
- package/packages/server/dist/cli/paddock.js +98 -37
- package/packages/server/dist/cli/service/backend.d.ts +48 -4
- package/packages/server/dist/cli/service/index.d.ts +68 -2
- package/packages/server/dist/cli/service/index.js +321 -60
- package/packages/server/dist/cli/service/launchd.d.ts +23 -1
- package/packages/server/dist/cli/service/launchd.js +145 -25
- package/packages/server/dist/cli/service/logs.d.ts +39 -0
- package/packages/server/dist/cli/service/logs.js +115 -0
- package/packages/server/dist/cli/service/path-env.d.ts +27 -0
- package/packages/server/dist/cli/service/path-env.js +120 -0
- package/packages/server/dist/cli/service/ready.d.ts +22 -0
- package/packages/server/dist/cli/service/ready.js +30 -0
- package/packages/server/dist/cli/service/spec.d.ts +45 -6
- package/packages/server/dist/cli/service/spec.js +34 -7
- package/packages/server/dist/cli/service/systemd.d.ts +47 -4
- package/packages/server/dist/cli/service/systemd.js +127 -22
- package/packages/server/dist/herdctl.d.ts +19 -0
- package/packages/server/dist/herdctl.js +26 -0
- package/packages/server/dist/management-mcp-server.d.ts +21 -3
- package/packages/server/dist/management-mcp-server.js +4 -13
- package/packages/server/dist/routes/meta.js +56 -3
- package/packages/server/dist/runtime-file.d.ts +26 -0
- package/packages/server/dist/runtime-file.js +91 -0
- package/packages/server/dist/service-log.d.ts +29 -0
- package/packages/server/dist/service-log.js +104 -0
- package/packages/server/dist/start.d.ts +33 -0
- package/packages/server/dist/start.js +126 -8
- package/packages/server/dist/version.d.ts +3 -0
- package/packages/server/dist/version.js +32 -0
- package/packages/server/dist/ws-context.d.ts +9 -0
- package/packages/server/dist/ws-turn.js +1 -0
- package/packages/server/package.json +1 -1
- package/packages/web/dist/assets/{DeleteProjectDialog-BcxJMA1w.js → DeleteProjectDialog-D8oIJ8HC.js} +1 -1
- package/packages/web/dist/assets/DiscoverPage-CQlzxsn4.js +2 -0
- package/packages/web/dist/assets/{DiscoverView-DOmTmiVv.js → DiscoverView-o0KCr3aB.js} +1 -1
- package/packages/web/dist/assets/{InstanceConfigPage-DWLQqHW0.js → InstanceConfigPage-RSJz7oL3.js} +1 -1
- package/packages/web/dist/assets/{MarkdownRenderer-Y25JWlMR.js → MarkdownRenderer-39mafH8b.js} +1 -1
- package/packages/web/dist/assets/{ProjectRedirect-CCI1VWM6.js → ProjectRedirect-Bq7GVguY.js} +1 -1
- package/packages/web/dist/assets/{ProjectView-DzfVhjya.js → ProjectView-BoBIru1g.js} +3 -3
- package/packages/web/dist/assets/{ProjectsGrid-yIyyb2HA.js → ProjectsGrid-mJ4gqbr6.js} +1 -1
- package/packages/web/dist/assets/{RootHome-x0Om9-Fd.js → RootHome-C0kwFyYp.js} +1 -1
- package/packages/web/dist/assets/{Surfaces-wbcMQIsK.js → Surfaces-MkhXlB8z.js} +1 -1
- package/packages/web/dist/assets/{arc-tWN7FYIq.js → arc-DExlwAwO.js} +1 -1
- package/packages/web/dist/assets/{architectureDiagram-3BPJPVTR-Ccj6Vlyy.js → architectureDiagram-3BPJPVTR-CZly3W79.js} +1 -1
- package/packages/web/dist/assets/{blockDiagram-GPEHLZMM-B7iGXwqY.js → blockDiagram-GPEHLZMM-AgES73i3.js} +1 -1
- package/packages/web/dist/assets/{c4Diagram-AAUBKEIU-SkLKFSso.js → c4Diagram-AAUBKEIU-N9mD8Pfk.js} +1 -1
- package/packages/web/dist/assets/channel-ExsAG7qT.js +2 -0
- package/packages/web/dist/assets/{chunk-2J33WTMH-BrS8C-YK.js → chunk-2J33WTMH-Dsx_kfUl.js} +1 -1
- package/packages/web/dist/assets/{chunk-4BX2VUAB-BUR6nG8w.js → chunk-4BX2VUAB-BQjqcjPj.js} +1 -1
- package/packages/web/dist/assets/{chunk-55IACEB6-BRBNJb-u.js → chunk-55IACEB6-CCxpYQpY.js} +1 -1
- package/packages/web/dist/assets/{chunk-727SXJPM-DKeNPTXz.js → chunk-727SXJPM-DpFRdnUa.js} +1 -1
- package/packages/web/dist/assets/{chunk-AQP2D5EJ-BOS1K-DX.js → chunk-AQP2D5EJ-D2BAlzoO.js} +1 -1
- package/packages/web/dist/assets/{chunk-FMBD7UC4-D3PZL8fo.js → chunk-FMBD7UC4-BFaMRzUF.js} +1 -1
- package/packages/web/dist/assets/{chunk-ND2GUHAM-DA9Y77Zj.js → chunk-ND2GUHAM-B9NGJ9xZ.js} +1 -1
- package/packages/web/dist/assets/{chunk-QZHKN3VN-wPph-weq.js → chunk-QZHKN3VN-CF1IHyMS.js} +1 -1
- package/packages/web/dist/assets/classDiagram-4FO5ZUOK-si7g9okq.js +2 -0
- package/packages/web/dist/assets/classDiagram-v2-Q7XG4LA2-si7g9okq.js +2 -0
- package/packages/web/dist/assets/{core-I1PQdGMJ.js → core-XjElsvQ7.js} +1 -1
- package/packages/web/dist/assets/{cose-bilkent-S5V4N54A-BA7N2vVK.js → cose-bilkent-S5V4N54A-CF972Flx.js} +1 -1
- package/packages/web/dist/assets/{dagre-BM42HDAG-DKpON5p2.js → dagre-BM42HDAG-DsneI8YE.js} +1 -1
- package/packages/web/dist/assets/{diagram-2AECGRRQ-Db0I-eEL.js → diagram-2AECGRRQ-BHyrVzIk.js} +1 -1
- package/packages/web/dist/assets/{diagram-5GNKFQAL-BEuqOTfn.js → diagram-5GNKFQAL-Dj4q8xEi.js} +1 -1
- package/packages/web/dist/assets/{diagram-KO2AKTUF-CnyX_0Xk.js → diagram-KO2AKTUF-BHe3Puv_.js} +1 -1
- package/packages/web/dist/assets/{diagram-LMA3HP47-CYvknzDd.js → diagram-LMA3HP47-wzt6G2FE.js} +1 -1
- package/packages/web/dist/assets/{diagram-OG6HWLK6-DlgKxbYz.js → diagram-OG6HWLK6-CZMbMRJq.js} +1 -1
- package/packages/web/dist/assets/{documentTitle-Cvob-HVs.js → documentTitle-BwTIA8rI.js} +1 -1
- package/packages/web/dist/assets/{erDiagram-TEJ5UH35-BkO5JmiM.js → erDiagram-TEJ5UH35-Dtu14BL1.js} +1 -1
- package/packages/web/dist/assets/{flowDiagram-I6XJVG4X-DG0o_zc1.js → flowDiagram-I6XJVG4X-T34ql9j0.js} +1 -1
- package/packages/web/dist/assets/{ganttDiagram-6RSMTGT7-Bs71No2S.js → ganttDiagram-6RSMTGT7-DQLOeCXK.js} +1 -1
- package/packages/web/dist/assets/{gitGraphDiagram-PVQCEYII-CxODhRKd.js → gitGraphDiagram-PVQCEYII-Bk11zea6.js} +1 -1
- package/packages/web/dist/assets/{index-C4tsPSvG.js → index-CvaWErFv.js} +2 -2
- package/packages/web/dist/assets/{infoDiagram-5YYISTIA-C412TQaw.js → infoDiagram-5YYISTIA-CsE83lu0.js} +1 -1
- package/packages/web/dist/assets/{ishikawaDiagram-YF4QCWOH-DUlcZXQQ.js → ishikawaDiagram-YF4QCWOH-DNVgA93n.js} +1 -1
- package/packages/web/dist/assets/{journeyDiagram-JHISSGLW-iL0klVhn.js → journeyDiagram-JHISSGLW-CLpSNjM3.js} +1 -1
- package/packages/web/dist/assets/{kanban-definition-UN3LZRKU-CO02Zl_Q.js → kanban-definition-UN3LZRKU-BRzuXcKr.js} +1 -1
- package/packages/web/dist/assets/{linear-DtmLY8AG.js → linear-BxOawONt.js} +1 -1
- package/packages/web/dist/assets/{mermaid.core-Dvzxug80.js → mermaid.core-Dvs1QCcz.js} +4 -4
- package/packages/web/dist/assets/{mindmap-definition-RKZ34NQL-q950AAWL.js → mindmap-definition-RKZ34NQL-Dmt7pYwF.js} +1 -1
- package/packages/web/dist/assets/{pieDiagram-4H26LBE5-BVrv8A-H.js → pieDiagram-4H26LBE5-BHiZaJD-.js} +1 -1
- package/packages/web/dist/assets/{quadrantDiagram-W4KKPZXB-Bp5D2MFq.js → quadrantDiagram-W4KKPZXB-U0A1noRE.js} +1 -1
- package/packages/web/dist/assets/{requirementDiagram-4Y6WPE33-yZfQU4Ix.js → requirementDiagram-4Y6WPE33-CZLsr6yi.js} +1 -1
- package/packages/web/dist/assets/{sankeyDiagram-5OEKKPKP-5R8_OZir.js → sankeyDiagram-5OEKKPKP-BTlLGILU.js} +1 -1
- package/packages/web/dist/assets/{sequenceDiagram-3UESZ5HK-C3A9rinG.js → sequenceDiagram-3UESZ5HK-DpSS_Gb1.js} +1 -1
- package/packages/web/dist/assets/{stateDiagram-AJRCARHV-Cshrt_XB.js → stateDiagram-AJRCARHV-D9srE-tV.js} +1 -1
- package/packages/web/dist/assets/stateDiagram-v2-BHNVJYJU-AxWqsDh-.js +2 -0
- package/packages/web/dist/assets/{timeline-definition-PNZ67QCA-kNkiddCX.js → timeline-definition-PNZ67QCA-BYQ5CKab.js} +1 -1
- package/packages/web/dist/assets/{vennDiagram-CIIHVFJN-BiNJm17g.js → vennDiagram-CIIHVFJN-B4W2CtSV.js} +1 -1
- package/packages/web/dist/assets/{wardley-L42UT6IY-CjExbCuX.js → wardley-L42UT6IY--XUV7GvD.js} +1 -1
- package/packages/web/dist/assets/{wardleyDiagram-YWT4CUSO-CMn3z-G2.js → wardleyDiagram-YWT4CUSO-Bl2VIEGY.js} +1 -1
- package/packages/web/dist/assets/{xychartDiagram-2RQKCTM6-B-UhnooW.js → xychartDiagram-2RQKCTM6-Bj2H9Rcf.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-CSPRrFVk.js +0 -2
- package/packages/web/dist/assets/channel-B2NSmlBB.js +0 -2
- package/packages/web/dist/assets/classDiagram-4FO5ZUOK-B3tLFeZC.js +0 -2
- package/packages/web/dist/assets/classDiagram-v2-Q7XG4LA2-B3tLFeZC.js +0 -2
- package/packages/web/dist/assets/stateDiagram-v2-BHNVJYJU-DgiazF78.js +0 -2
|
@@ -12,6 +12,23 @@ export interface RunResult {
|
|
|
12
12
|
*/
|
|
13
13
|
export type Runner = (cmd: string, args: string[]) => RunResult;
|
|
14
14
|
export declare const spawnRunner: Runner;
|
|
15
|
+
/**
|
|
16
|
+
* Whether the user lingers — Linux only (SPEC §7).
|
|
17
|
+
*
|
|
18
|
+
* `loginctl show-user <user> -p Linger` is the question, and all three answers
|
|
19
|
+
* are distinct and useful: on (nothing to say), off (the service dies at
|
|
20
|
+
* logout, and the fix needs `sudo`), and *unknown* — no `loginctl`, no logind,
|
|
21
|
+
* a container. Collapsing unknown into "off" would print a `sudo` instruction
|
|
22
|
+
* to someone whose machine has no `loginctl` to run it with.
|
|
23
|
+
*/
|
|
24
|
+
export interface LingerState {
|
|
25
|
+
/** False where the platform has no such concept (darwin). */
|
|
26
|
+
applies: boolean;
|
|
27
|
+
/** On, off, or undefined when the check could not answer. */
|
|
28
|
+
enabled?: boolean;
|
|
29
|
+
/** The user the check asked about, for the printed command. */
|
|
30
|
+
user?: string;
|
|
31
|
+
}
|
|
15
32
|
export interface ServiceState {
|
|
16
33
|
/** Is a unit file installed for us? */
|
|
17
34
|
registered: boolean;
|
|
@@ -20,6 +37,28 @@ export interface ServiceState {
|
|
|
20
37
|
pid?: string;
|
|
21
38
|
/** The Paddock arguments recorded in the installed unit, interpreter stripped. */
|
|
22
39
|
argv: string[];
|
|
40
|
+
/**
|
|
41
|
+
* The interpreter and script the installed unit actually names.
|
|
42
|
+
*
|
|
43
|
+
* Reported separately from {@link argv} because `status` has to be able to
|
|
44
|
+
* say "the Node it runs is not there any more" (SPEC §7), which is only
|
|
45
|
+
* answerable from the paths in the FILE. Under nvm, fnm or volta, removing a
|
|
46
|
+
* Node version leaves a unit naming an interpreter that no longer exists —
|
|
47
|
+
* and the symptom is Paddock being absent with an empty log, because it never
|
|
48
|
+
* got as far as running. Undefined for a unit we could not parse.
|
|
49
|
+
*/
|
|
50
|
+
nodePath?: string;
|
|
51
|
+
cliPath?: string;
|
|
52
|
+
/**
|
|
53
|
+
* The log file the installed unit names, if it names one.
|
|
54
|
+
*
|
|
55
|
+
* Read back rather than recomputed so `status` and `logs` report the file the
|
|
56
|
+
* service is really appending to. A unit written before the logs moved to
|
|
57
|
+
* `<dataDir>/logs/service.log` names the old pair under `<dataDir>/service/`,
|
|
58
|
+
* and telling its owner to read a file nothing writes to is worse than
|
|
59
|
+
* telling them nothing.
|
|
60
|
+
*/
|
|
61
|
+
logPath?: string;
|
|
23
62
|
/**
|
|
24
63
|
* Does the unit ON DISK come back after a graceful stop (#872)?
|
|
25
64
|
*
|
|
@@ -64,8 +103,13 @@ export interface ServiceBackend {
|
|
|
64
103
|
start(): void;
|
|
65
104
|
stop(): void;
|
|
66
105
|
restart(): void;
|
|
67
|
-
/**
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
106
|
+
/**
|
|
107
|
+
* Does this user linger? (SPEC §7.) Asked of the OS, not assumed.
|
|
108
|
+
*
|
|
109
|
+
* The note used to be printed unconditionally on Linux, which meant the
|
|
110
|
+
* people who had already run `enable-linger` were told to run it at every
|
|
111
|
+
* install and every status — so the one line that mattered to the people who
|
|
112
|
+
* had NOT was indistinguishable from boilerplate.
|
|
113
|
+
*/
|
|
114
|
+
linger(): LingerState;
|
|
71
115
|
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { type CliOptions, type ServiceAction } from "../args.js";
|
|
2
2
|
import { isNpxPath, serviceDir, type ServiceSpec } from "./spec.js";
|
|
3
3
|
import { type Runner } from "./backend.js";
|
|
4
|
-
import { type Readiness } from "./ready.js";
|
|
4
|
+
import { type HealthAnswer, type Readiness } from "./ready.js";
|
|
5
5
|
export interface ServiceContext {
|
|
6
6
|
/** `process.platform`. */
|
|
7
7
|
platform: NodeJS.Platform;
|
|
@@ -26,8 +26,74 @@ export interface ServiceContext {
|
|
|
26
26
|
* message — twenty seconds per case, and green for the wrong reason.
|
|
27
27
|
*/
|
|
28
28
|
checkReady?: (url: string) => Promise<Readiness>;
|
|
29
|
+
/**
|
|
30
|
+
* "What does `/api/health` say?", injected for the same reason `checkReady`
|
|
31
|
+
* is — `status` has to be assertable without a server to ask.
|
|
32
|
+
*/
|
|
33
|
+
probeHealth?: (url: string) => Promise<HealthAnswer | undefined>;
|
|
34
|
+
/**
|
|
35
|
+
* The busy check `stop` and `restart` run first (SPEC §1, §4): null to go
|
|
36
|
+
* ahead, or 75 having printed the refusal.
|
|
37
|
+
*
|
|
38
|
+
* Injected for the third time for the third version of the same reason: the
|
|
39
|
+
* real one reads `<home>/runtime.json` and fetches a URL, so a test of "does
|
|
40
|
+
* stop refuse" would otherwise need a server to be busy at it.
|
|
41
|
+
*/
|
|
42
|
+
guardBusy?: (action: "stop" | "restart", home: string, opts: {
|
|
43
|
+
force?: boolean;
|
|
44
|
+
json?: boolean;
|
|
45
|
+
}) => Promise<number | null>;
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* The version of the Paddock the unit points at.
|
|
49
|
+
*
|
|
50
|
+
* Found by walking up from the `cli.js` in the unit to the nearest
|
|
51
|
+
* `package.json`, which is `packages/server/package.json` in both the repo and
|
|
52
|
+
* the published tarball — the same hop the CLI itself makes to answer
|
|
53
|
+
* `--version`. Undefined when the unit names a path that is no longer there,
|
|
54
|
+
* which is a state `status` reports rather than guesses around.
|
|
55
|
+
*/
|
|
56
|
+
export declare function installedVersionOf(cliPath: string | undefined): string | undefined;
|
|
57
|
+
/**
|
|
58
|
+
* What `service status` knows, before it is either printed or serialised.
|
|
59
|
+
*
|
|
60
|
+
* One shape for both renderings, so `--json` cannot quietly disagree with the
|
|
61
|
+
* prose above it — which is the usual way a `--json` flag rots.
|
|
62
|
+
*/
|
|
63
|
+
export interface StatusReport {
|
|
64
|
+
installed: boolean;
|
|
65
|
+
running: boolean;
|
|
66
|
+
/** Did `/api/health` answer? Only then is the service actually usable. */
|
|
67
|
+
answering: boolean;
|
|
68
|
+
/** The version the unit points at, and the version that answered. */
|
|
69
|
+
installedVersion: string | null;
|
|
70
|
+
runningVersion: string | null;
|
|
71
|
+
port: number | null;
|
|
72
|
+
host: string | null;
|
|
73
|
+
url: string | null;
|
|
74
|
+
pid: number | null;
|
|
75
|
+
/** The Paddock arguments the unit records, interpreter and script stripped. */
|
|
76
|
+
args: string[];
|
|
77
|
+
/** The plist / unit file — present whether or not it exists. */
|
|
78
|
+
unit: string;
|
|
79
|
+
manager: "launchd" | "systemd";
|
|
80
|
+
/** The one file both streams append to. */
|
|
81
|
+
log: string | null;
|
|
82
|
+
dataDir: string | null;
|
|
83
|
+
node: string | null;
|
|
84
|
+
cli: string | null;
|
|
85
|
+
/** Linux only: null where the platform has no such concept. */
|
|
86
|
+
linger: boolean | null;
|
|
87
|
+
/** A stale unit, a missing Node — things that are wrong but not fatal. */
|
|
88
|
+
warnings: string[];
|
|
29
89
|
}
|
|
30
|
-
|
|
90
|
+
/**
|
|
91
|
+
* Dispatch, returning the exit code rather than setting one (SPEC §2).
|
|
92
|
+
*
|
|
93
|
+
* The code is returned up to `paddock.ts`, which assigns `process.exitCode` —
|
|
94
|
+
* never `process.exit()`, which truncates piped output mid-write.
|
|
95
|
+
*/
|
|
96
|
+
export declare function runService(action: ServiceAction, opts: CliOptions, ctx: ServiceContext): Promise<number>;
|
|
31
97
|
/** Exported for the entrypoint's context assembly, and for tests. */
|
|
32
98
|
export { serviceDir, isNpxPath };
|
|
33
99
|
export type { ServiceSpec };
|
|
@@ -26,11 +26,14 @@ import fs from "node:fs";
|
|
|
26
26
|
import os from "node:os";
|
|
27
27
|
import path from "node:path";
|
|
28
28
|
import { CliError } from "../args.js";
|
|
29
|
-
import {
|
|
29
|
+
import { EXIT } from "../exit-codes.js";
|
|
30
|
+
import { buildSpec, defaultDataDir, hostFromArgv, isNpxPath, NPX_REFUSAL, portFromArgv, serviceDir, serviceLogPath, } from "./spec.js";
|
|
30
31
|
import { spawnRunner, } from "./backend.js";
|
|
31
32
|
import { createLaunchdBackend } from "./launchd.js";
|
|
32
33
|
import { createSystemdBackend } from "./systemd.js";
|
|
33
|
-
import {
|
|
34
|
+
import { missingPaths } from "./path-env.js";
|
|
35
|
+
import { DEFAULT_LOG_LINES, followFile, sizeOf, tailFile } from "./logs.js";
|
|
36
|
+
import { waitForReady, fetchHealth, READY_TIMEOUT_LABEL, } from "./ready.js";
|
|
34
37
|
/**
|
|
35
38
|
* Pick a backend, or explain why there isn't one.
|
|
36
39
|
*
|
|
@@ -87,15 +90,13 @@ const AT_LOGIN = " Paddock starts when you LOG IN, not when the machine boots
|
|
|
87
90
|
* signalled rather than crashed. Naming the fix here is the only path from that
|
|
88
91
|
* symptom back to a working service.
|
|
89
92
|
*/
|
|
90
|
-
function
|
|
93
|
+
function staleUnitWarning(backend) {
|
|
91
94
|
const key = backend.platform === "darwin"
|
|
92
95
|
? "KeepAlive is conditional on a non-zero exit"
|
|
93
96
|
: "Restart=on-failure";
|
|
94
|
-
return (`
|
|
95
|
-
"
|
|
96
|
-
"
|
|
97
|
-
"\n" +
|
|
98
|
-
" paddock service install");
|
|
97
|
+
return (`This unit predates a fix (#872): ${key}, so a graceful stop — the kind ` +
|
|
98
|
+
"sleep, logout or a stray `kill` sends — puts Paddock down until you start " +
|
|
99
|
+
"it again by hand. Re-run `paddock service install` to update the unit.");
|
|
99
100
|
}
|
|
100
101
|
/**
|
|
101
102
|
* Say what is about to happen, before the part that looks like a hang (#861).
|
|
@@ -121,6 +122,17 @@ function installProgress(backend) {
|
|
|
121
122
|
" macOS may take a few seconds to approve the background item."
|
|
122
123
|
: `\n Installing ${kind} and starting Paddock…`;
|
|
123
124
|
}
|
|
125
|
+
/** The `enable-linger` line, printed only to the people it applies to (SPEC §7). */
|
|
126
|
+
function lingerNote(linger) {
|
|
127
|
+
if (!linger.applies || linger.enabled === true)
|
|
128
|
+
return undefined;
|
|
129
|
+
const head = linger.enabled === false
|
|
130
|
+
? " Lingering is off, so this service stops when you log out — including"
|
|
131
|
+
: " If you log out (this check could not reach loginctl), a user service";
|
|
132
|
+
return (`${head}\n` +
|
|
133
|
+
" out of an SSH session. To keep Paddock up, once:\n" +
|
|
134
|
+
` sudo loginctl enable-linger ${linger.user ?? "$USER"}`);
|
|
135
|
+
}
|
|
124
136
|
async function install(opts, ctx, backend) {
|
|
125
137
|
if (isNpxPath(ctx.packageRoot))
|
|
126
138
|
throw new CliError(NPX_REFUSAL);
|
|
@@ -137,7 +149,10 @@ async function install(opts, ctx, backend) {
|
|
|
137
149
|
console.log(` Waiting for ${url} to answer…`);
|
|
138
150
|
const readiness = await (ctx.checkReady ?? ((u) => waitForReady({ url: u })))(url);
|
|
139
151
|
const kind = backend.platform === "darwin" ? "a launchd LaunchAgent" : "a systemd --user service";
|
|
140
|
-
|
|
152
|
+
// Asked of the OS now, rather than printed at everyone (SPEC §7): the people
|
|
153
|
+
// who have already enabled lingering were being told to enable it at every
|
|
154
|
+
// install, which is how the line stopped being read by the people it is for.
|
|
155
|
+
const linger = lingerNote(backend.linger());
|
|
141
156
|
console.log([
|
|
142
157
|
"",
|
|
143
158
|
readiness === "ready"
|
|
@@ -149,7 +164,7 @@ async function install(opts, ctx, backend) {
|
|
|
149
164
|
...(readiness === "ready" ? [] : [` URL: ${url}`]),
|
|
150
165
|
` Data: ${dataDir}`,
|
|
151
166
|
` Unit: ${backend.unitPath}`,
|
|
152
|
-
|
|
167
|
+
` Logs: ${spec.logPath} (paddock service logs -f)`,
|
|
153
168
|
"",
|
|
154
169
|
AT_LOGIN,
|
|
155
170
|
...(linger !== undefined ? ["", linger] : []),
|
|
@@ -159,66 +174,228 @@ async function install(opts, ctx, backend) {
|
|
|
159
174
|
" paddock service uninstall stop it and remove the unit",
|
|
160
175
|
"",
|
|
161
176
|
].join("\n"));
|
|
177
|
+
// "It is installed but did not answer" is a failure, and used to exit 0 — so
|
|
178
|
+
// an Ansible task or a CI step could not tell it from a working install.
|
|
179
|
+
return readiness === "ready" ? EXIT.ok : EXIT.failure;
|
|
162
180
|
}
|
|
163
181
|
function uninstall(ctx, backend) {
|
|
164
182
|
const existed = backend.uninstall();
|
|
165
183
|
console.log(existed
|
|
166
184
|
? `\n Paddock's ${backend.platform === "darwin" ? "LaunchAgent" : "user service"} is stopped and removed.\n ${backend.unitPath}\n\n Your data is untouched — it was never in there.\n`
|
|
167
185
|
: `\n No Paddock service was installed (${backend.unitPath}).\n Nothing to do.\n`);
|
|
168
|
-
// Leave the service directory
|
|
169
|
-
//
|
|
186
|
+
// Leave the service directory and the logs: the last thing anyone wants from
|
|
187
|
+
// `uninstall` is for the record of why they are uninstalling to vanish (and
|
|
188
|
+
// SPEC §1 requires it — `uninstall` keeps the data home and the logs).
|
|
170
189
|
void ctx;
|
|
190
|
+
// 0 either way: "there was nothing to remove" is the state the caller asked
|
|
191
|
+
// for. `uninstall` is idempotent on purpose, so a teardown script can run it
|
|
192
|
+
// without first asking whether it has anything to do.
|
|
193
|
+
return EXIT.ok;
|
|
194
|
+
}
|
|
195
|
+
/**
|
|
196
|
+
* The data dir the INSTALLED unit uses, read out of its own argument vector.
|
|
197
|
+
*
|
|
198
|
+
* Today's `--data-dir` flag is deliberately ignored (#824): every reporting
|
|
199
|
+
* surface here describes the unit on disk, and letting a flag through printed
|
|
200
|
+
* the unit's data dir beside a log path under a directory the service never
|
|
201
|
+
* writes to. `--data-dir` is documented as install-only (SERVICE_USAGE).
|
|
202
|
+
*/
|
|
203
|
+
function dataDirOf(state, ctx) {
|
|
204
|
+
const i = state.argv.indexOf("--data-dir");
|
|
205
|
+
return i >= 0 && state.argv[i + 1] !== undefined
|
|
206
|
+
? state.argv[i + 1]
|
|
207
|
+
: defaultDataDir(ctx.homeDir);
|
|
208
|
+
}
|
|
209
|
+
/** The URL the installed unit serves, read back out of the unit. */
|
|
210
|
+
function urlOf(state) {
|
|
211
|
+
return `http://${hostFromArgv(state.argv)}:${portFromArgv(state.argv)}`;
|
|
212
|
+
}
|
|
213
|
+
/**
|
|
214
|
+
* The log file to read: the one the unit names, else where we would write it.
|
|
215
|
+
*
|
|
216
|
+
* A unit written before the logs moved to `<dataDir>/logs/service.log` names
|
|
217
|
+
* the old `<dataDir>/service/paddock.log`, and that is the file its service is
|
|
218
|
+
* still appending to — so that is the file `logs` has to read. The fallback
|
|
219
|
+
* covers a unit we could not parse.
|
|
220
|
+
*/
|
|
221
|
+
function logPathOf(state, dataDir) {
|
|
222
|
+
return state.logPath ?? serviceLogPath(dataDir);
|
|
223
|
+
}
|
|
224
|
+
/**
|
|
225
|
+
* The version of the Paddock the unit points at.
|
|
226
|
+
*
|
|
227
|
+
* Found by walking up from the `cli.js` in the unit to the nearest
|
|
228
|
+
* `package.json`, which is `packages/server/package.json` in both the repo and
|
|
229
|
+
* the published tarball — the same hop the CLI itself makes to answer
|
|
230
|
+
* `--version`. Undefined when the unit names a path that is no longer there,
|
|
231
|
+
* which is a state `status` reports rather than guesses around.
|
|
232
|
+
*/
|
|
233
|
+
export function installedVersionOf(cliPath) {
|
|
234
|
+
if (cliPath === undefined)
|
|
235
|
+
return undefined;
|
|
236
|
+
let dir = path.dirname(cliPath);
|
|
237
|
+
for (;;) {
|
|
238
|
+
const manifest = path.join(dir, "package.json");
|
|
239
|
+
if (fs.existsSync(manifest)) {
|
|
240
|
+
try {
|
|
241
|
+
const raw = JSON.parse(fs.readFileSync(manifest, "utf8"));
|
|
242
|
+
return raw.version;
|
|
243
|
+
}
|
|
244
|
+
catch {
|
|
245
|
+
return undefined;
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
const parent = path.dirname(dir);
|
|
249
|
+
if (parent === dir)
|
|
250
|
+
return undefined;
|
|
251
|
+
dir = parent;
|
|
252
|
+
}
|
|
171
253
|
}
|
|
172
|
-
function
|
|
254
|
+
async function buildStatus(ctx, backend) {
|
|
173
255
|
const state = backend.status();
|
|
256
|
+
const base = {
|
|
257
|
+
installed: state.registered,
|
|
258
|
+
args: state.argv,
|
|
259
|
+
manager: backend.platform === "darwin" ? "launchd" : "systemd",
|
|
260
|
+
unit: backend.unitPath,
|
|
261
|
+
};
|
|
262
|
+
const linger = backend.linger();
|
|
263
|
+
const lingerValue = linger.applies ? (linger.enabled ?? null) : null;
|
|
174
264
|
if (!state.registered) {
|
|
265
|
+
return {
|
|
266
|
+
...base,
|
|
267
|
+
running: false,
|
|
268
|
+
answering: false,
|
|
269
|
+
installedVersion: null,
|
|
270
|
+
runningVersion: null,
|
|
271
|
+
port: null,
|
|
272
|
+
host: null,
|
|
273
|
+
url: null,
|
|
274
|
+
pid: null,
|
|
275
|
+
log: null,
|
|
276
|
+
dataDir: null,
|
|
277
|
+
node: null,
|
|
278
|
+
cli: null,
|
|
279
|
+
linger: lingerValue,
|
|
280
|
+
warnings: [],
|
|
281
|
+
};
|
|
282
|
+
}
|
|
283
|
+
const dataDir = dataDirOf(state, ctx);
|
|
284
|
+
const url = urlOf(state);
|
|
285
|
+
const health = await (ctx.probeHealth ?? ((u) => fetchHealth(u)))(url);
|
|
286
|
+
const installedVersion = installedVersionOf(state.cliPath);
|
|
287
|
+
const warnings = [];
|
|
288
|
+
if (!state.keepsAlive)
|
|
289
|
+
warnings.push(staleUnitWarning(backend));
|
|
290
|
+
for (const gone of missingPaths([state.nodePath, state.cliPath])) {
|
|
291
|
+
warnings.push(`The unit runs ${gone}, which is not there any more. ` +
|
|
292
|
+
"That happens when a Node version is removed (nvm, fnm, volta) or the " +
|
|
293
|
+
"package is reinstalled elsewhere. Re-run `paddock service install`.");
|
|
294
|
+
}
|
|
295
|
+
if (installedVersion !== undefined &&
|
|
296
|
+
health?.version !== undefined &&
|
|
297
|
+
installedVersion !== health.version) {
|
|
298
|
+
warnings.push(`It is running ${health.version}, but ${installedVersion} is installed. ` +
|
|
299
|
+
"The package was upgraded under a service that has not restarted: " +
|
|
300
|
+
"`paddock service restart`.");
|
|
301
|
+
}
|
|
302
|
+
if (linger.applies && linger.enabled === false) {
|
|
303
|
+
warnings.push("Lingering is off, so this service stops when you log out — including out " +
|
|
304
|
+
`of an SSH session. To keep it up: sudo loginctl enable-linger ${linger.user ?? "$USER"}`);
|
|
305
|
+
}
|
|
306
|
+
return {
|
|
307
|
+
...base,
|
|
308
|
+
running: state.running,
|
|
309
|
+
answering: health?.ok === true,
|
|
310
|
+
installedVersion: installedVersion ?? null,
|
|
311
|
+
runningVersion: health?.version ?? null,
|
|
312
|
+
port: Number(portFromArgv(state.argv)),
|
|
313
|
+
host: hostFromArgv(state.argv),
|
|
314
|
+
url,
|
|
315
|
+
pid: state.pid !== undefined ? Number(state.pid) : null,
|
|
316
|
+
log: logPathOf(state, dataDir),
|
|
317
|
+
dataDir,
|
|
318
|
+
node: state.nodePath ?? null,
|
|
319
|
+
cli: state.cliPath ?? null,
|
|
320
|
+
linger: lingerValue,
|
|
321
|
+
warnings,
|
|
322
|
+
};
|
|
323
|
+
}
|
|
324
|
+
/**
|
|
325
|
+
* `status` (SPEC §1, §2).
|
|
326
|
+
*
|
|
327
|
+
* The exit code is the headline and the reason this command is worth scripting:
|
|
328
|
+
*
|
|
329
|
+
* - **4** — no unit. Nothing to start, nothing to ask.
|
|
330
|
+
* - **3** — installed, but not running or not answering. "Not answering"
|
|
331
|
+
* counts, because a supervisor reports a crash-looping job as `running` for
|
|
332
|
+
* most of the window between restarts, and a service nothing can reach is not
|
|
333
|
+
* up whatever the supervisor thinks.
|
|
334
|
+
* - **0** — installed, running and answering.
|
|
335
|
+
*
|
|
336
|
+
* Those are the numbers `systemctl status` and LSB init scripts already use, so
|
|
337
|
+
* an Ansible task or a shell `if` written against either keeps working.
|
|
338
|
+
*/
|
|
339
|
+
async function status(opts, ctx, backend) {
|
|
340
|
+
const report = await buildStatus(ctx, backend);
|
|
341
|
+
if (opts.json === true) {
|
|
342
|
+
// Exactly one JSON document on stdout, and nothing else (SPEC §1). Every
|
|
343
|
+
// other line this command can print goes to stderr under --json or is not
|
|
344
|
+
// printed at all.
|
|
345
|
+
console.log(JSON.stringify(report, null, 2));
|
|
346
|
+
return statusExit(report);
|
|
347
|
+
}
|
|
348
|
+
if (!report.installed) {
|
|
175
349
|
console.log([
|
|
176
350
|
"",
|
|
177
351
|
" Paddock is not installed as a service.",
|
|
178
|
-
` Looked for: ${
|
|
352
|
+
` Looked for: ${report.unit}`,
|
|
179
353
|
"",
|
|
180
354
|
" paddock service install register it and start it now",
|
|
181
355
|
"",
|
|
182
356
|
].join("\n"));
|
|
183
|
-
return;
|
|
357
|
+
return EXIT.notInstalled;
|
|
184
358
|
}
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
const dataFromUnit = state.argv.indexOf("--data-dir");
|
|
189
|
-
const dataDir = dataFromUnit >= 0 && state.argv[dataFromUnit + 1] !== undefined
|
|
190
|
-
? state.argv[dataFromUnit + 1]
|
|
191
|
-
: defaultDataDir(ctx.homeDir);
|
|
192
|
-
// Drop `--data-dir` before building the log-path spec (#824). `resolveDataDir`
|
|
193
|
-
// gives `opts.dataDir` precedence over `ctx.envDataDir`, which is right for
|
|
194
|
-
// `install` (see the comment there) and wrong here: this whole block reports
|
|
195
|
-
// the INSTALLED unit, so letting today's flag through printed the unit's data
|
|
196
|
-
// dir beside a `Logs:` path under a directory the service never writes to. On
|
|
197
|
-
// macOS that log path is real and load-bearing, not just cosmetic.
|
|
198
|
-
//
|
|
199
|
-
// `--data-dir` is documented as install-only (SERVICE_USAGE), so ignoring it
|
|
200
|
-
// here matches both the help text and the stated intent of this block.
|
|
201
|
-
const statusOpts = { ...opts };
|
|
202
|
-
delete statusOpts.dataDir;
|
|
203
|
-
const { spec } = specFor(statusOpts, { ...ctx, envDataDir: dataDir });
|
|
204
|
-
const linger = backend.lingerNote();
|
|
359
|
+
const versions = report.runningVersion !== null && report.runningVersion !== report.installedVersion
|
|
360
|
+
? ` Version: ${report.runningVersion} running, ${report.installedVersion ?? "unknown"} installed`
|
|
361
|
+
: ` Version: ${report.runningVersion ?? report.installedVersion ?? "unknown"}`;
|
|
205
362
|
console.log([
|
|
206
363
|
"",
|
|
207
|
-
|
|
208
|
-
?
|
|
209
|
-
|
|
364
|
+
report.running
|
|
365
|
+
? report.answering
|
|
366
|
+
? ` Paddock is running${report.pid !== null ? ` (pid ${String(report.pid)})` : ""} and answering.`
|
|
367
|
+
: ` Paddock is running${report.pid !== null ? ` (pid ${String(report.pid)})` : ""}, but ${report.url ?? "its URL"} is not answering.`
|
|
368
|
+
: report.answering
|
|
369
|
+
? // Found in QA, and worth its own line: the supervisor does not have
|
|
370
|
+
// the job, yet something IS serving that URL — usually a `paddock
|
|
371
|
+
// serve` in a terminal on the same port. "Not running" on its own
|
|
372
|
+
// reads as "nothing there", and then `service start` fails on a
|
|
373
|
+
// port clash for reasons the status screen had the answer to.
|
|
374
|
+
` The service is not running, but something IS answering at ${report.url ?? "its URL"}\n` +
|
|
375
|
+
" — most likely a `paddock serve` in a terminal. The service cannot take\n" +
|
|
376
|
+
" that port while it is held."
|
|
377
|
+
: " Paddock is registered as a service, but not running right now.",
|
|
210
378
|
"",
|
|
211
|
-
` URL: ${url}`,
|
|
212
|
-
|
|
213
|
-
`
|
|
214
|
-
`
|
|
215
|
-
|
|
379
|
+
` URL: ${report.url ?? "unknown"}`,
|
|
380
|
+
versions,
|
|
381
|
+
` Data: ${report.dataDir ?? "unknown"}`,
|
|
382
|
+
` Unit: ${report.unit}`,
|
|
383
|
+
` Args: paddock ${report.args.join(" ")}`,
|
|
384
|
+
` Logs: ${report.log ?? "unknown"} (paddock service logs -f)`,
|
|
216
385
|
"",
|
|
217
386
|
AT_LOGIN,
|
|
218
|
-
...(
|
|
219
|
-
...(linger !== undefined && !state.running ? ["", linger] : []),
|
|
387
|
+
...report.warnings.flatMap((w) => ["", ` ⚠ ${w}`]),
|
|
220
388
|
"",
|
|
221
389
|
].join("\n"));
|
|
390
|
+
return statusExit(report);
|
|
391
|
+
}
|
|
392
|
+
/** The exit code a report means (SPEC §2). */
|
|
393
|
+
function statusExit(report) {
|
|
394
|
+
if (!report.installed)
|
|
395
|
+
return EXIT.notInstalled;
|
|
396
|
+
if (!report.running || !report.answering)
|
|
397
|
+
return EXIT.notRunning;
|
|
398
|
+
return EXIT.ok;
|
|
222
399
|
}
|
|
223
400
|
/**
|
|
224
401
|
* The URL an installed unit is actually serving, or undefined if none is.
|
|
@@ -255,16 +432,24 @@ function notInstalled(backend) {
|
|
|
255
432
|
* `ThrottleInterval`, and `launchctl print` says `running` for most of that
|
|
256
433
|
* window). The wait is what makes the last line true.
|
|
257
434
|
*/
|
|
258
|
-
async function startOrRestart(action, ctx, backend) {
|
|
435
|
+
async function startOrRestart(action, opts, ctx, backend) {
|
|
259
436
|
const before = backend.status();
|
|
260
437
|
if (!before.registered) {
|
|
261
438
|
console.log(notInstalled(backend));
|
|
262
|
-
return;
|
|
439
|
+
return EXIT.notInstalled;
|
|
263
440
|
}
|
|
264
441
|
const url = installedUrl(before);
|
|
265
442
|
if (action === "start" && before.running) {
|
|
266
443
|
console.log(`\n Paddock is already running${pid(before)}.\n\n URL: ${url}\n`);
|
|
267
|
-
return;
|
|
444
|
+
return EXIT.ok;
|
|
445
|
+
}
|
|
446
|
+
// A restart stops the instance first, so it asks the same question `stop`
|
|
447
|
+
// asks (SPEC §1): refuse with 75 unless `--force`. `start` does not — there
|
|
448
|
+
// is nothing running for it to cut short, and asking would be theatre.
|
|
449
|
+
if (action === "restart") {
|
|
450
|
+
const refused = await busyCheck(action, opts, ctx, before);
|
|
451
|
+
if (refused !== null)
|
|
452
|
+
return refused;
|
|
268
453
|
}
|
|
269
454
|
console.log(`\n ${action === "start" ? "Starting" : "Restarting"} Paddock…`);
|
|
270
455
|
if (action === "start")
|
|
@@ -279,21 +464,54 @@ async function startOrRestart(action, ctx, backend) {
|
|
|
279
464
|
? ` ✓ Paddock is running${pid(after)} at ${url}`
|
|
280
465
|
: ` Paddock was ${action === "start" ? "started" : "restarted"}, but ${url} did not\n` +
|
|
281
466
|
` answer within ${READY_TIMEOUT_LABEL}. It may still be coming up — check:`,
|
|
282
|
-
...(readiness === "ready"
|
|
467
|
+
...(readiness === "ready"
|
|
468
|
+
? []
|
|
469
|
+
: [
|
|
470
|
+
"",
|
|
471
|
+
" paddock service status",
|
|
472
|
+
` paddock service logs (${logPathOf(after, dataDirOf(after, ctx))})`,
|
|
473
|
+
]),
|
|
283
474
|
"",
|
|
284
475
|
].join("\n"));
|
|
476
|
+
// A start that never answered is a failure, not a success with a caveat: the
|
|
477
|
+
// caller — often an Ansible task — has no other way to find out.
|
|
478
|
+
return readiness === "ready" ? EXIT.ok : EXIT.failure;
|
|
479
|
+
}
|
|
480
|
+
/**
|
|
481
|
+
* The busy check, asked of the instance the UNIT describes (SPEC §1, §4).
|
|
482
|
+
*
|
|
483
|
+
* `home` is `dataDirOf(state, ctx)` — the unit's own data dir, not whatever
|
|
484
|
+
* `--data-dir` this invocation carries. Same rule as `status` and `logs`
|
|
485
|
+
* (#824): the instance about to be stopped is the one the unit points at, and
|
|
486
|
+
* reading `runtime.json` from somewhere else would ask a different Paddock
|
|
487
|
+
* whether this one is busy.
|
|
488
|
+
*
|
|
489
|
+
* Returns null to go ahead, or the code to return (75).
|
|
490
|
+
*/
|
|
491
|
+
async function busyCheck(action, opts, ctx, state) {
|
|
492
|
+
const guard = ctx.guardBusy ?? (await import("../busy-command.js")).guardServiceBusy;
|
|
493
|
+
return guard(action, dataDirOf(state, ctx), {
|
|
494
|
+
...(opts.force === true ? { force: true } : {}),
|
|
495
|
+
...(opts.json === true ? { json: true } : {}),
|
|
496
|
+
});
|
|
285
497
|
}
|
|
286
498
|
/** `stop` (#873): put it down, leave it installed. */
|
|
287
|
-
function stop(backend) {
|
|
499
|
+
async function stop(opts, ctx, backend) {
|
|
288
500
|
const state = backend.status();
|
|
289
501
|
if (!state.registered) {
|
|
290
502
|
console.log(notInstalled(backend));
|
|
291
|
-
return;
|
|
503
|
+
return EXIT.notInstalled;
|
|
292
504
|
}
|
|
293
505
|
if (!state.running) {
|
|
294
506
|
console.log(`\n Paddock is already stopped.\n Unit: ${backend.unitPath}\n`);
|
|
295
|
-
return;
|
|
507
|
+
return EXIT.ok;
|
|
296
508
|
}
|
|
509
|
+
// Refuse with 75 if it is in the middle of something, unless --force (SPEC
|
|
510
|
+
// §4). Deliberately AFTER the two cases above: an unit that is not there, or
|
|
511
|
+
// a service already stopped, has no instance to ask and no work to cut short.
|
|
512
|
+
const refused = await busyCheck("stop", opts, ctx, state);
|
|
513
|
+
if (refused !== null)
|
|
514
|
+
return refused;
|
|
297
515
|
backend.stop();
|
|
298
516
|
console.log([
|
|
299
517
|
"",
|
|
@@ -305,27 +523,70 @@ function stop(backend) {
|
|
|
305
523
|
" paddock service start",
|
|
306
524
|
"",
|
|
307
525
|
].join("\n"));
|
|
526
|
+
return EXIT.ok;
|
|
308
527
|
}
|
|
309
528
|
function pid(state) {
|
|
310
529
|
return state.pid !== undefined ? ` (pid ${state.pid})` : "";
|
|
311
530
|
}
|
|
312
|
-
/**
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
531
|
+
/**
|
|
532
|
+
* `logs [-f] [-n <lines>]` (SPEC §1, §7).
|
|
533
|
+
*
|
|
534
|
+
* Reads the file the INSTALLED unit names, which is the only file its service
|
|
535
|
+
* is actually appending to — a unit written before the logs moved still names
|
|
536
|
+
* the old path, and sending its owner to a file nothing writes to would be
|
|
537
|
+
* worse than sending them nowhere.
|
|
538
|
+
*
|
|
539
|
+
* Exit 4 with no unit, like every other command that needs one. A unit whose
|
|
540
|
+
* log file does not exist yet is NOT an error: a service installed thirty
|
|
541
|
+
* seconds ago and still booting has nothing to show, and that is worth saying
|
|
542
|
+
* rather than failing over.
|
|
543
|
+
*/
|
|
544
|
+
async function logs(opts, ctx, backend) {
|
|
545
|
+
const state = backend.status();
|
|
546
|
+
if (!state.registered) {
|
|
547
|
+
console.log(notInstalled(backend));
|
|
548
|
+
return EXIT.notInstalled;
|
|
549
|
+
}
|
|
550
|
+
const file = logPathOf(state, dataDirOf(state, ctx));
|
|
551
|
+
const lines = opts.lines ?? DEFAULT_LOG_LINES;
|
|
552
|
+
if (!fs.existsSync(file)) {
|
|
553
|
+
// stderr: with `-f` this is a caveat ahead of a stream, and prose on stdout
|
|
554
|
+
// would be mixed into the log output itself.
|
|
555
|
+
console.error(`paddock: nothing has been written to ${file} yet.`);
|
|
556
|
+
if (opts.follow !== true)
|
|
557
|
+
return EXIT.ok;
|
|
558
|
+
}
|
|
559
|
+
else {
|
|
560
|
+
const tail = tailFile(file, lines);
|
|
561
|
+
if (tail !== "")
|
|
562
|
+
console.log(tail);
|
|
563
|
+
}
|
|
564
|
+
if (opts.follow === true) {
|
|
565
|
+
console.error(`paddock: following ${file} — Ctrl-C to stop.`);
|
|
566
|
+
await followFile(file, { from: sizeOf(file) });
|
|
567
|
+
}
|
|
568
|
+
return EXIT.ok;
|
|
318
569
|
}
|
|
570
|
+
/**
|
|
571
|
+
* Dispatch, returning the exit code rather than setting one (SPEC §2).
|
|
572
|
+
*
|
|
573
|
+
* The code is returned up to `paddock.ts`, which assigns `process.exitCode` —
|
|
574
|
+
* never `process.exit()`, which truncates piped output mid-write.
|
|
575
|
+
*/
|
|
319
576
|
export async function runService(action, opts, ctx) {
|
|
577
|
+
// `--force` is rejected on every action but `stop` and `restart` — in
|
|
578
|
+
// `parseCommand`, once, for every verb. See the note there.
|
|
320
579
|
const backend = backendFor(ctx);
|
|
321
580
|
if (action === "install")
|
|
322
581
|
return install(opts, ctx, backend);
|
|
323
582
|
if (action === "uninstall")
|
|
324
583
|
return uninstall(ctx, backend);
|
|
325
584
|
if (action === "start" || action === "restart")
|
|
326
|
-
return startOrRestart(action, ctx, backend);
|
|
585
|
+
return startOrRestart(action, opts, ctx, backend);
|
|
327
586
|
if (action === "stop")
|
|
328
|
-
return stop(backend);
|
|
587
|
+
return stop(opts, ctx, backend);
|
|
588
|
+
if (action === "logs")
|
|
589
|
+
return logs(opts, ctx, backend);
|
|
329
590
|
return status(opts, ctx, backend);
|
|
330
591
|
}
|
|
331
592
|
/** Exported for the entrypoint's context assembly, and for tests. */
|