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.
@@ -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
+ }