Coverage for integrations / remote_desktop / rustdesk_bridge.py: 54.6%

119 statements  

« prev     ^ index     » next       coverage.py v7.14.0, created at 2026-05-12 04:49 +0000

1""" 

2RustDesk Bridge — Wraps RustDesk CLI/API for HARTOS agent integration. 

3 

4RustDesk is installed as a native OS app. HARTOS invokes it via CLI commands 

5and manages sessions through its API. No code linking — separate process. 

6 

7RustDesk CLI commands: 

8 rustdesk --get-id # Get this device's RustDesk ID 

9 rustdesk --password <pass> # Set permanent password 

10 rustdesk --config <key> <value> # Set configuration 

11 rustdesk --connect <id> # Connect to remote device 

12 rustdesk --file-transfer <id> # Open file transfer 

13 rustdesk --port-forward <id> ... # Port forwarding 

14 

15RustDesk capabilities: 

16 - Screen sharing + remote control (VP8/VP9/AV1) 

17 - File transfer (drag-and-drop) 

18 - Clipboard sync (text, images, files, HTML, RTF) 

19 - Audio streaming 

20 - Chat 

21 - P2P with NAT traversal (UDP/TCP hole punching) 

22 - Self-hosted relay server (hbbs + hbbr) 

23""" 

24 

25import json 

26import logging 

27import os 

28import platform 

29import shutil 

30import subprocess 

31import time 

32from typing import Any, Dict, List, Optional, Tuple 

33 

34logger = logging.getLogger('hevolve.remote_desktop') 

35 

36 

37class RustDeskBridge: 

38 """Bridge between HARTOS and RustDesk native application.""" 

39 

40 # RustDesk binary names by platform 

41 BINARY_NAMES = { 

42 'Windows': ['rustdesk.exe', 'RustDesk.exe'], 

43 'Linux': ['rustdesk'], 

44 'Darwin': ['rustdesk', 'RustDesk'], 

45 } 

46 

47 # Common installation paths 

48 INSTALL_PATHS = { 

49 'Windows': [ 

50 os.path.join(os.environ.get('PROGRAMFILES', 'C:\\Program Files'), 'RustDesk'), 

51 os.path.join(os.environ.get('LOCALAPPDATA', ''), 'RustDesk'), 

52 ], 

53 'Linux': [ 

54 '/usr/bin', 

55 '/usr/local/bin', 

56 '/opt/rustdesk', 

57 '/snap/bin', 

58 '/flatpak/exports/bin', 

59 ], 

60 'Darwin': [ 

61 '/Applications/RustDesk.app/Contents/MacOS', 

62 '/usr/local/bin', 

63 ], 

64 } 

65 

66 def __init__(self, binary_path: Optional[str] = None, 

67 server_url: Optional[str] = None): 

68 """ 

69 Args: 

70 binary_path: Explicit path to rustdesk binary. Auto-detected if None. 

71 server_url: Custom relay server URL (self-hosted). Uses default if None. 

72 """ 

73 self._binary = binary_path or self._find_binary() 

74 self._server_url = server_url or os.environ.get('RUSTDESK_SERVER') 

75 self._device_id: Optional[str] = None 

76 

77 # ── Detection ─────────────────────────────────────────────── 

78 

79 def _find_binary(self) -> Optional[str]: 

80 """Auto-detect RustDesk binary on this system.""" 

81 system = platform.system() 

82 binary_names = self.BINARY_NAMES.get(system, ['rustdesk']) 

83 

84 # Check PATH first 

85 for name in binary_names: 

86 path = shutil.which(name) 

87 if path: 

88 logger.info(f"RustDesk found in PATH: {path}") 

89 return path 

90 

91 # Check common install paths 

92 install_paths = self.INSTALL_PATHS.get(system, []) 

93 for dir_path in install_paths: 

94 for name in binary_names: 

95 full_path = os.path.join(dir_path, name) 

96 if os.path.isfile(full_path): 

97 logger.info(f"RustDesk found at: {full_path}") 

98 return full_path 

99 

100 logger.info("RustDesk not found on this system") 

101 return None 

102 

103 @property 

104 def available(self) -> bool: 

