ccf7401f71
Mouse movement works universally on any remote OS (Windows, Ubuntu, RED OS, Astra). Alternates between (960,540) and (961,541) every 30s inside xfreerdp window via xdotool mousemove --window.
166 lines
5.4 KiB
Python
166 lines
5.4 KiB
Python
#!/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()
|
|
def _anti_idle_loop():
|
|
"""Move mouse inside xfreerdp window every 30s — works on any remote OS."""
|
|
env = {**os.environ, "DISPLAY": DISPLAY}
|
|
toggle = False
|
|
while True:
|
|
time.sleep(30)
|
|
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:
|
|
# Чередуем позицию — tiny mouse jiggle внутри окна xfreerdp
|
|
x, y = (960, 540) if toggle else (961, 541)
|
|
subprocess.run(
|
|
["xdotool", "mousemove", "--window", win_id, str(x), str(y)],
|
|
env=env, capture_output=True, timeout=5,
|
|
)
|
|
toggle = not toggle
|
|
log.debug("anti_idle mousemove 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=_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
|
|
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()
|