← 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

John Lauer ·4fc9cc2369 ·4mo ago ·parent 7f31ffe
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):