105 """Whether RustDesk is installed and accessible.""" 

106 return self._binary is not None 

107 

108 @property 

109 def binary_path(self) -> Optional[str]: 

110 return self._binary 

111 

112 # ── CLI Commands ──────────────────────────────────────────── 

113 

114 def _run(self, args: List[str], timeout: int = 10) -> Tuple[bool, str]: 

115 """Run RustDesk CLI command. 

116 

117 Returns: 

118 (success, output) 

119 """ 

120 if not self._binary: 

121 return False, 'RustDesk not installed' 

122 

123 cmd = [self._binary] + args 

124 try: 

125 result = subprocess.run( 

126 cmd, 

127 capture_output=True, 

128 text=True, 

129 timeout=timeout, 

130 creationflags=getattr(subprocess, 'CREATE_NO_WINDOW', 0), 

131 ) 

132 output = (result.stdout or '').strip() 

133 if result.returncode == 0: 

134 return True, output 

135 error = (result.stderr or '').strip() 

136 return False, error or output or f'Exit code {result.returncode}' 

137 except subprocess.TimeoutExpired: 

138 return False, 'Command timed out' 

139 except FileNotFoundError: 

140 self._binary = None 

141 return False, 'RustDesk binary not found' 

142 except Exception as e: 

143 return False, str(e) 

144 

145 def get_id(self) -> Optional[str]: 

146 """Get this device's RustDesk ID. 

147 

148 Returns: 

149 RustDesk ID string (e.g., '123456789') or None. 

150 """ 

151 if self._device_id: 

152 return self._device_id 

153 

154 ok, output = self._run(['--get-id']) 

155 if ok and output: 

156 self._device_id = output.strip() 

157 logger.info(f"RustDesk ID: {self._device_id}") 

158 return self._device_id 

159 return None 

160 

161 def set_password(self, password: str) -> bool: 

162 """Set permanent access password. 

163 

164 Returns: 

165 True if password was set. 

166 """ 

167 ok, _ = self._run(['--password', password]) 

168 return ok 

169 

170 def get_config(self, key: str) -> Optional[str]: 

171 """Get RustDesk configuration value.""" 

172 ok, output = self._run(['--get-config', key]) 

173 return output if ok else None 

174 

175 def set_config(self, key: str, value: str) -> bool: 

176 """Set RustDesk configuration value.""" 

177 ok, _ = self._run(['--config', key, value]) 

178 return ok 

179 

180 def configure_server(self, relay_server: str, 

181 api_server: Optional[str] = None, 

182 key: Optional[str] = None) -> bool: 

183 """Configure custom relay server (self-hosted). 

184 

185 Args: 

186 relay_server: Relay server address (e.g., 'relay.example.com') 

187 api_server: API server address (optional) 

188 key: Server public key (optional) 

189 """ 

190 results = [self.set_config('relay-server', relay_server)] 

191 if api_server: 

192 results.append(self.set_config('api-server', api_server)) 

193 if key: 

194 results.append(self.set_config('key', key)) 

195 return all(results) 

196 

197 # ── Session Control ───────────────────────────────────────── 

198 

199 def connect(self, remote_id: str, password: Optional[str] = None, 

200 file_transfer: bool = False) -> Tuple[bool, str]: 

201 """Connect to remote device. 

202 

203 Args: 

204 remote_id: Target RustDesk ID 

205 password: Access password (optional for same-user) 

206 file_transfer: Open file transfer mode instead of remote control 

207 

208 Returns: 

209 (success, message) 

210 """ 

211 args = ['--file-transfer' if file_transfer else '--connect', remote_id] 

212 if password: 

213 args.extend(['--password', password]) 

214 

215 # Launch as background process (non-blocking) 

216 if not self._binary: 

217 return False, 'RustDesk not installed' 

218 

219 try: 

220 cmd = [self._binary] + args 

221 proc = subprocess.Popen( 

222 cmd, 

223 stdout=subprocess.DEVNULL, 

224 stderr=subprocess.DEVNULL, 

225 creationflags=getattr(subprocess, 'CREATE_NO_WINDOW', 0), 

226 ) 

