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.
Files changed (47) hide show
  1. package/README.md +460 -12
  2. package/assets/screenshot-1.png +0 -0
  3. package/assets/screenshot-2.png +0 -0
  4. package/assets/screenshot-3.png +0 -0
  5. package/assets/screenshot-4.png +0 -0
  6. package/assets/screenshot-5.png +0 -0
  7. package/client.js +6329 -3863
  8. package/cordis.patch.yml +44 -1
  9. package/docs/README.zh.md +297 -9
  10. package/docs/platform-matrix.md +92 -0
  11. package/docs/rescue-drill.md +606 -0
  12. package/index.js +2867 -199
  13. package/lib/access.js +212 -0
  14. package/lib/autostart-advice.js +198 -0
  15. package/lib/backup.js +131 -4
  16. package/lib/blockers.js +228 -0
  17. package/lib/checkpoint.js +147 -0
  18. package/lib/diagnostics.js +188 -0
  19. package/lib/health.js +264 -0
  20. package/lib/host-kind.js +145 -0
  21. package/lib/node-runtime.js +144 -0
  22. package/lib/rescue-paths.js +253 -0
  23. package/lib/rescue.js +596 -0
  24. package/lib/restart-verdict.js +327 -0
  25. package/lib/restart.js +823 -56
  26. package/lib/scheduler-capability.js +300 -0
  27. package/lib/sleep.js +46 -49
  28. package/lib/startup-mode.js +491 -0
  29. package/lib/supervisor-config.js +735 -0
  30. package/lib/supervisor-install.js +557 -0
  31. package/lib/supervisor.js +1276 -0
  32. package/lib/watchdog-arbiter.js +117 -0
  33. package/lib/watchdog-schedule.js +310 -0
  34. package/lib/watchdog-tier.js +182 -0
  35. package/lib/watchdog.js +479 -0
  36. package/locale/en.json +274 -64
  37. package/locale/zh.json +274 -64
  38. package/package.json +7 -2
  39. package/screenshots.json +6 -4
  40. package/scripts/README.md +65 -0
  41. package/scripts/drill-rescue.ps1 +508 -0
  42. package/scripts/emergency-disable.cmd +27 -0
  43. package/scripts/emergency-toggle.ps1 +373 -0
  44. package/scripts/restore-profile.cmd +44 -0
  45. package/scripts/restore-profile.ps1 +608 -0
  46. package/scripts/run-tests.ps1 +46 -0
  47. 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
+ }