dsh-autorestart 0.7.0 → 0.8.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.
@@ -0,0 +1,203 @@
1
+ /**
2
+ * dsh-autorestart watchdog: the one-off process that watches the relaunch.
3
+ *
4
+ * The tiers (a Windows scheduled task, a systemd user timer, a detached
5
+ * process of our own) differ only in HOW this file is made to run. What it
6
+ * does is always the same, and it lives in lib/watchdog.js:
7
+ *
8
+ * wait for the ready marker -> ask again -> is an instance alive? -> take
9
+ * the lock -> restore the snapshot -> start the host again -> report.
10
+ *
11
+ * This file only provides the parts that touch the machine: the health probe,
12
+ * the restore command, the start-again command, and the lock.
13
+ *
14
+ * It is ASCII on purpose (like every other script of this plugin): the
15
+ * console reads it with the code page of the machine.
16
+ */
17
+
18
+ import { execFileSync, spawn } from 'node:child_process';
19
+ import fs from 'node:fs';
20
+ import http from 'node:http';
21
+ import path from 'node:path';
22
+
23
+ import {
24
+ acquireLock,
25
+ LOCK_NAME,
26
+ readPending,
27
+ releaseLock,
28
+ restoreFromSnapshot,
29
+ runWatchdogFlow,
30
+ } from '../lib/watchdog.js';
31
+ import { removeScheduleItem } from '../lib/watchdog-schedule.js';
32
+
33
+ /**
34
+ * The value of a command line flag.
35
+ *
36
+ * @param {string[]} args
37
+ * @param {string} name
38
+ * @returns {string}
39
+ */
40
+ function valueOf(args, name) {
41
+ const index = args.indexOf(name);
42
+ if (index === -1) return '';
43
+ const next = args[index + 1];
44
+ return typeof next === 'string' && !next.startsWith('--') ? next : '';
45
+ }
46
+
47
+ /**
48
+ * Ask a health route whether it is alive.
49
+ *
50
+ * @param {string} url
51
+ * @param {number} timeoutMs
52
+ * @returns {Promise<boolean>}
53
+ */
54
+ function healthOk(url, timeoutMs = 5000) {
55
+ return new Promise((resolve) => {
56
+ let settled = false;
57
+ const done = (value) => {
58
+ if (settled) return;
59
+ settled = true;
60
+ resolve(value);
61
+ };
62
+ try {
63
+ const request = http.get(url, { timeout: timeoutMs }, (response) => {
64
+ let body = '';
65
+ response.on('data', (chunk) => { body += chunk; });
66
+ response.on('end', () => {
67
+ try {
68
+ const parsed = JSON.parse(body);
69
+ done(parsed && parsed.ok === true);
70
+ } catch {
71
+ done(false);
72
+ }
73
+ });
74
+ });
75
+ request.on('timeout', () => { request.destroy(); done(false); });
76
+ request.on('error', () => done(false));
77
+ } catch {
78
+ done(false);
79
+ }
80
+ });
81
+ }
82
+
83
+ /**
84
+ * Is an instance of the host alive?
85
+ *
86
+ * The NEW instance publishes its own port (the desktop app listens on a
87
+ * random port, so the port of the old one is worthless). When there is no
88
+ * record at all, the fallback URL of the host is asked.
89
+ *
90
+ * @param {object} pending
91
+ * @returns {Promise<boolean>}
92
+ */
93
+ async function probeAlive(pending) {
94
+ const record = (() => {
95
+ if (!pending.portFile) return null;
96
+ try {
97
+ return JSON.parse(fs.readFileSync(pending.portFile, 'utf8'));
98
+ } catch {
99
+ return null;
100
+ }
101
+ })();
102
+ if (record && record.bootId && record.port && record.bootId !== pending.expectedBootId) {
103
+ const url = 'http://127.0.0.1:' + record.port + (pending.healthPath || '/__dsh_autorestart/health');
104
+ if (await healthOk(url)) return true;
105
+ }
106
+ if (pending.healthUrl) {
107
+ if (await healthOk(pending.healthUrl)) return true;
108
+ }
109
+ return false;
110
+ }
111
+
112
+ /**
113
+ * Run a command and report whether it worked.
114
+ *
115
+ * @param {{ file?: string, args?: string[] }|null} command
116
+ * @returns {{ ok: boolean, error: string|null }}
117
+ */
118
+ function runCommand(command) {
119
+ if (!command || typeof command.file !== 'string' || command.file === '') {
120
+ return { ok: false, error: 'there is no command to run' };
121
+ }
122
+ try {
123
+ execFileSync(command.file, Array.isArray(command.args) ? command.args : [], {
124
+ timeout: 10 * 60 * 1000,
125
+ windowsHide: true,
126
+ stdio: 'ignore',
127
+ });
128
+ return { ok: true, error: null };
129
+ } catch (error) {
130
+ return { ok: false, error: error && error.message ? error.message : String(error) };
131
+ }
132
+ }
133
+
134
+ /**
135
+ * Start the host again (it must survive this process).
136
+ *
137
+ * @param {{ file?: string, args?: string[] }|null} command
138
+ * @returns {{ ok: boolean, error: string|null }}
139
+ */
140
+ function startAgain(command) {
141
+ if (!command || typeof command.file !== 'string' || command.file === '') {
142
+ return { ok: false, error: 'there is no command to start the host' };
143
+ }
144
+ try {
145
+ const child = spawn(command.file, Array.isArray(command.args) ? command.args : [], {
146
+ detached: true,
147
+ stdio: 'ignore',
148
+ windowsHide: true,
149
+ });
150
+ child.unref();
151
+ return { ok: true, error: null };
152
+ } catch (error) {
153
+ return { ok: false, error: error && error.message ? error.message : String(error) };
154
+ }
155
+ }
156
+
157
+ const args = process.argv.slice(2);
158
+ const stateDir = valueOf(args, '--state-dir') || process.env.DSH_WATCHDOG_STATE_DIR || '';
159
+ if (stateDir === '') {
160
+ console.error('dsh-autorestart watchdog: no state directory (--state-dir)');
161
+ process.exit(2);
162
+ }
163
+ const pending = readPending(stateDir);
164
+ if (!pending) {
165
+ // Nothing to watch: the plugin decided on its own, or another watchdog is
166
+ // already on it. The scheduling item still goes: a task that fires once and
167
+ // then stays in the task scheduler is garbage.
168
+ try {
169
+ removeScheduleItem({ tier: 'scheduled-task', taskName: 'dsh-autorestart-watchdog' });
170
+ removeScheduleItem({ tier: 'systemd-run', unitId: 'dsh-autorestart-watchdog' });
171
+ } catch {
172
+ // It was never there.
173
+ }
174
+ console.log('dsh-autorestart watchdog: no pending marker - nothing to watch');
175
+ process.exit(0);
176
+ }
177
+ const lockFile = path.join(stateDir, LOCK_NAME);
178
+
179
+ const report = await runWatchdogFlow({
180
+ pending,
181
+ stateDir,
182
+ deps: {
183
+ probeAlive: () => probeAlive(pending),
184
+ runRestore: () => (pending.restoreCommand
185
+ ? runCommand(pending.restoreCommand)
186
+ : restoreFromSnapshot({ snapshotDir: pending.snapshotDir, profileDir: pending.profileDir })),
187
+ runRelaunch: () => startAgain(pending.relaunchCommand),
188
+ acquireLock: () => acquireLock(lockFile),
189
+ releaseLock: () => releaseLock(lockFile),
190
+ removeSchedule: () => {
191
+ removeScheduleItem({
192
+ tier: pending.tier,
193
+ taskName: pending.taskName,
194
+ unitId: pending.unitId,
195
+ pidPath: pending.pidPath,
196
+ });
197
+ },
198
+ },
199
+ });
200
+
201
+ console.log('dsh-autorestart watchdog: ' + report.result + ' (tier ' + report.tier + ')');
202
+ if (report.error) console.log(' ' + report.error);
203
+ process.exit(report.result === 'failed' ? 1 : 0);