ai-remote 0.4.0 → 0.4.2
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/{src/index.ts → dist/index.d.ts} +0 -1
- package/dist/index.js +0 -1
- package/dist/protocols/index.d.ts +31 -0
- package/dist/protocols/index.js +0 -1
- package/dist/protocols/rdp/buffer.d.ts +38 -0
- package/dist/protocols/rdp/buffer.js +0 -1
- package/dist/protocols/rdp/caps.d.ts +49 -0
- package/dist/protocols/rdp/caps.js +0 -1
- package/dist/protocols/rdp/cert.d.ts +18 -0
- package/dist/protocols/rdp/cert.js +0 -1
- package/dist/protocols/rdp/client.d.ts +56 -0
- package/dist/protocols/rdp/client.js +0 -1
- package/dist/protocols/rdp/cliprdr.d.ts +27 -0
- package/dist/protocols/rdp/cliprdr.js +0 -1
- package/dist/protocols/rdp/credssp.d.ts +34 -0
- package/dist/protocols/rdp/credssp.js +0 -1
- package/dist/protocols/rdp/crypto.d.ts +63 -0
- package/dist/protocols/rdp/crypto.js +0 -1
- package/dist/protocols/rdp/display.d.ts +38 -0
- package/dist/protocols/rdp/display.js +0 -1
- package/dist/protocols/rdp/gcc.d.ts +23 -0
- package/dist/protocols/rdp/gcc.js +0 -1
- package/dist/protocols/rdp/keymap.d.ts +9 -0
- package/dist/protocols/rdp/keymap.js +0 -1
- package/dist/protocols/rdp/mcs.d.ts +40 -0
- package/dist/protocols/rdp/mcs.js +0 -1
- package/dist/protocols/rdp/ntlm.d.ts +61 -0
- package/dist/protocols/rdp/ntlm.js +0 -1
- package/dist/protocols/rdp/pdu.d.ts +155 -0
- package/dist/protocols/rdp/pdu.js +0 -1
- package/dist/protocols/rdp/rail.d.ts +119 -0
- package/dist/protocols/rdp/rail.js +0 -1
- package/dist/protocols/rdp/rle.d.ts +13 -0
- package/dist/protocols/rdp/rle.js +0 -1
- package/dist/protocols/rdp/sec.d.ts +59 -0
- package/dist/protocols/rdp/sec.js +0 -1
- package/dist/protocols/rdp/session.d.ts +62 -0
- package/dist/protocols/rdp/session.js +0 -1
- package/dist/protocols/rdp/tls.d.ts +18 -0
- package/dist/protocols/rdp/tls.js +0 -1
- package/dist/protocols/rdp/vchannel.d.ts +28 -0
- package/dist/protocols/rdp/vchannel.js +0 -1
- package/dist/protocols/rdp/x224.d.ts +40 -0
- package/dist/protocols/rdp/x224.js +0 -1
- package/dist/protocols/ssh/kex.d.ts +97 -0
- package/dist/protocols/ssh/kex.js +0 -1
- package/dist/protocols/ssh/messages.d.ts +50 -0
- package/dist/protocols/ssh/messages.js +0 -1
- package/dist/protocols/ssh/session.d.ts +66 -0
- package/dist/protocols/ssh/session.js +0 -1
- package/dist/protocols/ssh/terminal.d.ts +46 -0
- package/dist/protocols/ssh/terminal.js +0 -1
- package/dist/protocols/ssh/transport.d.ts +62 -0
- package/dist/protocols/ssh/transport.js +0 -1
- package/dist/protocols/ssh/wire.d.ts +52 -0
- package/dist/protocols/ssh/wire.js +0 -1
- package/dist/protocols/types.d.ts +127 -0
- package/dist/protocols/types.js +0 -1
- package/dist/protocols/vnc/input.d.ts +5 -0
- package/dist/protocols/vnc/input.js +0 -1
- package/dist/protocols/vnc/session.d.ts +12 -0
- package/dist/protocols/vnc/session.js +0 -1
- package/dist/shared/connection.d.ts +81 -0
- package/dist/shared/connection.js +0 -1
- package/dist/shared/device.d.ts +26 -0
- package/dist/shared/device.js +0 -1
- package/dist/shared/hosts.d.ts +63 -0
- package/dist/shared/hosts.js +0 -1
- package/dist/shared/icons.d.ts +46 -0
- package/dist/shared/icons.js +0 -1
- package/dist/shared/protocol.d.ts +28 -0
- package/dist/shared/protocol.js +0 -1
- package/dist/shared/signals.d.ts +37 -0
- package/dist/shared/signals.js +0 -1
- package/package.json +10 -11
- package/dist/index.js.map +0 -7
- package/dist/protocols/index.js.map +0 -7
- package/dist/protocols/rdp/buffer.js.map +0 -7
- package/dist/protocols/rdp/caps.js.map +0 -7
- package/dist/protocols/rdp/cert.js.map +0 -7
- package/dist/protocols/rdp/client.js.map +0 -7
- package/dist/protocols/rdp/cliprdr.js.map +0 -7
- package/dist/protocols/rdp/credssp.js.map +0 -7
- package/dist/protocols/rdp/crypto.js.map +0 -7
- package/dist/protocols/rdp/display.js.map +0 -7
- package/dist/protocols/rdp/gcc.js.map +0 -7
- package/dist/protocols/rdp/keymap.js.map +0 -7
- package/dist/protocols/rdp/mcs.js.map +0 -7
- package/dist/protocols/rdp/ntlm.js.map +0 -7
- package/dist/protocols/rdp/pdu.js.map +0 -7
- package/dist/protocols/rdp/rail.js.map +0 -7
- package/dist/protocols/rdp/rle.js.map +0 -7
- package/dist/protocols/rdp/sec.js.map +0 -7
- package/dist/protocols/rdp/session.js.map +0 -7
- package/dist/protocols/rdp/tls.js.map +0 -7
- package/dist/protocols/rdp/vchannel.js.map +0 -7
- package/dist/protocols/rdp/x224.js.map +0 -7
- package/dist/protocols/ssh/kex.js.map +0 -7
- package/dist/protocols/ssh/messages.js.map +0 -7
- package/dist/protocols/ssh/session.js.map +0 -7
- package/dist/protocols/ssh/terminal.js.map +0 -7
- package/dist/protocols/ssh/transport.js.map +0 -7
- package/dist/protocols/ssh/wire.js.map +0 -7
- package/dist/protocols/types.js.map +0 -7
- package/dist/protocols/vnc/input.js.map +0 -7
- package/dist/protocols/vnc/session.js.map +0 -7
- package/dist/shared/connection.js.map +0 -7
- package/dist/shared/device.js.map +0 -7
- package/dist/shared/hosts.js.map +0 -7
- package/dist/shared/icons.js.map +0 -7
- package/dist/shared/protocol.js.map +0 -7
- package/dist/shared/signals.js.map +0 -7
- package/src/cli/cli.ts +0 -462
- package/src/cli/daemon.ts +0 -306
- package/src/cli/framebuffer.ts +0 -115
- package/src/cli/generated/viewer-bundle.ts +0 -5
- package/src/cli/ipc.ts +0 -78
- package/src/cli/native/window.swift +0 -151
- package/src/cli/paths.ts +0 -28
- package/src/cli/png.ts +0 -106
- package/src/cli/session.ts +0 -263
- package/src/cli/shell.ts +0 -184
- package/src/cli/transport.ts +0 -86
- package/src/cli/viewer-client/assets.d.ts +0 -4
- package/src/cli/viewer-client/main.ts +0 -323
- package/src/cli/viewer-page.ts +0 -138
- package/src/cli/viewer.ts +0 -296
- package/src/cli/window.ts +0 -140
- package/src/cli/wsserver.ts +0 -144
- package/src/protocols/index.ts +0 -72
- package/src/protocols/rdp/buffer.ts +0 -204
- package/src/protocols/rdp/caps.ts +0 -304
- package/src/protocols/rdp/cert.ts +0 -190
- package/src/protocols/rdp/client.ts +0 -1371
- package/src/protocols/rdp/cliprdr.ts +0 -344
- package/src/protocols/rdp/credssp.ts +0 -319
- package/src/protocols/rdp/crypto.ts +0 -416
- package/src/protocols/rdp/display.ts +0 -750
- package/src/protocols/rdp/gcc.ts +0 -162
- package/src/protocols/rdp/keymap.ts +0 -68
- package/src/protocols/rdp/mcs.ts +0 -283
- package/src/protocols/rdp/ntlm.ts +0 -380
- package/src/protocols/rdp/pdu.ts +0 -497
- package/src/protocols/rdp/rail.ts +0 -480
- package/src/protocols/rdp/rle.ts +0 -343
- package/src/protocols/rdp/sec.ts +0 -184
- package/src/protocols/rdp/session.ts +0 -393
- package/src/protocols/rdp/tls.ts +0 -452
- package/src/protocols/rdp/vchannel.ts +0 -89
- package/src/protocols/rdp/x224.ts +0 -139
- package/src/protocols/ssh/kex.ts +0 -227
- package/src/protocols/ssh/messages.ts +0 -54
- package/src/protocols/ssh/session.ts +0 -879
- package/src/protocols/ssh/terminal.ts +0 -242
- package/src/protocols/ssh/transport.ts +0 -745
- package/src/protocols/ssh/wire.ts +0 -220
- package/src/protocols/types.ts +0 -146
- package/src/protocols/vnc/input.ts +0 -13
- package/src/protocols/vnc/novnc.d.ts +0 -75
- package/src/protocols/vnc/session.ts +0 -367
- package/src/shared/connection.ts +0 -206
- package/src/shared/device.ts +0 -28
- package/src/shared/hosts.ts +0 -240
- package/src/shared/icons.ts +0 -49
- package/src/shared/protocol.ts +0 -55
- package/src/shared/signals.ts +0 -67
package/src/cli/daemon.ts
DELETED
|
@@ -1,306 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* A session that outlives the command that started it.
|
|
3
|
-
*
|
|
4
|
-
* An RDP handshake costs a second or two and everything after it is stateful:
|
|
5
|
-
* a click, then a screenshot of what the click did, are two halves of one
|
|
6
|
-
* observation. A tool that reconnects between them can never see the result of
|
|
7
|
-
* its own action, so the session lives here and commands attach to it.
|
|
8
|
-
*
|
|
9
|
-
* The viewer attaches the same way, and so does the terminal. None of them own
|
|
10
|
-
* the session; it outlives all of them and ends when it is told to.
|
|
11
|
-
*/
|
|
12
|
-
|
|
13
|
-
import net from 'node:net';
|
|
14
|
-
import { unlinkSync, writeFileSync, existsSync } from 'node:fs';
|
|
15
|
-
import { RdpSession, type SessionOptions } from './session';
|
|
16
|
-
import { bareUsername, Shell } from './shell';
|
|
17
|
-
import { downscale, encodePng } from './png';
|
|
18
|
-
import { startViewer, type Viewer } from './viewer';
|
|
19
|
-
import { readMessages, writeMessage, type Request } from './ipc';
|
|
20
|
-
import { ensureRunDir, metaPath, socketPath } from './paths';
|
|
21
|
-
|
|
22
|
-
export interface DaemonOptions extends SessionOptions {
|
|
23
|
-
name: string;
|
|
24
|
-
view: boolean;
|
|
25
|
-
launchBrowser: boolean;
|
|
26
|
-
viewPort: number;
|
|
27
|
-
/** Account for the terminal, when one is wanted. Blank leaves SSH unopened. */
|
|
28
|
-
sshUsername: string;
|
|
29
|
-
/** Open in the default browser rather than a window of its own. */
|
|
30
|
-
tab: boolean;
|
|
31
|
-
/** Start the window already filling the display. */
|
|
32
|
-
fullscreen: boolean;
|
|
33
|
-
sshPort: number;
|
|
34
|
-
idleMs: number;
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
export async function runDaemon(options: DaemonOptions): Promise<void> {
|
|
38
|
-
ensureRunDir();
|
|
39
|
-
|
|
40
|
-
const session = new RdpSession(options);
|
|
41
|
-
const size = await session.connect();
|
|
42
|
-
|
|
43
|
-
let shell: Shell | null = null;
|
|
44
|
-
let shellStarting: Promise<Shell> | null = null;
|
|
45
|
-
|
|
46
|
-
/**
|
|
47
|
-
* The terminal is opened on first use rather than at connect time. Most
|
|
48
|
-
* sessions never ask for one, and a second sign-in that nobody wanted is a
|
|
49
|
-
* second chance to lock an account out.
|
|
50
|
-
*
|
|
51
|
-
* It signs in as the desktop's own account and password by default. Needing a
|
|
52
|
-
* separate flag to get a terminal on the machine you are already looking at
|
|
53
|
-
* is a question with an obvious answer, so it is not asked.
|
|
54
|
-
*/
|
|
55
|
-
function openShell(): Promise<Shell> {
|
|
56
|
-
if (shell) return Promise.resolve(shell);
|
|
57
|
-
if (shellStarting) return shellStarting;
|
|
58
|
-
|
|
59
|
-
shellStarting = (async () => {
|
|
60
|
-
const started = new Shell({
|
|
61
|
-
host: options.host,
|
|
62
|
-
port: options.sshPort,
|
|
63
|
-
username: options.sshUsername || bareUsername(options.username),
|
|
64
|
-
password: options.password,
|
|
65
|
-
});
|
|
66
|
-
await started.connect();
|
|
67
|
-
shell = started;
|
|
68
|
-
return started;
|
|
69
|
-
})();
|
|
70
|
-
|
|
71
|
-
shellStarting.catch(() => { shellStarting = null; });
|
|
72
|
-
return shellStarting;
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
const startedAt = new Date().toISOString();
|
|
76
|
-
|
|
77
|
-
/**
|
|
78
|
-
* What `ai-remote list` reads. Rewritten whenever the viewer comes or goes,
|
|
79
|
-
* so the listing never advertises a window that is not there.
|
|
80
|
-
*/
|
|
81
|
-
function writeMeta(): void {
|
|
82
|
-
writeFileSync(metaPath(options.name), JSON.stringify({
|
|
83
|
-
name: options.name,
|
|
84
|
-
host: options.host,
|
|
85
|
-
port: options.port,
|
|
86
|
-
pid: process.pid,
|
|
87
|
-
viewer: viewer?.url ?? null,
|
|
88
|
-
width: session.framebuffer.width || size.width,
|
|
89
|
-
height: session.framebuffer.height || size.height,
|
|
90
|
-
startedAt,
|
|
91
|
-
}, null, 2));
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
/**
|
|
95
|
-
* The viewer is not part of the session; it attaches to one.
|
|
96
|
-
*
|
|
97
|
-
* So it can be opened and closed while the session keeps running, and a
|
|
98
|
-
* session with no viewer is not a lesser session -- the framebuffer is the
|
|
99
|
-
* authority either way.
|
|
100
|
-
*/
|
|
101
|
-
// Annotated rather than inferred: every assignment happens inside a closure,
|
|
102
|
-
// which control-flow analysis cannot follow, so it would otherwise narrow to
|
|
103
|
-
// `null` and then to `never` at the first use.
|
|
104
|
-
let viewer: Viewer | null = null as Viewer | null;
|
|
105
|
-
|
|
106
|
-
async function openViewer(launch: boolean): Promise<Viewer> {
|
|
107
|
-
if (viewer) {
|
|
108
|
-
if (launch) viewer.launch();
|
|
109
|
-
return viewer;
|
|
110
|
-
}
|
|
111
|
-
viewer = await startViewer(session, options.viewPort, {
|
|
112
|
-
launch,
|
|
113
|
-
openShell,
|
|
114
|
-
tab: options.tab,
|
|
115
|
-
fullscreen: options.fullscreen,
|
|
116
|
-
});
|
|
117
|
-
writeMeta();
|
|
118
|
-
return viewer;
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
function closeViewer(): boolean {
|
|
122
|
-
if (!viewer) return false;
|
|
123
|
-
viewer.close();
|
|
124
|
-
viewer = null;
|
|
125
|
-
writeMeta();
|
|
126
|
-
return true;
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
if (options.view) await openViewer(options.launchBrowser);
|
|
130
|
-
|
|
131
|
-
const path = socketPath(options.name);
|
|
132
|
-
if (existsSync(path)) unlinkSync(path);
|
|
133
|
-
|
|
134
|
-
let lastUsed = Date.now();
|
|
135
|
-
const touch = () => { lastUsed = Date.now(); };
|
|
136
|
-
|
|
137
|
-
const server = net.createServer((socket) => {
|
|
138
|
-
readMessages(socket, async (message: Request) => {
|
|
139
|
-
touch();
|
|
140
|
-
try {
|
|
141
|
-
const result = await handle(message);
|
|
142
|
-
writeMessage(socket, { id: message.id, ok: true, result });
|
|
143
|
-
} catch (error) {
|
|
144
|
-
writeMessage(socket, {
|
|
145
|
-
id: message.id,
|
|
146
|
-
ok: false,
|
|
147
|
-
error: error instanceof Error ? error.message : String(error),
|
|
148
|
-
});
|
|
149
|
-
}
|
|
150
|
-
});
|
|
151
|
-
});
|
|
152
|
-
|
|
153
|
-
async function handle(message: Request): Promise<unknown> {
|
|
154
|
-
const args = (message.args ?? {}) as Record<string, any>;
|
|
155
|
-
|
|
156
|
-
switch (message.op) {
|
|
157
|
-
case 'view-open': {
|
|
158
|
-
const opened = await openViewer(args.launch !== false);
|
|
159
|
-
return { viewer: opened.url, window: opened.window?.kind ?? null };
|
|
160
|
-
}
|
|
161
|
-
|
|
162
|
-
case 'view-close':
|
|
163
|
-
return { closed: closeViewer() };
|
|
164
|
-
|
|
165
|
-
case 'status':
|
|
166
|
-
return {
|
|
167
|
-
host: options.host,
|
|
168
|
-
port: options.port,
|
|
169
|
-
name: options.name,
|
|
170
|
-
width: session.framebuffer.width,
|
|
171
|
-
height: session.framebuffer.height,
|
|
172
|
-
connected: session.connected,
|
|
173
|
-
viewer: viewer?.url ?? null,
|
|
174
|
-
window: viewer?.window?.kind ?? null,
|
|
175
|
-
viewers: viewer?.viewers ?? 0,
|
|
176
|
-
shell: shell ? 'open' : shellStarting ? 'opening' : 'closed',
|
|
177
|
-
pid: process.pid,
|
|
178
|
-
};
|
|
179
|
-
|
|
180
|
-
case 'shot': {
|
|
181
|
-
await session.settle({ quietMs: 250, timeoutMs: 3000 });
|
|
182
|
-
const shot = session.framebuffer.snapshot();
|
|
183
|
-
const scaled = args.maxEdge ? downscale(shot.rgba, shot.width, shot.height, args.maxEdge) : shot;
|
|
184
|
-
const png = encodePng(scaled.rgba, scaled.width, scaled.height);
|
|
185
|
-
if (args.file) {
|
|
186
|
-
writeFileSync(args.file, png);
|
|
187
|
-
return { file: args.file, width: scaled.width, height: scaled.height };
|
|
188
|
-
}
|
|
189
|
-
return { base64: Buffer.from(png).toString('base64'), width: scaled.width, height: scaled.height };
|
|
190
|
-
}
|
|
191
|
-
|
|
192
|
-
case 'script':
|
|
193
|
-
return { steps: await runScript(session, String(args.script ?? '')) };
|
|
194
|
-
|
|
195
|
-
case 'exec': {
|
|
196
|
-
const live = await openShell();
|
|
197
|
-
const result = await live.runCommand(String(args.command ?? ''));
|
|
198
|
-
return result;
|
|
199
|
-
}
|
|
200
|
-
|
|
201
|
-
case 'shell-open':
|
|
202
|
-
await openShell();
|
|
203
|
-
return { shell: 'open' };
|
|
204
|
-
|
|
205
|
-
case 'refresh':
|
|
206
|
-
session.requestFullRepaint();
|
|
207
|
-
return {};
|
|
208
|
-
|
|
209
|
-
case 'close':
|
|
210
|
-
setTimeout(() => shutdown('asked to close'), 50);
|
|
211
|
-
return { closing: true };
|
|
212
|
-
|
|
213
|
-
default:
|
|
214
|
-
throw new Error(`Unknown operation "${message.op}".`);
|
|
215
|
-
}
|
|
216
|
-
}
|
|
217
|
-
|
|
218
|
-
await new Promise<void>((resolve) => server.listen(path, resolve));
|
|
219
|
-
|
|
220
|
-
writeMeta();
|
|
221
|
-
|
|
222
|
-
// A session whose host went away is not worth keeping a socket open for.
|
|
223
|
-
session.client.addEventListener('close', () => shutdown('the host closed the connection'));
|
|
224
|
-
|
|
225
|
-
const idleTimer = options.idleMs > 0
|
|
226
|
-
? setInterval(() => {
|
|
227
|
-
if (Date.now() - lastUsed > options.idleMs) shutdown('idle');
|
|
228
|
-
}, 30_000)
|
|
229
|
-
: null;
|
|
230
|
-
|
|
231
|
-
let shuttingDown = false;
|
|
232
|
-
function shutdown(why: string): void {
|
|
233
|
-
if (shuttingDown) return;
|
|
234
|
-
shuttingDown = true;
|
|
235
|
-
console.log(`[session] closing: ${why}`);
|
|
236
|
-
|
|
237
|
-
if (idleTimer) clearInterval(idleTimer);
|
|
238
|
-
viewer?.close();
|
|
239
|
-
shell?.disconnect();
|
|
240
|
-
session.disconnect();
|
|
241
|
-
server.close();
|
|
242
|
-
for (const file of [path, metaPath(options.name)]) {
|
|
243
|
-
try { unlinkSync(file); } catch { /* already gone */ }
|
|
244
|
-
}
|
|
245
|
-
setTimeout(() => process.exit(0), 100);
|
|
246
|
-
}
|
|
247
|
-
|
|
248
|
-
for (const signal of ['SIGINT', 'SIGTERM'] as const) {
|
|
249
|
-
process.on(signal, () => shutdown(signal));
|
|
250
|
-
}
|
|
251
|
-
|
|
252
|
-
console.log(`[session] ${options.name} ready at ${size.width}x${size.height}`);
|
|
253
|
-
if (viewer) console.log(`[session] viewer ${viewer.url}`);
|
|
254
|
-
}
|
|
255
|
-
|
|
256
|
-
/**
|
|
257
|
-
* Run several steps against one live session.
|
|
258
|
-
*
|
|
259
|
-
* Shared by the `do` command and the daemon, because they mean the same thing:
|
|
260
|
-
* a sequence that keeps its own state between steps.
|
|
261
|
-
*/
|
|
262
|
-
export async function runScript(session: RdpSession, script: string): Promise<string[]> {
|
|
263
|
-
const done: string[] = [];
|
|
264
|
-
|
|
265
|
-
for (const raw of script.split(';')) {
|
|
266
|
-
const step = raw.trim();
|
|
267
|
-
if (!step) continue;
|
|
268
|
-
|
|
269
|
-
const [verb, ...rest] = step.split(/\s+/);
|
|
270
|
-
const argument = rest.join(' ');
|
|
271
|
-
const numbers = () => argument.split(',').map(Number);
|
|
272
|
-
|
|
273
|
-
switch (verb) {
|
|
274
|
-
case 'move': { const [x, y] = numbers(); session.movePointer(x, y); break; }
|
|
275
|
-
case 'click': {
|
|
276
|
-
const [x, y] = numbers();
|
|
277
|
-
const named = argument.split(',')[2]?.trim();
|
|
278
|
-
session.click(x, y, { right: 2, middle: 1 }[named ?? ''] ?? 0);
|
|
279
|
-
break;
|
|
280
|
-
}
|
|
281
|
-
case 'dblclick': { const [x, y] = numbers(); session.click(x, y, 0); session.click(x, y, 0); break; }
|
|
282
|
-
case 'scroll': { const [x, y, dx, dy] = numbers(); session.scroll(x, y, dx || 0, dy || 0); break; }
|
|
283
|
-
case 'type': session.typeText(argument); break;
|
|
284
|
-
case 'key': {
|
|
285
|
-
const codes = argument.split('+').map((code) => code.trim()).filter(Boolean);
|
|
286
|
-
for (const code of codes) session.sendKeyCode(code, true);
|
|
287
|
-
for (const code of [...codes].reverse()) session.sendKeyCode(code, false);
|
|
288
|
-
break;
|
|
289
|
-
}
|
|
290
|
-
case 'cad': session.sendCtrlAltDel(); break;
|
|
291
|
-
case 'wait': await new Promise((resolve) => setTimeout(resolve, Number(argument) || 500)); break;
|
|
292
|
-
case 'shot': {
|
|
293
|
-
await session.settle({ quietMs: 350, timeoutMs: 4000 });
|
|
294
|
-
const shot = session.framebuffer.snapshot();
|
|
295
|
-
writeFileSync(argument || 'screen.png', encodePng(shot.rgba, shot.width, shot.height));
|
|
296
|
-
break;
|
|
297
|
-
}
|
|
298
|
-
default:
|
|
299
|
-
throw new Error(`Unknown step "${verb}" in the script.`);
|
|
300
|
-
}
|
|
301
|
-
|
|
302
|
-
done.push(step);
|
|
303
|
-
}
|
|
304
|
-
|
|
305
|
-
return done;
|
|
306
|
-
}
|
package/src/cli/framebuffer.ts
DELETED
|
@@ -1,115 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* The desktop as pixels, held in memory.
|
|
3
|
-
*
|
|
4
|
-
* In a browser the canvas is both the surface the protocol paints into and the
|
|
5
|
-
* thing a person looks at. Headless, those come apart: this holds the pixels so
|
|
6
|
-
* a screenshot can be taken at any moment, and the viewer -- when there is one
|
|
7
|
-
* -- is a separate attachment fed the same rectangles.
|
|
8
|
-
*
|
|
9
|
-
* Which is what makes the viewer optional. The framebuffer is the authority,
|
|
10
|
-
* so a session with nobody watching is not a degraded session.
|
|
11
|
-
*/
|
|
12
|
-
|
|
13
|
-
import { decodeBitmapRect } from '../protocols/rdp/rle';
|
|
14
|
-
|
|
15
|
-
export interface PaintedRect {
|
|
16
|
-
left: number;
|
|
17
|
-
top: number;
|
|
18
|
-
width: number;
|
|
19
|
-
height: number;
|
|
20
|
-
rgba: Uint8Array;
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
/** Palettes only exist for 8bpp updates; anything else ignores this. */
|
|
24
|
-
const NO_PALETTE = new Uint32Array(256);
|
|
25
|
-
|
|
26
|
-
export class Framebuffer {
|
|
27
|
-
width = 0;
|
|
28
|
-
height = 0;
|
|
29
|
-
/** RGBA, row-major, `width * height * 4` bytes. */
|
|
30
|
-
pixels = new Uint8Array(0);
|
|
31
|
-
/**
|
|
32
|
-
* Bumped on anything that changes what a viewer should be showing, including
|
|
33
|
-
* a resize.
|
|
34
|
-
*/
|
|
35
|
-
generation = 0;
|
|
36
|
-
/**
|
|
37
|
-
* Rectangles painted. Kept apart from `generation` because "the desktop has
|
|
38
|
-
* been sized" and "the desktop has sent pixels" are different questions, and
|
|
39
|
-
* a screenshot taken on the strength of the first one is a black image.
|
|
40
|
-
*/
|
|
41
|
-
paints = 0;
|
|
42
|
-
|
|
43
|
-
palette: Uint32Array | null = null;
|
|
44
|
-
|
|
45
|
-
resize(width: number, height: number): void {
|
|
46
|
-
if (width === this.width && height === this.height) return;
|
|
47
|
-
this.width = width;
|
|
48
|
-
this.height = height;
|
|
49
|
-
this.pixels = new Uint8Array(width * height * 4);
|
|
50
|
-
// Opaque black, so an unpainted desktop reads as a screen rather than as
|
|
51
|
-
// transparency that a PNG viewer will render as a checkerboard.
|
|
52
|
-
for (let i = 3; i < this.pixels.length; i += 4) this.pixels[i] = 255;
|
|
53
|
-
this.generation++;
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
/**
|
|
57
|
-
* Decode and blit one update. Returns what was painted, so a viewer can be
|
|
58
|
-
* sent the same rectangles without decoding them a second time.
|
|
59
|
-
*/
|
|
60
|
-
draw(rects: readonly any[], palette: Uint32Array | null): PaintedRect[] {
|
|
61
|
-
if (palette) this.palette = palette;
|
|
62
|
-
const painted: PaintedRect[] = [];
|
|
63
|
-
|
|
64
|
-
for (const rect of rects) {
|
|
65
|
-
if (!rect.width || !rect.height) continue;
|
|
66
|
-
let rgba: Uint8Array;
|
|
67
|
-
try {
|
|
68
|
-
// decodeBitmapRect hands back a Uint8ClampedArray, which is the right
|
|
69
|
-
// thing for a canvas and the wrong thing for everything else here; the
|
|
70
|
-
// view costs nothing and shares the same bytes.
|
|
71
|
-
const decoded = decodeBitmapRect(rect, this.palette ?? NO_PALETTE);
|
|
72
|
-
rgba = new Uint8Array(decoded.buffer, decoded.byteOffset, decoded.byteLength);
|
|
73
|
-
} catch (error) {
|
|
74
|
-
// One malformed rectangle must not end a session; the next full update
|
|
75
|
-
// repaints the area anyway. Same rule the canvas renderer follows.
|
|
76
|
-
console.warn('[rdp] dropped an undecodable rectangle', {
|
|
77
|
-
geometry: `${rect.width}x${rect.height}+${rect.left}+${rect.top}`,
|
|
78
|
-
error: error instanceof Error ? error.message : String(error),
|
|
79
|
-
});
|
|
80
|
-
continue;
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
this.blit(rect.left, rect.top, rect.width, rect.height, rgba);
|
|
84
|
-
painted.push({ left: rect.left, top: rect.top, width: rect.width, height: rect.height, rgba });
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
if (painted.length) {
|
|
88
|
-
this.generation++;
|
|
89
|
-
this.paints += painted.length;
|
|
90
|
-
}
|
|
91
|
-
return painted;
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
/** Copy one RGBA rectangle in, clipped to the framebuffer. */
|
|
95
|
-
blit(left: number, top: number, width: number, height: number, rgba: Uint8Array): void {
|
|
96
|
-
for (let row = 0; row < height; row++) {
|
|
97
|
-
const y = top + row;
|
|
98
|
-
if (y < 0 || y >= this.height) continue;
|
|
99
|
-
|
|
100
|
-
const from = row * width * 4;
|
|
101
|
-
const columns = Math.min(width, this.width - left);
|
|
102
|
-
if (columns <= 0) continue;
|
|
103
|
-
|
|
104
|
-
this.pixels.set(
|
|
105
|
-
rgba.subarray(from, from + columns * 4),
|
|
106
|
-
(y * this.width + left) * 4
|
|
107
|
-
);
|
|
108
|
-
}
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
/** A copy, so a caller holding a screenshot is not looking at live memory. */
|
|
112
|
-
snapshot(): { rgba: Uint8Array; width: number; height: number } {
|
|
113
|
-
return { rgba: this.pixels.slice(), width: this.width, height: this.height };
|
|
114
|
-
}
|
|
115
|
-
}
|