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
package/lib/diagnostics.js
CHANGED
|
@@ -173,6 +173,14 @@ export function buildDiagnostics(payload = {}) {
|
|
|
173
173
|
'last restart result: ' + redactSecrets(field(lastRestart, 'result', 'unknown')),
|
|
174
174
|
'last restart failed checks: ' + (failedChecks.length > 0 ? failedChecks.join(', ') : 'none'),
|
|
175
175
|
]
|
|
176
|
+
// THE WATCHDOG: which tier this machine uses, and what the last run did.
|
|
177
|
+
const watchdog = field(source, 'watchdog', {})
|
|
178
|
+
const watchdogReport = field(watchdog, 'report', {})
|
|
179
|
+
lines.push('watchdog tier: ' + redactSecrets(field(watchdog, 'tier', 'unknown')))
|
|
180
|
+
lines.push('watchdog reason: ' + redactSecrets(field(watchdog, 'tierReason', 'unknown')))
|
|
181
|
+
lines.push('watchdog available: ' + (field(watchdog, 'available', false) === true ? 'yes' : 'no'))
|
|
182
|
+
lines.push('watchdog last result: ' + redactSecrets(field(watchdogReport, 'result', 'never ran')))
|
|
183
|
+
lines.push('watchdog last tier: ' + redactSecrets(field(watchdogReport, 'tier', 'unknown')))
|
|
176
184
|
const stateDir = truncatePath(field(source, 'stateDir', ''))
|
|
177
185
|
if (stateDir !== '') lines.push('dsh home: ' + redactSecrets(stateDir))
|
|
178
186
|
lines.push('--- end ---')
|
|
@@ -0,0 +1,253 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Where the rescue scripts live, and the desktop shortcut that points at them.
|
|
3
|
+
*
|
|
4
|
+
* The rescue kit is for EVERYBODY who installed the plugin, not for the author:
|
|
5
|
+
* when DSH does not come up, the user cannot open a README — they need a path
|
|
6
|
+
* they can copy, and a file they can double-click on the desktop. So the
|
|
7
|
+
* settings page shows the two scripts where they really are, and the desktop
|
|
8
|
+
* shortcut is a file the user asks for.
|
|
9
|
+
*
|
|
10
|
+
* Everything here is pure (the file system is injected), so a test can pin it
|
|
11
|
+
* down without touching a real desktop.
|
|
12
|
+
*
|
|
13
|
+
* THE PATH IS NEVER GUESSED: it comes from the plugin directory the Host
|
|
14
|
+
* resolved for itself. Nothing here assumes `node_modules/dsh-autorestart`.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
import path from 'node:path';
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* The rescue scripts of each platform.
|
|
21
|
+
*
|
|
22
|
+
* A Windows user gets the `.cmd` files, a Linux/WSL/macOS user the `.sh`
|
|
23
|
+
* ones. The other platform's scripts are NOT offered: a path to a file that
|
|
24
|
+
* does not exist on this machine is worse than no path at all.
|
|
25
|
+
*/
|
|
26
|
+
export const RESCUE_SCRIPT_NAMES = Object.freeze({
|
|
27
|
+
win32: Object.freeze({ restoreProfile: 'restore-profile.cmd', emergencyDisable: 'emergency-disable.cmd' }),
|
|
28
|
+
posix: Object.freeze({ restoreProfile: 'restore-profile.sh', emergencyDisable: 'emergency-disable.sh' }),
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* The script names of a platform.
|
|
33
|
+
*
|
|
34
|
+
* @param {string} platform
|
|
35
|
+
* @returns {{ restoreProfile: string, emergencyDisable: string }}
|
|
36
|
+
*/
|
|
37
|
+
export function rescueScriptNames(platform) {
|
|
38
|
+
return platform === 'win32' ? RESCUE_SCRIPT_NAMES.win32 : RESCUE_SCRIPT_NAMES.posix;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/** The file the desktop shortcut is written as (the NAME may be Chinese). */
|
|
42
|
+
export const DESKTOP_SHORTCUT_NAME = 'DSH 出事了双击这里.cmd';
|
|
43
|
+
|
|
44
|
+
/** Does the path exist? (Injected so a test never touches the disk.) */
|
|
45
|
+
function defaultExists(target) {
|
|
46
|
+
try {
|
|
47
|
+
// eslint-disable-next-line no-undef
|
|
48
|
+
return require('node:fs').existsSync(target)
|
|
49
|
+
} catch {
|
|
50
|
+
return false
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* The directory that holds the scripts of a plugin directory.
|
|
56
|
+
*
|
|
57
|
+
* @param {string} pluginDir the directory of the plugin itself
|
|
58
|
+
* @returns {string}
|
|
59
|
+
*/
|
|
60
|
+
export function rescueScriptsDir(pluginDir) {
|
|
61
|
+
const dir = typeof pluginDir === 'string' ? pluginDir.trim() : '';
|
|
62
|
+
if (dir === '') return '';
|
|
63
|
+
return path.join(dir, 'scripts');
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* The absolute paths of the two rescue scripts.
|
|
68
|
+
*
|
|
69
|
+
* @param {string} pluginDir
|
|
70
|
+
* @returns {{ restoreProfile: string, emergencyDisable: string }}
|
|
71
|
+
*/
|
|
72
|
+
export function rescueScriptPaths(pluginDir, platform = process.platform) {
|
|
73
|
+
const scripts = rescueScriptsDir(pluginDir);
|
|
74
|
+
if (scripts === '') return { restoreProfile: '', emergencyDisable: '' };
|
|
75
|
+
const names = rescueScriptNames(platform);
|
|
76
|
+
return {
|
|
77
|
+
restoreProfile: path.join(scripts, names.restoreProfile),
|
|
78
|
+
emergencyDisable: path.join(scripts, names.emergencyDisable),
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* The desktop directory of this machine — or an empty string when there is
|
|
84
|
+
* none (a service, a container, a machine without a desktop).
|
|
85
|
+
*
|
|
86
|
+
* @param {{ env?: object, platform?: string, exists?: (p: string) => boolean }} [options]
|
|
87
|
+
* @returns {string}
|
|
88
|
+
*/
|
|
89
|
+
export function desktopDir({ env = process.env, platform = process.platform, exists = defaultExists } = {}) {
|
|
90
|
+
const candidates = [];
|
|
91
|
+
if (platform === 'win32') {
|
|
92
|
+
const profile = env.USERPROFILE || env.HOME || '';
|
|
93
|
+
if (profile !== '') candidates.push(path.join(profile, 'Desktop'));
|
|
94
|
+
// OneDrive moves the desktop folder; the shell still knows where it is.
|
|
95
|
+
if (env.OneDrive) candidates.push(path.join(String(env.OneDrive), 'Desktop'));
|
|
96
|
+
}
|
|
97
|
+
const home = env.HOME || env.USERPROFILE || '';
|
|
98
|
+
if (home !== '') candidates.push(path.join(home, 'Desktop'));
|
|
99
|
+
for (const candidate of candidates) {
|
|
100
|
+
try {
|
|
101
|
+
if (exists(candidate)) return candidate;
|
|
102
|
+
} catch {
|
|
103
|
+
// A candidate that cannot be checked is not the desktop.
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
return '';
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Where the desktop shortcut would live (empty when there is no desktop).
|
|
111
|
+
*
|
|
112
|
+
* @param {string} desktop
|
|
113
|
+
* @returns {string}
|
|
114
|
+
*/
|
|
115
|
+
export function desktopShortcutPath(desktop) {
|
|
116
|
+
const dir = typeof desktop === 'string' ? desktop.trim() : '';
|
|
117
|
+
if (dir === '') return '';
|
|
118
|
+
return path.join(dir, DESKTOP_SHORTCUT_NAME);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* The content of the desktop shortcut.
|
|
123
|
+
*
|
|
124
|
+
* A .cmd file is read by cmd.exe with the console code page, so its content
|
|
125
|
+
* has to stay pure ASCII — a Chinese character in it turns into mojibake and
|
|
126
|
+
* the shortcut does nothing. The home directory may well hold such a
|
|
127
|
+
* character (a Chinese user name), so the home prefix is written as
|
|
128
|
+
* %USERPROFILE%: cmd.exe expands it to the very same directory at run time
|
|
129
|
+
* and the file stays ASCII.
|
|
130
|
+
*
|
|
131
|
+
* @param {string} restoreProfilePath the absolute path of restore-profile.cmd
|
|
132
|
+
* @param {{ home?: string }} [options]
|
|
133
|
+
* @returns {string}
|
|
134
|
+
*/
|
|
135
|
+
export function desktopShortcutContent(restoreProfilePath, { home = '' } = {}) {
|
|
136
|
+
const target = String(restoreProfilePath ?? '');
|
|
137
|
+
const homeDir = typeof home === 'string' ? home.trim() : '';
|
|
138
|
+
let written = target;
|
|
139
|
+
if (homeDir !== '' && target.toLowerCase().startsWith(homeDir.toLowerCase() + path.sep)) {
|
|
140
|
+
written = '%USERPROFILE%' + target.slice(homeDir.length);
|
|
141
|
+
}
|
|
142
|
+
return '@echo off\r\nstart "" "' + written + '"\r\n';
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* Is this text pure ASCII?
|
|
147
|
+
*
|
|
148
|
+
* @param {string} text
|
|
149
|
+
* @returns {boolean}
|
|
150
|
+
*/
|
|
151
|
+
export function isAsciiText(text) {
|
|
152
|
+
const value = String(text ?? '');
|
|
153
|
+
for (let index = 0; index < value.length; index += 1) {
|
|
154
|
+
if (value.charCodeAt(index) > 127) return false;
|
|
155
|
+
}
|
|
156
|
+
return true;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/**
|
|
160
|
+
* Write the desktop shortcut.
|
|
161
|
+
*
|
|
162
|
+
* NEVER automatic: the caller only gets here when the user pressed the
|
|
163
|
+
* button. The file is written with the content above and nothing else.
|
|
164
|
+
*
|
|
165
|
+
* @param {{
|
|
166
|
+
* desktop?: string,
|
|
167
|
+
* restoreProfilePath?: string,
|
|
168
|
+
* home?: string,
|
|
169
|
+
* writeFile?: (path: string, content: string) => void,
|
|
170
|
+
* exists?: (path: string) => boolean,
|
|
171
|
+
* }} [options]
|
|
172
|
+
* @returns {{ ok: boolean, path: string, error: string|null }}
|
|
173
|
+
*/
|
|
174
|
+
export function createDesktopShortcut({
|
|
175
|
+
desktop = '',
|
|
176
|
+
restoreProfilePath = '',
|
|
177
|
+
home = '',
|
|
178
|
+
writeFile,
|
|
179
|
+
exists = defaultExists,
|
|
180
|
+
} = {}) {
|
|
181
|
+
const dir = typeof desktop === 'string' ? desktop.trim() : '';
|
|
182
|
+
if (dir === '') {
|
|
183
|
+
return { ok: false, path: '', error: 'there is no desktop directory on this machine' };
|
|
184
|
+
}
|
|
185
|
+
if (typeof writeFile !== 'function') {
|
|
186
|
+
return { ok: false, path: '', error: 'the shortcut cannot be written here' };
|
|
187
|
+
}
|
|
188
|
+
const target = desktopShortcutPath(dir);
|
|
189
|
+
const content = desktopShortcutContent(restoreProfilePath, { home });
|
|
190
|
+
if (!isAsciiText(content)) {
|
|
191
|
+
return {
|
|
192
|
+
ok: false,
|
|
193
|
+
path: target,
|
|
194
|
+
error: 'the path of the script holds characters a .cmd file cannot carry — copy the path by hand instead',
|
|
195
|
+
};
|
|
196
|
+
}
|
|
197
|
+
try {
|
|
198
|
+
writeFile(target, content);
|
|
199
|
+
} catch (error) {
|
|
200
|
+
return {
|
|
201
|
+
ok: false,
|
|
202
|
+
path: target,
|
|
203
|
+
error: error && error.message ? error.message : String(error),
|
|
204
|
+
};
|
|
205
|
+
}
|
|
206
|
+
return { ok: true, path: target, error: null };
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
/**
|
|
210
|
+
* Everything the settings page needs to show the rescue kit.
|
|
211
|
+
*
|
|
212
|
+
* @param {{
|
|
213
|
+
* pluginDir?: string,
|
|
214
|
+
* env?: object,
|
|
215
|
+
* platform?: string,
|
|
216
|
+
* exists?: (p: string) => boolean,
|
|
217
|
+
* }} [options]
|
|
218
|
+
* @returns {{
|
|
219
|
+
* pluginDir: string,
|
|
220
|
+
* scriptsDir: string,
|
|
221
|
+
* restoreProfile: string,
|
|
222
|
+
* emergencyDisable: string,
|
|
223
|
+
* desktopDir: string,
|
|
224
|
+
* shortcutPath: string,
|
|
225
|
+
* shortcutExists: boolean,
|
|
226
|
+
* }}
|
|
227
|
+
*/
|
|
228
|
+
export function rescueInfo({ pluginDir = '', env = process.env, platform = process.platform, exists = defaultExists } = {}) {
|
|
229
|
+
const scripts = rescueScriptsDir(pluginDir);
|
|
230
|
+
const names = rescueScriptNames(platform);
|
|
231
|
+
// The desktop shortcut is a Windows `.cmd` file: on the other platforms
|
|
232
|
+
// there is nothing to offer (and a dead path would be worse than none).
|
|
233
|
+
const desktop = platform === 'win32' ? desktopDir({ env, platform, exists }) : '';
|
|
234
|
+
const shortcut = desktopShortcutPath(desktop);
|
|
235
|
+
let shortcutExists = false;
|
|
236
|
+
if (shortcut !== '') {
|
|
237
|
+
try {
|
|
238
|
+
shortcutExists = exists(shortcut) === true;
|
|
239
|
+
} catch {
|
|
240
|
+
shortcutExists = false;
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
return {
|
|
244
|
+
pluginDir: typeof pluginDir === 'string' ? pluginDir : '',
|
|
245
|
+
platform,
|
|
246
|
+
scriptsDir: scripts,
|
|
247
|
+
restoreProfile: scripts === '' ? '' : path.join(scripts, names.restoreProfile),
|
|
248
|
+
emergencyDisable: scripts === '' ? '' : path.join(scripts, names.emergencyDisable),
|
|
249
|
+
desktopDir: desktop,
|
|
250
|
+
shortcutPath: shortcut,
|
|
251
|
+
shortcutExists,
|
|
252
|
+
};
|
|
253
|
+
}
|