home-hosted 0.7.19 → 0.7.21
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/AGENTS.md +9 -1
- package/README.md +1 -1
- package/dist/cli.js +236 -34
- package/dist/cli.js.map +1 -1
- package/package.json +1 -1
- package/uis/stock/dist/assets/{index-DpddvXhj.js → index-3hQv8lsr.js} +1 -1
- package/uis/stock/dist/index.html +1 -1
- package/uis/stock/dist/ui.json +3 -3
package/AGENTS.md
CHANGED
|
@@ -121,6 +121,12 @@ nobody reads is worse than a long file.
|
|
|
121
121
|
never recreated.
|
|
122
122
|
- **A port is only ever freed by re-listing its listeners.** `free-port` never trusts a pid quoted in
|
|
123
123
|
a message, and refuses any listener in `supervisedPids()` instead of killing it.
|
|
124
|
+
- **A live pid is not the running panel.** Pids are recycled and a zombie still answers signal 0, so
|
|
125
|
+
`up`/`status`/`down` decide with `runtimeIsAlive`/`runtimeLiveness` (pid **plus** `run.json`'s own
|
|
126
|
+
`startedAt`), never `isProcessAlive` alone — otherwise `up` refuses against a dead panel, `status`
|
|
127
|
+
reports "running, but not answering", and `down` signals an unrelated process. Only `'gone'`
|
|
128
|
+
(nothing behind the pid) deletes a record; `'recycled'` blocks a **signal** but still allows the
|
|
129
|
+
token-authenticated `/_hh/shutdown`, because the birth time is wall-clock-derived.
|
|
124
130
|
- **Only `kill` ever touches a process the panel could not identify** — which is why it logs the pids
|
|
125
131
|
it stopped. `follow` adopts this entry's own successor, `reclaim` replaces it. Ownership is the
|
|
126
132
|
entry's whole **tree**, and the panel's own tree is excluded. Full matching rules:
|
|
@@ -185,7 +191,9 @@ nobody reads is worse than a long file.
|
|
|
185
191
|
commit*. A stale doc is a bug like stale code.
|
|
186
192
|
- **UIs move together.** `uis/stock` is not the only client: a change to it — or to a shared contract
|
|
187
193
|
it reads — lands in every other UI under `uis/`, and each altered UI bumps its `ui.json` (see
|
|
188
|
-
[`.agentDocs/ARCHITECTURE.md`](.agentDocs/ARCHITECTURE.md)).
|
|
194
|
+
[`.agentDocs/ARCHITECTURE.md`](.agentDocs/ARCHITECTURE.md)). `src/shared/**` is a UI build input, so
|
|
195
|
+
a contract a UI *calls* at runtime ships in its bundle and needs the bump — the pre-commit hook only
|
|
196
|
+
inspects `uis/` paths and stays silent on that commit.
|
|
189
197
|
|
|
190
198
|
## Releasing
|
|
191
199
|
|
package/README.md
CHANGED
|
@@ -341,7 +341,7 @@ restarts itself), and how hand-edits are validated: [SERVERS.md](./docs/SERVERS.
|
|
|
341
341
|
| `home-hosted up` | start the panel detached, and keep it alive in the background |
|
|
342
342
|
| `home-hosted down` | stop it cleanly — supervised processes included, persistent entries left running |
|
|
343
343
|
| `home-hosted restart` | `down`, then `up` — or `restart <id>` to restart one server, leaving the panel up |
|
|
344
|
-
| `home-hosted status` | pid, URL, health, uptime, state and log paths — the panel's console *and* the per-server directory, naming the `<id>.log` convention once logs exist (`--json` for scripts; stopped answers `{ running: false, initialised }`, where `initialised: false` means nothing was ever set up here) |
|
|
344
|
+
| `home-hosted status` | pid, URL, health, uptime, state and log paths — the panel's console *and* the per-server directory, naming the `<id>.log` convention once logs exist (`--json` for scripts; stopped answers `{ running: false, initialised }`, where `initialised: false` means nothing was ever set up here, plus `liveness` — `gone`, or `recycled` when the OS has handed the recorded pid to another process and the record is stale) |
|
|
345
345
|
| `home-hosted start <id>` | start one server in the default workspace — and anything it `dependsOn` (`--workspace <id>`) |
|
|
346
346
|
| `home-hosted stop <id>` | stop one server, nothing else (`--workspace <id>`) |
|
|
347
347
|
| `home-hosted restart <id>` | restart one server, nothing else (`--workspace <id>`) — the same as the panel's per-server Restart |
|
package/dist/cli.js
CHANGED
|
@@ -1077,7 +1077,14 @@ maxRssBytes: "number.integer >= 0 = 0" }).onUndeclaredKey("reject");
|
|
|
1077
1077
|
logBufferLines: logBufferLinesSchema.optional()
|
|
1078
1078
|
}).onUndeclaredKey("reject");
|
|
1079
1079
|
editableFields = {
|
|
1080
|
-
|
|
1080
|
+
/**
|
|
1081
|
+
* `null` clears it, the same spelling `serverSchema` accepts and every display's `label ?? id`
|
|
1082
|
+
* fallback needs — an empty string satisfies neither. `serverPatchSchema` is what the editor
|
|
1083
|
+
* validates its payload against and what `PATCH /api/servers/:id` checks, so without this a
|
|
1084
|
+
* cleared Label was refused with "label must be a string (was null)" and could not be saved at all
|
|
1085
|
+
* (`1f58a5d` fixed `applyPatch` and the *control* patch schema, and cited that one in its message).
|
|
1086
|
+
*/
|
|
1087
|
+
label: labelOrNullSchema.optional(),
|
|
1081
1088
|
enabled: "boolean?",
|
|
1082
1089
|
autostart: "boolean?",
|
|
1083
1090
|
persistent: "boolean?",
|
|
@@ -7301,6 +7308,125 @@ function isProcessAlive(pid) {
|
|
|
7301
7308
|
return error.code === "EPERM";
|
|
7302
7309
|
}
|
|
7303
7310
|
}
|
|
7311
|
+
/**
|
|
7312
|
+
* `/proc/<pid>/stat` field 22 is the start time in clock ticks since boot, which only
|
|
7313
|
+
* becomes an epoch by adding `btime` from `/proc/stat`. Field 3 is the state.
|
|
7314
|
+
*
|
|
7315
|
+
* The comm field is parenthesised and may itself hold spaces and parentheses, so the
|
|
7316
|
+
* fields are counted from after the **last** `)`. Counting from the first would shift
|
|
7317
|
+
* every offset for a process whose name contains a `)`, and this value decides whether a
|
|
7318
|
+
* live-looking pid is really the daemon.
|
|
7319
|
+
*/
|
|
7320
|
+
function parseProcBirth(content, btimeSeconds, hz = LINUX_HZ) {
|
|
7321
|
+
const close = content.lastIndexOf(")");
|
|
7322
|
+
if (close < 0) return null;
|
|
7323
|
+
const fields = content.slice(close + 2).split(" ");
|
|
7324
|
+
const ticks = Number.parseInt(fields[19] ?? "", 10);
|
|
7325
|
+
if (!Number.isFinite(ticks) || ticks < 0) return null;
|
|
7326
|
+
return {
|
|
7327
|
+
startMs: btimeSeconds * 1e3 + ticks / hz * 1e3,
|
|
7328
|
+
zombie: fields[0] === "Z"
|
|
7329
|
+
};
|
|
7330
|
+
}
|
|
7331
|
+
/**
|
|
7332
|
+
* One `ps -o state=,lstart=` row, e.g. `S Wed Oct 7 19:43:26 2026` (macOS, other POSIX).
|
|
7333
|
+
* The leading weekday is stripped before parsing: `Date.parse` is not obliged to accept it
|
|
7334
|
+
* and it says nothing the date does not.
|
|
7335
|
+
*/
|
|
7336
|
+
function parsePsBirth(output) {
|
|
7337
|
+
const line = output.trim().split("\n")[0]?.trim() ?? "";
|
|
7338
|
+
if (line.length === 0) return null;
|
|
7339
|
+
const [state = "", ...rest] = line.split(/\s+/);
|
|
7340
|
+
const startMs = Date.parse(rest.join(" ").replace(/^[A-Z]{3,9}\s+/i, ""));
|
|
7341
|
+
if (!Number.isFinite(startMs)) return null;
|
|
7342
|
+
return {
|
|
7343
|
+
startMs,
|
|
7344
|
+
zombie: state.startsWith("Z")
|
|
7345
|
+
};
|
|
7346
|
+
}
|
|
7347
|
+
/**
|
|
7348
|
+
* FILETIME ticks (100ns since **1601**, UTC) to a Unix epoch in ms — the shape
|
|
7349
|
+
* `DateTime.ToFileTimeUtc()` returns.
|
|
7350
|
+
*
|
|
7351
|
+
* The epoch matters: `DateTime.Ticks` counts from **0001**, not 1601, and the two differ by
|
|
7352
|
+
* 1600 years. Reading the wrong one put every Windows panel ~1600 years in the future, which
|
|
7353
|
+
* `processLiveness` then called `recycled` — a live panel that could never signal-stop itself.
|
|
7354
|
+
* The PowerShell call below therefore asks for `ToFileTimeUtc()` explicitly, and this is exported
|
|
7355
|
+
* so the conversion is pinned by a test.
|
|
7356
|
+
*/
|
|
7357
|
+
function parseFiletime(ticks) {
|
|
7358
|
+
if (!Number.isFinite(ticks) || ticks <= 0) return null;
|
|
7359
|
+
return ticks / 1e4 - FILETIME_UNIX_OFFSET_MS;
|
|
7360
|
+
}
|
|
7361
|
+
/**
|
|
7362
|
+
* When the pid started, from the platform's own process table. `null` when the platform
|
|
7363
|
+
* cannot say — an unreadable `/proc`, no `ps`, or no PowerShell — which the caller must
|
|
7364
|
+
* read as "unknown", never as "gone".
|
|
7365
|
+
*/
|
|
7366
|
+
async function processBirth(pid) {
|
|
7367
|
+
if (!Number.isInteger(pid) || pid <= 0) return null;
|
|
7368
|
+
if (process.platform === "linux") try {
|
|
7369
|
+
const [stat, system] = await Promise.all([fs.promises.readFile(`/proc/${pid}/stat`, "utf8"), fs.promises.readFile("/proc/stat", "utf8")]);
|
|
7370
|
+
const btime = /^btime (\d+)$/m.exec(system)?.[1];
|
|
7371
|
+
if (btime === void 0) return null;
|
|
7372
|
+
return parseProcBirth(stat, Number.parseInt(btime, 10));
|
|
7373
|
+
} catch {
|
|
7374
|
+
return null;
|
|
7375
|
+
}
|
|
7376
|
+
if (process.platform === "win32") try {
|
|
7377
|
+
const script = `$p = Get-CimInstance Win32_Process -Filter "ProcessId=${pid}"; if ($p) { $p.CreationDate.ToFileTimeUtc() }`;
|
|
7378
|
+
const { stdout } = await execFileAsync$2("powershell", [
|
|
7379
|
+
"-NoProfile",
|
|
7380
|
+
"-NonInteractive",
|
|
7381
|
+
"-Command",
|
|
7382
|
+
script
|
|
7383
|
+
], {
|
|
7384
|
+
timeout: 2e4,
|
|
7385
|
+
windowsHide: true
|
|
7386
|
+
});
|
|
7387
|
+
const startMs = parseFiletime(Number.parseFloat(stdout.trim()));
|
|
7388
|
+
return startMs === null ? null : {
|
|
7389
|
+
startMs,
|
|
7390
|
+
zombie: false
|
|
7391
|
+
};
|
|
7392
|
+
} catch {
|
|
7393
|
+
return null;
|
|
7394
|
+
}
|
|
7395
|
+
try {
|
|
7396
|
+
const { stdout } = await execFileAsync$2("ps", [
|
|
7397
|
+
"-p",
|
|
7398
|
+
String(pid),
|
|
7399
|
+
"-o",
|
|
7400
|
+
"state=,lstart="
|
|
7401
|
+
], { timeout: 3e3 });
|
|
7402
|
+
return parsePsBirth(stdout);
|
|
7403
|
+
} catch {
|
|
7404
|
+
return null;
|
|
7405
|
+
}
|
|
7406
|
+
}
|
|
7407
|
+
/**
|
|
7408
|
+
* A pid is **not** an identity. The OS hands a dead process's pid to an unrelated one later, and a
|
|
7409
|
+
* zombie — exited but not yet reaped by its parent — still answers signal 0. So `isProcessAlive`
|
|
7410
|
+
* alone can report a long-dead daemon as live, which is what let `up` say "already running" and
|
|
7411
|
+
* `status` say "running, but not answering" against a stale `run.json`, and what let `down` signal
|
|
7412
|
+
* whatever stranger had inherited the pid.
|
|
7413
|
+
*
|
|
7414
|
+
* `startedAt` settles it in one direction, with no threshold to guess at: the daemon writes its
|
|
7415
|
+
* record *during its own boot*, so its process is necessarily born **before** that stamp. A pid born
|
|
7416
|
+
* after it is not that process, whoever else it now is. The comparison is deliberately one-sided —
|
|
7417
|
+
* the other direction would have to tell a recycled pid from a wall clock that merely moved, and it
|
|
7418
|
+
* cannot, so a record is never called stale for being *older* than its process.
|
|
7419
|
+
*
|
|
7420
|
+
* `'unknown'` is not `'gone'`: a platform that cannot report a birth time must not have its live pid
|
|
7421
|
+
* dismissed, or a running panel's record would be cleared out from under it.
|
|
7422
|
+
*/
|
|
7423
|
+
async function processLiveness(pid, startedAt, options = {}) {
|
|
7424
|
+
if (!isProcessAlive(pid)) return "gone";
|
|
7425
|
+
const birth = await (options.readBirth ?? processBirth)(pid);
|
|
7426
|
+
if (birth === null) return "unknown";
|
|
7427
|
+
if (birth.zombie) return "gone";
|
|
7428
|
+
return birth.startMs > startedAt + (options.toleranceMs ?? 2e3) ? "recycled" : "live";
|
|
7429
|
+
}
|
|
7304
7430
|
function signalPid$1(pid, name) {
|
|
7305
7431
|
try {
|
|
7306
7432
|
process.kill(pid, name);
|
|
@@ -7384,9 +7510,11 @@ async function listPortHolders(port) {
|
|
|
7384
7510
|
function parsePids(stdout) {
|
|
7385
7511
|
return [...new Set(stdout.split(/\s+/).map((entry) => Number.parseInt(entry, 10)).filter((pid) => Number.isInteger(pid) && pid > 0 && pid !== process.pid))];
|
|
7386
7512
|
}
|
|
7387
|
-
var execFileAsync$2;
|
|
7513
|
+
var execFileAsync$2, LINUX_HZ, FILETIME_UNIX_OFFSET_MS;
|
|
7388
7514
|
var init_port = __esmMin((() => {
|
|
7389
7515
|
execFileAsync$2 = promisify(execFile);
|
|
7516
|
+
LINUX_HZ = 100;
|
|
7517
|
+
FILETIME_UNIX_OFFSET_MS = 0xa9730b66800;
|
|
7390
7518
|
}));
|
|
7391
7519
|
//#endregion
|
|
7392
7520
|
//#region src/providers/nanny.ts
|
|
@@ -8215,6 +8343,7 @@ var init_proxy$1 = __esmMin((() => {
|
|
|
8215
8343
|
init_types();
|
|
8216
8344
|
init_nanny$2();
|
|
8217
8345
|
init_port();
|
|
8346
|
+
init_proc();
|
|
8218
8347
|
init_proxy$2();
|
|
8219
8348
|
init_proxy_config();
|
|
8220
8349
|
init_tls$1();
|
|
@@ -9610,7 +9739,9 @@ var init_proxy$1 = __esmMin((() => {
|
|
|
9610
9739
|
const statePath = nannyStatePath(this.options.stateDir, PROXY_ID);
|
|
9611
9740
|
const state = readNannyState(statePath);
|
|
9612
9741
|
if (state !== null && state.serverId === PROXY_ID) {
|
|
9613
|
-
const
|
|
9742
|
+
const nanny = await nannyIsAlive(state, PROXY_ID);
|
|
9743
|
+
const child = state.childPid !== null && state.childPid > 0 && await processCarriesServerId(state.childPid, PROXY_ID);
|
|
9744
|
+
const pids = [...nanny ? [state.nannyPid] : [], ...nanny || child ? [state.childPid] : []].filter((pid) => pid !== null && pid > 0);
|
|
9614
9745
|
for (const pid of pids) {
|
|
9615
9746
|
if (!isProcessAlive(pid)) continue;
|
|
9616
9747
|
try {
|
|
@@ -9621,10 +9752,12 @@ var init_proxy$1 = __esmMin((() => {
|
|
|
9621
9752
|
}
|
|
9622
9753
|
const deadline = Date.now() + (this.options.stopGraceMs ?? 1e4);
|
|
9623
9754
|
while (Date.now() < deadline && pids.some((pid) => isProcessAlive(pid))) await new Promise((resolve) => setTimeout(resolve, 100));
|
|
9624
|
-
|
|
9755
|
+
const survivors = pids.filter(isProcessAlive);
|
|
9756
|
+
for (const pid of survivors) try {
|
|
9625
9757
|
process.kill(pid, "SIGKILL");
|
|
9626
9758
|
} catch {}
|
|
9627
|
-
|
|
9759
|
+
if (survivors.some(isProcessAlive)) logger.warn(`proxy: pid ${survivors.join(", ")} ignored SIGKILL — keeping ${path.basename(statePath)} so a later boot can still stop it`);
|
|
9760
|
+
else fs.rmSync(statePath, { force: true });
|
|
9628
9761
|
}
|
|
9629
9762
|
this.nanny = null;
|
|
9630
9763
|
this.admin = null;
|
|
@@ -11126,12 +11259,16 @@ var init_layout = __esmMin((() => {
|
|
|
11126
11259
|
//#region src/helpers/daemon.ts
|
|
11127
11260
|
var daemon_exports = /* @__PURE__ */ __exportAll({
|
|
11128
11261
|
clearRuntime: () => clearRuntime,
|
|
11262
|
+
isLiveLiveness: () => isLiveLiveness,
|
|
11129
11263
|
isProcessAlive: () => isProcessAlive,
|
|
11130
11264
|
newToken: () => newToken,
|
|
11131
11265
|
probeRuntime: () => probeRuntime,
|
|
11132
11266
|
readRuntime: () => readRuntime,
|
|
11133
11267
|
requestControl: () => requestControl,
|
|
11134
11268
|
requestShutdown: () => requestShutdown,
|
|
11269
|
+
runtimeIsAlive: () => runtimeIsAlive,
|
|
11270
|
+
runtimeIsGone: () => runtimeIsGone,
|
|
11271
|
+
runtimeLiveness: () => runtimeLiveness,
|
|
11135
11272
|
runtimeSchema: () => runtimeSchema,
|
|
11136
11273
|
writeRuntime: () => writeRuntime
|
|
11137
11274
|
});
|
|
@@ -11152,6 +11289,51 @@ function clearRuntime() {
|
|
|
11152
11289
|
function newToken() {
|
|
11153
11290
|
return randomBytes(32).toString("base64url");
|
|
11154
11291
|
}
|
|
11292
|
+
/**
|
|
11293
|
+
* What became of the daemon this record describes: the verdict `run.json`'s pid cannot give on
|
|
11294
|
+
* its own. See `processLiveness` in `providers/port.ts` for why the pid is not an identity.
|
|
11295
|
+
*/
|
|
11296
|
+
async function runtimeLiveness(runtime) {
|
|
11297
|
+
return await processLiveness(runtime.pid, runtime.startedAt);
|
|
11298
|
+
}
|
|
11299
|
+
/**
|
|
11300
|
+
* Is the daemon this record describes still the one running?
|
|
11301
|
+
*
|
|
11302
|
+
* `readRuntime()` answers "does a record exist", never "is the panel behind it alive" — and a pid
|
|
11303
|
+
* alone cannot answer that either. The OS reuses pids, and a zombie still answers signal 0, so a
|
|
11304
|
+
* bare `isProcessAlive(runtime.pid)` reported a stale `run.json` as a live panel: `up` refused with
|
|
11305
|
+
* "already running", `status` said "running, but not answering", and `down` signalled whatever
|
|
11306
|
+
* unrelated process had since inherited the pid.
|
|
11307
|
+
*
|
|
11308
|
+
* Every caller must read this one predicate, because a caller that re-derives it slightly differently
|
|
11309
|
+
* is how a `down` comes to signal a stranger.
|
|
11310
|
+
*/
|
|
11311
|
+
async function runtimeIsAlive(runtime) {
|
|
11312
|
+
return isLiveLiveness(await runtimeLiveness(runtime));
|
|
11313
|
+
}
|
|
11314
|
+
/**
|
|
11315
|
+
* Only `'live'` and `'unknown'` count as alive. `'unknown'` means the platform could not read a start
|
|
11316
|
+
* time, so the live pid is all there is — dismissing it would clear a record the running panel still
|
|
11317
|
+
* owns. `'recycled'` is emphatically **not** alive: the pid belongs to somebody else now.
|
|
11318
|
+
*/
|
|
11319
|
+
function isLiveLiveness(liveness) {
|
|
11320
|
+
return liveness === "live" || liveness === "unknown";
|
|
11321
|
+
}
|
|
11322
|
+
/**
|
|
11323
|
+
* Is the record positively pointing at nothing at all — no process behind the pid?
|
|
11324
|
+
*
|
|
11325
|
+
* This is the **only** condition under which deleting `run.json` is safe, so it is deliberately
|
|
11326
|
+
* narrower than `!runtimeIsAlive`. The start-time check can be wrong in one direction: Linux derives
|
|
11327
|
+
* a process's birth epoch from `btime`, which the kernel computes from the wall clock
|
|
11328
|
+
* (`getboottime64`: "calls to settimeofday will affect the value returned"), so a forward clock step
|
|
11329
|
+
* — common when a laptop or a WSL host wakes from sleep — makes a *live* panel look recycled. Deleting
|
|
11330
|
+
* its record over that would orphan a running panel: still supervising servers, no longer stoppable by
|
|
11331
|
+
* `down`. A record that some process may still own is left alone and simply overwritten when a new
|
|
11332
|
+
* panel finishes booting.
|
|
11333
|
+
*/
|
|
11334
|
+
async function runtimeIsGone(runtime) {
|
|
11335
|
+
return await runtimeLiveness(runtime) === "gone";
|
|
11336
|
+
}
|
|
11155
11337
|
/** A degraded panel is still answering: a 503 must not read as "not running". */
|
|
11156
11338
|
async function probeRuntime(runtime, timeoutMs = 2500) {
|
|
11157
11339
|
const answer = await localCall(runtime, "/healthz", "GET", void 0, timeoutMs);
|
|
@@ -11792,28 +11974,24 @@ function signalPid(pid, signal, killGroup) {
|
|
|
11792
11974
|
process.kill(pid, signal);
|
|
11793
11975
|
} catch {}
|
|
11794
11976
|
}
|
|
11795
|
-
function alive(pid) {
|
|
11796
|
-
try {
|
|
11797
|
-
process.kill(pid, 0);
|
|
11798
|
-
return true;
|
|
11799
|
-
} catch {
|
|
11800
|
-
return false;
|
|
11801
|
-
}
|
|
11802
|
-
}
|
|
11803
11977
|
/**
|
|
11804
11978
|
* The same shutdown a supervised child gets, for a process we adopted instead of
|
|
11805
11979
|
* spawned: a detached successor is not our child, so it is signalled by pid (and
|
|
11806
11980
|
* by process group when asked) and its exit is polled rather than awaited.
|
|
11981
|
+
*
|
|
11982
|
+
* Liveness comes from `isProcessAlive`, never a local signal-0 copy: a second definition returned
|
|
11983
|
+
* `false` for `EPERM`, so a process that was there but not ours to signal read as "exited" and
|
|
11984
|
+
* `'force-killed'` was returned for one still running. One predicate, one answer.
|
|
11807
11985
|
*/
|
|
11808
11986
|
async function terminatePid(pid, options) {
|
|
11809
|
-
if (!
|
|
11987
|
+
if (!isProcessAlive(pid)) return "exited";
|
|
11810
11988
|
signalPid(pid, options.signal, options.killGroup);
|
|
11811
11989
|
const deadline = Date.now() + Math.max(0, options.graceMs);
|
|
11812
|
-
while (Date.now() < deadline &&
|
|
11813
|
-
if (!
|
|
11990
|
+
while (Date.now() < deadline && isProcessAlive(pid)) await delay$1(50);
|
|
11991
|
+
if (!isProcessAlive(pid)) return "exited";
|
|
11814
11992
|
signalPid(pid, "SIGKILL", options.killGroup);
|
|
11815
11993
|
const hardDeadline = Date.now() + 2e3;
|
|
11816
|
-
while (Date.now() < hardDeadline &&
|
|
11994
|
+
while (Date.now() < hardDeadline && isProcessAlive(pid)) await delay$1(50);
|
|
11817
11995
|
return "force-killed";
|
|
11818
11996
|
}
|
|
11819
11997
|
function delay$1(ms) {
|
|
@@ -11837,6 +12015,7 @@ function waitForExit$1(child, timeoutMs) {
|
|
|
11837
12015
|
var execFileAsync$1, WINDOWS_SHIM_EXTENSIONS;
|
|
11838
12016
|
var init_process = __esmMin((() => {
|
|
11839
12017
|
init_paths();
|
|
12018
|
+
init_port();
|
|
11840
12019
|
execFileAsync$1 = promisify(execFile);
|
|
11841
12020
|
WINDOWS_SHIM_EXTENSIONS = [
|
|
11842
12021
|
".cmd",
|
|
@@ -12653,6 +12832,7 @@ var init_supervisor = __esmMin((() => {
|
|
|
12653
12832
|
nannyExit: null,
|
|
12654
12833
|
starting: false,
|
|
12655
12834
|
stopping: false,
|
|
12835
|
+
removed: false,
|
|
12656
12836
|
bootstrapDone: !config.bootstrap,
|
|
12657
12837
|
logs: new LogBuffer(config.logBufferLines),
|
|
12658
12838
|
echoed: /* @__PURE__ */ new Set(),
|
|
@@ -12671,6 +12851,7 @@ var init_supervisor = __esmMin((() => {
|
|
|
12671
12851
|
this.relay.unfollow(id);
|
|
12672
12852
|
this.options.logFiles.clear(id);
|
|
12673
12853
|
this.options.history.forget(id);
|
|
12854
|
+
entry.removed = true;
|
|
12674
12855
|
this.stopEntry(entry).catch((error) => {
|
|
12675
12856
|
logger.error(`could not stop the removed server ${id}`, error);
|
|
12676
12857
|
});
|
|
@@ -12856,7 +13037,7 @@ var init_supervisor = __esmMin((() => {
|
|
|
12856
13037
|
entry.responseMs = null;
|
|
12857
13038
|
entry.exitCode = null;
|
|
12858
13039
|
entry.exitSignal = null;
|
|
12859
|
-
this.options.history.record(entry.config.id, {
|
|
13040
|
+
if (!entry.removed) this.options.history.record(entry.config.id, {
|
|
12860
13041
|
type: "exit",
|
|
12861
13042
|
detail,
|
|
12862
13043
|
runtimeMs: ranForMs
|
|
@@ -13255,7 +13436,7 @@ var init_supervisor = __esmMin((() => {
|
|
|
13255
13436
|
at: Date.now()
|
|
13256
13437
|
});
|
|
13257
13438
|
const ranForMs = nannyExit?.runtimeMs ?? (entry.startedAt === null ? 0 : Date.now() - entry.startedAt);
|
|
13258
|
-
this.options.history.record(entry.config.id, {
|
|
13439
|
+
if (!entry.removed) this.options.history.record(entry.config.id, {
|
|
13259
13440
|
type: "exit",
|
|
13260
13441
|
detail,
|
|
13261
13442
|
runtimeMs: ranForMs
|
|
@@ -13446,6 +13627,10 @@ var init_supervisor = __esmMin((() => {
|
|
|
13446
13627
|
};
|
|
13447
13628
|
}
|
|
13448
13629
|
log(entry, stream, text) {
|
|
13630
|
+
if (entry.removed) {
|
|
13631
|
+
logger.debug(`[${entry.config.id}] ${text}`);
|
|
13632
|
+
return;
|
|
13633
|
+
}
|
|
13449
13634
|
const line = {
|
|
13450
13635
|
ts: Date.now(),
|
|
13451
13636
|
stream,
|
|
@@ -15525,6 +15710,7 @@ var init_log_files = __esmMin((() => {
|
|
|
15525
15710
|
return lines.slice(-tail);
|
|
15526
15711
|
}
|
|
15527
15712
|
clear(serverId) {
|
|
15713
|
+
this.pending.delete(serverId);
|
|
15528
15714
|
for (const file of this.rotateTargets(serverId)) try {
|
|
15529
15715
|
fs.rmSync(file, { force: true });
|
|
15530
15716
|
} catch {}
|
|
@@ -16906,11 +17092,11 @@ function probeHostFor(bindHost) {
|
|
|
16906
17092
|
async function runControlPlane(options) {
|
|
16907
17093
|
ensureLayout();
|
|
16908
17094
|
const existing = readRuntime();
|
|
16909
|
-
if (existing !== null && existing.pid !== process.pid &&
|
|
17095
|
+
if (existing !== null && existing.pid !== process.pid && await runtimeIsAlive(existing)) {
|
|
16910
17096
|
logger.error(`already running (pid ${existing.pid}) at ${existing.url} — run \`home-hosted down\` first`);
|
|
16911
17097
|
process.exit(1);
|
|
16912
17098
|
}
|
|
16913
|
-
if (existing !== null) clearRuntime();
|
|
17099
|
+
if (existing !== null && await runtimeIsGone(existing)) clearRuntime();
|
|
16914
17100
|
const settings = new GlobalSettingsStore();
|
|
16915
17101
|
settings.load();
|
|
16916
17102
|
settings.writeJsonSchema();
|
|
@@ -17265,17 +17451,17 @@ async function runUp(flags, entry) {
|
|
|
17265
17451
|
});
|
|
17266
17452
|
return;
|
|
17267
17453
|
}
|
|
17268
|
-
const { clearRuntime,
|
|
17454
|
+
const { clearRuntime, readRuntime, runtimeIsAlive, runtimeIsGone } = await Promise.resolve().then(() => (init_daemon(), daemon_exports));
|
|
17269
17455
|
const { daemonLogPath, dataRoot, projectDir } = await Promise.resolve().then(() => (init_paths(), paths_exports));
|
|
17270
17456
|
const existing = readRuntime();
|
|
17271
|
-
if (existing !== null &&
|
|
17457
|
+
if (existing !== null && await runtimeIsAlive(existing)) {
|
|
17272
17458
|
process.stdout.write(`${green("already running")} home-hosted ${existing.version} (pid ${existing.pid}) at ${existing.url}\n`);
|
|
17273
17459
|
const note = versionMismatchNote(existing.version);
|
|
17274
17460
|
if (note !== null) process.stdout.write(`${dim(note)}\n`);
|
|
17275
17461
|
process.stdout.write(`${dim("stop it with `home-hosted down`")}\n`);
|
|
17276
17462
|
return;
|
|
17277
17463
|
}
|
|
17278
|
-
if (existing !== null) clearRuntime();
|
|
17464
|
+
if (existing !== null && await runtimeIsGone(existing)) clearRuntime();
|
|
17279
17465
|
fs.mkdirSync(path.dirname(daemonLogPath), { recursive: true });
|
|
17280
17466
|
rotateLog(daemonLogPath);
|
|
17281
17467
|
const log = fs.openSync(daemonLogPath, "a");
|
|
@@ -17383,13 +17569,14 @@ var down_exports = /* @__PURE__ */ __exportAll({
|
|
|
17383
17569
|
});
|
|
17384
17570
|
/** `down` stops the panel and everything it supervises. */
|
|
17385
17571
|
async function runDown() {
|
|
17386
|
-
const { clearRuntime,
|
|
17572
|
+
const { clearRuntime, isLiveLiveness, readRuntime, requestShutdown, runtimeLiveness } = await Promise.resolve().then(() => (init_daemon(), daemon_exports));
|
|
17387
17573
|
const runtime = readRuntime();
|
|
17388
17574
|
if (runtime === null) {
|
|
17389
17575
|
process.stdout.write("home-hosted is not running\n");
|
|
17390
17576
|
return;
|
|
17391
17577
|
}
|
|
17392
|
-
|
|
17578
|
+
const liveness = await runtimeLiveness(runtime);
|
|
17579
|
+
if (liveness === "gone") {
|
|
17393
17580
|
clearRuntime();
|
|
17394
17581
|
process.stdout.write(`home-hosted is not running (removed a stale run.json from home-hosted ${runtime.version})\n`);
|
|
17395
17582
|
return;
|
|
@@ -17400,8 +17587,21 @@ async function runDown() {
|
|
|
17400
17587
|
const note = versionMismatchNote(runtime.version);
|
|
17401
17588
|
if (note !== null) process.stdout.write(`${dim(note)}\n`);
|
|
17402
17589
|
};
|
|
17403
|
-
|
|
17404
|
-
if (
|
|
17590
|
+
let answered = false;
|
|
17591
|
+
if (isLiveLiveness(liveness)) {
|
|
17592
|
+
process.stdout.write(`stopping pid ${runtime.pid}…\n`);
|
|
17593
|
+
answered = await requestShutdown(runtime);
|
|
17594
|
+
if (!answered) signal(runtime.pid, "SIGTERM");
|
|
17595
|
+
} else {
|
|
17596
|
+
if (!await requestShutdown(runtime)) {
|
|
17597
|
+
process.stdout.write(`home-hosted is not running (the pid in run.json, ${runtime.pid}, belongs to another process — it was not signalled)\n`);
|
|
17598
|
+
return;
|
|
17599
|
+
}
|
|
17600
|
+
process.stdout.write(`stopped the panel on ${runtime.url} (its pid in run.json, ${runtime.pid}, was another process and was left alone)\n`);
|
|
17601
|
+
clearRuntime();
|
|
17602
|
+
await reportPersistent();
|
|
17603
|
+
return;
|
|
17604
|
+
}
|
|
17405
17605
|
if (await waitForExit(runtime.pid, 2e4)) {
|
|
17406
17606
|
clearRuntime();
|
|
17407
17607
|
reportStopped(false);
|
|
@@ -17490,10 +17690,10 @@ var server_exports = /* @__PURE__ */ __exportAll({
|
|
|
17490
17690
|
stopCommand: () => stopCommand
|
|
17491
17691
|
});
|
|
17492
17692
|
async function runServerAction(action, id, workspace) {
|
|
17493
|
-
const { clearRuntime,
|
|
17693
|
+
const { clearRuntime, readRuntime, requestControl, runtimeIsAlive, runtimeIsGone } = await Promise.resolve().then(() => (init_daemon(), daemon_exports));
|
|
17494
17694
|
const runtime = readRuntime();
|
|
17495
|
-
if (runtime === null || !
|
|
17496
|
-
if (runtime !== null) clearRuntime();
|
|
17695
|
+
if (runtime === null || !await runtimeIsAlive(runtime)) {
|
|
17696
|
+
if (runtime !== null && await runtimeIsGone(runtime)) clearRuntime();
|
|
17497
17697
|
fail("home-hosted is not running — start it with `home-hosted up`");
|
|
17498
17698
|
}
|
|
17499
17699
|
const query = workspace === void 0 || workspace.length === 0 ? "" : `?workspace=${encodeURIComponent(workspace)}`;
|
|
@@ -17590,7 +17790,7 @@ var status_exports = /* @__PURE__ */ __exportAll({
|
|
|
17590
17790
|
statusCommand: () => statusCommand
|
|
17591
17791
|
});
|
|
17592
17792
|
async function runStatus(json) {
|
|
17593
|
-
const {
|
|
17793
|
+
const { isLiveLiveness, probeRuntime, readRuntime, runtimeLiveness } = await Promise.resolve().then(() => (init_daemon(), daemon_exports));
|
|
17594
17794
|
const { UiService } = await Promise.resolve().then(() => (init_ui(), ui_exports));
|
|
17595
17795
|
const { hhDir: dataRoot, workspaceDir, DEFAULT_WORKSPACE_ID, workspacesPath } = await Promise.resolve().then(() => (init_paths(), paths_exports));
|
|
17596
17796
|
const runtime = readRuntime();
|
|
@@ -17605,7 +17805,8 @@ async function runStatus(json) {
|
|
|
17605
17805
|
process.exitCode = 1;
|
|
17606
17806
|
return;
|
|
17607
17807
|
}
|
|
17608
|
-
const
|
|
17808
|
+
const liveness = await runtimeLiveness(runtime);
|
|
17809
|
+
const running = isLiveLiveness(liveness);
|
|
17609
17810
|
const probe = running ? await probeRuntime(runtime) : {
|
|
17610
17811
|
reachable: false,
|
|
17611
17812
|
degraded: false
|
|
@@ -17617,6 +17818,7 @@ async function runStatus(json) {
|
|
|
17617
17818
|
const { token: _token, ...safe } = runtime;
|
|
17618
17819
|
process.stdout.write(`${JSON.stringify({
|
|
17619
17820
|
running,
|
|
17821
|
+
liveness,
|
|
17620
17822
|
answering: probe.reachable,
|
|
17621
17823
|
degraded: probe.degraded,
|
|
17622
17824
|
...safe,
|
|
@@ -17632,7 +17834,7 @@ async function runStatus(json) {
|
|
|
17632
17834
|
}
|
|
17633
17835
|
const uptime = formatDuration(Date.now() - runtime.startedAt);
|
|
17634
17836
|
const rows = [
|
|
17635
|
-
["status", !running ? paint("31", "stale (the process is gone)") : probe.degraded ? paint("33", "running — a server needs attention") : probe.reachable ? green("running") : paint("33", "running, but not answering")],
|
|
17837
|
+
["status", !running ? liveness === "recycled" ? paint("31", "stale (the pid belongs to another process now)") : paint("31", "stale (the process is gone)") : probe.degraded ? paint("33", "running — a server needs attention") : probe.reachable ? green("running") : paint("33", "running, but not answering")],
|
|
17636
17838
|
["pid", running ? `${runtime.pid} · up ${uptime}` : String(runtime.pid)],
|
|
17637
17839
|
["url", `${runtime.url} ${dim(`(${runtime.protocol})`)}`],
|
|
17638
17840
|
["version", runtime.version],
|