import gi gi.require_version('Gtk', '3.0') webkit_loaded = False webkit_version = None for version in ['4.1', '4.0', '3.0']: try: gi.require_version('WebKit2', version) webkit_loaded = True webkit_version = version break except ValueError: continue if not webkit_loaded: print("Error: WebKit2 is not installed or available.") print("Please install it using:") print(" Ubuntu/Debian: sudo apt-get install gir1.2-webkit2-4.0") print(" Fedora: sudo dnf install webkit2gtk3") print(" Arch: sudo pacman -S webkit2gtk") exit(1) from gi.repository import Gtk, WebKit2, GLib, Gdk print(f"Successfully loaded WebKit2 version {webkit_version}") import asyncio import websockets import json import threading import uuid from typing import Dict, Optional, Callable, Any import time import base64 class RemoteBrowser: """ A WebKit2GTK browser instance that can be controlled via WebSocket. Each connection gets its own browser window. """ def __init__(self, connection_id: str, websocket, server): self.connection_id = connection_id self.websocket = websocket self.server = server self.window = None self.webview = None self.pending_callbacks = {} self.is_closing = False def create_browser(self): """Create the browser window in the GTK thread.""" self.window = Gtk.Window(title=f"Remote Browser - {self.connection_id[:8]}") self.window.set_default_size(1024, 768) self.window.connect("destroy", self.on_window_destroy) main_box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL) self.window.add(main_box) nav_bar = self.create_navigation_bar() main_box.pack_start(nav_bar, False, False, 0) self.webview = WebKit2.WebView() self.setup_webview() scrolled_window = Gtk.ScrolledWindow() scrolled_window.add(self.webview) main_box.pack_start(scrolled_window, True, True, 0) self.status_bar = Gtk.Statusbar() self.status_bar.push(0, f"Connected: {self.connection_id[:8]}") main_box.pack_start(self.status_bar, False, False, 0) self.window.show_all() def create_navigation_bar(self): """Create a simple navigation bar.""" nav_bar = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=5) nav_bar.set_margin_top(5) nav_bar.set_margin_bottom(5) nav_bar.set_margin_left(5) nav_bar.set_margin_right(5) self.url_label = Gtk.Label() self.url_label.set_ellipsize(True) self.url_label.set_max_width_chars(50) nav_bar.pack_start(self.url_label, True, True, 0) return nav_bar def setup_webview(self): """Configure WebKit view settings.""" settings = self.webview.get_settings() settings.set_enable_developer_extras(True) settings.set_enable_javascript(True) settings.set_allow_file_access_from_file_urls(True) self.webview.connect("load-changed", self.on_load_changed) self.webview.connect("notify::uri", self.on_uri_changed) self.webview.connect("load-failed", self.on_load_failed) def on_window_destroy(self, widget): """Handle window close event.""" if not self.is_closing: self.is_closing = True asyncio.create_task(self.close()) def on_load_changed(self, webview, load_event): """Handle page load events.""" if load_event == WebKit2.LoadEvent.FINISHED: asyncio.create_task(self.send_event("load_finished", { "url": webview.get_uri(), "title": webview.get_title() })) elif load_event == WebKit2.LoadEvent.STARTED: asyncio.create_task(self.send_event("load_started", { "url": webview.get_uri() })) def on_uri_changed(self, webview, param): """Update URL label when URI changes.""" uri = webview.get_uri() if uri: self.url_label.set_text(uri) def on_load_failed(self, webview, load_event, failing_uri, error): """Handle load failures.""" asyncio.create_task(self.send_event("load_failed", { "url": failing_uri, "error": error.message if error else "Unknown error" })) async def send_event(self, event_type: str, data: dict): """Send an event to the WebSocket client.""" try: await self.websocket.send(json.dumps({ "type": "event", "event": event_type, "data": data })) except: pass async def send_response(self, request_id: str, result: Any, error: Optional[str] = None): """Send a response to a command.""" try: await self.websocket.send(json.dumps({ "type": "response", "request_id": request_id, "result": result, "error": error })) except: pass def execute_javascript(self, script: str, request_id: str): """Execute JavaScript and send result back via WebSocket.""" def js_finished(webview, task, user_data): try: result = webview.run_javascript_finish(task) js_result = result.get_js_value() if js_result.is_string(): value = js_result.to_string() elif js_result.is_number(): value = js_result.to_double() elif js_result.is_boolean(): value = js_result.to_boolean() elif js_result.is_object(): self.webview.run_javascript( f"JSON.stringify({script})", None, lambda wv, t, ud: self._handle_json_result(wv, t, request_id) ) return else: value = None asyncio.create_task(self.send_response(request_id, value)) except Exception as e: asyncio.create_task(self.send_response(request_id, None, str(e))) self.webview.run_javascript(script, None, js_finished, None) def _handle_json_result(self, webview, task, request_id): """Handle JSON stringified JavaScript results.""" try: result = webview.run_javascript_finish(task) js_result = result.get_js_value() json_str = js_result.to_string() value = json.loads(json_str) if json_str else None asyncio.create_task(self.send_response(request_id, value)) except Exception as e: asyncio.create_task(self.send_response(request_id, None, str(e))) async def handle_command(self, command: dict): """Handle commands from WebSocket client.""" cmd_type = command.get("command") request_id = command.get("request_id", str(uuid.uuid4())) try: if cmd_type == "navigate": url = command.get("url") GLib.idle_add(self.webview.load_uri, url) await self.send_response(request_id, {"status": "navigating", "url": url}) elif cmd_type == "execute_js": script = command.get("script") GLib.idle_add(self.execute_javascript, script, request_id) elif cmd_type == "go_back": GLib.idle_add(self.webview.go_back) await self.send_response(request_id, {"status": "ok"}) elif cmd_type == "go_forward": GLib.idle_add(self.webview.go_forward) await self.send_response(request_id, {"status": "ok"}) elif cmd_type == "reload": GLib.idle_add(self.webview.reload) await self.send_response(request_id, {"status": "reloading"}) elif cmd_type == "stop": GLib.idle_add(self.webview.stop_loading) await self.send_response(request_id, {"status": "stopped"}) elif cmd_type == "get_info": def get_browser_info(): info = { "url": self.webview.get_uri(), "title": self.webview.get_title(), "can_go_back": self.webview.can_go_back(), "can_go_forward": self.webview.can_go_forward(), "is_loading": self.webview.is_loading() } asyncio.create_task(self.send_response(request_id, info)) GLib.idle_add(get_browser_info) elif cmd_type == "screenshot": def take_screenshot(): def on_snapshot(webview, task, data): try: surface = webview.get_snapshot_finish(task) import io import cairo buf = io.BytesIO() surface.write_to_png(buf) buf.seek(0) screenshot_b64 = base64.b64encode(buf.read()).decode('utf-8') asyncio.create_task(self.send_response(request_id, { "screenshot": screenshot_b64, "format": "png" })) except Exception as e: asyncio.create_task(self.send_response(request_id, None, str(e))) self.webview.get_snapshot( WebKit2.SnapshotRegion.FULL_DOCUMENT, WebKit2.SnapshotOptions.NONE, None, on_snapshot, None ) GLib.idle_add(take_screenshot) elif cmd_type == "set_html": html = command.get("html", "") base_uri = command.get("base_uri") GLib.idle_add(self.webview.load_html, html, base_uri) await self.send_response(request_id, {"status": "html_loaded"}) else: await self.send_response(request_id, None, f"Unknown command: {cmd_type}") except Exception as e: await self.send_response(request_id, None, str(e)) async def close(self): """Close the browser window and cleanup.""" if not self.is_closing: self.is_closing = True def close_window(): if self.window: self.window.destroy() GLib.idle_add(close_window) if self.connection_id in self.server.connections: del self.server.connections[self.connection_id] class BrowserServer: """ WebSocket server that manages multiple browser instances. """ def __init__(self, host: str = "localhost", port: int = 8765): self.host = host self.port = port self.connections: Dict[str, RemoteBrowser] = {} self.gtk_thread = None self.loop = None def start_gtk_thread(self): """Start GTK main loop in a separate thread.""" def gtk_main(): Gtk.main() self.gtk_thread = threading.Thread(target=gtk_main, daemon=True) self.gtk_thread.start() async def handle_connection(self, websocket, path): """Handle a new WebSocket connection.""" connection_id = str(uuid.uuid4()) browser = RemoteBrowser(connection_id, websocket, self) self.connections[connection_id] = browser GLib.idle_add(browser.create_browser) await websocket.send(json.dumps({ "type": "connected", "connection_id": connection_id, "message": "Browser window created" })) try: async for message in websocket: try: command = json.loads(message) await browser.handle_command(command) except json.JSONDecodeError: await websocket.send(json.dumps({ "type": "error", "error": "Invalid JSON" })) except Exception as e: await websocket.send(json.dumps({ "type": "error", "error": str(e) })) finally: await browser.close() async def start_server(self): """Start the WebSocket server.""" print(f"Starting WebSocket server on ws://{self.host}:{self.port}") await websockets.serve(self.handle_connection, self.host, self.port) await asyncio.Future() async def run(self): """Run the server.""" self.start_gtk_thread() await self.start_server() CLIENT_EXAMPLE = ''' import asyncio import websockets import json async def test_browser(): """Example client that controls the browser.""" uri = "ws://localhost:8765" async with websockets.connect(uri) as websocket: # Wait for connection confirmation response = await websocket.recv() print(f"Connected: {response}") # Navigate to a website await websocket.send(json.dumps({ "command": "navigate", "url": "https://www.example.com", "request_id": "nav1" })) # Wait for navigation response response = await websocket.recv() print(f"Navigation response: {response}") # Wait a bit for page to load await asyncio.sleep(2) # Execute JavaScript await websocket.send(json.dumps({ "command": "execute_js", "script": "document.title", "request_id": "js1" })) response = await websocket.recv() print(f"Page title: {response}") # Get browser info await websocket.send(json.dumps({ "command": "get_info", "request_id": "info1" })) response = await websocket.recv() print(f"Browser info: {response}") # Take screenshot await websocket.send(json.dumps({ "command": "screenshot", "request_id": "screenshot1" })) response = await websocket.recv() result = json.loads(response) if result.get("result") and result["result"].get("screenshot"): print("Screenshot captured!") # You can save the base64 image here # Load custom HTML await websocket.send(json.dumps({ "command": "set_html", "html": "
This is custom HTML
", "request_id": "html1" })) response = await websocket.recv() print(f"HTML loaded: {response}") # Keep connection open for events while True: try: message = await asyncio.wait_for(websocket.recv(), timeout=30) print(f"Event: {message}") except asyncio.TimeoutError: break asyncio.run(test_browser()) ''' if __name__ == "__main__": server = BrowserServer(host="localhost", port=8765) print("WebSocket Browser Server") print("=" * 50) print(f"Server will run on ws://localhost:8765") print("\nAvailable commands:") print("- navigate: Load a URL") print("- execute_js: Execute JavaScript and get result") print("- go_back/go_forward: Navigate history") print("- reload/stop: Reload or stop loading") print("- get_info: Get browser state info") print("- screenshot: Take a screenshot (base64)") print("- set_html: Load custom HTML") print("\nExample client code:") print("-" * 50) print(CLIENT_EXAMPLE) print("-" * 50) try: asyncio.run(server.run()) except KeyboardInterrupt: print("\nShutting down server...") Gtk.main_quit()