@jiayunxie/aerial 0.2.1 → 0.2.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/README.md +1 -1
- package/docs/usage.md +2 -2
- package/package.json +5 -5
- package/src/cli/args.js +28 -0
- package/src/cli/config-command.js +39 -0
- package/src/cli/disable-command.js +28 -0
- package/src/{doctor.js → cli/doctor.js} +3 -3
- package/src/cli/help.js +23 -0
- package/src/cli/index.js +92 -0
- package/src/cli/key-command.js +20 -0
- package/src/cli/login-command.js +28 -0
- package/src/{model-selection.js → cli/model-selection.js} +5 -5
- package/src/cli/output.js +75 -0
- package/src/{probe.js → cli/probe.js} +3 -3
- package/src/cli/proxy-command.js +120 -0
- package/src/cli/runtime-auth.js +21 -0
- package/src/cli/service-command.js +120 -0
- package/src/cli/setup-command.js +122 -0
- package/src/{setup-selection.js → cli/setup-selection.js} +3 -20
- package/src/cli/start-command.js +11 -0
- package/src/cli/status-command.js +26 -0
- package/src/{version.js → cli/version.js} +1 -1
- package/src/proxy/cache-policy.js +89 -0
- package/src/proxy/cache-telemetry.js +172 -0
- package/src/proxy/effort.js +120 -0
- package/src/proxy/headers.js +33 -0
- package/src/proxy/index.js +85 -0
- package/src/proxy/models.js +27 -0
- package/src/{responses-websocket.js → proxy/responses-websocket.js} +2 -2
- package/src/{server.js → proxy/server.js} +5 -5
- package/src/proxy/transport.js +55 -0
- package/src/service/health.js +54 -0
- package/src/service/index.js +38 -0
- package/src/service/lifecycle.js +301 -0
- package/src/service/platform.js +227 -0
- package/src/service/runner.js +45 -0
- package/src/service/status.js +296 -0
- package/src/service/wrapper-render.js +228 -0
- package/src/setup/backup.js +38 -0
- package/src/{setup.js → setup/clients.js} +11 -198
- package/src/setup/index.js +4 -0
- package/src/setup/restore.js +90 -0
- package/src/setup/status.js +24 -0
- package/src/setup/toml.js +41 -0
- package/src/{auth.js → shared/auth.js} +1 -1
- package/src/{config.js → shared/config.js} +2 -2
- package/src/shared/effort.js +19 -0
- package/src/shared/file-utils.js +31 -0
- package/src/{paths.js → shared/paths.js} +2 -3
- package/src/{upstream-fetch.js → upstream/fetch.js} +1 -1
- package/src/cli.js +0 -684
- package/src/copilot.js +0 -572
- package/src/service.js +0 -1182
- /package/src/{app-status.js → cli/app-status.js} +0 -0
- /package/src/{model-catalog.js → proxy/model-catalog.js} +0 -0
- /package/src/{model-utils.js → proxy/model-utils.js} +0 -0
- /package/src/{constants.js → shared/constants.js} +0 -0
- /package/src/{crypto.js → shared/crypto.js} +0 -0
- /package/src/{http-utils.js → shared/http-utils.js} +0 -0
- /package/src/{log.js → shared/log.js} +0 -0
- /package/src/{prompt-utils.js → shared/prompt-utils.js} +0 -0
- /package/src/{proxy-config.js → upstream/proxy-config.js} +0 -0
- /package/src/{socks5-bridge.js → upstream/socks5-bridge.js} +0 -0
|
@@ -0,0 +1,301 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import { githubTokenPath } from "../shared/paths.js";
|
|
3
|
+
import { loadConfig } from "../shared/config.js";
|
|
4
|
+
import { logEvent } from "../shared/log.js";
|
|
5
|
+
import { defaultHealthFetch, classifyHealth, pollForAerialUp } from "./health.js";
|
|
6
|
+
import { defaultRunCommand } from "./runner.js";
|
|
7
|
+
import { requireServiceAdapter } from "./platform.js";
|
|
8
|
+
import { aerialLogPath, stdioLogPath } from "./wrapper-render.js";
|
|
9
|
+
import { readWrapperNodePath } from "./status.js";
|
|
10
|
+
|
|
11
|
+
function tokenWarning() {
|
|
12
|
+
const tokenFile = githubTokenPath();
|
|
13
|
+
if (fs.existsSync(tokenFile)) {
|
|
14
|
+
try {
|
|
15
|
+
const data = fs.readFileSync(tokenFile, "utf8");
|
|
16
|
+
if (data && data.trim()) return undefined;
|
|
17
|
+
} catch {}
|
|
18
|
+
}
|
|
19
|
+
const envToken = process.env.AERIAL_GITHUB_TOKEN;
|
|
20
|
+
const envOnly = envToken && envToken.trim();
|
|
21
|
+
const message = envOnly
|
|
22
|
+
? "AERIAL_GITHUB_TOKEN is set in this shell only; the background service does not inherit it. Run `aerial login` to persist a service-readable GitHub token, otherwise proxy requests return 503."
|
|
23
|
+
: "GitHub token is not configured. Service will start, but proxy requests return 503 until you run `aerial login`.";
|
|
24
|
+
return { level: "warning", code: "github_token_missing", message };
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function healthFailedDiagnostics({ wrapper, probe, attempts, elapsedMs }) {
|
|
28
|
+
const out = {
|
|
29
|
+
stdioLog: stdioLogPath(),
|
|
30
|
+
aerialLog: aerialLogPath(),
|
|
31
|
+
wrapperNode: readWrapperNodePath(wrapper),
|
|
32
|
+
health: { attempts, elapsedMs }
|
|
33
|
+
};
|
|
34
|
+
if (probe && probe.error) out.health.lastError = probe.error;
|
|
35
|
+
if (probe && probe.status !== undefined) out.health.lastStatus = probe.status;
|
|
36
|
+
return out;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function commandResultBlock(key, result) {
|
|
40
|
+
return { [key]: { status: result.status, stderr: result.stderr } };
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function buildDefinitionRefreshFailure({ adapter, supervisor, config, definition }) {
|
|
44
|
+
const isManaged = supervisor === "service-managed";
|
|
45
|
+
const status = definition.info.create?.status;
|
|
46
|
+
const reason = isManaged ? "managed_definition_refresh_failed" : "foreground_definition_refresh_failed";
|
|
47
|
+
const message = isManaged
|
|
48
|
+
? `schtasks /Create failed (status ${status}) while refreshing the managed-service definition. The running service was not disturbed, but wrapper/env changes were NOT applied. Resolve the underlying schtasks error and rerun \`aerial service install\`.`
|
|
49
|
+
: `Aerial is already running in the foreground on port ${config.port}. The wrapper was rewritten but schtasks /Create failed (status ${status}), so the Task Scheduler definition was NOT refreshed. Resolve the underlying schtasks error and rerun \`aerial service install\`.`;
|
|
50
|
+
return {
|
|
51
|
+
ok: false,
|
|
52
|
+
action: "install",
|
|
53
|
+
platform: adapter.platform,
|
|
54
|
+
reason,
|
|
55
|
+
...(isManaged ? {} : { definitionUpdated: false }),
|
|
56
|
+
message,
|
|
57
|
+
warning: tokenWarning(),
|
|
58
|
+
...definition.info
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function buildRunningDefinitionResult({ adapter, supervisor, config, definition }) {
|
|
63
|
+
const managed = supervisor === "service-managed";
|
|
64
|
+
return {
|
|
65
|
+
ok: managed,
|
|
66
|
+
action: "install",
|
|
67
|
+
platform: adapter.platform,
|
|
68
|
+
...(managed
|
|
69
|
+
? {
|
|
70
|
+
note: "already running (service-managed); definition refreshed; run `aerial service restart` to apply wrapper/env changes"
|
|
71
|
+
}
|
|
72
|
+
: {
|
|
73
|
+
reason: "foreground_running",
|
|
74
|
+
message: `Aerial is already running in the foreground on port ${config.port}. The service definition has been updated, but the service was NOT started to avoid running two instances. Next step: stop the foreground process, then run \`aerial service start\`.`
|
|
75
|
+
}),
|
|
76
|
+
definitionUpdated: true,
|
|
77
|
+
warning: tokenWarning(),
|
|
78
|
+
...definition.info
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
async function confirmStarted({ action, result, config, wrapper, healthFetch, healthDeadlineMs }) {
|
|
83
|
+
if (!result.ok) return result;
|
|
84
|
+
if (process.env.AERIAL_SERVICE_DRYRUN === "1") {
|
|
85
|
+
return { ...result, health: { ok: true, attempts: 0, elapsedMs: 0, dryRun: true } };
|
|
86
|
+
}
|
|
87
|
+
const poll = await pollForAerialUp(config.host, config.port, healthFetch, healthDeadlineMs);
|
|
88
|
+
if (poll.cls.mode === "aerial_running") {
|
|
89
|
+
return { ...result, health: { ok: true, attempts: poll.attempts, elapsedMs: poll.elapsedMs } };
|
|
90
|
+
}
|
|
91
|
+
const diagnostics = healthFailedDiagnostics({
|
|
92
|
+
wrapper,
|
|
93
|
+
probe: poll.probe,
|
|
94
|
+
attempts: poll.attempts,
|
|
95
|
+
elapsedMs: poll.elapsedMs
|
|
96
|
+
});
|
|
97
|
+
if (poll.cls.mode === "port_conflict") {
|
|
98
|
+
const prefix = action === "install" ? "After install" : "After start";
|
|
99
|
+
return {
|
|
100
|
+
...result,
|
|
101
|
+
ok: false,
|
|
102
|
+
reason: "port_conflict",
|
|
103
|
+
message: `${prefix}, port ${config.port} is responding as a non-Aerial process: ${poll.cls.reason}. Free the port and rerun.`,
|
|
104
|
+
diagnostics
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
const message = action === "install"
|
|
108
|
+
? `Service definition was written and start was triggered, but /health did not become Aerial within ${poll.elapsedMs}ms (${poll.attempts} attempts). Inspect logs and rerun \`aerial service status --json\`.`
|
|
109
|
+
: `Start was triggered, but /health did not become Aerial within ${poll.elapsedMs}ms (${poll.attempts} attempts). Inspect logs and rerun \`aerial service status --json\`.`;
|
|
110
|
+
return {
|
|
111
|
+
...result,
|
|
112
|
+
ok: false,
|
|
113
|
+
reason: "health_check_failed",
|
|
114
|
+
...(action === "install" ? { definitionWritten: true } : {}),
|
|
115
|
+
startAttempted: true,
|
|
116
|
+
message,
|
|
117
|
+
diagnostics
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
async function describeRunning(adapter, host, port, healthFetch, knownState) {
|
|
122
|
+
const probe = await (healthFetch || defaultHealthFetch)(host, port);
|
|
123
|
+
const cls = classifyHealth(probe);
|
|
124
|
+
if (cls.mode !== "aerial_running") return { cls, probe };
|
|
125
|
+
const state = knownState || adapter.state();
|
|
126
|
+
const supervisor = state.installed && state.loaded ? "service-managed" : "foreground";
|
|
127
|
+
return { cls, probe, supervisor };
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
export async function serviceInstall({ run = defaultRunCommand, healthFetch, healthDeadlineMs } = {}) {
|
|
131
|
+
const ctx = { run };
|
|
132
|
+
const adapter = requireServiceAdapter(ctx, "install");
|
|
133
|
+
const config = loadConfig();
|
|
134
|
+
const { cls, supervisor } = await describeRunning(adapter, config.host, config.port, healthFetch);
|
|
135
|
+
if (cls.mode === "port_conflict") {
|
|
136
|
+
logEvent("service_install", { platform: adapter.platform, ok: false, reason: "port_conflict" });
|
|
137
|
+
return {
|
|
138
|
+
ok: false,
|
|
139
|
+
action: "install",
|
|
140
|
+
platform: adapter.platform,
|
|
141
|
+
reason: "port_conflict",
|
|
142
|
+
message: `Port ${config.port} is already in use by a non-Aerial process: ${cls.reason}. Free the port and rerun.`
|
|
143
|
+
};
|
|
144
|
+
}
|
|
145
|
+
if (cls.mode === "aerial_running") {
|
|
146
|
+
const definition = adapter.writeDefinition();
|
|
147
|
+
if (!definition.ok) {
|
|
148
|
+
const failure = buildDefinitionRefreshFailure({ adapter, supervisor, config, definition });
|
|
149
|
+
logEvent("service_install", {
|
|
150
|
+
platform: adapter.platform,
|
|
151
|
+
ok: false,
|
|
152
|
+
reason: failure.reason,
|
|
153
|
+
status: definition.info.create?.status
|
|
154
|
+
});
|
|
155
|
+
return failure;
|
|
156
|
+
}
|
|
157
|
+
const result = buildRunningDefinitionResult({ adapter, supervisor, config, definition });
|
|
158
|
+
logEvent("service_install", {
|
|
159
|
+
platform: adapter.platform,
|
|
160
|
+
ok: result.ok,
|
|
161
|
+
reason: result.reason,
|
|
162
|
+
note: supervisor === "service-managed" ? "managed_refreshed" : undefined,
|
|
163
|
+
definitionUpdated: true
|
|
164
|
+
});
|
|
165
|
+
return result;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
const definition = adapter.writeDefinition();
|
|
169
|
+
let result = {
|
|
170
|
+
ok: definition.ok,
|
|
171
|
+
action: "install",
|
|
172
|
+
platform: adapter.platform,
|
|
173
|
+
...definition.info
|
|
174
|
+
};
|
|
175
|
+
if (!definition.ok) {
|
|
176
|
+
result.reason = "create_failed";
|
|
177
|
+
} else {
|
|
178
|
+
const start = adapter.triggerStart();
|
|
179
|
+
const triggerOk = start.status === 0;
|
|
180
|
+
result = {
|
|
181
|
+
...result,
|
|
182
|
+
ok: triggerOk,
|
|
183
|
+
...commandResultBlock(adapter.startResultKey, start),
|
|
184
|
+
...(triggerOk ? {} : { reason: adapter.startFailureReason })
|
|
185
|
+
};
|
|
186
|
+
}
|
|
187
|
+
result = await confirmStarted({
|
|
188
|
+
action: "install",
|
|
189
|
+
result,
|
|
190
|
+
config,
|
|
191
|
+
wrapper: result.wrapper,
|
|
192
|
+
healthFetch,
|
|
193
|
+
healthDeadlineMs
|
|
194
|
+
});
|
|
195
|
+
result.warning = tokenWarning();
|
|
196
|
+
logEvent("service_install", { platform: adapter.platform, ok: result.ok, reason: result.reason });
|
|
197
|
+
return result;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
export async function serviceStart({ run = defaultRunCommand, healthFetch, healthDeadlineMs } = {}) {
|
|
201
|
+
const ctx = { run };
|
|
202
|
+
const adapter = requireServiceAdapter(ctx, "start");
|
|
203
|
+
const config = loadConfig();
|
|
204
|
+
const state = adapter.state();
|
|
205
|
+
if (!state.installed) {
|
|
206
|
+
return {
|
|
207
|
+
ok: false,
|
|
208
|
+
action: "start",
|
|
209
|
+
platform: adapter.platform,
|
|
210
|
+
reason: "not_installed",
|
|
211
|
+
message: "Service is not installed. Run `aerial service install` first."
|
|
212
|
+
};
|
|
213
|
+
}
|
|
214
|
+
const { cls, supervisor } = await describeRunning(adapter, config.host, config.port, healthFetch, state);
|
|
215
|
+
if (cls.mode === "port_conflict") {
|
|
216
|
+
return {
|
|
217
|
+
ok: false,
|
|
218
|
+
action: "start",
|
|
219
|
+
platform: adapter.platform,
|
|
220
|
+
reason: "port_conflict",
|
|
221
|
+
message: `Port ${config.port} is already in use by a non-Aerial process: ${cls.reason}. Free the port and rerun.`
|
|
222
|
+
};
|
|
223
|
+
}
|
|
224
|
+
if (cls.mode === "aerial_running" && supervisor === "foreground") {
|
|
225
|
+
return {
|
|
226
|
+
ok: false,
|
|
227
|
+
action: "start",
|
|
228
|
+
platform: adapter.platform,
|
|
229
|
+
reason: "foreground_running",
|
|
230
|
+
message: `Aerial is already running in the foreground on port ${config.port}. Stop the foreground process before starting the service.`
|
|
231
|
+
};
|
|
232
|
+
}
|
|
233
|
+
if (cls.mode === "aerial_running" && supervisor === "service-managed") {
|
|
234
|
+
return {
|
|
235
|
+
ok: true,
|
|
236
|
+
action: "start",
|
|
237
|
+
platform: adapter.platform,
|
|
238
|
+
note: "already running (service-managed)",
|
|
239
|
+
warning: tokenWarning()
|
|
240
|
+
};
|
|
241
|
+
}
|
|
242
|
+
const r = adapter.triggerStart();
|
|
243
|
+
const triggerOk = r.status === 0;
|
|
244
|
+
let result = {
|
|
245
|
+
ok: triggerOk,
|
|
246
|
+
action: "start",
|
|
247
|
+
platform: adapter.platform,
|
|
248
|
+
status: r.status,
|
|
249
|
+
stderr: r.stderr,
|
|
250
|
+
warning: tokenWarning(),
|
|
251
|
+
...(triggerOk ? {} : { reason: adapter.startFailureReason })
|
|
252
|
+
};
|
|
253
|
+
if (!triggerOk) {
|
|
254
|
+
logEvent("service_start", { platform: adapter.platform, status: r.status, reason: result.reason });
|
|
255
|
+
return result;
|
|
256
|
+
}
|
|
257
|
+
result = await confirmStarted({
|
|
258
|
+
action: "start",
|
|
259
|
+
result,
|
|
260
|
+
config,
|
|
261
|
+
wrapper: adapter.wrapperPath(),
|
|
262
|
+
healthFetch,
|
|
263
|
+
healthDeadlineMs
|
|
264
|
+
});
|
|
265
|
+
logEvent("service_start", { platform: adapter.platform, status: r.status, ok: result.ok, reason: result.reason, dryRun: result.health?.dryRun });
|
|
266
|
+
return result;
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
export function serviceStop({ run = defaultRunCommand } = {}) {
|
|
270
|
+
const ctx = { run };
|
|
271
|
+
const adapter = requireServiceAdapter(ctx, "stop");
|
|
272
|
+
const state = adapter.state();
|
|
273
|
+
if (!state.installed) {
|
|
274
|
+
return { ok: true, action: "stop", platform: adapter.platform, note: "not installed" };
|
|
275
|
+
}
|
|
276
|
+
if (!state.loaded) {
|
|
277
|
+
return { ok: true, action: "stop", platform: adapter.platform, note: "not running" };
|
|
278
|
+
}
|
|
279
|
+
const r = adapter.triggerStop();
|
|
280
|
+
logEvent("service_stop", { platform: adapter.platform, status: r.status });
|
|
281
|
+
return { ok: r.status === 0, action: "stop", platform: adapter.platform, status: r.status, stderr: r.stderr };
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
export async function serviceRestart(opts = {}) {
|
|
285
|
+
const stop = serviceStop(opts);
|
|
286
|
+
if (!stop.ok) {
|
|
287
|
+
return { ok: false, action: "restart", platform: process.platform, stop, reason: "stop_failed" };
|
|
288
|
+
}
|
|
289
|
+
const start = await serviceStart(opts);
|
|
290
|
+
return { ok: start.ok, action: "restart", platform: process.platform, stop, start, warning: start.warning };
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
export function serviceUninstall({ run = defaultRunCommand } = {}) {
|
|
294
|
+
const ctx = { run };
|
|
295
|
+
const adapter = requireServiceAdapter(ctx, "uninstall");
|
|
296
|
+
const state = adapter.state();
|
|
297
|
+
if (!state.installed) {
|
|
298
|
+
return { ok: true, action: "uninstall", platform: adapter.platform, note: "no service installed" };
|
|
299
|
+
}
|
|
300
|
+
return adapter.uninstall(state);
|
|
301
|
+
}
|
|
@@ -0,0 +1,227 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import { loadConfig } from "../shared/config.js";
|
|
3
|
+
import { logEvent } from "../shared/log.js";
|
|
4
|
+
import { atomicWriteFile } from "../shared/file-utils.js";
|
|
5
|
+
import {
|
|
6
|
+
SERVICE_LABEL,
|
|
7
|
+
WIN_TASK_NAME,
|
|
8
|
+
aerialLogPath,
|
|
9
|
+
buildSchtasksArgs,
|
|
10
|
+
buildSchtasksCreateArgs,
|
|
11
|
+
cliEntry,
|
|
12
|
+
darwinWrapperPath,
|
|
13
|
+
explicitConfigDir,
|
|
14
|
+
nodeBinary,
|
|
15
|
+
plistPath,
|
|
16
|
+
renderDarwinWrapper,
|
|
17
|
+
renderPlist,
|
|
18
|
+
renderWindowsWrapper,
|
|
19
|
+
stdioLogPath,
|
|
20
|
+
uidString,
|
|
21
|
+
winWrapperPath,
|
|
22
|
+
wrapperLogConfig
|
|
23
|
+
} from "./wrapper-render.js";
|
|
24
|
+
|
|
25
|
+
export function isUnsupportedPlatform() {
|
|
26
|
+
return process.platform !== "darwin" && process.platform !== "win32";
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export function unsupportedError(action) {
|
|
30
|
+
const platform = process.platform;
|
|
31
|
+
return new Error(`aerial service ${action}: unsupported platform (${platform}). Service management is implemented for macOS (launchd) and Windows (Task Scheduler). On ${platform}, run \`aerial start\` directly or wrap it in your own init system.`);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function darwinServiceState(ctx) {
|
|
35
|
+
const r = ctx.run("launchctl", ["list", SERVICE_LABEL]);
|
|
36
|
+
const installed = fs.existsSync(plistPath());
|
|
37
|
+
if (!installed) return { installed: false, loaded: false };
|
|
38
|
+
if (r.status !== 0) return { installed: true, loaded: false };
|
|
39
|
+
const pidMatch = /"PID"\s*=\s*(\d+)/.exec(r.stdout);
|
|
40
|
+
const lastExitMatch = /"LastExitStatus"\s*=\s*(-?\d+)/.exec(r.stdout);
|
|
41
|
+
return {
|
|
42
|
+
installed: true,
|
|
43
|
+
loaded: true,
|
|
44
|
+
pid: pidMatch ? Number(pidMatch[1]) : undefined,
|
|
45
|
+
lastExitStatus: lastExitMatch ? Number(lastExitMatch[1]) : undefined
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function windowsServiceState(ctx) {
|
|
50
|
+
const r = ctx.run("schtasks.exe", buildSchtasksArgs("query"));
|
|
51
|
+
if (r.status !== 0) return { installed: false, loaded: false };
|
|
52
|
+
const statusMatch = /Status:\s*(\S+)/i.exec(r.stdout);
|
|
53
|
+
const status = statusMatch ? statusMatch[1].trim() : undefined;
|
|
54
|
+
return { installed: true, loaded: status === "Running", status };
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function darwinWriteDefinition() {
|
|
58
|
+
const wrapper = darwinWrapperPath();
|
|
59
|
+
const config = loadConfig();
|
|
60
|
+
const logCfg = wrapperLogConfig();
|
|
61
|
+
const wrapperContent = renderDarwinWrapper({
|
|
62
|
+
nodePath: nodeBinary(),
|
|
63
|
+
cliPath: cliEntry(),
|
|
64
|
+
host: config.host,
|
|
65
|
+
port: config.port,
|
|
66
|
+
stdioLog: stdioLogPath(),
|
|
67
|
+
aerialLog: aerialLogPath(),
|
|
68
|
+
configDir: explicitConfigDir(),
|
|
69
|
+
maxBytes: logCfg.maxBytes,
|
|
70
|
+
backups: logCfg.backups
|
|
71
|
+
});
|
|
72
|
+
atomicWriteFile(wrapper, wrapperContent, { mode: 0o755 });
|
|
73
|
+
const file = plistPath();
|
|
74
|
+
const plistContent = renderPlist({ wrapperPath: wrapper });
|
|
75
|
+
atomicWriteFile(file, plistContent, { mode: 0o644 });
|
|
76
|
+
return { file, wrapper };
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function darwinBootstrap(ctx) {
|
|
80
|
+
const file = plistPath();
|
|
81
|
+
const existing = ctx.run("launchctl", ["list", SERVICE_LABEL]);
|
|
82
|
+
if (existing.status === 0) {
|
|
83
|
+
ctx.run("launchctl", ["bootout", `gui/${uidString()}`, file], { stdio: "ignore" });
|
|
84
|
+
}
|
|
85
|
+
return ctx.run("launchctl", ["bootstrap", `gui/${uidString()}`, file]);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function darwinBootout(ctx) {
|
|
89
|
+
const file = plistPath();
|
|
90
|
+
return ctx.run("launchctl", ["bootout", `gui/${uidString()}`, file]);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function windowsWriteDefinition(ctx) {
|
|
94
|
+
const wrapper = winWrapperPath();
|
|
95
|
+
const config = loadConfig();
|
|
96
|
+
const logCfg = wrapperLogConfig();
|
|
97
|
+
const wrapperContent = renderWindowsWrapper({
|
|
98
|
+
nodePath: nodeBinary(),
|
|
99
|
+
cliPath: cliEntry(),
|
|
100
|
+
host: config.host,
|
|
101
|
+
port: config.port,
|
|
102
|
+
stdioLog: stdioLogPath(),
|
|
103
|
+
aerialLog: aerialLogPath(),
|
|
104
|
+
configDir: explicitConfigDir(),
|
|
105
|
+
maxBytes: logCfg.maxBytes,
|
|
106
|
+
backups: logCfg.backups
|
|
107
|
+
});
|
|
108
|
+
atomicWriteFile(wrapper, wrapperContent);
|
|
109
|
+
const args = buildSchtasksCreateArgs({ wrapperPath: wrapper });
|
|
110
|
+
const create = ctx.run("schtasks.exe", args);
|
|
111
|
+
return { wrapper, create };
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
export function removeFileIfExists(file) {
|
|
115
|
+
if (!fs.existsSync(file)) return false;
|
|
116
|
+
try {
|
|
117
|
+
fs.unlinkSync(file);
|
|
118
|
+
return true;
|
|
119
|
+
} catch {
|
|
120
|
+
return false;
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
function darwinUninstall(ctx, state) {
|
|
125
|
+
const file = plistPath();
|
|
126
|
+
const wrapper = darwinWrapperPath();
|
|
127
|
+
if (state.loaded) {
|
|
128
|
+
const bootout = darwinBootout(ctx);
|
|
129
|
+
if (bootout.status !== 0) {
|
|
130
|
+
logEvent("service_uninstall", { platform: "darwin", ok: false, reason: "bootout_failed", status: bootout.status });
|
|
131
|
+
return {
|
|
132
|
+
ok: false,
|
|
133
|
+
action: "uninstall",
|
|
134
|
+
platform: "darwin",
|
|
135
|
+
reason: "bootout_failed",
|
|
136
|
+
file,
|
|
137
|
+
wrapper,
|
|
138
|
+
bootout: { status: bootout.status, stderr: bootout.stderr },
|
|
139
|
+
message: `launchctl bootout failed (status ${bootout.status}). Service is still loaded; plist and wrapper were preserved. Retry with \`aerial service uninstall\`.`
|
|
140
|
+
};
|
|
141
|
+
}
|
|
142
|
+
removeFileIfExists(file);
|
|
143
|
+
removeFileIfExists(wrapper);
|
|
144
|
+
logEvent("service_uninstall", { platform: "darwin", ok: true });
|
|
145
|
+
return { ok: true, action: "uninstall", platform: "darwin", file, wrapper, bootout: { status: bootout.status, stderr: bootout.stderr } };
|
|
146
|
+
}
|
|
147
|
+
removeFileIfExists(file);
|
|
148
|
+
removeFileIfExists(wrapper);
|
|
149
|
+
logEvent("service_uninstall", { platform: "darwin", ok: true });
|
|
150
|
+
return { ok: true, action: "uninstall", platform: "darwin", file, wrapper, bootout: { status: 0, skipped: "not_loaded" } };
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
function windowsUninstall(ctx, state) {
|
|
154
|
+
if (state.loaded) {
|
|
155
|
+
ctx.run("schtasks.exe", buildSchtasksArgs("end"));
|
|
156
|
+
}
|
|
157
|
+
const del = ctx.run("schtasks.exe", buildSchtasksArgs("delete"));
|
|
158
|
+
const wrapper = winWrapperPath();
|
|
159
|
+
const wrapperRemoved = del.status === 0 ? removeFileIfExists(wrapper) : false;
|
|
160
|
+
logEvent("service_uninstall", { platform: "win32", ok: del.status === 0 });
|
|
161
|
+
return {
|
|
162
|
+
ok: del.status === 0,
|
|
163
|
+
action: "uninstall",
|
|
164
|
+
platform: "win32",
|
|
165
|
+
taskName: WIN_TASK_NAME,
|
|
166
|
+
wrapper,
|
|
167
|
+
wrapperRemoved,
|
|
168
|
+
delete: { status: del.status, stderr: del.stderr },
|
|
169
|
+
...(del.status === 0 ? {} : { reason: "delete_failed", message: `schtasks /Delete failed (status ${del.status}). Task and wrapper were preserved. Retry with \`aerial service uninstall\`.` })
|
|
170
|
+
};
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
export function serviceAdapter(ctx) {
|
|
174
|
+
if (process.platform === "darwin") {
|
|
175
|
+
return {
|
|
176
|
+
platform: "darwin",
|
|
177
|
+
wrapperPath: darwinWrapperPath,
|
|
178
|
+
state: () => darwinServiceState(ctx),
|
|
179
|
+
writeDefinition: () => {
|
|
180
|
+
const written = darwinWriteDefinition();
|
|
181
|
+
return {
|
|
182
|
+
ok: true,
|
|
183
|
+
info: { file: written.file, wrapper: written.wrapper, label: SERVICE_LABEL }
|
|
184
|
+
};
|
|
185
|
+
},
|
|
186
|
+
triggerStart: () => darwinBootstrap(ctx),
|
|
187
|
+
triggerStop: () => darwinBootout(ctx),
|
|
188
|
+
startFailureReason: "bootstrap_failed",
|
|
189
|
+
startResultKey: "bootstrap",
|
|
190
|
+
uninstall: (state) => darwinUninstall(ctx, state)
|
|
191
|
+
};
|
|
192
|
+
}
|
|
193
|
+
if (process.platform === "win32") {
|
|
194
|
+
return {
|
|
195
|
+
platform: "win32",
|
|
196
|
+
wrapperPath: winWrapperPath,
|
|
197
|
+
state: () => windowsServiceState(ctx),
|
|
198
|
+
writeDefinition: () => {
|
|
199
|
+
const written = windowsWriteDefinition(ctx);
|
|
200
|
+
const info = {
|
|
201
|
+
taskName: WIN_TASK_NAME,
|
|
202
|
+
wrapper: written.wrapper,
|
|
203
|
+
create: { status: written.create.status, stderr: written.create.stderr }
|
|
204
|
+
};
|
|
205
|
+
return { ok: written.create.status === 0, info };
|
|
206
|
+
},
|
|
207
|
+
triggerStart: () => ctx.run("schtasks.exe", buildSchtasksArgs("run")),
|
|
208
|
+
triggerStop: () => ctx.run("schtasks.exe", buildSchtasksArgs("end")),
|
|
209
|
+
startFailureReason: "run_failed",
|
|
210
|
+
startResultKey: "run",
|
|
211
|
+
uninstall: (state) => windowsUninstall(ctx, state)
|
|
212
|
+
};
|
|
213
|
+
}
|
|
214
|
+
return undefined;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
export function serviceState(ctx) {
|
|
218
|
+
const adapter = serviceAdapter(ctx);
|
|
219
|
+
if (adapter) return adapter.state();
|
|
220
|
+
return { installed: false, loaded: false, reason: "unsupported_platform" };
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
export function requireServiceAdapter(ctx, action) {
|
|
224
|
+
const adapter = serviceAdapter(ctx);
|
|
225
|
+
if (!adapter) throw unsupportedError(action);
|
|
226
|
+
return adapter;
|
|
227
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { spawnSync } from "node:child_process";
|
|
2
|
+
|
|
3
|
+
export function defaultRunCommand(file, args, opts = {}) {
|
|
4
|
+
if (process.env.AERIAL_SERVICE_DRYRUN === "1") {
|
|
5
|
+
const installed = process.env.AERIAL_SERVICE_DRYRUN_INSTALLED === "1";
|
|
6
|
+
const fail = process.env.AERIAL_SERVICE_DRYRUN_FAIL || "";
|
|
7
|
+
if (file === "schtasks.exe" && Array.isArray(args)) {
|
|
8
|
+
if (args.includes("/Query")) {
|
|
9
|
+
if (installed) {
|
|
10
|
+
return { status: 0, signal: undefined, stdout: "TaskName: \\AerialLocalProxy\r\nStatus: Running", stderr: "", error: undefined, dryRun: true };
|
|
11
|
+
}
|
|
12
|
+
return { status: 1, signal: undefined, stdout: "", stderr: "(dryrun) task not registered", error: undefined, dryRun: true };
|
|
13
|
+
}
|
|
14
|
+
if (args.includes("/Delete") && fail === "delete") {
|
|
15
|
+
return { status: 9, signal: undefined, stdout: "", stderr: "ERROR: Access is denied.", error: undefined, dryRun: true };
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
if (file === "launchctl" && Array.isArray(args)) {
|
|
19
|
+
if (args[0] === "list") {
|
|
20
|
+
if (installed) {
|
|
21
|
+
return { status: 0, signal: undefined, stdout: '{\n\t"PID" = 1234;\n\t"LastExitStatus" = 0;\n};', stderr: "", error: undefined, dryRun: true };
|
|
22
|
+
}
|
|
23
|
+
return { status: 1, signal: undefined, stdout: "", stderr: "(dryrun) service not loaded", error: undefined, dryRun: true };
|
|
24
|
+
}
|
|
25
|
+
if (args[0] === "bootout" && fail === "bootout") {
|
|
26
|
+
return { status: 9216, signal: undefined, stdout: "", stderr: "Boot-out failed: 5: Input/output error", error: undefined, dryRun: true };
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
return { status: 0, signal: undefined, stdout: "", stderr: "", error: undefined, dryRun: true };
|
|
30
|
+
}
|
|
31
|
+
const res = spawnSync(file, args, {
|
|
32
|
+
stdio: opts.stdio || "pipe",
|
|
33
|
+
encoding: "utf8",
|
|
34
|
+
timeout: opts.timeout || 15000,
|
|
35
|
+
env: opts.env || process.env,
|
|
36
|
+
windowsHide: true
|
|
37
|
+
});
|
|
38
|
+
return {
|
|
39
|
+
status: res.status,
|
|
40
|
+
signal: res.signal,
|
|
41
|
+
stdout: res.stdout || "",
|
|
42
|
+
stderr: res.stderr || "",
|
|
43
|
+
error: res.error
|
|
44
|
+
};
|
|
45
|
+
}
|