replx-agent 2.0.0__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.
Potentially problematic release.
This version of replx-agent might be problematic. Click here for more details.
- replx_agent/__init__.py +3 -0
- replx_agent/__main__.py +5 -0
- replx_agent/commands.py +131 -0
- replx_agent/helpers/__init__.py +3 -0
- replx_agent/helpers/scanner.py +308 -0
- replx_agent/pkg/__init__.py +11 -0
- replx_agent/pkg/compiler.py +153 -0
- replx_agent/pkg/registry.py +387 -0
- replx_agent/pkg/service.py +690 -0
- replx_agent/pkg/store.py +83 -0
- replx_agent/protocol/__init__.py +11 -0
- replx_agent/protocol/repl.py +1029 -0
- replx_agent/protocol/storage.py +1018 -0
- replx_agent/rpc/__init__.py +3 -0
- replx_agent/rpc/protocol.py +220 -0
- replx_agent/server/__init__.py +3 -0
- replx_agent/server/__main__.py +4 -0
- replx_agent/server/command_dispatcher.py +49 -0
- replx_agent/server/connection_manager.py +654 -0
- replx_agent/server/core.py +1221 -0
- replx_agent/server/handlers/__init__.py +24 -0
- replx_agent/server/handlers/exec.py +553 -0
- replx_agent/server/handlers/filesystem.py +383 -0
- replx_agent/server/handlers/firmware.py +118 -0
- replx_agent/server/handlers/i2c.py +23 -0
- replx_agent/server/handlers/package.py +182 -0
- replx_agent/server/handlers/repl.py +188 -0
- replx_agent/server/handlers/session.py +205 -0
- replx_agent/server/handlers/spi.py +23 -0
- replx_agent/server/handlers/transfer.py +600 -0
- replx_agent/server/handlers/uart.py +32 -0
- replx_agent/server/session_manager.py +391 -0
- replx_agent/server/tcp.py +207 -0
- replx_agent/terminal.py +566 -0
- replx_agent/transport/__init__.py +32 -0
- replx_agent/transport/serial.py +220 -0
- replx_agent/utils/__init__.py +95 -0
- replx_agent/utils/constants.py +78 -0
- replx_agent/utils/device_info.py +183 -0
- replx_agent/utils/exceptions.py +31 -0
- replx_agent-2.0.0.dist-info/METADATA +74 -0
- replx_agent-2.0.0.dist-info/RECORD +46 -0
- replx_agent-2.0.0.dist-info/WHEEL +5 -0
- replx_agent-2.0.0.dist-info/entry_points.txt +2 -0
- replx_agent-2.0.0.dist-info/licenses/LICENSE +21 -0
- replx_agent-2.0.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,220 @@
|
|
|
1
|
+
import base64
|
|
2
|
+
import json
|
|
3
|
+
import socket
|
|
4
|
+
import struct
|
|
5
|
+
import threading
|
|
6
|
+
import time
|
|
7
|
+
from enum import IntEnum
|
|
8
|
+
from typing import Any, Dict, List, Optional
|
|
9
|
+
|
|
10
|
+
from replx_agent.utils.constants import MAX_PAYLOAD_SIZE
|
|
11
|
+
|
|
12
|
+
_MAGIC = b"RPLX"
|
|
13
|
+
_VERSION = 2
|
|
14
|
+
_HEADER = struct.Struct("!4sBBII")
|
|
15
|
+
_MAX_BINARY_FRAME_SIZE = 256 * 1024
|
|
16
|
+
_READ_SIZE = 64 * 1024
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class FrameType(IntEnum):
|
|
20
|
+
JSON = 1
|
|
21
|
+
BINARY = 2
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
_seq_lock = threading.Lock()
|
|
25
|
+
_seq_counter = int(time.time() * 1000000) & 0xFFFFFFFE
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def _next_seq() -> int:
|
|
29
|
+
global _seq_counter
|
|
30
|
+
with _seq_lock:
|
|
31
|
+
_seq_counter = (_seq_counter + 1) & 0xFFFFFFFF
|
|
32
|
+
return _seq_counter
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
class FrameReader:
|
|
36
|
+
def __init__(self, sock: socket.socket):
|
|
37
|
+
self._sock = sock
|
|
38
|
+
self._buffer = bytearray()
|
|
39
|
+
|
|
40
|
+
def _extract_frame(self) -> Optional[bytes]:
|
|
41
|
+
if len(self._buffer) < _HEADER.size:
|
|
42
|
+
return None
|
|
43
|
+
|
|
44
|
+
magic, version, frame_type, _seq, length = _HEADER.unpack_from(self._buffer)
|
|
45
|
+
if magic != _MAGIC or version != _VERSION:
|
|
46
|
+
raise ValueError("Invalid agent frame header")
|
|
47
|
+
try:
|
|
48
|
+
kind = FrameType(frame_type)
|
|
49
|
+
except ValueError as exc:
|
|
50
|
+
raise ValueError(f"Unknown agent frame type: {frame_type}") from exc
|
|
51
|
+
|
|
52
|
+
maximum = MAX_PAYLOAD_SIZE if kind is FrameType.JSON else _MAX_BINARY_FRAME_SIZE
|
|
53
|
+
if length > maximum:
|
|
54
|
+
raise ValueError(f"Agent frame exceeds {maximum} bytes")
|
|
55
|
+
|
|
56
|
+
frame_size = _HEADER.size + length
|
|
57
|
+
if len(self._buffer) < frame_size:
|
|
58
|
+
return None
|
|
59
|
+
|
|
60
|
+
frame = bytes(self._buffer[:frame_size])
|
|
61
|
+
del self._buffer[:frame_size]
|
|
62
|
+
return frame
|
|
63
|
+
|
|
64
|
+
def receive(self, timeout: Optional[float] = None) -> Optional[bytes]:
|
|
65
|
+
self._sock.settimeout(timeout)
|
|
66
|
+
while True:
|
|
67
|
+
frame = self._extract_frame()
|
|
68
|
+
if frame is not None:
|
|
69
|
+
return frame
|
|
70
|
+
|
|
71
|
+
try:
|
|
72
|
+
chunk = self._sock.recv(_READ_SIZE)
|
|
73
|
+
except socket.timeout:
|
|
74
|
+
return None
|
|
75
|
+
|
|
76
|
+
if not chunk:
|
|
77
|
+
if self._buffer:
|
|
78
|
+
raise ConnectionError("Agent closed the connection mid-frame")
|
|
79
|
+
raise ConnectionError("Agent closed the connection")
|
|
80
|
+
self._buffer.extend(chunk)
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
class AgentProtocol:
|
|
84
|
+
@staticmethod
|
|
85
|
+
def _encode_frame(frame_type: FrameType, seq: int, payload: bytes) -> bytes:
|
|
86
|
+
maximum = MAX_PAYLOAD_SIZE if frame_type is FrameType.JSON else _MAX_BINARY_FRAME_SIZE
|
|
87
|
+
if len(payload) > maximum:
|
|
88
|
+
raise ValueError(f"Agent frame too large: {len(payload)} bytes")
|
|
89
|
+
return _HEADER.pack(_MAGIC, _VERSION, int(frame_type), seq & 0xFFFFFFFF, len(payload)) + payload
|
|
90
|
+
|
|
91
|
+
@staticmethod
|
|
92
|
+
def encode_message(msg: Dict[str, Any]) -> bytes:
|
|
93
|
+
payload = json.dumps(msg, separators=(",", ":")).encode("utf-8")
|
|
94
|
+
return AgentProtocol._encode_frame(
|
|
95
|
+
FrameType.JSON, int(msg.get("seq", 0)), payload
|
|
96
|
+
)
|
|
97
|
+
|
|
98
|
+
@staticmethod
|
|
99
|
+
def encode_binary(seq: int, payload: bytes) -> bytes:
|
|
100
|
+
return AgentProtocol._encode_frame(FrameType.BINARY, seq, payload)
|
|
101
|
+
|
|
102
|
+
@staticmethod
|
|
103
|
+
def decode_frame(data: bytes) -> tuple[FrameType, int, bytes]:
|
|
104
|
+
if len(data) < _HEADER.size:
|
|
105
|
+
raise ValueError("Agent frame header is incomplete")
|
|
106
|
+
|
|
107
|
+
magic, version, frame_type, seq, length = _HEADER.unpack_from(data)
|
|
108
|
+
if magic != _MAGIC or version != _VERSION:
|
|
109
|
+
raise ValueError("Invalid agent frame header")
|
|
110
|
+
try:
|
|
111
|
+
kind = FrameType(frame_type)
|
|
112
|
+
except ValueError as exc:
|
|
113
|
+
raise ValueError(f"Unknown agent frame type: {frame_type}") from exc
|
|
114
|
+
|
|
115
|
+
maximum = MAX_PAYLOAD_SIZE if kind is FrameType.JSON else _MAX_BINARY_FRAME_SIZE
|
|
116
|
+
if length > maximum:
|
|
117
|
+
raise ValueError(f"Agent frame exceeds {maximum} bytes")
|
|
118
|
+
if len(data) != _HEADER.size + length:
|
|
119
|
+
raise ValueError("Agent frame length does not match its header")
|
|
120
|
+
|
|
121
|
+
return kind, seq, data[_HEADER.size:]
|
|
122
|
+
|
|
123
|
+
@staticmethod
|
|
124
|
+
def decode_message(data: bytes) -> Optional[Dict[str, Any]]:
|
|
125
|
+
try:
|
|
126
|
+
frame_type, _seq, payload = AgentProtocol.decode_frame(data)
|
|
127
|
+
if frame_type is not FrameType.JSON:
|
|
128
|
+
return None
|
|
129
|
+
message = json.loads(payload.decode("utf-8"))
|
|
130
|
+
return message if isinstance(message, dict) else None
|
|
131
|
+
except (UnicodeDecodeError, ValueError, json.JSONDecodeError):
|
|
132
|
+
return None
|
|
133
|
+
|
|
134
|
+
@staticmethod
|
|
135
|
+
def create_request(
|
|
136
|
+
command: str,
|
|
137
|
+
seq: int = None,
|
|
138
|
+
ppid: int = None,
|
|
139
|
+
port: str = None,
|
|
140
|
+
auth_token: str = None,
|
|
141
|
+
**args,
|
|
142
|
+
) -> Dict[str, Any]:
|
|
143
|
+
if seq is None:
|
|
144
|
+
seq = _next_seq()
|
|
145
|
+
|
|
146
|
+
request = {
|
|
147
|
+
"seq": seq,
|
|
148
|
+
"type": "request",
|
|
149
|
+
"command": command,
|
|
150
|
+
"args": args,
|
|
151
|
+
}
|
|
152
|
+
if ppid is not None:
|
|
153
|
+
request["ppid"] = ppid
|
|
154
|
+
if port is not None:
|
|
155
|
+
request["port"] = port
|
|
156
|
+
if auth_token:
|
|
157
|
+
request["auth_token"] = auth_token
|
|
158
|
+
return request
|
|
159
|
+
|
|
160
|
+
@staticmethod
|
|
161
|
+
def create_response(seq: int, result: Any = None, error: str = None) -> Dict[str, Any]:
|
|
162
|
+
return {
|
|
163
|
+
"seq": seq,
|
|
164
|
+
"type": "response",
|
|
165
|
+
"result": result,
|
|
166
|
+
"error": error,
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
@staticmethod
|
|
170
|
+
def create_ack(seq: int) -> Dict[str, Any]:
|
|
171
|
+
return {"seq": seq, "type": "ack"}
|
|
172
|
+
|
|
173
|
+
@staticmethod
|
|
174
|
+
def create_stream(seq: int, data: bytes, stream_type: str = "stdout") -> Dict[str, Any]:
|
|
175
|
+
return {
|
|
176
|
+
"seq": seq,
|
|
177
|
+
"type": "stream",
|
|
178
|
+
"data": base64.b64encode(data).decode("ascii"),
|
|
179
|
+
"stream_type": stream_type,
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
@staticmethod
|
|
183
|
+
def create_progress_stream(seq: int, progress_data: Dict[str, Any]) -> Dict[str, Any]:
|
|
184
|
+
return {
|
|
185
|
+
"seq": seq,
|
|
186
|
+
"type": "stream",
|
|
187
|
+
"data": progress_data,
|
|
188
|
+
"stream_type": "progress",
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
@staticmethod
|
|
192
|
+
def create_input(seq: int, data: bytes, ppid: int = None, port: str = None) -> Dict[str, Any]:
|
|
193
|
+
message = {
|
|
194
|
+
"seq": seq,
|
|
195
|
+
"type": "input",
|
|
196
|
+
"data": base64.b64encode(data).decode("ascii"),
|
|
197
|
+
}
|
|
198
|
+
if ppid is not None:
|
|
199
|
+
message["ppid"] = ppid
|
|
200
|
+
if port is not None:
|
|
201
|
+
message["port"] = port
|
|
202
|
+
return message
|
|
203
|
+
|
|
204
|
+
@staticmethod
|
|
205
|
+
def create_stream_ack(seq: int) -> Dict[str, Any]:
|
|
206
|
+
return {"seq": seq, "type": "stream_ack"}
|
|
207
|
+
|
|
208
|
+
@staticmethod
|
|
209
|
+
def decode_stream_data(msg: Dict[str, Any]) -> bytes:
|
|
210
|
+
data_b64 = msg.get("data", "")
|
|
211
|
+
return base64.b64decode(data_b64) if data_b64 else b""
|
|
212
|
+
|
|
213
|
+
@staticmethod
|
|
214
|
+
def chunk_large_data(data: str, max_size: int = MAX_PAYLOAD_SIZE) -> List[str]:
|
|
215
|
+
data_bytes = data.encode("utf-8")
|
|
216
|
+
chunk_size = max_size - 100
|
|
217
|
+
return [
|
|
218
|
+
data_bytes[offset:offset + chunk_size].decode("utf-8", errors="ignore")
|
|
219
|
+
for offset in range(0, len(data_bytes), chunk_size)
|
|
220
|
+
]
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
from typing import Optional, Hashable
|
|
2
|
+
|
|
3
|
+
from .connection_manager import BoardConnection
|
|
4
|
+
from replx_agent.commands import CmdGroups
|
|
5
|
+
|
|
6
|
+
class CommandContext:
|
|
7
|
+
def __init__(
|
|
8
|
+
self,
|
|
9
|
+
connection: Optional[BoardConnection] = None,
|
|
10
|
+
ppid: Optional[Hashable] = None,
|
|
11
|
+
sid: Optional[int] = None,
|
|
12
|
+
client_ip: Optional[str] = None,
|
|
13
|
+
explicit_port: Optional[str] = None,
|
|
14
|
+
seq: int = 0,
|
|
15
|
+
client_addr: tuple = None
|
|
16
|
+
):
|
|
17
|
+
self.connection = connection
|
|
18
|
+
self.ppid = ppid
|
|
19
|
+
self.sid = sid if sid is not None else (ppid[1] if isinstance(ppid, tuple) else ppid)
|
|
20
|
+
address = getattr(client_addr, 'address', client_addr)
|
|
21
|
+
self.client_ip = client_ip or (address[0] if address else None)
|
|
22
|
+
self.explicit_port = explicit_port
|
|
23
|
+
self.seq = seq
|
|
24
|
+
self.client_addr = client_addr
|
|
25
|
+
|
|
26
|
+
if connection:
|
|
27
|
+
self.repl_protocol = connection.repl_protocol
|
|
28
|
+
self.file_system = connection.file_system
|
|
29
|
+
self.port = connection.port
|
|
30
|
+
self.core = connection.core
|
|
31
|
+
self.device = connection.device
|
|
32
|
+
self.manufacturer = connection.manufacturer
|
|
33
|
+
self.version = connection.version
|
|
34
|
+
self.device_root_fs = connection.device_root_fs
|
|
35
|
+
else:
|
|
36
|
+
self.repl_protocol = None
|
|
37
|
+
self.file_system = None
|
|
38
|
+
self.port = ""
|
|
39
|
+
self.core = ""
|
|
40
|
+
self.device = ""
|
|
41
|
+
self.manufacturer = "?"
|
|
42
|
+
self.version = "?"
|
|
43
|
+
self.device_root_fs = "/"
|
|
44
|
+
|
|
45
|
+
NON_REPL_COMMANDS = CmdGroups.NON_REPL
|
|
46
|
+
READ_ONLY_COMMANDS = CmdGroups.READ_ONLY
|
|
47
|
+
SESSION_COMMANDS = CmdGroups.SESSION
|
|
48
|
+
PERSISTENT_BUSY_COMMANDS = CmdGroups.PERSISTENT_BUSY
|
|
49
|
+
STREAMING_COMMANDS = CmdGroups.STREAMING
|