main.py
1 # Copyright 2025 Alibaba Group Holding Ltd. 2 # 3 # Licensed under the Apache License, Version 2.0 (the "License"); 4 # you may not use this file except in compliance with the License. 5 # You may obtain a copy of the License at 6 # 7 # http://www.apache.org/licenses/LICENSE-2.0 8 # 9 # Unless required by applicable law or agreed to in writing, software 10 # distributed under the License is distributed on an "AS IS" BASIS, 11 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 # See the License for the specific language governing permissions and 13 # limitations under the License. 14 15 import asyncio 16 import os 17 from datetime import timedelta 18 19 from opensandbox import Sandbox 20 from opensandbox.config import ConnectionConfig 21 from opensandbox.models.execd import RunCommandOpts 22 23 24 def _required_env(name: str) -> str: 25 value = os.getenv(name) 26 if not value: 27 raise RuntimeError(f"{name} is required") 28 return value 29 30 31 async def _print_logs(label: str, execution) -> None: 32 for msg in execution.logs.stdout: 33 print(f"[{label} stdout] {msg.text}") 34 for msg in execution.logs.stderr: 35 print(f"[{label} stderr] {msg.text}") 36 if execution.error: 37 print(f"[{label} error] {execution.error.name}: {execution.error.value}") 38 39 40 async def main() -> None: 41 domain = os.getenv("SANDBOX_DOMAIN", "localhost:8080") 42 api_key = os.getenv("SANDBOX_API_KEY") 43 image = os.getenv( 44 "SANDBOX_IMAGE", 45 "opensandbox/desktop:latest", 46 ) 47 python_version = os.getenv("PYTHON_VERSION", "3.11") 48 vnc_password = _required_env("VNC_PASSWORD") 49 50 config = ConnectionConfig( 51 domain=domain, 52 api_key=api_key, 53 request_timeout=timedelta(seconds=60), 54 ) 55 56 sandbox = await Sandbox.create( 57 image, 58 connection_config=config, 59 env={ 60 "PYTHON_VERSION": python_version, 61 "VNC_PASSWORD": vnc_password, 62 }, 63 ) 64 65 async with sandbox: 66 # Desktop and VNC components are pre-installed in the image, just start them 67 # Start virtual display, window manager, and VNC server (in background) 68 xvfb_exec = await sandbox.commands.run( 69 "Xvfb :0 -screen 0 1280x800x24", 70 opts=RunCommandOpts(background=True), 71 ) 72 await _print_logs("xvfb", xvfb_exec) 73 74 # Start XFCE session (provides panel, file manager, terminal) 75 xfce_exec = await sandbox.commands.run( 76 "DISPLAY=:0 dbus-launch startxfce4", 77 opts=RunCommandOpts(background=True), 78 ) 79 await _print_logs("xfce", xfce_exec) 80 81 vnc_exec = await sandbox.commands.run( 82 "x11vnc -display :0 " 83 "-passwd \"$VNC_PASSWORD\" " 84 "-forever -shared -rfbport 5900", 85 opts=RunCommandOpts(background=True), 86 ) 87 await _print_logs("x11vnc", vnc_exec) 88 89 # Start noVNC/websockify to expose VNC over WebSocket/HTTP 90 novnc_exec = await sandbox.commands.run( 91 "/usr/bin/websockify --web=/usr/share/novnc 6080 localhost:5900", 92 opts=RunCommandOpts(background=True), 93 ) 94 await _print_logs("novnc", novnc_exec) 95 96 endpoint_vnc = await sandbox.get_endpoint(5900) 97 endpoint_novnc = await sandbox.get_endpoint(6080) 98 99 # Build noVNC URL with host/port/path for routed endpoint, e.g., host:port/proxy/6080 100 novnc_host_port, novnc_path = endpoint_novnc.endpoint.split("/", 1) 101 novnc_host, novnc_port = novnc_host_port.split(":") 102 novnc_url = ( 103 f"http://{endpoint_novnc.endpoint}/vnc.html" 104 f"?host={novnc_host}&port={novnc_port}&path={novnc_path}" 105 ) 106 107 print("\nVNC endpoint (native clients):") 108 print(f" {endpoint_vnc.endpoint}") 109 print(f"Password: {vnc_password}") 110 111 print("\nnoVNC (browser):") 112 print(f" {novnc_url}") 113 print(f"Password: {vnc_password}") 114 115 print("\nKeeping sandbox alive for 5 minutes. Press Ctrl+C to exit sooner.") 116 try: 117 await asyncio.sleep(300) 118 except KeyboardInterrupt: 119 print("Stopping...") 120 finally: 121 await sandbox.kill() 122 123 124 if __name__ == "__main__": 125 asyncio.run(main())