dsh-autorestart 0.7.0 → 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 +137 -6
- package/client.js +387 -0
- package/cordis.patch.yml +7 -0
- package/docs/README.zh.md +83 -5
- package/docs/platform-matrix.md +92 -0
- package/docs/rescue-drill.md +606 -0
- package/index.js +598 -3
- package/lib/diagnostics.js +8 -0
- package/lib/rescue-paths.js +253 -0
- package/lib/rescue.js +596 -0
- package/lib/scheduler-capability.js +300 -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 +28 -1
- package/locale/zh.json +28 -1
- package/package.json +1 -1
- package/scripts/README.md +65 -0
- package/scripts/drill-rescue.ps1 +508 -0
- package/scripts/emergency-toggle.ps1 +136 -65
- package/scripts/restore-profile.cmd +44 -0
- package/scripts/restore-profile.ps1 +608 -0
- package/scripts/run-tests.ps1 +1 -1
|
@@ -0,0 +1,310 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* REGISTERING THE WATCHDOG — one way per tier.
|
|
3
|
+
*
|
|
4
|
+
* The tiers differ in ONE thing only: how the watchdog is made to run later.
|
|
5
|
+
* Everything after that (wait, re-check, restore, start again, report) is the
|
|
6
|
+
* same code for every tier — see lib/watchdog.js.
|
|
7
|
+
*
|
|
8
|
+
* Each registration says what it did:
|
|
9
|
+
*
|
|
10
|
+
* - the Windows task is read back after it was registered (and deleted
|
|
11
|
+
* completely afterwards);
|
|
12
|
+
* - the systemd unit is reset (`reset-failed`) when it is over, so it does
|
|
13
|
+
* not lie around as a failed unit;
|
|
14
|
+
* - the detached process writes its pid into the state directory, and the
|
|
15
|
+
* process is READ BACK to prove that it is really running.
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
import fs from 'node:fs';
|
|
19
|
+
import path from 'node:path';
|
|
20
|
+
|
|
21
|
+
import { defaultExec, TIERS } from './watchdog-tier.js';
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* The XML of a one-off Windows task.
|
|
25
|
+
*
|
|
26
|
+
* Two things are easy to get wrong, and both are learned the hard way:
|
|
27
|
+
*
|
|
28
|
+
* - a one-off task needs an `EndBoundary`: without it the task has no end
|
|
29
|
+
* and the registration is refused;
|
|
30
|
+
* - `MultipleInstancesPolicy` must be `IgnoreNew`: a second instance of the
|
|
31
|
+
* watchdog would fight the first one over the same lock.
|
|
32
|
+
*
|
|
33
|
+
* @param {{ startIso: string, endIso: string, command: string, description?: string }} options
|
|
34
|
+
* @returns {string}
|
|
35
|
+
*/
|
|
36
|
+
export function buildScheduledTaskXml({ startIso, endIso, command, description = 'dsh-autorestart watchdog' }) {
|
|
37
|
+
const escape = (value) => String(value ?? '')
|
|
38
|
+
.replace(/&/g, '&')
|
|
39
|
+
.replace(/</g, '<')
|
|
40
|
+
.replace(/>/g, '>')
|
|
41
|
+
.replace(/"/g, '"');
|
|
42
|
+
return [
|
|
43
|
+
'<?xml version="1.0" encoding="UTF-16"?>',
|
|
44
|
+
'<Task version="1.4" xmlns="http://schemas.microsoft.com/windows/2004/02/mit/task">',
|
|
45
|
+
' <RegistrationInfo>',
|
|
46
|
+
' <Description>' + escape(description) + '</Description>',
|
|
47
|
+
' </RegistrationInfo>',
|
|
48
|
+
' <Triggers>',
|
|
49
|
+
' <TimeTrigger>',
|
|
50
|
+
' <StartBoundary>' + escape(startIso) + '</StartBoundary>',
|
|
51
|
+
' <EndBoundary>' + escape(endIso) + '</EndBoundary>',
|
|
52
|
+
' <Enabled>true</Enabled>',
|
|
53
|
+
' </TimeTrigger>',
|
|
54
|
+
' </Triggers>',
|
|
55
|
+
' <Principals>',
|
|
56
|
+
' <Principal id="Author">',
|
|
57
|
+
' <LogonType>InteractiveToken</LogonType>',
|
|
58
|
+
' <RunLevel>LeastPrivilege</RunLevel>',
|
|
59
|
+
' </Principal>',
|
|
60
|
+
' </Principals>',
|
|
61
|
+
' <Settings>',
|
|
62
|
+
' <MultipleInstancesPolicy>IgnoreNew</MultipleInstancesPolicy>',
|
|
63
|
+
' <DisallowStartIfOnBatteries>false</DisallowStartIfOnBatteries>',
|
|
64
|
+
' <StopIfGoingOnBatteries>false</StopIfGoingOnBatteries>',
|
|
65
|
+
' <Enabled>true</Enabled>',
|
|
66
|
+
' <ExecutionTimeLimit>PT1H</ExecutionTimeLimit>',
|
|
67
|
+
' </Settings>',
|
|
68
|
+
' <Actions Context="Author">',
|
|
69
|
+
' <Exec>',
|
|
70
|
+
' <Command>cmd</Command>',
|
|
71
|
+
' <Arguments>/c ' + escape(command) + '</Arguments>',
|
|
72
|
+
' </Exec>',
|
|
73
|
+
' </Actions>',
|
|
74
|
+
'</Task>',
|
|
75
|
+
'',
|
|
76
|
+
].join('\r\n');
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Register the one-off Windows task and read it back.
|
|
81
|
+
*
|
|
82
|
+
* @param {{
|
|
83
|
+
* taskName: string,
|
|
84
|
+
* startIso: string,
|
|
85
|
+
* endIso: string,
|
|
86
|
+
* command: string,
|
|
87
|
+
* xmlPath: string,
|
|
88
|
+
* exec?: Function,
|
|
89
|
+
* writeFile?: Function,
|
|
90
|
+
* removeFile?: Function,
|
|
91
|
+
* }} options
|
|
92
|
+
* @returns {{ ok: boolean, taskName: string, error: string|null, steps: Array<object> }}
|
|
93
|
+
*/
|
|
94
|
+
export function registerScheduledTask({
|
|
95
|
+
taskName,
|
|
96
|
+
startIso,
|
|
97
|
+
endIso,
|
|
98
|
+
command,
|
|
99
|
+
xmlPath,
|
|
100
|
+
exec = defaultExec,
|
|
101
|
+
writeFile = (target, content) => fs.writeFileSync(target, content, 'utf8'),
|
|
102
|
+
removeFile = (target) => fs.rmSync(target, { force: true }),
|
|
103
|
+
}) {
|
|
104
|
+
const steps = [];
|
|
105
|
+
const step = (kind, ok, detail) => {
|
|
106
|
+
steps.push({ kind, ok, detail: detail === undefined ? null : detail });
|
|
107
|
+
return ok;
|
|
108
|
+
};
|
|
109
|
+
const xml = buildScheduledTaskXml({ startIso, endIso, command });
|
|
110
|
+
try {
|
|
111
|
+
writeFile(xmlPath, xml);
|
|
112
|
+
} catch (error) {
|
|
113
|
+
step('write-xml', false, error && error.message ? error.message : String(error));
|
|
114
|
+
return { ok: false, taskName, error: 'the task description could not be written', steps };
|
|
115
|
+
}
|
|
116
|
+
step('write-xml', true, xmlPath);
|
|
117
|
+
const created = exec('schtasks', ['/Create', '/TN', taskName, '/XML', xmlPath, '/F']);
|
|
118
|
+
if (!step('create', created.ok, created.ok ? 'the task was registered' : created.error)) {
|
|
119
|
+
removeFile(xmlPath);
|
|
120
|
+
return { ok: false, taskName, error: created.error || 'the task was refused', steps };
|
|
121
|
+
}
|
|
122
|
+
// The read back: "it answered" is not "it is there".
|
|
123
|
+
const query = exec('schtasks', ['/Query', '/TN', taskName, '/XML']);
|
|
124
|
+
if (!step('verify', query.ok, query.ok ? 'the task was read back' : query.error)) {
|
|
125
|
+
removeFile(xmlPath);
|
|
126
|
+
return { ok: false, taskName, error: query.error || 'the task cannot be read back', steps };
|
|
127
|
+
}
|
|
128
|
+
const start = /<StartBoundary>([^<]+)<\/StartBoundary>/i.exec(query.stdout || '');
|
|
129
|
+
const end = /<EndBoundary>([^<]+)<\/EndBoundary>/i.exec(query.stdout || '');
|
|
130
|
+
const triggerOk = start !== null && String(start[1]).slice(0, 16) === String(startIso).slice(0, 16);
|
|
131
|
+
if (!step('trigger', triggerOk, 'expected ' + startIso + ', got ' + (start ? start[1] : 'nothing'))) {
|
|
132
|
+
removeFile(xmlPath);
|
|
133
|
+
return { ok: false, taskName, error: 'the task was registered with another trigger', steps };
|
|
134
|
+
}
|
|
135
|
+
if (!step('end-boundary', end !== null, 'a one-off task needs an EndBoundary')) {
|
|
136
|
+
removeFile(xmlPath);
|
|
137
|
+
return { ok: false, taskName, error: 'the task has no end boundary', steps };
|
|
138
|
+
}
|
|
139
|
+
const ignoreNew = /<MultipleInstancesPolicy>IgnoreNew<\/MultipleInstancesPolicy>/i.test(query.stdout || '');
|
|
140
|
+
step('ignore-new', ignoreNew, 'a second instance must not fight the first one');
|
|
141
|
+
removeFile(xmlPath);
|
|
142
|
+
return { ok: true, taskName, error: null, steps };
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* Delete the one-off Windows task completely.
|
|
147
|
+
*
|
|
148
|
+
* @param {{ taskName: string, exec?: Function }} options
|
|
149
|
+
* @returns {{ ok: boolean, error: string|null }}
|
|
150
|
+
*/
|
|
151
|
+
export function removeScheduledTask({ taskName, exec = defaultExec }) {
|
|
152
|
+
const answer = exec('schtasks', ['/Delete', '/TN', taskName, '/F']);
|
|
153
|
+
return { ok: answer.ok === true, error: answer.ok ? null : (answer.error || 'the task could not be deleted') };
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* The local time of a moment, in the format the task XML wants.
|
|
158
|
+
*
|
|
159
|
+
* A one-off task is triggered by a LOCAL time: an ISO time with a `Z` would
|
|
160
|
+
* be read as UTC and the task would fire hours later (or never).
|
|
161
|
+
*
|
|
162
|
+
* @param {Date|number} when
|
|
163
|
+
* @returns {string}
|
|
164
|
+
*/
|
|
165
|
+
export function localIso(when) {
|
|
166
|
+
const date = when instanceof Date ? when : new Date(Number(when) || Date.now());
|
|
167
|
+
const pad = (value) => String(value).padStart(2, '0');
|
|
168
|
+
return date.getFullYear()
|
|
169
|
+
+ '-' + pad(date.getMonth() + 1)
|
|
170
|
+
+ '-' + pad(date.getDate())
|
|
171
|
+
+ 'T' + pad(date.getHours())
|
|
172
|
+
+ ':' + pad(date.getMinutes())
|
|
173
|
+
+ ':' + pad(date.getSeconds());
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
/**
|
|
177
|
+
* Register a systemd user timer.
|
|
178
|
+
*
|
|
179
|
+
* @param {{ unitId: string, delaySeconds: number, command: string[], exec?: Function }} options
|
|
180
|
+
* @returns {{ ok: boolean, unitId: string, error: string|null }}
|
|
181
|
+
*/
|
|
182
|
+
export function registerSystemdRun({ unitId, delaySeconds, command, exec = defaultExec }) {
|
|
183
|
+
const seconds = Math.max(1, Math.trunc(Number(delaySeconds) || 1));
|
|
184
|
+
const argv = Array.isArray(command) ? command.map((entry) => String(entry)) : [String(command ?? '')];
|
|
185
|
+
const answer = exec('systemd-run', [
|
|
186
|
+
'--user',
|
|
187
|
+
'--on-active=' + seconds + 's',
|
|
188
|
+
'--unit=' + unitId,
|
|
189
|
+
'--',
|
|
190
|
+
...argv,
|
|
191
|
+
]);
|
|
192
|
+
return { ok: answer.ok === true, unitId, error: answer.ok ? null : (answer.error || 'systemd-run refused') };
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
/**
|
|
196
|
+
* Clean the systemd unit up (a failed unit would stay in the list).
|
|
197
|
+
*
|
|
198
|
+
* @param {{ unitId: string, exec?: Function }} options
|
|
199
|
+
* @returns {{ ok: boolean, error: string|null }}
|
|
200
|
+
*/
|
|
201
|
+
export function cleanupSystemdUnit({ unitId, exec = defaultExec }) {
|
|
202
|
+
const answer = exec('systemctl', ['--user', 'reset-failed', unitId]);
|
|
203
|
+
return { ok: answer.ok === true, error: answer.ok ? null : (answer.error || null) };
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
/**
|
|
207
|
+
* Start the watchdog as a background process of our own.
|
|
208
|
+
*
|
|
209
|
+
* The pid is written into the state directory (so the process can be cleaned
|
|
210
|
+
* up later) and the process is READ BACK: a background job that died at once
|
|
211
|
+
* is not a watchdog.
|
|
212
|
+
*
|
|
213
|
+
* @param {{
|
|
214
|
+
* delaySeconds: number,
|
|
215
|
+
* command: string,
|
|
216
|
+
* pidPath: string,
|
|
217
|
+
* exec?: Function,
|
|
218
|
+
* writeFile?: Function,
|
|
219
|
+
* isAlive?: (pid: number) => boolean,
|
|
220
|
+
* }} options
|
|
221
|
+
* @returns {{ ok: boolean, pid: number, pidPath: string, error: string|null }}
|
|
222
|
+
*/
|
|
223
|
+
export function registerDetachedProcess({
|
|
224
|
+
delaySeconds,
|
|
225
|
+
command,
|
|
226
|
+
pidPath,
|
|
227
|
+
exec = defaultExec,
|
|
228
|
+
writeFile = (target, content) => fs.writeFileSync(target, content, 'utf8'),
|
|
229
|
+
isAlive = (pid) => {
|
|
230
|
+
try {
|
|
231
|
+
process.kill(pid, 0);
|
|
232
|
+
return true;
|
|
233
|
+
} catch {
|
|
234
|
+
return false;
|
|
235
|
+
}
|
|
236
|
+
},
|
|
237
|
+
}) {
|
|
238
|
+
const seconds = Math.max(0, Math.trunc(Number(delaySeconds) || 0));
|
|
239
|
+
// `setsid` detaches the process from this session: it survives the restart
|
|
240
|
+
// that is about to happen.
|
|
241
|
+
const shell = "setsid nohup sh -c 'sleep " + seconds + "; " + command + "' >/dev/null 2>&1 & echo $!";
|
|
242
|
+
const answer = exec('sh', ['-c', shell]);
|
|
243
|
+
if (!answer.ok) {
|
|
244
|
+
return { ok: false, pid: 0, pidPath, error: answer.error || 'the background process could not be started' };
|
|
245
|
+
}
|
|
246
|
+
const pid = Number.parseInt(String(answer.stdout || '').trim(), 10);
|
|
247
|
+
if (!Number.isFinite(pid) || pid <= 0) {
|
|
248
|
+
return { ok: false, pid: 0, pidPath, error: 'the background process did not report a pid' };
|
|
249
|
+
}
|
|
250
|
+
try {
|
|
251
|
+
writeFile(pidPath, String(pid) + '\n', 'utf8');
|
|
252
|
+
} catch {
|
|
253
|
+
// The pid file is a convenience; the watchdog itself is running.
|
|
254
|
+
}
|
|
255
|
+
if (!isAlive(pid)) {
|
|
256
|
+
return { ok: false, pid, pidPath, error: 'the background process is not running' };
|
|
257
|
+
}
|
|
258
|
+
return { ok: true, pid, pidPath, error: null };
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
/**
|
|
262
|
+
* Stop the background process of our own (when it is still around).
|
|
263
|
+
*
|
|
264
|
+
* @param {{ pidPath: string, kill?: Function, readPid?: Function }} options
|
|
265
|
+
* @returns {{ ok: boolean, pid: number, error: string|null }}
|
|
266
|
+
*/
|
|
267
|
+
export function removeDetachedProcess({
|
|
268
|
+
pidPath,
|
|
269
|
+
kill = (pid) => { try { process.kill(pid, 'SIGTERM'); } catch { /* it is gone */ } },
|
|
270
|
+
readPid = () => {
|
|
271
|
+
try {
|
|
272
|
+
return Number.parseInt(fs.readFileSync(pidPath, 'utf8').trim(), 10);
|
|
273
|
+
} catch {
|
|
274
|
+
return 0;
|
|
275
|
+
}
|
|
276
|
+
},
|
|
277
|
+
}) {
|
|
278
|
+
const pid = readPid();
|
|
279
|
+
if (!Number.isFinite(pid) || pid <= 0) return { ok: true, pid: 0, error: null };
|
|
280
|
+
kill(pid);
|
|
281
|
+
try {
|
|
282
|
+
fs.rmSync(pidPath, { force: true });
|
|
283
|
+
} catch {
|
|
284
|
+
// The file is gone on the next run anyway.
|
|
285
|
+
}
|
|
286
|
+
return { ok: true, pid, error: null };
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
/**
|
|
290
|
+
* Remove the scheduling item of a tier.
|
|
291
|
+
*
|
|
292
|
+
* @param {{ tier: string, taskName?: string, unitId?: string, pidPath?: string, exec?: Function }} options
|
|
293
|
+
* @returns {{ ok: boolean, error: string|null }}
|
|
294
|
+
*/
|
|
295
|
+
export function removeScheduleItem({ tier, taskName = '', unitId = '', pidPath = '', exec = defaultExec }) {
|
|
296
|
+
if (tier === TIERS.SCHEDULED_TASK) return removeScheduledTask({ taskName, exec });
|
|
297
|
+
if (tier === TIERS.SYSTEMD_RUN) return cleanupSystemdUnit({ unitId, exec });
|
|
298
|
+
if (tier === TIERS.DETACHED_PROCESS) return removeDetachedProcess({ pidPath });
|
|
299
|
+
return { ok: true, error: null };
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
/**
|
|
303
|
+
* Where the pid of the background process is written.
|
|
304
|
+
*
|
|
305
|
+
* @param {string} stateDir
|
|
306
|
+
* @returns {string}
|
|
307
|
+
*/
|
|
308
|
+
export function detachedPidPath(stateDir) {
|
|
309
|
+
return path.join(stateDir, 'watchdog.pid');
|
|
310
|
+
}
|
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* HOW CAN THE WATCHDOG BE TRIGGERED ON THIS MACHINE?
|
|
3
|
+
*
|
|
4
|
+
* The watchdog is the safety net of the pre-restart backup: it waits for the
|
|
5
|
+
* relaunched host, and when the host never answers, it puts the snapshot back
|
|
6
|
+
* and starts the host again. To do that it has to RUN LATER, and "later" is
|
|
7
|
+
* the part that differs between the machines:
|
|
8
|
+
*
|
|
9
|
+
* 1. `scheduled-task` — a one-off Windows scheduled task (the desktop app
|
|
10
|
+
* and `dsh web` on Windows);
|
|
11
|
+
* 2. `systemd-run` — a systemd user timer (`dsh web` on Linux, WSL with
|
|
12
|
+
* systemd);
|
|
13
|
+
* 3. `detached-process` — a background process of our own (containers, WSL
|
|
14
|
+
* without systemd, everything else that can detach a
|
|
15
|
+
* process).
|
|
16
|
+
*
|
|
17
|
+
* When NONE of them is available there is no safety net at all — and the user
|
|
18
|
+
* has to be told, because "the automatic rollback" is a promise.
|
|
19
|
+
*
|
|
20
|
+
* ONE SOURCE OF TRUTH: `pickWatchdogTier` decides. The capability check of
|
|
21
|
+
* the plugin (what the settings page shows) and the watchdog that is actually
|
|
22
|
+
* registered ask THE SAME function, so the warning can never drift away from
|
|
23
|
+
* the behaviour.
|
|
24
|
+
*/
|
|
25
|
+
|
|
26
|
+
import { execFileSync } from 'node:child_process';
|
|
27
|
+
|
|
28
|
+
/** The ways a watchdog can be triggered. */
|
|
29
|
+
export const TIERS = Object.freeze({
|
|
30
|
+
SCHEDULED_TASK: 'scheduled-task',
|
|
31
|
+
SYSTEMD_RUN: 'systemd-run',
|
|
32
|
+
DETACHED_PROCESS: 'detached-process',
|
|
33
|
+
NONE: 'none',
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
/** What each tier means, for the report and for the settings page. */
|
|
37
|
+
export const TIER_LABELS = Object.freeze({
|
|
38
|
+
'scheduled-task': 'a one-off Windows scheduled task',
|
|
39
|
+
'systemd-run': 'a systemd user timer',
|
|
40
|
+
'detached-process': 'a detached background process',
|
|
41
|
+
none: 'nothing on this machine',
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Run a command and report whether it worked.
|
|
46
|
+
*
|
|
47
|
+
* @param {string} file
|
|
48
|
+
* @param {string[]} args
|
|
49
|
+
* @returns {{ ok: boolean, stdout: string, error: string|null }}
|
|
50
|
+
*/
|
|
51
|
+
export function defaultExec(file, args) {
|
|
52
|
+
try {
|
|
53
|
+
const stdout = execFileSync(file, args, { encoding: 'utf8', timeout: 15000, windowsHide: true });
|
|
54
|
+
return { ok: true, stdout: typeof stdout === 'string' ? stdout : '', error: null };
|
|
55
|
+
} catch (error) {
|
|
56
|
+
return {
|
|
57
|
+
ok: false,
|
|
58
|
+
stdout: error && typeof error.stdout === 'string' ? error.stdout : '',
|
|
59
|
+
error: error && error.message ? error.message : String(error),
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Is this command there?
|
|
66
|
+
*
|
|
67
|
+
* @param {Function} exec
|
|
68
|
+
* @param {string} file
|
|
69
|
+
* @param {string[]} args
|
|
70
|
+
* @returns {boolean}
|
|
71
|
+
*/
|
|
72
|
+
export function commandWorks(exec, file, args) {
|
|
73
|
+
const answer = exec(file, args);
|
|
74
|
+
return answer !== null && answer !== undefined && answer.ok === true;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* WHAT THIS MACHINE CAN DO.
|
|
79
|
+
*
|
|
80
|
+
* @param {{ platform?: string, exec?: Function }} [options]
|
|
81
|
+
* @returns {{ platform: string, hasSchtasks: boolean, hasSystemdRun: boolean, hasAt: boolean, canDetach: boolean }}
|
|
82
|
+
*/
|
|
83
|
+
export function detectWatchdogCapabilities({ platform = process.platform, exec = defaultExec } = {}) {
|
|
84
|
+
const windows = platform === 'win32';
|
|
85
|
+
return {
|
|
86
|
+
platform,
|
|
87
|
+
hasSchtasks: windows ? commandWorks(exec, 'schtasks', ['/?']) : false,
|
|
88
|
+
hasSystemdRun: windows ? false : commandWorks(exec, 'systemd-run', ['--version']),
|
|
89
|
+
hasAt: windows ? false : commandWorks(exec, 'at', ['--version']),
|
|
90
|
+
canDetach: windows ? false : commandWorks(exec, 'setsid', ['--version']),
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* WHICH TIER DOES THIS MACHINE GET?
|
|
96
|
+
*
|
|
97
|
+
* A pure function: it takes what the machine can do and says which tier to
|
|
98
|
+
* use — and WHY. The reason goes into the report, so a reader of
|
|
99
|
+
* `last-watchdog-report.json` knows what happened without guessing.
|
|
100
|
+
*
|
|
101
|
+
* @param {{
|
|
102
|
+
* platform?: string,
|
|
103
|
+
* hasSchtasks?: boolean,
|
|
104
|
+
* hasSystemdRun?: boolean,
|
|
105
|
+
* hasAt?: boolean,
|
|
106
|
+
* canDetach?: boolean,
|
|
107
|
+
* }} [options]
|
|
108
|
+
* @returns {{ tier: string, reason: string }}
|
|
109
|
+
*/
|
|
110
|
+
export function pickWatchdogTier({
|
|
111
|
+
platform = process.platform,
|
|
112
|
+
hasSchtasks = false,
|
|
113
|
+
hasSystemdRun = false,
|
|
114
|
+
hasAt = false,
|
|
115
|
+
canDetach = false,
|
|
116
|
+
} = {}) {
|
|
117
|
+
if (platform === 'win32') {
|
|
118
|
+
if (hasSchtasks) {
|
|
119
|
+
return {
|
|
120
|
+
tier: TIERS.SCHEDULED_TASK,
|
|
121
|
+
reason: 'this Windows can register a one-off scheduled task (schtasks)',
|
|
122
|
+
};
|
|
123
|
+
}
|
|
124
|
+
return {
|
|
125
|
+
tier: TIERS.NONE,
|
|
126
|
+
reason: 'this Windows cannot register a scheduled task (schtasks is not usable here)',
|
|
127
|
+
};
|
|
128
|
+
}
|
|
129
|
+
if (hasSystemdRun) {
|
|
130
|
+
return {
|
|
131
|
+
tier: TIERS.SYSTEMD_RUN,
|
|
132
|
+
reason: 'systemd user timers are available here (systemd-run)',
|
|
133
|
+
};
|
|
134
|
+
}
|
|
135
|
+
if (canDetach) {
|
|
136
|
+
return {
|
|
137
|
+
tier: TIERS.DETACHED_PROCESS,
|
|
138
|
+
reason: 'there is no systemd here'
|
|
139
|
+
+ (hasAt
|
|
140
|
+
? ' (the machine has at, but the plugin starts a process of its own: no daemon, nothing to configure)'
|
|
141
|
+
: ' (no systemd and no at): the plugin starts a process of its own'),
|
|
142
|
+
};
|
|
143
|
+
}
|
|
144
|
+
return {
|
|
145
|
+
tier: TIERS.NONE,
|
|
146
|
+
reason: 'this machine can neither schedule a task nor detach a process of its own',
|
|
147
|
+
};
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/**
|
|
151
|
+
* Does this tier give the user the automatic rollback?
|
|
152
|
+
*
|
|
153
|
+
* @param {string} tier
|
|
154
|
+
* @returns {boolean}
|
|
155
|
+
*/
|
|
156
|
+
export function tierProvidesRollback(tier) {
|
|
157
|
+
return typeof tier === 'string' && tier !== '' && tier !== TIERS.NONE;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/**
|
|
161
|
+
* The tier from a capability record that ALSO carries a real proof.
|
|
162
|
+
*
|
|
163
|
+
* On Windows the plugin does not trust "the command is there": it really
|
|
164
|
+
* registers a task and reads it back (see lib/scheduler-capability.js). That
|
|
165
|
+
* proof is the answer to "hasSchtasks" — the same answer the watchdog uses
|
|
166
|
+
* when it registers its own task.
|
|
167
|
+
*
|
|
168
|
+
* @param {{
|
|
169
|
+
* platform?: string,
|
|
170
|
+
* probeOk?: boolean|null,
|
|
171
|
+
* hasSystemdRun?: boolean,
|
|
172
|
+
* hasAt?: boolean,
|
|
173
|
+
* canDetach?: boolean,
|
|
174
|
+
* }} options
|
|
175
|
+
* @returns {{ tier: string, reason: string }}
|
|
176
|
+
*/
|
|
177
|
+
export function tierWithProof({ platform = process.platform, probeOk = null, hasSystemdRun = false, hasAt = false, canDetach = false } = {}) {
|
|
178
|
+
const hasSchtasks = platform === 'win32'
|
|
179
|
+
? probeOk === true
|
|
180
|
+
: false;
|
|
181
|
+
return pickWatchdogTier({ platform, hasSchtasks, hasSystemdRun, hasAt, canDetach });
|
|
182
|
+
}
|