diggylab 0.1.36 → 0.1.39
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/README.md +55 -17
- package/cli.js +398 -94
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -7,7 +7,7 @@ headless Chromium — the Electron app is not involved.
|
|
|
7
7
|
## Requirements
|
|
8
8
|
|
|
9
9
|
- Node.js 20+
|
|
10
|
-
- Linux packages: `xvfb
|
|
10
|
+
- Linux packages: `xvfb` and `x11vnc` — run `diggylab install-display` (Debian/Ubuntu) or install them yourself. The Docker image below bundles them.
|
|
11
11
|
- Chromium for Playwright: downloaded automatically during `npm install`; rerun `diggylab install-browser` if that download failed.
|
|
12
12
|
|
|
13
13
|
## Install
|
|
@@ -58,37 +58,75 @@ npm publish apps/desktop-agent/dist-npm --access public
|
|
|
58
58
|
| Command | What it does |
|
|
59
59
|
| --- | --- |
|
|
60
60
|
| `pair --code <code> [--name <device name>] [--api <origin>]` | Pairs the server as a device. Create the code on `/settings/desktop-agent` in the web app. |
|
|
61
|
-
| `login` |
|
|
61
|
+
| `login` | Renders the Facebook login page on a virtual screen and prints a DiggyLab link + QR code. Open it on any phone or laptop — no ports, no tunnel — and the session is stored encrypted on the server. |
|
|
62
62
|
| `run` | Connects the WebSocket worker, receives assigned scans, scans headlessly, uploads results. |
|
|
63
63
|
| `status` | Shows the config directory, API origin, paired device, and Facebook session state. |
|
|
64
64
|
| `install-browser` | Downloads the Chromium build Playwright needs (~150 MB). It also runs during `npm install`, so you only need it if that download failed. |
|
|
65
|
-
| `install-display` | On Debian/Ubuntu, installs `xvfb
|
|
65
|
+
| `install-display` | On Debian/Ubuntu, installs `xvfb` and `x11vnc` for the login screen. |
|
|
66
66
|
| `unpair` | Removes device credentials, the Facebook session, and pending scans. |
|
|
67
67
|
|
|
68
|
-
Useful options: `--api <origin>` (default `https://api.diggylab.com`), `--
|
|
69
|
-
|
|
68
|
+
Useful options: `--api <origin>` (default `https://api.diggylab.com`), `--web-url <origin>` (only for a
|
|
69
|
+
self-hosted web app, or `DIGGYLAB_WEB_URL`), `--display <number>` to avoid an Xvfb collision.
|
|
70
70
|
|
|
71
71
|
## How `login` works on a server
|
|
72
72
|
|
|
73
|
-
A server has no screen, so the agent starts `Xvfb` (virtual screen)
|
|
74
|
-
|
|
73
|
+
A server has no screen, so the agent starts `Xvfb` (virtual screen) and `x11vnc` (shares it), then
|
|
74
|
+
carries that screen to the customer through the API connection it already uses for scans:
|
|
75
75
|
|
|
76
76
|
```
|
|
77
|
-
Xvfb → Chromium opens Facebook's real login page →
|
|
77
|
+
Xvfb → Chromium opens Facebook's real login page → encrypted over the API → link + QR → phone logs in
|
|
78
78
|
```
|
|
79
79
|
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
80
|
+
Nothing has to be opened on the server: no port, no tunnel, no firewall change. The link points at
|
|
81
|
+
the web app (`https://diggylab.com/agent-login/<session>#<key>`), and everything is torn down when the
|
|
82
|
+
login finishes or times out (10 minutes).
|
|
83
83
|
|
|
84
|
-
###
|
|
84
|
+
### The screen is encrypted end to end
|
|
85
85
|
|
|
86
|
-
The link
|
|
87
|
-
|
|
88
|
-
|
|
86
|
+
The part of the link after `#` is the decryption key, and browsers never send a fragment to a server —
|
|
87
|
+
so the relay passes bytes it cannot read. DiggyLab stores nothing: no frames, no keystrokes, no
|
|
88
|
+
session. The Facebook cookies stay on the customer's server, sealed with `~/.diggylab/key`.
|
|
89
89
|
|
|
90
|
-
|
|
91
|
-
|
|
90
|
+
The link itself is the only way in: it is single use, expires with the login window, and the session
|
|
91
|
+
is created per `login` run.
|
|
92
|
+
|
|
93
|
+
### When the screen does not appear
|
|
94
|
+
|
|
95
|
+
`login` checks the virtual screen before printing the link: it reads x11vnc's greeting, so a broken
|
|
96
|
+
screen stops the command with the reason instead of leaving the customer with a blank page.
|
|
97
|
+
|
|
98
|
+
Each process writes a log to `~/.diggylab/logs/` (`xvfb.log`, `x11vnc.log`). The tail is printed when
|
|
99
|
+
the screen fails or a process dies during the login, and the files stay there for later inspection.
|
|
100
|
+
|
|
101
|
+
Manual checks while `login` is running:
|
|
102
|
+
|
|
103
|
+
```bash
|
|
104
|
+
pgrep -a Xvfb; pgrep -a x11vnc # both must be running
|
|
105
|
+
ss -ltnp | grep :5999 # x11vnc listens on loopback
|
|
106
|
+
timeout 2 bash -c 'exec 3<>/dev/tcp/127.0.0.1/5999 && head -c 12 <&3' # expect: RFB 003.008
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
If the browser says the link expired, run `diggylab login` again — each run mints a new session.
|
|
110
|
+
|
|
111
|
+
### Logging in with cookies from your own browser
|
|
112
|
+
|
|
113
|
+
If you would rather not share a screen, log into Facebook in your own browser and import those
|
|
114
|
+
cookies instead — no Xvfb, no link, no port:
|
|
115
|
+
|
|
116
|
+
1. Install **Cookie-Editor** (Chrome, Edge, Firefox), open `facebook.com`, then *Export → JSON*.
|
|
117
|
+
(Or copy the `cookie:` request header from your browser's DevTools.)
|
|
118
|
+
2. On the server:
|
|
119
|
+
|
|
120
|
+
```bash
|
|
121
|
+
diggylab login --cookies cookies.json # or: diggylab login --cookies - and paste, then Ctrl-D
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
The agent keeps only the `facebook.com` cookies, saves them encrypted, and opens Facebook once to
|
|
125
|
+
confirm the session before it accepts it. Cookies exported from a different account, or expired ones,
|
|
126
|
+
are rejected with a message instead of failing during a scan.
|
|
127
|
+
|
|
128
|
+
Note: Facebook sometimes asks for a security check when a session moves to a server's IP. If that
|
|
129
|
+
happens the command says so — approve the login from your phone and import again.
|
|
92
130
|
|
|
93
131
|
## Security notes
|
|
94
132
|
|
package/cli.js
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
// src/cli/cli.ts
|
|
4
4
|
import { randomUUID as randomUUID3 } from "node:crypto";
|
|
5
5
|
import { spawn as spawn2 } from "node:child_process";
|
|
6
|
-
import { existsSync as existsSync2, readFileSync as
|
|
6
|
+
import { existsSync as existsSync2, readdirSync, readFileSync as readFileSync2, realpathSync } from "node:fs";
|
|
7
7
|
import { hostname, homedir } from "node:os";
|
|
8
8
|
import { dirname as dirname3, join as join4 } from "node:path";
|
|
9
9
|
import { fileURLToPath } from "node:url";
|
|
@@ -513,7 +513,7 @@ function withTimeout(promise, timeoutMs) {
|
|
|
513
513
|
|
|
514
514
|
// src/cli/cli.ts
|
|
515
515
|
import { chromium } from "playwright";
|
|
516
|
-
import
|
|
516
|
+
import WebSocket2 from "ws";
|
|
517
517
|
|
|
518
518
|
// src/main/activity-log.ts
|
|
519
519
|
import { gzip } from "node:zlib";
|
|
@@ -2312,14 +2312,12 @@ function readPrivateKey(privateKey) {
|
|
|
2312
2312
|
// src/cli/args.ts
|
|
2313
2313
|
var FLAGS = {
|
|
2314
2314
|
"--api": { key: "api", type: "string" },
|
|
2315
|
-
"--bind": { key: "bind", type: "string" },
|
|
2316
2315
|
"--code": { key: "code", type: "string" },
|
|
2317
2316
|
"--config": { key: "config", type: "string" },
|
|
2317
|
+
"--cookies": { key: "cookies", type: "string" },
|
|
2318
2318
|
"--display": { key: "display", type: "number" },
|
|
2319
2319
|
"--name": { key: "name", type: "string" },
|
|
2320
|
-
"--
|
|
2321
|
-
"--port": { key: "port", type: "number" },
|
|
2322
|
-
"--public-host": { key: "publicHost", type: "string" }
|
|
2320
|
+
"--web-url": { key: "webUrl", type: "string" }
|
|
2323
2321
|
};
|
|
2324
2322
|
function parseCliArgs(argv) {
|
|
2325
2323
|
const options = {};
|
|
@@ -2357,6 +2355,77 @@ function browserInstallCommands(platform) {
|
|
|
2357
2355
|
return platform === "linux" ? [["install", "--with-deps", "chromium"], install] : [install];
|
|
2358
2356
|
}
|
|
2359
2357
|
|
|
2358
|
+
// src/cli/cookie-import.ts
|
|
2359
|
+
var FACEBOOK_DOMAIN = ".facebook.com";
|
|
2360
|
+
var FACEBOOK_HOST = /(^|\.)facebook\.com$/i;
|
|
2361
|
+
function isFacebookDomain(domain) {
|
|
2362
|
+
return Boolean(domain && FACEBOOK_HOST.test(domain.replace(/^\./, "")));
|
|
2363
|
+
}
|
|
2364
|
+
function normalizeSameSite(value) {
|
|
2365
|
+
if (typeof value !== "string") return void 0;
|
|
2366
|
+
const lower = value.toLowerCase().replace(/[^a-z]/g, "");
|
|
2367
|
+
if (lower === "lax") return "Lax";
|
|
2368
|
+
if (lower === "strict") return "Strict";
|
|
2369
|
+
if (lower === "none" || lower === "norestriction") return "None";
|
|
2370
|
+
return void 0;
|
|
2371
|
+
}
|
|
2372
|
+
function toCookie(value) {
|
|
2373
|
+
const name = typeof value.name === "string" ? value.name.trim() : "";
|
|
2374
|
+
const cookieValue = typeof value.value === "string" ? value.value : "";
|
|
2375
|
+
if (!name || !cookieValue) return null;
|
|
2376
|
+
return {
|
|
2377
|
+
domain: typeof value.domain === "string" ? value.domain : void 0,
|
|
2378
|
+
expires: typeof value.expires === "number" ? value.expires : typeof value.expirationDate === "number" ? value.expirationDate : void 0,
|
|
2379
|
+
httpOnly: typeof value.httpOnly === "boolean" ? value.httpOnly : void 0,
|
|
2380
|
+
name,
|
|
2381
|
+
path: typeof value.path === "string" ? value.path : void 0,
|
|
2382
|
+
sameSite: normalizeSameSite(value.sameSite),
|
|
2383
|
+
secure: typeof value.secure === "boolean" ? value.secure : void 0,
|
|
2384
|
+
value: cookieValue
|
|
2385
|
+
};
|
|
2386
|
+
}
|
|
2387
|
+
function parseCookieHeader(input) {
|
|
2388
|
+
const header = input.replace(/^\s*cookie\s*:/i, "");
|
|
2389
|
+
return header.split(";").map((part) => part.trim()).filter(Boolean).map((part) => {
|
|
2390
|
+
const separator = part.indexOf("=");
|
|
2391
|
+
if (separator <= 0) return null;
|
|
2392
|
+
return { name: part.slice(0, separator).trim(), value: part.slice(separator + 1) };
|
|
2393
|
+
}).filter((cookie) => cookie !== null);
|
|
2394
|
+
}
|
|
2395
|
+
function parseCookieInput(input) {
|
|
2396
|
+
const trimmed = input.trim();
|
|
2397
|
+
if (!trimmed) return [];
|
|
2398
|
+
if (!trimmed.startsWith("{") && !trimmed.startsWith("[")) return parseCookieHeader(trimmed);
|
|
2399
|
+
try {
|
|
2400
|
+
const parsed = JSON.parse(trimmed);
|
|
2401
|
+
if (Array.isArray(parsed)) {
|
|
2402
|
+
return parsed.map((entry) => entry && typeof entry === "object" ? toCookie(entry) : null).filter((cookie) => cookie !== null);
|
|
2403
|
+
}
|
|
2404
|
+
if (parsed && typeof parsed === "object") {
|
|
2405
|
+
const cookies = parsed.cookies;
|
|
2406
|
+
if (Array.isArray(cookies)) {
|
|
2407
|
+
return cookies.map((entry) => entry && typeof entry === "object" ? toCookie(entry) : null).filter((cookie) => cookie !== null);
|
|
2408
|
+
}
|
|
2409
|
+
}
|
|
2410
|
+
} catch {
|
|
2411
|
+
return [];
|
|
2412
|
+
}
|
|
2413
|
+
return [];
|
|
2414
|
+
}
|
|
2415
|
+
function toFacebookStorageState(cookies) {
|
|
2416
|
+
const usable = cookies.filter((cookie) => isFacebookDomain(cookie.domain) || cookie.domain === void 0).map((cookie) => ({
|
|
2417
|
+
domain: isFacebookDomain(cookie.domain) ? cookie.domain : FACEBOOK_DOMAIN,
|
|
2418
|
+
expires: typeof cookie.expires === "number" && cookie.expires > 0 ? cookie.expires : -1,
|
|
2419
|
+
httpOnly: cookie.httpOnly ?? true,
|
|
2420
|
+
name: cookie.name,
|
|
2421
|
+
path: cookie.path?.startsWith("/") ? cookie.path : "/",
|
|
2422
|
+
sameSite: cookie.sameSite ?? "Lax",
|
|
2423
|
+
secure: cookie.secure ?? true,
|
|
2424
|
+
value: cookie.value
|
|
2425
|
+
}));
|
|
2426
|
+
return { cookies: usable, origins: [] };
|
|
2427
|
+
}
|
|
2428
|
+
|
|
2360
2429
|
// src/cli/file-crypto.ts
|
|
2361
2430
|
import { createCipheriv, createDecipheriv, randomBytes } from "node:crypto";
|
|
2362
2431
|
import { chmodSync, existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
@@ -2404,6 +2473,163 @@ function createFileCrypto(keyPath) {
|
|
|
2404
2473
|
};
|
|
2405
2474
|
}
|
|
2406
2475
|
|
|
2476
|
+
// src/cli/login-screen-relay.ts
|
|
2477
|
+
import { createConnection } from "node:net";
|
|
2478
|
+
import WebSocket from "ws";
|
|
2479
|
+
|
|
2480
|
+
// src/cli/relay-crypto.ts
|
|
2481
|
+
import { createCipheriv as createCipheriv2, createDecipheriv as createDecipheriv2, hkdfSync, randomBytes as randomBytes2 } from "node:crypto";
|
|
2482
|
+
var FRAME_VERSION = 1;
|
|
2483
|
+
var IV_BYTES2 = 12;
|
|
2484
|
+
var KEY_BYTES2 = 32;
|
|
2485
|
+
var TAG_BYTES2 = 16;
|
|
2486
|
+
function createRelayKey() {
|
|
2487
|
+
return randomBytes2(KEY_BYTES2).toString("base64url");
|
|
2488
|
+
}
|
|
2489
|
+
function deriveRelayKeys(key, sessionId) {
|
|
2490
|
+
const secret = Buffer.from(key, "base64url");
|
|
2491
|
+
const salt = Buffer.from(sessionId, "utf8");
|
|
2492
|
+
const derive = (info) => Buffer.from(hkdfSync("sha256", secret, salt, Buffer.from(info, "utf8"), KEY_BYTES2));
|
|
2493
|
+
return { fromAgent: derive("agent-to-browser"), toAgent: derive("browser-to-agent") };
|
|
2494
|
+
}
|
|
2495
|
+
function sealFrame(key, payload) {
|
|
2496
|
+
const iv = randomBytes2(IV_BYTES2);
|
|
2497
|
+
const cipher = createCipheriv2("aes-256-gcm", key, iv);
|
|
2498
|
+
const sealed = Buffer.concat([cipher.update(payload), cipher.final()]);
|
|
2499
|
+
return Buffer.concat([Buffer.from([FRAME_VERSION]), iv, sealed, cipher.getAuthTag()]);
|
|
2500
|
+
}
|
|
2501
|
+
function openFrame(key, frame) {
|
|
2502
|
+
if (frame.length < 1 + IV_BYTES2 + TAG_BYTES2 || frame[0] !== FRAME_VERSION) {
|
|
2503
|
+
throw new Error("Invalid login screen frame.");
|
|
2504
|
+
}
|
|
2505
|
+
const decipher = createDecipheriv2("aes-256-gcm", key, frame.subarray(1, 1 + IV_BYTES2));
|
|
2506
|
+
decipher.setAuthTag(frame.subarray(frame.length - TAG_BYTES2));
|
|
2507
|
+
return Buffer.concat([
|
|
2508
|
+
decipher.update(frame.subarray(1 + IV_BYTES2, frame.length - TAG_BYTES2)),
|
|
2509
|
+
decipher.final()
|
|
2510
|
+
]);
|
|
2511
|
+
}
|
|
2512
|
+
|
|
2513
|
+
// src/cli/login-screen-relay.ts
|
|
2514
|
+
var RELAY_PATH = "/desktop-agent/login-screen";
|
|
2515
|
+
var READY_TIMEOUT_MS = 1e4;
|
|
2516
|
+
var DEFAULT_WEB_ORIGIN = "https://diggylab.com";
|
|
2517
|
+
async function openLoginScreenRelay(options) {
|
|
2518
|
+
const socket = new WebSocket(`${toWebSocketOrigin(options.apiOrigin)}${RELAY_PATH}`, {
|
|
2519
|
+
headers: { authorization: `Bearer ${options.accessToken}` }
|
|
2520
|
+
});
|
|
2521
|
+
let closed = false;
|
|
2522
|
+
let keys = null;
|
|
2523
|
+
let vnc = null;
|
|
2524
|
+
let resolveScreen = () => void 0;
|
|
2525
|
+
let rejectFailure = () => void 0;
|
|
2526
|
+
const failure = new Promise((_resolve, reject) => {
|
|
2527
|
+
rejectFailure = reject;
|
|
2528
|
+
});
|
|
2529
|
+
const screenConnected = new Promise((resolve) => {
|
|
2530
|
+
resolveScreen = resolve;
|
|
2531
|
+
});
|
|
2532
|
+
failure.catch(() => void 0);
|
|
2533
|
+
const close = () => {
|
|
2534
|
+
closed = true;
|
|
2535
|
+
vnc?.destroy();
|
|
2536
|
+
vnc = null;
|
|
2537
|
+
if (socket.readyState === WebSocket.OPEN || socket.readyState === WebSocket.CONNECTING) socket.close();
|
|
2538
|
+
};
|
|
2539
|
+
const fail = (message) => {
|
|
2540
|
+
close();
|
|
2541
|
+
rejectFailure(new Error(message));
|
|
2542
|
+
};
|
|
2543
|
+
const openScreen = () => {
|
|
2544
|
+
if (closed || vnc) return;
|
|
2545
|
+
const connection = createConnection({ host: "127.0.0.1", port: options.rfbPort });
|
|
2546
|
+
vnc = connection;
|
|
2547
|
+
connection.on("data", (chunk) => {
|
|
2548
|
+
if (closed || !keys || socket.readyState !== WebSocket.OPEN) return;
|
|
2549
|
+
socket.send(sealFrame(keys.fromAgent, chunk), { binary: true });
|
|
2550
|
+
});
|
|
2551
|
+
connection.once("error", () => {
|
|
2552
|
+
if (vnc === connection) vnc = null;
|
|
2553
|
+
fail("Could not reach the virtual screen.");
|
|
2554
|
+
});
|
|
2555
|
+
connection.once("close", () => {
|
|
2556
|
+
if (vnc === connection) vnc = null;
|
|
2557
|
+
});
|
|
2558
|
+
};
|
|
2559
|
+
socket.on("message", (data, isBinary) => {
|
|
2560
|
+
if (isBinary) {
|
|
2561
|
+
if (!keys || !vnc || vnc.destroyed) return;
|
|
2562
|
+
try {
|
|
2563
|
+
vnc.write(openFrame(keys.toAgent, Buffer.from(data)));
|
|
2564
|
+
} catch {
|
|
2565
|
+
fail("The login screen received a corrupted frame.");
|
|
2566
|
+
}
|
|
2567
|
+
return;
|
|
2568
|
+
}
|
|
2569
|
+
const message = parseRelayMessage(data.toString());
|
|
2570
|
+
if (message?.type === "screen-connected") {
|
|
2571
|
+
openScreen();
|
|
2572
|
+
resolveScreen();
|
|
2573
|
+
} else if (message?.type === "screen-disconnected") {
|
|
2574
|
+
vnc?.destroy();
|
|
2575
|
+
vnc = null;
|
|
2576
|
+
}
|
|
2577
|
+
});
|
|
2578
|
+
socket.once("error", () => fail("Could not reach the DiggyLab relay."));
|
|
2579
|
+
socket.once("close", () => {
|
|
2580
|
+
if (!closed) fail("The DiggyLab relay closed the login screen.");
|
|
2581
|
+
});
|
|
2582
|
+
const ready = await waitForReady(socket);
|
|
2583
|
+
keys = deriveRelayKeys(options.key, ready.id);
|
|
2584
|
+
const webOrigin = (options.webUrl ?? ready.webUrl ?? DEFAULT_WEB_ORIGIN).replace(/\/+$/, "");
|
|
2585
|
+
return {
|
|
2586
|
+
close,
|
|
2587
|
+
failure,
|
|
2588
|
+
screenConnected,
|
|
2589
|
+
url: `${webOrigin}/agent-login/${encodeURIComponent(ready.id)}#${options.key}`
|
|
2590
|
+
};
|
|
2591
|
+
}
|
|
2592
|
+
function parseRelayMessage(value) {
|
|
2593
|
+
try {
|
|
2594
|
+
const parsed = JSON.parse(value);
|
|
2595
|
+
if (parsed && typeof parsed === "object" && typeof parsed.type === "string") {
|
|
2596
|
+
return parsed;
|
|
2597
|
+
}
|
|
2598
|
+
} catch {
|
|
2599
|
+
}
|
|
2600
|
+
return null;
|
|
2601
|
+
}
|
|
2602
|
+
function waitForReady(socket) {
|
|
2603
|
+
return new Promise((resolve, reject) => {
|
|
2604
|
+
const timer = setTimeout(() => reject(new Error("The DiggyLab relay did not answer in time.")), READY_TIMEOUT_MS);
|
|
2605
|
+
timer.unref();
|
|
2606
|
+
socket.once("message", (data, isBinary) => {
|
|
2607
|
+
clearTimeout(timer);
|
|
2608
|
+
if (isBinary) {
|
|
2609
|
+
reject(new Error("The DiggyLab relay answered with an unexpected frame."));
|
|
2610
|
+
return;
|
|
2611
|
+
}
|
|
2612
|
+
const message = parseRelayMessage(data.toString());
|
|
2613
|
+
if (message?.type !== "ready") {
|
|
2614
|
+
reject(new Error("The DiggyLab relay did not accept the login screen."));
|
|
2615
|
+
return;
|
|
2616
|
+
}
|
|
2617
|
+
resolve(message);
|
|
2618
|
+
});
|
|
2619
|
+
socket.once("error", () => {
|
|
2620
|
+
clearTimeout(timer);
|
|
2621
|
+
reject(new Error("Could not reach the DiggyLab relay."));
|
|
2622
|
+
});
|
|
2623
|
+
socket.once("close", () => {
|
|
2624
|
+
clearTimeout(timer);
|
|
2625
|
+
reject(new Error("The DiggyLab relay refused the login screen."));
|
|
2626
|
+
});
|
|
2627
|
+
});
|
|
2628
|
+
}
|
|
2629
|
+
function toWebSocketOrigin(origin) {
|
|
2630
|
+
return origin.replace(/^http/i, "ws").replace(/\/+$/, "");
|
|
2631
|
+
}
|
|
2632
|
+
|
|
2407
2633
|
// src/cli/qr.ts
|
|
2408
2634
|
import qrcode from "qrcode-terminal";
|
|
2409
2635
|
function terminalQr(value) {
|
|
@@ -2416,19 +2642,41 @@ function terminalQr(value) {
|
|
|
2416
2642
|
|
|
2417
2643
|
// src/cli/virtual-display.ts
|
|
2418
2644
|
import { spawn } from "node:child_process";
|
|
2419
|
-
import {
|
|
2420
|
-
import {
|
|
2421
|
-
import { createConnection } from "node:net";
|
|
2422
|
-
import { networkInterfaces } from "node:os";
|
|
2645
|
+
import { createWriteStream, mkdirSync as mkdirSync2 } from "node:fs";
|
|
2646
|
+
import { createConnection as createConnection3 } from "node:net";
|
|
2423
2647
|
import { join as join3 } from "node:path";
|
|
2648
|
+
|
|
2649
|
+
// src/cli/vnc-probe.ts
|
|
2650
|
+
import { createConnection as createConnection2 } from "node:net";
|
|
2651
|
+
var PROBE_TIMEOUT_MS = 4e3;
|
|
2652
|
+
var GREETING_BYTES = 12;
|
|
2653
|
+
var GREETING_PATTERN = /^RFB \d{3}\.\d{3}\n$/;
|
|
2654
|
+
function probeRfb(host, port, timeoutMs = PROBE_TIMEOUT_MS) {
|
|
2655
|
+
return new Promise((resolve) => {
|
|
2656
|
+
const socket = createConnection2({ host, port });
|
|
2657
|
+
const finish = (result) => {
|
|
2658
|
+
clearTimeout(timer);
|
|
2659
|
+
socket.destroy();
|
|
2660
|
+
resolve(result);
|
|
2661
|
+
};
|
|
2662
|
+
const timer = setTimeout(() => finish(false), timeoutMs);
|
|
2663
|
+
let received = "";
|
|
2664
|
+
socket.on("data", (chunk) => {
|
|
2665
|
+
received += chunk.toString("latin1");
|
|
2666
|
+
if (received.length >= GREETING_BYTES) finish(GREETING_PATTERN.test(received.slice(0, GREETING_BYTES)));
|
|
2667
|
+
});
|
|
2668
|
+
socket.once("error", () => finish(false));
|
|
2669
|
+
socket.once("close", () => finish(false));
|
|
2670
|
+
});
|
|
2671
|
+
}
|
|
2672
|
+
|
|
2673
|
+
// src/cli/virtual-display.ts
|
|
2424
2674
|
var START_TIMEOUT_MS = 15e3;
|
|
2425
|
-
var PUBLIC_IP_TIMEOUT_MS = 3e3;
|
|
2426
|
-
var PUBLIC_IP_ENDPOINT = "https://api.ipify.org";
|
|
2427
2675
|
function waitForPort(host, port, timeoutMs) {
|
|
2428
2676
|
const deadline = Date.now() + timeoutMs;
|
|
2429
2677
|
return new Promise((resolve, reject) => {
|
|
2430
2678
|
const attempt = () => {
|
|
2431
|
-
const socket =
|
|
2679
|
+
const socket = createConnection3({ host, port });
|
|
2432
2680
|
socket.once("connect", () => {
|
|
2433
2681
|
socket.destroy();
|
|
2434
2682
|
resolve();
|
|
@@ -2445,94 +2693,56 @@ function waitForPort(host, port, timeoutMs) {
|
|
|
2445
2693
|
attempt();
|
|
2446
2694
|
});
|
|
2447
2695
|
}
|
|
2448
|
-
function
|
|
2449
|
-
const [first, second] = address.split(".").map(Number);
|
|
2450
|
-
return first === 10 || first === 127 || first === 169 && second === 254 || first === 172 && second >= 16 && second <= 31 || first === 192 && second === 168 || first === 100 && second >= 64 && second <= 127;
|
|
2451
|
-
}
|
|
2452
|
-
function defaultRouteInterface() {
|
|
2453
|
-
if (process.platform !== "linux") return null;
|
|
2454
|
-
try {
|
|
2455
|
-
for (const line of readFileSync2("/proc/net/route", "utf8").split("\n").slice(1)) {
|
|
2456
|
-
const [iface, destination, , , , , flags] = line.trim().split(/\s+/);
|
|
2457
|
-
if (destination === "00000000" && Number.parseInt(flags ?? "0", 16) & 2) return iface;
|
|
2458
|
-
}
|
|
2459
|
-
} catch {
|
|
2460
|
-
}
|
|
2461
|
-
return null;
|
|
2462
|
-
}
|
|
2463
|
-
function localAddress() {
|
|
2464
|
-
const interfaces = networkInterfaces();
|
|
2465
|
-
const addressOf = (addresses) => addresses?.find((entry) => entry.family === "IPv4" && !entry.internal)?.address ?? null;
|
|
2466
|
-
const preferred = defaultRouteInterface();
|
|
2467
|
-
return (preferred ? addressOf(interfaces[preferred]) : null) ?? Object.values(interfaces).map(addressOf).find((address) => address !== null) ?? "127.0.0.1";
|
|
2468
|
-
}
|
|
2469
|
-
async function detectPublicIp(fetcher = fetch) {
|
|
2470
|
-
try {
|
|
2471
|
-
const response = await fetcher(PUBLIC_IP_ENDPOINT, { signal: AbortSignal.timeout(PUBLIC_IP_TIMEOUT_MS) });
|
|
2472
|
-
if (!response.ok) return null;
|
|
2473
|
-
const value = (await response.text()).trim();
|
|
2474
|
-
return /^\d{1,3}(?:\.\d{1,3}){3}$/.test(value) ? value : null;
|
|
2475
|
-
} catch {
|
|
2476
|
-
return null;
|
|
2477
|
-
}
|
|
2478
|
-
}
|
|
2479
|
-
function start(command, args, env, deps) {
|
|
2696
|
+
function start(name, command, args, env, deps, logDirectory) {
|
|
2480
2697
|
const child = deps.spawnProcess(command, args, env);
|
|
2698
|
+
const log = createWriteStream(join3(logDirectory, `${name}.log`), { mode: 384 });
|
|
2481
2699
|
const failure = new Promise((_resolve, reject) => {
|
|
2482
2700
|
child.on("error", (error) => reject(error instanceof Error ? error : new Error(String(error))));
|
|
2483
2701
|
child.on("exit", (code) => reject(new Error(`${command} exited with code ${String(code)}.`)));
|
|
2484
2702
|
});
|
|
2703
|
+
log.on("error", () => void 0);
|
|
2704
|
+
log.write(`# ${command} ${args.join(" ")}
|
|
2705
|
+
`);
|
|
2706
|
+
child.stdout?.pipe?.(log, { end: false });
|
|
2707
|
+
child.stderr?.pipe?.(log, { end: false });
|
|
2485
2708
|
return { child, failure };
|
|
2486
2709
|
}
|
|
2487
2710
|
async function startVirtualDisplay(options, overrides = {}) {
|
|
2488
2711
|
const deps = {
|
|
2489
|
-
|
|
2490
|
-
localAddress,
|
|
2712
|
+
probeRfb,
|
|
2491
2713
|
spawnProcess: spawn,
|
|
2492
2714
|
waitForPort,
|
|
2493
2715
|
...overrides
|
|
2494
2716
|
};
|
|
2495
|
-
const bind = options.bind ?? "0.0.0.0";
|
|
2496
2717
|
const displayNumber = options.displayNumber ?? 99;
|
|
2718
|
+
const display = `:${displayNumber}`;
|
|
2497
2719
|
const height = options.height ?? 900;
|
|
2498
|
-
const novncWeb = options.novncWeb ?? "/usr/share/novnc";
|
|
2499
|
-
const port = options.port ?? 6080;
|
|
2500
2720
|
const rfbPort = 5900 + displayNumber;
|
|
2501
|
-
|
|
2502
|
-
const
|
|
2503
|
-
|
|
2504
|
-
|
|
2505
|
-
|
|
2506
|
-
const xvfb = start("Xvfb", [`:${displayNumber}`, "-screen", `0`, `${options.width ?? 1280}x${height}x24`, "-nolisten", "tcp"], process.env, deps);
|
|
2507
|
-
const vnc = start("x11vnc", ["-display", `:${displayNumber}`, "-localhost", "-rfbport", String(rfbPort), "-forever", "-shared", "-nopw", "-quiet"], process.env, deps);
|
|
2508
|
-
const proxy = start("websockify", ["--web", novncWeb, "--token-plugin", "TokenFile", "--token-source", tokenFile, `${bind}:${port}`], process.env, deps);
|
|
2721
|
+
mkdirSync2(options.logDirectory, { mode: 448, recursive: true });
|
|
2722
|
+
const xvfb = start("xvfb", "Xvfb", [display, "-screen", "0", `${options.width ?? 1280}x${height}x24`, "-nolisten", "tcp"], process.env, deps, options.logDirectory);
|
|
2723
|
+
const vnc = start("x11vnc", "x11vnc", ["-display", display, "-localhost", "-rfbport", String(rfbPort), "-forever", "-shared", "-nopw", "-quiet"], process.env, deps, options.logDirectory);
|
|
2724
|
+
const failure = Promise.race([vnc.failure, xvfb.failure]);
|
|
2725
|
+
failure.catch(() => void 0);
|
|
2509
2726
|
const stop = async () => {
|
|
2510
|
-
for (const { child } of [
|
|
2727
|
+
for (const { child } of [vnc, xvfb]) {
|
|
2511
2728
|
child.kill("SIGTERM");
|
|
2512
2729
|
await new Promise((resolve) => setTimeout(resolve, 200));
|
|
2513
2730
|
child.kill("SIGKILL");
|
|
2514
2731
|
}
|
|
2515
|
-
rmSync(tokenFile, { force: true });
|
|
2516
2732
|
};
|
|
2517
2733
|
try {
|
|
2518
2734
|
await Promise.race([
|
|
2519
|
-
deps.waitForPort(
|
|
2520
|
-
|
|
2521
|
-
vnc.failure,
|
|
2522
|
-
xvfb.failure
|
|
2735
|
+
deps.waitForPort("127.0.0.1", rfbPort, options.timeoutMs ?? START_TIMEOUT_MS),
|
|
2736
|
+
failure
|
|
2523
2737
|
]);
|
|
2738
|
+
if (!await deps.probeRfb("127.0.0.1", rfbPort)) {
|
|
2739
|
+
throw new Error(`The virtual screen did not answer on 127.0.0.1:${rfbPort}.`);
|
|
2740
|
+
}
|
|
2524
2741
|
} catch (error) {
|
|
2525
2742
|
await stop();
|
|
2526
2743
|
throw error;
|
|
2527
2744
|
}
|
|
2528
|
-
|
|
2529
|
-
const link = (host) => `http://${host}:${port}/vnc.html?autoconnect=1&token=${encodeURIComponent(token)}`;
|
|
2530
|
-
const publicIp = options.publicHost || !isPrivateAddress(publicHost) ? null : await deps.detectPublicIp();
|
|
2531
|
-
return {
|
|
2532
|
-
alternateUrl: publicIp && publicIp !== publicHost ? link(publicIp) : null,
|
|
2533
|
-
stop,
|
|
2534
|
-
url: link(publicHost)
|
|
2535
|
-
};
|
|
2745
|
+
return { display, failure, logDirectory: options.logDirectory, rfbPort, stop };
|
|
2536
2746
|
}
|
|
2537
2747
|
|
|
2538
2748
|
// src/cli/cli.ts
|
|
@@ -2542,7 +2752,8 @@ var USAGE = `DiggyLab Desktop Agent CLI
|
|
|
2542
2752
|
|
|
2543
2753
|
Usage:
|
|
2544
2754
|
diggylab pair --code <code> [--name <device name>] [--api <https://api.diggylab.com>]
|
|
2545
|
-
diggylab login [--
|
|
2755
|
+
diggylab login [--display <number>] [--web-url <https://diggylab.com>]
|
|
2756
|
+
diggylab login --cookies <file or -> (import cookies exported from your own browser)
|
|
2546
2757
|
diggylab run
|
|
2547
2758
|
diggylab status
|
|
2548
2759
|
diggylab install-browser
|
|
@@ -2553,14 +2764,14 @@ Global options:
|
|
|
2553
2764
|
--config <directory> Agent state directory (default ~/.diggylab)
|
|
2554
2765
|
--api <origin> API origin (default $DIGGYLAB_API_URL or https://api.diggylab.com)`;
|
|
2555
2766
|
var MISSING_BROWSER_HINT = "Chromium for Playwright is not installed yet. Run `diggylab install-browser` once.";
|
|
2556
|
-
var MISSING_DISPLAY_HINT = "The
|
|
2767
|
+
var MISSING_DISPLAY_HINT = "The login screen needs Xvfb and x11vnc. Run `diggylab install-display` (Debian/Ubuntu) or use the Docker image.";
|
|
2557
2768
|
function agentVersion() {
|
|
2558
2769
|
let directory = fileURLToPath(new URL(".", import.meta.url));
|
|
2559
2770
|
for (let depth = 0; depth < 4; depth += 1) {
|
|
2560
2771
|
const manifest = join4(directory, "package.json");
|
|
2561
2772
|
if (existsSync2(manifest)) {
|
|
2562
2773
|
try {
|
|
2563
|
-
const version = JSON.parse(
|
|
2774
|
+
const version = JSON.parse(readFileSync2(manifest, "utf8")).version;
|
|
2564
2775
|
if (typeof version === "string" && version) return version;
|
|
2565
2776
|
} catch {
|
|
2566
2777
|
}
|
|
@@ -2594,6 +2805,19 @@ function createRuntime(options) {
|
|
|
2594
2805
|
const manager = new DeviceCredentialManager(sessions, http, createDeviceIdentity);
|
|
2595
2806
|
return { activity, browserType: createBrowserLauncher(chromium), directory, endpoint, facebook, manager, sessions, spool };
|
|
2596
2807
|
}
|
|
2808
|
+
function printDisplayLogs(directory, io, lines = 12) {
|
|
2809
|
+
io.error(`Login screen logs: ${directory}`);
|
|
2810
|
+
try {
|
|
2811
|
+
for (const name of readdirSync(directory).sort()) {
|
|
2812
|
+
if (!name.endsWith(".log")) continue;
|
|
2813
|
+
const tail = readFileSync2(join4(directory, name), "utf8").split("\n").filter((line) => line.trim() !== "").slice(-lines);
|
|
2814
|
+
if (tail.length === 0) continue;
|
|
2815
|
+
io.error(`--- ${name} ---`);
|
|
2816
|
+
for (const line of tail) io.error(line);
|
|
2817
|
+
}
|
|
2818
|
+
} catch {
|
|
2819
|
+
}
|
|
2820
|
+
}
|
|
2597
2821
|
function waitForFacebookLogin(login, timeoutMs) {
|
|
2598
2822
|
return new Promise((resolve, reject) => {
|
|
2599
2823
|
const timer = setTimeout(() => {
|
|
@@ -2659,6 +2883,7 @@ async function commandPair(runtime, options, io) {
|
|
|
2659
2883
|
}
|
|
2660
2884
|
}
|
|
2661
2885
|
async function commandLogin(runtime, options, io) {
|
|
2886
|
+
if (options.cookies) return importFacebookCookies(runtime, options.cookies, io);
|
|
2662
2887
|
const login = new FacebookLoginService({
|
|
2663
2888
|
browserType: runtime.browserType,
|
|
2664
2889
|
clearTimer: (timer) => clearTimeout(timer),
|
|
@@ -2677,49 +2902,128 @@ async function commandLogin(runtime, options, io) {
|
|
|
2677
2902
|
return 0;
|
|
2678
2903
|
}
|
|
2679
2904
|
let display = null;
|
|
2905
|
+
let relay = null;
|
|
2680
2906
|
try {
|
|
2681
2907
|
const needsVirtualDisplay = process.platform === "linux" && !process.env.DISPLAY;
|
|
2682
2908
|
if (!needsVirtualDisplay) {
|
|
2683
2909
|
io.log(process.env.DISPLAY ? `Opening the Facebook login page on display ${process.env.DISPLAY}.` : "Opening the Facebook login page in a browser window.");
|
|
2684
2910
|
} else {
|
|
2685
|
-
const
|
|
2911
|
+
const access = runtime.manager.getAccessCredential();
|
|
2912
|
+
if (!access) {
|
|
2913
|
+
io.error("This server is not paired yet. Run `diggylab pair --code <code>` first.");
|
|
2914
|
+
return 1;
|
|
2915
|
+
}
|
|
2916
|
+
const webUrl = options.webUrl ?? process.env.DIGGYLAB_WEB_URL?.trim();
|
|
2686
2917
|
display = await startVirtualDisplay({
|
|
2687
|
-
bind: options.bind,
|
|
2688
2918
|
displayNumber: options.display,
|
|
2689
|
-
|
|
2690
|
-
port: options.port,
|
|
2691
|
-
publicHost: configuredHost || void 0,
|
|
2692
|
-
tokenDirectory: join4(runtime.directory, "novnc")
|
|
2919
|
+
logDirectory: join4(runtime.directory, "logs")
|
|
2693
2920
|
});
|
|
2694
|
-
|
|
2695
|
-
|
|
2921
|
+
relay = await openLoginScreenRelay({
|
|
2922
|
+
accessToken: access.accessToken,
|
|
2923
|
+
apiOrigin: runtime.endpoint.httpOrigin,
|
|
2924
|
+
key: createRelayKey(),
|
|
2925
|
+
rfbPort: display.rfbPort,
|
|
2926
|
+
webUrl: webUrl || void 0
|
|
2927
|
+
});
|
|
2928
|
+
process.env.DISPLAY = display.display;
|
|
2929
|
+
io.log(terminalQr(relay.url));
|
|
2696
2930
|
io.log(`Open this link on your phone, or scan the QR above:
|
|
2697
|
-
${
|
|
2698
|
-
if (display.alternateUrl) {
|
|
2699
|
-
io.log(`Phone on another network? Forward port ${options.port ?? 6080} and use:
|
|
2700
|
-
${display.alternateUrl}`);
|
|
2701
|
-
}
|
|
2931
|
+
${relay.url}`);
|
|
2702
2932
|
io.log("Log into Facebook on that screen; the agent saves the session automatically.");
|
|
2933
|
+
void relay.screenConnected.then(() => io.log("Login screen opened in the browser."));
|
|
2703
2934
|
}
|
|
2704
|
-
|
|
2935
|
+
const waiting = waitForFacebookLogin(login, LOGIN_TIMEOUT_MS + 6e4);
|
|
2936
|
+
const failures = [display?.failure, relay?.failure].filter((value) => Boolean(value));
|
|
2937
|
+
await (failures.length > 0 ? Promise.race([waiting, ...failures]) : waiting);
|
|
2705
2938
|
io.log("Facebook session saved.");
|
|
2706
2939
|
return 0;
|
|
2707
2940
|
} catch (error) {
|
|
2708
2941
|
const message = error instanceof Error ? error.message : String(error);
|
|
2709
|
-
if (/spawn (?:Xvfb|x11vnc
|
|
2942
|
+
if (/spawn (?:Xvfb|x11vnc) ENOENT/.test(message)) {
|
|
2710
2943
|
io.error(MISSING_DISPLAY_HINT);
|
|
2711
2944
|
return 1;
|
|
2712
2945
|
}
|
|
2946
|
+
if (/virtual screen|login screen|DiggyLab relay|Xvfb exited|x11vnc exited/i.test(message)) {
|
|
2947
|
+
io.error(`The login screen failed: ${message}`);
|
|
2948
|
+
printDisplayLogs(join4(runtime.directory, "logs"), io);
|
|
2949
|
+
return 1;
|
|
2950
|
+
}
|
|
2713
2951
|
if (/Executable doesn't exist|playwright install/i.test(message)) {
|
|
2714
2952
|
io.error(MISSING_BROWSER_HINT);
|
|
2715
2953
|
return 1;
|
|
2716
2954
|
}
|
|
2717
2955
|
throw error;
|
|
2718
2956
|
} finally {
|
|
2957
|
+
relay?.close();
|
|
2719
2958
|
await login.close();
|
|
2720
2959
|
await display?.stop();
|
|
2721
2960
|
}
|
|
2722
2961
|
}
|
|
2962
|
+
async function importFacebookCookies(runtime, source, io) {
|
|
2963
|
+
let input;
|
|
2964
|
+
try {
|
|
2965
|
+
input = source === "-" ? await readStdin() : readFileSync2(source, "utf8");
|
|
2966
|
+
} catch {
|
|
2967
|
+
io.error(`Could not read "${source}". Pass a file path, or "-" to paste the cookies instead.`);
|
|
2968
|
+
return 1;
|
|
2969
|
+
}
|
|
2970
|
+
const cookies = parseCookieInput(input);
|
|
2971
|
+
if (cookies.length === 0) {
|
|
2972
|
+
io.error("No cookies found there. Export the facebook.com cookies as JSON, or copy the browser's `cookie:` request header.");
|
|
2973
|
+
io.error("Cookie-Editor (Chrome, Edge, Firefox) \u2192 Export \u2192 JSON is the easiest way.");
|
|
2974
|
+
return 1;
|
|
2975
|
+
}
|
|
2976
|
+
const state = toFacebookStorageState(cookies);
|
|
2977
|
+
if (!state.cookies.some((cookie) => cookie.name === "c_user")) {
|
|
2978
|
+
io.error("Those cookies have no `c_user` value, so they are not a Facebook login.");
|
|
2979
|
+
io.error("Open facebook.com in your browser, make sure you are logged in, then export the facebook.com cookies.");
|
|
2980
|
+
return 1;
|
|
2981
|
+
}
|
|
2982
|
+
try {
|
|
2983
|
+
const accountId = await runtime.facebook.save(state);
|
|
2984
|
+
io.log(`Facebook session saved for account ${accountId}. Asking Facebook to confirm it...`);
|
|
2985
|
+
const condition = await validateImportedSession(runtime, state);
|
|
2986
|
+
if (condition !== "READY") {
|
|
2987
|
+
await runtime.facebook.clear();
|
|
2988
|
+
io.error(condition === "CHECKPOINT_REQUIRED" ? "Facebook asked for a security check on this server. Open facebook.com on your phone and approve the login, then import again." : "Facebook did not accept those cookies \u2014 they are usually expired. Log in again in your browser and export fresh ones.");
|
|
2989
|
+
return 1;
|
|
2990
|
+
}
|
|
2991
|
+
} catch (error) {
|
|
2992
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
2993
|
+
if (/Executable doesn't exist|playwright install/i.test(message)) {
|
|
2994
|
+
io.error(MISSING_BROWSER_HINT);
|
|
2995
|
+
} else if (message === "FACEBOOK_SESSION_INVALID") {
|
|
2996
|
+
io.error("Those cookies are incomplete: the export must include `c_user` and `xs`.");
|
|
2997
|
+
} else {
|
|
2998
|
+
io.error(`Could not use those cookies: ${message}`);
|
|
2999
|
+
}
|
|
3000
|
+
return 1;
|
|
3001
|
+
}
|
|
3002
|
+
io.log("Facebook session saved.");
|
|
3003
|
+
return 0;
|
|
3004
|
+
}
|
|
3005
|
+
async function validateImportedSession(runtime, state) {
|
|
3006
|
+
const browser = await runtime.browserType.launch({ headless: true });
|
|
3007
|
+
try {
|
|
3008
|
+
const context = await browser.newContext({ storageState: state });
|
|
3009
|
+
const page = await context.newPage();
|
|
3010
|
+
await page.goto("https://www.facebook.com/", { timeout: 45e3, waitUntil: "domcontentloaded" });
|
|
3011
|
+
return await detectFacebookSession(page, context);
|
|
3012
|
+
} finally {
|
|
3013
|
+
await browser.close();
|
|
3014
|
+
}
|
|
3015
|
+
}
|
|
3016
|
+
function readStdin() {
|
|
3017
|
+
return new Promise((resolve, reject) => {
|
|
3018
|
+
let value = "";
|
|
3019
|
+
process.stdin.setEncoding("utf8");
|
|
3020
|
+
process.stdin.on("data", (chunk) => {
|
|
3021
|
+
value += chunk;
|
|
3022
|
+
});
|
|
3023
|
+
process.stdin.on("end", () => resolve(value));
|
|
3024
|
+
process.stdin.on("error", reject);
|
|
3025
|
+
});
|
|
3026
|
+
}
|
|
2723
3027
|
function playwrightCliPath() {
|
|
2724
3028
|
try {
|
|
2725
3029
|
return fileURLToPath(new URL("cli.js", import.meta.resolve("playwright")));
|
|
@@ -2759,7 +3063,7 @@ async function commandInstallDisplay(io) {
|
|
|
2759
3063
|
return 0;
|
|
2760
3064
|
}
|
|
2761
3065
|
if (!existsSync2("/usr/bin/apt-get")) {
|
|
2762
|
-
io.error("Automatic installation supports Debian/Ubuntu. Install xvfb
|
|
3066
|
+
io.error("Automatic installation supports Debian/Ubuntu. Install xvfb and x11vnc with your package manager.");
|
|
2763
3067
|
return 1;
|
|
2764
3068
|
}
|
|
2765
3069
|
const sudo = typeof process.getuid === "function" && process.getuid() !== 0 ? ["sudo"] : [];
|
|
@@ -2768,9 +3072,9 @@ async function commandInstallDisplay(io) {
|
|
|
2768
3072
|
child.once("error", reject);
|
|
2769
3073
|
child.once("exit", (value) => resolve(value ?? 1));
|
|
2770
3074
|
});
|
|
2771
|
-
io.log("Installing xvfb
|
|
3075
|
+
io.log("Installing xvfb and x11vnc...");
|
|
2772
3076
|
await run([...sudo, "apt-get", "update"]).catch(() => 1);
|
|
2773
|
-
const code = await run([...sudo, "apt-get", "install", "-y", "xvfb", "x11vnc"
|
|
3077
|
+
const code = await run([...sudo, "apt-get", "install", "-y", "xvfb", "x11vnc"]);
|
|
2774
3078
|
if (code !== 0) {
|
|
2775
3079
|
io.error("Installation failed. Run the same command with apt manually.");
|
|
2776
3080
|
return code;
|
|
@@ -2792,7 +3096,7 @@ async function commandRun(runtime, io) {
|
|
|
2792
3096
|
agentVersion: agentVersion(),
|
|
2793
3097
|
clearTimer: (timer) => clearTimeout(timer),
|
|
2794
3098
|
createSocket: (url, accessToken) => {
|
|
2795
|
-
const socket = new
|
|
3099
|
+
const socket = new WebSocket2(url, {
|
|
2796
3100
|
headers: { authorization: `Bearer ${accessToken}` },
|
|
2797
3101
|
maxPayload: 16 * 1024
|
|
2798
3102
|
});
|