@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.
- package/CHANGELOG.md +48 -0
- package/README.md +22 -7
- 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 -31
- package/dist/commands/logs.js +11 -4
- package/dist/commands/shared.js +2 -4
- package/dist/commands/status.js +6 -7
- package/dist/commands/update.js +7 -3
- package/dist/commands/web.js +19 -27
- package/dist/contracts.js +171 -335
- 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 +132 -113
- package/dist/daemon/context.js +13 -7
- package/dist/daemon/entry.js +21 -38
- package/dist/daemon/errors.js +6 -1
- package/dist/daemon/handlers/autostart.js +18 -0
- package/dist/daemon/handlers/core.js +32 -9
- package/dist/daemon/handlers/daemon.js +32 -6
- package/dist/daemon/handlers/profiles.js +12 -2
- package/dist/daemon/handlers/settings.js +9 -36
- package/dist/daemon/router.js +40 -27
- package/dist/daemon/scheduler.js +3 -1
- package/dist/daemon/server.js +6 -3
- package/dist/daemon/web-auth.js +52 -0
- package/dist/daemon-auth.js +2 -2
- package/dist/daemon-client.js +18 -2
- package/dist/daemon-http.js +7 -0
- package/dist/daemon-lifecycle.js +19 -196
- package/dist/github.js +7 -2
- package/dist/http.js +7 -5
- package/dist/log-follow.js +2 -1
- package/dist/mihomo-config.js +7 -9
- 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 +63 -21
- package/dist/settings-service.js +29 -228
- package/dist/settings.js +61 -285
- package/dist/status.js +24 -40
- 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-DNGmZBSU.js → ConnectionsView-BgXQM6Z4.js} +1 -1
- package/dist/ui/assets/{LogsView-fSiaxQ13.js → LogsView-2f542P8z.js} +1 -1
- package/dist/ui/assets/{PaginationFooter-B3kHzRfB.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-D9vZBiJ1.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/{0be242294f7d791af850c6df38ac78a0-2cL6Ntwf.woff2 → e2a57555d97d0b02b45d9418eb6ee295-D9nhF3rM.woff2} +0 -0
- package/dist/ui/assets/index-CBInDvdJ.js +19 -0
- package/dist/ui/assets/index-mOLy7fkB.css +1 -0
- package/dist/ui/assets/{theme-BNq4FkXS.js → theme-D40MF8cQ.js} +1 -1
- package/dist/ui/index.html +2 -2
- package/dist/web-bootstrap.js +113 -0
- package/docs/architecture-proposal.md +109 -0
- package/docs/autostart.md +101 -0
- package/docs/backend.md +92 -216
- package/docs/frontend.md +61 -97
- package/docs/usage.md +88 -186
- package/package.json +10 -6
- 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 -377
- 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/tun-guidance.js +0 -11
- package/dist/ui/assets/CodeEditorModal-6m0TJWyF.css +0 -1
- package/dist/ui/assets/CodeEditorModal-CFEnWsyh.js +0 -14
- package/dist/ui/assets/ProfileEditorDialog-BydoZthX.js +0 -1
- package/dist/ui/assets/ProfilesView-Btc1DOxE.js +0 -2
- package/dist/ui/assets/ProfilesView-ByqdFNHN.css +0 -1
- package/dist/ui/assets/SettingsFileDialog-CNFEAVs4.js +0 -1
- package/dist/ui/assets/SettingsView-BlDhZkXQ.js +0 -2
- package/dist/ui/assets/SettingsView-CngS3vBM.css +0 -1
- package/dist/ui/assets/index-B61V60w_.js +0 -19
- package/dist/ui/assets/index-bkyxJG8J.css +0 -1
|
@@ -1,377 +0,0 @@
|
|
|
1
|
-
import fs from "node:fs";
|
|
2
|
-
import { atomicWriteFileSync, durableRemoveFileSync } from "./fs-atomic.js";
|
|
3
|
-
import { hasExactOwnKeys, isCanonicalIsoTimestamp, isPlainObject } from "./json-shape.js";
|
|
4
|
-
import { profileFilePath, serializeProfiles } from "./profiles.js";
|
|
5
|
-
const MAX_JOURNAL_BYTES = 36 * 1024 * 1024;
|
|
6
|
-
const MAX_SNAPSHOT_BYTES = 24 * 1024 * 1024;
|
|
7
|
-
export const defaultManagedStateFileOperations = {
|
|
8
|
-
write: atomicWriteFileSync,
|
|
9
|
-
remove: durableRemoveFileSync,
|
|
10
|
-
};
|
|
11
|
-
function decodeSnapshot(value, role) {
|
|
12
|
-
if (!isPlainObject(value) || !hasExactOwnKeys(value, ["data"])) {
|
|
13
|
-
throw new Error(`Managed-state transaction journal has invalid ${role} snapshot`);
|
|
14
|
-
}
|
|
15
|
-
if (value.data === null)
|
|
16
|
-
return null;
|
|
17
|
-
if (typeof value.data !== "string") {
|
|
18
|
-
throw new Error(`Managed-state transaction journal has invalid ${role} snapshot data`);
|
|
19
|
-
}
|
|
20
|
-
const decoded = Buffer.from(value.data, "base64");
|
|
21
|
-
if (decoded.toString("base64") !== value.data) {
|
|
22
|
-
throw new Error(`Managed-state transaction journal has non-canonical ${role} snapshot data`);
|
|
23
|
-
}
|
|
24
|
-
return decoded;
|
|
25
|
-
}
|
|
26
|
-
function parseJournal(raw) {
|
|
27
|
-
if (!isPlainObject(raw)) {
|
|
28
|
-
throw new Error("Managed-state transaction journal has an invalid root shape");
|
|
29
|
-
}
|
|
30
|
-
const legacy = raw.version === 1;
|
|
31
|
-
const coordinated = raw.version === 3;
|
|
32
|
-
const allowed = legacy
|
|
33
|
-
? ["version", "createdAt", "index", "profile", "config"]
|
|
34
|
-
: [
|
|
35
|
-
"version",
|
|
36
|
-
"phase",
|
|
37
|
-
"createdAt",
|
|
38
|
-
"index",
|
|
39
|
-
"profile",
|
|
40
|
-
"config",
|
|
41
|
-
"settings",
|
|
42
|
-
...(coordinated ? ["coordination"] : []),
|
|
43
|
-
];
|
|
44
|
-
const required = legacy
|
|
45
|
-
? ["version", "createdAt", "index"]
|
|
46
|
-
: ["version", "phase", "createdAt", ...(coordinated ? ["coordination"] : [])];
|
|
47
|
-
if (Object.keys(raw).some((key) => !allowed.includes(key)) ||
|
|
48
|
-
!required.every((key) => key in raw)) {
|
|
49
|
-
throw new Error("Managed-state transaction journal has an invalid root shape");
|
|
50
|
-
}
|
|
51
|
-
if (raw.version !== 1 && raw.version !== 2 && raw.version !== 3) {
|
|
52
|
-
throw new Error("Managed-state transaction journal has an unsupported version");
|
|
53
|
-
}
|
|
54
|
-
let phase;
|
|
55
|
-
if (raw.version === 1) {
|
|
56
|
-
phase = "publishing";
|
|
57
|
-
}
|
|
58
|
-
else if (raw.phase === "publishing" ||
|
|
59
|
-
raw.phase === "committed" ||
|
|
60
|
-
(raw.version === 3 && raw.phase === "retained")) {
|
|
61
|
-
phase = raw.phase;
|
|
62
|
-
}
|
|
63
|
-
else {
|
|
64
|
-
throw new Error("Managed-state transaction journal has an invalid phase");
|
|
65
|
-
}
|
|
66
|
-
if (!isCanonicalIsoTimestamp(raw.createdAt)) {
|
|
67
|
-
throw new Error("Managed-state transaction journal has an invalid timestamp");
|
|
68
|
-
}
|
|
69
|
-
const coordination = raw.version === 3 && raw.coordination === "core-update" ? raw.coordination : undefined;
|
|
70
|
-
if (raw.version === 3 && !coordination) {
|
|
71
|
-
throw new Error("Managed-state transaction journal has invalid coordination");
|
|
72
|
-
}
|
|
73
|
-
const index = raw.index === undefined ? undefined : decodeSnapshot(raw.index, "index");
|
|
74
|
-
let profile;
|
|
75
|
-
if (raw.profile !== undefined) {
|
|
76
|
-
if (!isPlainObject(raw.profile) || !hasExactOwnKeys(raw.profile, ["id", "data"])) {
|
|
77
|
-
throw new Error("Managed-state transaction journal has an invalid profile snapshot");
|
|
78
|
-
}
|
|
79
|
-
if (typeof raw.profile.id !== "string" || !/^[0-9]+$/.test(raw.profile.id)) {
|
|
80
|
-
throw new Error("Managed-state transaction journal has an invalid profile id");
|
|
81
|
-
}
|
|
82
|
-
profile = { id: raw.profile.id, data: decodeSnapshot({ data: raw.profile.data }, "profile") };
|
|
83
|
-
}
|
|
84
|
-
const config = raw.config === undefined ? undefined : decodeSnapshot(raw.config, "config");
|
|
85
|
-
const settings = raw.settings === undefined ? undefined : decodeSnapshot(raw.settings, "settings");
|
|
86
|
-
const decodedSize = (index?.length ?? 0) +
|
|
87
|
-
(profile?.data?.length ?? 0) +
|
|
88
|
-
(config?.length ?? 0) +
|
|
89
|
-
(settings?.length ?? 0);
|
|
90
|
-
if (decodedSize > MAX_SNAPSHOT_BYTES) {
|
|
91
|
-
throw new Error("Managed-state transaction journal snapshots exceed the size limit");
|
|
92
|
-
}
|
|
93
|
-
return {
|
|
94
|
-
phase,
|
|
95
|
-
createdAt: raw.createdAt,
|
|
96
|
-
...(coordination ? { coordination } : {}),
|
|
97
|
-
snapshots: [
|
|
98
|
-
...(profile ? [{ path: profile.id, data: profile.data }] : []),
|
|
99
|
-
...(index !== undefined ? [{ path: "index", data: index }] : []),
|
|
100
|
-
...(config !== undefined ? [{ path: "config", data: config }] : []),
|
|
101
|
-
...(settings !== undefined ? [{ path: "settings", data: settings }] : []),
|
|
102
|
-
],
|
|
103
|
-
};
|
|
104
|
-
}
|
|
105
|
-
function readJournal(layout) {
|
|
106
|
-
let text;
|
|
107
|
-
try {
|
|
108
|
-
const stat = fs.lstatSync(layout.managedStateTransactionFile);
|
|
109
|
-
if (!stat.isFile() || stat.size > MAX_JOURNAL_BYTES) {
|
|
110
|
-
throw new Error("Managed-state transaction journal is not a bounded regular file");
|
|
111
|
-
}
|
|
112
|
-
text = fs.readFileSync(layout.managedStateTransactionFile, "utf8");
|
|
113
|
-
}
|
|
114
|
-
catch (err) {
|
|
115
|
-
if (err.code === "ENOENT")
|
|
116
|
-
return undefined;
|
|
117
|
-
throw err;
|
|
118
|
-
}
|
|
119
|
-
let raw;
|
|
120
|
-
try {
|
|
121
|
-
raw = JSON.parse(text);
|
|
122
|
-
}
|
|
123
|
-
catch (err) {
|
|
124
|
-
throw new Error(`Managed-state transaction journal is invalid JSON: ${err.message}`);
|
|
125
|
-
}
|
|
126
|
-
return parseJournal(raw);
|
|
127
|
-
}
|
|
128
|
-
export function readManagedStateTransactionStatus(layout) {
|
|
129
|
-
const stored = readJournal(layout);
|
|
130
|
-
if (!stored)
|
|
131
|
-
return undefined;
|
|
132
|
-
return {
|
|
133
|
-
phase: stored.phase,
|
|
134
|
-
createdAt: stored.createdAt,
|
|
135
|
-
...(stored.coordination ? { coordination: stored.coordination } : {}),
|
|
136
|
-
};
|
|
137
|
-
}
|
|
138
|
-
function snapshot(paths) {
|
|
139
|
-
return [...new Set(paths)].map((file) => {
|
|
140
|
-
try {
|
|
141
|
-
return { path: file, data: fs.readFileSync(file) };
|
|
142
|
-
}
|
|
143
|
-
catch (err) {
|
|
144
|
-
if (err.code === "ENOENT")
|
|
145
|
-
return { path: file, data: null };
|
|
146
|
-
throw err;
|
|
147
|
-
}
|
|
148
|
-
});
|
|
149
|
-
}
|
|
150
|
-
function journalFromSnapshots(snapshots, layout, profileId, coordination) {
|
|
151
|
-
const indexEntry = snapshots.find((entry) => entry.path === layout.profilesIndexFile);
|
|
152
|
-
const profileEntry = profileId
|
|
153
|
-
? snapshots.find((entry) => entry.path === profileFilePath(layout, profileId))
|
|
154
|
-
: undefined;
|
|
155
|
-
const configEntry = snapshots.find((entry) => entry.path === layout.configFile);
|
|
156
|
-
const settingsEntry = snapshots.find((entry) => entry.path === layout.settingsFile);
|
|
157
|
-
const size = (indexEntry?.data?.length ?? 0) +
|
|
158
|
-
(profileEntry?.data?.length ?? 0) +
|
|
159
|
-
(configEntry?.data?.length ?? 0) +
|
|
160
|
-
(settingsEntry?.data?.length ?? 0);
|
|
161
|
-
if (size > MAX_SNAPSHOT_BYTES) {
|
|
162
|
-
throw new Error("Managed-state transaction snapshots exceed the journal size limit");
|
|
163
|
-
}
|
|
164
|
-
const entries = {
|
|
165
|
-
createdAt: new Date().toISOString(),
|
|
166
|
-
...(indexEntry ? { index: { data: indexEntry.data?.toString("base64") ?? null } } : {}),
|
|
167
|
-
...(profileEntry && profileId
|
|
168
|
-
? {
|
|
169
|
-
profile: {
|
|
170
|
-
id: profileId,
|
|
171
|
-
data: profileEntry.data?.toString("base64") ?? null,
|
|
172
|
-
},
|
|
173
|
-
}
|
|
174
|
-
: {}),
|
|
175
|
-
...(configEntry ? { config: { data: configEntry.data?.toString("base64") ?? null } } : {}),
|
|
176
|
-
...(settingsEntry
|
|
177
|
-
? { settings: { data: settingsEntry.data?.toString("base64") ?? null } }
|
|
178
|
-
: {}),
|
|
179
|
-
};
|
|
180
|
-
return coordination
|
|
181
|
-
? { version: 3, phase: "publishing", coordination, ...entries }
|
|
182
|
-
: { version: 2, phase: "publishing", ...entries };
|
|
183
|
-
}
|
|
184
|
-
function restore(snapshots, files) {
|
|
185
|
-
const errors = [];
|
|
186
|
-
for (const entry of snapshots) {
|
|
187
|
-
try {
|
|
188
|
-
if (entry.data === null)
|
|
189
|
-
files.remove(entry.path);
|
|
190
|
-
else
|
|
191
|
-
files.write(entry.path, entry.data);
|
|
192
|
-
}
|
|
193
|
-
catch (err) {
|
|
194
|
-
errors.push(`${entry.path}: ${err.message}`);
|
|
195
|
-
}
|
|
196
|
-
}
|
|
197
|
-
return errors;
|
|
198
|
-
}
|
|
199
|
-
function resolvedSnapshots(stored, layout) {
|
|
200
|
-
return stored.snapshots.map((entry) => ({
|
|
201
|
-
path: entry.path === "index"
|
|
202
|
-
? layout.profilesIndexFile
|
|
203
|
-
: entry.path === "config"
|
|
204
|
-
? layout.configFile
|
|
205
|
-
: entry.path === "settings"
|
|
206
|
-
? layout.settingsFile
|
|
207
|
-
: profileFilePath(layout, entry.path),
|
|
208
|
-
data: entry.data,
|
|
209
|
-
}));
|
|
210
|
-
}
|
|
211
|
-
function transactionSnapshots(layout, transaction) {
|
|
212
|
-
const profilePath = transaction.profile
|
|
213
|
-
? profileFilePath(layout, transaction.profile.id)
|
|
214
|
-
: undefined;
|
|
215
|
-
return {
|
|
216
|
-
...(profilePath ? { profilePath } : {}),
|
|
217
|
-
snapshots: snapshot([
|
|
218
|
-
...(profilePath ? [profilePath] : []),
|
|
219
|
-
...(transaction.index ? [layout.profilesIndexFile] : []),
|
|
220
|
-
...(transaction.config ? [layout.configFile] : []),
|
|
221
|
-
...(transaction.settings ? [layout.settingsFile] : []),
|
|
222
|
-
]),
|
|
223
|
-
};
|
|
224
|
-
}
|
|
225
|
-
function publishFiles(layout, transaction, profilePath, files) {
|
|
226
|
-
if (profilePath && transaction.profile) {
|
|
227
|
-
if (transaction.profile.yamlText === null)
|
|
228
|
-
files.remove(profilePath);
|
|
229
|
-
else
|
|
230
|
-
files.write(profilePath, transaction.profile.yamlText);
|
|
231
|
-
}
|
|
232
|
-
if (transaction.index) {
|
|
233
|
-
files.write(layout.profilesIndexFile, serializeProfiles(transaction.index));
|
|
234
|
-
}
|
|
235
|
-
if (transaction.config)
|
|
236
|
-
files.write(layout.configFile, transaction.config.yaml);
|
|
237
|
-
if (transaction.settings) {
|
|
238
|
-
files.write(layout.settingsFile, `${JSON.stringify(transaction.settings, null, 2)}\n`);
|
|
239
|
-
}
|
|
240
|
-
}
|
|
241
|
-
function writeJournal(layout, journal) {
|
|
242
|
-
atomicWriteFileSync(layout.managedStateTransactionFile, `${JSON.stringify(journal)}\n`);
|
|
243
|
-
}
|
|
244
|
-
function restoreAndClear(stored, layout, files, errorPrefix) {
|
|
245
|
-
const errors = restore(resolvedSnapshots(stored, layout), files);
|
|
246
|
-
if (errors.length === 0) {
|
|
247
|
-
try {
|
|
248
|
-
durableRemoveFileSync(layout.managedStateTransactionFile);
|
|
249
|
-
}
|
|
250
|
-
catch (err) {
|
|
251
|
-
errors.push(`journal: ${err.message}`);
|
|
252
|
-
}
|
|
253
|
-
}
|
|
254
|
-
if (errors.length)
|
|
255
|
-
throw new Error(`${errorPrefix}: ${errors.join("; ")}`);
|
|
256
|
-
}
|
|
257
|
-
export function recoverManagedStateTransaction(layout, files = defaultManagedStateFileOperations) {
|
|
258
|
-
const stored = readJournal(layout);
|
|
259
|
-
if (!stored)
|
|
260
|
-
return;
|
|
261
|
-
if (stored.phase === "committed") {
|
|
262
|
-
durableRemoveFileSync(layout.managedStateTransactionFile);
|
|
263
|
-
return;
|
|
264
|
-
}
|
|
265
|
-
if (stored.phase === "retained") {
|
|
266
|
-
throw new Error("A retained managed-state transaction requires coordinated Core update recovery");
|
|
267
|
-
}
|
|
268
|
-
restoreAndClear(stored, layout, files, "Managed-state transaction recovery failed");
|
|
269
|
-
}
|
|
270
|
-
export async function retainManagedStateTransaction(layout, transaction, files = defaultManagedStateFileOperations) {
|
|
271
|
-
if (transaction.applyRuntime || transaction.reloadRuntime === true || transaction.settings) {
|
|
272
|
-
throw new Error("Retained Core update publications cannot transition runtime or publish settings");
|
|
273
|
-
}
|
|
274
|
-
recoverManagedStateTransaction(layout, files);
|
|
275
|
-
const { profilePath, snapshots } = transactionSnapshots(layout, transaction);
|
|
276
|
-
const journal = journalFromSnapshots(snapshots, layout, transaction.profile?.id, "core-update");
|
|
277
|
-
if (journal.version !== 3) {
|
|
278
|
-
throw new Error("Failed to create a coordinated managed-state transaction");
|
|
279
|
-
}
|
|
280
|
-
writeJournal(layout, journal);
|
|
281
|
-
try {
|
|
282
|
-
publishFiles(layout, transaction, profilePath, files);
|
|
283
|
-
writeJournal(layout, { ...journal, phase: "retained" });
|
|
284
|
-
}
|
|
285
|
-
catch (err) {
|
|
286
|
-
const rollbackErrors = restore(snapshots, files);
|
|
287
|
-
if (rollbackErrors.length === 0) {
|
|
288
|
-
try {
|
|
289
|
-
durableRemoveFileSync(layout.managedStateTransactionFile);
|
|
290
|
-
}
|
|
291
|
-
catch (clearErr) {
|
|
292
|
-
rollbackErrors.push(`journal: ${clearErr.message}`);
|
|
293
|
-
}
|
|
294
|
-
}
|
|
295
|
-
const suffix = rollbackErrors.length
|
|
296
|
-
? `; managed-state transaction rollback failed: ${rollbackErrors.join("; ")}`
|
|
297
|
-
: "";
|
|
298
|
-
throw new Error(`${err.message}${suffix}`);
|
|
299
|
-
}
|
|
300
|
-
}
|
|
301
|
-
export function rollbackRetainedManagedStateTransaction(layout, files = defaultManagedStateFileOperations) {
|
|
302
|
-
const stored = readJournal(layout);
|
|
303
|
-
if (!stored)
|
|
304
|
-
return false;
|
|
305
|
-
if (stored.coordination !== "core-update" || stored.phase === "committed") {
|
|
306
|
-
throw new Error("Managed-state transaction is not a retained Core update publication");
|
|
307
|
-
}
|
|
308
|
-
restoreAndClear(stored, layout, files, "Retained managed-state transaction rollback failed");
|
|
309
|
-
return true;
|
|
310
|
-
}
|
|
311
|
-
export function markRetainedManagedStateTransactionCommitted(layout) {
|
|
312
|
-
const stored = readJournal(layout);
|
|
313
|
-
if (!stored)
|
|
314
|
-
return false;
|
|
315
|
-
if (stored.coordination !== "core-update" || stored.phase !== "retained") {
|
|
316
|
-
throw new Error("Managed-state transaction is not ready for coordinated commit");
|
|
317
|
-
}
|
|
318
|
-
const raw = JSON.parse(fs.readFileSync(layout.managedStateTransactionFile, "utf8"));
|
|
319
|
-
if (raw?.version !== 3 || raw.coordination !== "core-update") {
|
|
320
|
-
throw new Error("Managed-state transaction changed during coordinated commit");
|
|
321
|
-
}
|
|
322
|
-
writeJournal(layout, { ...raw, phase: "committed" });
|
|
323
|
-
return true;
|
|
324
|
-
}
|
|
325
|
-
export function clearCommittedManagedStateTransaction(layout) {
|
|
326
|
-
const stored = readJournal(layout);
|
|
327
|
-
if (!stored)
|
|
328
|
-
return false;
|
|
329
|
-
if (stored.coordination !== "core-update" || stored.phase !== "committed") {
|
|
330
|
-
throw new Error("Managed-state transaction is not committed coordinated state");
|
|
331
|
-
}
|
|
332
|
-
durableRemoveFileSync(layout.managedStateTransactionFile);
|
|
333
|
-
return true;
|
|
334
|
-
}
|
|
335
|
-
export async function commitManagedStateTransaction(layout, transaction, reloadConfig, files = defaultManagedStateFileOperations) {
|
|
336
|
-
recoverManagedStateTransaction(layout, files);
|
|
337
|
-
const { profilePath, snapshots } = transactionSnapshots(layout, transaction);
|
|
338
|
-
const journal = journalFromSnapshots(snapshots, layout, transaction.profile?.id);
|
|
339
|
-
writeJournal(layout, journal);
|
|
340
|
-
let reloadAttempted = false;
|
|
341
|
-
try {
|
|
342
|
-
publishFiles(layout, transaction, profilePath, files);
|
|
343
|
-
if (transaction.config && transaction.reloadRuntime !== false && reloadConfig) {
|
|
344
|
-
reloadAttempted = true;
|
|
345
|
-
await reloadConfig(layout.configFile);
|
|
346
|
-
}
|
|
347
|
-
await transaction.applyRuntime?.();
|
|
348
|
-
writeJournal(layout, { ...journal, phase: "committed" });
|
|
349
|
-
durableRemoveFileSync(layout.managedStateTransactionFile);
|
|
350
|
-
}
|
|
351
|
-
catch (err) {
|
|
352
|
-
const rollbackErrors = restore(snapshots, files);
|
|
353
|
-
if (reloadAttempted && transaction.config && reloadConfig) {
|
|
354
|
-
const oldConfig = snapshots.find((entry) => entry.path === layout.configFile);
|
|
355
|
-
if (oldConfig?.data !== null) {
|
|
356
|
-
try {
|
|
357
|
-
await reloadConfig(layout.configFile);
|
|
358
|
-
}
|
|
359
|
-
catch (reloadErr) {
|
|
360
|
-
rollbackErrors.push(`config rollback reload failed: ${reloadErr.message}`);
|
|
361
|
-
}
|
|
362
|
-
}
|
|
363
|
-
}
|
|
364
|
-
if (rollbackErrors.length === 0) {
|
|
365
|
-
try {
|
|
366
|
-
durableRemoveFileSync(layout.managedStateTransactionFile);
|
|
367
|
-
}
|
|
368
|
-
catch (clearErr) {
|
|
369
|
-
rollbackErrors.push(`journal: ${clearErr.message}`);
|
|
370
|
-
}
|
|
371
|
-
}
|
|
372
|
-
const suffix = rollbackErrors.length
|
|
373
|
-
? `; managed-state transaction rollback failed: ${rollbackErrors.join("; ")}`
|
|
374
|
-
: "";
|
|
375
|
-
throw new Error(`${err.message}${suffix}`);
|
|
376
|
-
}
|
|
377
|
-
}
|
package/dist/offline-mutation.js
DELETED
|
@@ -1,58 +0,0 @@
|
|
|
1
|
-
import fs from "node:fs";
|
|
2
|
-
import { assertCoreInstallationConsistent } from "./core.js";
|
|
3
|
-
import { recoverCoreInstallTransaction } from "./core-install-transaction.js";
|
|
4
|
-
import { recoverCoordinatedCoreUpdate } from "./core-update-coordination.js";
|
|
5
|
-
import { evaluateDaemon } from "./daemon-lifecycle.js";
|
|
6
|
-
import { readManagedStateTransactionStatus, recoverManagedStateTransaction, } from "./managed-state-transaction.js";
|
|
7
|
-
import { isProcessAlive, readPidRecord } from "./process.js";
|
|
8
|
-
import { migrateProfileState } from "./profile-migration.js";
|
|
9
|
-
import { reconcileOrphanedRuntime } from "./runtime-recovery.js";
|
|
10
|
-
import { loadSettings } from "./settings.js";
|
|
11
|
-
import { withStateLock } from "./state-lock.js";
|
|
12
|
-
export async function runOfflineMutation(ctx, purpose, action, options = {}) {
|
|
13
|
-
return withStateLock(ctx.layout.mutationLockFile, { purpose, timeoutMs: 30_000 }, async () => {
|
|
14
|
-
ctx.settings = loadSettings(ctx.layout);
|
|
15
|
-
const daemon = await evaluateDaemon(ctx.layout, ctx.settings);
|
|
16
|
-
if (daemon.kind !== "stopped") {
|
|
17
|
-
const owner = daemon.pid ? ` (PID=${daemon.pid})` : "";
|
|
18
|
-
throw new Error(`sashd owns the data directory${owner} but is not accepting this operation; stop or recover it before retrying`);
|
|
19
|
-
}
|
|
20
|
-
const reconciliation = options.reconcileRuntime;
|
|
21
|
-
if (!reconciliation) {
|
|
22
|
-
const core = readPidRecord(ctx.layout.pidFile);
|
|
23
|
-
if (!core && fs.existsSync(ctx.layout.pidFile)) {
|
|
24
|
-
throw new Error(`Core PID record is corrupt: ${ctx.layout.pidFile}; reconcile it before modifying state`);
|
|
25
|
-
}
|
|
26
|
-
if (core && isProcessAlive(core.pid)) {
|
|
27
|
-
throw new Error(`Core PID ${core.pid} is still alive without sashd; run \`sash stop\` to reconcile it before modifying state`);
|
|
28
|
-
}
|
|
29
|
-
}
|
|
30
|
-
recoverCoreInstallTransaction(ctx.layout);
|
|
31
|
-
if (reconciliation) {
|
|
32
|
-
const managed = readManagedStateTransactionStatus(ctx.layout);
|
|
33
|
-
if (managed?.coordination !== "core-update") {
|
|
34
|
-
recoverManagedStateTransaction(ctx.layout);
|
|
35
|
-
}
|
|
36
|
-
ctx.settings = loadSettings(ctx.layout);
|
|
37
|
-
await reconcileOrphanedRuntime({
|
|
38
|
-
layout: ctx.layout,
|
|
39
|
-
settings: ctx.settings,
|
|
40
|
-
...(reconciliation.legacyDaemon ? { legacyDaemon: reconciliation.legacyDaemon } : {}),
|
|
41
|
-
...(reconciliation.verifyControllerVacant
|
|
42
|
-
? { verifyControllerVacant: reconciliation.verifyControllerVacant }
|
|
43
|
-
: {}),
|
|
44
|
-
}, reconciliation.deps);
|
|
45
|
-
}
|
|
46
|
-
else {
|
|
47
|
-
recoverCoordinatedCoreUpdate(ctx.layout);
|
|
48
|
-
if (options.allowInconsistentCore !== "force-update") {
|
|
49
|
-
assertCoreInstallationConsistent(ctx.layout);
|
|
50
|
-
}
|
|
51
|
-
}
|
|
52
|
-
ctx.settings = loadSettings(ctx.layout);
|
|
53
|
-
if (options.migrateProfiles) {
|
|
54
|
-
await migrateProfileState(ctx.settings, ctx.layout);
|
|
55
|
-
}
|
|
56
|
-
return action();
|
|
57
|
-
});
|
|
58
|
-
}
|
|
@@ -1,101 +0,0 @@
|
|
|
1
|
-
import fs from "node:fs";
|
|
2
|
-
import { isDeepStrictEqual } from "node:util";
|
|
3
|
-
import YAML from "yaml";
|
|
4
|
-
import { commitManagedStateTransaction, defaultManagedStateFileOperations, } from "./managed-state-transaction.js";
|
|
5
|
-
import { buildDefaultConfig, isValidMihomoConfig, PROFILE_DOWNLOAD_SIZE_LIMIT, stripManagedKeys, } from "./mihomo-config.js";
|
|
6
|
-
import { allocateProfileId, DEFAULT_PROFILE_INTERVAL_HOURS, findProfileByUrl, loadProfiles, NEVER_UPDATED, profileNameFromUrl, } from "./profiles.js";
|
|
7
|
-
export const IMPORTED_CONFIG_PROFILE_NAME = "Imported config";
|
|
8
|
-
export async function migrateLegacyProfileSetting(settings, layout, files = defaultManagedStateFileOperations) {
|
|
9
|
-
if (!("subscriptionUrl" in settings))
|
|
10
|
-
return false;
|
|
11
|
-
const candidate = { ...settings };
|
|
12
|
-
const url = candidate.subscriptionUrl?.trim() ?? "";
|
|
13
|
-
delete candidate.subscriptionUrl;
|
|
14
|
-
let migratedIndex;
|
|
15
|
-
if (url) {
|
|
16
|
-
const index = loadProfiles(layout);
|
|
17
|
-
if (!findProfileByUrl(index, url)) {
|
|
18
|
-
const now = new Date().toISOString();
|
|
19
|
-
const profile = {
|
|
20
|
-
id: allocateProfileId(index, layout),
|
|
21
|
-
name: profileNameFromUrl(url),
|
|
22
|
-
url,
|
|
23
|
-
intervalHours: DEFAULT_PROFILE_INTERVAL_HOURS,
|
|
24
|
-
createdAt: now,
|
|
25
|
-
updatedAt: NEVER_UPDATED,
|
|
26
|
-
};
|
|
27
|
-
migratedIndex = {
|
|
28
|
-
activeId: index.activeId ?? profile.id,
|
|
29
|
-
profiles: [...index.profiles, profile],
|
|
30
|
-
};
|
|
31
|
-
}
|
|
32
|
-
}
|
|
33
|
-
await commitManagedStateTransaction(layout, { ...(migratedIndex ? { index: migratedIndex } : {}), settings: candidate }, undefined, files);
|
|
34
|
-
delete settings.subscriptionUrl;
|
|
35
|
-
return true;
|
|
36
|
-
}
|
|
37
|
-
function hasMeaningfulRoutingContent(doc) {
|
|
38
|
-
const unmanaged = stripManagedKeys(doc);
|
|
39
|
-
const defaultConfig = buildDefaultConfig();
|
|
40
|
-
if (isDeepStrictEqual(unmanaged, defaultConfig))
|
|
41
|
-
return false;
|
|
42
|
-
if (Array.isArray(unmanaged.proxies) && unmanaged.proxies.length > 0)
|
|
43
|
-
return true;
|
|
44
|
-
const providers = unmanaged["proxy-providers"];
|
|
45
|
-
if (typeof providers === "object" &&
|
|
46
|
-
providers !== null &&
|
|
47
|
-
!Array.isArray(providers) &&
|
|
48
|
-
Object.keys(providers).length > 0) {
|
|
49
|
-
return true;
|
|
50
|
-
}
|
|
51
|
-
const groups = unmanaged["proxy-groups"];
|
|
52
|
-
if (Array.isArray(groups) &&
|
|
53
|
-
groups.length > 0 &&
|
|
54
|
-
!isDeepStrictEqual(groups, defaultConfig["proxy-groups"])) {
|
|
55
|
-
return true;
|
|
56
|
-
}
|
|
57
|
-
return (Array.isArray(unmanaged.rules) &&
|
|
58
|
-
unmanaged.rules.length > 0 &&
|
|
59
|
-
!isDeepStrictEqual(unmanaged.rules, defaultConfig.rules));
|
|
60
|
-
}
|
|
61
|
-
export async function migrateUnmanagedConfig(layout, files = defaultManagedStateFileOperations) {
|
|
62
|
-
if (fs.existsSync(layout.profilesIndexFile) || !fs.existsSync(layout.configFile))
|
|
63
|
-
return false;
|
|
64
|
-
const stat = fs.lstatSync(layout.configFile);
|
|
65
|
-
if (!stat.isFile() || stat.size > PROFILE_DOWNLOAD_SIZE_LIMIT) {
|
|
66
|
-
throw new Error(`Unmanaged config must be a regular file no larger than ${PROFILE_DOWNLOAD_SIZE_LIMIT} bytes: ${layout.configFile}`);
|
|
67
|
-
}
|
|
68
|
-
const yamlText = fs.readFileSync(layout.configFile, "utf8");
|
|
69
|
-
let doc;
|
|
70
|
-
try {
|
|
71
|
-
doc = YAML.parse(yamlText);
|
|
72
|
-
}
|
|
73
|
-
catch (err) {
|
|
74
|
-
throw new Error(`Unmanaged config is invalid YAML and was left unchanged: ${layout.configFile}: ${err.message}`);
|
|
75
|
-
}
|
|
76
|
-
if (!isValidMihomoConfig(doc)) {
|
|
77
|
-
throw new Error(`Unmanaged config is not a valid core configuration and was left unchanged: ${layout.configFile}`);
|
|
78
|
-
}
|
|
79
|
-
if (!hasMeaningfulRoutingContent(doc))
|
|
80
|
-
return false;
|
|
81
|
-
const now = new Date().toISOString();
|
|
82
|
-
const id = allocateProfileId({ activeId: null, profiles: [] }, layout);
|
|
83
|
-
const profile = {
|
|
84
|
-
id,
|
|
85
|
-
name: IMPORTED_CONFIG_PROFILE_NAME,
|
|
86
|
-
url: "",
|
|
87
|
-
intervalHours: 0,
|
|
88
|
-
createdAt: now,
|
|
89
|
-
updatedAt: now,
|
|
90
|
-
};
|
|
91
|
-
await commitManagedStateTransaction(layout, {
|
|
92
|
-
index: { activeId: id, profiles: [profile] },
|
|
93
|
-
profile: { id, yamlText },
|
|
94
|
-
}, undefined, files);
|
|
95
|
-
return true;
|
|
96
|
-
}
|
|
97
|
-
export async function migrateProfileState(settings, layout, files = defaultManagedStateFileOperations) {
|
|
98
|
-
const legacySubscription = await migrateLegacyProfileSetting(settings, layout, files);
|
|
99
|
-
const unmanagedConfig = await migrateUnmanagedConfig(layout, files);
|
|
100
|
-
return { legacySubscription, unmanagedConfig };
|
|
101
|
-
}
|
package/dist/runtime-recovery.js
DELETED
|
@@ -1,31 +0,0 @@
|
|
|
1
|
-
import { MihomoApi } from "./api.js";
|
|
2
|
-
import { recoverCoordinatedCoreUpdate } from "./core-update-coordination.js";
|
|
3
|
-
import { CoreSupervisor } from "./supervisor.js";
|
|
4
|
-
import { disableLegacySystemProxyIfOwned } from "./sysproxy.js";
|
|
5
|
-
import { SystemProxyManager } from "./system-proxy-manager.js";
|
|
6
|
-
export async function assertCoreControllerVacant(settings, probe = (current) => new MihomoApi(current.controller, current.secret).isReachable()) {
|
|
7
|
-
if (await probe(settings)) {
|
|
8
|
-
throw new Error("A Core controller is active without a live Sash PID owner; refusing to replace the executable");
|
|
9
|
-
}
|
|
10
|
-
}
|
|
11
|
-
export async function reconcileOrphanedRuntime(options, deps = {}) {
|
|
12
|
-
const { layout, settings } = options;
|
|
13
|
-
if (options.legacyDaemon && settings.systemProxy) {
|
|
14
|
-
await (deps.disableLegacyProxy ?? disableLegacySystemProxyIfOwned)({
|
|
15
|
-
port: settings.mixedPort,
|
|
16
|
-
});
|
|
17
|
-
}
|
|
18
|
-
const systemProxy = deps.systemProxy ?? new SystemProxyManager({ layout });
|
|
19
|
-
await systemProxy.release();
|
|
20
|
-
const supervisor = deps.supervisor ??
|
|
21
|
-
new CoreSupervisor({
|
|
22
|
-
layout,
|
|
23
|
-
settings: () => settings,
|
|
24
|
-
});
|
|
25
|
-
await supervisor.cleanStaleCore();
|
|
26
|
-
const pending = (deps.recoverCoreUpdate ?? recoverCoordinatedCoreUpdate)(layout);
|
|
27
|
-
if (options.verifyControllerVacant) {
|
|
28
|
-
await assertCoreControllerVacant(settings, deps.controllerReachable);
|
|
29
|
-
}
|
|
30
|
-
return pending;
|
|
31
|
-
}
|