replx 1.1.1__tar.gz → 1.3__tar.gz
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.
- {replx-1.1.1 → replx-1.3}/LICENSE +1 -1
- replx-1.3/PKG-INFO +25 -0
- {replx-1.1.1 → replx-1.3}/pyproject.toml +10 -3
- {replx-1.1.1 → replx-1.3}/replx/__init__.py +1 -1
- replx-1.3/replx/cli/__init__.py +13 -0
- replx-1.3/replx/cli/agent/__init__.py +14 -0
- replx-1.3/replx/cli/agent/client/__init__.py +14 -0
- replx-1.3/replx/cli/agent/client/core.py +373 -0
- replx-1.3/replx/cli/agent/client/session.py +148 -0
- replx-1.3/replx/cli/agent/protocol.py +131 -0
- replx-1.3/replx/cli/agent/server/__init__.py +3 -0
- replx-1.3/replx/cli/agent/server/__main__.py +4 -0
- replx-1.3/replx/cli/agent/server/command_dispatcher.py +44 -0
- replx-1.3/replx/cli/agent/server/connection_manager.py +538 -0
- replx-1.3/replx/cli/agent/server/core.py +701 -0
- replx-1.3/replx/cli/agent/server/handlers/__init__.py +14 -0
- replx-1.3/replx/cli/agent/server/handlers/exec.py +485 -0
- replx-1.3/replx/cli/agent/server/handlers/filesystem.py +381 -0
- replx-1.3/replx/cli/agent/server/handlers/repl.py +123 -0
- replx-1.3/replx/cli/agent/server/handlers/session.py +223 -0
- replx-1.3/replx/cli/agent/server/handlers/transfer.py +552 -0
- replx-1.3/replx/cli/agent/server/session_manager.py +277 -0
- replx-1.3/replx/cli/app.py +574 -0
- replx-1.3/replx/cli/commands/__init__.py +18 -0
- replx-1.3/replx/cli/commands/device.py +1782 -0
- replx-1.3/replx/cli/commands/exec.py +1647 -0
- replx-1.3/replx/cli/commands/file.py +2225 -0
- replx-1.3/replx/cli/commands/firmware.py +713 -0
- replx-1.3/replx/cli/commands/package.py +1668 -0
- replx-1.3/replx/cli/commands/utility.py +1322 -0
- replx-1.3/replx/cli/config.py +462 -0
- replx-1.3/replx/cli/connection.py +437 -0
- replx-1.3/replx/cli/helpers/__init__.py +59 -0
- replx-1.3/replx/cli/helpers/compiler.py +122 -0
- replx-1.3/replx/cli/helpers/environment.py +24 -0
- replx-1.3/replx/cli/helpers/output.py +205 -0
- replx-1.3/replx/cli/helpers/registry.py +385 -0
- replx-1.3/replx/cli/helpers/scanner.py +222 -0
- replx-1.3/replx/cli/helpers/store.py +99 -0
- replx-1.3/replx/cli/helpers/updater.py +69 -0
- replx-1.3/replx/commands.py +97 -0
- replx-1.3/replx/protocol/__init__.py +12 -0
- replx-1.3/replx/protocol/repl.py +933 -0
- replx-1.3/replx/protocol/storage.py +932 -0
- replx-1.3/replx/terminal.py +244 -0
- replx-1.3/replx/tests/__init__.py +0 -0
- replx-1.3/replx/tests/test_compiler_arch.py +17 -0
- replx-1.3/replx/tests/test_connection_info_lookup.py +30 -0
- replx-1.3/replx/tests/test_device_info_esp_multi_core.py +56 -0
- replx-1.3/replx/tests/test_pkg_local_version.py +44 -0
- replx-1.3/replx/tests/test_session_id_fallback.py +32 -0
- replx-1.3/replx/transport/__init__.py +17 -0
- replx-1.3/replx/transport/base.py +43 -0
- replx-1.3/replx/transport/serial.py +156 -0
- replx-1.3/replx/typehints/comm/_thread.pyi +219 -0
- replx-1.3/replx/typehints/comm/aioble/__init__.pyi +816 -0
- replx-1.3/replx/typehints/comm/array.pyi +183 -0
- replx-1.3/replx/typehints/comm/asyncio/__init__.pyi +833 -0
- replx-1.3/replx/typehints/comm/binascii.pyi +132 -0
- replx-1.3/replx/typehints/comm/bluetooth.pyi +580 -0
- replx-1.3/replx/typehints/comm/builtins.pyi +2013 -0
- replx-1.3/replx/typehints/comm/cmath.pyi +279 -0
- replx-1.3/replx/typehints/comm/collections.pyi +243 -0
- replx-1.3/replx/typehints/comm/cryptolib.pyi +117 -0
- replx-1.3/replx/typehints/comm/deflate.pyi +185 -0
- replx-1.3/replx/typehints/comm/errno.pyi +96 -0
- replx-1.3/replx/typehints/comm/framebuf.pyi +376 -0
- replx-1.3/replx/typehints/comm/gc.pyi +168 -0
- replx-1.3/replx/typehints/comm/hashlib.pyi +205 -0
- replx-1.3/replx/typehints/comm/heapq.pyi +87 -0
- replx-1.3/replx/typehints/comm/io.pyi +434 -0
- replx-1.3/replx/typehints/comm/json.pyi +121 -0
- replx-1.3/replx/typehints/comm/lwip.pyi +48 -0
- replx-1.3/replx/typehints/comm/machine.pyi +1594 -0
- replx-1.3/replx/typehints/comm/math.pyi +816 -0
- replx-1.3/replx/typehints/comm/micropython.pyi +340 -0
- replx-1.3/replx/typehints/comm/mip/__init__.pyi +81 -0
- replx-1.3/replx/typehints/comm/network.pyi +435 -0
- replx-1.3/replx/typehints/comm/ntptime.pyi +76 -0
- replx-1.3/replx/typehints/comm/os.pyi +372 -0
- replx-1.3/replx/typehints/comm/platform.pyi +71 -0
- replx-1.3/replx/typehints/comm/random.pyi +189 -0
- replx-1.3/replx/typehints/comm/re.pyi +314 -0
- replx-1.3/replx/typehints/comm/requests/__init__.pyi +347 -0
- replx-1.3/replx/typehints/comm/select.pyi +180 -0
- replx-1.3/replx/typehints/comm/socket.pyi +577 -0
- replx-1.3/replx/typehints/comm/ssl.pyi +336 -0
- replx-1.3/replx/typehints/comm/struct.pyi +145 -0
- replx-1.3/replx/typehints/comm/sys.pyi +169 -0
- replx-1.3/replx/typehints/comm/time.pyi +308 -0
- replx-1.3/replx/typehints/comm/tls.pyi +25 -0
- replx-1.3/replx/typehints/comm/uasyncio.pyi +23 -0
- replx-1.3/replx/typehints/comm/uctypes.pyi +200 -0
- replx-1.3/replx/typehints/comm/urequests.pyi +272 -0
- replx-1.3/replx/typehints/comm/vfs.pyi +320 -0
- replx-1.3/replx/typehints/comm_separate/EFR32MG/binascii.pyi +21 -0
- replx-1.3/replx/typehints/comm_separate/EFR32MG/errno.pyi +21 -0
- replx-1.3/replx/typehints/comm_separate/EFR32MG/hashlib.pyi +21 -0
- replx-1.3/replx/typehints/comm_separate/EFR32MG/io.pyi +21 -0
- replx-1.3/replx/typehints/comm_separate/EFR32MG/json.pyi +21 -0
- replx-1.3/replx/typehints/comm_separate/EFR32MG/machine.pyi +21 -0
- replx-1.3/replx/typehints/comm_separate/EFR32MG/math.pyi +174 -0
- replx-1.3/replx/typehints/comm_separate/EFR32MG/micropython.pyi +16 -0
- replx-1.3/replx/typehints/comm_separate/EFR32MG/network.pyi +286 -0
- replx-1.3/replx/typehints/comm_separate/EFR32MG/os.pyi +21 -0
- replx-1.3/replx/typehints/comm_separate/EFR32MG/select.pyi +21 -0
- replx-1.3/replx/typehints/comm_separate/EFR32MG/socket.pyi +21 -0
- replx-1.3/replx/typehints/comm_separate/EFR32MG/ssl.pyi +21 -0
- replx-1.3/replx/typehints/comm_separate/EFR32MG/struct.pyi +21 -0
- replx-1.3/replx/typehints/comm_separate/EFR32MG/sys.pyi +57 -0
- replx-1.3/replx/typehints/comm_separate/EFR32MG/time.pyi +21 -0
- replx-1.3/replx/typehints/comm_separate/EFR32MG/ubinascii.pyi +74 -0
- replx-1.3/replx/typehints/comm_separate/EFR32MG/ucryptolib.pyi +83 -0
- replx-1.3/replx/typehints/comm_separate/EFR32MG/uerrno.pyi +46 -0
- replx-1.3/replx/typehints/comm_separate/EFR32MG/uhashlib.pyi +52 -0
- replx-1.3/replx/typehints/comm_separate/EFR32MG/uio.pyi +297 -0
- replx-1.3/replx/typehints/comm_separate/EFR32MG/ujson.pyi +72 -0
- replx-1.3/replx/typehints/comm_separate/EFR32MG/umachine.pyi +792 -0
- replx-1.3/replx/typehints/comm_separate/EFR32MG/uos.pyi +226 -0
- replx-1.3/replx/typehints/comm_separate/EFR32MG/uselect.pyi +187 -0
- replx-1.3/replx/typehints/comm_separate/EFR32MG/usocket.pyi +271 -0
- replx-1.3/replx/typehints/comm_separate/EFR32MG/ussl.pyi +69 -0
- replx-1.3/replx/typehints/comm_separate/EFR32MG/ustruct.pyi +82 -0
- replx-1.3/replx/typehints/comm_separate/EFR32MG/utime.pyi +163 -0
- replx-1.3/replx/typehints/core/ESP32/aioespnow.pyi +137 -0
- replx-1.3/replx/typehints/core/ESP32/esp.pyi +482 -0
- replx-1.3/replx/typehints/core/ESP32/esp32.pyi +1196 -0
- replx-1.3/replx/typehints/core/ESP32/espnow.pyi +525 -0
- replx-1.3/replx/typehints/core/MIMXRT1062DVJ6A/mimxrt.pyi +148 -0
- replx-1.3/replx/typehints/core/RP2350/rp2.pyi +852 -0
- replx-1.3/replx/utils/__init__.py +82 -0
- replx-1.3/replx/utils/constants.py +55 -0
- replx-1.3/replx/utils/device_info.py +121 -0
- replx-1.3/replx/utils/exceptions.py +55 -0
- replx-1.3/replx.egg-info/PKG-INFO +25 -0
- replx-1.3/replx.egg-info/SOURCES.txt +139 -0
- replx-1.3/replx.egg-info/entry_points.txt +3 -0
- {replx-1.1.1 → replx-1.3}/replx.egg-info/requires.txt +1 -0
- replx-1.1.1/PKG-INFO +0 -531
- replx-1.1.1/README.md +0 -485
- replx-1.1.1/replx/exceptions.py +0 -14
- replx-1.1.1/replx/file_system.py +0 -550
- replx-1.1.1/replx/helpers.py +0 -812
- replx-1.1.1/replx/repl_protocol.py +0 -974
- replx-1.1.1/replx/replx.py +0 -1683
- replx-1.1.1/replx/terminal.py +0 -241
- replx-1.1.1/replx.egg-info/PKG-INFO +0 -531
- replx-1.1.1/replx.egg-info/SOURCES.txt +0 -16
- replx-1.1.1/replx.egg-info/entry_points.txt +0 -2
- {replx-1.1.1 → replx-1.3}/replx.egg-info/dependency_links.txt +0 -0
- {replx-1.1.1 → replx-1.3}/replx.egg-info/top_level.txt +0 -0
- {replx-1.1.1 → replx-1.3}/setup.cfg +0 -0
replx-1.3/PKG-INFO
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: replx
|
|
3
|
+
Version: 1.3
|
|
4
|
+
Summary: replx is a fast, modern MicroPython CLI: turbo REPL, robust file sync (put/get), project install, mpy-cross integration, and smart port discovery.
|
|
5
|
+
Author-email: "chanmin.park" <devcamp@gmail.com>
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/PlanXLab/replx
|
|
8
|
+
Project-URL: Repository, https://github.com/PlanXLab/replx
|
|
9
|
+
Project-URL: Issues, https://github.com/PlanXLab/replx/issues
|
|
10
|
+
Keywords: micropython,repl,serial,pyserial,typer,mpy-cross,deploy
|
|
11
|
+
Classifier: Environment :: Console
|
|
12
|
+
Classifier: Programming Language :: Python :: 3
|
|
13
|
+
Classifier: Programming Language :: Python :: 3 :: Only
|
|
14
|
+
Classifier: Operating System :: OS Independent
|
|
15
|
+
Classifier: Topic :: Software Development :: Embedded Systems
|
|
16
|
+
Classifier: Topic :: System :: Hardware :: Universal Serial Bus (USB)
|
|
17
|
+
Requires-Python: >=3.10
|
|
18
|
+
Description-Content-Type: text/markdown
|
|
19
|
+
License-File: LICENSE
|
|
20
|
+
Requires-Dist: typer>=0.12
|
|
21
|
+
Requires-Dist: rich>=13.0
|
|
22
|
+
Requires-Dist: pyserial>=3.5
|
|
23
|
+
Requires-Dist: mpy-cross>=1.26
|
|
24
|
+
Requires-Dist: psutil>=5.9.0
|
|
25
|
+
Dynamic: license-file
|
|
@@ -10,13 +10,13 @@ readme = "README.md"
|
|
|
10
10
|
authors = [{ name = "chanmin.park", email = "devcamp@gmail.com" }]
|
|
11
11
|
requires-python = ">=3.10"
|
|
12
12
|
keywords = ["micropython", "repl", "serial", "pyserial", "typer", "mpy-cross", "deploy"]
|
|
13
|
-
license =
|
|
13
|
+
license = "MIT"
|
|
14
|
+
license-files = ["LICENSE"]
|
|
14
15
|
|
|
15
16
|
classifiers = [
|
|
16
17
|
"Environment :: Console",
|
|
17
18
|
"Programming Language :: Python :: 3",
|
|
18
19
|
"Programming Language :: Python :: 3 :: Only",
|
|
19
|
-
"License :: OSI Approved :: MIT License",
|
|
20
20
|
"Operating System :: OS Independent",
|
|
21
21
|
"Topic :: Software Development :: Embedded Systems",
|
|
22
22
|
"Topic :: System :: Hardware :: Universal Serial Bus (USB)",
|
|
@@ -27,19 +27,26 @@ dependencies = [
|
|
|
27
27
|
"rich>=13.0",
|
|
28
28
|
"pyserial>=3.5",
|
|
29
29
|
"mpy-cross>=1.26",
|
|
30
|
+
"psutil>=5.9.0",
|
|
30
31
|
]
|
|
31
32
|
|
|
33
|
+
[project.optional-dependencies]
|
|
34
|
+
|
|
32
35
|
[project.urls]
|
|
33
36
|
Homepage = "https://github.com/PlanXLab/replx"
|
|
34
37
|
Repository = "https://github.com/PlanXLab/replx"
|
|
35
38
|
Issues = "https://github.com/PlanXLab/replx/issues"
|
|
36
39
|
|
|
37
40
|
[project.scripts]
|
|
38
|
-
replx = "replx.
|
|
41
|
+
replx = "replx.cli.app:main"
|
|
42
|
+
rx = "replx.cli.app:main"
|
|
39
43
|
|
|
40
44
|
[tool.setuptools.packages.find]
|
|
41
45
|
where = ["."]
|
|
42
46
|
include = ["replx*"]
|
|
43
47
|
|
|
48
|
+
[tool.setuptools.package-data]
|
|
49
|
+
replx = ["typehints/**/*.pyi"]
|
|
50
|
+
|
|
44
51
|
[tool.setuptools.dynamic]
|
|
45
52
|
version = { attr = "replx.__init__.__version__" }
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
from .config import (
|
|
2
|
+
RuntimeState, STATE, GLOBAL_OPTIONS,
|
|
3
|
+
ConfigManager, AgentPortManager, ConnectionResolver,
|
|
4
|
+
)
|
|
5
|
+
from replx.utils.constants import DEFAULT_AGENT_PORT, MAX_AGENT_PORT
|
|
6
|
+
from .app import app, main
|
|
7
|
+
|
|
8
|
+
__all__ = [
|
|
9
|
+
'RuntimeState', 'STATE', 'GLOBAL_OPTIONS',
|
|
10
|
+
'ConfigManager', 'AgentPortManager', 'ConnectionResolver',
|
|
11
|
+
'DEFAULT_AGENT_PORT', 'MAX_AGENT_PORT',
|
|
12
|
+
'app', 'main'
|
|
13
|
+
]
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
|
|
2
|
+
from .protocol import AgentProtocol
|
|
3
|
+
from .client import AgentClient, get_session_id, get_cached_session_id, clear_session_cache
|
|
4
|
+
from .server import AgentServer, main as agent_main
|
|
5
|
+
|
|
6
|
+
__all__ = [
|
|
7
|
+
'AgentProtocol',
|
|
8
|
+
'AgentClient',
|
|
9
|
+
'get_session_id',
|
|
10
|
+
'get_cached_session_id',
|
|
11
|
+
'clear_session_cache',
|
|
12
|
+
'AgentServer',
|
|
13
|
+
'agent_main',
|
|
14
|
+
]
|
|
@@ -0,0 +1,373 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import sys
|
|
3
|
+
import socket
|
|
4
|
+
import time
|
|
5
|
+
from typing import Dict, Any, Optional, Callable
|
|
6
|
+
|
|
7
|
+
from replx.utils.constants import DEFAULT_AGENT_PORT, AGENT_HOST, MAX_UDP_SIZE
|
|
8
|
+
from replx.commands import Cmd
|
|
9
|
+
from replx.cli.agent.protocol import AgentProtocol
|
|
10
|
+
from .session import get_cached_session_id
|
|
11
|
+
|
|
12
|
+
LOCAL_PATH_PARAMS = frozenset({'local_path', 'local'})
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class AgentClient:
|
|
16
|
+
TIMEOUT = 5.0
|
|
17
|
+
MAX_RETRIES = 3
|
|
18
|
+
|
|
19
|
+
def __init__(self, port: int = None, device_port: str = None):
|
|
20
|
+
self.agent_port = port or DEFAULT_AGENT_PORT
|
|
21
|
+
self.device_port = device_port
|
|
22
|
+
self.sock: Optional[socket.socket] = None
|
|
23
|
+
|
|
24
|
+
self._ppid = get_cached_session_id()
|
|
25
|
+
|
|
26
|
+
def connect(self):
|
|
27
|
+
if not self.sock:
|
|
28
|
+
self.sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
|
|
29
|
+
self.sock.settimeout(self.TIMEOUT)
|
|
30
|
+
|
|
31
|
+
def disconnect(self):
|
|
32
|
+
if self.sock:
|
|
33
|
+
self.sock.close()
|
|
34
|
+
self.sock = None
|
|
35
|
+
|
|
36
|
+
def send_command(self, command: str, timeout: float = None, **args) -> Dict[str, Any]:
|
|
37
|
+
if not self.sock:
|
|
38
|
+
self.connect()
|
|
39
|
+
|
|
40
|
+
effective_timeout = timeout if timeout else self.TIMEOUT
|
|
41
|
+
|
|
42
|
+
if timeout:
|
|
43
|
+
self.sock.settimeout(timeout)
|
|
44
|
+
|
|
45
|
+
port_to_use = args.pop('port', None) or self.device_port
|
|
46
|
+
|
|
47
|
+
for param in LOCAL_PATH_PARAMS:
|
|
48
|
+
if param in args and args[param]:
|
|
49
|
+
args[param] = os.path.abspath(args[param])
|
|
50
|
+
|
|
51
|
+
request = AgentProtocol.create_request(
|
|
52
|
+
command,
|
|
53
|
+
ppid=self._ppid,
|
|
54
|
+
port=port_to_use,
|
|
55
|
+
**args
|
|
56
|
+
)
|
|
57
|
+
seq = request['seq']
|
|
58
|
+
request_data = AgentProtocol.encode_message(request)
|
|
59
|
+
|
|
60
|
+
response = None
|
|
61
|
+
|
|
62
|
+
max_attempts = 1 if effective_timeout < 1.0 else self.MAX_RETRIES
|
|
63
|
+
|
|
64
|
+
for attempt in range(max_attempts):
|
|
65
|
+
try:
|
|
66
|
+
self.sock.sendto(request_data, (AGENT_HOST, self.agent_port))
|
|
67
|
+
|
|
68
|
+
start_time = time.time()
|
|
69
|
+
while time.time() - start_time < effective_timeout:
|
|
70
|
+
try:
|
|
71
|
+
data, addr = self.sock.recvfrom(MAX_UDP_SIZE)
|
|
72
|
+
|
|
73
|
+
msg = AgentProtocol.decode_message(data)
|
|
74
|
+
if not msg or msg.get('seq') != seq:
|
|
75
|
+
continue
|
|
76
|
+
|
|
77
|
+
if msg.get('type') == 'ack':
|
|
78
|
+
continue
|
|
79
|
+
|
|
80
|
+
if msg.get('type') == 'response':
|
|
81
|
+
response = msg
|
|
82
|
+
break
|
|
83
|
+
|
|
84
|
+
except socket.timeout:
|
|
85
|
+
break
|
|
86
|
+
|
|
87
|
+
if response:
|
|
88
|
+
break
|
|
89
|
+
|
|
90
|
+
if attempt < max_attempts - 1 and effective_timeout >= 1.0:
|
|
91
|
+
time.sleep(0.1 * (attempt + 1))
|
|
92
|
+
|
|
93
|
+
except socket.timeout:
|
|
94
|
+
if attempt < max_attempts - 1:
|
|
95
|
+
continue
|
|
96
|
+
raise RuntimeError(f"Agent timeout after {max_attempts} attempts")
|
|
97
|
+
|
|
98
|
+
if not response:
|
|
99
|
+
raise RuntimeError("No response from agent")
|
|
100
|
+
|
|
101
|
+
if response.get('error'):
|
|
102
|
+
raise RuntimeError(response['error'])
|
|
103
|
+
|
|
104
|
+
return response.get('result', {})
|
|
105
|
+
|
|
106
|
+
def run_interactive(self, script_path: str = None, script_content: str = None,
|
|
107
|
+
echo: bool = False,
|
|
108
|
+
output_callback: Callable[[bytes, str], None] = None,
|
|
109
|
+
input_provider: Callable[[], Optional[bytes]] = None,
|
|
110
|
+
stop_check: Callable[[], bool] = None) -> Dict[str, Any]:
|
|
111
|
+
if not self.sock:
|
|
112
|
+
self.connect()
|
|
113
|
+
|
|
114
|
+
if script_path:
|
|
115
|
+
script_path = os.path.abspath(script_path)
|
|
116
|
+
|
|
117
|
+
request = AgentProtocol.create_request(
|
|
118
|
+
'run_interactive',
|
|
119
|
+
ppid=self._ppid,
|
|
120
|
+
port=self.device_port,
|
|
121
|
+
script_path=script_path,
|
|
122
|
+
script_content=script_content,
|
|
123
|
+
echo=echo
|
|
124
|
+
)
|
|
125
|
+
seq = request['seq']
|
|
126
|
+
request_data = AgentProtocol.encode_message(request)
|
|
127
|
+
|
|
128
|
+
self.sock.sendto(request_data, (AGENT_HOST, self.agent_port))
|
|
129
|
+
|
|
130
|
+
self.sock.settimeout(5.0)
|
|
131
|
+
ack_received = False
|
|
132
|
+
error_response = None
|
|
133
|
+
|
|
134
|
+
while True:
|
|
135
|
+
try:
|
|
136
|
+
data, addr = self.sock.recvfrom(MAX_UDP_SIZE)
|
|
137
|
+
msg = AgentProtocol.decode_message(data)
|
|
138
|
+
if msg and msg.get('seq') == seq:
|
|
139
|
+
if msg.get('type') == 'ack':
|
|
140
|
+
ack_received = True
|
|
141
|
+
break
|
|
142
|
+
elif msg.get('type') == 'response' and msg.get('error'):
|
|
143
|
+
error_response = msg
|
|
144
|
+
break
|
|
145
|
+
except socket.timeout:
|
|
146
|
+
break
|
|
147
|
+
|
|
148
|
+
if error_response:
|
|
149
|
+
raise RuntimeError(error_response['error'])
|
|
150
|
+
|
|
151
|
+
if not ack_received:
|
|
152
|
+
raise RuntimeError("No ACK from agent - run_interactive failed to start")
|
|
153
|
+
|
|
154
|
+
self.sock.settimeout(0.01)
|
|
155
|
+
input_interval = 0.001
|
|
156
|
+
last_input_time = 0
|
|
157
|
+
error_check_until = time.time() + 0.1
|
|
158
|
+
|
|
159
|
+
try:
|
|
160
|
+
while True:
|
|
161
|
+
if stop_check and stop_check():
|
|
162
|
+
try:
|
|
163
|
+
self.send_command(Cmd.RUN_STOP, timeout=0.5)
|
|
164
|
+
except Exception:
|
|
165
|
+
pass
|
|
166
|
+
break
|
|
167
|
+
|
|
168
|
+
now = time.time()
|
|
169
|
+
|
|
170
|
+
if now - last_input_time >= input_interval:
|
|
171
|
+
last_input_time = now
|
|
172
|
+
if input_provider:
|
|
173
|
+
try:
|
|
174
|
+
input_data = input_provider()
|
|
175
|
+
if input_data:
|
|
176
|
+
input_msg = AgentProtocol.create_input(seq, input_data, ppid=self._ppid, port=self.device_port)
|
|
177
|
+
input_data_encoded = AgentProtocol.encode_message(input_msg)
|
|
178
|
+
self.sock.sendto(input_data_encoded, (AGENT_HOST, self.agent_port))
|
|
179
|
+
except Exception:
|
|
180
|
+
pass
|
|
181
|
+
|
|
182
|
+
try:
|
|
183
|
+
data, addr = self.sock.recvfrom(MAX_UDP_SIZE)
|
|
184
|
+
msg = AgentProtocol.decode_message(data)
|
|
185
|
+
|
|
186
|
+
if msg and msg.get('seq') == seq:
|
|
187
|
+
if now < error_check_until and msg.get('type') == 'response' and msg.get('error'):
|
|
188
|
+
raise RuntimeError(msg['error'])
|
|
189
|
+
|
|
190
|
+
if msg.get('type') == 'stream':
|
|
191
|
+
output = msg.get('output', '')
|
|
192
|
+
if output and output_callback:
|
|
193
|
+
output_callback(output.encode('utf-8'), 'stdout')
|
|
194
|
+
|
|
195
|
+
if msg.get('completed'):
|
|
196
|
+
error = msg.get('error')
|
|
197
|
+
if error and output_callback:
|
|
198
|
+
output_callback(error.encode('utf-8'), 'stderr')
|
|
199
|
+
break
|
|
200
|
+
|
|
201
|
+
except socket.timeout:
|
|
202
|
+
pass
|
|
203
|
+
except Exception:
|
|
204
|
+
pass
|
|
205
|
+
|
|
206
|
+
except KeyboardInterrupt:
|
|
207
|
+
try:
|
|
208
|
+
self.send_command(Cmd.RUN_STOP, timeout=0.5)
|
|
209
|
+
except Exception:
|
|
210
|
+
pass
|
|
211
|
+
raise
|
|
212
|
+
|
|
213
|
+
self.sock.settimeout(self.TIMEOUT)
|
|
214
|
+
|
|
215
|
+
return {"run": True, "completed": True}
|
|
216
|
+
|
|
217
|
+
def send_command_streaming(self, command: str, timeout: float = None,
|
|
218
|
+
progress_callback: Callable[[Dict[str, Any]], None] = None,
|
|
219
|
+
**args) -> Dict[str, Any]:
|
|
220
|
+
if not self.sock:
|
|
221
|
+
self.connect()
|
|
222
|
+
|
|
223
|
+
effective_timeout = timeout if timeout else 60.0
|
|
224
|
+
|
|
225
|
+
port_to_use = args.pop('port', None) or self.device_port
|
|
226
|
+
|
|
227
|
+
for param in LOCAL_PATH_PARAMS:
|
|
228
|
+
if param in args and args[param]:
|
|
229
|
+
args[param] = os.path.abspath(args[param])
|
|
230
|
+
|
|
231
|
+
request = AgentProtocol.create_request(
|
|
232
|
+
command,
|
|
233
|
+
ppid=self._ppid,
|
|
234
|
+
port=port_to_use,
|
|
235
|
+
**args
|
|
236
|
+
)
|
|
237
|
+
seq = request['seq']
|
|
238
|
+
request_data = AgentProtocol.encode_message(request)
|
|
239
|
+
|
|
240
|
+
self.sock.sendto(request_data, (AGENT_HOST, self.agent_port))
|
|
241
|
+
|
|
242
|
+
self.sock.settimeout(0.1)
|
|
243
|
+
|
|
244
|
+
ack_received = False
|
|
245
|
+
response = None
|
|
246
|
+
start_time = time.time()
|
|
247
|
+
|
|
248
|
+
while time.time() - start_time < effective_timeout:
|
|
249
|
+
try:
|
|
250
|
+
data, addr = self.sock.recvfrom(MAX_UDP_SIZE)
|
|
251
|
+
msg = AgentProtocol.decode_message(data)
|
|
252
|
+
|
|
253
|
+
if not msg or msg.get('seq') != seq:
|
|
254
|
+
continue
|
|
255
|
+
|
|
256
|
+
msg_type = msg.get('type')
|
|
257
|
+
|
|
258
|
+
if msg_type == 'ack':
|
|
259
|
+
ack_received = True
|
|
260
|
+
continue
|
|
261
|
+
|
|
262
|
+
elif msg_type == 'stream':
|
|
263
|
+
if progress_callback:
|
|
264
|
+
stream_data = msg.get('data', {})
|
|
265
|
+
progress_callback(stream_data)
|
|
266
|
+
continue
|
|
267
|
+
|
|
268
|
+
elif msg_type == 'response':
|
|
269
|
+
response = msg
|
|
270
|
+
break
|
|
271
|
+
|
|
272
|
+
except socket.timeout:
|
|
273
|
+
if not ack_received and time.time() - start_time > 5.0:
|
|
274
|
+
raise RuntimeError("No response from agent")
|
|
275
|
+
continue
|
|
276
|
+
except Exception as e:
|
|
277
|
+
raise RuntimeError(f"Communication error: {e}")
|
|
278
|
+
|
|
279
|
+
self.sock.settimeout(self.TIMEOUT)
|
|
280
|
+
|
|
281
|
+
if not response:
|
|
282
|
+
raise RuntimeError("No response from agent (timeout)")
|
|
283
|
+
|
|
284
|
+
if response.get('error'):
|
|
285
|
+
raise RuntimeError(response['error'])
|
|
286
|
+
|
|
287
|
+
return response.get('result', {})
|
|
288
|
+
|
|
289
|
+
def ping(self) -> bool:
|
|
290
|
+
try:
|
|
291
|
+
result = self.send_command(Cmd.PING, timeout=0.3)
|
|
292
|
+
return result.get('pong', False)
|
|
293
|
+
except Exception:
|
|
294
|
+
return False
|
|
295
|
+
|
|
296
|
+
def __enter__(self):
|
|
297
|
+
self.connect()
|
|
298
|
+
return self
|
|
299
|
+
|
|
300
|
+
def __exit__(self, _exc_type, _exc_val, _exc_tb):
|
|
301
|
+
self.disconnect()
|
|
302
|
+
|
|
303
|
+
@staticmethod
|
|
304
|
+
def is_agent_running(port: int = None) -> bool:
|
|
305
|
+
try:
|
|
306
|
+
client = AgentClient(port=port)
|
|
307
|
+
return client.ping()
|
|
308
|
+
except Exception:
|
|
309
|
+
return False
|
|
310
|
+
|
|
311
|
+
@staticmethod
|
|
312
|
+
def start_agent(port: int = None, background: bool = True) -> bool:
|
|
313
|
+
if AgentClient.is_agent_running(port=port):
|
|
314
|
+
return False
|
|
315
|
+
|
|
316
|
+
import subprocess
|
|
317
|
+
python_exe = sys.executable
|
|
318
|
+
agent_module = 'replx.cli.agent.server'
|
|
319
|
+
|
|
320
|
+
cmd = [python_exe, '-m', agent_module]
|
|
321
|
+
if port:
|
|
322
|
+
cmd.append(str(port))
|
|
323
|
+
|
|
324
|
+
if background:
|
|
325
|
+
if sys.platform == 'win32':
|
|
326
|
+
startupinfo = subprocess.STARTUPINFO()
|
|
327
|
+
startupinfo.dwFlags |= subprocess.STARTF_USESHOWWINDOW
|
|
328
|
+
startupinfo.wShowWindow = 0
|
|
329
|
+
|
|
330
|
+
subprocess.Popen(
|
|
331
|
+
cmd,
|
|
332
|
+
creationflags=subprocess.CREATE_NEW_PROCESS_GROUP | subprocess.DETACHED_PROCESS,
|
|
333
|
+
stdout=subprocess.DEVNULL,
|
|
334
|
+
stderr=subprocess.DEVNULL,
|
|
335
|
+
startupinfo=startupinfo
|
|
336
|
+
)
|
|
337
|
+
else:
|
|
338
|
+
subprocess.Popen(
|
|
339
|
+
cmd,
|
|
340
|
+
stdout=subprocess.DEVNULL,
|
|
341
|
+
stderr=subprocess.DEVNULL,
|
|
342
|
+
stdin=subprocess.DEVNULL,
|
|
343
|
+
start_new_session=True,
|
|
344
|
+
close_fds=True
|
|
345
|
+
)
|
|
346
|
+
else:
|
|
347
|
+
subprocess.Popen(cmd)
|
|
348
|
+
|
|
349
|
+
for i in range(30):
|
|
350
|
+
time.sleep(0.1)
|
|
351
|
+
if AgentClient.is_agent_running(port=port):
|
|
352
|
+
return True
|
|
353
|
+
|
|
354
|
+
raise RuntimeError("Failed to start agent (timeout)")
|
|
355
|
+
|
|
356
|
+
@staticmethod
|
|
357
|
+
def stop_agent(port: int = None, timeout: float = 1.5) -> bool:
|
|
358
|
+
if not AgentClient.is_agent_running(port=port):
|
|
359
|
+
return False
|
|
360
|
+
|
|
361
|
+
try:
|
|
362
|
+
client = AgentClient(port=port)
|
|
363
|
+
client.send_command(Cmd.SHUTDOWN, timeout=0.5)
|
|
364
|
+
except Exception:
|
|
365
|
+
pass
|
|
366
|
+
|
|
367
|
+
start_time = time.time()
|
|
368
|
+
while time.time() - start_time < timeout:
|
|
369
|
+
time.sleep(0.05)
|
|
370
|
+
if not AgentClient.is_agent_running(port=port):
|
|
371
|
+
return True
|
|
372
|
+
|
|
373
|
+
return not AgentClient.is_agent_running(port=port)
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
import os
|
|
2
|
+
from typing import Optional
|
|
3
|
+
import psutil
|
|
4
|
+
|
|
5
|
+
def _find_terminal_process() -> Optional[dict]:
|
|
6
|
+
shell_names = {
|
|
7
|
+
'powershell.exe', 'pwsh.exe', 'cmd.exe', 'bash.exe', 'zsh.exe', 'sh.exe', 'fish.exe',
|
|
8
|
+
'windowsterminal.exe',
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
ide_names = {
|
|
12
|
+
'code.exe',
|
|
13
|
+
'conemu64.exe', 'conemu.exe',
|
|
14
|
+
'pycharm.exe', 'pycharm64.exe', 'idea.exe', 'idea64.exe',
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
try:
|
|
18
|
+
parent_pid = os.getppid()
|
|
19
|
+
if parent_pid and parent_pid > 0:
|
|
20
|
+
parent = psutil.Process(parent_pid)
|
|
21
|
+
pname = (parent.name() or '').lower()
|
|
22
|
+
if pname in shell_names:
|
|
23
|
+
return {
|
|
24
|
+
'pid': parent.pid,
|
|
25
|
+
'name': parent.name(),
|
|
26
|
+
'create_time': parent.create_time(),
|
|
27
|
+
'cwd': None,
|
|
28
|
+
'level': 0,
|
|
29
|
+
}
|
|
30
|
+
except (psutil.NoSuchProcess, psutil.AccessDenied, psutil.ZombieProcess, OSError):
|
|
31
|
+
pass
|
|
32
|
+
|
|
33
|
+
try:
|
|
34
|
+
current = psutil.Process()
|
|
35
|
+
best_ide = None
|
|
36
|
+
|
|
37
|
+
for level in range(12):
|
|
38
|
+
if current is None:
|
|
39
|
+
break
|
|
40
|
+
|
|
41
|
+
try:
|
|
42
|
+
name = (current.name() or '').lower()
|
|
43
|
+
except (psutil.NoSuchProcess, psutil.AccessDenied, psutil.ZombieProcess):
|
|
44
|
+
break
|
|
45
|
+
|
|
46
|
+
if name in shell_names:
|
|
47
|
+
return {
|
|
48
|
+
'pid': current.pid,
|
|
49
|
+
'name': current.name(),
|
|
50
|
+
'create_time': current.create_time(),
|
|
51
|
+
'cwd': None,
|
|
52
|
+
'level': level,
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
if name in ide_names and best_ide is None:
|
|
56
|
+
try:
|
|
57
|
+
best_ide = {
|
|
58
|
+
'pid': current.pid,
|
|
59
|
+
'name': current.name(),
|
|
60
|
+
'create_time': current.create_time(),
|
|
61
|
+
'cwd': None,
|
|
62
|
+
'level': level,
|
|
63
|
+
}
|
|
64
|
+
except Exception:
|
|
65
|
+
best_ide = None
|
|
66
|
+
|
|
67
|
+
try:
|
|
68
|
+
if current.ppid() == 0:
|
|
69
|
+
break
|
|
70
|
+
except Exception:
|
|
71
|
+
break
|
|
72
|
+
|
|
73
|
+
try:
|
|
74
|
+
parent = current.parent()
|
|
75
|
+
except Exception:
|
|
76
|
+
parent = None
|
|
77
|
+
|
|
78
|
+
if parent is None:
|
|
79
|
+
break
|
|
80
|
+
current = parent
|
|
81
|
+
|
|
82
|
+
if best_ide is not None:
|
|
83
|
+
return best_ide
|
|
84
|
+
|
|
85
|
+
except Exception:
|
|
86
|
+
pass
|
|
87
|
+
|
|
88
|
+
return None
|
|
89
|
+
|
|
90
|
+
def _find_jupyter_kernel() -> Optional[dict]:
|
|
91
|
+
try:
|
|
92
|
+
current = psutil.Process()
|
|
93
|
+
for level in range(10):
|
|
94
|
+
if current is None:
|
|
95
|
+
break
|
|
96
|
+
|
|
97
|
+
cmdline = ' '.join(current.cmdline()).lower()
|
|
98
|
+
|
|
99
|
+
if any(keyword in cmdline for keyword in ['jupyter', 'ipykernel', 'ipython']):
|
|
100
|
+
return {
|
|
101
|
+
'pid': current.pid,
|
|
102
|
+
'name': current.name(),
|
|
103
|
+
'cmdline': cmdline,
|
|
104
|
+
'level': level
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
if current.ppid() == 0:
|
|
108
|
+
break
|
|
109
|
+
|
|
110
|
+
parent = current.parent()
|
|
111
|
+
if parent is None:
|
|
112
|
+
break
|
|
113
|
+
current = parent
|
|
114
|
+
|
|
115
|
+
except (psutil.NoSuchProcess, psutil.AccessDenied, psutil.ZombieProcess):
|
|
116
|
+
pass
|
|
117
|
+
|
|
118
|
+
return None
|
|
119
|
+
|
|
120
|
+
def get_session_id() -> int:
|
|
121
|
+
terminal = _find_terminal_process()
|
|
122
|
+
if terminal:
|
|
123
|
+
return terminal['pid']
|
|
124
|
+
|
|
125
|
+
jupyter = _find_jupyter_kernel()
|
|
126
|
+
if jupyter:
|
|
127
|
+
return jupyter['pid']
|
|
128
|
+
|
|
129
|
+
ppid = os.getppid()
|
|
130
|
+
if ppid and ppid > 0:
|
|
131
|
+
return ppid
|
|
132
|
+
|
|
133
|
+
workspace_hash = abs(hash(os.getcwd())) % (10**8)
|
|
134
|
+
return workspace_hash
|
|
135
|
+
|
|
136
|
+
_session_id_cache: Optional[int] = None
|
|
137
|
+
|
|
138
|
+
def get_cached_session_id() -> int:
|
|
139
|
+
global _session_id_cache
|
|
140
|
+
|
|
141
|
+
if _session_id_cache is None:
|
|
142
|
+
_session_id_cache = get_session_id()
|
|
143
|
+
|
|
144
|
+
return _session_id_cache
|
|
145
|
+
|
|
146
|
+
def clear_session_cache():
|
|
147
|
+
global _session_id_cache
|
|
148
|
+
_session_id_cache = None
|