replicas-computer 0.3.26 → 0.3.28
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 +1 -1
- package/assets/desktop/replicas-symbol.svg +3 -0
- package/assets/desktop/start-services.sh +7 -2
- package/assets/desktop/themes/dark.env +1 -0
- package/assets/desktop/themes/light.env +1 -0
- package/assets/desktop/tint2rc +1 -17
- package/assets/desktop/topbar-tint2rc +33 -0
- package/dist/index.js +251 -57
- package/dist/viewer.js +36 -4
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -15,7 +15,7 @@ The versioned computer-use runtime for Replicas workspace images.
|
|
|
15
15
|
- `src/viewer.ts` owns the optional noVNC fallback.
|
|
16
16
|
- `assets/desktop/start-services.sh` owns the X11 and stream process lifecycle.
|
|
17
17
|
- `xsettingsd` broadcasts theme changes to running GTK apps and Chrome. Chrome follows the dashboard theme without reopening tabs; launch it without a `GTK_THEME` override.
|
|
18
|
-
- `assets/desktop/` owns the desktop theme: `wallpapers/` (a light and dark JPEG per scene; each new desktop picks a different scene), `tint2rc` dock, `openbox-theme/`, `gtk-settings.ini`, the `replicas-terminal` wrapper, `themes/` palettes that `start-services.sh` renders into those templates (`--theme light|dark` switches a running desktop), and the bundled fonts (static TTF instances of General Sans and Geist Mono; variable and WOFF2 builds render with wrong metrics in Pango and VTE).
|
|
18
|
+
- `assets/desktop/` owns the desktop theme: `wallpapers/` (a light and dark JPEG per scene; each new desktop picks a different scene), `tint2rc` dock, `topbar-tint2rc` top bar (the `replicas-symbol.svg` logo and the clock), `openbox-theme/`, `gtk-settings.ini`, the `replicas-terminal` wrapper, `themes/` palettes that `start-services.sh` renders into those templates (`--theme light|dark` switches a running desktop), and the bundled fonts (static TTF instances of General Sans and Geist Mono; variable and WOFF2 builds render with wrong metrics in Pango and VTE).
|
|
19
19
|
|
|
20
20
|
The viewer and agent command paths are independent. `start-services.sh` runs Xvfb, the compositor, PulseAudio and the stream server (and so the encoders it spawns) at nice −10 when `sudo` is available, so CPU load from the apps on the desktop slows those apps instead of stalling the picture of them. One on-demand encoder pair is shared by all viewers, stops when the last viewer disconnects, and restarts without reloading the page. A viewer out of sight (the dashboard keeps one connected while the Desktop tab is hidden) reports `standby`, detected with an `IntersectionObserver` because a hidden iframe keeps its last layout size; while every viewer is in standby the encoder runs at 5 fps on the 1.5 Mbps rung, and the first viewer to become visible returns it to full rate in place.
|
|
21
21
|
|
|
@@ -336,6 +336,8 @@ mkdir -p \
|
|
|
336
336
|
"$HOME/.icons/default"
|
|
337
337
|
|
|
338
338
|
[ -f "$REPLICAS_DESKTOP_TEMPLATES/tint2rc" ] && render_template "$REPLICAS_DESKTOP_TEMPLATES/tint2rc" "$HOME/.config/tint2/tint2rc"
|
|
339
|
+
[ -f "$REPLICAS_DESKTOP_TEMPLATES/topbar-tint2rc" ] && render_template "$REPLICAS_DESKTOP_TEMPLATES/topbar-tint2rc" "$HOME/.config/tint2/topbar-tint2rc"
|
|
340
|
+
[ -f "$REPLICAS_DESKTOP_TEMPLATES/replicas-symbol.svg" ] && render_template "$REPLICAS_DESKTOP_TEMPLATES/replicas-symbol.svg" "$HOME/.config/tint2/replicas-symbol.svg"
|
|
339
341
|
[ -f "$REPLICAS_DESKTOP_TEMPLATES/openbox-rc.xml" ] && cp "$REPLICAS_DESKTOP_TEMPLATES/openbox-rc.xml" "$HOME/.config/openbox/rc.xml"
|
|
340
342
|
[ -f "$REPLICAS_DESKTOP_TEMPLATES/picom.conf" ] && cp "$REPLICAS_DESKTOP_TEMPLATES/picom.conf" "$HOME/.config/picom/picom.conf"
|
|
341
343
|
[ -f "$REPLICAS_DESKTOP_TEMPLATES/gtk-settings.ini" ] && render_template "$REPLICAS_DESKTOP_TEMPLATES/gtk-settings.ini" "$HOME/.config/gtk-3.0/settings.ini"
|
|
@@ -395,14 +397,17 @@ paint_wallpaper
|
|
|
395
397
|
|
|
396
398
|
if [ -n "$REQUESTED_THEME" ] && [ "$THEME" != "$PREVIOUS_THEME" ]; then
|
|
397
399
|
DISPLAY=":$DISPLAY_NUM" openbox --reconfigure >/dev/null 2>&1 || true
|
|
398
|
-
kill_pids "$(cat "$STATE_DIR/tint2.pid" 2>/dev/null)"
|
|
399
|
-
rm -f "$STATE_DIR/tint2.pid"
|
|
400
|
+
kill_pids "$(cat "$STATE_DIR/tint2.pid" "$STATE_DIR/topbar.pid" 2>/dev/null)"
|
|
401
|
+
rm -f "$STATE_DIR/tint2.pid" "$STATE_DIR/topbar.pid"
|
|
400
402
|
fi
|
|
401
403
|
|
|
402
404
|
# tint2 panel anchors window stacking on bare openbox.
|
|
403
405
|
start_bg tint2 \
|
|
404
406
|
"false" \
|
|
405
407
|
"DISPLAY=:$DISPLAY_NUM exec tint2"
|
|
408
|
+
start_bg topbar \
|
|
409
|
+
"false" \
|
|
410
|
+
"DISPLAY=:$DISPLAY_NUM exec tint2 -c $HOME/.config/tint2/topbar-tint2rc"
|
|
406
411
|
|
|
407
412
|
if [ "$COMPOSITOR" = "1" ]; then
|
|
408
413
|
start_bg picom \
|
package/assets/desktop/tint2rc
CHANGED
|
@@ -47,7 +47,7 @@ border_width = 1
|
|
|
47
47
|
background_color = @SURFACE_ALT@ 96
|
|
48
48
|
border_color = @LINE@ 16
|
|
49
49
|
|
|
50
|
-
panel_items = LT
|
|
50
|
+
panel_items = LT
|
|
51
51
|
panel_size = 100% 72
|
|
52
52
|
panel_margin = 0 12
|
|
53
53
|
panel_padding = 10 7 6
|
|
@@ -103,22 +103,6 @@ task_active_icon_asb = 100 0 0
|
|
|
103
103
|
task_iconified_icon_asb = 55 0 0
|
|
104
104
|
urgent_nb_of_blink = 8
|
|
105
105
|
|
|
106
|
-
separator = new
|
|
107
|
-
separator_background_id = 0
|
|
108
|
-
separator_color = @LINE@ 18
|
|
109
|
-
separator_style = line
|
|
110
|
-
separator_size = 1
|
|
111
|
-
separator_padding = 6 14
|
|
112
|
-
|
|
113
|
-
time1_format = %H:%M
|
|
114
|
-
time2_format = %a %-d %b
|
|
115
|
-
time1_font = General Sans Semibold 12
|
|
116
|
-
time2_font = Geist Mono 8
|
|
117
|
-
clock_font_color = @INK@ 100
|
|
118
|
-
clock_padding = 12 0
|
|
119
|
-
clock_background_id = 0
|
|
120
|
-
clock_tooltip = %A, %B %-d
|
|
121
|
-
|
|
122
106
|
tooltip_padding = 10 6
|
|
123
107
|
tooltip_show_timeout = 0.4
|
|
124
108
|
tooltip_hide_timeout = 0.1
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
rounded = 0
|
|
2
|
+
border_width = 1
|
|
3
|
+
border_sides = B
|
|
4
|
+
background_color = @SURFACE@ 100
|
|
5
|
+
border_color = @LINE@ 10
|
|
6
|
+
|
|
7
|
+
panel_items = PFC
|
|
8
|
+
panel_size = 100% 36
|
|
9
|
+
panel_margin = 0 0
|
|
10
|
+
panel_padding = 0 0 0
|
|
11
|
+
panel_background_id = 1
|
|
12
|
+
panel_position = top center horizontal
|
|
13
|
+
panel_layer = top
|
|
14
|
+
panel_monitor = all
|
|
15
|
+
panel_window_name = tint2-topbar
|
|
16
|
+
disable_transparency = 1
|
|
17
|
+
strut_policy = follow_size
|
|
18
|
+
wm_menu = 0
|
|
19
|
+
font_shadow = 0
|
|
20
|
+
mouse_effects = 0
|
|
21
|
+
|
|
22
|
+
button = new
|
|
23
|
+
button_icon = ~/.config/tint2/replicas-symbol.svg
|
|
24
|
+
button_padding = 13 0 0
|
|
25
|
+
button_background_id = 0
|
|
26
|
+
button_centered = 0
|
|
27
|
+
button_max_icon_size = 24
|
|
28
|
+
|
|
29
|
+
time1_format = %a %-d %b %H:%M
|
|
30
|
+
time1_font = General Sans 11
|
|
31
|
+
clock_font_color = @INK@ 100
|
|
32
|
+
clock_padding = 16 0
|
|
33
|
+
clock_background_id = 0
|
package/dist/index.js
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
3
|
// src/client.ts
|
|
4
|
-
import { createConnection as
|
|
4
|
+
import { createConnection as createConnection3 } from "net";
|
|
5
5
|
|
|
6
6
|
// src/daemon.ts
|
|
7
7
|
import { spawnSync as spawnSync5 } from "child_process";
|
|
8
|
-
import { chmodSync, existsSync as
|
|
9
|
-
import { createConnection, createServer } from "net";
|
|
8
|
+
import { chmodSync, existsSync as existsSync6, mkdirSync as mkdirSync6, rmSync as rmSync5 } from "fs";
|
|
9
|
+
import { createConnection as createConnection2, createServer as createServer2 } from "net";
|
|
10
10
|
import { dirname as dirname5 } from "path";
|
|
11
11
|
|
|
12
12
|
// src/commands/desktop.ts
|
|
@@ -14,8 +14,23 @@ import { spawnSync } from "child_process";
|
|
|
14
14
|
import { existsSync, mkdirSync, readFileSync } from "fs";
|
|
15
15
|
import { dirname, isAbsolute, resolve } from "path";
|
|
16
16
|
|
|
17
|
+
// ../shared/src/type-guards.ts
|
|
18
|
+
function isRecord(value) {
|
|
19
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
20
|
+
}
|
|
21
|
+
|
|
17
22
|
// ../shared/src/engine/environment.ts
|
|
18
23
|
var DESKTOP_STREAM_PORT = 6080;
|
|
24
|
+
var DEVICES_DIR = ".ssh/replicas-devices";
|
|
25
|
+
var DEVICE_SCREEN_FILE_SUFFIX = ".screen.json";
|
|
26
|
+
function parseDeviceScreenState(value) {
|
|
27
|
+
if (!isRecord(value)) return null;
|
|
28
|
+
if (value.state === "connected" && typeof value.width === "number" && typeof value.height === "number") {
|
|
29
|
+
return { state: "connected", width: value.width, height: value.height };
|
|
30
|
+
}
|
|
31
|
+
if (value.state === "failed" && typeof value.reason === "string") return { state: "failed", reason: value.reason };
|
|
32
|
+
return value.state === "connecting" ? { state: "connecting" } : null;
|
|
33
|
+
}
|
|
19
34
|
function getDesktopViewerUrl(publicUrl) {
|
|
20
35
|
const url = new URL(`${publicUrl.replace(/\/$/, "")}/`);
|
|
21
36
|
url.searchParams.set("v", "15");
|
|
@@ -31,6 +46,7 @@ var STREAM_PORT = process.env.REPLICAS_DESKTOP_STREAM_PORT ? parseInt(process.en
|
|
|
31
46
|
var SERVICES_SCRIPT = process.env.REPLICAS_COMPUTER_SERVICES_SCRIPT || "/usr/local/bin/replicas-start-desktop-services";
|
|
32
47
|
var INPUT_LOCK_FILE = process.env.REPLICAS_DESKTOP_INPUT_LOCK_FILE || `${STATE_DIR}/input.lock`;
|
|
33
48
|
var CHROME_WRAPPER = "/usr/local/bin/replicas-chrome";
|
|
49
|
+
var NOVNC_VIEWER_ROOT = "/usr/local/share/replicas/desktop/novnc";
|
|
34
50
|
var TERMINAL_WRAPPER = "/usr/local/bin/replicas-terminal";
|
|
35
51
|
var INFO_WAIT_TIMEOUT_MS = 1e4;
|
|
36
52
|
var INFO_WAIT_INTERVAL_MS = 500;
|
|
@@ -58,7 +74,7 @@ function getDesktopBridgeStatus() {
|
|
|
58
74
|
}
|
|
59
75
|
function desktopStackHealthy() {
|
|
60
76
|
const pids = {};
|
|
61
|
-
for (const name of ["openbox", "tint2", "stream"]) {
|
|
77
|
+
for (const name of ["openbox", "tint2", "topbar", "stream"]) {
|
|
62
78
|
try {
|
|
63
79
|
const pid = Number.parseInt(readFileSync(`${STATE_DIR}/${name}.pid`, "utf8").trim(), 10);
|
|
64
80
|
if (!Number.isFinite(pid)) return false;
|
|
@@ -1111,11 +1127,6 @@ async function computerRecordStopCommand() {
|
|
|
1111
1127
|
clearRecordingState();
|
|
1112
1128
|
}
|
|
1113
1129
|
|
|
1114
|
-
// ../shared/src/type-guards.ts
|
|
1115
|
-
function isRecord(value) {
|
|
1116
|
-
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
1117
|
-
}
|
|
1118
|
-
|
|
1119
1130
|
// src/lib/chrome-session.ts
|
|
1120
1131
|
async function openChromeSession(webSocketDebuggerUrl) {
|
|
1121
1132
|
const socket = new WebSocket(webSocketDebuggerUrl);
|
|
@@ -2996,11 +3007,193 @@ async function computerDragCommand(fx, fy, tx, ty) {
|
|
|
2996
3007
|
console.log(`dragged (${fromX},${fromY}) -> (${toX},${toY})`);
|
|
2997
3008
|
}
|
|
2998
3009
|
|
|
2999
|
-
// src/commands/
|
|
3000
|
-
import {
|
|
3001
|
-
import { readFileSync as readFileSync5 } from "fs";
|
|
3010
|
+
// src/commands/screen.ts
|
|
3011
|
+
import { spawn as spawn3 } from "child_process";
|
|
3012
|
+
import { closeSync, existsSync as existsSync5, mkdirSync as mkdirSync5, openSync, readdirSync as readdirSync2, readFileSync as readFileSync5, rmSync as rmSync4, symlinkSync, writeFileSync as writeFileSync5 } from "fs";
|
|
3013
|
+
import { createConnection, createServer } from "net";
|
|
3002
3014
|
import { homedir } from "os";
|
|
3003
3015
|
import { join } from "path";
|
|
3016
|
+
import { setTimeout as sleep2 } from "timers/promises";
|
|
3017
|
+
var SCREEN_PORT = 5900;
|
|
3018
|
+
var CONNECT_TIMEOUT_MS = 3e4;
|
|
3019
|
+
var devicesDir = join(homedir(), DEVICES_DIR);
|
|
3020
|
+
function ownsProcess(session, pid) {
|
|
3021
|
+
try {
|
|
3022
|
+
return readFileSync5(`/proc/${pid}/cmdline`, "utf8").split("\0").some((arg) => arg.includes(`127.0.0.1:${session.tunnelPort}`));
|
|
3023
|
+
} catch {
|
|
3024
|
+
return false;
|
|
3025
|
+
}
|
|
3026
|
+
}
|
|
3027
|
+
function isListening(port2) {
|
|
3028
|
+
return new Promise((resolve5) => {
|
|
3029
|
+
const socket = createConnection({ host: "127.0.0.1", port: port2 });
|
|
3030
|
+
const done = (listening) => {
|
|
3031
|
+
socket.destroy();
|
|
3032
|
+
resolve5(listening);
|
|
3033
|
+
};
|
|
3034
|
+
socket.once("connect", () => done(true));
|
|
3035
|
+
socket.once("error", () => done(false));
|
|
3036
|
+
socket.setTimeout(1e3, () => done(false));
|
|
3037
|
+
});
|
|
3038
|
+
}
|
|
3039
|
+
async function freePorts(count) {
|
|
3040
|
+
const servers = await Promise.all(Array.from({ length: count }, () => new Promise((resolve5, reject) => {
|
|
3041
|
+
const server = createServer().once("error", reject);
|
|
3042
|
+
server.listen(0, "127.0.0.1", () => resolve5(server));
|
|
3043
|
+
})));
|
|
3044
|
+
const ports = servers.map((server) => {
|
|
3045
|
+
const address = server.address();
|
|
3046
|
+
return typeof address === "object" && address ? address.port : fail("Could not reserve a local port");
|
|
3047
|
+
});
|
|
3048
|
+
await Promise.all(servers.map((server) => new Promise((resolve5) => server.close(resolve5))));
|
|
3049
|
+
return ports;
|
|
3050
|
+
}
|
|
3051
|
+
function spawnLogged(bin, args, log) {
|
|
3052
|
+
const fd = openSync(log, "a");
|
|
3053
|
+
const child = spawn3(bin, args, { detached: true, stdio: ["ignore", fd, fd] });
|
|
3054
|
+
closeSync(fd);
|
|
3055
|
+
child.unref();
|
|
3056
|
+
let exited = false;
|
|
3057
|
+
child.once("exit", () => {
|
|
3058
|
+
exited = true;
|
|
3059
|
+
});
|
|
3060
|
+
return { pid: child.pid ?? fail(`Could not start ${bin}`), exited: () => exited };
|
|
3061
|
+
}
|
|
3062
|
+
async function waitForPort(port2, child) {
|
|
3063
|
+
const deadline = Date.now() + CONNECT_TIMEOUT_MS;
|
|
3064
|
+
while (Date.now() < deadline && !child.exited()) {
|
|
3065
|
+
if (await isListening(port2)) return true;
|
|
3066
|
+
await sleep2(200);
|
|
3067
|
+
}
|
|
3068
|
+
return false;
|
|
3069
|
+
}
|
|
3070
|
+
function readBanner(port2) {
|
|
3071
|
+
return new Promise((resolve5) => {
|
|
3072
|
+
const socket = createConnection({ host: "127.0.0.1", port: port2 });
|
|
3073
|
+
let banner = "";
|
|
3074
|
+
const done = () => {
|
|
3075
|
+
socket.destroy();
|
|
3076
|
+
resolve5(banner);
|
|
3077
|
+
};
|
|
3078
|
+
socket.setTimeout(1e4, done);
|
|
3079
|
+
socket.on("data", (chunk) => {
|
|
3080
|
+
banner += chunk.toString("latin1");
|
|
3081
|
+
if (banner.length >= 12) done();
|
|
3082
|
+
});
|
|
3083
|
+
socket.once("error", done);
|
|
3084
|
+
socket.once("close", done);
|
|
3085
|
+
});
|
|
3086
|
+
}
|
|
3087
|
+
function readSession(file) {
|
|
3088
|
+
try {
|
|
3089
|
+
const value = JSON.parse(readFileSync5(file, "utf8"));
|
|
3090
|
+
return isRecord(value) && typeof value.webPort === "number" && typeof value.tunnelPort === "number" && Array.isArray(value.pids) && value.pids.every((pid) => typeof pid === "number") ? { webPort: value.webPort, tunnelPort: value.tunnelPort, pids: value.pids } : null;
|
|
3091
|
+
} catch {
|
|
3092
|
+
return null;
|
|
3093
|
+
}
|
|
3094
|
+
}
|
|
3095
|
+
async function closeScreen(dir, session) {
|
|
3096
|
+
if (session) {
|
|
3097
|
+
const pages = await getChromePages().catch(() => []);
|
|
3098
|
+
for (const page of pages) {
|
|
3099
|
+
if (page.webSocketDebuggerUrl && page.url?.startsWith(`http://127.0.0.1:${session.webPort}/`)) {
|
|
3100
|
+
await sendChromeCommand(page.webSocketDebuggerUrl, "Page.close", {}).catch(() => void 0);
|
|
3101
|
+
}
|
|
3102
|
+
}
|
|
3103
|
+
for (const pid of session.pids.filter((candidate) => ownsProcess(session, candidate))) {
|
|
3104
|
+
try {
|
|
3105
|
+
process.kill(pid);
|
|
3106
|
+
} catch {
|
|
3107
|
+
}
|
|
3108
|
+
}
|
|
3109
|
+
}
|
|
3110
|
+
rmSync4(dir, { recursive: true, force: true });
|
|
3111
|
+
}
|
|
3112
|
+
async function openSession(device, dir, previous) {
|
|
3113
|
+
await closeScreen(dir, previous);
|
|
3114
|
+
const web = join(dir, "web");
|
|
3115
|
+
mkdirSync5(web, { recursive: true, mode: 448 });
|
|
3116
|
+
for (const entry of readdirSync2(NOVNC_VIEWER_ROOT)) symlinkSync(join(NOVNC_VIEWER_ROOT, entry), join(web, entry));
|
|
3117
|
+
const credentials = join(devicesDir, `${device}${DEVICE_SCREEN_FILE_SUFFIX}`);
|
|
3118
|
+
if (existsSync5(credentials)) symlinkSync(credentials, join(web, "credentials.json"));
|
|
3119
|
+
const [tunnelPort, webPort] = await freePorts(2);
|
|
3120
|
+
const tunnelLog = join(dir, "tunnel.log");
|
|
3121
|
+
const tunnel = spawnLogged("ssh", ["-N", "-o", "BatchMode=yes", "-o", "ExitOnForwardFailure=yes", "-L", `127.0.0.1:${tunnelPort}:localhost:${SCREEN_PORT}`, device], tunnelLog);
|
|
3122
|
+
const pids = [tunnel.pid];
|
|
3123
|
+
const abort = (message) => {
|
|
3124
|
+
for (const pid of pids) {
|
|
3125
|
+
try {
|
|
3126
|
+
process.kill(pid);
|
|
3127
|
+
} catch {
|
|
3128
|
+
}
|
|
3129
|
+
}
|
|
3130
|
+
return fail(message);
|
|
3131
|
+
};
|
|
3132
|
+
if (!await waitForPort(tunnelPort, tunnel)) abort(`Could not connect to ${device} over SSH: ${readFileSync5(tunnelLog, "utf8").trim() || "timed out"}`);
|
|
3133
|
+
if (!(await readBanner(tunnelPort)).startsWith("RFB ")) {
|
|
3134
|
+
abort(`${device} isn't sharing its screen: nothing answered on port ${SCREEN_PORT}. On a Mac, turn on Screen Sharing in System Settings \u2192 General \u2192 Sharing.`);
|
|
3135
|
+
}
|
|
3136
|
+
const bridgeLog = join(dir, "bridge.log");
|
|
3137
|
+
const bridge = spawnLogged("websockify", ["--web", web, `127.0.0.1:${webPort}`, `127.0.0.1:${tunnelPort}`], bridgeLog);
|
|
3138
|
+
pids.push(bridge.pid);
|
|
3139
|
+
if (!await waitForPort(webPort, bridge)) abort(`Could not start the screen viewer: ${readFileSync5(bridgeLog, "utf8").trim() || "timed out"}`);
|
|
3140
|
+
const session = { webPort, tunnelPort, pids };
|
|
3141
|
+
writeFileSync5(join(dir, "session.json"), JSON.stringify(session));
|
|
3142
|
+
return session;
|
|
3143
|
+
}
|
|
3144
|
+
async function computerScreenCommand(device, options) {
|
|
3145
|
+
if (!/^[a-z0-9-]+$/.test(device)) fail(`Invalid device name: ${device}`);
|
|
3146
|
+
const dir = join(STATE_DIR, "screens", device);
|
|
3147
|
+
const previous = readSession(join(dir, "session.json"));
|
|
3148
|
+
if (options.close) {
|
|
3149
|
+
await closeScreen(dir, previous);
|
|
3150
|
+
console.log(`Closed ${device}'s screen`);
|
|
3151
|
+
return;
|
|
3152
|
+
}
|
|
3153
|
+
if (!existsSync5(join(devicesDir, device))) {
|
|
3154
|
+
const names = existsSync5(devicesDir) ? readdirSync2(devicesDir).filter((name) => name !== "devices.conf" && !name.endsWith(DEVICE_SCREEN_FILE_SUFFIX)) : [];
|
|
3155
|
+
fail(names.length > 0 ? `No device named ${device}. Devices: ${names.join(", ")}.` : "This workspace has no devices. Add them in the environment's Devices settings.");
|
|
3156
|
+
}
|
|
3157
|
+
ensureServicesRunning();
|
|
3158
|
+
const session = previous && previous.pids.every((pid) => ownsProcess(previous, pid)) && await isListening(previous.webPort) ? previous : await openSession(device, dir, previous);
|
|
3159
|
+
try {
|
|
3160
|
+
await showScreen(device, session);
|
|
3161
|
+
} catch (error) {
|
|
3162
|
+
await closeScreen(dir, session);
|
|
3163
|
+
throw error;
|
|
3164
|
+
}
|
|
3165
|
+
}
|
|
3166
|
+
async function showScreen(device, session) {
|
|
3167
|
+
const origin = `http://127.0.0.1:${session.webPort}/`;
|
|
3168
|
+
const findPage = async () => (await getChromePages().catch(() => [])).find((page2) => page2.webSocketDebuggerUrl && page2.url?.startsWith(origin));
|
|
3169
|
+
let page = await findPage();
|
|
3170
|
+
if (!page) {
|
|
3171
|
+
spawn3(CHROME_WRAPPER, ["--new-window", `--app=${origin}?device=${device}&requestFps=0`], { env: withDisplay(), detached: true, stdio: "ignore" }).unref();
|
|
3172
|
+
const deadline2 = Date.now() + CONNECT_TIMEOUT_MS;
|
|
3173
|
+
while (!page && Date.now() < deadline2) {
|
|
3174
|
+
await sleep2(250);
|
|
3175
|
+
page = await findPage();
|
|
3176
|
+
}
|
|
3177
|
+
}
|
|
3178
|
+
const target = page?.webSocketDebuggerUrl ?? fail("Chrome did not open the screen viewer");
|
|
3179
|
+
let state = { state: "connecting" };
|
|
3180
|
+
const deadline = Date.now() + CONNECT_TIMEOUT_MS;
|
|
3181
|
+
while (state.state === "connecting" && Date.now() < deadline) {
|
|
3182
|
+
state = parseDeviceScreenState(await evaluateChromeTarget(target, "globalThis.__replicasScreen ?? null").catch(() => null)) ?? state;
|
|
3183
|
+
if (state.state === "connecting") await sleep2(250);
|
|
3184
|
+
}
|
|
3185
|
+
if (state.state !== "connected") {
|
|
3186
|
+
fail(state.state === "failed" ? state.reason : `${device}'s screen didn't connect within ${CONNECT_TIMEOUT_MS / 1e3} seconds.`);
|
|
3187
|
+
}
|
|
3188
|
+
await sendChromeCommand(target, "Page.bringToFront", {});
|
|
3189
|
+
console.log(`Showing ${device}'s screen (${state.width}\xD7${state.height}) in a window on this desktop. Drive it with \`replicas computer observe\`, \`click\`, \`type\` and \`key\`; \`replicas computer screen ${device} --close\` closes it.`);
|
|
3190
|
+
}
|
|
3191
|
+
|
|
3192
|
+
// src/commands/system.ts
|
|
3193
|
+
import { spawnSync as spawnSync4 } from "child_process";
|
|
3194
|
+
import { readFileSync as readFileSync6 } from "fs";
|
|
3195
|
+
import { homedir as homedir2 } from "os";
|
|
3196
|
+
import { join as join2 } from "path";
|
|
3004
3197
|
import chalk from "chalk";
|
|
3005
3198
|
function bridgeStatus(details, includeBacklog = false, rootsOnly = false) {
|
|
3006
3199
|
const pids = rootsOnly ? details.rootPids ?? [] : details.listenerPids ?? [];
|
|
@@ -3014,7 +3207,7 @@ function bridgeStatus(details, includeBacklog = false, rootsOnly = false) {
|
|
|
3014
3207
|
}
|
|
3015
3208
|
async function lookupDesktopViewerUrl() {
|
|
3016
3209
|
try {
|
|
3017
|
-
const value = JSON.parse(
|
|
3210
|
+
const value = JSON.parse(readFileSync6(join2(homedir2(), ".replicas", "preview-ports.json"), "utf8"));
|
|
3018
3211
|
if (!isRecord(value) || !Array.isArray(value.previews)) return null;
|
|
3019
3212
|
const preview = value.previews.find(
|
|
3020
3213
|
(item) => isRecord(item) && item.port === STREAM_PORT && typeof item.publicUrl === "string"
|
|
@@ -3050,7 +3243,8 @@ async function computerStatusCommand() {
|
|
|
3050
3243
|
["Xvfb", "Xvfb"],
|
|
3051
3244
|
["openbox", "openbox"],
|
|
3052
3245
|
["XSETTINGS", "xsettingsd"],
|
|
3053
|
-
["
|
|
3246
|
+
["dock", "^tint2$"],
|
|
3247
|
+
["top bar", "tint2 -c .*topbar"],
|
|
3054
3248
|
["PulseAudio", "pulseaudio"],
|
|
3055
3249
|
["desktop stream", "replicas-computer.*stream"]
|
|
3056
3250
|
];
|
|
@@ -3069,7 +3263,7 @@ async function computerStatusCommand() {
|
|
|
3069
3263
|
}
|
|
3070
3264
|
|
|
3071
3265
|
// src/version.ts
|
|
3072
|
-
var COMPUTER_USE_VERSION = "0.3.
|
|
3266
|
+
var COMPUTER_USE_VERSION = "0.3.28";
|
|
3073
3267
|
var COMPUTER_USE_PROTOCOL_VERSION = 1;
|
|
3074
3268
|
|
|
3075
3269
|
// src/program.ts
|
|
@@ -3107,6 +3301,7 @@ function createProgram() {
|
|
|
3107
3301
|
program.command("scroll <direction>").description("Scroll the desktop").option("--amount <n>", "Wheel ticks").option("--x <x>", "Pointer x").option("--y <y>", "Pointer y").action((direction, options) => computerScrollCommand(direction, options));
|
|
3108
3302
|
program.command("drag <fromX> <fromY> <toX> <toY>").description("Drag between desktop coordinates").action((fromX, fromY, toX, toY) => computerDragCommand(fromX, fromY, toX, toY));
|
|
3109
3303
|
program.command("launch <app> [args...]").description("Launch an application on the desktop").action((app, args) => computerLaunchCommand(app, args));
|
|
3304
|
+
program.command("screen <device>", { hidden: true }).description("Show a device's screen in a window on this desktop, where the other commands drive it").option("--close", "Close the window and its connection").action((device, options) => computerScreenCommand(device, options));
|
|
3110
3305
|
const record = program.command("record").description("Record the workspace display");
|
|
3111
3306
|
record.command("start <path>").description("Start a recording").option("--fps <n>", "Frame rate. Default 60.").action((path, options) => computerRecordStartCommand(path, options));
|
|
3112
3307
|
record.command("stop").description("Stop and finalize the active recording").action(() => computerRecordStopCommand());
|
|
@@ -3158,9 +3353,9 @@ function writeResult(socket, result) {
|
|
|
3158
3353
|
`);
|
|
3159
3354
|
}
|
|
3160
3355
|
async function hasSocketListener() {
|
|
3161
|
-
if (!
|
|
3356
|
+
if (!existsSync6(COMPUTER_USE_SOCKET_PATH)) return false;
|
|
3162
3357
|
return await new Promise((resolve5) => {
|
|
3163
|
-
const socket =
|
|
3358
|
+
const socket = createConnection2(COMPUTER_USE_SOCKET_PATH);
|
|
3164
3359
|
socket.setTimeout(500, () => socket.destroy());
|
|
3165
3360
|
socket.once("connect", () => {
|
|
3166
3361
|
socket.destroy();
|
|
@@ -3173,11 +3368,11 @@ async function hasSocketListener() {
|
|
|
3173
3368
|
async function runComputerDaemon() {
|
|
3174
3369
|
if (await hasSocketListener()) throw new Error(`Computer-use daemon is already listening on ${COMPUTER_USE_SOCKET_PATH}`);
|
|
3175
3370
|
ensureServicesRunning();
|
|
3176
|
-
|
|
3177
|
-
|
|
3371
|
+
mkdirSync6(dirname5(COMPUTER_USE_SOCKET_PATH), { recursive: true, mode: 448 });
|
|
3372
|
+
rmSync5(COMPUTER_USE_SOCKET_PATH, { force: true });
|
|
3178
3373
|
let queue = Promise.resolve();
|
|
3179
3374
|
const clients = /* @__PURE__ */ new Set();
|
|
3180
|
-
const server =
|
|
3375
|
+
const server = createServer2({ allowHalfOpen: true }, (socket) => {
|
|
3181
3376
|
if (clients.size >= MAX_CLIENTS) {
|
|
3182
3377
|
writeResult(socket, { exitCode: 75, stdout: "", stderr: "Computer-use daemon is busy; retry the request\n" });
|
|
3183
3378
|
return;
|
|
@@ -3250,7 +3445,7 @@ async function runComputerDaemon() {
|
|
|
3250
3445
|
process.once("SIGINT", close);
|
|
3251
3446
|
});
|
|
3252
3447
|
spawnSync5("bash", [SERVICES_SCRIPT, "--stop"], { stdio: "inherit" });
|
|
3253
|
-
|
|
3448
|
+
rmSync5(COMPUTER_USE_SOCKET_PATH, { force: true });
|
|
3254
3449
|
}
|
|
3255
3450
|
|
|
3256
3451
|
// src/client.ts
|
|
@@ -3259,7 +3454,7 @@ var ComputerDaemonUnavailableError = class extends Error {
|
|
|
3259
3454
|
};
|
|
3260
3455
|
function sendComputerRequest(argv) {
|
|
3261
3456
|
return new Promise((resolve5, reject) => {
|
|
3262
|
-
const socket =
|
|
3457
|
+
const socket = createConnection3(COMPUTER_USE_SOCKET_PATH);
|
|
3263
3458
|
let response = "";
|
|
3264
3459
|
let settled = false;
|
|
3265
3460
|
const finish = (error, result) => {
|
|
@@ -3310,17 +3505,17 @@ async function runComputerClient(argv) {
|
|
|
3310
3505
|
}
|
|
3311
3506
|
|
|
3312
3507
|
// src/media-server.ts
|
|
3313
|
-
import { spawn as
|
|
3508
|
+
import { spawn as spawn6 } from "child_process";
|
|
3314
3509
|
import { createHash as createHash3 } from "crypto";
|
|
3315
|
-
import { createReadStream, existsSync as
|
|
3316
|
-
import { createServer as
|
|
3510
|
+
import { createReadStream, existsSync as existsSync8, readdirSync as readdirSync3, readFileSync as readFileSync8, statSync as statSync2 } from "fs";
|
|
3511
|
+
import { createServer as createServer3 } from "http";
|
|
3317
3512
|
import { availableParallelism as availableParallelism2, loadavg } from "os";
|
|
3318
3513
|
import { dirname as dirname7, extname, resolve as resolve3 } from "path";
|
|
3319
3514
|
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
3320
3515
|
import WebSocket2, { WebSocketServer } from "ws";
|
|
3321
3516
|
|
|
3322
3517
|
// src/encoder.ts
|
|
3323
|
-
import { spawn as
|
|
3518
|
+
import { spawn as spawn4 } from "child_process";
|
|
3324
3519
|
import { createSocket } from "dgram";
|
|
3325
3520
|
import { createInterface } from "readline";
|
|
3326
3521
|
|
|
@@ -3555,7 +3750,7 @@ function superviseEncoder(options) {
|
|
|
3555
3750
|
const warmupFrames = active ? options.warmupFrames ?? 0 : 0;
|
|
3556
3751
|
const command = options.command?.() ?? "ffmpeg";
|
|
3557
3752
|
const args = options.args(socket.address().port, warmupFrames);
|
|
3558
|
-
const child =
|
|
3753
|
+
const child = spawn4(command, args, { env: options.env, stdio: [options.controllable?.(command) ? "pipe" : "ignore", options.onOutput ? "pipe" : "ignore", "pipe"] });
|
|
3559
3754
|
child.stdin?.on("error", () => {
|
|
3560
3755
|
});
|
|
3561
3756
|
child.stderr?.on("data", (chunk) => process.stderr.write(`[desktop-stream] ${chunk}`));
|
|
@@ -3657,7 +3852,7 @@ function superviseEncoder(options) {
|
|
|
3657
3852
|
}
|
|
3658
3853
|
|
|
3659
3854
|
// src/ice-servers.ts
|
|
3660
|
-
import { readFileSync as
|
|
3855
|
+
import { readFileSync as readFileSync7 } from "fs";
|
|
3661
3856
|
|
|
3662
3857
|
// ../shared/src/desktop-stream.ts
|
|
3663
3858
|
function isStreamIceServer(value) {
|
|
@@ -3802,7 +3997,7 @@ var refreshing;
|
|
|
3802
3997
|
async function currentIceServers() {
|
|
3803
3998
|
let env = process.env;
|
|
3804
3999
|
try {
|
|
3805
|
-
env = { ...env, ...parsePosixEnvFile(
|
|
4000
|
+
env = { ...env, ...parsePosixEnvFile(readFileSync7(runtimeEnvFile, "utf8")) };
|
|
3806
4001
|
} catch {
|
|
3807
4002
|
}
|
|
3808
4003
|
const monolithUrl = readReplicasRuntimeEnv((name) => env[name], REPLICAS_RUNTIME_ENV_ALIASES.monolithUrl);
|
|
@@ -3856,7 +4051,7 @@ function toIceServers(spec = "stun:stun.l.google.com:19302", relayServers) {
|
|
|
3856
4051
|
}
|
|
3857
4052
|
|
|
3858
4053
|
// src/video-encoder.ts
|
|
3859
|
-
import { existsSync as
|
|
4054
|
+
import { existsSync as existsSync7 } from "fs";
|
|
3860
4055
|
import { availableParallelism } from "os";
|
|
3861
4056
|
import { dirname as dirname6, resolve as resolve2 } from "path";
|
|
3862
4057
|
import { fileURLToPath } from "url";
|
|
@@ -4305,7 +4500,7 @@ var captureBinary = resolve2(dirname6(fileURLToPath(import.meta.url)), "..", "di
|
|
|
4305
4500
|
function superviseVideoEncoder(options) {
|
|
4306
4501
|
const { display: display2, width: width2, height: height2, colorMode: colorMode2 } = options;
|
|
4307
4502
|
const env = { ...process.env, DISPLAY: display2 };
|
|
4308
|
-
let nativeCapture = process.env.REPLICAS_DESKTOP_STREAM_CAPTURE !== "ffmpeg" &&
|
|
4503
|
+
let nativeCapture = process.env.REPLICAS_DESKTOP_STREAM_CAPTURE !== "ffmpeg" && existsSync7(captureBinary);
|
|
4309
4504
|
let captureMetrics;
|
|
4310
4505
|
const encoder = superviseEncoder({
|
|
4311
4506
|
label: "video",
|
|
@@ -4479,7 +4674,7 @@ function superviseVideoEncoder(options) {
|
|
|
4479
4674
|
}
|
|
4480
4675
|
|
|
4481
4676
|
// src/input-bridge.ts
|
|
4482
|
-
import { spawn as
|
|
4677
|
+
import { spawn as spawn5 } from "child_process";
|
|
4483
4678
|
var scrollPixelsPerStep = 80;
|
|
4484
4679
|
var namedKeys = {
|
|
4485
4680
|
AltLeft: "Alt_L",
|
|
@@ -4660,7 +4855,7 @@ var InputBridge = class {
|
|
|
4660
4855
|
}
|
|
4661
4856
|
write(command) {
|
|
4662
4857
|
if (!this.process || this.process.exitCode !== null || this.process.stdin.destroyed) {
|
|
4663
|
-
const child =
|
|
4858
|
+
const child = spawn5("xte", [], {
|
|
4664
4859
|
env: { ...process.env, DISPLAY: this.display },
|
|
4665
4860
|
stdio: ["pipe", "pipe", "pipe"]
|
|
4666
4861
|
});
|
|
@@ -4761,7 +4956,7 @@ function drainDesktopTheme() {
|
|
|
4761
4956
|
if (applied) appliedTheme = theme;
|
|
4762
4957
|
drainDesktopTheme();
|
|
4763
4958
|
};
|
|
4764
|
-
const child =
|
|
4959
|
+
const child = spawn6("bash", [SERVICES_SCRIPT, "--theme", theme], { detached: true, stdio: "ignore" });
|
|
4765
4960
|
child.unref();
|
|
4766
4961
|
child.once("exit", (code) => finish(code === 0));
|
|
4767
4962
|
child.once("error", () => finish(false));
|
|
@@ -4769,7 +4964,7 @@ function drainDesktopTheme() {
|
|
|
4769
4964
|
function processCpuMs(pid) {
|
|
4770
4965
|
if (!pid) return 0;
|
|
4771
4966
|
try {
|
|
4772
|
-
const stat =
|
|
4967
|
+
const stat = readFileSync8(`/proc/${pid}/stat`, "utf8");
|
|
4773
4968
|
const fields = stat.slice(stat.lastIndexOf(")") + 2).split(" ");
|
|
4774
4969
|
return (Number(fields[11]) + Number(fields[12])) * 10;
|
|
4775
4970
|
} catch {
|
|
@@ -5497,7 +5692,7 @@ async function runMediaServer() {
|
|
|
5497
5692
|
audioSampleRate: 48e3,
|
|
5498
5693
|
audioChannels: 2
|
|
5499
5694
|
};
|
|
5500
|
-
const httpServer =
|
|
5695
|
+
const httpServer = createServer3((request, response) => {
|
|
5501
5696
|
const url = new URL(request.url || "/", "http://localhost");
|
|
5502
5697
|
if (url.pathname === "/health" || url.pathname === "/telemetry") {
|
|
5503
5698
|
const all = [...viewers.values()];
|
|
@@ -5551,7 +5746,7 @@ async function runMediaServer() {
|
|
|
5551
5746
|
return;
|
|
5552
5747
|
}
|
|
5553
5748
|
if (url.pathname === "/snapshot") {
|
|
5554
|
-
const capture =
|
|
5749
|
+
const capture = spawn6("ffmpeg", [
|
|
5555
5750
|
"-hide_banner",
|
|
5556
5751
|
"-loglevel",
|
|
5557
5752
|
"error",
|
|
@@ -5588,7 +5783,7 @@ async function runMediaServer() {
|
|
|
5588
5783
|
}
|
|
5589
5784
|
const requested = url.pathname === "/" ? "stream-index.html" : url.pathname.slice(1);
|
|
5590
5785
|
const path = resolve3(webroot, requested);
|
|
5591
|
-
if (!path.startsWith(resolve3(webroot) + "/") || !
|
|
5786
|
+
if (!path.startsWith(resolve3(webroot) + "/") || !existsSync8(path) || !statSync2(path).isFile()) {
|
|
5592
5787
|
response.statusCode = 404;
|
|
5593
5788
|
response.end("Not found");
|
|
5594
5789
|
return;
|
|
@@ -5600,8 +5795,8 @@ async function runMediaServer() {
|
|
|
5600
5795
|
if (webrtc) void currentIceServers();
|
|
5601
5796
|
response.setHeader("cache-control", "no-store");
|
|
5602
5797
|
const digest = createHash3("sha1");
|
|
5603
|
-
for (const name of
|
|
5604
|
-
response.end(
|
|
5798
|
+
for (const name of readdirSync3(webroot).filter((file) => /\.(js|css)$/.test(file)).sort()) digest.update(readFileSync8(resolve3(webroot, name)));
|
|
5799
|
+
response.end(readFileSync8(path, "utf8").replaceAll("__VERSION__", `${COMPUTER_USE_VERSION}-${digest.digest("hex").slice(0, 8)}`));
|
|
5605
5800
|
return;
|
|
5606
5801
|
}
|
|
5607
5802
|
response.setHeader("cache-control", "private, max-age=31536000, immutable");
|
|
@@ -5713,7 +5908,7 @@ async function runMediaServer() {
|
|
|
5713
5908
|
|
|
5714
5909
|
// src/setup.ts
|
|
5715
5910
|
import { spawnSync as spawnSync6 } from "child_process";
|
|
5716
|
-
import { accessSync, chmodSync as chmodSync2, constants, cpSync, existsSync as
|
|
5911
|
+
import { accessSync, chmodSync as chmodSync2, constants, cpSync, existsSync as existsSync9, mkdirSync as mkdirSync7, readdirSync as readdirSync4, writeFileSync as writeFileSync6 } from "fs";
|
|
5717
5912
|
import { dirname as dirname8, resolve as resolve4 } from "path";
|
|
5718
5913
|
import { fileURLToPath as fileURLToPath3 } from "url";
|
|
5719
5914
|
var dependencies = [
|
|
@@ -5814,7 +6009,7 @@ async function setupComputer(options = {}) {
|
|
|
5814
6009
|
const noVncPackageRoot = dirname8(dirname8(fileURLToPath3(import.meta.resolve("@novnc/novnc"))));
|
|
5815
6010
|
const noVncCore = resolve4(noVncPackageRoot, "core");
|
|
5816
6011
|
const noVncVendor = resolve4(noVncPackageRoot, "vendor");
|
|
5817
|
-
if (!
|
|
6012
|
+
if (!existsSync9(noVncCore) || !existsSync9(noVncVendor)) {
|
|
5818
6013
|
missing.push("bundled noVNC client");
|
|
5819
6014
|
console.error("\u2717 bundled noVNC client");
|
|
5820
6015
|
} else {
|
|
@@ -5831,23 +6026,22 @@ async function setupComputer(options = {}) {
|
|
|
5831
6026
|
if (process.getuid?.() !== 0) throw new Error("replicas-computer setup must run as root to install runtime files");
|
|
5832
6027
|
const packageRoot2 = resolve4(dirname8(fileURLToPath3(import.meta.url)), "..");
|
|
5833
6028
|
const assets = resolve4(packageRoot2, "assets", "desktop");
|
|
5834
|
-
if (!
|
|
5835
|
-
|
|
6029
|
+
if (!existsSync9(assets)) throw new Error(`Computer-use assets are missing from ${assets}`);
|
|
6030
|
+
mkdirSync7("/usr/local/share/replicas/desktop", { recursive: true });
|
|
5836
6031
|
cpSync(assets, "/usr/local/share/replicas/desktop", { recursive: true, force: true });
|
|
5837
6032
|
const fontRoot = "/usr/local/share/fonts/replicas";
|
|
5838
|
-
|
|
5839
|
-
for (const file of
|
|
6033
|
+
mkdirSync7(fontRoot, { recursive: true });
|
|
6034
|
+
for (const file of readdirSync4(assets).filter((name) => name.endsWith(".ttf"))) cpSync(resolve4(assets, file), resolve4(fontRoot, file), { force: true });
|
|
5840
6035
|
spawnSync6("fc-cache", ["-f", fontRoot], { stdio: "ignore" });
|
|
5841
|
-
|
|
5842
|
-
|
|
5843
|
-
cpSync(
|
|
5844
|
-
cpSync(noVncVendor, resolve4(viewerRoot, "vendor"), { recursive: true, force: true });
|
|
6036
|
+
mkdirSync7(NOVNC_VIEWER_ROOT, { recursive: true });
|
|
6037
|
+
cpSync(noVncCore, resolve4(NOVNC_VIEWER_ROOT, "core"), { recursive: true, force: true });
|
|
6038
|
+
cpSync(noVncVendor, resolve4(NOVNC_VIEWER_ROOT, "vendor"), { recursive: true, force: true });
|
|
5845
6039
|
for (const file of ["novnc-index.html", "viewer.css", "viewer-theme.css", "viewer-theme.js", "viewer-icon.svg", "geist-mono-400.ttf", "geist-mono-700.ttf"]) {
|
|
5846
|
-
cpSync(resolve4(assets, file), resolve4(
|
|
6040
|
+
cpSync(resolve4(assets, file), resolve4(NOVNC_VIEWER_ROOT, file === "novnc-index.html" ? "index.html" : file), { force: true });
|
|
5847
6041
|
}
|
|
5848
|
-
cpSync(resolve4(packageRoot2, "dist", "viewer.js"), resolve4(
|
|
6042
|
+
cpSync(resolve4(packageRoot2, "dist", "viewer.js"), resolve4(NOVNC_VIEWER_ROOT, "viewer.js"), { force: true });
|
|
5849
6043
|
const streamRoot = "/usr/local/share/replicas/desktop/stream";
|
|
5850
|
-
|
|
6044
|
+
mkdirSync7(streamRoot, { recursive: true });
|
|
5851
6045
|
for (const file of ["stream-index.html", "stream-viewer.css", "viewer-theme.css", "viewer-theme.js", "viewer-icon.svg", "geist-mono-400.ttf", "geist-mono-700.ttf"]) {
|
|
5852
6046
|
cpSync(resolve4(assets, file), resolve4(streamRoot, file), { force: true });
|
|
5853
6047
|
}
|
|
@@ -5859,13 +6053,13 @@ async function setupComputer(options = {}) {
|
|
|
5859
6053
|
chmodSync2("/usr/local/bin/replicas-start-desktop-services", 493);
|
|
5860
6054
|
chmodSync2("/usr/local/bin/replicas-chrome", 493);
|
|
5861
6055
|
chmodSync2("/usr/local/bin/replicas-terminal", 493);
|
|
5862
|
-
|
|
5863
|
-
if (
|
|
6056
|
+
writeFileSync6("/etc/systemd/system/replicas-computer.service", serviceUnit, { mode: 420 });
|
|
6057
|
+
if (existsSync9("/run/systemd/system")) {
|
|
5864
6058
|
const reload = spawnSync6("systemctl", ["daemon-reload"], { stdio: "pipe" });
|
|
5865
6059
|
if (reload.status !== 0) throw new Error(`Failed to reload systemd: ${reload.stderr?.toString().trim() || `exit ${reload.status}`}`);
|
|
5866
6060
|
}
|
|
5867
|
-
|
|
5868
|
-
|
|
6061
|
+
mkdirSync7("/usr/local/share/replicas-computer", { recursive: true });
|
|
6062
|
+
writeFileSync6("/usr/local/share/replicas-computer/install.json", `${JSON.stringify({
|
|
5869
6063
|
version: COMPUTER_USE_VERSION,
|
|
5870
6064
|
protocolVersion: COMPUTER_USE_PROTOCOL_VERSION,
|
|
5871
6065
|
dependencies: found
|
package/dist/viewer.js
CHANGED
|
@@ -2,6 +2,11 @@
|
|
|
2
2
|
import RFB from "./core/rfb.js";
|
|
3
3
|
import { encodings } from "./core/encodings.js";
|
|
4
4
|
|
|
5
|
+
// ../shared/src/type-guards.ts
|
|
6
|
+
function isRecord(value) {
|
|
7
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
8
|
+
}
|
|
9
|
+
|
|
5
10
|
// src/wheel.ts
|
|
6
11
|
function wheelDeltaPixels(event, pageHeight) {
|
|
7
12
|
const scale = event.deltaMode === WheelEvent.DOM_DELTA_LINE ? 32 : event.deltaMode === WheelEvent.DOM_DELTA_PAGE ? pageHeight : 1;
|
|
@@ -15,6 +20,17 @@ var statusText = document.querySelector("#status-text");
|
|
|
15
20
|
var hint = document.querySelector("#hint");
|
|
16
21
|
if (!screen || !status || !statusText || !hint) throw new Error("Desktop viewer markup is incomplete");
|
|
17
22
|
var params = new URLSearchParams(location.search);
|
|
23
|
+
var device = params.get("device");
|
|
24
|
+
var credentials = device ? await fetch("./credentials.json").then((response) => response.ok ? response.json() : void 0, () => void 0) : void 0;
|
|
25
|
+
var screenFailed = false;
|
|
26
|
+
var reportScreen = (state) => {
|
|
27
|
+
screenFailed = state.state === "failed";
|
|
28
|
+
globalThis.__replicasScreen = state;
|
|
29
|
+
};
|
|
30
|
+
if (device) {
|
|
31
|
+
document.title = `${device} screen`;
|
|
32
|
+
reportScreen({ state: "connecting" });
|
|
33
|
+
}
|
|
18
34
|
var intParam = (name, fallback, min = 0, max = 9) => {
|
|
19
35
|
const value = Number.parseInt(params.get(name) ?? "", 10);
|
|
20
36
|
return Number.isInteger(value) && value >= min && value <= max ? value : fallback;
|
|
@@ -93,7 +109,7 @@ var configureEncodings = (rfb) => {
|
|
|
93
109
|
};
|
|
94
110
|
};
|
|
95
111
|
var scheduleReconnect = (immediate = false) => {
|
|
96
|
-
if (reconnectTimer || document.visibilityState === "hidden" || !navigator.onLine) return;
|
|
112
|
+
if (screenFailed || reconnectTimer || document.visibilityState === "hidden" || !navigator.onLine) return;
|
|
97
113
|
const delay = immediate ? 0 : Math.min(1e4, 250 * 2 ** Math.min(reconnectAttempt, 6)) * (0.8 + Math.random() * 0.4);
|
|
98
114
|
reconnectAttempt += 1;
|
|
99
115
|
showStatus(reconnectAttempt > 1 ? "reconnecting\u2026" : "connecting\u2026");
|
|
@@ -113,7 +129,7 @@ var connect = () => {
|
|
|
113
129
|
showStatus(reconnectAttempt ? "reconnecting\u2026" : "connecting\u2026");
|
|
114
130
|
let rfb;
|
|
115
131
|
try {
|
|
116
|
-
rfb = new RFB(screen, wsUrl);
|
|
132
|
+
rfb = new RFB(screen, wsUrl, { credentials });
|
|
117
133
|
} catch {
|
|
118
134
|
scheduleReconnect();
|
|
119
135
|
return;
|
|
@@ -132,6 +148,7 @@ var connect = () => {
|
|
|
132
148
|
if (generation !== connectionGeneration) return;
|
|
133
149
|
reconnectAttempt = 0;
|
|
134
150
|
status.classList.remove("visible");
|
|
151
|
+
if (device) reportScreen({ state: "connected", width: rfb._fbWidth, height: rfb._fbHeight });
|
|
135
152
|
applyModernCursor();
|
|
136
153
|
startUpdatePump();
|
|
137
154
|
});
|
|
@@ -139,14 +156,29 @@ var connect = () => {
|
|
|
139
156
|
if (generation !== connectionGeneration) return;
|
|
140
157
|
activeRfb = null;
|
|
141
158
|
stopUpdatePump();
|
|
159
|
+
if (device && !screenFailed) reportScreen({ state: "connecting" });
|
|
142
160
|
scheduleReconnect();
|
|
143
161
|
});
|
|
144
|
-
rfb.addEventListener("securityfailure", () => {
|
|
162
|
+
rfb.addEventListener("securityfailure", (event) => {
|
|
145
163
|
if (generation !== connectionGeneration) return;
|
|
146
164
|
activeRfb = null;
|
|
147
165
|
stopUpdatePump();
|
|
148
|
-
|
|
166
|
+
if (!device) {
|
|
167
|
+
scheduleReconnect();
|
|
168
|
+
return;
|
|
169
|
+
}
|
|
170
|
+
const reason = event instanceof CustomEvent && isRecord(event.detail) && typeof event.detail.reason === "string" ? event.detail.reason : void 0;
|
|
171
|
+
showStatus("the device rejected the screen password");
|
|
172
|
+
reportScreen({ state: "failed", reason: `${device} rejected the screen password${reason ? ` (${reason})` : ""}. Update it in the environment's Devices settings.` });
|
|
149
173
|
});
|
|
174
|
+
if (device) {
|
|
175
|
+
rfb.addEventListener("credentialsrequired", () => {
|
|
176
|
+
if (generation !== connectionGeneration) return;
|
|
177
|
+
reportScreen({ state: "failed", reason: `${device} asks for a password. Add its screen password in the environment's Devices settings.` });
|
|
178
|
+
showStatus("the device asks for a screen password");
|
|
179
|
+
rfb.disconnect();
|
|
180
|
+
});
|
|
181
|
+
}
|
|
150
182
|
};
|
|
151
183
|
var shiftCanvasForWheel = (event) => {
|
|
152
184
|
if (!optimisticScroll || event.ctrlKey || event.metaKey || Math.abs(event.deltaY) <= Math.abs(event.deltaX)) return;
|