@openparachute/hub 0.6.2 → 0.6.3-rc.1
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/package.json +1 -1
- package/src/__tests__/api-modules-ops.test.ts +359 -3
- package/src/__tests__/api-modules.test.ts +54 -0
- package/src/__tests__/hub-unit.test.ts +574 -0
- package/src/__tests__/init.test.ts +219 -2
- package/src/__tests__/lifecycle.test.ts +423 -0
- package/src/__tests__/managed-unit.test.ts +575 -0
- package/src/__tests__/module-ops-client.test.ts +556 -0
- package/src/__tests__/port-probe.test.ts +23 -0
- package/src/__tests__/setup-wizard.test.ts +130 -0
- package/src/__tests__/status-supervisor.test.ts +569 -0
- package/src/__tests__/supervisor.test.ts +471 -6
- package/src/api-modules-ops.ts +221 -0
- package/src/api-modules.ts +18 -2
- package/src/cli.ts +14 -4
- package/src/cloudflare/connector-service.ts +117 -322
- package/src/commands/init.ts +225 -12
- package/src/commands/lifecycle.ts +366 -38
- package/src/commands/serve-boot.ts +71 -25
- package/src/commands/status.ts +596 -49
- package/src/hub-server.ts +11 -0
- package/src/hub-unit.ts +735 -0
- package/src/managed-unit.ts +674 -0
- package/src/module-ops-client.ts +457 -0
- package/src/port-probe.ts +50 -0
- package/src/setup-wizard.ts +80 -1
- package/src/supervisor.ts +360 -14
|
@@ -1,6 +1,18 @@
|
|
|
1
|
-
import {
|
|
2
|
-
|
|
3
|
-
|
|
1
|
+
import {
|
|
2
|
+
type ManagedUnit,
|
|
3
|
+
type ManagedUnitDeps,
|
|
4
|
+
type ManagedUnitInstallResult,
|
|
5
|
+
type ManagedUnitMessages,
|
|
6
|
+
type ManagedUnitRemoveResult,
|
|
7
|
+
type ServiceCommandResult,
|
|
8
|
+
defaultManagedUnitDeps,
|
|
9
|
+
installManagedUnit,
|
|
10
|
+
launchdPlistPathForLabel,
|
|
11
|
+
removeManagedUnit,
|
|
12
|
+
renderManagedLaunchdPlist,
|
|
13
|
+
renderManagedSystemdUnit,
|
|
14
|
+
systemdUnitPathForName,
|
|
15
|
+
} from "../managed-unit.ts";
|
|
4
16
|
|
|
5
17
|
/**
|
|
6
18
|
* Reboot-persistent cloudflared connector.
|
|
@@ -25,10 +37,20 @@ import { dirname, join } from "node:path";
|
|
|
25
37
|
* `/etc/systemd/system/parachute-cloudflared-<tunnelName>.service`,
|
|
26
38
|
* `systemctl enable --now`. No linger needed — system units run on boot.
|
|
27
39
|
*
|
|
28
|
-
*
|
|
29
|
-
*
|
|
30
|
-
*
|
|
31
|
-
*
|
|
40
|
+
* As of Phase 2b of the hub-as-supervisor unification, the per-platform
|
|
41
|
+
* install/remove/render machinery lives in `src/managed-unit.ts` as a reusable
|
|
42
|
+
* `ManagedUnit` abstraction (so the hub unit can reuse it — design §4.1). This
|
|
43
|
+
* module is now a thin connector-specific layer over that machinery: it builds
|
|
44
|
+
* the connector's `ManagedUnit` descriptor (empty env, no crash-loop ceiling —
|
|
45
|
+
* which keeps its rendered output BYTE-IDENTICAL to the pre-extraction code, the
|
|
46
|
+
* hard constraint since the connector is live on production) and supplies the
|
|
47
|
+
* connector-flavored install/remove messages. The public exports below keep
|
|
48
|
+
* their original signatures so `expose-cloudflare.ts` and the connector tests
|
|
49
|
+
* need no behavioral change.
|
|
50
|
+
*
|
|
51
|
+
* Everything is behind an injectable deps seam (re-exported `ConnectorServiceDeps`
|
|
52
|
+
* = the generalized `ManagedUnitDeps`) so tests drive the install/remove without
|
|
53
|
+
* touching real launchctl/systemctl or the operator's home directory.
|
|
32
54
|
*
|
|
33
55
|
* Service name keyed by the same per-host tunnel name the 0.6.1 work derives
|
|
34
56
|
* (`deriveTunnelName`), so install / remove always target the connector for
|
|
@@ -36,74 +58,23 @@ import { dirname, join } from "node:path";
|
|
|
36
58
|
*/
|
|
37
59
|
|
|
38
60
|
/** Synchronous command result from the injected service runner. */
|
|
39
|
-
export
|
|
40
|
-
code: number;
|
|
41
|
-
stdout: string;
|
|
42
|
-
stderr: string;
|
|
43
|
-
}
|
|
61
|
+
export type { ServiceCommandResult };
|
|
44
62
|
|
|
45
63
|
/**
|
|
46
|
-
* Injectable side-effect seam for the connector-service module.
|
|
47
|
-
*
|
|
48
|
-
*
|
|
49
|
-
* command sequence without a live launchctl/systemctl.
|
|
64
|
+
* Injectable side-effect seam for the connector-service module. This is the
|
|
65
|
+
* generalized `ManagedUnitDeps` re-exported under the connector's historical
|
|
66
|
+
* name so `expose-cloudflare.ts` + the connector tests are unchanged.
|
|
50
67
|
*/
|
|
51
|
-
export
|
|
52
|
-
/** `process.platform`. */
|
|
53
|
-
platform: NodeJS.Platform;
|
|
54
|
-
/**
|
|
55
|
-
* Effective uid. Linux uses `0 === root` to pick a system vs user systemd
|
|
56
|
-
* unit. `undefined` (Windows / platforms without getuid) → treated as
|
|
57
|
-
* non-root. macOS ignores this (LaunchAgents are always per-user).
|
|
58
|
-
*/
|
|
59
|
-
getuid: () => number | undefined;
|
|
60
|
-
/** `$HOME`. */
|
|
61
|
-
homeDir: () => string;
|
|
62
|
-
/** Username for the linger call + systemd unit `User=` (system unit). */
|
|
63
|
-
userName: () => string;
|
|
64
|
-
/** Resolve a binary to an absolute path (launchd/systemd don't inherit PATH). */
|
|
65
|
-
which: (binary: string) => string | null;
|
|
66
|
-
/** Run launchctl/systemctl/loginctl synchronously. */
|
|
67
|
-
run: (cmd: readonly string[]) => ServiceCommandResult;
|
|
68
|
-
/** Write a service file (creates parent dirs). */
|
|
69
|
-
writeFile: (path: string, content: string) => void;
|
|
70
|
-
/** Remove a service file if present (no-op when absent). */
|
|
71
|
-
removeFile: (path: string) => void;
|
|
72
|
-
/** Read a service file, or undefined when absent. */
|
|
73
|
-
readFile: (path: string) => string | undefined;
|
|
74
|
-
/** True when the path exists. */
|
|
75
|
-
exists: (path: string) => boolean;
|
|
76
|
-
}
|
|
68
|
+
export type ConnectorServiceDeps = ManagedUnitDeps;
|
|
77
69
|
|
|
78
|
-
export const defaultServiceDeps: ConnectorServiceDeps =
|
|
79
|
-
platform: process.platform,
|
|
80
|
-
getuid: () => (typeof process.getuid === "function" ? process.getuid() : undefined),
|
|
81
|
-
homeDir: () => homedir(),
|
|
82
|
-
userName: () => process.env.USER ?? process.env.LOGNAME ?? process.env.USERNAME ?? "",
|
|
83
|
-
which: (binary) => Bun.which(binary),
|
|
84
|
-
run: (cmd) => {
|
|
85
|
-
const proc = Bun.spawnSync([...cmd], { env: process.env });
|
|
86
|
-
return {
|
|
87
|
-
code: proc.exitCode ?? 1,
|
|
88
|
-
stdout: proc.stdout?.toString() ?? "",
|
|
89
|
-
stderr: proc.stderr?.toString() ?? "",
|
|
90
|
-
};
|
|
91
|
-
},
|
|
92
|
-
writeFile: (path, content) => {
|
|
93
|
-
mkdirSync(dirname(path), { recursive: true });
|
|
94
|
-
writeFileSync(path, content);
|
|
95
|
-
},
|
|
96
|
-
removeFile: (path) => {
|
|
97
|
-
if (existsSync(path)) rmSync(path, { force: true });
|
|
98
|
-
},
|
|
99
|
-
readFile: (path) => (existsSync(path) ? readFileSync(path, "utf8") : undefined),
|
|
100
|
-
exists: (path) => existsSync(path),
|
|
101
|
-
};
|
|
70
|
+
export const defaultServiceDeps: ConnectorServiceDeps = defaultManagedUnitDeps;
|
|
102
71
|
|
|
103
72
|
/** Reverse-DNS prefix for the launchd label + plist filename. */
|
|
104
73
|
const LAUNCHD_LABEL_PREFIX = "computer.parachute.cloudflared";
|
|
105
74
|
/** systemd unit name prefix. */
|
|
106
75
|
const SYSTEMD_UNIT_PREFIX = "parachute-cloudflared-";
|
|
76
|
+
/** Provenance comment baked into every rendered connector unit file. */
|
|
77
|
+
const CONNECTOR_HEADER = "Generated by parachute expose public --cloudflare — do not edit by hand.";
|
|
107
78
|
|
|
108
79
|
/** launchd label for a tunnel (also the plist basename, minus `.plist`). */
|
|
109
80
|
export function launchdLabel(tunnelName: string): string {
|
|
@@ -112,7 +83,7 @@ export function launchdLabel(tunnelName: string): string {
|
|
|
112
83
|
|
|
113
84
|
/** launchd plist path under the user's LaunchAgents dir. */
|
|
114
85
|
export function launchdPlistPath(tunnelName: string, home: string): string {
|
|
115
|
-
return
|
|
86
|
+
return launchdPlistPathForLabel(launchdLabel(tunnelName), home);
|
|
116
87
|
}
|
|
117
88
|
|
|
118
89
|
/** systemd unit name (with `.service` suffix). */
|
|
@@ -122,23 +93,37 @@ export function systemdUnitName(tunnelName: string): string {
|
|
|
122
93
|
|
|
123
94
|
/** systemd unit path — user-level under $HOME, system-level under /etc. */
|
|
124
95
|
export function systemdUnitPath(tunnelName: string, home: string, root: boolean): string {
|
|
125
|
-
return root
|
|
126
|
-
? join("/etc/systemd/system", systemdUnitName(tunnelName))
|
|
127
|
-
: join(home, ".config", "systemd", "user", systemdUnitName(tunnelName));
|
|
96
|
+
return systemdUnitPathForName(systemdUnitName(tunnelName), home, root);
|
|
128
97
|
}
|
|
129
98
|
|
|
130
|
-
/**
|
|
131
|
-
|
|
132
|
-
|
|
99
|
+
/**
|
|
100
|
+
* Build the connector's `ManagedUnit` descriptor. Empty `env` + no `crashLoop`
|
|
101
|
+
* are what keep the rendered output byte-identical to the pre-extraction code.
|
|
102
|
+
* `runAsInvokingUserOnSystemUnit: true` reproduces the connector's historical
|
|
103
|
+
* `User=` pin on the root/system unit.
|
|
104
|
+
*/
|
|
105
|
+
function connectorUnit(opts: {
|
|
106
|
+
tunnelName: string;
|
|
107
|
+
cloudflaredPath: string;
|
|
108
|
+
configPath: string;
|
|
109
|
+
logPath: string;
|
|
110
|
+
}): ManagedUnit {
|
|
111
|
+
return {
|
|
112
|
+
launchdLabel: launchdLabel(opts.tunnelName),
|
|
113
|
+
systemdUnitName: systemdUnitName(opts.tunnelName),
|
|
114
|
+
headerComment: CONNECTOR_HEADER,
|
|
115
|
+
systemdDescription: `Parachute Cloudflare connector (${opts.tunnelName})`,
|
|
116
|
+
execStart: [opts.cloudflaredPath, "tunnel", "--config", opts.configPath, "run"],
|
|
117
|
+
env: {},
|
|
118
|
+
logPath: opts.logPath,
|
|
119
|
+
runAsInvokingUserOnSystemUnit: true,
|
|
120
|
+
};
|
|
133
121
|
}
|
|
134
122
|
|
|
135
123
|
/**
|
|
136
|
-
* Render the launchd LaunchAgent plist.
|
|
137
|
-
*
|
|
138
|
-
*
|
|
139
|
-
* because launchd does not search `$PATH`. Logs go to the same per-tunnel log
|
|
140
|
-
* file the transient spawn used, so `parachute status`/the operator find them
|
|
141
|
-
* in one place.
|
|
124
|
+
* Render the launchd LaunchAgent plist. Thin wrapper over the generalized
|
|
125
|
+
* renderer with the connector's descriptor — preserved signature so existing
|
|
126
|
+
* callers/tests are unchanged.
|
|
142
127
|
*/
|
|
143
128
|
export function renderLaunchdPlist(opts: {
|
|
144
129
|
tunnelName: string;
|
|
@@ -146,39 +131,13 @@ export function renderLaunchdPlist(opts: {
|
|
|
146
131
|
configPath: string;
|
|
147
132
|
logPath: string;
|
|
148
133
|
}): string {
|
|
149
|
-
|
|
150
|
-
const args = [cloudflaredPath, "tunnel", "--config", configPath, "run"];
|
|
151
|
-
const argXml = args.map((a) => ` <string>${plistEscape(a)}</string>`).join("\n");
|
|
152
|
-
return `<?xml version="1.0" encoding="UTF-8"?>
|
|
153
|
-
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
|
154
|
-
<!-- Generated by parachute expose public --cloudflare — do not edit by hand. -->
|
|
155
|
-
<plist version="1.0">
|
|
156
|
-
<dict>
|
|
157
|
-
<key>Label</key>
|
|
158
|
-
<string>${plistEscape(launchdLabel(tunnelName))}</string>
|
|
159
|
-
<key>ProgramArguments</key>
|
|
160
|
-
<array>
|
|
161
|
-
${argXml}
|
|
162
|
-
</array>
|
|
163
|
-
<key>RunAtLoad</key>
|
|
164
|
-
<true/>
|
|
165
|
-
<key>KeepAlive</key>
|
|
166
|
-
<true/>
|
|
167
|
-
<key>StandardOutPath</key>
|
|
168
|
-
<string>${plistEscape(logPath)}</string>
|
|
169
|
-
<key>StandardErrorPath</key>
|
|
170
|
-
<string>${plistEscape(logPath)}</string>
|
|
171
|
-
</dict>
|
|
172
|
-
</plist>
|
|
173
|
-
`;
|
|
134
|
+
return renderManagedLaunchdPlist(connectorUnit(opts));
|
|
174
135
|
}
|
|
175
136
|
|
|
176
137
|
/**
|
|
177
|
-
* Render a systemd unit.
|
|
178
|
-
* `
|
|
179
|
-
*
|
|
180
|
-
* as root unnecessarily when we know the invoking user. `ExecStart` uses the
|
|
181
|
-
* resolved absolute `cloudflaredPath` (systemd doesn't search a login `$PATH`).
|
|
138
|
+
* Render a systemd unit. Thin wrapper over the generalized renderer — preserved
|
|
139
|
+
* signature (`{ root, userName }` threaded through) so existing callers/tests
|
|
140
|
+
* are unchanged.
|
|
182
141
|
*/
|
|
183
142
|
export function renderSystemdUnit(opts: {
|
|
184
143
|
tunnelName: string;
|
|
@@ -188,25 +147,8 @@ export function renderSystemdUnit(opts: {
|
|
|
188
147
|
root: boolean;
|
|
189
148
|
userName: string;
|
|
190
149
|
}): string {
|
|
191
|
-
const {
|
|
192
|
-
|
|
193
|
-
const userLine = root && userName ? `User=${userName}\n` : "";
|
|
194
|
-
const wantedBy = root ? "multi-user.target" : "default.target";
|
|
195
|
-
return `# Generated by parachute expose public --cloudflare — do not edit by hand.
|
|
196
|
-
[Unit]
|
|
197
|
-
Description=Parachute Cloudflare connector (${tunnelName})
|
|
198
|
-
After=network-online.target
|
|
199
|
-
Wants=network-online.target
|
|
200
|
-
|
|
201
|
-
[Service]
|
|
202
|
-
Type=simple
|
|
203
|
-
${userLine}ExecStart=${execStart}
|
|
204
|
-
Restart=always
|
|
205
|
-
RestartSec=5
|
|
206
|
-
|
|
207
|
-
[Install]
|
|
208
|
-
WantedBy=${wantedBy}
|
|
209
|
-
`;
|
|
150
|
+
const { root, userName } = opts;
|
|
151
|
+
return renderManagedSystemdUnit(connectorUnit(opts), { root, userName });
|
|
210
152
|
}
|
|
211
153
|
|
|
212
154
|
export interface InstallResult {
|
|
@@ -231,6 +173,31 @@ export interface ConnectorServiceOpts {
|
|
|
231
173
|
deps?: ConnectorServiceDeps;
|
|
232
174
|
}
|
|
233
175
|
|
|
176
|
+
/** Connector-flavored install/remove messages, supplied to the generalized installer. */
|
|
177
|
+
function connectorMessages(): ManagedUnitMessages {
|
|
178
|
+
const lingerWarning =
|
|
179
|
+
"Note: could not enable lingering (loginctl enable-linger) — the connector will run while you're logged in but may not start on a cold boot before login. To run on cold boot without an active login, re-run this command as root (installs a system unit that needs no linger).";
|
|
180
|
+
return {
|
|
181
|
+
launchctlMissing: "launchctl not found; using a transient connector (won't survive a reboot).",
|
|
182
|
+
systemctlMissing: "systemctl not found; using a transient connector (won't survive a reboot).",
|
|
183
|
+
lingerWarning,
|
|
184
|
+
writeFailedPrefix:
|
|
185
|
+
"Failed to write service file; using a transient connector (won't survive a reboot)",
|
|
186
|
+
launchctlLoadFailedPrefix:
|
|
187
|
+
"launchctl could not load the connector service; using a transient connector (won't survive a reboot)",
|
|
188
|
+
daemonReloadFailedPrefix:
|
|
189
|
+
"systemctl daemon-reload failed; using a transient connector (won't survive a reboot)",
|
|
190
|
+
enableFailedPrefix:
|
|
191
|
+
"systemctl enable --now failed; using a transient connector (won't survive a reboot)",
|
|
192
|
+
// The connector always installs with start:true, so the `started` param these
|
|
193
|
+
// callbacks receive is unused here — the message is always the "starts now" variant.
|
|
194
|
+
launchdInstalled: (label, _started) =>
|
|
195
|
+
`Installed launchd LaunchAgent ${label} — the connector now starts on login/boot.`,
|
|
196
|
+
systemdInstalled: (unitName, root, _started) =>
|
|
197
|
+
`Installed systemd ${root ? "system" : "user"} unit ${unitName} — the connector now starts on boot.`,
|
|
198
|
+
};
|
|
199
|
+
}
|
|
200
|
+
|
|
234
201
|
/**
|
|
235
202
|
* Install (or refresh) the reboot-persistent connector service for one tunnel
|
|
236
203
|
* and start it. Idempotent: re-installing overwrites the service file and
|
|
@@ -251,172 +218,27 @@ export function installConnectorService(opts: ConnectorServiceOpts): InstallResu
|
|
|
251
218
|
messages: ["Could not resolve the cloudflared binary path; skipping boot-service install."],
|
|
252
219
|
};
|
|
253
220
|
}
|
|
254
|
-
|
|
255
|
-
if (deps.platform === "darwin") {
|
|
256
|
-
return installLaunchd({ ...opts, deps, cloudflaredPath });
|
|
257
|
-
}
|
|
258
|
-
if (deps.platform === "linux") {
|
|
259
|
-
return installSystemd({ ...opts, deps, cloudflaredPath });
|
|
260
|
-
}
|
|
261
|
-
return {
|
|
262
|
-
outcome: "fallback",
|
|
263
|
-
messages: [
|
|
264
|
-
`Boot-persistent connector isn't supported on ${deps.platform}; using a transient connector.`,
|
|
265
|
-
],
|
|
266
|
-
};
|
|
267
|
-
}
|
|
268
|
-
|
|
269
|
-
function installLaunchd(
|
|
270
|
-
opts: ConnectorServiceOpts & { deps: ConnectorServiceDeps; cloudflaredPath: string },
|
|
271
|
-
): InstallResult {
|
|
272
|
-
const { deps, tunnelName, configPath, logPath, cloudflaredPath } = opts;
|
|
273
|
-
if (deps.which("launchctl") === null) {
|
|
274
|
-
return {
|
|
275
|
-
outcome: "fallback",
|
|
276
|
-
messages: ["launchctl not found; using a transient connector (won't survive a reboot)."],
|
|
277
|
-
};
|
|
278
|
-
}
|
|
279
|
-
const home = deps.homeDir();
|
|
280
|
-
const plistPath = launchdPlistPath(tunnelName, home);
|
|
281
|
-
const label = launchdLabel(tunnelName);
|
|
282
|
-
const uid = deps.getuid() ?? 0;
|
|
283
|
-
const domain = `gui/${uid}`;
|
|
284
|
-
|
|
285
|
-
try {
|
|
286
|
-
deps.writeFile(
|
|
287
|
-
plistPath,
|
|
288
|
-
renderLaunchdPlist({ tunnelName, cloudflaredPath, configPath, logPath }),
|
|
289
|
-
);
|
|
290
|
-
} catch (err) {
|
|
291
|
-
return {
|
|
292
|
-
outcome: "fallback",
|
|
293
|
-
messages: [
|
|
294
|
-
`Failed to write LaunchAgent (${err instanceof Error ? err.message : String(err)}); using a transient connector (won't survive a reboot).`,
|
|
295
|
-
],
|
|
296
|
-
};
|
|
297
|
-
}
|
|
298
|
-
|
|
299
|
-
// Re-install must be idempotent: bootout any prior load (ignore failure when
|
|
300
|
-
// nothing's loaded), then bootstrap the freshly-written plist. `bootstrap`
|
|
301
|
-
// both loads AND starts (RunAtLoad), so no separate `kickstart` is needed on
|
|
302
|
-
// the happy path; we add a `kickstart -k` to force a restart when the label
|
|
303
|
-
// was already bootstrapped (bootstrap is a no-op then).
|
|
304
|
-
deps.run(["launchctl", "bootout", `${domain}/${label}`]);
|
|
305
|
-
const boot = deps.run(["launchctl", "bootstrap", domain, plistPath]);
|
|
306
|
-
if (boot.code !== 0) {
|
|
307
|
-
// Older macOS (or a sandboxed context) may not accept `bootstrap`; fall back
|
|
308
|
-
// to the legacy `load -w`, then to a transient connector.
|
|
309
|
-
const legacy = deps.run(["launchctl", "load", "-w", plistPath]);
|
|
310
|
-
if (legacy.code !== 0) {
|
|
311
|
-
deps.removeFile(plistPath);
|
|
312
|
-
return {
|
|
313
|
-
outcome: "fallback",
|
|
314
|
-
messages: [
|
|
315
|
-
`launchctl could not load the connector service (${boot.stderr.trim() || legacy.stderr.trim() || "unknown error"}); using a transient connector (won't survive a reboot).`,
|
|
316
|
-
],
|
|
317
|
-
};
|
|
318
|
-
}
|
|
319
|
-
} else {
|
|
320
|
-
deps.run(["launchctl", "kickstart", "-k", `${domain}/${label}`]);
|
|
321
|
-
}
|
|
322
|
-
|
|
323
|
-
return {
|
|
324
|
-
outcome: "installed",
|
|
325
|
-
kind: "launchd",
|
|
326
|
-
servicePath: plistPath,
|
|
327
|
-
messages: [`Installed launchd LaunchAgent ${label} — the connector now starts on login/boot.`],
|
|
328
|
-
};
|
|
329
|
-
}
|
|
330
|
-
|
|
331
|
-
function installSystemd(
|
|
332
|
-
opts: ConnectorServiceOpts & { deps: ConnectorServiceDeps; cloudflaredPath: string },
|
|
333
|
-
): InstallResult {
|
|
334
|
-
const { deps, tunnelName, configPath, logPath, cloudflaredPath } = opts;
|
|
335
|
-
if (deps.which("systemctl") === null) {
|
|
336
|
-
return {
|
|
337
|
-
outcome: "fallback",
|
|
338
|
-
messages: ["systemctl not found; using a transient connector (won't survive a reboot)."],
|
|
339
|
-
};
|
|
340
|
-
}
|
|
341
|
-
const root = (deps.getuid() ?? 1000) === 0;
|
|
342
|
-
const home = deps.homeDir();
|
|
343
|
-
const unitName = systemdUnitName(tunnelName);
|
|
344
|
-
const unitPath = systemdUnitPath(tunnelName, home, root);
|
|
345
|
-
const userName = deps.userName();
|
|
346
|
-
|
|
347
|
-
try {
|
|
348
|
-
deps.writeFile(
|
|
349
|
-
unitPath,
|
|
350
|
-
renderSystemdUnit({ tunnelName, cloudflaredPath, configPath, logPath, root, userName }),
|
|
351
|
-
);
|
|
352
|
-
} catch (err) {
|
|
221
|
+
if (deps.platform !== "darwin" && deps.platform !== "linux") {
|
|
353
222
|
return {
|
|
354
223
|
outcome: "fallback",
|
|
355
224
|
messages: [
|
|
356
|
-
`
|
|
225
|
+
`Boot-persistent connector isn't supported on ${deps.platform}; using a transient connector.`,
|
|
357
226
|
],
|
|
358
227
|
};
|
|
359
228
|
}
|
|
360
229
|
|
|
361
|
-
const
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
const lingerWarning =
|
|
374
|
-
"Note: could not enable lingering (loginctl enable-linger) — the connector will run while you're logged in but may not start on a cold boot before login. To run on cold boot without an active login, re-run this command as root (installs a system unit that needs no linger).";
|
|
375
|
-
if (deps.which("loginctl") === null) {
|
|
376
|
-
messages.push(lingerWarning);
|
|
377
|
-
} else {
|
|
378
|
-
try {
|
|
379
|
-
const linger = deps.run(["loginctl", "enable-linger", userName]);
|
|
380
|
-
if (linger.code !== 0) messages.push(lingerWarning);
|
|
381
|
-
} catch {
|
|
382
|
-
// loginctl vanished between probe and run, or threw (ENOENT/EACCES) —
|
|
383
|
-
// never fatal; linger is a best-effort nicety.
|
|
384
|
-
messages.push(lingerWarning);
|
|
385
|
-
}
|
|
386
|
-
}
|
|
387
|
-
}
|
|
388
|
-
|
|
389
|
-
const reload = deps.run(["systemctl", ...scope, "daemon-reload"]);
|
|
390
|
-
if (reload.code !== 0) {
|
|
391
|
-
deps.removeFile(unitPath);
|
|
392
|
-
return {
|
|
393
|
-
outcome: "fallback",
|
|
394
|
-
messages: [
|
|
395
|
-
`systemctl daemon-reload failed (${reload.stderr.trim() || "unknown error"}); using a transient connector (won't survive a reboot).`,
|
|
396
|
-
],
|
|
397
|
-
};
|
|
398
|
-
}
|
|
399
|
-
const enable = deps.run(["systemctl", ...scope, "enable", "--now", unitName]);
|
|
400
|
-
if (enable.code !== 0) {
|
|
401
|
-
deps.removeFile(unitPath);
|
|
402
|
-
deps.run(["systemctl", ...scope, "daemon-reload"]);
|
|
403
|
-
return {
|
|
404
|
-
outcome: "fallback",
|
|
405
|
-
messages: [
|
|
406
|
-
`systemctl enable --now failed (${enable.stderr.trim() || "unknown error"}); using a transient connector (won't survive a reboot).`,
|
|
407
|
-
],
|
|
408
|
-
};
|
|
409
|
-
}
|
|
410
|
-
|
|
411
|
-
messages.unshift(
|
|
412
|
-
`Installed systemd ${root ? "system" : "user"} unit ${unitName} — the connector now starts on boot.`,
|
|
413
|
-
);
|
|
414
|
-
return {
|
|
415
|
-
outcome: "installed",
|
|
416
|
-
kind: root ? "systemd-system" : "systemd-user",
|
|
417
|
-
servicePath: unitPath,
|
|
418
|
-
messages,
|
|
419
|
-
};
|
|
230
|
+
const result: ManagedUnitInstallResult = installManagedUnit({
|
|
231
|
+
unit: connectorUnit({
|
|
232
|
+
tunnelName: opts.tunnelName,
|
|
233
|
+
cloudflaredPath,
|
|
234
|
+
configPath: opts.configPath,
|
|
235
|
+
logPath: opts.logPath,
|
|
236
|
+
}),
|
|
237
|
+
deps,
|
|
238
|
+
messages: connectorMessages(),
|
|
239
|
+
// start: true (default) — the connector's behavior is unchanged.
|
|
240
|
+
});
|
|
241
|
+
return result;
|
|
420
242
|
}
|
|
421
243
|
|
|
422
244
|
export interface RemoveResult {
|
|
@@ -440,39 +262,12 @@ export function removeConnectorService(opts: {
|
|
|
440
262
|
deps?: ConnectorServiceDeps;
|
|
441
263
|
}): RemoveResult {
|
|
442
264
|
const deps = opts.deps ?? defaultServiceDeps;
|
|
443
|
-
const
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
// bootout unloads + stops; ignore its exit (nothing-loaded is fine).
|
|
452
|
-
deps.run(["launchctl", "bootout", `gui/${uid}/${label}`]);
|
|
453
|
-
deps.removeFile(plistPath);
|
|
454
|
-
return {
|
|
455
|
-
removed: true,
|
|
456
|
-
messages: [`Removed launchd LaunchAgent ${label}.`],
|
|
457
|
-
};
|
|
458
|
-
}
|
|
459
|
-
|
|
460
|
-
if (deps.platform === "linux") {
|
|
461
|
-
const root = (deps.getuid() ?? 1000) === 0;
|
|
462
|
-
const home = deps.homeDir();
|
|
463
|
-
const unitName = systemdUnitName(tunnelName);
|
|
464
|
-
const unitPath = systemdUnitPath(tunnelName, home, root);
|
|
465
|
-
if (!deps.exists(unitPath)) return { removed: false, messages: [] };
|
|
466
|
-
const scope = root ? [] : ["--user"];
|
|
467
|
-
// disable --now stops + removes the enable symlink; ignore exit (best-effort).
|
|
468
|
-
deps.run(["systemctl", ...scope, "disable", "--now", unitName]);
|
|
469
|
-
deps.removeFile(unitPath);
|
|
470
|
-
deps.run(["systemctl", ...scope, "daemon-reload"]);
|
|
471
|
-
return {
|
|
472
|
-
removed: true,
|
|
473
|
-
messages: [`Removed systemd unit ${unitName}.`],
|
|
474
|
-
};
|
|
475
|
-
}
|
|
476
|
-
|
|
477
|
-
return { removed: false, messages: [] };
|
|
265
|
+
const result: ManagedUnitRemoveResult = removeManagedUnit({
|
|
266
|
+
launchdLabel: launchdLabel(opts.tunnelName),
|
|
267
|
+
systemdUnitName: systemdUnitName(opts.tunnelName),
|
|
268
|
+
deps,
|
|
269
|
+
removedLaunchdMessage: (label) => `Removed launchd LaunchAgent ${label}.`,
|
|
270
|
+
removedSystemdMessage: (unitName) => `Removed systemd unit ${unitName}.`,
|
|
271
|
+
});
|
|
272
|
+
return result;
|
|
478
273
|
}
|