herdr-remote 0.2.9 → 0.2.10
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/tui.mjs +1 -1
- package/herdr-plugin.toml +1 -1
- package/package.json +1 -1
- package/src/host-connector.js +78 -7
package/dist/tui.mjs
CHANGED
|
@@ -4015,7 +4015,7 @@ function About({ ctx }) {
|
|
|
4015
4015
|
children: updateLabel
|
|
4016
4016
|
}
|
|
4017
4017
|
) }),
|
|
4018
|
-
/* @__PURE__ */ jsx10(Row, { label: t("about.version"), children: /* @__PURE__ */ jsx10(Text9, { color: theme.muted, children: "0.2.
|
|
4018
|
+
/* @__PURE__ */ jsx10(Row, { label: t("about.version"), children: /* @__PURE__ */ jsx10(Text9, { color: theme.muted, children: "0.2.10" }) }),
|
|
4019
4019
|
/* @__PURE__ */ jsx10(Row, { label: t("about.configPath"), children: /* @__PURE__ */ jsx10(Text9, { color: theme.muted, children: (0, import_config.configPath)() }) }),
|
|
4020
4020
|
/* @__PURE__ */ jsx10(Row, { label: t("about.statePath"), children: /* @__PURE__ */ jsx10(Text9, { color: theme.muted, children: (0, import_config.stateDir)() }) }),
|
|
4021
4021
|
/* @__PURE__ */ jsx10(Box9, { marginTop: 1, children: /* @__PURE__ */ jsx10(Text9, { color: theme.muted, children: t("about.docs") }) }),
|
package/herdr-plugin.toml
CHANGED
package/package.json
CHANGED
package/src/host-connector.js
CHANGED
|
@@ -12,7 +12,13 @@ const { PtySession } = require('./pty-session');
|
|
|
12
12
|
const { resolveHerdrCommand, verifyHerdrCommand, herdrNotFoundMessage } = require('./herdr-command');
|
|
13
13
|
// The wire format lives in the relay package so both ends of the protocol are
|
|
14
14
|
// generated from one definition.
|
|
15
|
-
const {
|
|
15
|
+
const {
|
|
16
|
+
packStreamFrame,
|
|
17
|
+
unpackStreamFrame,
|
|
18
|
+
packStreamFrameV2,
|
|
19
|
+
FRAME_TYPE_OUTPUT,
|
|
20
|
+
PROTOCOL_VERSION,
|
|
21
|
+
} = require('herdr-remote-relay/protocol');
|
|
16
22
|
const { resolveHostPalette } = require('./terminal-palette');
|
|
17
23
|
const { EXIT_REPLACED, EXIT_AUTH_FAILED } = require('./exit-codes');
|
|
18
24
|
const { savePastedFile, cleanPastedDir } = require('./pasted-files');
|
|
@@ -77,6 +83,8 @@ class HostConnector {
|
|
|
77
83
|
: resolveHostPalette();
|
|
78
84
|
this.ws = null;
|
|
79
85
|
this.sessions = new Map();
|
|
86
|
+
this.streamIndexToId = new Map();
|
|
87
|
+
this.PtySession = options.PtySession || PtySession;
|
|
80
88
|
this.fastFailures = 0;
|
|
81
89
|
this.reconnectTimer = null;
|
|
82
90
|
this.heartbeatTimer = null;
|
|
@@ -170,6 +178,8 @@ class HostConnector {
|
|
|
170
178
|
this.ws = ws;
|
|
171
179
|
ws.isAlive = true;
|
|
172
180
|
ws.on('open', () => {
|
|
181
|
+
// Disable Nagle's algorithm to prevent small frames from stalling ~40ms when delayed ACK is active.
|
|
182
|
+
try { ws._socket?.setNoDelay(true); } catch {}
|
|
173
183
|
ws.isAlive = true;
|
|
174
184
|
this.ready = false;
|
|
175
185
|
this.authFailure = false;
|
|
@@ -183,7 +193,7 @@ class HostConnector {
|
|
|
183
193
|
platform: process.platform,
|
|
184
194
|
arch: process.arch,
|
|
185
195
|
terminalPalette: this.terminalPalette || null,
|
|
186
|
-
capabilities: ['host_handoff', 'idle_heartbeat'],
|
|
196
|
+
capabilities: ['host_handoff', 'idle_heartbeat', 'binary_frame_v2'],
|
|
187
197
|
});
|
|
188
198
|
// The relay sends host_ready with the current browser count. No business
|
|
189
199
|
// heartbeat is started until that message says somebody is watching.
|
|
@@ -246,7 +256,10 @@ class HostConnector {
|
|
|
246
256
|
process.stderr.write(`herdr-remote host connector: invalid relay frame: ${error.message}\n`);
|
|
247
257
|
return;
|
|
248
258
|
}
|
|
249
|
-
if (frame.type === 'input')
|
|
259
|
+
if (frame.type === 'input') {
|
|
260
|
+
const streamId = frame.version === 2 ? this.streamIndexToId.get(frame.streamIndex) : frame.streamId;
|
|
261
|
+
if (streamId) this.sessions.get(streamId)?.pty.write(frame.payload);
|
|
262
|
+
}
|
|
250
263
|
return;
|
|
251
264
|
}
|
|
252
265
|
let message;
|
|
@@ -312,6 +325,7 @@ class HostConnector {
|
|
|
312
325
|
const streamId = typeof message.streamId === 'string' ? message.streamId : message.clientId;
|
|
313
326
|
if (!streamId) return;
|
|
314
327
|
this.stopSession(streamId);
|
|
328
|
+
const streamIndex = typeof message.streamIndex === 'number' ? message.streamIndex : null;
|
|
315
329
|
const missingHerdr = this.ensureHerdrCommand();
|
|
316
330
|
if (missingHerdr) {
|
|
317
331
|
process.stderr.write(`herdr-remote host connector: ${missingHerdr}\n`);
|
|
@@ -323,7 +337,7 @@ class HostConnector {
|
|
|
323
337
|
sendJson(this.ws, { type: 'error', clientId: streamId, code: 'herdr_socket_unavailable', message: socketInfo.reason });
|
|
324
338
|
return;
|
|
325
339
|
}
|
|
326
|
-
const pty = new PtySession({
|
|
340
|
+
const pty = new this.PtySession({
|
|
327
341
|
command: this.herdrCommand,
|
|
328
342
|
args: this.herdrArgs,
|
|
329
343
|
cwd: this.cwd,
|
|
@@ -331,34 +345,75 @@ class HostConnector {
|
|
|
331
345
|
});
|
|
332
346
|
const session = {
|
|
333
347
|
id: streamId,
|
|
348
|
+
streamIndex,
|
|
334
349
|
pty,
|
|
335
350
|
cols: Number(message.cols) || PtySession.DEFAULT_COLS,
|
|
336
351
|
rows: Number(message.rows) || PtySession.DEFAULT_ROWS,
|
|
337
352
|
createdAt: new Date().toISOString(),
|
|
338
353
|
startedAtMs: Date.now(),
|
|
339
354
|
clientId: streamId,
|
|
355
|
+
pendingOutput: [],
|
|
356
|
+
flushImmediate: null,
|
|
357
|
+
};
|
|
358
|
+
const flushOutput = () => {
|
|
359
|
+
if (session.flushImmediate) {
|
|
360
|
+
clearImmediate(session.flushImmediate);
|
|
361
|
+
session.flushImmediate = null;
|
|
362
|
+
}
|
|
363
|
+
if (session.pendingOutput.length === 0) return;
|
|
364
|
+
const chunks = session.pendingOutput;
|
|
365
|
+
session.pendingOutput = [];
|
|
366
|
+
if (this.ws?.readyState === WebSocket.OPEN) {
|
|
367
|
+
const payload = Buffer.concat(chunks);
|
|
368
|
+
const frame = typeof session.streamIndex === 'number'
|
|
369
|
+
? packStreamFrameV2(FRAME_TYPE_OUTPUT, session.streamIndex, payload)
|
|
370
|
+
: packStreamFrame('output', streamId, payload);
|
|
371
|
+
this.ws.send(frame);
|
|
372
|
+
}
|
|
340
373
|
};
|
|
341
374
|
try {
|
|
342
375
|
pty.start({
|
|
343
376
|
cols: session.cols,
|
|
344
377
|
rows: session.rows,
|
|
345
378
|
onData: (data) => {
|
|
346
|
-
const
|
|
347
|
-
if (
|
|
379
|
+
const chunk = Buffer.isBuffer(data) ? data : Buffer.from(data, 'utf8');
|
|
380
|
+
if (chunk.length === 0) return;
|
|
381
|
+
session.pendingOutput.push(chunk);
|
|
382
|
+
if (!session.flushImmediate) {
|
|
383
|
+
// Coalesce burst output from the same tick into a single frame to reduce
|
|
384
|
+
// packet overhead without introducing timer latency.
|
|
385
|
+
session.flushImmediate = setImmediate(flushOutput);
|
|
386
|
+
}
|
|
348
387
|
},
|
|
349
388
|
onExit: ({ exitCode }) => {
|
|
350
389
|
if (this.sessions.get(streamId) !== session) return;
|
|
351
390
|
this.sessions.delete(streamId);
|
|
391
|
+
if (typeof session.streamIndex === 'number') {
|
|
392
|
+
this.streamIndexToId.delete(session.streamIndex);
|
|
393
|
+
}
|
|
394
|
+
if (session.flushImmediate) {
|
|
395
|
+
clearImmediate(session.flushImmediate);
|
|
396
|
+
session.flushImmediate = null;
|
|
397
|
+
}
|
|
398
|
+
session.pendingOutput = [];
|
|
352
399
|
this.reportSessionExit(session, exitCode);
|
|
353
400
|
this.sendHeartbeat();
|
|
354
401
|
},
|
|
355
402
|
});
|
|
356
403
|
} catch (error) {
|
|
404
|
+
if (session.flushImmediate) {
|
|
405
|
+
clearImmediate(session.flushImmediate);
|
|
406
|
+
session.flushImmediate = null;
|
|
407
|
+
}
|
|
408
|
+
session.pendingOutput = [];
|
|
357
409
|
sendJson(this.ws, { type: 'error', clientId: streamId, code: 'pty_start_failed', message: error.message });
|
|
358
410
|
pty.kill();
|
|
359
411
|
return;
|
|
360
412
|
}
|
|
361
413
|
this.sessions.set(streamId, session);
|
|
414
|
+
if (typeof streamIndex === 'number') {
|
|
415
|
+
this.streamIndexToId.set(streamIndex, streamId);
|
|
416
|
+
}
|
|
362
417
|
sendJson(this.ws, { type: 'session_ready', clientId: streamId });
|
|
363
418
|
this.sendHeartbeat();
|
|
364
419
|
}
|
|
@@ -393,6 +448,14 @@ class HostConnector {
|
|
|
393
448
|
const session = this.sessions.get(streamId);
|
|
394
449
|
if (!session) return;
|
|
395
450
|
this.sessions.delete(streamId);
|
|
451
|
+
if (typeof session.streamIndex === 'number') {
|
|
452
|
+
this.streamIndexToId.delete(session.streamIndex);
|
|
453
|
+
}
|
|
454
|
+
if (session.flushImmediate) {
|
|
455
|
+
clearImmediate(session.flushImmediate);
|
|
456
|
+
session.flushImmediate = null;
|
|
457
|
+
}
|
|
458
|
+
session.pendingOutput = [];
|
|
396
459
|
session.pty.kill();
|
|
397
460
|
this.sendHeartbeat();
|
|
398
461
|
}
|
|
@@ -445,8 +508,16 @@ class HostConnector {
|
|
|
445
508
|
}
|
|
446
509
|
|
|
447
510
|
destroySessions() {
|
|
448
|
-
for (const session of this.sessions.values())
|
|
511
|
+
for (const session of this.sessions.values()) {
|
|
512
|
+
if (session.flushImmediate) {
|
|
513
|
+
clearImmediate(session.flushImmediate);
|
|
514
|
+
session.flushImmediate = null;
|
|
515
|
+
}
|
|
516
|
+
session.pendingOutput = [];
|
|
517
|
+
session.pty.kill();
|
|
518
|
+
}
|
|
449
519
|
this.sessions.clear();
|
|
520
|
+
this.streamIndexToId.clear();
|
|
450
521
|
}
|
|
451
522
|
|
|
452
523
|
sendHeartbeat(force = false) {
|