herdr-remote 0.2.17 → 0.2.19
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/bin/herdr-remote.js +3 -221
- package/dist/agent-status.js +130 -0
- package/dist/binary-plist.js +157 -0
- package/dist/cli.js +255 -0
- package/dist/config.js +270 -0
- package/dist/connector/agent-watch.js +110 -0
- package/dist/connector/font-server.js +165 -0
- package/dist/connector/host-connector.js +430 -0
- package/dist/connector/lock.js +66 -0
- package/dist/connector/main.js +18 -0
- package/dist/connector/sessions.js +366 -0
- package/dist/connector/update-report.js +67 -0
- package/dist/connector/wire.js +42 -0
- package/{src → dist}/exit-codes.js +1 -4
- package/dist/font-subset.js +110 -0
- package/dist/herdr-api.js +246 -0
- package/dist/herdr-command.js +223 -0
- package/dist/herdr-plugin.js +116 -0
- package/dist/herdr-server.js +189 -0
- package/dist/i18n/en.js +223 -0
- package/dist/i18n/index.js +48 -0
- package/dist/i18n/zh.js +222 -0
- package/dist/keepalive/detached.js +78 -0
- package/dist/keepalive/environment.js +44 -0
- package/dist/keepalive/index.js +110 -0
- package/dist/keepalive/launchd.js +117 -0
- package/dist/keepalive/systemd.js +108 -0
- package/dist/keepalive/types.js +1 -0
- package/dist/lib/process.js +12 -0
- package/dist/lifecycle.js +59 -0
- package/dist/net-interfaces.js +73 -0
- package/dist/pasted-files.js +115 -0
- package/dist/paths.js +62 -0
- package/dist/pty-session.js +96 -0
- package/dist/relay-client.js +106 -0
- package/dist/relay-urls.js +99 -0
- package/dist/runtime.js +106 -0
- package/dist/service.js +260 -0
- package/dist/settings-model.js +298 -0
- package/dist/socket-discovery.js +48 -0
- package/dist/supervisor.js +319 -0
- package/dist/terminal-font/deps.js +73 -0
- package/dist/terminal-font/files.js +354 -0
- package/dist/terminal-font/formats.js +335 -0
- package/dist/terminal-font/index.js +187 -0
- package/dist/terminal-font/readers.js +316 -0
- package/dist/terminal-font/terminals.js +128 -0
- package/dist/terminal-palette.js +339 -0
- package/dist/tui/App.js +195 -0
- package/dist/tui/api.js +79 -0
- package/dist/tui/components/TextField.js +50 -0
- package/dist/tui/components/common.js +80 -0
- package/dist/tui/index.js +36 -0
- package/dist/tui/mouse/index.js +164 -0
- package/dist/tui/mouse/protocol.js +96 -0
- package/dist/tui/mouse/source.js +74 -0
- package/dist/tui/screens/About.js +166 -0
- package/dist/tui/screens/Herdr.js +74 -0
- package/dist/tui/screens/Keepalive.js +84 -0
- package/dist/tui/screens/Overview.js +36 -0
- package/dist/tui/screens/Pair.js +74 -0
- package/dist/tui/screens/Relay.js +242 -0
- package/dist/tui/screens/Services.js +63 -0
- package/dist/tui/screens/Wizard.js +235 -0
- package/dist/tui/terminal.js +39 -0
- package/dist/tui/theme.js +33 -0
- package/dist/updater.js +382 -0
- package/herdr-plugin.toml +1 -1
- package/package.json +10 -9
- package/dist/tui.mjs +0 -4613
- package/src/agent-status.js +0 -139
- package/src/config.js +0 -454
- package/src/herdr-api.js +0 -244
- package/src/herdr-command.js +0 -231
- package/src/herdr-plugin.js +0 -130
- package/src/herdr-server.js +0 -201
- package/src/host-connector.js +0 -927
- package/src/i18n/en.js +0 -240
- package/src/i18n/index.js +0 -56
- package/src/i18n/zh.js +0 -239
- package/src/keepalive.js +0 -460
- package/src/lifecycle.js +0 -62
- package/src/net-interfaces.js +0 -76
- package/src/pasted-files.js +0 -176
- package/src/pty-session.js +0 -92
- package/src/service.js +0 -516
- package/src/settings-model.js +0 -323
- package/src/socket-discovery.js +0 -60
- package/src/state.js +0 -47
- package/src/supervisor.js +0 -250
- package/src/terminal-palette.js +0 -325
- package/src/updater.js +0 -394
package/src/keepalive.js
DELETED
|
@@ -1,460 +0,0 @@
|
|
|
1
|
-
'use strict';
|
|
2
|
-
|
|
3
|
-
// Keep-alive integration.
|
|
4
|
-
//
|
|
5
|
-
// "Start the services" and "keep the services running" are different problems:
|
|
6
|
-
// a detached spawn dies with the first crash and never comes back. This module
|
|
7
|
-
// hands supervision to the platform's service manager where one exists
|
|
8
|
-
// (systemd --user on Linux, launchd on macOS) and falls back to a detached copy
|
|
9
|
-
// of our own supervisor where neither does.
|
|
10
|
-
|
|
11
|
-
const fs = require('node:fs');
|
|
12
|
-
const os = require('node:os');
|
|
13
|
-
const path = require('node:path');
|
|
14
|
-
const { spawn, spawnSync } = require('node:child_process');
|
|
15
|
-
const { PACKAGE_ROOT, loadConfig, stateDir } = require('./config');
|
|
16
|
-
const { ensureDir, readJson, writeJsonAtomic } = require('./state');
|
|
17
|
-
const { logPath, pidAlive } = require('./service');
|
|
18
|
-
const { findHerdrCommand } = require('./herdr-command');
|
|
19
|
-
|
|
20
|
-
const SYSTEMD_UNIT_NAME = 'herdr-remote.service';
|
|
21
|
-
const LAUNCHD_LABEL = 'dev.herdr.remote';
|
|
22
|
-
const FALLBACK_PID_FILE = 'supervisor.pid';
|
|
23
|
-
|
|
24
|
-
function cliEntryPoint() {
|
|
25
|
-
return path.join(PACKAGE_ROOT, 'bin', 'herdr-remote.js');
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
function systemdUnitPath() {
|
|
29
|
-
const configHome = process.env.XDG_CONFIG_HOME || path.join(os.homedir(), '.config');
|
|
30
|
-
return path.join(configHome, 'systemd', 'user', SYSTEMD_UNIT_NAME);
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
function launchdPlistPath() {
|
|
34
|
-
return path.join(os.homedir(), 'Library', 'LaunchAgents', `${LAUNCHD_LABEL}.plist`);
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
function fallbackPidPath() {
|
|
38
|
-
return path.join(stateDir(), FALLBACK_PID_FILE);
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
/**
|
|
42
|
-
* Look for an executable on PATH without going through a shell: `spawnSync`
|
|
43
|
-
* with `shell: true` concatenates rather than escapes its arguments, and Node
|
|
44
|
-
* now warns about it on every call.
|
|
45
|
-
*/
|
|
46
|
-
function commandExists(command) {
|
|
47
|
-
const searchPath = process.env.PATH || '';
|
|
48
|
-
return searchPath.split(path.delimiter).filter(Boolean).some((directory) => {
|
|
49
|
-
try {
|
|
50
|
-
fs.accessSync(path.join(directory, command), fs.constants.X_OK);
|
|
51
|
-
return true;
|
|
52
|
-
} catch {
|
|
53
|
-
return false;
|
|
54
|
-
}
|
|
55
|
-
});
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
/** Which manager to use given the platform, the user's preference and reality. */
|
|
59
|
-
function detectManager(preference = 'auto') {
|
|
60
|
-
if (preference && preference !== 'auto') return preference;
|
|
61
|
-
if (process.platform === 'linux') {
|
|
62
|
-
// `systemctl --user` needs a user bus; containers and bare TTY logins often
|
|
63
|
-
// have systemd installed but no session bus, where it would fail at runtime.
|
|
64
|
-
if (commandExists('systemctl') && (process.env.DBUS_SESSION_BUS_ADDRESS || process.env.XDG_RUNTIME_DIR)) {
|
|
65
|
-
return 'systemd';
|
|
66
|
-
}
|
|
67
|
-
return 'supervisor';
|
|
68
|
-
}
|
|
69
|
-
if (process.platform === 'darwin') {
|
|
70
|
-
return commandExists('launchctl') ? 'launchd' : 'supervisor';
|
|
71
|
-
}
|
|
72
|
-
return 'supervisor';
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
// ---------------------------------------------------------------------------
|
|
76
|
-
// The environment a supervised copy needs
|
|
77
|
-
// ---------------------------------------------------------------------------
|
|
78
|
-
|
|
79
|
-
/**
|
|
80
|
-
* `PATH` for the unit: the one that is resolving commands right now, plus the
|
|
81
|
-
* directory Herdr was found in.
|
|
82
|
-
*
|
|
83
|
-
* A service manager does not inherit the shell's environment. `systemd --user`
|
|
84
|
-
* hands a unit something close to `/usr/local/bin:/usr/bin:/bin` and `launchd`
|
|
85
|
-
* is no more generous, so a copy that works from a terminal loses `~/.local/bin`
|
|
86
|
-
* — and with it `herdr` — the moment it is installed as a service. Freezing the
|
|
87
|
-
* installing shell's `PATH` into the unit keeps the supervised copy able to find
|
|
88
|
-
* Herdr, and Herdr able to find the tools it spawns in turn.
|
|
89
|
-
*/
|
|
90
|
-
function servicePath({ env = process.env, herdrCommand = null } = {}) {
|
|
91
|
-
const entries = String(env.PATH || '')
|
|
92
|
-
.split(path.delimiter)
|
|
93
|
-
.map((entry) => entry.trim())
|
|
94
|
-
.filter(Boolean);
|
|
95
|
-
// The override already pins the exact binary; this is only so the session
|
|
96
|
-
// itself can still reach it by name.
|
|
97
|
-
if (herdrCommand) entries.push(path.dirname(herdrCommand));
|
|
98
|
-
const seen = new Set();
|
|
99
|
-
const unique = entries.filter((entry) => (seen.has(entry) ? false : seen.add(entry)));
|
|
100
|
-
return unique.join(path.delimiter);
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
/**
|
|
104
|
-
* What has to be written into the unit file for a supervised start to behave
|
|
105
|
-
* like the one the user just ran by hand.
|
|
106
|
-
*/
|
|
107
|
-
function serviceEnvironment({ env = process.env, home = os.homedir(), directories } = {}) {
|
|
108
|
-
const environment = {};
|
|
109
|
-
const herdr = findHerdrCommand({ env, home, ...(directories ? { directories } : {}) });
|
|
110
|
-
if (herdr.found) environment.HERDR_BIN_PATH = herdr.command;
|
|
111
|
-
const searchPath = servicePath({ env, herdrCommand: herdr.found ? herdr.command : null });
|
|
112
|
-
if (searchPath) environment.PATH = searchPath;
|
|
113
|
-
return environment;
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
// ---------------------------------------------------------------------------
|
|
117
|
-
// Unit file rendering (pure, so it can be asserted in tests)
|
|
118
|
-
// ---------------------------------------------------------------------------
|
|
119
|
-
|
|
120
|
-
/**
|
|
121
|
-
* One `KEY=value` per directive. An unquoted systemd value ends at the first
|
|
122
|
-
* space, and `%` starts a specifier, so a `PATH` with either in it would be
|
|
123
|
-
* silently truncated or rewritten.
|
|
124
|
-
*/
|
|
125
|
-
function systemdEnvironmentLine(key, value) {
|
|
126
|
-
const text = String(value).replace(/[\r\n]+/g, ' ').replace(/%/g, '%%');
|
|
127
|
-
if (/^[\w@+=:,./-]*$/.test(text)) return `Environment=${key}=${text}`;
|
|
128
|
-
return `Environment="${key}=${text.replace(/([\\"])/g, '\\$1')}"`;
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
function renderSystemdUnit({ nodePath = process.execPath, entryPoint = cliEntryPoint(), environment = {} } = {}) {
|
|
132
|
-
const environmentLines = Object.entries(environment)
|
|
133
|
-
.map(([key, value]) => systemdEnvironmentLine(key, value))
|
|
134
|
-
.join('\n');
|
|
135
|
-
return `[Unit]
|
|
136
|
-
Description=Herdr Remote (relay and host connector)
|
|
137
|
-
Documentation=https://github.com/herdr/herdr-remote
|
|
138
|
-
After=default.target
|
|
139
|
-
|
|
140
|
-
[Service]
|
|
141
|
-
Type=simple
|
|
142
|
-
ExecStart=${nodePath} ${entryPoint} run
|
|
143
|
-
Restart=always
|
|
144
|
-
RestartSec=3
|
|
145
|
-
# Give the relay and host connector time to close sessions cleanly.
|
|
146
|
-
TimeoutStopSec=15
|
|
147
|
-
${environmentLines}
|
|
148
|
-
|
|
149
|
-
[Install]
|
|
150
|
-
WantedBy=default.target
|
|
151
|
-
`;
|
|
152
|
-
}
|
|
153
|
-
|
|
154
|
-
function escapeXml(value) {
|
|
155
|
-
return String(value)
|
|
156
|
-
.replace(/&/g, '&')
|
|
157
|
-
.replace(/</g, '<')
|
|
158
|
-
.replace(/>/g, '>')
|
|
159
|
-
.replace(/"/g, '"')
|
|
160
|
-
.replace(/'/g, ''');
|
|
161
|
-
}
|
|
162
|
-
|
|
163
|
-
function renderLaunchdPlist({
|
|
164
|
-
nodePath = process.execPath,
|
|
165
|
-
entryPoint = cliEntryPoint(),
|
|
166
|
-
label = LAUNCHD_LABEL,
|
|
167
|
-
outLog = logPath('supervisor'),
|
|
168
|
-
errLog = logPath('supervisor'),
|
|
169
|
-
environment = {},
|
|
170
|
-
} = {}) {
|
|
171
|
-
const environmentEntries = Object.entries(environment)
|
|
172
|
-
.map(([key, value]) => ` <key>${escapeXml(key)}</key>\n <string>${escapeXml(value)}</string>`)
|
|
173
|
-
.join('\n');
|
|
174
|
-
const environmentBlock = environmentEntries
|
|
175
|
-
? ` <key>EnvironmentVariables</key>\n <dict>\n${environmentEntries}\n </dict>\n`
|
|
176
|
-
: '';
|
|
177
|
-
return `<?xml version="1.0" encoding="UTF-8"?>
|
|
178
|
-
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
|
179
|
-
<plist version="1.0">
|
|
180
|
-
<dict>
|
|
181
|
-
<key>Label</key>
|
|
182
|
-
<string>${escapeXml(label)}</string>
|
|
183
|
-
<key>ProgramArguments</key>
|
|
184
|
-
<array>
|
|
185
|
-
<string>${escapeXml(nodePath)}</string>
|
|
186
|
-
<string>${escapeXml(entryPoint)}</string>
|
|
187
|
-
<string>run</string>
|
|
188
|
-
</array>
|
|
189
|
-
<key>RunAtLoad</key>
|
|
190
|
-
<true/>
|
|
191
|
-
<key>KeepAlive</key>
|
|
192
|
-
<true/>
|
|
193
|
-
<key>ProcessType</key>
|
|
194
|
-
<string>Background</string>
|
|
195
|
-
${environmentBlock} <key>StandardOutPath</key>
|
|
196
|
-
<string>${escapeXml(outLog)}</string>
|
|
197
|
-
<key>StandardErrorPath</key>
|
|
198
|
-
<string>${escapeXml(errLog)}</string>
|
|
199
|
-
</dict>
|
|
200
|
-
</plist>
|
|
201
|
-
`;
|
|
202
|
-
}
|
|
203
|
-
|
|
204
|
-
// ---------------------------------------------------------------------------
|
|
205
|
-
// systemd
|
|
206
|
-
// ---------------------------------------------------------------------------
|
|
207
|
-
|
|
208
|
-
function systemctl(args, { capture = true } = {}) {
|
|
209
|
-
return spawnSync('systemctl', ['--user', ...args], {
|
|
210
|
-
encoding: 'utf8',
|
|
211
|
-
stdio: capture ? 'pipe' : 'inherit',
|
|
212
|
-
});
|
|
213
|
-
}
|
|
214
|
-
|
|
215
|
-
function systemdStatus() {
|
|
216
|
-
const installed = fs.existsSync(systemdUnitPath());
|
|
217
|
-
if (!installed) return { manager: 'systemd', installed: false, active: false, enabled: false };
|
|
218
|
-
const active = systemctl(['is-active', SYSTEMD_UNIT_NAME]);
|
|
219
|
-
const enabled = systemctl(['is-enabled', SYSTEMD_UNIT_NAME]);
|
|
220
|
-
const lingering = spawnSync('loginctl', ['show-user', os.userInfo().username, '--property=Linger'], { encoding: 'utf8' });
|
|
221
|
-
return {
|
|
222
|
-
manager: 'systemd',
|
|
223
|
-
installed: true,
|
|
224
|
-
active: String(active.stdout || '').trim() === 'active',
|
|
225
|
-
enabled: String(enabled.stdout || '').trim().startsWith('enabled'),
|
|
226
|
-
linger: String(lingering.stdout || '').includes('Linger=yes'),
|
|
227
|
-
unitPath: systemdUnitPath(),
|
|
228
|
-
state: String(active.stdout || active.stderr || '').trim(),
|
|
229
|
-
};
|
|
230
|
-
}
|
|
231
|
-
|
|
232
|
-
function systemdInstall() {
|
|
233
|
-
const unitPath = systemdUnitPath();
|
|
234
|
-
ensureDir(path.dirname(unitPath));
|
|
235
|
-
fs.writeFileSync(unitPath, renderSystemdUnit({ environment: serviceEnvironment() }), { mode: 0o644 });
|
|
236
|
-
const reload = systemctl(['daemon-reload']);
|
|
237
|
-
if (reload.status !== 0) {
|
|
238
|
-
throw new Error(`systemctl --user daemon-reload failed: ${String(reload.stderr || '').trim()}`);
|
|
239
|
-
}
|
|
240
|
-
const enable = systemctl(['enable', '--now', SYSTEMD_UNIT_NAME]);
|
|
241
|
-
if (enable.status !== 0) {
|
|
242
|
-
throw new Error(`systemctl --user enable --now failed: ${String(enable.stderr || '').trim()}`);
|
|
243
|
-
}
|
|
244
|
-
return { ok: true, unitPath, hint: `loginctl enable-linger ${os.userInfo().username}` };
|
|
245
|
-
}
|
|
246
|
-
|
|
247
|
-
function systemdUninstall() {
|
|
248
|
-
const unitPath = systemdUnitPath();
|
|
249
|
-
systemctl(['disable', '--now', SYSTEMD_UNIT_NAME]);
|
|
250
|
-
if (fs.existsSync(unitPath)) fs.rmSync(unitPath, { force: true });
|
|
251
|
-
systemctl(['daemon-reload']);
|
|
252
|
-
return { ok: true, unitPath };
|
|
253
|
-
}
|
|
254
|
-
|
|
255
|
-
// ---------------------------------------------------------------------------
|
|
256
|
-
// launchd
|
|
257
|
-
// ---------------------------------------------------------------------------
|
|
258
|
-
|
|
259
|
-
function launchdDomainTarget() {
|
|
260
|
-
return `gui/${process.getuid ? process.getuid() : ''}`;
|
|
261
|
-
}
|
|
262
|
-
|
|
263
|
-
function launchdStatus() {
|
|
264
|
-
const plistPath = launchdPlistPath();
|
|
265
|
-
const installed = fs.existsSync(plistPath);
|
|
266
|
-
if (!installed) return { manager: 'launchd', installed: false, active: false, enabled: false };
|
|
267
|
-
const result = spawnSync('launchctl', ['print', `${launchdDomainTarget()}/${LAUNCHD_LABEL}`], { encoding: 'utf8' });
|
|
268
|
-
const output = String(result.stdout || '');
|
|
269
|
-
return {
|
|
270
|
-
manager: 'launchd',
|
|
271
|
-
installed: true,
|
|
272
|
-
active: result.status === 0 && /state = running/.test(output),
|
|
273
|
-
enabled: result.status === 0,
|
|
274
|
-
unitPath: plistPath,
|
|
275
|
-
state: result.status === 0 ? 'loaded' : 'not loaded',
|
|
276
|
-
};
|
|
277
|
-
}
|
|
278
|
-
|
|
279
|
-
function launchdInstall() {
|
|
280
|
-
const plistPath = launchdPlistPath();
|
|
281
|
-
ensureDir(path.dirname(plistPath));
|
|
282
|
-
ensureDir(stateDir());
|
|
283
|
-
fs.writeFileSync(plistPath, renderLaunchdPlist({ environment: serviceEnvironment() }), { mode: 0o644 });
|
|
284
|
-
// bootout first so a re-install picks up the rewritten plist.
|
|
285
|
-
spawnSync('launchctl', ['bootout', `${launchdDomainTarget()}/${LAUNCHD_LABEL}`], { stdio: 'ignore' });
|
|
286
|
-
const result = spawnSync('launchctl', ['bootstrap', launchdDomainTarget(), plistPath], { encoding: 'utf8' });
|
|
287
|
-
if (result.status !== 0) {
|
|
288
|
-
throw new Error(`launchctl bootstrap failed: ${String(result.stderr || '').trim()}`);
|
|
289
|
-
}
|
|
290
|
-
spawnSync('launchctl', ['enable', `${launchdDomainTarget()}/${LAUNCHD_LABEL}`], { stdio: 'ignore' });
|
|
291
|
-
return { ok: true, unitPath: plistPath };
|
|
292
|
-
}
|
|
293
|
-
|
|
294
|
-
function launchdUninstall() {
|
|
295
|
-
const plistPath = launchdPlistPath();
|
|
296
|
-
spawnSync('launchctl', ['bootout', `${launchdDomainTarget()}/${LAUNCHD_LABEL}`], { stdio: 'ignore' });
|
|
297
|
-
if (fs.existsSync(plistPath)) fs.rmSync(plistPath, { force: true });
|
|
298
|
-
return { ok: true, unitPath: plistPath };
|
|
299
|
-
}
|
|
300
|
-
|
|
301
|
-
// ---------------------------------------------------------------------------
|
|
302
|
-
// Fallback supervisor daemon
|
|
303
|
-
// ---------------------------------------------------------------------------
|
|
304
|
-
|
|
305
|
-
function readFallbackPid() {
|
|
306
|
-
const record = readJson(fallbackPidPath(), {});
|
|
307
|
-
return Number.isInteger(record.pid) ? record.pid : null;
|
|
308
|
-
}
|
|
309
|
-
|
|
310
|
-
function fallbackStatus() {
|
|
311
|
-
const pid = readFallbackPid();
|
|
312
|
-
const alive = pidAlive(pid);
|
|
313
|
-
return {
|
|
314
|
-
manager: 'supervisor',
|
|
315
|
-
installed: alive,
|
|
316
|
-
active: alive,
|
|
317
|
-
enabled: false,
|
|
318
|
-
pid: alive ? pid : null,
|
|
319
|
-
unitPath: fallbackPidPath(),
|
|
320
|
-
state: alive ? 'running' : 'stopped',
|
|
321
|
-
};
|
|
322
|
-
}
|
|
323
|
-
|
|
324
|
-
function fallbackInstall() {
|
|
325
|
-
if (pidAlive(readFallbackPid())) return { ok: true, alreadyRunning: true, pid: readFallbackPid() };
|
|
326
|
-
ensureDir(stateDir());
|
|
327
|
-
const logFd = fs.openSync(logPath('supervisor'), 'a');
|
|
328
|
-
try {
|
|
329
|
-
const child = spawn(process.execPath, [cliEntryPoint(), 'run', '--daemon'], {
|
|
330
|
-
cwd: PACKAGE_ROOT,
|
|
331
|
-
env: { ...process.env, HERDR_REMOTE_SERVICE: '1', ...serviceEnvironment() },
|
|
332
|
-
detached: true,
|
|
333
|
-
stdio: ['ignore', logFd, logFd],
|
|
334
|
-
});
|
|
335
|
-
child.unref();
|
|
336
|
-
writeJsonAtomic(fallbackPidPath(), { pid: child.pid, startedAt: new Date().toISOString() });
|
|
337
|
-
return { ok: true, pid: child.pid, note: 'this fallback does not survive a reboot' };
|
|
338
|
-
} finally {
|
|
339
|
-
fs.closeSync(logFd);
|
|
340
|
-
}
|
|
341
|
-
}
|
|
342
|
-
|
|
343
|
-
function fallbackUninstall() {
|
|
344
|
-
const pid = readFallbackPid();
|
|
345
|
-
if (pidAlive(pid)) {
|
|
346
|
-
try { process.kill(pid, 'SIGTERM'); } catch {}
|
|
347
|
-
}
|
|
348
|
-
try { fs.rmSync(fallbackPidPath(), { force: true }); } catch {}
|
|
349
|
-
return { ok: true, stoppedPid: pid };
|
|
350
|
-
}
|
|
351
|
-
|
|
352
|
-
// ---------------------------------------------------------------------------
|
|
353
|
-
// Public surface
|
|
354
|
-
// ---------------------------------------------------------------------------
|
|
355
|
-
|
|
356
|
-
function resolveManager(config = loadConfig()) {
|
|
357
|
-
return detectManager(config.keepalive?.manager || 'auto');
|
|
358
|
-
}
|
|
359
|
-
|
|
360
|
-
function status(config = loadConfig()) {
|
|
361
|
-
const manager = resolveManager(config);
|
|
362
|
-
if (manager === 'none') return { manager: 'none', installed: false, active: false, enabled: false };
|
|
363
|
-
if (manager === 'systemd') return systemdStatus();
|
|
364
|
-
if (manager === 'launchd') return launchdStatus();
|
|
365
|
-
return fallbackStatus();
|
|
366
|
-
}
|
|
367
|
-
|
|
368
|
-
function install(config = loadConfig()) {
|
|
369
|
-
const manager = resolveManager(config);
|
|
370
|
-
if (manager === 'systemd') return { manager, ...systemdInstall() };
|
|
371
|
-
if (manager === 'launchd') return { manager, ...launchdInstall() };
|
|
372
|
-
if (manager === 'none') throw new Error('keep-alive is disabled in the configuration');
|
|
373
|
-
return { manager: 'supervisor', ...fallbackInstall() };
|
|
374
|
-
}
|
|
375
|
-
|
|
376
|
-
function uninstall(config = loadConfig()) {
|
|
377
|
-
const manager = resolveManager(config);
|
|
378
|
-
if (manager === 'systemd') return { manager, ...systemdUninstall() };
|
|
379
|
-
if (manager === 'launchd') return { manager, ...launchdUninstall() };
|
|
380
|
-
return { manager: 'supervisor', ...fallbackUninstall() };
|
|
381
|
-
}
|
|
382
|
-
|
|
383
|
-
/** Restart whatever manages the services, so config edits take effect. */
|
|
384
|
-
function restart(config = loadConfig()) {
|
|
385
|
-
const manager = resolveManager(config);
|
|
386
|
-
if (manager === 'systemd') {
|
|
387
|
-
const result = systemctl(['restart', SYSTEMD_UNIT_NAME]);
|
|
388
|
-
if (result.status !== 0) throw new Error(String(result.stderr || '').trim() || 'systemctl restart failed');
|
|
389
|
-
return { manager, ok: true };
|
|
390
|
-
}
|
|
391
|
-
if (manager === 'launchd') {
|
|
392
|
-
spawnSync('launchctl', ['kickstart', '-k', `${launchdDomainTarget()}/${LAUNCHD_LABEL}`], { stdio: 'ignore' });
|
|
393
|
-
return { manager, ok: true };
|
|
394
|
-
}
|
|
395
|
-
fallbackUninstall();
|
|
396
|
-
return { manager: 'supervisor', ...fallbackInstall() };
|
|
397
|
-
}
|
|
398
|
-
|
|
399
|
-
/**
|
|
400
|
-
* Stop the managed services. Killing the child pids directly would only make
|
|
401
|
-
* the service manager start them again, so an installed manager is asked to
|
|
402
|
-
* stop instead.
|
|
403
|
-
*/
|
|
404
|
-
function stopManaged(config = loadConfig()) {
|
|
405
|
-
const current = status(config);
|
|
406
|
-
if (!current.installed && !current.active) return { managed: false };
|
|
407
|
-
if (current.manager === 'systemd') {
|
|
408
|
-
systemctl(['stop', SYSTEMD_UNIT_NAME]);
|
|
409
|
-
return { managed: true, manager: 'systemd' };
|
|
410
|
-
}
|
|
411
|
-
if (current.manager === 'launchd') {
|
|
412
|
-
spawnSync('launchctl', ['bootout', `${launchdDomainTarget()}/${LAUNCHD_LABEL}`], { stdio: 'ignore' });
|
|
413
|
-
return { managed: true, manager: 'launchd' };
|
|
414
|
-
}
|
|
415
|
-
if (current.manager === 'supervisor' && current.active) {
|
|
416
|
-
fallbackUninstall();
|
|
417
|
-
return { managed: true, manager: 'supervisor' };
|
|
418
|
-
}
|
|
419
|
-
return { managed: false };
|
|
420
|
-
}
|
|
421
|
-
|
|
422
|
-
function enableLinger() {
|
|
423
|
-
const username = os.userInfo().username;
|
|
424
|
-
const result = spawnSync('loginctl', ['enable-linger', username], { encoding: 'utf8' });
|
|
425
|
-
if (result.status !== 0) {
|
|
426
|
-
throw new Error(String(result.stderr || '').trim() || 'loginctl enable-linger failed');
|
|
427
|
-
}
|
|
428
|
-
return { ok: true, username };
|
|
429
|
-
}
|
|
430
|
-
|
|
431
|
-
function logsHint(config = loadConfig()) {
|
|
432
|
-
const manager = resolveManager(config);
|
|
433
|
-
if (manager === 'systemd') return `journalctl --user -u ${SYSTEMD_UNIT_NAME} -f`;
|
|
434
|
-
if (manager === 'launchd') return `tail -f ${logPath('supervisor')}`;
|
|
435
|
-
return `tail -f ${logPath('supervisor')}`;
|
|
436
|
-
}
|
|
437
|
-
|
|
438
|
-
module.exports = {
|
|
439
|
-
SYSTEMD_UNIT_NAME,
|
|
440
|
-
LAUNCHD_LABEL,
|
|
441
|
-
cliEntryPoint,
|
|
442
|
-
systemdUnitPath,
|
|
443
|
-
launchdPlistPath,
|
|
444
|
-
fallbackPidPath,
|
|
445
|
-
detectManager,
|
|
446
|
-
resolveManager,
|
|
447
|
-
renderSystemdUnit,
|
|
448
|
-
renderLaunchdPlist,
|
|
449
|
-
escapeXml,
|
|
450
|
-
servicePath,
|
|
451
|
-
serviceEnvironment,
|
|
452
|
-
systemdEnvironmentLine,
|
|
453
|
-
status,
|
|
454
|
-
install,
|
|
455
|
-
uninstall,
|
|
456
|
-
restart,
|
|
457
|
-
stopManaged,
|
|
458
|
-
enableLinger,
|
|
459
|
-
logsHint,
|
|
460
|
-
};
|
package/src/lifecycle.js
DELETED
|
@@ -1,62 +0,0 @@
|
|
|
1
|
-
'use strict';
|
|
2
|
-
|
|
3
|
-
// Start/stop/restart that respect the keep-alive manager.
|
|
4
|
-
//
|
|
5
|
-
// When systemd or launchd supervises the services, killing the processes just
|
|
6
|
-
// makes the manager start them again — and starting them by hand produces a
|
|
7
|
-
// second, unmanaged copy fighting for the port. Every entry point goes through
|
|
8
|
-
// here so the managed and unmanaged paths behave the same from the outside.
|
|
9
|
-
|
|
10
|
-
const { loadConfig } = require('./config');
|
|
11
|
-
const keepalive = require('./keepalive');
|
|
12
|
-
const { restartServices, startServices, statusServices, stopServices } = require('./service');
|
|
13
|
-
|
|
14
|
-
function managerInUse(config) {
|
|
15
|
-
const status = keepalive.status(config);
|
|
16
|
-
return status.installed || status.active ? status : null;
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
function startAll(config = loadConfig()) {
|
|
20
|
-
const managed = managerInUse(config);
|
|
21
|
-
if (managed) {
|
|
22
|
-
// Start means "make it run", not "restart it". Herdr runs this through the
|
|
23
|
-
// plugin's startup hook every time a Herdr server starts, and a restart
|
|
24
|
-
// there dropped every browser attached to the workstation — including the
|
|
25
|
-
// one that had just asked for that Herdr to be started.
|
|
26
|
-
if (!managed.active) keepalive.restart(config);
|
|
27
|
-
return { ok: true, managed: true, manager: managed.manager, alreadyRunning: Boolean(managed.active) };
|
|
28
|
-
}
|
|
29
|
-
return { ...startServices(), managed: false };
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
function stopAll(config = loadConfig()) {
|
|
33
|
-
const result = keepalive.stopManaged(config);
|
|
34
|
-
if (result.managed) {
|
|
35
|
-
// The manager stops the supervisor, which stops its children; clear the
|
|
36
|
-
// recorded pids so status does not report ghosts.
|
|
37
|
-
stopServices();
|
|
38
|
-
return { ok: true, managed: true, manager: result.manager };
|
|
39
|
-
}
|
|
40
|
-
return { ...stopServices(), managed: false };
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
function restartAll(config = loadConfig()) {
|
|
44
|
-
const managed = managerInUse(config);
|
|
45
|
-
if (managed) {
|
|
46
|
-
keepalive.restart(config);
|
|
47
|
-
return { ok: true, managed: true, manager: managed.manager };
|
|
48
|
-
}
|
|
49
|
-
return { ...restartServices(), managed: false };
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
/** Service status plus the keep-alive picture, which is what the TUI shows. */
|
|
53
|
-
async function fullStatus(config = loadConfig()) {
|
|
54
|
-
const [services, keepaliveStatus] = [await statusServices(), keepalive.status(config)];
|
|
55
|
-
return {
|
|
56
|
-
...services,
|
|
57
|
-
keepalive: keepaliveStatus,
|
|
58
|
-
logsHint: keepalive.logsHint(config),
|
|
59
|
-
};
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
module.exports = { startAll, stopAll, restartAll, fullStatus, managerInUse };
|
package/src/net-interfaces.js
DELETED
|
@@ -1,76 +0,0 @@
|
|
|
1
|
-
'use strict';
|
|
2
|
-
|
|
3
|
-
const os = require('node:os');
|
|
4
|
-
|
|
5
|
-
// Interface names that are almost always virtual bridges rather than something
|
|
6
|
-
// a phone can reach. They are still listed, just ranked last.
|
|
7
|
-
const VIRTUAL_NAME_PATTERN = /^(docker|br-|virbr|veth|vmnet|vboxnet|lxcbr|cni|flannel|kube)/i;
|
|
8
|
-
const TAILSCALE_NAME_PATTERN = /^(tailscale|ts)\d*$/i;
|
|
9
|
-
|
|
10
|
-
/**
|
|
11
|
-
* Is this address inside Tailscale's 100.64.0.0/10 CGNAT range? Interface names
|
|
12
|
-
* differ per platform (tailscale0 on Linux, utunN on macOS), so the address
|
|
13
|
-
* range is the portable signal.
|
|
14
|
-
*/
|
|
15
|
-
function isTailscaleAddress(address) {
|
|
16
|
-
const octets = String(address).split('.');
|
|
17
|
-
if (octets.length !== 4) return false;
|
|
18
|
-
const first = Number(octets[0]);
|
|
19
|
-
const second = Number(octets[1]);
|
|
20
|
-
if (!Number.isInteger(first) || !Number.isInteger(second)) return false;
|
|
21
|
-
return first === 100 && second >= 64 && second <= 127;
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
function classify(name, info) {
|
|
25
|
-
if (info.internal) return 'loopback';
|
|
26
|
-
if (TAILSCALE_NAME_PATTERN.test(name) || isTailscaleAddress(info.address)) return 'tailscale';
|
|
27
|
-
if (VIRTUAL_NAME_PATTERN.test(name)) return 'virtual';
|
|
28
|
-
return 'lan';
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
const KIND_ORDER = { tailscale: 0, lan: 1, virtual: 2, loopback: 3 };
|
|
32
|
-
|
|
33
|
-
/**
|
|
34
|
-
* Addresses this machine can be reached at, best candidate first.
|
|
35
|
-
*
|
|
36
|
-
* Tailscale addresses come first because a device on the tailnet reaches them
|
|
37
|
-
* from anywhere, which is exactly the "works away from home without running a
|
|
38
|
-
* relay" case; plain LAN addresses follow, then virtual bridges, then loopback.
|
|
39
|
-
*/
|
|
40
|
-
function listReachableAddresses({ includeLoopback = true, includeIpv6 = false } = {}) {
|
|
41
|
-
const interfaces = os.networkInterfaces();
|
|
42
|
-
const results = [];
|
|
43
|
-
for (const [name, entries] of Object.entries(interfaces)) {
|
|
44
|
-
for (const info of entries || []) {
|
|
45
|
-
const family = typeof info.family === 'string' ? info.family : `IPv${info.family}`;
|
|
46
|
-
if (family !== 'IPv4' && !(includeIpv6 && family === 'IPv6')) continue;
|
|
47
|
-
const kind = classify(name, info);
|
|
48
|
-
if (kind === 'loopback' && !includeLoopback) continue;
|
|
49
|
-
results.push({
|
|
50
|
-
name,
|
|
51
|
-
address: info.address,
|
|
52
|
-
family,
|
|
53
|
-
kind,
|
|
54
|
-
internal: Boolean(info.internal),
|
|
55
|
-
});
|
|
56
|
-
}
|
|
57
|
-
}
|
|
58
|
-
results.sort((a, b) => {
|
|
59
|
-
const byKind = KIND_ORDER[a.kind] - KIND_ORDER[b.kind];
|
|
60
|
-
if (byKind !== 0) return byKind;
|
|
61
|
-
return a.address.localeCompare(b.address);
|
|
62
|
-
});
|
|
63
|
-
return results;
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
/** Best guess for the address to advertise when binding to every interface. */
|
|
67
|
-
function preferredLanAddress() {
|
|
68
|
-
const candidates = listReachableAddresses({ includeLoopback: false });
|
|
69
|
-
return candidates.length > 0 ? candidates[0].address : null;
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
module.exports = {
|
|
73
|
-
isTailscaleAddress,
|
|
74
|
-
listReachableAddresses,
|
|
75
|
-
preferredLanAddress,
|
|
76
|
-
};
|