home-hosted 0.5.0 → 0.6.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/docs/SERVERS.md CHANGED
@@ -26,7 +26,7 @@ servers and its state together.
26
26
  | `command`, `args`, `cwd` | what to run, with `{placeholders}` resolved per entry |
27
27
  | `env`, `dataEnvs`, `envFile` | environment; `dataEnvs` also marks data directories for backups, `envFile` keeps secrets out of the config |
28
28
  | `port`, `bind` | enables the readiness wait, health checks and the conflict preflight; `local` keeps it on `127.0.0.1` |
29
- | `onPortConflict` | `block` (default), `warn`, `follow`, or `reclaim` — see below |
29
+ | `onPortConflict` | `block` (default), `warn`, `follow`, `reclaim`, or `kill` — see below |
30
30
  | `health.mode` | `port` (TCP connect) or `http` (path, expected status, expected body) |
31
31
  | `health.unhealthyThreshold`, `forceRestartAfterMs` | how many failed probes before the card warns, and when to restart anyway |
32
32
  | `restart.*` | backoff: `maxRetries`, `baseDelayMs`, `factor`, `maxDelayMs`, `resetAfterMs` |
@@ -66,6 +66,7 @@ Preflight runs before every start, so two servers cannot silently fight over one
66
66
  | `warn` | it starts anyway — useful when the listener is a leftover you are replacing |
67
67
  | `follow` | if the listener is a **detached restart of this same entry**, the panel adopts it as-is; anything else blocks, exactly like `block` |
68
68
  | `reclaim` | same detection, but it stops that successor and starts a fully supervised process of its own |
69
+ | `kill` | stops **whatever holds the port** — no ownership test — then starts. The blunt one: see below |
69
70
 
70
71
  ### When a program restarts itself
71
72
 
@@ -74,14 +75,32 @@ update, a `re-exec` on config change. The panel used to see the successor's port
74
75
  there blocked while the service was actually up.
75
76
 
76
77
  Every process gets `HHOSTED_SERVER_ID` in its environment and a successor inherits it, so the preflight
77
- can tell a successor from a stranger (Linux `/proc`, macOS `ps -E`; Windows has no per-process
78
- environment, so there you get the conflict banner and the free-port button). Two policies act on that:
78
+ can tell a successor from a stranger — read from `/proc` on Linux and `ps -E` on macOS. **Windows
79
+ cannot read another process's environment**, so there the entry's own argv answers instead: a process
80
+ holding the port is the successor when its image and its arguments are both the entry's own. That
81
+ fallback also covers a Linux or macOS program that restarted itself in a way that dropped the marker.
82
+ Two policies act on the result:
79
83
 
80
84
  | policy | what it does | trade-off |
81
85
  | --- | --- | --- |
82
86
  | `follow` | adopts the successor: pid, health probe, CPU/RSS, stop, and it starts its own process again when the successor exits | **keeps exactly what the program set up**, but its output is not captured — the pipe belongs to whoever spawned it, so that entry's log in the panel goes quiet |
83
87
  | `reclaim` | stops the successor, then starts a fully supervised process of its own | **full features** — live logs, resources, stop semantics all behave like any other entry — at the cost of one restart |
84
88
 
89
+ Identification is deliberately strict, because `reclaim` kills what it identifies: the image has to be
90
+ the entry's resolved command **and** the arguments have to be the entry's own, compared literally — an
91
+ argument sharing a basename with another program's is not a match. When **more than one** holder
92
+ matches, the panel refuses to guess and blocks, naming the pids. These are the cases that still block
93
+ rather than being identified:
94
+
95
+ - a successor that re-execs under a **different image**, or rewrites its own arguments;
96
+ - an argument read back ambiguously — on macOS `ps` joins argv with spaces, so an argument that itself
97
+ contains a space cannot be told from two arguments;
98
+ - a Windows entry launched through an `npm`/`pnpm` **`.cmd` shim**: the shim runs under `cmd.exe`, so
99
+ the process actually holding the port is `node.exe` and its argv never mentions the shim;
100
+ - a Windows entry whose `CommandLine` PowerShell cannot read.
101
+
102
+ `kill` exists precisely for those cases: it does not need to identify anything.
103
+
85
104
  Both are shown as **detached** in the panel while adopted, both can be stopped and restarted like any
86
105
  other entry, and neither will ever start a second copy on top of a **stranger**: that still blocks,
87
106
  exactly like `block`.
@@ -91,9 +110,32 @@ is discovered:
91
110
 
92
111
  ```text
93
112
  port 4374 is already in use (pid 912) — pid 912 is a detached restart of this entry:
94
- set onPortConflict to "follow" to adopt it, or "reclaim" to replace it with a supervised process
113
+ set onPortConflict to "follow" to adopt it, "reclaim" to replace it with a supervised process,
114
+ or "kill" to stop whatever holds the port
95
115
  ```
96
116
 
117
+ ### The `kill` policy
118
+
119
+ `follow` and `reclaim` need to know *whose* port it is. When they cannot answer that — a program that
120
+ re-execs under a different image, or two holders that both look like the entry — `kill` skips the
121
+ question:
122
+
123
+ 1. SIGTERM every listener on the port, escalating to SIGKILL after `stop.graceMs`.
124
+ 2. Wait for the port to actually accept nothing, then start as usual.
125
+ 3. If the port is somehow still held, the entry goes to **conflict** with the pids it tried —
126
+ it does **not** retry in a loop.
127
+
128
+ It is the one policy that will stop a **stranger** whose port you have configured, so the log says
129
+ which pids it stopped and why. It will not touch the panel itself or any process the panel
130
+ supervises: a port held by a sibling entry is a config mistake and still blocks, exactly as it does
131
+ under `block`. If it is a stray leftover you would rather inspect first, the **Free port** button on
132
+ the server card does the same thing on demand.
133
+
134
+ Because a detached successor is not something the panel supervises, `kill` ends up doing what
135
+ `reclaim` does for your own restarted process — stops it and starts a supervised child — without ever
136
+ asking whether it was yours. Only `follow` ever adopts, and only `reclaim` will refuse to act on a
137
+ holder it cannot prove is yours.
138
+
97
139
  ## Editing fields
98
140
 
99
141
  Changes from the panel are atomic and validated before they are written. Editing the file by hand —
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "home-hosted",
3
3
  "type": "module",
4
- "version": "0.5.0",
4
+ "version": "0.6.0",
5
5
  "packageManager": "pnpm@12.5.1",
6
6
  "description": "A self-hosted control panel that keeps your home server processes alive - with a UI you can replace.",
7
7
  "author": "NamesMT <dangquoctrung123@gmail.com>",
@@ -33,7 +33,8 @@
33
33
  },
34
34
  "main": "./dist/cli.js",
35
35
  "bin": {
36
- "home-hosted": "bin/home-hosted.mjs"
36
+ "home-hosted": "bin/home-hosted.mjs",
37
+ "hh": "bin/home-hosted.mjs"
37
38
  },
38
39
  "files": [
39
40
  "AGENTS.md",