autoglm-gui 0.3.1__py3-none-any.whl → 0.4.1__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (34) hide show
  1. AutoGLM_GUI/__main__.py +6 -2
  2. AutoGLM_GUI/adb_plus/__init__.py +8 -1
  3. AutoGLM_GUI/adb_plus/screenshot.py +1 -4
  4. AutoGLM_GUI/adb_plus/touch.py +92 -0
  5. AutoGLM_GUI/api/__init__.py +66 -0
  6. AutoGLM_GUI/api/agents.py +231 -0
  7. AutoGLM_GUI/api/control.py +111 -0
  8. AutoGLM_GUI/api/devices.py +29 -0
  9. AutoGLM_GUI/api/media.py +163 -0
  10. AutoGLM_GUI/schemas.py +127 -0
  11. AutoGLM_GUI/scrcpy_stream.py +65 -28
  12. AutoGLM_GUI/server.py +2 -491
  13. AutoGLM_GUI/state.py +33 -0
  14. AutoGLM_GUI/static/assets/{about-C71SI8ZQ.js → about-gHEqXVMQ.js} +1 -1
  15. AutoGLM_GUI/static/assets/chat-6a-qTECg.js +25 -0
  16. AutoGLM_GUI/static/assets/index-C8KPPfxe.js +10 -0
  17. AutoGLM_GUI/static/assets/index-D2-3f619.css +1 -0
  18. AutoGLM_GUI/static/assets/{index-DUCan6m6.js → index-DgzeSwgt.js} +1 -1
  19. AutoGLM_GUI/static/index.html +2 -2
  20. AutoGLM_GUI/version.py +8 -0
  21. {autoglm_gui-0.3.1.dist-info → autoglm_gui-0.4.1.dist-info}/METADATA +64 -9
  22. autoglm_gui-0.4.1.dist-info/RECORD +44 -0
  23. phone_agent/adb/connection.py +0 -1
  24. phone_agent/adb/device.py +0 -2
  25. phone_agent/adb/input.py +0 -1
  26. phone_agent/adb/screenshot.py +0 -1
  27. phone_agent/agent.py +1 -1
  28. AutoGLM_GUI/static/assets/chat-C6WtEfKW.js +0 -14
  29. AutoGLM_GUI/static/assets/index-Dd1xMRCa.css +0 -1
  30. AutoGLM_GUI/static/assets/index-RqglIZxV.js +0 -10
  31. autoglm_gui-0.3.1.dist-info/RECORD +0 -35
  32. {autoglm_gui-0.3.1.dist-info → autoglm_gui-0.4.1.dist-info}/WHEEL +0 -0
  33. {autoglm_gui-0.3.1.dist-info → autoglm_gui-0.4.1.dist-info}/entry_points.txt +0 -0
  34. {autoglm_gui-0.3.1.dist-info → autoglm_gui-0.4.1.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,163 @@
1
+ """Media routes: screenshot, video stream, stream reset."""
2
+
3
+ import asyncio
4
+
5
+ from fastapi import APIRouter, WebSocket, WebSocketDisconnect
6
+
7
+ from AutoGLM_GUI.adb_plus import capture_screenshot
8
+ from AutoGLM_GUI.schemas import ScreenshotRequest, ScreenshotResponse
9
+ from AutoGLM_GUI.scrcpy_stream import ScrcpyStreamer
10
+ from AutoGLM_GUI.state import scrcpy_locks, scrcpy_streamers
11
+
12
+ router = APIRouter()
13
+
14
+
15
+ @router.post("/api/video/reset")
16
+ async def reset_video_stream(device_id: str | None = None) -> dict:
17
+ """Reset video stream (cleanup scrcpy server,多设备支持)."""
18
+ if device_id:
19
+ if device_id in scrcpy_locks:
20
+ async with scrcpy_locks[device_id]:
21
+ if device_id in scrcpy_streamers:
22
+ print(f"[video/reset] Stopping streamer for device {device_id}")
23
+ scrcpy_streamers[device_id].stop()
24
+ del scrcpy_streamers[device_id]
25
+ print(f"[video/reset] Streamer reset for device {device_id}")
26
+ return {
27
+ "success": True,
28
+ "message": f"Video stream reset for device {device_id}",
29
+ }
30
+ return {
31
+ "success": True,
32
+ "message": f"No active video stream for device {device_id}",
33
+ }
34
+ return {"success": True, "message": f"No video stream for device {device_id}"}
35
+
36
+ device_ids = list(scrcpy_streamers.keys())
37
+ for dev_id in device_ids:
38
+ if dev_id in scrcpy_locks:
39
+ async with scrcpy_locks[dev_id]:
40
+ if dev_id in scrcpy_streamers:
41
+ scrcpy_streamers[dev_id].stop()
42
+ del scrcpy_streamers[dev_id]
43
+ print("[video/reset] All streamers reset")
44
+ return {"success": True, "message": "All video streams reset"}
45
+
46
+
47
+ @router.post("/api/screenshot", response_model=ScreenshotResponse)
48
+ def take_screenshot(request: ScreenshotRequest) -> ScreenshotResponse:
49
+ """获取设备截图。此操作无副作用,不影响 PhoneAgent 运行。"""
50
+ try:
51
+ screenshot = capture_screenshot(device_id=request.device_id)
52
+ return ScreenshotResponse(
53
+ success=True,
54
+ image=screenshot.base64_data,
55
+ width=screenshot.width,
56
+ height=screenshot.height,
57
+ is_sensitive=screenshot.is_sensitive,
58
+ )
59
+ except Exception as e:
60
+ return ScreenshotResponse(
61
+ success=False,
62
+ image="",
63
+ width=0,
64
+ height=0,
65
+ is_sensitive=False,
66
+ error=str(e),
67
+ )
68
+
69
+
70
+ @router.websocket("/api/video/stream")
71
+ async def video_stream_ws(websocket: WebSocket, device_id: str | None = None):
72
+ """Stream real-time H.264 video from scrcpy server via WebSocket(多设备支持)."""
73
+ await websocket.accept()
74
+
75
+ if not device_id:
76
+ await websocket.send_json({"error": "device_id is required"})
77
+ return
78
+
79
+ print(f"[video/stream] WebSocket connection for device {device_id}")
80
+
81
+ if device_id not in scrcpy_locks:
82
+ scrcpy_locks[device_id] = asyncio.Lock()
83
+
84
+ async with scrcpy_locks[device_id]:
85
+ if device_id not in scrcpy_streamers:
86
+ print(f"[video/stream] Creating streamer for device {device_id}")
87
+ scrcpy_streamers[device_id] = ScrcpyStreamer(
88
+ device_id=device_id, max_size=1280, bit_rate=4_000_000
89
+ )
90
+
91
+ try:
92
+ print(f"[video/stream] Starting scrcpy server for device {device_id}")
93
+ await scrcpy_streamers[device_id].start()
94
+ print(f"[video/stream] Scrcpy server started for device {device_id}")
95
+ except Exception as e:
96
+ import traceback
97
+
98
+ print(f"[video/stream] Failed to start streamer: {e}")
99
+ print(f"[video/stream] Traceback:\n{traceback.format_exc()}")
100
+ scrcpy_streamers[device_id].stop()
101
+ del scrcpy_streamers[device_id]
102
+ try:
103
+ await websocket.send_json({"error": str(e)})
104
+ except Exception:
105
+ pass
106
+ return
107
+ else:
108
+ print(f"[video/stream] Reusing streamer for device {device_id}")
109
+
110
+ streamer = scrcpy_streamers[device_id]
111
+ if streamer.cached_sps and streamer.cached_pps:
112
+ init_data = streamer.cached_sps + streamer.cached_pps
113
+ await websocket.send_bytes(init_data)
114
+ print(f"[video/stream] Sent SPS/PPS for device {device_id}")
115
+ else:
116
+ print(
117
+ f"[video/stream] Warning: No cached SPS/PPS for device {device_id}"
118
+ )
119
+
120
+ streamer = scrcpy_streamers[device_id]
121
+
122
+ stream_failed = False
123
+ try:
124
+ chunk_count = 0
125
+ while True:
126
+ try:
127
+ h264_chunk = await streamer.read_h264_chunk()
128
+ await websocket.send_bytes(h264_chunk)
129
+ chunk_count += 1
130
+ if chunk_count % 100 == 0:
131
+ print(
132
+ f"[video/stream] Device {device_id}: Sent {chunk_count} chunks"
133
+ )
134
+ except ConnectionError as e:
135
+ print(f"[video/stream] Device {device_id}: Connection error: {e}")
136
+ stream_failed = True
137
+ try:
138
+ await websocket.send_json({"error": f"Stream error: {str(e)}"})
139
+ except Exception:
140
+ pass
141
+ break
142
+
143
+ except WebSocketDisconnect:
144
+ print(f"[video/stream] Device {device_id}: Client disconnected")
145
+ except Exception as e:
146
+ import traceback
147
+
148
+ print(f"[video/stream] Device {device_id}: Error: {e}")
149
+ print(f"[video/stream] Traceback:\n{traceback.format_exc()}")
150
+ stream_failed = True
151
+ try:
152
+ await websocket.send_json({"error": str(e)})
153
+ except Exception:
154
+ pass
155
+
156
+ if stream_failed:
157
+ async with scrcpy_locks[device_id]:
158
+ if device_id in scrcpy_streamers:
159
+ print(f"[video/stream] Resetting streamer for device {device_id}")
160
+ scrcpy_streamers[device_id].stop()
161
+ del scrcpy_streamers[device_id]
162
+
163
+ print(f"[video/stream] Device {device_id}: Stream ended")
AutoGLM_GUI/schemas.py ADDED
@@ -0,0 +1,127 @@
1
+ """Shared Pydantic models for the AutoGLM-GUI API."""
2
+
3
+ from pydantic import BaseModel, Field
4
+
5
+
6
+ class APIModelConfig(BaseModel):
7
+ base_url: str | None = None
8
+ api_key: str | None = None
9
+ model_name: str | None = None
10
+ max_tokens: int = 3000
11
+ temperature: float = 0.0
12
+ top_p: float = 0.85
13
+ frequency_penalty: float = 0.2
14
+
15
+
16
+ class APIAgentConfig(BaseModel):
17
+ max_steps: int = 100
18
+ device_id: str | None = None
19
+ lang: str = "cn"
20
+ system_prompt: str | None = None
21
+ verbose: bool = True
22
+
23
+
24
+ class InitRequest(BaseModel):
25
+ model: APIModelConfig | None = Field(default=None, alias="model_config")
26
+ agent: APIAgentConfig | None = Field(default=None, alias="agent_config")
27
+
28
+
29
+ class ChatRequest(BaseModel):
30
+ message: str
31
+ device_id: str # 设备 ID(必填)
32
+
33
+
34
+ class ChatResponse(BaseModel):
35
+ result: str
36
+ steps: int
37
+ success: bool
38
+
39
+
40
+ class StatusResponse(BaseModel):
41
+ version: str
42
+ initialized: bool
43
+ step_count: int
44
+
45
+
46
+ class ResetRequest(BaseModel):
47
+ device_id: str # 设备 ID(必填)
48
+
49
+
50
+ class ScreenshotRequest(BaseModel):
51
+ device_id: str | None = None
52
+
53
+
54
+ class ScreenshotResponse(BaseModel):
55
+ success: bool
56
+ image: str # base64 encoded PNG
57
+ width: int
58
+ height: int
59
+ is_sensitive: bool
60
+ error: str | None = None
61
+
62
+
63
+ class TapRequest(BaseModel):
64
+ x: int
65
+ y: int
66
+ device_id: str | None = None
67
+ delay: float = 0.0
68
+
69
+
70
+ class TapResponse(BaseModel):
71
+ success: bool
72
+ error: str | None = None
73
+
74
+
75
+ class SwipeRequest(BaseModel):
76
+ start_x: int
77
+ start_y: int
78
+ end_x: int
79
+ end_y: int
80
+ duration_ms: int | None = None
81
+ device_id: str | None = None
82
+ delay: float = 0.0
83
+
84
+
85
+ class SwipeResponse(BaseModel):
86
+ success: bool
87
+ error: str | None = None
88
+
89
+
90
+ class TouchDownRequest(BaseModel):
91
+ x: int
92
+ y: int
93
+ device_id: str | None = None
94
+ delay: float = 0.0
95
+
96
+
97
+ class TouchDownResponse(BaseModel):
98
+ success: bool
99
+ error: str | None = None
100
+
101
+
102
+ class TouchMoveRequest(BaseModel):
103
+ x: int
104
+ y: int
105
+ device_id: str | None = None
106
+ delay: float = 0.0
107
+
108
+
109
+ class TouchMoveResponse(BaseModel):
110
+ success: bool
111
+ error: str | None = None
112
+
113
+
114
+ class TouchUpRequest(BaseModel):
115
+ x: int
116
+ y: int
117
+ device_id: str | None = None
118
+ delay: float = 0.0
119
+
120
+
121
+ class TouchUpResponse(BaseModel):
122
+ success: bool
123
+ error: str | None = None
124
+
125
+
126
+ class DeviceListResponse(BaseModel):
127
+ devices: list[dict]
@@ -104,6 +104,7 @@ class ScrcpyStreamer:
104
104
  except Exception as e:
105
105
  print(f"[ScrcpyStreamer] Failed to start: {e}")
106
106
  import traceback
107
+
107
108
  traceback.print_exc()
108
109
  self.stop()
109
110
  raise RuntimeError(f"Failed to start scrcpy server: {e}") from e
@@ -124,7 +125,7 @@ class ScrcpyStreamer:
124
125
  # Method 2: Find and kill by PID (more reliable)
125
126
  cmd = cmd_base + [
126
127
  "shell",
127
- "ps -ef | grep 'app_process.*scrcpy' | grep -v grep | awk '{print $2}' | xargs kill -9"
128
+ "ps -ef | grep 'app_process.*scrcpy' | grep -v grep | awk '{print $2}' | xargs kill -9",
128
129
  ]
129
130
  process = await asyncio.create_subprocess_exec(
130
131
  *cmd, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL
@@ -200,9 +201,7 @@ class ScrcpyStreamer:
200
201
 
201
202
  # Capture stderr to see error messages
202
203
  self.scrcpy_process = await asyncio.create_subprocess_exec(
203
- *cmd,
204
- stdout=subprocess.PIPE,
205
- stderr=subprocess.PIPE
204
+ *cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE
206
205
  )
207
206
 
208
207
  # Wait for server to start
@@ -217,12 +216,16 @@ class ScrcpyStreamer:
217
216
  # Check if it's an "Address already in use" error
218
217
  if "Address already in use" in error_msg:
219
218
  if attempt < max_retries - 1:
220
- print(f"[ScrcpyStreamer] Address in use, retrying in {retry_delay}s (attempt {attempt + 1}/{max_retries})...")
219
+ print(
220
+ f"[ScrcpyStreamer] Address in use, retrying in {retry_delay}s (attempt {attempt + 1}/{max_retries})..."
221
+ )
221
222
  await self._cleanup_existing_server()
222
223
  await asyncio.sleep(retry_delay)
223
224
  continue
224
225
  else:
225
- raise RuntimeError(f"scrcpy server failed after {max_retries} attempts: {error_msg}")
226
+ raise RuntimeError(
227
+ f"scrcpy server failed after {max_retries} attempts: {error_msg}"
228
+ )
226
229
  else:
227
230
  raise RuntimeError(f"scrcpy server exited immediately: {error_msg}")
228
231
 
@@ -239,7 +242,9 @@ class ScrcpyStreamer:
239
242
  # Increase socket buffer size for high-resolution video
240
243
  # Default is often 64KB, but complex frames can be 200-500KB
241
244
  try:
242
- self.tcp_socket.setsockopt(socket.SOL_SOCKET, socket.SO_RCVBUF, 2 * 1024 * 1024) # 2MB
245
+ self.tcp_socket.setsockopt(
246
+ socket.SOL_SOCKET, socket.SO_RCVBUF, 2 * 1024 * 1024
247
+ ) # 2MB
243
248
  print("[ScrcpyStreamer] Set socket receive buffer to 2MB")
244
249
  except OSError as e:
245
250
  print(f"[ScrcpyStreamer] Warning: Failed to set socket buffer size: {e}")
@@ -267,9 +272,9 @@ class ScrcpyStreamer:
267
272
 
268
273
  while i < data_len - 4:
269
274
  # Look for start codes: 0x00 0x00 0x00 0x01 or 0x00 0x00 0x01
270
- if data[i:i+4] == b'\x00\x00\x00\x01':
275
+ if data[i : i + 4] == b"\x00\x00\x00\x01":
271
276
  start_code_len = 4
272
- elif data[i:i+3] == b'\x00\x00\x01':
277
+ elif data[i : i + 3] == b"\x00\x00\x01":
273
278
  start_code_len = 3
274
279
  else:
275
280
  i += 1
@@ -285,8 +290,10 @@ class ScrcpyStreamer:
285
290
  # Find next start code to determine NAL unit size
286
291
  next_start = nal_start + 1
287
292
  while next_start < data_len - 3:
288
- if (data[next_start:next_start+4] == b'\x00\x00\x00\x01' or
289
- data[next_start:next_start+3] == b'\x00\x00\x01'):
293
+ if (
294
+ data[next_start : next_start + 4] == b"\x00\x00\x00\x01"
295
+ or data[next_start : next_start + 3] == b"\x00\x00\x01"
296
+ ):
290
297
  break
291
298
  next_start += 1
292
299
  else:
@@ -309,7 +316,7 @@ class ScrcpyStreamer:
309
316
  nal_units = self._find_nal_units(data)
310
317
 
311
318
  for start, nal_type, size in nal_units:
312
- nal_data = data[start:start+size]
319
+ nal_data = data[start : start + size]
313
320
 
314
321
  if nal_type == 7: # SPS
315
322
  # Only cache SPS if not yet locked
@@ -317,10 +324,16 @@ class ScrcpyStreamer:
317
324
  # Validate: SPS should be at least 10 bytes
318
325
  if size >= 10 and not self.cached_sps:
319
326
  self.cached_sps = nal_data
320
- hex_preview = ' '.join(f'{b:02x}' for b in nal_data[:min(12, len(nal_data))])
321
- print(f"[ScrcpyStreamer] Cached complete SPS ({size} bytes): {hex_preview}...")
327
+ hex_preview = " ".join(
328
+ f"{b:02x}" for b in nal_data[: min(12, len(nal_data))]
329
+ )
330
+ print(
331
+ f"[ScrcpyStreamer] ✓ Cached complete SPS ({size} bytes): {hex_preview}..."
332
+ )
322
333
  elif size < 10:
323
- print(f"[ScrcpyStreamer] ✗ Skipped truncated SPS ({size} bytes, too short)")
334
+ print(
335
+ f"[ScrcpyStreamer] ✗ Skipped truncated SPS ({size} bytes, too short)"
336
+ )
324
337
 
325
338
  elif nal_type == 8: # PPS
326
339
  # Only cache PPS if not yet locked
@@ -328,10 +341,16 @@ class ScrcpyStreamer:
328
341
  # Validate: PPS should be at least 6 bytes
329
342
  if size >= 6 and not self.cached_pps:
330
343
  self.cached_pps = nal_data
331
- hex_preview = ' '.join(f'{b:02x}' for b in nal_data[:min(12, len(nal_data))])
332
- print(f"[ScrcpyStreamer] Cached complete PPS ({size} bytes): {hex_preview}...")
344
+ hex_preview = " ".join(
345
+ f"{b:02x}" for b in nal_data[: min(12, len(nal_data))]
346
+ )
347
+ print(
348
+ f"[ScrcpyStreamer] ✓ Cached complete PPS ({size} bytes): {hex_preview}..."
349
+ )
333
350
  elif size < 6:
334
- print(f"[ScrcpyStreamer] ✗ Skipped truncated PPS ({size} bytes, too short)")
351
+ print(
352
+ f"[ScrcpyStreamer] ✗ Skipped truncated PPS ({size} bytes, too short)"
353
+ )
335
354
 
336
355
  elif nal_type == 5: # IDR frame
337
356
  # ✅ ALWAYS update IDR to keep the LATEST frame
@@ -340,7 +359,9 @@ class ScrcpyStreamer:
340
359
  is_first = self.cached_idr is None
341
360
  self.cached_idr = nal_data
342
361
  if is_first:
343
- print(f"[ScrcpyStreamer] ✓ Cached initial IDR frame ({size} bytes)")
362
+ print(
363
+ f"[ScrcpyStreamer] ✓ Cached initial IDR frame ({size} bytes)"
364
+ )
344
365
  # Don't log every IDR update (too verbose)
345
366
 
346
367
  # Lock SPS/PPS once we have complete initial parameters
@@ -366,7 +387,9 @@ class ScrcpyStreamer:
366
387
  return data
367
388
 
368
389
  # Find all IDR frames
369
- idr_positions = [(start, size) for start, nal_type, size in nal_units if nal_type == 5]
390
+ idr_positions = [
391
+ (start, size) for start, nal_type, size in nal_units if nal_type == 5
392
+ ]
370
393
 
371
394
  if not idr_positions:
372
395
  return data
@@ -386,7 +409,9 @@ class ScrcpyStreamer:
386
409
  if data[prepend_offset:idr_start] != sps_pps:
387
410
  # Prepend SPS/PPS before this IDR
388
411
  result.extend(sps_pps)
389
- print(f"[ScrcpyStreamer] Prepended SPS/PPS before IDR at position {idr_start}")
412
+ print(
413
+ f"[ScrcpyStreamer] Prepended SPS/PPS before IDR at position {idr_start}"
414
+ )
390
415
 
391
416
  # Update position to start of IDR
392
417
  last_pos = idr_start
@@ -409,11 +434,17 @@ class ScrcpyStreamer:
409
434
  init_data += self.cached_idr
410
435
 
411
436
  # Validate data integrity
412
- print(f"[ScrcpyStreamer] Returning init data:")
413
- print(f" - SPS: {len(self.cached_sps)} bytes, starts with {' '.join(f'{b:02x}' for b in self.cached_sps[:8])}")
414
- print(f" - PPS: {len(self.cached_pps)} bytes, starts with {' '.join(f'{b:02x}' for b in self.cached_pps[:8])}")
437
+ print("[ScrcpyStreamer] Returning init data:")
438
+ print(
439
+ f" - SPS: {len(self.cached_sps)} bytes, starts with {' '.join(f'{b:02x}' for b in self.cached_sps[:8])}"
440
+ )
441
+ print(
442
+ f" - PPS: {len(self.cached_pps)} bytes, starts with {' '.join(f'{b:02x}' for b in self.cached_pps[:8])}"
443
+ )
415
444
  if self.cached_idr:
416
- print(f" - IDR: {len(self.cached_idr)} bytes, starts with {' '.join(f'{b:02x}' for b in self.cached_idr[:8])}")
445
+ print(
446
+ f" - IDR: {len(self.cached_idr)} bytes, starts with {' '.join(f'{b:02x}' for b in self.cached_idr[:8])}"
447
+ )
417
448
  print(f" - Total: {len(init_data)} bytes")
418
449
 
419
450
  return init_data
@@ -442,7 +473,9 @@ class ScrcpyStreamer:
442
473
 
443
474
  # Log large chunks (might indicate complex frames)
444
475
  if len(data) > 200 * 1024: # > 200KB
445
- print(f"[ScrcpyStreamer] Large chunk received: {len(data) / 1024:.1f} KB")
476
+ print(
477
+ f"[ScrcpyStreamer] Large chunk received: {len(data) / 1024:.1f} KB"
478
+ )
446
479
 
447
480
  # Cache INITIAL complete SPS/PPS/IDR for future use
448
481
  # (Later chunks may have truncated NAL units, so we only cache once)
@@ -457,7 +490,9 @@ class ScrcpyStreamer:
457
490
  except ConnectionError:
458
491
  raise
459
492
  except Exception as e:
460
- print(f"[ScrcpyStreamer] Unexpected error in read_h264_chunk: {type(e).__name__}: {e}")
493
+ print(
494
+ f"[ScrcpyStreamer] Unexpected error in read_h264_chunk: {type(e).__name__}: {e}"
495
+ )
461
496
  raise ConnectionError(f"Failed to read from socket: {e}") from e
462
497
 
463
498
  def stop(self) -> None:
@@ -489,7 +524,9 @@ class ScrcpyStreamer:
489
524
  if self.device_id:
490
525
  cmd.extend(["-s", self.device_id])
491
526
  cmd.extend(["forward", "--remove", f"tcp:{self.port}"])
492
- subprocess.run(cmd, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL, timeout=2)
527
+ subprocess.run(
528
+ cmd, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL, timeout=2
529
+ )
493
530
  except Exception:
494
531
  pass
495
532
  self.forward_cleanup_needed = False