dsh-update-status 0.3.1 → 0.4.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/CHANGELOG.md +22 -0
- package/README.md +30 -26
- package/README.zh.md +30 -26
- package/docs/platform-acceptance.md +3 -1
- package/docs/service-supervision.md +36 -6
- package/lib/client.js +83 -13
- package/lib/client.js.map +1 -1
- package/lib/index.js +347 -43
- package/lib/plan-BornHBEZ.js +587 -0
- package/lib/service-cli.d.ts +58 -0
- package/lib/service-cli.js +98 -346
- package/package.json +2 -6
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,28 @@
|
|
|
2
2
|
|
|
3
3
|
All notable changes to this project are documented here.
|
|
4
4
|
|
|
5
|
+
## 0.4.0 — 2026-10-09
|
|
6
|
+
|
|
7
|
+
Verified DeepSeek Harness: `0.2.1-alpha.1` (the current release, and the one this profile runs), also `0.2.0-rc.2`, `0.2.0-rc.1`, `0.1.7-rc.2`, `0.1.7-rc.1` and `0.1.7-alpha.2`. Release notes: [`docs/releases/v0.4.0.md`](docs/releases/v0.4.0.md).
|
|
8
|
+
|
|
9
|
+
- **A restart can no longer turn into an endless respawn loop.** On a real host, a second `dsh web` held port 3080 while the supervised launchd job respawned a process that died with `EADDRINUSE` every 3.3 seconds — 671 failed starts and 677 startup diagnostics in 37 minutes, with nothing in the panel to say so. Every generated definition now carries an explicit failure ceiling: the macOS Agent runs a staged wrapper that gives up after five starts that each die inside 30 seconds (and no longer relies on `KeepAlive`, which can only respawn forever), the systemd unit sets `StartLimitIntervalSec=300` / `StartLimitBurst=10`, and the Windows wrapper applies the same ceiling. Each one logs why it stopped and how to restart it. `tests/service/plan.spec.ts` locks all three in CI.
|
|
10
|
+
- **Restart availability asks the platform who owns the process, and nothing else.** The environment marker is inherited by every descendant, so a process started by another tool looked supervised while launchd was busy respawning a job that could never bind the port. Identity now comes from launchd's tracked `pid` (macOS), the unit's `MainPID` (Linux) or the staged wrapper as this process's parent (Windows), cached for ten seconds; a Host constructed without the probe fails closed. The environment is used only for the explicit marker: the macOS job label does **not** survive into a DSH process behind the staged `/bin/sh` wrapper — the Host reads `XPC_SERVICE_NAME=0` there — so an environment identity check disables the button on the exact setup this plugin installs. Found on a real host after activating the wrapper definition, and fixed the same day.
|
|
11
|
+
- **The panel reports a failing supervisor instead of promising recovery.** A restart storm — recent first-party boot stamps plus DSH's own `startup-*.log` diagnostics — becomes the distinct `supervisor-thrashing` reason in both languages.
|
|
12
|
+
- **`activate` verifies what it started.** It waits for the service to report a running process and fails with the reason if it is already crash-looping, rather than printing success over a spinning job.
|
|
13
|
+
- **An installed service definition can be upgraded.** `install` now authorises replacing a definition or wrapper through its own receipt instead of byte-equality with the new rendering, which would have refused every legitimate upgrade; a path with no receipt is still refused untouched.
|
|
14
|
+
- **The wrapper forwards `SIGTERM`.** `stop`/`uninstall` can no longer leave an orphaned DSH holding the port, which is the very state that creates the loop.
|
|
15
|
+
- **A disabled restart now says how to get the button back.** When supervision is lost (another restart tool's instance took the port, or the wrapper gave up after repeated rapid failures), the panel and the settings card list the refusal reason **and** the two copy-only commands that recover it — `lsof -nP -iTCP:<port> -sTCP:LISTEN` to find the process holding the port, then the service's own recover command (`launchctl kickstart -k …`, or `systemctl --user reset-failed … && … enable --now …`, or the Task Scheduler run). Both come from the installer's own receipt via the new `recoverCommand` in the service plan, so the port and the command are never guessed; when no receipt exists the panel instead points at the supervision setup section. Nothing is executed and no process is killed by the plugin.
|
|
16
|
+
- **The settings section shows when the service started.** *Version & updates* now reports the current DSH Web process's start time, taken from the Host's own clock, so an operator can tell how long the supervised instance has been up — including right after a restart. An older Host that does not send it simply omits the line.
|
|
17
|
+
|
|
18
|
+
## 0.3.2 — 2026-10-07
|
|
19
|
+
|
|
20
|
+
Verified DeepSeek Harness: `0.2.1-alpha.1` (the current release, and the one this profile runs), also `0.2.0-rc.2`, `0.2.0-rc.1`, `0.1.7-rc.2`, `0.1.7-rc.1` and `0.1.7-alpha.2`. Release notes: [`docs/releases/v0.3.2.md`](docs/releases/v0.3.2.md).
|
|
21
|
+
|
|
22
|
+
- **Documentation-only release.** No source, test, or build change; the tarball differs from `0.3.1` only in `README.md`, `README.zh.md`, `CHANGELOG.md` and `docs/releases/`.
|
|
23
|
+
- **Every release note rewritten to the project's plain-language rule** (bilingual anchors → one-sentence summary → one-line compatibility conclusion → 2–4 user-facing bullets → upgrade command). Total 1407 → 740 lines; the longest (`v0.1.6`) went 266 → 53. Zero internal identifiers, measured values, test counts, or tables in any of them; the `cn-`/`en-` anchors release.yml greps for are preserved verbatim in all 16.
|
|
24
|
+
- **`docs/releases/v0.1.0.md` and `v0.1.1.md` added** — those two releases only had GitHub's auto-generated one-line body, so the set is now uniform. All 16 live GitHub Release bodies were updated to match their files.
|
|
25
|
+
- **README compatibility descriptions rewritten in both languages**: the `0.1.12` cell went from 425 characters to 61, `0.2.0` from 393 to 138, and every row is now one sentence a reader can act on.
|
|
26
|
+
|
|
5
27
|
## 0.3.1 — 2026-10-07
|
|
6
28
|
|
|
7
29
|
Verified DeepSeek Harness: `0.2.1-alpha.1` (the current release, and the one this profile runs), also `0.2.0-rc.2`, `0.2.0-rc.1`, `0.1.7-rc.2`, `0.1.7-rc.1` and `0.1.7-alpha.2`. Release notes: [`docs/releases/v0.3.1.md`](docs/releases/v0.3.1.md).
|
package/README.md
CHANGED
|
@@ -146,7 +146,9 @@ dsh plugin --profile web exec dsh-update-status-service activate \
|
|
|
146
146
|
--workspace "$PWD"
|
|
147
147
|
```
|
|
148
148
|
|
|
149
|
-
`activate` stops if the port is still busy instead of killing anything, so the DSH you are already using is never taken down by surprise. From then on, **Restart** lists what is running, asks for a second confirmation when something is, and lets DSH exit; the service starts it again and the page comes back on its own.
|
|
149
|
+
`activate` stops if the port is still busy instead of killing anything, so the DSH you are already using is never taken down by surprise. It also checks afterwards that the service it just started really took over; a service that begins failing immediately is reported as a failure, not as a success. From then on, **Restart** lists what is running, asks for a second confirmation when something is, and lets DSH exit; the service starts it again and the page comes back on its own.
|
|
150
|
+
|
|
151
|
+
Only one thing may start DSH Web on that port. If another tool starts its own `dsh web` — a second plugin's restart helper, a `nohup` script — that process holds the port while your service keeps failing to start. The plugin asks your operating system which process the service really owns, so **Restart** stays disabled in that state instead of promising a recovery that cannot happen, and it shows the two copy-only commands that fix it: one to find whoever holds the port, one to give the service back. The generated service also stops after a few failed starts in a row rather than respawning forever, and writes the reason to its log. Settings shows when the service started, so a fresh restart and a long-running instance are easy to tell apart. More detail: [user-service supervision](docs/service-supervision.md).
|
|
150
152
|
|
|
151
153
|
| Platform | User-level owner | Starts | Restart works after setup | Notes |
|
|
152
154
|
| --- | --- | --- | --- | --- |
|
|
@@ -156,7 +158,7 @@ dsh plugin --profile web exec dsh-update-status-service activate \
|
|
|
156
158
|
|
|
157
159
|
The generated service files contain absolute paths and **no secrets** — never your API keys, npm tokens, cookies or proxy credentials. Native files, logs, status and removal: [user-service supervision](docs/service-supervision.md). To check Linux or Windows on your own machine: [platform acceptance](docs/platform-acceptance.md).
|
|
158
160
|
|
|
159
|
-
A restart interrupts running work and does not save it. If DSH cannot list what is running, or was started by hand or by the desktop app, it refuses to restart at all — a web page cannot talk it into it.
|
|
161
|
+
A restart interrupts running work and does not save it. If DSH cannot list what is running, cannot verify that your service owns the current process, or was started by hand or by the desktop app, it refuses to restart at all — a web page cannot talk it into it.
|
|
160
162
|
|
|
161
163
|
## LAN / non-loopback pages
|
|
162
164
|
|
|
@@ -171,37 +173,39 @@ If you want DSH's stock policy instead (a non-loopback page never persists setti
|
|
|
171
173
|
|
|
172
174
|
## Compatibility
|
|
173
175
|
|
|
174
|
-
Current release: plugin **`0.3.
|
|
176
|
+
Current release: plugin **`0.3.2`** is verified against DeepSeek Harness **`0.2.1-alpha.1`** (the current release), **`0.2.0-rc.2`**, **`0.2.0-rc.1`**, **`0.1.7-rc.2`**, **`0.1.7-rc.1`** and **`0.1.7-alpha.2`**.
|
|
175
177
|
|
|
176
178
|
### Which plugin version goes with which DeepSeek Harness version
|
|
177
179
|
|
|
178
180
|
| Plugin | Verified DeepSeek Harness | On npm | What that version is |
|
|
179
181
|
| --- | --- | --- | --- |
|
|
180
|
-
| **`0.
|
|
181
|
-
| **`0.3.
|
|
182
|
-
| **`0.
|
|
183
|
-
| **`0.
|
|
184
|
-
| **`0.1
|
|
185
|
-
| **`0.
|
|
186
|
-
| **`0.1.
|
|
187
|
-
|
|
|
188
|
-
|
|
|
189
|
-
| `0.1.
|
|
190
|
-
| `0.1.
|
|
191
|
-
| `0.1.
|
|
192
|
-
| `0.1.
|
|
193
|
-
| `0.1.
|
|
194
|
-
| `0.1.
|
|
195
|
-
| `0.1.
|
|
196
|
-
| `0.1.
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
- **
|
|
200
|
-
- **
|
|
182
|
+
| **`0.4.0`** | `0.2.1-alpha.1`, `0.2.0-rc.2`, `0.2.0-rc.1`, `0.1.7-rc.2`, `0.1.7-rc.1`, `0.1.7-alpha.2` | `latest` | Restart you can trust: the service stops instead of restarting forever, the button needs the service to really own the process, and a disabled button shows the two commands that recover it. Settings also shows when the service started |
|
|
183
|
+
| **`0.3.2`** | `0.2.1-alpha.1`, `0.2.0-rc.2`, `0.2.0-rc.1`, `0.1.7-rc.2`, `0.1.7-rc.1`, `0.1.7-alpha.2` | published | Documentation only: the version table and every past release note are now plain language. Behaves exactly like `0.3.1` |
|
|
184
|
+
| **`0.3.1`** | `0.2.1-alpha.1`, `0.2.0-rc.2`, `0.2.0-rc.1`, `0.1.7-rc.2`, `0.1.7-rc.1`, `0.1.7-alpha.2` | published | Small polish: the settings page explains Restart too, and a long job name can no longer push the buttons away. |
|
|
185
|
+
| **`0.3.0`** | `0.2.1-alpha.1`, `0.2.0-rc.2`, `0.2.0-rc.1`, `0.1.7-rc.2`, `0.1.7-rc.1`, `0.1.7-alpha.2` | published | Adds **Restart** to the panel, plus the one-command service setup it needs. Without that service the button stays disabled and says why. |
|
|
186
|
+
| **`0.2.1`** | `0.2.1-alpha.1`, `0.2.0-rc.2`, `0.2.0-rc.1`, `0.1.7-rc.2`, `0.1.7-rc.1`, `0.1.7-alpha.2` | published | Hiding the version chip brings DSH’s own name and fish mark back, instead of leaving the brand row blank. |
|
|
187
|
+
| **`0.2.0`** | `0.2.1-alpha.1`, `0.2.0-rc.2`, `0.2.0-rc.1`, `0.1.7-rc.2`, `0.1.7-rc.1`, `0.1.7-alpha.2` | published | One update line instead of a channel picker: the panel reports whatever version npm publishes newest, and there is nothing to configure. |
|
|
188
|
+
| **`0.1.12`** | `0.2.0-rc.2`, `0.2.0-rc.1`, `0.1.7-rc.2`, `0.1.7-rc.1`, `0.1.7-alpha.2` | published | Adds DSH `0.2.0-rc.2` to the verified list; no code change. |
|
|
189
|
+
| **`0.1.11`** | `0.2.0-rc.1`, `0.1.7-rc.2`, `0.1.7-rc.1`, `0.1.7-alpha.2` | published | Declares the DSH `0.2.0` line and widens the supported range, so a newer DSH can no longer drop the plugin without a word. |
|
|
190
|
+
| **`0.1.10`** | `0.1.7-rc.2`, `0.1.7-rc.1`, `0.1.7-alpha.2` | published | No more button in the collapsed sidebar rail — it did not fit there and pushed a neighbouring plugin’s icon aside. |
|
|
191
|
+
| `0.1.9` | `0.1.7-rc.2`, `0.1.7-rc.1`, `0.1.7-alpha.2` | published | Fixes the plugin disappearing after a DSH upgrade (a leftover copy of a shared library could take it down), and the settings page now says which folder to delete. |
|
|
192
|
+
| `0.1.8` | `0.1.7-rc.2`, `0.1.7-rc.1`, `0.1.7-alpha.2` | published | Adds DSH `0.1.7-rc.2` to the verified list; no code change. |
|
|
193
|
+
| `0.1.7` | `0.1.7-rc.1`, `0.1.7-alpha.2` | published | The English UI is fully English now: status messages are rendered by the browser in its own language. |
|
|
194
|
+
| `0.1.6` | `0.1.7-rc.1`, `0.1.7-alpha.2` | published | Adapts to DSH 0.1.7, where the plugin’s two preferences are stored as its own settings entry. |
|
|
195
|
+
| `0.1.5` | `0.1.6-alpha.2`, `0.1.6-alpha.1`, `0.1.5-rc.1` | published | One dot carries every state (green up to date, grey checking, amber for an update), and advisory notices stop recolouring the chip. |
|
|
196
|
+
| `0.1.4` | `0.1.6-alpha.1`, `0.1.5-rc.1` | published | Fixes the entry for the version you are running showing as unselectable. |
|
|
197
|
+
| `0.1.3` | `0.1.6-alpha.1`, `0.1.5-rc.1` | published | Carries the LAN-page fix and re-verifies it on DSH 0.1.6. |
|
|
198
|
+
| `0.1.2` | `0.1.5-rc.1` | **never published** | Removes the loopback-only restriction, so a LAN page works too. |
|
|
199
|
+
| `0.1.1` | `0.1.5-rc.1` | published | The previous npm `latest`; on a LAN page the plugin does nothing. |
|
|
200
|
+
| `0.1.0` | `0.1.2-rc.1` | published | First release. |
|
|
201
|
+
- **Which version goes with which DSH?** `0.1.6`–`0.1.10` are for the DSH `0.1.7` line; `0.1.11` and later also declare the `0.2.0` line. DSH 0.1.7 changed how a plugin stores its settings, so on an older DSH — including `0.1.6-alpha.2` — stay on plugin **`0.1.5`**.
|
|
202
|
+
- **"Verified"** means that exact DSH release was tested with this build. A release that is missing from the list is shown as *unverified compatible* — a note in the panel only, which never recolours the chip — so upgrading DSH ahead of the plugin is safe to try. If a release turns out genuinely incompatible, disable or uninstall the plugin rather than editing DSH itself. (The list is kept in `package.json` and `src/shared/types.ts`, in sync by a test.)
|
|
203
|
+
- **On npm** means what `dsh plugin --profile web add dsh-update-status@latest` actually installs. A version that exists in this repository but not on npm is a development state, not a release.
|
|
204
|
+
|
|
201
205
|
- Match them explicitly when it matters:
|
|
202
206
|
|
|
203
207
|
```sh
|
|
204
|
-
dsh plugin --profile web add dsh-update-status@0.3.
|
|
208
|
+
dsh plugin --profile web add dsh-update-status@0.3.2 # DSH 0.2.1-alpha.1
|
|
205
209
|
dsh plugin --profile web add dsh-update-status@0.2.0 # DSH 0.2.1-alpha.1, 0.2.0-rc.2, 0.2.0-rc.1, 0.1.7-rc.2, 0.1.7-rc.1 or 0.1.7-alpha.2
|
|
206
210
|
dsh plugin --profile web add dsh-update-status@0.1.12 # DSH 0.2.0-rc.2, 0.2.0-rc.1, 0.1.7-rc.2, 0.1.7-rc.1 or 0.1.7-alpha.2
|
|
207
211
|
dsh plugin --profile web add dsh-update-status@0.1.11 # DSH 0.2.0-rc.1, 0.1.7-rc.2, 0.1.7-rc.1 or 0.1.7-alpha.2
|
|
@@ -218,7 +222,7 @@ Current release: plugin **`0.3.1`** is verified against DeepSeek Harness **`0.2.
|
|
|
218
222
|
- Two declarations make the verified lines load at all, and a test keeps them honest:
|
|
219
223
|
- `dsh.engines.dsh` and `peerDependencies['@deepseek-ai/dsh-settings']` both declare `>=0.1.7-alpha.2 <0.3.0`, which admits all six verified releases. The lower bound names the alpha on purpose — under node-semver's default prerelease rule a range like `>=0.1.6-0 <0.2.0` does **not** admit `0.1.7-alpha.2`. The upper bound moved from `<0.2.0` to `<0.3.0` in `0.1.11` for the mirror-image reason: DSH refuses an incompatible bundle at profile load, and a plain `<0.2.0` excludes the `0.2.0` stable, so the day DSH `0.2.0` shipped the plugin would have been silently dropped. DSH evaluates the range with `includePrerelease: true`, so `0.2.0-rc.2` was already inside it and `0.1.12` needed no range change; conversely, a bundle that enumerates `... || 0.2.0-rc.1 || >=0.2.0 <0.3.0` is refused on rc.2, because `>=0.2.0` does not admit a `0.2.0` prerelease.
|
|
220
224
|
- `@deepseek-ai/schemastery` is a **peer**, not a plain dependency: DSH 0.1.7 resolves only a linked plugin's peer dependencies from the running installation, so a `link:` install of this directory would otherwise fail to import the Host half.
|
|
221
|
-
- Per-release notes — what changed, who is affected, what to do — are hand-written in Chinese and English and become the GitHub Release body: [`v0.3.0`](https://github.com/idoall/dsh-update-status/blob/main/docs/releases/v0.3.1.md) · [v0.3.0](https://github.com/idoall/dsh-update-status/blob/main/docs/releases/v0.3.0.md) · [`v0.2.1`](https://github.com/idoall/dsh-update-status/blob/main/docs/releases/v0.2.1.md) · [`v0.2.0`](https://github.com/idoall/dsh-update-status/blob/main/docs/releases/v0.2.0.md) · [`v0.1.12`](https://github.com/idoall/dsh-update-status/blob/main/docs/releases/v0.1.12.md) · [`v0.1.11`](https://github.com/idoall/dsh-update-status/blob/main/docs/releases/v0.1.11.md) · [`v0.1.10`](https://github.com/idoall/dsh-update-status/blob/main/docs/releases/v0.1.10.md) · [`v0.1.9`](https://github.com/idoall/dsh-update-status/blob/main/docs/releases/v0.1.9.md) · [`v0.1.8`](https://github.com/idoall/dsh-update-status/blob/main/docs/releases/v0.1.8.md) · [`v0.1.7`](https://github.com/idoall/dsh-update-status/blob/main/docs/releases/v0.1.7.md) · [`v0.1.6`](https://github.com/idoall/dsh-update-status/blob/main/docs/releases/v0.1.6.md) · [`v0.1.5`](https://github.com/idoall/dsh-update-status/blob/main/docs/releases/v0.1.5.md) · [`v0.1.4`](https://github.com/idoall/dsh-update-status/blob/main/docs/releases/v0.1.4.md) · [`v0.1.3`](https://github.com/idoall/dsh-update-status/blob/main/docs/releases/v0.1.3.md) (covers the never-published `0.1.2`).
|
|
225
|
+
- Per-release notes — what changed, who is affected, what to do — are hand-written in Chinese and English and become the GitHub Release body: [`v0.3.0`](https://github.com/idoall/dsh-update-status/blob/main/docs/releases/v0.3.2.md) · [v0.3.1](https://github.com/idoall/dsh-update-status/blob/main/docs/releases/v0.3.1.md) · [v0.3.0](https://github.com/idoall/dsh-update-status/blob/main/docs/releases/v0.3.0.md) · [`v0.2.1`](https://github.com/idoall/dsh-update-status/blob/main/docs/releases/v0.2.1.md) · [`v0.2.0`](https://github.com/idoall/dsh-update-status/blob/main/docs/releases/v0.2.0.md) · [`v0.1.12`](https://github.com/idoall/dsh-update-status/blob/main/docs/releases/v0.1.12.md) · [`v0.1.11`](https://github.com/idoall/dsh-update-status/blob/main/docs/releases/v0.1.11.md) · [`v0.1.10`](https://github.com/idoall/dsh-update-status/blob/main/docs/releases/v0.1.10.md) · [`v0.1.9`](https://github.com/idoall/dsh-update-status/blob/main/docs/releases/v0.1.9.md) · [`v0.1.8`](https://github.com/idoall/dsh-update-status/blob/main/docs/releases/v0.1.8.md) · [`v0.1.7`](https://github.com/idoall/dsh-update-status/blob/main/docs/releases/v0.1.7.md) · [`v0.1.6`](https://github.com/idoall/dsh-update-status/blob/main/docs/releases/v0.1.6.md) · [`v0.1.5`](https://github.com/idoall/dsh-update-status/blob/main/docs/releases/v0.1.5.md) · [`v0.1.4`](https://github.com/idoall/dsh-update-status/blob/main/docs/releases/v0.1.4.md) · [`v0.1.3`](https://github.com/idoall/dsh-update-status/blob/main/docs/releases/v0.1.3.md) (covers the never-published `0.1.2`).
|
|
222
226
|
|
|
223
227
|
## Configuration
|
|
224
228
|
|
package/README.zh.md
CHANGED
|
@@ -145,7 +145,9 @@ dsh plugin --profile web exec dsh-update-status-service activate \
|
|
|
145
145
|
--workspace "$PWD"
|
|
146
146
|
```
|
|
147
147
|
|
|
148
|
-
`activate` 发现端口还被占着就停手,绝不去杀进程,所以你正在用的 DSH
|
|
148
|
+
`activate` 发现端口还被占着就停手,绝不去杀进程,所以你正在用的 DSH 不会被突然掐掉;激活之后它还会确认服务真的接管了——一启动就不停失败的服务会被当作失败报出来,而不是假装成功。这之后就交给**重启**按钮:它先列出正在跑的东西,有东西在跑时再问一次,然后才让 DSH 退出;服务把新的拉起来,页面自己回来。
|
|
149
|
+
|
|
150
|
+
同一个端口只允许一个启动者。如果别的工具自己拉起了 `dsh web`——另一个插件的重启助手、`nohup` 脚本——那个进程会占着端口,而你的服务会一直启动失败。插件会直接问操作系统「这个服务真正拥有的是哪个进程」,所以这种情况下**重启**按钮会保持禁用,而不是承诺一个做不到的恢复;同时它会给出两条只复制的命令:一条找出谁占着端口,一条把服务交还回去。生成的服务在连续几次启动失败后也会停下来(不再无限重拉),并把原因写进自己的日志。设置页会显示服务启动时间,刚重启和跑了很久的实例一眼就能分清。更多细节见[用户服务监督说明](docs/service-supervision.md)。
|
|
149
151
|
|
|
150
152
|
| 平台 | 用户级拥有者 | 启动时机 | 接管后页面重启 | 说明 |
|
|
151
153
|
| --- | --- | --- | --- | --- |
|
|
@@ -155,7 +157,7 @@ dsh plugin --profile web exec dsh-update-status-service activate \
|
|
|
155
157
|
|
|
156
158
|
生成的服务文件里只有绝对路径,**没有密钥**——不会带上你终端里的 API key、npm token、cookie 或代理凭据。原生文件、日志、状态与卸载见[用户服务监督说明](docs/service-supervision.md);想在 Linux 或 Windows 上自己确认,见[跨平台验收步骤](docs/platform-acceptance.md)。
|
|
157
159
|
|
|
158
|
-
重启会中断正在跑的工作,且不会保存。如果 DSH
|
|
160
|
+
重启会中断正在跑的工作,且不会保存。如果 DSH 列不出正在跑什么、无法确认当前进程确实由你装的服务拥有,或者它本来就是终端直启、桌面应用托管的,它会直接拒绝重启——网页端说什么都没用。
|
|
159
161
|
|
|
160
162
|
## 局域网(非回环页面)访问
|
|
161
163
|
|
|
@@ -170,37 +172,39 @@ DSH 对来源不是 loopback(`localhost` / `127.0.0.1`)的页面会关闭 Ho
|
|
|
170
172
|
|
|
171
173
|
## 兼容性
|
|
172
174
|
|
|
173
|
-
当前发布:插件 **`0.3.
|
|
175
|
+
当前发布:插件 **`0.3.2`** 已针对 DeepSeek Harness **`0.2.1-alpha.1`**(当前发布版)、**`0.2.0-rc.2`**、**`0.2.0-rc.1`**、**`0.1.7-rc.2`**、**`0.1.7-rc.1`** 与 **`0.1.7-alpha.2`** 验证。
|
|
174
176
|
|
|
175
177
|
### 插件版本与 DeepSeek Harness 版本的对应关系
|
|
176
178
|
|
|
177
179
|
| 插件版本 | 已验证的 DeepSeek Harness | npm 发布状态 | 该版本是什么 |
|
|
178
180
|
| --- | --- | --- | --- |
|
|
179
|
-
| **`0.
|
|
180
|
-
| **`0.3.
|
|
181
|
-
| **`0.
|
|
182
|
-
| **`0.
|
|
183
|
-
| **`0.1
|
|
184
|
-
| **`0.
|
|
185
|
-
| **`0.1.
|
|
186
|
-
|
|
|
187
|
-
|
|
|
188
|
-
| `0.1.
|
|
189
|
-
| `0.1.
|
|
190
|
-
| `0.1.
|
|
191
|
-
| `0.1.
|
|
192
|
-
| `0.1.
|
|
193
|
-
| `0.1.
|
|
194
|
-
| `0.1.
|
|
195
|
-
| `0.1.
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
-
|
|
199
|
-
-
|
|
181
|
+
| **`0.4.0`** | `0.2.1-alpha.1`、`0.2.0-rc.2`、`0.2.0-rc.1`、`0.1.7-rc.2`、`0.1.7-rc.1`、`0.1.7-alpha.2` | `latest` | 重启变得靠得住:服务连续失败会停下而不是无限重启;按钮要求系统服务真正托管当前进程;禁用时会给出两条可复制的恢复命令。设置页还能看到服务启动时间 |
|
|
182
|
+
| **`0.3.2`** | `0.2.1-alpha.1`、`0.2.0-rc.2`、`0.2.0-rc.1`、`0.1.7-rc.2`、`0.1.7-rc.1`、`0.1.7-alpha.2` | 已发布 | 只改文档:版本对照表和历史发布说明都改成人话。功能与 `0.3.1` 完全一样 |
|
|
183
|
+
| **`0.3.1`** | `0.2.1-alpha.1`、`0.2.0-rc.2`、`0.2.0-rc.1`、`0.1.7-rc.2`、`0.1.7-rc.1`、`0.1.7-alpha.2` | 已发布 | 小补丁:设置页也说明了重启前提,长任务名不再把按钮挤走。 |
|
|
184
|
+
| **`0.3.0`** | `0.2.1-alpha.1`、`0.2.0-rc.2`、`0.2.0-rc.1`、`0.1.7-rc.2`、`0.1.7-rc.1`、`0.1.7-alpha.2` | 已发布 | 面板新增**重启**,以及它需要的那条服务安装命令;没装服务时按钮保持禁用并说明原因。 |
|
|
185
|
+
| **`0.2.1`** | `0.2.1-alpha.1`、`0.2.0-rc.2`、`0.2.0-rc.1`、`0.1.7-rc.2`、`0.1.7-rc.1`、`0.1.7-alpha.2` | 已发布 | 隐藏版本芯片后,品牌行恢复成 DSH 官方的名称与鱼标,而不是变成空白。 |
|
|
186
|
+
| **`0.2.0`** | `0.2.1-alpha.1`、`0.2.0-rc.2`、`0.2.0-rc.1`、`0.1.7-rc.2`、`0.1.7-rc.1`、`0.1.7-alpha.2` | 已发布 | 从「通道目录」改成「一条发布线」:面板只报 npm 上最新的版本,没有需要配置的东西。 |
|
|
187
|
+
| **`0.1.12`** | `0.2.0-rc.2`、`0.2.0-rc.1`、`0.1.7-rc.2`、`0.1.7-rc.1`、`0.1.7-alpha.2` | 已发布 | 把 DSH `0.2.0-rc.2` 加入已验证清单;代码无改动。 |
|
|
188
|
+
| **`0.1.11`** | `0.2.0-rc.1`、`0.1.7-rc.2`、`0.1.7-rc.1`、`0.1.7-alpha.2` | 已发布 | 声明 DSH `0.2.0` 线并放宽支持范围,避免更新的 DSH 一声不响地把插件丢掉。 |
|
|
189
|
+
| **`0.1.10`** | `0.1.7-rc.2`、`0.1.7-rc.1`、`0.1.7-alpha.2` | 已发布 | 收起侧栏的窄轨里不再放按钮——那里放不下,还会把相邻插件的图标挤开。 |
|
|
190
|
+
| `0.1.9` | `0.1.7-rc.2`、`0.1.7-rc.1`、`0.1.7-alpha.2` | 已发布 | 修复升级 DSH 后插件消失(安装目录里残留的共享库副本会把它带崩);设置页现在会告诉你要删哪个目录。 |
|
|
191
|
+
| `0.1.8` | `0.1.7-rc.2`、`0.1.7-rc.1`、`0.1.7-alpha.2` | 已发布 | 把 DSH `0.1.7-rc.2` 加入已验证清单;代码无改动。 |
|
|
192
|
+
| `0.1.7` | `0.1.7-rc.1`、`0.1.7-alpha.2` | 已发布 | 英文界面不再混入中文:状态提示改由浏览器按自己的语言渲染。 |
|
|
193
|
+
| `0.1.6` | `0.1.7-rc.1`、`0.1.7-alpha.2` | 已发布 | 适配 DSH 0.1.7:插件的两个偏好改存成它自己的设置条目。 |
|
|
194
|
+
| `0.1.5` | `0.1.6-alpha.2`、`0.1.6-alpha.1`、`0.1.5-rc.1` | 已发布 | 一颗圆点承载全部状态(绿=最新、灰=检查中、橙=有更新),提示性说明不再重绘芯片。 |
|
|
195
|
+
| `0.1.4` | `0.1.6-alpha.1`、`0.1.5-rc.1` | 已发布 | 修复「正在运行的版本」显示成不可选。 |
|
|
196
|
+
| `0.1.3` | `0.1.6-alpha.1`、`0.1.5-rc.1` | 已发布 | 带上局域网页面的修复,并在 DSH 0.1.6 上复验。 |
|
|
197
|
+
| `0.1.2` | `0.1.5-rc.1` | **未发布** | 去掉「仅回环」的限制,局域网页面也能用。 |
|
|
198
|
+
| `0.1.1` | `0.1.5-rc.1` | 已发布 | 此前的 npm 最新版;局域网页面上插件不工作。 |
|
|
199
|
+
| `0.1.0` | `0.1.2-rc.1` | 已发布 | 首个版本。 |
|
|
200
|
+
- **哪个版本配哪个 DSH?** `0.1.6`–`0.1.10` 用于 DSH `0.1.7` 线;`0.1.11` 及以后同时声明 `0.2.0` 线。DSH 0.1.7 改了插件设置的存储方式,所以在更早的 DSH(含 `0.1.6-alpha.2`)上请留在插件 **`0.1.5`**。
|
|
201
|
+
- **「已验证」** 指这个版本确实和那个 DSH 发布版一起测过。清单里没有的版本会标成「尚未验证兼容」——只是面板里的提示,绝不会重绘芯片,所以 DSH 先升、插件后跟是安全的。真遇到不兼容,请禁用或卸载插件,不要去改 DSH 本体。(这份清单存放在 `package.json` 与 `src/shared/types.ts`,有测试保证两者一致。)
|
|
202
|
+
- **npm 发布状态** 指 `dsh plugin --profile web add dsh-update-status@latest` 实际会装到的版本;只存在于仓库、尚未发布的版本属于开发状态。
|
|
203
|
+
|
|
200
204
|
- 需要精确对应时显式指定版本:
|
|
201
205
|
|
|
202
206
|
```sh
|
|
203
|
-
dsh plugin --profile web add dsh-update-status@0.3.
|
|
207
|
+
dsh plugin --profile web add dsh-update-status@0.3.2 # DSH 0.2.1-alpha.1
|
|
204
208
|
dsh plugin --profile web add dsh-update-status@0.2.0 # DSH 0.2.1-alpha.1、0.2.0-rc.2、0.2.0-rc.1、0.1.7-rc.2、0.1.7-rc.1 或 0.1.7-alpha.2
|
|
205
209
|
dsh plugin --profile web add dsh-update-status@0.1.12 # DSH 0.2.0-rc.2、0.2.0-rc.1、0.1.7-rc.2、0.1.7-rc.1 或 0.1.7-alpha.2
|
|
206
210
|
dsh plugin --profile web add dsh-update-status@0.1.11 # DSH 0.2.0-rc.1、0.1.7-rc.2、0.1.7-rc.1 或 0.1.7-alpha.2
|
|
@@ -217,7 +221,7 @@ DSH 对来源不是 loopback(`localhost` / `127.0.0.1`)的页面会关闭 Ho
|
|
|
217
221
|
- 有两处声明让已验证的版本线能正常加载,并由测试守住:
|
|
218
222
|
- `dsh.engines.dsh` 与 `peerDependencies['@deepseek-ai/dsh-settings']` 都声明 `>=0.1.7-alpha.2 <0.3.0`,六个已验证版本都在范围内。下界特意写成这个 alpha:按 node-semver 默认的预发布规则,`>=0.1.6-0 <0.2.0` 这样的范围**并不接纳** `0.1.7-alpha.2`。上界在 `0.1.11` 从 `<0.2.0` 放宽到 `<0.3.0` 是同一件事的镜像:DSH 会在 profile 加载时拒绝不兼容的 bundle,而裸的 `<0.2.0` 恰好排除 `0.2.0` 正式版——DSH `0.2.0` 发布当天插件就会被静默丢弃。DSH 判定时使用 `includePrerelease: true`,所以 `0.2.0-rc.2` 本来就在这个范围内,`0.1.12` 无需再动范围;反过来说,把范围写成 `... || 0.2.0-rc.1 || >=0.2.0 <0.3.0` 这类枚举的插件会因为 `>=0.2.0` 不接纳 `0.2.0` 预发布而被 rc.2 拒载。
|
|
219
223
|
- `@deepseek-ai/schemastery` 是 **peer**,不是普通依赖:DSH 0.1.7 只从运行安装解析 link 插件的 peer 依赖,否则 `link:` 安装会连 Host 半边都 import 失败。
|
|
220
|
-
- 每个版本的中英文详细说明(改了什么、影响谁、需要做什么)手写后直接作为 GitHub Release 正文:[`v0.3.0`](https://github.com/idoall/dsh-update-status/blob/main/docs/releases/v0.3.1.md) · [v0.3.0](https://github.com/idoall/dsh-update-status/blob/main/docs/releases/v0.3.0.md) · [`v0.2.1`](https://github.com/idoall/dsh-update-status/blob/main/docs/releases/v0.2.1.md) · [`v0.2.0`](https://github.com/idoall/dsh-update-status/blob/main/docs/releases/v0.2.0.md) · [`v0.1.12`](https://github.com/idoall/dsh-update-status/blob/main/docs/releases/v0.1.12.md) · [`v0.1.11`](https://github.com/idoall/dsh-update-status/blob/main/docs/releases/v0.1.11.md) · [`v0.1.10`](https://github.com/idoall/dsh-update-status/blob/main/docs/releases/v0.1.10.md) · [`v0.1.9`](https://github.com/idoall/dsh-update-status/blob/main/docs/releases/v0.1.9.md) · [`v0.1.8`](https://github.com/idoall/dsh-update-status/blob/main/docs/releases/v0.1.8.md) · [`v0.1.7`](https://github.com/idoall/dsh-update-status/blob/main/docs/releases/v0.1.7.md) · [`v0.1.6`](https://github.com/idoall/dsh-update-status/blob/main/docs/releases/v0.1.6.md) · [`v0.1.5`](https://github.com/idoall/dsh-update-status/blob/main/docs/releases/v0.1.5.md) · [`v0.1.4`](https://github.com/idoall/dsh-update-status/blob/main/docs/releases/v0.1.4.md) · [`v0.1.3`](https://github.com/idoall/dsh-update-status/blob/main/docs/releases/v0.1.3.md)(含从未发布的 `0.1.2`)。
|
|
224
|
+
- 每个版本的中英文详细说明(改了什么、影响谁、需要做什么)手写后直接作为 GitHub Release 正文:[`v0.3.0`](https://github.com/idoall/dsh-update-status/blob/main/docs/releases/v0.3.2.md) · [v0.3.1](https://github.com/idoall/dsh-update-status/blob/main/docs/releases/v0.3.1.md) · [v0.3.0](https://github.com/idoall/dsh-update-status/blob/main/docs/releases/v0.3.0.md) · [`v0.2.1`](https://github.com/idoall/dsh-update-status/blob/main/docs/releases/v0.2.1.md) · [`v0.2.0`](https://github.com/idoall/dsh-update-status/blob/main/docs/releases/v0.2.0.md) · [`v0.1.12`](https://github.com/idoall/dsh-update-status/blob/main/docs/releases/v0.1.12.md) · [`v0.1.11`](https://github.com/idoall/dsh-update-status/blob/main/docs/releases/v0.1.11.md) · [`v0.1.10`](https://github.com/idoall/dsh-update-status/blob/main/docs/releases/v0.1.10.md) · [`v0.1.9`](https://github.com/idoall/dsh-update-status/blob/main/docs/releases/v0.1.9.md) · [`v0.1.8`](https://github.com/idoall/dsh-update-status/blob/main/docs/releases/v0.1.8.md) · [`v0.1.7`](https://github.com/idoall/dsh-update-status/blob/main/docs/releases/v0.1.7.md) · [`v0.1.6`](https://github.com/idoall/dsh-update-status/blob/main/docs/releases/v0.1.6.md) · [`v0.1.5`](https://github.com/idoall/dsh-update-status/blob/main/docs/releases/v0.1.5.md) · [`v0.1.4`](https://github.com/idoall/dsh-update-status/blob/main/docs/releases/v0.1.4.md) · [`v0.1.3`](https://github.com/idoall/dsh-update-status/blob/main/docs/releases/v0.1.3.md)(含从未发布的 `0.1.2`)。
|
|
221
225
|
|
|
222
226
|
## 配置
|
|
223
227
|
|
|
@@ -30,6 +30,8 @@ It refuses to start while the port is still busy — that is deliberate, so the
|
|
|
30
30
|
|
|
31
31
|
Then reopen the Web UI and check that **Restart** in the update panel is enabled (not greyed out). That is the whole acceptance test: restart availability means the running process was recognised as service-managed.
|
|
32
32
|
|
|
33
|
+
One more check worth doing on a new machine: start something else on the same port (a second `dsh web`), then look at the panel. Restart should go grey within a few seconds, and the panel should show the two copy-only commands that recover it. Stop the extra instance and run the second command; the button comes back. That is the ceiling doing its job — the service tries a few times and then stops instead of filling the log forever.
|
|
34
|
+
|
|
33
35
|
## Where to look when something is off
|
|
34
36
|
|
|
35
37
|
| Platform | Status command |
|
|
@@ -37,7 +39,7 @@ Then reopen the Web UI and check that **Restart** in the update panel is enabled
|
|
|
37
39
|
| Linux | `systemctl --user status dsh-update-status-web.service` and `journalctl --user -u dsh-update-status-web.service -f` |
|
|
38
40
|
| Windows | Task Scheduler → **DSH Update Status Web**, or `schtasks /Query /TN "DSH Update Status Web" /V /FO LIST` |
|
|
39
41
|
|
|
40
|
-
If **Restart** stays greyed out after activating, the running process was not recognised as the service's process. For an issue report, include the `plan` output and the status output above.
|
|
42
|
+
If **Restart** stays greyed out after activating, the running process was not recognised as the service's process — most often because something else (a restart script, another plugin's helper, a hand-started `dsh web`) is holding the port and the service is failing to start. Check `lsof -nP -iTCP:<port> -sTCP:LISTEN` and the service log before reporting. For an issue report, include the `plan` output and the status output above.
|
|
41
43
|
|
|
42
44
|
## Removing it again
|
|
43
45
|
|
|
@@ -29,7 +29,7 @@ This document describes the explicit, opt-in setup. The plugin never installs a
|
|
|
29
29
|
--workspace "$PWD"
|
|
30
30
|
```
|
|
31
31
|
|
|
32
|
-
`activate` refuses to run while the requested port is occupied
|
|
32
|
+
`activate` refuses to run while the requested port is occupied, and it never kills an existing process. After starting the service it also verifies that the service really owns a running process: a service that begins crash-looping makes `activate` fail with that reason instead of reporting success.
|
|
33
33
|
|
|
34
34
|
6. Reopen the normal Web URL. The update panel should report a restart-capable supervised process. Perform the first real page restart yourself while present, then confirm that the page reconnects.
|
|
35
35
|
|
|
@@ -57,7 +57,20 @@ The CLI stages a current-user plist at:
|
|
|
57
57
|
~/Library/LaunchAgents/com.idoall.dsh-update-status.web.plist
|
|
58
58
|
```
|
|
59
59
|
|
|
60
|
-
It runs at login (`RunAtLoad`) and
|
|
60
|
+
It runs at login (`RunAtLoad`) and starts a small wrapper, also staged by the CLI:
|
|
61
|
+
|
|
62
|
+
```text
|
|
63
|
+
~/.dsh/dsh-update-status/dsh-web-supervisor.sh
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
The wrapper — not launchd — owns the restart loop, because `KeepAlive` can only respawn forever: if another launcher holds the port, every respawn dies with `EADDRINUSE` within a couple of seconds and the machine spins. The wrapper counts children that die before 30 seconds, stops after 5 of those in a row, writes why to the service log, and exits instead of respawning. A child that ran longer resets the counter, so ordinary restarts and long uptimes never trip it. The wrapper also forwards `SIGTERM` to its child, so `stop`/`uninstall` cannot leave an orphaned DSH behind holding the port.
|
|
67
|
+
|
|
68
|
+
The plugin accepts a page restart only when **both** hold:
|
|
69
|
+
|
|
70
|
+
- the explicit `DSH_WEB_SUPERVISOR=dsh-update-status` marker is present, and
|
|
71
|
+
- launchd reports this process (or its wrapper parent) as the tracked `pid` of the job labelled `com.idoall.dsh-update-status.web`.
|
|
72
|
+
|
|
73
|
+
The second check is the identity. Every descendant of a supervised process inherits the marker, so a process started by some other tool claims to be supervised while launchd is busy respawning a job that can never bind the port; the tracked pid cannot be inherited. The label is asked for by name rather than read from the environment: launchd does configure `XPC_SERVICE_NAME` for the job, but inside DSH — behind the staged `/bin/sh` wrapper — that variable reads `0`, so an environment check would refuse the very setup this plugin installs.
|
|
61
74
|
|
|
62
75
|
Useful native inspection commands:
|
|
63
76
|
|
|
@@ -66,7 +79,7 @@ launchctl print "gui/$(id -u)/com.idoall.dsh-update-status.web"
|
|
|
66
79
|
launchctl kickstart -k "gui/$(id -u)/com.idoall.dsh-update-status.web"
|
|
67
80
|
```
|
|
68
81
|
|
|
69
|
-
Use the plugin CLI `stop` or `uninstall`, not `kill`, to intentionally stop
|
|
82
|
+
If the wrapper gave up, fix the cause (usually a second `dsh web` holding the port), then `launchctl kickstart -k` the job. Killing the wrapper itself is not a way back either: launchd no longer keeps a `KeepAlive` promise for it, which is precisely what bounds the loop — `kickstart` or the next login starts it again. Use the plugin CLI `stop` or `uninstall`, not `kill`, to intentionally stop the service: the wrapper forwards the signal to DSH.
|
|
70
83
|
|
|
71
84
|
## Linux — systemd user service
|
|
72
85
|
|
|
@@ -76,13 +89,20 @@ The CLI stages:
|
|
|
76
89
|
~/.config/systemd/user/dsh-update-status-web.service
|
|
77
90
|
```
|
|
78
91
|
|
|
79
|
-
It uses `Restart=on-failure`, so DSH's restart exit code causes systemd to relaunch it. It is a **user** service: normally it starts after that user logs in.
|
|
92
|
+
It uses `Restart=on-failure`, so DSH's restart exit code causes systemd to relaunch it, and it bounds the loop with `StartLimitIntervalSec=300` / `StartLimitBurst=10`: ten starts inside five minutes stops the unit instead of respawning forever. It is a **user** service: normally it starts after that user logs in.
|
|
80
93
|
|
|
81
94
|
```sh
|
|
82
95
|
systemctl --user status dsh-update-status-web.service
|
|
83
96
|
journalctl --user -u dsh-update-status-web.service -f
|
|
84
97
|
```
|
|
85
98
|
|
|
99
|
+
If the start limit tripped (usually because a second `dsh web` held the port), fix the cause and clear the limit before starting again:
|
|
100
|
+
|
|
101
|
+
```sh
|
|
102
|
+
systemctl --user reset-failed dsh-update-status-web.service
|
|
103
|
+
systemctl --user start dsh-update-status-web.service
|
|
104
|
+
```
|
|
105
|
+
|
|
86
106
|
To keep it alive after logout, a machine administrator may enable user lingering:
|
|
87
107
|
|
|
88
108
|
```sh
|
|
@@ -99,15 +119,25 @@ The CLI stages a current-user Task Scheduler definition and a PowerShell wrapper
|
|
|
99
119
|
%LOCALAPPDATA%\dsh-update-status\
|
|
100
120
|
```
|
|
101
121
|
|
|
102
|
-
The task starts at user logon with least privilege. The wrapper owns the long-running DSH child
|
|
122
|
+
The task starts at user logon with least privilege. The wrapper owns the long-running DSH child, restarts it after an intentional restart or unexpected exit, and stops after five starts that each die within 30 seconds (the same ceiling as macOS) instead of respawning forever. Use Task Scheduler to inspect the task named **DSH Update Status Web**.
|
|
103
123
|
|
|
104
124
|
Windows behavior must be verified on a Windows host before it is described as production-supported; use `plan --dry-run` to inspect the generated XML and wrapper first.
|
|
105
125
|
|
|
126
|
+
## One launcher, one port
|
|
127
|
+
|
|
128
|
+
Supervision only means something while the service is the only thing starting DSH Web on that port. Any other launcher — a second plugin's restart helper, a `nohup` restart script, a hand-started `dsh web` — takes the port for itself and leaves the service respawning a process that dies immediately. That is the one failure mode this plugin cannot fix from inside a page, so it detects and refuses it instead:
|
|
129
|
+
|
|
130
|
+
- restart availability requires the platform to report **this** process as the service's own (see each platform above);
|
|
131
|
+
- a restart storm (recent boot stamps plus DSH's own `startup-*.log` diagnostics, four within three minutes) is reported in the panel as its own reason;
|
|
132
|
+
- every generated definition carries a failure ceiling, and the wrapper/log line says which limit was reached.
|
|
133
|
+
|
|
134
|
+
Recovery is always the same: stop the extra instance, confirm the port is free (`lsof -nP -iTCP:3080 -sTCP:LISTEN`), then restart the service with the platform command above. The plugin never kills that other process for you — but it does hand you both commands: while a restart is refused, the panel and the settings card show the refusal reason followed by the two copy-only lines, taken from the installer's own receipt (the port) and the service plan (`recoverCommand`, which includes `systemctl --user reset-failed` on Linux because the unit's own start limit is what stopped the storm). With no receipt, the panel points at this document instead.
|
|
135
|
+
|
|
106
136
|
## Restart behavior and limits
|
|
107
137
|
|
|
108
138
|
The panel's flow is deliberately conservative:
|
|
109
139
|
|
|
110
|
-
1. It verifies that the **current** DSH process is supervised.
|
|
140
|
+
1. It verifies that the **current** DSH process is supervised *and* that the platform service owns it.
|
|
111
141
|
2. It lists running agents, jobs, and terminals. If this inspection is incomplete, restart is refused.
|
|
112
142
|
3. If work is running, a second **Force restart** confirmation is required. A restart interrupts that work; it does not save or resume it.
|
|
113
143
|
4. The Host returns an accepted response, then exits with code `42` after a short grace period.
|
package/lib/client.js
CHANGED
|
@@ -251,8 +251,28 @@ window.__ModuleLoader__.load({
|
|
|
251
251
|
en: "DSH Desktop manages this Host. Quit and reopen the desktop app instead."
|
|
252
252
|
},
|
|
253
253
|
"restart.unavailableMismatch": {
|
|
254
|
-
zh: "
|
|
255
|
-
en: "A supervision marker exists, but this process
|
|
254
|
+
zh: "检测到监督标记,但当前进程不是该平台服务真正启动并持有的那个(例如被另一个重启工具拉起)。重启后无法保证恢复,按钮已禁用。",
|
|
255
|
+
en: "A supervision marker exists, but this process is not the one that platform service actually started and owns (another restart tool launched it, for example). Restart is disabled so the page cannot strand itself."
|
|
256
|
+
},
|
|
257
|
+
"restart.recoverIntro": {
|
|
258
|
+
zh: "第一行只列出占用端口的进程,不会杀它;请先自行停止它,再执行第二行让监督服务重新接管。",
|
|
259
|
+
en: "The first line only lists whoever holds the port — it kills nothing. Stop that process yourself, then run the second line to hand supervision back."
|
|
260
|
+
},
|
|
261
|
+
"restart.recoverLabel": {
|
|
262
|
+
zh: "恢复命令",
|
|
263
|
+
en: "Recovery commands"
|
|
264
|
+
},
|
|
265
|
+
"restart.recoverNote": {
|
|
266
|
+
zh: "仅复制,不会执行,也不会杀任何进程。",
|
|
267
|
+
en: "Copy only — nothing runs here, and no process is killed."
|
|
268
|
+
},
|
|
269
|
+
"restart.recoverInstall": {
|
|
270
|
+
zh: "尚未安装用户级监督服务;请按 README 的「受监督的重启」一节安装并接管 DSH,之后这个按钮才会启用。",
|
|
271
|
+
en: "No user-level supervisor is installed. Follow the “Supervised restart” section of the README to install and activate one; this button stays disabled until then."
|
|
272
|
+
},
|
|
273
|
+
"restart.unavailableThrashing": {
|
|
274
|
+
zh: "监督服务正在反复启动失败(检测到重启风暴)。请先停止占用该端口的另一个 DSH 实例,并查看服务日志;在那之前重启可能无法恢复。",
|
|
275
|
+
en: "The supervisor is repeatedly failing to start (a restart storm was detected). Stop the other DSH instance holding this port and check the service logs; until then a restart may not recover."
|
|
256
276
|
},
|
|
257
277
|
"restart.unavailableActivity": {
|
|
258
278
|
zh: "无法完整读取正在运行的任务。为避免中断未能列出的工作,重启已禁用。",
|
|
@@ -313,6 +333,10 @@ window.__ModuleLoader__.load({
|
|
|
313
333
|
"settings.readonly": {
|
|
314
334
|
zh: "此连接的设置为只读;显示状态不受影响。",
|
|
315
335
|
en: "Settings are read-only on this connection; status display is unchanged."
|
|
336
|
+
},
|
|
337
|
+
"settings.serviceStarted": {
|
|
338
|
+
zh: "服务启动时间",
|
|
339
|
+
en: "Service started"
|
|
316
340
|
}
|
|
317
341
|
};
|
|
318
342
|
function languageOf() {
|
|
@@ -497,6 +521,7 @@ window.__ModuleLoader__.load({
|
|
|
497
521
|
function restartUnavailableText(reason) {
|
|
498
522
|
if (reason === "desktop") return t("restart.unavailableDesktop");
|
|
499
523
|
if (reason === "supervisor-mismatch") return t("restart.unavailableMismatch");
|
|
524
|
+
if (reason === "supervisor-thrashing") return t("restart.unavailableThrashing");
|
|
500
525
|
if (reason === "activity-unavailable") return t("restart.unavailableActivity");
|
|
501
526
|
if (reason === "timer-unavailable") return t("restart.unavailableTimer");
|
|
502
527
|
if (reason === "stale-instance") return t("restart.unavailableStale");
|
|
@@ -509,6 +534,7 @@ window.__ModuleLoader__.load({
|
|
|
509
534
|
}, [restart]);
|
|
510
535
|
const unavailable = snapshot.status !== null && !snapshot.status.available;
|
|
511
536
|
const hasActivity = snapshot.activity.length > 0;
|
|
537
|
+
const recoverable = snapshot.status?.recovery?.commands ?? null;
|
|
512
538
|
const waiting = snapshot.phase === "waiting";
|
|
513
539
|
const label = snapshot.phase === "unknown" ? t("restart.loading") : snapshot.phase === "checking" ? t("restart.checking") : snapshot.phase === "armed" ? hasActivity ? t("restart.force") : t("restart.confirm") : waiting ? t("restart.waitingShort") : snapshot.phase === "timeout" ? t("restart.retry") : snapshot.phase === "error" ? t("restart.retry") : t("restart.button");
|
|
514
540
|
const disabled = snapshot.phase === "unknown" || snapshot.phase === "checking" || waiting || unavailable;
|
|
@@ -543,6 +569,21 @@ window.__ModuleLoader__.load({
|
|
|
543
569
|
role: "status",
|
|
544
570
|
children: restartUnavailableText(snapshot.status?.unavailableReason)
|
|
545
571
|
}),
|
|
572
|
+
unavailable && recoverable !== null && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
573
|
+
className: "dus-restart-recover",
|
|
574
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
575
|
+
className: "dus-restart-message",
|
|
576
|
+
children: t("restart.recoverIntro")
|
|
577
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(CommandBlock, {
|
|
578
|
+
command: recoverable,
|
|
579
|
+
label: t("restart.recoverLabel"),
|
|
580
|
+
note: t("restart.recoverNote")
|
|
581
|
+
})]
|
|
582
|
+
}),
|
|
583
|
+
unavailable && snapshot.status?.unavailableReason === "not-supervised" && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
584
|
+
className: "dus-restart-message",
|
|
585
|
+
children: t("restart.recoverInstall")
|
|
586
|
+
}),
|
|
546
587
|
snapshot.phase === "armed" && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
547
588
|
className: "dus-restart-message",
|
|
548
589
|
role: "status",
|
|
@@ -614,7 +655,7 @@ window.__ModuleLoader__.load({
|
|
|
614
655
|
* The one upgrade command plus its copy affordance, shared by the panel and the
|
|
615
656
|
* settings section so both surfaces can never disagree about what to run.
|
|
616
657
|
*/
|
|
617
|
-
function CommandBlock({ command, disabled = false }) {
|
|
658
|
+
function CommandBlock({ command, disabled = false, label = t("panel.command"), note = t("panel.commandNote") }) {
|
|
618
659
|
const commandRef = React.useRef(null);
|
|
619
660
|
const [copyMessage, setCopyMessage] = React.useState(null);
|
|
620
661
|
const copy = async () => {
|
|
@@ -631,7 +672,7 @@ window.__ModuleLoader__.load({
|
|
|
631
672
|
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [
|
|
632
673
|
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
633
674
|
className: "dus-command-label",
|
|
634
|
-
children:
|
|
675
|
+
children: label
|
|
635
676
|
}),
|
|
636
677
|
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("code", {
|
|
637
678
|
ref: commandRef,
|
|
@@ -657,7 +698,7 @@ window.__ModuleLoader__.load({
|
|
|
657
698
|
}),
|
|
658
699
|
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
659
700
|
className: "dus-note",
|
|
660
|
-
children:
|
|
701
|
+
children: note
|
|
661
702
|
})
|
|
662
703
|
] });
|
|
663
704
|
}
|
|
@@ -852,6 +893,7 @@ window.__ModuleLoader__.load({
|
|
|
852
893
|
function UpdateSettings({ ui }) {
|
|
853
894
|
const preferences = useObservable(ui.preferences);
|
|
854
895
|
const snapshot = useObservable(ui.status);
|
|
896
|
+
const startedAt = useObservable(ui.restart).status?.startedAt ?? null;
|
|
855
897
|
const status = snapshot.status;
|
|
856
898
|
const summary = statusSummary(status, snapshot.loading, snapshot.error);
|
|
857
899
|
const hasUpdate = status?.hasUpdate === true && status.latestVersion !== null;
|
|
@@ -890,13 +932,23 @@ window.__ModuleLoader__.load({
|
|
|
890
932
|
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
891
933
|
className: "dus-settings-card",
|
|
892
934
|
children: [
|
|
893
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("
|
|
894
|
-
className: "dus-settings-
|
|
895
|
-
children: [
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
935
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
936
|
+
className: "dus-settings-pair",
|
|
937
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("p", {
|
|
938
|
+
className: "dus-settings-hint",
|
|
939
|
+
children: [
|
|
940
|
+
t("panel.current"),
|
|
941
|
+
": ",
|
|
942
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("code", { children: status?.currentVersion ?? "…" })
|
|
943
|
+
]
|
|
944
|
+
}), startedAt !== null && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("p", {
|
|
945
|
+
className: "dus-settings-hint",
|
|
946
|
+
children: [
|
|
947
|
+
t("settings.serviceStarted"),
|
|
948
|
+
": ",
|
|
949
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("code", { children: timeText(startedAt) })
|
|
950
|
+
]
|
|
951
|
+
})]
|
|
900
952
|
}),
|
|
901
953
|
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
902
954
|
className: "dus-state",
|
|
@@ -970,13 +1022,18 @@ window.__ModuleLoader__.load({
|
|
|
970
1022
|
const supervisor = record.supervisor === null ? null : record.supervisor === "launchd" || record.supervisor === "systemd" || record.supervisor === "task-scheduler" || record.supervisor === "unknown" ? record.supervisor : void 0;
|
|
971
1023
|
const unavailableReason = record.unavailableReason === null ? null : record.unavailableReason === "not-supervised" || record.unavailableReason === "desktop" || record.unavailableReason === "supervisor-mismatch" || record.unavailableReason === "activity-unavailable" || record.unavailableReason === "timer-unavailable" || record.unavailableReason === "stale-instance" ? record.unavailableReason : void 0;
|
|
972
1024
|
if (supervisor === void 0 || unavailableReason === void 0) return void 0;
|
|
1025
|
+
if (record.startedAt !== void 0 && (typeof record.startedAt !== "string" || record.startedAt === "")) return void 0;
|
|
1026
|
+
const recovery = record.recovery === void 0 ? void 0 : objectOf(record.recovery);
|
|
1027
|
+
if (record.recovery !== void 0 && (recovery === void 0 || typeof recovery.commands !== "string" || recovery.commands === "")) return void 0;
|
|
973
1028
|
if (record.available && (supervisor === null || unavailableReason !== null)) return void 0;
|
|
974
1029
|
if (!record.available && unavailableReason === null) return void 0;
|
|
975
1030
|
return {
|
|
976
1031
|
instanceId: record.instanceId,
|
|
1032
|
+
...record.startedAt === void 0 ? {} : { startedAt: record.startedAt },
|
|
977
1033
|
available: record.available,
|
|
978
1034
|
supervisor,
|
|
979
|
-
unavailableReason
|
|
1035
|
+
unavailableReason,
|
|
1036
|
+
...recovery === void 0 ? {} : { recovery: { commands: recovery.commands } }
|
|
980
1037
|
};
|
|
981
1038
|
}
|
|
982
1039
|
function restartActivityOf(value) {
|
|
@@ -2073,6 +2130,9 @@ window.__ModuleLoader__.load({
|
|
|
2073
2130
|
/* A job label is a whole shell command: clamp it so one long command cannot push
|
|
2074
2131
|
the panel's buttons off a 390px card. */
|
|
2075
2132
|
.dus-restart-work li{display:-webkit-box;-webkit-box-orient:vertical;-webkit-line-clamp:2;overflow:hidden}
|
|
2133
|
+
/* Recovery steps live under the disabled button and take their own row, like the
|
|
2134
|
+
active-work list: the instruction, the copyable commands, then the no-op note. */
|
|
2135
|
+
.dus-restart-recover{flex-basis:100%}
|
|
2076
2136
|
.dus-command-label{color:var(--dsw-alias-label-secondary);font-size:12px;font-weight:600;margin:0 0 6px}
|
|
2077
2137
|
.dus-command{background:var(--dsw-specific-input-major);border:1px solid var(--dsw-alias-border-l2);border-radius:8px;display:block;font-family:var(--dsw-font-family-mono,ui-monospace,SFMono-Regular,Menlo,monospace);font-size:11px;line-height:1.5;margin:0;overflow-wrap:anywhere;padding:9px;tab-size:2;user-select:text;white-space:pre-wrap}
|
|
2078
2138
|
.dus-command:focus{outline:2px solid var(--dsw-alias-state-business-primary);outline-offset:2px}
|
|
@@ -2084,6 +2144,16 @@ window.__ModuleLoader__.load({
|
|
|
2084
2144
|
.dus-settings-toggle{align-items:flex-start;cursor:pointer;display:flex;gap:10px;font-size:13px;line-height:1.4}
|
|
2085
2145
|
.dus-settings-toggle input{accent-color:var(--dsw-alias-state-business-primary);height:18px;margin:0;min-height:18px;min-width:18px;width:18px}
|
|
2086
2146
|
.dus-settings-hint{color:var(--dsw-alias-label-secondary);font-size:12px;line-height:1.45;margin:8px 0 0}
|
|
2147
|
+
/* Running version and service start time share one row: version left, start time
|
|
2148
|
+
flush right. Each line asks for a 200px basis, so the row wraps into two stacked
|
|
2149
|
+
lines on a phone-width card instead of squeezing either value — the break is
|
|
2150
|
+
driven by available width, which keeps one declaration correct at every size and
|
|
2151
|
+
needs no width query. min(200px,100%) keeps a very narrow card from overflowing.
|
|
2152
|
+
The 5px bottom margin is the breathing room the status block below used to do
|
|
2153
|
+
without: the hints carry margin:8px 0 0, so the row's own box ends at the text and
|
|
2154
|
+
the green state block sat flush against it. */
|
|
2155
|
+
.dus-settings-pair{align-items:baseline;display:flex;flex-wrap:wrap;gap:0 12px;justify-content:space-between;margin:0 0 5px}
|
|
2156
|
+
.dus-settings-pair>.dus-settings-hint{flex:0 1 auto;min-width:min(200px,100%);overflow-wrap:anywhere}
|
|
2087
2157
|
.dus-compat{color:var(--dsw-alias-label-secondary)}
|
|
2088
2158
|
.dus-compat[data-compatibility=verified]{color:var(--dsw-alias-state-success-primary)}
|
|
2089
2159
|
.dus-compat[data-compatibility=incompatible]{color:var(--dsw-alias-state-error-primary)}
|