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.
- package/CHANGELOG.md +47 -0
- 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 +6 -1
- package/scripts/README.md +65 -0
- package/scripts/drill-rescue.ps1 +508 -0
- package/scripts/drill-rescue.sh +427 -0
- package/scripts/drill-watchdog.mjs +341 -0
- package/scripts/emergency-disable.sh +314 -0
- package/scripts/emergency-toggle.ps1 +136 -65
- package/scripts/rescue-strings.zh.json +168 -0
- package/scripts/restore-profile.cmd +44 -0
- package/scripts/restore-profile.ps1 +608 -0
- package/scripts/restore-profile.sh +593 -0
- package/scripts/run-tests.ps1 +1 -1
- package/scripts/watchdog.mjs +203 -0
|
@@ -1,22 +1,37 @@
|
|
|
1
|
-
|
|
1
|
+
<#
|
|
2
2
|
.SYNOPSIS
|
|
3
|
-
dsh-autorestart
|
|
3
|
+
The dsh-autorestart emergency switch: take this plugin out (or put it back)
|
|
4
|
+
when DSH does not come up.
|
|
4
5
|
|
|
5
6
|
.DESCRIPTION
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
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.
|
|
20
35
|
|
|
21
36
|
.EXAMPLE
|
|
22
37
|
powershell -NoProfile -ExecutionPolicy Bypass -File .\emergency-toggle.ps1 -Action status
|
|
@@ -25,29 +40,76 @@
|
|
|
25
40
|
powershell -NoProfile -ExecutionPolicy Bypass -File .\emergency-toggle.ps1 -Action disable
|
|
26
41
|
|
|
27
42
|
.NOTES
|
|
28
|
-
Windows PowerShell 5.1
|
|
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.
|
|
29
45
|
#>
|
|
30
46
|
[CmdletBinding()]
|
|
31
47
|
param(
|
|
32
|
-
#
|
|
48
|
+
# What to do.
|
|
33
49
|
[ValidateSet('disable', 'enable', 'status')]
|
|
34
50
|
[string] $Action = 'status',
|
|
35
51
|
|
|
36
|
-
#
|
|
52
|
+
# Only print what would be written, change nothing.
|
|
37
53
|
[switch] $DryRun,
|
|
38
54
|
|
|
39
|
-
#
|
|
55
|
+
# The package.json to work on (for tests and for other profiles).
|
|
40
56
|
[string] $PackageJson = (Join-Path $env:USERPROFILE '.dsh\profiles\desktop\package.json')
|
|
41
57
|
)
|
|
42
58
|
|
|
43
59
|
$ErrorActionPreference = 'Stop'
|
|
44
60
|
|
|
45
|
-
#
|
|
61
|
+
# The name of this plugin inside the bundles.
|
|
46
62
|
$PackageName = 'dsh-autorestart'
|
|
47
63
|
|
|
48
|
-
#
|
|
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).
|
|
49
66
|
$AnchorName = 'dsh-github-guard'
|
|
50
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
|
+
|
|
51
113
|
function Say {
|
|
52
114
|
param([string] $Text, [string] $Color = 'Gray')
|
|
53
115
|
Write-Host $Text -ForegroundColor $Color
|
|
@@ -55,22 +117,22 @@ function Say {
|
|
|
55
117
|
|
|
56
118
|
function Fail {
|
|
57
119
|
param([string] $Text)
|
|
58
|
-
Write-Host ('
|
|
120
|
+
Write-Host ((T 'emergency.failed' 'the emergency switch failed: {0}') -f $Text) -ForegroundColor Red
|
|
59
121
|
exit 1
|
|
60
122
|
}
|
|
61
123
|
|
|
62
124
|
# ---------------------------------------------------------------------------
|
|
63
|
-
#
|
|
125
|
+
# Text helpers: only the bundles array is touched, every other byte stays.
|
|
64
126
|
# ---------------------------------------------------------------------------
|
|
65
127
|
|
|
66
|
-
# bundles
|
|
128
|
+
# The square brackets of the bundles array.
|
|
67
129
|
function Get-ArrayRange {
|
|
68
130
|
param([string] $Text)
|
|
69
131
|
$marker = '"bundles"'
|
|
70
132
|
$markerIndex = $Text.IndexOf($marker)
|
|
71
|
-
if ($markerIndex -lt 0) { throw 'package.json
|
|
133
|
+
if ($markerIndex -lt 0) { throw 'package.json has no bundles entry' }
|
|
72
134
|
$open = $Text.IndexOf('[', $markerIndex)
|
|
73
|
-
if ($open -lt 0) { throw 'bundles
|
|
135
|
+
if ($open -lt 0) { throw 'bundles is not an array' }
|
|
74
136
|
$depth = 0
|
|
75
137
|
$inString = $false
|
|
76
138
|
$escaped = $false
|
|
@@ -89,10 +151,11 @@ function Get-ArrayRange {
|
|
|
89
151
|
if ($depth -eq 0) { return @($open, $i) }
|
|
90
152
|
}
|
|
91
153
|
}
|
|
92
|
-
throw 'bundles
|
|
154
|
+
throw 'the bundles array is not closed'
|
|
93
155
|
}
|
|
94
156
|
|
|
95
|
-
#
|
|
157
|
+
# Every item of the array: the name, the indentation, the comma, the position
|
|
158
|
+
# in the text.
|
|
96
159
|
function Get-Items {
|
|
97
160
|
param([string] $Text, [int] $Open, [int] $Close)
|
|
98
161
|
$body = $Text.Substring($Open + 1, $Close - $Open - 1)
|
|
@@ -101,7 +164,7 @@ function Get-Items {
|
|
|
101
164
|
foreach ($match in [regex]::Matches($body, $pattern)) {
|
|
102
165
|
$lineStart = $Open + 1 + $match.Index
|
|
103
166
|
$lineEnd = $lineStart + $match.Length
|
|
104
|
-
#
|
|
167
|
+
# The line ending of that line (\r\n or \n) belongs to the line.
|
|
105
168
|
if ($lineEnd -lt $Text.Length -and $Text[$lineEnd] -eq "`n") { $lineEnd++ }
|
|
106
169
|
$nameGroup = $match.Groups['name']
|
|
107
170
|
$commaGroup = $match.Groups['comma']
|
|
@@ -110,7 +173,8 @@ function Get-Items {
|
|
|
110
173
|
Indent = $match.Groups['indent'].Value
|
|
111
174
|
HasComma = ($commaGroup.Value -eq ',')
|
|
112
175
|
CommaIndex = $commaGroup.Index
|
|
113
|
-
#
|
|
176
|
+
# The position right after the closing quote of the name (where the
|
|
177
|
+
# comma goes).
|
|
114
178
|
ContentEnd = $nameGroup.Index + $nameGroup.Length + 1
|
|
115
179
|
LineStart = $lineStart
|
|
116
180
|
LineEnd = $lineEnd
|
|
@@ -119,7 +183,7 @@ function Get-Items {
|
|
|
119
183
|
return $items
|
|
120
184
|
}
|
|
121
185
|
|
|
122
|
-
#
|
|
186
|
+
# The indentation of a line (so a new line can look the same).
|
|
123
187
|
function Get-LineIndent {
|
|
124
188
|
param([string] $Text, [int] $Index)
|
|
125
189
|
$start = $Text.LastIndexOf("`n", [Math]::Max(0, $Index - 1))
|
|
@@ -133,7 +197,7 @@ function Get-LineIndent {
|
|
|
133
197
|
return $indent
|
|
134
198
|
}
|
|
135
199
|
|
|
136
|
-
#
|
|
200
|
+
# The line ending the file uses.
|
|
137
201
|
function Get-LineEnding {
|
|
138
202
|
param([string] $Text)
|
|
139
203
|
if ($Text.Contains("`r`n")) { return "`r`n" }
|
|
@@ -141,11 +205,15 @@ function Get-LineEnding {
|
|
|
141
205
|
}
|
|
142
206
|
|
|
143
207
|
# ---------------------------------------------------------------------------
|
|
144
|
-
#
|
|
208
|
+
# Start.
|
|
145
209
|
# ---------------------------------------------------------------------------
|
|
146
210
|
|
|
211
|
+
if ($script:StringsError -ne '') {
|
|
212
|
+
Write-Host ('WARNING: ' + $script:StringsError) -ForegroundColor Yellow
|
|
213
|
+
}
|
|
214
|
+
|
|
147
215
|
if (-not (Test-Path -LiteralPath $PackageJson -PathType Leaf)) {
|
|
148
|
-
Fail ('
|
|
216
|
+
Fail ((T 'emergency.notFound' 'the package.json of the profile cannot be found: {0}') -f $PackageJson)
|
|
149
217
|
}
|
|
150
218
|
|
|
151
219
|
$path = (Resolve-Path -LiteralPath $PackageJson).Path
|
|
@@ -156,7 +224,7 @@ $raw = [System.IO.File]::ReadAllText($path)
|
|
|
156
224
|
try {
|
|
157
225
|
$document = $raw | ConvertFrom-Json
|
|
158
226
|
} catch {
|
|
159
|
-
Fail ('
|
|
227
|
+
Fail ((T 'emergency.badJson' 'this package.json is not valid JSON: {0}') -f $_.Exception.Message)
|
|
160
228
|
}
|
|
161
229
|
|
|
162
230
|
$bundles = @()
|
|
@@ -168,20 +236,20 @@ if ($null -ne $document -and $null -ne $document.dsh -and $null -ne $document.ds
|
|
|
168
236
|
|
|
169
237
|
$present = ($bundles -contains $PackageName)
|
|
170
238
|
|
|
171
|
-
Say ('
|
|
239
|
+
Say ((T 'emergency.file' 'file: {0}') -f $path)
|
|
172
240
|
|
|
173
241
|
if ($Action -eq 'status') {
|
|
174
|
-
if ($present) { Say ('
|
|
175
|
-
else { Say ('
|
|
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' }
|
|
176
244
|
exit 0
|
|
177
245
|
}
|
|
178
246
|
|
|
179
247
|
if ($Action -eq 'enable' -and -not $hasBundles) {
|
|
180
|
-
Fail '
|
|
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).')
|
|
181
249
|
}
|
|
182
250
|
|
|
183
251
|
# ---------------------------------------------------------------------------
|
|
184
|
-
#
|
|
252
|
+
# Work out the new text.
|
|
185
253
|
# ---------------------------------------------------------------------------
|
|
186
254
|
|
|
187
255
|
$new = $raw
|
|
@@ -191,14 +259,15 @@ $eol = Get-LineEnding $raw
|
|
|
191
259
|
|
|
192
260
|
if ($Action -eq 'disable') {
|
|
193
261
|
if (-not $present) {
|
|
194
|
-
Say ('
|
|
262
|
+
Say ((T 'emergency.alreadyDisabled' 'already disabled: {0} was not in the bundles anyway (no file was changed).') -f $PackageName) 'Yellow'
|
|
195
263
|
exit 0
|
|
196
264
|
}
|
|
197
265
|
$target = $items | Where-Object { $_.Name -eq $PackageName } | Select-Object -First 1
|
|
198
|
-
if ($null -eq $target) { Fail ('
|
|
266
|
+
if ($null -eq $target) { Fail ((T 'emergency.notInBundles' '{0} cannot be found in the bundles') -f $PackageName) }
|
|
199
267
|
$shift = 0
|
|
200
268
|
if (-not $target.HasComma) {
|
|
201
|
-
#
|
|
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.
|
|
202
271
|
$previous = $items | Where-Object { $_.LineEnd -le $target.LineStart } | Select-Object -Last 1
|
|
203
272
|
if ($null -ne $previous -and $previous.HasComma) {
|
|
204
273
|
$new = $new.Remove($previous.CommaIndex, 1)
|
|
@@ -206,11 +275,11 @@ if ($Action -eq 'disable') {
|
|
|
206
275
|
}
|
|
207
276
|
}
|
|
208
277
|
$new = $new.Remove($target.LineStart + $shift, $target.LineEnd - $target.LineStart)
|
|
209
|
-
$summary = ('
|
|
278
|
+
$summary = (T 'emergency.summaryRemove' 'take {0} out of the bundles') -f $PackageName
|
|
210
279
|
} else {
|
|
211
280
|
# enable
|
|
212
281
|
if ($present) {
|
|
213
|
-
Say ('
|
|
282
|
+
Say ((T 'emergency.alreadyEnabled' 'already enabled: {0} was in the bundles anyway (no file was changed).') -f $PackageName) 'Yellow'
|
|
214
283
|
exit 0
|
|
215
284
|
}
|
|
216
285
|
$anchor = $items | Where-Object { $_.Name -eq $AnchorName } | Select-Object -First 1
|
|
@@ -221,7 +290,8 @@ if ($Action -eq 'disable') {
|
|
|
221
290
|
$insertAt = $anchor.LineEnd
|
|
222
291
|
$line = $anchor.Indent + '"' + $PackageName + '",'
|
|
223
292
|
} else {
|
|
224
|
-
#
|
|
293
|
+
# The anchor is the last item: give it a comma first, then insert the new
|
|
294
|
+
# item (the new one has no comma).
|
|
225
295
|
$new = $new.Insert($anchor.ContentEnd, ',')
|
|
226
296
|
$insertAt = $anchor.LineEnd + 1
|
|
227
297
|
$line = $anchor.Indent + '"' + $PackageName + '"'
|
|
@@ -237,39 +307,40 @@ if ($Action -eq 'disable') {
|
|
|
237
307
|
}
|
|
238
308
|
$line = $last.Indent + '"' + $PackageName + '"'
|
|
239
309
|
} else {
|
|
240
|
-
#
|
|
310
|
+
# The array is empty.
|
|
241
311
|
$insertAt = $range[1]
|
|
242
312
|
$line = (Get-LineIndent $raw $range[1]) + ' "' + $PackageName + '"'
|
|
243
313
|
}
|
|
244
314
|
}
|
|
245
315
|
$new = $new.Insert($insertAt, $line + $eol)
|
|
246
|
-
$summary = ('
|
|
316
|
+
$summary = (T 'emergency.summaryAdd' 'put {0} back into the bundles') -f $PackageName
|
|
247
317
|
}
|
|
248
318
|
|
|
249
319
|
if ($DryRun) {
|
|
250
|
-
Say ('[
|
|
251
|
-
Say ('[
|
|
252
|
-
Say ('[
|
|
253
|
-
Say ('[
|
|
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'
|
|
254
324
|
exit 0
|
|
255
325
|
}
|
|
256
326
|
|
|
257
327
|
# ---------------------------------------------------------------------------
|
|
258
|
-
#
|
|
328
|
+
# Write: back up first, write, then parse and check again; on a failure, roll
|
|
329
|
+
# back.
|
|
259
330
|
# ---------------------------------------------------------------------------
|
|
260
331
|
|
|
261
332
|
$backup = $path + '.bak-emergency-' + (Get-Date -Format 'yyyyMMdd-HHmmss')
|
|
262
333
|
try {
|
|
263
334
|
Copy-Item -LiteralPath $path -Destination $backup -Force
|
|
264
335
|
} catch {
|
|
265
|
-
Fail ('
|
|
336
|
+
Fail ((T 'emergency.backupFailed' 'the backup failed (the original file was not touched): {0}') -f $_.Exception.Message)
|
|
266
337
|
}
|
|
267
338
|
|
|
268
339
|
try {
|
|
269
340
|
$encoding = New-Object System.Text.UTF8Encoding($hasBom)
|
|
270
341
|
[System.IO.File]::WriteAllText($path, $new, $encoding)
|
|
271
342
|
} catch {
|
|
272
|
-
Fail ('
|
|
343
|
+
Fail ((T 'emergency.writeFailed' 'the write failed (the backup is at {0}): {1}') -f $backup, $_.Exception.Message)
|
|
273
344
|
}
|
|
274
345
|
|
|
275
346
|
try {
|
|
@@ -280,23 +351,23 @@ try {
|
|
|
280
351
|
$names = @($parsed.dsh.profile.bundles)
|
|
281
352
|
}
|
|
282
353
|
if ($Action -eq 'enable' -and -not ($names -contains $PackageName)) {
|
|
283
|
-
throw '
|
|
354
|
+
throw ('after writing, the bundles still do not hold ' + $PackageName)
|
|
284
355
|
}
|
|
285
356
|
if ($Action -eq 'disable' -and ($names -contains $PackageName)) {
|
|
286
|
-
throw '
|
|
357
|
+
throw ('after writing, the bundles still hold ' + $PackageName)
|
|
287
358
|
}
|
|
288
359
|
} catch {
|
|
289
|
-
#
|
|
360
|
+
# The check failed: put the original file back, then report honestly.
|
|
290
361
|
try {
|
|
291
362
|
Copy-Item -LiteralPath $backup -Destination $path -Force
|
|
292
|
-
Say ('
|
|
363
|
+
Say ((T 'emergency.rolledBack' 'the backup was put back: {0}') -f $backup) 'Yellow'
|
|
293
364
|
} catch {
|
|
294
|
-
Fail ('
|
|
365
|
+
Fail ((T 'emergency.verifyFailedNoRollback' 'the check failed and the rollback failed too (the backup is at {0}): {1}') -f $backup, $_.Exception.Message)
|
|
295
366
|
}
|
|
296
|
-
Fail ('
|
|
367
|
+
Fail ((T 'emergency.verifyFailedRolledBack' 'the check failed (it was rolled back): {0}') -f $_.Exception.Message)
|
|
297
368
|
}
|
|
298
369
|
|
|
299
|
-
Say ('
|
|
300
|
-
Say ('
|
|
301
|
-
Say ('
|
|
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'
|
|
302
373
|
exit 0
|
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
{
|
|
2
|
+
"_meta": {
|
|
3
|
+
"purpose": "User-facing Chinese for the dsh-autorestart rescue kit.",
|
|
4
|
+
"encoding": "UTF-8 WITHOUT a byte order mark. The .ps1 files next to this one are pure ASCII, so this is the only place a non-ASCII byte may live. A BOM here would break JSON.parse.",
|
|
5
|
+
"placeholders": "Values may contain {0}, {1} ... placeholders that the scripts fill in with -f formatting.",
|
|
6
|
+
"fallback": "Every key has an English fallback inside the .ps1. If this file is missing or broken, the scripts say so in English and keep working in English."
|
|
7
|
+
},
|
|
8
|
+
|
|
9
|
+
"common": {
|
|
10
|
+
"stringsMissing": "中文文案文件不在:{0}(改用英文兜底)",
|
|
11
|
+
"stringsBroken": "中文文案文件读不出来:{0}(改用英文兜底)",
|
|
12
|
+
"stringsMissingEn": "the Chinese strings file is missing: {0} (falling back to English messages)",
|
|
13
|
+
"stringsBrokenEn": "the Chinese strings file could not be read: {0} (falling back to English messages)"
|
|
14
|
+
},
|
|
15
|
+
|
|
16
|
+
"restore": {
|
|
17
|
+
"title": "dsh-autorestart —— profile 快照还原器",
|
|
18
|
+
"failPrefix": "还原失败:",
|
|
19
|
+
"brokenKept": "你原来的 profile 完整地留在这里:{0}",
|
|
20
|
+
"brokenUntouched": "(这个目录没有被动过,里面还是你出事之前的样子。)",
|
|
21
|
+
"noProfileDir": "找不到要还原的 profile 目录(用 -ProfileDir 说清楚要还原哪一个)。",
|
|
22
|
+
"target": "目标 profile:{0}",
|
|
23
|
+
"targetSuffix": "(目标:{0})",
|
|
24
|
+
"onlyProfiles": "只允许还原 {0} 里的 profile(目标:{1})",
|
|
25
|
+
"missingDir": "目标 profile 目录不存在:{0}(还原需要那个目录已经在那儿;脚本不会为了还原去新建一个 profile 目录)",
|
|
26
|
+
"notProfile": "{0}(目标:{1})。拒绝动手 —— 免得把不是 profile 的目录弄坏。",
|
|
27
|
+
"notProfileSuffix": "。拒绝动手 —— 免得把不是 profile 的目录弄坏。",
|
|
28
|
+
"brokenNotice": "这个 profile 已经坏了:{0}",
|
|
29
|
+
"backupsDir": "快照目录:{0}",
|
|
30
|
+
"usingSnapshot": "要用快照:{0}({1})",
|
|
31
|
+
"snapshotMissing": "找不到这份快照:{0}",
|
|
32
|
+
"snapshotNoManifest": "这份快照没有 manifest.json,不是一份快照:{0}",
|
|
33
|
+
"snapshotSuffix": "{0}(快照:{1})",
|
|
34
|
+
"dryRunMove": "[试运行] 会先把现在的 profile 挪到:{0}",
|
|
35
|
+
"dryRunCopy": "[试运行] 会把 {0} 里的配置和插件复制回:{1}",
|
|
36
|
+
"dryRunVerify": "[试运行] 会回读校验 package.json / dsh.profile.bundles / {0} 的版本号。",
|
|
37
|
+
"dryRunNoChange": "[试运行] 没有改动任何文件。",
|
|
38
|
+
"moveFailed": "挪走现在的 profile 失败(没有动任何东西):{0}",
|
|
39
|
+
"moved": "原来那份已经挪到:{0}",
|
|
40
|
+
"noProfileBefore": "原来那个 profile 目录本来就不在,直接新建。",
|
|
41
|
+
"copyFailed": "把快照复制回去失败(原来那份还在:{0}):{1}",
|
|
42
|
+
"checkOk": " [好] {0} —— {1}",
|
|
43
|
+
"checkSkipped": " [跳过] {0} —— {1}",
|
|
44
|
+
"checkBad": " [坏] {0} —— {1}",
|
|
45
|
+
"verifyFailed": "还原后的 profile 没通过校验({0})。",
|
|
46
|
+
"noteWithPlugins": "(含插件)",
|
|
47
|
+
"noteNoPlugins": "(只有配置文件,没有插件)",
|
|
48
|
+
"done": "已还原到 {0} 的快照{1};现在可以重新打开 DSH。",
|
|
49
|
+
"brokenLeft": "原来那份留在了:{0}(确认没问题之后可以自己删掉它)",
|
|
50
|
+
|
|
51
|
+
"gateEmptyPath": "目标路径是空的",
|
|
52
|
+
"gateDriveRoot": "拒绝动盘符根目录",
|
|
53
|
+
"gateFsRoot": "拒绝动文件系统根目录",
|
|
54
|
+
"gateHome": "拒绝动用户主目录本身",
|
|
55
|
+
"gateSystemDir": "拒绝动系统目录:{0}",
|
|
56
|
+
"gateOtherUser": "拒绝动别人的用户目录",
|
|
57
|
+
|
|
58
|
+
"fingerprintNotProfile": "这里没有 package.json —— 不像 DSH profile",
|
|
59
|
+
"fingerprintProfile": "profile 的 package.json 里有 dsh.profile",
|
|
60
|
+
"fingerprintBrokenNoDsh": "profile 坏了(package.json 不再是 profile),但 profile 的文件还在",
|
|
61
|
+
"fingerprintBrokenNoPkg": "profile 坏了(package.json 没了),但 profile 的文件还在",
|
|
62
|
+
"fingerprintNotDshProfile": "package.json 里没有 dsh.profile,也没有别的东西能证明这是 profile",
|
|
63
|
+
|
|
64
|
+
"snapshotDirMissing": "找不到快照目录:{0}",
|
|
65
|
+
"snapshotNone": "这个目录里没有一份带 manifest 的快照:{0}",
|
|
66
|
+
"snapshotNoConfigPkg": "这份快照里没有 config\\package.json",
|
|
67
|
+
"snapshotBadConfigPkg": "这份快照的 package.json 不是合法的 JSON",
|
|
68
|
+
"snapshotNoDshProfile": "这份快照的 package.json 里没有 dsh.profile",
|
|
69
|
+
"snapshotIsProfile": "这份快照里装的是一个 DSH profile",
|
|
70
|
+
"snapshotUnknownTime": "时间不明",
|
|
71
|
+
|
|
72
|
+
"verifyPkgLabel": "package.json 能解析",
|
|
73
|
+
"verifyPkgMissing": "package.json 不存在",
|
|
74
|
+
"verifyPkgBad": "不是合法的 JSON",
|
|
75
|
+
"verifyBundlesLabel": "dsh.profile.bundles 有内容",
|
|
76
|
+
"verifyBundlesCount": "{0} 个",
|
|
77
|
+
"verifyBundlesEmpty": "没有 dsh.profile.bundles,或者它是空的",
|
|
78
|
+
"verifyPluginLabel": "{0} 的版本号读得到",
|
|
79
|
+
"verifyPluginNotDeclared": "这个 profile 没有声明要装 {0}(没什么可读)",
|
|
80
|
+
"verifyPluginMissing": "profile 里写着要装 {0},但 node_modules\\{0} 不在 —— 配置说一套、磁盘是另一套",
|
|
81
|
+
"verifyPluginNoVersion": "读不到版本号"
|
|
82
|
+
},
|
|
83
|
+
|
|
84
|
+
"drill": {
|
|
85
|
+
"dir": "演练目录:{0}",
|
|
86
|
+
"broke": "已经故意弄坏:package.json 写成非法 JSON,插件目录删掉了。",
|
|
87
|
+
"outputHeader": "--- 还原脚本的输出({0})---",
|
|
88
|
+
"outputEnd": "--- 输出结束 ---",
|
|
89
|
+
"assertions": "断言:",
|
|
90
|
+
"assertPass": " [通过] {0}",
|
|
91
|
+
"assertFail": " [失败] {0} —— {1}",
|
|
92
|
+
"assertFailPlain": " [失败] {0}",
|
|
93
|
+
"refusedNoResult": " [失败] {0} —— 没有拿到运行结果",
|
|
94
|
+
"refusedNotRefused": " [失败] {0} —— 退出码是 0,脚本根本没有拒绝",
|
|
95
|
+
"refusedNoReason": " [失败] {0} —— 退出码是 {1},但输出里没有拒绝原因(期望其中之一:{2})",
|
|
96
|
+
"refusedOk": " [通过] {0} —— 退出码 {1},输出里有拒绝原因「{2}」",
|
|
97
|
+
"cleanup": "清理临时目录:{0}",
|
|
98
|
+
"cleaned": "临时目录已清理干净。",
|
|
99
|
+
"notCleaned": "临时目录没有清理干净,请自己删掉:{0}",
|
|
100
|
+
"failed": "演练失败:{0} 条断言没过。",
|
|
101
|
+
"passed": "演练全部通过:故意弄坏 → 还原 → 校验 → 清理,一整圈跑通了。",
|
|
102
|
+
"error": "演练出错:{0}",
|
|
103
|
+
"noScript": "演练失败:找不到 {0}",
|
|
104
|
+
"dirOccupied": "演练失败:这个演练目录已经被占用了:{0}",
|
|
105
|
+
|
|
106
|
+
"assertDryRunExit": "试运行退出码是 0",
|
|
107
|
+
"assertDryRunUntouched": "试运行没有改动 package.json",
|
|
108
|
+
"assertDryRunNoBroken": "试运行没有留下 broken 目录",
|
|
109
|
+
"assertRestoreExit": "还原退出码是 0",
|
|
110
|
+
"assertChineseOutput": "还原脚本的输出里有中文(文案是从 JSON 读的)",
|
|
111
|
+
"assertParses": "还原后 package.json 能解析",
|
|
112
|
+
"assertUsedSnapshot": "用的是带 manifest 的那份快照(跳过了更新的诱饵)",
|
|
113
|
+
"assertBundles": "还原后 dsh.profile.bundles 有内容",
|
|
114
|
+
"assertVersionReadable": "还原后插件版本读得到",
|
|
115
|
+
"assertVersionSame": "插件版本是快照里的那一个",
|
|
116
|
+
"assertBrokenExists": "broken 证据目录存在",
|
|
117
|
+
"assertBrokenKeeps": "broken 目录里保留的还是坏掉的那份",
|
|
118
|
+
"assertNotProfileRefused": "不像 profile 的目录被拒绝(退出码 + 拒绝原因)",
|
|
119
|
+
"assertNotProfileUntouched": "被拒绝的时候没有动那个目录",
|
|
120
|
+
"assertDriveRootRefused": "盘符根目录被拒绝(退出码 + 拒绝原因)",
|
|
121
|
+
"assertSystemDirRefused": "系统目录 {0} 被拒绝(退出码 + 拒绝原因)",
|
|
122
|
+
"assertSystemDirIntact": "系统目录 {0} 原封不动(没被挪走)",
|
|
123
|
+
"assertMissingDirRefused": "目标 profile 目录不存在时被拒绝(退出码 + 拒绝原因)",
|
|
124
|
+
"assertMissingDirNotCreated": "目标目录不存在时没有把它创建出来",
|
|
125
|
+
"assertLyingRefused": "声明了插件却没装 → 还原必须失败(退出码 + 拒绝原因)",
|
|
126
|
+
"assertLyingBrokenHint": "声明了插件却没装 → 明确说 broken 目录还在",
|
|
127
|
+
"assertPs1Ascii": "restore-profile.ps1 是纯 ASCII(maxByte <= 127)",
|
|
128
|
+
"assertCmdAscii": "restore-profile.cmd 是纯 ASCII",
|
|
129
|
+
"assertOnlyTemp": "演练只在临时目录里动手(broken 目录在临时目录下面)",
|
|
130
|
+
"assertLockFile": "演练目录里有锁文件",
|
|
131
|
+
|
|
132
|
+
"keywords": {
|
|
133
|
+
"notProfile": ["不像", "不是 profile"],
|
|
134
|
+
"driveRoot": ["盘符", "根目录"],
|
|
135
|
+
"systemDir": ["系统目录", "不允许"],
|
|
136
|
+
"missingDir": ["不存在"],
|
|
137
|
+
"notValidated": ["没通过校验", "校验"],
|
|
138
|
+
"dryRun": ["试运行"]
|
|
139
|
+
}
|
|
140
|
+
},
|
|
141
|
+
|
|
142
|
+
"emergency": {
|
|
143
|
+
"failed": "急救开关失败:{0}",
|
|
144
|
+
"notFound": "找不到 profile 的 package.json:{0}",
|
|
145
|
+
"badJson": "这个 package.json 不是合法的 JSON:{0}",
|
|
146
|
+
"file": "文件:{0}",
|
|
147
|
+
"enabled": "已启用:{0} 在 bundles 里。",
|
|
148
|
+
"disabled": "已禁用:{0} 不在 bundles 里。",
|
|
149
|
+
"notInBundles": "在 bundles 里找不到 {0}",
|
|
150
|
+
"alreadyDisabled": "已经禁用:bundles 里本来就没有 {0}(没有改动任何文件)。",
|
|
151
|
+
"alreadyEnabled": "已经启用:{0} 本来就在 bundles 里(没有改动任何文件)。",
|
|
152
|
+
"noBundles": "这个 profile 里没有 dsh.profile.bundles 列表,装不回去(请手动加一行 \"dsh-autorestart\")。",
|
|
153
|
+
"dryRunWill": "[试运行] 将会{0}。",
|
|
154
|
+
"dryRunWrite": "[试运行] 将会写入:{0}",
|
|
155
|
+
"dryRunBackup": "[试运行] 将会先备份成:{0}.bak-emergency-<时间戳>",
|
|
156
|
+
"dryRunNoChange": "[试运行] 没有改动任何文件。",
|
|
157
|
+
"backupFailed": "备份失败(没有动原文件):{0}",
|
|
158
|
+
"writeFailed": "写入失败(备份在 {0}):{1}",
|
|
159
|
+
"rolledBack": "已经用备份回滚:{0}",
|
|
160
|
+
"verifyFailedNoRollback": "校验失败,而且回滚也失败了(备份在 {0}):{1}",
|
|
161
|
+
"verifyFailedRolledBack": "校验失败(已回滚):{0}",
|
|
162
|
+
"done": "已经{0}。",
|
|
163
|
+
"backup": "备份:{0}",
|
|
164
|
+
"restartHint": "请重启 DSH 才会生效。",
|
|
165
|
+
"summaryRemove": "把 dsh-autorestart 从 bundles 里拿掉",
|
|
166
|
+
"summaryAdd": "把 dsh-autorestart 加回 bundles"
|
|
167
|
+
}
|
|
168
|
+
}
|
|
@@ -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%
|