@myagentroam/node 0.13.10 → 0.13.12

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.
Files changed (42) hide show
  1. package/dist/connection/direct-transfer-service.js +60 -0
  2. package/dist/connection/endpoints.js +16 -0
  3. package/dist/connector.js +298 -10
  4. package/dist/ctags-binary.js +5 -1
  5. package/dist/managed-binaries.js +8 -0
  6. package/dist/marctl/package.json +1 -1
  7. package/dist/remote-desktop/desktop-byte-stream.js +128 -0
  8. package/dist/remote-desktop/desktop-error.js +35 -0
  9. package/dist/remote-desktop/desktop-service-core.js +384 -0
  10. package/dist/remote-desktop/direct-desktop-service.js +46 -0
  11. package/dist/remote-desktop/linux-managed-binaries.js +46 -0
  12. package/dist/remote-desktop/macos-managed-binaries.js +19 -0
  13. package/dist/remote-desktop/macos-remote-desktop-runtime.js +121 -0
  14. package/dist/remote-desktop/macvnc-managed-process.js +196 -0
  15. package/dist/remote-desktop/pooled-desktop-attachment.js +62 -0
  16. package/dist/remote-desktop/relay-desktop-service.js +40 -0
  17. package/dist/remote-desktop/remote-desktop-attachment.js +87 -0
  18. package/dist/remote-desktop/rfb-backend-handshake.js +98 -0
  19. package/dist/remote-desktop/rfb-client-gate.js +318 -0
  20. package/dist/remote-desktop/rfb-frontend-handshake.js +38 -0
  21. package/dist/remote-desktop/rfb-handshake-io.js +91 -0
  22. package/dist/remote-desktop/rfb-input-release.js +77 -0
  23. package/dist/remote-desktop/standard-rfb-attachment.js +40 -0
  24. package/dist/remote-desktop/tigervnc-attachment.js +68 -0
  25. package/dist/remote-desktop/tigervnc-backend-pool.js +88 -0
  26. package/dist/remote-desktop/tigervnc-cleanup-channel.js +129 -0
  27. package/dist/remote-desktop/tigervnc-managed-process.js +274 -0
  28. package/dist/remote-desktop/tigervnc-rfb-session.js +190 -0
  29. package/dist/remote-desktop/ultravnc-config.js +35 -0
  30. package/dist/remote-desktop/ultravnc-managed-process.js +180 -0
  31. package/dist/remote-desktop/vnc-password.js +15 -0
  32. package/dist/remote-desktop/windows-managed-binaries.js +18 -0
  33. package/dist/remote-desktop/windows-remote-desktop-runtime.js +126 -0
  34. package/dist/remote-desktop/x11-desktop-topology.js +82 -0
  35. package/dist/remote-desktop/x11-remote-desktop-runtime.js +99 -0
  36. package/dist/remote-desktop/x11-topology-source.js +205 -0
  37. package/dist/remote-desktop/x11-topology.js +88 -0
  38. package/dist/ripgrep-binary.js +5 -1
  39. package/dist/runner/runner-error.js +28 -7
  40. package/dist/service/node-control-channel.js +7 -1
  41. package/dist/service/node-control-message-service.js +11 -0
  42. package/package.json +6 -6
