dsh-autorestart 0.6.2 → 0.8.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/README.md +460 -12
- package/assets/screenshot-1.png +0 -0
- package/assets/screenshot-2.png +0 -0
- package/assets/screenshot-3.png +0 -0
- package/assets/screenshot-4.png +0 -0
- package/assets/screenshot-5.png +0 -0
- package/client.js +6329 -3863
- package/cordis.patch.yml +44 -1
- package/docs/README.zh.md +297 -9
- package/docs/platform-matrix.md +92 -0
- package/docs/rescue-drill.md +606 -0
- package/index.js +2867 -199
- package/lib/access.js +212 -0
- package/lib/autostart-advice.js +198 -0
- package/lib/backup.js +131 -4
- package/lib/blockers.js +228 -0
- package/lib/checkpoint.js +147 -0
- package/lib/diagnostics.js +188 -0
- package/lib/health.js +264 -0
- package/lib/host-kind.js +145 -0
- package/lib/node-runtime.js +144 -0
- package/lib/rescue-paths.js +253 -0
- package/lib/rescue.js +596 -0
- package/lib/restart-verdict.js +327 -0
- package/lib/restart.js +823 -56
- package/lib/scheduler-capability.js +300 -0
- package/lib/sleep.js +46 -49
- package/lib/startup-mode.js +491 -0
- package/lib/supervisor-config.js +735 -0
- package/lib/supervisor-install.js +557 -0
- package/lib/supervisor.js +1276 -0
- package/lib/watchdog-arbiter.js +117 -0
- package/lib/watchdog-schedule.js +310 -0
- package/lib/watchdog-tier.js +182 -0
- package/lib/watchdog.js +479 -0
- package/locale/en.json +274 -64
- package/locale/zh.json +274 -64
- package/package.json +7 -2
- package/screenshots.json +6 -4
- package/scripts/README.md +65 -0
- package/scripts/drill-rescue.ps1 +508 -0
- package/scripts/emergency-disable.cmd +27 -0
- package/scripts/emergency-toggle.ps1 +373 -0
- package/scripts/restore-profile.cmd +44 -0
- package/scripts/restore-profile.ps1 +608 -0
- package/scripts/run-tests.ps1 +46 -0
- package/supervisor/supervisor.js +884 -0
|
@@ -0,0 +1,557 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* dsh-autorestart — the platform "start at login" records of the supervisor.
|
|
3
|
+
*
|
|
4
|
+
* This module GENERATES the commands and the unit files. It never runs
|
|
5
|
+
* them: the running of a command is the job of `lib/supervisor.js`, and the
|
|
6
|
+
* tests check the generated commands against a fake executor — so the tests
|
|
7
|
+
* never register anything on the machine they run on.
|
|
8
|
+
*
|
|
9
|
+
* THE FORMS (and why):
|
|
10
|
+
*
|
|
11
|
+
* - **Windows / scheduled task** (`schtasks /SC ONLOGON /RL LIMITED`):
|
|
12
|
+
* the default. It needs NO administrator (a service would), it runs in the
|
|
13
|
+
* user's own session, and it starts when the user LOGS IN (not when the
|
|
14
|
+
* machine boots — a boot task would need `/ru SYSTEM`, and then the host
|
|
15
|
+
* would read the WRONG home directory).
|
|
16
|
+
* `/F` makes it idempotent: installing twice does not create a second
|
|
17
|
+
* task. `/ru`, `/NP` and `/IT` are deliberately NOT passed: `/NP` needs
|
|
18
|
+
* the "log on as batch job" right that ordinary users do not have, and
|
|
19
|
+
* `/IT` kills the task when the user logs off.
|
|
20
|
+
* - **Windows / startup folder** (a `.cmd` in the Startup folder): the
|
|
21
|
+
* DEGRADATION when the scheduled task cannot be created (some machines
|
|
22
|
+
* forbid ordinary users from creating tasks). Same zero rights, same
|
|
23
|
+
* "start at login" semantics.
|
|
24
|
+
* - **Linux / systemd user unit** (`~/.config/systemd/user/…`): the
|
|
25
|
+
* default on Linux and WSL. `systemctl --user` needs no administrator.
|
|
26
|
+
* - **Linux / systemd system unit** (`/etc/systemd/system/…`): only an
|
|
27
|
+
* EXPLICIT choice ("install it as a system service"), because it needs
|
|
28
|
+
* root — and it must carry `User=` so the host does not read root's home
|
|
29
|
+
* directory.
|
|
30
|
+
*
|
|
31
|
+
* A machine WITHOUT a system service manager (a container, a WSL without
|
|
32
|
+
* systemd) has no "start at login" at all: there is no form to register, so
|
|
33
|
+
* the plugin refuses and leaves the start command to the user.
|
|
34
|
+
*
|
|
35
|
+
* ESM only, Node builtins only.
|
|
36
|
+
*/
|
|
37
|
+
|
|
38
|
+
import fs from 'node:fs'
|
|
39
|
+
import os from 'node:os'
|
|
40
|
+
import path from 'node:path'
|
|
41
|
+
import { execFileSync } from 'node:child_process'
|
|
42
|
+
|
|
43
|
+
/** The name of the autostart record (a scheduled task, a systemd unit). */
|
|
44
|
+
export const SUPERVISOR_TASK_NAME = 'dsh-autorestart-web'
|
|
45
|
+
|
|
46
|
+
/** The kinds the plugin knows. */
|
|
47
|
+
export const SUPERVISOR_KINDS = Object.freeze([
|
|
48
|
+
'windows-schtasks',
|
|
49
|
+
'windows-startup',
|
|
50
|
+
'systemd-user',
|
|
51
|
+
'systemd-system',
|
|
52
|
+
])
|
|
53
|
+
|
|
54
|
+
/** @param {unknown} value */
|
|
55
|
+
function errorText(value) {
|
|
56
|
+
if (value && typeof value.message === 'string' && value.message) return value.message
|
|
57
|
+
return String(value)
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* The command line that starts the supervisor.
|
|
62
|
+
*
|
|
63
|
+
* @param {{ nodePath?: string, supervisorPath?: string, configPath?: string }} options
|
|
64
|
+
* @returns {string}
|
|
65
|
+
*/
|
|
66
|
+
export function supervisorCommandLine({ nodePath = '', supervisorPath = '', configPath = '' } = {}) {
|
|
67
|
+
const node = String(nodePath ?? '')
|
|
68
|
+
const supervisor = String(supervisorPath ?? '')
|
|
69
|
+
const config = String(configPath ?? '')
|
|
70
|
+
if (node === '' || supervisor === '') return ''
|
|
71
|
+
return `"${node}" "${supervisor}" --config "${config}"`
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* The arguments of `schtasks /Create` that register the supervisor.
|
|
76
|
+
*
|
|
77
|
+
* @param {{ nodePath?: string, supervisorPath?: string, configPath?: string, taskName?: string }} options
|
|
78
|
+
* @returns {string[]}
|
|
79
|
+
*/
|
|
80
|
+
export function windowsTaskCreateArgs({
|
|
81
|
+
nodePath = '',
|
|
82
|
+
supervisorPath = '',
|
|
83
|
+
configPath = '',
|
|
84
|
+
taskName = SUPERVISOR_TASK_NAME,
|
|
85
|
+
} = {}) {
|
|
86
|
+
const command = supervisorCommandLine({ nodePath, supervisorPath, configPath })
|
|
87
|
+
return [
|
|
88
|
+
'/Create',
|
|
89
|
+
'/TN', taskName,
|
|
90
|
+
'/SC', 'ONLOGON',
|
|
91
|
+
'/TR', command,
|
|
92
|
+
'/RL', 'LIMITED',
|
|
93
|
+
'/F',
|
|
94
|
+
]
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* The arguments of `schtasks /Query` that check the record.
|
|
99
|
+
*
|
|
100
|
+
* @param {{ taskName?: string }} [options]
|
|
101
|
+
* @returns {string[]}
|
|
102
|
+
*/
|
|
103
|
+
export function windowsTaskQueryArgs({ taskName = SUPERVISOR_TASK_NAME } = {}) {
|
|
104
|
+
return ['/Query', '/TN', taskName]
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* The arguments of `schtasks /Delete` that remove the record.
|
|
109
|
+
*
|
|
110
|
+
* @param {{ taskName?: string }} [options]
|
|
111
|
+
* @returns {string[]}
|
|
112
|
+
*/
|
|
113
|
+
export function windowsTaskDeleteArgs({ taskName = SUPERVISOR_TASK_NAME } = {}) {
|
|
114
|
+
return ['/Delete', '/TN', taskName, '/F']
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* The path of the `.cmd` in the startup folder (the Windows degradation).
|
|
119
|
+
*
|
|
120
|
+
* @param {{ startupDir?: string, taskName?: string }} [options]
|
|
121
|
+
* @returns {string}
|
|
122
|
+
*/
|
|
123
|
+
export function windowsStartupCmdPath({ startupDir = '', taskName = SUPERVISOR_TASK_NAME } = {}) {
|
|
124
|
+
const dir = String(startupDir ?? '')
|
|
125
|
+
if (dir === '') return ''
|
|
126
|
+
return path.join(dir, taskName + '.cmd')
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* The content of the `.cmd` in the startup folder.
|
|
131
|
+
*
|
|
132
|
+
* `start ""` detaches the supervisor from the console, so the window closes
|
|
133
|
+
* right away (a console parked in front of the user is not acceptable).
|
|
134
|
+
*
|
|
135
|
+
* @param {{ nodePath?: string, supervisorPath?: string, configPath?: string }} options
|
|
136
|
+
* @returns {string} the script text (CRLF lines)
|
|
137
|
+
*/
|
|
138
|
+
export function windowsStartupCmdContent({ nodePath = '', supervisorPath = '', configPath = '' } = {}) {
|
|
139
|
+
const command = supervisorCommandLine({ nodePath, supervisorPath, configPath })
|
|
140
|
+
return [
|
|
141
|
+
'@echo off',
|
|
142
|
+
'rem dsh-autorestart: start the supervisor when the user logs in.',
|
|
143
|
+
'start "" ' + command,
|
|
144
|
+
'',
|
|
145
|
+
].join('\r\n')
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* The path of the systemd USER unit.
|
|
150
|
+
*
|
|
151
|
+
* A systemd path is ALWAYS a POSIX path, so it is built with POSIX
|
|
152
|
+
* separators — even when the plugin runs on Windows (a WSL instance is
|
|
153
|
+
* configured from a Windows host more often than not).
|
|
154
|
+
*
|
|
155
|
+
* @param {{ homeDir?: string, taskName?: string }} [options]
|
|
156
|
+
* @returns {string}
|
|
157
|
+
*/
|
|
158
|
+
export function systemdUserUnitPath({ homeDir = '', taskName = SUPERVISOR_TASK_NAME } = {}) {
|
|
159
|
+
const home = String(homeDir ?? '').replace(/\\/g, '/').replace(/\/+$/, '')
|
|
160
|
+
if (home === '') return ''
|
|
161
|
+
return home + '/.config/systemd/user/' + taskName + '.service'
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/**
|
|
165
|
+
* The content of the systemd USER unit.
|
|
166
|
+
*
|
|
167
|
+
* `StartLimit*` belongs to `[Unit]` (systemd.unit(5)), not to `[Service]`.
|
|
168
|
+
* `Restart=always` only covers the supervisor itself (the supervisor never
|
|
169
|
+
* exits by itself), and `systemctl stop` does NOT restart it — so the
|
|
170
|
+
* uninstall is clean.
|
|
171
|
+
*
|
|
172
|
+
* @param {{ nodePath?: string, supervisorPath?: string, configPath?: string }} options
|
|
173
|
+
* @returns {string} the unit text
|
|
174
|
+
*/
|
|
175
|
+
export function systemdUserUnitContent({ nodePath = '', supervisorPath = '', configPath = '' } = {}) {
|
|
176
|
+
const command = supervisorCommandLine({ nodePath, supervisorPath, configPath })
|
|
177
|
+
return [
|
|
178
|
+
'[Unit]',
|
|
179
|
+
'Description=dsh-autorestart web supervisor',
|
|
180
|
+
'# StartLimit* must be written in the Unit section (systemd.unit(5)), not in the Service section.',
|
|
181
|
+
'StartLimitIntervalSec=60',
|
|
182
|
+
'StartLimitBurst=5',
|
|
183
|
+
'',
|
|
184
|
+
'[Service]',
|
|
185
|
+
'Type=exec',
|
|
186
|
+
'ExecStart=' + command,
|
|
187
|
+
'Restart=always',
|
|
188
|
+
'RestartSec=3',
|
|
189
|
+
'Environment=DSH_AUTORESTART_SUPERVISED=1',
|
|
190
|
+
'KillSignal=SIGTERM',
|
|
191
|
+
'TimeoutStopSec=30',
|
|
192
|
+
'',
|
|
193
|
+
'[Install]',
|
|
194
|
+
'WantedBy=default.target',
|
|
195
|
+
'',
|
|
196
|
+
].join('\n')
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
/**
|
|
200
|
+
* The content of the systemd SYSTEM unit.
|
|
201
|
+
*
|
|
202
|
+
* It needs `User=` (and the matching `HOME`): without it the unit would run
|
|
203
|
+
* as root and the host would read ROOT's home directory — a different
|
|
204
|
+
* profile, a different port file, a different everything.
|
|
205
|
+
*
|
|
206
|
+
* @param {{ nodePath?: string, supervisorPath?: string, configPath?: string, user?: string, homeDir?: string }} options
|
|
207
|
+
* @returns {string} the unit text
|
|
208
|
+
*/
|
|
209
|
+
export function systemdSystemUnitContent({
|
|
210
|
+
nodePath = '',
|
|
211
|
+
supervisorPath = '',
|
|
212
|
+
configPath = '',
|
|
213
|
+
user = '',
|
|
214
|
+
homeDir = '',
|
|
215
|
+
} = {}) {
|
|
216
|
+
const command = supervisorCommandLine({ nodePath, supervisorPath, configPath })
|
|
217
|
+
const lines = [
|
|
218
|
+
'[Unit]',
|
|
219
|
+
'Description=dsh-autorestart web supervisor',
|
|
220
|
+
'StartLimitIntervalSec=60',
|
|
221
|
+
'StartLimitBurst=5',
|
|
222
|
+
'',
|
|
223
|
+
'[Service]',
|
|
224
|
+
'Type=exec',
|
|
225
|
+
'ExecStart=' + command,
|
|
226
|
+
'Restart=always',
|
|
227
|
+
'RestartSec=3',
|
|
228
|
+
'Environment=DSH_AUTORESTART_SUPERVISED=1',
|
|
229
|
+
'KillSignal=SIGTERM',
|
|
230
|
+
'TimeoutStopSec=30',
|
|
231
|
+
]
|
|
232
|
+
if (user !== '') lines.push('User=' + user)
|
|
233
|
+
if (homeDir !== '') {
|
|
234
|
+
lines.push('Environment=HOME=' + homeDir)
|
|
235
|
+
lines.push('Environment=USER=' + (user || ''))
|
|
236
|
+
}
|
|
237
|
+
lines.push('', '[Install]', 'WantedBy=multi-user.target', '')
|
|
238
|
+
return lines.join('\n')
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
/**
|
|
242
|
+
* The arguments of the systemd commands the plugin uses.
|
|
243
|
+
*
|
|
244
|
+
* THE PROGRAM IS NOT PART OF THE ARGUMENTS. A step carries the program in
|
|
245
|
+
* `file` and the arguments in `args`, so `systemctl` must NOT appear in the
|
|
246
|
+
* arguments as well: `systemctl systemctl --user daemon-reload` is a command
|
|
247
|
+
* nobody can run ("Unknown command verb systemctl"), and it made the whole
|
|
248
|
+
* "install at login" fail on every Linux machine.
|
|
249
|
+
*
|
|
250
|
+
* @param {{ taskName?: string, system?: boolean }} [options]
|
|
251
|
+
* @returns {{
|
|
252
|
+
* daemonReload: string[],
|
|
253
|
+
* enable: string[],
|
|
254
|
+
* disable: string[],
|
|
255
|
+
* isActive: string[],
|
|
256
|
+
* }}
|
|
257
|
+
*/
|
|
258
|
+
export function systemdArgs({ taskName = SUPERVISOR_TASK_NAME, system = false } = {}) {
|
|
259
|
+
const scope = system ? ['--system'] : ['--user']
|
|
260
|
+
const unit = taskName + '.service'
|
|
261
|
+
return {
|
|
262
|
+
daemonReload: [...scope, 'daemon-reload'],
|
|
263
|
+
enable: [...scope, 'enable', '--now', unit],
|
|
264
|
+
disable: [...scope, 'disable', '--now', unit],
|
|
265
|
+
isActive: [...scope, 'is-active', '--quiet', unit],
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
/**
|
|
270
|
+
* The arguments of a command, without a repeated program name.
|
|
271
|
+
*
|
|
272
|
+
* A step carries the program in `file` and the arguments in `args`. When the
|
|
273
|
+
* arguments repeat the program (`['systemctl', '--user', …]` for the file
|
|
274
|
+
* `systemctl`), the command would run as `systemctl systemctl …` and fail.
|
|
275
|
+
* The repetition is dropped here, so such a mistake can never reach the
|
|
276
|
+
* machine — and the command line that is reported to the user is the one
|
|
277
|
+
* that really runs.
|
|
278
|
+
*
|
|
279
|
+
* @param {string} file
|
|
280
|
+
* @param {string[]} args
|
|
281
|
+
* @returns {string[]}
|
|
282
|
+
*/
|
|
283
|
+
export function normalizeCommandArgs(file, args) {
|
|
284
|
+
const list = Array.isArray(args) ? args.map(String) : []
|
|
285
|
+
if (list.length < 2) return list
|
|
286
|
+
const program = path.basename(String(file ?? '')).toLowerCase()
|
|
287
|
+
const first = path.basename(list[0]).toLowerCase()
|
|
288
|
+
if (program !== '' && first === program) return list.slice(1)
|
|
289
|
+
return list
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
/**
|
|
293
|
+
* The command line of a step (what the user can run by hand).
|
|
294
|
+
*
|
|
295
|
+
* @param {{ file?: string, args?: string[], path?: string }} step
|
|
296
|
+
* @returns {string}
|
|
297
|
+
*/
|
|
298
|
+
export function stepCommandLine(step) {
|
|
299
|
+
if (!step || typeof step !== 'object') return ''
|
|
300
|
+
if (typeof step.file === 'string' && step.file !== '') {
|
|
301
|
+
return [step.file, ...normalizeCommandArgs(step.file, step.args)].join(' ')
|
|
302
|
+
}
|
|
303
|
+
if (typeof step.path === 'string' && step.path !== '') return 'delete ' + step.path
|
|
304
|
+
return ''
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
/**
|
|
308
|
+
* Run one command and report what happened (never throws).
|
|
309
|
+
*
|
|
310
|
+
* @param {string} file
|
|
311
|
+
* @param {string[]} args
|
|
312
|
+
* @param {{ execFileSyncFn?: Function, timeoutMs?: number }} [options]
|
|
313
|
+
* @returns {{ ok: boolean, code: number|null, stdout: string, stderr: string, error: string|null }}
|
|
314
|
+
*/
|
|
315
|
+
export function runCommand(file, args, options = {}) {
|
|
316
|
+
const exec = typeof options.execFileSyncFn === 'function' ? options.execFileSyncFn : execFileSync
|
|
317
|
+
if (typeof exec !== 'function') {
|
|
318
|
+
return { ok: false, code: null, stdout: '', stderr: '', error: 'no executor' }
|
|
319
|
+
}
|
|
320
|
+
// The program is in `file`; the arguments must not repeat it.
|
|
321
|
+
const commandArgs = normalizeCommandArgs(file, args)
|
|
322
|
+
try {
|
|
323
|
+
// `stdio: ['ignore','pipe','pipe']` + a timeout: a command that asks for a
|
|
324
|
+
// password gets EOF on stdin instead of hanging for ever.
|
|
325
|
+
const stdout = exec(file, commandArgs, {
|
|
326
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
327
|
+
encoding: 'utf8',
|
|
328
|
+
timeout: Number.isFinite(Number(options.timeoutMs)) ? Number(options.timeoutMs) : 20000,
|
|
329
|
+
windowsHide: true,
|
|
330
|
+
})
|
|
331
|
+
return { ok: true, code: 0, stdout: String(stdout ?? ''), stderr: '', error: null }
|
|
332
|
+
} catch (error) {
|
|
333
|
+
const code = error && Number.isFinite(Number(error.status)) ? Number(error.status) : null
|
|
334
|
+
const stdout = error && typeof error.stdout === 'string' ? error.stdout : ''
|
|
335
|
+
const stderr = error && typeof error.stderr === 'string' ? error.stderr : ''
|
|
336
|
+
return { ok: false, code, stdout, stderr, error: errorText(error) }
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
/**
|
|
341
|
+
* What this machine offers.
|
|
342
|
+
*
|
|
343
|
+
* NOTHING is registered here: the detection only LOOKS. The permission of a
|
|
344
|
+
* scheduled task is probed by really creating it during the installation (it
|
|
345
|
+
* is idempotent) — the MS documentation is ambiguous about whether an
|
|
346
|
+
* ordinary user may create a task, so the plugin asks the machine instead of
|
|
347
|
+
* assuming.
|
|
348
|
+
*
|
|
349
|
+
* @param {{
|
|
350
|
+
* platform?: string,
|
|
351
|
+
* homeDir?: string,
|
|
352
|
+
* env?: object,
|
|
353
|
+
* existsSyncFn?: Function,
|
|
354
|
+
* execFileSyncFn?: Function,
|
|
355
|
+
* }} [options]
|
|
356
|
+
* @returns {{
|
|
357
|
+
* platform: string,
|
|
358
|
+
* schtasks: boolean,
|
|
359
|
+
* startupFolder: string|null,
|
|
360
|
+
* systemdUser: boolean,
|
|
361
|
+
* systemdSystem: boolean,
|
|
362
|
+
* container: boolean,
|
|
363
|
+
* reason: string,
|
|
364
|
+
* }}
|
|
365
|
+
*/
|
|
366
|
+
export function detectInitSystems(options = {}) {
|
|
367
|
+
const platform = typeof options.platform === 'string' ? options.platform : process.platform
|
|
368
|
+
const home = typeof options.homeDir === 'string' && options.homeDir !== '' ? options.homeDir : os.homedir()
|
|
369
|
+
const env = options.env && typeof options.env === 'object' ? options.env : process.env
|
|
370
|
+
const exists = typeof options.existsSyncFn === 'function' ? options.existsSyncFn : fs.existsSync
|
|
371
|
+
const exec = typeof options.execFileSyncFn === 'function' ? options.execFileSyncFn : execFileSync
|
|
372
|
+
|
|
373
|
+
if (platform === 'win32') {
|
|
374
|
+
const startupDir = env.APPDATA
|
|
375
|
+
? path.join(String(env.APPDATA), 'Microsoft', 'Windows', 'Start Menu', 'Programs', 'Startup')
|
|
376
|
+
: ''
|
|
377
|
+
const startupFolder = startupDir !== '' && exists(startupDir) ? startupDir : null
|
|
378
|
+
return {
|
|
379
|
+
platform: 'win32',
|
|
380
|
+
// Whether `schtasks` MAY create a task is NOT probed here: the
|
|
381
|
+
// documentation is ambiguous about ordinary users, so the INSTALLATION
|
|
382
|
+
// asks the machine (it runs the idempotent `/Create` and falls back when
|
|
383
|
+
// it is refused). What is reported here is whether the tool is there at
|
|
384
|
+
// all — and the fallback that exists when it is not.
|
|
385
|
+
schtasks: exec !== null,
|
|
386
|
+
startupFolder,
|
|
387
|
+
systemdUser: false,
|
|
388
|
+
systemdSystem: false,
|
|
389
|
+
container: false,
|
|
390
|
+
reason: startupFolder === null
|
|
391
|
+
? 'the startup folder was not found'
|
|
392
|
+
: 'a scheduled task is the default; the startup folder is the fallback',
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
// POSIX: is there a system service manager at all?
|
|
397
|
+
const systemdPresent = exists('/run/systemd/system')
|
|
398
|
+
let systemdUser = false
|
|
399
|
+
if (systemdPresent && exec !== null) {
|
|
400
|
+
const answer = runCommand('systemctl', ['--user', 'is-system-running'], { execFileSyncFn: exec })
|
|
401
|
+
// `is-system-running` answers "degraded" with a non-zero code when the
|
|
402
|
+
// manager is up but some unit failed — that still means the manager works.
|
|
403
|
+
systemdUser = answer.code !== null && answer.code <= 1
|
|
404
|
+
}
|
|
405
|
+
const container = exists('/.dockerenv') || exists('/run/.containerenv')
|
|
406
|
+
const reason = !systemdPresent
|
|
407
|
+
? 'this machine has no system service manager (a container, or a WSL without systemd)'
|
|
408
|
+
: (systemdUser
|
|
409
|
+
? 'a systemd user manager is available'
|
|
410
|
+
: 'systemd is present but the user manager is not reachable')
|
|
411
|
+
return {
|
|
412
|
+
platform,
|
|
413
|
+
schtasks: false,
|
|
414
|
+
startupFolder: null,
|
|
415
|
+
systemdUser,
|
|
416
|
+
systemdSystem: systemdPresent,
|
|
417
|
+
container,
|
|
418
|
+
reason,
|
|
419
|
+
}
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
/**
|
|
423
|
+
* The ordered list of steps that install the supervisor.
|
|
424
|
+
*
|
|
425
|
+
* The list is the CONTRACT: the installer walks it and stops at the first
|
|
426
|
+
* step that fails. The Windows list has a degradation built in (the
|
|
427
|
+
* scheduled task, then the startup folder), the POSIX list has none (there is
|
|
428
|
+
* no second way to register a user unit).
|
|
429
|
+
*
|
|
430
|
+
* @param {{
|
|
431
|
+
* platform?: string,
|
|
432
|
+
* kind?: string,
|
|
433
|
+
* nodePath?: string,
|
|
434
|
+
* supervisorPath?: string,
|
|
435
|
+
* configPath?: string,
|
|
436
|
+
* homeDir?: string,
|
|
437
|
+
* startupDir?: string,
|
|
438
|
+
* user?: string,
|
|
439
|
+
* taskName?: string,
|
|
440
|
+
* }} options
|
|
441
|
+
* @returns {Array<{ kind: string, description: string, file?: string, args?: string[], unit?: string, path?: string, nodePath?: string, command?: string }>}
|
|
442
|
+
*/
|
|
443
|
+
export function buildInstallSteps(options = {}) {
|
|
444
|
+
const platform = typeof options.platform === 'string' ? options.platform : process.platform
|
|
445
|
+
const taskName = typeof options.taskName === 'string' && options.taskName !== '' ? options.taskName : SUPERVISOR_TASK_NAME
|
|
446
|
+
const nodePath = String(options.nodePath ?? '')
|
|
447
|
+
const supervisorPath = String(options.supervisorPath ?? '')
|
|
448
|
+
const configPath = String(options.configPath ?? '')
|
|
449
|
+
const command = supervisorCommandLine({ nodePath, supervisorPath, configPath })
|
|
450
|
+
const steps = []
|
|
451
|
+
if (platform === 'win32') {
|
|
452
|
+
steps.push({
|
|
453
|
+
kind: 'windows-schtasks',
|
|
454
|
+
description: 'register a scheduled task that starts the supervisor at login',
|
|
455
|
+
file: 'schtasks',
|
|
456
|
+
args: windowsTaskCreateArgs({ nodePath, supervisorPath, configPath, taskName }),
|
|
457
|
+
// The interpreter and the command it starts: both are reported so the
|
|
458
|
+
// caller (and the tests) can see that the record starts a REAL node and
|
|
459
|
+
// not the application executable.
|
|
460
|
+
nodePath,
|
|
461
|
+
command,
|
|
462
|
+
})
|
|
463
|
+
steps.push({
|
|
464
|
+
kind: 'windows-startup',
|
|
465
|
+
description: 'put a .cmd into the startup folder (the fallback when tasks are not allowed)',
|
|
466
|
+
path: windowsStartupCmdPath({ startupDir: String(options.startupDir ?? ''), taskName }),
|
|
467
|
+
content: windowsStartupCmdContent({ nodePath, supervisorPath, configPath }),
|
|
468
|
+
nodePath,
|
|
469
|
+
command,
|
|
470
|
+
})
|
|
471
|
+
return steps
|
|
472
|
+
}
|
|
473
|
+
const unit = options.kind === 'systemd-system'
|
|
474
|
+
? systemdSystemUnitContent({ nodePath, supervisorPath, configPath, user: String(options.user ?? ''), homeDir: String(options.homeDir ?? '') })
|
|
475
|
+
: systemdUserUnitContent({ nodePath, supervisorPath, configPath })
|
|
476
|
+
const args = systemdArgs({ taskName, system: options.kind === 'systemd-system' })
|
|
477
|
+
steps.push({
|
|
478
|
+
kind: options.kind === 'systemd-system' ? 'systemd-system' : 'systemd-user',
|
|
479
|
+
description: 'write the systemd unit',
|
|
480
|
+
path: options.kind === 'systemd-system'
|
|
481
|
+
? '/etc/systemd/system/' + taskName + '.service'
|
|
482
|
+
: systemdUserUnitPath({ homeDir: String(options.homeDir ?? ''), taskName }),
|
|
483
|
+
content: unit,
|
|
484
|
+
nodePath,
|
|
485
|
+
command,
|
|
486
|
+
})
|
|
487
|
+
steps.push({ kind: 'daemon-reload', description: 'reload the systemd units', file: 'systemctl', args: args.daemonReload })
|
|
488
|
+
steps.push({ kind: 'enable', description: 'enable and start the unit', file: 'systemctl', args: args.enable })
|
|
489
|
+
steps.push({ kind: 'verify', description: 'check that the unit is active', file: 'systemctl', args: args.isActive })
|
|
490
|
+
return steps
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
/**
|
|
494
|
+
* The ordered list of steps that uninstall the supervisor.
|
|
495
|
+
*
|
|
496
|
+
* @param {{
|
|
497
|
+
* platform?: string,
|
|
498
|
+
* kind?: string,
|
|
499
|
+
* homeDir?: string,
|
|
500
|
+
* startupDir?: string,
|
|
501
|
+
* taskName?: string,
|
|
502
|
+
* }} [options]
|
|
503
|
+
* @returns {Array<{ kind: string, description: string, file?: string, args?: string[], path?: string }>}
|
|
504
|
+
*/
|
|
505
|
+
export function buildUninstallSteps(options = {}) {
|
|
506
|
+
const platform = typeof options.platform === 'string' ? options.platform : process.platform
|
|
507
|
+
const taskName = typeof options.taskName === 'string' && options.taskName !== '' ? options.taskName : SUPERVISOR_TASK_NAME
|
|
508
|
+
const steps = []
|
|
509
|
+
if (platform === 'win32') {
|
|
510
|
+
if (options.kind === 'windows-startup') {
|
|
511
|
+
steps.push({
|
|
512
|
+
kind: 'windows-startup',
|
|
513
|
+
description: 'remove the .cmd from the startup folder',
|
|
514
|
+
path: windowsStartupCmdPath({ startupDir: String(options.startupDir ?? ''), taskName }),
|
|
515
|
+
})
|
|
516
|
+
return steps
|
|
517
|
+
}
|
|
518
|
+
steps.push({
|
|
519
|
+
kind: 'windows-schtasks',
|
|
520
|
+
description: 'delete the scheduled task',
|
|
521
|
+
file: 'schtasks',
|
|
522
|
+
args: windowsTaskDeleteArgs({ taskName }),
|
|
523
|
+
})
|
|
524
|
+
return steps
|
|
525
|
+
}
|
|
526
|
+
const args = systemdArgs({ taskName, system: options.kind === 'systemd-system' })
|
|
527
|
+
steps.push({ kind: 'disable', description: 'stop and disable the unit', file: 'systemctl', args: args.disable })
|
|
528
|
+
steps.push({
|
|
529
|
+
kind: 'unit-file',
|
|
530
|
+
description: 'remove the unit file',
|
|
531
|
+
path: options.kind === 'systemd-system'
|
|
532
|
+
? '/etc/systemd/system/' + taskName + '.service'
|
|
533
|
+
: systemdUserUnitPath({ homeDir: String(options.homeDir ?? ''), taskName }),
|
|
534
|
+
})
|
|
535
|
+
steps.push({ kind: 'daemon-reload', description: 'reload the systemd units', file: 'systemctl', args: args.daemonReload })
|
|
536
|
+
return steps
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
/**
|
|
540
|
+
* The compose fragment for a machine without a service manager.
|
|
541
|
+
*
|
|
542
|
+
* A container has no "start at login": the only correct answer is the
|
|
543
|
+
* orchestrator's own restart policy.
|
|
544
|
+
*
|
|
545
|
+
* @param {{ nodePath?: string, supervisorPath?: string, configPath?: string }} options
|
|
546
|
+
* @returns {string}
|
|
547
|
+
*/
|
|
548
|
+
export function composeFragment({ nodePath = '', supervisorPath = '', configPath = '' } = {}) {
|
|
549
|
+
const command = supervisorCommandLine({ nodePath, supervisorPath, configPath })
|
|
550
|
+
return [
|
|
551
|
+
'services:',
|
|
552
|
+
' dsh:',
|
|
553
|
+
' image: <your dsh image>',
|
|
554
|
+
' command: ["' + String(nodePath) + '", "' + String(supervisorPath) + '", "--config", "' + String(configPath) + '"]',
|
|
555
|
+
' restart: always',
|
|
556
|
+
].join('\n')
|
|
557
|
+
}
|