@myagentroam/marctl 0.11.3 → 0.11.5
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/package.json +1 -1
- package/src/cli.mjs +58 -33
- package/src/client.mjs +26 -6
- package/src/config.mjs +1 -11
- package/src/direct-control.mjs +225 -0
- package/src/direct-errors.mjs +48 -0
- package/src/direct-process.mjs +40 -17
- package/src/direct-transport.mjs +235 -0
- package/src/direct.mjs +102 -494
- package/src/errors.mjs +11 -0
- package/src/i18n/en-US.mjs +62 -119
- package/src/i18n/zh-CN.mjs +57 -111
- package/src/scp-channel.mjs +99 -0
- package/src/scp-copy.mjs +46 -0
- package/src/scp-endpoint.mjs +29 -0
- package/src/scp-protocol.mjs +2 -0
- package/src/scp-workers.mjs +179 -0
- package/src/scp.mjs +26 -344
- package/src/session-args.mjs +247 -0
- package/src/session-input.mjs +88 -0
- package/src/session-prepare.mjs +99 -0
- package/src/session-target.mjs +26 -0
- package/src/session-wait.mjs +232 -0
- package/src/sessions.mjs +45 -573
- package/src/terminal-frame.mjs +48 -0
- package/src/terminal-interaction.mjs +177 -0
- package/src/terminal-race.mjs +117 -0
- package/src/terminal-relay.mjs +305 -0
- package/src/terminal.mjs +50 -603
package/src/terminal.mjs
CHANGED
|
@@ -1,11 +1,10 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
1
|
+
import { MarctlError } from './errors.mjs';
|
|
2
|
+
import { DirectRejectedError } from './direct-errors.mjs';
|
|
3
3
|
import { createTranslator } from './i18n/index.mjs';
|
|
4
|
-
import {
|
|
4
|
+
import { raceTerminalConnections } from './terminal-race.mjs';
|
|
5
|
+
import { openRelayTransport } from './terminal-relay.mjs';
|
|
6
|
+
import { getDimensions, runAttachedTerminal } from './terminal-interaction.mjs';
|
|
5
7
|
|
|
6
|
-
const MAX_INPUT_BYTES = 65536;
|
|
7
|
-
const DEFAULT_ROWS = 24;
|
|
8
|
-
const DEFAULT_COLS = 80;
|
|
9
8
|
const DEFAULT_CONNECTION_TIMEOUT_MS = 15000;
|
|
10
9
|
|
|
11
10
|
export async function runTerminalSession({
|
|
@@ -17,7 +16,7 @@ export async function runTerminalSession({
|
|
|
17
16
|
attached,
|
|
18
17
|
stdin = process.stdin,
|
|
19
18
|
stdout = process.stdout,
|
|
20
|
-
|
|
19
|
+
signal,
|
|
21
20
|
translate = createTranslator(),
|
|
22
21
|
connectionTimeoutMs = DEFAULT_CONNECTION_TIMEOUT_MS,
|
|
23
22
|
openDirectTerminal: openDirectTerminalImpl,
|
|
@@ -31,10 +30,12 @@ export async function runTerminalSession({
|
|
|
31
30
|
let manifest;
|
|
32
31
|
|
|
33
32
|
try {
|
|
33
|
+
if (signal?.aborted) return signal.reason === 'SIGTERM' ? 143 : 130;
|
|
34
34
|
if (binary) {
|
|
35
35
|
if (command === undefined)
|
|
36
36
|
throw new MarctlError(translate('errors.terminalBinaryUnavailable'));
|
|
37
37
|
manifest = await client.getTerminalManifest(nodeId);
|
|
38
|
+
if (signal?.aborted) return signal.reason === 'SIGTERM' ? 143 : 130;
|
|
38
39
|
if (manifest?.binaryTerminal !== true)
|
|
39
40
|
throw new MarctlError(translate('errors.terminalBinaryUnavailable'));
|
|
40
41
|
}
|
|
@@ -47,6 +48,7 @@ export async function runTerminalSession({
|
|
|
47
48
|
terminalId = binary ? created?.terminalId : created;
|
|
48
49
|
if (binary && created?.binary !== true)
|
|
49
50
|
throw new MarctlError(translate('errors.terminalBinaryUnavailable'));
|
|
51
|
+
if (signal?.aborted) return signal.reason === 'SIGTERM' ? 143 : 130;
|
|
50
52
|
try {
|
|
51
53
|
const openDirectTerminal =
|
|
52
54
|
openDirectTerminalImpl ?? (await import('./direct.mjs')).openDirectTerminal;
|
|
@@ -59,7 +61,8 @@ export async function runTerminalSession({
|
|
|
59
61
|
terminalId,
|
|
60
62
|
manifest,
|
|
61
63
|
afterSequence: 0,
|
|
62
|
-
timeoutMs: connectionTimeoutMs
|
|
64
|
+
timeoutMs: connectionTimeoutMs,
|
|
65
|
+
signal
|
|
63
66
|
};
|
|
64
67
|
const canRace = raceConnections && manifest?.deferredRelay === true;
|
|
65
68
|
transport = canRace
|
|
@@ -71,9 +74,11 @@ export async function runTerminalSession({
|
|
|
71
74
|
nodeId,
|
|
72
75
|
terminalId,
|
|
73
76
|
translate,
|
|
74
|
-
connectionTimeoutMs
|
|
77
|
+
connectionTimeoutMs,
|
|
78
|
+
signal
|
|
75
79
|
})
|
|
76
80
|
: await openDirectTerminal(directOptions);
|
|
81
|
+
if (signal?.aborted) return signal.reason === 'SIGTERM' ? 143 : 130;
|
|
77
82
|
result =
|
|
78
83
|
attached === undefined
|
|
79
84
|
? await runAttachedTerminal({
|
|
@@ -82,37 +87,44 @@ export async function runTerminalSession({
|
|
|
82
87
|
commandMode: command !== undefined,
|
|
83
88
|
stdin,
|
|
84
89
|
stdout,
|
|
85
|
-
|
|
90
|
+
signal,
|
|
86
91
|
translate
|
|
87
92
|
})
|
|
88
93
|
: await attached({ transport, terminalId, translate });
|
|
89
94
|
} catch (error) {
|
|
90
|
-
if (
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
95
|
+
if (signal?.aborted) {
|
|
96
|
+
result = signal.reason === 'SIGTERM' ? 143 : 130;
|
|
97
|
+
} else {
|
|
98
|
+
if (error?.name === 'DirectUnsafeError')
|
|
99
|
+
throw new MarctlError(translate('errors.terminalDirectUnsafeFallback'));
|
|
100
|
+
if (error?.name !== 'DirectFallbackError') throw error;
|
|
101
|
+
transport = await openRelayTransport({
|
|
102
|
+
client,
|
|
103
|
+
WebSocketClass,
|
|
104
|
+
nodeId,
|
|
105
|
+
terminalId,
|
|
106
|
+
translate,
|
|
107
|
+
connectionTimeoutMs,
|
|
108
|
+
signal
|
|
109
|
+
});
|
|
110
|
+
if (signal?.aborted) return signal.reason === 'SIGTERM' ? 143 : 130;
|
|
111
|
+
result =
|
|
112
|
+
attached === undefined
|
|
113
|
+
? await runAttachedTerminal({
|
|
114
|
+
transport,
|
|
115
|
+
terminalId,
|
|
116
|
+
commandMode: command !== undefined,
|
|
117
|
+
stdin,
|
|
118
|
+
stdout,
|
|
119
|
+
signal,
|
|
120
|
+
translate
|
|
121
|
+
})
|
|
122
|
+
: await attached({ transport, terminalId, translate });
|
|
123
|
+
}
|
|
113
124
|
}
|
|
114
125
|
} catch (error) {
|
|
115
|
-
|
|
126
|
+
if (signal?.aborted) result = signal.reason === 'SIGTERM' ? 143 : 130;
|
|
127
|
+
else primaryError = toMarctlError(error, translate);
|
|
116
128
|
} finally {
|
|
117
129
|
transport?.close?.();
|
|
118
130
|
if (terminalId !== undefined) {
|
|
@@ -129,576 +141,11 @@ export async function runTerminalSession({
|
|
|
129
141
|
return result;
|
|
130
142
|
}
|
|
131
143
|
|
|
132
|
-
async function raceTerminalConnections({
|
|
133
|
-
openDirectTerminal,
|
|
134
|
-
directOptions,
|
|
135
|
-
client,
|
|
136
|
-
WebSocketClass,
|
|
137
|
-
nodeId,
|
|
138
|
-
terminalId,
|
|
139
|
-
translate,
|
|
140
|
-
connectionTimeoutMs
|
|
141
|
-
}) {
|
|
142
|
-
const abort = new AbortController();
|
|
143
|
-
const relayAbort = new AbortController();
|
|
144
|
-
const preparing = new Set(['direct', 'relay']);
|
|
145
|
-
let start;
|
|
146
|
-
const preparation = new Promise((resolve) => {
|
|
147
|
-
start = resolve;
|
|
148
|
-
});
|
|
149
|
-
const prepared = (candidate) => {
|
|
150
|
-
preparing.delete(candidate);
|
|
151
|
-
if (preparing.size === 0) start();
|
|
152
|
-
return preparation;
|
|
153
|
-
};
|
|
154
|
-
let selected;
|
|
155
|
-
let announce;
|
|
156
|
-
const first = new Promise((resolve) => {
|
|
157
|
-
announce = resolve;
|
|
158
|
-
});
|
|
159
|
-
const select = (candidate) => {
|
|
160
|
-
if (selected) return false;
|
|
161
|
-
selected = candidate;
|
|
162
|
-
announce(candidate);
|
|
163
|
-
if (candidate === 'relay') abort.abort();
|
|
164
|
-
else relayAbort.abort();
|
|
165
|
-
return true;
|
|
166
|
-
};
|
|
167
|
-
const direct = openDirectTerminal({
|
|
168
|
-
...directOptions,
|
|
169
|
-
signal: abort.signal,
|
|
170
|
-
onPrepared: () => prepared('direct'),
|
|
171
|
-
onIceConnected: () => {
|
|
172
|
-
if (!select('direct')) throw new Error('DIRECT_RACE_CANCELLED');
|
|
173
|
-
},
|
|
174
|
-
onTransportReady: () => {
|
|
175
|
-
if (selected !== 'direct') throw new Error('DIRECT_RACE_CANCELLED');
|
|
176
|
-
}
|
|
177
|
-
}).then(
|
|
178
|
-
(value) => ({ value }),
|
|
179
|
-
(error) => {
|
|
180
|
-
prepared('direct');
|
|
181
|
-
return { error };
|
|
182
|
-
}
|
|
183
|
-
);
|
|
184
|
-
const relay = prepareRelayTransport({
|
|
185
|
-
client,
|
|
186
|
-
WebSocketClass,
|
|
187
|
-
nodeId,
|
|
188
|
-
terminalId,
|
|
189
|
-
translate,
|
|
190
|
-
connectionTimeoutMs,
|
|
191
|
-
signal: relayAbort.signal,
|
|
192
|
-
onPrepared: () => prepared('relay')
|
|
193
|
-
}).then(
|
|
194
|
-
(value) => {
|
|
195
|
-
if (!select('relay')) value.close();
|
|
196
|
-
return { value };
|
|
197
|
-
},
|
|
198
|
-
(error) => {
|
|
199
|
-
prepared('relay');
|
|
200
|
-
return { error };
|
|
201
|
-
}
|
|
202
|
-
);
|
|
203
|
-
const winner = await Promise.race([
|
|
204
|
-
first,
|
|
205
|
-
Promise.all([direct, relay]).then(() => selected ?? 'none')
|
|
206
|
-
]);
|
|
207
|
-
if (winner === 'direct') {
|
|
208
|
-
const result = await direct;
|
|
209
|
-
if (result.error) throw result.error;
|
|
210
|
-
return result.value;
|
|
211
|
-
}
|
|
212
|
-
if (winner === 'relay') {
|
|
213
|
-
const result = await relay;
|
|
214
|
-
if (result.error) throw result.error;
|
|
215
|
-
return result.value.activate();
|
|
216
|
-
}
|
|
217
|
-
const [directResult, relayResult] = await Promise.all([direct, relay]);
|
|
218
|
-
throw relayResult.error ?? directResult.error;
|
|
219
|
-
}
|
|
220
|
-
|
|
221
|
-
export function splitInput(text, maxBytes = MAX_INPUT_BYTES) {
|
|
222
|
-
const value = Buffer.isBuffer(text) ? text.toString('utf8') : String(text);
|
|
223
|
-
const chunks = [];
|
|
224
|
-
let chunk = '';
|
|
225
|
-
let chunkBytes = 0;
|
|
226
|
-
|
|
227
|
-
for (const character of value) {
|
|
228
|
-
const characterBytes = Buffer.byteLength(character);
|
|
229
|
-
if (chunk !== '' && chunkBytes + characterBytes > maxBytes) {
|
|
230
|
-
chunks.push(chunk);
|
|
231
|
-
chunk = '';
|
|
232
|
-
chunkBytes = 0;
|
|
233
|
-
}
|
|
234
|
-
if (characterBytes > maxBytes) continue;
|
|
235
|
-
chunk += character;
|
|
236
|
-
chunkBytes += characterBytes;
|
|
237
|
-
}
|
|
238
|
-
if (chunk !== '') chunks.push(chunk);
|
|
239
|
-
return chunks;
|
|
240
|
-
}
|
|
241
|
-
|
|
242
|
-
function getDimensions(stdout) {
|
|
243
|
-
const rows = Number.isSafeInteger(stdout.rows) && stdout.rows > 0 ? stdout.rows : DEFAULT_ROWS;
|
|
244
|
-
const cols =
|
|
245
|
-
Number.isSafeInteger(stdout.columns) && stdout.columns > 0 ? stdout.columns : DEFAULT_COLS;
|
|
246
|
-
return { rows: Math.min(rows, 1000), cols: Math.min(cols, 1000) };
|
|
247
|
-
}
|
|
248
|
-
|
|
249
|
-
async function openRelayTransport({
|
|
250
|
-
client,
|
|
251
|
-
WebSocketClass,
|
|
252
|
-
nodeId,
|
|
253
|
-
terminalId,
|
|
254
|
-
translate,
|
|
255
|
-
connectionTimeoutMs
|
|
256
|
-
}) {
|
|
257
|
-
const ticket = await client.createAttachTicket({ nodeId, terminalId });
|
|
258
|
-
const socket = client.openTerminalWebSocket(WebSocketClass, { terminalId, ticket });
|
|
259
|
-
const transport = new BufferedRelayTransport({
|
|
260
|
-
socket,
|
|
261
|
-
terminalId,
|
|
262
|
-
translate,
|
|
263
|
-
connectionTimeoutMs
|
|
264
|
-
});
|
|
265
|
-
try {
|
|
266
|
-
await transport.waitForAttachment();
|
|
267
|
-
return transport;
|
|
268
|
-
} catch (error) {
|
|
269
|
-
transport.close();
|
|
270
|
-
throw error;
|
|
271
|
-
}
|
|
272
|
-
}
|
|
273
|
-
|
|
274
|
-
async function prepareRelayTransport({
|
|
275
|
-
client,
|
|
276
|
-
WebSocketClass,
|
|
277
|
-
nodeId,
|
|
278
|
-
terminalId,
|
|
279
|
-
translate,
|
|
280
|
-
connectionTimeoutMs,
|
|
281
|
-
signal,
|
|
282
|
-
onPrepared
|
|
283
|
-
}) {
|
|
284
|
-
const ticket = await client.createAttachTicket({ nodeId, terminalId });
|
|
285
|
-
if (onPrepared) await onPrepared();
|
|
286
|
-
if (signal?.aborted) throw new MarctlError(translate('errors.terminalAttachConnection'));
|
|
287
|
-
const socket = client.openTerminalWebSocket(WebSocketClass, {
|
|
288
|
-
terminalId,
|
|
289
|
-
ticket,
|
|
290
|
-
deferred: true
|
|
291
|
-
});
|
|
292
|
-
// Keep an error listener after readiness until the socket is discarded.
|
|
293
|
-
socket.on('error', () => {});
|
|
294
|
-
let activated = false;
|
|
295
|
-
const ready = new Promise((resolve, reject) => {
|
|
296
|
-
const timer = setTimeout(() => fail(), connectionTimeoutMs);
|
|
297
|
-
const cleanup = () => {
|
|
298
|
-
clearTimeout(timer);
|
|
299
|
-
socket.removeListener?.('message', onMessage);
|
|
300
|
-
socket.removeListener?.('error', fail);
|
|
301
|
-
socket.removeListener?.('close', fail);
|
|
302
|
-
signal?.removeEventListener('abort', fail);
|
|
303
|
-
};
|
|
304
|
-
const fail = () => {
|
|
305
|
-
cleanup();
|
|
306
|
-
try {
|
|
307
|
-
socket.close();
|
|
308
|
-
} catch {
|
|
309
|
-
/* Connection not yet established. */
|
|
310
|
-
}
|
|
311
|
-
reject(new MarctlError(translate('errors.terminalAttachConnection')));
|
|
312
|
-
};
|
|
313
|
-
const onMessage = (raw) => {
|
|
314
|
-
let frame;
|
|
315
|
-
try {
|
|
316
|
-
frame = JSON.parse(String(raw));
|
|
317
|
-
} catch {
|
|
318
|
-
return fail();
|
|
319
|
-
}
|
|
320
|
-
if (frame?.type !== 'ready' || frame.terminalId !== terminalId) return fail();
|
|
321
|
-
cleanup();
|
|
322
|
-
resolve();
|
|
323
|
-
};
|
|
324
|
-
socket.on('message', onMessage);
|
|
325
|
-
socket.on('error', fail);
|
|
326
|
-
socket.on('close', fail);
|
|
327
|
-
signal?.addEventListener('abort', fail, { once: true });
|
|
328
|
-
if (signal?.aborted) fail();
|
|
329
|
-
});
|
|
330
|
-
await ready;
|
|
331
|
-
return {
|
|
332
|
-
async activate() {
|
|
333
|
-
if (activated || socket.readyState !== 1)
|
|
334
|
-
throw new MarctlError(translate('errors.terminalAttachConnection'));
|
|
335
|
-
activated = true;
|
|
336
|
-
const transport = new BufferedRelayTransport({
|
|
337
|
-
socket,
|
|
338
|
-
terminalId,
|
|
339
|
-
translate,
|
|
340
|
-
connectionTimeoutMs
|
|
341
|
-
});
|
|
342
|
-
try {
|
|
343
|
-
socket.send(JSON.stringify({ type: 'activate', terminalId }));
|
|
344
|
-
await transport.waitForAttachment();
|
|
345
|
-
return transport;
|
|
346
|
-
} catch (error) {
|
|
347
|
-
transport.close();
|
|
348
|
-
throw error;
|
|
349
|
-
}
|
|
350
|
-
},
|
|
351
|
-
close() {
|
|
352
|
-
socket.close();
|
|
353
|
-
}
|
|
354
|
-
};
|
|
355
|
-
}
|
|
356
|
-
|
|
357
|
-
class BufferedRelayTransport extends EventEmitter {
|
|
358
|
-
#socket;
|
|
359
|
-
#sender;
|
|
360
|
-
#terminalId;
|
|
361
|
-
#translate;
|
|
362
|
-
#attached = false;
|
|
363
|
-
#active = false;
|
|
364
|
-
#pendingEvents = [];
|
|
365
|
-
#attachmentResolve;
|
|
366
|
-
#attachmentReject;
|
|
367
|
-
#openTimer;
|
|
368
|
-
#attachTimer;
|
|
369
|
-
#closed = false;
|
|
370
|
-
#onOpen;
|
|
371
|
-
#onMessage;
|
|
372
|
-
#onError;
|
|
373
|
-
#onClose;
|
|
374
|
-
|
|
375
|
-
constructor({ socket, terminalId, translate, connectionTimeoutMs }) {
|
|
376
|
-
super();
|
|
377
|
-
this.#socket = socket;
|
|
378
|
-
this.#sender = new TerminalRelaySender(socket);
|
|
379
|
-
this.#terminalId = terminalId;
|
|
380
|
-
this.#translate = translate;
|
|
381
|
-
this.#onOpen = () => {
|
|
382
|
-
clearTimeout(this.#openTimer);
|
|
383
|
-
this.#armAttachTimer(connectionTimeoutMs);
|
|
384
|
-
};
|
|
385
|
-
this.#onMessage = (raw, isBinary) => {
|
|
386
|
-
try {
|
|
387
|
-
const frame = parseFrame(raw, isBinary, this.#terminalId, this.#translate);
|
|
388
|
-
if (!this.#attached) {
|
|
389
|
-
if (frame.type === 'attach') {
|
|
390
|
-
this.#attached = true;
|
|
391
|
-
clearTimeout(this.#attachTimer);
|
|
392
|
-
this.#attachmentResolve?.();
|
|
393
|
-
this.#attachmentResolve = undefined;
|
|
394
|
-
this.#attachmentReject = undefined;
|
|
395
|
-
return;
|
|
396
|
-
}
|
|
397
|
-
if (frame.type === 'error')
|
|
398
|
-
throw new MarctlError(this.#translate('errors.terminalRemoteError'));
|
|
399
|
-
this.#queueOrEmit('message', raw, isBinary);
|
|
400
|
-
return;
|
|
401
|
-
}
|
|
402
|
-
this.#queueOrEmit('message', raw, isBinary);
|
|
403
|
-
} catch (error) {
|
|
404
|
-
this.#fail(
|
|
405
|
-
error instanceof MarctlError
|
|
406
|
-
? error
|
|
407
|
-
: new MarctlError(this.#translate('errors.terminalFrameInvalid'))
|
|
408
|
-
);
|
|
409
|
-
}
|
|
410
|
-
};
|
|
411
|
-
this.#onError = () => {
|
|
412
|
-
const error = new MarctlError(
|
|
413
|
-
this.#translate(
|
|
414
|
-
this.#attached ? 'errors.terminalConnectionFailed' : 'errors.terminalAttachConnection'
|
|
415
|
-
)
|
|
416
|
-
);
|
|
417
|
-
if (this.#attached) this.#queueOrEmit('error', error);
|
|
418
|
-
else this.#fail(error);
|
|
419
|
-
};
|
|
420
|
-
this.#onClose = () => {
|
|
421
|
-
this.#sender.close();
|
|
422
|
-
const error = new MarctlError(
|
|
423
|
-
this.#translate(
|
|
424
|
-
this.#attached ? 'errors.terminalConnectionLost' : 'errors.terminalAttachLost'
|
|
425
|
-
)
|
|
426
|
-
);
|
|
427
|
-
if (this.#attached) this.#queueOrEmit('close');
|
|
428
|
-
else this.#fail(error);
|
|
429
|
-
};
|
|
430
|
-
|
|
431
|
-
socket.on?.('open', this.#onOpen);
|
|
432
|
-
socket.on?.('message', this.#onMessage);
|
|
433
|
-
socket.on?.('error', this.#onError);
|
|
434
|
-
socket.on?.('close', this.#onClose);
|
|
435
|
-
if (socket.readyState === 1) this.#onOpen();
|
|
436
|
-
else
|
|
437
|
-
this.#openTimer = setTimeout(
|
|
438
|
-
() => this.#fail(new MarctlError(this.#translate('errors.terminalConnectionOpen'))),
|
|
439
|
-
connectionTimeoutMs
|
|
440
|
-
);
|
|
441
|
-
}
|
|
442
|
-
|
|
443
|
-
waitForAttachment() {
|
|
444
|
-
return new Promise((resolve, reject) => {
|
|
445
|
-
if (this.#closed) {
|
|
446
|
-
reject(new MarctlError(this.#translate('errors.terminalAttachLost')));
|
|
447
|
-
return;
|
|
448
|
-
}
|
|
449
|
-
this.#attachmentResolve = resolve;
|
|
450
|
-
this.#attachmentReject = reject;
|
|
451
|
-
if (this.#attached) {
|
|
452
|
-
this.#attachmentResolve = undefined;
|
|
453
|
-
this.#attachmentReject = undefined;
|
|
454
|
-
resolve();
|
|
455
|
-
}
|
|
456
|
-
});
|
|
457
|
-
}
|
|
458
|
-
|
|
459
|
-
activate() {
|
|
460
|
-
if (this.#active) return;
|
|
461
|
-
this.#active = true;
|
|
462
|
-
const pendingEvents = this.#pendingEvents;
|
|
463
|
-
this.#pendingEvents = [];
|
|
464
|
-
for (const { type, args } of pendingEvents) this.emit(type, ...args);
|
|
465
|
-
}
|
|
466
|
-
|
|
467
|
-
get readyState() {
|
|
468
|
-
return this.#socket.readyState;
|
|
469
|
-
}
|
|
470
|
-
|
|
471
|
-
send(value) {
|
|
472
|
-
if (this.#closed) throw new Error('TERMINAL_CONNECTION_LOST');
|
|
473
|
-
if (JSON.parse(value).type === 'input') return this.#sender.send(value);
|
|
474
|
-
this.#socket.send(value);
|
|
475
|
-
}
|
|
476
|
-
|
|
477
|
-
close() {
|
|
478
|
-
if (this.#closed) return;
|
|
479
|
-
this.#closed = true;
|
|
480
|
-
this.#sender.close();
|
|
481
|
-
clearTimeout(this.#openTimer);
|
|
482
|
-
clearTimeout(this.#attachTimer);
|
|
483
|
-
this.#socket.removeListener?.('open', this.#onOpen);
|
|
484
|
-
this.#socket.removeListener?.('message', this.#onMessage);
|
|
485
|
-
this.#socket.removeListener?.('error', this.#onError);
|
|
486
|
-
this.#socket.removeListener?.('close', this.#onClose);
|
|
487
|
-
try {
|
|
488
|
-
this.#socket.close();
|
|
489
|
-
} catch {
|
|
490
|
-
// The connection is already unavailable.
|
|
491
|
-
}
|
|
492
|
-
}
|
|
493
|
-
|
|
494
|
-
#armAttachTimer(connectionTimeoutMs) {
|
|
495
|
-
if (this.#attached) return;
|
|
496
|
-
this.#attachTimer = setTimeout(
|
|
497
|
-
() => this.#fail(new MarctlError(this.#translate('errors.terminalAttachTimeout'))),
|
|
498
|
-
connectionTimeoutMs
|
|
499
|
-
);
|
|
500
|
-
}
|
|
501
|
-
|
|
502
|
-
#fail(error) {
|
|
503
|
-
if (this.#closed) return;
|
|
504
|
-
clearTimeout(this.#openTimer);
|
|
505
|
-
clearTimeout(this.#attachTimer);
|
|
506
|
-
this.#attachmentResolve = undefined;
|
|
507
|
-
const reject = this.#attachmentReject;
|
|
508
|
-
this.#attachmentReject = undefined;
|
|
509
|
-
if (!this.#attached) reject?.(error);
|
|
510
|
-
else this.#queueOrEmit('error', error);
|
|
511
|
-
}
|
|
512
|
-
|
|
513
|
-
#queueOrEmit(type, ...args) {
|
|
514
|
-
if (this.#active) {
|
|
515
|
-
if (type !== 'error' || this.listenerCount('error') > 0) this.emit(type, ...args);
|
|
516
|
-
return;
|
|
517
|
-
}
|
|
518
|
-
this.#pendingEvents.push({ type, args });
|
|
519
|
-
}
|
|
520
|
-
}
|
|
521
|
-
|
|
522
|
-
function runAttachedTerminal({
|
|
523
|
-
transport,
|
|
524
|
-
terminalId,
|
|
525
|
-
commandMode,
|
|
526
|
-
stdin,
|
|
527
|
-
stdout,
|
|
528
|
-
signals,
|
|
529
|
-
translate
|
|
530
|
-
}) {
|
|
531
|
-
return new Promise((resolve, reject) => {
|
|
532
|
-
let settled = false;
|
|
533
|
-
let inputEnabled = false;
|
|
534
|
-
let rawModeEnabled = false;
|
|
535
|
-
let inputTasks = 0;
|
|
536
|
-
|
|
537
|
-
const cleanup = () => {
|
|
538
|
-
transport.removeListener?.('message', onMessage);
|
|
539
|
-
transport.removeListener?.('error', onError);
|
|
540
|
-
transport.removeListener?.('close', onClose);
|
|
541
|
-
stdin.removeListener?.('data', onInput);
|
|
542
|
-
stdout.removeListener?.('resize', onResize);
|
|
543
|
-
signals.removeListener?.('SIGINT', onSigint);
|
|
544
|
-
signals.removeListener?.('SIGTERM', onSigterm);
|
|
545
|
-
if (rawModeEnabled) {
|
|
546
|
-
try {
|
|
547
|
-
stdin.setRawMode(false);
|
|
548
|
-
} catch {
|
|
549
|
-
// The terminal may already be gone.
|
|
550
|
-
}
|
|
551
|
-
}
|
|
552
|
-
stdin.pause?.();
|
|
553
|
-
};
|
|
554
|
-
|
|
555
|
-
const finish = (error, exitCode) => {
|
|
556
|
-
if (settled) return;
|
|
557
|
-
settled = true;
|
|
558
|
-
cleanup();
|
|
559
|
-
sendCloseFrame(transport, terminalId);
|
|
560
|
-
transport.close?.();
|
|
561
|
-
if (error === undefined) resolve(exitCode);
|
|
562
|
-
else reject(error);
|
|
563
|
-
};
|
|
564
|
-
const fail = (key) => finish(new MarctlError(translate(key)));
|
|
565
|
-
const send = (frame) => {
|
|
566
|
-
return transport.send(JSON.stringify(frame));
|
|
567
|
-
};
|
|
568
|
-
const onInput = async (data) => {
|
|
569
|
-
inputTasks += 1;
|
|
570
|
-
stdin.pause?.();
|
|
571
|
-
try {
|
|
572
|
-
for (const chunk of splitInput(data)) {
|
|
573
|
-
if (settled) return;
|
|
574
|
-
await send({ type: 'input', terminalId, data: chunk });
|
|
575
|
-
}
|
|
576
|
-
} catch {
|
|
577
|
-
fail('errors.terminalInputFailed');
|
|
578
|
-
} finally {
|
|
579
|
-
inputTasks -= 1;
|
|
580
|
-
if (!settled && inputTasks === 0) stdin.resume?.();
|
|
581
|
-
}
|
|
582
|
-
};
|
|
583
|
-
const onResize = () => {
|
|
584
|
-
try {
|
|
585
|
-
const { rows, cols } = getDimensions(stdout);
|
|
586
|
-
Promise.resolve(send({ type: 'resize', terminalId, rows, cols })).catch(() =>
|
|
587
|
-
fail('errors.terminalResizeFailed')
|
|
588
|
-
);
|
|
589
|
-
} catch {
|
|
590
|
-
fail('errors.terminalResizeFailed');
|
|
591
|
-
}
|
|
592
|
-
};
|
|
593
|
-
const onMessage = (raw, isBinary) => {
|
|
594
|
-
try {
|
|
595
|
-
const frame = parseFrame(raw, isBinary, terminalId, translate);
|
|
596
|
-
if (frame.type === 'output') stdout.write(frame.data);
|
|
597
|
-
else if (frame.type === 'exit') {
|
|
598
|
-
finish(undefined, frame.exitCode === null ? 1 : frame.exitCode);
|
|
599
|
-
} else if (frame.type === 'error') fail('errors.terminalRemoteError');
|
|
600
|
-
else fail('errors.terminalUnexpectedFrame');
|
|
601
|
-
} catch (error) {
|
|
602
|
-
finish(
|
|
603
|
-
error instanceof MarctlError
|
|
604
|
-
? error
|
|
605
|
-
: new MarctlError(translate('errors.terminalFrameInvalid'))
|
|
606
|
-
);
|
|
607
|
-
}
|
|
608
|
-
};
|
|
609
|
-
const onError = () => finish(new MarctlError(translate('errors.terminalConnectionFailed')));
|
|
610
|
-
const onClose = () => finish(new MarctlError(translate('errors.terminalConnectionLost')));
|
|
611
|
-
const onSigint = () => finish(undefined, 130);
|
|
612
|
-
const onSigterm = () => finish(undefined, 143);
|
|
613
|
-
const startInput = () => {
|
|
614
|
-
if (settled || inputEnabled) return;
|
|
615
|
-
inputEnabled = true;
|
|
616
|
-
try {
|
|
617
|
-
if (!commandMode) {
|
|
618
|
-
if (typeof stdin.setRawMode !== 'function') {
|
|
619
|
-
fail('errors.terminalInteractiveRequired');
|
|
620
|
-
return;
|
|
621
|
-
}
|
|
622
|
-
stdin.setRawMode(true);
|
|
623
|
-
rawModeEnabled = true;
|
|
624
|
-
}
|
|
625
|
-
stdin.resume?.();
|
|
626
|
-
stdin.on?.('data', onInput);
|
|
627
|
-
stdout.on?.('resize', onResize);
|
|
628
|
-
onResize();
|
|
629
|
-
} catch {
|
|
630
|
-
fail(commandMode ? 'errors.terminalSessionFailed' : 'errors.terminalInteractiveInit');
|
|
631
|
-
}
|
|
632
|
-
};
|
|
633
|
-
transport.on?.('message', onMessage);
|
|
634
|
-
transport.on?.('error', onError);
|
|
635
|
-
transport.on?.('close', onClose);
|
|
636
|
-
signals.on?.('SIGINT', onSigint);
|
|
637
|
-
signals.on?.('SIGTERM', onSigterm);
|
|
638
|
-
transport.activate?.();
|
|
639
|
-
startInput();
|
|
640
|
-
});
|
|
641
|
-
}
|
|
642
|
-
|
|
643
|
-
export function parseFrame(raw, isBinary, terminalId, translate) {
|
|
644
|
-
if (isBinary) throw new MarctlError(translate('errors.terminalBinaryFrame'));
|
|
645
|
-
let frame;
|
|
646
|
-
try {
|
|
647
|
-
frame = JSON.parse(rawToString(raw));
|
|
648
|
-
} catch {
|
|
649
|
-
throw new MarctlError(translate('errors.terminalInvalidJson'));
|
|
650
|
-
}
|
|
651
|
-
if (frame === null || typeof frame !== 'object' || frame.terminalId !== terminalId)
|
|
652
|
-
throw new MarctlError(translate('errors.terminalFrameInvalid'));
|
|
653
|
-
|
|
654
|
-
if (frame.type === 'attach') {
|
|
655
|
-
if (!Number.isSafeInteger(frame.afterSequence) || frame.afterSequence < 0)
|
|
656
|
-
throw new MarctlError(translate('errors.terminalFrameInvalid'));
|
|
657
|
-
return frame;
|
|
658
|
-
}
|
|
659
|
-
if (frame.type === 'output') {
|
|
660
|
-
if (
|
|
661
|
-
!Number.isSafeInteger(frame.sequence) ||
|
|
662
|
-
frame.sequence < 1 ||
|
|
663
|
-
typeof frame.data !== 'string' ||
|
|
664
|
-
Buffer.byteLength(frame.data) > MAX_INPUT_BYTES
|
|
665
|
-
)
|
|
666
|
-
throw new MarctlError(translate('errors.terminalFrameInvalid'));
|
|
667
|
-
return frame;
|
|
668
|
-
}
|
|
669
|
-
if (frame.type === 'exit') {
|
|
670
|
-
if (frame.exitCode !== null && !Number.isSafeInteger(frame.exitCode))
|
|
671
|
-
throw new MarctlError(translate('errors.terminalFrameInvalid'));
|
|
672
|
-
return frame;
|
|
673
|
-
}
|
|
674
|
-
if (frame.type === 'error' && typeof frame.code === 'string' && frame.code.length > 0)
|
|
675
|
-
return frame;
|
|
676
|
-
throw new MarctlError(translate('errors.terminalFrameInvalid'));
|
|
677
|
-
}
|
|
678
|
-
|
|
679
|
-
function rawToString(raw) {
|
|
680
|
-
if (typeof raw === 'string') return raw;
|
|
681
|
-
if (Buffer.isBuffer(raw)) return raw.toString('utf8');
|
|
682
|
-
if (raw instanceof ArrayBuffer) return Buffer.from(raw).toString('utf8');
|
|
683
|
-
if (ArrayBuffer.isView(raw))
|
|
684
|
-
return Buffer.from(raw.buffer, raw.byteOffset, raw.byteLength).toString('utf8');
|
|
685
|
-
return String(raw);
|
|
686
|
-
}
|
|
687
|
-
|
|
688
|
-
function sendCloseFrame(socket, terminalId) {
|
|
689
|
-
if (!terminalId || !isSocketOpen(socket)) return;
|
|
690
|
-
try {
|
|
691
|
-
Promise.resolve(socket.send(JSON.stringify({ type: 'close', terminalId }))).catch(() => {});
|
|
692
|
-
} catch {
|
|
693
|
-
// The DELETE request below remains the authoritative cleanup path.
|
|
694
|
-
}
|
|
695
|
-
}
|
|
696
|
-
|
|
697
|
-
function isSocketOpen(socket) {
|
|
698
|
-
return socket.readyState === undefined || socket.readyState === 1;
|
|
699
|
-
}
|
|
700
|
-
|
|
701
144
|
function toMarctlError(error, translate) {
|
|
145
|
+
if (error instanceof DirectRejectedError)
|
|
146
|
+
return new MarctlError(`${translate('errors.terminalSessionFailed')} [${error.code}]`, {
|
|
147
|
+
code: error.code
|
|
148
|
+
});
|
|
702
149
|
return error instanceof MarctlError
|
|
703
150
|
? error
|
|
704
151
|
: new MarctlError(translate('errors.terminalSessionFailed'));
|