herdr-remote 0.2.18 → 0.2.20
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 -225
- 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 +8 -8
- package/dist/tui.mjs +0 -5775
- package/src/agent-status.js +0 -139
- package/src/binary-plist.js +0 -146
- package/src/config.js +0 -454
- package/src/font-subset.js +0 -110
- 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 -1069
- 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 -530
- 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-font.js +0 -1204
- package/src/terminal-palette.js +0 -325
- package/src/updater.js +0 -394
package/src/service.js
DELETED
|
@@ -1,530 +0,0 @@
|
|
|
1
|
-
'use strict';
|
|
2
|
-
|
|
3
|
-
const fs = require('node:fs');
|
|
4
|
-
const http = require('node:http');
|
|
5
|
-
const https = require('node:https');
|
|
6
|
-
const path = require('node:path');
|
|
7
|
-
const { spawn } = require('node:child_process');
|
|
8
|
-
const {
|
|
9
|
-
PACKAGE_ROOT,
|
|
10
|
-
bindAddress,
|
|
11
|
-
configDir,
|
|
12
|
-
loadConfig,
|
|
13
|
-
resolveAdminOrigin,
|
|
14
|
-
resolveHostRelayUrl,
|
|
15
|
-
resolvePublicUrl,
|
|
16
|
-
runsLocalRelay,
|
|
17
|
-
runtimeStatePath,
|
|
18
|
-
stateDir,
|
|
19
|
-
} = require('./config');
|
|
20
|
-
const { ensureDir, randomToken, readJson, writeJsonAtomic } = require('./state');
|
|
21
|
-
const {
|
|
22
|
-
probeTerminalPalette,
|
|
23
|
-
paletteFromEnvironment,
|
|
24
|
-
rememberTerminalPalette,
|
|
25
|
-
rememberedTerminalPalette,
|
|
26
|
-
} = require('./terminal-palette');
|
|
27
|
-
const { resolveHostFont } = require('./terminal-font');
|
|
28
|
-
const { resolveSocketPath } = require('./socket-discovery');
|
|
29
|
-
const { preferredLanAddress } = require('./net-interfaces');
|
|
30
|
-
const { MIN_HERDR_VERSION, findHerdrCommand, herdrVersion, resolveHerdrCommand } = require('./herdr-command');
|
|
31
|
-
|
|
32
|
-
const RUNTIME_VERSION = 2;
|
|
33
|
-
|
|
34
|
-
function pidAlive(pid) {
|
|
35
|
-
if (!Number.isInteger(pid) || pid <= 0) return false;
|
|
36
|
-
try {
|
|
37
|
-
process.kill(pid, 0);
|
|
38
|
-
return true;
|
|
39
|
-
} catch (error) {
|
|
40
|
-
return error.code === 'EPERM';
|
|
41
|
-
}
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
function readRuntime() {
|
|
45
|
-
const state = readJson(runtimeStatePath(), {});
|
|
46
|
-
return state && typeof state === 'object' ? state : {};
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
/**
|
|
50
|
-
* Load (creating if needed) the long-lived identifiers and secrets this
|
|
51
|
-
* workstation uses. These live in the state directory rather than config.json
|
|
52
|
-
* because they are credentials: writeJsonAtomic stores them mode 0600.
|
|
53
|
-
*/
|
|
54
|
-
function ensureRuntime() {
|
|
55
|
-
ensureDir(configDir());
|
|
56
|
-
ensureDir(stateDir());
|
|
57
|
-
const state = readRuntime();
|
|
58
|
-
if (!state.version) state.version = RUNTIME_VERSION;
|
|
59
|
-
if (!state.hostId) state.hostId = `host-${randomToken(9)}`;
|
|
60
|
-
if (!state.hostToken) state.hostToken = randomToken(32);
|
|
61
|
-
writeJsonAtomic(runtimeStatePath(), state);
|
|
62
|
-
return state;
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
/**
|
|
66
|
-
* Set the password used to join a relay. An empty value means "no password",
|
|
67
|
-
* which is what a public relay expects.
|
|
68
|
-
*
|
|
69
|
-
* It lives in the state file rather than config.json because it is a secret:
|
|
70
|
-
* writeJsonAtomic stores that file mode 0600.
|
|
71
|
-
*/
|
|
72
|
-
function setRelayPassword(password) {
|
|
73
|
-
const state = ensureRuntime();
|
|
74
|
-
state.relayPassword = typeof password === 'string' ? password.trim() : '';
|
|
75
|
-
writeJsonAtomic(runtimeStatePath(), state);
|
|
76
|
-
return state;
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
/**
|
|
80
|
-
* Issue this workstation a new identity on the relay.
|
|
81
|
-
*
|
|
82
|
-
* The host token is what proves ownership of this workstation, so replacing it
|
|
83
|
-
* means the relay no longer recognises the old one — useful if it leaked, but
|
|
84
|
-
* it also orphans the previous record on a relay that already enrolled it.
|
|
85
|
-
*/
|
|
86
|
-
function regenerateHostIdentity() {
|
|
87
|
-
const state = ensureRuntime();
|
|
88
|
-
state.hostId = `host-${randomToken(9)}`;
|
|
89
|
-
state.hostToken = randomToken(32);
|
|
90
|
-
writeJsonAtomic(runtimeStatePath(), state);
|
|
91
|
-
return state;
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
/**
|
|
95
|
-
* The colors of the terminal this workstation is looked at through.
|
|
96
|
-
*
|
|
97
|
-
* Asked once per process, from whichever start path has a real terminal, and
|
|
98
|
-
* remembered in the state file: a later start from a service manager has no
|
|
99
|
-
* terminal to ask, and the workstation's appearance has not changed just
|
|
100
|
-
* because systemd, and not a person, launched it this time.
|
|
101
|
-
*/
|
|
102
|
-
let cachedTerminalPalette;
|
|
103
|
-
function hostTerminalPalette({ refresh = false } = {}) {
|
|
104
|
-
if (!refresh && cachedTerminalPalette !== undefined) return cachedTerminalPalette;
|
|
105
|
-
|
|
106
|
-
const inherited = paletteFromEnvironment();
|
|
107
|
-
if (inherited) {
|
|
108
|
-
cachedTerminalPalette = inherited;
|
|
109
|
-
return cachedTerminalPalette;
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
const probed = probeTerminalPalette();
|
|
113
|
-
if (probed) {
|
|
114
|
-
cachedTerminalPalette = rememberTerminalPalette(probed);
|
|
115
|
-
return cachedTerminalPalette;
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
// Nothing to ask: fall back to what a start with a terminal wrote down,
|
|
119
|
-
// re-validated, because a state file is not a trusted wire either.
|
|
120
|
-
cachedTerminalPalette = rememberedTerminalPalette();
|
|
121
|
-
return cachedTerminalPalette;
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
/**
|
|
125
|
-
* The terminal font a service start hands its connector: what this command
|
|
126
|
-
* detected at its entry point, else what an earlier start with a terminal
|
|
127
|
-
* wrote down. Only the family, size and terminal travel here; the connector
|
|
128
|
-
* finds the files itself, on the machine that has them.
|
|
129
|
-
*/
|
|
130
|
-
function hostTerminalFont() {
|
|
131
|
-
return resolveHostFont();
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
function logPath(name) {
|
|
135
|
-
return path.join(stateDir(), `${name}.log`);
|
|
136
|
-
}
|
|
137
|
-
|
|
138
|
-
function relayBinPath() {
|
|
139
|
-
// Resolved rather than hard-coded: the relay is a separate npm package, so
|
|
140
|
-
// its location depends on how npm hoisted it.
|
|
141
|
-
const manifest = require.resolve('herdr-remote-relay/package.json');
|
|
142
|
-
return path.join(path.dirname(manifest), 'bin', 'herdr-remote-relay.js');
|
|
143
|
-
}
|
|
144
|
-
|
|
145
|
-
function relayAuthStatePath() {
|
|
146
|
-
return path.join(stateDir(), 'relay-auth.json');
|
|
147
|
-
}
|
|
148
|
-
|
|
149
|
-
/**
|
|
150
|
-
* The processes that make up a running herdr-remote, as declarative specs.
|
|
151
|
-
*
|
|
152
|
-
* Both the detached starter and the foreground supervisor build their children
|
|
153
|
-
* from this one list so the two paths cannot drift in how they configure the
|
|
154
|
-
* relay or the host connector.
|
|
155
|
-
*/
|
|
156
|
-
function serviceSpecs(config = loadConfig(), state = ensureRuntime()) {
|
|
157
|
-
const specs = [];
|
|
158
|
-
const publicUrl = resolvePublicUrl(config, preferredLanAddress());
|
|
159
|
-
|
|
160
|
-
if (runsLocalRelay(config)) {
|
|
161
|
-
specs.push({
|
|
162
|
-
name: 'relay',
|
|
163
|
-
command: process.execPath,
|
|
164
|
-
args: [relayBinPath()],
|
|
165
|
-
env: {
|
|
166
|
-
// Let the shared web UI tell a private workstation relay apart from
|
|
167
|
-
// the operator-facing self-hosted relay. Standalone relay installs
|
|
168
|
-
// default to remote mode.
|
|
169
|
-
RELAY_DEPLOYMENT_MODE: 'local',
|
|
170
|
-
RELAY_BIND: bindAddress(config),
|
|
171
|
-
RELAY_PORT: String(config.relay.port),
|
|
172
|
-
RELAY_PUBLIC_URL: publicUrl,
|
|
173
|
-
// A relay we start ourselves is closed to everything but this
|
|
174
|
-
// workstation: reusing the host token as its password costs nothing and
|
|
175
|
-
// stops another machine on the LAN from enrolling into it.
|
|
176
|
-
RELAY_PASSWORD: state.hostToken,
|
|
177
|
-
RELAY_AUTH_STATE_FILE: relayAuthStatePath(),
|
|
178
|
-
RELAY_ALLOWED_ORIGINS: (config.relay.allowedOrigins || []).join(','),
|
|
179
|
-
RELAY_MAX_CLIENTS_PER_HOST: String(config.relay.maxClientsPerHost),
|
|
180
|
-
RELAY_MAX_HOSTS: String(config.relay.maxHosts),
|
|
181
|
-
RELAY_MAX_PENDING_HANDSHAKES: String(config.relay.maxPendingHandshakes),
|
|
182
|
-
RELAY_MAX_BUFFERED_BYTES_PER_CLIENT: String(config.relay.maxBufferedBytesPerClient),
|
|
183
|
-
RELAY_HOST_RECONNECT_GRACE_MS: String(config.relay.hostReconnectGraceMs),
|
|
184
|
-
},
|
|
185
|
-
});
|
|
186
|
-
}
|
|
187
|
-
|
|
188
|
-
const terminalPalette = hostTerminalPalette();
|
|
189
|
-
const terminalFont = hostTerminalFont();
|
|
190
|
-
|
|
191
|
-
specs.push({
|
|
192
|
-
name: 'host',
|
|
193
|
-
command: process.execPath,
|
|
194
|
-
args: [path.join(PACKAGE_ROOT, 'src', 'host-connector.js')],
|
|
195
|
-
env: {
|
|
196
|
-
// Captured here, where a terminal may still be attached, because the
|
|
197
|
-
// connector itself usually runs detached with no terminal to ask.
|
|
198
|
-
...(terminalPalette ? { HERDR_TERM_PALETTE_JSON: JSON.stringify(terminalPalette) } : {}),
|
|
199
|
-
...(terminalFont ? { HERDR_TERM_FONT_JSON: JSON.stringify(terminalFont) } : {}),
|
|
200
|
-
RELAY_URL: resolveHostRelayUrl(config),
|
|
201
|
-
RELAY_HOST_ID: state.hostId,
|
|
202
|
-
RELAY_HOST_TOKEN: state.hostToken,
|
|
203
|
-
RELAY_PASSWORD: runsLocalRelay(config) ? state.hostToken : (state.relayPassword || ''),
|
|
204
|
-
HERDR_SOCKET_PATH: resolveSocketPath(config.herdr.socketPath),
|
|
205
|
-
HERDR_ARGS_JSON: JSON.stringify(config.herdr.args),
|
|
206
|
-
HERDR_CWD: config.herdr.cwd,
|
|
207
|
-
HERDR_BIN_PATH: resolveHerdrCommand(),
|
|
208
|
-
},
|
|
209
|
-
});
|
|
210
|
-
|
|
211
|
-
return specs;
|
|
212
|
-
}
|
|
213
|
-
|
|
214
|
-
function baseEnvironment() {
|
|
215
|
-
return { ...process.env, HERDR_REMOTE_SERVICE: '1' };
|
|
216
|
-
}
|
|
217
|
-
|
|
218
|
-
function spawnDetached(spec) {
|
|
219
|
-
ensureDir(stateDir());
|
|
220
|
-
const logFd = fs.openSync(logPath(spec.name), 'a');
|
|
221
|
-
try {
|
|
222
|
-
const child = spawn(spec.command, spec.args, {
|
|
223
|
-
cwd: PACKAGE_ROOT,
|
|
224
|
-
env: { ...baseEnvironment(), ...spec.env },
|
|
225
|
-
detached: true,
|
|
226
|
-
stdio: ['ignore', logFd, logFd],
|
|
227
|
-
});
|
|
228
|
-
child.unref();
|
|
229
|
-
return child.pid;
|
|
230
|
-
} finally {
|
|
231
|
-
fs.closeSync(logFd);
|
|
232
|
-
}
|
|
233
|
-
}
|
|
234
|
-
|
|
235
|
-
/**
|
|
236
|
-
* Remember a process we started, in a list that survives being overwritten.
|
|
237
|
-
*
|
|
238
|
-
* `relayPid`/`hostPid` alone were not enough: the supervisor rewrote them with
|
|
239
|
-
* its own children, which orphaned anything an earlier detached start had
|
|
240
|
-
* spawned. Those orphans kept the port and the host session, so the relay could
|
|
241
|
-
* never bind and "stop" had nothing left to kill. The ledger is append-only
|
|
242
|
-
* (minus dead entries) precisely so no writer can lose another writer's pids.
|
|
243
|
-
*/
|
|
244
|
-
function recordManagedPid(state, name, pid) {
|
|
245
|
-
const existing = Array.isArray(state.managedPids) ? state.managedPids : [];
|
|
246
|
-
const kept = existing.filter((entry) => entry && entry.pid !== pid && pidAlive(entry.pid));
|
|
247
|
-
// Only track something that is actually running: a pid that already exited
|
|
248
|
-
// would sit in the ledger until its number is recycled by an unrelated
|
|
249
|
-
// process, which we would then happily signal.
|
|
250
|
-
if (Number.isInteger(pid) && pidAlive(pid)) {
|
|
251
|
-
kept.push({ name, pid, startedAt: new Date().toISOString() });
|
|
252
|
-
}
|
|
253
|
-
state.managedPids = kept;
|
|
254
|
-
return state;
|
|
255
|
-
}
|
|
256
|
-
|
|
257
|
-
/** Every process we believe we started, alive right now. */
|
|
258
|
-
function managedPids(state = readRuntime()) {
|
|
259
|
-
const pids = new Map();
|
|
260
|
-
// Supervisor first and never overwritten: `stopServices` relies on the order
|
|
261
|
-
// to signal it before its children, and one pid can appear under several
|
|
262
|
-
// fields.
|
|
263
|
-
for (const [name, pid] of [['supervisor', state.supervisorPid], ['host', state.hostPid], ['relay', state.relayPid]]) {
|
|
264
|
-
if (pidAlive(pid) && !pids.has(pid)) pids.set(pid, name);
|
|
265
|
-
}
|
|
266
|
-
for (const entry of state.managedPids || []) {
|
|
267
|
-
if (entry && pidAlive(entry.pid) && !pids.has(entry.pid)) pids.set(entry.pid, entry.name);
|
|
268
|
-
}
|
|
269
|
-
return [...pids].map(([pid, name]) => ({ pid, name }));
|
|
270
|
-
}
|
|
271
|
-
|
|
272
|
-
function startServices() {
|
|
273
|
-
const config = loadConfig();
|
|
274
|
-
const state = ensureRuntime();
|
|
275
|
-
const next = { ...state };
|
|
276
|
-
const specs = serviceSpecs(config, state);
|
|
277
|
-
|
|
278
|
-
for (const spec of specs) {
|
|
279
|
-
const pidKey = `${spec.name}Pid`;
|
|
280
|
-
if (pidAlive(next[pidKey])) continue;
|
|
281
|
-
next[pidKey] = spawnDetached(spec);
|
|
282
|
-
recordManagedPid(next, spec.name, next[pidKey]);
|
|
283
|
-
}
|
|
284
|
-
// A relay that is no longer part of the plan (switched to remote mode) must
|
|
285
|
-
// not be left running on the old port.
|
|
286
|
-
if (!specs.some((spec) => spec.name === 'relay') && pidAlive(next.relayPid)) {
|
|
287
|
-
try { process.kill(next.relayPid, 'SIGTERM'); } catch {}
|
|
288
|
-
next.relayPid = null;
|
|
289
|
-
}
|
|
290
|
-
|
|
291
|
-
next.startedAt = next.startedAt || new Date().toISOString();
|
|
292
|
-
next.mode = config.relay.mode;
|
|
293
|
-
writeJsonAtomic(runtimeStatePath(), next);
|
|
294
|
-
return {
|
|
295
|
-
ok: true,
|
|
296
|
-
mode: config.relay.mode,
|
|
297
|
-
relay: {
|
|
298
|
-
local: runsLocalRelay(config),
|
|
299
|
-
pid: next.relayPid || null,
|
|
300
|
-
alive: pidAlive(next.relayPid),
|
|
301
|
-
bind: runsLocalRelay(config) ? bindAddress(config) : null,
|
|
302
|
-
port: config.relay.port,
|
|
303
|
-
remoteUrl: config.relay.remoteUrl || null,
|
|
304
|
-
},
|
|
305
|
-
host: {
|
|
306
|
-
pid: next.hostPid || null,
|
|
307
|
-
alive: pidAlive(next.hostPid),
|
|
308
|
-
socketPath: resolveSocketPath(config.herdr.socketPath),
|
|
309
|
-
},
|
|
310
|
-
publicUrl: resolvePublicUrl(config, preferredLanAddress()),
|
|
311
|
-
};
|
|
312
|
-
}
|
|
313
|
-
|
|
314
|
-
function stopServices() {
|
|
315
|
-
const state = readRuntime();
|
|
316
|
-
const stopped = [];
|
|
317
|
-
|
|
318
|
-
// Supervisor first: it would otherwise see its children die and restart them
|
|
319
|
-
// faster than we can kill them.
|
|
320
|
-
const targets = managedPids(state).sort((a, b) => (
|
|
321
|
-
(a.name === 'supervisor' ? 0 : 1) - (b.name === 'supervisor' ? 0 : 1)
|
|
322
|
-
));
|
|
323
|
-
|
|
324
|
-
for (const { pid, name } of targets) {
|
|
325
|
-
try {
|
|
326
|
-
process.kill(pid, 'SIGTERM');
|
|
327
|
-
stopped.push({ name, pid });
|
|
328
|
-
} catch (error) {
|
|
329
|
-
process.stderr.write(`herdr-remote: could not stop ${name} (pid ${pid}): ${error.message}\n`);
|
|
330
|
-
}
|
|
331
|
-
}
|
|
332
|
-
|
|
333
|
-
state.hostPid = null;
|
|
334
|
-
state.relayPid = null;
|
|
335
|
-
state.supervisorPid = null;
|
|
336
|
-
state.managedPids = [];
|
|
337
|
-
state.startedAt = null;
|
|
338
|
-
ensureDir(stateDir());
|
|
339
|
-
writeJsonAtomic(runtimeStatePath(), state);
|
|
340
|
-
return { ok: true, stopped };
|
|
341
|
-
}
|
|
342
|
-
|
|
343
|
-
function restartServices() {
|
|
344
|
-
stopServices();
|
|
345
|
-
return startServices();
|
|
346
|
-
}
|
|
347
|
-
|
|
348
|
-
/**
|
|
349
|
-
* Minimal JSON client. Picks http or https from the URL: a self-hosted relay is
|
|
350
|
-
* reached over https, while the local relay is plain http on loopback.
|
|
351
|
-
*/
|
|
352
|
-
function requestJson(urlString, options = {}) {
|
|
353
|
-
return new Promise((resolve, reject) => {
|
|
354
|
-
let url;
|
|
355
|
-
try { url = new URL(urlString); } catch (error) { return reject(error); }
|
|
356
|
-
const transport = url.protocol === 'https:' ? https : http;
|
|
357
|
-
const request = transport.request(url, {
|
|
358
|
-
method: options.method || 'GET',
|
|
359
|
-
headers: options.headers || {},
|
|
360
|
-
timeout: options.timeout || 1500,
|
|
361
|
-
}, (response) => {
|
|
362
|
-
const chunks = [];
|
|
363
|
-
response.on('data', (chunk) => chunks.push(chunk));
|
|
364
|
-
response.on('end', () => {
|
|
365
|
-
const text = Buffer.concat(chunks).toString('utf8');
|
|
366
|
-
let body;
|
|
367
|
-
try { body = JSON.parse(text); } catch { body = { raw: text }; }
|
|
368
|
-
if (response.statusCode >= 400) {
|
|
369
|
-
const error = new Error(body.message || `HTTP ${response.statusCode}`);
|
|
370
|
-
error.statusCode = response.statusCode;
|
|
371
|
-
error.body = body;
|
|
372
|
-
reject(error);
|
|
373
|
-
} else resolve(body);
|
|
374
|
-
});
|
|
375
|
-
});
|
|
376
|
-
request.on('timeout', () => request.destroy(new Error('request timed out')));
|
|
377
|
-
request.on('error', reject);
|
|
378
|
-
if (options.body) request.write(options.body);
|
|
379
|
-
request.end();
|
|
380
|
-
});
|
|
381
|
-
}
|
|
382
|
-
|
|
383
|
-
function healthUrl(config) {
|
|
384
|
-
return `${resolveAdminOrigin(config)}/healthz`;
|
|
385
|
-
}
|
|
386
|
-
|
|
387
|
-
async function waitForRelay(config, { attempts = 20, delayMs = 100 } = {}) {
|
|
388
|
-
let lastError;
|
|
389
|
-
for (let attempt = 0; attempt < attempts; attempt += 1) {
|
|
390
|
-
try { return await requestJson(healthUrl(config), { timeout: 800 }); } catch (error) { lastError = error; }
|
|
391
|
-
await new Promise((resolve) => setTimeout(resolve, delayMs));
|
|
392
|
-
}
|
|
393
|
-
throw lastError || new Error('relay did not become ready');
|
|
394
|
-
}
|
|
395
|
-
|
|
396
|
-
async function waitForHost(config, { attempts = 30, delayMs = 100 } = {}) {
|
|
397
|
-
const state = ensureRuntime();
|
|
398
|
-
const statusEndpoint = `${resolveAdminOrigin(config)}/api/status`;
|
|
399
|
-
let lastStatus = null;
|
|
400
|
-
for (let attempt = 0; attempt < attempts; attempt += 1) {
|
|
401
|
-
try {
|
|
402
|
-
lastStatus = await requestJson(statusEndpoint, {
|
|
403
|
-
timeout: 800,
|
|
404
|
-
headers: {
|
|
405
|
-
'X-Herdr-Host-Id': state.hostId,
|
|
406
|
-
'X-Herdr-Host-Token': state.hostToken,
|
|
407
|
-
},
|
|
408
|
-
});
|
|
409
|
-
if (lastStatus.hosts?.some((host) => host.id === state.hostId)) return lastStatus;
|
|
410
|
-
} catch (error) {
|
|
411
|
-
lastStatus = { ok: false, message: error.message };
|
|
412
|
-
}
|
|
413
|
-
await new Promise((resolve) => setTimeout(resolve, delayMs));
|
|
414
|
-
}
|
|
415
|
-
const error = new Error(lastStatus?.message || 'the Herdr host connector did not register with the relay');
|
|
416
|
-
error.health = lastStatus;
|
|
417
|
-
throw error;
|
|
418
|
-
}
|
|
419
|
-
|
|
420
|
-
async function statusServices() {
|
|
421
|
-
const config = loadConfig();
|
|
422
|
-
const state = readRuntime();
|
|
423
|
-
const lanAddress = preferredLanAddress();
|
|
424
|
-
let health = null;
|
|
425
|
-
try {
|
|
426
|
-
health = await requestJson(healthUrl(config), { timeout: 1200 });
|
|
427
|
-
} catch (error) {
|
|
428
|
-
health = { ok: false, message: error.message };
|
|
429
|
-
}
|
|
430
|
-
const socketPath = resolveSocketPath(config.herdr.socketPath);
|
|
431
|
-
// Where Herdr was found, and whether it was found at all: under a service
|
|
432
|
-
// manager this is the first thing to check when sessions will not start.
|
|
433
|
-
const herdr = findHerdrCommand();
|
|
434
|
-
// Only worth spawning when there is something to spawn; an unresolved command
|
|
435
|
-
// has already answered the question the version would.
|
|
436
|
-
const installed = herdr.found ? herdrVersion({ command: herdr.command }) : { version: null, supported: true };
|
|
437
|
-
return {
|
|
438
|
-
ok: true,
|
|
439
|
-
mode: config.relay.mode,
|
|
440
|
-
relay: {
|
|
441
|
-
local: runsLocalRelay(config),
|
|
442
|
-
pid: state.relayPid || null,
|
|
443
|
-
alive: runsLocalRelay(config) ? pidAlive(state.relayPid) : null,
|
|
444
|
-
bind: runsLocalRelay(config) ? bindAddress(config) : null,
|
|
445
|
-
port: config.relay.port,
|
|
446
|
-
remoteUrl: config.relay.remoteUrl || null,
|
|
447
|
-
health,
|
|
448
|
-
},
|
|
449
|
-
host: {
|
|
450
|
-
pid: state.hostPid || null,
|
|
451
|
-
alive: pidAlive(state.hostPid),
|
|
452
|
-
hostId: state.hostId || null,
|
|
453
|
-
socketPath,
|
|
454
|
-
socketExists: Boolean(socketPath) && fs.existsSync(socketPath),
|
|
455
|
-
herdrCommand: herdr.command,
|
|
456
|
-
herdrCommandFound: herdr.found,
|
|
457
|
-
herdrCommandSource: herdr.source,
|
|
458
|
-
herdrVersion: installed.version,
|
|
459
|
-
herdrVersionSupported: installed.supported,
|
|
460
|
-
herdrVersionMinimum: MIN_HERDR_VERSION,
|
|
461
|
-
},
|
|
462
|
-
publicUrl: resolvePublicUrl(config, lanAddress),
|
|
463
|
-
startedAt: state.startedAt || null,
|
|
464
|
-
};
|
|
465
|
-
}
|
|
466
|
-
|
|
467
|
-
async function pair() {
|
|
468
|
-
const config = loadConfig();
|
|
469
|
-
const state = ensureRuntime();
|
|
470
|
-
if (runsLocalRelay(config)) startServices();
|
|
471
|
-
await waitForRelay(config);
|
|
472
|
-
await waitForHost(config);
|
|
473
|
-
// Authenticated with this workstation's own host token: on a shared relay
|
|
474
|
-
// that is what proves the pairing code is being minted for our terminal and
|
|
475
|
-
// nobody else's.
|
|
476
|
-
const pairing = await requestJson(`${resolveAdminOrigin(config)}/api/pair/start`, {
|
|
477
|
-
method: 'POST',
|
|
478
|
-
headers: {
|
|
479
|
-
'X-Herdr-Host-Id': state.hostId,
|
|
480
|
-
'X-Herdr-Host-Token': state.hostToken,
|
|
481
|
-
},
|
|
482
|
-
timeout: 5000,
|
|
483
|
-
});
|
|
484
|
-
return pairing;
|
|
485
|
-
}
|
|
486
|
-
|
|
487
|
-
function extractPairingCode(response) {
|
|
488
|
-
const code = response?.code || response?.pairCode;
|
|
489
|
-
if (typeof code !== 'string' || code.length === 0) {
|
|
490
|
-
throw new Error('the relay response did not contain a valid pairing code');
|
|
491
|
-
}
|
|
492
|
-
return code;
|
|
493
|
-
}
|
|
494
|
-
|
|
495
|
-
function readLogTail(name, lines = 40) {
|
|
496
|
-
try {
|
|
497
|
-
const content = fs.readFileSync(logPath(name), 'utf8');
|
|
498
|
-
return content.split('\n').filter(Boolean).slice(-lines);
|
|
499
|
-
} catch {
|
|
500
|
-
return [];
|
|
501
|
-
}
|
|
502
|
-
}
|
|
503
|
-
|
|
504
|
-
module.exports = {
|
|
505
|
-
RUNTIME_VERSION,
|
|
506
|
-
pidAlive,
|
|
507
|
-
readRuntime,
|
|
508
|
-
ensureRuntime,
|
|
509
|
-
recordManagedPid,
|
|
510
|
-
managedPids,
|
|
511
|
-
setRelayPassword,
|
|
512
|
-
regenerateHostIdentity,
|
|
513
|
-
serviceSpecs,
|
|
514
|
-
baseEnvironment,
|
|
515
|
-
hostTerminalPalette,
|
|
516
|
-
hostTerminalFont,
|
|
517
|
-
startServices,
|
|
518
|
-
stopServices,
|
|
519
|
-
restartServices,
|
|
520
|
-
statusServices,
|
|
521
|
-
requestJson,
|
|
522
|
-
waitForRelay,
|
|
523
|
-
waitForHost,
|
|
524
|
-
pair,
|
|
525
|
-
extractPairingCode,
|
|
526
|
-
relayBinPath,
|
|
527
|
-
relayAuthStatePath,
|
|
528
|
-
logPath,
|
|
529
|
-
readLogTail,
|
|
530
|
-
};
|