227 logger.info(f"RustDesk connecting to {remote_id} (pid={proc.pid})") 

228 return True, f'Connecting to {remote_id}' 

229 except Exception as e: 

230 return False, str(e) 

231 

232 def disconnect_all(self) -> bool: 

233 """Close all RustDesk connections.""" 

234 # RustDesk doesn't have a direct disconnect CLI; close the process 

235 system = platform.system() 

236 try: 

237 if system == 'Windows': 

238 subprocess.run(['taskkill', '/f', '/im', 'rustdesk.exe'], 

239 capture_output=True, timeout=5) 

240 else: 

241 subprocess.run(['pkill', '-f', 'rustdesk'], 

242 capture_output=True, timeout=5) 

243 return True 

244 except Exception: 

245 return False 

246 

247 # ── Service Management ────────────────────────────────────── 

248 

249 def start_service(self) -> bool: 

250 """Start RustDesk background service (enables incoming connections).""" 

251 ok, _ = self._run(['--service', 'start'], timeout=15) 

252 if not ok: 

253 # Alternative: launch in service mode 

254 try: 

255 if self._binary: 

256 subprocess.Popen( 

257 [self._binary, '--service'], 

258 stdout=subprocess.DEVNULL, 

259 stderr=subprocess.DEVNULL, 

260 ) 

261 return True 

262 except Exception: 

263 pass 

264 return ok 

265 

266 def stop_service(self) -> bool: 

267 """Stop RustDesk background service.""" 

268 ok, _ = self._run(['--service', 'stop'], timeout=15) 

269 return ok 

270 

271 def is_service_running(self) -> bool: 

272 """Check if RustDesk service is running.""" 

273 system = platform.system() 

274 try: 

275 if system == 'Windows': 

276 result = subprocess.run( 

277 ['tasklist', '/fi', 'imagename eq rustdesk.exe'], 

278 capture_output=True, text=True, timeout=5, 

279 ) 

280 return 'rustdesk.exe' in result.stdout.lower() 

281 else: 

282 result = subprocess.run( 

283 ['pgrep', '-f', 'rustdesk'], 

284 capture_output=True, timeout=5, 

285 ) 

286 return result.returncode == 0 

287 except Exception: 

288 return False 

289 

290 # ── Installation ──────────────────────────────────────────── 

291 

292 def get_install_command(self) -> str: 

293 """Get platform-specific install command for RustDesk.""" 

294 system = platform.system() 

295 if system == 'Linux': 

296 return ( 

297 "# Debian/Ubuntu:\n" 

298 "wget https://github.com/rustdesk/rustdesk/releases/latest/download/" 

299 "rustdesk-<version>-x86_64.deb && sudo dpkg -i rustdesk-*.deb\n" 

300 "# NixOS:\n" 

301 "nix-env -iA nixpkgs.rustdesk\n" 

302 "# Flatpak:\n" 

303 "flatpak install flathub com.rustdesk.RustDesk" 

304 ) 

305 elif system == 'Darwin': 

306 return "brew install --cask rustdesk" 

307 elif system == 'Windows': 

308 return "winget install RustDesk.RustDesk" 

309 return "Visit https://rustdesk.com/download" 

310 

311 # ── Status ────────────────────────────────────────────────── 

312 

313 def get_status(self) -> dict: 

314 """Get RustDesk status for HARTOS.""" 

315 return { 

316 'engine': 'rustdesk', 

317 'available': self.available, 

318 'binary_path': self._binary, 

319 'device_id': self.get_id() if self.available else None, 

320 'service_running': self.is_service_running() if self.available else False, 

321 'server_url': self._server_url, 

322 'install_command': self.get_install_command() if not self.available else None, 

323 } 

324 

325 

326# ── Singleton ─────────────────────────────────────────────────── 

327 

328_rustdesk_bridge: Optional[RustDeskBridge] = None 

329 

330 

331def get_rustdesk_bridge() -> RustDeskBridge: 

332 """Get or create the singleton RustDeskBridge.""" 

333 global _rustdesk_bridge 

334 if _rustdesk_bridge is None: 

335 _rustdesk_bridge = RustDeskBridge() 

336 return _rustdesk_bridge