#!/usr/bin/env python3 """On-demand xfreerdp manager. HTTP on port 7001.""" import json import logging import os import subprocess import threading import time from http.server import BaseHTTPRequestHandler, HTTPServer logging.basicConfig(level=logging.INFO, format="%(asctime)s %(levelname)s %(message)s") log = logging.getLogger("rdp-manager") DISPLAY = os.environ.get("DISPLAY", ":1") RDP_HOST = os.environ.get("RDP_HOST", "") RDP_PORT = os.environ.get("RDP_PORT", "3389") RDP_USER = os.environ.get("RDP_USER", "") RDP_PASSWORD = os.environ.get("RDP_PASSWORD", "") RDP_DOMAIN = os.environ.get("RDP_DOMAIN", "") RDP_SECURITY = os.environ.get("RDP_SECURITY", "") STATE_FILE = "/tmp/rdp_state.json" _lock = threading.Lock() _proc: subprocess.Popen | None = None _should_be_connected = False def _save_state(): try: with open(STATE_FILE, "w") as f: json.dump({"should_be_connected": _should_be_connected}, f) except Exception: pass def _load_state() -> bool: try: with open(STATE_FILE) as f: return json.load(f).get("should_be_connected", False) except Exception: return False def _build_args(): args = [ "xfreerdp", f"/v:{RDP_HOST}:{RDP_PORT}", "/cert:ignore", "/f", "/dynamic-resolution", "/gfx-h264:avc444", "/network:auto", "+clipboard", ] if RDP_SECURITY: args.append(f"/sec:{RDP_SECURITY}") if RDP_USER: args.append(f"/u:{RDP_USER}") if RDP_PASSWORD: args.append(f"/p:{RDP_PASSWORD}") if RDP_DOMAIN: args.append(f"/d:{RDP_DOMAIN}") return args def _launch(): global _proc env = dict(os.environ) env["DISPLAY"] = DISPLAY log_file = open("/tmp/xfreerdp.log", "a") _proc = subprocess.Popen(_build_args(), stdout=log_file, stderr=log_file, env=env) log.info("xfreerdp started pid=%s target=%s:%s", _proc.pid, RDP_HOST, RDP_PORT) return _proc def _monitor_loop(): while True: time.sleep(5) with _lock: if not _should_be_connected: continue if _proc is None or _proc.poll() is not None: log.info("xfreerdp exited unexpectedly, reconnecting in 3s") time.sleep(3) _launch() def _anti_idle_loop(): env = {**os.environ, "DISPLAY": DISPLAY} toggle = False while True: time.sleep(60) with _lock: active = _should_be_connected and _proc is not None and _proc.poll() is None if not active: continue try: r = subprocess.run( ["xdotool", "search", "--name", "FreeRDP"], env=env, capture_output=True, timeout=5, ) win_id = r.stdout.decode().strip().splitlines()[0] if r.stdout.strip() else "" if win_id: x, y = (5, 5) if toggle else (6, 6) subprocess.run( ["xdotool", "mousemove", "--window", win_id, str(x), str(y)], env=env, capture_output=True, timeout=5, ) subprocess.run( ["xdotool", "click", "--window", win_id, "1"], env=env, capture_output=True, timeout=5, ) toggle = not toggle log.debug("anti_idle click window=%s pos=%s,%s", win_id, x, y) else: log.debug("anti_idle: xfreerdp window not found") except Exception as e: log.debug("anti_idle error: %s", e) threading.Thread(target=_monitor_loop, daemon=True).start() threading.Thread(target=_anti_idle_loop, daemon=True).start() class Handler(BaseHTTPRequestHandler): def log_message(self, fmt, *args): pass def _json(self, code, data): body = json.dumps(data).encode() self.send_response(code) self.send_header("Content-Type", "application/json") self.send_header("Content-Length", str(len(body))) self.end_headers() self.wfile.write(body) def do_GET(self): if self.path == "/health": with _lock: connected = _proc is not None and _proc.poll() is None pid = _proc.pid if connected else None self._json(200, { "connected": connected, "pid": pid, "target": f"{RDP_HOST}:{RDP_PORT}", "should_be_connected": _should_be_connected, }) else: self._json(404, {"error": "not found"}) def do_POST(self): global _proc, _should_be_connected if self.path == "/connect": with _lock: _should_be_connected = True _save_state() if _proc is not None and _proc.poll() is None: self._json(200, {"ok": True, "pid": _proc.pid, "already": True}) return proc = _launch() self._json(200, {"ok": True, "pid": proc.pid}) elif self.path == "/disconnect": with _lock: _should_be_connected = False _save_state() if _proc is not None: _proc.terminate() try: _proc.wait(timeout=5) except subprocess.TimeoutExpired: _proc.kill() _proc = None log.info("xfreerdp disconnected") self._json(200, {"ok": True}) else: self._json(404, {"error": "not found"}) if __name__ == "__main__": if not RDP_HOST: log.warning("RDP_HOST not set — connect calls will fail") if _load_state(): log.info("restoring state: reconnecting xfreerdp") _should_be_connected = True _launch() log.info("manager started on :7001") HTTPServer(("0.0.0.0", 7001), Handler).serve_forever()