dsh-update-status 0.3.2 → 0.4.1
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 +20 -0
- package/README.md +10 -7
- package/README.zh.md +10 -7
- package/docs/platform-acceptance.md +7 -5
- package/docs/service-supervision.md +50 -13
- package/lib/client.js +83 -13
- package/lib/client.js.map +1 -1
- package/lib/index.js +347 -43
- package/lib/plan-D9Ybaj-3.js +746 -0
- package/lib/service-cli.d.ts +94 -0
- package/lib/service-cli.js +244 -403
- 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, isAbsolute
|
|
4
|
-
import { homedir } from "node:os";
|
|
5
|
-
import { createServer } from "node:net";
|
|
1
|
+
import { _ as parseSystemdMainPid, a as planFor, b as bootStampFile, c as specFromArgs, d as servicePathOf, g as parseLaunchdTrackedPid, i as pathsFor, m as WINDOWS_TASK_NAME, n as nativeServiceCommandCandidates, o as servicePlatform, p as SYSTEMD_UNIT, r as nativeServiceCommandOf, s as serviceStateFileFromArgs, t as SERVICE_LABEL, u as normalizeServicePathExt, v as parseWindowsTaskRunning, w as serviceLogDir, x as countRecentRestarts, y as RESTART_STORM_WINDOW_MS } from "./plan-D9Ybaj-3.js";
|
|
2
|
+
import { accessSync, constants, existsSync, mkdirSync, readFileSync, renameSync, rmSync, statSync, writeFileSync } from "node:fs";
|
|
3
|
+
import { dirname, isAbsolute } 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",
|
|
268
|
-
"--user",
|
|
269
|
-
"daemon-reload"
|
|
270
|
-
],
|
|
271
|
-
startCommand: [
|
|
272
|
-
"systemctl",
|
|
273
|
-
"--user",
|
|
274
|
-
"enable",
|
|
275
|
-
"--now",
|
|
276
|
-
"dsh-update-status-web.service"
|
|
277
|
-
],
|
|
278
|
-
statusCommand: [
|
|
279
|
-
"systemctl",
|
|
280
|
-
"--user",
|
|
281
|
-
"status",
|
|
282
|
-
"dsh-update-status-web.service"
|
|
283
|
-
],
|
|
284
|
-
stopCommand: [
|
|
285
|
-
"systemctl",
|
|
286
|
-
"--user",
|
|
287
|
-
"disable",
|
|
288
|
-
"--now",
|
|
289
|
-
"dsh-update-status-web.service"
|
|
290
|
-
],
|
|
291
|
-
uninstallCommand: [
|
|
292
|
-
"systemctl",
|
|
15
|
+
if (spec.platform === "linux") {
|
|
16
|
+
const result = run("systemctl", [
|
|
293
17
|
"--user",
|
|
294
|
-
"
|
|
295
|
-
|
|
296
|
-
"
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
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
|
|
@@ -380,14 +103,25 @@ Options:
|
|
|
380
103
|
--host <127.0.0.1|0.0.0.0> Bind host (default: 127.0.0.1)
|
|
381
104
|
--port <1..65535> Web port (default: 3080)
|
|
382
105
|
--node <path> Absolute Node executable (default: current Node)
|
|
383
|
-
--dsh <path> Absolute dsh executable (default: resolve from
|
|
106
|
+
--dsh <path> Absolute dsh executable (default: resolve from profile)
|
|
384
107
|
--log-dir <path> Absolute log directory (default: $DSH_HOME/logs)
|
|
108
|
+
--path-source <mode> Service PATH source: current (default), minimal, explicit
|
|
109
|
+
--service-path <value> Explicit PATH (implies --path-source explicit)
|
|
110
|
+
--service-pathext <v> Windows PATHEXT override (for example .COM;.EXE;.CMD)
|
|
111
|
+
|
|
112
|
+
current snapshots this command's PATH; it never executes your shell startup files.
|
|
113
|
+
The final PATH, source, capture time and warnings are stored in the receipt and
|
|
114
|
+
reused by preflight and every later service action.
|
|
385
115
|
|
|
386
116
|
Workflow: plan --dry-run → install → stop the terminal-started DSH yourself → activate.
|
|
117
|
+
|
|
118
|
+
activate refuses an occupied port, never kills a process, and then verifies that
|
|
119
|
+
the service it started actually took over; a service that begins crash-looping
|
|
120
|
+
makes activate fail instead of reporting success.
|
|
387
121
|
`;
|
|
388
122
|
}
|
|
389
123
|
function assertSafeText(value, description) {
|
|
390
|
-
if (/[\u0000\
|
|
124
|
+
if (/[\u0000-\u0008\u000A-\u001F\u007F]/.test(value)) throw new Error(`${description} must not contain XML-illegal control characters or line breaks`);
|
|
391
125
|
}
|
|
392
126
|
function assertReadableRegular(path, description) {
|
|
393
127
|
assertSafeText(path, description);
|
|
@@ -408,20 +142,73 @@ function assertExecutable(path, description) {
|
|
|
408
142
|
throw new Error(`${description} is not executable: ${path}`);
|
|
409
143
|
}
|
|
410
144
|
}
|
|
411
|
-
function
|
|
145
|
+
function envValue(env, key) {
|
|
146
|
+
return Object.entries(env).find(([name]) => name.toLowerCase() === key.toLowerCase())?.[1];
|
|
147
|
+
}
|
|
148
|
+
function validateSpec(spec, level = "launch", expectedPlatform, expectedHome, expectedSystemRoot) {
|
|
149
|
+
if (spec.schemaVersion !== 1) throw new Error("managed service receipt has an unsupported schema version");
|
|
150
|
+
if (spec.platform !== "darwin" && spec.platform !== "linux" && spec.platform !== "win32") throw new Error("managed service receipt has an invalid platform");
|
|
151
|
+
if (expectedPlatform !== void 0 && spec.platform !== expectedPlatform) throw new Error("managed service receipt belongs to a different platform");
|
|
152
|
+
if (spec.label !== SERVICE_LABEL || spec.supervisorMarker !== "dsh-update-status") throw new Error("managed service receipt has an unexpected service identity");
|
|
153
|
+
if (expectedHome !== void 0 && spec.home !== expectedHome) throw new Error("managed service receipt belongs to a different home directory");
|
|
412
154
|
for (const [description, value] of [
|
|
413
155
|
["profile", spec.profile],
|
|
414
156
|
["workspace", spec.workspace],
|
|
415
157
|
["home", spec.home],
|
|
416
158
|
["log directory", spec.logDir],
|
|
417
|
-
["host", spec.host]
|
|
418
|
-
|
|
159
|
+
["host", spec.host],
|
|
160
|
+
["Node executable", spec.nodePath],
|
|
161
|
+
["dsh executable", spec.dshPath]
|
|
162
|
+
]) {
|
|
163
|
+
if (typeof value !== "string") throw new Error(`managed service receipt has an invalid ${description}`);
|
|
164
|
+
assertSafeText(value, description);
|
|
165
|
+
}
|
|
166
|
+
for (const [description, value] of [
|
|
167
|
+
["workspace", spec.workspace],
|
|
168
|
+
["home", spec.home],
|
|
169
|
+
["log directory", spec.logDir],
|
|
170
|
+
["Node executable", spec.nodePath],
|
|
171
|
+
["dsh executable", spec.dshPath]
|
|
172
|
+
]) if (!isAbsolute(value)) throw new Error(`managed service receipt has a non-absolute ${description}`);
|
|
173
|
+
if (spec.dshHome !== void 0) {
|
|
174
|
+
if (typeof spec.dshHome !== "string") throw new Error("managed service receipt has an invalid DSH_HOME");
|
|
175
|
+
assertSafeText(spec.dshHome, "DSH_HOME");
|
|
176
|
+
if (!isAbsolute(spec.dshHome)) throw new Error("managed service receipt has a non-absolute DSH_HOME");
|
|
177
|
+
}
|
|
178
|
+
if (!Number.isInteger(spec.port) || spec.port < 1 || spec.port > 65535) throw new Error("managed service receipt has an invalid port");
|
|
179
|
+
if (spec.servicePath !== void 0 && typeof spec.servicePath !== "string") throw new Error("managed service receipt has an invalid service PATH");
|
|
180
|
+
if (spec.servicePathSource !== void 0 && spec.servicePathSource !== "current" && spec.servicePathSource !== "minimal" && spec.servicePathSource !== "explicit") throw new Error("managed service receipt has an invalid service PATH source");
|
|
181
|
+
if (spec.servicePathCapturedAt !== void 0 && (typeof spec.servicePathCapturedAt !== "string" || Number.isNaN(Date.parse(spec.servicePathCapturedAt)))) throw new Error("managed service receipt has an invalid service PATH capture time");
|
|
182
|
+
if (spec.servicePathExt !== void 0 && typeof spec.servicePathExt !== "string") throw new Error("managed service receipt has an invalid PATHEXT");
|
|
183
|
+
if (spec.servicePathWarnings !== void 0 && (!Array.isArray(spec.servicePathWarnings) || spec.servicePathWarnings.length > 256 || spec.servicePathWarnings.some((value) => typeof value !== "string"))) throw new Error("managed service receipt has invalid service PATH warnings");
|
|
184
|
+
if (spec.servicePath === void 0 && (spec.servicePathSource !== void 0 || spec.servicePathCapturedAt !== void 0 || spec.servicePathExt !== void 0 || spec.servicePathWarnings !== void 0)) throw new Error("managed service receipt has PATH metadata without a service PATH");
|
|
185
|
+
if (spec.servicePath !== void 0 && (spec.servicePathSource === void 0 || spec.servicePathCapturedAt === void 0)) throw new Error("managed service receipt has an incomplete service PATH record");
|
|
186
|
+
if (spec.platform === "win32" && spec.servicePath !== void 0 && spec.servicePathExt === void 0) throw new Error("managed Windows service receipt has no PATHEXT");
|
|
187
|
+
if (spec.platform !== "win32" && spec.servicePathExt !== void 0) throw new Error("managed non-Windows service receipt has PATHEXT");
|
|
188
|
+
if (spec.nativeServiceCommand !== void 0 && typeof spec.nativeServiceCommand !== "string") throw new Error("managed service receipt has an invalid native service command");
|
|
189
|
+
const servicePath = servicePathOf(spec);
|
|
190
|
+
assertSafeText(servicePath, "service PATH");
|
|
191
|
+
if (servicePath.length > 8192) throw new Error("managed service receipt has an overlong service PATH");
|
|
192
|
+
if (spec.servicePathExt !== void 0) {
|
|
193
|
+
assertSafeText(spec.servicePathExt, "PATHEXT");
|
|
194
|
+
if (spec.servicePathExt.length > 8192) throw new Error("managed service receipt has an overlong PATHEXT");
|
|
195
|
+
normalizeServicePathExt(spec.servicePathExt);
|
|
196
|
+
}
|
|
197
|
+
for (const warning of spec.servicePathWarnings ?? []) assertSafeText(warning, "service PATH warning");
|
|
198
|
+
const nativeCommand = nativeServiceCommandOf(spec);
|
|
199
|
+
assertSafeText(nativeCommand, "native service command");
|
|
200
|
+
if (!isAbsolute(nativeCommand)) throw new Error("managed service receipt has a non-absolute native service command");
|
|
201
|
+
const allowedNativeCommands = nativeServiceCommandCandidates(spec.platform, expectedSystemRoot);
|
|
202
|
+
const nativeKey = spec.platform === "win32" ? nativeCommand.toLowerCase() : nativeCommand;
|
|
203
|
+
if (!allowedNativeCommands.some((candidate) => (spec.platform === "win32" ? candidate.toLowerCase() : candidate) === nativeKey)) throw new Error("managed service receipt has an unexpected native service command");
|
|
419
204
|
if (spec.host !== "127.0.0.1" && spec.host !== "0.0.0.0") throw new Error("`--host` must be 127.0.0.1 or 0.0.0.0");
|
|
420
205
|
if (!/^[A-Za-z0-9][A-Za-z0-9._-]*$/.test(spec.profile)) throw new Error("`--profile` must contain only letters, numbers, dot, underscore, or hyphen");
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
206
|
+
if (level === "launch") {
|
|
207
|
+
assertExecutable(spec.nodePath, "Node executable");
|
|
208
|
+
assertReadableRegular(spec.dshPath, "dsh executable");
|
|
209
|
+
assertExecutable(nativeCommand, "Native service command");
|
|
210
|
+
if (!existsSync(spec.workspace) || !statSync(spec.workspace).isDirectory()) throw new Error(`workspace is not a directory: ${spec.workspace}`);
|
|
211
|
+
}
|
|
425
212
|
}
|
|
426
213
|
function atomicWrite(path, content) {
|
|
427
214
|
mkdirSync(dirname(path), {
|
|
@@ -435,7 +222,14 @@ function atomicWrite(path, content) {
|
|
|
435
222
|
});
|
|
436
223
|
renameSync(temporary, path);
|
|
437
224
|
}
|
|
438
|
-
|
|
225
|
+
/**
|
|
226
|
+
* Replace an artifact only when we can prove we wrote it: either it already holds
|
|
227
|
+
* exactly what we are about to write, or our own receipt names that path. The
|
|
228
|
+
* receipt is what makes an upgrade possible at all — an improved definition
|
|
229
|
+
* differs from the stored one by exactly the fix being installed, so
|
|
230
|
+
* byte-equality with the previous rendering can never authorise it.
|
|
231
|
+
*/
|
|
232
|
+
function ensureReplaceable(path, expected, managed) {
|
|
439
233
|
if (!existsSync(path)) return;
|
|
440
234
|
let current;
|
|
441
235
|
try {
|
|
@@ -443,13 +237,36 @@ function ensureManagedTarget(path, expected) {
|
|
|
443
237
|
} catch {
|
|
444
238
|
throw new Error(`refusing to replace unreadable existing file: ${path}`);
|
|
445
239
|
}
|
|
446
|
-
if (current
|
|
240
|
+
if (current === expected || managed) return;
|
|
241
|
+
throw new Error(`refusing to overwrite an existing unmanaged service artifact: ${path}`);
|
|
242
|
+
}
|
|
243
|
+
function receiptOwnsPath(previous, path) {
|
|
244
|
+
if (previous === void 0) return false;
|
|
245
|
+
const owned = pathsFor(previous.spec);
|
|
246
|
+
return owned.definitionFile === path || owned.stateFile === path || owned.wrapperFile === path;
|
|
247
|
+
}
|
|
248
|
+
function receiptTextOf(receipt) {
|
|
249
|
+
return JSON.stringify(receipt, null, 2) + "\n";
|
|
250
|
+
}
|
|
251
|
+
function samePaths(left, right) {
|
|
252
|
+
return left.stateFile === right.stateFile && left.definitionFile === right.definitionFile && left.wrapperFile === right.wrapperFile && left.stdoutFile === right.stdoutFile && left.stderrFile === right.stderrFile;
|
|
253
|
+
}
|
|
254
|
+
function readReceiptIfPresent(path, env) {
|
|
255
|
+
return existsSync(path) ? readReceipt(path, env) : void 0;
|
|
447
256
|
}
|
|
448
|
-
function readReceipt(path) {
|
|
257
|
+
function readReceipt(path, env) {
|
|
449
258
|
try {
|
|
450
259
|
const value = JSON.parse(readFileSync(path, "utf8"));
|
|
451
260
|
if (value.version !== RECEIPT_VERSION || value.managedBy !== "dsh-update-status" || value.spec === void 0 || value.paths === void 0) throw new Error("not a dsh-update-status service receipt");
|
|
452
|
-
|
|
261
|
+
validateSpec(value.spec, "structural", servicePlatform(env.platform), env.home, envValue(env.env, "SYSTEMROOT"));
|
|
262
|
+
const canonical = pathsFor(value.spec);
|
|
263
|
+
if (canonical.stateFile !== path || !samePaths(value.paths, canonical)) throw new Error("managed service receipt paths do not match its service spec");
|
|
264
|
+
return {
|
|
265
|
+
version: RECEIPT_VERSION,
|
|
266
|
+
managedBy: "dsh-update-status",
|
|
267
|
+
spec: value.spec,
|
|
268
|
+
paths: canonical
|
|
269
|
+
};
|
|
453
270
|
} catch (error) {
|
|
454
271
|
throw new Error(`cannot read managed service receipt ${path}: ${error instanceof Error ? error.message : String(error)}`);
|
|
455
272
|
}
|
|
@@ -462,6 +279,7 @@ async function portIsFree(host, port) {
|
|
|
462
279
|
});
|
|
463
280
|
}
|
|
464
281
|
function serialisePlan(plan) {
|
|
282
|
+
const inheritedWindowsPath = plan.spec.platform === "win32" && plan.spec.servicePath === void 0;
|
|
465
283
|
return JSON.stringify({
|
|
466
284
|
platform: plan.spec.platform,
|
|
467
285
|
label: plan.spec.label,
|
|
@@ -471,6 +289,14 @@ function serialisePlan(plan) {
|
|
|
471
289
|
workspace: plan.spec.workspace,
|
|
472
290
|
host: plan.spec.host,
|
|
473
291
|
port: plan.spec.port,
|
|
292
|
+
nativeServiceCommand: nativeServiceCommandOf(plan.spec),
|
|
293
|
+
servicePath: {
|
|
294
|
+
source: inheritedWindowsPath ? "inherited logon environment (legacy receipt)" : plan.spec.servicePathSource ?? "minimal (legacy receipt)",
|
|
295
|
+
value: inheritedWindowsPath ? null : servicePathOf(plan.spec),
|
|
296
|
+
...plan.spec.servicePathCapturedAt === void 0 ? {} : { capturedAt: plan.spec.servicePathCapturedAt },
|
|
297
|
+
...plan.spec.servicePathExt === void 0 ? {} : { pathExt: plan.spec.servicePathExt },
|
|
298
|
+
warnings: plan.spec.servicePathWarnings ?? []
|
|
299
|
+
},
|
|
474
300
|
definitionFile: plan.paths.definitionFile,
|
|
475
301
|
wrapperFile: plan.paths.wrapperFile,
|
|
476
302
|
stateFile: plan.paths.stateFile,
|
|
@@ -483,42 +309,14 @@ function serialisePlan(plan) {
|
|
|
483
309
|
start: plan.startCommand,
|
|
484
310
|
status: plan.statusCommand,
|
|
485
311
|
stop: plan.stopCommand,
|
|
486
|
-
uninstall: plan.uninstallCommand
|
|
312
|
+
uninstall: plan.uninstallCommand,
|
|
313
|
+
recover: plan.recoverCommand
|
|
487
314
|
}
|
|
488
315
|
}, null, 2) + "\n";
|
|
489
316
|
}
|
|
490
|
-
function commandsForActivate(plan,
|
|
491
|
-
if (plan.spec.platform === "darwin") return [
|
|
492
|
-
|
|
493
|
-
"bootstrap",
|
|
494
|
-
`gui/${uid}`,
|
|
495
|
-
plan.paths.definitionFile
|
|
496
|
-
]];
|
|
497
|
-
if (plan.spec.platform === "linux") return [[
|
|
498
|
-
"systemctl",
|
|
499
|
-
"--user",
|
|
500
|
-
"daemon-reload"
|
|
501
|
-
], [
|
|
502
|
-
"systemctl",
|
|
503
|
-
"--user",
|
|
504
|
-
"enable",
|
|
505
|
-
"--now",
|
|
506
|
-
"dsh-update-status-web.service"
|
|
507
|
-
]];
|
|
508
|
-
return [[
|
|
509
|
-
"schtasks.exe",
|
|
510
|
-
"/Create",
|
|
511
|
-
"/TN",
|
|
512
|
-
"DSH Update Status Web",
|
|
513
|
-
"/XML",
|
|
514
|
-
plan.paths.definitionFile,
|
|
515
|
-
"/F"
|
|
516
|
-
], [
|
|
517
|
-
"schtasks.exe",
|
|
518
|
-
"/Run",
|
|
519
|
-
"/TN",
|
|
520
|
-
"DSH Update Status Web"
|
|
521
|
-
]];
|
|
317
|
+
function commandsForActivate(plan, _uid) {
|
|
318
|
+
if (plan.spec.platform === "darwin") return [plan.installCommand];
|
|
319
|
+
return [plan.installCommand, plan.startCommand];
|
|
522
320
|
}
|
|
523
321
|
function invoke(command, run) {
|
|
524
322
|
const [file, ...args] = command;
|
|
@@ -540,7 +338,8 @@ function preflight(spec) {
|
|
|
540
338
|
env: {
|
|
541
339
|
HOME: spec.home,
|
|
542
340
|
...spec.dshHome === void 0 ? {} : { DSH_HOME: spec.dshHome },
|
|
543
|
-
PATH:
|
|
341
|
+
PATH: servicePathOf(spec),
|
|
342
|
+
...spec.platform === "win32" && spec.servicePathExt !== void 0 ? { PATHEXT: spec.servicePathExt } : {}
|
|
544
343
|
}
|
|
545
344
|
});
|
|
546
345
|
if (result.status !== 0) {
|
|
@@ -548,6 +347,32 @@ function preflight(spec) {
|
|
|
548
347
|
throw new Error(`DSH boot-free preflight failed; no service definition was written: ${diagnostic}`);
|
|
549
348
|
}
|
|
550
349
|
}
|
|
350
|
+
const VALUE_OPTIONS = /* @__PURE__ */ new Set([
|
|
351
|
+
"--profile",
|
|
352
|
+
"--workspace",
|
|
353
|
+
"--dsh-home",
|
|
354
|
+
"--host",
|
|
355
|
+
"--port",
|
|
356
|
+
"--node",
|
|
357
|
+
"--dsh",
|
|
358
|
+
"--log-dir",
|
|
359
|
+
"--path-source",
|
|
360
|
+
"--service-path",
|
|
361
|
+
"--service-pathext"
|
|
362
|
+
]);
|
|
363
|
+
function assertKnownOptions(args) {
|
|
364
|
+
for (let index = 0; index < args.length; index += 1) {
|
|
365
|
+
const value = args[index];
|
|
366
|
+
if (value === "--dry-run") continue;
|
|
367
|
+
if (!VALUE_OPTIONS.has(value)) throw new Error(`unknown option: ${value}`);
|
|
368
|
+
const next = args[index + 1];
|
|
369
|
+
if (next === void 0 || next.startsWith("--")) throw new Error(`missing value after ${value}`);
|
|
370
|
+
index += 1;
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
function warnAboutPath(spec) {
|
|
374
|
+
for (const warning of spec.servicePathWarnings ?? []) process.stderr.write(`Warning: ${warning}\n`);
|
|
375
|
+
}
|
|
551
376
|
async function main(argv = process.argv.slice(2), env = environment(), run = runner, hooks = {}) {
|
|
552
377
|
const [action, ...rawArgs] = argv;
|
|
553
378
|
if (action === void 0 || action === "--help" || action === "-h") {
|
|
@@ -565,28 +390,36 @@ async function main(argv = process.argv.slice(2), env = environment(), run = run
|
|
|
565
390
|
process.stderr.write(`Unknown command: ${action}\n${usage()}`);
|
|
566
391
|
return 2;
|
|
567
392
|
}
|
|
393
|
+
try {
|
|
394
|
+
assertKnownOptions(rawArgs);
|
|
395
|
+
} catch (error) {
|
|
396
|
+
process.stderr.write(`dsh-update-status-service: ${error instanceof Error ? error.message : String(error)}\n`);
|
|
397
|
+
return 2;
|
|
398
|
+
}
|
|
568
399
|
const dryRun = rawArgs.includes("--dry-run") || action === "plan";
|
|
569
400
|
const args = rawArgs.filter((value) => value !== "--dry-run");
|
|
570
401
|
try {
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
402
|
+
if (dryRun || action === "install") {
|
|
403
|
+
const spec = specFromArgs(args, env);
|
|
404
|
+
validateSpec(spec, "launch", servicePlatform(env.platform), env.home, envValue(env.env, "SYSTEMROOT"));
|
|
405
|
+
const plan = planFor(spec, env.uid ?? 0);
|
|
406
|
+
if (dryRun) {
|
|
407
|
+
process.stdout.write(serialisePlan(plan));
|
|
408
|
+
return 0;
|
|
409
|
+
}
|
|
410
|
+
process.stdout.write(`Service PATH (${spec.servicePathSource ?? "minimal"}): ${servicePathOf(spec)}\n`);
|
|
411
|
+
warnAboutPath(spec);
|
|
579
412
|
(hooks.preflight ?? preflight)(spec);
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
413
|
+
const previous = readReceiptIfPresent(plan.paths.stateFile, env);
|
|
414
|
+
ensureReplaceable(plan.paths.definitionFile, plan.definition, receiptOwnsPath(previous, plan.paths.definitionFile));
|
|
415
|
+
if (plan.wrapper !== void 0 && plan.paths.wrapperFile !== void 0) ensureReplaceable(plan.paths.wrapperFile, plan.wrapper, receiptOwnsPath(previous, plan.paths.wrapperFile));
|
|
416
|
+
const receiptText = receiptTextOf({
|
|
583
417
|
version: RECEIPT_VERSION,
|
|
584
418
|
managedBy: "dsh-update-status",
|
|
585
419
|
spec,
|
|
586
420
|
paths: plan.paths
|
|
587
|
-
};
|
|
588
|
-
|
|
589
|
-
ensureManagedTarget(plan.paths.stateFile, receiptText);
|
|
421
|
+
});
|
|
422
|
+
ensureReplaceable(plan.paths.stateFile, receiptText, receiptOwnsPath(previous, plan.paths.stateFile));
|
|
590
423
|
mkdirSync(spec.logDir, {
|
|
591
424
|
recursive: true,
|
|
592
425
|
mode: 448
|
|
@@ -597,11 +430,19 @@ async function main(argv = process.argv.slice(2), env = environment(), run = run
|
|
|
597
430
|
process.stdout.write(`Staged user service definition: ${plan.paths.definitionFile}\nNo service has started. Stop your terminal-started DSH, then run activate.\n`);
|
|
598
431
|
return 0;
|
|
599
432
|
}
|
|
600
|
-
const receipt = readReceipt(
|
|
433
|
+
const receipt = readReceipt(serviceStateFileFromArgs(args, env), env);
|
|
434
|
+
const plan = planFor(receipt.spec, env.uid ?? 0);
|
|
435
|
+
if (action === "activate") validateSpec(receipt.spec, "launch", servicePlatform(env.platform), env.home, envValue(env.env, "SYSTEMROOT"));
|
|
601
436
|
if (action === "activate") {
|
|
602
437
|
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
438
|
for (const command of commandsForActivate(plan, env.uid ?? 0)) invoke(command, run);
|
|
604
|
-
|
|
439
|
+
const verification = await (hooks.verify ?? ((installed) => verifyActivatedService(installed, {
|
|
440
|
+
run,
|
|
441
|
+
uid: env.uid ?? 0
|
|
442
|
+
})))(receipt.spec);
|
|
443
|
+
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.`);
|
|
444
|
+
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.`);
|
|
445
|
+
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
446
|
return 0;
|
|
606
447
|
}
|
|
607
448
|
if (action === "status") {
|
|
@@ -617,9 +458,9 @@ async function main(argv = process.argv.slice(2), env = environment(), run = run
|
|
|
617
458
|
}
|
|
618
459
|
const result = run(plan.uninstallCommand[0], plan.uninstallCommand.slice(1));
|
|
619
460
|
if (result.status !== 0) process.stderr.write(`Warning: ${result.stderr || result.stdout || "service was not loaded"}\n`);
|
|
620
|
-
rmSync(
|
|
621
|
-
if (
|
|
622
|
-
rmSync(
|
|
461
|
+
rmSync(plan.paths.definitionFile, { force: true });
|
|
462
|
+
if (plan.paths.wrapperFile !== void 0) rmSync(plan.paths.wrapperFile, { force: true });
|
|
463
|
+
rmSync(plan.paths.stateFile, { force: true });
|
|
623
464
|
process.stdout.write("Removed the managed user-service definition. Logs were preserved.\n");
|
|
624
465
|
return 0;
|
|
625
466
|
} catch (error) {
|
|
@@ -631,4 +472,4 @@ if (import.meta.url === new URL(process.argv[1] ?? "", "file:").href) main().the
|
|
|
631
472
|
process.exitCode = code;
|
|
632
473
|
});
|
|
633
474
|
//#endregion
|
|
634
|
-
export { main };
|
|
475
|
+
export { main, serialisePlan };
|