@myagentroam/node 0.13.11 → 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 (41) 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/service/node-control-channel.js +7 -1
  40. package/dist/service/node-control-message-service.js +11 -0
  41. package/package.json +6 -6
@@ -0,0 +1,35 @@
1
+ import { obfuscateVncPassword } from './vnc-password.js';
2
+ /** Writes the Win32 profile representation of UltraVNC's eight-byte password. */
3
+ export function ultraVncProfilePassword(password) {
4
+ const encoded = obfuscateVncPassword(password);
5
+ try {
6
+ const checksum = encoded.reduce((sum, byte) => (sum + byte) & 255, 0);
7
+ return Buffer.concat([encoded, Buffer.from([checksum])]).toString('hex');
8
+ }
9
+ finally {
10
+ encoded.fill(0);
11
+ }
12
+ }
13
+ /** Configures an authenticated local-only RFB server without file transfer. */
14
+ export function ultraVncConfig(port, password) {
15
+ if (!Number.isInteger(port) || port < 1 || port > 65535)
16
+ throw new Error('DESKTOP_BACKEND_CONFIG_INVALID');
17
+ return [
18
+ '[admin]',
19
+ 'LoopbackOnly=1',
20
+ 'AllowLoopback=1',
21
+ 'AuthRequired=1',
22
+ 'SocketConnect=1',
23
+ 'HTTPConnect=0',
24
+ 'UseIpv6=0',
25
+ 'AutoPortSelect=0',
26
+ `PortNumber=${port}`,
27
+ 'InputsEnabled=1',
28
+ 'QuerySetting=0',
29
+ 'FileTransferEnabled=0',
30
+ 'secondary=1',
31
+ '[UltraVNC]',
32
+ `passwd=${ultraVncProfilePassword(password)}`,
33
+ ''
34
+ ].join('\r\n');
35
+ }
@@ -0,0 +1,180 @@
1
+ import { spawn } from 'node:child_process';
2
+ import { randomBytes } from 'node:crypto';
3
+ import { mkdtemp, rm, writeFile } from 'node:fs/promises';
4
+ import { connect, createServer } from 'node:net';
5
+ import { tmpdir } from 'node:os';
6
+ import { dirname, isAbsolute, join } from 'node:path';
7
+ import { setTimeout as delay } from 'node:timers/promises';
8
+ import { RemoteDesktopError } from './desktop-error.js';
9
+ import { handshakeRfbBackend } from './rfb-backend-handshake.js';
10
+ import { ultraVncConfig } from './ultravnc-config.js';
11
+ /** Owns one UltraVNC process and its authenticated loopback connections. */
12
+ export class UltraVncManagedProcess {
13
+ child;
14
+ directory;
15
+ port;
16
+ secret;
17
+ onUnexpectedExit;
18
+ #connections = new Set();
19
+ #exited;
20
+ #stopping = false;
21
+ #ready = false;
22
+ #didExit = false;
23
+ #stopPromise;
24
+ constructor(child, directory, port, secret, onUnexpectedExit) {
25
+ this.child = child;
26
+ this.directory = directory;
27
+ this.port = port;
28
+ this.secret = secret;
29
+ this.onUnexpectedExit = onUnexpectedExit;
30
+ child.on('error', () => { });
31
+ child.stderr?.resume();
32
+ this.#exited = new Promise((resolve) => {
33
+ child.once('close', () => {
34
+ this.#didExit = true;
35
+ if (this.#ready && !this.#stopping) {
36
+ try {
37
+ this.onUnexpectedExit();
38
+ }
39
+ catch {
40
+ // A notification failure must not prevent process cleanup.
41
+ }
42
+ }
43
+ resolve();
44
+ });
45
+ });
46
+ }
47
+ static async start(options, signal) {
48
+ if (process.platform !== 'win32' || !isAbsolute(options.binaryPath))
49
+ throw new RemoteDesktopError('DESKTOP_BACKEND_CONFIG_INVALID');
50
+ const deadline = AbortSignal.any([signal, AbortSignal.timeout(10_000)]);
51
+ if (deadline.aborted)
52
+ throw new RemoteDesktopError('DESKTOP_SETUP_CANCELLED');
53
+ const directory = await mkdtemp(join(tmpdir(), 'mar-ultravnc-'));
54
+ const alphabet = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_';
55
+ const secret = randomBytes(8);
56
+ for (let i = 0; i < secret.length; i++)
57
+ secret[i] = alphabet.charCodeAt(secret[i] & 63);
58
+ let backend;
59
+ try {
60
+ const port = await availableLoopbackPort();
61
+ if (deadline.aborted)
62
+ throw new RemoteDesktopError('DESKTOP_SETUP_CANCELLED');
63
+ const config = join(directory, 'ultravnc.ini');
64
+ await writeFile(config, ultraVncConfig(port, secret), { flag: 'wx', mode: 0o600 });
65
+ if (deadline.aborted)
66
+ throw new RemoteDesktopError('DESKTOP_SETUP_CANCELLED');
67
+ const child = spawn(options.binaryPath, ['-run', '-multi', '-config', config], {
68
+ cwd: dirname(options.binaryPath),
69
+ windowsHide: true,
70
+ stdio: ['ignore', 'ignore', 'pipe']
71
+ });
72
+ backend = new UltraVncManagedProcess(child, directory, port, secret, options.onUnexpectedExit);
73
+ while (!deadline.aborted && !backend.#didExit) {
74
+ try {
75
+ const connection = await backend.connect(64 * 1024 * 1024, deadline);
76
+ connection.socket.destroy();
77
+ if (backend.#didExit || deadline.aborted)
78
+ throw new RemoteDesktopError('DESKTOP_BACKEND_CLOSED');
79
+ backend.#ready = true;
80
+ return backend;
81
+ }
82
+ catch (error) {
83
+ if (deadline.aborted || backend.#didExit)
84
+ throw error;
85
+ await delay(50, undefined, { signal: deadline });
86
+ }
87
+ }
88
+ throw new RemoteDesktopError('DESKTOP_BACKEND_START_FAILED');
89
+ }
90
+ catch (error) {
91
+ if (backend)
92
+ await backend.stop();
93
+ else {
94
+ secret.fill(0);
95
+ await rm(directory, { recursive: true, force: true });
96
+ }
97
+ if (deadline.aborted)
98
+ throw new RemoteDesktopError('DESKTOP_SETUP_CANCELLED');
99
+ if (error instanceof RemoteDesktopError)
100
+ throw error;
101
+ throw new RemoteDesktopError('DESKTOP_BACKEND_START_FAILED');
102
+ }
103
+ }
104
+ async connect(maxPixels, signal) {
105
+ if (this.#stopping || this.#didExit)
106
+ throw new RemoteDesktopError('DESKTOP_BACKEND_CLOSED');
107
+ const socket = connect({ host: '127.0.0.1', port: this.port });
108
+ this.#connections.add(socket);
109
+ socket.once('close', () => this.#connections.delete(socket));
110
+ try {
111
+ await new Promise((resolve, reject) => {
112
+ const cleanup = () => {
113
+ socket.off('connect', connected);
114
+ socket.off('error', failed);
115
+ signal.removeEventListener('abort', aborted);
116
+ };
117
+ const connected = () => {
118
+ cleanup();
119
+ resolve();
120
+ };
121
+ const failed = () => {
122
+ cleanup();
123
+ reject(new RemoteDesktopError('DESKTOP_BACKEND_CLOSED'));
124
+ };
125
+ const aborted = () => {
126
+ cleanup();
127
+ reject(new RemoteDesktopError('DESKTOP_SETUP_CANCELLED'));
128
+ };
129
+ socket.once('connect', connected);
130
+ socket.once('error', failed);
131
+ if (signal.aborted)
132
+ aborted();
133
+ else
134
+ signal.addEventListener('abort', aborted, { once: true });
135
+ });
136
+ const init = await handshakeRfbBackend(socket, this.secret, maxPixels, signal);
137
+ return { socket, init };
138
+ }
139
+ catch (error) {
140
+ socket.on('error', () => { });
141
+ socket.destroy();
142
+ throw error;
143
+ }
144
+ }
145
+ stop() {
146
+ if (this.#stopPromise)
147
+ return this.#stopPromise;
148
+ this.#stopping = true;
149
+ this.#stopPromise = (async () => {
150
+ for (const socket of this.#connections)
151
+ socket.destroy();
152
+ if (!this.#didExit)
153
+ this.child.kill();
154
+ const exited = await Promise.race([
155
+ this.#exited.then(() => true),
156
+ delay(5_000, undefined, { ref: false }).then(() => false)
157
+ ]);
158
+ if (!exited)
159
+ throw new RemoteDesktopError('DESKTOP_BACKEND_CLOSED');
160
+ this.secret.fill(0);
161
+ await rm(this.directory, { recursive: true, force: true });
162
+ })().catch((error) => {
163
+ this.#stopPromise = undefined;
164
+ throw error;
165
+ });
166
+ return this.#stopPromise;
167
+ }
168
+ }
169
+ async function availableLoopbackPort() {
170
+ const listener = createServer();
171
+ return new Promise((resolve, reject) => {
172
+ listener.once('error', reject);
173
+ listener.listen(0, '127.0.0.1', () => {
174
+ const address = listener.address();
175
+ if (!address || typeof address === 'string')
176
+ return listener.close(() => reject(new Error('port')));
177
+ listener.close((error) => (error ? reject(error) : resolve(address.port)));
178
+ });
179
+ });
180
+ }
@@ -0,0 +1,15 @@
1
+ import { createCipheriv } from 'node:crypto';
2
+ /** Standard VNC password-file obfuscation; the result is not a password hash. */
3
+ export function obfuscateVncPassword(password) {
4
+ if (password.length !== 8 || password.includes(0))
5
+ throw new Error('DESKTOP_BACKEND_CONFIG_INVALID');
6
+ const key = Buffer.from('e84ad660c4721ae0'.repeat(3), 'hex');
7
+ try {
8
+ const cipher = createCipheriv('des-ede3', key, null);
9
+ cipher.setAutoPadding(false);
10
+ return Buffer.concat([cipher.update(password), cipher.final()]);
11
+ }
12
+ finally {
13
+ key.fill(0);
14
+ }
15
+ }
@@ -0,0 +1,18 @@
1
+ import { access } from 'node:fs/promises';
2
+ import { constants } from 'node:fs';
3
+ import { join } from 'node:path';
4
+ import { managedBinariesPlatformDirectory } from '../managed-binaries.js';
5
+ /** Discover only the packaged Windows backend, never a system-wide VNC service. */
6
+ export async function windowsManagedDesktopBinaries(packageDirectory, platform = process.platform, architecture = process.arch) {
7
+ if (platform !== 'win32' || architecture !== 'x64')
8
+ return undefined;
9
+ try {
10
+ const directory = join(packageDirectory ?? managedBinariesPlatformDirectory('win32', 'x64'), 'vendor/ultravnc/win32-x64');
11
+ for (const name of ['winvnc.exe', 'vnchooks.dll', 'ddengine64.dll', 'ultravnc.portable'])
12
+ await access(join(directory, name), constants.R_OK);
13
+ return { binaryPath: join(directory, 'winvnc.exe') };
14
+ }
15
+ catch {
16
+ return undefined;
17
+ }
18
+ }
@@ -0,0 +1,126 @@
1
+ import { execFile } from 'node:child_process';
2
+ import { randomUUID } from 'node:crypto';
3
+ import { promisify } from 'node:util';
4
+ import { remoteDesktopDescriptionSchema } from '@myagentroam/protocol';
5
+ import { RemoteDesktopError } from './desktop-error.js';
6
+ import { PooledDesktopAttachment } from './pooled-desktop-attachment.js';
7
+ import { RemoteDesktopAttachment } from './remote-desktop-attachment.js';
8
+ import { TigerVncBackendPool } from './tigervnc-backend-pool.js';
9
+ import { UltraVncManagedProcess } from './ultravnc-managed-process.js';
10
+ const run = promisify(execFile);
11
+ const SCREEN_QUERY = `
12
+ $ErrorActionPreference = "Stop"
13
+ if (-not [Environment]::UserInteractive) { exit 2 }
14
+ Add-Type -AssemblyName System.Windows.Forms
15
+ [Console]::OutputEncoding = [System.Text.UTF8Encoding]::new()
16
+ $screens = @([System.Windows.Forms.Screen]::AllScreens | ForEach-Object {
17
+ @{ name=$_.DeviceName; primary=$_.Primary; x=$_.Bounds.X; y=$_.Bounds.Y;
18
+ width=$_.Bounds.Width; height=$_.Bounds.Height }
19
+ })
20
+ ConvertTo-Json -InputObject $screens -Compress
21
+ `;
22
+ /** Maps Windows virtual-screen coordinates into the full VNC framebuffer. */
23
+ export function windowsDesktopLayout(screens, generation, revision) {
24
+ if (!screens.length)
25
+ throw new RemoteDesktopError('DESKTOP_TOPOLOGY_UNAVAILABLE');
26
+ const minX = Math.min(...screens.map((screen) => screen.x));
27
+ const minY = Math.min(...screens.map((screen) => screen.y));
28
+ const maxX = Math.max(...screens.map((screen) => screen.x + screen.width));
29
+ const maxY = Math.max(...screens.map((screen) => screen.y + screen.height));
30
+ const result = remoteDesktopDescriptionSchema.safeParse({
31
+ desktopGeneration: generation,
32
+ topologyRevision: revision,
33
+ framebuffer: { width: maxX - minX, height: maxY - minY },
34
+ displays: screens.map((screen) => ({
35
+ displayId: Buffer.from(screen.name, 'utf8').toString('base64url').padEnd(8, '_'),
36
+ name: screen.name,
37
+ primary: screen.primary,
38
+ nativeBounds: { x: screen.x, y: screen.y, width: screen.width, height: screen.height },
39
+ framebufferBounds: {
40
+ x: screen.x - minX,
41
+ y: screen.y - minY,
42
+ width: screen.width,
43
+ height: screen.height
44
+ },
45
+ nativeToFramebuffer: [1, 0, 0, 1, minX === 0 ? 0 : -minX, minY === 0 ? 0 : -minY]
46
+ }))
47
+ });
48
+ if (!result.success)
49
+ throw new RemoteDesktopError('DESKTOP_TOPOLOGY_UNAVAILABLE');
50
+ return result.data;
51
+ }
52
+ async function queryScreens(signal) {
53
+ const { stdout } = await run('powershell.exe', ['-NoProfile', '-NonInteractive', '-Command', SCREEN_QUERY], {
54
+ windowsHide: true,
55
+ encoding: 'utf8',
56
+ maxBuffer: 64 * 1024,
57
+ signal
58
+ });
59
+ return JSON.parse(stdout);
60
+ }
61
+ /** Owns the current interactive Windows GUI layout and one shared UltraVNC backend. */
62
+ export class WindowsRemoteDesktopRuntime {
63
+ options;
64
+ #pool;
65
+ #generation = randomUUID();
66
+ #layout;
67
+ #signature = '';
68
+ #revision = 0;
69
+ #disposed = false;
70
+ constructor(options) {
71
+ this.options = options;
72
+ this.#pool = new TigerVncBackendPool((signal, onUnexpectedExit) => UltraVncManagedProcess.start({ binaryPath: options.binaryPath, onUnexpectedExit }, signal), () => options.fence('DESKTOP_BACKEND_CLOSED'));
73
+ }
74
+ static async start(options, signal) {
75
+ const runtime = new WindowsRemoteDesktopRuntime(options);
76
+ await runtime.refresh(signal);
77
+ return runtime;
78
+ }
79
+ async refresh(signal) {
80
+ const screens = (await queryScreens(signal)).sort((left, right) => left.name.localeCompare(right.name));
81
+ const signature = JSON.stringify(screens.map((screen) => [
82
+ screen.name, screen.primary, screen.x, screen.y, screen.width, screen.height
83
+ ]));
84
+ if (this.#disposed)
85
+ throw new RemoteDesktopError('DESKTOP_BACKEND_CLOSED');
86
+ if (signature === this.#signature)
87
+ return;
88
+ const layout = windowsDesktopLayout(screens, this.#generation, this.#revision + 1);
89
+ if (layout.framebuffer.width * layout.framebuffer.height > this.options.maxPixels)
90
+ throw new RemoteDesktopError('DESKTOP_TOPOLOGY_UNAVAILABLE');
91
+ if (this.#layout)
92
+ this.options.fence('DESKTOP_TOPOLOGY_UNAVAILABLE');
93
+ this.#signature = signature;
94
+ this.#revision++;
95
+ this.#layout = layout;
96
+ }
97
+ describe() {
98
+ if (this.#disposed || !this.#layout || !this.#pool.isAvailable())
99
+ throw new RemoteDesktopError('DESKTOP_BACKEND_CLOSED');
100
+ return structuredClone(this.#layout);
101
+ }
102
+ isCurrent = (command) => !this.#disposed &&
103
+ this.#pool.isAvailable() &&
104
+ !!this.#layout &&
105
+ this.#layout.desktopGeneration === command.desktopGeneration &&
106
+ this.#layout.topologyRevision === command.topologyRevision;
107
+ createAttachment = (command, frontend, callbacks) => new PooledDesktopAttachment(this.#pool, frontend, (backend) => new RemoteDesktopAttachment({
108
+ attachmentId: command.attachmentId,
109
+ frontend,
110
+ backend,
111
+ mode: command.mode,
112
+ maxPixels: this.options.maxPixels,
113
+ isCurrentTopology: (width, height) => this.isCurrent(command) &&
114
+ this.#layout?.framebuffer.width === width &&
115
+ this.#layout.framebuffer.height === height,
116
+ onAttached: callbacks.attached,
117
+ onFailure: callbacks.failed
118
+ }));
119
+ async dispose() {
120
+ if (!this.#disposed) {
121
+ this.#disposed = true;
122
+ this.options.fence('DESKTOP_SESSION_UNAVAILABLE');
123
+ }
124
+ await this.#pool.dispose();
125
+ }
126
+ }
@@ -0,0 +1,82 @@
1
+ import { RemoteDesktopError } from './desktop-error.js';
2
+ import { X11TopologySource } from './x11-topology-source.js';
3
+ import { X11Topology } from './x11-topology.js';
4
+ /** Owns one XRandR subscription and synchronously fences users before accepting a new layout. */
5
+ export class X11DesktopTopology {
6
+ fence;
7
+ #topology;
8
+ #source;
9
+ #stopped = false;
10
+ #unavailable = false;
11
+ #stopPromise;
12
+ constructor(generation, fence) {
13
+ this.fence = fence;
14
+ this.#topology = new X11Topology(generation);
15
+ }
16
+ static async start(options, signal, startSource = X11TopologySource.start) {
17
+ const owner = new X11DesktopTopology(options.generation, options.fence);
18
+ owner.#source = await startSource({
19
+ binaryPath: options.binaryPath,
20
+ display: options.display,
21
+ xAuthority: options.xAuthority,
22
+ onSnapshot: (snapshot) => {
23
+ if (!owner.#stopped && !owner.#unavailable)
24
+ owner.#topology.accept(snapshot);
25
+ },
26
+ onInvalidated: (event) => {
27
+ if (owner.#stopped || owner.#unavailable)
28
+ return;
29
+ owner.#topology.invalidate(event);
30
+ owner.fence();
31
+ },
32
+ onUnavailable: () => {
33
+ if (owner.#stopped || owner.#unavailable)
34
+ return;
35
+ owner.#unavailable = true;
36
+ owner.#topology.unavailable();
37
+ owner.fence();
38
+ }
39
+ }, signal);
40
+ // A source must not become usable merely because its start promise resolved.
41
+ try {
42
+ owner.describe();
43
+ }
44
+ catch (error) {
45
+ await owner.stop();
46
+ throw error;
47
+ }
48
+ return owner;
49
+ }
50
+ describe() {
51
+ if (this.#stopped || this.#unavailable)
52
+ throw new RemoteDesktopError('DESKTOP_TOPOLOGY_UNAVAILABLE');
53
+ return this.#topology.describe();
54
+ }
55
+ matches(generation, revision, width, height) {
56
+ try {
57
+ const layout = this.describe();
58
+ return (layout.desktopGeneration === generation &&
59
+ layout.topologyRevision === revision &&
60
+ layout.framebuffer.width === width &&
61
+ layout.framebuffer.height === height);
62
+ }
63
+ catch {
64
+ return false;
65
+ }
66
+ }
67
+ stop() {
68
+ if (this.#stopPromise)
69
+ return this.#stopPromise;
70
+ this.#stopped = true;
71
+ this.#topology.unavailable();
72
+ this.#stopPromise = (async () => {
73
+ try {
74
+ this.fence();
75
+ }
76
+ finally {
77
+ await this.#source?.stop();
78
+ }
79
+ })();
80
+ return this.#stopPromise;
81
+ }
82
+ }
@@ -0,0 +1,99 @@
1
+ import { randomUUID } from 'node:crypto';
2
+ import { RemoteDesktopError } from './desktop-error.js';
3
+ import { PooledDesktopAttachment } from './pooled-desktop-attachment.js';
4
+ import { RemoteDesktopAttachment } from './remote-desktop-attachment.js';
5
+ import { TigerVncBackendPool } from './tigervnc-backend-pool.js';
6
+ import { TigerVncManagedProcess } from './tigervnc-managed-process.js';
7
+ import { X11DesktopTopology } from './x11-desktop-topology.js';
8
+ /** Composes one X11 desktop and its Remote-only backend. */
9
+ export class X11RemoteDesktopRuntime {
10
+ options;
11
+ #pool;
12
+ #topology;
13
+ #disposed = false;
14
+ constructor(options) {
15
+ this.options = options;
16
+ this.#pool = new TigerVncBackendPool((signal, onUnexpectedExit) => TigerVncManagedProcess.start({
17
+ binaryPath: options.binaryPath,
18
+ display: options.display,
19
+ xAuthority: options.xAuthority,
20
+ onUnexpectedExit
21
+ }, signal), () => options.fence('DESKTOP_BACKEND_CLOSED'));
22
+ }
23
+ static async start(options, signal) {
24
+ if (!Number.isSafeInteger(options.maxPixels) || options.maxPixels < 1)
25
+ throw new RemoteDesktopError('DESKTOP_BACKEND_CONFIG_INVALID');
26
+ const runtime = new X11RemoteDesktopRuntime(options);
27
+ try {
28
+ runtime.#topology = await X11DesktopTopology.start({
29
+ generation: randomUUID(),
30
+ binaryPath: options.topologyBinary,
31
+ display: options.display,
32
+ xAuthority: options.xAuthority,
33
+ fence: () => options.fence('DESKTOP_TOPOLOGY_UNAVAILABLE')
34
+ }, signal);
35
+ return runtime;
36
+ }
37
+ catch (error) {
38
+ await runtime.dispose();
39
+ throw error;
40
+ }
41
+ }
42
+ describe() {
43
+ if (this.#disposed || !this.#topology)
44
+ throw new RemoteDesktopError('DESKTOP_BACKEND_CLOSED');
45
+ return this.#topology.describe();
46
+ }
47
+ isCurrent = (command) => {
48
+ try {
49
+ const layout = this.describe();
50
+ return (this.#pool.isAvailable() &&
51
+ layout.desktopGeneration === command.desktopGeneration &&
52
+ layout.topologyRevision === command.topologyRevision);
53
+ }
54
+ catch {
55
+ return false;
56
+ }
57
+ };
58
+ createAttachment = (command, frontend, callbacks) => new PooledDesktopAttachment(this.#pool, frontend, (backend) => new RemoteDesktopAttachment({
59
+ attachmentId: command.attachmentId,
60
+ frontend,
61
+ backend,
62
+ mode: command.mode,
63
+ maxPixels: this.options.maxPixels,
64
+ isCurrentTopology: (width, height) => this.#matches(command, width, height),
65
+ onAttached: callbacks.attached,
66
+ onFailure: callbacks.failed
67
+ }));
68
+ async dispose() {
69
+ let failure;
70
+ if (!this.#disposed) {
71
+ this.#disposed = true;
72
+ try {
73
+ this.options.fence('DESKTOP_SESSION_UNAVAILABLE');
74
+ }
75
+ catch (error) {
76
+ failure ??= error;
77
+ }
78
+ }
79
+ try {
80
+ await this.#topology?.stop();
81
+ }
82
+ catch (error) {
83
+ failure ??= error;
84
+ }
85
+ try {
86
+ await this.#pool.dispose();
87
+ }
88
+ catch (error) {
89
+ failure ??= error;
90
+ }
91
+ if (failure !== undefined)
92
+ throw failure;
93
+ }
94
+ #matches(command, width, height) {
95
+ if (!this.isCurrent(command) || !this.#topology)
96
+ return false;
97
+ return this.#topology.matches(command.desktopGeneration, this.#topology.describe().topologyRevision, width, height);
98
+ }
99
+ }