@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
package/dist/runtime-owner.js
CHANGED
|
@@ -1,87 +1,48 @@
|
|
|
1
|
-
import
|
|
2
|
-
import {
|
|
3
|
-
import { recoverCoordinatedCoreUpdate } from "./core-update-coordination.js";
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import { loadSettings } from "./app-state.js";
|
|
4
3
|
import { SashDaemonClient } from "./daemon-client.js";
|
|
5
|
-
import { ensureDaemon, evaluateDaemon,
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
}
|
|
4
|
+
import { ensureDaemon, evaluateDaemon, stopDaemonFromCli, } from "./daemon-lifecycle.js";
|
|
5
|
+
export async function resolveRuntimeOwner(ctx, deps = {}) {
|
|
6
|
+
const daemon = await (deps.evaluateDaemon ?? evaluateDaemon)(ctx.layout, ctx.settings);
|
|
7
|
+
if (daemon.kind === "healthy")
|
|
8
|
+
return {
|
|
9
|
+
kind: "daemon",
|
|
10
|
+
daemon,
|
|
11
|
+
client: (deps.clientFactory ?? ((port, secret) => new SashDaemonClient(port, secret)))(daemon.port, ctx.settings.daemonSecret),
|
|
12
|
+
};
|
|
15
13
|
return daemon.kind === "stopped" ? { kind: "offline", daemon } : { kind: "unhealthy", daemon };
|
|
16
14
|
}
|
|
17
|
-
export async function
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
assertCoreInstallationConsistent(ctx.layout);
|
|
21
|
-
if (coreInstalled(ctx.layout))
|
|
22
|
-
return;
|
|
23
|
-
log.info("mihomo core not installed; downloading latest release...");
|
|
24
|
-
const { version } = await installCore({ layout: ctx.layout });
|
|
25
|
-
log.ok(`mihomo core ${version} installed`);
|
|
26
|
-
}
|
|
27
|
-
function withRuntimeOperation(ctx, purpose, action) {
|
|
28
|
-
return withStateLock(ctx.layout.runtimeOperationLockFile, { purpose, timeoutMs: 120_000 }, () => action());
|
|
29
|
-
}
|
|
30
|
-
async function healthyDaemonOwner(ctx) {
|
|
15
|
+
export async function ensureManagement(ctx) {
|
|
16
|
+
await ensureDaemon({ layout: ctx.layout, settings: ctx.settings });
|
|
17
|
+
ctx.settings = loadSettings(ctx.layout);
|
|
31
18
|
const owner = await resolveRuntimeOwner(ctx);
|
|
32
|
-
if (owner.kind !== "daemon")
|
|
33
|
-
throw new Error("sashd did not become healthy
|
|
34
|
-
}
|
|
19
|
+
if (owner.kind !== "daemon")
|
|
20
|
+
throw new Error("sashd did not become healthy");
|
|
35
21
|
return owner;
|
|
36
22
|
}
|
|
37
23
|
export async function ensureRunning(ctx) {
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
if (initialOwner.kind === "unhealthy") {
|
|
41
|
-
throw new Error("sashd is already starting or unresponsive; refusing a competing start");
|
|
42
|
-
}
|
|
43
|
-
if (initialOwner.kind === "offline") {
|
|
44
|
-
await runOfflineMutation(ctx, "install Core before start", () => ensureCore(ctx));
|
|
45
|
-
}
|
|
46
|
-
await ensureDaemon({ layout: ctx.layout, settings: ctx.settings });
|
|
47
|
-
const owner = await healthyDaemonOwner(ctx);
|
|
48
|
-
const result = await owner.client.startCore();
|
|
49
|
-
return { owner, result };
|
|
50
|
-
});
|
|
24
|
+
const owner = await ensureManagement(ctx);
|
|
25
|
+
return { owner, result: await owner.client.startCore() };
|
|
51
26
|
}
|
|
52
27
|
export async function stopRuntime(ctx) {
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
});
|
|
28
|
+
const initial = await resolveRuntimeOwner(ctx);
|
|
29
|
+
if (initial.kind === "unhealthy")
|
|
30
|
+
throw new Error("sashd is unresponsive or its ownership is unknown; refusing an unverified stop");
|
|
31
|
+
if (initial.kind === "offline") {
|
|
32
|
+
const leftovers = [
|
|
33
|
+
ctx.layout.pidFile,
|
|
34
|
+
ctx.layout.systemProxyStateFile,
|
|
35
|
+
ctx.layout.coreUpdateTransactionFile,
|
|
36
|
+
];
|
|
37
|
+
if (!leftovers.some((file) => fs.existsSync(file)))
|
|
38
|
+
return { wasRunning: false };
|
|
39
|
+
await ensureManagement(ctx);
|
|
40
|
+
}
|
|
41
|
+
if (!(await stopDaemonFromCli({ layout: ctx.layout, settings: ctx.settings })))
|
|
42
|
+
throw new Error("sashd shutdown could not be verified");
|
|
43
|
+
return { wasRunning: initial.kind === "daemon" };
|
|
70
44
|
}
|
|
71
|
-
export async function restartRuntime(ctx
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
if (maintenance.legacyDaemon) {
|
|
75
|
-
await runOfflineMutation(ctx, "clean up legacy runtime", () => undefined, {
|
|
76
|
-
reconcileRuntime: { legacyDaemon: true },
|
|
77
|
-
});
|
|
78
|
-
}
|
|
79
|
-
if (!maintenance.daemonWasRunning) {
|
|
80
|
-
await runOfflineMutation(ctx, "install Core before restart", () => ensureCore(ctx));
|
|
81
|
-
}
|
|
82
|
-
const { pid: daemonPid } = await spawnDaemon({ layout: ctx.layout, settings: ctx.settings });
|
|
83
|
-
const owner = await healthyDaemonOwner(ctx);
|
|
84
|
-
const result = await owner.client.startCore();
|
|
85
|
-
return { owner, result, daemonPid, daemonWasRunning: maintenance.daemonWasRunning };
|
|
86
|
-
});
|
|
45
|
+
export async function restartRuntime(ctx) {
|
|
46
|
+
const owner = await ensureManagement(ctx);
|
|
47
|
+
return { owner, result: await owner.client.restartCore() };
|
|
87
48
|
}
|
package/dist/sash-client.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { parseAutostartStatus } from "./autostart-contract.js";
|
|
2
|
+
import { parseApiErrorBody, parseCoreStartResult, parseCoreUpdateResponse, parseDaemonStatus, parseHealthInfo, parseProfileActionResponse, parseProfileActivateResponse, parseProfileContentResponse, parseProfileRemoveResponse, parseProfileRenameResponse, parseProfilesIndex, parseProfilesUpdateAllResponse, parseProfileUpdateResponse, parsePublicSettings, parseSettingsWriteResult, parseSystemProxyStatusResponse, parseWebBootstrapInfo, parseWebSessionInfo, } from "./contracts.js";
|
|
2
3
|
const defaultFetch = async (url, init) => {
|
|
3
4
|
const response = await fetch(url, {
|
|
4
5
|
method: init.method,
|
|
@@ -8,6 +9,7 @@ const defaultFetch = async (url, init) => {
|
|
|
8
9
|
});
|
|
9
10
|
return { status: response.status, text: () => response.text() };
|
|
10
11
|
};
|
|
12
|
+
const CORE_OPERATION_TIMEOUT_MS = 20 * 60_000;
|
|
11
13
|
export class SashApiError extends Error {
|
|
12
14
|
status;
|
|
13
15
|
code;
|
|
@@ -24,6 +26,7 @@ export class SashClient {
|
|
|
24
26
|
tokenHeader;
|
|
25
27
|
fetchFn;
|
|
26
28
|
timeoutMs;
|
|
29
|
+
onUnauthorized;
|
|
27
30
|
constructor(options) {
|
|
28
31
|
this.baseUrl = options.baseUrl.replace(/\/+$/, "");
|
|
29
32
|
if (options.token)
|
|
@@ -31,10 +34,12 @@ export class SashClient {
|
|
|
31
34
|
this.tokenHeader = options.tokenHeader ?? "authorization";
|
|
32
35
|
this.fetchFn = options.fetchFn ?? defaultFetch;
|
|
33
36
|
this.timeoutMs = options.timeoutMs ?? 5_000;
|
|
37
|
+
if (options.onUnauthorized)
|
|
38
|
+
this.onUnauthorized = options.onUnauthorized;
|
|
34
39
|
}
|
|
35
40
|
async request(endpoint, options = {}) {
|
|
36
41
|
const headers = {};
|
|
37
|
-
const token = this.token?.() ?? "";
|
|
42
|
+
const token = options.authenticate === false ? "" : (this.token?.() ?? "");
|
|
38
43
|
if (token) {
|
|
39
44
|
if (this.tokenHeader === "x-sash-token")
|
|
40
45
|
headers["X-Sash-Token"] = token;
|
|
@@ -64,6 +69,8 @@ export class SashClient {
|
|
|
64
69
|
}
|
|
65
70
|
data = text;
|
|
66
71
|
}
|
|
72
|
+
if (response.status === 401 && token)
|
|
73
|
+
this.onUnauthorized?.(token);
|
|
67
74
|
if (response.status < 200 || response.status >= 300) {
|
|
68
75
|
const parsedError = parseApiErrorBody(data);
|
|
69
76
|
const message = parsedError?.message ?? (typeof data === "string" ? data.slice(0, 300).trim() : "");
|
|
@@ -72,25 +79,67 @@ export class SashClient {
|
|
|
72
79
|
return data;
|
|
73
80
|
}
|
|
74
81
|
async health() {
|
|
75
|
-
return parseHealthInfo(await this.request("/sash/daemon/health", {
|
|
82
|
+
return parseHealthInfo(await this.request("/sash/daemon/health", {
|
|
83
|
+
timeoutMs: 2_000,
|
|
84
|
+
attempts: 1,
|
|
85
|
+
authenticate: false,
|
|
86
|
+
}));
|
|
87
|
+
}
|
|
88
|
+
async createWebBootstrap() {
|
|
89
|
+
return parseWebBootstrapInfo(await this.request("/sash/web/bootstrap", { method: "POST", timeoutMs: 5_000 }));
|
|
90
|
+
}
|
|
91
|
+
async redeemWebBootstrap(token) {
|
|
92
|
+
return parseWebSessionInfo(await this.request("/sash/web/session", {
|
|
93
|
+
method: "POST",
|
|
94
|
+
body: { token },
|
|
95
|
+
timeoutMs: 5_000,
|
|
96
|
+
attempts: 1,
|
|
97
|
+
authenticate: false,
|
|
98
|
+
}));
|
|
76
99
|
}
|
|
77
100
|
async status(fresh = false) {
|
|
78
|
-
return parseDaemonStatus(await this.request(fresh ? "/sash/daemon/status?fresh=1" : "/sash/daemon/status"
|
|
101
|
+
return parseDaemonStatus(await this.request(fresh ? "/sash/daemon/status?fresh=1" : "/sash/daemon/status", {
|
|
102
|
+
timeoutMs: 8000,
|
|
103
|
+
}));
|
|
79
104
|
}
|
|
80
105
|
async shutdown() {
|
|
81
|
-
|
|
106
|
+
await this.request("/sash/daemon/shutdown", { method: "POST", timeoutMs: 45_000 });
|
|
107
|
+
}
|
|
108
|
+
async autostartStatus() {
|
|
109
|
+
return parseAutostartStatus(await this.request("/sash/autostart", { timeoutMs: 15_000, attempts: 1 }));
|
|
110
|
+
}
|
|
111
|
+
async setAutostart(enabled) {
|
|
112
|
+
return parseAutostartStatus(await this.request("/sash/autostart", {
|
|
113
|
+
method: "PUT",
|
|
114
|
+
body: { enabled },
|
|
115
|
+
timeoutMs: 60_000,
|
|
116
|
+
attempts: 1,
|
|
117
|
+
}));
|
|
82
118
|
}
|
|
83
119
|
async startCore() {
|
|
84
|
-
return parseCoreStartResult(await this.request("/sash/core/start", {
|
|
120
|
+
return parseCoreStartResult(await this.request("/sash/core/start", {
|
|
121
|
+
method: "POST",
|
|
122
|
+
timeoutMs: CORE_OPERATION_TIMEOUT_MS,
|
|
123
|
+
}));
|
|
85
124
|
}
|
|
86
125
|
async stopCore() {
|
|
87
126
|
await this.request("/sash/core/stop", { method: "POST", timeoutMs: 30_000 });
|
|
88
127
|
}
|
|
89
128
|
async restartCore() {
|
|
90
|
-
return parseCoreStartResult(await this.request("/sash/core/restart", {
|
|
129
|
+
return parseCoreStartResult(await this.request("/sash/core/restart", {
|
|
130
|
+
method: "POST",
|
|
131
|
+
timeoutMs: CORE_OPERATION_TIMEOUT_MS,
|
|
132
|
+
}));
|
|
133
|
+
}
|
|
134
|
+
async updateCore(version) {
|
|
135
|
+
return parseCoreUpdateResponse(await this.request("/sash/core/update", {
|
|
136
|
+
method: "POST",
|
|
137
|
+
body: version ? { version } : {},
|
|
138
|
+
timeoutMs: CORE_OPERATION_TIMEOUT_MS,
|
|
139
|
+
}));
|
|
91
140
|
}
|
|
92
|
-
async
|
|
93
|
-
|
|
141
|
+
async setMode(mode) {
|
|
142
|
+
await this.request("/sash/core/mode", { method: "PUT", body: { mode } });
|
|
94
143
|
}
|
|
95
144
|
async proxyStatus(fresh = false) {
|
|
96
145
|
return parseSystemProxyStatusResponse(await this.request(fresh ? "/sash/proxy?fresh=1" : "/sash/proxy"));
|
|
@@ -101,19 +150,12 @@ export class SashClient {
|
|
|
101
150
|
async patchSettings(patch) {
|
|
102
151
|
return parseSettingsWriteResult(await this.request("/sash/settings", { method: "PATCH", body: patch, timeoutMs: 45_000 }));
|
|
103
152
|
}
|
|
104
|
-
async getSettingsFile() {
|
|
105
|
-
return parseSettingsFileContent(await this.request("/sash/settings/file"));
|
|
106
|
-
}
|
|
107
|
-
async writeSettingsFile(content) {
|
|
108
|
-
return parseSettingsWriteResult(await this.request("/sash/settings/file", {
|
|
109
|
-
method: "PUT",
|
|
110
|
-
body: { content },
|
|
111
|
-
timeoutMs: 45_000,
|
|
112
|
-
}));
|
|
113
|
-
}
|
|
114
153
|
async listProfiles() {
|
|
115
154
|
return parseProfilesIndex(await this.request("/sash/profiles"));
|
|
116
155
|
}
|
|
156
|
+
async reorderProfiles(ids) {
|
|
157
|
+
return parseProfilesIndex(await this.request("/sash/profiles/order", { method: "PUT", body: { ids } }));
|
|
158
|
+
}
|
|
117
159
|
async addProfile(url, opts = {}) {
|
|
118
160
|
return parseProfileActionResponse(await this.request("/sash/profiles", {
|
|
119
161
|
method: "POST",
|
|
@@ -147,10 +189,10 @@ export class SashClient {
|
|
|
147
189
|
async getProfileContent(id) {
|
|
148
190
|
return parseProfileContentResponse(await this.request(`/sash/profiles/${id}/content`));
|
|
149
191
|
}
|
|
150
|
-
async writeProfileContent(id, content) {
|
|
192
|
+
async writeProfileContent(id, content, revision) {
|
|
151
193
|
return parseProfileUpdateResponse(await this.request(`/sash/profiles/${id}/content`, {
|
|
152
194
|
method: "PUT",
|
|
153
|
-
body: { content },
|
|
195
|
+
body: { content, revision },
|
|
154
196
|
timeoutMs: 30_000,
|
|
155
197
|
}));
|
|
156
198
|
}
|
package/dist/settings-service.js
CHANGED
|
@@ -1,242 +1,43 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { ProfileConflictError, } from "./profile-service.js";
|
|
3
|
-
import { sameSettings, validateSettingsCandidate } from "./settings.js";
|
|
4
|
-
import { tunPrivilegeGuidance } from "./tun-guidance.js";
|
|
1
|
+
import { validateSettingsCandidate } from "./settings.js";
|
|
5
2
|
export class SettingsInputError extends Error {
|
|
6
3
|
}
|
|
7
4
|
export class CoreUnhealthyError extends Error {
|
|
8
5
|
}
|
|
9
|
-
|
|
6
|
+
export { StateConflictError as SettingsConflictError } from "./app-state.js";
|
|
10
7
|
export class SettingsService {
|
|
11
8
|
options;
|
|
12
9
|
constructor(options) {
|
|
13
10
|
this.options = options;
|
|
14
11
|
}
|
|
15
12
|
async apply(patch) {
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
candidate = validateSettingsCandidate(mergePatch(previous, patch));
|
|
20
|
-
}
|
|
21
|
-
catch (err) {
|
|
22
|
-
throw new SettingsInputError(err.message);
|
|
23
|
-
}
|
|
24
|
-
const restartRequired = candidate.daemonPort !== previous.daemonPort;
|
|
25
|
-
const proxyChanged = candidate.systemProxy !== previous.systemProxy;
|
|
26
|
-
const coreChanged = CORE_SETTING_KEYS.some((key) => candidate[key] !== previous[key]);
|
|
27
|
-
const daemonChanged = restartRequired || candidate.daemonSecret !== previous.daemonSecret;
|
|
28
|
-
let committed = previous;
|
|
29
|
-
if (coreChanged || daemonChanged) {
|
|
30
|
-
const staged = { ...candidate, systemProxy: previous.systemProxy };
|
|
31
|
-
committed = coreChanged
|
|
32
|
-
? await this.commitCoreChange(previous, staged, {
|
|
33
|
-
verifyTun: candidate.tun && !previous.tun,
|
|
34
|
-
})
|
|
35
|
-
: await this.commitSettingsOnly(previous, staged);
|
|
36
|
-
}
|
|
37
|
-
if (proxyChanged) {
|
|
38
|
-
committed = await this.commitSystemProxy(committed, candidate);
|
|
39
|
-
}
|
|
40
|
-
return { settings: committed, restartRequired };
|
|
41
|
-
}
|
|
42
|
-
async applyFileSettings(next) {
|
|
43
|
-
const current = this.options.getCommitted();
|
|
44
|
-
const patch = {};
|
|
45
|
-
if (next.mixedPort !== current.mixedPort)
|
|
46
|
-
patch.mixedPort = next.mixedPort;
|
|
47
|
-
if (next.controller !== current.controller)
|
|
48
|
-
patch.controller = next.controller;
|
|
49
|
-
if (next.secret !== current.secret)
|
|
50
|
-
patch.secret = next.secret;
|
|
51
|
-
if (next.tun !== current.tun)
|
|
52
|
-
patch.tun = next.tun;
|
|
53
|
-
if (next.allowLan !== current.allowLan)
|
|
54
|
-
patch.allowLan = next.allowLan;
|
|
55
|
-
if (next.systemProxy !== current.systemProxy)
|
|
56
|
-
patch.systemProxy = next.systemProxy;
|
|
57
|
-
if (next.daemonPort !== current.daemonPort)
|
|
58
|
-
patch.daemonPort = next.daemonPort;
|
|
59
|
-
if (next.daemonSecret !== current.daemonSecret)
|
|
60
|
-
patch.daemonSecret = next.daemonSecret;
|
|
61
|
-
return this.apply(patch);
|
|
62
|
-
}
|
|
63
|
-
async commitSettingsOnly(previous, candidate) {
|
|
64
|
-
return this.options.commit("update daemon settings", async () => {
|
|
65
|
-
this.assertCurrent(previous);
|
|
66
|
-
this.options.setRuntime(candidate);
|
|
13
|
+
return this.options.commit("save settings", async () => {
|
|
14
|
+
const state = this.options.state.snapshot();
|
|
15
|
+
let settings;
|
|
67
16
|
try {
|
|
68
|
-
|
|
69
|
-
}
|
|
70
|
-
catch (
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
17
|
+
settings = validateSettingsCandidate({ ...state.settings, ...patch });
|
|
18
|
+
}
|
|
19
|
+
catch (error) {
|
|
20
|
+
throw new SettingsInputError(error instanceof Error ? error.message : String(error));
|
|
21
|
+
}
|
|
22
|
+
if (patch.systemProxy === true) {
|
|
23
|
+
const core = await this.options.supervisor.status();
|
|
24
|
+
if (!core.running || !core.healthy)
|
|
25
|
+
throw new CoreUnhealthyError("Cannot enable system proxy: Core is not healthy");
|
|
26
|
+
}
|
|
27
|
+
this.options.state.assertCurrent(state.revision);
|
|
28
|
+
const changed = settings.mixedPort !== state.settings.mixedPort ||
|
|
29
|
+
settings.allowLan !== state.settings.allowLan ||
|
|
30
|
+
settings.systemProxy !== state.settings.systemProxy;
|
|
31
|
+
const saved = changed ? this.options.state.commit({ ...state, settings }) : state;
|
|
32
|
+
if (patch.systemProxy !== undefined) {
|
|
33
|
+
await this.options.lifecycle.reconcileSystemProxy();
|
|
34
|
+
}
|
|
35
|
+
const running = this.options.lifecycle.settings();
|
|
36
|
+
return {
|
|
37
|
+
settings: saved.settings,
|
|
38
|
+
restartRequired: saved.settings.mixedPort !== running.mixedPort ||
|
|
39
|
+
saved.settings.allowLan !== running.allowLan,
|
|
40
|
+
};
|
|
76
41
|
});
|
|
77
42
|
}
|
|
78
|
-
async commitCoreChange(previous, candidate, opts) {
|
|
79
|
-
for (let attempt = 0;; attempt += 1) {
|
|
80
|
-
const prepared = await this.options.profiles.prepareActiveConfig(candidate, previous);
|
|
81
|
-
let retryableProfileConflict;
|
|
82
|
-
let callbackEntered = false;
|
|
83
|
-
try {
|
|
84
|
-
return await this.options.commit("update settings", async () => {
|
|
85
|
-
this.assertCurrent(previous);
|
|
86
|
-
try {
|
|
87
|
-
return await this.options.profiles.withPreparedActivePublication(prepared, async (publication) => {
|
|
88
|
-
callbackEntered = true;
|
|
89
|
-
return this.commitCoreSettings(previous, candidate, opts, publication);
|
|
90
|
-
});
|
|
91
|
-
}
|
|
92
|
-
catch (err) {
|
|
93
|
-
if (err instanceof ProfileConflictError && !callbackEntered) {
|
|
94
|
-
retryableProfileConflict = err;
|
|
95
|
-
}
|
|
96
|
-
throw err;
|
|
97
|
-
}
|
|
98
|
-
});
|
|
99
|
-
}
|
|
100
|
-
catch (err) {
|
|
101
|
-
if (err !== retryableProfileConflict || attempt >= 1)
|
|
102
|
-
throw err;
|
|
103
|
-
}
|
|
104
|
-
}
|
|
105
|
-
}
|
|
106
|
-
async commitSystemProxy(previous, candidate) {
|
|
107
|
-
if (!candidate.systemProxy) {
|
|
108
|
-
return this.options.commit("disable system proxy", async () => {
|
|
109
|
-
this.assertCurrent(previous);
|
|
110
|
-
this.options.setRuntime(candidate);
|
|
111
|
-
try {
|
|
112
|
-
await commitManagedStateTransaction(this.options.layout, { settings: candidate }, undefined);
|
|
113
|
-
}
|
|
114
|
-
catch (err) {
|
|
115
|
-
this.options.setRuntime(previous);
|
|
116
|
-
throw err;
|
|
117
|
-
}
|
|
118
|
-
this.options.setCommitted(candidate);
|
|
119
|
-
if (this.options.lifecycle)
|
|
120
|
-
await this.options.lifecycle.reconcileSystemProxy();
|
|
121
|
-
else
|
|
122
|
-
await this.options.releaseSystemProxy?.();
|
|
123
|
-
return candidate;
|
|
124
|
-
});
|
|
125
|
-
}
|
|
126
|
-
const core = await this.options.supervisor?.status();
|
|
127
|
-
if (!core?.running || !core.healthy) {
|
|
128
|
-
throw new CoreUnhealthyError("Cannot enable system proxy: core is not healthy");
|
|
129
|
-
}
|
|
130
|
-
return this.options.commit("enable system proxy", async () => {
|
|
131
|
-
this.assertCurrent(previous);
|
|
132
|
-
this.options.setRuntime(candidate);
|
|
133
|
-
try {
|
|
134
|
-
await commitManagedStateTransaction(this.options.layout, {
|
|
135
|
-
settings: candidate,
|
|
136
|
-
applyRuntime: () => this.requireLifecycle().reconcileSystemProxy(),
|
|
137
|
-
}, undefined);
|
|
138
|
-
this.options.setCommitted(candidate);
|
|
139
|
-
return candidate;
|
|
140
|
-
}
|
|
141
|
-
catch (err) {
|
|
142
|
-
this.options.setRuntime(previous);
|
|
143
|
-
try {
|
|
144
|
-
await this.requireLifecycle().reconcileSystemProxy();
|
|
145
|
-
}
|
|
146
|
-
catch (rollback) {
|
|
147
|
-
throw new Error(`${err.message}; system proxy rollback failed: ${rollback.message}`);
|
|
148
|
-
}
|
|
149
|
-
throw err;
|
|
150
|
-
}
|
|
151
|
-
});
|
|
152
|
-
}
|
|
153
|
-
async commitCoreSettings(previous, candidate, opts, publication) {
|
|
154
|
-
const wasRunning = this.options.supervisor?.isRunning() ?? false;
|
|
155
|
-
this.options.setRuntime(candidate);
|
|
156
|
-
try {
|
|
157
|
-
await commitManagedStateTransaction(this.options.layout, {
|
|
158
|
-
...(publication.index ? { index: publication.index } : {}),
|
|
159
|
-
...(publication.profile
|
|
160
|
-
? {
|
|
161
|
-
profile: {
|
|
162
|
-
id: publication.profile.id,
|
|
163
|
-
yamlText: publication.profile.yamlText,
|
|
164
|
-
},
|
|
165
|
-
}
|
|
166
|
-
: {}),
|
|
167
|
-
config: publication.config,
|
|
168
|
-
settings: candidate,
|
|
169
|
-
reloadRuntime: false,
|
|
170
|
-
applyRuntime: async () => {
|
|
171
|
-
if (!wasRunning)
|
|
172
|
-
return;
|
|
173
|
-
const result = await this.requireLifecycle().restart();
|
|
174
|
-
if (opts.verifyTun && result.tunActive !== true) {
|
|
175
|
-
throw new Error(result.tunActive === false
|
|
176
|
-
? `TUN did not become active. ${tunPrivilegeGuidance("activation-rolled-back", { root: this.options.layout.root })}`
|
|
177
|
-
: `TUN activation could not be verified through the Core controller. ${tunPrivilegeGuidance("activation-rolled-back", { root: this.options.layout.root })}`);
|
|
178
|
-
}
|
|
179
|
-
},
|
|
180
|
-
}, undefined);
|
|
181
|
-
this.options.setCommitted(candidate);
|
|
182
|
-
return candidate;
|
|
183
|
-
}
|
|
184
|
-
catch (err) {
|
|
185
|
-
this.options.setRuntime(previous);
|
|
186
|
-
const rollbackErrors = [];
|
|
187
|
-
try {
|
|
188
|
-
await this.options.profiles.commitPreparedActiveReload(publication.rollback, {
|
|
189
|
-
reloadRuntime: false,
|
|
190
|
-
boundary: "already-held",
|
|
191
|
-
});
|
|
192
|
-
}
|
|
193
|
-
catch (rollback) {
|
|
194
|
-
rollbackErrors.push(`config rollback failed: ${rollback.message}`);
|
|
195
|
-
}
|
|
196
|
-
if (wasRunning) {
|
|
197
|
-
try {
|
|
198
|
-
if (this.options.supervisor?.isRunning())
|
|
199
|
-
await this.requireLifecycle().restart();
|
|
200
|
-
else
|
|
201
|
-
await this.requireLifecycle().start();
|
|
202
|
-
}
|
|
203
|
-
catch (rollback) {
|
|
204
|
-
rollbackErrors.push(`runtime rollback failed: ${rollback.message}`);
|
|
205
|
-
}
|
|
206
|
-
}
|
|
207
|
-
if (rollbackErrors.length)
|
|
208
|
-
throw new Error(`${err.message}; ${rollbackErrors.join("; ")}`);
|
|
209
|
-
throw err;
|
|
210
|
-
}
|
|
211
|
-
}
|
|
212
|
-
requireLifecycle() {
|
|
213
|
-
if (!this.options.lifecycle)
|
|
214
|
-
throw new Error("No runtime lifecycle is available for this settings mutation");
|
|
215
|
-
return this.options.lifecycle;
|
|
216
|
-
}
|
|
217
|
-
assertCurrent(previous) {
|
|
218
|
-
if (!sameSettings(this.options.getCommitted(), previous)) {
|
|
219
|
-
throw new Error("Settings changed while preparing configuration");
|
|
220
|
-
}
|
|
221
|
-
}
|
|
222
|
-
}
|
|
223
|
-
function mergePatch(previous, patch) {
|
|
224
|
-
const candidate = { ...previous };
|
|
225
|
-
if (patch.mixedPort !== undefined)
|
|
226
|
-
candidate.mixedPort = patch.mixedPort;
|
|
227
|
-
if (patch.controller !== undefined)
|
|
228
|
-
candidate.controller = patch.controller;
|
|
229
|
-
if (patch.secret !== undefined)
|
|
230
|
-
candidate.secret = patch.secret;
|
|
231
|
-
if (patch.tun !== undefined)
|
|
232
|
-
candidate.tun = patch.tun;
|
|
233
|
-
if (patch.allowLan !== undefined)
|
|
234
|
-
candidate.allowLan = patch.allowLan;
|
|
235
|
-
if (patch.systemProxy !== undefined)
|
|
236
|
-
candidate.systemProxy = patch.systemProxy;
|
|
237
|
-
if (patch.daemonPort !== undefined)
|
|
238
|
-
candidate.daemonPort = patch.daemonPort;
|
|
239
|
-
if (patch.daemonSecret !== undefined)
|
|
240
|
-
candidate.daemonSecret = patch.daemonSecret;
|
|
241
|
-
return candidate;
|
|
242
43
|
}
|