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/lib/service-cli.js
CHANGED
|
@@ -1,344 +1,67 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { accessSync, constants, existsSync, mkdirSync, readFileSync,
|
|
3
|
-
import { dirname
|
|
4
|
-
import { homedir } from "node:os";
|
|
5
|
-
import { createServer } from "node:net";
|
|
1
|
+
import { a as WINDOWS_TASK_NAME, c as parseSystemdMainPid, d as bootStampFile, f as countRecentRestarts, h as serviceLogDir, i as SYSTEMD_UNIT, l as parseWindowsTaskRunning, n as specFromArgs, s as parseLaunchdTrackedPid, t as planFor, u as RESTART_STORM_WINDOW_MS } from "./plan-BornHBEZ.js";
|
|
2
|
+
import { accessSync, constants, existsSync, mkdirSync, readFileSync, renameSync, rmSync, statSync, writeFileSync } from "node:fs";
|
|
3
|
+
import { dirname } from "node:path";
|
|
6
4
|
import { spawnSync } from "node:child_process";
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
const SERVICE_LABEL = "com.idoall.dsh-update-status.web";
|
|
10
|
-
const SUPERVISOR_MARKER = "dsh-update-status";
|
|
11
|
-
function xml(value) {
|
|
12
|
-
return value.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """).replace(/'/g, "'");
|
|
13
|
-
}
|
|
14
|
-
function systemd(value) {
|
|
15
|
-
return value.replace(/\\/g, "\\\\").replace(/"/g, "\\\"");
|
|
16
|
-
}
|
|
17
|
-
function powerShell(value) {
|
|
18
|
-
return value.replace(/'/g, "''");
|
|
19
|
-
}
|
|
20
|
-
function boundedPort(value) {
|
|
21
|
-
const port = typeof value === "number" ? value : Number(value);
|
|
22
|
-
if (!Number.isInteger(port) || port < 1 || port > 65535) throw new Error("`--port` must be an integer from 1 to 65535");
|
|
23
|
-
return port;
|
|
24
|
-
}
|
|
25
|
-
function valueAfter(args, flag) {
|
|
26
|
-
const index = args.indexOf(flag);
|
|
27
|
-
if (index < 0) return void 0;
|
|
28
|
-
const value = args[index + 1];
|
|
29
|
-
if (value === void 0 || value.startsWith("--")) throw new Error(`missing value after ${flag}`);
|
|
30
|
-
return value;
|
|
31
|
-
}
|
|
32
|
-
/**
|
|
33
|
-
* Resolve the real JS file behind `@deepseek-ai/dsh`'s `bin.dsh`, using the
|
|
34
|
-
* selected profile as a Node resolution anchor. This avoids platform shell
|
|
35
|
-
* shims (`dsh.cmd`) and keeps every native service on `node <absolute-js>`.
|
|
36
|
-
*/
|
|
37
|
-
function dshPathOf(env, profile, dshHome, override) {
|
|
38
|
-
const fromArg = override?.trim();
|
|
39
|
-
if (fromArg !== void 0 && fromArg !== "") return isAbsolute(fromArg) ? fromArg : resolve(env.cwd, fromArg);
|
|
40
|
-
const home = dshHome ?? join(env.home || homedir(), ".dsh");
|
|
41
|
-
try {
|
|
42
|
-
const anchor = join(home, "profiles", profile, "dsh-update-status-service-resolver.cjs");
|
|
43
|
-
const manifestPath = createRequire(anchor).resolve("@deepseek-ai/dsh/package.json");
|
|
44
|
-
const manifest = JSON.parse(readFileSync(manifestPath, "utf8"));
|
|
45
|
-
if (manifest.name !== "@deepseek-ai/dsh" || typeof manifest.bin?.dsh !== "string") throw new Error("invalid dsh package manifest");
|
|
46
|
-
return realpathSync(join(dirname(manifestPath), manifest.bin.dsh));
|
|
47
|
-
} catch (error) {
|
|
48
|
-
throw new Error(`cannot resolve @deepseek-ai/dsh from profile ${profile}; rerun with --dsh /absolute/path/to/lib/bin.js (${error instanceof Error ? error.message : String(error)})`);
|
|
49
|
-
}
|
|
50
|
-
}
|
|
51
|
-
function servicePlatform(value) {
|
|
52
|
-
if (value === "darwin" || value === "linux" || value === "win32") return value;
|
|
53
|
-
throw new Error(`unsupported platform: ${value}; supported: macOS, Linux, Windows`);
|
|
54
|
-
}
|
|
55
|
-
function specFromArgs(args, env) {
|
|
56
|
-
const platform = servicePlatform(env.platform);
|
|
57
|
-
const profile = valueAfter(args, "--profile") ?? env.env.DSH_PROFILE ?? "web";
|
|
58
|
-
const workspace = valueAfter(args, "--workspace");
|
|
59
|
-
if (workspace === void 0) throw new Error("`--workspace` is required and must be the absolute directory DSH should use");
|
|
60
|
-
const host = valueAfter(args, "--host") ?? "127.0.0.1";
|
|
61
|
-
const port = boundedPort(valueAfter(args, "--port") ?? 3080);
|
|
62
|
-
const home = env.home || homedir();
|
|
63
|
-
const dshHome = valueAfter(args, "--dsh-home") ?? (env.env.DSH_HOME?.trim() || void 0);
|
|
64
|
-
if (dshHome !== void 0 && !isAbsolute(dshHome)) throw new Error("`--dsh-home` must be an absolute path");
|
|
65
|
-
const logDir = valueAfter(args, "--log-dir") ?? join(dshHome ?? join(home, ".dsh"), "logs");
|
|
66
|
-
const dshPath = dshPathOf(env, profile, dshHome, valueAfter(args, "--dsh"));
|
|
67
|
-
const nodePath = valueAfter(args, "--node") ?? env.execPath;
|
|
68
|
-
if (!isAbsolute(nodePath)) throw new Error("`--node` must be an absolute executable path");
|
|
69
|
-
if (!isAbsolute(dshPath)) throw new Error("`--dsh` must be an absolute executable path");
|
|
70
|
-
if (!isAbsolute(workspace)) throw new Error("`--workspace` must be an absolute path");
|
|
71
|
-
if (!isAbsolute(logDir)) throw new Error("`--log-dir` must be an absolute path");
|
|
72
|
-
return {
|
|
73
|
-
schemaVersion: 1,
|
|
74
|
-
platform,
|
|
75
|
-
label: SERVICE_LABEL,
|
|
76
|
-
nodePath,
|
|
77
|
-
dshPath,
|
|
78
|
-
profile,
|
|
79
|
-
workspace,
|
|
80
|
-
host,
|
|
81
|
-
port,
|
|
82
|
-
home,
|
|
83
|
-
...dshHome === void 0 ? {} : { dshHome },
|
|
84
|
-
logDir,
|
|
85
|
-
supervisorMarker: SUPERVISOR_MARKER
|
|
86
|
-
};
|
|
87
|
-
}
|
|
88
|
-
function pathsFor(spec) {
|
|
89
|
-
const root = spec.dshHome ?? join(spec.home, ".dsh");
|
|
90
|
-
const stdoutFile = join(spec.logDir, "dsh-web-service.stdout.log");
|
|
91
|
-
const stderrFile = join(spec.logDir, "dsh-web-service.stderr.log");
|
|
92
|
-
if (spec.platform === "darwin") return {
|
|
93
|
-
stateFile: join(root, "dsh-update-status-service.json"),
|
|
94
|
-
definitionFile: join(spec.home, "Library", "LaunchAgents", `${spec.label}.plist`),
|
|
95
|
-
stdoutFile,
|
|
96
|
-
stderrFile
|
|
97
|
-
};
|
|
98
|
-
if (spec.platform === "linux") return {
|
|
99
|
-
stateFile: join(root, "dsh-update-status-service.json"),
|
|
100
|
-
definitionFile: join(spec.home, ".config", "systemd", "user", "dsh-update-status-web.service"),
|
|
101
|
-
stdoutFile,
|
|
102
|
-
stderrFile
|
|
103
|
-
};
|
|
104
|
-
const base = envLocalAppData(spec);
|
|
105
|
-
return {
|
|
106
|
-
stateFile: join(root, "dsh-update-status-service.json"),
|
|
107
|
-
definitionFile: join(base, "dsh-update-status", "dsh-web-task.xml"),
|
|
108
|
-
wrapperFile: join(base, "dsh-update-status", "dsh-web-supervisor.ps1"),
|
|
109
|
-
stdoutFile,
|
|
110
|
-
stderrFile
|
|
111
|
-
};
|
|
112
|
-
}
|
|
113
|
-
function envLocalAppData(spec) {
|
|
114
|
-
return join(spec.home, "AppData", "Local");
|
|
115
|
-
}
|
|
116
|
-
function webArguments(spec) {
|
|
117
|
-
return [
|
|
118
|
-
spec.dshPath,
|
|
119
|
-
"--profile",
|
|
120
|
-
spec.profile,
|
|
121
|
-
"--host",
|
|
122
|
-
spec.host,
|
|
123
|
-
"--port",
|
|
124
|
-
String(spec.port),
|
|
125
|
-
"--no-open"
|
|
126
|
-
];
|
|
127
|
-
}
|
|
128
|
-
function launchdDefinition(spec, paths) {
|
|
129
|
-
const args = [spec.nodePath, ...webArguments(spec)].map((value) => ` <string>${xml(value)}</string>`).join("\n");
|
|
130
|
-
const envXml = [
|
|
131
|
-
["HOME", spec.home],
|
|
132
|
-
["PATH", `${dirname(spec.nodePath)}:/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin`],
|
|
133
|
-
["DSH_WEB_SUPERVISOR", spec.supervisorMarker],
|
|
134
|
-
...spec.dshHome === void 0 ? [] : [["DSH_HOME", spec.dshHome]]
|
|
135
|
-
].map(([key, value]) => ` <key>${xml(key)}</key>\n <string>${xml(value)}</string>`).join("\n");
|
|
136
|
-
return `<?xml version="1.0" encoding="UTF-8"?>
|
|
137
|
-
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
|
138
|
-
<plist version="1.0">
|
|
139
|
-
<dict>
|
|
140
|
-
<key>Label</key>
|
|
141
|
-
<string>${xml(spec.label)}</string>
|
|
142
|
-
<key>ProgramArguments</key>
|
|
143
|
-
<array>
|
|
144
|
-
${args}
|
|
145
|
-
</array>
|
|
146
|
-
<key>WorkingDirectory</key>
|
|
147
|
-
<string>${xml(spec.workspace)}</string>
|
|
148
|
-
<key>RunAtLoad</key>
|
|
149
|
-
<true/>
|
|
150
|
-
<key>KeepAlive</key>
|
|
151
|
-
<true/>
|
|
152
|
-
<key>ThrottleInterval</key>
|
|
153
|
-
<integer>3</integer>
|
|
154
|
-
<key>EnvironmentVariables</key>
|
|
155
|
-
<dict>
|
|
156
|
-
${envXml}
|
|
157
|
-
</dict>
|
|
158
|
-
<key>StandardOutPath</key>
|
|
159
|
-
<string>${xml(paths.stdoutFile)}</string>
|
|
160
|
-
<key>StandardErrorPath</key>
|
|
161
|
-
<string>${xml(paths.stderrFile)}</string>
|
|
162
|
-
</dict>
|
|
163
|
-
</plist>
|
|
164
|
-
`;
|
|
165
|
-
}
|
|
166
|
-
function systemdDefinition(spec) {
|
|
167
|
-
const command = [spec.nodePath, ...webArguments(spec)].map((value) => `"${systemd(value)}"`).join(" ");
|
|
168
|
-
const env = [
|
|
169
|
-
`Environment="HOME=${systemd(spec.home)}"`,
|
|
170
|
-
`Environment="PATH=${systemd(`${dirname(spec.nodePath)}:/usr/local/bin:/usr/bin:/bin`)}"`,
|
|
171
|
-
`Environment="DSH_WEB_SUPERVISOR=${spec.supervisorMarker}"`,
|
|
172
|
-
...spec.dshHome === void 0 ? [] : [`Environment="DSH_HOME=${systemd(spec.dshHome)}"`]
|
|
173
|
-
].join("\n");
|
|
174
|
-
return `[Unit]
|
|
175
|
-
Description=DSH Web managed by dsh-update-status
|
|
176
|
-
After=network-online.target
|
|
177
|
-
|
|
178
|
-
[Service]
|
|
179
|
-
Type=simple
|
|
180
|
-
WorkingDirectory="${systemd(spec.workspace)}"
|
|
181
|
-
${env}
|
|
182
|
-
ExecStart=${command}
|
|
183
|
-
Restart=on-failure
|
|
184
|
-
RestartSec=3
|
|
185
|
-
|
|
186
|
-
[Install]
|
|
187
|
-
WantedBy=default.target
|
|
188
|
-
`;
|
|
189
|
-
}
|
|
190
|
-
function windowsWrapper(spec, paths) {
|
|
191
|
-
const node = powerShell(spec.nodePath);
|
|
192
|
-
const dsh = powerShell(spec.dshPath);
|
|
193
|
-
const workspace = powerShell(spec.workspace);
|
|
194
|
-
const out = powerShell(paths.stdoutFile);
|
|
195
|
-
const err = powerShell(paths.stderrFile);
|
|
196
|
-
const home = powerShell(spec.home);
|
|
197
|
-
const dshHome = spec.dshHome === void 0 ? "" : `$env:DSH_HOME = '${powerShell(spec.dshHome)}'\n`;
|
|
198
|
-
return `$ErrorActionPreference = 'Stop'
|
|
199
|
-
$env:HOME = '${home}'
|
|
200
|
-
$env:DSH_WEB_SUPERVISOR = '${spec.supervisorMarker}'
|
|
201
|
-
${dshHome}Set-Location -LiteralPath '${workspace}'
|
|
202
|
-
while ($true) {
|
|
203
|
-
& '${node}' '${dsh}' --profile '${powerShell(spec.profile)}' --host '${powerShell(spec.host)}' --port '${String(spec.port)}' --no-open 1>> '${out}' 2>> '${err}'
|
|
204
|
-
$exitCode = $LASTEXITCODE
|
|
205
|
-
Start-Sleep -Seconds 3
|
|
206
|
-
# The user service owns deliberate restarts (exit 42) and unexpected exits.
|
|
207
|
-
# Manual uninstall stops the scheduled task, which ends this wrapper.
|
|
208
|
-
}
|
|
209
|
-
`;
|
|
210
|
-
}
|
|
211
|
-
function windowsDefinition(spec, paths) {
|
|
212
|
-
const wrapper = paths.wrapperFile;
|
|
213
|
-
if (wrapper === void 0) throw new Error("Windows service plan requires a wrapper path");
|
|
214
|
-
return `<?xml version="1.0" encoding="UTF-8"?>
|
|
215
|
-
<Task version="1.4" xmlns="http://schemas.microsoft.com/windows/2004/02/mit/task">
|
|
216
|
-
<RegistrationInfo><Description>DSH Web managed by dsh-update-status</Description></RegistrationInfo>
|
|
217
|
-
<Triggers><LogonTrigger><Enabled>true</Enabled></LogonTrigger></Triggers>
|
|
218
|
-
<Principals><Principal id="Author"><LogonType>InteractiveToken</LogonType><RunLevel>LeastPrivilege</RunLevel></Principal></Principals>
|
|
219
|
-
<Settings><MultipleInstancesPolicy>IgnoreNew</MultipleInstancesPolicy><StartWhenAvailable>true</StartWhenAvailable><ExecutionTimeLimit>PT0S</ExecutionTimeLimit></Settings>
|
|
220
|
-
<Actions Context="Author"><Exec><Command>powershell.exe</Command><Arguments>-NoProfile -ExecutionPolicy Bypass -File "${xml(wrapper)}"</Arguments></Exec></Actions>
|
|
221
|
-
</Task>
|
|
222
|
-
`;
|
|
223
|
-
}
|
|
224
|
-
/** Generate a service plan without writing, starting, or stopping anything. */
|
|
225
|
-
function planFor(spec, uid = 0) {
|
|
226
|
-
const paths = pathsFor(spec);
|
|
5
|
+
import { createServer } from "node:net";
|
|
6
|
+
async function observeServiceState(spec, run, uid) {
|
|
227
7
|
if (spec.platform === "darwin") {
|
|
228
|
-
const
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
installCommand: [
|
|
234
|
-
"launchctl",
|
|
235
|
-
"bootstrap",
|
|
236
|
-
domain,
|
|
237
|
-
paths.definitionFile
|
|
238
|
-
],
|
|
239
|
-
startCommand: [
|
|
240
|
-
"launchctl",
|
|
241
|
-
"kickstart",
|
|
242
|
-
"-k",
|
|
243
|
-
`${domain}/${spec.label}`
|
|
244
|
-
],
|
|
245
|
-
statusCommand: [
|
|
246
|
-
"launchctl",
|
|
247
|
-
"print",
|
|
248
|
-
`${domain}/${spec.label}`
|
|
249
|
-
],
|
|
250
|
-
stopCommand: [
|
|
251
|
-
"launchctl",
|
|
252
|
-
"bootout",
|
|
253
|
-
`${domain}/${spec.label}`
|
|
254
|
-
],
|
|
255
|
-
uninstallCommand: [
|
|
256
|
-
"launchctl",
|
|
257
|
-
"bootout",
|
|
258
|
-
`${domain}/${spec.label}`
|
|
259
|
-
]
|
|
8
|
+
const result = run("launchctl", ["print", `gui/${String(uid)}/${spec.label}`]);
|
|
9
|
+
const pid = parseLaunchdTrackedPid(result.stdout);
|
|
10
|
+
return pid === void 0 ? { running: false } : {
|
|
11
|
+
running: true,
|
|
12
|
+
pid
|
|
260
13
|
};
|
|
261
14
|
}
|
|
262
|
-
if (spec.platform === "linux")
|
|
263
|
-
|
|
264
|
-
paths,
|
|
265
|
-
definition: systemdDefinition(spec),
|
|
266
|
-
installCommand: [
|
|
267
|
-
"systemctl",
|
|
15
|
+
if (spec.platform === "linux") {
|
|
16
|
+
const result = run("systemctl", [
|
|
268
17
|
"--user",
|
|
269
|
-
"
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
"
|
|
273
|
-
"--
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
"/TN",
|
|
317
|
-
"DSH Update Status Web"
|
|
318
|
-
],
|
|
319
|
-
statusCommand: [
|
|
320
|
-
"schtasks.exe",
|
|
321
|
-
"/Query",
|
|
322
|
-
"/TN",
|
|
323
|
-
"DSH Update Status Web",
|
|
324
|
-
"/V",
|
|
325
|
-
"/FO",
|
|
326
|
-
"LIST"
|
|
327
|
-
],
|
|
328
|
-
stopCommand: [
|
|
329
|
-
"schtasks.exe",
|
|
330
|
-
"/End",
|
|
331
|
-
"/TN",
|
|
332
|
-
"DSH Update Status Web"
|
|
333
|
-
],
|
|
334
|
-
uninstallCommand: [
|
|
335
|
-
"schtasks.exe",
|
|
336
|
-
"/Delete",
|
|
337
|
-
"/TN",
|
|
338
|
-
"DSH Update Status Web",
|
|
339
|
-
"/F"
|
|
340
|
-
]
|
|
341
|
-
};
|
|
18
|
+
"show",
|
|
19
|
+
SYSTEMD_UNIT,
|
|
20
|
+
"-p",
|
|
21
|
+
"MainPID",
|
|
22
|
+
"--value"
|
|
23
|
+
]);
|
|
24
|
+
const pid = parseSystemdMainPid(result.stdout);
|
|
25
|
+
return pid === void 0 ? { running: false } : {
|
|
26
|
+
running: true,
|
|
27
|
+
pid
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
const result = run("powershell.exe", [
|
|
31
|
+
"-NoProfile",
|
|
32
|
+
"-NonInteractive",
|
|
33
|
+
"-Command",
|
|
34
|
+
`(Get-ScheduledTask -TaskName '${WINDOWS_TASK_NAME}').State`
|
|
35
|
+
]);
|
|
36
|
+
return { running: parseWindowsTaskRunning(result.stdout) };
|
|
37
|
+
}
|
|
38
|
+
function defaultSleep(ms) {
|
|
39
|
+
return new Promise((resolve) => {
|
|
40
|
+
setTimeout(resolve, ms);
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
async function verifyActivatedService(spec, options) {
|
|
44
|
+
const attempts = options.attempts ?? 4;
|
|
45
|
+
const intervalMs = options.intervalMs ?? 5e3;
|
|
46
|
+
const sleep = options.sleep ?? defaultSleep;
|
|
47
|
+
const uid = options.uid ?? 0;
|
|
48
|
+
const recentRestarts = options.recentRestarts ?? (() => countRecentRestarts(serviceLogDir(), bootStampFile(), Date.now(), 18e4));
|
|
49
|
+
for (let attempt = 0; attempt < attempts; attempt += 1) {
|
|
50
|
+
const restarts = recentRestarts();
|
|
51
|
+
if (restarts >= 4) return {
|
|
52
|
+
kind: "crash-loop",
|
|
53
|
+
recentRestarts: restarts,
|
|
54
|
+
windowMs: RESTART_STORM_WINDOW_MS
|
|
55
|
+
};
|
|
56
|
+
if ((await observeServiceState(spec, options.run, uid)).running) {
|
|
57
|
+
if (attempt === attempts - 1) return { kind: "settled" };
|
|
58
|
+
await sleep(intervalMs);
|
|
59
|
+
continue;
|
|
60
|
+
}
|
|
61
|
+
if (attempt === attempts - 1) return { kind: "not-running" };
|
|
62
|
+
await sleep(intervalMs);
|
|
63
|
+
}
|
|
64
|
+
return { kind: "not-running" };
|
|
342
65
|
}
|
|
343
66
|
//#endregion
|
|
344
67
|
//#region src/service/cli.ts
|
|
@@ -384,6 +107,10 @@ Options:
|
|
|
384
107
|
--log-dir <path> Absolute log directory (default: $DSH_HOME/logs)
|
|
385
108
|
|
|
386
109
|
Workflow: plan --dry-run → install → stop the terminal-started DSH yourself → activate.
|
|
110
|
+
|
|
111
|
+
activate refuses an occupied port, never kills a process, and then verifies that
|
|
112
|
+
the service it started actually took over; a service that begins crash-looping
|
|
113
|
+
makes activate fail instead of reporting success.
|
|
387
114
|
`;
|
|
388
115
|
}
|
|
389
116
|
function assertSafeText(value, description) {
|
|
@@ -435,7 +162,14 @@ function atomicWrite(path, content) {
|
|
|
435
162
|
});
|
|
436
163
|
renameSync(temporary, path);
|
|
437
164
|
}
|
|
438
|
-
|
|
165
|
+
/**
|
|
166
|
+
* Replace an artifact only when we can prove we wrote it: either it already holds
|
|
167
|
+
* exactly what we are about to write, or our own receipt names that path. The
|
|
168
|
+
* receipt is what makes an upgrade possible at all — an improved definition
|
|
169
|
+
* differs from the stored one by exactly the fix being installed, so
|
|
170
|
+
* byte-equality with the previous rendering can never authorise it.
|
|
171
|
+
*/
|
|
172
|
+
function ensureReplaceable(path, expected, managed) {
|
|
439
173
|
if (!existsSync(path)) return;
|
|
440
174
|
let current;
|
|
441
175
|
try {
|
|
@@ -443,7 +177,18 @@ function ensureManagedTarget(path, expected) {
|
|
|
443
177
|
} catch {
|
|
444
178
|
throw new Error(`refusing to replace unreadable existing file: ${path}`);
|
|
445
179
|
}
|
|
446
|
-
if (current
|
|
180
|
+
if (current === expected || managed) return;
|
|
181
|
+
throw new Error(`refusing to overwrite an existing unmanaged service artifact: ${path}`);
|
|
182
|
+
}
|
|
183
|
+
function receiptOwnsPath(previous, path) {
|
|
184
|
+
if (previous === void 0) return false;
|
|
185
|
+
return previous.paths.definitionFile === path || previous.paths.stateFile === path || previous.paths.wrapperFile === path;
|
|
186
|
+
}
|
|
187
|
+
function receiptTextOf(receipt) {
|
|
188
|
+
return JSON.stringify(receipt, null, 2) + "\n";
|
|
189
|
+
}
|
|
190
|
+
function readReceiptIfPresent(path) {
|
|
191
|
+
return existsSync(path) ? readReceipt(path) : void 0;
|
|
447
192
|
}
|
|
448
193
|
function readReceipt(path) {
|
|
449
194
|
try {
|
|
@@ -483,7 +228,8 @@ function serialisePlan(plan) {
|
|
|
483
228
|
start: plan.startCommand,
|
|
484
229
|
status: plan.statusCommand,
|
|
485
230
|
stop: plan.stopCommand,
|
|
486
|
-
uninstall: plan.uninstallCommand
|
|
231
|
+
uninstall: plan.uninstallCommand,
|
|
232
|
+
recover: plan.recoverCommand
|
|
487
233
|
}
|
|
488
234
|
}, null, 2) + "\n";
|
|
489
235
|
}
|
|
@@ -577,16 +323,16 @@ async function main(argv = process.argv.slice(2), env = environment(), run = run
|
|
|
577
323
|
}
|
|
578
324
|
if (action === "install") {
|
|
579
325
|
(hooks.preflight ?? preflight)(spec);
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
326
|
+
const previous = readReceiptIfPresent(plan.paths.stateFile);
|
|
327
|
+
ensureReplaceable(plan.paths.definitionFile, plan.definition, receiptOwnsPath(previous, plan.paths.definitionFile));
|
|
328
|
+
if (plan.wrapper !== void 0 && plan.paths.wrapperFile !== void 0) ensureReplaceable(plan.paths.wrapperFile, plan.wrapper, receiptOwnsPath(previous, plan.paths.wrapperFile));
|
|
329
|
+
const receiptText = receiptTextOf({
|
|
583
330
|
version: RECEIPT_VERSION,
|
|
584
331
|
managedBy: "dsh-update-status",
|
|
585
332
|
spec,
|
|
586
333
|
paths: plan.paths
|
|
587
|
-
};
|
|
588
|
-
|
|
589
|
-
ensureManagedTarget(plan.paths.stateFile, receiptText);
|
|
334
|
+
});
|
|
335
|
+
ensureReplaceable(plan.paths.stateFile, receiptText, receiptOwnsPath(previous, plan.paths.stateFile));
|
|
590
336
|
mkdirSync(spec.logDir, {
|
|
591
337
|
recursive: true,
|
|
592
338
|
mode: 448
|
|
@@ -601,7 +347,13 @@ async function main(argv = process.argv.slice(2), env = environment(), run = run
|
|
|
601
347
|
if (action === "activate") {
|
|
602
348
|
if (!await portIsFree(receipt.spec.host, receipt.spec.port)) throw new Error(`port ${receipt.spec.host}:${String(receipt.spec.port)} is in use; stop the existing DSH yourself before activate. Nothing was killed.`);
|
|
603
349
|
for (const command of commandsForActivate(plan, env.uid ?? 0)) invoke(command, run);
|
|
604
|
-
|
|
350
|
+
const verification = await (hooks.verify ?? ((installed) => verifyActivatedService(installed, {
|
|
351
|
+
run,
|
|
352
|
+
uid: env.uid ?? 0
|
|
353
|
+
})))(receipt.spec);
|
|
354
|
+
if (verification.kind === "crash-loop") throw new Error(`the activated service is restarting repeatedly (${String(verification.recentRestarts)} starts within ${String(Math.round(verification.windowMs / 1e3))}s); another process probably holds ${receipt.spec.host}:${String(receipt.spec.port)}. Nothing was killed. Stop that instance, fix the cause, then run activate again.`);
|
|
355
|
+
if (verification.kind === "not-running") throw new Error(`the activated service reported no running process; check the native status and the logs in ${receipt.spec.logDir}. Nothing was killed.`);
|
|
356
|
+
process.stdout.write("User service activated and verified to be running. Wait for DSH to become ready, then open the existing Web URL.\n");
|
|
605
357
|
return 0;
|
|
606
358
|
}
|
|
607
359
|
if (action === "status") {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-update-status",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.0",
|
|
4
4
|
"description": "DeepSeek Harness Web plugin for version status, copy-only upgrade guidance, and safe restart when an explicit user-level supervisor is installed.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "lib/index.js",
|
|
@@ -17,11 +17,7 @@
|
|
|
17
17
|
"./package.json": "./package.json"
|
|
18
18
|
},
|
|
19
19
|
"files": [
|
|
20
|
-
"lib
|
|
21
|
-
"lib/index.d.ts",
|
|
22
|
-
"lib/client.js",
|
|
23
|
-
"lib/client.js.map",
|
|
24
|
-
"lib/service-cli.js",
|
|
20
|
+
"lib",
|
|
25
21
|
"bin/dsh-update-status-service.mjs",
|
|
26
22
|
"cordis.patch.yml",
|
|
27
23
|
"README.md",
|