@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/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 {
|
|
6
|
-
import {
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
if (
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
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((
|
|
34
|
+
return index.profiles.find((profile) => profile.id === index.activeId) ?? null;
|
|
103
35
|
}
|
|
104
|
-
export function
|
|
105
|
-
|
|
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
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
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
|
-
|
|
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,
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
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
|
-
|
|
2
|
-
|
|
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
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
operationQueue = Promise.resolve();
|
|
10
|
-
phase;
|
|
11
|
-
generation = 0;
|
|
6
|
+
options;
|
|
7
|
+
runtimeRevision = 0;
|
|
8
|
+
applied;
|
|
9
|
+
runtimeSettings;
|
|
12
10
|
constructor(options) {
|
|
13
|
-
this.
|
|
14
|
-
this.
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
return {
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
return this.
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
await this.
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
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
|
-
|
|
188
|
-
|
|
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
|
|
193
|
-
const
|
|
194
|
-
if (!
|
|
195
|
-
|
|
196
|
-
|
|
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
|
-
|
|
201
|
-
|
|
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
|
}
|