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.
@@ -1,22 +1,37 @@
1
- <#
1
+ <#
2
2
  .SYNOPSIS
3
- dsh-autorestart 的急救开关:DSH 起不来的时候,不用 DSH 也能把本插件摘掉(或装回去)。
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
- “刚打开插件,DSH 就起不来了” 是插件最难受的时刻:修它需要它自己。
7
- 这个脚本只动一个文件 —— 桌面版 profile 的 package.json —— 而且只动一个数组:
8
- `dsh.profile.bundles`。
9
-
10
- - disable:从 bundles 里拿掉 "dsh-autorestart"(本来就不在 → 报告“已经禁用”,不算失败);
11
- - enable :把 "dsh-autorestart" 加回 bundles(已经在 → 报告“已经启用”);
12
- - status :只读,打印现在在不在 bundles 里。
13
-
14
- 两条硬规矩:
15
-
16
- 1. 写之前先备份成 `package.json.bak-emergency-<yyyyMMdd-HHmmss>`;
17
- 2. 写完之后重新解析一遍 JSON 校验;校验不过就用备份回滚,并且如实报错。
18
-
19
- 文件的缩进、行尾和末尾换行都原样保留(改的是文本,不是重新序列化整个 JSON)。
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 就能跑(没有用 6.0+ 的语法),双击的 .cmd 入口也是它。
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
- # 要操作的 package.json(测试和别的 profile 用)。
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
- # 本插件在 bundles 里的名字。
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 ('急救开关失败:' + $Text) -ForegroundColor Red
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
- # 文本工具:只在 bundles 数组那一段里动刀,其余字节一个不碰。
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 里没有 bundles 这一项' }
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
- # 这一行末尾的行尾符(\r\n 或 \n)算在行里。
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 ('找不到 profile 的 package.json:' + $PackageJson)
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 ('这个 package.json 不是合法的 JSON:' + $_.Exception.Message)
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 ('文件:' + $path)
239
+ Say ((T 'emergency.file' 'file: {0}') -f $path)
172
240
 
173
241
  if ($Action -eq 'status') {
174
- if ($present) { Say ('已启用:' + $PackageName + ' 在 bundles 里。') 'Green' }
175
- else { Say ('已禁用:' + $PackageName + ' 不在 bundles 里。') 'Yellow' }
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 '这个 profile 里没有 dsh.profile.bundles 列表,装不回去(请手动加一行 "dsh-autorestart")。'
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 ('已经禁用:bundles 里本来就没有 ' + $PackageName + '(没有改动任何文件)。') 'Yellow'
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 ('在 bundles 里找不到 ' + $PackageName) }
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
- # 它是数组里的最后一项:前一项的逗号要去掉,否则 JSON 非法。
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 = ('把 ' + $PackageName + ' 从 bundles 里拿掉')
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 ('已经启用:' + $PackageName + ' 本来就在 bundles 里(没有改动任何文件)。') 'Yellow'
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 = ('把 ' + $PackageName + ' 加回 bundles')
316
+ $summary = (T 'emergency.summaryAdd' 'put {0} back into the bundles') -f $PackageName
247
317
  }
248
318
 
249
319
  if ($DryRun) {
250
- Say ('[试运行] 将会' + $summary + '。') 'Cyan'
251
- Say ('[试运行] 将会写入:' + $path) 'Cyan'
252
- Say ('[试运行] 将会先备份成:' + $path + '.bak-emergency-<时间戳>') 'Cyan'
253
- Say ('[试运行] 没有改动任何文件。') 'Cyan'
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 ('备份失败(没有动原文件):' + $_.Exception.Message)
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 ('写入失败(备份在 ' + $backup + '):' + $_.Exception.Message)
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 '写完之后 bundles 里仍然没有 ' + $PackageName
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 '写完之后 bundles 里仍然有 ' + $PackageName
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 ('已经用备份回滚:' + $backup) 'Yellow'
363
+ Say ((T 'emergency.rolledBack' 'the backup was put back: {0}') -f $backup) 'Yellow'
293
364
  } catch {
294
- Fail ('校验失败,而且回滚也失败了(备份在 ' + $backup + '):' + $_.Exception.Message)
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 ('校验失败(已回滚):' + $_.Exception.Message)
367
+ Fail ((T 'emergency.verifyFailedRolledBack' 'the check failed (it was rolled back): {0}') -f $_.Exception.Message)
297
368
  }
298
369
 
299
- Say ('已经' + $summary + '。') 'Green'
300
- Say ('备份:' + $backup) 'Gray'
301
- Say ('请重启 DSH 才会生效。') 'Gray'
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,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%