@astralyn/sash 0.1.0 → 0.1.2

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 (108) hide show
  1. package/CHANGELOG.md +48 -0
  2. package/README.md +22 -7
  3. package/THIRD_PARTY_NOTICES.md +9 -0
  4. package/dist/api.js +14 -45
  5. package/dist/app-state.js +99 -0
  6. package/dist/autostart/command.js +24 -0
  7. package/dist/autostart/context.js +43 -0
  8. package/dist/autostart/files.js +60 -0
  9. package/dist/autostart/installation.js +48 -0
  10. package/dist/autostart/start.js +47 -0
  11. package/dist/autostart/windows-registry.js +88 -0
  12. package/dist/autostart/windows.js +59 -0
  13. package/dist/autostart-contract.js +31 -0
  14. package/dist/autostart-entry.js +10 -0
  15. package/dist/autostart.js +69 -0
  16. package/dist/cli.js +11 -11
  17. package/dist/commands/auto.js +15 -0
  18. package/dist/commands/lifecycle.js +5 -31
  19. package/dist/commands/logs.js +11 -4
  20. package/dist/commands/shared.js +2 -4
  21. package/dist/commands/status.js +6 -7
  22. package/dist/commands/update.js +7 -3
  23. package/dist/commands/web.js +19 -27
  24. package/dist/contracts.js +171 -335
  25. package/dist/core-config-validation.js +22 -38
  26. package/dist/core-update.js +126 -762
  27. package/dist/core.js +20 -46
  28. package/dist/daemon/app.js +132 -113
  29. package/dist/daemon/context.js +13 -7
  30. package/dist/daemon/entry.js +21 -38
  31. package/dist/daemon/errors.js +6 -1
  32. package/dist/daemon/handlers/autostart.js +18 -0
  33. package/dist/daemon/handlers/core.js +32 -9
  34. package/dist/daemon/handlers/daemon.js +32 -6
  35. package/dist/daemon/handlers/profiles.js +12 -2
  36. package/dist/daemon/handlers/settings.js +9 -36
  37. package/dist/daemon/router.js +40 -27
  38. package/dist/daemon/scheduler.js +3 -1
  39. package/dist/daemon/server.js +6 -3
  40. package/dist/daemon/web-auth.js +52 -0
  41. package/dist/daemon-auth.js +2 -2
  42. package/dist/daemon-client.js +18 -2
  43. package/dist/daemon-http.js +7 -0
  44. package/dist/daemon-lifecycle.js +19 -196
  45. package/dist/github.js +7 -2
  46. package/dist/http.js +7 -5
  47. package/dist/log-follow.js +2 -1
  48. package/dist/mihomo-config.js +7 -9
  49. package/dist/paths.js +5 -7
  50. package/dist/profile-model.js +87 -0
  51. package/dist/profile-service.js +250 -587
  52. package/dist/profiles.js +43 -171
  53. package/dist/runtime-lifecycle.js +112 -194
  54. package/dist/runtime-owner.js +37 -76
  55. package/dist/sash-client.js +63 -21
  56. package/dist/settings-service.js +29 -228
  57. package/dist/settings.js +61 -285
  58. package/dist/status.js +24 -40
  59. package/dist/supervisor.js +1 -14
  60. package/dist/sysproxy/common.js +5 -45
  61. package/dist/sysproxy/factory.js +24 -65
  62. package/dist/sysproxy/snapshot.js +24 -257
  63. package/dist/sysproxy.js +1 -4
  64. package/dist/system-proxy-manager.js +37 -35
  65. package/dist/ui/assets/{ConnectionsView-DNGmZBSU.js → ConnectionsView-BgXQM6Z4.js} +1 -1
  66. package/dist/ui/assets/{LogsView-fSiaxQ13.js → LogsView-2f542P8z.js} +1 -1
  67. package/dist/ui/assets/{PaginationFooter-B3kHzRfB.js → PaginationFooter-cj1tcZej.js} +1 -1
  68. package/dist/ui/assets/ProfileEditorDialog-C9M8uKZC.css +1 -0
  69. package/dist/ui/assets/ProfileEditorDialog-DD4y4GBC.js +14 -0
  70. package/dist/ui/assets/ProfilesView-BXU2DOA7.css +1 -0
  71. package/dist/ui/assets/ProfilesView-DNnzYIEY.js +7 -0
  72. package/dist/ui/assets/{RulesView-D9vZBiJ1.js → RulesView-CtKvlckZ.js} +1 -1
  73. package/dist/ui/assets/SettingsView-BaeKFF_N.js +1 -0
  74. package/dist/ui/assets/SettingsView-CWjIe05v.css +1 -0
  75. package/dist/ui/assets/{0be242294f7d791af850c6df38ac78a0-2cL6Ntwf.woff2 → e2a57555d97d0b02b45d9418eb6ee295-D9nhF3rM.woff2} +0 -0
  76. package/dist/ui/assets/index-CBInDvdJ.js +19 -0
  77. package/dist/ui/assets/index-mOLy7fkB.css +1 -0
  78. package/dist/ui/assets/{theme-BNq4FkXS.js → theme-D40MF8cQ.js} +1 -1
  79. package/dist/ui/index.html +2 -2
  80. package/dist/web-bootstrap.js +113 -0
  81. package/docs/architecture-proposal.md +109 -0
  82. package/docs/autostart.md +101 -0
  83. package/docs/backend.md +92 -216
  84. package/docs/frontend.md +61 -97
  85. package/docs/usage.md +88 -186
  86. package/package.json +10 -6
  87. package/dist/commands/upgrade.js +0 -43
  88. package/dist/core-install-transaction.js +0 -114
  89. package/dist/core-update-coordination.js +0 -105
  90. package/dist/core-update-service.js +0 -245
  91. package/dist/managed-state-transaction.js +0 -377
  92. package/dist/offline-mutation.js +0 -58
  93. package/dist/profile-migration.js +0 -101
  94. package/dist/runtime-recovery.js +0 -31
  95. package/dist/sysproxy/darwin.js +0 -287
  96. package/dist/sysproxy/gnome.js +0 -181
  97. package/dist/sysproxy/legacy.js +0 -58
  98. package/dist/tun-guidance.js +0 -11
  99. package/dist/ui/assets/CodeEditorModal-6m0TJWyF.css +0 -1
  100. package/dist/ui/assets/CodeEditorModal-CFEnWsyh.js +0 -14
  101. package/dist/ui/assets/ProfileEditorDialog-BydoZthX.js +0 -1
  102. package/dist/ui/assets/ProfilesView-Btc1DOxE.js +0 -2
  103. package/dist/ui/assets/ProfilesView-ByqdFNHN.css +0 -1
  104. package/dist/ui/assets/SettingsFileDialog-CNFEAVs4.js +0 -1
  105. package/dist/ui/assets/SettingsView-BlDhZkXQ.js +0 -2
  106. package/dist/ui/assets/SettingsView-CngS3vBM.css +0 -1
  107. package/dist/ui/assets/index-B61V60w_.js +0 -19
  108. package/dist/ui/assets/index-bkyxJG8J.css +0 -1
