dsh-home-hosted 0.7.5 → 0.7.6
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/docs/DESIGN.md +1 -1
- package/docs/PANEL.md +10 -1
- package/lib/index.js +43 -26
- package/lib/index.js.map +2 -2
- package/lib/types/home-hosted/resolve.d.ts +1 -1
- package/lib/types/service.d.ts +9 -0
- package/package.json +2 -2
package/docs/DESIGN.md
CHANGED
|
@@ -38,7 +38,7 @@ its page.
|
|
|
38
38
|
`onPortConflict: kill` arrived in home-hosted 0.6.0 and `persistent` in 0.6.3, and an older panel
|
|
39
39
|
handed either key can refuse to *boot* from the config. The plugin used to consult the answering
|
|
40
40
|
panel's version and refuse or drop them (`KILL_UNSUPPORTED`). That is gone on purpose: it pins
|
|
41
|
-
`home-hosted@^0.7.
|
|
41
|
+
`home-hosted@^0.7.20` and autostarts its own copy, so a panel older than those keys is only reachable by
|
|
42
42
|
deliberately preferring an old global install — and this is pre-1.0. Supporting older panels again means
|
|
43
43
|
bringing that check back from history, not re-deriving it.
|
|
44
44
|
|
package/docs/PANEL.md
CHANGED
|
@@ -38,7 +38,16 @@ a `null` at all — exactly the two whose schema is `… | null` — while the p
|
|
|
38
38
|
drift was real but latent; it is fixed because a copy of the panel's function is a fork of its
|
|
39
39
|
semantics, and the next field to allow `null` would have made it reachable.
|
|
40
40
|
|
|
41
|
-
|
|
41
|
+
**0.7.20 is one fix, and it found a live bug here.** It teaches that a pid is not an identity: the OS
|
|
42
|
+
recycles pids and a zombie still answers signal 0, so the panel's `up`/`status`/`down` now decide with
|
|
43
|
+
the pid *plus* `run.json`'s `startedAt`, and `down` refuses to signal a pid it cannot prove is the
|
|
44
|
+
panel. This plugin's two generated hand-over helpers had re-derived the weak predicate and escalated
|
|
45
|
+
`SIGTERM`→`SIGKILL` on "the pid answers" — **measured** by running the generated script against a live
|
|
46
|
+
`sleep` holding the recorded pid, and watching the stranger's own `SIGTERM` handler fire. A takeover
|
|
47
|
+
could therefore kill an unrelated process. Both helpers now leave every signal to the CLI's own `down`,
|
|
48
|
+
which is the only component that can prove identity; what they kept is a wait, never a signal.
|
|
49
|
+
|
|
50
|
+
Verified against a real 0.7.20 panel and its own CLI: `PATCH {port:null}` deletes the key; only `port`
|
|
42
51
|
and `bootstrap` have a schema that admits `null`; `_hh` is still exactly four routes; and every route,
|
|
43
52
|
subcommand and flag this plugin drives is unchanged. The console **file** — `.hh/.logs/home-hosted.log`
|
|
44
53
|
plus its `.1`, and the 5 MB rotation — is unchanged too, and this plugin's reader still matches
|
package/lib/index.js
CHANGED
|
@@ -2863,7 +2863,7 @@ import { createRequire } from "node:module";
|
|
|
2863
2863
|
import fs11 from "node:fs";
|
|
2864
2864
|
import path9 from "node:path";
|
|
2865
2865
|
import process5 from "node:process";
|
|
2866
|
-
var EXPECTED_RANGE = "^0.7.
|
|
2866
|
+
var EXPECTED_RANGE = "^0.7.20";
|
|
2867
2867
|
var MIN_SUPPORTED_VERSION = "0.7.0";
|
|
2868
2868
|
function pinnedManifestPath() {
|
|
2869
2869
|
try {
|
|
@@ -4229,18 +4229,27 @@ log('stopping the answering panel: ' + STOP.join(' '))
|
|
|
4229
4229
|
const stopped = spawnSync(STOP[0], STOP.slice(1), { stdio: 'ignore', timeout: 90000 })
|
|
4230
4230
|
log('down exited ' + String(stopped.status ?? stopped.error))
|
|
4231
4231
|
|
|
4232
|
-
|
|
4233
|
-
|
|
4234
|
-
|
|
4232
|
+
/**
|
|
4233
|
+
* Never signal OLD_PID from here.
|
|
4234
|
+
*
|
|
4235
|
+
* The CLI's own 'down' is the only thing that may stop the panel, and it already escalates
|
|
4236
|
+
* (SIGTERM, then SIGKILL) for a panel it can prove is alive. A pid is not an identity: the OS
|
|
4237
|
+
* recycles pids, so the number in run.json may belong to an unrelated process, and this helper
|
|
4238
|
+
* has no birth time to tell them apart. Signalling on "the pid answers" is how a takeover
|
|
4239
|
+
* SIGTERMs a stranger \u2014 measured, before this guard existed.
|
|
4240
|
+
*
|
|
4241
|
+
* 'down' succeeds on both real cases, so there is usually nothing to wait for: it returns once
|
|
4242
|
+
* the panel exited, and since home-hosted 0.7.20 the start below succeeds even when the recorded
|
|
4243
|
+
* pid was recycled (its 'up' overwrites a record it cannot prove is live), where earlier releases
|
|
4244
|
+
* refused with "already running". The wait is kept for a 'down' that did not report success, and
|
|
4245
|
+
* it never signals.
|
|
4246
|
+
*/
|
|
4247
|
+
if (stopped.status !== 0 && alive(OLD_PID)) {
|
|
4248
|
+
log('down did not report success and the recorded pid is still alive; waiting, and NOT signalling it \u2014 it may not be the panel any more')
|
|
4235
4249
|
for (let i = 0; i < 40 && alive(OLD_PID); i += 1)
|
|
4236
4250
|
await sleep(250)
|
|
4237
|
-
|
|
4238
|
-
|
|
4239
|
-
if (alive(OLD_PID)) {
|
|
4240
|
-
log('still alive; sending SIGKILL')
|
|
4241
|
-
try { process.kill(OLD_PID, 'SIGKILL') } catch {}
|
|
4242
|
-
for (let i = 0; i < 20 && alive(OLD_PID); i += 1)
|
|
4243
|
-
await sleep(250)
|
|
4251
|
+
if (alive(OLD_PID))
|
|
4252
|
+
log('the recorded pid is still alive after waiting; the start below may be refused')
|
|
4244
4253
|
}
|
|
4245
4254
|
|
|
4246
4255
|
for (const step of STEPS) {
|
|
@@ -4295,22 +4304,22 @@ log('stopping the answering panel: ' + PROGRAM + ' ' + DOWN_ARGS.join(' '))
|
|
|
4295
4304
|
const down = spawnSync(PROGRAM, DOWN_ARGS, { env: { ...process.env, ...ENV }, stdio: 'ignore' })
|
|
4296
4305
|
log('down exited ' + String(down.status))
|
|
4297
4306
|
|
|
4298
|
-
|
|
4307
|
+
/**
|
|
4308
|
+
* Never signal OLD_PID from here \u2014 see the same guard in the activation helper.
|
|
4309
|
+
*
|
|
4310
|
+
* 'down' is the only thing that may stop the panel, and it escalates for a panel it can prove is
|
|
4311
|
+
* alive. This helper has no birth time, so "the pid answers" cannot tell the panel from a stranger
|
|
4312
|
+
* that inherited the number; signalling on it SIGTERMs an unrelated process.
|
|
4313
|
+
*
|
|
4314
|
+
* A takeover of a live panel ends with the panel stopped either way: 'down' stops it, and it
|
|
4315
|
+
* escalates itself when the panel ignores SIGTERM. A pid that is still alive after a successful
|
|
4316
|
+
* 'down' is therefore not the panel, and the start below must not race it.
|
|
4317
|
+
*/
|
|
4318
|
+
for (let i = 0; i < 40 && down.status !== 0 && alive(OLD_PID); i += 1)
|
|
4299
4319
|
await sleep(250)
|
|
4300
4320
|
|
|
4301
|
-
if (alive(OLD_PID))
|
|
4302
|
-
log('still alive;
|
|
4303
|
-
try { process.kill(OLD_PID, 'SIGTERM') } catch {}
|
|
4304
|
-
for (let i = 0; i < 40 && alive(OLD_PID); i += 1)
|
|
4305
|
-
await sleep(250)
|
|
4306
|
-
}
|
|
4307
|
-
|
|
4308
|
-
if (alive(OLD_PID)) {
|
|
4309
|
-
log('still alive; sending SIGKILL')
|
|
4310
|
-
try { process.kill(OLD_PID, 'SIGKILL') } catch {}
|
|
4311
|
-
for (let i = 0; i < 20 && alive(OLD_PID); i += 1)
|
|
4312
|
-
await sleep(250)
|
|
4313
|
-
}
|
|
4321
|
+
if (down.status !== 0 && alive(OLD_PID))
|
|
4322
|
+
log('down did not report success and the recorded pid is still alive; NOT signalling it \u2014 it may not be the panel any more')
|
|
4314
4323
|
|
|
4315
4324
|
log('starting ' + PROGRAM + ' ' + ARGS.join(' '))
|
|
4316
4325
|
const result = spawnSync(PROGRAM, ARGS, { env: { ...process.env, ...ENV }, stdio: 'ignore' })
|
|
@@ -5171,7 +5180,6 @@ var HomeHostedService = class extends Service {
|
|
|
5171
5180
|
const runtime = this.runtime();
|
|
5172
5181
|
const stored = readStoredToken(this.options.stateDir);
|
|
5173
5182
|
const enrolledOnDisk = apiTokenEnrolled(this.options.home);
|
|
5174
|
-
const reachable = runtime !== null && (pidAlive(runtime.pid) || await probePanel(runtime.url));
|
|
5175
5183
|
const answered = runtime !== null && await probePanel(runtime.url);
|
|
5176
5184
|
let token = "unknown";
|
|
5177
5185
|
let tokenVerified = false;
|
|
@@ -5397,6 +5405,15 @@ var HomeHostedService = class extends Service {
|
|
|
5397
5405
|
}
|
|
5398
5406
|
writeConfig(this.options.home, upsertEntry(raw, entry), this.writtenBy(), workspace);
|
|
5399
5407
|
}
|
|
5408
|
+
/**
|
|
5409
|
+
* Is a panel up and watching this workspace's config?
|
|
5410
|
+
*
|
|
5411
|
+
* Deliberately still `pid alive OR answering`, unlike the status read: this only chooses between
|
|
5412
|
+
* one write and two, and a live pid errs toward the *cautious* two-step write (add disabled, then
|
|
5413
|
+
* flip autostart) for a panel too wedged to answer — whose watcher may still fire and would start
|
|
5414
|
+
* a new autostart entry at once. A recycled pid therefore costs a redundant second write, which is
|
|
5415
|
+
* the harmless direction; the harmful one was signalling it, which no helper here does any more.
|
|
5416
|
+
*/
|
|
5400
5417
|
async panelRunning() {
|
|
5401
5418
|
const runtime = this.runtime();
|
|
5402
5419
|
return runtime !== null && (pidAlive(runtime.pid) || await probePanel(runtime.url));
|