herdr-remote 0.2.38 → 0.3.31
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/dist/chunks/353DMT35.js +3 -0
- package/dist/chunks/42KARAL4.js +31 -0
- package/dist/chunks/7AUQHB3O.js +10 -0
- package/dist/chunks/7SMZ43LI.js +9 -0
- package/dist/chunks/AM3OEMFK.js +16 -0
- package/dist/chunks/ANNJESOL.js +10 -0
- package/dist/chunks/AWB4A65P.js +63 -0
- package/dist/chunks/C2UUZY6J.js +2 -0
- package/dist/chunks/DGUZCGQN.js +3 -0
- package/dist/chunks/H24ZP5D5.js +16 -0
- package/dist/chunks/I6NRCYLI.js +3 -0
- package/dist/chunks/JJGEV4EZ.js +3 -0
- package/dist/chunks/KRSURB5L.js +5 -0
- package/dist/chunks/KRVF37OW.js +3 -0
- package/dist/chunks/KUDCC7ZO.js +6 -0
- package/dist/chunks/M3NRPJK2.js +77 -0
- package/dist/chunks/M4AFZHKH.js +3 -0
- package/dist/chunks/M52LJEES.js +6 -0
- package/dist/chunks/O2LNVFUT.js +3 -0
- package/dist/chunks/OJPBI7KB.js +3 -0
- package/dist/chunks/OMN6C7J6.js +3 -0
- package/dist/chunks/PBMBFWLQ.js +3 -0
- package/dist/chunks/PBRQ3HEG.js +3 -0
- package/dist/chunks/PFFVGFPL.js +45 -0
- package/dist/chunks/QRGNEKME.js +5 -0
- package/dist/chunks/TNLCNVA2.js +3 -0
- package/dist/chunks/U27JJKJ6.js +3 -0
- package/dist/chunks/U6QAYG3E.js +3 -0
- package/dist/chunks/UEOQE77N.js +4 -0
- package/dist/chunks/WSACSE45.js +4 -0
- package/dist/chunks/WY7WHEJ4.js +3 -0
- package/dist/chunks/XJKWLK43.js +4 -0
- package/dist/chunks/XN6D5S55.js +3 -0
- package/dist/chunks/YD3MIP7B.js +5 -0
- package/dist/chunks/YXDQ62BB.js +4 -0
- package/dist/cli.js +21 -294
- package/dist/connector/admin-broker.js +3 -302
- package/dist/connector/main.js +20 -18
- package/dist/harfbuzz-subset.wasm +0 -0
- package/dist/postinstall.js +17 -0
- package/herdr-plugin.toml +1 -1
- package/node_modules/herdr-remote-relay/bin/herdr-remote-relay.js +33 -92
- package/node_modules/herdr-remote-relay/package.json +3 -46
- package/package.json +8 -27
- package/dist/agent-status.js +0 -130
- package/dist/binary-plist.js +0 -157
- package/dist/config.js +0 -278
- package/dist/connector/admin-broker-setup.js +0 -40
- package/dist/connector/admin-shell.js +0 -67
- package/dist/connector/admin-tab.js +0 -90
- package/dist/connector/adminBrokerEndpoint.js +0 -68
- package/dist/connector/agent-watch.js +0 -123
- package/dist/connector/elevatedPty.js +0 -141
- package/dist/connector/font-server.js +0 -165
- package/dist/connector/herdr-prefix-keys.js +0 -28
- package/dist/connector/host-connector.js +0 -433
- package/dist/connector/host-hello.js +0 -32
- package/dist/connector/lock.js +0 -73
- package/dist/connector/sessions.js +0 -381
- package/dist/connector/update-report.js +0 -70
- package/dist/connector/wire.js +0 -42
- package/dist/exit-codes.js +0 -13
- package/dist/font-subset.js +0 -110
- package/dist/herdr-api.js +0 -254
- package/dist/herdr-command.js +0 -245
- package/dist/herdr-plugin.js +0 -120
- package/dist/herdr-server.js +0 -190
- package/dist/i18n/en.js +0 -257
- package/dist/i18n/index.js +0 -48
- package/dist/i18n/zh.js +0 -256
- package/dist/keepalive/detached.js +0 -97
- package/dist/keepalive/environment.js +0 -44
- package/dist/keepalive/index.js +0 -129
- package/dist/keepalive/launchd.js +0 -117
- package/dist/keepalive/systemd.js +0 -112
- package/dist/keepalive/types.js +0 -1
- package/dist/keepalive/windows-service-setup.js +0 -138
- package/dist/keepalive/windows-service.js +0 -272
- package/dist/keepalive/windows-task.js +0 -191
- package/dist/lib/process.js +0 -132
- package/dist/lifecycle.js +0 -79
- package/dist/net-interfaces.js +0 -82
- package/dist/node-pty-loader.js +0 -80
- package/dist/pasted-files.js +0 -115
- package/dist/paths.js +0 -74
- package/dist/pty-session.js +0 -133
- package/dist/relay-client.js +0 -106
- package/dist/relay-urls.js +0 -99
- package/dist/runtime.js +0 -107
- package/dist/service.js +0 -267
- package/dist/settings-model.js +0 -298
- package/dist/socket-discovery.js +0 -70
- package/dist/supervisor.js +0 -323
- package/dist/terminal-font/deps.js +0 -83
- package/dist/terminal-font/files.js +0 -357
- package/dist/terminal-font/formats.js +0 -355
- package/dist/terminal-font/index.js +0 -187
- package/dist/terminal-font/readers.js +0 -373
- package/dist/terminal-font/terminals.js +0 -147
- package/dist/terminal-palette.js +0 -399
- package/dist/tui/App.js +0 -199
- package/dist/tui/api.js +0 -91
- package/dist/tui/components/TextField.js +0 -50
- package/dist/tui/components/common.js +0 -80
- package/dist/tui/index.js +0 -36
- package/dist/tui/mouse/index.js +0 -164
- package/dist/tui/mouse/protocol.js +0 -96
- package/dist/tui/mouse/source.js +0 -79
- package/dist/tui/screens/About.js +0 -172
- package/dist/tui/screens/Herdr.js +0 -75
- package/dist/tui/screens/Keepalive.js +0 -141
- package/dist/tui/screens/Overview.js +0 -36
- package/dist/tui/screens/Pair.js +0 -74
- package/dist/tui/screens/Relay.js +0 -243
- package/dist/tui/screens/Services.js +0 -63
- package/dist/tui/screens/Wizard.js +0 -241
- package/dist/tui/terminal.js +0 -39
- package/dist/tui/theme.js +0 -33
- package/dist/update-command.js +0 -118
- package/dist/updater/in-place.js +0 -83
- package/dist/updater/npm.js +0 -62
- package/dist/updater.js +0 -401
- package/node_modules/herdr-remote-relay/README.md +0 -76
- package/node_modules/herdr-remote-relay/README.zh-CN.md +0 -76
- package/node_modules/herdr-remote-relay/config.example.json +0 -18
- package/node_modules/herdr-remote-relay/deploy/cloudflared/config.yml.example +0 -9
- package/node_modules/herdr-remote-relay/deploy/docker/compose.yaml +0 -23
- package/node_modules/herdr-remote-relay/deploy/nginx/herdr-remote-relay.conf.example +0 -49
- package/node_modules/herdr-remote-relay/deploy/systemd/herdr-remote-relay.service +0 -41
- package/node_modules/herdr-remote-relay/deploy/systemd/relay.env.example +0 -25
- package/node_modules/herdr-remote-relay/dist/auth-store.d.ts +0 -114
- package/node_modules/herdr-remote-relay/dist/auth-store.js +0 -289
- package/node_modules/herdr-remote-relay/dist/metrics.d.ts +0 -76
- package/node_modules/herdr-remote-relay/dist/metrics.js +0 -249
- package/node_modules/herdr-remote-relay/dist/protocol/frames.d.ts +0 -27
- package/node_modules/herdr-remote-relay/dist/protocol/frames.js +0 -110
- package/node_modules/herdr-remote-relay/dist/protocol/herdr-keys.d.ts +0 -8
- package/node_modules/herdr-remote-relay/dist/protocol/herdr-keys.js +0 -34
- package/node_modules/herdr-remote-relay/dist/protocol/http.d.ts +0 -165
- package/node_modules/herdr-remote-relay/dist/protocol/http.js +0 -3
- package/node_modules/herdr-remote-relay/dist/protocol/index.d.ts +0 -7
- package/node_modules/herdr-remote-relay/dist/protocol/index.js +0 -9
- package/node_modules/herdr-remote-relay/dist/protocol/messages.d.ts +0 -362
- package/node_modules/herdr-remote-relay/dist/protocol/messages.js +0 -20
- package/node_modules/herdr-remote-relay/dist/protocol/paste.d.ts +0 -14
- package/node_modules/herdr-remote-relay/dist/protocol/paste.js +0 -50
- package/node_modules/herdr-remote-relay/dist/protocol/release.d.ts +0 -8
- package/node_modules/herdr-remote-relay/dist/protocol/release.js +0 -17
- package/node_modules/herdr-remote-relay/dist/protocol/terminal.d.ts +0 -79
- package/node_modules/herdr-remote-relay/dist/protocol/terminal.js +0 -150
- package/node_modules/herdr-remote-relay/dist/relay-config.d.ts +0 -74
- package/node_modules/herdr-remote-relay/dist/relay-config.js +0 -316
- package/node_modules/herdr-remote-relay/dist/relay-server.d.ts +0 -67
- package/node_modules/herdr-remote-relay/dist/relay-server.js +0 -196
- package/node_modules/herdr-remote-relay/dist/scroll-input.d.ts +0 -17
- package/node_modules/herdr-remote-relay/dist/scroll-input.js +0 -124
- package/node_modules/herdr-remote-relay/dist/server/client-channel.d.ts +0 -3
- package/node_modules/herdr-remote-relay/dist/server/client-channel.js +0 -396
- package/node_modules/herdr-remote-relay/dist/server/font-proxy.d.ts +0 -19
- package/node_modules/herdr-remote-relay/dist/server/font-proxy.js +0 -155
- package/node_modules/herdr-remote-relay/dist/server/host-broadcasts.d.ts +0 -27
- package/node_modules/herdr-remote-relay/dist/server/host-broadcasts.js +0 -83
- package/node_modules/herdr-remote-relay/dist/server/host-channel.d.ts +0 -3
- package/node_modules/herdr-remote-relay/dist/server/host-channel.js +0 -271
- package/node_modules/herdr-remote-relay/dist/server/http-api.d.ts +0 -4
- package/node_modules/herdr-remote-relay/dist/server/http-api.js +0 -219
- package/node_modules/herdr-remote-relay/dist/server/maintenance.d.ts +0 -5
- package/node_modules/herdr-remote-relay/dist/server/maintenance.js +0 -64
- package/node_modules/herdr-remote-relay/dist/server/requests.d.ts +0 -34
- package/node_modules/herdr-remote-relay/dist/server/requests.js +0 -142
- package/node_modules/herdr-remote-relay/dist/server/sessions.d.ts +0 -36
- package/node_modules/herdr-remote-relay/dist/server/sessions.js +0 -230
- package/node_modules/herdr-remote-relay/dist/server/sockets.d.ts +0 -12
- package/node_modules/herdr-remote-relay/dist/server/sockets.js +0 -64
- package/node_modules/herdr-remote-relay/dist/server/status.d.ts +0 -11
- package/node_modules/herdr-remote-relay/dist/server/status.js +0 -99
- package/node_modules/herdr-remote-relay/dist/server/transport.d.ts +0 -16
- package/node_modules/herdr-remote-relay/dist/server/transport.js +0 -110
- package/node_modules/herdr-remote-relay/dist/server/types.d.ts +0 -133
- package/node_modules/herdr-remote-relay/dist/server/types.js +0 -3
- package/node_modules/herdr-remote-relay/dist/server/version.d.ts +0 -2
- package/node_modules/herdr-remote-relay/dist/server/version.js +0 -5
- package/node_modules/herdr-remote-relay/dist/state.d.ts +0 -6
- package/node_modules/herdr-remote-relay/dist/state.js +0 -47
- package/node_modules/ws/LICENSE +0 -20
- package/node_modules/ws/README.md +0 -548
- package/node_modules/ws/browser.js +0 -8
- package/node_modules/ws/index.js +0 -22
- package/node_modules/ws/lib/buffer-util.js +0 -131
- package/node_modules/ws/lib/constants.js +0 -19
- package/node_modules/ws/lib/event-target.js +0 -292
- package/node_modules/ws/lib/extension.js +0 -203
- package/node_modules/ws/lib/limiter.js +0 -55
- package/node_modules/ws/lib/permessage-deflate.js +0 -530
- package/node_modules/ws/lib/receiver.js +0 -743
- package/node_modules/ws/lib/sender.js +0 -607
- package/node_modules/ws/lib/stream.js +0 -161
- package/node_modules/ws/lib/subprotocol.js +0 -62
- package/node_modules/ws/lib/validation.js +0 -152
- package/node_modules/ws/lib/websocket-server.js +0 -562
- package/node_modules/ws/lib/websocket.js +0 -1407
- package/node_modules/ws/package.json +0 -74
- package/node_modules/ws/wrapper.mjs +0 -21
|
@@ -1,67 +0,0 @@
|
|
|
1
|
-
// `herdr-remote admin-shell`: the administrator terminal inside a Herdr tab.
|
|
2
|
-
//
|
|
3
|
-
// This process runs in the tab's pane with the pane's standard rights and
|
|
4
|
-
// relays its console to an elevated PowerShell that the admin broker runs. On
|
|
5
|
-
// Windows, Node's raw mode reads keys as VT input and writes VT output through
|
|
6
|
-
// unchanged, so the relay is byte for byte.
|
|
7
|
-
import { requestHerdr } from '../herdr-api.js';
|
|
8
|
-
import { ADMIN_TAB_ENV } from './admin-tab.js';
|
|
9
|
-
import { ElevatedPty } from './elevatedPty.js';
|
|
10
|
-
const BROKER_UNAVAILABLE_CODES = ['ENOENT', 'ECONNREFUSED', 'ETIMEDOUT'];
|
|
11
|
-
/** Close the pane this runs in, which closes the tab `openAdminTab` made for it. */
|
|
12
|
-
async function closeOwnPane() {
|
|
13
|
-
const socketPath = process.env.HERDR_SOCKET_PATH;
|
|
14
|
-
const paneId = process.env.HERDR_PANE_ID;
|
|
15
|
-
if (process.env[ADMIN_TAB_ENV] !== '1' || !socketPath || !paneId)
|
|
16
|
-
return;
|
|
17
|
-
try {
|
|
18
|
-
await requestHerdr(socketPath, 'pane.close', { pane_id: paneId });
|
|
19
|
-
}
|
|
20
|
-
catch {
|
|
21
|
-
// The shell's prompt comes back instead; the user can close the tab.
|
|
22
|
-
}
|
|
23
|
-
}
|
|
24
|
-
export async function runAdminShell(t, { stdin = process.stdin, stdout = process.stdout, platform = process.platform, } = {}) {
|
|
25
|
-
if (platform !== 'win32')
|
|
26
|
-
throw new Error(t('adminBroker.windowsOnly'));
|
|
27
|
-
if (!stdin.isTTY || !stdout.isTTY)
|
|
28
|
-
throw new Error(t('adminShell.needsTerminal'));
|
|
29
|
-
const pty = new ElevatedPty({
|
|
30
|
-
command: 'powershell.exe',
|
|
31
|
-
args: ['-NoLogo', '-NoProfile'],
|
|
32
|
-
cwd: process.cwd(),
|
|
33
|
-
});
|
|
34
|
-
const relayInput = (data) => pty.write(data);
|
|
35
|
-
const relayResize = () => pty.resize(stdout.columns, stdout.rows);
|
|
36
|
-
stdin.setRawMode(true);
|
|
37
|
-
stdin.on('data', relayInput);
|
|
38
|
-
// The CLI's Windows color probe pauses stdin before any command runs, and a
|
|
39
|
-
// paused stream ignores a new 'data' listener: no key would reach PowerShell.
|
|
40
|
-
stdin.resume();
|
|
41
|
-
stdout.on('resize', relayResize);
|
|
42
|
-
try {
|
|
43
|
-
const exitCode = await new Promise((resolve, reject) => {
|
|
44
|
-
pty.start({
|
|
45
|
-
cols: stdout.columns,
|
|
46
|
-
rows: stdout.rows,
|
|
47
|
-
onData: (data) => stdout.write(data),
|
|
48
|
-
onExit: (event) => resolve(event.exitCode),
|
|
49
|
-
onError: reject,
|
|
50
|
-
});
|
|
51
|
-
});
|
|
52
|
-
await closeOwnPane();
|
|
53
|
-
return exitCode;
|
|
54
|
-
}
|
|
55
|
-
catch (error) {
|
|
56
|
-
const { code } = error;
|
|
57
|
-
if (code && BROKER_UNAVAILABLE_CODES.includes(code))
|
|
58
|
-
throw new Error(t('adminShell.brokerUnavailable'));
|
|
59
|
-
throw error;
|
|
60
|
-
}
|
|
61
|
-
finally {
|
|
62
|
-
stdin.off('data', relayInput);
|
|
63
|
-
stdout.off('resize', relayResize);
|
|
64
|
-
stdin.setRawMode(false);
|
|
65
|
-
stdin.pause();
|
|
66
|
-
}
|
|
67
|
-
}
|
|
@@ -1,90 +0,0 @@
|
|
|
1
|
-
// An administrator terminal as a tab of the workstation's own Herdr, opened at
|
|
2
|
-
// a browser's request.
|
|
3
|
-
//
|
|
4
|
-
// Herdr's API cannot start a tab running a command of our choosing, so the tab
|
|
5
|
-
// starts with the user's shell and `herdr-remote admin-shell` is typed into it
|
|
6
|
-
// once that shell is running. Nor can the API focus a tab for one client:
|
|
7
|
-
// `tab.focus` moves every client attached to this Herdr, which is the price of
|
|
8
|
-
// the requesting window showing the new tab at once.
|
|
9
|
-
import path from 'node:path';
|
|
10
|
-
import { requestHerdr } from '../herdr-api.js';
|
|
11
|
-
import { createTranslator, detectLocale } from '../i18n/index.js';
|
|
12
|
-
import { cliEntryPoint } from '../keepalive/environment.js';
|
|
13
|
-
import { adminBrokerStatus } from './admin-broker.js';
|
|
14
|
-
const SHELL_START_TIMEOUT_MS = 5_000;
|
|
15
|
-
const SHELL_START_POLL_MS = 100;
|
|
16
|
-
/** Marks the tab's shell, so `admin-shell` knows it may close the pane when done. */
|
|
17
|
-
export const ADMIN_TAB_ENV = 'HERDR_REMOTE_ADMIN_TAB';
|
|
18
|
-
function powershellQuote(value) {
|
|
19
|
-
return `'${value.replaceAll("'", "''")}'`;
|
|
20
|
-
}
|
|
21
|
-
function posixQuote(value) {
|
|
22
|
-
return `'${value.replaceAll("'", "'\\''")}'`;
|
|
23
|
-
}
|
|
24
|
-
/** The command line that starts `admin-shell`, in the syntax of the tab's shell. */
|
|
25
|
-
export function adminShellCommand(shell, node, entry) {
|
|
26
|
-
const name = path.win32
|
|
27
|
-
.basename(shell)
|
|
28
|
-
.toLowerCase()
|
|
29
|
-
.replace(/\.exe$/, '');
|
|
30
|
-
if (name === 'powershell' || name === 'pwsh') {
|
|
31
|
-
return `& ${powershellQuote(node)} ${powershellQuote(entry)} admin-shell`;
|
|
32
|
-
}
|
|
33
|
-
if (name === 'cmd')
|
|
34
|
-
return `"${node}" "${entry}" admin-shell`;
|
|
35
|
-
// Git Bash and other POSIX shells: forward slashes, or C:\ reads as escapes.
|
|
36
|
-
const posix = (file) => posixQuote(file.replaceAll('\\', '/'));
|
|
37
|
-
return `${posix(node)} ${posix(entry)} admin-shell`;
|
|
38
|
-
}
|
|
39
|
-
/** The new tab's shell, once Herdr reports it running. */
|
|
40
|
-
async function waitForShell(request, socketPath, paneId) {
|
|
41
|
-
const deadline = Date.now() + SHELL_START_TIMEOUT_MS;
|
|
42
|
-
while (true) {
|
|
43
|
-
const info = await request(socketPath, 'pane.process_info', {
|
|
44
|
-
pane_id: paneId,
|
|
45
|
-
});
|
|
46
|
-
const shell = info.process_info?.foreground_processes?.[0]?.name;
|
|
47
|
-
if (shell)
|
|
48
|
-
return shell;
|
|
49
|
-
if (Date.now() >= deadline)
|
|
50
|
-
throw new Error('The new tab did not start a shell in time.');
|
|
51
|
-
await new Promise((resolve) => setTimeout(resolve, SHELL_START_POLL_MS));
|
|
52
|
-
}
|
|
53
|
-
}
|
|
54
|
-
export async function openAdminTab({ socketPath, label, request = requestHerdr, brokerReady = adminBrokerStatus, }) {
|
|
55
|
-
if (!(await brokerReady())) {
|
|
56
|
-
throw Object.assign(new Error('The administrator terminal broker is not running.'), {
|
|
57
|
-
code: 'admin_broker_unavailable',
|
|
58
|
-
});
|
|
59
|
-
}
|
|
60
|
-
const created = await request(socketPath, 'tab.create', {
|
|
61
|
-
label,
|
|
62
|
-
focus: false,
|
|
63
|
-
env: { [ADMIN_TAB_ENV]: '1' },
|
|
64
|
-
});
|
|
65
|
-
const paneId = created.root_pane.pane_id;
|
|
66
|
-
await request(socketPath, 'tab.focus', { tab_id: created.tab.tab_id });
|
|
67
|
-
const shell = await waitForShell(request, socketPath, paneId);
|
|
68
|
-
await request(socketPath, 'pane.send_input', {
|
|
69
|
-
pane_id: paneId,
|
|
70
|
-
text: adminShellCommand(shell, process.execPath, cliEntryPoint()),
|
|
71
|
-
keys: ['enter'],
|
|
72
|
-
});
|
|
73
|
-
}
|
|
74
|
-
/** Open the tab for a browser window, and tell that window when it could not. */
|
|
75
|
-
export async function openAdminTabFor(streamId, options) {
|
|
76
|
-
const t = createTranslator(detectLocale({ preference: options.language }));
|
|
77
|
-
try {
|
|
78
|
-
await openAdminTab({ socketPath: options.socketPath, label: t('adminShell.tabLabel') });
|
|
79
|
-
}
|
|
80
|
-
catch (error) {
|
|
81
|
-
const { code, message } = error;
|
|
82
|
-
process.stderr.write(`herdr-remote host connector: could not open an admin tab: ${message}\n`);
|
|
83
|
-
options.send({
|
|
84
|
-
type: 'error',
|
|
85
|
-
clientId: streamId,
|
|
86
|
-
code: code === 'admin_broker_unavailable' ? code : 'admin_tab_failed',
|
|
87
|
-
message,
|
|
88
|
-
});
|
|
89
|
-
}
|
|
90
|
-
}
|
|
@@ -1,68 +0,0 @@
|
|
|
1
|
-
import { createHash, randomBytes, timingSafeEqual } from 'node:crypto';
|
|
2
|
-
import fs from 'node:fs';
|
|
3
|
-
import net from 'node:net';
|
|
4
|
-
import path from 'node:path';
|
|
5
|
-
import { stateDir } from '../paths.js';
|
|
6
|
-
const ADMIN_BROKER_TASK_NAME = 'HerdrRemoteAdminBroker';
|
|
7
|
-
const ADMIN_BROKER_HOST = '127.0.0.1';
|
|
8
|
-
export const ADMIN_BROKER_MAX_MESSAGE_BYTES = 2 * 1_024 * 1_024;
|
|
9
|
-
function adminBrokerAccountId() {
|
|
10
|
-
const account = `${process.env.USERDOMAIN || ''}\\${process.env.USERNAME || process.env.USERPROFILE || 'default'}`;
|
|
11
|
-
return createHash('sha256').update(account.toLowerCase()).digest('hex').slice(0, 24);
|
|
12
|
-
}
|
|
13
|
-
export function adminBrokerTaskName() {
|
|
14
|
-
return `${ADMIN_BROKER_TASK_NAME}-${adminBrokerAccountId()}`;
|
|
15
|
-
}
|
|
16
|
-
// Not a named pipe: one created by an elevated process admits only
|
|
17
|
-
// Administrators, so the connector's standard token gets EPERM, and opening it
|
|
18
|
-
// to everyone would let any account add an instance of the same name and sit
|
|
19
|
-
// in the middle. A loopback port cannot be taken over by another account, and
|
|
20
|
-
// the token lives in the state directory, which only this account can read.
|
|
21
|
-
function endpointPath() {
|
|
22
|
-
return path.join(stateDir(), 'admin-broker.json');
|
|
23
|
-
}
|
|
24
|
-
/** Record where the broker listens, with a fresh token callers must present. */
|
|
25
|
-
export function publishAdminBrokerEndpoint(port) {
|
|
26
|
-
const token = randomBytes(32).toString('hex');
|
|
27
|
-
const file = endpointPath();
|
|
28
|
-
fs.mkdirSync(path.dirname(file), { recursive: true });
|
|
29
|
-
const temporary = `${file}.${process.pid}.tmp`;
|
|
30
|
-
fs.writeFileSync(temporary, JSON.stringify({ port, token }), { mode: 0o600 });
|
|
31
|
-
fs.renameSync(temporary, file);
|
|
32
|
-
return token;
|
|
33
|
-
}
|
|
34
|
-
export function adminBrokerTokenMatches(expected, received) {
|
|
35
|
-
if (!expected || typeof received !== 'string')
|
|
36
|
-
return false;
|
|
37
|
-
const want = Buffer.from(expected);
|
|
38
|
-
const got = Buffer.from(received);
|
|
39
|
-
return want.length === got.length && timingSafeEqual(want, got);
|
|
40
|
-
}
|
|
41
|
-
function brokerUnavailable() {
|
|
42
|
-
return Object.assign(new Error('The administrator terminal broker is not running.'), {
|
|
43
|
-
code: 'ENOENT',
|
|
44
|
-
});
|
|
45
|
-
}
|
|
46
|
-
/** Connect to the running broker; the token goes in the first message. */
|
|
47
|
-
export function connectAdminBroker() {
|
|
48
|
-
let endpoint;
|
|
49
|
-
try {
|
|
50
|
-
endpoint = JSON.parse(fs.readFileSync(endpointPath(), 'utf8'));
|
|
51
|
-
}
|
|
52
|
-
catch {
|
|
53
|
-
// Missing or half-written: either way no broker has published itself.
|
|
54
|
-
throw brokerUnavailable();
|
|
55
|
-
}
|
|
56
|
-
const { port, token } = endpoint;
|
|
57
|
-
if (!Number.isInteger(port) || typeof token !== 'string')
|
|
58
|
-
throw brokerUnavailable();
|
|
59
|
-
return { socket: net.createConnection({ host: ADMIN_BROKER_HOST, port: port }), token };
|
|
60
|
-
}
|
|
61
|
-
export function listenAdminBroker(server, onListening) {
|
|
62
|
-
server.listen(0, ADMIN_BROKER_HOST, () => {
|
|
63
|
-
onListening(server.address().port);
|
|
64
|
-
});
|
|
65
|
-
}
|
|
66
|
-
export function sendAdminBrokerMessage(socket, message) {
|
|
67
|
-
socket.write(`${JSON.stringify(message)}\n`);
|
|
68
|
-
}
|
|
@@ -1,123 +0,0 @@
|
|
|
1
|
-
// Watching what the agents on this workstation are doing, for the status bar
|
|
2
|
-
// of every window attached to it.
|
|
3
|
-
import { sameSummary, summarizeAgents, } from '../agent-status.js';
|
|
4
|
-
import { requestHerdr, subscribeHerdr, } from '../herdr-api.js';
|
|
5
|
-
/** How often the agent summary is re-read while a browser is watching. */
|
|
6
|
-
const AGENT_STATUS_POLL_MS = 5_000;
|
|
7
|
-
const AGENT_STATUS_EVENT_DEBOUNCE_MS = 150;
|
|
8
|
-
const AGENT_STATUS_SUBSCRIPTIONS = [
|
|
9
|
-
{ type: 'pane.focused' },
|
|
10
|
-
{ type: 'tab.focused' },
|
|
11
|
-
{ type: 'workspace.focused' },
|
|
12
|
-
{ type: 'pane.agent_detected' },
|
|
13
|
-
];
|
|
14
|
-
/**
|
|
15
|
-
* A side channel, on its own socket, alive only while a browser is attached.
|
|
16
|
-
* Herdr's server can be restarted underneath a running herdr-remote — the
|
|
17
|
-
* PTYs survive it — so every failure here is a retry, never anything a
|
|
18
|
-
* terminal session notices.
|
|
19
|
-
*
|
|
20
|
-
* A long-lived subscription provides fast refreshes on pane and workspace
|
|
21
|
-
* focus changes; the five-second poll still repairs missed events and a
|
|
22
|
-
* restarted Herdr server. Both paths only read the side-channel snapshot, and
|
|
23
|
-
* events only invalidate it: Herdr's snapshots and events share no sequence,
|
|
24
|
-
* so each (re)started subscription and each event mid-read asks for a fresh
|
|
25
|
-
* read rather than patching the last one.
|
|
26
|
-
*/
|
|
27
|
-
export class AgentWatch {
|
|
28
|
-
socketPath;
|
|
29
|
-
request;
|
|
30
|
-
subscribe;
|
|
31
|
-
send;
|
|
32
|
-
pollTimer = null;
|
|
33
|
-
debounceTimer = null;
|
|
34
|
-
subscription = null;
|
|
35
|
-
pending = false;
|
|
36
|
-
stale = false;
|
|
37
|
-
lastSummary = null;
|
|
38
|
-
constructor(options, send) {
|
|
39
|
-
this.socketPath = options.socketPath;
|
|
40
|
-
this.request = options.requestHerdr || requestHerdr;
|
|
41
|
-
this.subscribe = options.subscribeHerdr || subscribeHerdr;
|
|
42
|
-
this.send = send;
|
|
43
|
-
}
|
|
44
|
-
start() {
|
|
45
|
-
if (this.pollTimer)
|
|
46
|
-
return;
|
|
47
|
-
this.pollTimer = setInterval(() => this.read(), AGENT_STATUS_POLL_MS);
|
|
48
|
-
this.pollTimer.unref?.();
|
|
49
|
-
try {
|
|
50
|
-
this.subscription = this.subscribe(this.socketPath, AGENT_STATUS_SUBSCRIPTIONS, () => this.scheduleRead(), { onStarted: () => this.scheduleRead() });
|
|
51
|
-
}
|
|
52
|
-
catch {
|
|
53
|
-
// A missing or restarting Herdr server leaves the regular poll in place.
|
|
54
|
-
}
|
|
55
|
-
this.read();
|
|
56
|
-
}
|
|
57
|
-
stop() {
|
|
58
|
-
if (this.pollTimer) {
|
|
59
|
-
clearInterval(this.pollTimer);
|
|
60
|
-
this.pollTimer = null;
|
|
61
|
-
}
|
|
62
|
-
if (this.debounceTimer) {
|
|
63
|
-
clearTimeout(this.debounceTimer);
|
|
64
|
-
this.debounceTimer = null;
|
|
65
|
-
}
|
|
66
|
-
if (this.subscription) {
|
|
67
|
-
this.subscription.close();
|
|
68
|
-
this.subscription = null;
|
|
69
|
-
}
|
|
70
|
-
this.stale = false;
|
|
71
|
-
this.lastSummary = null;
|
|
72
|
-
}
|
|
73
|
-
scheduleRead() {
|
|
74
|
-
if (!this.pollTimer)
|
|
75
|
-
return;
|
|
76
|
-
if (this.debounceTimer)
|
|
77
|
-
clearTimeout(this.debounceTimer);
|
|
78
|
-
this.debounceTimer = setTimeout(() => {
|
|
79
|
-
this.debounceTimer = null;
|
|
80
|
-
this.read();
|
|
81
|
-
}, AGENT_STATUS_EVENT_DEBOUNCE_MS);
|
|
82
|
-
this.debounceTimer.unref?.();
|
|
83
|
-
}
|
|
84
|
-
/**
|
|
85
|
-
* Read the summary and forward it if it changed.
|
|
86
|
-
*
|
|
87
|
-
* A slow answer must not stack up behind the next tick, and a failed one
|
|
88
|
-
* leaves the last summary standing rather than blanking the browser's chip:
|
|
89
|
-
* Herdr's server not running is an ordinary state, not news.
|
|
90
|
-
*/
|
|
91
|
-
async read() {
|
|
92
|
-
if (!this.pollTimer)
|
|
93
|
-
return;
|
|
94
|
-
if (this.pending) {
|
|
95
|
-
this.stale = true;
|
|
96
|
-
return;
|
|
97
|
-
}
|
|
98
|
-
this.pending = true;
|
|
99
|
-
this.stale = false;
|
|
100
|
-
let snapshot;
|
|
101
|
-
try {
|
|
102
|
-
const result = await this.request(this.socketPath, 'session.snapshot', {});
|
|
103
|
-
snapshot = result?.snapshot || result;
|
|
104
|
-
}
|
|
105
|
-
catch {
|
|
106
|
-
return;
|
|
107
|
-
}
|
|
108
|
-
finally {
|
|
109
|
-
this.pending = false;
|
|
110
|
-
// Something changed while this answer was in flight; it may predate it.
|
|
111
|
-
if (this.stale)
|
|
112
|
-
this.scheduleRead();
|
|
113
|
-
}
|
|
114
|
-
// Stopped while the answer was in flight; the browser it was for is gone.
|
|
115
|
-
if (!this.pollTimer)
|
|
116
|
-
return;
|
|
117
|
-
const summary = summarizeAgents(snapshot);
|
|
118
|
-
if (sameSummary(this.lastSummary, summary))
|
|
119
|
-
return;
|
|
120
|
-
this.lastSummary = summary;
|
|
121
|
-
this.send({ type: 'agent_status', ...summary });
|
|
122
|
-
}
|
|
123
|
-
}
|
|
@@ -1,141 +0,0 @@
|
|
|
1
|
-
import { ADMIN_BROKER_MAX_MESSAGE_BYTES, connectAdminBroker, sendAdminBrokerMessage, } from './adminBrokerEndpoint.js';
|
|
2
|
-
const BROKER_CONNECT_TIMEOUT_MS = 5_000;
|
|
3
|
-
/** A PTY the admin broker runs elevated, driven over its loopback socket. */
|
|
4
|
-
export class ElevatedPty {
|
|
5
|
-
socket = null;
|
|
6
|
-
input = '';
|
|
7
|
-
connected = false;
|
|
8
|
-
started = false;
|
|
9
|
-
finished = false;
|
|
10
|
-
killed = false;
|
|
11
|
-
/** Keystrokes typed while the elevated shell was still starting. */
|
|
12
|
-
early = [];
|
|
13
|
-
dataListener = null;
|
|
14
|
-
exitListener = null;
|
|
15
|
-
errorListener = null;
|
|
16
|
-
options;
|
|
17
|
-
constructor(options) {
|
|
18
|
-
this.options = options;
|
|
19
|
-
}
|
|
20
|
-
start({ cols, rows, onData, onExit, onError }) {
|
|
21
|
-
this.dataListener = onData;
|
|
22
|
-
this.exitListener = onExit;
|
|
23
|
-
this.errorListener = onError;
|
|
24
|
-
let broker;
|
|
25
|
-
try {
|
|
26
|
-
broker = connectAdminBroker();
|
|
27
|
-
}
|
|
28
|
-
catch (error) {
|
|
29
|
-
this.fail(error);
|
|
30
|
-
return;
|
|
31
|
-
}
|
|
32
|
-
const { socket, token } = broker;
|
|
33
|
-
this.socket = socket;
|
|
34
|
-
socket.setEncoding('utf8');
|
|
35
|
-
socket.setTimeout(BROKER_CONNECT_TIMEOUT_MS, () => {
|
|
36
|
-
socket.destroy(Object.assign(new Error('Administrator terminal broker connection timed out'), {
|
|
37
|
-
code: 'ETIMEDOUT',
|
|
38
|
-
}));
|
|
39
|
-
});
|
|
40
|
-
socket.on('connect', () => {
|
|
41
|
-
this.connected = true;
|
|
42
|
-
socket.setTimeout(0);
|
|
43
|
-
sendAdminBrokerMessage(socket, { type: 'start', token, ...this.options, cols, rows });
|
|
44
|
-
});
|
|
45
|
-
socket.on('data', (chunk) => this.receive(chunk));
|
|
46
|
-
socket.on('error', (error) => this.fail(error));
|
|
47
|
-
socket.on('close', () => {
|
|
48
|
-
if (this.started && !this.finished && !this.killed) {
|
|
49
|
-
this.finished = true;
|
|
50
|
-
this.exitListener?.({ exitCode: 1 });
|
|
51
|
-
}
|
|
52
|
-
});
|
|
53
|
-
}
|
|
54
|
-
write(data) {
|
|
55
|
-
if (this.finished)
|
|
56
|
-
return;
|
|
57
|
-
if (!this.started) {
|
|
58
|
-
this.early.push(Buffer.from(data));
|
|
59
|
-
return;
|
|
60
|
-
}
|
|
61
|
-
if (this.socket?.writable) {
|
|
62
|
-
sendAdminBrokerMessage(this.socket, {
|
|
63
|
-
type: 'input',
|
|
64
|
-
dataBase64: Buffer.from(data).toString('base64'),
|
|
65
|
-
});
|
|
66
|
-
}
|
|
67
|
-
}
|
|
68
|
-
resize(cols, rows) {
|
|
69
|
-
if (this.started && this.socket?.writable) {
|
|
70
|
-
sendAdminBrokerMessage(this.socket, { type: 'resize', cols, rows });
|
|
71
|
-
}
|
|
72
|
-
}
|
|
73
|
-
kill() {
|
|
74
|
-
this.killed = true;
|
|
75
|
-
if (!this.socket)
|
|
76
|
-
return;
|
|
77
|
-
if (this.connected && this.socket.writable) {
|
|
78
|
-
sendAdminBrokerMessage(this.socket, { type: 'kill' });
|
|
79
|
-
this.socket.end();
|
|
80
|
-
}
|
|
81
|
-
else {
|
|
82
|
-
this.socket.destroy();
|
|
83
|
-
}
|
|
84
|
-
}
|
|
85
|
-
receive(chunk) {
|
|
86
|
-
this.input += chunk;
|
|
87
|
-
if (Buffer.byteLength(this.input, 'utf8') > ADMIN_BROKER_MAX_MESSAGE_BYTES) {
|
|
88
|
-
this.socket?.destroy(new Error('Administrator terminal broker message exceeded the limit'));
|
|
89
|
-
return;
|
|
90
|
-
}
|
|
91
|
-
while (true) {
|
|
92
|
-
const newline = this.input.indexOf('\n');
|
|
93
|
-
if (newline < 0)
|
|
94
|
-
return;
|
|
95
|
-
const line = this.input.slice(0, newline);
|
|
96
|
-
this.input = this.input.slice(newline + 1);
|
|
97
|
-
let message;
|
|
98
|
-
try {
|
|
99
|
-
message = JSON.parse(line);
|
|
100
|
-
}
|
|
101
|
-
catch {
|
|
102
|
-
this.socket?.destroy(new Error('Administrator terminal broker sent invalid JSON'));
|
|
103
|
-
return;
|
|
104
|
-
}
|
|
105
|
-
if (message.type === 'started') {
|
|
106
|
-
this.started = true;
|
|
107
|
-
const early = Buffer.concat(this.early);
|
|
108
|
-
this.early = [];
|
|
109
|
-
if (early.length > 0)
|
|
110
|
-
this.write(early);
|
|
111
|
-
}
|
|
112
|
-
else if (message.type === 'data' && typeof message.dataBase64 === 'string') {
|
|
113
|
-
this.dataListener?.(Buffer.from(message.dataBase64, 'base64').toString('utf8'));
|
|
114
|
-
}
|
|
115
|
-
else if (message.type === 'exit') {
|
|
116
|
-
this.finished = true;
|
|
117
|
-
this.exitListener?.({
|
|
118
|
-
exitCode: Number(message.exitCode) || 0,
|
|
119
|
-
...(typeof message.signal === 'number' ? { signal: message.signal } : {}),
|
|
120
|
-
});
|
|
121
|
-
this.socket?.end();
|
|
122
|
-
}
|
|
123
|
-
else if (message.type === 'error') {
|
|
124
|
-
this.fail(new Error(typeof message.message === 'string' ? message.message : 'Admin PTY failed'));
|
|
125
|
-
}
|
|
126
|
-
}
|
|
127
|
-
}
|
|
128
|
-
fail(error) {
|
|
129
|
-
if (this.started) {
|
|
130
|
-
if (!this.finished && !this.killed) {
|
|
131
|
-
this.finished = true;
|
|
132
|
-
this.exitListener?.({ exitCode: 1 });
|
|
133
|
-
}
|
|
134
|
-
return;
|
|
135
|
-
}
|
|
136
|
-
if (this.finished || this.killed)
|
|
137
|
-
return;
|
|
138
|
-
this.finished = true;
|
|
139
|
-
this.errorListener?.(error);
|
|
140
|
-
}
|
|
141
|
-
}
|
|
@@ -1,165 +0,0 @@
|
|
|
1
|
-
// Serving the workstation's terminal font to browser windows: its public
|
|
2
|
-
// description, its files a chunk at a time, and characters cut out of large
|
|
3
|
-
// (CJK) fonts on demand.
|
|
4
|
-
import crypto from 'node:crypto';
|
|
5
|
-
import fs from 'node:fs';
|
|
6
|
-
import { TERMINAL_FONT_CHUNK_BYTES } from 'herdr-remote-relay/protocol';
|
|
7
|
-
import { FontSubsetter } from '../font-subset.js';
|
|
8
|
-
import { loadHostTerminalFont, publicTerminalFont, readFontChunk, } from '../terminal-font/index.js';
|
|
9
|
-
/** Cut fonts kept for the slices a browser has yet to fetch, by total size. */
|
|
10
|
-
const FONT_SUBSET_CACHE_BYTES = 48 * 1024 * 1024;
|
|
11
|
-
const FONT_CHANGED = 'The terminal font changed or is no longer on this workstation';
|
|
12
|
-
/**
|
|
13
|
-
* Only the family, size and file hashes leave this process; a browser that
|
|
14
|
-
* lacks the font fetches the files by hash, one chunk at a time.
|
|
15
|
-
*/
|
|
16
|
-
export class FontServer {
|
|
17
|
-
font;
|
|
18
|
-
load;
|
|
19
|
-
subsetter;
|
|
20
|
-
/** Subsets too large for one message, by hash, until fetched. */
|
|
21
|
-
subsets = new Map();
|
|
22
|
-
send;
|
|
23
|
-
constructor(options, send) {
|
|
24
|
-
this.load = options.loadTerminalFont || loadHostTerminalFont;
|
|
25
|
-
this.font = options.terminalFont !== undefined ? options.terminalFont : this.load();
|
|
26
|
-
this.subsetter = options.fontSubsetter || new FontSubsetter();
|
|
27
|
-
this.send = send;
|
|
28
|
-
}
|
|
29
|
-
/** What may leave this machine: the record without its file paths. */
|
|
30
|
-
publicFont() {
|
|
31
|
-
return publicTerminalFont(this.font);
|
|
32
|
-
}
|
|
33
|
-
/**
|
|
34
|
-
* A service started at boot runs before anything has seen a terminal, so it
|
|
35
|
-
* starts with no font. The first `herdr-remote start` from a terminal (or
|
|
36
|
-
* from Herdr's plugin hook) writes one down; the next window to open picks
|
|
37
|
-
* it up here instead of waiting for a restart.
|
|
38
|
-
*/
|
|
39
|
-
pickUp() {
|
|
40
|
-
if (this.font)
|
|
41
|
-
return;
|
|
42
|
-
let next = null;
|
|
43
|
-
try {
|
|
44
|
-
next = this.load();
|
|
45
|
-
}
|
|
46
|
-
catch {
|
|
47
|
-
// Unreadable now; the next window to open asks again.
|
|
48
|
-
return;
|
|
49
|
-
}
|
|
50
|
-
if (!next)
|
|
51
|
-
return;
|
|
52
|
-
this.font = next;
|
|
53
|
-
this.send({ type: 'terminal_font', terminalFont: publicTerminalFont(next) });
|
|
54
|
-
}
|
|
55
|
-
/**
|
|
56
|
-
* Read the terminal's font settings again, at a browser's request. Every
|
|
57
|
-
* window of this workstation hears the answer, since they all draw with it.
|
|
58
|
-
*/
|
|
59
|
-
refresh() {
|
|
60
|
-
try {
|
|
61
|
-
const next = this.load({ refresh: true });
|
|
62
|
-
if (next)
|
|
63
|
-
this.font = next;
|
|
64
|
-
}
|
|
65
|
-
catch (error) {
|
|
66
|
-
process.stderr.write(`herdr-remote host connector: could not read the terminal font: ${error.message}\n`);
|
|
67
|
-
}
|
|
68
|
-
this.send({ type: 'terminal_font', terminalFont: this.publicFont() });
|
|
69
|
-
}
|
|
70
|
-
/** One slice of a terminal font file, for the window that asked for it. */
|
|
71
|
-
sendChunk(streamId, request) {
|
|
72
|
-
const chunk = this.subsetChunk(request.sha256, request.index) ||
|
|
73
|
-
readFontChunk(this.font, request.sha256, request.index);
|
|
74
|
-
if (!chunk) {
|
|
75
|
-
this.send({
|
|
76
|
-
type: 'error',
|
|
77
|
-
clientId: streamId,
|
|
78
|
-
code: 'host_font_unavailable',
|
|
79
|
-
message: FONT_CHANGED,
|
|
80
|
-
});
|
|
81
|
-
return;
|
|
82
|
-
}
|
|
83
|
-
this.send({
|
|
84
|
-
type: 'host_font_chunk',
|
|
85
|
-
clientId: streamId,
|
|
86
|
-
sha256: request.sha256,
|
|
87
|
-
index: request.index,
|
|
88
|
-
total: chunk.total,
|
|
89
|
-
dataBase64: chunk.data.toString('base64'),
|
|
90
|
-
});
|
|
91
|
-
}
|
|
92
|
-
/**
|
|
93
|
-
* The characters a window is about to draw, cut out of a large font (CJK).
|
|
94
|
-
* A small result travels in the answer itself; a large one (the common
|
|
95
|
-
* characters fetched up front) is held here and pulled in slices.
|
|
96
|
-
*/
|
|
97
|
-
sendSubset(streamId, request) {
|
|
98
|
-
const fail = (code, text) => this.send({ type: 'error', clientId: streamId, code, message: text });
|
|
99
|
-
const source = this.font?.subsets?.find((candidate) => candidate.sha256 === request.sha256);
|
|
100
|
-
if (!source || !this.unchanged(source)) {
|
|
101
|
-
fail('host_font_unavailable', FONT_CHANGED);
|
|
102
|
-
return;
|
|
103
|
-
}
|
|
104
|
-
const codepoints = [
|
|
105
|
-
...new Set(Array.from(String(request.text || ''), (char) => char.codePointAt(0))),
|
|
106
|
-
];
|
|
107
|
-
let data;
|
|
108
|
-
try {
|
|
109
|
-
data = this.subsetter.subset(source, codepoints);
|
|
110
|
-
}
|
|
111
|
-
catch (error) {
|
|
112
|
-
fail('host_font_subset_failed', error.message || 'The font could not be subset');
|
|
113
|
-
return;
|
|
114
|
-
}
|
|
115
|
-
const subsetSha = crypto.createHash('sha256').update(data).digest('hex');
|
|
116
|
-
const answer = {
|
|
117
|
-
type: 'host_font_subset_ready',
|
|
118
|
-
clientId: streamId,
|
|
119
|
-
requestId: request.requestId,
|
|
120
|
-
sha256: source.sha256,
|
|
121
|
-
subsetSha,
|
|
122
|
-
bytes: data.length,
|
|
123
|
-
};
|
|
124
|
-
if (data.length <= TERMINAL_FONT_CHUNK_BYTES) {
|
|
125
|
-
answer.dataBase64 = data.toString('base64');
|
|
126
|
-
}
|
|
127
|
-
else {
|
|
128
|
-
this.hold(subsetSha, data);
|
|
129
|
-
}
|
|
130
|
-
this.send(answer);
|
|
131
|
-
}
|
|
132
|
-
/** Whether the file behind `source` is still the one that was announced. */
|
|
133
|
-
unchanged(source) {
|
|
134
|
-
try {
|
|
135
|
-
const stat = fs.statSync(source.path);
|
|
136
|
-
return stat.size === source.bytes && stat.mtimeMs === source.mtimeMs;
|
|
137
|
-
}
|
|
138
|
-
catch {
|
|
139
|
-
return false;
|
|
140
|
-
}
|
|
141
|
-
}
|
|
142
|
-
/** Keep a cut font for its slices, dropping the oldest beyond the cache size. */
|
|
143
|
-
hold(subsetSha, data) {
|
|
144
|
-
this.subsets.delete(subsetSha);
|
|
145
|
-
this.subsets.set(subsetSha, data);
|
|
146
|
-
let held = [...this.subsets.values()].reduce((sum, item) => sum + item.length, 0);
|
|
147
|
-
for (const [key, item] of this.subsets) {
|
|
148
|
-
if (held <= FONT_SUBSET_CACHE_BYTES || key === subsetSha)
|
|
149
|
-
break;
|
|
150
|
-
this.subsets.delete(key);
|
|
151
|
-
held -= item.length;
|
|
152
|
-
}
|
|
153
|
-
}
|
|
154
|
-
/** A slice of a cut font still held for the browser that asked for it. */
|
|
155
|
-
subsetChunk(sha256, index) {
|
|
156
|
-
const data = this.subsets.get(sha256);
|
|
157
|
-
if (!data)
|
|
158
|
-
return null;
|
|
159
|
-
const total = Math.ceil(data.length / TERMINAL_FONT_CHUNK_BYTES);
|
|
160
|
-
if (!Number.isInteger(index) || index < 0 || index >= total)
|
|
161
|
-
return null;
|
|
162
|
-
const start = index * TERMINAL_FONT_CHUNK_BYTES;
|
|
163
|
-
return { data: data.subarray(start, start + TERMINAL_FONT_CHUNK_BYTES), total };
|
|
164
|
-
}
|
|
165
|
-
}
|
|
@@ -1,28 +0,0 @@
|
|
|
1
|
-
// The keys that put this workstation's Herdr into prefix mode. A browser has
|
|
2
|
-
// to tell a Herdr command from typed text, and only Herdr's own config file
|
|
3
|
-
// says which key starts one, so the host reads it and reports it in its hello.
|
|
4
|
-
import fs from 'node:fs';
|
|
5
|
-
import { sanitizeHerdrPrefixKeys } from 'herdr-remote-relay/protocol';
|
|
6
|
-
import { herdrConfigPath } from '../socket-discovery.js';
|
|
7
|
-
import { parseTomlSubset } from '../terminal-font/formats.js';
|
|
8
|
-
/** What Herdr uses when `keys.prefix` is absent or names nothing it can read. */
|
|
9
|
-
const DEFAULT_PREFIX_KEYS = ['ctrl+b'];
|
|
10
|
-
/**
|
|
11
|
-
* `keys.prefix` from the Herdr config the connector's Herdr reads: one key or
|
|
12
|
-
* a list, each a combo like `ctrl+space`. Always at least one key, because
|
|
13
|
-
* Herdr itself falls back to `ctrl+b` when the file, the key or every entry
|
|
14
|
-
* is unusable.
|
|
15
|
-
*/
|
|
16
|
-
export function readHerdrPrefixKeys(env = process.env, platform = process.platform) {
|
|
17
|
-
let text;
|
|
18
|
-
try {
|
|
19
|
-
text = fs.readFileSync(herdrConfigPath(env, platform), 'utf8');
|
|
20
|
-
}
|
|
21
|
-
catch {
|
|
22
|
-
// No readable config file means Herdr runs on its defaults.
|
|
23
|
-
return [...DEFAULT_PREFIX_KEYS];
|
|
24
|
-
}
|
|
25
|
-
const configured = parseTomlSubset(text)['keys.prefix'];
|
|
26
|
-
const entries = typeof configured === 'string' ? [configured] : configured;
|
|
27
|
-
return sanitizeHerdrPrefixKeys(entries) ?? [...DEFAULT_PREFIX_KEYS];
|
|
28
|
-
}
|