@astralyn/sash 0.1.1 → 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.
- package/CHANGELOG.md +25 -0
- package/README.md +17 -5
- package/THIRD_PARTY_NOTICES.md +9 -0
- package/dist/api.js +14 -45
- package/dist/app-state.js +99 -0
- package/dist/autostart/command.js +24 -0
- package/dist/autostart/context.js +43 -0
- package/dist/autostart/files.js +60 -0
- package/dist/autostart/installation.js +48 -0
- package/dist/autostart/start.js +47 -0
- package/dist/autostart/windows-registry.js +88 -0
- package/dist/autostart/windows.js +59 -0
- package/dist/autostart-contract.js +31 -0
- package/dist/autostart-entry.js +10 -0
- package/dist/autostart.js +69 -0
- package/dist/cli.js +11 -11
- package/dist/commands/auto.js +15 -0
- package/dist/commands/lifecycle.js +5 -15
- package/dist/commands/logs.js +11 -4
- package/dist/commands/shared.js +2 -4
- package/dist/commands/status.js +5 -1
- package/dist/commands/update.js +7 -3
- package/dist/commands/web.js +14 -50
- package/dist/contracts.js +168 -348
- package/dist/core-config-validation.js +22 -38
- package/dist/core-update.js +126 -762
- package/dist/core.js +20 -46
- package/dist/daemon/app.js +129 -117
- package/dist/daemon/context.js +13 -7
- package/dist/daemon/entry.js +21 -38
- package/dist/daemon/errors.js +3 -0
- package/dist/daemon/handlers/autostart.js +18 -0
- package/dist/daemon/handlers/core.js +32 -9
- package/dist/daemon/handlers/daemon.js +17 -6
- package/dist/daemon/handlers/profiles.js +12 -2
- package/dist/daemon/handlers/settings.js +9 -36
- package/dist/daemon/router.js +26 -21
- package/dist/daemon/scheduler.js +3 -1
- package/dist/daemon/server.js +5 -2
- package/dist/daemon-client.js +13 -3
- package/dist/daemon-http.js +6 -0
- package/dist/daemon-lifecycle.js +19 -196
- package/dist/github.js +7 -2
- package/dist/http.js +7 -5
- package/dist/mihomo-config.js +2 -1
- package/dist/paths.js +5 -7
- package/dist/profile-model.js +87 -0
- package/dist/profile-service.js +250 -587
- package/dist/profiles.js +43 -171
- package/dist/runtime-lifecycle.js +112 -194
- package/dist/runtime-owner.js +37 -76
- package/dist/sash-client.js +40 -19
- package/dist/settings-service.js +29 -224
- package/dist/settings.js +61 -289
- package/dist/status.js +24 -16
- package/dist/supervisor.js +1 -14
- package/dist/sysproxy/common.js +5 -45
- package/dist/sysproxy/factory.js +24 -65
- package/dist/sysproxy/snapshot.js +24 -257
- package/dist/sysproxy.js +1 -4
- package/dist/system-proxy-manager.js +37 -35
- package/dist/ui/assets/{ConnectionsView-CfaOG2JV.js → ConnectionsView-BgXQM6Z4.js} +1 -1
- package/dist/ui/assets/{LogsView-BDiRATXN.js → LogsView-2f542P8z.js} +1 -1
- package/dist/ui/assets/{PaginationFooter-DTytr1iu.js → PaginationFooter-cj1tcZej.js} +1 -1
- package/dist/ui/assets/ProfileEditorDialog-C9M8uKZC.css +1 -0
- package/dist/ui/assets/ProfileEditorDialog-DD4y4GBC.js +14 -0
- package/dist/ui/assets/ProfilesView-BXU2DOA7.css +1 -0
- package/dist/ui/assets/ProfilesView-DNnzYIEY.js +7 -0
- package/dist/ui/assets/{RulesView-CjEyCN1A.js → RulesView-CtKvlckZ.js} +1 -1
- package/dist/ui/assets/SettingsView-BaeKFF_N.js +1 -0
- package/dist/ui/assets/SettingsView-CWjIe05v.css +1 -0
- package/dist/ui/assets/index-CBInDvdJ.js +19 -0
- package/dist/ui/assets/index-mOLy7fkB.css +1 -0
- package/dist/ui/assets/{theme-BwDBMsKO.js → theme-D40MF8cQ.js} +1 -1
- package/dist/ui/index.html +2 -2
- package/docs/architecture-proposal.md +109 -0
- package/docs/autostart.md +101 -0
- package/docs/backend.md +92 -224
- package/docs/frontend.md +60 -106
- package/docs/usage.md +92 -164
- package/package.json +3 -3
- package/dist/commands/upgrade.js +0 -43
- package/dist/core-install-transaction.js +0 -114
- package/dist/core-update-coordination.js +0 -105
- package/dist/core-update-service.js +0 -245
- package/dist/managed-state-transaction.js +0 -375
- package/dist/offline-mutation.js +0 -58
- package/dist/profile-migration.js +0 -101
- package/dist/runtime-recovery.js +0 -31
- package/dist/sysproxy/darwin.js +0 -287
- package/dist/sysproxy/gnome.js +0 -181
- package/dist/sysproxy/legacy.js +0 -58
- package/dist/ui/assets/CodeEditorModal-6m0TJWyF.css +0 -1
- package/dist/ui/assets/CodeEditorModal-D1zn-jRS.js +0 -14
- package/dist/ui/assets/ProfileEditorDialog-CyyP6OF1.js +0 -1
- package/dist/ui/assets/ProfilesView-ByqdFNHN.css +0 -1
- package/dist/ui/assets/ProfilesView-hFa7-O72.js +0 -2
- package/dist/ui/assets/SettingsFileDialog-CjBSyH4K.js +0 -1
- package/dist/ui/assets/SettingsView-D5QvJId6.css +0 -1
- package/dist/ui/assets/SettingsView-DsRntRfn.js +0 -2
- package/dist/ui/assets/index-CxQTf_P9.css +0 -1
- package/dist/ui/assets/index-_mb4OfYp.js +0 -19
package/dist/core-update.js
CHANGED
|
@@ -1,804 +1,168 @@
|
|
|
1
1
|
import fs from "node:fs";
|
|
2
2
|
import { verifyCoreExecutable } from "./core.js";
|
|
3
|
-
import { installRecordsEqual, parseInstallRecord, readInstallRecord,
|
|
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,
|
|
5
|
+
import { hasExactOwnKeys, isPlainObject } from "./json-shape.js";
|
|
6
6
|
import { waitForBinaryUnlocked } from "./process.js";
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
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
|
|
13
|
-
|
|
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
|
|
16
|
+
let text;
|
|
133
17
|
try {
|
|
134
|
-
stat = fs.lstatSync(layout.coreUpdateTransactionFile);
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
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
|
|
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
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
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
|
|
311
|
-
|
|
312
|
-
|
|
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
|
|
315
|
-
|
|
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
|
-
|
|
47
|
+
function clearJournal(layout) {
|
|
326
48
|
durableRemoveFileSync(layout.coreUpdateTransactionFile);
|
|
327
49
|
}
|
|
328
|
-
function
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
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
|
-
|
|
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 (
|
|
356
|
-
throw new Error(
|
|
357
|
-
|
|
358
|
-
|
|
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
|
-
|
|
364
|
-
|
|
365
|
-
|
|
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
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
const
|
|
457
|
-
if (
|
|
458
|
-
if (
|
|
459
|
-
|
|
460
|
-
|
|
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
|
-
|
|
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,
|
|
93
|
+
export function recoverCoreUpdateTransaction(layout, verify = defaultVerifier) {
|
|
578
94
|
const transaction = readCoreUpdateTransaction(layout);
|
|
579
95
|
if (!transaction) {
|
|
580
|
-
|
|
581
|
-
|
|
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
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
}
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
}
|
|
632
|
-
|
|
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
|
-
|
|
663
|
-
|
|
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
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
}
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
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
|
-
|
|
788
|
-
|
|
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
|
}
|