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,373 @@
1
+ <#
2
+ .SYNOPSIS
3
+ The dsh-autorestart emergency switch: take this plugin out (or put it back)
4
+ when DSH does not come up.
5
+
6
+ .DESCRIPTION
7
+ "I enabled the plugin and DSH does not start any more" is the hardest moment
8
+ for a plugin: fixing it needs the plugin itself. This script touches ONE
9
+ file - the package.json of the desktop profile - and ONE array in it:
10
+ `dsh.profile.bundles`.
11
+
12
+ - disable: take "dsh-autorestart" out of the bundles (it was not there
13
+ anyway -> report "already disabled", that is not a failure);
14
+ - enable : put "dsh-autorestart" back into the bundles (it was already
15
+ there -> report "already enabled");
16
+ - status : read only, prints whether it is in the bundles right now.
17
+
18
+ Two hard rules:
19
+
20
+ 1. the file is backed up as `package.json.bak-emergency-<yyyyMMdd-HHmmss>`
21
+ before anything is written;
22
+ 2. after writing, the JSON is parsed again and checked; if the check
23
+ fails, the backup is put back and the failure is reported honestly.
24
+
25
+ The indentation, the line endings and the trailing newline of the file are
26
+ kept as they are (the text is edited, the JSON is not re-serialized).
27
+
28
+ LANGUAGE: this file is pure ASCII on purpose. Windows PowerShell 5.1 reads a
29
+ UTF-8 file without a byte order mark as ANSI, so one Chinese character
30
+ inside the script turns into mojibake and the script dies with a ParserError
31
+ before it does anything. The Chinese wording lives in
32
+ scripts\rescue-strings.zh.json (UTF-8, no BOM) and is read at runtime; if
33
+ that file is missing or broken, every message falls back to English and the
34
+ script says so.
35
+
36
+ .EXAMPLE
37
+ powershell -NoProfile -ExecutionPolicy Bypass -File .\emergency-toggle.ps1 -Action status
38
+
39
+ .EXAMPLE
40
+ powershell -NoProfile -ExecutionPolicy Bypass -File .\emergency-toggle.ps1 -Action disable
41
+
42
+ .NOTES
43
+ Windows PowerShell 5.1 is enough (nothing from 6.0+ is used); the .cmd entry
44
+ point double-clicked by the user is that one too.
45
+ #>
46
+ [CmdletBinding()]
47
+ param(
48
+ # What to do.
49
+ [ValidateSet('disable', 'enable', 'status')]
50
+ [string] $Action = 'status',
51
+
52
+ # Only print what would be written, change nothing.
53
+ [switch] $DryRun,
54
+
55
+ # The package.json to work on (for tests and for other profiles).
56
+ [string] $PackageJson = (Join-Path $env:USERPROFILE '.dsh\profiles\desktop\package.json')
57
+ )
58
+
59
+ $ErrorActionPreference = 'Stop'
60
+
61
+ # The name of this plugin inside the bundles.
62
+ $PackageName = 'dsh-autorestart'
63
+
64
+ # The anchor it is put back after (the position does not matter, but an anchor
65
+ # is steadier than the end of the array).
66
+ $AnchorName = 'dsh-github-guard'
67
+
68
+ # ---------------------------------------------------------------------------
69
+ # The Chinese strings: read from rescue-strings.zh.json, fall back to English.
70
+ # ---------------------------------------------------------------------------
71
+
72
+ $script:Strings = $null
73
+ $script:StringsError = ''
74
+
75
+ function Initialize-Strings {
76
+ $path = Join-Path $PSScriptRoot 'rescue-strings.zh.json'
77
+ if (-not (Test-Path -LiteralPath $path -PathType Leaf)) {
78
+ $script:StringsError = (T 'common.stringsMissingEn' 'the Chinese strings file is missing: {0} (falling back to English messages)') -f $path
79
+ return
80
+ }
81
+ try {
82
+ # Read as UTF-8 explicitly: Get-Content would use the console code page.
83
+ $text = [System.IO.File]::ReadAllText($path, [System.Text.Encoding]::UTF8)
84
+ $script:Strings = $text | ConvertFrom-Json
85
+ } catch {
86
+ $script:StringsError = (T 'common.stringsBrokenEn' 'the Chinese strings file could not be read: {0} (falling back to English messages)') -f $_.Exception.Message
87
+ $script:Strings = $null
88
+ }
89
+ }
90
+
91
+ # Look a string up. The English fallback is right here in the script, so a
92
+ # missing key (or a broken strings file) never stops the rescue.
93
+ function T {
94
+ param([string] $Key, [string] $Fallback)
95
+ if ($null -ne $script:Strings) {
96
+ $node = $script:Strings
97
+ foreach ($part in $Key.Split('.')) {
98
+ if ($null -eq $node) { break }
99
+ $property = $node.PSObject.Properties[$part]
100
+ if ($null -eq $property) { $node = $null; break }
101
+ $node = $property.Value
102
+ }
103
+ if ($null -ne $node) {
104
+ $text = [string] $node
105
+ if ($text -ne '') { return $text }
106
+ }
107
+ }
108
+ return $Fallback
109
+ }
110
+
111
+ Initialize-Strings
112
+
113
+ function Say {
114
+ param([string] $Text, [string] $Color = 'Gray')
115
+ Write-Host $Text -ForegroundColor $Color
116
+ }
117
+
118
+ function Fail {
119
+ param([string] $Text)
120
+ Write-Host ((T 'emergency.failed' 'the emergency switch failed: {0}') -f $Text) -ForegroundColor Red
121
+ exit 1
122
+ }
123
+
124
+ # ---------------------------------------------------------------------------
125
+ # Text helpers: only the bundles array is touched, every other byte stays.
126
+ # ---------------------------------------------------------------------------
127
+
128
+ # The square brackets of the bundles array.
129
+ function Get-ArrayRange {
130
+ param([string] $Text)
131
+ $marker = '"bundles"'
132
+ $markerIndex = $Text.IndexOf($marker)
133
+ if ($markerIndex -lt 0) { throw 'package.json has no bundles entry' }
134
+ $open = $Text.IndexOf('[', $markerIndex)
135
+ if ($open -lt 0) { throw 'bundles is not an array' }
136
+ $depth = 0
137
+ $inString = $false
138
+ $escaped = $false
139
+ for ($i = $open; $i -lt $Text.Length; $i++) {
140
+ $c = $Text[$i]
141
+ if ($inString) {
142
+ if ($escaped) { $escaped = $false; continue }
143
+ if ($c -eq '\') { $escaped = $true; continue }
144
+ if ($c -eq '"') { $inString = $false }
145
+ continue
146
+ }
147
+ if ($c -eq '"') { $inString = $true; continue }
148
+ if ($c -eq '[') { $depth++; continue }
149
+ if ($c -eq ']') {
150
+ $depth--
151
+ if ($depth -eq 0) { return @($open, $i) }
152
+ }
153
+ }
154
+ throw 'the bundles array is not closed'
155
+ }
156
+
157
+ # Every item of the array: the name, the indentation, the comma, the position
158
+ # in the text.
159
+ function Get-Items {
160
+ param([string] $Text, [int] $Open, [int] $Close)
161
+ $body = $Text.Substring($Open + 1, $Close - $Open - 1)
162
+ $items = @()
163
+ $pattern = '(?m)^(?<indent>[ \t]*)"(?<name>[^"]*)"(?<comma>,?)[ \t]*\r?$'
164
+ foreach ($match in [regex]::Matches($body, $pattern)) {
165
+ $lineStart = $Open + 1 + $match.Index
166
+ $lineEnd = $lineStart + $match.Length
167
+ # The line ending of that line (\r\n or \n) belongs to the line.
168
+ if ($lineEnd -lt $Text.Length -and $Text[$lineEnd] -eq "`n") { $lineEnd++ }
169
+ $nameGroup = $match.Groups['name']
170
+ $commaGroup = $match.Groups['comma']
171
+ $items += [pscustomobject]@{
172
+ Name = $nameGroup.Value
173
+ Indent = $match.Groups['indent'].Value
174
+ HasComma = ($commaGroup.Value -eq ',')
175
+ CommaIndex = $commaGroup.Index
176
+ # The position right after the closing quote of the name (where the
177
+ # comma goes).
178
+ ContentEnd = $nameGroup.Index + $nameGroup.Length + 1
179
+ LineStart = $lineStart
180
+ LineEnd = $lineEnd
181
+ }
182
+ }
183
+ return $items
184
+ }
185
+
186
+ # The indentation of a line (so a new line can look the same).
187
+ function Get-LineIndent {
188
+ param([string] $Text, [int] $Index)
189
+ $start = $Text.LastIndexOf("`n", [Math]::Max(0, $Index - 1))
190
+ if ($start -lt 0) { $start = 0 } else { $start++ }
191
+ $indent = ''
192
+ for ($i = $start; $i -lt $Text.Length; $i++) {
193
+ $c = $Text[$i]
194
+ if ($c -eq ' ' -or $c -eq "`t") { $indent += $c; continue }
195
+ break
196
+ }
197
+ return $indent
198
+ }
199
+
200
+ # The line ending the file uses.
201
+ function Get-LineEnding {
202
+ param([string] $Text)
203
+ if ($Text.Contains("`r`n")) { return "`r`n" }
204
+ return "`n"
205
+ }
206
+
207
+ # ---------------------------------------------------------------------------
208
+ # Start.
209
+ # ---------------------------------------------------------------------------
210
+
211
+ if ($script:StringsError -ne '') {
212
+ Write-Host ('WARNING: ' + $script:StringsError) -ForegroundColor Yellow
213
+ }
214
+
215
+ if (-not (Test-Path -LiteralPath $PackageJson -PathType Leaf)) {
216
+ Fail ((T 'emergency.notFound' 'the package.json of the profile cannot be found: {0}') -f $PackageJson)
217
+ }
218
+
219
+ $path = (Resolve-Path -LiteralPath $PackageJson).Path
220
+ $bytes = [System.IO.File]::ReadAllBytes($path)
221
+ $hasBom = ($bytes.Length -ge 3 -and $bytes[0] -eq 0xEF -and $bytes[1] -eq 0xBB -and $bytes[2] -eq 0xBF)
222
+ $raw = [System.IO.File]::ReadAllText($path)
223
+
224
+ try {
225
+ $document = $raw | ConvertFrom-Json
226
+ } catch {
227
+ Fail ((T 'emergency.badJson' 'this package.json is not valid JSON: {0}') -f $_.Exception.Message)
228
+ }
229
+
230
+ $bundles = @()
231
+ $hasBundles = $false
232
+ if ($null -ne $document -and $null -ne $document.dsh -and $null -ne $document.dsh.profile -and $null -ne $document.dsh.profile.bundles) {
233
+ $bundles = @($document.dsh.profile.bundles)
234
+ $hasBundles = $true
235
+ }
236
+
237
+ $present = ($bundles -contains $PackageName)
238
+
239
+ Say ((T 'emergency.file' 'file: {0}') -f $path)
240
+
241
+ if ($Action -eq 'status') {
242
+ if ($present) { Say ((T 'emergency.enabled' 'enabled: {0} is in the bundles.') -f $PackageName) 'Green' }
243
+ else { Say ((T 'emergency.disabled' 'disabled: {0} is not in the bundles.') -f $PackageName) 'Yellow' }
244
+ exit 0
245
+ }
246
+
247
+ if ($Action -eq 'enable' -and -not $hasBundles) {
248
+ Fail (T 'emergency.noBundles' 'this profile has no dsh.profile.bundles list, so the plugin cannot be put back (please add a line "dsh-autorestart" by hand).')
249
+ }
250
+
251
+ # ---------------------------------------------------------------------------
252
+ # Work out the new text.
253
+ # ---------------------------------------------------------------------------
254
+
255
+ $new = $raw
256
+ $range = Get-ArrayRange $raw
257
+ $items = Get-Items $raw $range[0] $range[1]
258
+ $eol = Get-LineEnding $raw
259
+
260
+ if ($Action -eq 'disable') {
261
+ if (-not $present) {
262
+ Say ((T 'emergency.alreadyDisabled' 'already disabled: {0} was not in the bundles anyway (no file was changed).') -f $PackageName) 'Yellow'
263
+ exit 0
264
+ }
265
+ $target = $items | Where-Object { $_.Name -eq $PackageName } | Select-Object -First 1
266
+ if ($null -eq $target) { Fail ((T 'emergency.notInBundles' '{0} cannot be found in the bundles') -f $PackageName) }
267
+ $shift = 0
268
+ if (-not $target.HasComma) {
269
+ # It is the last item of the array: the comma of the item before it has to
270
+ # go, otherwise the JSON is invalid.
271
+ $previous = $items | Where-Object { $_.LineEnd -le $target.LineStart } | Select-Object -Last 1
272
+ if ($null -ne $previous -and $previous.HasComma) {
273
+ $new = $new.Remove($previous.CommaIndex, 1)
274
+ $shift = -1
275
+ }
276
+ }
277
+ $new = $new.Remove($target.LineStart + $shift, $target.LineEnd - $target.LineStart)
278
+ $summary = (T 'emergency.summaryRemove' 'take {0} out of the bundles') -f $PackageName
279
+ } else {
280
+ # enable
281
+ if ($present) {
282
+ Say ((T 'emergency.alreadyEnabled' 'already enabled: {0} was in the bundles anyway (no file was changed).') -f $PackageName) 'Yellow'
283
+ exit 0
284
+ }
285
+ $anchor = $items | Where-Object { $_.Name -eq $AnchorName } | Select-Object -First 1
286
+ $line = ''
287
+ $insertAt = 0
288
+ if ($null -ne $anchor) {
289
+ if ($anchor.HasComma) {
290
+ $insertAt = $anchor.LineEnd
291
+ $line = $anchor.Indent + '"' + $PackageName + '",'
292
+ } else {
293
+ # The anchor is the last item: give it a comma first, then insert the new
294
+ # item (the new one has no comma).
295
+ $new = $new.Insert($anchor.ContentEnd, ',')
296
+ $insertAt = $anchor.LineEnd + 1
297
+ $line = $anchor.Indent + '"' + $PackageName + '"'
298
+ }
299
+ } else {
300
+ $last = $items | Select-Object -Last 1
301
+ if ($null -ne $last) {
302
+ if ($last.HasComma) {
303
+ $insertAt = $last.LineEnd
304
+ } else {
305
+ $new = $new.Insert($last.ContentEnd, ',')
306
+ $insertAt = $last.LineEnd + 1
307
+ }
308
+ $line = $last.Indent + '"' + $PackageName + '"'
309
+ } else {
310
+ # The array is empty.
311
+ $insertAt = $range[1]
312
+ $line = (Get-LineIndent $raw $range[1]) + ' "' + $PackageName + '"'
313
+ }
314
+ }
315
+ $new = $new.Insert($insertAt, $line + $eol)
316
+ $summary = (T 'emergency.summaryAdd' 'put {0} back into the bundles') -f $PackageName
317
+ }
318
+
319
+ if ($DryRun) {
320
+ Say ((T 'emergency.dryRunWill' '[dry run] would {0}.') -f $summary) 'Cyan'
321
+ Say ((T 'emergency.dryRunWrite' '[dry run] would write: {0}') -f $path) 'Cyan'
322
+ Say ((T 'emergency.dryRunBackup' '[dry run] would back it up as: {0}.bak-emergency-<timestamp>') -f $path) 'Cyan'
323
+ Say (T 'emergency.dryRunNoChange' '[dry run] no file was changed.') 'Cyan'
324
+ exit 0
325
+ }
326
+
327
+ # ---------------------------------------------------------------------------
328
+ # Write: back up first, write, then parse and check again; on a failure, roll
329
+ # back.
330
+ # ---------------------------------------------------------------------------
331
+
332
+ $backup = $path + '.bak-emergency-' + (Get-Date -Format 'yyyyMMdd-HHmmss')
333
+ try {
334
+ Copy-Item -LiteralPath $path -Destination $backup -Force
335
+ } catch {
336
+ Fail ((T 'emergency.backupFailed' 'the backup failed (the original file was not touched): {0}') -f $_.Exception.Message)
337
+ }
338
+
339
+ try {
340
+ $encoding = New-Object System.Text.UTF8Encoding($hasBom)
341
+ [System.IO.File]::WriteAllText($path, $new, $encoding)
342
+ } catch {
343
+ Fail ((T 'emergency.writeFailed' 'the write failed (the backup is at {0}): {1}') -f $backup, $_.Exception.Message)
344
+ }
345
+
346
+ try {
347
+ $written = [System.IO.File]::ReadAllText($path)
348
+ $parsed = $written | ConvertFrom-Json
349
+ $names = @()
350
+ if ($null -ne $parsed -and $null -ne $parsed.dsh -and $null -ne $parsed.dsh.profile -and $null -ne $parsed.dsh.profile.bundles) {
351
+ $names = @($parsed.dsh.profile.bundles)
352
+ }
353
+ if ($Action -eq 'enable' -and -not ($names -contains $PackageName)) {
354
+ throw ('after writing, the bundles still do not hold ' + $PackageName)
355
+ }
356
+ if ($Action -eq 'disable' -and ($names -contains $PackageName)) {
357
+ throw ('after writing, the bundles still hold ' + $PackageName)
358
+ }
359
+ } catch {
360
+ # The check failed: put the original file back, then report honestly.
361
+ try {
362
+ Copy-Item -LiteralPath $backup -Destination $path -Force
363
+ Say ((T 'emergency.rolledBack' 'the backup was put back: {0}') -f $backup) 'Yellow'
364
+ } catch {
365
+ Fail ((T 'emergency.verifyFailedNoRollback' 'the check failed and the rollback failed too (the backup is at {0}): {1}') -f $backup, $_.Exception.Message)
366
+ }
367
+ Fail ((T 'emergency.verifyFailedRolledBack' 'the check failed (it was rolled back): {0}') -f $_.Exception.Message)
368
+ }
369
+
370
+ Say ((T 'emergency.done' 'done: {0}.') -f $summary) 'Green'
371
+ Say ((T 'emergency.backup' 'backup: {0}') -f $backup) 'Gray'
372
+ Say (T 'emergency.restartHint' 'restart DSH for it to take effect.') 'Gray'
373
+ exit 0
@@ -0,0 +1,44 @@
1
+ @echo off
2
+ rem dsh-autorestart - restore the profile snapshot.
3
+ rem
4
+ rem Double-click this file when DSH does not start any more (a plugin upgrade
5
+ rem broke the profile, the configuration was damaged, node_modules is a mess):
6
+ rem it puts the newest snapshot that carries a manifest back over the profile.
7
+ rem
8
+ rem What it does, in this order:
9
+ rem 1. finds the newest snapshot with a manifest under <stateDir>\backups
10
+ rem 2. MOVES the current profile to <profile>\.broken-<timestamp> (nothing is
11
+ rem deleted, the evidence stays)
12
+ rem 3. copies the configuration files and the plugin tree back
13
+ rem 4. reads the result back: package.json, dsh.profile.bundles and the
14
+ rem version of dsh-autorestart have to be readable
15
+ rem
16
+ rem It never restarts DSH for you - you do that yourself.
17
+ rem
18
+ rem Optional arguments are passed straight to the PowerShell script, e.g.
19
+ rem restore-profile.cmd -DryRun
20
+ rem restore-profile.cmd -ProfileDir C:\temp\drill\profile -StateDir C:\temp\drill\state
21
+ rem
22
+ rem NOTE: this file is ASCII on purpose. cmd.exe reads a batch file with the
23
+ rem console code page, and a UTF-8 character in it would shift the rest of the
24
+ rem line and be run as a command. The Chinese explanation comes from the
25
+ rem PowerShell script below (and from scripts\README.md).
26
+ setlocal
27
+ set "SCRIPT=%~dp0restore-profile.ps1"
28
+ if not exist "%SCRIPT%" (
29
+ echo Cannot find "%SCRIPT%".
30
+ echo The rescue kit is incomplete - keep the plugin folder together.
31
+ pause
32
+ exit /b 1
33
+ )
34
+ powershell.exe -NoProfile -ExecutionPolicy Bypass -File "%SCRIPT%" %*
35
+ set "RC=%ERRORLEVEL%"
36
+ echo.
37
+ if "%RC%"=="0" (
38
+ echo Done. The result is above - open DSH again.
39
+ ) else (
40
+ echo The restore did NOT finish. Read the red lines above.
41
+ echo Your old profile was not deleted - it is kept as a ".broken-<timestamp>" folder.
42
+ )
43
+ pause
44
+ exit /b %RC%