@@ -0,0 +1,318 @@
1
+ var _a;
2
+ import { RemoteDesktopError } from './desktop-error.js';
3
+ /**
4
+ * Frames and admits client-to-backend RFB messages for one attachment after
5
+ * both handshakes. It owns neither socket nor lease; the session owner fences,
6
+ * waits for stream drain, then makes a best-effort standard RFB release.
7
+ */
8
+ export class RfbClientGate {
9
+ target;
10
+ mode;
11
+ isControlOwner;
12
+ onFailure;
13
+ static maxClipboardBytes = 1024 * 1024;
14
+ static maxEncodings = 256;
15
+ static maxPressedKeys = 256;
16
+ static maxQueuedFrames = 1024;
17
+ static maxFrameBytes = this.maxClipboardBytes + 8;
18
+ #buffer = Buffer.allocUnsafe(_a.maxFrameBytes);
19
+ #queue = [];
20
+ #keys = new Set();
21
+ #begin = 0;
22
+ #end = 0;
23
+ #queuedBytes = 0;
24
+ #buttons = 0;
25
+ #pointerX = 0;
26
+ #pointerY = 0;
27
+ #waitingDrain = false;
28
+ #inFlight = 0;
29
+ #inFlightBytes = 0;
30
+ #extendedClipboard = false;
31
+ #state = 'active';
32
+ #transportFailed = false;
33
+ #fencePromise;
34
+ #fenceResolve;
35
+ #fenceReject;
36
+ constructor(target, mode, isControlOwner, onFailure) {
37
+ this.target = target;
38
+ this.mode = mode;
39
+ this.isControlOwner = isControlOwner;
40
+ this.onFailure = onFailure;
41
+ target.on('drain', this.#drain);
42
+ target.on('error', this.#streamError);
43
+ target.on('close', this.#streamError);
44
+ }
45
+ receive(data) {
46
+ if (this.#state !== 'active')
47
+ return;
48
+ let offset = 0;
49
+ while (offset < data.byteLength && this.#state === 'active') {
50
+ if (this.#end === this.#buffer.length && this.#begin > 0) {
51
+ this.#buffer.copyWithin(0, this.#begin, this.#end);
52
+ this.#end -= this.#begin;
53
+ this.#begin = 0;
54
+ }
55
+ const count = Math.min(data.byteLength - offset, this.#buffer.length - this.#end);
56
+ if (count === 0) {
57
+ this.#fail('DESKTOP_RFB_INVALID');
58
+ return;
59
+ }
60
+ this.#buffer.set(data.subarray(offset, offset + count), this.#end);
61
+ this.#end += count;
62
+ offset += count;
63
+ this.#parse();
64
+ }
65
+ }
66
+ /** Fencing discards unwritten messages; write completion is not an OS input ACK. */
67
+ fence() {
68
+ if (this.#fencePromise !== undefined)
69
+ return this.#fencePromise;
70
+ if (this.#transportFailed)
71
+ return Promise.reject(new RemoteDesktopError('DESKTOP_RFB_CLOSED'));
72
+ if (this.#state === 'active')
73
+ this.#state = 'fenced';
74
+ this.#queue.length = 0;
75
+ this.#queuedBytes = 0;
76
+ this.#begin = this.#end = 0;
77
+ this.#fencePromise =
78
+ (this.#state === 'fenced' && this.#waitingDrain) || this.#inFlight > 0
79
+ ? new Promise((resolve, reject) => {
80
+ this.#fenceResolve = resolve;
81
+ this.#fenceReject = reject;
82
+ })
83
+ : Promise.resolve(this.#snapshot());
84
+ return this.#fencePromise;
85
+ }
86
+ /** Available after failure for cleanup; it does not confirm delivery or release. */
87
+ submittedInputSnapshot() {
88
+ return this.#snapshot();
89
+ }
90
+ dispose() {
91
+ this.#state = 'failed';
92
+ this.#transportFailed = true;
93
+ this.#queue.length = 0;
94
+ this.#queuedBytes = 0;
95
+ this.target.removeListener('drain', this.#drain);
96
+ this.target.removeListener('error', this.#streamError);
97
+ this.target.removeListener('close', this.#streamError);
98
+ this.#fenceReject?.(new RemoteDesktopError('DESKTOP_RFB_CLOSED'));
99
+ this.#fenceResolve = this.#fenceReject = undefined;
100
+ }
101
+ #parse() {
102
+ while (this.#begin < this.#end && this.#state === 'active') {
103
+ const length = this.#frameLength();
104
+ if (length === undefined || this.#state !== 'active')
105
+ return;
106
+ if (this.#end - this.#begin < length)
107
+ return;
108
+ let bytes = Buffer.from(this.#buffer.subarray(this.#begin, this.#begin + length));
109
+ this.#begin += length;
110
+ if (this.#begin === this.#end)
111
+ this.#begin = this.#end = 0;
112
+ const input = bytes[0] === 4 || bytes[0] === 5 || bytes[0] === 6;
113
+ if (input && this.mode === 'VIEW') {
114
+ this.#fail('DESKTOP_INPUT_DENIED');
115
+ return;
116
+ }
117
+ if (bytes[0] === 4 && bytes[1] > 1) {
118
+ this.#fail('DESKTOP_RFB_INVALID');
119
+ return;
120
+ }
121
+ if (bytes[0] === 2) {
122
+ bytes = this.#filterEncodings(bytes);
123
+ this.#extendedClipboard = this.mode === 'CONTROL' && this.#hasExtendedClipboard(bytes);
124
+ }
125
+ if (bytes[0] === 6 && !this.#validClipboard(bytes)) {
126
+ this.#fail('DESKTOP_RFB_INVALID');
127
+ return;
128
+ }
129
+ if (this.#queue.length >= _a.maxQueuedFrames ||
130
+ this.#queuedBytes + bytes.length > _a.maxFrameBytes) {
131
+ this.#fail('DESKTOP_BACKPRESSURE');
132
+ return;
133
+ }
134
+ this.#queue.push({ bytes, input });
135
+ this.#queuedBytes += bytes.length;
136
+ this.#pump();
137
+ }
138
+ }
139
+ #frameLength() {
140
+ const available = this.#end - this.#begin;
141
+ const type = this.#buffer[this.#begin];
142
+ switch (type) {
143
+ case 0:
144
+ return 20;
145
+ case 2: {
146
+ if (available < 4)
147
+ return undefined;
148
+ const count = this.#buffer.readUInt16BE(this.#begin + 2);
149
+ if (count > _a.maxEncodings) {
150
+ this.#fail('DESKTOP_RFB_INVALID');
151
+ return undefined;
152
+ }
153
+ return 4 + count * 4;
154
+ }
155
+ case 3:
156
+ return 10;
157
+ case 4:
158
+ return 8;
159
+ case 5:
160
+ return 6;
161
+ case 6: {
162
+ if (available < 8)
163
+ return undefined;
164
+ const declared = this.#buffer.readInt32BE(this.#begin + 4);
165
+ const length = Math.abs(declared);
166
+ if (length > _a.maxClipboardBytes) {
167
+ this.#fail('DESKTOP_RFB_INVALID');
168
+ return undefined;
169
+ }
170
+ return 8 + length;
171
+ }
172
+ default:
173
+ this.#fail('DESKTOP_RFB_INVALID');
174
+ return undefined;
175
+ }
176
+ }
177
+ #filterEncodings(frame) {
178
+ const allowed = [];
179
+ for (let offset = 4; offset < frame.length; offset += 4) {
180
+ const encoding = frame.readInt32BE(offset);
181
+ if ([0, 1, 2, 5, 7, 16, 21, -223, -224, -239, -308].includes(encoding) ||
182
+ (encoding >= -32 && encoding <= -23) ||
183
+ (encoding >= -256 && encoding <= -247) ||
184
+ (this.mode === 'CONTROL' && encoding === -1063131698))
185
+ allowed.push(encoding);
186
+ }
187
+ const filtered = Buffer.allocUnsafe(4 + allowed.length * 4);
188
+ filtered[0] = 2;
189
+ filtered[1] = 0;
190
+ filtered.writeUInt16BE(allowed.length, 2);
191
+ allowed.forEach((encoding, index) => filtered.writeInt32BE(encoding, 4 + index * 4));
192
+ return filtered;
193
+ }
194
+ #hasExtendedClipboard(frame) {
195
+ for (let offset = 4; offset < frame.length; offset += 4) {
196
+ if (frame.readInt32BE(offset) === -1063131698)
197
+ return true;
198
+ }
199
+ return false;
200
+ }
201
+ #validClipboard(frame) {
202
+ if (frame.readInt32BE(4) >= 0)
203
+ return true;
204
+ if (!this.#extendedClipboard || frame.length < 12)
205
+ return false;
206
+ const flags = frame.readUInt32BE(8);
207
+ const formats = flags & 0xffff;
208
+ const actions = flags & 0xff000000;
209
+ if ((flags & 0x00ff0000) !== 0 || (formats & ~1) !== 0)
210
+ return false;
211
+ if ((actions & 0x01000000) !== 0) {
212
+ return (actions & ~0x1b000000) === 0 && frame.length === 12 + (formats === 1 ? 4 : 0);
213
+ }
214
+ if (actions === 0x08000000)
215
+ return frame.length === 12;
216
+ return actions === 0x10000000 && formats === 1 && frame.length > 12;
217
+ }
218
+ #pump() {
219
+ while (this.#state === 'active' && !this.#waitingDrain && this.#queue.length > 0) {
220
+ const next = this.#queue[0];
221
+ if (this.#inFlight >= _a.maxQueuedFrames ||
222
+ this.#inFlightBytes + next.bytes.length > _a.maxFrameBytes)
223
+ return;
224
+ const frame = this.#queue.shift();
225
+ this.#queuedBytes -= frame.bytes.length;
226
+ if (frame.input) {
227
+ let ownsControl = false;
228
+ try {
229
+ ownsControl = this.isControlOwner();
230
+ }
231
+ catch {
232
+ // An unavailable authority is not evidence of ownership.
233
+ }
234
+ if (!ownsControl) {
235
+ this.#fail('DESKTOP_CONTROL_LOST');
236
+ return;
237
+ }
238
+ }
239
+ if (frame.bytes[0] === 4 &&
240
+ frame.bytes[1] === 1 &&
241
+ !this.#keys.has(frame.bytes.readUInt32BE(4)) &&
242
+ this.#keys.size >= _a.maxPressedKeys) {
243
+ this.#fail('DESKTOP_RFB_INVALID');
244
+ return;
245
+ }
246
+ try {
247
+ this.#inFlight++;
248
+ this.#inFlightBytes += frame.bytes.length;
249
+ const accepted = this.target.write(frame.bytes, (error) => {
250
+ this.#inFlight--;
251
+ this.#inFlightBytes -= frame.bytes.length;
252
+ if (error)
253
+ this.#fail('DESKTOP_TRANSPORT_ERROR');
254
+ else if (this.#state !== 'active')
255
+ this.#finishFence();
256
+ else
257
+ this.#pump();
258
+ });
259
+ this.#recordInput(frame.bytes);
260
+ this.#waitingDrain = !accepted;
261
+ }
262
+ catch {
263
+ this.#inFlight--;
264
+ this.#inFlightBytes -= frame.bytes.length;
265
+ this.#fail('DESKTOP_TRANSPORT_ERROR');
266
+ }
267
+ }
268
+ }
269
+ #recordInput(frame) {
270
+ if (frame[0] === 4) {
271
+ const key = frame.readUInt32BE(4);
272
+ if (frame[1] !== 0)
273
+ this.#keys.add(key);
274
+ else
275
+ this.#keys.delete(key);
276
+ }
277
+ else if (frame[0] === 5) {
278
+ this.#buttons = frame[1];
279
+ this.#pointerX = frame.readUInt16BE(2);
280
+ this.#pointerY = frame.readUInt16BE(4);
281
+ }
282
+ }
283
+ #snapshot() {
284
+ return { keys: [...this.#keys], buttons: this.#buttons, x: this.#pointerX, y: this.#pointerY };
285
+ }
286
+ #drain = () => {
287
+ this.#waitingDrain = false;
288
+ if (this.#state !== 'active')
289
+ this.#finishFence();
290
+ else
291
+ this.#pump();
292
+ };
293
+ #finishFence() {
294
+ if (this.#state !== 'active' &&
295
+ !this.#transportFailed &&
296
+ (this.#state === 'failed' || !this.#waitingDrain) &&
297
+ this.#inFlight === 0) {
298
+ this.#fenceResolve?.(this.#snapshot());
299
+ this.#fenceResolve = this.#fenceReject = undefined;
300
+ }
301
+ }
302
+ #streamError = () => this.#fail('DESKTOP_TRANSPORT_ERROR');
303
+ #fail(code) {
304
+ if (code === 'DESKTOP_TRANSPORT_ERROR') {
305
+ this.#transportFailed = true;
306
+ this.#fenceReject?.(new RemoteDesktopError(code));
307
+ this.#fenceResolve = this.#fenceReject = undefined;
308
+ }
309
+ if (this.#state === 'failed')
310
+ return;
311
+ this.#state = 'failed';
312
+ this.#queue.length = 0;
313
+ this.#queuedBytes = 0;
314
+ this.#finishFence();
315
+ this.onFailure(code);
316
+ }
317
+ }
318
+ _a = RfbClientGate;
@@ -0,0 +1,38 @@
1
+ import { RemoteDesktopError } from './desktop-error.js';
2
+ import { RfbHandshakeIo } from './rfb-handshake-io.js';
3
+ /**
4
+ * Negotiates RFB 3.8 on an already authorized attachment. None is only the
5
+ * inner RFB security type: this function must never admit an unauthenticated
6
+ * transport. On success the caller regains the paused stream, including any
7
+ * buffered client messages, and must pass them through the input gate.
8
+ */
9
+ export async function handshakeRfbFrontend(stream, serverInit, signal) {
10
+ const io = new RfbHandshakeIo(stream, signal);
11
+ try {
12
+ if (serverInit.length < 24 ||
13
+ serverInit.length > 24 + 4096 ||
14
+ serverInit.readUInt32BE(20) !== serverInit.length - 24)
15
+ throw new RemoteDesktopError('DESKTOP_RFB_INVALID');
16
+ const banner = Buffer.from('RFB 003.008\n');
17
+ await io.write(banner);
18
+ if (!(await io.read(12)).equals(banner))
19
+ throw new RemoteDesktopError('DESKTOP_RFB_INVALID');
20
+ await io.write(Buffer.from([1, 1]));
21
+ if ((await io.read(1))[0] !== 1)
22
+ throw new RemoteDesktopError('DESKTOP_RFB_INVALID');
23
+ await io.write(Buffer.alloc(4));
24
+ // Never let an exclusive browser connection disconnect other observers.
25
+ if ((await io.read(1))[0] !== 1)
26
+ throw new RemoteDesktopError('DESKTOP_RFB_INVALID');
27
+ await io.write(serverInit);
28
+ }
29
+ catch (error) {
30
+ stream.destroy();
31
+ if (error instanceof RemoteDesktopError)
32
+ throw error;
33
+ throw new RemoteDesktopError('DESKTOP_TRANSPORT_ERROR');
34
+ }
35
+ finally {
36
+ io.dispose();
37
+ }
38
+ }
@@ -0,0 +1,91 @@
1
+ import { RemoteDesktopError } from './desktop-error.js';
2
+ /** Temporarily owns paused stream reads and bounded setup writes; it does not own the stream. */
3
+ export class RfbHandshakeIo {
4
+ stream;
5
+ signal;
6
+ #error;
7
+ #pending;
8
+ constructor(stream, signal) {
9
+ this.stream = stream;
10
+ this.signal = signal;
11
+ stream.pause();
12
+ stream.on('error', this.#failed);
13
+ stream.on('close', this.#closed);
14
+ stream.on('end', this.#closed);
15
+ signal.addEventListener('abort', this.#aborted);
16
+ if (signal.aborted)
17
+ this.#aborted();
18
+ }
19
+ async read(size) {
20
+ if (size === 0)
21
+ return Buffer.alloc(0);
22
+ while (true) {
23
+ this.#check();
24
+ const value = this.stream.read(size);
25
+ if (value !== null) {
26
+ if (!Buffer.isBuffer(value) || value.length !== size)
27
+ throw new RemoteDesktopError('DESKTOP_RFB_INVALID');
28
+ return value;
29
+ }
30
+ await new Promise((resolve, reject) => {
31
+ const readable = () => finish();
32
+ const finish = (error) => {
33
+ this.stream.off('readable', readable);
34
+ this.#pending = undefined;
35
+ if (error)
36
+ reject(error);
37
+ else
38
+ resolve();
39
+ };
40
+ this.#pending = finish;
41
+ this.stream.once('readable', readable);
42
+ });
43
+ }
44
+ }
45
+ async write(value) {
46
+ this.#check();
47
+ await new Promise((resolve, reject) => {
48
+ const finish = (error) => {
49
+ if (this.#pending !== finish)
50
+ return;
51
+ this.#pending = undefined;
52
+ if (error)
53
+ reject(error);
54
+ else
55
+ resolve();
56
+ };
57
+ this.#pending = finish;
58
+ try {
59
+ this.stream.write(value, (error) => finish(error ? new RemoteDesktopError('DESKTOP_TRANSPORT_ERROR') : undefined));
60
+ }
61
+ catch {
62
+ finish(new RemoteDesktopError('DESKTOP_TRANSPORT_ERROR'));
63
+ }
64
+ });
65
+ }
66
+ dispose() {
67
+ if (this.stream.destroyed && !this.stream.closed) {
68
+ this.stream.once('close', () => this.stream.off('error', this.#failed));
69
+ }
70
+ else
71
+ this.stream.off('error', this.#failed);
72
+ this.stream.off('close', this.#closed);
73
+ this.stream.off('end', this.#closed);
74
+ this.signal.removeEventListener('abort', this.#aborted);
75
+ }
76
+ #check() {
77
+ if (this.#error)
78
+ throw this.#error;
79
+ if (this.stream.destroyed || this.stream.readableEnded)
80
+ throw new RemoteDesktopError('DESKTOP_TRANSPORT_ERROR');
81
+ }
82
+ #fail(error) {
83
+ if (this.#error)
84
+ return;
85
+ this.#error = error;
86
+ this.#pending?.(error);
87
+ }
88
+ #failed = () => this.#fail(new RemoteDesktopError('DESKTOP_TRANSPORT_ERROR'));
89
+ #closed = () => this.#fail(new RemoteDesktopError('DESKTOP_TRANSPORT_ERROR'));
90
+ #aborted = () => this.#fail(new RemoteDesktopError('DESKTOP_SETUP_CANCELLED'));
91
+ }
@@ -0,0 +1,77 @@
1
+ import { RemoteDesktopError } from './desktop-error.js';
2
+ /** Sends this attachment's held inputs up before closing an unmodified RFB backend.
3
+ * A successful write does not confirm that the OS or application processed it.
4
+ */
5
+ export async function submitRfbInputRelease(gate, target, signal) {
6
+ const snapshot = await abortable(gate.fence(), signal);
7
+ const bytes = releaseFrames(snapshot);
8
+ if (!bytes.length)
9
+ return;
10
+ if (signal.aborted || target.destroyed || !target.writable)
11
+ throw new RemoteDesktopError('DESKTOP_NATIVE_CLEANUP_FAILED');
12
+ await new Promise((resolve, reject) => {
13
+ const onError = () => finish(new RemoteDesktopError('DESKTOP_NATIVE_CLEANUP_FAILED'));
14
+ const onClose = () => finish(new RemoteDesktopError('DESKTOP_NATIVE_CLEANUP_FAILED'));
15
+ const onAbort = () => finish(new RemoteDesktopError('DESKTOP_NATIVE_CLEANUP_TIMEOUT'));
16
+ let finished = false;
17
+ const finish = (error) => {
18
+ if (finished)
19
+ return;
20
+ finished = true;
21
+ target.off('error', onError);
22
+ target.off('close', onClose);
23
+ signal.removeEventListener('abort', onAbort);
24
+ if (error)
25
+ reject(error);
26
+ else
27
+ resolve();
28
+ };
29
+ target.once('error', onError);
30
+ target.once('close', onClose);
31
+ signal.addEventListener('abort', onAbort, { once: true });
32
+ try {
33
+ target.write(bytes, (error) => finish(error ? new RemoteDesktopError('DESKTOP_NATIVE_CLEANUP_FAILED') : undefined));
34
+ }
35
+ catch {
36
+ finish(new RemoteDesktopError('DESKTOP_NATIVE_CLEANUP_FAILED'));
37
+ }
38
+ });
39
+ }
40
+ function releaseFrames(snapshot) {
41
+ // Release ordinary keys before modifiers so the backend interprets their
42
+ // key-up events under the same modifiers as their key-down events.
43
+ const modifier = (key) => key === 0xff7e || (key >= 0xffe1 && key <= 0xffee);
44
+ const keys = [
45
+ ...snapshot.keys.filter((key) => !modifier(key)),
46
+ ...snapshot.keys.filter(modifier)
47
+ ];
48
+ const frames = keys.map((key) => {
49
+ const frame = Buffer.alloc(8);
50
+ frame[0] = 4;
51
+ frame.writeUInt32BE(key, 4);
52
+ return frame;
53
+ });
54
+ if (snapshot.buttons !== 0) {
55
+ const frame = Buffer.alloc(6);
56
+ frame[0] = 5;
57
+ frame.writeUInt16BE(snapshot.x, 2);
58
+ frame.writeUInt16BE(snapshot.y, 4);
59
+ frames.push(frame);
60
+ }
61
+ return Buffer.concat(frames);
62
+ }
63
+ async function abortable(operation, signal) {
64
+ if (signal.aborted) {
65
+ void operation.catch(() => { });
66
+ throw new RemoteDesktopError('DESKTOP_NATIVE_CLEANUP_TIMEOUT');
67
+ }
68
+ return await new Promise((resolve, reject) => {
69
+ const abort = () => finish(() => reject(new RemoteDesktopError('DESKTOP_NATIVE_CLEANUP_TIMEOUT')));
70
+ const finish = (done) => {
71
+ signal.removeEventListener('abort', abort);
72
+ done();
73
+ };
74
+ signal.addEventListener('abort', abort, { once: true });
75
+ operation.then((value) => finish(() => resolve(value)), (error) => finish(() => reject(error)));
76
+ });
77
+ }
@@ -0,0 +1,40 @@
1
+ import { submitRfbInputRelease } from './rfb-input-release.js';
2
+ /** Owns a standard RFB connection; submitted releases are not OS execution receipts. */
3
+ export class StandardRfbAttachment {
4
+ socket;
5
+ gate;
6
+ #closing;
7
+ #closed = false;
8
+ constructor(socket, gate) {
9
+ this.socket = socket;
10
+ this.gate = gate;
11
+ socket.on('error', this.#ignoreLateError);
12
+ socket.once('close', () => socket.off('error', this.#ignoreLateError));
13
+ }
14
+ #ignoreLateError = () => { };
15
+ close(signal) {
16
+ if (this.#closed)
17
+ return Promise.resolve();
18
+ if (this.#closing)
19
+ return this.#closing;
20
+ this.#closing = this.#close(signal).finally(() => {
21
+ this.#closing = undefined;
22
+ });
23
+ return this.#closing;
24
+ }
25
+ async #close(signal) {
26
+ try {
27
+ // A lost backend cannot accept releases; still reclaim this attachment.
28
+ await submitRfbInputRelease(this.gate, this.socket, signal).catch(() => { });
29
+ }
30
+ finally {
31
+ const closed = this.socket.closed
32
+ ? Promise.resolve()
33
+ : new Promise((resolve) => this.socket.once('close', resolve));
34
+ this.socket.destroy();
35
+ this.gate.dispose();
36
+ await closed;
37
+ this.#closed = true;
38
+ }
39
+ }
40
+ }
@@ -0,0 +1,68 @@
1
+ import { RemoteDesktopError } from './desktop-error.js';
2
+ /** Owns one registered RFB stream and its native cleanup ticket, not the control lease. */
3
+ export class TigerVncAttachment {
4
+ socket;
5
+ gate;
6
+ ticket;
7
+ #phase = 'active';
8
+ #closing = false;
9
+ constructor(socket, gate, ticket) {
10
+ this.socket = socket;
11
+ this.gate = gate;
12
+ this.ticket = ticket;
13
+ // A pending socket error can arrive after the gate has been disposed.
14
+ socket.on('error', this.#ignoreLateError);
15
+ socket.once('close', () => socket.off('error', this.#ignoreLateError));
16
+ }
17
+ #ignoreLateError = () => { };
18
+ async close(signal) {
19
+ if (this.#phase === 'closed')
20
+ return;
21
+ if (this.#closing)
22
+ throw new RemoteDesktopError('DESKTOP_NATIVE_WAIT_IN_PROGRESS');
23
+ this.#closing = true;
24
+ try {
25
+ if (this.#phase === 'active') {
26
+ try {
27
+ // An already-failed stream is still closed and must receive a native
28
+ // cleanup receipt. A write callback alone never proves OS cleanup.
29
+ await this.#waitForFence(signal);
30
+ }
31
+ finally {
32
+ this.socket.destroy();
33
+ this.gate.dispose();
34
+ this.#phase = 'waiting';
35
+ }
36
+ }
37
+ await this.ticket.wait(signal);
38
+ this.#phase = 'closed';
39
+ }
40
+ finally {
41
+ this.#closing = false;
42
+ }
43
+ }
44
+ #waitForFence(signal) {
45
+ const fence = this.gate.fence();
46
+ return new Promise((resolve, reject) => {
47
+ let settled = false;
48
+ const finish = (timedOut) => {
49
+ if (settled)
50
+ return;
51
+ settled = true;
52
+ signal.removeEventListener('abort', abort);
53
+ if (timedOut)
54
+ reject(new RemoteDesktopError('DESKTOP_NATIVE_CLEANUP_TIMEOUT'));
55
+ else
56
+ resolve();
57
+ };
58
+ const abort = () => finish(true);
59
+ // A failed fence is followed by socket destruction and native cleanup;
60
+ // the native receipt, not the failed write, decides whether it succeeded.
61
+ fence.then(() => finish(false), () => finish(false));
62
+ if (signal.aborted)
63
+ abort();
64
+ else
65
+ signal.addEventListener('abort', abort, { once: true });
66
+ });
67
+ }
68
+ }