feat: on-demand RDP - connect xfreerdp only when session opens
Replaces always-on xfreerdp with on-demand model (load 12 to under 1 at idle). - rdp-proxy/manager.py: HTTP server port 7001 managing xfreerdp lifecycle - rdp-proxy/entrypoint.sh: starts Xvfb+x11vnc+websockify+manager, no auto-connect - rdp-proxy/Dockerfile: adds python3, copies manager.py, exposes 7001 - runtime.py: connect_rdp_slot and disconnect_rdp_slot via manager HTTP API - terminate_session_record: disconnect instead of container restart - main.py: calls connect_rdp_slot in background thread on session create - maintenance.py: cleanup_loop disconnects on expire, run_maintenance_service includes RDP slot init, maintenance_runner fixed to import maintenance
This commit is contained in:
@@ -0,0 +1,131 @@
|
||||
#!/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", "")
|
||||
|
||||
_lock = threading.Lock()
|
||||
_proc: subprocess.Popen | None = None
|
||||
_should_be_connected = False # set True on /connect, False on /disconnect
|
||||
|
||||
|
||||
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():
|
||||
"""Auto-reconnect if xfreerdp crashes while session should be active."""
|
||||
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()
|
||||
|
||||
|
||||
threading.Thread(target=_monitor_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
|
||||
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
|
||||
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")
|
||||
log.info("manager started on :7001")
|
||||
HTTPServer(("0.0.0.0", 7001), Handler).serve_forever()
|
||||
Reference in New Issue
Block a user