UNPKG

@bigppwong/desktop

Version:

E2B Desktop Sandbox - isolated cloud environment with a desktop-like interface powered by E2B. Ready for AI Computer Use

665 lines (661 loc) 20 kB
"use strict"; var __defProp = Object.defineProperty; var __getOwnPropDesc = Object.getOwnPropertyDescriptor; var __getOwnPropNames = Object.getOwnPropertyNames; var __getOwnPropSymbols = Object.getOwnPropertySymbols; var __hasOwnProp = Object.prototype.hasOwnProperty; var __propIsEnum = Object.prototype.propertyIsEnumerable; var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value; var __spreadValues = (a, b) => { for (var prop in b || (b = {})) if (__hasOwnProp.call(b, prop)) __defNormalProp(a, prop, b[prop]); if (__getOwnPropSymbols) for (var prop of __getOwnPropSymbols(b)) { if (__propIsEnum.call(b, prop)) __defNormalProp(a, prop, b[prop]); } return a; }; var __export = (target, all) => { for (var name in all) __defProp(target, name, { get: all[name], enumerable: true }); }; var __copyProps = (to, from, except, desc) => { if (from && typeof from === "object" || typeof from === "function") { for (let key of __getOwnPropNames(from)) if (!__hasOwnProp.call(to, key) && key !== except) __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable }); } return to; }; var __reExport = (target, mod, secondTarget) => (__copyProps(target, mod, "default"), secondTarget && __copyProps(secondTarget, mod, "default")); var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod); var __async = (__this, __arguments, generator) => { return new Promise((resolve, reject) => { var fulfilled = (value) => { try { step(generator.next(value)); } catch (e) { reject(e); } }; var rejected = (value) => { try { step(generator.throw(value)); } catch (e) { reject(e); } }; var step = (x) => x.done ? resolve(x.value) : Promise.resolve(x.value).then(fulfilled, rejected); step((generator = generator.apply(__this, __arguments)).next()); }); }; // src/index.ts var src_exports = {}; __export(src_exports, { Sandbox: () => Sandbox }); module.exports = __toCommonJS(src_exports); __reExport(src_exports, require("@bigppwong/e2b"), module.exports); // src/sandbox.ts var import_e2b = require("@bigppwong/e2b"); // src/utils.ts var import_crypto = require("crypto"); function generateRandomString(length = 16) { const characters = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"; const bytes = (0, import_crypto.randomBytes)(length); let result = ""; for (let i = 0; i < length; i++) { result += characters[bytes[i] % characters.length]; } return result; } // src/sandbox.ts var MOUSE_BUTTONS = { left: 1, right: 3, middle: 2 }; var KEYS = { alt: "Alt_L", alt_left: "Alt_L", alt_right: "Alt_R", backspace: "BackSpace", break: "Pause", caps_lock: "Caps_Lock", cmd: "Super_L", command: "Super_L", control: "Control_L", control_left: "Control_L", control_right: "Control_R", ctrl: "Control_L", del: "Delete", delete: "Delete", down: "Down", end: "End", enter: "Return", esc: "Escape", escape: "Escape", f1: "F1", f2: "F2", f3: "F3", f4: "F4", f5: "F5", f6: "F6", f7: "F7", f8: "F8", f9: "F9", f10: "F10", f11: "F11", f12: "F12", home: "Home", insert: "Insert", left: "Left", menu: "Menu", meta: "Meta_L", num_lock: "Num_Lock", page_down: "Page_Down", page_up: "Page_Up", pause: "Pause", print: "Print", right: "Right", scroll_lock: "Scroll_Lock", shift: "Shift_L", shift_left: "Shift_L", shift_right: "Shift_R", space: "space", super: "Super_L", super_left: "Super_L", super_right: "Super_R", tab: "Tab", up: "Up", win: "Super_L", windows: "Super_L" }; function mapKey(key) { const lowerKey = key.toLowerCase(); if (lowerKey in KEYS) { return KEYS[lowerKey]; } return lowerKey; } var Sandbox = class extends import_e2b.Sandbox { /** * Use {@link Sandbox.create} to create a new Sandbox instead. * * @hidden * @hide * @internal * @access protected */ constructor(opts) { super(opts); this.lastXfce4Pid = null; this.display = opts.display || ":0"; this.lastXfce4Pid = null; this.stream = new VNCServer(this); } static create(templateOrOpts, opts) { return __async(this, null, function* () { var _a, _b, _c; const { template, sandboxOpts } = typeof templateOrOpts === "string" ? { template: templateOrOpts, sandboxOpts: opts } : { template: this.defaultTemplate, sandboxOpts: templateOrOpts }; const config = new import_e2b.ConnectionConfig(sandboxOpts); let sbx; if (config.debug) { sbx = new this(__spreadValues(__spreadValues({ sandboxId: "desktop" }, sandboxOpts), config)); } else { const sandbox = yield this.createSandbox( template, (_a = sandboxOpts == null ? void 0 : sandboxOpts.timeoutMs) != null ? _a : this.defaultSandboxTimeoutMs, sandboxOpts ); sbx = new this(__spreadValues(__spreadValues(__spreadValues({}, sandbox), sandboxOpts), config)); } const [width, height] = (_b = sandboxOpts == null ? void 0 : sandboxOpts.resolution) != null ? _b : [1024, 768]; yield sbx.commands.run( `Xvfb ${sbx.display} -ac -screen 0 ${width}x${height}x24 -retro -dpi ${(_c = sandboxOpts == null ? void 0 : sandboxOpts.dpi) != null ? _c : 96} -nolisten tcp -nolisten unix`, { background: true, timeoutMs: 0 } ); let hasStarted = yield sbx.waitAndVerify( `xdpyinfo -display ${sbx.display}`, (r) => r.exitCode === 0 ); if (!hasStarted) { throw new import_e2b.TimeoutError("Could not start Xvfb"); } yield sbx.startXfce4(); return sbx; }); } /** * Wait for a command to return a specific result. * @param cmd - The command to run. * @param onResult - The function to check the result of the command. * @param timeout - The maximum time to wait for the command to return the result. * @param interval - The interval to wait between checks. * @returns `true` if the command returned the result within the timeout, otherwise `false`. */ waitAndVerify(cmd, onResult, timeout = 10, interval = 0.5) { return __async(this, null, function* () { let elapsed = 0; while (elapsed < timeout) { try { if (onResult(yield this.commands.run(cmd))) { return true; } } catch (e) { if (e instanceof import_e2b.CommandExitError) { continue; } throw e; } yield new Promise((resolve) => setTimeout(resolve, interval * 1e3)); elapsed += interval; } return false; }); } /** * Start xfce4 session if logged out or not running. */ startXfce4() { return __async(this, null, function* () { if (this.lastXfce4Pid === null || (yield this.commands.run( `ps aux | grep ${this.lastXfce4Pid} | grep -v grep | head -n 1` )).stdout.trim().includes("[xfce4-session] <defunct>")) { const result = yield this.commands.run("startxfce4", { envs: { DISPLAY: this.display }, background: true, timeoutMs: 0 }); this.lastXfce4Pid = result.pid; } }); } screenshot(format = "bytes") { return __async(this, null, function* () { const path = `/tmp/screenshot-${generateRandomString()}.png`; yield this.commands.run(`scrot --pointer ${path}`, { envs: { DISPLAY: this.display } }); const file = yield this.files.read(path, { format }); this.files.remove(path); return file; }); } /** * Left click on the mouse position. */ leftClick(x, y) { return __async(this, null, function* () { if (x && y) { yield this.moveMouse(x, y); } yield this.commands.run("xdotool click 1", { envs: { DISPLAY: this.display } }); }); } /** * Double left click on the mouse position. */ doubleClick(x, y) { return __async(this, null, function* () { if (x && y) { yield this.moveMouse(x, y); } yield this.commands.run("xdotool click --repeat 2 1", { envs: { DISPLAY: this.display } }); }); } /** * Right click on the mouse position. */ rightClick(x, y) { return __async(this, null, function* () { if (x && y) { yield this.moveMouse(x, y); } yield this.commands.run("xdotool click 3", { envs: { DISPLAY: this.display } }); }); } /** * Middle click on the mouse position. */ middleClick(x, y) { return __async(this, null, function* () { if (x && y) { yield this.moveMouse(x, y); } yield this.commands.run("xdotool click 2", { envs: { DISPLAY: this.display } }); }); } /** * Scroll the mouse wheel by the given amount. * @param direction - The direction to scroll. Can be "up" or "down". * @param amount - The amount to scroll. */ scroll(direction = "down", amount = 1) { return __async(this, null, function* () { const button = direction === "up" ? "4" : "5"; yield this.commands.run(`xdotool click --repeat ${amount} ${button}`, { envs: { DISPLAY: this.display } }); }); } /** * Move the mouse to the given coordinates. * @param x - The x coordinate. * @param y - The y coordinate. */ moveMouse(x, y) { return __async(this, null, function* () { yield this.commands.run(`xdotool mousemove --sync ${x} ${y}`, { envs: { DISPLAY: this.display } }); }); } /** * Press the mouse button. */ mousePress(button = "left") { return __async(this, null, function* () { yield this.commands.run(`xdotool mousedown ${MOUSE_BUTTONS[button]}`, { envs: { DISPLAY: this.display } }); }); } /** * Release the mouse button. */ mouseRelease(button = "left") { return __async(this, null, function* () { yield this.commands.run(`xdotool mouseup ${MOUSE_BUTTONS[button]}`, { envs: { DISPLAY: this.display } }); }); } /** * Get the current cursor position. * @returns A object with the x and y coordinates * @throws Error if cursor position cannot be determined */ getCursorPosition() { return __async(this, null, function* () { const result = yield this.commands.run("xdotool getmouselocation", { envs: { DISPLAY: this.display } }); const match = result.stdout.match(/x:(\d+)\s+y:(\d+)/); if (!match) { throw new Error( `Failed to parse cursor position from output: ${result.stdout}` ); } const [, x, y] = match; if (!x || !y) { throw new Error(`Invalid cursor position values: x=${x}, y=${y}`); } return { x: parseInt(x), y: parseInt(y) }; }); } /** * Get the current screen size. * @returns An {@link ScreenSize} object * @throws Error if screen size cannot be determined */ getScreenSize() { return __async(this, null, function* () { const result = yield this.commands.run("xrandr", { envs: { DISPLAY: this.display } }); const match = result.stdout.match(/(\d+x\d+)/); if (!match) { throw new Error( `Failed to parse screen size from output: ${result.stdout}` ); } try { const [width, height] = match[1].split("x").map((val) => parseInt(val)); return { width, height }; } catch (error) { throw new Error(`Invalid screen size format: ${match[1]}`); } }); } *breakIntoChunks(text, n) { for (let i = 0; i < text.length; i += n) { yield text.slice(i, i + n); } } quoteString(s) { if (!s) { return "''"; } if (!/[^\w@%+=:,./-]/.test(s)) { return s; } return "'" + s.replace(/'/g, `'"'"'`) + "'"; } /** * Write the given text at the current cursor position. * @param text - The text to write. * @param options - An object containing the chunk size and delay between each chunk of text. * @param options.chunkSize - The size of each chunk of text to write. Default is 25 characters. * @param options.delayInMs - The delay between each chunk of text. Default is 75 ms. */ write(_0) { return __async(this, arguments, function* (text, options = { chunkSize: 25, delayInMs: 75 }) { const chunks = this.breakIntoChunks(text, options.chunkSize); for (const chunk of chunks) { yield this.commands.run( `xdotool type --delay ${options.delayInMs} ${this.quoteString(chunk)}`, { envs: { DISPLAY: this.display } } ); } }); } /** * Press a key. * @param key - The key to press (e.g. "enter", "space", "backspace", etc.). Can be a single key or an array of keys. */ press(key) { return __async(this, null, function* () { if (Array.isArray(key)) { key = key.map(mapKey).join("+"); } else { key = mapKey(key); } yield this.commands.run(`xdotool key ${key}`, { envs: { DISPLAY: this.display } }); }); } /** * Drag the mouse from the given position to the given position. * @param from - The starting position. * @param to - The ending position. */ drag(_0, _1) { return __async(this, arguments, function* ([x1, y1], [x2, y2]) { yield this.moveMouse(x1, y1); yield this.mousePress(); yield this.moveMouse(x2, y2); yield this.mouseRelease(); }); } /** * Wait for the given amount of time. * @param ms - The amount of time to wait in milliseconds. */ wait(ms) { return __async(this, null, function* () { yield this.commands.run(`sleep ${ms / 1e3}`, { envs: { DISPLAY: this.display } }); }); } /** * Open a file or a URL in the default application. * @param fileOrUrl - The file or URL to open. */ open(fileOrUrl) { return __async(this, null, function* () { yield this.commands.run(`xdg-open ${fileOrUrl}`, { background: true, envs: { DISPLAY: this.display } }); }); } /** * Get the current window ID. * @returns The ID of the current window. */ getCurrentWindowId() { return __async(this, null, function* () { const result = yield this.commands.run("xdotool getwindowfocus", { envs: { DISPLAY: this.display } }); return result.stdout.trim(); }); } /** * Get the window ID of the window with the given title. * @param title - The title of the window. * @returns The ID of the window. */ getApplicationWindows(application) { return __async(this, null, function* () { const result = yield this.commands.run( `xdotool search --onlyvisible --class ${application}`, { envs: { DISPLAY: this.display } } ); return result.stdout.trim().split("\n"); }); } /** * Get the title of the window with the given ID. * @param windowId - The ID of the window. * @returns The title of the window. */ getWindowTitle(windowId) { return __async(this, null, function* () { const result = yield this.commands.run( `xdotool getwindowname ${windowId}`, { envs: { DISPLAY: this.display } } ); return result.stdout.trim(); }); } }; Sandbox.defaultTemplate = "desktop"; var VNCServer = class { constructor(desktop) { this.vncPort = 5900; this.port = 6080; this.novncAuthEnabled = false; this.url = null; this.novncHandle = null; this.desktop = desktop; this.novncCommand = `cd /opt/noVNC/utils && ./novnc_proxy --vnc localhost:${this.vncPort} --listen ${this.port} --web /opt/noVNC > /tmp/novnc.log 2>&1`; } getAuthKey() { if (!this.password) { throw new Error( "Unable to retrieve stream auth key, check if requireAuth is enabled" ); } return this.password; } /** * Set the VNC command to start the VNC server. */ getVNCCommand(windowId) { return __async(this, null, function* () { let pwdFlag = "-nopw"; if (this.novncAuthEnabled) { yield this.desktop.commands.run("mkdir ~/.vnc"); yield this.desktop.commands.run( `x11vnc -storepasswd ${this.password} ~/.vnc/passwd` ); pwdFlag = "-usepw"; } return `x11vnc -bg -display ${this.desktop.display} -forever -wait 50 -shared -rfbport ${this.vncPort} ${pwdFlag} 2>/tmp/x11vnc_stderr.log` + (windowId ? ` -id ${windowId}` : ""); }); } waitForPort(port) { return __async(this, null, function* () { return yield this.desktop.waitAndVerify( `netstat -tuln | grep ":${port} "`, (r) => r.stdout.trim() !== "" ); }); } /** * Check if the VNC server is running. * @returns Whether the VNC server is running. */ checkVNCRunning() { return __async(this, null, function* () { try { const result = yield this.desktop.commands.run("pgrep -x x11vnc"); return result.stdout.trim() !== ""; } catch (error) { return false; } }); } /** * Get the URL to a web page with a stream of the desktop sandbox. * @param autoConnect - Whether to automatically connect to the server after opening the URL. * @param viewOnly - Whether to prevent user interaction through the client. * @param resize - Whether to resize the view when the window resizes. * @param authKey - The password to use to connect to the server. * @returns The URL to connect to the VNC server. */ getUrl({ autoConnect = true, viewOnly = false, resize = "scale", authKey } = {}) { if (this.url === null) { throw new Error("Server is not running"); } let url = new URL(this.url); if (autoConnect) { url.searchParams.set("autoconnect", "true"); } if (viewOnly) { url.searchParams.set("view_only", "true"); } if (resize) { url.searchParams.set("resize", resize); } if (authKey) { url.searchParams.set("password", authKey); } return url.toString(); } /** * Start the VNC server. */ start() { return __async(this, arguments, function* (opts = {}) { var _a, _b, _c; if (yield this.checkVNCRunning()) { console.log("Stream is already running"); this.url = new URL(`http://${this.desktop.getHost(this.port)}/vnc.html`); return; } this.vncPort = (_a = opts.vncPort) != null ? _a : this.vncPort; this.port = (_b = opts.port) != null ? _b : this.port; this.novncAuthEnabled = (_c = opts.requireAuth) != null ? _c : this.novncAuthEnabled; this.password = this.novncAuthEnabled ? generateRandomString() : void 0; this.url = new URL(`https://${this.desktop.getHost(this.port)}/vnc.html`); const vncCommand = yield this.getVNCCommand(opts.windowId); yield this.desktop.commands.run(vncCommand); this.novncHandle = yield this.desktop.commands.run(this.novncCommand, { background: true, timeoutMs: 0 }); if (!(yield this.waitForPort(this.port))) { throw new Error("Could not start noVNC server"); } }); } /** * Stop the VNC server. */ stop() { return __async(this, null, function* () { if (yield this.checkVNCRunning()) { yield this.desktop.commands.run("pkill x11vnc"); } if (this.novncHandle) { yield this.novncHandle.kill(); this.novncHandle = null; } }); } }; // Annotate the CommonJS export names for ESM import in node: 0 && (module.exports = { Sandbox, ...require("@bigppwong/e2b") }); //# sourceMappingURL=index.js.map