@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.
Files changed (102) hide show
  1. package/CHANGELOG.md +25 -0
  2. package/README.md +17 -5
  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 -15
  19. package/dist/commands/logs.js +11 -4
  20. package/dist/commands/shared.js +2 -4
  21. package/dist/commands/status.js +5 -1
  22. package/dist/commands/update.js +7 -3
  23. package/dist/commands/web.js +14 -50
  24. package/dist/contracts.js +168 -348
  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 +129 -117
  29. package/dist/daemon/context.js +13 -7
  30. package/dist/daemon/entry.js +21 -38
  31. package/dist/daemon/errors.js +3 -0
  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 +17 -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 +26 -21
  38. package/dist/daemon/scheduler.js +3 -1
  39. package/dist/daemon/server.js +5 -2
  40. package/dist/daemon-client.js +13 -3
  41. package/dist/daemon-http.js +6 -0
  42. package/dist/daemon-lifecycle.js +19 -196
  43. package/dist/github.js +7 -2
  44. package/dist/http.js +7 -5
  45. package/dist/mihomo-config.js +2 -1
  46. package/dist/paths.js +5 -7
  47. package/dist/profile-model.js +87 -0
  48. package/dist/profile-service.js +250 -587
  49. package/dist/profiles.js +43 -171
  50. package/dist/runtime-lifecycle.js +112 -194
  51. package/dist/runtime-owner.js +37 -76
  52. package/dist/sash-client.js +40 -19
  53. package/dist/settings-service.js +29 -224
  54. package/dist/settings.js +61 -289
  55. package/dist/status.js +24 -16
  56. package/dist/supervisor.js +1 -14
  57. package/dist/sysproxy/common.js +5 -45
  58. package/dist/sysproxy/factory.js +24 -65
  59. package/dist/sysproxy/snapshot.js +24 -257
  60. package/dist/sysproxy.js +1 -4
  61. package/dist/system-proxy-manager.js +37 -35
  62. package/dist/ui/assets/{ConnectionsView-CfaOG2JV.js → ConnectionsView-BgXQM6Z4.js} +1 -1
  63. package/dist/ui/assets/{LogsView-BDiRATXN.js → LogsView-2f542P8z.js} +1 -1
  64. package/dist/ui/assets/{PaginationFooter-DTytr1iu.js → PaginationFooter-cj1tcZej.js} +1 -1
  65. package/dist/ui/assets/ProfileEditorDialog-C9M8uKZC.css +1 -0
  66. package/dist/ui/assets/ProfileEditorDialog-DD4y4GBC.js +14 -0
  67. package/dist/ui/assets/ProfilesView-BXU2DOA7.css +1 -0
  68. package/dist/ui/assets/ProfilesView-DNnzYIEY.js +7 -0
  69. package/dist/ui/assets/{RulesView-CjEyCN1A.js → RulesView-CtKvlckZ.js} +1 -1
  70. package/dist/ui/assets/SettingsView-BaeKFF_N.js +1 -0
  71. package/dist/ui/assets/SettingsView-CWjIe05v.css +1 -0
  72. package/dist/ui/assets/index-CBInDvdJ.js +19 -0
  73. package/dist/ui/assets/index-mOLy7fkB.css +1 -0
  74. package/dist/ui/assets/{theme-BwDBMsKO.js → theme-D40MF8cQ.js} +1 -1
  75. package/dist/ui/index.html +2 -2
  76. package/docs/architecture-proposal.md +109 -0
  77. package/docs/autostart.md +101 -0
  78. package/docs/backend.md +92 -224
  79. package/docs/frontend.md +60 -106
  80. package/docs/usage.md +92 -164
  81. package/package.json +3 -3
  82. package/dist/commands/upgrade.js +0 -43
  83. package/dist/core-install-transaction.js +0 -114
  84. package/dist/core-update-coordination.js +0 -105
  85. package/dist/core-update-service.js +0 -245
  86. package/dist/managed-state-transaction.js +0 -375
  87. package/dist/offline-mutation.js +0 -58
  88. package/dist/profile-migration.js +0 -101
  89. package/dist/runtime-recovery.js +0 -31
  90. package/dist/sysproxy/darwin.js +0 -287
  91. package/dist/sysproxy/gnome.js +0 -181
  92. package/dist/sysproxy/legacy.js +0 -58
  93. package/dist/ui/assets/CodeEditorModal-6m0TJWyF.css +0 -1
  94. package/dist/ui/assets/CodeEditorModal-D1zn-jRS.js +0 -14
  95. package/dist/ui/assets/ProfileEditorDialog-CyyP6OF1.js +0 -1
  96. package/dist/ui/assets/ProfilesView-ByqdFNHN.css +0 -1
  97. package/dist/ui/assets/ProfilesView-hFa7-O72.js +0 -2
  98. package/dist/ui/assets/SettingsFileDialog-CjBSyH4K.js +0 -1
  99. package/dist/ui/assets/SettingsView-D5QvJId6.css +0 -1
  100. package/dist/ui/assets/SettingsView-DsRntRfn.js +0 -2
  101. package/dist/ui/assets/index-CxQTf_P9.css +0 -1
  102. package/dist/ui/assets/index-_mb4OfYp.js +0 -19
