herdr-remote 0.2.39 → 0.3.32
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +4 -2
- package/README.zh-CN.md +2 -2
- package/dist/chunks/3FQAXVGC.js +3 -0
- package/dist/chunks/42KARAL4.js +31 -0
- package/dist/chunks/44IGVPMA.js +5 -0
- package/dist/chunks/5LZFJ6UZ.js +3 -0
- package/dist/chunks/7SMZ43LI.js +9 -0
- package/dist/chunks/AX4EPJCP.js +4 -0
- package/dist/chunks/B76DFI4H.js +16 -0
- package/dist/chunks/BYGDPZZD.js +3 -0
- package/dist/chunks/C2UUZY6J.js +2 -0
- package/dist/chunks/CVJ4HBMP.js +3 -0
- package/dist/chunks/DGUZCGQN.js +3 -0
- package/dist/chunks/FONUO5O5.js +4 -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/K5RFPHSF.js +5 -0
- package/dist/chunks/KGOQKOHH.js +5 -0
- package/dist/chunks/KRVF37OW.js +3 -0
- package/dist/chunks/KUDCC7ZO.js +6 -0
- package/dist/chunks/LPR662JH.js +77 -0
- package/dist/chunks/LY2ZGCXN.js +4 -0
- package/dist/chunks/M52LJEES.js +6 -0
- package/dist/chunks/MCUVFSSK.js +3 -0
- package/dist/chunks/MEFDV375.js +10 -0
- package/dist/chunks/MKAEXKXG.js +10 -0
- package/dist/chunks/O2LNVFUT.js +3 -0
- package/dist/chunks/PBMBFWLQ.js +3 -0
- package/dist/chunks/PBRQ3HEG.js +3 -0
- package/dist/chunks/QCSILMFB.js +3 -0
- package/dist/chunks/QRGNEKME.js +5 -0
- package/dist/chunks/RMHH3IAX.js +63 -0
- package/dist/chunks/TNLCNVA2.js +3 -0
- package/dist/chunks/U6QAYG3E.js +3 -0
- package/dist/chunks/VD54DF7R.js +45 -0
- package/dist/chunks/WY7WHEJ4.js +3 -0
- package/dist/cli.js +21 -294
- package/dist/connector/admin-broker.js +3 -303
- 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 -116
- 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,381 +0,0 @@
|
|
|
1
|
-
// One Herdr client in a PTY per browser window, and starting Herdr's server
|
|
2
|
-
// when a window finds it not running.
|
|
3
|
-
import { FRAME_TYPE_OUTPUT, packStreamFrame, packStreamFrameV2, } from 'herdr-remote-relay/protocol';
|
|
4
|
-
import { herdrNotFoundMessage, herdrOutdatedMessage, herdrVersion, resolveHerdrCommand, verifyHerdrCommand, } from '../herdr-command.js';
|
|
5
|
-
import { ensureHerdrServer, probeHerdrServer, } from '../herdr-server.js';
|
|
6
|
-
import { PtySession } from '../pty-session.js';
|
|
7
|
-
import { inspectSocket } from '../socket-discovery.js';
|
|
8
|
-
import { streamOf } from './wire.js';
|
|
9
|
-
/**
|
|
10
|
-
* A session that dies this fast never really started. Anything longer is a
|
|
11
|
-
* session the user actually used and then left.
|
|
12
|
-
*/
|
|
13
|
-
const FAST_FAILURE_MS = 1500;
|
|
14
|
-
/** Coalesce Windows ConPTY's many small output chunks before they reach the relay. */
|
|
15
|
-
const WINDOWS_OUTPUT_BATCH_MS = 4;
|
|
16
|
-
/** How many broken starts in a row before we stop calling them exits. */
|
|
17
|
-
export const FAST_FAILURE_LIMIT = 3;
|
|
18
|
-
export class Sessions {
|
|
19
|
-
/** Running sessions by stream id. */
|
|
20
|
-
byStream = new Map();
|
|
21
|
-
/** v2 frame index → stream id. */
|
|
22
|
-
streamIndexToId = new Map();
|
|
23
|
-
/** Windows whose session waits on a Herdr that is not running, by stream. */
|
|
24
|
-
waitingForHerdr = new Map();
|
|
25
|
-
/** Session starts between their liveness check and their PTY, by stream. */
|
|
26
|
-
probingStarts = new Map();
|
|
27
|
-
/** One start at a time, however many windows asked for it. */
|
|
28
|
-
herdrLaunch = null;
|
|
29
|
-
herdrVersionWarned = false;
|
|
30
|
-
fastFailures = 0;
|
|
31
|
-
herdrCommand;
|
|
32
|
-
herdrArgs;
|
|
33
|
-
socketPath;
|
|
34
|
-
cwd;
|
|
35
|
-
herdrLookup;
|
|
36
|
-
herdrLogPath;
|
|
37
|
-
readHerdrVersion;
|
|
38
|
-
Pty;
|
|
39
|
-
probeHerdr;
|
|
40
|
-
ensureHerdr;
|
|
41
|
-
link;
|
|
42
|
-
constructor(options, link) {
|
|
43
|
-
this.socketPath = options.socketPath;
|
|
44
|
-
this.herdrLookup = options.herdrLookup || {};
|
|
45
|
-
this.herdrCommand = options.herdrCommand || resolveHerdrCommand(this.herdrLookup);
|
|
46
|
-
this.herdrArgs = options.herdrArgs;
|
|
47
|
-
this.cwd = options.cwd;
|
|
48
|
-
this.herdrLogPath = options.herdrLogPath;
|
|
49
|
-
this.readHerdrVersion = options.readHerdrVersion || herdrVersion;
|
|
50
|
-
this.Pty = options.PtySession || PtySession;
|
|
51
|
-
this.probeHerdr = options.probeHerdr || probeHerdrServer;
|
|
52
|
-
this.ensureHerdr = options.ensureHerdr || ensureHerdrServer;
|
|
53
|
-
this.link = link;
|
|
54
|
-
}
|
|
55
|
-
sendError(streamId, code, message) {
|
|
56
|
-
this.link.send({ type: 'error', clientId: streamId, code, message });
|
|
57
|
-
}
|
|
58
|
-
/**
|
|
59
|
-
* Confirm Herdr is really there before the name reaches `pty.spawn`.
|
|
60
|
-
*
|
|
61
|
-
* node-pty resolves the command with `execvp(3)` inside the forked child, so
|
|
62
|
-
* a missing binary is not a spawn error anyone can catch: the child writes
|
|
63
|
-
* "execvp(3) failed.: No such file or directory" into the PTY and exits. The
|
|
64
|
-
* `session_exit` that follows makes the relay drop the browser's socket, the
|
|
65
|
-
* browser reconnects into the same broken start, and that is the reconnect
|
|
66
|
-
* loop of issue #1. Refusing to start keeps the socket up and puts the actual
|
|
67
|
-
* reason in front of the user.
|
|
68
|
-
*
|
|
69
|
-
* Re-resolved per session rather than cached from the constructor: a service
|
|
70
|
-
* that started before Herdr was installed should pick it up without a restart.
|
|
71
|
-
*/
|
|
72
|
-
ensureHerdrCommand() {
|
|
73
|
-
const resolved = verifyHerdrCommand(this.herdrCommand, this.herdrLookup);
|
|
74
|
-
if (resolved.found) {
|
|
75
|
-
this.herdrCommand = resolved.command;
|
|
76
|
-
this.warnIfHerdrOutdated();
|
|
77
|
-
return null;
|
|
78
|
-
}
|
|
79
|
-
return herdrNotFoundMessage(this.herdrLookup);
|
|
80
|
-
}
|
|
81
|
-
/**
|
|
82
|
-
* Say once that the installed Herdr is older than this release expects.
|
|
83
|
-
*
|
|
84
|
-
* Never a refusal to start. An older Herdr still runs — it just misbehaves in
|
|
85
|
-
* ways a browser window notices — and the comment on `start` explains what a
|
|
86
|
-
* refused start does to a reconnecting browser.
|
|
87
|
-
*/
|
|
88
|
-
warnIfHerdrOutdated() {
|
|
89
|
-
if (this.herdrVersionWarned)
|
|
90
|
-
return;
|
|
91
|
-
const installed = this.readHerdrVersion({ command: this.herdrCommand });
|
|
92
|
-
if (installed.supported) {
|
|
93
|
-
// A version we could not read is checked again on the next session: the
|
|
94
|
-
// install may simply not have been finished writing.
|
|
95
|
-
if (installed.version)
|
|
96
|
-
this.herdrVersionWarned = true;
|
|
97
|
-
return;
|
|
98
|
-
}
|
|
99
|
-
this.herdrVersionWarned = true;
|
|
100
|
-
process.stderr.write(`herdr-remote host connector: ${herdrOutdatedMessage(installed.version)}\n`);
|
|
101
|
-
}
|
|
102
|
-
/** Start a window's session, or hold it until Herdr runs. */
|
|
103
|
-
start(message) {
|
|
104
|
-
const streamId = streamOf(message);
|
|
105
|
-
if (!streamId)
|
|
106
|
-
return undefined;
|
|
107
|
-
this.stop(streamId);
|
|
108
|
-
const missingHerdr = this.ensureHerdrCommand();
|
|
109
|
-
if (missingHerdr) {
|
|
110
|
-
process.stderr.write(`herdr-remote host connector: ${missingHerdr}\n`);
|
|
111
|
-
this.sendError(streamId, 'herdr_not_found', missingHerdr);
|
|
112
|
-
return undefined;
|
|
113
|
-
}
|
|
114
|
-
const socketInfo = inspectSocket(this.socketPath);
|
|
115
|
-
if (!socketInfo.ok && !socketInfo.missing) {
|
|
116
|
-
this.sendError(streamId, 'herdr_socket_unavailable', socketInfo.reason);
|
|
117
|
-
return undefined;
|
|
118
|
-
}
|
|
119
|
-
if (!socketInfo.ok) {
|
|
120
|
-
this.waitForHerdr(streamId, message);
|
|
121
|
-
return undefined;
|
|
122
|
-
}
|
|
123
|
-
// The socket file alone proves nothing: a `herdr` client pointed at a dead
|
|
124
|
-
// one starts a server of its own, inside this process's service, where the
|
|
125
|
-
// next restart of herdr-remote would kill it. Only a server that accepts a
|
|
126
|
-
// connection gets a client.
|
|
127
|
-
const pending = { message };
|
|
128
|
-
this.probingStarts.set(streamId, pending);
|
|
129
|
-
return this.probeHerdr(this.socketPath).then((probe) => {
|
|
130
|
-
// Stopped, or started again, while the answer was in flight.
|
|
131
|
-
if (this.probingStarts.get(streamId) !== pending)
|
|
132
|
-
return;
|
|
133
|
-
this.probingStarts.delete(streamId);
|
|
134
|
-
if (probe.state === 'running')
|
|
135
|
-
this.spawn(pending.message);
|
|
136
|
-
else if (probe.state === 'stopped')
|
|
137
|
-
this.waitForHerdr(streamId, pending.message);
|
|
138
|
-
else
|
|
139
|
-
this.sendError(streamId, 'herdr_socket_unavailable', probe.reason);
|
|
140
|
-
});
|
|
141
|
-
}
|
|
142
|
-
/**
|
|
143
|
-
* Hold a window's session until Herdr runs, and ask the browser whether it
|
|
144
|
-
* should be started. Nothing starts until somebody says yes.
|
|
145
|
-
*/
|
|
146
|
-
waitForHerdr(streamId, message) {
|
|
147
|
-
this.waitingForHerdr.set(streamId, message);
|
|
148
|
-
this.sendError(streamId, 'herdr_not_running', `Herdr is not running on this workstation (${this.socketPath}).`);
|
|
149
|
-
}
|
|
150
|
-
/**
|
|
151
|
-
* Start this workstation's Herdr server, once, however many windows or
|
|
152
|
-
* callers ask at the same time.
|
|
153
|
-
*/
|
|
154
|
-
startHerdr() {
|
|
155
|
-
if (!this.herdrLaunch) {
|
|
156
|
-
this.herdrLaunch = Promise.resolve()
|
|
157
|
-
.then(() => {
|
|
158
|
-
const missing = this.ensureHerdrCommand();
|
|
159
|
-
if (missing)
|
|
160
|
-
throw new Error(missing);
|
|
161
|
-
return this.ensureHerdr({
|
|
162
|
-
command: this.herdrCommand,
|
|
163
|
-
args: this.herdrArgs,
|
|
164
|
-
socketPath: this.socketPath,
|
|
165
|
-
cwd: this.cwd,
|
|
166
|
-
logPath: this.herdrLogPath,
|
|
167
|
-
});
|
|
168
|
-
})
|
|
169
|
-
.finally(() => {
|
|
170
|
-
this.herdrLaunch = null;
|
|
171
|
-
});
|
|
172
|
-
}
|
|
173
|
-
return this.herdrLaunch;
|
|
174
|
-
}
|
|
175
|
-
/**
|
|
176
|
-
* A browser said yes. The relay only ever routes this from a window paired
|
|
177
|
-
* to *this* workstation, so what starts is this user's own Herdr, at the
|
|
178
|
-
* socket this connector is configured for — never one named by the message.
|
|
179
|
-
* Once it runs, every window waiting on it gets its session.
|
|
180
|
-
*/
|
|
181
|
-
async startHerdrFor(streamId) {
|
|
182
|
-
try {
|
|
183
|
-
const result = await this.startHerdr();
|
|
184
|
-
if (result.started)
|
|
185
|
-
process.stderr.write("herdr-remote host connector: started Herdr at a browser's request\n");
|
|
186
|
-
}
|
|
187
|
-
catch (error) {
|
|
188
|
-
const { message } = error;
|
|
189
|
-
process.stderr.write(`herdr-remote host connector: could not start Herdr: ${message}\n`);
|
|
190
|
-
if (streamId)
|
|
191
|
-
this.sendError(streamId, 'herdr_start_failed', message);
|
|
192
|
-
return;
|
|
193
|
-
}
|
|
194
|
-
const waiting = [...this.waitingForHerdr.values()];
|
|
195
|
-
this.waitingForHerdr.clear();
|
|
196
|
-
for (const held of waiting)
|
|
197
|
-
this.spawn(held);
|
|
198
|
-
}
|
|
199
|
-
/** Start the PTY for a window once Herdr is known to be running. */
|
|
200
|
-
spawn(message) {
|
|
201
|
-
const streamId = streamOf(message);
|
|
202
|
-
const streamIndex = typeof message.streamIndex === 'number' ? message.streamIndex : null;
|
|
203
|
-
const pty = new this.Pty({
|
|
204
|
-
command: this.herdrCommand,
|
|
205
|
-
args: this.herdrArgs,
|
|
206
|
-
cwd: this.cwd,
|
|
207
|
-
socketPath: this.socketPath,
|
|
208
|
-
});
|
|
209
|
-
const session = {
|
|
210
|
-
id: streamId,
|
|
211
|
-
streamIndex,
|
|
212
|
-
pty,
|
|
213
|
-
cols: Number(message.cols) || PtySession.DEFAULT_COLS,
|
|
214
|
-
rows: Number(message.rows) || PtySession.DEFAULT_ROWS,
|
|
215
|
-
createdAt: new Date().toISOString(),
|
|
216
|
-
startedAtMs: Date.now(),
|
|
217
|
-
pendingOutput: [],
|
|
218
|
-
flushImmediate: null,
|
|
219
|
-
flushTimeout: null,
|
|
220
|
-
};
|
|
221
|
-
const flushOutput = () => {
|
|
222
|
-
if (session.flushImmediate) {
|
|
223
|
-
clearImmediate(session.flushImmediate);
|
|
224
|
-
session.flushImmediate = null;
|
|
225
|
-
}
|
|
226
|
-
if (session.flushTimeout) {
|
|
227
|
-
clearTimeout(session.flushTimeout);
|
|
228
|
-
session.flushTimeout = null;
|
|
229
|
-
}
|
|
230
|
-
if (session.pendingOutput.length === 0)
|
|
231
|
-
return;
|
|
232
|
-
const payload = Buffer.concat(session.pendingOutput);
|
|
233
|
-
session.pendingOutput = [];
|
|
234
|
-
this.link.sendFrame(session.streamIndex !== null
|
|
235
|
-
? packStreamFrameV2(FRAME_TYPE_OUTPUT, session.streamIndex, payload)
|
|
236
|
-
: packStreamFrame('output', streamId, payload));
|
|
237
|
-
};
|
|
238
|
-
try {
|
|
239
|
-
pty.start({
|
|
240
|
-
cols: session.cols,
|
|
241
|
-
rows: session.rows,
|
|
242
|
-
onData: (data) => {
|
|
243
|
-
const chunk = Buffer.isBuffer(data) ? data : Buffer.from(data, 'utf8');
|
|
244
|
-
if (chunk.length === 0)
|
|
245
|
-
return;
|
|
246
|
-
session.pendingOutput.push(chunk);
|
|
247
|
-
if (!session.flushImmediate && !session.flushTimeout) {
|
|
248
|
-
if (process.platform === 'win32') {
|
|
249
|
-
session.flushTimeout = setTimeout(flushOutput, WINDOWS_OUTPUT_BATCH_MS);
|
|
250
|
-
}
|
|
251
|
-
else {
|
|
252
|
-
session.flushImmediate = setImmediate(flushOutput);
|
|
253
|
-
}
|
|
254
|
-
}
|
|
255
|
-
},
|
|
256
|
-
onExit: ({ exitCode }) => {
|
|
257
|
-
if (this.byStream.get(streamId) !== session)
|
|
258
|
-
return;
|
|
259
|
-
this.forget(session);
|
|
260
|
-
this.reportExit(session, exitCode);
|
|
261
|
-
this.link.changed();
|
|
262
|
-
},
|
|
263
|
-
});
|
|
264
|
-
}
|
|
265
|
-
catch (error) {
|
|
266
|
-
discardOutput(session);
|
|
267
|
-
this.sendError(streamId, 'pty_start_failed', error.message);
|
|
268
|
-
pty.kill();
|
|
269
|
-
return;
|
|
270
|
-
}
|
|
271
|
-
this.byStream.set(streamId, session);
|
|
272
|
-
if (streamIndex !== null)
|
|
273
|
-
this.streamIndexToId.set(streamIndex, streamId);
|
|
274
|
-
this.link.send({ type: 'session_ready', clientId: streamId });
|
|
275
|
-
this.link.changed();
|
|
276
|
-
}
|
|
277
|
-
/**
|
|
278
|
-
* Tell the relay how a session ended.
|
|
279
|
-
*
|
|
280
|
-
* `session_exit` is the honest answer for a session that ran, and the relay
|
|
281
|
-
* closes the browser's socket on it so the window can start a fresh one. That
|
|
282
|
-
* is also what turns a start that keeps failing into a reconnect loop: exit,
|
|
283
|
-
* close, reconnect, exit. Once a few starts in a row have died immediately
|
|
284
|
-
* with a non-zero code the failure is systemic, not a session ending, so it is
|
|
285
|
-
* reported as an error — which the relay forwards without dropping the socket,
|
|
286
|
-
* leaving the user with a message instead of a spinner.
|
|
287
|
-
*/
|
|
288
|
-
reportExit(session, exitCode) {
|
|
289
|
-
const streamId = session.id;
|
|
290
|
-
const lifetimeMs = Date.now() - session.startedAtMs;
|
|
291
|
-
const failedFast = exitCode !== 0 && lifetimeMs < FAST_FAILURE_MS;
|
|
292
|
-
this.fastFailures = failedFast ? this.fastFailures + 1 : 0;
|
|
293
|
-
if (this.fastFailures < FAST_FAILURE_LIMIT) {
|
|
294
|
-
this.link.send({ type: 'session_exit', clientId: streamId, code: exitCode });
|
|
295
|
-
return;
|
|
296
|
-
}
|
|
297
|
-
const message = `"${this.herdrCommand}" exited immediately with code ${exitCode} on ${this.fastFailures} attempts in a row. ` +
|
|
298
|
-
'Check that it runs from a terminal, and see the host connector log for what it printed.';
|
|
299
|
-
process.stderr.write(`herdr-remote host connector: ${message}\n`);
|
|
300
|
-
this.sendError(streamId, 'herdr_start_failed', message);
|
|
301
|
-
}
|
|
302
|
-
/** Keyboard input for the session behind a stream id or v2 index. */
|
|
303
|
-
write(stream, payload) {
|
|
304
|
-
const streamId = stream.streamIndex !== undefined
|
|
305
|
-
? this.streamIndexToId.get(stream.streamIndex)
|
|
306
|
-
: stream.streamId;
|
|
307
|
-
if (streamId)
|
|
308
|
-
this.byStream.get(streamId)?.pty.write(payload);
|
|
309
|
-
}
|
|
310
|
-
resize(message) {
|
|
311
|
-
const id = streamOf(message);
|
|
312
|
-
if (!id)
|
|
313
|
-
return;
|
|
314
|
-
// A window still waiting on Herdr starts at the size it has by then.
|
|
315
|
-
const held = this.waitingForHerdr.get(id) || this.probingStarts.get(id)?.message;
|
|
316
|
-
if (held) {
|
|
317
|
-
if (Number.isInteger(message.cols))
|
|
318
|
-
held.cols = message.cols;
|
|
319
|
-
if (Number.isInteger(message.rows))
|
|
320
|
-
held.rows = message.rows;
|
|
321
|
-
}
|
|
322
|
-
const session = this.byStream.get(id);
|
|
323
|
-
if (!session)
|
|
324
|
-
return;
|
|
325
|
-
if (Number.isInteger(message.cols))
|
|
326
|
-
session.cols = PtySession.clampDimension(message.cols, 0);
|
|
327
|
-
if (Number.isInteger(message.rows))
|
|
328
|
-
session.rows = PtySession.clampDimension(message.rows, 0);
|
|
329
|
-
session.pty.resize(session.cols, session.rows);
|
|
330
|
-
}
|
|
331
|
-
stop(streamId) {
|
|
332
|
-
this.waitingForHerdr.delete(streamId);
|
|
333
|
-
this.probingStarts.delete(streamId);
|
|
334
|
-
const session = this.byStream.get(streamId);
|
|
335
|
-
if (!session)
|
|
336
|
-
return;
|
|
337
|
-
this.forget(session);
|
|
338
|
-
session.pty.kill();
|
|
339
|
-
this.link.changed();
|
|
340
|
-
}
|
|
341
|
-
stopAll() {
|
|
342
|
-
for (const session of this.byStream.values()) {
|
|
343
|
-
discardOutput(session);
|
|
344
|
-
session.pty.kill();
|
|
345
|
-
}
|
|
346
|
-
this.byStream.clear();
|
|
347
|
-
this.streamIndexToId.clear();
|
|
348
|
-
this.waitingForHerdr.clear();
|
|
349
|
-
this.probingStarts.clear();
|
|
350
|
-
}
|
|
351
|
-
/** The running PTYs, as the relay's status page lists them. */
|
|
352
|
-
summaries() {
|
|
353
|
-
return [...this.byStream.values()].map((session) => ({
|
|
354
|
-
id: session.id,
|
|
355
|
-
pid: session.pty.terminal?.pid || null,
|
|
356
|
-
command: session.pty.command,
|
|
357
|
-
cols: session.cols,
|
|
358
|
-
rows: session.rows,
|
|
359
|
-
cwd: session.pty.cwd,
|
|
360
|
-
createdAt: session.createdAt,
|
|
361
|
-
activeClients: 1,
|
|
362
|
-
}));
|
|
363
|
-
}
|
|
364
|
-
forget(session) {
|
|
365
|
-
this.byStream.delete(session.id);
|
|
366
|
-
if (session.streamIndex !== null)
|
|
367
|
-
this.streamIndexToId.delete(session.streamIndex);
|
|
368
|
-
discardOutput(session);
|
|
369
|
-
}
|
|
370
|
-
}
|
|
371
|
-
function discardOutput(session) {
|
|
372
|
-
if (session.flushImmediate) {
|
|
373
|
-
clearImmediate(session.flushImmediate);
|
|
374
|
-
session.flushImmediate = null;
|
|
375
|
-
}
|
|
376
|
-
if (session.flushTimeout) {
|
|
377
|
-
clearTimeout(session.flushTimeout);
|
|
378
|
-
session.flushTimeout = null;
|
|
379
|
-
}
|
|
380
|
-
session.pendingOutput = [];
|
|
381
|
-
}
|
|
@@ -1,70 +0,0 @@
|
|
|
1
|
-
// Telling a workstation's windows whether its herdr-remote is behind.
|
|
2
|
-
import { checkForUpdate, compareVersions, currentVersion, installedVersionOnDisk, updateChecksEnabled, } from '../updater.js';
|
|
3
|
-
/** Windows opening within this long of a check reuse its answer. */
|
|
4
|
-
const UPDATE_RECHECK_MS = 60_000;
|
|
5
|
-
/**
|
|
6
|
-
* Whether a newer herdr-remote is out, asked when a window opens. The version
|
|
7
|
-
* this process runs is fixed at start; the one on disk moves when somebody
|
|
8
|
-
* updates without restarting.
|
|
9
|
-
*/
|
|
10
|
-
export class UpdateReport {
|
|
11
|
-
checkUpdate;
|
|
12
|
-
readInstalledVersion;
|
|
13
|
-
/** The release this process runs, fixed at start. */
|
|
14
|
-
runningVersion;
|
|
15
|
-
send;
|
|
16
|
-
checkedAt = 0;
|
|
17
|
-
/** The check in flight, if any. */
|
|
18
|
-
pending = null;
|
|
19
|
-
constructor(options, send) {
|
|
20
|
-
this.checkUpdate =
|
|
21
|
-
options.checkUpdate !== undefined
|
|
22
|
-
? options.checkUpdate
|
|
23
|
-
: updateChecksEnabled()
|
|
24
|
-
? checkForUpdate
|
|
25
|
-
: null;
|
|
26
|
-
this.readInstalledVersion = options.readInstalledVersion || installedVersionOnDisk;
|
|
27
|
-
this.runningVersion = options.runningVersion || currentVersion();
|
|
28
|
-
this.send = send;
|
|
29
|
-
}
|
|
30
|
-
/**
|
|
31
|
-
* Tell every window whether this workstation's herdr-remote is behind the
|
|
32
|
-
* newest release. Asked at most once a minute however many windows open;
|
|
33
|
-
* the relay keeps the last answer for windows that open in between. A failed
|
|
34
|
-
* check says nothing: offline is not news.
|
|
35
|
-
*/
|
|
36
|
-
report({ now = Date.now() } = {}) {
|
|
37
|
-
const checkUpdate = this.checkUpdate;
|
|
38
|
-
if (!checkUpdate || this.pending)
|
|
39
|
-
return this.pending;
|
|
40
|
-
if (now - this.checkedAt < UPDATE_RECHECK_MS)
|
|
41
|
-
return null;
|
|
42
|
-
this.checkedAt = now;
|
|
43
|
-
this.pending = Promise.resolve()
|
|
44
|
-
.then(() => checkUpdate())
|
|
45
|
-
.then((result) => {
|
|
46
|
-
if (!result?.ok || !result.latest)
|
|
47
|
-
return;
|
|
48
|
-
const running = this.runningVersion;
|
|
49
|
-
const installed = this.readInstalledVersion() || running;
|
|
50
|
-
this.send({
|
|
51
|
-
type: 'update_status',
|
|
52
|
-
current: running,
|
|
53
|
-
installed,
|
|
54
|
-
latest: result.latest,
|
|
55
|
-
updateAvailable: compareVersions(result.latest, running) > 0,
|
|
56
|
-
// Updated on disk, still running the old code: a restart finishes it.
|
|
57
|
-
restartPending: compareVersions(installed, running) > 0,
|
|
58
|
-
// Older connectors leave this out, and their windows keep the npm steps.
|
|
59
|
-
updateCommand: true,
|
|
60
|
-
});
|
|
61
|
-
})
|
|
62
|
-
.catch(() => {
|
|
63
|
-
// Offline, or the registry is down: nothing worth telling a window.
|
|
64
|
-
})
|
|
65
|
-
.finally(() => {
|
|
66
|
-
this.pending = null;
|
|
67
|
-
});
|
|
68
|
-
return this.pending;
|
|
69
|
-
}
|
|
70
|
-
}
|
package/dist/connector/wire.js
DELETED
|
@@ -1,42 +0,0 @@
|
|
|
1
|
-
// The host connector's side of its WebSocket to the relay.
|
|
2
|
-
import { WebSocket } from 'ws';
|
|
3
|
-
export function sendJson(ws, payload) {
|
|
4
|
-
if (ws && ws.readyState === WebSocket.OPEN)
|
|
5
|
-
ws.send(JSON.stringify(payload));
|
|
6
|
-
}
|
|
7
|
-
export function closeSocket(ws, reason = 'host connector stopping') {
|
|
8
|
-
if (!ws || ws.readyState === WebSocket.CLOSED || ws.readyState === WebSocket.CLOSING)
|
|
9
|
-
return;
|
|
10
|
-
try {
|
|
11
|
-
ws.close(1000, reason);
|
|
12
|
-
}
|
|
13
|
-
catch {
|
|
14
|
-
// Closing a socket that is already failing needs no further handling.
|
|
15
|
-
}
|
|
16
|
-
}
|
|
17
|
-
export function terminateSocket(ws) {
|
|
18
|
-
if (!ws || ws.readyState === WebSocket.CLOSED)
|
|
19
|
-
return;
|
|
20
|
-
// Tests stand in a plain object for the socket; end it however it allows.
|
|
21
|
-
const socket = ws;
|
|
22
|
-
try {
|
|
23
|
-
if (typeof socket.terminate === 'function')
|
|
24
|
-
socket.terminate();
|
|
25
|
-
else if (typeof socket.destroy === 'function')
|
|
26
|
-
socket.destroy();
|
|
27
|
-
else
|
|
28
|
-
socket.close?.();
|
|
29
|
-
}
|
|
30
|
-
catch {
|
|
31
|
-
// Tearing down a dead socket; there is nothing left to recover.
|
|
32
|
-
}
|
|
33
|
-
}
|
|
34
|
-
/**
|
|
35
|
-
* The stream a relay message is about. The relay sends `clientId` and
|
|
36
|
-
* `streamId` with the same value; `clientId` is read first, as the protocol
|
|
37
|
-
* says.
|
|
38
|
-
*/
|
|
39
|
-
export function streamOf(message) {
|
|
40
|
-
const id = message.clientId || message.streamId;
|
|
41
|
-
return typeof id === 'string' && id ? id : null;
|
|
42
|
-
}
|
package/dist/exit-codes.js
DELETED
|
@@ -1,13 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Exit codes the supervisor interprets.
|
|
3
|
-
*
|
|
4
|
-
* `REPLACED` means another host connector has taken over this workstation on
|
|
5
|
-
* the relay. Restarting after that would make the two instances kick each other
|
|
6
|
-
* off in turn, and every browser attached to the relay is disconnected on each
|
|
7
|
-
* swap — the endless "Connection closed (1012). Retrying…" loop. So it is the
|
|
8
|
-
* one exit the supervisor must respect rather than recover from.
|
|
9
|
-
*/
|
|
10
|
-
const EXIT_REPLACED = 12;
|
|
11
|
-
/** Credentials need operator intervention; a supervisor must not loop. */
|
|
12
|
-
const EXIT_AUTH_FAILED = 13;
|
|
13
|
-
export { EXIT_REPLACED, EXIT_AUTH_FAILED };
|
package/dist/font-subset.js
DELETED
|
@@ -1,116 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Cuts the characters a browser needs out of a large font.
|
|
3
|
-
*
|
|
4
|
-
* A CJK face is 16–20 MB — too much to send a phone whole — but a terminal
|
|
5
|
-
* only ever shows a few thousand distinct characters. HarfBuzz's subsetter
|
|
6
|
-
* (the same code Google Fonts slices its CJK families with) produces a small,
|
|
7
|
-
* valid font holding exactly the requested characters: 3,755 common Hanzi in
|
|
8
|
-
* about 1.7 MB and 50 ms, a handful of rare ones in a few KB and milliseconds.
|
|
9
|
-
*
|
|
10
|
-
* The WebAssembly build has no imports and runs synchronously. A loaded face
|
|
11
|
-
* costs its file size twice over (the file and the wasm heap), so the whole
|
|
12
|
-
* instance is dropped after a quiet minute; wasm memory never shrinks, only
|
|
13
|
-
* a new instance gives it back.
|
|
14
|
-
*/
|
|
15
|
-
import { createRequire } from 'node:module';
|
|
16
|
-
import fs from 'node:fs';
|
|
17
|
-
import path from 'node:path';
|
|
18
|
-
import { PACKAGE_ROOT } from './paths.js';
|
|
19
|
-
/** How long a loaded font may sit unused before its memory is released. */
|
|
20
|
-
const IDLE_RELEASE_MS = 60_000;
|
|
21
|
-
/** Never cut more than this many characters in one go. */
|
|
22
|
-
const MAX_CODEPOINTS = 16_384;
|
|
23
|
-
const HB_MEMORY_MODE_READONLY = 1;
|
|
24
|
-
let wasmModule = null;
|
|
25
|
-
function loadModule() {
|
|
26
|
-
if (!wasmModule) {
|
|
27
|
-
// A release bundles harfbuzzjs away and ships only this file beside the bundle.
|
|
28
|
-
const bundled = path.join(PACKAGE_ROOT, 'dist', 'harfbuzz-subset.wasm');
|
|
29
|
-
const file = fs.existsSync(bundled)
|
|
30
|
-
? bundled
|
|
31
|
-
: createRequire(import.meta.url).resolve('harfbuzzjs/dist/harfbuzz-subset.wasm');
|
|
32
|
-
wasmModule = new WebAssembly.Module(fs.readFileSync(file));
|
|
33
|
-
}
|
|
34
|
-
return wasmModule;
|
|
35
|
-
}
|
|
36
|
-
class FontSubsetter {
|
|
37
|
-
idleMs;
|
|
38
|
-
instance = null;
|
|
39
|
-
/** Loaded faces by source key, in the current instance. */
|
|
40
|
-
faces = new Map();
|
|
41
|
-
idleTimer = null;
|
|
42
|
-
constructor({ idleMs = IDLE_RELEASE_MS } = {}) {
|
|
43
|
-
this.idleMs = idleMs;
|
|
44
|
-
}
|
|
45
|
-
exports() {
|
|
46
|
-
if (!this.instance) {
|
|
47
|
-
this.instance = new WebAssembly.Instance(loadModule(), {}).exports;
|
|
48
|
-
// An Emscripten reactor: static constructors run here, before any call.
|
|
49
|
-
this.instance._initialize();
|
|
50
|
-
}
|
|
51
|
-
return this.instance;
|
|
52
|
-
}
|
|
53
|
-
face(source) {
|
|
54
|
-
const key = `${source.path}\0${source.index || 0}`;
|
|
55
|
-
const loaded = this.faces.get(key);
|
|
56
|
-
if (loaded)
|
|
57
|
-
return loaded.face;
|
|
58
|
-
const hb = this.exports();
|
|
59
|
-
const data = fs.readFileSync(source.path);
|
|
60
|
-
const pointer = hb.malloc(data.length);
|
|
61
|
-
if (!pointer)
|
|
62
|
-
throw new Error('out of memory loading font');
|
|
63
|
-
new Uint8Array(hb.memory.buffer).set(data, pointer);
|
|
64
|
-
const blob = hb.hb_blob_create(pointer, data.length, HB_MEMORY_MODE_READONLY, 0, 0);
|
|
65
|
-
const face = hb.hb_face_create(blob, source.index || 0);
|
|
66
|
-
hb.hb_blob_destroy(blob);
|
|
67
|
-
this.faces.set(key, { pointer, face });
|
|
68
|
-
return face;
|
|
69
|
-
}
|
|
70
|
-
/** A standalone font with just `codepoints` from `source` (`{ path, index }`). */
|
|
71
|
-
subset(source, codepoints) {
|
|
72
|
-
const hb = this.exports();
|
|
73
|
-
const face = this.face(source);
|
|
74
|
-
const input = hb.hb_subset_input_create_or_fail();
|
|
75
|
-
if (!input)
|
|
76
|
-
throw new Error('font subsetting is unavailable');
|
|
77
|
-
let result = 0;
|
|
78
|
-
let subsetFace = 0;
|
|
79
|
-
try {
|
|
80
|
-
const set = hb.hb_subset_input_unicode_set(input);
|
|
81
|
-
for (const codepoint of codepoints.slice(0, MAX_CODEPOINTS))
|
|
82
|
-
hb.hb_set_add(set, codepoint);
|
|
83
|
-
subsetFace = hb.hb_subset_or_fail(face, input);
|
|
84
|
-
if (!subsetFace)
|
|
85
|
-
throw new Error('the font could not be subset');
|
|
86
|
-
result = hb.hb_face_reference_blob(subsetFace);
|
|
87
|
-
const length = hb.hb_blob_get_length(result);
|
|
88
|
-
const pointer = hb.hb_blob_get_data(result, 0);
|
|
89
|
-
// Copied out: the heap may grow (and move) on the next call.
|
|
90
|
-
return Buffer.from(new Uint8Array(hb.memory.buffer, pointer, length));
|
|
91
|
-
}
|
|
92
|
-
finally {
|
|
93
|
-
if (result)
|
|
94
|
-
hb.hb_blob_destroy(result);
|
|
95
|
-
if (subsetFace)
|
|
96
|
-
hb.hb_face_destroy(subsetFace);
|
|
97
|
-
hb.hb_subset_input_destroy(input);
|
|
98
|
-
this.scheduleRelease();
|
|
99
|
-
}
|
|
100
|
-
}
|
|
101
|
-
scheduleRelease() {
|
|
102
|
-
if (this.idleTimer)
|
|
103
|
-
clearTimeout(this.idleTimer);
|
|
104
|
-
this.idleTimer = setTimeout(() => this.release(), this.idleMs);
|
|
105
|
-
this.idleTimer.unref?.();
|
|
106
|
-
}
|
|
107
|
-
/** Drop every loaded face and the instance holding them. */
|
|
108
|
-
release() {
|
|
109
|
-
if (this.idleTimer)
|
|
110
|
-
clearTimeout(this.idleTimer);
|
|
111
|
-
this.idleTimer = null;
|
|
112
|
-
this.faces.clear();
|
|
113
|
-
this.instance = null;
|
|
114
|
-
}
|
|
115
|
-
}
|
|
116
|
-
export { FontSubsetter };
|