app
Adom Desktop - RustDesk Bridge
Public Made by Adomby adom
Drive RustDesk remote-desktop sessions from cloud Claude via Adom Desktop — connect out to any host by ID or IP (with a password), and list/close sessions. Works where RDP can't (Windows Home).
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424
#!/usr/bin/env python3
"""Adom Desktop - RustDesk Bridge (Phase 1: client side).
Drives the CONTROLLING side of RustDesk from cloud Claude via adom-desktop:
launch a connection out to a remote ID/IP, list active sessions, close them,
and ensure the client binary is present (download is NON-BLOCKING).
HTTP contract (adom-desktop bridge SDK):
GET /status -> {ok, bridge, version, uptimeSec, led, summary, tooltip}
POST /command {command,args} -> {success, output|error, _hint, ...}
The runtime spawns this as: python server.py --port <ephemeral>
Every verb returns a rich `_hint`. Nothing blocks on a heavy install.
"""
import csv
import glob
import io
import json
import os
import re
import subprocess
import sys
import threading
import time
import urllib.request
import ctypes
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
BRIDGE_NAME = "rustdesk"
BRIDGE_VERSION = "0.3.2"
DEFAULT_PORT = 8810
RUSTDESK_DL_URL = "https://github.com/rustdesk/rustdesk/releases/download/1.4.8/rustdesk-1.4.8-x86_64.exe"
_CREATE_FLAGS = 0x00000008 | 0x00000200 # DETACHED_PROCESS | CREATE_NEW_PROCESS_GROUP
_NO_WINDOW = 0x08000000 # CREATE_NO_WINDOW for tasklist/taskkill helpers
_START = time.time()
# ---- non-blocking client-download state (so no verb blocks on the ~24 MB fetch) ----
_dl_lock = threading.Lock()
_dl = {"state": "idle", "pct": 0, "path": None, "error": None}
# ---- safeguard: auto-disconnect a session when THIS (client) machine goes idle ----
# A live RustDesk client session keeps the local display awake (like media playback).
# So if the user walks away (no input on this laptop) we drop the session -> the screen
# can sleep/screensaver normally. Active use resets the timer (mouse/keys into the
# session ARE local input). The timer is the OS's own last-input clock (self-refreshing:
# any activity zeroes it), NOT a countdown from connect. The HOST is untouched. 0 disables.
# Default 60 min: long enough not to interrupt passive watching, short enough to kill the
# overnight/walked-away case (burn-in needs hours of static content; 60 min never gets there).
IDLE_DISCONNECT_SECS = int(os.environ.get("RUSTDESK_IDLE_DISCONNECT_SECS", "3600"))
# ---------------------------------------------------------------- helpers
def _candidate_exes():
la = os.environ.get("LOCALAPPDATA", "")
pf = os.environ.get("ProgramFiles", r"C:\Program Files")
up = os.environ.get("USERPROFILE", "")
out = [
os.path.join(la, "rustdesk", "rustdesk.exe"),
os.path.join(pf, "RustDesk", "rustdesk.exe"),
]
if up:
out += sorted(glob.glob(os.path.join(up, "Downloads", "rustdesk-*.exe")), reverse=True)
return out
def find_rustdesk():
for p in _candidate_exes():
if p and os.path.isfile(p):
return p
from shutil import which
return which("rustdesk")
def _tasklist_rustdesk():
try:
out = subprocess.run(
["tasklist", "/FI", "IMAGENAME eq rustdesk.exe", "/V", "/FO", "CSV", "/NH"],
capture_output=True, text=True, timeout=10, creationflags=_NO_WINDOW,
).stdout
except Exception:
return []
rows = []
for r in csv.reader(io.StringIO(out)):
if len(r) >= 9 and r[0].lower().startswith("rustdesk"):
try:
pid = int(r[1])
except ValueError:
continue
rows.append({"pid": pid, "title": r[-1]})
return rows
_SESSION_RE = re.compile(r"^\s*(\S.*?)\s*-\s*Remote Desktop\s*-\s*RustDesk", re.I)
def _sessions():
out = []
for p in _tasklist_rustdesk():
m = _SESSION_RE.match(p.get("title") or "")
if m:
out.append({"pid": p["pid"], "peer": m.group(1).strip(), "title": p["title"]})
return out
def _norm(s):
return re.sub(r"\s+", "", s or "")
def _download_client():
up = os.environ.get("USERPROFILE", "")
dest = os.path.join(up, "Downloads", "rustdesk-1.4.8-x86_64.exe")
def hook(blocks, bs, total):
if total and total > 0:
with _dl_lock:
_dl["pct"] = min(99, int(blocks * bs * 100 / total))
try:
urllib.request.urlretrieve(RUSTDESK_DL_URL, dest, hook)
with _dl_lock:
_dl.update(state="done", pct=100, path=dest, error=None)
except Exception as e:
with _dl_lock:
_dl.update(state="error", error=str(e))
def _user_idle_secs():
"""Seconds since the last user input on THIS (client) machine. 0 on non-Windows/error."""
try:
class _LII(ctypes.Structure):
_fields_ = [("cbSize", ctypes.c_uint), ("dwTime", ctypes.c_uint)]
lii = _LII()
lii.cbSize = ctypes.sizeof(lii)
if ctypes.windll.user32.GetLastInputInfo(ctypes.byref(lii)):
tick = ctypes.windll.kernel32.GetTickCount()
return max(0, (int(tick) - int(lii.dwTime)) // 1000)
except Exception:
pass
return 0
def _idle_monitor():
"""Drop active sessions once the local user has been idle past IDLE_DISCONNECT_SECS,
so the client's screen can sleep. Never touches the host side."""
while True:
time.sleep(30)
limit = IDLE_DISCONNECT_SECS
if limit <= 0:
continue
sess = _sessions()
if sess and _user_idle_secs() >= limit:
for s in sess:
try:
subprocess.run(["taskkill", "/PID", str(s["pid"]), "/F"],
capture_output=True, timeout=10, creationflags=_NO_WINDOW)
except Exception:
pass
sys.stderr.write("rustdesk bridge: auto-disconnected %d idle session(s) after %ds idle\n"
% (len(sess), limit))
sys.stderr.flush()
# ---------------------------------------------------------------- verbs
def v_connect(args):
to = (args.get("to") or args.get("id") or args.get("ip") or "").strip()
if not to:
return {"success": False, "error": "missing 'to' (RustDesk ID or IP)",
"_hint": "Pass {\"to\":\"<id-or-ip>\",\"password\":\"<pw>\"}. Get the host's ID/password from its RustDesk window or the host-side bridge."}
exe = find_rustdesk()
if not exe:
return {"success": False, "error": "rustdesk client not found",
"_hint": "The client isn't installed. Call rustdesk_ensure_client (non-blocking download), poll rustdesk_client_status until installed:true, then retry."}
cmd = [exe, "--connect", to]
pw = args.get("password")
if pw:
cmd += ["--password", str(pw)]
try:
p = subprocess.Popen(cmd, creationflags=_CREATE_FLAGS,
stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
except Exception as e:
return {"success": False, "error": "launch failed: %s" % e,
"_hint": "The client exe was found but wouldn't launch. Check rustdesk_client_status; the exe path may be stale."}
return {"success": True, "output": "connecting to %s" % to,
"to": to, "pid": p.pid, "withPassword": bool(pw), "exe": exe,
"_hint": "Session window opens as '%s - Remote Desktop - RustDesk'. NOTE: while open this keeps the CONTROLLING machine's display awake (RustDesk treats it like live playback) - the bridge auto-disconnects after %ds of local idle so the screen can still sleep; close sooner with rustdesk_disconnect. Confirm with rustdesk_client_status; without a password it waits at a prompt on the controlling machine." % (to, IDLE_DISCONNECT_SECS)}
def v_disconnect(args):
to = (args.get("to") or args.get("id") or "").strip()
killed = []
for s in _sessions():
if not to or _norm(s["peer"]) == _norm(to):
try:
subprocess.run(["taskkill", "/PID", str(s["pid"]), "/F"],
capture_output=True, timeout=10, creationflags=_NO_WINDOW)
killed.append(s["peer"])
except Exception:
pass
hint = ("Closed %d session window(s). " % len(killed)) + (
"Pass {\"to\":\"<id>\"} to close just one; omit it to close all." if not to
else "Run rustdesk_client_status to confirm none remain.")
return {"success": True, "output": "disconnected %d session(s)" % len(killed),
"disconnected": killed, "count": len(killed), "_hint": hint}
def v_client_status(args):
exe = find_rustdesk()
sess = _sessions()
with _dl_lock:
dl = dict(_dl)
if not exe and dl["state"] == "downloading":
hint = "Client still downloading (%d%%). Keep polling; when downloadState is 'done', rustdesk_connect will work." % dl["pct"]
elif not exe:
hint = "Client not installed. Call rustdesk_ensure_client (non-blocking) to fetch the portable build, then poll here."
elif sess:
hint = "%d active session(s): %s. Close with rustdesk_disconnect." % (len(sess), ", ".join(s["peer"] for s in sess))
else:
hint = "Client installed, no active sessions. rustdesk_connect {to,password} to start one."
return {"success": True,
"output": "%d active session(s)" % len(sess),
"installed": bool(exe), "path": exe, "sessions": sess,
"downloadState": dl["state"], "downloadPct": dl["pct"], "downloadError": dl["error"],
"bridgeVersion": BRIDGE_VERSION, "_hint": hint}
def v_ensure_client(args):
exe = find_rustdesk()
if exe:
return {"success": True, "output": "client present", "present": True, "path": exe,
"_hint": "RustDesk client already installed — rustdesk_connect is ready."}
with _dl_lock:
if _dl["state"] == "downloading":
return {"success": True, "output": "download already in progress", "downloading": True,
"pct": _dl["pct"],
"_hint": "A background download is already running (%d%%). Poll rustdesk_client_status (downloadPct)." % _dl["pct"]}
_dl.update(state="downloading", pct=0, path=None, error=None)
threading.Thread(target=_download_client, daemon=True).start()
return {"success": True, "output": "started background download of the portable client",
"downloading": True, "pct": 0,
"_hint": "NON-BLOCKING: the ~24 MB portable client is downloading in the background (returns instantly). Poll rustdesk_client_status — when downloadState becomes 'done'/installed:true, rustdesk_connect works."}
def v_readiness(args):
"""READ-ONLY cold-start probe. Never downloads. {ready, installing, installProgressPct, lastError}."""
exe = find_rustdesk()
with _dl_lock:
dl = dict(_dl)
ready = bool(exe)
installing = (not ready) and dl["state"] == "downloading"
if ready:
hint = "Ready - rustdesk_connect {to,password} works now."
elif installing:
hint = "Client downloading in the background (%d%%). Re-poll rustdesk_readiness; never block on it." % dl["pct"]
else:
hint = "Client not present. Call rustdesk_prewarm (non-blocking) to fetch it, then poll here."
return {"success": True, "ready": ready, "installing": installing,
"installProgressPct": (dl["pct"] if installing else (100 if ready else 0)),
"lastError": dl["error"], "_hint": hint}
def v_prewarm(args):
"""Kick off the client download in the background; supports {\"wait\":true} to block until ready."""
if find_rustdesk():
return {"success": True, "ready": True, "installing": False, "output": "already warm",
"_hint": "Client already present - nothing to prewarm; rustdesk_connect is ready."}
started = False
with _dl_lock:
if _dl["state"] != "downloading":
_dl.update(state="downloading", pct=0, path=None, error=None)
started = True
if started:
threading.Thread(target=_download_client, daemon=True).start()
if args.get("wait"):
deadline = time.time() + 50 # stay under AD's ~60s verb cap
while time.time() < deadline:
if find_rustdesk():
return {"success": True, "ready": True, "installing": False, "output": "warmed",
"_hint": "Client downloaded - rustdesk_connect is ready."}
with _dl_lock:
err = _dl["error"]
if err:
return {"success": False, "ready": False, "error": err,
"_hint": "Download failed: %s. Retry rustdesk_prewarm." % err}
time.sleep(1.5)
with _dl_lock:
pct = _dl["pct"]
return {"success": True, "ready": False, "installing": True, "installProgressPct": pct,
"_hint": "Still downloading (%d%%) after the wait window; poll rustdesk_readiness." % pct}
with _dl_lock:
pct = _dl["pct"]
return {"success": True, "ready": False, "installing": True, "installProgressPct": pct,
"_hint": "NON-BLOCKING prewarm started; poll rustdesk_readiness until ready:true (pass {\"wait\":true} to block)."}
_CATALOG = [
{"name": "rustdesk_connect", "summary": "Open a remote-control session to a host by RustDesk ID or IP.",
"input": {"to": "ID or IP (required)", "password": "optional, auto-authenticates"},
"output": "{to, pid, withPassword}", "example": {"to": "123456789", "password": "<pw>"}},
{"name": "rustdesk_disconnect", "summary": "Close a session window (or all sessions).",
"input": {"to": "optional ID; omit = close all"}, "output": "{disconnected[], count}"},
{"name": "rustdesk_client_status", "summary": "Installed path + active sessions + download state (may spawn the bridge).",
"input": {}, "output": "{installed, path, sessions[], downloadState, downloadPct}"},
{"name": "rustdesk_readiness", "summary": "READ-ONLY cold-start probe; never downloads.",
"input": {}, "output": "{ready, installing, installProgressPct, lastError}"},
{"name": "rustdesk_prewarm", "summary": "Kick off the client download in the background; {wait:true} blocks until ready.",
"input": {"wait": "optional bool"}, "output": "{ready, installing, installProgressPct}"},
{"name": "rustdesk_ensure_client", "summary": "Ensure the client binary is present (non-blocking download).",
"input": {}, "output": "{present|downloading, path}"},
{"name": "rustdesk_describe", "summary": "This machine-readable verb catalog.", "input": {}, "output": "{verbs[]}"},
]
def v_describe(args):
return {"success": True, "verbs": _CATALOG, "version": BRIDGE_VERSION,
"_hint": "80%% path: rustdesk_connect / rustdesk_client_status / rustdesk_disconnect. Cold start: rustdesk_readiness (read-only) + rustdesk_prewarm. Connect with {to,password}."}
VERBS = {
"rustdesk_connect": v_connect,
"rustdesk_disconnect": v_disconnect,
"rustdesk_client_status": v_client_status,
"rustdesk_ensure_client": v_ensure_client,
"rustdesk_readiness": v_readiness,
"rustdesk_prewarm": v_prewarm,
"rustdesk_describe": v_describe,
}
# ---------------------------------------------------------------- status chip
def status_payload():
exe = find_rustdesk()
sess = _sessions()
with _dl_lock:
dl = dict(_dl)
if not exe and dl["state"] == "downloading":
led, summary = "yellow", "client %d%%" % dl["pct"]
elif not exe:
led, summary = "yellow", "client missing"
elif sess:
led, summary = "green", "%d session%s" % (len(sess), "" if len(sess) == 1 else "s")
else:
led, summary = "green", "ready"
tooltip = "RustDesk client bridge v%s\n%s" % (
BRIDGE_VERSION,
("Sessions: " + ", ".join(s["peer"] for s in sess)) if sess else "No active sessions",
)
return {"ok": True, "bridge": BRIDGE_NAME, "version": BRIDGE_VERSION,
"uptimeSec": int(time.time() - _START),
"led": led, "summary": summary, "tooltip": tooltip}
# ---------------------------------------------------------------- http
class Handler(BaseHTTPRequestHandler):
def _send(self, code, obj):
body = json.dumps(obj).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 log_message(self, *a):
pass
def do_GET(self):
if self.path.rstrip("/") in ("/status", "/health", ""):
self._send(200, status_payload())
else:
self._send(404, {"ok": False, "error": "not found"})
def do_POST(self):
if self.path.rstrip("/") != "/command":
self._send(404, {"success": False, "error": "not found"})
return
try:
n = int(self.headers.get("Content-Length", 0) or 0)
body = json.loads(self.rfile.read(n) or b"{}")
except Exception as e:
self._send(400, {"success": False, "error": "bad json: %s" % e})
return
verb = body.get("command") or body.get("verb") or ""
args = body.get("args") or {}
fn = VERBS.get(verb)
if not fn:
self._send(404, {"success": False, "error": "unknown verb: %s" % verb,
"known": list(VERBS),
"_hint": "Valid verbs: " + ", ".join(VERBS)})
return
try:
self._send(200, fn(args))
except Exception as e:
self._send(200, {"success": False, "error": "%s: %s" % (type(e).__name__, e),
"_hint": "Unexpected bridge error; retry. If it persists, bridge_list to check instanceCount, or reinstall."})
def _parse_port(argv):
for i, a in enumerate(argv):
if a == "--port" and i + 1 < len(argv):
try:
return int(argv[i + 1])
except ValueError:
pass
if a.startswith("--port="):
try:
return int(a.split("=", 1)[1])
except ValueError:
pass
env = os.environ.get("BRIDGE_PORT") or os.environ.get("ADOM_BRIDGE_PORT")
return int(env) if env else DEFAULT_PORT
def main():
port = _parse_port(sys.argv[1:])
srv = ThreadingHTTPServer(("127.0.0.1", port), Handler)
sys.stderr.write("rustdesk bridge v%s listening on 127.0.0.1:%d (idle-disconnect %ds)\n"
% (BRIDGE_VERSION, port, IDLE_DISCONNECT_SECS))
sys.stderr.flush()
threading.Thread(target=_idle_monitor, daemon=True).start()
srv.serve_forever()
if __name__ == "__main__":
main()