@edspencer/paddock 0.81.0 → 0.82.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/packages/server/dist/cli/args.d.ts +56 -31
- package/packages/server/dist/cli/args.js +210 -24
- package/packages/server/dist/cli/busy-command.js +1 -1
- package/packages/server/dist/cli/busy.js +10 -12
- 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 +60 -30
- package/packages/server/dist/cli/service/index.d.ts +54 -2
- package/packages/server/dist/cli/service/index.js +198 -25
- package/packages/server/dist/cli/service/launchd.d.ts +31 -84
- package/packages/server/dist/cli/service/launchd.js +104 -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 +30 -0
- package/packages/server/dist/cli/service/ready.js +35 -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 +106 -235
- 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 +84 -0
- package/packages/server/dist/cli/update/install-method.js +183 -0
- package/packages/server/dist/cli/update/lock.d.ts +35 -0
- package/packages/server/dist/cli/update/lock.js +74 -0
- package/packages/server/dist/cli/update/package-manager.d.ts +74 -0
- package/packages/server/dist/cli/update/package-manager.js +156 -0
- package/packages/server/dist/cli/update/registry.d.ts +52 -0
- package/packages/server/dist/cli/update/registry.js +102 -0
- package/packages/server/dist/cli/update/semver.d.ts +45 -0
- package/packages/server/dist/cli/update/semver.js +182 -0
- package/packages/server/dist/cli/update/update-check.d.ts +69 -0
- package/packages/server/dist/cli/update/update-check.js +86 -0
- package/packages/server/dist/cli/update/update.d.ts +126 -0
- package/packages/server/dist/cli/update/update.js +408 -0
- package/packages/server/dist/cli/update-command.d.ts +20 -0
- package/packages/server/dist/cli/update-command.js +111 -0
- package/packages/server/package.json +1 -1
- package/packages/web/dist/assets/{DeleteProjectDialog-D8oIJ8HC.js → DeleteProjectDialog-CgHpQctt.js} +1 -1
- package/packages/web/dist/assets/DiscoverPage-CG6cf-0i.js +2 -0
- package/packages/web/dist/assets/{DiscoverView-o0KCr3aB.js → DiscoverView-BKqI0I1E.js} +1 -1
- package/packages/web/dist/assets/{InstanceConfigPage-RSJz7oL3.js → InstanceConfigPage-DL3raCK1.js} +1 -1
- package/packages/web/dist/assets/{MarkdownRenderer-39mafH8b.js → MarkdownRenderer-DcjEuf3s.js} +1 -1
- package/packages/web/dist/assets/{ProjectRedirect-Bq7GVguY.js → ProjectRedirect-DerkA-nE.js} +1 -1
- package/packages/web/dist/assets/{ProjectView-BoBIru1g.js → ProjectView-NwfC3_w7.js} +3 -3
- package/packages/web/dist/assets/{ProjectsGrid-mJ4gqbr6.js → ProjectsGrid-BeORa0UH.js} +1 -1
- package/packages/web/dist/assets/{RootHome-C0kwFyYp.js → RootHome-BYJs8qJ6.js} +1 -1
- package/packages/web/dist/assets/{Surfaces-MkhXlB8z.js → Surfaces-w-kEaAYU.js} +1 -1
- package/packages/web/dist/assets/{arc-DExlwAwO.js → arc-Bv56hasM.js} +1 -1
- package/packages/web/dist/assets/{architectureDiagram-3BPJPVTR-CZly3W79.js → architectureDiagram-3BPJPVTR-BHEknx9i.js} +1 -1
- package/packages/web/dist/assets/{blockDiagram-GPEHLZMM-AgES73i3.js → blockDiagram-GPEHLZMM-DtxTMrPX.js} +1 -1
- package/packages/web/dist/assets/{c4Diagram-AAUBKEIU-N9mD8Pfk.js → c4Diagram-AAUBKEIU-DRpEYOlM.js} +1 -1
- package/packages/web/dist/assets/channel-CdIiAqY2.js +2 -0
- package/packages/web/dist/assets/{chunk-2J33WTMH-Dsx_kfUl.js → chunk-2J33WTMH-S4s7GCg7.js} +1 -1
- package/packages/web/dist/assets/{chunk-4BX2VUAB-BQjqcjPj.js → chunk-4BX2VUAB-BkWVPLf1.js} +1 -1
- package/packages/web/dist/assets/{chunk-55IACEB6-CCxpYQpY.js → chunk-55IACEB6-4-FQp01F.js} +1 -1
- package/packages/web/dist/assets/{chunk-727SXJPM-DpFRdnUa.js → chunk-727SXJPM-BeyDWdIb.js} +1 -1
- package/packages/web/dist/assets/{chunk-AQP2D5EJ-D2BAlzoO.js → chunk-AQP2D5EJ-ZSxKy745.js} +1 -1
- package/packages/web/dist/assets/{chunk-FMBD7UC4-BFaMRzUF.js → chunk-FMBD7UC4-Bq8uV9j-.js} +1 -1
- package/packages/web/dist/assets/{chunk-ND2GUHAM-B9NGJ9xZ.js → chunk-ND2GUHAM-BOFsRyHk.js} +1 -1
- package/packages/web/dist/assets/{chunk-QZHKN3VN-CF1IHyMS.js → chunk-QZHKN3VN-uZ6oNc4m.js} +1 -1
- package/packages/web/dist/assets/classDiagram-4FO5ZUOK-DCCUHba0.js +2 -0
- package/packages/web/dist/assets/classDiagram-v2-Q7XG4LA2-DCCUHba0.js +2 -0
- package/packages/web/dist/assets/{core-XjElsvQ7.js → core-CYuJAM2U.js} +1 -1
- package/packages/web/dist/assets/{cose-bilkent-S5V4N54A-CF972Flx.js → cose-bilkent-S5V4N54A-DIzkVSFb.js} +1 -1
- package/packages/web/dist/assets/{dagre-BM42HDAG-DsneI8YE.js → dagre-BM42HDAG-CHR6jJBv.js} +1 -1
- package/packages/web/dist/assets/{diagram-2AECGRRQ-BHyrVzIk.js → diagram-2AECGRRQ-BAFXCH8Q.js} +1 -1
- package/packages/web/dist/assets/{diagram-5GNKFQAL-Dj4q8xEi.js → diagram-5GNKFQAL-BJ5t5gbz.js} +1 -1
- package/packages/web/dist/assets/{diagram-KO2AKTUF-BHe3Puv_.js → diagram-KO2AKTUF-D9Xt0IVg.js} +1 -1
- package/packages/web/dist/assets/{diagram-LMA3HP47-wzt6G2FE.js → diagram-LMA3HP47-Bq38u6sy.js} +1 -1
- package/packages/web/dist/assets/{diagram-OG6HWLK6-CZMbMRJq.js → diagram-OG6HWLK6-CMQVqkpi.js} +1 -1
- package/packages/web/dist/assets/{documentTitle-BwTIA8rI.js → documentTitle-M1lrfXRU.js} +1 -1
- package/packages/web/dist/assets/{erDiagram-TEJ5UH35-Dtu14BL1.js → erDiagram-TEJ5UH35-C63M1g9b.js} +1 -1
- package/packages/web/dist/assets/{flowDiagram-I6XJVG4X-T34ql9j0.js → flowDiagram-I6XJVG4X-D3j8ACN7.js} +1 -1
- package/packages/web/dist/assets/{ganttDiagram-6RSMTGT7-DQLOeCXK.js → ganttDiagram-6RSMTGT7-B1Fd1A2Y.js} +1 -1
- package/packages/web/dist/assets/{gitGraphDiagram-PVQCEYII-Bk11zea6.js → gitGraphDiagram-PVQCEYII-CEhZhDdR.js} +1 -1
- package/packages/web/dist/assets/{index-CvaWErFv.js → index-1E9yHciS.js} +2 -2
- package/packages/web/dist/assets/{infoDiagram-5YYISTIA-CsE83lu0.js → infoDiagram-5YYISTIA-Ctquaew6.js} +1 -1
- package/packages/web/dist/assets/{ishikawaDiagram-YF4QCWOH-DNVgA93n.js → ishikawaDiagram-YF4QCWOH-DV4GvA1R.js} +1 -1
- package/packages/web/dist/assets/{journeyDiagram-JHISSGLW-CLpSNjM3.js → journeyDiagram-JHISSGLW-C_Cy9waA.js} +1 -1
- package/packages/web/dist/assets/{kanban-definition-UN3LZRKU-BRzuXcKr.js → kanban-definition-UN3LZRKU-CGr9ctEf.js} +1 -1
- package/packages/web/dist/assets/{linear-BxOawONt.js → linear-D3U8NOkt.js} +1 -1
- package/packages/web/dist/assets/{mermaid.core-Dvs1QCcz.js → mermaid.core-BiArdWKV.js} +4 -4
- package/packages/web/dist/assets/{mindmap-definition-RKZ34NQL-Dmt7pYwF.js → mindmap-definition-RKZ34NQL-CZIpzj8Z.js} +1 -1
- package/packages/web/dist/assets/{pieDiagram-4H26LBE5-BHiZaJD-.js → pieDiagram-4H26LBE5-1MaeS3rt.js} +1 -1
- package/packages/web/dist/assets/{quadrantDiagram-W4KKPZXB-U0A1noRE.js → quadrantDiagram-W4KKPZXB-B5pgQ54e.js} +1 -1
- package/packages/web/dist/assets/{requirementDiagram-4Y6WPE33-CZLsr6yi.js → requirementDiagram-4Y6WPE33-BNnxrpGr.js} +1 -1
- package/packages/web/dist/assets/{sankeyDiagram-5OEKKPKP-BTlLGILU.js → sankeyDiagram-5OEKKPKP-DsxVH-hM.js} +1 -1
- package/packages/web/dist/assets/{sequenceDiagram-3UESZ5HK-DpSS_Gb1.js → sequenceDiagram-3UESZ5HK-Ke9LcFZI.js} +1 -1
- package/packages/web/dist/assets/{stateDiagram-AJRCARHV-D9srE-tV.js → stateDiagram-AJRCARHV-UJzmflND.js} +1 -1
- package/packages/web/dist/assets/stateDiagram-v2-BHNVJYJU-DgUc3HkP.js +2 -0
- package/packages/web/dist/assets/{timeline-definition-PNZ67QCA-BYQ5CKab.js → timeline-definition-PNZ67QCA-BRjNALVI.js} +1 -1
- package/packages/web/dist/assets/{vennDiagram-CIIHVFJN-B4W2CtSV.js → vennDiagram-CIIHVFJN-ClMAspxz.js} +1 -1
- package/packages/web/dist/assets/{wardley-L42UT6IY--XUV7GvD.js → wardley-L42UT6IY-UPv4JEmz.js} +1 -1
- package/packages/web/dist/assets/{wardleyDiagram-YWT4CUSO-Bl2VIEGY.js → wardleyDiagram-YWT4CUSO-BgYJbdEx.js} +1 -1
- package/packages/web/dist/assets/{xychartDiagram-2RQKCTM6-Bj2H9Rcf.js → xychartDiagram-2RQKCTM6-Bfk5HjGx.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-CQlzxsn4.js +0 -2
- package/packages/web/dist/assets/channel-ExsAG7qT.js +0 -2
- package/packages/web/dist/assets/classDiagram-4FO5ZUOK-si7g9okq.js +0 -2
- package/packages/web/dist/assets/classDiagram-v2-Q7XG4LA2-si7g9okq.js +0 -2
- package/packages/web/dist/assets/stateDiagram-v2-BHNVJYJU-AxWqsDh-.js +0 -2
|
@@ -27,12 +27,15 @@ import os from "node:os";
|
|
|
27
27
|
import path from "node:path";
|
|
28
28
|
import { CliError } from "../args.js";
|
|
29
29
|
import { EXIT } from "../exit-codes.js";
|
|
30
|
-
import { buildSpec, defaultDataDir, hostFromArgv, isNpxPath, NPX_REFUSAL, portFromArgv, serviceDir, serviceLogPath, } from "./spec.js";
|
|
31
|
-
import { spawnRunner, } from "./
|
|
32
|
-
import {
|
|
33
|
-
import {
|
|
30
|
+
import { buildSpec, defaultDataDir, hostFromArgv, isNpxPath, NPX_REFUSAL, portFromArgv, serviceDir, serviceLogPath, PADDOCK_SERVICE, } from "./spec.js";
|
|
31
|
+
import { spawnRunner, } from "./manager.js";
|
|
32
|
+
import { createLaunchd } from "./launchd.js";
|
|
33
|
+
import { createSystemd } from "./systemd.js";
|
|
34
34
|
import { missingPaths } from "./path-env.js";
|
|
35
35
|
import { DEFAULT_LOG_LINES, followFile, sizeOf, tailFile } from "./logs.js";
|
|
36
|
+
import { readLiveRuntimeFile } from "../../runtime-file.js";
|
|
37
|
+
import { PADDOCK_VERSION } from "../../version.js";
|
|
38
|
+
import { updateSummary } from "../update/update-check.js";
|
|
36
39
|
import { waitForReady, fetchHealth, READY_TIMEOUT_LABEL, } from "./ready.js";
|
|
37
40
|
/**
|
|
38
41
|
* Pick a backend, or explain why there isn't one.
|
|
@@ -44,9 +47,14 @@ import { waitForReady, fetchHealth, READY_TIMEOUT_LABEL, } from "./ready.js";
|
|
|
44
47
|
function backendFor(ctx) {
|
|
45
48
|
const run = ctx.run ?? spawnRunner;
|
|
46
49
|
if (ctx.platform === "darwin")
|
|
47
|
-
return
|
|
48
|
-
if (ctx.platform === "linux")
|
|
49
|
-
return
|
|
50
|
+
return createLaunchd(PADDOCK_SERVICE, { run, homeDir: ctx.homeDir });
|
|
51
|
+
if (ctx.platform === "linux") {
|
|
52
|
+
return createSystemd(PADDOCK_SERVICE, {
|
|
53
|
+
run,
|
|
54
|
+
homeDir: ctx.homeDir,
|
|
55
|
+
...(ctx.xdgConfigHome !== undefined ? { xdgConfigHome: ctx.xdgConfigHome } : {}),
|
|
56
|
+
});
|
|
57
|
+
}
|
|
50
58
|
throw new CliError(`\`paddock service\` supports macOS (launchd) and Linux (systemd --user), not ${ctx.platform}.\n` +
|
|
51
59
|
"Run `paddock` in a terminal, or keep it up with whatever your platform uses.");
|
|
52
60
|
}
|
|
@@ -63,8 +71,52 @@ function resolveDataDir(opts, ctx) {
|
|
|
63
71
|
}
|
|
64
72
|
return { dataDir: defaultDataDir(ctx.homeDir), explicit: undefined };
|
|
65
73
|
}
|
|
66
|
-
|
|
67
|
-
|
|
74
|
+
/** The value of a flag in a recorded argument vector, or undefined. */
|
|
75
|
+
function argValue(argv, names) {
|
|
76
|
+
for (let i = 0; i < argv.length; i++) {
|
|
77
|
+
if (names.includes(argv[i]) && argv[i + 1] !== undefined)
|
|
78
|
+
return argv[i + 1];
|
|
79
|
+
}
|
|
80
|
+
return undefined;
|
|
81
|
+
}
|
|
82
|
+
/**
|
|
83
|
+
* The settings an EXISTING unit carries, as if they had been typed
|
|
84
|
+
* (`service install --refresh`, SPEC §1 and §5 step 11).
|
|
85
|
+
*
|
|
86
|
+
* This is what makes a reinstall idempotent rather than a reset. A unit
|
|
87
|
+
* installed with `--port 9000 --data-dir /srv/paddock` must come back on 9000
|
|
88
|
+
* pointing at /srv/paddock — and `update` is precisely the caller that has no
|
|
89
|
+
* way to know those values, because it runs the NEW version's CLI in a shell
|
|
90
|
+
* that never typed them.
|
|
91
|
+
*
|
|
92
|
+
* Flags still win, so `service install --refresh --port 7300` moves the port.
|
|
93
|
+
* The ENVIRONMENT deliberately does not: `resolveDataDir` normally lets
|
|
94
|
+
* `PADDOCK_DATA_DIR` stand in for `--data-dir`, which would be wrong here —
|
|
95
|
+
* the refresh is a faithful re-render of a unit, and inheriting the updating
|
|
96
|
+
* shell's data dir would silently repoint somebody's service at a different
|
|
97
|
+
* instance. A unit with no recorded `--data-dir` keeps having none, which is
|
|
98
|
+
* how it stays the same `~/.paddock` a bare `paddock` reaches.
|
|
99
|
+
*/
|
|
100
|
+
function inherit(opts, state) {
|
|
101
|
+
const port = argValue(state.argv, ["--port", "-p"]);
|
|
102
|
+
const host = argValue(state.argv, ["--host"]);
|
|
103
|
+
const dataDir = argValue(state.argv, ["--data-dir", "-d"]);
|
|
104
|
+
return {
|
|
105
|
+
...opts,
|
|
106
|
+
...(opts.port === undefined && port !== undefined ? { port } : {}),
|
|
107
|
+
...(opts.host === undefined && host !== undefined ? { host } : {}),
|
|
108
|
+
...(opts.dataDir === undefined && dataDir !== undefined ? { dataDir } : {}),
|
|
109
|
+
verbose: opts.verbose || state.argv.includes("--verbose"),
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
function specFor(opts, ctx,
|
|
113
|
+
/** Set on a `--refresh`: the data dir comes from the unit, not the environment. */
|
|
114
|
+
fromUnit = false) {
|
|
115
|
+
const { dataDir, explicit } = fromUnit
|
|
116
|
+
? opts.dataDir !== undefined
|
|
117
|
+
? { dataDir: path.resolve(opts.dataDir), explicit: path.resolve(opts.dataDir) }
|
|
118
|
+
: { dataDir: defaultDataDir(ctx.homeDir), explicit: undefined }
|
|
119
|
+
: resolveDataDir(opts, ctx);
|
|
68
120
|
const spec = buildSpec({
|
|
69
121
|
nodePath: ctx.nodePath,
|
|
70
122
|
scriptPath: ctx.scriptPath,
|
|
@@ -136,8 +188,27 @@ function lingerNote(linger) {
|
|
|
136
188
|
async function install(opts, ctx, backend) {
|
|
137
189
|
if (isNpxPath(ctx.packageRoot))
|
|
138
190
|
throw new CliError(NPX_REFUSAL);
|
|
139
|
-
|
|
191
|
+
// `--refresh` (SPEC §5 step 11): re-render an EXISTING unit, keeping what it
|
|
192
|
+
// records. With nothing installed there is nothing to refresh — exit 4, the
|
|
193
|
+
// same code every other command that needs a unit returns, rather than
|
|
194
|
+
// quietly performing a first install from defaults. That distinction is the
|
|
195
|
+
// whole reason the flag exists: `update` runs this through the new version's
|
|
196
|
+
// CLI, and a silent fallback would move a `--port 9000` service to 7233 on
|
|
197
|
+
// the one run nobody is watching.
|
|
198
|
+
const refreshing = opts.refresh === true;
|
|
199
|
+
let existing;
|
|
200
|
+
if (refreshing) {
|
|
201
|
+
existing = backend.status();
|
|
202
|
+
if (!existing.registered) {
|
|
203
|
+
console.log(notInstalled(backend));
|
|
204
|
+
return EXIT.NOT_INSTALLED;
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
const { spec, dataDir } = specFor(existing !== undefined ? inherit(opts, existing) : opts, ctx, refreshing);
|
|
140
208
|
const url = `http://${hostFromArgv(spec.args)}:${portFromArgv(spec.args)}`;
|
|
209
|
+
if (refreshing) {
|
|
210
|
+
console.log(`\n Re-writing ${backend.unitPath}\n Keeping: paddock ${spec.args.join(" ")}`);
|
|
211
|
+
}
|
|
141
212
|
console.log(installProgress(backend));
|
|
142
213
|
backend.install(spec);
|
|
143
214
|
// The unit is written and the supervisor has accepted it. That is NOT the
|
|
@@ -176,7 +247,7 @@ async function install(opts, ctx, backend) {
|
|
|
176
247
|
].join("\n"));
|
|
177
248
|
// "It is installed but did not answer" is a failure, and used to exit 0 — so
|
|
178
249
|
// an Ansible task or a CI step could not tell it from a working install.
|
|
179
|
-
return readiness === "ready" ? EXIT.
|
|
250
|
+
return readiness === "ready" ? EXIT.OK : EXIT.FAILED;
|
|
180
251
|
}
|
|
181
252
|
function uninstall(ctx, backend) {
|
|
182
253
|
const existed = backend.uninstall();
|
|
@@ -190,7 +261,7 @@ function uninstall(ctx, backend) {
|
|
|
190
261
|
// 0 either way: "there was nothing to remove" is the state the caller asked
|
|
191
262
|
// for. `uninstall` is idempotent on purpose, so a teardown script can run it
|
|
192
263
|
// without first asking whether it has anything to do.
|
|
193
|
-
return EXIT.
|
|
264
|
+
return EXIT.OK;
|
|
194
265
|
}
|
|
195
266
|
/**
|
|
196
267
|
* The data dir the INSTALLED unit uses, read out of its own argument vector.
|
|
@@ -251,6 +322,11 @@ export function installedVersionOf(cliPath) {
|
|
|
251
322
|
dir = parent;
|
|
252
323
|
}
|
|
253
324
|
}
|
|
325
|
+
/** `{ update }` when there is a recent check to repeat, and `{}` otherwise. */
|
|
326
|
+
function withUpdate(home) {
|
|
327
|
+
const summary = updateSummary(home, { currentVersion: PADDOCK_VERSION });
|
|
328
|
+
return summary !== undefined ? { update: summary } : {};
|
|
329
|
+
}
|
|
254
330
|
async function buildStatus(ctx, backend) {
|
|
255
331
|
const state = backend.status();
|
|
256
332
|
const base = {
|
|
@@ -277,6 +353,7 @@ async function buildStatus(ctx, backend) {
|
|
|
277
353
|
node: null,
|
|
278
354
|
cli: null,
|
|
279
355
|
linger: lingerValue,
|
|
356
|
+
...withUpdate(path.resolve(ctx.envDataDir ?? defaultDataDir(ctx.homeDir))),
|
|
280
357
|
warnings: [],
|
|
281
358
|
};
|
|
282
359
|
}
|
|
@@ -318,6 +395,7 @@ async function buildStatus(ctx, backend) {
|
|
|
318
395
|
node: state.nodePath ?? null,
|
|
319
396
|
cli: state.cliPath ?? null,
|
|
320
397
|
linger: lingerValue,
|
|
398
|
+
...withUpdate(dataDir),
|
|
321
399
|
warnings,
|
|
322
400
|
};
|
|
323
401
|
}
|
|
@@ -354,7 +432,13 @@ async function status(opts, ctx, backend) {
|
|
|
354
432
|
" paddock service install register it and start it now",
|
|
355
433
|
"",
|
|
356
434
|
].join("\n"));
|
|
357
|
-
|
|
435
|
+
// The notice belongs here too. Found in QA: it was printed only on the
|
|
436
|
+
// installed path, so a machine with no service got `update` in `--json`
|
|
437
|
+
// and silence in the prose — the exact `--json`-disagrees-with-the-text rot
|
|
438
|
+
// that one StatusReport is supposed to prevent. A newer version is worth
|
|
439
|
+
// knowing about whether or not a unit exists.
|
|
440
|
+
noteUpdate(report);
|
|
441
|
+
return EXIT.NOT_INSTALLED;
|
|
358
442
|
}
|
|
359
443
|
const versions = report.runningVersion !== null && report.runningVersion !== report.installedVersion
|
|
360
444
|
? ` Version: ${report.runningVersion} running, ${report.installedVersion ?? "unknown"} installed`
|
|
@@ -387,15 +471,40 @@ async function status(opts, ctx, backend) {
|
|
|
387
471
|
...report.warnings.flatMap((w) => ["", ` ⚠ ${w}`]),
|
|
388
472
|
"",
|
|
389
473
|
].join("\n"));
|
|
474
|
+
// The update notice goes to STDERR, and only ever repeats the cache (SPEC
|
|
475
|
+
// §3). stderr because it is prose about something other than what was asked
|
|
476
|
+
// for, and because that keeps every byte of stdout under the `--json`
|
|
477
|
+
// discipline's one rule rather than two.
|
|
478
|
+
noteUpdate(report);
|
|
390
479
|
return statusExit(report);
|
|
391
480
|
}
|
|
481
|
+
/**
|
|
482
|
+
* Repeat the cached update answer on stderr, when there is one worth repeating
|
|
483
|
+
* (SPEC §3).
|
|
484
|
+
*
|
|
485
|
+
* Nothing is fetched here. The `Run:` line is the plain `paddock update`
|
|
486
|
+
* rather than §6's per-install variant, because working that out needs the
|
|
487
|
+
* install method and the target's tag — and `status` has deliberately not
|
|
488
|
+
* asked the registry, so it has neither. Anyone whose install cannot update
|
|
489
|
+
* itself gets the right command from `update --check`, which has.
|
|
490
|
+
*/
|
|
491
|
+
function noteUpdate(report) {
|
|
492
|
+
const update = report.update;
|
|
493
|
+
if (update === undefined || !update.updateAvailable)
|
|
494
|
+
return;
|
|
495
|
+
console.error("");
|
|
496
|
+
const have = report.runningVersion ?? report.installedVersion;
|
|
497
|
+
console.error(` ${update.latest} is available${have !== null && have !== undefined ? ` (you have ${have})` : ""}.`);
|
|
498
|
+
console.error(" Run: paddock update");
|
|
499
|
+
console.error(` Checked: ${update.checkedAt}`);
|
|
500
|
+
}
|
|
392
501
|
/** The exit code a report means (SPEC §2). */
|
|
393
502
|
function statusExit(report) {
|
|
394
503
|
if (!report.installed)
|
|
395
|
-
return EXIT.
|
|
504
|
+
return EXIT.NOT_INSTALLED;
|
|
396
505
|
if (!report.running || !report.answering)
|
|
397
|
-
return EXIT.
|
|
398
|
-
return EXIT.
|
|
506
|
+
return EXIT.NOT_RUNNING;
|
|
507
|
+
return EXIT.OK;
|
|
399
508
|
}
|
|
400
509
|
/**
|
|
401
510
|
* The URL an installed unit is actually serving, or undefined if none is.
|
|
@@ -436,12 +545,12 @@ async function startOrRestart(action, opts, ctx, backend) {
|
|
|
436
545
|
const before = backend.status();
|
|
437
546
|
if (!before.registered) {
|
|
438
547
|
console.log(notInstalled(backend));
|
|
439
|
-
return EXIT.
|
|
548
|
+
return EXIT.NOT_INSTALLED;
|
|
440
549
|
}
|
|
441
550
|
const url = installedUrl(before);
|
|
442
551
|
if (action === "start" && before.running) {
|
|
443
552
|
console.log(`\n Paddock is already running${pid(before)}.\n\n URL: ${url}\n`);
|
|
444
|
-
return EXIT.
|
|
553
|
+
return EXIT.OK;
|
|
445
554
|
}
|
|
446
555
|
// A restart stops the instance first, so it asks the same question `stop`
|
|
447
556
|
// asks (SPEC §1): refuse with 75 unless `--force`. `start` does not — there
|
|
@@ -475,7 +584,7 @@ async function startOrRestart(action, opts, ctx, backend) {
|
|
|
475
584
|
].join("\n"));
|
|
476
585
|
// A start that never answered is a failure, not a success with a caveat: the
|
|
477
586
|
// caller — often an Ansible task — has no other way to find out.
|
|
478
|
-
return readiness === "ready" ? EXIT.
|
|
587
|
+
return readiness === "ready" ? EXIT.OK : EXIT.FAILED;
|
|
479
588
|
}
|
|
480
589
|
/**
|
|
481
590
|
* The busy check, asked of the instance the UNIT describes (SPEC §1, §4).
|
|
@@ -500,11 +609,11 @@ async function stop(opts, ctx, backend) {
|
|
|
500
609
|
const state = backend.status();
|
|
501
610
|
if (!state.registered) {
|
|
502
611
|
console.log(notInstalled(backend));
|
|
503
|
-
return EXIT.
|
|
612
|
+
return EXIT.NOT_INSTALLED;
|
|
504
613
|
}
|
|
505
614
|
if (!state.running) {
|
|
506
615
|
console.log(`\n Paddock is already stopped.\n Unit: ${backend.unitPath}\n`);
|
|
507
|
-
return EXIT.
|
|
616
|
+
return EXIT.OK;
|
|
508
617
|
}
|
|
509
618
|
// Refuse with 75 if it is in the middle of something, unless --force (SPEC
|
|
510
619
|
// §4). Deliberately AFTER the two cases above: an unit that is not there, or
|
|
@@ -523,7 +632,7 @@ async function stop(opts, ctx, backend) {
|
|
|
523
632
|
" paddock service start",
|
|
524
633
|
"",
|
|
525
634
|
].join("\n"));
|
|
526
|
-
return EXIT.
|
|
635
|
+
return EXIT.OK;
|
|
527
636
|
}
|
|
528
637
|
function pid(state) {
|
|
529
638
|
return state.pid !== undefined ? ` (pid ${state.pid})` : "";
|
|
@@ -545,7 +654,7 @@ async function logs(opts, ctx, backend) {
|
|
|
545
654
|
const state = backend.status();
|
|
546
655
|
if (!state.registered) {
|
|
547
656
|
console.log(notInstalled(backend));
|
|
548
|
-
return EXIT.
|
|
657
|
+
return EXIT.NOT_INSTALLED;
|
|
549
658
|
}
|
|
550
659
|
const file = logPathOf(state, dataDirOf(state, ctx));
|
|
551
660
|
const lines = opts.lines ?? DEFAULT_LOG_LINES;
|
|
@@ -554,7 +663,7 @@ async function logs(opts, ctx, backend) {
|
|
|
554
663
|
// would be mixed into the log output itself.
|
|
555
664
|
console.error(`paddock: nothing has been written to ${file} yet.`);
|
|
556
665
|
if (opts.follow !== true)
|
|
557
|
-
return EXIT.
|
|
666
|
+
return EXIT.OK;
|
|
558
667
|
}
|
|
559
668
|
else {
|
|
560
669
|
const tail = tailFile(file, lines);
|
|
@@ -565,7 +674,7 @@ async function logs(opts, ctx, backend) {
|
|
|
565
674
|
console.error(`paddock: following ${file} — Ctrl-C to stop.`);
|
|
566
675
|
await followFile(file, { from: sizeOf(file) });
|
|
567
676
|
}
|
|
568
|
-
return EXIT.
|
|
677
|
+
return EXIT.OK;
|
|
569
678
|
}
|
|
570
679
|
/**
|
|
571
680
|
* Dispatch, returning the exit code rather than setting one (SPEC §2).
|
|
@@ -589,6 +698,70 @@ export async function runService(action, opts, ctx) {
|
|
|
589
698
|
return logs(opts, ctx, backend);
|
|
590
699
|
return status(opts, ctx, backend);
|
|
591
700
|
}
|
|
701
|
+
/**
|
|
702
|
+
* What `paddock update` needs to know and do about the service (SPEC §5).
|
|
703
|
+
*
|
|
704
|
+
* Built here rather than in the update module so that everything which reads a
|
|
705
|
+
* unit file stays in one place: `update` gets the same answer `status` and
|
|
706
|
+
* `logs` get, from the same backend, including the #824 rule that the instance
|
|
707
|
+
* in question is the one the UNIT describes and not whatever `--data-dir` the
|
|
708
|
+
* invocation carries.
|
|
709
|
+
*
|
|
710
|
+
* Returns `home` too, which is the data dir `update.lock` and
|
|
711
|
+
* `update-check.json` go in. It has to be the unit's: the cache's reader is
|
|
712
|
+
* `service status`, which looks in the unit's data dir, so writing it anywhere
|
|
713
|
+
* else would be writing a file nothing reads.
|
|
714
|
+
*/
|
|
715
|
+
export function updateServiceHooks(ctx) {
|
|
716
|
+
const backend = backendFor(ctx);
|
|
717
|
+
const first = backend.status();
|
|
718
|
+
const home = first.registered
|
|
719
|
+
? dataDirOf(first, ctx)
|
|
720
|
+
: path.resolve(ctx.envDataDir ?? defaultDataDir(ctx.homeDir));
|
|
721
|
+
const inspect = () => {
|
|
722
|
+
const state = backend.status();
|
|
723
|
+
if (!state.registered) {
|
|
724
|
+
// No unit. A foreground `paddock serve` is still worth finding, because
|
|
725
|
+
// SPEC §5's "without a service" path has to be able to say "restart
|
|
726
|
+
// Paddock to use the new version" — and `runtime.json` is the only honest
|
|
727
|
+
// way to find it (no guessing at a default port, the busy client's rule).
|
|
728
|
+
const live = readLiveRuntimeFile(home);
|
|
729
|
+
return {
|
|
730
|
+
installed: false,
|
|
731
|
+
running: live !== null,
|
|
732
|
+
...(live !== null ? { url: live.url } : {}),
|
|
733
|
+
};
|
|
734
|
+
}
|
|
735
|
+
return {
|
|
736
|
+
installed: true,
|
|
737
|
+
running: state.running,
|
|
738
|
+
url: urlOf(state),
|
|
739
|
+
log: logPathOf(state, dataDirOf(state, ctx)),
|
|
740
|
+
};
|
|
741
|
+
};
|
|
742
|
+
return {
|
|
743
|
+
installed: first.registered,
|
|
744
|
+
home,
|
|
745
|
+
hooks: {
|
|
746
|
+
inspect,
|
|
747
|
+
stop: () => backend.stop(),
|
|
748
|
+
// The NEW version's CLI writes the unit (SPEC §5 step 11). Run with the
|
|
749
|
+
// interpreter we are running, not the bin shim: the shim's shebang has to
|
|
750
|
+
// find `node` on PATH, and under a version manager there may not be one.
|
|
751
|
+
refresh: (cliPath) => {
|
|
752
|
+
const run = ctx.run ?? spawnRunner;
|
|
753
|
+
return run(process.execPath, [cliPath, "service", "install", "--refresh"]);
|
|
754
|
+
},
|
|
755
|
+
tail: (lines) => {
|
|
756
|
+
const state = backend.status();
|
|
757
|
+
if (!state.registered)
|
|
758
|
+
return "";
|
|
759
|
+
const file = logPathOf(state, dataDirOf(state, ctx));
|
|
760
|
+
return fs.existsSync(file) ? tailFile(file, lines) : "";
|
|
761
|
+
},
|
|
762
|
+
},
|
|
763
|
+
};
|
|
764
|
+
}
|
|
592
765
|
/** Exported for the entrypoint's context assembly, and for tests. */
|
|
593
766
|
export { serviceDir, isNpxPath };
|
|
594
767
|
/** Best-effort home dir; `os.homedir()` throws on a machine with no passwd entry. */
|
|
@@ -1,92 +1,39 @@
|
|
|
1
|
-
import { type ServiceSpec } from
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
export declare function plistPath(homeDir?: string): string;
|
|
1
|
+
import { type Runner, type ServiceIdentity, type ServiceManager, type ServiceSpec } from './manager.js';
|
|
2
|
+
/** `~/Library/LaunchAgents/<label>.plist`. */
|
|
3
|
+
export declare function plistPath(id: Pick<ServiceIdentity, 'label'>, homeDir?: string): string;
|
|
5
4
|
/**
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
* `KeepAlive` is unconditional `<true
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
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.
|
|
24
|
-
*
|
|
25
|
-
* `EnvironmentVariables` carries PATH and nothing else. It carries PATH because
|
|
26
|
-
* launchd hands a process a stub PATH that a version manager's `node`, and the
|
|
27
|
-
* `git` an agent will want, are both missing from. It carries nothing else
|
|
28
|
-
* because a plist is a world-readable file in the user's home, and the one
|
|
29
|
-
* category of value that would be tempting to put here — a credential — is
|
|
30
|
-
* precisely what the agent shape exists to avoid needing.
|
|
31
|
-
*
|
|
32
|
-
* No `PADDOCK_DATA_DIR`: see `buildSpec`. One `~/.paddock` instance, reachable
|
|
33
|
-
* from the service and from a terminal.
|
|
34
|
-
*
|
|
35
|
-
* `StandardOutPath` and `StandardErrorPath` name the SAME file (SPEC §7).
|
|
36
|
-
* launchd opens both in append mode, so interleaving is what a reader wants
|
|
37
|
-
* anyway — a warning and the stack trace that followed it used to land in two
|
|
38
|
-
* separate files, in an order nothing recorded. One file is also what makes
|
|
39
|
-
* `paddock service logs` one implementation across platforms.
|
|
40
|
-
*/
|
|
41
|
-
export declare function renderPlist(spec: ServiceSpec): string;
|
|
42
|
-
/**
|
|
43
|
-
* Pull `ProgramArguments` back out of a plist we wrote.
|
|
44
|
-
*
|
|
45
|
-
* `status` reports the port the service is ACTUALLY installed with, which is
|
|
46
|
-
* only knowable from the installed unit — recomputing it from today's flags
|
|
47
|
-
* would print a confident URL for a server listening somewhere else. Regex
|
|
48
|
-
* rather than a plist parser because the input is a file this module wrote, in
|
|
49
|
-
* a shape this module controls, and adding an XML dependency to read it back
|
|
50
|
-
* would be the larger risk.
|
|
5
|
+
* The plist.
|
|
6
|
+
*
|
|
7
|
+
* - `KeepAlive` is an unconditional `<true/>`. `{ SuccessfulExit: false }` reads as the polite choice, but a server
|
|
8
|
+
* exits 0 on SIGTERM, so every SIGTERM launchd sends an agent (sleep, logout) would leave it down until the next
|
|
9
|
+
* login. `bootout` unloads the job, so `KeepAlive` doesn't fight a deliberate stop.
|
|
10
|
+
* - `StandardOutPath` and `StandardErrorPath` name the same file.
|
|
11
|
+
* - `EnvironmentVariables` has PATH (launchd's is a stub) and the spec's `env`. Never a credential: a plist is a
|
|
12
|
+
* readable file.
|
|
51
13
|
*/
|
|
14
|
+
export declare function renderPlist(id: Pick<ServiceIdentity, 'label'>, spec: ServiceSpec): string;
|
|
15
|
+
/** `ProgramArguments`, read back from a plist this wrote. A regex is enough for a file whose shape we control. */
|
|
52
16
|
export declare function parsePlistArgv(xml: string): string[];
|
|
53
|
-
/**
|
|
54
|
-
* Does an installed plist carry the unconditional `KeepAlive` from #872?
|
|
55
|
-
*
|
|
56
|
-
* Deliberately narrow: it matches `<key>KeepAlive</key>` followed by `<true/>`
|
|
57
|
-
* and nothing else. The shape it must NOT accept is the old
|
|
58
|
-
* `<dict><key>SuccessfulExit</key><false/></dict>`, which is the one that
|
|
59
|
-
* leaves a service dead after a graceful stop — so a `false` here is the signal
|
|
60
|
-
* `status` turns into "re-run install", and defaulting to `true` on anything
|
|
61
|
-
* unrecognised would silence exactly the case this exists to catch.
|
|
62
|
-
*/
|
|
63
|
-
export declare function plistKeepsAlive(xml: string): boolean;
|
|
64
|
-
/** The log file an installed plist appends to, read back from `StandardOutPath`. */
|
|
17
|
+
/** The log the plist names (`StandardOutPath`). */
|
|
65
18
|
export declare function parsePlistLogPath(xml: string): string | undefined;
|
|
66
|
-
/**
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
* That command is the honest answer to "is it running": it reports the service
|
|
70
|
-
* as launchd currently understands it, including `state = running` and the pid.
|
|
71
|
-
* The alternative — checking whether the plist file exists — answers "is it
|
|
72
|
-
* registered", which is a different and much weaker question.
|
|
73
|
-
*/
|
|
19
|
+
/** Whether the plist has an unconditional `KeepAlive` (the old `SuccessfulExit` dict doesn't count). */
|
|
20
|
+
export declare function plistKeepsAlive(xml: string): boolean;
|
|
21
|
+
/** `state = running` and the pid, from `launchctl print gui/<uid>/<label>`. */
|
|
74
22
|
export declare function parseLaunchctlPrint(out: string): {
|
|
75
23
|
running: boolean;
|
|
76
24
|
pid?: string;
|
|
77
25
|
};
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
export declare function
|
|
92
|
-
export declare function createLaunchdBackend(run: Runner, homeDir?: string, sleep?: (ms: number) => void): ServiceBackend;
|
|
26
|
+
export declare const BOOTOUT_POLLS = 40;
|
|
27
|
+
export declare const BOOTOUT_POLL_MS = 250;
|
|
28
|
+
export declare const BOOTSTRAP_ATTEMPTS = 6;
|
|
29
|
+
export declare const BOOTSTRAP_BACKOFF_MS = 500;
|
|
30
|
+
/** 5 is `EIO`; 37 is "operation already in progress". Both mean the last bootout hasn't finished. */
|
|
31
|
+
export declare const BOOTSTRAP_RACE_CODES: ReadonlySet<number>;
|
|
32
|
+
export interface LaunchdOptions {
|
|
33
|
+
run: Runner;
|
|
34
|
+
homeDir?: string;
|
|
35
|
+
uid?: number;
|
|
36
|
+
/** Injected for tests. */
|
|
37
|
+
sleep?: (ms: number) => void;
|
|
38
|
+
}
|
|
39
|
+
export declare function createLaunchd(id: ServiceIdentity, o: LaunchdOptions): ServiceManager;
|