@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
JavaScript
;
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