@@ -1,804 +1,168 @@
1
1
  import fs from "node:fs";
2
2
  import { verifyCoreExecutable } from "./core.js";
3
- import { installRecordsEqual, parseInstallRecord, readInstallRecord, validateCoreReleaseTag, writeInstallRecord, } from "./core-install-record.js";
3
+ import { installRecordsEqual, parseInstallRecord, readInstallRecord, writeInstallRecord, } from "./core-install-record.js";
4
4
  import { atomicWriteFileSync, durableRemoveFileSync, durableRenameSync, pathEntryExists, } from "./fs-atomic.js";
5
- import { hasExactOwnKeys, isCanonicalIsoTimestamp, isPlainObject } from "./json-shape.js";
5
+ import { hasExactOwnKeys, isPlainObject } from "./json-shape.js";
6
6
  import { waitForBinaryUnlocked } from "./process.js";
7
- const UPDATE_TRANSACTION_SIZE_LIMIT = 16 * 1024;
8
- function parseTargetRecord(value, createdAt) {
9
- const targetRecord = parseInstallRecord(value);
10
- return targetRecord?.installedAt === createdAt ? targetRecord : undefined;
7
+ function verifyBinary(file, version, verify) {
8
+ if (!fs.lstatSync(file).isFile())
9
+ throw new Error(`Core binary must be a regular file: ${file}`);
10
+ verify(file, version);
11
11
  }
12
- function parseNormalTransaction(value) {
13
- if (!hasExactOwnKeys(value, [
14
- "createdAt",
15
- "deferredHealth",
16
- "phase",
17
- "previousRecord",
18
- "targetRecord",
19
- "version",
20
- ]) ||
21
- value.version !== 1 ||
22
- (value.phase !== "prepared" &&
23
- value.phase !== "swapped" &&
24
- value.phase !== "health-verified") ||
25
- !isCanonicalIsoTimestamp(value.createdAt) ||
26
- typeof value.deferredHealth !== "boolean") {
27
- return undefined;
28
- }
29
- let previousRecord = null;
30
- if (value.previousRecord !== null) {
31
- const parsedPrevious = parseInstallRecord(value.previousRecord);
32
- if (!parsedPrevious)
33
- return undefined;
34
- previousRecord = parsedPrevious;
35
- }
36
- const targetRecord = parseTargetRecord(value.targetRecord, value.createdAt);
37
- if (!targetRecord)
38
- return undefined;
39
- return {
40
- version: 1,
41
- phase: value.phase,
42
- createdAt: value.createdAt,
43
- previousRecord,
44
- targetRecord,
45
- deferredHealth: value.deferredHealth,
46
- };
47
- }
48
- function parseRepairTransaction(value) {
49
- if (!hasExactOwnKeys(value, [
50
- "createdAt",
51
- "deferredHealth",
52
- "phase",
53
- "previousRecord",
54
- "repair",
55
- "targetRecord",
56
- "version",
57
- ]) ||
58
- value.version !== 2 ||
59
- (value.phase !== "repair-prepared" &&
60
- value.phase !== "repair-restoring" &&
61
- value.phase !== "prepared" &&
62
- value.phase !== "swapped" &&
63
- value.phase !== "health-verified") ||
64
- !isCanonicalIsoTimestamp(value.createdAt) ||
65
- typeof value.deferredHealth !== "boolean" ||
66
- value.previousRecord !== null ||
67
- !isPlainObject(value.repair) ||
68
- !hasExactOwnKeys(value.repair, ["binaryExisted", "installRecordExisted"]) ||
69
- typeof value.repair.binaryExisted !== "boolean" ||
70
- typeof value.repair.installRecordExisted !== "boolean" ||
71
- (!value.repair.binaryExisted && !value.repair.installRecordExisted)) {
72
- return undefined;
73
- }
74
- const targetRecord = parseTargetRecord(value.targetRecord, value.createdAt);
75
- if (!targetRecord)
76
- return undefined;
77
- return {
78
- version: 2,
79
- phase: value.phase,
80
- createdAt: value.createdAt,
81
- previousRecord: null,
82
- targetRecord,
83
- deferredHealth: value.deferredHealth,
84
- repair: {
85
- binaryExisted: value.repair.binaryExisted,
86
- installRecordExisted: value.repair.installRecordExisted,
87
- },
88
- };
89
- }
90
- function parseTransaction(value) {
91
- if (!isPlainObject(value))
92
- return undefined;
93
- return value.version === 1
94
- ? parseNormalTransaction(value)
95
- : value.version === 2
96
- ? parseRepairTransaction(value)
97
- : undefined;
98
- }
99
- function backupPath(layout) {
100
- return `${layout.coreExe}.bak`;
101
- }
102
- function repairBinaryBackupPath(layout) {
103
- return `${layout.coreExe}.repair.bak`;
104
- }
105
- function repairInstallBackupPath(layout) {
106
- return `${layout.installFile}.repair.bak`;
107
- }
108
- function defaultVerifier(exe, expectedVersion) {
109
- verifyCoreExecutable(exe, 5000, expectedVersion);
110
- }
111
- function verifyTransactionBinary(file, expectedVersion, verifier, role) {
112
- let stat;
113
- try {
114
- stat = fs.lstatSync(file);
115
- }
116
- catch (err) {
117
- throw new Error(`${role} is missing: ${file}: ${err.message}`);
118
- }
119
- if (!stat.isFile())
120
- throw new Error(`${role} is not a regular file: ${file}`);
121
- try {
122
- verifier(file, expectedVersion);
123
- }
124
- catch (err) {
125
- throw new Error(`${role} does not match expected version ${expectedVersion}: ${file}: ${err.message}`);
126
- }
127
- }
128
- function writeTransaction(transaction, layout) {
129
- atomicWriteFileSync(layout.coreUpdateTransactionFile, `${JSON.stringify(transaction, null, 2)}\n`);
12
+ function defaultVerifier(exe, version) {
13
+ verifyCoreExecutable(exe, 5000, version);
130
14
  }
131
15
  export function readCoreUpdateTransaction(layout) {
132
- let stat;
16
+ let text;
133
17
  try {
134
- stat = fs.lstatSync(layout.coreUpdateTransactionFile);
135
- }
136
- catch (err) {
137
- if (err.code === "ENOENT")
18
+ const stat = fs.lstatSync(layout.coreUpdateTransactionFile);
19
+ if (!stat.isFile() || stat.size > 16 * 1024)
20
+ throw new Error("Core update journal must be a bounded regular file");
21
+ text = fs.readFileSync(layout.coreUpdateTransactionFile, "utf8");
22
+ if (Buffer.byteLength(text) > 16 * 1024)
23
+ throw new Error("Core update journal is too large");
24
+ }
25
+ catch (error) {
26
+ if (error.code === "ENOENT")
138
27
  return undefined;
139
- throw new Error(`Cannot inspect Core update transaction ${layout.coreUpdateTransactionFile}: ${err.message}`);
140
- }
141
- if (!stat.isFile()) {
142
- throw new Error(`Core update transaction is not a regular file: ${layout.coreUpdateTransactionFile}`);
143
- }
144
- if (stat.size > UPDATE_TRANSACTION_SIZE_LIMIT) {
145
- throw new Error(`Core update transaction exceeds ${UPDATE_TRANSACTION_SIZE_LIMIT} bytes: ${layout.coreUpdateTransactionFile}`);
146
- }
147
- let parsed;
148
- try {
149
- parsed = JSON.parse(fs.readFileSync(layout.coreUpdateTransactionFile, "utf8"));
150
- }
151
- catch (err) {
152
- throw new Error(`Core update transaction is not valid JSON: ${layout.coreUpdateTransactionFile} (${err.message})`);
153
- }
154
- const transaction = parseTransaction(parsed);
155
- if (!transaction) {
156
- throw new Error(`Core update transaction has an invalid version, phase, timestamp, records, repair snapshot, or shape: ${layout.coreUpdateTransactionFile}`);
157
- }
158
- return transaction;
159
- }
160
- function assertRollbackSlotsClear(layout) {
161
- for (const file of [
162
- backupPath(layout),
163
- repairBinaryBackupPath(layout),
164
- repairInstallBackupPath(layout),
165
- ]) {
166
- if (pathEntryExists(file))
167
- throw new Error(`Previous Core rollback state still exists: ${file}`);
168
- }
169
- }
170
- export function beginCoreUpdateTransaction(layout, targetVersion, previousRecord, deferredHealth, createdAt = new Date().toISOString()) {
171
- const targetRecord = {
172
- coreVersion: validateCoreReleaseTag(targetVersion),
173
- installedAt: createdAt,
174
- };
175
- if (!isCanonicalIsoTimestamp(createdAt)) {
176
- throw new Error(`Invalid Core update timestamp: ${createdAt}`);
177
- }
178
- if (readCoreUpdateTransaction(layout)) {
179
- throw new Error(`A Core update transaction is already pending: ${layout.coreUpdateTransactionFile}`);
180
- }
181
- assertRollbackSlotsClear(layout);
182
- const currentExists = pathEntryExists(layout.coreExe);
183
- const installEntryExists = pathEntryExists(layout.installFile);
184
- if (currentExists !== Boolean(previousRecord) || installEntryExists !== Boolean(previousRecord)) {
185
- throw new Error("Cannot begin Core update with inconsistent binary/install metadata state");
186
- }
187
- const transaction = {
188
- version: 1,
189
- phase: "prepared",
190
- createdAt,
191
- previousRecord: previousRecord ?? null,
192
- targetRecord,
193
- deferredHealth,
194
- };
195
- writeTransaction(transaction, layout);
196
- return transaction;
197
- }
198
- function isRegularFile(file) {
199
- try {
200
- return fs.lstatSync(file).isFile();
201
- }
202
- catch {
203
- return false;
204
- }
205
- }
206
- function assertRepairableCoreEntry(file, role) {
207
- if (!pathEntryExists(file))
208
- return;
209
- if (fs.lstatSync(file).isDirectory()) {
210
- throw new Error(`Cannot repair Core installation: ${role} path is a directory: ${file}`);
211
- }
212
- }
213
- export function beginCoreRepairUpdateTransaction(layout, targetVersion, deferredHealth, createdAt = new Date().toISOString()) {
214
- const targetRecord = {
215
- coreVersion: validateCoreReleaseTag(targetVersion),
216
- installedAt: createdAt,
217
- };
218
- if (!isCanonicalIsoTimestamp(createdAt)) {
219
- throw new Error(`Invalid Core update timestamp: ${createdAt}`);
220
- }
221
- if (readCoreUpdateTransaction(layout)) {
222
- throw new Error(`A Core update transaction is already pending: ${layout.coreUpdateTransactionFile}`);
223
- }
224
- assertRollbackSlotsClear(layout);
225
- const binaryExisted = pathEntryExists(layout.coreExe);
226
- const installRecordExisted = pathEntryExists(layout.installFile);
227
- const previousRecord = readInstallRecord(layout);
228
- if ((!binaryExisted && !installRecordExisted) ||
229
- (binaryExisted && installRecordExisted && isRegularFile(layout.coreExe) && previousRecord)) {
230
- throw new Error("Core installation does not require forced repair");
231
- }
232
- assertRepairableCoreEntry(layout.coreExe, "executable");
233
- assertRepairableCoreEntry(layout.installFile, "install metadata");
234
- const transaction = {
235
- version: 2,
236
- phase: "repair-prepared",
237
- createdAt,
238
- previousRecord: null,
239
- targetRecord,
240
- deferredHealth,
241
- repair: { binaryExisted, installRecordExisted },
242
- };
243
- writeTransaction(transaction, layout);
244
- return transaction;
245
- }
246
- function assertSameTransaction(expected, current) {
247
- const sameRepair = expected.version === 1
248
- ? current?.version === 1
249
- : current?.version === 2 &&
250
- current.repair.binaryExisted === expected.repair.binaryExisted &&
251
- current.repair.installRecordExisted === expected.repair.installRecordExisted;
252
- if (!current ||
253
- current.version !== expected.version ||
254
- current.createdAt !== expected.createdAt ||
255
- current.targetRecord.coreVersion !== expected.targetRecord.coreVersion ||
256
- current.targetRecord.installedAt !== expected.targetRecord.installedAt ||
257
- !sameRepair) {
258
- throw new Error("Core update transaction changed during publication");
259
- }
260
- }
261
- export function quarantineCoreInstallationForUpdate(transaction, layout) {
262
- const current = readCoreUpdateTransaction(layout);
263
- assertSameTransaction(transaction, current);
264
- if (current.version !== 2 || current.phase !== "repair-prepared") {
265
- throw new Error("Core repair transaction is not ready for quarantine");
266
- }
267
- if (current.repair.binaryExisted) {
268
- if (!pathEntryExists(layout.coreExe)) {
269
- throw new Error("Core repair executable disappeared before quarantine");
270
- }
271
- durableRenameSync(layout.coreExe, repairBinaryBackupPath(layout));
272
- }
273
- else if (pathEntryExists(layout.coreExe)) {
274
- throw new Error("Core executable appeared before repair quarantine");
275
- }
276
- if (current.repair.installRecordExisted) {
277
- if (!pathEntryExists(layout.installFile)) {
278
- throw new Error("Core install metadata disappeared before quarantine");
279
- }
280
- durableRenameSync(layout.installFile, repairInstallBackupPath(layout));
28
+ throw error;
281
29
  }
282
- else if (pathEntryExists(layout.installFile)) {
283
- throw new Error("Core install metadata appeared before repair quarantine");
284
- }
285
- if (pathEntryExists(layout.coreExe) ||
286
- pathEntryExists(layout.installFile) ||
287
- pathEntryExists(repairBinaryBackupPath(layout)) !== current.repair.binaryExisted ||
288
- pathEntryExists(repairInstallBackupPath(layout)) !== current.repair.installRecordExisted) {
289
- throw new Error("Core repair quarantine could not be verified");
290
- }
291
- const prepared = { ...current, phase: "prepared" };
292
- writeTransaction(prepared, layout);
293
- return prepared;
294
- }
295
- export function markCoreUpdateSwapped(transaction, layout) {
296
- const current = readCoreUpdateTransaction(layout);
297
- assertSameTransaction(transaction, current);
298
- if (current.phase !== "prepared")
299
- throw new Error("Core update is not ready to mark swapped");
300
- const swapped = { ...current, phase: "swapped" };
301
- writeTransaction(swapped, layout);
302
- return swapped;
303
- }
304
- export function markCoreUpdateHealthVerified(transaction, layout) {
305
- const current = readCoreUpdateTransaction(layout);
306
- assertSameTransaction(transaction, current);
307
- if (current.phase !== "swapped") {
308
- throw new Error("Core update is not ready to mark health-verified");
30
+ const value = JSON.parse(text);
31
+ if (!isPlainObject(value) ||
32
+ !hasExactOwnKeys(value, ["version", "phase", "previous", "target"]) ||
33
+ value.version !== 1 ||
34
+ typeof value.phase !== "string" ||
35
+ !["prepared", "swapped", "verified"].includes(value.phase)) {
36
+ throw new Error("Invalid Core update journal");
309
37
  }
310
- const verified = { ...current, phase: "health-verified" };
311
- writeTransaction(verified, layout);
312
- return verified;
38
+ const previous = value.previous === null ? null : parseInstallRecord(value.previous);
39
+ const target = parseInstallRecord(value.target);
40
+ if (previous === undefined || !target)
41
+ throw new Error("Invalid Core update install records");
42
+ return { version: 1, phase: value.phase, previous, target };
313
43
  }
314
- function markCoreRepairRestoring(transaction, layout) {
315
- const current = readCoreUpdateTransaction(layout);
316
- assertSameTransaction(transaction, current);
317
- if (current.version !== 2)
318
- throw new Error("Core update is not a repair transaction");
319
- if (current.phase === "repair-restoring")
320
- return current;
321
- const restoring = { ...current, phase: "repair-restoring" };
322
- writeTransaction(restoring, layout);
323
- return restoring;
44
+ function writeJournal(layout, transaction) {
45
+ atomicWriteFileSync(layout.coreUpdateTransactionFile, `${JSON.stringify(transaction, null, 2)}\n`);
324
46
  }
325
- export function clearCoreUpdateTransaction(layout) {
47
+ function clearJournal(layout) {
326
48
  durableRemoveFileSync(layout.coreUpdateTransactionFile);
327
49
  }
328
- function restoreInstallRecord(record, layout) {
329
- if (record)
330
- writeInstallRecord(record, layout);
331
- else
332
- durableRemoveFileSync(layout.installFile);
333
- }
334
- function rollbackNormalCoreUpdate(transaction, layout, verifier) {
335
- const backup = backupPath(layout);
336
- const currentExists = pathEntryExists(layout.coreExe);
337
- const backupExists = pathEntryExists(backup);
338
- if (transaction.previousRecord) {
339
- if (backupExists) {
340
- verifyTransactionBinary(backup, transaction.previousRecord.coreVersion, verifier, "Core rollback backup");
341
- if (currentExists) {
342
- verifyTransactionBinary(layout.coreExe, transaction.targetRecord.coreVersion, verifier, "Core update candidate");
50
+ function restoreFiles(layout, transaction, verify) {
51
+ const backup = `${layout.coreExe}.bak`;
52
+ if (transaction.previous) {
53
+ if (pathEntryExists(backup)) {
54
+ verifyBinary(backup, transaction.previous.coreVersion, verify);
55
+ if (pathEntryExists(layout.coreExe)) {
56
+ verifyBinary(layout.coreExe, transaction.target.coreVersion, verify);
343
57
  durableRemoveFileSync(layout.coreExe);
344
58
  }
345
59
  durableRenameSync(backup, layout.coreExe);
346
60
  }
347
61
  else {
348
- if (!currentExists) {
349
- throw new Error("Core update rollback has neither the previous binary nor its backup");
350
- }
351
- verifyTransactionBinary(layout.coreExe, transaction.previousRecord.coreVersion, verifier, "Previous Core binary");
62
+ verifyBinary(layout.coreExe, transaction.previous.coreVersion, verify);
352
63
  }
64
+ writeInstallRecord(transaction.previous, layout);
353
65
  }
354
66
  else {
355
- if (backupExists) {
356
- throw new Error(`Unexpected rollback backup for an update without a previous Core: ${backup}`);
357
- }
358
- if (currentExists) {
359
- verifyTransactionBinary(layout.coreExe, transaction.targetRecord.coreVersion, verifier, "Core update candidate");
67
+ if (pathEntryExists(backup))
68
+ throw new Error("Unexpected Core backup for a first install");
69
+ if (pathEntryExists(layout.coreExe)) {
70
+ verifyBinary(layout.coreExe, transaction.target.coreVersion, verify);
360
71
  durableRemoveFileSync(layout.coreExe);
361
72
  }
362
- }
363
- restoreInstallRecord(transaction.previousRecord, layout);
364
- clearCoreUpdateTransaction(layout);
365
- return transaction.previousRecord;
366
- }
367
- function removeRepairCandidateBinary(transaction, layout, verifier) {
368
- if (!pathEntryExists(layout.coreExe))
369
- return;
370
- verifyTransactionBinary(layout.coreExe, transaction.targetRecord.coreVersion, verifier, "Core repair candidate");
371
- durableRemoveFileSync(layout.coreExe);
372
- }
373
- function verifyRepairCandidateRecord(transaction, layout) {
374
- const current = readInstallRecord(layout);
375
- if (!installRecordsEqual(current, transaction.targetRecord)) {
376
- throw new Error("Core repair candidate install metadata does not match its journal");
377
- }
378
- }
379
- function removeRepairCandidateRecord(transaction, layout) {
380
- if (!pathEntryExists(layout.installFile))
381
- return;
382
- verifyRepairCandidateRecord(transaction, layout);
383
- durableRemoveFileSync(layout.installFile);
384
- }
385
- function assertRepairRollbackOwnership(transaction, layout, verifier) {
386
- if (pathEntryExists(backupPath(layout))) {
387
- throw new Error(`Core repair has an unexpected normal rollback slot: ${backupPath(layout)}`);
388
- }
389
- if (transaction.phase === "repair-restoring")
390
- return;
391
- const binaryCurrent = pathEntryExists(layout.coreExe);
392
- const binaryBackup = pathEntryExists(repairBinaryBackupPath(layout));
393
- if (transaction.repair.binaryExisted) {
394
- if (transaction.phase === "repair-prepared") {
395
- if (binaryCurrent === binaryBackup) {
396
- throw new Error("Core repair cannot identify the original executable during rollback");
397
- }
398
- }
399
- else if (!binaryBackup) {
400
- throw new Error("Core repair rollback lost the quarantined executable");
73
+ if (pathEntryExists(layout.installFile)) {
74
+ if (!installRecordsEqual(readInstallRecord(layout), transaction.target))
75
+ throw new Error("Unrecognized Core install metadata; preserved for inspection");
76
+ durableRemoveFileSync(layout.installFile);
401
77
  }
402
78
  }
403
- else if (binaryBackup) {
404
- throw new Error(`Core repair has an unexpected executable backup: ${repairBinaryBackupPath(layout)}`);
405
- }
406
- if (transaction.phase === "repair-prepared" &&
407
- !transaction.repair.binaryExisted &&
408
- binaryCurrent) {
409
- throw new Error("Core executable appeared during repair preparation");
410
- }
411
- if (transaction.phase !== "repair-prepared" && binaryCurrent) {
412
- verifyTransactionBinary(layout.coreExe, transaction.targetRecord.coreVersion, verifier, "Core repair candidate");
413
- }
414
- if ((transaction.phase === "swapped" || transaction.phase === "health-verified") &&
415
- !binaryCurrent) {
416
- throw new Error("Core repair candidate disappeared before rollback");
417
- }
418
- const installCurrent = pathEntryExists(layout.installFile);
419
- const installBackup = pathEntryExists(repairInstallBackupPath(layout));
420
- if (transaction.repair.installRecordExisted) {
421
- if (transaction.phase === "repair-prepared") {
422
- if (installCurrent === installBackup) {
423
- throw new Error("Core repair cannot identify the original install metadata during rollback");
424
- }
425
- }
426
- else if (!installBackup) {
427
- throw new Error("Core repair rollback lost the quarantined install metadata");
428
- }
429
- }
430
- else if (installBackup) {
431
- throw new Error(`Core repair has an unexpected install metadata backup: ${repairInstallBackupPath(layout)}`);
432
- }
433
- if (transaction.phase === "repair-prepared") {
434
- if (!transaction.repair.installRecordExisted && installCurrent) {
435
- throw new Error("Core install metadata appeared during repair preparation");
436
- }
437
- return;
438
- }
439
- if (transaction.phase === "prepared") {
440
- if (installCurrent) {
441
- throw new Error("Core install metadata appeared before the candidate swap was journaled");
442
- }
443
- return;
444
- }
445
- const shouldHaveTargetRecord = transaction.phase === "health-verified" || transaction.deferredHealth;
446
- if (shouldHaveTargetRecord !== installCurrent) {
447
- throw new Error("Core repair candidate install metadata does not match its journal phase");
448
- }
449
- if (installCurrent)
450
- verifyRepairCandidateRecord(transaction, layout);
451
79
  }
452
- function rollbackRepairCoreUpdate(transaction, layout, verifier) {
453
- assertRepairRollbackOwnership(transaction, layout, verifier);
454
- const restoring = markCoreRepairRestoring(transaction, layout);
455
- const binaryBackup = repairBinaryBackupPath(layout);
456
- const installBackup = repairInstallBackupPath(layout);
457
- if (restoring.repair.binaryExisted) {
458
- if (pathEntryExists(binaryBackup)) {
459
- removeRepairCandidateBinary(restoring, layout, verifier);
460
- durableRenameSync(binaryBackup, layout.coreExe);
461
- }
462
- else if (!pathEntryExists(layout.coreExe)) {
463
- throw new Error("Core repair rollback lost the quarantined executable");
464
- }
465
- }
466
- else {
467
- if (pathEntryExists(binaryBackup)) {
468
- throw new Error(`Core repair has an unexpected executable backup: ${binaryBackup}`);
469
- }
470
- removeRepairCandidateBinary(restoring, layout, verifier);
471
- }
472
- if (restoring.repair.installRecordExisted) {
473
- if (pathEntryExists(installBackup)) {
474
- removeRepairCandidateRecord(restoring, layout);
475
- durableRenameSync(installBackup, layout.installFile);
476
- }
477
- else if (!pathEntryExists(layout.installFile)) {
478
- throw new Error("Core repair rollback lost the quarantined install metadata");
479
- }
480
- }
481
- else {
482
- if (pathEntryExists(installBackup)) {
483
- throw new Error(`Core repair has an unexpected install metadata backup: ${installBackup}`);
484
- }
485
- removeRepairCandidateRecord(restoring, layout);
486
- }
487
- if (pathEntryExists(binaryBackup) ||
488
- pathEntryExists(installBackup) ||
489
- pathEntryExists(layout.coreExe) !== restoring.repair.binaryExisted ||
490
- pathEntryExists(layout.installFile) !== restoring.repair.installRecordExisted) {
491
- throw new Error("Core repair rollback could not restore the exact prior entry presence");
492
- }
493
- clearCoreUpdateTransaction(layout);
494
- return null;
495
- }
496
- export function rollbackCoreUpdateTransaction(layout, verifier = defaultVerifier) {
497
- const transaction = readCoreUpdateTransaction(layout);
498
- if (!transaction)
499
- return undefined;
500
- return transaction.version === 1
501
- ? rollbackNormalCoreUpdate(transaction, layout, verifier)
502
- : rollbackRepairCoreUpdate(transaction, layout, verifier);
503
- }
504
- function assertRepairBackupEntry(file, role) {
505
- if (!pathEntryExists(file))
506
- return;
507
- if (fs.lstatSync(file).isDirectory()) {
508
- throw new Error(`Pending Core repair ${role} backup is a directory: ${file}`);
509
- }
510
- }
511
- function validatePendingRepairAssets(transaction, layout) {
512
- if (pathEntryExists(backupPath(layout))) {
513
- throw new Error(`Pending Core repair has an unexpected normal backup: ${backupPath(layout)}`);
514
- }
515
- const allowRemoved = transaction.phase === "health-verified";
516
- const binaryBackup = repairBinaryBackupPath(layout);
517
- const installBackup = repairInstallBackupPath(layout);
518
- assertRepairBackupEntry(binaryBackup, "executable");
519
- assertRepairBackupEntry(installBackup, "install metadata");
520
- if (transaction.repair.binaryExisted && !allowRemoved && !pathEntryExists(binaryBackup)) {
521
- throw new Error(`Pending Core repair is missing its executable backup: ${binaryBackup}`);
522
- }
523
- if (!transaction.repair.binaryExisted && pathEntryExists(binaryBackup)) {
524
- throw new Error(`Pending Core repair has an unexpected executable backup: ${binaryBackup}`);
525
- }
526
- if (transaction.repair.installRecordExisted && !allowRemoved && !pathEntryExists(installBackup)) {
527
- throw new Error(`Pending Core repair is missing its install metadata backup: ${installBackup}`);
528
- }
529
- if (!transaction.repair.installRecordExisted && pathEntryExists(installBackup)) {
530
- throw new Error(`Pending Core repair has an unexpected install metadata backup: ${installBackup}`);
531
- }
532
- }
533
- function validatePendingTransaction(transaction, layout, verifier) {
534
- if (transaction.phase !== "swapped" && transaction.phase !== "health-verified") {
535
- throw new Error("Core update is not pending a managed health decision");
80
+ function finishVerified(layout, transaction, verify) {
81
+ verifyBinary(layout.coreExe, transaction.target.coreVersion, verify);
82
+ if (!installRecordsEqual(readInstallRecord(layout), transaction.target))
83
+ throw new Error("Verified Core install metadata changed");
84
+ const backup = `${layout.coreExe}.bak`;
85
+ if (pathEntryExists(backup)) {
86
+ if (!transaction.previous)
87
+ throw new Error("Unexpected backup for a first install");
88
+ verifyBinary(backup, transaction.previous.coreVersion, verify);
89
+ durableRemoveFileSync(backup);
536
90
  }
537
- verifyTransactionBinary(layout.coreExe, transaction.targetRecord.coreVersion, verifier, "Pending Core update candidate");
538
- if (transaction.version === 1) {
539
- const backup = backupPath(layout);
540
- if (transaction.previousRecord) {
541
- if (pathEntryExists(backup)) {
542
- verifyTransactionBinary(backup, transaction.previousRecord.coreVersion, verifier, "Pending Core rollback backup");
543
- }
544
- else if (transaction.phase !== "health-verified") {
545
- throw new Error(`Pending Core update is missing its rollback backup: ${backup}`);
546
- }
547
- }
548
- else if (pathEntryExists(backup)) {
549
- throw new Error(`Pending first Core update has an unexpected rollback backup: ${backup}`);
550
- }
551
- }
552
- else {
553
- validatePendingRepairAssets(transaction, layout);
554
- }
555
- const currentRecord = readInstallRecord(layout);
556
- if (!currentRecord && pathEntryExists(layout.installFile)) {
557
- throw new Error("Pending Core update install metadata is malformed or non-regular");
558
- }
559
- const recordMatchesPhase = transaction.version === 2
560
- ? transaction.phase === "health-verified" || transaction.deferredHealth
561
- ? installRecordsEqual(currentRecord, transaction.targetRecord)
562
- : currentRecord === undefined
563
- : transaction.phase === "health-verified"
564
- ? installRecordsEqual(currentRecord, transaction.targetRecord) ||
565
- installRecordsEqual(currentRecord, transaction.previousRecord)
566
- : installRecordsEqual(currentRecord, transaction.previousRecord ?? transaction.targetRecord);
567
- if (!recordMatchesPhase) {
568
- throw new Error("Pending Core update install metadata does not match its journal phase");
569
- }
570
- }
571
- function rollbackRequired(transaction) {
572
- return (transaction.phase === "repair-prepared" ||
573
- transaction.phase === "repair-restoring" ||
574
- transaction.phase === "prepared" ||
575
- (!transaction.deferredHealth && transaction.phase === "swapped"));
91
+ clearJournal(layout);
576
92
  }
577
- export function recoverCoreUpdateTransaction(layout, verifier = defaultVerifier) {
93
+ export function recoverCoreUpdateTransaction(layout, verify = defaultVerifier) {
578
94
  const transaction = readCoreUpdateTransaction(layout);
579
95
  if (!transaction) {
580
- recoverLegacyInterruptedCoreUpdate(layout, verifier);
581
- return undefined;
582
- }
583
- if (rollbackRequired(transaction)) {
584
- rollbackCoreUpdateTransaction(layout, verifier);
585
- return undefined;
586
- }
587
- validatePendingTransaction(transaction, layout, verifier);
588
- if (transaction.phase === "health-verified") {
589
- writeInstallRecord(transaction.targetRecord, layout);
590
- }
591
- return transaction;
592
- }
593
- export function pendingCoreUpdateVersion(layout) {
594
- const transaction = readCoreUpdateTransaction(layout);
595
- return transaction?.phase === "swapped" || transaction?.phase === "health-verified"
596
- ? transaction.targetRecord.coreVersion
597
- : undefined;
598
- }
599
- function removeCommittedRollbackAssets(transaction, layout) {
600
- if (transaction.version === 1) {
601
- durableRemoveFileSync(backupPath(layout));
96
+ if (pathEntryExists(`${layout.coreExe}.bak`))
97
+ throw new Error("Core backup has no ownership journal; preserved for inspection");
602
98
  return;
603
99
  }
604
- durableRemoveFileSync(repairBinaryBackupPath(layout));
605
- durableRemoveFileSync(repairInstallBackupPath(layout));
606
- }
607
- function rollbackAssetsExist(transaction, layout) {
608
- return transaction.version === 1
609
- ? pathEntryExists(backupPath(layout))
610
- : pathEntryExists(repairBinaryBackupPath(layout)) ||
611
- pathEntryExists(repairInstallBackupPath(layout));
612
- }
613
- export function completePendingCoreUpdateAfterStart(layout, verifier = defaultVerifier) {
614
- let transaction = readCoreUpdateTransaction(layout);
615
- if (!transaction)
616
- return undefined;
617
- if (transaction.phase !== "swapped" && transaction.phase !== "health-verified") {
618
- throw new Error("Cannot complete a Core update which has not been swapped");
619
- }
620
- validatePendingTransaction(transaction, layout, verifier);
621
- if (transaction.phase === "swapped") {
622
- if (!transaction.deferredHealth) {
623
- throw new Error("Cannot complete a Core update whose in-command health check was interrupted");
624
- }
625
- transaction = markCoreUpdateHealthVerified(transaction, layout);
626
- }
627
- writeInstallRecord(transaction.targetRecord, layout);
628
- removeCommittedRollbackAssets(transaction, layout);
629
- clearCoreUpdateTransaction(layout);
630
- return transaction.targetRecord.coreVersion;
631
- }
632
- export function finalizeCoreUpdateTransaction(layout, verifier = defaultVerifier) {
633
- const transaction = readCoreUpdateTransaction(layout);
634
- if (!transaction) {
635
- recoverLegacyInterruptedCoreUpdate(layout, verifier, true);
636
- return;
637
- }
638
- if (transaction.phase !== "health-verified") {
639
- throw new Error(`Core update still requires a health check: ${transaction.targetRecord.coreVersion}`);
640
- }
641
- validatePendingTransaction(transaction, layout, verifier);
642
- writeInstallRecord(transaction.targetRecord, layout);
643
- removeCommittedRollbackAssets(transaction, layout);
644
- clearCoreUpdateTransaction(layout);
645
- }
646
- export async function commitCoreUpdate(opts) {
647
- try {
648
- return await commitCoreUpdateUnlocked(opts);
649
- }
650
- finally {
651
- fs.rmSync(opts.staged.exe, { force: true });
652
- }
653
- }
654
- async function commitCoreUpdateUnlocked(opts) {
655
- const { layout, staged, runtime } = opts;
656
- const verifier = opts.verifyExecutable ?? defaultVerifier;
657
- let transaction;
658
- let transactionStarted = false;
659
- let swapped = false;
660
- let runtimeStopped = false;
100
+ if (transaction.phase === "verified")
101
+ finishVerified(layout, transaction, verify);
102
+ else {
103
+ restoreFiles(layout, transaction, verify);
104
+ clearJournal(layout);
105
+ }
106
+ }
107
+ export async function commitCoreUpdate(options) {
108
+ const { layout, staged, runtime } = options;
109
+ const verify = options.verifyExecutable ?? defaultVerifier;
110
+ if (readCoreUpdateTransaction(layout) || pathEntryExists(`${layout.coreExe}.bak`)) {
111
+ throw new Error("An unfinished Core update requires recovery before another update");
112
+ }
113
+ const previous = readInstallRecord(layout) ?? null;
114
+ if (pathEntryExists(layout.coreExe) !== Boolean(previous) ||
115
+ pathEntryExists(layout.installFile) !== Boolean(previous)) {
116
+ throw new Error("Core installation is inconsistent; preserve its files and reinstall into a clean data directory");
117
+ }
118
+ if (previous)
119
+ verifyBinary(layout.coreExe, previous.coreVersion, verify);
120
+ verifyBinary(staged.exe, staged.version, verify);
121
+ await runtime.stop();
122
+ let transaction = {
123
+ version: 1,
124
+ phase: "prepared",
125
+ previous,
126
+ target: { coreVersion: staged.version, installedAt: new Date().toISOString() },
127
+ };
128
+ let committed = false;
661
129
  try {
662
- if (readCoreUpdateTransaction(layout))
663
- recoverCoreUpdateTransaction(layout, verifier);
130
+ writeJournal(layout, transaction);
131
+ await waitForBinaryUnlocked(layout.coreExe);
132
+ if (previous)
133
+ durableRenameSync(layout.coreExe, `${layout.coreExe}.bak`);
134
+ durableRenameSync(staged.exe, layout.coreExe);
135
+ writeInstallRecord(transaction.target, layout);
136
+ transaction = { ...transaction, phase: "swapped" };
137
+ writeJournal(layout, transaction);
138
+ await runtime.startAndVerify(staged.version);
139
+ if (runtime.wasRunning)
140
+ await runtime.applySystemProxy();
664
141
  else
665
- recoverLegacyInterruptedCoreUpdate(layout, verifier, true);
666
- if (readCoreUpdateTransaction(layout)) {
667
- throw new Error(`A Core update is pending managed startup/finalization: ${layout.coreUpdateTransactionFile}`);
668
- }
669
- let previousRecord = readInstallRecord(layout);
670
- const binaryExists = pathEntryExists(layout.coreExe);
671
- const installEntryExists = pathEntryExists(layout.installFile);
672
- const consistentEmpty = !binaryExists && !installEntryExists;
673
- const consistentInstalled = binaryExists && installEntryExists && isRegularFile(layout.coreExe) && previousRecord;
674
- const repairRequired = !consistentEmpty && !consistentInstalled;
675
- if (repairRequired && !opts.forceRepair) {
676
- throw new Error("Cannot update inconsistent Core binary/install metadata state");
677
- }
678
- const deferredHealth = runtime?.verifyRuntime !== true;
679
- if (repairRequired) {
680
- transaction = beginCoreRepairUpdateTransaction(layout, staged.version, deferredHealth);
681
- previousRecord = undefined;
682
- }
683
- else {
684
- transaction = beginCoreUpdateTransaction(layout, staged.version, previousRecord, deferredHealth);
685
- }
686
- transactionStarted = true;
687
- if (runtime?.verifyRuntime) {
688
142
  await runtime.stop();
689
- runtimeStopped = true;
690
- }
691
- if (transaction.version === 2) {
692
- transaction = quarantineCoreInstallationForUpdate(transaction, layout);
693
- }
694
- else if (previousRecord) {
695
- await waitForBinaryUnlocked(layout.coreExe);
696
- durableRenameSync(layout.coreExe, backupPath(layout));
697
- }
698
- durableRenameSync(staged.exe, layout.coreExe);
699
- swapped = true;
700
- transaction = markCoreUpdateSwapped(transaction, layout);
701
- if (deferredHealth && !previousRecord)
702
- writeInstallRecord(transaction.targetRecord, layout);
703
- if (runtime?.verifyRuntime) {
704
- await runtime.startAndVerify(staged.version);
705
- transaction = markCoreUpdateHealthVerified(transaction, layout);
706
- writeInstallRecord(transaction.targetRecord, layout);
707
- }
708
- return {
709
- version: staged.version,
710
- backupRemoved: !rollbackAssetsExist(transaction, layout),
711
- pendingStartupValidation: deferredHealth,
712
- };
713
- }
714
- catch (err) {
715
- const rollbackErrors = [];
716
- let mayRestoreFiles = true;
717
- if (runtime?.verifyRuntime && swapped) {
718
- try {
719
- await runtime.stop();
720
- runtimeStopped = true;
721
- }
722
- catch (rollbackErr) {
723
- mayRestoreFiles = false;
724
- rollbackErrors.push(rollbackErr.message);
725
- }
726
- }
727
- let restored;
728
- if (mayRestoreFiles) {
729
- try {
730
- await opts.beforeRollback?.();
731
- }
732
- catch (rollbackErr) {
733
- rollbackErrors.push(`managed state: ${rollbackErr.message}`);
734
- }
735
- if (transactionStarted) {
736
- try {
737
- restored = rollbackCoreUpdateTransaction(layout, verifier);
738
- }
739
- catch (rollbackErr) {
740
- rollbackErrors.push(`Core binary: ${rollbackErr.message}`);
741
- }
742
- }
743
- if (runtime?.verifyRuntime && runtimeStopped && restored) {
744
- try {
745
- await runtime.startAndVerify(restored.coreVersion);
746
- }
747
- catch (rollbackErr) {
748
- rollbackErrors.push(`previous runtime: ${rollbackErr.message}`);
749
- }
750
- }
751
- }
752
- const message = err.message;
753
- if (rollbackErrors.length) {
754
- throw new Error(`${message}; rollback also failed: ${rollbackErrors.join("; ")}`);
755
- }
756
- throw err;
757
- }
758
- }
759
- function recoverLegacyInterruptedCoreUpdate(layout, verifier = defaultVerifier, finalizeCommittedBackup = false) {
760
- for (const repairBackup of [repairBinaryBackupPath(layout), repairInstallBackupPath(layout)]) {
761
- if (pathEntryExists(repairBackup)) {
762
- throw new Error(`Cannot recover Core repair backup without its transaction journal: ${repairBackup}`);
763
- }
764
- }
765
- const backup = backupPath(layout);
766
- if (!pathEntryExists(backup))
767
- return;
768
- const previousRecord = readInstallRecord(layout);
769
- if (!previousRecord) {
770
- throw new Error(`Cannot recover ambiguous Core backup without install metadata: ${backup}`);
771
- }
772
- if (!pathEntryExists(layout.coreExe)) {
773
- verifyTransactionBinary(backup, previousRecord.coreVersion, verifier, "Core backup");
774
- durableRenameSync(backup, layout.coreExe);
775
- return;
776
- }
777
- let currentMatches = false;
778
- let backupMatches = false;
779
- try {
780
- verifier(layout.coreExe, previousRecord.coreVersion);
781
- currentMatches = true;
782
- }
783
- catch {
784
- }
785
- if (!currentMatches) {
143
+ transaction = { ...transaction, phase: "verified" };
144
+ writeJournal(layout, transaction);
145
+ committed = true;
146
+ finishVerified(layout, transaction, verify);
147
+ return { version: staged.version };
148
+ }
149
+ catch (error) {
150
+ if (committed || readCoreUpdateTransaction(layout)?.phase === "verified")
151
+ throw error;
786
152
  try {
787
- verifier(backup, previousRecord.coreVersion);
788
- backupMatches = true;
153
+ await runtime.stop();
154
+ const journal = readCoreUpdateTransaction(layout);
155
+ if (journal)
156
+ restoreFiles(layout, journal, verify);
157
+ if (runtime.wasRunning && previous) {
158
+ await runtime.startAndVerify(previous.coreVersion);
159
+ await runtime.applySystemProxy();
160
+ }
161
+ clearJournal(layout);
789
162
  }
790
- catch {
163
+ catch (rollback) {
164
+ throw new Error(`${error instanceof Error ? error.message : String(error)}; Core rollback failed: ${rollback instanceof Error ? rollback.message : String(rollback)}`, { cause: error });
791
165
  }
792
- }
793
- if (currentMatches) {
794
- if (finalizeCommittedBackup)
795
- durableRemoveFileSync(backup);
796
- }
797
- else if (backupMatches) {
798
- durableRemoveFileSync(layout.coreExe);
799
- durableRenameSync(backup, layout.coreExe);
800
- }
801
- else {
802
- throw new Error(`Cannot determine a safe Core after an interrupted update; preserved ${layout.coreExe} and ${backup}`);
166
+ throw error;
803
167
  }
804
168
  }