@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/package.json
CHANGED
package/src/cli.mjs
CHANGED
|
@@ -1,10 +1,26 @@
|
|
|
1
1
|
import { readFile } from 'node:fs/promises';
|
|
2
2
|
import { MarctlClient } from './client.mjs';
|
|
3
|
-
import {
|
|
3
|
+
import { loadConfig } from './config.mjs';
|
|
4
|
+
import { MarctlError } from './errors.mjs';
|
|
4
5
|
import { createTranslator } from './i18n/index.mjs';
|
|
5
6
|
import { runTerminalSession } from './terminal.mjs';
|
|
6
7
|
import { runScp } from './scp.mjs';
|
|
7
|
-
import { parseSessionArgs
|
|
8
|
+
import { parseSessionArgs } from './session-args.mjs';
|
|
9
|
+
import { runSessionCommand, sessionExitCode } from './sessions.mjs';
|
|
10
|
+
|
|
11
|
+
async function withCommandSignals(run) {
|
|
12
|
+
const controller = new AbortController();
|
|
13
|
+
const onSigint = () => controller.abort('SIGINT');
|
|
14
|
+
const onSigterm = () => controller.abort('SIGTERM');
|
|
15
|
+
process.once('SIGINT', onSigint);
|
|
16
|
+
process.once('SIGTERM', onSigterm);
|
|
17
|
+
try {
|
|
18
|
+
return await run(controller.signal);
|
|
19
|
+
} finally {
|
|
20
|
+
process.off('SIGINT', onSigint);
|
|
21
|
+
process.off('SIGTERM', onSigterm);
|
|
22
|
+
}
|
|
23
|
+
}
|
|
8
24
|
|
|
9
25
|
export async function main(
|
|
10
26
|
argv = process.argv.slice(2),
|
|
@@ -59,45 +75,54 @@ export async function main(
|
|
|
59
75
|
}
|
|
60
76
|
return 0;
|
|
61
77
|
}
|
|
62
|
-
if (args.command === '
|
|
63
|
-
const
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
78
|
+
if (args.command === 'agents') {
|
|
79
|
+
const execute = (signal) =>
|
|
80
|
+
runSessionCommand({
|
|
81
|
+
args,
|
|
82
|
+
client: marctlClient,
|
|
83
|
+
env,
|
|
84
|
+
stdin,
|
|
85
|
+
WebSocketClass,
|
|
86
|
+
translate,
|
|
87
|
+
signal
|
|
88
|
+
});
|
|
89
|
+
const result = args.action === 'wait' ? await withCommandSignals(execute) : await execute();
|
|
71
90
|
writeLine(stdout, JSON.stringify(result, null, args.json ? undefined : 2));
|
|
72
91
|
return 0;
|
|
73
92
|
}
|
|
74
93
|
if (args.command === 'scp') {
|
|
75
94
|
const socketClass = WebSocketClass ?? (await import('ws')).default;
|
|
76
|
-
return await
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
95
|
+
return await withCommandSignals((signal) =>
|
|
96
|
+
runScp({
|
|
97
|
+
source: args.source,
|
|
98
|
+
destination: args.destination,
|
|
99
|
+
preserve: args.preserve,
|
|
100
|
+
client: marctlClient,
|
|
101
|
+
WebSocketClass: socketClass,
|
|
102
|
+
resolveNodeId: (reference) => resolveNodeId(marctlClient, reference, translate),
|
|
103
|
+
translate,
|
|
104
|
+
signal
|
|
105
|
+
})
|
|
106
|
+
);
|
|
85
107
|
}
|
|
86
108
|
|
|
87
109
|
const nodeId = await resolveNodeId(marctlClient, args.nodeId, translate);
|
|
88
110
|
const socketClass = WebSocketClass ?? (await import('ws')).default;
|
|
89
|
-
return await
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
111
|
+
return await withCommandSignals((signal) =>
|
|
112
|
+
runTerminalSession({
|
|
113
|
+
client: marctlClient,
|
|
114
|
+
WebSocketClass: socketClass,
|
|
115
|
+
nodeId,
|
|
116
|
+
command: args.remoteCommand,
|
|
117
|
+
raceConnections: true,
|
|
118
|
+
stdout,
|
|
119
|
+
stderr,
|
|
120
|
+
translate,
|
|
121
|
+
signal
|
|
122
|
+
})
|
|
123
|
+
);
|
|
99
124
|
} catch (error) {
|
|
100
|
-
if (argv[0] === '
|
|
125
|
+
if (argv[0] === 'agents') {
|
|
101
126
|
const failure = {
|
|
102
127
|
code: error.code ?? 'MARCTL_COMMAND_FAILED',
|
|
103
128
|
message: error instanceof MarctlError ? error.message : translate('errors.commandFailed'),
|
|
@@ -151,7 +176,7 @@ export function executableArgs(executable, args) {
|
|
|
151
176
|
}
|
|
152
177
|
|
|
153
178
|
export function parseArgs(argv, translate = createTranslator()) {
|
|
154
|
-
if (argv[0] === '
|
|
179
|
+
if (argv[0] === 'agents') return parseSessionArgs(argv.slice(1), translate);
|
|
155
180
|
let command;
|
|
156
181
|
let nodeId;
|
|
157
182
|
const remoteArgs = [];
|
|
@@ -183,7 +208,7 @@ export function parseArgs(argv, translate = createTranslator()) {
|
|
|
183
208
|
argument !== 'nodes' &&
|
|
184
209
|
argument !== 'ssh' &&
|
|
185
210
|
argument !== 'scp' &&
|
|
186
|
-
argument !== '
|
|
211
|
+
argument !== 'agents' &&
|
|
187
212
|
argument !== 'version'
|
|
188
213
|
)
|
|
189
214
|
throw unsupportedSshOption(argument, translate);
|
package/src/client.mjs
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { MarctlError } from './
|
|
1
|
+
import { MarctlError } from './errors.mjs';
|
|
2
2
|
import { createTranslator } from './i18n/index.mjs';
|
|
3
3
|
|
|
4
4
|
const DEFAULT_REQUEST_TIMEOUT_MS = 15000;
|
|
@@ -49,11 +49,13 @@ export class MarctlClient {
|
|
|
49
49
|
});
|
|
50
50
|
}
|
|
51
51
|
|
|
52
|
-
async sessionCommand(body) {
|
|
52
|
+
async sessionCommand(body, { timeoutMs, signal } = {}) {
|
|
53
53
|
const result = await this.#requestJson('/api/marctl/sessions', {
|
|
54
54
|
method: 'POST',
|
|
55
55
|
body,
|
|
56
|
-
preserveError: true
|
|
56
|
+
preserveError: true,
|
|
57
|
+
...(timeoutMs === undefined ? {} : { timeoutMs }),
|
|
58
|
+
...(signal === undefined ? {} : { signal })
|
|
57
59
|
});
|
|
58
60
|
if (!isRecord(result))
|
|
59
61
|
throw new MarctlError(this.#translate('errors.serverInvalidJson'), {
|
|
@@ -171,19 +173,33 @@ export class MarctlClient {
|
|
|
171
173
|
return this.#request(pathname, { ...options, json: true });
|
|
172
174
|
}
|
|
173
175
|
|
|
174
|
-
async #request(
|
|
176
|
+
async #request(
|
|
177
|
+
pathname,
|
|
178
|
+
{
|
|
179
|
+
method = 'GET',
|
|
180
|
+
body,
|
|
181
|
+
json = false,
|
|
182
|
+
preserveError = false,
|
|
183
|
+
timeoutMs = this.#requestTimeoutMs,
|
|
184
|
+
signal
|
|
185
|
+
} = {}
|
|
186
|
+
) {
|
|
175
187
|
const headers = {
|
|
176
188
|
Accept: 'application/json',
|
|
177
189
|
Authorization: `Bearer ${this.#key}`
|
|
178
190
|
};
|
|
179
191
|
const controller = new AbortController();
|
|
180
|
-
const request = {
|
|
192
|
+
const request = {
|
|
193
|
+
method,
|
|
194
|
+
headers,
|
|
195
|
+
signal: signal ? AbortSignal.any([controller.signal, signal]) : controller.signal
|
|
196
|
+
};
|
|
181
197
|
if (body !== undefined) {
|
|
182
198
|
headers['Content-Type'] = 'application/json';
|
|
183
199
|
request.body = JSON.stringify(body);
|
|
184
200
|
}
|
|
185
201
|
|
|
186
|
-
const timer = setTimeout(() => controller.abort(),
|
|
202
|
+
const timer = setTimeout(() => controller.abort(), timeoutMs);
|
|
187
203
|
try {
|
|
188
204
|
const response = await this.#fetch(new URL(pathname, `${this.#serverUrl}/`), request);
|
|
189
205
|
if (!response?.ok) {
|
|
@@ -227,6 +243,10 @@ export class MarctlClient {
|
|
|
227
243
|
);
|
|
228
244
|
}
|
|
229
245
|
} catch (error) {
|
|
246
|
+
if (signal?.aborted)
|
|
247
|
+
throw new MarctlError(this.#translate('errors.sessionWaitCancelled'), {
|
|
248
|
+
code: 'MARCTL_WAIT_CANCELLED'
|
|
249
|
+
});
|
|
230
250
|
if (error instanceof MarctlError) throw error;
|
|
231
251
|
throw new MarctlError(
|
|
232
252
|
this.#translate(
|
package/src/config.mjs
CHANGED
|
@@ -1,16 +1,6 @@
|
|
|
1
1
|
import { createTranslator } from './i18n/index.mjs';
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
constructor(message, options = {}) {
|
|
5
|
-
super(message, options);
|
|
6
|
-
this.name = 'MarctlError';
|
|
7
|
-
this.code = options.code;
|
|
8
|
-
this.requestId = options.requestId;
|
|
9
|
-
this.upstreamRequestId = options.upstreamRequestId;
|
|
10
|
-
this.details = options.details;
|
|
11
|
-
this.statusCode = options.statusCode;
|
|
12
|
-
}
|
|
13
|
-
}
|
|
3
|
+
import { MarctlError } from './errors.mjs';
|
|
14
4
|
|
|
15
5
|
export async function loadConfig({ env = process.env, translate = createTranslator(env) } = {}) {
|
|
16
6
|
const serverUrl = nonEmptyString(env.MAR_CTL_SERVER_URL);
|
|
@@ -0,0 +1,225 @@
|
|
|
1
|
+
import { DirectFallbackError, directControlError } from './direct-errors.mjs';
|
|
2
|
+
|
|
3
|
+
export class LiveControl {
|
|
4
|
+
#socket;
|
|
5
|
+
#timeoutMs;
|
|
6
|
+
#ready = false;
|
|
7
|
+
#closed = false;
|
|
8
|
+
#waiters = new Set();
|
|
9
|
+
#listeners = new Set();
|
|
10
|
+
#onMessage;
|
|
11
|
+
#onError;
|
|
12
|
+
#onClose;
|
|
13
|
+
|
|
14
|
+
constructor(socket, timeoutMs) {
|
|
15
|
+
this.#socket = socket;
|
|
16
|
+
this.#timeoutMs = timeoutMs;
|
|
17
|
+
this.#onMessage = (raw, isBinary) => {
|
|
18
|
+
if (isBinary) return;
|
|
19
|
+
let message;
|
|
20
|
+
try {
|
|
21
|
+
message = JSON.parse(rawToString(raw));
|
|
22
|
+
} catch {
|
|
23
|
+
return;
|
|
24
|
+
}
|
|
25
|
+
if (!isRecord(message) || typeof message.type !== 'string') return;
|
|
26
|
+
for (const listener of this.#listeners) listener(message);
|
|
27
|
+
for (const waiter of [...this.#waiters]) {
|
|
28
|
+
if (
|
|
29
|
+
((waiter.request?.type === 'terminal.direct.prepare' &&
|
|
30
|
+
message.type === 'terminal.direct.error') ||
|
|
31
|
+
(waiter.request?.type === 'direct.prepare' && message.type === 'direct.error')) &&
|
|
32
|
+
message.requestId === waiter.request.requestId
|
|
33
|
+
) {
|
|
34
|
+
this.#waiters.delete(waiter);
|
|
35
|
+
clearTimeout(waiter.timer);
|
|
36
|
+
waiter.reject(directControlError(message.code));
|
|
37
|
+
continue;
|
|
38
|
+
}
|
|
39
|
+
if (!waiter.predicate(message, waiter.request)) continue;
|
|
40
|
+
this.#waiters.delete(waiter);
|
|
41
|
+
clearTimeout(waiter.timer);
|
|
42
|
+
waiter.resolve(message);
|
|
43
|
+
}
|
|
44
|
+
};
|
|
45
|
+
this.#onError = () => this.#fail(new DirectFallbackError('DIRECT_CONTROL_UNAVAILABLE'));
|
|
46
|
+
this.#onClose = () => this.#fail(new DirectFallbackError('DIRECT_CONTROL_CLOSED'));
|
|
47
|
+
socket.on?.('message', this.#onMessage);
|
|
48
|
+
socket.on?.('error', this.#onError);
|
|
49
|
+
socket.on?.('close', this.#onClose);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
async openAndResume() {
|
|
53
|
+
await this.#waitForSocket('open', 'DIRECT_CONTROL_TIMEOUT');
|
|
54
|
+
const ready = this.waitFor(
|
|
55
|
+
(message) =>
|
|
56
|
+
message.type === 'workbench.ready' && typeof message.workbenchConnectionId === 'string',
|
|
57
|
+
undefined,
|
|
58
|
+
'DIRECT_RESUME_TIMEOUT'
|
|
59
|
+
);
|
|
60
|
+
if (!this.send({ type: 'resume' })) {
|
|
61
|
+
ready.catch(() => undefined);
|
|
62
|
+
throw new DirectFallbackError('DIRECT_CONTROL_UNAVAILABLE');
|
|
63
|
+
}
|
|
64
|
+
await ready;
|
|
65
|
+
this.#ready = true;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
request(message, predicate, fallbackCode) {
|
|
69
|
+
if (!this.#ready) throw new DirectFallbackError('DIRECT_CONTROL_UNAVAILABLE');
|
|
70
|
+
const response = this.waitFor(predicate, message, fallbackCode);
|
|
71
|
+
if (!this.send(message)) {
|
|
72
|
+
response.catch(() => undefined);
|
|
73
|
+
throw new DirectFallbackError('DIRECT_CONTROL_UNAVAILABLE');
|
|
74
|
+
}
|
|
75
|
+
return response;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
waitFor(predicate, request, fallbackCode, timeoutMs = this.#timeoutMs) {
|
|
79
|
+
return new Promise((resolve, reject) => {
|
|
80
|
+
if (this.#closed) {
|
|
81
|
+
reject(new DirectFallbackError('DIRECT_CONTROL_CLOSED'));
|
|
82
|
+
return;
|
|
83
|
+
}
|
|
84
|
+
const waiter = {
|
|
85
|
+
predicate,
|
|
86
|
+
request,
|
|
87
|
+
resolve,
|
|
88
|
+
reject,
|
|
89
|
+
timer: setTimeout(() => {
|
|
90
|
+
this.#waiters.delete(waiter);
|
|
91
|
+
reject(new DirectFallbackError(fallbackCode ?? 'DIRECT_SETUP_TIMEOUT'));
|
|
92
|
+
}, timeoutMs)
|
|
93
|
+
};
|
|
94
|
+
this.#waiters.add(waiter);
|
|
95
|
+
});
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
onControl(listener) {
|
|
99
|
+
if (this.#closed) return () => {};
|
|
100
|
+
this.#listeners.add(listener);
|
|
101
|
+
return () => this.#listeners.delete(listener);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
send(message) {
|
|
105
|
+
if (this.#closed || (this.#socket.readyState !== undefined && this.#socket.readyState !== 1))
|
|
106
|
+
return false;
|
|
107
|
+
try {
|
|
108
|
+
this.#socket.send(JSON.stringify(message));
|
|
109
|
+
return true;
|
|
110
|
+
} catch {
|
|
111
|
+
return false;
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
close() {
|
|
116
|
+
if (this.#closed) return;
|
|
117
|
+
this.#closed = true;
|
|
118
|
+
this.#fail(new DirectFallbackError('DIRECT_CONTROL_CLOSED'));
|
|
119
|
+
this.#listeners.clear();
|
|
120
|
+
this.#socket.removeListener?.('message', this.#onMessage);
|
|
121
|
+
this.#socket.removeListener?.('error', this.#onError);
|
|
122
|
+
this.#socket.removeListener?.('close', this.#onClose);
|
|
123
|
+
try {
|
|
124
|
+
this.#socket.close();
|
|
125
|
+
} catch {
|
|
126
|
+
// The control socket is already closed.
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
#waitForSocket(event, code) {
|
|
131
|
+
if (this.#socket.readyState === 1) {
|
|
132
|
+
return Promise.resolve();
|
|
133
|
+
}
|
|
134
|
+
return new Promise((resolve, reject) => {
|
|
135
|
+
const onEvent = () => {
|
|
136
|
+
cleanup();
|
|
137
|
+
resolve();
|
|
138
|
+
};
|
|
139
|
+
const onError = () => {
|
|
140
|
+
cleanup();
|
|
141
|
+
reject(new DirectFallbackError(code));
|
|
142
|
+
};
|
|
143
|
+
const onClose = () => {
|
|
144
|
+
cleanup();
|
|
145
|
+
reject(new DirectFallbackError(code));
|
|
146
|
+
};
|
|
147
|
+
const timer = setTimeout(() => {
|
|
148
|
+
cleanup();
|
|
149
|
+
reject(new DirectFallbackError(code));
|
|
150
|
+
}, this.#timeoutMs);
|
|
151
|
+
const cleanup = () => {
|
|
152
|
+
clearTimeout(timer);
|
|
153
|
+
this.#socket.removeListener?.(event, onEvent);
|
|
154
|
+
this.#socket.removeListener?.('error', onError);
|
|
155
|
+
this.#socket.removeListener?.('close', onClose);
|
|
156
|
+
};
|
|
157
|
+
this.#socket.once?.(event, onEvent);
|
|
158
|
+
this.#socket.once?.('error', onError);
|
|
159
|
+
this.#socket.once?.('close', onClose);
|
|
160
|
+
});
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
#fail(error) {
|
|
164
|
+
if (this.#closed && this.#waiters.size === 0) return;
|
|
165
|
+
for (const waiter of [...this.#waiters]) {
|
|
166
|
+
this.#waiters.delete(waiter);
|
|
167
|
+
clearTimeout(waiter.timer);
|
|
168
|
+
waiter.reject(error);
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
export async function cancelSession(live, type, directSessionId) {
|
|
174
|
+
if (live === undefined) return;
|
|
175
|
+
const closed = live.waitFor(
|
|
176
|
+
(message) =>
|
|
177
|
+
message.type === `${type.replace('.cancel', '.closed')}` &&
|
|
178
|
+
message.directSessionId === directSessionId,
|
|
179
|
+
undefined,
|
|
180
|
+
'DIRECT_CANCEL_TIMEOUT',
|
|
181
|
+
1000
|
|
182
|
+
);
|
|
183
|
+
live.send({
|
|
184
|
+
type,
|
|
185
|
+
directSessionId,
|
|
186
|
+
...(type === 'direct.cancel' ? { reason: 'CLI_FALLBACK' } : {})
|
|
187
|
+
});
|
|
188
|
+
await closed.catch(() => undefined);
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
export function withTimeout(promise, timeoutMs, code, signal) {
|
|
192
|
+
let timer;
|
|
193
|
+
let onAbort;
|
|
194
|
+
return Promise.race([
|
|
195
|
+
promise,
|
|
196
|
+
new Promise((_, reject) => {
|
|
197
|
+
timer = setTimeout(() => reject(new DirectFallbackError(code)), timeoutMs);
|
|
198
|
+
}),
|
|
199
|
+
...(signal
|
|
200
|
+
? [
|
|
201
|
+
new Promise((_, reject) => {
|
|
202
|
+
onAbort = () => reject(new DirectFallbackError('DIRECT_RACE_CANCELLED'));
|
|
203
|
+
if (signal.aborted) onAbort();
|
|
204
|
+
else signal.addEventListener('abort', onAbort, { once: true });
|
|
205
|
+
})
|
|
206
|
+
]
|
|
207
|
+
: [])
|
|
208
|
+
]).finally(() => {
|
|
209
|
+
clearTimeout(timer);
|
|
210
|
+
if (onAbort) signal.removeEventListener('abort', onAbort);
|
|
211
|
+
});
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
export function rawToString(raw) {
|
|
215
|
+
if (typeof raw === 'string') return raw;
|
|
216
|
+
if (Buffer.isBuffer(raw)) return raw.toString('utf8');
|
|
217
|
+
if (raw instanceof ArrayBuffer) return Buffer.from(raw).toString('utf8');
|
|
218
|
+
if (ArrayBuffer.isView(raw))
|
|
219
|
+
return Buffer.from(raw.buffer, raw.byteOffset, raw.byteLength).toString('utf8');
|
|
220
|
+
return String(raw);
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
export function isRecord(value) {
|
|
224
|
+
return value !== null && typeof value === 'object' && !Array.isArray(value);
|
|
225
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
export class DirectFallbackError extends Error {
|
|
2
|
+
constructor(code = 'DIRECT_UNAVAILABLE') {
|
|
3
|
+
super(code);
|
|
4
|
+
this.name = 'DirectFallbackError';
|
|
5
|
+
this.code = code;
|
|
6
|
+
}
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export class DirectUnsafeError extends Error {
|
|
10
|
+
constructor(code = 'DIRECT_CANCEL_UNCONFIRMED') {
|
|
11
|
+
super(code);
|
|
12
|
+
this.name = 'DirectUnsafeError';
|
|
13
|
+
this.code = code;
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
const directRejections = new Set([
|
|
18
|
+
'DIRECT_TRANSFER_NOT_AUTHORIZED',
|
|
19
|
+
'DIRECT_SESSION_LIMIT_REACHED',
|
|
20
|
+
'DIRECT_NODE_GENERATION_MISMATCH',
|
|
21
|
+
'DIRECT_TRANSFER_SESSION_CONFLICT',
|
|
22
|
+
'TERMINAL_NOT_AUTHORIZED',
|
|
23
|
+
'TERMINAL_DIRECT_LEASE_EXPIRED',
|
|
24
|
+
'TERMINAL_NOT_FOUND',
|
|
25
|
+
'TERMINAL_CLOSED',
|
|
26
|
+
'TERMINAL_WORKSPACE_MISMATCH',
|
|
27
|
+
'TERMINAL_GIT_OWNER_REQUIRED',
|
|
28
|
+
'TERMINAL_ATTACH_CONFLICT',
|
|
29
|
+
'TERMINAL_RELAY_LIMIT_REACHED'
|
|
30
|
+
]);
|
|
31
|
+
|
|
32
|
+
export class DirectRejectedError extends Error {
|
|
33
|
+
constructor(code) {
|
|
34
|
+
super(code);
|
|
35
|
+
this.name = 'DirectRejectedError';
|
|
36
|
+
this.code = code;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export function directControlError(code) {
|
|
41
|
+
const safeCode =
|
|
42
|
+
typeof code === 'string' && /^[A-Z][A-Z0-9_]{0,127}$/u.test(code)
|
|
43
|
+
? code
|
|
44
|
+
: 'TERMINAL_DIRECT_UNAVAILABLE';
|
|
45
|
+
return directRejections.has(safeCode)
|
|
46
|
+
? new DirectRejectedError(safeCode)
|
|
47
|
+
: new DirectFallbackError(safeCode);
|
|
48
|
+
}
|
package/src/direct-process.mjs
CHANGED
|
@@ -1,7 +1,12 @@
|
|
|
1
1
|
import { fork } from 'node:child_process';
|
|
2
2
|
import { EventEmitter } from 'node:events';
|
|
3
3
|
import { fileURLToPath } from 'node:url';
|
|
4
|
-
import {
|
|
4
|
+
import {
|
|
5
|
+
DirectFallbackError,
|
|
6
|
+
DirectUnsafeError,
|
|
7
|
+
DirectRejectedError,
|
|
8
|
+
directControlError
|
|
9
|
+
} from './direct-errors.mjs';
|
|
5
10
|
|
|
6
11
|
const CLEANUP_TIMEOUT_MS = 1000;
|
|
7
12
|
const MAX_BUFFER_BYTES = 2 * 1024 * 1024;
|
|
@@ -113,11 +118,16 @@ export function openDirectTerminalProcess({
|
|
|
113
118
|
resolve(transport);
|
|
114
119
|
} else if (message.type === 'failure') {
|
|
115
120
|
const error = settled
|
|
116
|
-
?
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
+
? message.name === 'DirectRejectedError' ||
|
|
122
|
+
directControlError(message.code) instanceof DirectRejectedError
|
|
123
|
+
? directControlError(message.code)
|
|
124
|
+
: new Error(message.code ?? 'TERMINAL_CONNECTION_LOST')
|
|
125
|
+
: message.name === 'DirectRejectedError'
|
|
126
|
+
? directControlError(message.code)
|
|
127
|
+
: message.name === 'DirectUnsafeError' ||
|
|
128
|
+
(opening && message.name !== 'DirectFallbackError')
|
|
129
|
+
? new DirectUnsafeError(message.code)
|
|
130
|
+
: new DirectFallbackError(message.code);
|
|
121
131
|
fail(error);
|
|
122
132
|
} else if (message.type === 'frame') {
|
|
123
133
|
transport.frame(message.frame, message.isBinary);
|
|
@@ -216,11 +226,13 @@ class IsolatedDirectTransport extends EventEmitter {
|
|
|
216
226
|
if (this.#closed) return;
|
|
217
227
|
this.#emit(
|
|
218
228
|
'error',
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
229
|
+
error instanceof DirectRejectedError
|
|
230
|
+
? error
|
|
231
|
+
: new Error(
|
|
232
|
+
error instanceof DirectFallbackError || error instanceof DirectUnsafeError
|
|
233
|
+
? 'TERMINAL_CONNECTION_LOST'
|
|
234
|
+
: error.message
|
|
235
|
+
)
|
|
224
236
|
);
|
|
225
237
|
this.close();
|
|
226
238
|
}
|
|
@@ -298,6 +310,7 @@ function runChild() {
|
|
|
298
310
|
sendIpc(process, { type: 'send.result', id: message.id, code: 'TERMINAL_INPUT_FAILED' });
|
|
299
311
|
}
|
|
300
312
|
} else if (message.type === 'start') {
|
|
313
|
+
let rejectionSent = false;
|
|
301
314
|
try {
|
|
302
315
|
const { MarctlClient } = await import('./client.mjs');
|
|
303
316
|
const { openDirectTerminal } = await import('./direct.mjs');
|
|
@@ -312,6 +325,10 @@ function runChild() {
|
|
|
312
325
|
peerConnectionFactory: (name, config) => new native.PeerConnection(name, config),
|
|
313
326
|
cleanupNative: cleanup,
|
|
314
327
|
signal: controller.signal,
|
|
328
|
+
onRejected: (error) => {
|
|
329
|
+
rejectionSent = true;
|
|
330
|
+
sendIpc(process, { type: 'failure', name: 'DirectRejectedError', code: error.code });
|
|
331
|
+
},
|
|
315
332
|
onPrepared: () => callback('prepared'),
|
|
316
333
|
onIceConnected: () => {
|
|
317
334
|
iceSelection = callback('ice');
|
|
@@ -328,7 +345,7 @@ function runChild() {
|
|
|
328
345
|
sendIpc(process, { type: 'frame', frame: String(frame), isBinary }, abort)
|
|
329
346
|
);
|
|
330
347
|
transport.on('error', (error) => {
|
|
331
|
-
sendIpc(process, { type: 'failure', code: error.message });
|
|
348
|
+
sendIpc(process, { type: 'failure', code: error.code ?? error.message });
|
|
332
349
|
abort();
|
|
333
350
|
});
|
|
334
351
|
transport.on('close', () => {
|
|
@@ -337,11 +354,17 @@ function runChild() {
|
|
|
337
354
|
});
|
|
338
355
|
transport.activate();
|
|
339
356
|
} catch (error) {
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
357
|
+
if (!rejectionSent)
|
|
358
|
+
sendIpc(process, {
|
|
359
|
+
type: 'failure',
|
|
360
|
+
name:
|
|
361
|
+
error instanceof DirectRejectedError
|
|
362
|
+
? 'DirectRejectedError'
|
|
363
|
+
: error instanceof DirectUnsafeError
|
|
364
|
+
? 'DirectUnsafeError'
|
|
365
|
+
: 'DirectFallbackError',
|
|
366
|
+
code: error.code ?? 'DIRECT_UNAVAILABLE'
|
|
367
|
+
});
|
|
345
368
|
cleanup();
|
|
346
369
|
}
|
|
347
370
|
}
|