@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,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
- }
@@ -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
- }
@@ -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
- }