@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/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;
|
|
@@ -96,22 +98,48 @@ export class SashClient {
|
|
|
96
98
|
}));
|
|
97
99
|
}
|
|
98
100
|
async status(fresh = false) {
|
|
99
|
-
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
|
+
}));
|
|
100
104
|
}
|
|
101
105
|
async shutdown() {
|
|
102
|
-
|
|
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
|
+
}));
|
|
103
118
|
}
|
|
104
119
|
async startCore() {
|
|
105
|
-
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
|
+
}));
|
|
106
124
|
}
|
|
107
125
|
async stopCore() {
|
|
108
126
|
await this.request("/sash/core/stop", { method: "POST", timeoutMs: 30_000 });
|
|
109
127
|
}
|
|
110
128
|
async restartCore() {
|
|
111
|
-
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
|
+
}));
|
|
112
133
|
}
|
|
113
|
-
async
|
|
114
|
-
return
|
|
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
|
+
}));
|
|
140
|
+
}
|
|
141
|
+
async setMode(mode) {
|
|
142
|
+
await this.request("/sash/core/mode", { method: "PUT", body: { mode } });
|
|
115
143
|
}
|
|
116
144
|
async proxyStatus(fresh = false) {
|
|
117
145
|
return parseSystemProxyStatusResponse(await this.request(fresh ? "/sash/proxy?fresh=1" : "/sash/proxy"));
|
|
@@ -122,19 +150,12 @@ export class SashClient {
|
|
|
122
150
|
async patchSettings(patch) {
|
|
123
151
|
return parseSettingsWriteResult(await this.request("/sash/settings", { method: "PATCH", body: patch, timeoutMs: 45_000 }));
|
|
124
152
|
}
|
|
125
|
-
async getSettingsFile() {
|
|
126
|
-
return parseSettingsFileContent(await this.request("/sash/settings/file"));
|
|
127
|
-
}
|
|
128
|
-
async writeSettingsFile(content) {
|
|
129
|
-
return parseSettingsWriteResult(await this.request("/sash/settings/file", {
|
|
130
|
-
method: "PUT",
|
|
131
|
-
body: { content },
|
|
132
|
-
timeoutMs: 45_000,
|
|
133
|
-
}));
|
|
134
|
-
}
|
|
135
153
|
async listProfiles() {
|
|
136
154
|
return parseProfilesIndex(await this.request("/sash/profiles"));
|
|
137
155
|
}
|
|
156
|
+
async reorderProfiles(ids) {
|
|
157
|
+
return parseProfilesIndex(await this.request("/sash/profiles/order", { method: "PUT", body: { ids } }));
|
|
158
|
+
}
|
|
138
159
|
async addProfile(url, opts = {}) {
|
|
139
160
|
return parseProfileActionResponse(await this.request("/sash/profiles", {
|
|
140
161
|
method: "POST",
|
|
@@ -168,10 +189,10 @@ export class SashClient {
|
|
|
168
189
|
async getProfileContent(id) {
|
|
169
190
|
return parseProfileContentResponse(await this.request(`/sash/profiles/${id}/content`));
|
|
170
191
|
}
|
|
171
|
-
async writeProfileContent(id, content) {
|
|
192
|
+
async writeProfileContent(id, content, revision) {
|
|
172
193
|
return parseProfileUpdateResponse(await this.request(`/sash/profiles/${id}/content`, {
|
|
173
194
|
method: "PUT",
|
|
174
|
-
body: { content },
|
|
195
|
+
body: { content, revision },
|
|
175
196
|
timeoutMs: 30_000,
|
|
176
197
|
}));
|
|
177
198
|
}
|
package/dist/settings-service.js
CHANGED
|
@@ -1,238 +1,43 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { ProfileConflictError, } from "./profile-service.js";
|
|
3
|
-
import { sameSettings, validateSettingsCandidate } from "./settings.js";
|
|
1
|
+
import { validateSettingsCandidate } from "./settings.js";
|
|
4
2
|
export class SettingsInputError extends Error {
|
|
5
3
|
}
|
|
6
|
-
export class SettingsConflictError extends Error {
|
|
7
|
-
}
|
|
8
4
|
export class CoreUnhealthyError extends Error {
|
|
9
5
|
}
|
|
10
|
-
|
|
6
|
+
export { StateConflictError as SettingsConflictError } from "./app-state.js";
|
|
11
7
|
export class SettingsService {
|
|
12
8
|
options;
|
|
13
9
|
constructor(options) {
|
|
14
10
|
this.options = options;
|
|
15
11
|
}
|
|
16
12
|
async apply(patch) {
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
candidate = validateSettingsCandidate(mergePatch(previous, patch));
|
|
21
|
-
}
|
|
22
|
-
catch (err) {
|
|
23
|
-
throw new SettingsInputError(err.message);
|
|
24
|
-
}
|
|
25
|
-
const restartRequired = candidate.daemonPort !== previous.daemonPort;
|
|
26
|
-
const proxyChanged = candidate.systemProxy !== previous.systemProxy;
|
|
27
|
-
const coreChanged = CORE_SETTING_KEYS.some((key) => candidate[key] !== previous[key]);
|
|
28
|
-
const daemonChanged = restartRequired || candidate.daemonSecret !== previous.daemonSecret;
|
|
29
|
-
let committed = previous;
|
|
30
|
-
if (coreChanged || daemonChanged) {
|
|
31
|
-
const staged = { ...candidate, systemProxy: previous.systemProxy };
|
|
32
|
-
committed = coreChanged
|
|
33
|
-
? await this.commitCoreChange(previous, staged)
|
|
34
|
-
: await this.commitSettingsOnly(previous, staged);
|
|
35
|
-
}
|
|
36
|
-
if (proxyChanged || patch.systemProxy === false) {
|
|
37
|
-
committed = await this.commitSystemProxy(committed, candidate);
|
|
38
|
-
}
|
|
39
|
-
return { settings: committed, restartRequired };
|
|
40
|
-
}
|
|
41
|
-
async applyFileSettings(next) {
|
|
42
|
-
const current = this.options.getCommitted();
|
|
43
|
-
const patch = {};
|
|
44
|
-
if (next.mixedPort !== current.mixedPort)
|
|
45
|
-
patch.mixedPort = next.mixedPort;
|
|
46
|
-
if (next.controller !== current.controller)
|
|
47
|
-
patch.controller = next.controller;
|
|
48
|
-
if (next.secret !== current.secret)
|
|
49
|
-
patch.secret = next.secret;
|
|
50
|
-
if (next.tun !== current.tun)
|
|
51
|
-
patch.tun = next.tun;
|
|
52
|
-
if (next.allowLan !== current.allowLan)
|
|
53
|
-
patch.allowLan = next.allowLan;
|
|
54
|
-
if (next.systemProxy !== current.systemProxy)
|
|
55
|
-
patch.systemProxy = next.systemProxy;
|
|
56
|
-
if (next.daemonPort !== current.daemonPort)
|
|
57
|
-
patch.daemonPort = next.daemonPort;
|
|
58
|
-
if (next.daemonSecret !== current.daemonSecret)
|
|
59
|
-
patch.daemonSecret = next.daemonSecret;
|
|
60
|
-
return this.apply(patch);
|
|
61
|
-
}
|
|
62
|
-
async commitSettingsOnly(previous, candidate) {
|
|
63
|
-
return this.options.commit("update daemon settings", async () => {
|
|
64
|
-
this.assertCurrent(previous);
|
|
65
|
-
this.options.setRuntime(candidate);
|
|
66
|
-
try {
|
|
67
|
-
await commitManagedStateTransaction(this.options.layout, { settings: candidate }, undefined);
|
|
68
|
-
}
|
|
69
|
-
catch (err) {
|
|
70
|
-
this.options.setRuntime(previous);
|
|
71
|
-
throw err;
|
|
72
|
-
}
|
|
73
|
-
this.options.setCommitted(candidate);
|
|
74
|
-
return candidate;
|
|
75
|
-
});
|
|
76
|
-
}
|
|
77
|
-
async commitCoreChange(previous, candidate) {
|
|
78
|
-
for (let attempt = 0;; attempt += 1) {
|
|
79
|
-
const prepared = await this.options.profiles.prepareActiveConfig(candidate, previous);
|
|
80
|
-
let retryableProfileConflict;
|
|
81
|
-
let callbackEntered = false;
|
|
82
|
-
try {
|
|
83
|
-
return await this.options.commit("update settings", async () => {
|
|
84
|
-
this.assertCurrent(previous);
|
|
85
|
-
try {
|
|
86
|
-
return await this.options.profiles.withPreparedActivePublication(prepared, async (publication) => {
|
|
87
|
-
callbackEntered = true;
|
|
88
|
-
return this.commitCoreSettings(previous, candidate, publication);
|
|
89
|
-
});
|
|
90
|
-
}
|
|
91
|
-
catch (err) {
|
|
92
|
-
if (err instanceof ProfileConflictError && !callbackEntered) {
|
|
93
|
-
retryableProfileConflict = err;
|
|
94
|
-
}
|
|
95
|
-
throw err;
|
|
96
|
-
}
|
|
97
|
-
});
|
|
98
|
-
}
|
|
99
|
-
catch (err) {
|
|
100
|
-
if (err !== retryableProfileConflict || attempt >= 1)
|
|
101
|
-
throw err;
|
|
102
|
-
}
|
|
103
|
-
}
|
|
104
|
-
}
|
|
105
|
-
async commitSystemProxy(previous, candidate) {
|
|
106
|
-
if (!candidate.systemProxy) {
|
|
107
|
-
return this.options.commit("disable system proxy", async () => {
|
|
108
|
-
this.assertCurrent(previous);
|
|
109
|
-
this.options.setRuntime(candidate);
|
|
110
|
-
try {
|
|
111
|
-
if (!sameSettings(previous, candidate)) {
|
|
112
|
-
await commitManagedStateTransaction(this.options.layout, { settings: candidate }, undefined);
|
|
113
|
-
}
|
|
114
|
-
}
|
|
115
|
-
catch (err) {
|
|
116
|
-
this.options.setRuntime(previous);
|
|
117
|
-
throw err;
|
|
118
|
-
}
|
|
119
|
-
this.options.setCommitted(candidate);
|
|
120
|
-
if (this.options.lifecycle)
|
|
121
|
-
await this.options.lifecycle.reconcileSystemProxy();
|
|
122
|
-
else
|
|
123
|
-
await this.options.releaseSystemProxy?.();
|
|
124
|
-
return candidate;
|
|
125
|
-
});
|
|
126
|
-
}
|
|
127
|
-
const core = await this.options.supervisor?.status();
|
|
128
|
-
if (!core?.running || !core.healthy) {
|
|
129
|
-
throw new CoreUnhealthyError("Cannot enable system proxy: core is not healthy");
|
|
130
|
-
}
|
|
131
|
-
return this.options.commit("enable system proxy", async () => {
|
|
132
|
-
this.assertCurrent(previous);
|
|
133
|
-
this.options.setRuntime(candidate);
|
|
13
|
+
return this.options.commit("save settings", async () => {
|
|
14
|
+
const state = this.options.state.snapshot();
|
|
15
|
+
let settings;
|
|
134
16
|
try {
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
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
|
+
};
|
|
152
41
|
});
|
|
153
42
|
}
|
|
154
|
-
async commitCoreSettings(previous, candidate, publication) {
|
|
155
|
-
const wasRunning = this.options.supervisor?.isRunning() ?? false;
|
|
156
|
-
this.options.setRuntime(candidate);
|
|
157
|
-
try {
|
|
158
|
-
await commitManagedStateTransaction(this.options.layout, {
|
|
159
|
-
...(publication.index ? { index: publication.index } : {}),
|
|
160
|
-
...(publication.profile
|
|
161
|
-
? {
|
|
162
|
-
profile: {
|
|
163
|
-
id: publication.profile.id,
|
|
164
|
-
yamlText: publication.profile.yamlText,
|
|
165
|
-
},
|
|
166
|
-
}
|
|
167
|
-
: {}),
|
|
168
|
-
config: publication.config,
|
|
169
|
-
settings: candidate,
|
|
170
|
-
reloadRuntime: false,
|
|
171
|
-
applyRuntime: async () => {
|
|
172
|
-
if (!wasRunning)
|
|
173
|
-
return;
|
|
174
|
-
await this.requireLifecycle().restart();
|
|
175
|
-
},
|
|
176
|
-
}, undefined);
|
|
177
|
-
this.options.setCommitted(candidate);
|
|
178
|
-
return candidate;
|
|
179
|
-
}
|
|
180
|
-
catch (err) {
|
|
181
|
-
this.options.setRuntime(previous);
|
|
182
|
-
const rollbackErrors = [];
|
|
183
|
-
try {
|
|
184
|
-
await this.options.profiles.commitPreparedActiveReload(publication.rollback, {
|
|
185
|
-
reloadRuntime: false,
|
|
186
|
-
boundary: "already-held",
|
|
187
|
-
});
|
|
188
|
-
}
|
|
189
|
-
catch (rollback) {
|
|
190
|
-
rollbackErrors.push(`config rollback failed: ${rollback.message}`);
|
|
191
|
-
}
|
|
192
|
-
if (wasRunning) {
|
|
193
|
-
try {
|
|
194
|
-
if (this.options.supervisor?.isRunning())
|
|
195
|
-
await this.requireLifecycle().restart();
|
|
196
|
-
else
|
|
197
|
-
await this.requireLifecycle().start();
|
|
198
|
-
}
|
|
199
|
-
catch (rollback) {
|
|
200
|
-
rollbackErrors.push(`runtime rollback failed: ${rollback.message}`);
|
|
201
|
-
}
|
|
202
|
-
}
|
|
203
|
-
if (rollbackErrors.length)
|
|
204
|
-
throw new Error(`${err.message}; ${rollbackErrors.join("; ")}`, { cause: err });
|
|
205
|
-
throw err;
|
|
206
|
-
}
|
|
207
|
-
}
|
|
208
|
-
requireLifecycle() {
|
|
209
|
-
if (!this.options.lifecycle)
|
|
210
|
-
throw new Error("No runtime lifecycle is available for this settings mutation");
|
|
211
|
-
return this.options.lifecycle;
|
|
212
|
-
}
|
|
213
|
-
assertCurrent(previous) {
|
|
214
|
-
if (!sameSettings(this.options.getCommitted(), previous)) {
|
|
215
|
-
throw new SettingsConflictError("Settings changed while preparing configuration");
|
|
216
|
-
}
|
|
217
|
-
}
|
|
218
|
-
}
|
|
219
|
-
function mergePatch(previous, patch) {
|
|
220
|
-
const candidate = { ...previous };
|
|
221
|
-
if (patch.mixedPort !== undefined)
|
|
222
|
-
candidate.mixedPort = patch.mixedPort;
|
|
223
|
-
if (patch.controller !== undefined)
|
|
224
|
-
candidate.controller = patch.controller;
|
|
225
|
-
if (patch.secret !== undefined)
|
|
226
|
-
candidate.secret = patch.secret;
|
|
227
|
-
if (patch.tun !== undefined)
|
|
228
|
-
candidate.tun = patch.tun;
|
|
229
|
-
if (patch.allowLan !== undefined)
|
|
230
|
-
candidate.allowLan = patch.allowLan;
|
|
231
|
-
if (patch.systemProxy !== undefined)
|
|
232
|
-
candidate.systemProxy = patch.systemProxy;
|
|
233
|
-
if (patch.daemonPort !== undefined)
|
|
234
|
-
candidate.daemonPort = patch.daemonPort;
|
|
235
|
-
if (patch.daemonSecret !== undefined)
|
|
236
|
-
candidate.daemonSecret = patch.daemonSecret;
|
|
237
|
-
return candidate;
|
|
238
43
|
}
|