package/dist/profiles.js CHANGED
@@ -1,182 +1,54 @@
1
- import crypto from "node:crypto";
2
1
  import fs from "node:fs";
3
2
  import path from "node:path";
4
3
  import YAML from "yaml";
5
- import { atomicWriteFileSync } from "./fs-atomic.js";
6
- import { isPlainObject } from "./json-shape.js";
7
- import { isValidMihomoConfig, PROFILE_DOWNLOAD_SIZE_LIMIT, } from "./mihomo-config.js";
8
- import { sashLayout } from "./paths.js";
9
- export const DEFAULT_PROFILE_INTERVAL_HOURS = 24;
10
- export const MAX_PROFILE_INTERVAL_HOURS = 24 * 365;
11
- export const PROFILE_INDEX_SIZE_LIMIT = 2 * 1024 * 1024;
12
- export const NEVER_UPDATED = new Date(0).toISOString();
13
- export function profileFilePath(layout, id) {
14
- if (!/^[0-9]+$/.test(id))
15
- throw new Error(`invalid profile id: ${id}`);
16
- return path.join(layout.profilesDir, `${id}.yaml`);
17
- }
18
- function isSubscriptionUserinfo(value) {
19
- if (typeof value !== "object" || value === null || Array.isArray(value))
20
- return false;
21
- const info = value;
22
- return (Number.isFinite(info.upload) &&
23
- Number.isFinite(info.download) &&
24
- Number.isFinite(info.total) &&
25
- (info.expire === undefined || Number.isFinite(info.expire)));
26
- }
27
- function isProfileMeta(value) {
28
- if (typeof value !== "object" || value === null || Array.isArray(value))
29
- return false;
30
- const p = value;
31
- return (typeof p.id === "string" &&
32
- /^[0-9]+$/.test(p.id) &&
33
- typeof p.name === "string" &&
34
- typeof p.url === "string" &&
35
- typeof p.intervalHours === "number" &&
36
- Number.isSafeInteger(p.intervalHours) &&
37
- p.intervalHours >= 0 &&
38
- p.intervalHours <= MAX_PROFILE_INTERVAL_HOURS &&
39
- typeof p.createdAt === "string" &&
40
- Number.isFinite(new Date(p.createdAt).getTime()) &&
41
- typeof p.updatedAt === "string" &&
42
- Number.isFinite(new Date(p.updatedAt).getTime()) &&
43
- (p.subInfo === undefined || isSubscriptionUserinfo(p.subInfo)) &&
44
- (p.homePage === undefined || typeof p.homePage === "string") &&
45
- (p.lastError === undefined || typeof p.lastError === "string"));
46
- }
47
- export function loadProfiles(layout = sashLayout()) {
48
- let stat;
49
- try {
50
- stat = fs.lstatSync(layout.profilesIndexFile);
51
- }
52
- catch (err) {
53
- if (err.code === "ENOENT") {
54
- return { activeId: null, profiles: [] };
55
- }
56
- throw err;
57
- }
58
- if (!stat.isFile() || stat.size > PROFILE_INDEX_SIZE_LIMIT) {
59
- throw new Error(`Profiles index must be a regular file no larger than ${PROFILE_INDEX_SIZE_LIMIT} bytes: ${layout.profilesIndexFile}`);
60
- }
61
- const content = fs.readFileSync(layout.profilesIndexFile);
62
- if (content.length > PROFILE_INDEX_SIZE_LIMIT) {
63
- throw new Error(`Profiles index must be a regular file no larger than ${PROFILE_INDEX_SIZE_LIMIT} bytes: ${layout.profilesIndexFile}`);
64
- }
65
- const text = content.toString("utf8");
66
- let raw;
67
- try {
68
- raw = JSON.parse(text);
69
- }
70
- catch (err) {
71
- throw new Error(`Profiles index is invalid JSON: ${layout.profilesIndexFile}: ${err.message}`);
72
- }
73
- if (!isPlainObject(raw)) {
74
- throw new Error(`Profiles index root must be a plain object: ${layout.profilesIndexFile}`);
75
- }
76
- if (Object.keys(raw).some((key) => key !== "activeId" && key !== "profiles")) {
77
- throw new Error(`Profiles index has unexpected root fields: ${layout.profilesIndexFile}`);
78
- }
79
- if (!Array.isArray(raw.profiles) || !raw.profiles.every(isProfileMeta)) {
80
- throw new Error(`Profiles index has invalid profile metadata: ${layout.profilesIndexFile}`);
81
- }
82
- if (raw.activeId !== null && raw.activeId !== undefined && typeof raw.activeId !== "string") {
83
- throw new Error(`Profiles index has an invalid activeId: ${layout.profilesIndexFile}`);
84
- }
85
- const profiles = raw.profiles;
86
- if (new Set(profiles.map((profile) => profile.id)).size !== profiles.length) {
87
- throw new Error(`Profiles index has duplicate profile ids: ${layout.profilesIndexFile}`);
88
- }
89
- const activeId = typeof raw.activeId === "string" && profiles.some((p) => p.id === raw.activeId)
90
- ? raw.activeId
91
- : null;
92
- return { activeId, profiles };
93
- }
94
- export function serializeProfiles(index) {
95
- const activeId = index.profiles.some((p) => p.id === index.activeId) ? index.activeId : null;
96
- return `${JSON.stringify({ ...index, activeId }, null, 2)}\n`;
97
- }
98
- export function saveProfiles(index, layout = sashLayout()) {
99
- atomicWriteFileSync(layout.profilesIndexFile, serializeProfiles(index));
4
+ import { readState } from "./app-state.js";
5
+ import { buildDefaultConfig, isValidMihomoConfig, PROFILE_DOWNLOAD_SIZE_LIMIT, renderConfig, } from "./mihomo-config.js";
6
+ export { DEFAULT_PROFILE_INTERVAL_HOURS, MAX_PROFILE_INTERVAL_HOURS } from "./profile-model.js";
7
+ export function profileFilePath(layout, id, revision) {
8
+ if (!/^[0-9]+$/.test(id) || !Number.isSafeInteger(revision) || revision < 1) {
9
+ throw new Error("Invalid profile id or revision");
10
+ }
11
+ return path.join(layout.profilesDir, id, `${revision}.yaml`);
12
+ }
13
+ export function parseProfileText(text) {
14
+ if (!text.trim() || Buffer.byteLength(text) > PROFILE_DOWNLOAD_SIZE_LIMIT) {
15
+ throw new Error("Profile content must be non-empty and no larger than 8 MiB");
16
+ }
17
+ const doc = YAML.parse(text, { maxAliasCount: 50 });
18
+ if (!isValidMihomoConfig(doc))
19
+ throw new Error("Content is not a valid core configuration (missing proxies/rules)");
20
+ return doc;
21
+ }
22
+ export function readProfileSource(layout, profile) {
23
+ const file = profileFilePath(layout, profile.id, profile.revision);
24
+ const stat = fs.lstatSync(file);
25
+ if (!stat.isFile() || stat.size > PROFILE_DOWNLOAD_SIZE_LIMIT)
26
+ throw new Error("Profile must be a bounded regular file");
27
+ const yamlText = fs.readFileSync(file, "utf8");
28
+ return { yamlText, doc: parseProfileText(yamlText) };
29
+ }
30
+ export function loadProfiles(layout) {
31
+ return readState(layout)?.profiles ?? { activeId: null, profiles: [] };
100
32
  }
101
33
  export function getActiveProfile(index) {
102
- return index.profiles.find((p) => p.id === index.activeId) ?? null;
34
+ return index.profiles.find((profile) => profile.id === index.activeId) ?? null;
103
35
  }
104
- export function findProfileByUrl(index, url) {
105
- return index.profiles.find((p) => p.url !== "" && p.url === url) ?? null;
36
+ export function renderActiveConfig(state, layout) {
37
+ const active = getActiveProfile(state.profiles);
38
+ return renderConfig(active ? readProfileSource(layout, active).doc : buildDefaultConfig(), state.settings, active ? "subscription" : "default");
106
39
  }
107
- function readProfileBytes(layout, id) {
108
- const file = profileFilePath(layout, id);
109
- let stat;
110
- try {
111
- stat = fs.lstatSync(file);
112
- }
113
- catch (err) {
114
- if (err.code === "ENOENT")
115
- return undefined;
116
- throw err;
117
- }
118
- if (!stat.isFile() || stat.size > PROFILE_DOWNLOAD_SIZE_LIMIT) {
119
- throw new Error(`Profile ${id} must be a regular file no larger than ${PROFILE_DOWNLOAD_SIZE_LIMIT} bytes: ${file}`);
120
- }
121
- const content = fs.readFileSync(file);
122
- if (content.length > PROFILE_DOWNLOAD_SIZE_LIMIT) {
123
- throw new Error(`Profile ${id} must be a regular file no larger than ${PROFILE_DOWNLOAD_SIZE_LIMIT} bytes: ${file}`);
124
- }
125
- return content;
126
- }
127
- export function profileContentDigest(content) {
128
- return crypto.createHash("sha256").update(content).digest("hex");
129
- }
130
- export function readProfileSource(layout, id) {
131
- const content = readProfileBytes(layout, id);
132
- if (!content)
133
- return undefined;
134
- const yamlText = content.toString("utf8");
135
- let doc;
136
- try {
137
- doc = YAML.parse(yamlText);
138
- }
139
- catch (err) {
140
- throw new Error(`Profile ${id} contains invalid YAML: ${err.message}`);
141
- }
142
- if (!isValidMihomoConfig(doc)) {
143
- throw new Error(`Profile ${id} is not a valid core configuration`);
144
- }
145
- return { doc, yamlText, digest: profileContentDigest(content) };
146
- }
147
- export function readProfileDigest(layout, id) {
148
- const content = readProfileBytes(layout, id);
149
- return content ? profileContentDigest(content) : null;
150
- }
151
- export function readProfileDoc(layout, id) {
152
- return readProfileSource(layout, id)?.doc;
153
- }
154
- export function allocateProfileId(index, layout, nowMs = Date.now()) {
155
- let candidate = BigInt(nowMs);
156
- for (;;) {
157
- const id = candidate.toString();
158
- if (!index.profiles.some((profile) => profile.id === id) &&
159
- !fs.existsSync(profileFilePath(layout, id))) {
160
- return id;
161
- }
162
- candidate += 1n;
163
- }
40
+ export function allocateProfileId(index, layout) {
41
+ let id = BigInt(Date.now());
42
+ while (index.profiles.some((profile) => profile.id === String(id)) ||
43
+ fs.existsSync(path.join(layout.profilesDir, String(id))))
44
+ id += 1n;
45
+ return String(id);
164
46
  }
165
47
  export function profileNameFromUrl(url) {
166
- try {
167
- return new URL(url).hostname || "profile";
168
- }
169
- catch {
170
- return "profile";
171
- }
48
+ return new URL(url).hostname || "profile";
172
49
  }
173
- export function profileDueForUpdate(profile, fileExists, nowMs = Date.now()) {
174
- if (!profile.url || profile.intervalHours <= 0)
175
- return false;
176
- if (!fileExists)
177
- return true;
178
- const updatedMs = new Date(profile.updatedAt).getTime();
179
- if (!Number.isFinite(updatedMs))
180
- return true;
181
- return nowMs - updatedMs >= profile.intervalHours * 3_600_000;
50
+ export function profileDueForUpdate(profile, nowMs = Date.now()) {
51
+ return Boolean(profile.url &&
52
+ profile.intervalHours > 0 &&
53
+ nowMs - Date.parse(profile.updatedAt) >= profile.intervalHours * 3_600_000);
182
54
  }
@@ -1,205 +1,123 @@
1
- function sleep(ms) {
2
- return new Promise((resolve) => setTimeout(resolve, ms));
3
- }
1
+ import { MihomoApi } from "./api.js";
2
+ import { commitCoreUpdate, readCoreUpdateTransaction, recoverCoreUpdateTransaction, } from "./core-update.js";
3
+ import { atomicWriteFileSync } from "./fs-atomic.js";
4
+ import { recoverBinaryUnlockProbe } from "./process.js";
4
5
  export class RuntimeLifecycle {
5
- supervisor;
6
- systemProxy;
7
- getSettings;
8
- coreUpdate;
9
- operationQueue = Promise.resolve();
10
- phase;
11
- generation = 0;
6
+ options;
7
+ runtimeRevision = 0;
8
+ applied;
9
+ runtimeSettings;
12
10
  constructor(options) {
13
- this.supervisor = options.supervisor;
14
- this.systemProxy = options.systemProxy;
15
- this.getSettings = options.settings;
16
- this.coreUpdate = options.coreUpdate;
17
- this.phase = this.supervisor.isRunning() ? "running" : "stopped";
18
- }
19
- state() {
20
- return { phase: this.phase, generation: this.generation };
21
- }
22
- start(prepare) {
23
- return this.enqueue(() => this.startUnlocked(prepare));
24
- }
25
- stop() {
26
- return this.enqueue(() => this.stopUnlocked());
27
- }
28
- restart(prepare) {
29
- return this.enqueue(() => this.restartUnlocked(prepare));
30
- }
31
- reconcileSystemProxy() {
32
- return this.enqueue(() => this.reconcileSystemProxyUnlocked());
33
- }
34
- recoverStartup() {
35
- return this.enqueue(async () => {
36
- await this.systemProxy.release();
37
- this.phase = this.supervisor.isRunning() ? "running" : "stopped";
38
- });
39
- }
40
- handleUnexpectedCoreExit() {
41
- return this.enqueue(async () => {
42
- if (this.supervisor.isRunning())
43
- return;
44
- this.generation++;
45
- this.phase = "failed";
46
- let lastError;
47
- for (let attempt = 0; attempt < 3; attempt++) {
48
- try {
49
- await this.systemProxy.release();
50
- this.phase = "stopped";
51
- return;
52
- }
53
- catch (err) {
54
- lastError = err;
55
- if (attempt < 2)
56
- await sleep(250 * (attempt + 1));
57
- }
58
- }
59
- throw lastError;
60
- });
61
- }
62
- close() {
63
- return this.enqueue(() => this.stopUnlocked());
64
- }
65
- enqueue(operation) {
66
- const next = this.operationQueue.then(operation, operation);
67
- this.operationQueue = next.then(() => undefined, () => undefined);
68
- return next;
69
- }
70
- async startUnlocked(prepare) {
71
- if (this.supervisor.isRunning()) {
72
- const status = await this.supervisor.status();
73
- if (!status.pid)
74
- throw new Error("Core reports running without a process identifier");
75
- if (!status.healthy) {
76
- throw new Error(`Core process is running but unhealthy (PID=${status.pid})`);
77
- }
78
- this.coreUpdate?.completeAfterStart();
79
- await this.reconcileSystemProxyUnlocked();
80
- return {
81
- pid: status.pid,
82
- ...(status.version ? { version: status.version } : {}),
83
- ...(status.tunActive !== undefined ? { tunActive: status.tunActive } : {}),
84
- };
85
- }
86
- this.generation++;
87
- this.phase = "starting";
88
- let startAttempted = false;
89
- try {
90
- await this.systemProxy.release();
91
- await prepare?.();
92
- startAttempted = true;
93
- const result = await this.supervisor.start();
94
- this.coreUpdate?.completeAfterStart();
95
- this.phase = "running";
96
- await this.applyDesiredProxyUnlocked();
97
- return result;
98
- }
99
- catch (err) {
100
- if (startAttempted && !this.supervisor.isRunning() && this.coreUpdate) {
101
- let pending;
102
- try {
103
- pending = this.coreUpdate.pending();
104
- }
105
- catch (inspectError) {
106
- this.phase = "failed";
107
- throw new Error(`${err.message}; pending Core update inspection failed: ${inspectError.message}`);
108
- }
109
- if (pending) {
110
- let previous;
111
- try {
112
- previous = this.coreUpdate.rollbackAfterStartFailure();
113
- if (previous) {
114
- await this.supervisor.start();
115
- this.phase = "running";
116
- await this.applyDesiredProxyUnlocked();
117
- }
118
- else {
119
- this.phase = "stopped";
120
- }
121
- }
122
- catch (rollbackError) {
123
- this.phase = this.supervisor.isRunning() ? "running" : "failed";
124
- throw new Error(`${err.message}; Core update rollback failed: ${rollbackError.message}`);
125
- }
126
- const destination = previous ? ` to ${previous.coreVersion}` : "";
127
- throw new Error(`${err.message}; Core update rolled back${destination}`);
128
- }
129
- }
130
- this.phase = this.supervisor.isRunning() ? "running" : "failed";
131
- throw err;
132
- }
133
- }
134
- async stopUnlocked() {
135
- this.generation++;
136
- this.phase = "stopping";
137
- try {
138
- await this.systemProxy.release();
139
- await this.supervisor.stop();
140
- this.phase = "stopped";
141
- }
142
- catch (err) {
143
- this.phase = this.supervisor.isRunning() ? "running" : "failed";
144
- throw err;
145
- }
146
- }
147
- async restartUnlocked(prepare) {
148
- this.generation++;
149
- this.phase = "restarting";
150
- try {
151
- await prepare?.();
152
- await this.systemProxy.release();
153
- const result = await this.supervisor.restart();
154
- this.phase = "running";
155
- await this.applyDesiredProxyUnlocked();
156
- return result;
157
- }
158
- catch (err) {
159
- this.phase = this.supervisor.isRunning() ? "running" : "failed";
160
- throw err;
161
- }
162
- }
163
- async reconcileSystemProxyUnlocked() {
164
- const settings = this.getSettings();
165
- if (!settings.systemProxy) {
166
- await this.systemProxy.release();
167
- return;
168
- }
169
- await this.applyProxyToHealthyOwnedCoreUnlocked(settings.mixedPort);
170
- }
171
- async applyDesiredProxyUnlocked() {
172
- const settings = this.getSettings();
173
- if (!settings.systemProxy)
11
+ this.options = options;
12
+ this.runtimeSettings = { ...options.settings() };
13
+ }
14
+ get revision() {
15
+ return this.runtimeRevision;
16
+ }
17
+ settings() {
18
+ return { ...this.runtimeSettings };
19
+ }
20
+ configuration() {
21
+ return this.applied;
22
+ }
23
+ async recoverStartup() {
24
+ await this.options.systemProxy.release();
25
+ await this.options.supervisor.cleanStaleCore();
26
+ if (readCoreUpdateTransaction(this.options.layout))
27
+ await this.requireVacantController();
28
+ recoverBinaryUnlockProbe(this.options.layout.coreExe);
29
+ recoverCoreUpdateTransaction(this.options.layout, this.options.verifyExecutable);
30
+ }
31
+ async requireVacantController() {
32
+ const reachable = this.options.controllerProbe
33
+ ? await this.options.controllerProbe(this.runtimeSettings)
34
+ : await new MihomoApi(this.runtimeSettings.controller, this.runtimeSettings.secret).isReachable();
35
+ if (reachable)
36
+ throw new Error("An unowned Core controller is still active; refusing to replace its runtime");
37
+ }
38
+ startCore() {
39
+ this.runtimeRevision += 1;
40
+ return this.options.supervisor.start();
41
+ }
42
+ async start() {
43
+ const core = await this.options.supervisor.status();
44
+ if (!core.running || !core.healthy || !core.pid)
45
+ throw new Error("Core is no longer healthy; retry start");
46
+ await this.reconcileSystemProxy();
47
+ return { pid: core.pid, ...(core.version ? { version: core.version } : {}) };
48
+ }
49
+ async apply(configuration) {
50
+ await this.stop();
51
+ await this.requireVacantController();
52
+ this.applied = undefined;
53
+ atomicWriteFileSync(this.options.layout.configFile, configuration.generated.yaml);
54
+ this.runtimeSettings = { ...configuration.settings };
55
+ const result = await this.startCore();
56
+ this.applied = configuration;
57
+ await this.reconcileSystemProxy();
58
+ return result;
59
+ }
60
+ async stop() {
61
+ await this.options.systemProxy.release();
62
+ await this.options.supervisor.stop();
63
+ this.runtimeRevision += 1;
64
+ }
65
+ async reconcileSystemProxy() {
66
+ if (!this.options.settings().systemProxy) {
67
+ await this.options.systemProxy.release();
174
68
  return;
175
- await this.applyProxyToHealthyOwnedCoreUnlocked(settings.mixedPort);
176
- }
177
- async applyProxyToHealthyOwnedCoreUnlocked(port) {
178
- const ownership = await this.requireHealthyOwnedCoreUnlocked();
179
- await this.systemProxy.apply({ port });
180
- const coreAfterApply = await this.supervisor.status();
181
- if (coreAfterApply.running && coreAfterApply.healthy && this.supervisor.ownsCore(ownership)) {
182
- return;
183
- }
184
- try {
185
- await this.systemProxy.release();
186
69
  }
187
- catch (err) {
188
- throw new Error(`Core ownership was lost while applying the system proxy; proxy release also failed: ${err.message}`);
70
+ const { supervisor, systemProxy } = this.options;
71
+ const owner = supervisor.ownedCoreSnapshot();
72
+ const core = await supervisor.status();
73
+ if (!owner || !core.running || !core.healthy || !supervisor.ownsCore(owner)) {
74
+ throw new Error("Cannot enable system proxy without a healthy owned Core");
189
75
  }
76
+ await systemProxy.apply({ port: this.runtimeSettings.mixedPort });
77
+ const after = await supervisor.status();
78
+ if (after.running && after.healthy && supervisor.ownsCore(owner))
79
+ return;
80
+ await systemProxy.release();
190
81
  throw new Error("Core ownership was lost while applying the system proxy");
191
82
  }
192
- async requireHealthyOwnedCoreUnlocked() {
193
- const ownership = this.supervisor.ownedCoreSnapshot();
194
- if (!ownership)
195
- throw new Error("Cannot enable system proxy: core is not running");
196
- const core = await this.supervisor.status();
197
- if (!core.running || !core.healthy) {
198
- throw new Error("Cannot enable system proxy: core is not healthy");
83
+ async update(staged, configuration) {
84
+ const wasRunning = this.options.supervisor.isRunning();
85
+ if (!wasRunning) {
86
+ atomicWriteFileSync(this.options.layout.configFile, configuration.generated.yaml);
87
+ this.runtimeSettings = { ...configuration.settings };
199
88
  }
200
- if (!this.supervisor.ownsCore(ownership)) {
201
- throw new Error("Cannot enable system proxy: core ownership changed during health check");
89
+ return commitCoreUpdate({
90
+ layout: this.options.layout,
91
+ staged,
92
+ verifyExecutable: this.options.verifyExecutable,
93
+ runtime: {
94
+ wasRunning,
95
+ stop: async () => {
96
+ await this.stop();
97
+ await this.requireVacantController();
98
+ },
99
+ startAndVerify: async () => {
100
+ await this.startCore();
101
+ this.applied = configuration;
102
+ },
103
+ applySystemProxy: () => this.reconcileSystemProxy(),
104
+ },
105
+ });
106
+ }
107
+ async handleUnexpectedCoreExit() {
108
+ if (this.options.supervisor.isRunning())
109
+ return;
110
+ this.runtimeRevision += 1;
111
+ for (let attempt = 0;; attempt += 1) {
112
+ try {
113
+ await this.options.systemProxy.release();
114
+ return;
115
+ }
116
+ catch (error) {
117
+ if (attempt === 2)
118
+ throw error;
119
+ await new Promise((resolve) => setTimeout(resolve, 250 * (attempt + 1)));
120
+ }
202
121
  }
203
- return ownership;
204
122
  }
205
123
  }