#!/root/python3.11/bin/python3 """Web Browser server ‒ Flask + Playwright backend.""" from __future__ import annotations import atexit import functools import signal import sys import time from pathlib import Path from typing import Any lib_path = str(Path(__file__).resolve().parent.parent / "lib") sys.path.insert(0, lib_path) from browser_manager import BrowserManager from flask import Flask, Response, jsonify, request from web_browser_config import ServerConfig from web_browser_utils import catch_error, normalize_url, validate_request config = ServerConfig() browser_manager = BrowserManager() def cleanup_on_exit(): """Cleanup function for atexit and signal handlers.""" browser_manager.cleanup() def signal_handler(signum, frame): """Handle shutdown signals.""" print(f"\nReceived signal {signum}, shutting down...") cleanup_on_exit() sys.exit(0) # register cleanup handlers atexit.register(cleanup_on_exit) signal.signal(signal.SIGINT, signal_handler) signal.signal(signal.SIGTERM, signal_handler) app = Flask(__name__) def require_website_open(func): """Decorator to ensure a website is open before executing the route.""" @functools.wraps(func) def wrapper(*args, **kwargs): if not browser_manager.is_website_open(): return create_response({"status": "error", "message": "Please open a website first."}, False) return func(*args, **kwargs) return wrapper def _get_response_metadata() -> dict[str, Any]: """Gather comprehensive metadata for API responses.""" metadata = {} if browser_manager.is_website_open(): metadata["Browser Type"] = browser_manager.browser_name metadata["Mouse Position"] = f"({browser_manager.mouse_x},{browser_manager.mouse_y})" metadata["Viewport Size"] = f"{browser_manager.window_width}×{browser_manager.window_height}" with browser_manager._browser_lock() as page: metadata["Current URL"] = page.url metadata["Page Title"] = page.title() scroll_info = page.evaluate("""() => ({ scroll_position: { x: window.pageXOffset || document.documentElement.scrollLeft, y: window.pageYOffset || document.documentElement.scrollTop }, page_dimensions: { width: document.documentElement.scrollWidth, height: document.documentElement.scrollHeight }, visible_dimensions: { width: window.innerWidth, height: window.innerHeight } })""") scroll_pos = scroll_info["scroll_position"] page_dims = scroll_info["page_dimensions"] visible_dims = scroll_info["visible_dimensions"] metadata["Scroll Position"] = f"({scroll_pos['x']},{scroll_pos['y']})" metadata["Page Dimensions"] = f"{page_dims['width']}×{page_dims['height']}" metadata["Visible Dimensions"] = f"{visible_dims['width']}×{visible_dims['height']}" return metadata def create_response(data: dict[str, Any], return_screenshot: bool) -> Response: """Create a JSON response with comprehensive metadata and optional screenshot.""" response_data = {**data} if return_screenshot: response_data.update(browser_manager.take_screenshot()) if "metadata" not in response_data: response_data["metadata"] = {} response_data["metadata"].update(_get_response_metadata()) return jsonify(response_data) @app.route("/info", methods=["GET"]) @catch_error def info(): if not browser_manager.is_website_open(): return create_response({"status": "success", "message": "No page open"}, False) data = { "status": "success", "message": "Loaded info for current page", } return create_response(data, False) @app.route("/close", methods=["POST"]) @catch_error def close_browser(): browser_manager.cleanup() browser_manager._init_browser() return create_response({"status": "success", "message": "Closed browser"}, False) @app.route("/set_window_size", methods=["POST"]) @validate_request("width", "height", "return_screenshot") @require_website_open @catch_error def set_window_size(): width, height = request.json["width"], request.json["height"] return_screenshot = request.json["return_screenshot"] if width <= 0 or height <= 0: return create_response( {"status": "error", "message": f"Invalid dimensions ({width},{height}). Must be positive"}, False ) with browser_manager._browser_lock() as page: page.set_viewport_size({"width": width, "height": height}) browser_manager.window_width = width browser_manager.window_height = height browser_manager.constrain_mouse_position(page) data = {"status": "success", "message": f"Set viewport to {width}×{height}"} return create_response(data, return_screenshot) @app.route("/screenshot", methods=["GET"]) @require_website_open def screenshot(): data = {"status": "success", "message": "Screenshot"} return create_response(data, True) @app.route("/click", methods=["POST"]) @validate_request("x", "y", "button", "return_screenshot") @catch_error @require_website_open def click(): x, y = round(request.json["x"]), round(request.json["y"]) button = request.json["button"] return_screenshot = request.json["return_screenshot"] x_valid, y_valid = browser_manager.validate_coordinates(x, y) if not x_valid or not y_valid: return create_response( { "status": "error", "message": f"Invalid coordinates ({x},{y}). Must be within {browser_manager.window_width}x{browser_manager.window_height}", }, False, ) with browser_manager._browser_lock() as page: page.mouse.click(x, y, button=button) browser_manager.mouse_x, browser_manager.mouse_y = x, y data = {"status": "success", "message": f"Clicked '{button}' at ({x},{y})"} return create_response(data, return_screenshot) @app.route("/double_click", methods=["POST"]) @validate_request("x", "y", "return_screenshot") @catch_error @require_website_open def double_click(): x, y = round(request.json["x"]), round(request.json["y"]) return_screenshot = request.json["return_screenshot"] x_valid, y_valid = browser_manager.validate_coordinates(x, y) if not x_valid or not y_valid: return create_response( { "status": "error", "message": f"Invalid coordinates ({x},{y}). Must be within {browser_manager.window_width}x{browser_manager.window_height}", }, False, ) with browser_manager._browser_lock() as page: page.mouse.dblclick(x, y) browser_manager.mouse_x, browser_manager.mouse_y = x, y data = {"status": "success", "message": f"Double‑clicked at ({x},{y})"} return create_response(data, return_screenshot) @app.route("/move", methods=["POST"]) @validate_request("x", "y", "return_screenshot") @catch_error @require_website_open def move(): x, y = request.json["x"], request.json["y"] return_screenshot = request.json["return_screenshot"] x_valid, y_valid = browser_manager.validate_coordinates(x, y) if not x_valid or not y_valid: return create_response( { "status": "error", "message": f"Invalid coordinates ({x},{y}). Must be within {browser_manager.window_width}x{browser_manager.window_height}", }, False, ) with browser_manager._browser_lock() as page: page.mouse.move(x, y) browser_manager.mouse_x, browser_manager.mouse_y = x, y data = {"status": "success", "message": f"Moved mouse to ({x},{y})"} return create_response(data, return_screenshot) @app.route("/drag", methods=["POST"]) @validate_request("path", "return_screenshot") @catch_error @require_website_open def drag(): path: list[list[int]] = request.json["path"] return_screenshot = request.json["return_screenshot"] if not path or len(path) < 2: return create_response({"status": "error", "message": "Path needs at least two points"}, False) for ix, point in enumerate(path): if len(point) != 2: return create_response( {"status": "error", "message": f"Path point {ix} must have exactly 2 coordinates"}, False ) x, y = point x_valid, y_valid = browser_manager.validate_coordinates(x, y) if not x_valid or not y_valid: return create_response( { "status": "error", "message": f"Invalid coordinates ({x},{y}) at path point {ix}. Must be within {browser_manager.window_width}x{browser_manager.window_height}", }, False, ) with browser_manager._browser_lock() as page: page.mouse.move(*path[0]) page.mouse.down() for x, y in path[1:]: page.mouse.move(x, y) page.mouse.up() browser_manager.mouse_x, browser_manager.mouse_y = path[-1] data = {"status": "success", "message": "Dragged the mouse along the path"} return create_response(data, return_screenshot) @app.route("/type", methods=["POST"]) @validate_request("text", "return_screenshot") @catch_error @require_website_open def type_(): text = request.json["text"] return_screenshot = request.json["return_screenshot"] with browser_manager._browser_lock() as page: page.keyboard.type(text) data = {"status": "success", "message": f"Typed '{text}'"} return create_response(data, return_screenshot) @app.route("/scroll", methods=["POST"]) @validate_request("scroll_x", "scroll_y", "return_screenshot") @catch_error @require_website_open def scroll(): delta_x, delta_y = request.json["scroll_x"], request.json["scroll_y"] return_screenshot = request.json["return_screenshot"] with browser_manager._browser_lock() as page: page.mouse.wheel(delta_x, delta_y) data = {"status": "success", "message": f"Scrolled by ({delta_x},{delta_y})"} return create_response(data, return_screenshot) @app.route("/execute_script", methods=["POST"]) @validate_request("script", "return_screenshot") @catch_error @require_website_open def exec_script(): script = request.json["script"] return_screenshot = request.json["return_screenshot"] with browser_manager._browser_lock() as page: result = page.evaluate(script) data = {"status": "success", "message": (f"Script executed.\n\n{result}\n")} return create_response(data, return_screenshot) @app.route("/back", methods=["POST"]) @validate_request("return_screenshot") @catch_error @require_website_open def back(): return_screenshot = request.json["return_screenshot"] with browser_manager._browser_lock() as page: page.go_back() data = {"status": "success", "message": "Navigated back"} return create_response(data, return_screenshot) @app.route("/forward", methods=["POST"]) @validate_request("return_screenshot") @catch_error @require_website_open def forward(): return_screenshot = request.json["return_screenshot"] with browser_manager._browser_lock() as page: page.go_forward() data = {"status": "success", "message": "Navigated forward"} return create_response(data, return_screenshot) @app.route("/reload", methods=["POST"]) @validate_request("return_screenshot") @catch_error @require_website_open def reload(): return_screenshot = request.json["return_screenshot"] with browser_manager._browser_lock() as page: page.reload() data = {"status": "success", "message": "Reloaded the page"} return create_response(data, return_screenshot) @app.route("/wait", methods=["POST"]) @validate_request("ms", "return_screenshot") @catch_error @require_website_open def wait(): milliseconds = request.json["ms"] return_screenshot = request.json["return_screenshot"] time.sleep(milliseconds / 1000.0) data = {"status": "success", "message": f"Waited {milliseconds} ms"} return create_response(data, return_screenshot) @app.route("/keypress", methods=["POST"]) @validate_request("keys", "return_screenshot") @catch_error @require_website_open def keypress(): keys: list[str] = request.json["keys"] return_screenshot = request.json["return_screenshot"] if not isinstance(keys, list): return create_response({"status": "error", "message": "Keys must be a list"}, False) if not keys: return create_response({"status": "error", "message": "Keys list empty"}, False) with browser_manager._browser_lock() as _: for key in keys[:-1]: browser_manager.key_down(key) browser_manager.key_press(keys[-1]) for key in reversed(keys[:-1]): browser_manager.key_up(key) data = {"status": "success", "message": f"Pressed keys {keys}"} return create_response(data, return_screenshot) @app.route("/goto", methods=["POST"]) @validate_request("url", "return_screenshot") @catch_error def goto(): url = normalize_url(request.json["url"]) return_screenshot = request.json["return_screenshot"] with browser_manager._browser_lock() as page: page.goto(url, wait_until="load") data = {"status": "success", "message": f"Navigated to {url}"} return create_response(data, return_screenshot) @app.route("/console", methods=["GET"]) @catch_error @require_website_open def get_console(): console_messages = browser_manager.get_console_output() data = { "status": "success", "message": f"Retrieved {len(console_messages)} console messages", "console_messages": console_messages, } return create_response(data, False) def main(): """Run the Flask server with proper cleanup handling.""" try: app.run(host="0.0.0.0", port=config.port, threaded=False, use_reloader=False) except KeyboardInterrupt: print("\nShutting down gracefully...") finally: cleanup_on_exit() if __name__ == "__main__": main()