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).
← Commit history
Sync displayed source to 0.2.0 (SDK self-audit): bridge.json gains updateManifestUrl/docs/wiki-homepage/detect; server.py gains rich _hints + non-blocking ensure_client
3 files changed
+92−32
BRIDGE_VERSION+1−1@@ -1 +1 @@-0.1.1+0.2.0
bridge.json+15−1@@ -2,8 +2,11 @@ "manifest_version": 1, "name": "rustdesk", "displayName": "RustDesk",- "version": "0.1.1",+ "version": "0.2.0", "description": "Drive RustDesk remote-desktop connections (client side): connect out to an ID/IP, list and close sessions.",+ "homepage": "https://wiki.adom.inc/adom/adom-desktop-rustdesk-bridge",+ "docs": "https://wiki.adom.inc/adom/adom-desktop-rustdesk-bridge",+ "updateManifestUrl": "https://wiki.adom.inc/api/v1/pages/adom-desktop-rustdesk-bridge/files/rustdesk-bridge-manifest.json", "author": "adom", "license": "MIT", "spawn": {@@ -22,6 +25,17 @@ "rustdesk_ensure_client" ], "statusVerb": "rustdesk_client_status",+ "detect": {+ "hostApp": "RustDesk",+ "appPathsExe": "rustdesk.exe",+ "paths": {+ "windows": [+ "%LOCALAPPDATA%\\rustdesk\\rustdesk.exe",+ "%ProgramFiles%\\RustDesk\\rustdesk.exe",+ "%USERPROFILE%\\Downloads\\rustdesk-*.exe"+ ]+ }+ }, "dependencies": { "python": ">=3.8" }, "category": "Remote Access", "tags": ["rustdesk", "remote desktop", "remote access", "screen share", "vnc", "rdp alternative"],
server.py+76−30@@ -3,13 +3,14 @@ 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.+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, ...}+ 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@@ -19,20 +20,24 @@ import os import re import subprocess import sys+import threading import time import urllib.request from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer BRIDGE_NAME = "rustdesk"-BRIDGE_VERSION = "0.1.1"+BRIDGE_VERSION = "0.2.0" DEFAULT_PORT = 8810 RUSTDESK_DL_URL = "https://github.com/rustdesk/rustdesk/releases/download/1.4.8/rustdesk-1.4.8-x86_64.exe" -# DETACHED_PROCESS | CREATE_NEW_PROCESS_GROUP (Windows: don't tie the GUI to the bridge)-_CREATE_FLAGS = 0x00000008 | 0x00000200-_NO_WINDOW = 0x08000000 # CREATE_NO_WINDOW for the helper subprocesses (tasklist/taskkill)+_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}+ # ---------------------------------------------------------------- helpers def _candidate_exes():@@ -57,7 +62,6 @@ def find_rustdesk(): def _tasklist_rustdesk():- """[{pid, title}] for every running rustdesk.exe (window title via /V).""" try: out = subprocess.run( ["tasklist", "/FI", "IMAGENAME eq rustdesk.exe", "/V", "/FO", "CSV", "/NH"],@@ -80,7 +84,6 @@ _SESSION_RE = re.compile(r"^\s*(\S.*?)\s*-\s*Remote Desktop\s*-\s*RustDesk", re. def _sessions():- """Active outbound control sessions, parsed from '<peer> - Remote Desktop - RustDesk' titles.""" out = [] for p in _tasklist_rustdesk(): m = _SESSION_RE.match(p.get("title") or "")@@ -93,14 +96,34 @@ 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))++ # ---------------------------------------------------------------- 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)"}+ 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 - run rustdesk_ensure_client first"}+ 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:@@ -109,11 +132,11 @@ def v_connect(args): 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}+ 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 '<id> - Remote Desktop - RustDesk'. "- "Use rustdesk_client_status to confirm, rustdesk_disconnect to close."}+ "_hint": "Session window opens as '%s - Remote Desktop - RustDesk'. Confirm with rustdesk_client_status; close with rustdesk_disconnect. Without a password it waits at a prompt on the controlling machine." % to} def v_disconnect(args):@@ -127,31 +150,48 @@ def v_disconnect(args): 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)}+ "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, "bridgeVersion": BRIDGE_VERSION}+ "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}- up = os.environ.get("USERPROFILE", "")- dest = os.path.join(up, "Downloads", "rustdesk-1.4.8-x86_64.exe")- try:- urllib.request.urlretrieve(RUSTDESK_DL_URL, dest)- except Exception as e:- return {"success": False, "error": "download failed: %s" % e}- return {"success": True, "output": "downloaded portable client",- "present": True, "path": dest, "downloaded": True}+ 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."} VERBS = {@@ -166,7 +206,11 @@ VERBS = { def status_payload(): exe = find_rustdesk() sess = _sessions()- if not exe:+ 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")@@ -191,7 +235,7 @@ class Handler(BaseHTTPRequestHandler): self.end_headers() self.wfile.write(body) - def log_message(self, *a): # keep stdout/stderr clean+ def log_message(self, *a): pass def do_GET(self):@@ -215,12 +259,14 @@ class Handler(BaseHTTPRequestHandler): fn = VERBS.get(verb) if not fn: self._send(404, {"success": False, "error": "unknown verb: %s" % verb,- "known": list(VERBS)})+ "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)})+ 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):