@indigoai-us/hq-cli 5.345.201 → 5.345.203
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/CHANGELOG.md +16 -0
- package/dist/command-catalog.generated.d.ts +24 -0
- package/dist/command-catalog.generated.js +26 -0
- package/dist/commands/capacity.js +10 -2
- package/dist/commands/daemon.d.ts +2 -1
- package/dist/commands/daemon.js +225 -4
- package/dist/commands/integrations-core.d.ts +3 -0
- package/dist/commands/integrations-core.js +11 -0
- package/dist/commands/integrations-manage.js +2 -2
- package/dist/commands/integrations.js +5 -5
- package/dist/commands/sync-core.js +2 -1
- package/dist/lib/bot/executive-settings.d.ts +1 -1
- package/dist/lib/bot/executive-settings.js +16 -6
- package/dist/lib/bot/jobs/meeting-detect.js +2 -2
- package/dist/lib/bot/jobs/meeting-rider.js +2 -2
- package/dist/lib/bot/jobs/signal-to-lane.d.ts +3 -2
- package/dist/lib/bot/jobs/signal-to-lane.js +31 -8
- package/dist/lib/bot/run.js +3 -3
- package/dist/lib/capacity/codex-availability.d.ts +6 -0
- package/dist/lib/capacity/codex-availability.js +33 -0
- package/dist/lib/capacity/ledger.d.ts +0 -11
- package/dist/lib/capacity/ledger.js +30 -3
- package/dist/lib/capacity/route.js +11 -5
- package/dist/lib/daemon/catalog.d.ts +3 -0
- package/dist/lib/daemon/catalog.js +15 -2
- package/dist/lib/daemon/handoff.d.ts +2 -0
- package/dist/lib/daemon/handoff.js +9 -2
- package/dist/lib/daemon/install.d.ts +99 -0
- package/dist/lib/daemon/install.js +316 -7
- package/dist/lib/daemon/migrate.d.ts +3 -1
- package/dist/lib/daemon/migrate.js +14 -2
- package/dist/lib/daemon/plist-refresh.d.ts +34 -0
- package/dist/lib/daemon/plist-refresh.js +62 -0
- package/dist/lib/daemon/run.js +25 -0
- package/dist/lib/daemon/selfcheck.d.ts +71 -0
- package/dist/lib/daemon/selfcheck.js +150 -0
- package/dist/lib/daemon/supervisor.d.ts +31 -0
- package/dist/lib/daemon/supervisor.js +130 -6
- package/dist/lib/daemon/unit-holds.js +2 -1
- package/dist/lib/service-manager/launchd.d.ts +25 -0
- package/dist/lib/service-manager/launchd.js +51 -3
- package/dist/lib/service-manager/migration.d.ts +14 -0
- package/dist/lib/service-manager/migration.js +34 -0
- package/dist/lib/service-manager/systemd.d.ts +13 -0
- package/dist/lib/service-manager/systemd.js +19 -0
- package/dist/lib/service-manager/types.d.ts +8 -0
- package/dist/lib/sync-core/reconcile.d.ts +4 -1
- package/dist/lib/sync-core/reconcile.js +12 -2
- package/dist/lib/sync-core/release.d.ts +23 -0
- package/dist/lib/sync-core/release.js +39 -0
- package/dist/main.js +3 -3
- package/dist/utils/cli-network-lifecycle.d.ts +9 -0
- package/dist/utils/cli-network-lifecycle.js +25 -1
- package/package.json +9 -8
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The daemon's sync-engine self-check (`hq daemon service sync-core-selfcheck`).
|
|
3
|
+
*
|
|
4
|
+
* It reads the supervisor's state and holds and finds engine units
|
|
5
|
+
* (`sync-core:<vault>`) that have been held, or failing without a heartbeat,
|
|
6
|
+
* for more than ATTENTION_AFTER_MS. When there are any, it sends one DM to the
|
|
7
|
+
* person signed in on this machine, at most once a day per machine whichever
|
|
8
|
+
* units are affected, naming every affected vault in that one DM. The 16-hour outage of 2026-11-03 was
|
|
9
|
+
* found by hand; this makes the next one reach the owner.
|
|
10
|
+
*/
|
|
11
|
+
import * as fs from "node:fs";
|
|
12
|
+
import * as path from "node:path";
|
|
13
|
+
import { syncCoreUnitId } from "../sync-core/engine-home.js";
|
|
14
|
+
import { ensurePrivateDir } from "./paths.js";
|
|
15
|
+
/** An engine unit that has been held or failing this long needs a person. */
|
|
16
|
+
export const ATTENTION_AFTER_MS = 30 * 60_000;
|
|
17
|
+
/** This machine sends at most one self-check DM per this long. */
|
|
18
|
+
export const REPORT_INTERVAL_MS = 24 * 60 * 60_000;
|
|
19
|
+
const ENGINE_UNIT_PREFIX = syncCoreUnitId("");
|
|
20
|
+
/** Engine units held, or failing without a heartbeat, for more than ATTENTION_AFTER_MS at `now`. */
|
|
21
|
+
export function engineUnitsNeedingAttention(state, holds, now,
|
|
22
|
+
/** False for a vault that is skipped on this machine or not on the rollout. */
|
|
23
|
+
wanted = () => true) {
|
|
24
|
+
const problems = [];
|
|
25
|
+
const units = state?.units ?? [];
|
|
26
|
+
const seen = new Set();
|
|
27
|
+
for (const unit of units) {
|
|
28
|
+
if (!unit.id.startsWith(ENGINE_UNIT_PREFIX))
|
|
29
|
+
continue;
|
|
30
|
+
if (!wanted(unit.id.slice(ENGINE_UNIT_PREFIX.length)))
|
|
31
|
+
continue;
|
|
32
|
+
seen.add(unit.id);
|
|
33
|
+
const problem = unitProblem(unit, holds[unit.id], state, now);
|
|
34
|
+
if (problem && now - problem.since > ATTENTION_AFTER_MS)
|
|
35
|
+
problems.push(problem);
|
|
36
|
+
}
|
|
37
|
+
// With no daemon state, the persisted holds are all there is to go on. With
|
|
38
|
+
// state, a hold for a unit the daemon does not supervise is left over from a
|
|
39
|
+
// vault that is no longer on the engine: it stops nothing, so it is not reported.
|
|
40
|
+
if (state)
|
|
41
|
+
return problems.sort((a, b) => a.unitId.localeCompare(b.unitId));
|
|
42
|
+
for (const [unitId, hold] of Object.entries(holds)) {
|
|
43
|
+
if (seen.has(unitId) || !unitId.startsWith(ENGINE_UNIT_PREFIX) || hold.retryStopped === true)
|
|
44
|
+
continue;
|
|
45
|
+
if (!wanted(unitId.slice(ENGINE_UNIT_PREFIX.length)))
|
|
46
|
+
continue;
|
|
47
|
+
if (now - hold.since > ATTENTION_AFTER_MS) {
|
|
48
|
+
problems.push({ unitId, since: hold.since, what: `held since ${iso(hold.since)}: ${hold.detail}` });
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
return problems.sort((a, b) => a.unitId.localeCompare(b.unitId));
|
|
52
|
+
}
|
|
53
|
+
function unitProblem(unit, hold, state, now) {
|
|
54
|
+
// Stopped on request (`hq daemon stop`, or a stopped recovery probe) is not failing.
|
|
55
|
+
if (unit.hold?.retryStopped === true || hold?.retryStopped === true)
|
|
56
|
+
return null;
|
|
57
|
+
if (unit.status === "held") {
|
|
58
|
+
const since = unit.hold?.since ?? hold?.since ?? now;
|
|
59
|
+
const detail = unit.hold?.detail ?? hold?.detail ?? unit.reason ?? "held";
|
|
60
|
+
return { unitId: unit.id, since, what: `held since ${iso(since)}: ${detail}` };
|
|
61
|
+
}
|
|
62
|
+
// Paused on purpose (sync paused, waiting for the legacy sync to hand over) is not failing.
|
|
63
|
+
if (unit.status !== "backoff" && unit.status !== "exited" && unit.status !== "running")
|
|
64
|
+
return null;
|
|
65
|
+
const last = unit.lastHeartbeatAt ? Date.parse(unit.lastHeartbeatAt) : Number.NaN;
|
|
66
|
+
// No heartbeat yet: count from when the daemon started supervising it.
|
|
67
|
+
const since = Number.isFinite(last) ? last : state.startedAt;
|
|
68
|
+
if (unit.status === "running" && unit.restarts === 0)
|
|
69
|
+
return null;
|
|
70
|
+
if (unit.status === "running" && Number.isFinite(last) && now - last <= ATTENTION_AFTER_MS)
|
|
71
|
+
return null;
|
|
72
|
+
const exit = unit.lastExit ? ` (last exit code ${unit.lastExit.code ?? "none"}${unit.lastExit.signal ? `, signal ${unit.lastExit.signal}` : ""})` : "";
|
|
73
|
+
return {
|
|
74
|
+
unitId: unit.id,
|
|
75
|
+
since,
|
|
76
|
+
what: `${unit.status === "running" ? "restarting" : "failing"} with no heartbeat since ${iso(since)}, restarted ${unit.restarts} times${exit}`,
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
function iso(ms) {
|
|
80
|
+
return new Date(ms).toISOString().replace(/\.\d{3}Z$/, "Z");
|
|
81
|
+
}
|
|
82
|
+
export function readSelfCheckRecord(file) {
|
|
83
|
+
try {
|
|
84
|
+
const parsed = JSON.parse(fs.readFileSync(file, "utf8"));
|
|
85
|
+
if (typeof parsed.lastSentAt !== "number" || !Array.isArray(parsed.units))
|
|
86
|
+
return null;
|
|
87
|
+
return { lastSentAt: parsed.lastSentAt, units: parsed.units.filter((u) => typeof u === "string") };
|
|
88
|
+
}
|
|
89
|
+
catch {
|
|
90
|
+
return null;
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
export function writeSelfCheckRecord(file, record) {
|
|
94
|
+
ensurePrivateDir(path.dirname(file));
|
|
95
|
+
const tmp = `${file}.${process.pid}.tmp`;
|
|
96
|
+
fs.writeFileSync(tmp, `${JSON.stringify(record, null, 2)}\n`, { mode: 0o600 });
|
|
97
|
+
fs.renameSync(tmp, file);
|
|
98
|
+
}
|
|
99
|
+
/**
|
|
100
|
+
* Send now: something is wrong and this machine has not sent a self-check DM
|
|
101
|
+
* in the last day. Which units are affected does not matter, so units that
|
|
102
|
+
* take turns failing cannot send a DM on every change.
|
|
103
|
+
*/
|
|
104
|
+
export function shouldReport(problems, last, now) {
|
|
105
|
+
if (problems.length === 0)
|
|
106
|
+
return false;
|
|
107
|
+
if (!last)
|
|
108
|
+
return true;
|
|
109
|
+
return now - last.lastSentAt >= REPORT_INTERVAL_MS;
|
|
110
|
+
}
|
|
111
|
+
/** The DM text for the owner. Plain, with the commands that fix each case. */
|
|
112
|
+
export function selfCheckMessage(problems, machine,
|
|
113
|
+
/** A name for a vault (company slug, "personal"), when known. */
|
|
114
|
+
vaultName = () => undefined) {
|
|
115
|
+
const named = (p) => {
|
|
116
|
+
const uid = p.unitId.slice(ENGINE_UNIT_PREFIX.length);
|
|
117
|
+
const name = vaultName(uid);
|
|
118
|
+
return name ? `${name} (${uid})` : uid;
|
|
119
|
+
};
|
|
120
|
+
const names = problems.map(named).join(", ");
|
|
121
|
+
const body = `HQ sync on ${machine}: the sync engine for ${names} has not synced for over ${ATTENTION_AFTER_MS / 60_000} minutes. ` +
|
|
122
|
+
"Run `hq daemon status` on that machine.";
|
|
123
|
+
const lines = problems.map((p) => `- ${named(p)}: ${p.what}. Retry with: hq daemon restart ${p.unitId}`);
|
|
124
|
+
lines.push("", "Logs: hq daemon logs <unit>. This check runs in hq daemon and sends at most one DM a day from this machine.");
|
|
125
|
+
return { body, details: lines.join("\n") };
|
|
126
|
+
}
|
|
127
|
+
export async function runSelfCheck(deps) {
|
|
128
|
+
const now = deps.now();
|
|
129
|
+
const problems = engineUnitsNeedingAttention(deps.readState(), deps.readHolds(), now, deps.wanted);
|
|
130
|
+
if (problems.length === 0) {
|
|
131
|
+
deps.log("sync engine self-check: every engine unit is healthy");
|
|
132
|
+
return "healthy";
|
|
133
|
+
}
|
|
134
|
+
for (const p of problems)
|
|
135
|
+
deps.log(`sync engine self-check: ${p.unitId} ${p.what}`);
|
|
136
|
+
const last = readSelfCheckRecord(deps.recordPath);
|
|
137
|
+
if (!shouldReport(problems, last, now)) {
|
|
138
|
+
deps.log(`sync engine self-check: already reported at ${iso(last.lastSentAt)}; next report after ${iso(last.lastSentAt + REPORT_INTERVAL_MS)}`);
|
|
139
|
+
return "already-reported";
|
|
140
|
+
}
|
|
141
|
+
const to = await deps.sendToOwner(selfCheckMessage(problems, deps.machine, deps.vaultName));
|
|
142
|
+
if (to === null) {
|
|
143
|
+
deps.log("sync engine self-check: this machine is signed in as an agent, so there is no owner to DM; nothing was sent");
|
|
144
|
+
return "no-owner";
|
|
145
|
+
}
|
|
146
|
+
writeSelfCheckRecord(deps.recordPath, { lastSentAt: now, units: problems.map((p) => p.unitId) });
|
|
147
|
+
deps.log(`sync engine self-check: sent a DM to ${to}`);
|
|
148
|
+
return "reported";
|
|
149
|
+
}
|
|
150
|
+
//# sourceMappingURL=selfcheck.js.map
|
|
@@ -46,6 +46,30 @@ export declare const CLI_UPDATE_SYNC_IDLE_KILL_GRACE_MS = 60000;
|
|
|
46
46
|
export declare const HANDOFF_STAGGER_MS = 15000;
|
|
47
47
|
/** How long a draining unit may take to finish its work before it gets SIGTERM. */
|
|
48
48
|
export declare const HANDOFF_DRAIN_MAX_MS: number;
|
|
49
|
+
/** hq-syncd's exit code when another engine holds its engine lease or the HQ operation lock. */
|
|
50
|
+
export declare const SYNC_CORE_LOCK_HELD_EXIT = 17;
|
|
51
|
+
/**
|
|
52
|
+
* Heartbeat-timeout restarts of an engine that reported growing progress are
|
|
53
|
+
* not counted toward the hold, up to this many per failure episode. After
|
|
54
|
+
* that they count, so an engine that moves a few bytes each run and then hangs
|
|
55
|
+
* is still held (and reported by the self-check) instead of restarting forever.
|
|
56
|
+
*/
|
|
57
|
+
export declare const SYNC_CORE_PROGRESS_EXEMPT_MAX = 6;
|
|
58
|
+
/** How often a unit held for exit 17 checks whether the lock holder is still alive. */
|
|
59
|
+
export declare const LOCK_HOLDER_POLL_MS = 5000;
|
|
60
|
+
/**
|
|
61
|
+
* The pid hq-syncd names when it exits 17: "local sync engine is already
|
|
62
|
+
* running (pid N)" or "HQ operation lock for ... is held by "cmd" (pid N)".
|
|
63
|
+
* Null when the output names no live-looking pid.
|
|
64
|
+
*/
|
|
65
|
+
export declare function lockHolderPidFromOutput(output: string | undefined): number | null;
|
|
66
|
+
/**
|
|
67
|
+
* Bytes the engine reports having moved, from a protocol heartbeat that
|
|
68
|
+
* carries `progress` (see the engine contract in the hq-sync-core 0.1.24
|
|
69
|
+
* notes): `{"type":"heartbeat","progress":{"bytesReceived":N,"bytesSent":M}}`,
|
|
70
|
+
* both cumulative over the engine process. Null for a heartbeat without it.
|
|
71
|
+
*/
|
|
72
|
+
export declare function heartbeatProgressBytes(event: Record<string, unknown>): number | null;
|
|
49
73
|
export interface SupervisedChild {
|
|
50
74
|
pid?: number;
|
|
51
75
|
stdout?: {
|
|
@@ -84,6 +108,11 @@ export interface UnitHold {
|
|
|
84
108
|
probing?: boolean;
|
|
85
109
|
/** Explicit stop of a recovering unit; only an explicit restart clears it. */
|
|
86
110
|
retryStopped?: boolean;
|
|
111
|
+
/**
|
|
112
|
+
* Exit 17 holds: the pid hq-syncd said holds its lock. The hold is released
|
|
113
|
+
* as soon as that process is gone instead of waiting out the cooldown.
|
|
114
|
+
*/
|
|
115
|
+
lockHolderPid?: number;
|
|
87
116
|
}
|
|
88
117
|
/** The holds file: unit id to hold. */
|
|
89
118
|
export type PersistedHolds = Record<string, UnitHold>;
|
|
@@ -172,6 +201,8 @@ export interface SupervisorDeps {
|
|
|
172
201
|
read(): PersistedHolds;
|
|
173
202
|
write(holds: PersistedHolds): void;
|
|
174
203
|
};
|
|
204
|
+
/** Whether a process is alive (signal 0). Enables releasing exit-17 holds when the lock holder exits. */
|
|
205
|
+
pidAlive?: (pid: number) => boolean;
|
|
175
206
|
}
|
|
176
207
|
export interface Supervisor {
|
|
177
208
|
start(): void;
|
|
@@ -51,6 +51,47 @@ const MESH_OWNER_LOG_INTERVAL_MS = 10 * 60_000;
|
|
|
51
51
|
export const HANDOFF_STAGGER_MS = 15_000;
|
|
52
52
|
/** How long a draining unit may take to finish its work before it gets SIGTERM. */
|
|
53
53
|
export const HANDOFF_DRAIN_MAX_MS = 15 * 60_000;
|
|
54
|
+
/** hq-syncd's exit code when another engine holds its engine lease or the HQ operation lock. */
|
|
55
|
+
export const SYNC_CORE_LOCK_HELD_EXIT = 17;
|
|
56
|
+
/**
|
|
57
|
+
* Heartbeat-timeout restarts of an engine that reported growing progress are
|
|
58
|
+
* not counted toward the hold, up to this many per failure episode. After
|
|
59
|
+
* that they count, so an engine that moves a few bytes each run and then hangs
|
|
60
|
+
* is still held (and reported by the self-check) instead of restarting forever.
|
|
61
|
+
*/
|
|
62
|
+
export const SYNC_CORE_PROGRESS_EXEMPT_MAX = 6;
|
|
63
|
+
/** How often a unit held for exit 17 checks whether the lock holder is still alive. */
|
|
64
|
+
export const LOCK_HOLDER_POLL_MS = 5_000;
|
|
65
|
+
/**
|
|
66
|
+
* The pid hq-syncd names when it exits 17: "local sync engine is already
|
|
67
|
+
* running (pid N)" or "HQ operation lock for ... is held by "cmd" (pid N)".
|
|
68
|
+
* Null when the output names no live-looking pid.
|
|
69
|
+
*/
|
|
70
|
+
export function lockHolderPidFromOutput(output) {
|
|
71
|
+
if (!output)
|
|
72
|
+
return null;
|
|
73
|
+
const matches = [...output.matchAll(/\(pid (\d+)\)/g)];
|
|
74
|
+
const pid = Number(matches.at(-1)?.[1]);
|
|
75
|
+
return Number.isSafeInteger(pid) && pid > 1 ? pid : null;
|
|
76
|
+
}
|
|
77
|
+
/**
|
|
78
|
+
* Bytes the engine reports having moved, from a protocol heartbeat that
|
|
79
|
+
* carries `progress` (see the engine contract in the hq-sync-core 0.1.24
|
|
80
|
+
* notes): `{"type":"heartbeat","progress":{"bytesReceived":N,"bytesSent":M}}`,
|
|
81
|
+
* both cumulative over the engine process. Null for a heartbeat without it.
|
|
82
|
+
*/
|
|
83
|
+
export function heartbeatProgressBytes(event) {
|
|
84
|
+
if (event.type !== "heartbeat")
|
|
85
|
+
return null;
|
|
86
|
+
const progress = event.progress;
|
|
87
|
+
if (!progress || typeof progress !== "object" || Array.isArray(progress))
|
|
88
|
+
return null;
|
|
89
|
+
const { bytesReceived, bytesSent } = progress;
|
|
90
|
+
const count = (v) => (typeof v === "number" && Number.isFinite(v) && v >= 0 ? v : 0);
|
|
91
|
+
if (typeof bytesReceived !== "number" && typeof bytesSent !== "number")
|
|
92
|
+
return null;
|
|
93
|
+
return count(bytesReceived) + count(bytesSent);
|
|
94
|
+
}
|
|
54
95
|
const PASS_REQUEST_DIR_ENV = "HQ_SYNC_PASS_REQUEST_DIR";
|
|
55
96
|
function passRequestDirOf(env) {
|
|
56
97
|
const dir = env?.[PASS_REQUEST_DIR_ENV];
|
|
@@ -182,6 +223,9 @@ export function createSupervisor(deps) {
|
|
|
182
223
|
if (slot.recoveryHealthyTimer !== undefined)
|
|
183
224
|
clearT(slot.recoveryHealthyTimer);
|
|
184
225
|
slot.recoveryHealthyTimer = undefined;
|
|
226
|
+
if (slot.lockHolderTimer !== undefined)
|
|
227
|
+
clearT(slot.lockHolderTimer);
|
|
228
|
+
slot.lockHolderTimer = undefined;
|
|
185
229
|
};
|
|
186
230
|
const clearTimer = (slot) => {
|
|
187
231
|
clearRecoveryWindow(slot);
|
|
@@ -363,16 +407,60 @@ export function createSupervisor(deps) {
|
|
|
363
407
|
if (deps.holds && recoverableHold(slot.plan, slot.state.hold)) {
|
|
364
408
|
slot.state.hold.retryAt = now() + EPISODE_HEALTHY_RESET_MS;
|
|
365
409
|
}
|
|
410
|
+
if (kind === "exhausted" && slot.plan.service === "sync-core"
|
|
411
|
+
&& slot.state.lastExit?.code === SYNC_CORE_LOCK_HELD_EXIT && slot.lastLockHolderPid !== undefined) {
|
|
412
|
+
slot.state.hold.lockHolderPid = slot.lastLockHolderPid;
|
|
413
|
+
slot.state.hold.detail += `; the lock is held by pid ${slot.lastLockHolderPid}, released when it exits`;
|
|
414
|
+
}
|
|
366
415
|
persistedHolds[slot.plan.id] = slot.state.hold;
|
|
367
416
|
showHeld(slot);
|
|
368
417
|
deps.log("error", `holding ${slot.plan.id}: ${slot.state.reason}`);
|
|
369
418
|
writeHolds();
|
|
370
419
|
armRecoveryProbe(slot);
|
|
420
|
+
armLockHolderWatch(slot);
|
|
421
|
+
};
|
|
422
|
+
/**
|
|
423
|
+
* A unit held because its lock was taken (exit 17) starts again as soon as
|
|
424
|
+
* the process holding the lock is gone, instead of after the cooldown.
|
|
425
|
+
*/
|
|
426
|
+
const armLockHolderWatch = (slot) => {
|
|
427
|
+
const hold = slot.state.hold;
|
|
428
|
+
const pid = hold?.lockHolderPid;
|
|
429
|
+
if (!hold || pid === undefined || !deps.pidAlive || hold.retryStopped === true || stopped || slot.child)
|
|
430
|
+
return;
|
|
431
|
+
if (slot.lockHolderTimer !== undefined)
|
|
432
|
+
clearT(slot.lockHolderTimer);
|
|
433
|
+
const check = () => {
|
|
434
|
+
slot.lockHolderTimer = undefined;
|
|
435
|
+
if (stopped || slots.get(slot.plan.id) !== slot || slot.child || slot.state.hold !== hold || hold.retryStopped === true)
|
|
436
|
+
return;
|
|
437
|
+
let alive;
|
|
438
|
+
try {
|
|
439
|
+
alive = deps.pidAlive(pid);
|
|
440
|
+
}
|
|
441
|
+
catch {
|
|
442
|
+
alive = true;
|
|
443
|
+
}
|
|
444
|
+
if (alive) {
|
|
445
|
+
slot.lockHolderTimer = setT(check, LOCK_HOLDER_POLL_MS);
|
|
446
|
+
return;
|
|
447
|
+
}
|
|
448
|
+
deps.log("info", `releasing the hold on ${slot.plan.id}: pid ${pid}, which held its lock, has exited`);
|
|
449
|
+
clearTimer(slot);
|
|
450
|
+
clearHold(slot);
|
|
451
|
+
if (slot.plan.blocked)
|
|
452
|
+
setBlocked(slot);
|
|
453
|
+
else
|
|
454
|
+
startLong(slot);
|
|
455
|
+
emit();
|
|
456
|
+
};
|
|
457
|
+
slot.lockHolderTimer = setT(check, 0);
|
|
371
458
|
};
|
|
372
459
|
const clearHold = (slot) => {
|
|
373
460
|
clearRecoveryWindow(slot);
|
|
374
461
|
slot.attempts = 0;
|
|
375
462
|
slot.failures = 0;
|
|
463
|
+
slot.progressExemptRestarts = 0;
|
|
376
464
|
if (!slot.state.hold && !persistedHolds[slot.plan.id])
|
|
377
465
|
return;
|
|
378
466
|
delete slot.state.hold;
|
|
@@ -466,10 +554,11 @@ export function createSupervisor(deps) {
|
|
|
466
554
|
escalateToKill();
|
|
467
555
|
signalChild(slot, child, "SIGTERM");
|
|
468
556
|
};
|
|
469
|
-
const scheduleRestart = (slot) => {
|
|
557
|
+
const scheduleRestart = (slot, countFailure = true) => {
|
|
470
558
|
if (stopped)
|
|
471
559
|
return;
|
|
472
|
-
|
|
560
|
+
if (countFailure)
|
|
561
|
+
slot.failures += 1;
|
|
473
562
|
if (slot.failures >= EPISODE_MAX_FAILURES) {
|
|
474
563
|
holdUnit(slot, "exhausted", "transient", `${slot.failures} failures without staying up for ${EPISODE_HEALTHY_RESET_MS / 60_000} min`);
|
|
475
564
|
return;
|
|
@@ -502,8 +591,20 @@ export function createSupervisor(deps) {
|
|
|
502
591
|
slot.heartbeatTimer = undefined;
|
|
503
592
|
if (stopped || slot.child !== child || slot.expectingExit)
|
|
504
593
|
return;
|
|
505
|
-
|
|
506
|
-
|
|
594
|
+
// An engine that reported bytes moving during this run is slow, not
|
|
595
|
+
// hung: restart it, but do not count it toward the exhausted hold.
|
|
596
|
+
// Bounded per episode: past SYNC_CORE_PROGRESS_EXEMPT_MAX these count again.
|
|
597
|
+
const grew = slot.plan.service === "sync-core" && slot.progressedThisRun === true;
|
|
598
|
+
const progressed = grew && (slot.progressExemptRestarts ?? 0) < SYNC_CORE_PROGRESS_EXEMPT_MAX;
|
|
599
|
+
if (progressed)
|
|
600
|
+
slot.progressExemptRestarts = (slot.progressExemptRestarts ?? 0) + 1;
|
|
601
|
+
deps.log("warn", `${slot.plan.id} emitted no sync protocol heartbeat for ${timeoutMs}ms; restarting it` +
|
|
602
|
+
(progressed
|
|
603
|
+
? ` (it reported transfer progress this run, so this does not count toward holding it; ${slot.progressExemptRestarts}/${SYNC_CORE_PROGRESS_EXEMPT_MAX})`
|
|
604
|
+
: grew
|
|
605
|
+
? ` (it reported progress, but ${SYNC_CORE_PROGRESS_EXEMPT_MAX} such restarts already did not count, so this one does)`
|
|
606
|
+
: ""));
|
|
607
|
+
terminate(slot, () => scheduleRestart(slot, !progressed));
|
|
507
608
|
emit();
|
|
508
609
|
}, timeoutMs);
|
|
509
610
|
};
|
|
@@ -543,6 +644,16 @@ export function createSupervisor(deps) {
|
|
|
543
644
|
runDeferredCliRestart();
|
|
544
645
|
}
|
|
545
646
|
}
|
|
647
|
+
if (slot.plan.service === "sync-core") {
|
|
648
|
+
const bytes = heartbeatProgressBytes(event);
|
|
649
|
+
if (bytes !== null) {
|
|
650
|
+
// The first progress heartbeat of a run is the baseline: counters are
|
|
651
|
+
// cumulative per process, and a handshake alone moves some bytes.
|
|
652
|
+
if (slot.progressBytes !== undefined && bytes > slot.progressBytes)
|
|
653
|
+
slot.progressedThisRun = true;
|
|
654
|
+
slot.progressBytes = Math.max(bytes, slot.progressBytes ?? 0);
|
|
655
|
+
}
|
|
656
|
+
}
|
|
546
657
|
const at = now();
|
|
547
658
|
slot.state.lastHeartbeatAt = new Date(at).toISOString();
|
|
548
659
|
const pass = syncPassResult(event, slot.state.lastHeartbeatAt);
|
|
@@ -610,8 +721,10 @@ export function createSupervisor(deps) {
|
|
|
610
721
|
// run starts a new failure episode, as an unexpected exit would.
|
|
611
722
|
if (ranFor >= HEALTHY_RUN_MS)
|
|
612
723
|
slot.attempts = 0;
|
|
613
|
-
if (ranFor >= EPISODE_HEALTHY_RESET_MS)
|
|
724
|
+
if (ranFor >= EPISODE_HEALTHY_RESET_MS) {
|
|
614
725
|
slot.failures = 0;
|
|
726
|
+
slot.progressExemptRestarts = 0;
|
|
727
|
+
}
|
|
615
728
|
const then = slot.afterExit;
|
|
616
729
|
slot.afterExit = undefined;
|
|
617
730
|
then?.();
|
|
@@ -688,8 +801,10 @@ export function createSupervisor(deps) {
|
|
|
688
801
|
deps.log("warn", `${slot.plan.id} exited (code=${code ?? "null"} signal=${signal ?? "none"})`);
|
|
689
802
|
if (ranFor >= HEALTHY_RUN_MS)
|
|
690
803
|
slot.attempts = 0;
|
|
691
|
-
if (ranFor >= EPISODE_HEALTHY_RESET_MS)
|
|
804
|
+
if (ranFor >= EPISODE_HEALTHY_RESET_MS) {
|
|
692
805
|
slot.failures = 0;
|
|
806
|
+
slot.progressExemptRestarts = 0;
|
|
807
|
+
}
|
|
693
808
|
const stoppedFromOutside = signal === "SIGTERM" || signal === "SIGINT";
|
|
694
809
|
if (slot.plan.restart === "on-failure" && (code === 0 || stoppedFromOutside)) {
|
|
695
810
|
slot.state.status = "exited";
|
|
@@ -702,6 +817,12 @@ export function createSupervisor(deps) {
|
|
|
702
817
|
const spawnError = slot.lastSpawnError;
|
|
703
818
|
delete slot.lastSpawnError;
|
|
704
819
|
const outputTail = spawnError === undefined ? readExitOutputTail(slot) : undefined;
|
|
820
|
+
delete slot.lastLockHolderPid;
|
|
821
|
+
if (slot.plan.service === "sync-core" && code === SYNC_CORE_LOCK_HELD_EXIT) {
|
|
822
|
+
const holder = lockHolderPidFromOutput(outputTail);
|
|
823
|
+
if (holder !== null)
|
|
824
|
+
slot.lastLockHolderPid = holder;
|
|
825
|
+
}
|
|
705
826
|
if (!holdIfPermanent(slot, { code, signal, spawnError, outputTail }))
|
|
706
827
|
scheduleRestart(slot);
|
|
707
828
|
}
|
|
@@ -740,6 +861,8 @@ export function createSupervisor(deps) {
|
|
|
740
861
|
delete slot.lastProtocolLine;
|
|
741
862
|
delete slot.lastProtocolStatus;
|
|
742
863
|
delete slot.syncPassActive;
|
|
864
|
+
delete slot.progressBytes;
|
|
865
|
+
delete slot.progressedThisRun;
|
|
743
866
|
let child;
|
|
744
867
|
try {
|
|
745
868
|
child = deps.spawn(slot.plan);
|
|
@@ -923,6 +1046,7 @@ export function createSupervisor(deps) {
|
|
|
923
1046
|
if (slot.state.hold) {
|
|
924
1047
|
showHeld(slot);
|
|
925
1048
|
armRecoveryProbe(slot);
|
|
1049
|
+
armLockHolderWatch(slot);
|
|
926
1050
|
return;
|
|
927
1051
|
}
|
|
928
1052
|
startLong(slot);
|
|
@@ -20,7 +20,8 @@ function isHold(value) {
|
|
|
20
20
|
&& typeof h.detail === "string"
|
|
21
21
|
&& typeof h.since === "number"
|
|
22
22
|
&& typeof h.failures === "number"
|
|
23
|
-
&& typeof h.keyHash === "string"
|
|
23
|
+
&& typeof h.keyHash === "string"
|
|
24
|
+
&& (h.lockHolderPid === undefined || (typeof h.lockHolderPid === "number" && Number.isSafeInteger(h.lockHolderPid) && h.lockHolderPid > 1)));
|
|
24
25
|
}
|
|
25
26
|
export function readUnitHolds(file) {
|
|
26
27
|
let raw;
|
|
@@ -114,6 +114,31 @@ export declare function activateLaunchd(plistPath: string, run: RunCommand, uid?
|
|
|
114
114
|
sleep?: SleepFn;
|
|
115
115
|
unloadDeadlineMs?: number;
|
|
116
116
|
}): void;
|
|
117
|
+
export interface LaunchdBootstrapResult {
|
|
118
|
+
ok: boolean;
|
|
119
|
+
attempts: number;
|
|
120
|
+
error?: string;
|
|
121
|
+
}
|
|
122
|
+
/**
|
|
123
|
+
* Bootstrap a plist that is not loaded, retrying launchd's transient EIO with
|
|
124
|
+
* backoff. A label that is already loaded counts as success. Unlike
|
|
125
|
+
* reloadLaunchd this never boots anything out, so it is safe to call on a
|
|
126
|
+
* running job.
|
|
127
|
+
*/
|
|
128
|
+
export declare function bootstrapLaunchd(plistPath: string, run: RunCommand, uid?: number, opts?: {
|
|
129
|
+
sleep?: SleepFn;
|
|
130
|
+
}): LaunchdBootstrapResult;
|
|
131
|
+
export interface LaunchdJobInfo {
|
|
132
|
+
loaded: boolean;
|
|
133
|
+
/** launchd's `state = ...` line (running, not running, spawn scheduled, ...). */
|
|
134
|
+
state?: string;
|
|
135
|
+
pid?: number;
|
|
136
|
+
/** The loaded job's program arguments (`arguments = { ... }`), when printed. */
|
|
137
|
+
arguments?: string[];
|
|
138
|
+
}
|
|
139
|
+
/** What `launchctl print gui/<uid>/<label>` says about one job. */
|
|
140
|
+
export declare function launchdJobInfo(label: string, run: RunCommand, uid?: number): LaunchdJobInfo;
|
|
141
|
+
export declare function parseLaunchctlPrintJob(stdout: string): LaunchdJobInfo;
|
|
117
142
|
export declare function deactivateLaunchd(plistPath: string, run: RunCommand, uid?: number): void;
|
|
118
143
|
export declare function launchdIsLoaded(label: string, run: RunCommand, uid?: number): boolean;
|
|
119
144
|
//# sourceMappingURL=launchd.d.ts.map
|
|
@@ -58,9 +58,11 @@ export function launchdPlistPath(home, spec) {
|
|
|
58
58
|
return path.join(launchAgentsDir(home), `${spec.label}.plist`);
|
|
59
59
|
}
|
|
60
60
|
export function renderLaunchdPlist(spec, host) {
|
|
61
|
-
const discovered = discoverLaunchdIdentity(host.home);
|
|
62
|
-
const associatedBundleIdentifier =
|
|
63
|
-
|
|
61
|
+
const discovered = spec.independentOfApp ? {} : discoverLaunchdIdentity(host.home);
|
|
62
|
+
const associatedBundleIdentifier = spec.independentOfApp
|
|
63
|
+
? undefined
|
|
64
|
+
: host.associatedBundleIdentifier ?? discovered.associatedBundleIdentifier;
|
|
65
|
+
const launcherBinary = spec.independentOfApp ? undefined : host.launchdLauncherBinary ?? discovered.launcherBinary;
|
|
64
66
|
const args = launchdProgramArguments({ ...host, launchdLauncherBinary: launcherBinary }, spec.args)
|
|
65
67
|
.map((a) => ` <string>${escapeXml(a)}</string>`)
|
|
66
68
|
.join("\n");
|
|
@@ -303,6 +305,52 @@ export function activateLaunchd(plistPath, run, uid, opts) {
|
|
|
303
305
|
}
|
|
304
306
|
throw new Error(`${detail}. ${target} is NOT loaded and will not run until it is. Load it with: launchctl bootstrap ${domain} ${plistPath}`);
|
|
305
307
|
}
|
|
308
|
+
/**
|
|
309
|
+
* Bootstrap a plist that is not loaded, retrying launchd's transient EIO with
|
|
310
|
+
* backoff. A label that is already loaded counts as success. Unlike
|
|
311
|
+
* reloadLaunchd this never boots anything out, so it is safe to call on a
|
|
312
|
+
* running job.
|
|
313
|
+
*/
|
|
314
|
+
export function bootstrapLaunchd(plistPath, run, uid, opts) {
|
|
315
|
+
const domain = guiDomain(uid);
|
|
316
|
+
const target = launchdServiceTarget(plistPath, uid);
|
|
317
|
+
const sleep = opts?.sleep ?? blockingSleep;
|
|
318
|
+
if (run("launchctl", ["print", target]).status === 0)
|
|
319
|
+
return { ok: true, attempts: 0 };
|
|
320
|
+
let last = run("launchctl", ["bootstrap", domain, plistPath]);
|
|
321
|
+
let attempts = 1;
|
|
322
|
+
for (const backoff of LAUNCHD_BOOTSTRAP_BACKOFF_MS) {
|
|
323
|
+
if (last.status === 0 || !isRetryableLaunchdBootstrap(last))
|
|
324
|
+
break;
|
|
325
|
+
sleep(backoff);
|
|
326
|
+
last = run("launchctl", ["bootstrap", domain, plistPath]);
|
|
327
|
+
attempts++;
|
|
328
|
+
}
|
|
329
|
+
// "already bootstrapped" races (another caller loaded it) are success.
|
|
330
|
+
if (last.status === 0 || run("launchctl", ["print", target]).status === 0)
|
|
331
|
+
return { ok: true, attempts };
|
|
332
|
+
return { ok: false, attempts, error: last.stderr.trim() || last.stdout.trim() || `exit ${last.status}` };
|
|
333
|
+
}
|
|
334
|
+
/** What `launchctl print gui/<uid>/<label>` says about one job. */
|
|
335
|
+
export function launchdJobInfo(label, run, uid) {
|
|
336
|
+
const r = run("launchctl", ["print", `${guiDomain(uid)}/${label}`]);
|
|
337
|
+
if (r.status !== 0)
|
|
338
|
+
return { loaded: false };
|
|
339
|
+
return parseLaunchctlPrintJob(r.stdout);
|
|
340
|
+
}
|
|
341
|
+
export function parseLaunchctlPrintJob(stdout) {
|
|
342
|
+
const state = /^\s*state\s*=\s*(.+?)\s*$/m.exec(stdout)?.[1];
|
|
343
|
+
const pidText = /^\s*pid\s*=\s*(\d+)\s*$/m.exec(stdout)?.[1];
|
|
344
|
+
const pid = pidText ? Number(pidText) : undefined;
|
|
345
|
+
const argsBlock = /^\s*arguments\s*=\s*\{\s*\n([\s\S]*?)\n\s*\}/m.exec(stdout)?.[1];
|
|
346
|
+
const args = argsBlock?.split("\n").map((line) => line.trim()).filter((line) => line.length > 0);
|
|
347
|
+
return {
|
|
348
|
+
loaded: true,
|
|
349
|
+
...(state ? { state } : {}),
|
|
350
|
+
...(pid && pid > 0 ? { pid } : {}),
|
|
351
|
+
...(args && args.length > 0 ? { arguments: args } : {}),
|
|
352
|
+
};
|
|
353
|
+
}
|
|
306
354
|
export function deactivateLaunchd(plistPath, run, uid) {
|
|
307
355
|
run("launchctl", ["bootout", guiDomain(uid), plistPath]);
|
|
308
356
|
}
|
|
@@ -1,5 +1,11 @@
|
|
|
1
1
|
import * as fs from "node:fs";
|
|
2
2
|
import { type RunCommand, type SleepFn } from "./launchd.js";
|
|
3
|
+
/**
|
|
4
|
+
* Agents that must load at login without the app, so they are never wrapped
|
|
5
|
+
* in the HQ.app launcher or attributed to its bundle. Kept in step with
|
|
6
|
+
* DAEMON_LAUNCHD_LABEL in daemon/install.ts.
|
|
7
|
+
*/
|
|
8
|
+
export declare const APP_INDEPENDENT_LAUNCHD_LABELS: ReadonlySet<string>;
|
|
3
9
|
export interface LaunchdMigrationOptions {
|
|
4
10
|
home: string;
|
|
5
11
|
run: RunCommand;
|
|
@@ -19,4 +25,12 @@ export interface LaunchdMigrationResult {
|
|
|
19
25
|
}
|
|
20
26
|
/** First-CLI-run cleanup for HQ's old Node-first LaunchAgents and backup plists. */
|
|
21
27
|
export declare function migrateLegacyNodeLaunchAgents(options: LaunchdMigrationOptions): LaunchdMigrationResult;
|
|
28
|
+
/**
|
|
29
|
+
* The app-independent form of an HQ LaunchAgent plist written by an older
|
|
30
|
+
* install: the HQ.app launcher (`hq-sync-menubar --hq-launchd-task`) is
|
|
31
|
+
* dropped from ProgramArguments and the AssociatedBundleIdentifiers entry is
|
|
32
|
+
* removed. Everything else (environment, logs, KeepAlive) is kept as written.
|
|
33
|
+
* Returns null when the plist is already app-independent.
|
|
34
|
+
*/
|
|
35
|
+
export declare function appIndependentLaunchdPlist(source: string): string | null;
|
|
22
36
|
//# sourceMappingURL=migration.d.ts.map
|
|
@@ -5,6 +5,12 @@ import { isVersionedNodePath, preferStableNodeBinary } from "../node-binary.js";
|
|
|
5
5
|
import { preferStableHqBinary } from "../mesh/live/daemon/install.js";
|
|
6
6
|
import { launchdMigrationMarkerPath, shouldRunLaunchdMigration, writeLaunchdMigrationMarker } from "./migration-state.js";
|
|
7
7
|
const HQ_LABEL_PREFIXES = ["ai.indigo.hq-", "ai.getindigo.hq-"];
|
|
8
|
+
/**
|
|
9
|
+
* Agents that must load at login without the app, so they are never wrapped
|
|
10
|
+
* in the HQ.app launcher or attributed to its bundle. Kept in step with
|
|
11
|
+
* DAEMON_LAUNCHD_LABEL in daemon/install.ts.
|
|
12
|
+
*/
|
|
13
|
+
export const APP_INDEPENDENT_LAUNCHD_LABELS = new Set(["ai.getindigo.hq-daemon"]);
|
|
8
14
|
function xml(value) {
|
|
9
15
|
return value.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """);
|
|
10
16
|
}
|
|
@@ -36,6 +42,8 @@ export function migrateLegacyNodeLaunchAgents(options) {
|
|
|
36
42
|
}
|
|
37
43
|
if (!name.endsWith(".plist") || !HQ_LABEL_PREFIXES.some((prefix) => name.startsWith(prefix)))
|
|
38
44
|
continue;
|
|
45
|
+
if (APP_INDEPENDENT_LAUNCHD_LABELS.has(name.slice(0, -".plist".length)))
|
|
46
|
+
continue;
|
|
39
47
|
const source = files.readFileSync(fullPath, "utf8");
|
|
40
48
|
const args = launchdPlistProgramArguments(source);
|
|
41
49
|
if (args.length < 2 || !isNodeProgram(args[0]))
|
|
@@ -82,4 +90,30 @@ export function migrateLegacyNodeLaunchAgents(options) {
|
|
|
82
90
|
writeLaunchdMigrationMarker(options.home, deferred);
|
|
83
91
|
return { migrated, backupsRemoved, deferred, alreadyDone: false };
|
|
84
92
|
}
|
|
93
|
+
/**
|
|
94
|
+
* The app-independent form of an HQ LaunchAgent plist written by an older
|
|
95
|
+
* install: the HQ.app launcher (`hq-sync-menubar --hq-launchd-task`) is
|
|
96
|
+
* dropped from ProgramArguments and the AssociatedBundleIdentifiers entry is
|
|
97
|
+
* removed. Everything else (environment, logs, KeepAlive) is kept as written.
|
|
98
|
+
* Returns null when the plist is already app-independent.
|
|
99
|
+
*/
|
|
100
|
+
export function appIndependentLaunchdPlist(source) {
|
|
101
|
+
const args = launchdPlistProgramArguments(source);
|
|
102
|
+
const launcherIndex = args.indexOf("--hq-launchd-task");
|
|
103
|
+
const wrapped = launcherIndex === 1 && args.length > 3;
|
|
104
|
+
const associated = /<key>AssociatedBundleIdentifiers<\/key>/.test(source);
|
|
105
|
+
if (!wrapped && !associated)
|
|
106
|
+
return null;
|
|
107
|
+
let updated = source;
|
|
108
|
+
if (wrapped) {
|
|
109
|
+
updated = updated.replace(/<key>ProgramArguments<\/key>\s*<array>[\s\S]*?<\/array>/, plistArgs(args.slice(2)).trimStart());
|
|
110
|
+
}
|
|
111
|
+
if (associated) {
|
|
112
|
+
updated = updated.replace(/[ \t]*<key>AssociatedBundleIdentifiers<\/key>\s*(?:<array>[\s\S]*?<\/array>|<string>[^<]*<\/string>)[ \t]*\n?/, "");
|
|
113
|
+
}
|
|
114
|
+
if (updated === source || /<key>AssociatedBundleIdentifiers<\/key>/.test(updated) || launchdPlistProgramArguments(updated).includes("--hq-launchd-task")) {
|
|
115
|
+
throw new Error("Could not safely rewrite the LaunchAgent to load without HQ.app");
|
|
116
|
+
}
|
|
117
|
+
return updated;
|
|
118
|
+
}
|
|
85
119
|
//# sourceMappingURL=migration.js.map
|
|
@@ -16,4 +16,17 @@ export declare function renderSystemdUserUnit(spec: ServiceSpec, host: ServiceHo
|
|
|
16
16
|
export declare function activateSystemd(unitName: string, run: RunCommand): void;
|
|
17
17
|
export declare function deactivateSystemd(unitName: string, run: RunCommand): void;
|
|
18
18
|
export declare function systemdIsActive(unitName: string, run: RunCommand): boolean;
|
|
19
|
+
export interface SystemdUnitInfo {
|
|
20
|
+
/** `systemctl is-enabled` printed enabled (or enabled-runtime / linked / alias). */
|
|
21
|
+
enabled: boolean;
|
|
22
|
+
/** `systemctl is-active` printed active (or activating / reloading). */
|
|
23
|
+
active: boolean;
|
|
24
|
+
/** The raw `is-enabled` / `is-active` answers, for status output. */
|
|
25
|
+
enabledState: string;
|
|
26
|
+
activeState: string;
|
|
27
|
+
/** MainPID when systemd reports one. */
|
|
28
|
+
pid?: number;
|
|
29
|
+
}
|
|
30
|
+
/** Ask systemd about one unit; `scope: "user"` adds `--user`. */
|
|
31
|
+
export declare function systemdUnitInfo(unitName: string, run: RunCommand, scope?: "user" | "system"): SystemdUnitInfo;
|
|
19
32
|
//# sourceMappingURL=systemd.d.ts.map
|
|
@@ -69,4 +69,23 @@ export function systemdIsActive(unitName, run) {
|
|
|
69
69
|
const r = run("systemctl", ["--user", "is-active", "--quiet", unitName]);
|
|
70
70
|
return r.status === 0;
|
|
71
71
|
}
|
|
72
|
+
/** Ask systemd about one unit; `scope: "user"` adds `--user`. */
|
|
73
|
+
export function systemdUnitInfo(unitName, run, scope = "user") {
|
|
74
|
+
const prefix = scope === "user" ? ["--user"] : [];
|
|
75
|
+
const word = (args) => {
|
|
76
|
+
const r = run("systemctl", [...prefix, ...args, unitName]);
|
|
77
|
+
return (r.stdout.trim().split("\n")[0] ?? "").trim() || "unknown";
|
|
78
|
+
};
|
|
79
|
+
const enabledState = word(["is-enabled"]);
|
|
80
|
+
const activeState = word(["is-active"]);
|
|
81
|
+
const show = run("systemctl", [...prefix, "show", "-p", "MainPID", "--value", unitName]);
|
|
82
|
+
const pid = Number(show.stdout.trim());
|
|
83
|
+
return {
|
|
84
|
+
enabled: /^(enabled|enabled-runtime|linked|linked-runtime|alias|static|indirect|generated)$/.test(enabledState),
|
|
85
|
+
active: /^(active|activating|reloading)$/.test(activeState),
|
|
86
|
+
enabledState,
|
|
87
|
+
activeState,
|
|
88
|
+
...(show.status === 0 && Number.isInteger(pid) && pid > 0 ? { pid } : {}),
|
|
89
|
+
};
|
|
90
|
+
}
|
|
72
91
|
//# sourceMappingURL=systemd.js.map
|