@edspencer/paddock 0.80.0 → 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 +1 -1
- 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 +20 -4
- package/packages/server/dist/cli/args.js +120 -7
- 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/output.d.ts +52 -0
- package/packages/server/dist/cli/output.js +84 -0
- package/packages/server/dist/cli/paddock.js +16 -2
- package/packages/server/dist/cli/service/index.d.ts +12 -0
- package/packages/server/dist/cli/service/index.js +38 -4
- package/packages/server/dist/cli/service/launchd.d.ts +15 -1
- package/packages/server/dist/cli/service/launchd.js +116 -18
- package/packages/server/dist/herdctl.d.ts +19 -0
- package/packages/server/dist/herdctl.js +26 -0
- package/packages/server/dist/routes/meta.js +45 -0
- package/packages/server/dist/runtime-file.d.ts +26 -0
- package/packages/server/dist/runtime-file.js +91 -0
- package/packages/server/dist/start.d.ts +33 -0
- package/packages/server/dist/start.js +115 -8
- 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-DHczLhwQ.js → DeleteProjectDialog-D8oIJ8HC.js} +1 -1
- package/packages/web/dist/assets/DiscoverPage-CQlzxsn4.js +2 -0
- package/packages/web/dist/assets/{DiscoverView-DtTOpTji.js → DiscoverView-o0KCr3aB.js} +1 -1
- package/packages/web/dist/assets/{InstanceConfigPage-BRoBh1C1.js → InstanceConfigPage-RSJz7oL3.js} +1 -1
- package/packages/web/dist/assets/{MarkdownRenderer-DJf8IJuk.js → MarkdownRenderer-39mafH8b.js} +1 -1
- package/packages/web/dist/assets/{ProjectRedirect-EioguUKW.js → ProjectRedirect-Bq7GVguY.js} +1 -1
- package/packages/web/dist/assets/{ProjectView-BiKnGfk6.js → ProjectView-BoBIru1g.js} +3 -3
- package/packages/web/dist/assets/{ProjectsGrid-D6kfaIal.js → ProjectsGrid-mJ4gqbr6.js} +1 -1
- package/packages/web/dist/assets/{RootHome-DpcyuV6e.js → RootHome-C0kwFyYp.js} +1 -1
- package/packages/web/dist/assets/{Surfaces-CHM29PhP.js → Surfaces-MkhXlB8z.js} +1 -1
- package/packages/web/dist/assets/{arc-fiqD05Hu.js → arc-DExlwAwO.js} +1 -1
- package/packages/web/dist/assets/{architectureDiagram-3BPJPVTR-BuuB2Nkf.js → architectureDiagram-3BPJPVTR-CZly3W79.js} +1 -1
- package/packages/web/dist/assets/{blockDiagram-GPEHLZMM-DQMBuzfi.js → blockDiagram-GPEHLZMM-AgES73i3.js} +1 -1
- package/packages/web/dist/assets/{c4Diagram-AAUBKEIU-BKN0UxCH.js → c4Diagram-AAUBKEIU-N9mD8Pfk.js} +1 -1
- package/packages/web/dist/assets/channel-ExsAG7qT.js +2 -0
- package/packages/web/dist/assets/{chunk-2J33WTMH-DW37EupX.js → chunk-2J33WTMH-Dsx_kfUl.js} +1 -1
- package/packages/web/dist/assets/{chunk-4BX2VUAB-Dyv7zV_c.js → chunk-4BX2VUAB-BQjqcjPj.js} +1 -1
- package/packages/web/dist/assets/{chunk-55IACEB6-DzMR0dOk.js → chunk-55IACEB6-CCxpYQpY.js} +1 -1
- package/packages/web/dist/assets/{chunk-727SXJPM-BdZ3Sor8.js → chunk-727SXJPM-DpFRdnUa.js} +1 -1
- package/packages/web/dist/assets/{chunk-AQP2D5EJ-B4i7_og0.js → chunk-AQP2D5EJ-D2BAlzoO.js} +1 -1
- package/packages/web/dist/assets/{chunk-FMBD7UC4-jcTr85it.js → chunk-FMBD7UC4-BFaMRzUF.js} +1 -1
- package/packages/web/dist/assets/{chunk-ND2GUHAM-fgMsadqB.js → chunk-ND2GUHAM-B9NGJ9xZ.js} +1 -1
- package/packages/web/dist/assets/{chunk-QZHKN3VN-bJNHYS1W.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-Bk4KFrmN.js → core-XjElsvQ7.js} +1 -1
- package/packages/web/dist/assets/{cose-bilkent-S5V4N54A-BIqCSlQy.js → cose-bilkent-S5V4N54A-CF972Flx.js} +1 -1
- package/packages/web/dist/assets/{dagre-BM42HDAG-6nyhtVQz.js → dagre-BM42HDAG-DsneI8YE.js} +1 -1
- package/packages/web/dist/assets/{diagram-2AECGRRQ-CjizfipR.js → diagram-2AECGRRQ-BHyrVzIk.js} +1 -1
- package/packages/web/dist/assets/{diagram-5GNKFQAL-DlgB5vWb.js → diagram-5GNKFQAL-Dj4q8xEi.js} +1 -1
- package/packages/web/dist/assets/{diagram-KO2AKTUF-CNC9qanV.js → diagram-KO2AKTUF-BHe3Puv_.js} +1 -1
- package/packages/web/dist/assets/{diagram-LMA3HP47-DSJ-s9lh.js → diagram-LMA3HP47-wzt6G2FE.js} +1 -1
- package/packages/web/dist/assets/{diagram-OG6HWLK6-C9FoHBDg.js → diagram-OG6HWLK6-CZMbMRJq.js} +1 -1
- package/packages/web/dist/assets/{documentTitle-Cj_AY97j.js → documentTitle-BwTIA8rI.js} +1 -1
- package/packages/web/dist/assets/{erDiagram-TEJ5UH35-z6gku4Yt.js → erDiagram-TEJ5UH35-Dtu14BL1.js} +1 -1
- package/packages/web/dist/assets/{flowDiagram-I6XJVG4X-Dl5c_FgU.js → flowDiagram-I6XJVG4X-T34ql9j0.js} +1 -1
- package/packages/web/dist/assets/{ganttDiagram-6RSMTGT7-CIscJ-Dt.js → ganttDiagram-6RSMTGT7-DQLOeCXK.js} +1 -1
- package/packages/web/dist/assets/{gitGraphDiagram-PVQCEYII-CvttOXM_.js → gitGraphDiagram-PVQCEYII-Bk11zea6.js} +1 -1
- package/packages/web/dist/assets/{index-DN82do6Z.js → index-CvaWErFv.js} +2 -2
- package/packages/web/dist/assets/{infoDiagram-5YYISTIA-dzWYdDJj.js → infoDiagram-5YYISTIA-CsE83lu0.js} +1 -1
- package/packages/web/dist/assets/{ishikawaDiagram-YF4QCWOH-8yX9k4bo.js → ishikawaDiagram-YF4QCWOH-DNVgA93n.js} +1 -1
- package/packages/web/dist/assets/{journeyDiagram-JHISSGLW-BmitzJDR.js → journeyDiagram-JHISSGLW-CLpSNjM3.js} +1 -1
- package/packages/web/dist/assets/{kanban-definition-UN3LZRKU-BZmIkfkW.js → kanban-definition-UN3LZRKU-BRzuXcKr.js} +1 -1
- package/packages/web/dist/assets/{linear-BBLXE4o7.js → linear-BxOawONt.js} +1 -1
- package/packages/web/dist/assets/{mermaid.core-mSKff4yu.js → mermaid.core-Dvs1QCcz.js} +4 -4
- package/packages/web/dist/assets/{mindmap-definition-RKZ34NQL-BWAUa4CP.js → mindmap-definition-RKZ34NQL-Dmt7pYwF.js} +1 -1
- package/packages/web/dist/assets/{pieDiagram-4H26LBE5-VmVdiL4C.js → pieDiagram-4H26LBE5-BHiZaJD-.js} +1 -1
- package/packages/web/dist/assets/{quadrantDiagram-W4KKPZXB-Dybfv6NU.js → quadrantDiagram-W4KKPZXB-U0A1noRE.js} +1 -1
- package/packages/web/dist/assets/{requirementDiagram-4Y6WPE33-BPnOvB7w.js → requirementDiagram-4Y6WPE33-CZLsr6yi.js} +1 -1
- package/packages/web/dist/assets/{sankeyDiagram-5OEKKPKP-YXIEmKw7.js → sankeyDiagram-5OEKKPKP-BTlLGILU.js} +1 -1
- package/packages/web/dist/assets/{sequenceDiagram-3UESZ5HK-DintIRUe.js → sequenceDiagram-3UESZ5HK-DpSS_Gb1.js} +1 -1
- package/packages/web/dist/assets/{stateDiagram-AJRCARHV-eMsjahWp.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-as4zRCPn.js → timeline-definition-PNZ67QCA-BYQ5CKab.js} +1 -1
- package/packages/web/dist/assets/{vennDiagram-CIIHVFJN-CPzvUkM0.js → vennDiagram-CIIHVFJN-B4W2CtSV.js} +1 -1
- package/packages/web/dist/assets/{wardley-L42UT6IY-q6moXj5g.js → wardley-L42UT6IY--XUV7GvD.js} +1 -1
- package/packages/web/dist/assets/{wardleyDiagram-YWT4CUSO-BsEEb9Gm.js → wardleyDiagram-YWT4CUSO-Bl2VIEGY.js} +1 -1
- package/packages/web/dist/assets/{xychartDiagram-2RQKCTM6-DSgZlcZW.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-BSGSzTEs.js +0 -2
- package/packages/web/dist/assets/channel-B7yh6Su_.js +0 -2
- package/packages/web/dist/assets/classDiagram-4FO5ZUOK-DIlhMPgi.js +0 -2
- package/packages/web/dist/assets/classDiagram-v2-Q7XG4LA2-DIlhMPgi.js +0 -2
- package/packages/web/dist/assets/stateDiagram-v2-BHNVJYJU-CpAOGzbO.js +0 -2
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { type BusyResult, type Nouns } from "./busy.js";
|
|
2
|
+
/**
|
|
3
|
+
* How each count in `/api/busy` is said in a sentence (`busy.ts` on the server
|
|
4
|
+
* decides the keys).
|
|
5
|
+
*
|
|
6
|
+
* `background` is spelled out rather than left to the default pluraliser,
|
|
7
|
+
* because "3 backgrounds are running" is not English — and because "background
|
|
8
|
+
* task" is the thing a user recognises from the running-work bar in the UI,
|
|
9
|
+
* where they watched it start.
|
|
10
|
+
*/
|
|
11
|
+
export declare const PADDOCK_NOUNS: Nouns;
|
|
12
|
+
/**
|
|
13
|
+
* What to do about a busy answer — and for Paddock, the first thing to say is
|
|
14
|
+
* **where to run the command from**.
|
|
15
|
+
*
|
|
16
|
+
* This is the keeper-chat trap, and it is specific to Paddock and Managers
|
|
17
|
+
* among the six tools: Paddock's agents can drive Paddock. A keeper chat that
|
|
18
|
+
* runs `paddock service restart` or `paddock update` is itself one of the chat
|
|
19
|
+
* turns the answer just counted, so it will always see itself as busy — and
|
|
20
|
+
* `--force` from there does not override a mistaken reading, it kills the chat
|
|
21
|
+
* that typed it, mid-sentence, along with whatever it had not finished. The
|
|
22
|
+
* agent then has no way to report what happened, because the thing that would
|
|
23
|
+
* report it is the thing that died.
|
|
24
|
+
*
|
|
25
|
+
* So when `chat` is non-zero the advice is not "wait or use --force": it is
|
|
26
|
+
* "run this from a terminal". When the busy work is background tasks or
|
|
27
|
+
* scheduled jobs only, nobody is holding the keyboard inside the service and
|
|
28
|
+
* the ordinary advice applies.
|
|
29
|
+
*/
|
|
30
|
+
export declare function paddockBusyHint(result: BusyResult): string | undefined;
|
|
31
|
+
/**
|
|
32
|
+
* `paddock busy [--json]` — exit 0 not busy, 75 busy, 3 nothing answering
|
|
33
|
+
* (SPEC §1, §2).
|
|
34
|
+
*
|
|
35
|
+
* The instance is found through `<data-dir>/runtime.json` and nowhere else, so
|
|
36
|
+
* this reports on the same instance `paddock serve` and `paddock config show`
|
|
37
|
+
* resolve — `resolveDataDir` in paddock.ts is the one rule all three use.
|
|
38
|
+
*/
|
|
39
|
+
export declare function runBusy(home: string, json: boolean): Promise<number>;
|
|
40
|
+
/**
|
|
41
|
+
* The check before `service stop` and `service restart` (SPEC §1): refuse with
|
|
42
|
+
* 75 unless `--force`, which prints the same message prefixed `Overriding:` and
|
|
43
|
+
* carries on. Returns null to go ahead.
|
|
44
|
+
*
|
|
45
|
+
* `home` is the data dir of the INSTALLED unit, not whatever `--data-dir` this
|
|
46
|
+
* invocation carries — the same rule `status` and `logs` follow (#824), because
|
|
47
|
+
* the instance being stopped is the one the unit describes.
|
|
48
|
+
*
|
|
49
|
+
* Two deliberate non-refusals. Nothing answering goes ahead: there is nothing
|
|
50
|
+
* to cut short, which is also the answer for a service that is already stopped.
|
|
51
|
+
* And an instance running a Paddock from before this release has no
|
|
52
|
+
* `runtime.json`, so it reads as not running and the stop proceeds exactly as
|
|
53
|
+
* it did before — an upgrade cannot make a stop start failing.
|
|
54
|
+
*/
|
|
55
|
+
export declare function guardServiceBusy(action: "stop" | "restart", home: string, opts: {
|
|
56
|
+
force?: boolean;
|
|
57
|
+
json?: boolean;
|
|
58
|
+
}): Promise<number | null>;
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Paddock's bindings for the copied busy client (`cli/busy.ts`): the nouns its
|
|
3
|
+
* message uses, the keeper-chat trap, and the two entry points — `paddock busy`
|
|
4
|
+
* and the check `service stop` / `service restart` run first (CLI contract
|
|
5
|
+
* SPEC §1, §4).
|
|
6
|
+
*
|
|
7
|
+
* Kept separate from `cli/busy.ts` so that file stays a straight copy of
|
|
8
|
+
* `reference/busy-client.ts`, which is what wave C4 re-syncs. Everything in
|
|
9
|
+
* here is Paddock-specific by definition: the contract says each tool's kinds of
|
|
10
|
+
* work are its own.
|
|
11
|
+
*/
|
|
12
|
+
import { EXIT } from "./exit-codes.js";
|
|
13
|
+
import { createOutput } from "./output.js";
|
|
14
|
+
import { guardBusy, runBusyCommand, } from "./busy.js";
|
|
15
|
+
import { PADDOCK_NAME } from "../version.js";
|
|
16
|
+
/**
|
|
17
|
+
* How each count in `/api/busy` is said in a sentence (`busy.ts` on the server
|
|
18
|
+
* decides the keys).
|
|
19
|
+
*
|
|
20
|
+
* `background` is spelled out rather than left to the default pluraliser,
|
|
21
|
+
* because "3 backgrounds are running" is not English — and because "background
|
|
22
|
+
* task" is the thing a user recognises from the running-work bar in the UI,
|
|
23
|
+
* where they watched it start.
|
|
24
|
+
*/
|
|
25
|
+
export const PADDOCK_NOUNS = {
|
|
26
|
+
chat: ["chat turn", "chat turns"],
|
|
27
|
+
background: ["background task", "background tasks"],
|
|
28
|
+
job: ["scheduled job", "scheduled jobs"],
|
|
29
|
+
};
|
|
30
|
+
/**
|
|
31
|
+
* What to do about a busy answer — and for Paddock, the first thing to say is
|
|
32
|
+
* **where to run the command from**.
|
|
33
|
+
*
|
|
34
|
+
* This is the keeper-chat trap, and it is specific to Paddock and Managers
|
|
35
|
+
* among the six tools: Paddock's agents can drive Paddock. A keeper chat that
|
|
36
|
+
* runs `paddock service restart` or `paddock update` is itself one of the chat
|
|
37
|
+
* turns the answer just counted, so it will always see itself as busy — and
|
|
38
|
+
* `--force` from there does not override a mistaken reading, it kills the chat
|
|
39
|
+
* that typed it, mid-sentence, along with whatever it had not finished. The
|
|
40
|
+
* agent then has no way to report what happened, because the thing that would
|
|
41
|
+
* report it is the thing that died.
|
|
42
|
+
*
|
|
43
|
+
* So when `chat` is non-zero the advice is not "wait or use --force": it is
|
|
44
|
+
* "run this from a terminal". When the busy work is background tasks or
|
|
45
|
+
* scheduled jobs only, nobody is holding the keyboard inside the service and
|
|
46
|
+
* the ordinary advice applies.
|
|
47
|
+
*/
|
|
48
|
+
export function paddockBusyHint(result) {
|
|
49
|
+
if (result.state !== "busy")
|
|
50
|
+
return undefined;
|
|
51
|
+
const chats = result.answer.counts?.chat ?? 0;
|
|
52
|
+
if (chats > 0) {
|
|
53
|
+
// The lead sentence is short because `guardBusy` prints it on the same line
|
|
54
|
+
// as the message, after `paddock: `. The reasoning goes underneath, indented
|
|
55
|
+
// like every other continuation this CLI prints.
|
|
56
|
+
return ("Run it from a terminal, not from inside a Paddock chat.\n" +
|
|
57
|
+
" One of the turns counted above may be the chat asking, and --force from there\n" +
|
|
58
|
+
" stops the service running that chat — which then never gets to say so.\n" +
|
|
59
|
+
" Otherwise: wait for the turns to finish.");
|
|
60
|
+
}
|
|
61
|
+
return "Try again once it has finished, or use --force.";
|
|
62
|
+
}
|
|
63
|
+
/** The options both entry points share: name, nouns and hint are fixed for Paddock. */
|
|
64
|
+
function paddockBusyOptions(home, out) {
|
|
65
|
+
return { home, out, name: PADDOCK_NAME, nouns: PADDOCK_NOUNS, hint: paddockBusyHint };
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* `paddock busy [--json]` — exit 0 not busy, 75 busy, 3 nothing answering
|
|
69
|
+
* (SPEC §1, §2).
|
|
70
|
+
*
|
|
71
|
+
* The instance is found through `<data-dir>/runtime.json` and nowhere else, so
|
|
72
|
+
* this reports on the same instance `paddock serve` and `paddock config show`
|
|
73
|
+
* resolve — `resolveDataDir` in paddock.ts is the one rule all three use.
|
|
74
|
+
*/
|
|
75
|
+
export async function runBusy(home, json) {
|
|
76
|
+
const out = createOutput({ tool: "paddock", json });
|
|
77
|
+
return runBusyCommand(paddockBusyOptions(home, out));
|
|
78
|
+
}
|
|
79
|
+
/**
|
|
80
|
+
* The check before `service stop` and `service restart` (SPEC §1): refuse with
|
|
81
|
+
* 75 unless `--force`, which prints the same message prefixed `Overriding:` and
|
|
82
|
+
* carries on. Returns null to go ahead.
|
|
83
|
+
*
|
|
84
|
+
* `home` is the data dir of the INSTALLED unit, not whatever `--data-dir` this
|
|
85
|
+
* invocation carries — the same rule `status` and `logs` follow (#824), because
|
|
86
|
+
* the instance being stopped is the one the unit describes.
|
|
87
|
+
*
|
|
88
|
+
* Two deliberate non-refusals. Nothing answering goes ahead: there is nothing
|
|
89
|
+
* to cut short, which is also the answer for a service that is already stopped.
|
|
90
|
+
* And an instance running a Paddock from before this release has no
|
|
91
|
+
* `runtime.json`, so it reads as not running and the stop proceeds exactly as
|
|
92
|
+
* it did before — an upgrade cannot make a stop start failing.
|
|
93
|
+
*/
|
|
94
|
+
export async function guardServiceBusy(action, home, opts) {
|
|
95
|
+
// No `result()` call on this path, so `--json` only decides which stream the
|
|
96
|
+
// prose goes to — the one JSON document a `--json` stop may print belongs to
|
|
97
|
+
// the stop, not to the check it ran first.
|
|
98
|
+
const out = createOutput({ tool: "paddock", json: opts.json === true });
|
|
99
|
+
const refusal = await guardBusy({
|
|
100
|
+
...paddockBusyOptions(home, out),
|
|
101
|
+
...(opts.force === true ? { force: true } : {}),
|
|
102
|
+
});
|
|
103
|
+
if (refusal === null)
|
|
104
|
+
return null;
|
|
105
|
+
// Spell out the way past it, after the hint about where to run it from.
|
|
106
|
+
out.warn(`\n paddock service ${action} --force ${action} it anyway\n`);
|
|
107
|
+
return EXIT.busy;
|
|
108
|
+
}
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import type { Output } from "./output.js";
|
|
2
|
+
/** What `/api/busy` answers (SPEC §4). Counts only, until §10's first question is settled. */
|
|
3
|
+
export interface BusyAnswer {
|
|
4
|
+
busy: boolean;
|
|
5
|
+
version?: string;
|
|
6
|
+
counts?: Record<string, number>;
|
|
7
|
+
}
|
|
8
|
+
export type BusyResult = {
|
|
9
|
+
state: "not-running";
|
|
10
|
+
} | {
|
|
11
|
+
state: "idle" | "busy";
|
|
12
|
+
url: string;
|
|
13
|
+
answer: BusyAnswer;
|
|
14
|
+
}
|
|
15
|
+
/** Something answered, but not with a busy answer: an older version without `/api/busy`, or another program. */
|
|
16
|
+
| {
|
|
17
|
+
state: "unknown";
|
|
18
|
+
url: string;
|
|
19
|
+
detail: string;
|
|
20
|
+
};
|
|
21
|
+
export interface FindOptions {
|
|
22
|
+
/** The tool's data home, holding `runtime.json`. */
|
|
23
|
+
home: string;
|
|
24
|
+
/** Ask this URL instead of the one in `runtime.json` (`--url`, or a port the user named). */
|
|
25
|
+
url?: string;
|
|
26
|
+
/** How long to wait for an answer. §4 asks for well under a second; this allows for a slow machine. */
|
|
27
|
+
timeoutMs?: number;
|
|
28
|
+
fetch?: typeof globalThis.fetch;
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Asks the running instance whether it's busy. There's no fallback to a default port: an instance that's running
|
|
32
|
+
* wrote `runtime.json`, and guessing a port would ask whatever else is listening there.
|
|
33
|
+
*/
|
|
34
|
+
export declare function checkBusy(o: FindOptions): Promise<BusyResult>;
|
|
35
|
+
/** A kind's singular and plural, where adding an "s" is wrong: `{ batch: ['batch', 'batches'] }`. */
|
|
36
|
+
export type Nouns = Record<string, [singular: string, plural: string]>;
|
|
37
|
+
/** `2 chats and 1 job are running`, from the non-zero counts; '' if there are none. */
|
|
38
|
+
export declare function describeCounts(counts?: Record<string, number>, nouns?: Nouns): string;
|
|
39
|
+
/** The sentence for a person (SPEC §4): `Paddock is busy: 2 chats are running.` */
|
|
40
|
+
export declare function busyMessage(name: string, result: BusyResult, nouns?: Nouns): string;
|
|
41
|
+
/** 0 idle, 75 busy, 3 nothing answers, 1 an answer that isn't a busy answer. */
|
|
42
|
+
export declare function busyExitCode(result: BusyResult): number;
|
|
43
|
+
/** What `<tool> busy --json` prints: valid against `schemas/busy.schema.json`. */
|
|
44
|
+
export declare function busyJson(result: BusyResult): Record<string, unknown>;
|
|
45
|
+
/**
|
|
46
|
+
* LOCAL CHANGE (see the header): what to suggest when the answer is busy, as a string or as a function of the
|
|
47
|
+
* result. {@link resolveHint} is what both the command and the guard call, so the two can never print different
|
|
48
|
+
* advice about the same answer.
|
|
49
|
+
*/
|
|
50
|
+
export type BusyHint = string | ((result: BusyResult) => string | undefined);
|
|
51
|
+
export interface BusyCommandOptions extends FindOptions {
|
|
52
|
+
/** The product's name, for messages: `Local Review`. */
|
|
53
|
+
name: string;
|
|
54
|
+
out: Output;
|
|
55
|
+
nouns?: Nouns;
|
|
56
|
+
/** LOCAL CHANGE: the advice printed after a busy message — Paddock's keeper-chat trap. */
|
|
57
|
+
hint?: BusyHint;
|
|
58
|
+
}
|
|
59
|
+
/** `<tool> busy [--json]`. Returns the exit code; the caller sets `process.exitCode`. */
|
|
60
|
+
export declare function runBusyCommand(o: BusyCommandOptions): Promise<number>;
|
|
61
|
+
export interface GuardOptions extends BusyCommandOptions {
|
|
62
|
+
force?: boolean;
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* The check before `service stop`, `service restart` and `update` (SPEC §4). Returns null to go ahead, or 75 after
|
|
66
|
+
* printing the refusal. With `--force` it prints the same message, prefixed `Overriding:`, and goes ahead.
|
|
67
|
+
* Nothing answering goes ahead: there's nothing to cut short.
|
|
68
|
+
*/
|
|
69
|
+
export declare function guardBusy(o: GuardOptions): Promise<number | null>;
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
// From edspencer/cli-contract reference/busy-client.ts @ SPEC 0.1; local changes: `.ts` import specifiers → `.js`
|
|
2
|
+
// and `../runtime-file.js` (the writer lives server-side, next to the boot path that writes the file); `EXIT.OK` /
|
|
3
|
+
// `EXIT.BUSY` / `EXIT.NOT_RUNNING` / `EXIT.FAILED` → Paddock's lowercase `EXIT.ok` / `busy` / `notRunning` /
|
|
4
|
+
// `failure` (see `exit-codes.ts`, which wave C4 re-syncs); and `hint` widened from a string to
|
|
5
|
+
// `string | ((result) => string | undefined)`, with {@link runBusyCommand} printing it too — that is what carries
|
|
6
|
+
// Paddock's keeper-chat trap (ROADMAP "Busy, per tool"), where the line to print depends on WHICH counts are
|
|
7
|
+
// non-zero, so a fixed string could not say it.
|
|
8
|
+
//
|
|
9
|
+
// The CLI's side of the busy check (SPEC §4): find the running instance through `<home>/runtime.json`, ask its
|
|
10
|
+
// `/api/busy`, and map the answer to exit 0 (idle), 75 (busy) or 3 (nothing answers), with §4's messages.
|
|
11
|
+
// Used by `<tool> busy`, and before `service stop`, `service restart` and `update`. Node built-ins only.
|
|
12
|
+
import { EXIT } from "./exit-codes.js";
|
|
13
|
+
import { readLiveRuntimeFile } from "../runtime-file.js";
|
|
14
|
+
/**
|
|
15
|
+
* Asks the running instance whether it's busy. There's no fallback to a default port: an instance that's running
|
|
16
|
+
* wrote `runtime.json`, and guessing a port would ask whatever else is listening there.
|
|
17
|
+
*/
|
|
18
|
+
export async function checkBusy(o) {
|
|
19
|
+
const base = o.url ?? readLiveRuntimeFile(o.home)?.url;
|
|
20
|
+
if (!base)
|
|
21
|
+
return { state: "not-running" };
|
|
22
|
+
const url = new URL("/api/busy", base).toString();
|
|
23
|
+
const doFetch = o.fetch ?? globalThis.fetch;
|
|
24
|
+
let res;
|
|
25
|
+
try {
|
|
26
|
+
res = await doFetch(url, {
|
|
27
|
+
signal: AbortSignal.timeout(o.timeoutMs ?? 3000),
|
|
28
|
+
headers: { accept: "application/json" },
|
|
29
|
+
});
|
|
30
|
+
}
|
|
31
|
+
catch {
|
|
32
|
+
// Refused, reset or timed out: nothing is answering, which §4 calls not running.
|
|
33
|
+
return { state: "not-running" };
|
|
34
|
+
}
|
|
35
|
+
if (!res.ok)
|
|
36
|
+
return { state: "unknown", url, detail: `HTTP ${res.status}` };
|
|
37
|
+
let body;
|
|
38
|
+
try {
|
|
39
|
+
body = await res.json();
|
|
40
|
+
}
|
|
41
|
+
catch {
|
|
42
|
+
return { state: "unknown", url, detail: "the answer isn't JSON" };
|
|
43
|
+
}
|
|
44
|
+
if (!isBusyAnswer(body))
|
|
45
|
+
return { state: "unknown", url, detail: "the answer has no boolean `busy`" };
|
|
46
|
+
return { state: body.busy ? "busy" : "idle", url, answer: body };
|
|
47
|
+
}
|
|
48
|
+
function isBusyAnswer(v) {
|
|
49
|
+
if (typeof v !== "object" || v === null || typeof v.busy !== "boolean")
|
|
50
|
+
return false;
|
|
51
|
+
const counts = v.counts;
|
|
52
|
+
if (counts === undefined)
|
|
53
|
+
return true;
|
|
54
|
+
return (typeof counts === "object" &&
|
|
55
|
+
counts !== null &&
|
|
56
|
+
Object.values(counts).every((n) => Number.isInteger(n) && n >= 0));
|
|
57
|
+
}
|
|
58
|
+
/** `2 chats and 1 job are running`, from the non-zero counts; '' if there are none. */
|
|
59
|
+
export function describeCounts(counts = {}, nouns = {}) {
|
|
60
|
+
const parts = Object.entries(counts)
|
|
61
|
+
.filter(([, n]) => n > 0)
|
|
62
|
+
.map(([kind, n]) => {
|
|
63
|
+
const [one, many] = nouns[kind] ?? [kind, `${kind}s`];
|
|
64
|
+
return `${n} ${n === 1 ? one : many}`;
|
|
65
|
+
});
|
|
66
|
+
if (parts.length === 0)
|
|
67
|
+
return "";
|
|
68
|
+
const list = parts.length === 1 ? parts[0] : `${parts.slice(0, -1).join(", ")} and ${parts.at(-1)}`;
|
|
69
|
+
const total = Object.values(counts).reduce((a, n) => a + (n > 0 ? n : 0), 0);
|
|
70
|
+
return `${list} ${total === 1 ? "is" : "are"} running`;
|
|
71
|
+
}
|
|
72
|
+
/** The sentence for a person (SPEC §4): `Paddock is busy: 2 chats are running.` */
|
|
73
|
+
export function busyMessage(name, result, nouns = {}) {
|
|
74
|
+
switch (result.state) {
|
|
75
|
+
case "not-running":
|
|
76
|
+
return `${name} isn't running.`;
|
|
77
|
+
case "idle":
|
|
78
|
+
return `${name} isn't busy.`;
|
|
79
|
+
case "busy": {
|
|
80
|
+
const what = describeCounts(result.answer.counts, nouns);
|
|
81
|
+
return what ? `${name} is busy: ${what}.` : `${name} is busy.`;
|
|
82
|
+
}
|
|
83
|
+
case "unknown":
|
|
84
|
+
return `Something answered at ${result.url}, but not as ${name}'s busy check (${result.detail}).`;
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
/** 0 idle, 75 busy, 3 nothing answers, 1 an answer that isn't a busy answer. */
|
|
88
|
+
export function busyExitCode(result) {
|
|
89
|
+
switch (result.state) {
|
|
90
|
+
case "idle":
|
|
91
|
+
return EXIT.ok;
|
|
92
|
+
case "busy":
|
|
93
|
+
return EXIT.busy;
|
|
94
|
+
case "not-running":
|
|
95
|
+
return EXIT.notRunning;
|
|
96
|
+
case "unknown":
|
|
97
|
+
return EXIT.failure;
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
/** What `<tool> busy --json` prints: valid against `schemas/busy.schema.json`. */
|
|
101
|
+
export function busyJson(result) {
|
|
102
|
+
if (result.state === "not-running")
|
|
103
|
+
return { running: false, busy: false };
|
|
104
|
+
if (result.state === "unknown")
|
|
105
|
+
return { running: true, busy: false, url: base(result.url), error: result.detail };
|
|
106
|
+
return {
|
|
107
|
+
running: true,
|
|
108
|
+
busy: result.answer.busy,
|
|
109
|
+
url: base(result.url),
|
|
110
|
+
...(result.answer.version !== undefined ? { version: result.answer.version } : {}),
|
|
111
|
+
counts: result.answer.counts ?? {},
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
const base = (busyUrl) => busyUrl.replace(/\/api\/busy$/, "");
|
|
115
|
+
function resolveHint(hint, result) {
|
|
116
|
+
return typeof hint === "function" ? hint(result) : hint;
|
|
117
|
+
}
|
|
118
|
+
/** `<tool> busy [--json]`. Returns the exit code; the caller sets `process.exitCode`. */
|
|
119
|
+
export async function runBusyCommand(o) {
|
|
120
|
+
const result = await checkBusy(o);
|
|
121
|
+
const message = busyMessage(o.name, result, o.nouns);
|
|
122
|
+
o.out.result(busyJson(result));
|
|
123
|
+
if (result.state === "unknown")
|
|
124
|
+
o.out.error(message);
|
|
125
|
+
else
|
|
126
|
+
o.out.say(message);
|
|
127
|
+
// LOCAL CHANGE: the hint rides along with a busy answer here too, not only on the guard's refusal — someone who
|
|
128
|
+
// ran `paddock busy` from inside a chat needs the same sentence as someone whose `service stop` was refused.
|
|
129
|
+
const hint = result.state === "busy" ? resolveHint(o.hint, result) : undefined;
|
|
130
|
+
if (hint !== undefined)
|
|
131
|
+
o.out.say(hint);
|
|
132
|
+
return busyExitCode(result);
|
|
133
|
+
}
|
|
134
|
+
/**
|
|
135
|
+
* The check before `service stop`, `service restart` and `update` (SPEC §4). Returns null to go ahead, or 75 after
|
|
136
|
+
* printing the refusal. With `--force` it prints the same message, prefixed `Overriding:`, and goes ahead.
|
|
137
|
+
* Nothing answering goes ahead: there's nothing to cut short.
|
|
138
|
+
*/
|
|
139
|
+
export async function guardBusy(o) {
|
|
140
|
+
const result = await checkBusy(o);
|
|
141
|
+
if (result.state !== "busy")
|
|
142
|
+
return null;
|
|
143
|
+
const message = busyMessage(o.name, result, o.nouns);
|
|
144
|
+
if (o.force) {
|
|
145
|
+
o.out.warn(`Overriding: ${message}`);
|
|
146
|
+
return null;
|
|
147
|
+
}
|
|
148
|
+
const hint = resolveHint(o.hint, result) ?? "Try again once it has finished, or use --force.";
|
|
149
|
+
return o.out.error(`${message} ${hint}`, EXIT.busy);
|
|
150
|
+
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
export interface OutputStream {
|
|
2
|
+
write(chunk: string): unknown;
|
|
3
|
+
isTTY?: boolean;
|
|
4
|
+
}
|
|
5
|
+
export interface OutputOptions {
|
|
6
|
+
/** The bin, for `<tool>: <message>`. */
|
|
7
|
+
tool: string;
|
|
8
|
+
/** `--json` was given: stdout carries exactly one JSON document, and prose goes to stderr. */
|
|
9
|
+
json?: boolean;
|
|
10
|
+
stdout?: OutputStream;
|
|
11
|
+
stderr?: OutputStream;
|
|
12
|
+
env?: NodeJS.ProcessEnv;
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* Whether to colour what goes to this stream (SPEC §3).
|
|
16
|
+
*
|
|
17
|
+
* - `NO_COLOR` set to anything but the empty string: never (https://no-color.org).
|
|
18
|
+
* - `FORCE_COLOR` set: `0` or `false` means never, anything else always.
|
|
19
|
+
* - Otherwise only on a TTY, and not when `TERM=dumb`.
|
|
20
|
+
*/
|
|
21
|
+
export declare function useColor(stream: OutputStream, env?: NodeJS.ProcessEnv): boolean;
|
|
22
|
+
export interface Paint {
|
|
23
|
+
bold(s: string): string;
|
|
24
|
+
dim(s: string): string;
|
|
25
|
+
red(s: string): string;
|
|
26
|
+
green(s: string): string;
|
|
27
|
+
yellow(s: string): string;
|
|
28
|
+
}
|
|
29
|
+
/** Colour functions that do nothing when `on` is false. */
|
|
30
|
+
export declare function paint(on: boolean): Paint;
|
|
31
|
+
export interface Output {
|
|
32
|
+
readonly json: boolean;
|
|
33
|
+
/** Colour for prose on stdout (never with `--json`, which has no prose on stdout). */
|
|
34
|
+
readonly color: Paint;
|
|
35
|
+
/** Colour for stderr. */
|
|
36
|
+
readonly errColor: Paint;
|
|
37
|
+
/** Prose for a person: stdout, or stderr under `--json`. */
|
|
38
|
+
say(line: string): void;
|
|
39
|
+
/** A warning on stderr, always. */
|
|
40
|
+
warn(line: string): void;
|
|
41
|
+
/** `<tool>: <message>` on stderr. Returns `code` (default 1), for `return out.error(…)`. Sets nothing itself. */
|
|
42
|
+
error(message: string, code?: number): number;
|
|
43
|
+
/** The one JSON document `--json` promises. Prints nothing without `--json`. */
|
|
44
|
+
result(doc: unknown): void;
|
|
45
|
+
}
|
|
46
|
+
/** One place for a command's output. Make it once per command, after the flags are parsed. */
|
|
47
|
+
export declare function createOutput(o: OutputOptions): Output;
|
|
48
|
+
/**
|
|
49
|
+
* A closed pipe (`<tool> … | head -1`) exits quietly (SPEC §2): without this, Node throws EPIPE from a later write
|
|
50
|
+
* and prints a stack trace. Call once, at startup.
|
|
51
|
+
*/
|
|
52
|
+
export declare function exitQuietlyOnEpipe(stream?: NodeJS.WriteStream, exit?: (code: number) => void): void;
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
// From edspencer/cli-contract reference/output.ts @ SPEC 0.1; local changes: `./exit-codes.ts` → `./exit-codes.js`
|
|
2
|
+
// (Paddock compiles to ESM-with-extensions, not a TS runtime); `EXIT.FAILED`/`EXIT.USAGE` → Paddock's lowercase
|
|
3
|
+
// `EXIT.failure`/`EXIT.usage`, because Paddock's own `exit-codes.ts` predates the reference module and wave C4
|
|
4
|
+
// re-syncs that file; and the reference's `UsageError` / `reportUsageError` are NOT copied, because `cli/args.ts`
|
|
5
|
+
// already has a `UsageError` (a `CliError` subclass carrying its own code) that the entrypoint already reports.
|
|
6
|
+
//
|
|
7
|
+
// A CLI's output, the way SPEC §2 and §3 say: errors as `<tool>: <message>` on stderr, colour only on a TTY (honouring
|
|
8
|
+
// NO_COLOR and FORCE_COLOR), and with `--json`, one JSON document on stdout and every word of prose on stderr.
|
|
9
|
+
// Node built-ins only.
|
|
10
|
+
import { EXIT } from "./exit-codes.js";
|
|
11
|
+
/**
|
|
12
|
+
* Whether to colour what goes to this stream (SPEC §3).
|
|
13
|
+
*
|
|
14
|
+
* - `NO_COLOR` set to anything but the empty string: never (https://no-color.org).
|
|
15
|
+
* - `FORCE_COLOR` set: `0` or `false` means never, anything else always.
|
|
16
|
+
* - Otherwise only on a TTY, and not when `TERM=dumb`.
|
|
17
|
+
*/
|
|
18
|
+
export function useColor(stream, env = process.env) {
|
|
19
|
+
if (env.NO_COLOR !== undefined && env.NO_COLOR !== "")
|
|
20
|
+
return false;
|
|
21
|
+
if (env.FORCE_COLOR !== undefined)
|
|
22
|
+
return env.FORCE_COLOR !== "0" && env.FORCE_COLOR !== "false";
|
|
23
|
+
if (env.TERM === "dumb")
|
|
24
|
+
return false;
|
|
25
|
+
return stream.isTTY === true;
|
|
26
|
+
}
|
|
27
|
+
const sgr = (open, close) => (s) => `\u001b[${open}m${s}\u001b[${close}m`;
|
|
28
|
+
const plain = (s) => s;
|
|
29
|
+
/** Colour functions that do nothing when `on` is false. */
|
|
30
|
+
export function paint(on) {
|
|
31
|
+
return on
|
|
32
|
+
? {
|
|
33
|
+
bold: sgr(1, 22),
|
|
34
|
+
dim: sgr(2, 22),
|
|
35
|
+
red: sgr(31, 39),
|
|
36
|
+
green: sgr(32, 39),
|
|
37
|
+
yellow: sgr(33, 39),
|
|
38
|
+
}
|
|
39
|
+
: { bold: plain, dim: plain, red: plain, green: plain, yellow: plain };
|
|
40
|
+
}
|
|
41
|
+
/** One place for a command's output. Make it once per command, after the flags are parsed. */
|
|
42
|
+
export function createOutput(o) {
|
|
43
|
+
const stdout = o.stdout ?? process.stdout;
|
|
44
|
+
const stderr = o.stderr ?? process.stderr;
|
|
45
|
+
const env = o.env ?? process.env;
|
|
46
|
+
const json = o.json === true;
|
|
47
|
+
let printed = false;
|
|
48
|
+
const line = (s) => (s.endsWith("\n") ? s : `${s}\n`);
|
|
49
|
+
return {
|
|
50
|
+
json,
|
|
51
|
+
color: paint(!json && useColor(stdout, env)),
|
|
52
|
+
errColor: paint(useColor(stderr, env)),
|
|
53
|
+
say(s) {
|
|
54
|
+
(json ? stderr : stdout).write(line(s));
|
|
55
|
+
},
|
|
56
|
+
warn(s) {
|
|
57
|
+
stderr.write(line(s));
|
|
58
|
+
},
|
|
59
|
+
error(message, code = EXIT.failure) {
|
|
60
|
+
stderr.write(line(`${o.tool}: ${message}`));
|
|
61
|
+
return code;
|
|
62
|
+
},
|
|
63
|
+
result(doc) {
|
|
64
|
+
if (!json)
|
|
65
|
+
return;
|
|
66
|
+
if (printed)
|
|
67
|
+
throw new Error("--json prints one document; result() was called twice");
|
|
68
|
+
printed = true;
|
|
69
|
+
stdout.write(`${JSON.stringify(doc, null, 2)}\n`);
|
|
70
|
+
},
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* A closed pipe (`<tool> … | head -1`) exits quietly (SPEC §2): without this, Node throws EPIPE from a later write
|
|
75
|
+
* and prints a stack trace. Call once, at startup.
|
|
76
|
+
*/
|
|
77
|
+
export function exitQuietlyOnEpipe(stream = process.stdout, exit = (c) => process.exit(c)) {
|
|
78
|
+
stream.on("error", (err) => {
|
|
79
|
+
if (err.code === "EPIPE")
|
|
80
|
+
exit(typeof process.exitCode === "number" ? process.exitCode : 0);
|
|
81
|
+
else
|
|
82
|
+
throw err;
|
|
83
|
+
});
|
|
84
|
+
}
|
|
@@ -23,7 +23,7 @@ import os from "node:os";
|
|
|
23
23
|
import path from "node:path";
|
|
24
24
|
import { spawn } from "node:child_process";
|
|
25
25
|
import { fileURLToPath } from "node:url";
|
|
26
|
-
import { CliError, UsageError, USAGE, USAGE_SYNOPSIS, SERVICE_USAGE, CONFIG_USAGE, parseCommand, nodeVersionProblem, explainListenError, } from "./args.js";
|
|
26
|
+
import { CliError, UsageError, USAGE, USAGE_SYNOPSIS, SERVICE_USAGE, CONFIG_USAGE, BUSY_USAGE, parseCommand, nodeVersionProblem, explainListenError, } from "./args.js";
|
|
27
27
|
import { EXIT } from "./exit-codes.js";
|
|
28
28
|
import { runService, safeHomeDir, realpathOr } from "./service/index.js";
|
|
29
29
|
import { PADDOCK_NAME, PADDOCK_VERSION } from "../version.js";
|
|
@@ -270,7 +270,13 @@ async function main() {
|
|
|
270
270
|
}
|
|
271
271
|
const opts = command.opts;
|
|
272
272
|
if (opts.help) {
|
|
273
|
-
console.log(command.verb === "service"
|
|
273
|
+
console.log(command.verb === "service"
|
|
274
|
+
? SERVICE_USAGE
|
|
275
|
+
: command.verb === "config"
|
|
276
|
+
? CONFIG_USAGE
|
|
277
|
+
: command.verb === "busy"
|
|
278
|
+
? BUSY_USAGE
|
|
279
|
+
: USAGE);
|
|
274
280
|
return EXIT.ok;
|
|
275
281
|
}
|
|
276
282
|
if (opts.version) {
|
|
@@ -285,6 +291,14 @@ async function main() {
|
|
|
285
291
|
return service(command);
|
|
286
292
|
if (command.verb === "config")
|
|
287
293
|
return config(command);
|
|
294
|
+
// `busy` (SPEC §1, §4). Nothing is imported from the server for this: the
|
|
295
|
+
// answer comes off the wire, and `cli/busy.ts` is Node built-ins only — which
|
|
296
|
+
// is what keeps a question meant for a Watchtower hook from loading Fastify,
|
|
297
|
+
// herdctl and the project layer to ask it.
|
|
298
|
+
if (command.verb === "busy") {
|
|
299
|
+
const { runBusy } = await import("./busy-command.js");
|
|
300
|
+
return runBusy(resolveDataDir(opts), opts.json === true);
|
|
301
|
+
}
|
|
288
302
|
addBundledBinsToPath();
|
|
289
303
|
// NOTHING here reads `process.cwd()`, and that is the point (#798). `--here`
|
|
290
304
|
// used to make the current directory the workspace by setting
|
|
@@ -31,6 +31,18 @@ export interface ServiceContext {
|
|
|
31
31
|
* is — `status` has to be assertable without a server to ask.
|
|
32
32
|
*/
|
|
33
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>;
|
|
34
46
|
}
|
|
35
47
|
/**
|
|
36
48
|
* The version of the Paddock the unit points at.
|
|
@@ -432,7 +432,7 @@ function notInstalled(backend) {
|
|
|
432
432
|
* `ThrottleInterval`, and `launchctl print` says `running` for most of that
|
|
433
433
|
* window). The wait is what makes the last line true.
|
|
434
434
|
*/
|
|
435
|
-
async function startOrRestart(action, ctx, backend) {
|
|
435
|
+
async function startOrRestart(action, opts, ctx, backend) {
|
|
436
436
|
const before = backend.status();
|
|
437
437
|
if (!before.registered) {
|
|
438
438
|
console.log(notInstalled(backend));
|
|
@@ -443,6 +443,14 @@ async function startOrRestart(action, ctx, backend) {
|
|
|
443
443
|
console.log(`\n Paddock is already running${pid(before)}.\n\n URL: ${url}\n`);
|
|
444
444
|
return EXIT.ok;
|
|
445
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;
|
|
453
|
+
}
|
|
446
454
|
console.log(`\n ${action === "start" ? "Starting" : "Restarting"} Paddock…`);
|
|
447
455
|
if (action === "start")
|
|
448
456
|
backend.start();
|
|
@@ -469,8 +477,26 @@ async function startOrRestart(action, ctx, backend) {
|
|
|
469
477
|
// caller — often an Ansible task — has no other way to find out.
|
|
470
478
|
return readiness === "ready" ? EXIT.ok : EXIT.failure;
|
|
471
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
|
+
});
|
|
497
|
+
}
|
|
472
498
|
/** `stop` (#873): put it down, leave it installed. */
|
|
473
|
-
function stop(backend) {
|
|
499
|
+
async function stop(opts, ctx, backend) {
|
|
474
500
|
const state = backend.status();
|
|
475
501
|
if (!state.registered) {
|
|
476
502
|
console.log(notInstalled(backend));
|
|
@@ -480,6 +506,12 @@ function stop(backend) {
|
|
|
480
506
|
console.log(`\n Paddock is already stopped.\n Unit: ${backend.unitPath}\n`);
|
|
481
507
|
return EXIT.ok;
|
|
482
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;
|
|
483
515
|
backend.stop();
|
|
484
516
|
console.log([
|
|
485
517
|
"",
|
|
@@ -542,15 +574,17 @@ async function logs(opts, ctx, backend) {
|
|
|
542
574
|
* never `process.exit()`, which truncates piped output mid-write.
|
|
543
575
|
*/
|
|
544
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.
|
|
545
579
|
const backend = backendFor(ctx);
|
|
546
580
|
if (action === "install")
|
|
547
581
|
return install(opts, ctx, backend);
|
|
548
582
|
if (action === "uninstall")
|
|
549
583
|
return uninstall(ctx, backend);
|
|
550
584
|
if (action === "start" || action === "restart")
|
|
551
|
-
return startOrRestart(action, ctx, backend);
|
|
585
|
+
return startOrRestart(action, opts, ctx, backend);
|
|
552
586
|
if (action === "stop")
|
|
553
|
-
return stop(backend);
|
|
587
|
+
return stop(opts, ctx, backend);
|
|
554
588
|
if (action === "logs")
|
|
555
589
|
return logs(opts, ctx, backend);
|
|
556
590
|
return status(opts, ctx, backend);
|