portacode 0.2.2__tar.gz → 0.2.4.dev0__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.
- {portacode-0.2.2 → portacode-0.2.4.dev0}/PKG-INFO +1 -1
- {portacode-0.2.2 → portacode-0.2.4.dev0}/portacode/_version.py +2 -2
- portacode-0.2.4.dev0/portacode/connection/handlers/README.md +427 -0
- portacode-0.2.4.dev0/portacode/connection/handlers/__init__.py +41 -0
- portacode-0.2.4.dev0/portacode/connection/handlers/base.py +115 -0
- portacode-0.2.4.dev0/portacode/connection/handlers/file_handlers.py +209 -0
- portacode-0.2.4.dev0/portacode/connection/handlers/registry.py +111 -0
- portacode-0.2.4.dev0/portacode/connection/handlers/session.py +269 -0
- portacode-0.2.4.dev0/portacode/connection/handlers/system_handlers.py +32 -0
- portacode-0.2.4.dev0/portacode/connection/handlers/terminal_handlers.py +139 -0
- {portacode-0.2.2 → portacode-0.2.4.dev0}/portacode/connection/multiplex.py +7 -7
- portacode-0.2.4.dev0/portacode/connection/terminal.py +177 -0
- {portacode-0.2.2 → portacode-0.2.4.dev0}/portacode.egg-info/PKG-INFO +1 -1
- {portacode-0.2.2 → portacode-0.2.4.dev0}/portacode.egg-info/SOURCES.txt +9 -1
- portacode-0.2.2/portacode/connection/terminal.py +0 -501
- {portacode-0.2.2 → portacode-0.2.4.dev0}/.gitignore +0 -0
- {portacode-0.2.2 → portacode-0.2.4.dev0}/.gitmodules +0 -0
- {portacode-0.2.2 → portacode-0.2.4.dev0}/MANIFEST.in +0 -0
- {portacode-0.2.2 → portacode-0.2.4.dev0}/Makefile +0 -0
- {portacode-0.2.2 → portacode-0.2.4.dev0}/README.md +0 -0
- {portacode-0.2.2 → portacode-0.2.4.dev0}/docker-compose.yaml +0 -0
- {portacode-0.2.2 → portacode-0.2.4.dev0}/portacode/README.md +0 -0
- {portacode-0.2.2 → portacode-0.2.4.dev0}/portacode/__init__.py +0 -0
- {portacode-0.2.2 → portacode-0.2.4.dev0}/portacode/__main__.py +0 -0
- {portacode-0.2.2 → portacode-0.2.4.dev0}/portacode/cli.py +0 -0
- {portacode-0.2.2 → portacode-0.2.4.dev0}/portacode/connection/README.md +0 -0
- {portacode-0.2.2 → portacode-0.2.4.dev0}/portacode/connection/__init__.py +0 -0
- {portacode-0.2.2 → portacode-0.2.4.dev0}/portacode/connection/client.py +0 -0
- {portacode-0.2.2 → portacode-0.2.4.dev0}/portacode/data.py +0 -0
- {portacode-0.2.2 → portacode-0.2.4.dev0}/portacode/keypair.py +0 -0
- {portacode-0.2.2 → portacode-0.2.4.dev0}/portacode/service.py +0 -0
- {portacode-0.2.2 → portacode-0.2.4.dev0}/portacode.egg-info/dependency_links.txt +0 -0
- {portacode-0.2.2 → portacode-0.2.4.dev0}/portacode.egg-info/entry_points.txt +0 -0
- {portacode-0.2.2 → portacode-0.2.4.dev0}/portacode.egg-info/requires.txt +0 -0
- {portacode-0.2.2 → portacode-0.2.4.dev0}/portacode.egg-info/top_level.txt +0 -0
- {portacode-0.2.2 → portacode-0.2.4.dev0}/pyproject.toml +0 -0
- {portacode-0.2.2 → portacode-0.2.4.dev0}/setup.cfg +0 -0
- {portacode-0.2.2 → portacode-0.2.4.dev0}/setup.py +0 -0
|
@@ -17,5 +17,5 @@ __version__: str
|
|
|
17
17
|
__version_tuple__: VERSION_TUPLE
|
|
18
18
|
version_tuple: VERSION_TUPLE
|
|
19
19
|
|
|
20
|
-
__version__ = version = '0.2.
|
|
21
|
-
__version_tuple__ = version_tuple = (0, 2,
|
|
20
|
+
__version__ = version = '0.2.4.dev'
|
|
21
|
+
__version_tuple__ = version_tuple = (0, 2, 4, 'dev0')
|
|
@@ -0,0 +1,427 @@
|
|
|
1
|
+
# Portacode Handler System
|
|
2
|
+
|
|
3
|
+
This directory contains the modular command handler system for the Portacode client. The system provides a clean, extensible architecture for processing commands from the gateway.
|
|
4
|
+
|
|
5
|
+
## Architecture Overview
|
|
6
|
+
|
|
7
|
+
The handler system consists of:
|
|
8
|
+
|
|
9
|
+
- **Base Handler Classes**: `BaseHandler`, `AsyncHandler`, `SyncHandler`
|
|
10
|
+
- **Command Registry**: `CommandRegistry` for managing and dispatching handlers
|
|
11
|
+
- **Session Management**: `SessionManager` for terminal session lifecycle
|
|
12
|
+
- **Specific Handlers**: Individual command implementations
|
|
13
|
+
|
|
14
|
+
## Existing Handlers
|
|
15
|
+
|
|
16
|
+
### Terminal Handlers (`terminal_handlers.py`)
|
|
17
|
+
|
|
18
|
+
1. **`TerminalStartHandler`** - `terminal_start`
|
|
19
|
+
- Starts new terminal sessions
|
|
20
|
+
- Supports shell and cwd parameters
|
|
21
|
+
- Handles both Windows (ConPTY) and Unix (PTY) systems
|
|
22
|
+
|
|
23
|
+
2. **`TerminalSendHandler`** - `terminal_send`
|
|
24
|
+
- Sends data to existing terminal sessions
|
|
25
|
+
- Requires terminal_id and data parameters
|
|
26
|
+
|
|
27
|
+
3. **`TerminalStopHandler`** - `terminal_stop`
|
|
28
|
+
- Terminates terminal sessions
|
|
29
|
+
- Cleans up resources and sends exit events
|
|
30
|
+
|
|
31
|
+
4. **`TerminalListHandler`** - `terminal_list`
|
|
32
|
+
- Lists all active terminal sessions
|
|
33
|
+
- Returns session metadata and buffer contents
|
|
34
|
+
|
|
35
|
+
### System Handlers (`system_handlers.py`)
|
|
36
|
+
|
|
37
|
+
1. **`SystemInfoHandler`** - `system_info`
|
|
38
|
+
- Provides system resource information
|
|
39
|
+
- Returns CPU, memory, and disk usage data
|
|
40
|
+
|
|
41
|
+
## Adding New Handlers
|
|
42
|
+
|
|
43
|
+
### 1. Asynchronous Handlers
|
|
44
|
+
|
|
45
|
+
For commands that need to perform I/O operations, use `AsyncHandler`:
|
|
46
|
+
|
|
47
|
+
```python
|
|
48
|
+
from .base import AsyncHandler
|
|
49
|
+
from typing import Any, Dict
|
|
50
|
+
|
|
51
|
+
class FileReadHandler(AsyncHandler):
|
|
52
|
+
"""Handler for reading file contents."""
|
|
53
|
+
|
|
54
|
+
@property
|
|
55
|
+
def command_name(self) -> str:
|
|
56
|
+
return "file_read"
|
|
57
|
+
|
|
58
|
+
async def execute(self, message: Dict[str, Any]) -> Dict[str, Any]:
|
|
59
|
+
"""Read file contents asynchronously."""
|
|
60
|
+
file_path = message.get("path")
|
|
61
|
+
if not file_path:
|
|
62
|
+
raise ValueError("path parameter is required")
|
|
63
|
+
|
|
64
|
+
# Async file operations
|
|
65
|
+
import aiofiles
|
|
66
|
+
async with aiofiles.open(file_path, 'r') as f:
|
|
67
|
+
content = await f.read()
|
|
68
|
+
|
|
69
|
+
return {
|
|
70
|
+
"event": "file_read_response",
|
|
71
|
+
"path": file_path,
|
|
72
|
+
"content": content,
|
|
73
|
+
}
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
### 2. Synchronous Handlers
|
|
77
|
+
|
|
78
|
+
For CPU-bound operations or simple synchronous tasks, use `SyncHandler`:
|
|
79
|
+
|
|
80
|
+
```python
|
|
81
|
+
from .base import SyncHandler
|
|
82
|
+
from typing import Any, Dict
|
|
83
|
+
import os
|
|
84
|
+
|
|
85
|
+
class DirectoryListHandler(SyncHandler):
|
|
86
|
+
"""Handler for listing directory contents."""
|
|
87
|
+
|
|
88
|
+
@property
|
|
89
|
+
def command_name(self) -> str:
|
|
90
|
+
return "directory_list"
|
|
91
|
+
|
|
92
|
+
def execute(self, message: Dict[str, Any]) -> Dict[str, Any]:
|
|
93
|
+
"""List directory contents synchronously."""
|
|
94
|
+
path = message.get("path", ".")
|
|
95
|
+
|
|
96
|
+
try:
|
|
97
|
+
items = []
|
|
98
|
+
for item in os.listdir(path):
|
|
99
|
+
item_path = os.path.join(path, item)
|
|
100
|
+
items.append({
|
|
101
|
+
"name": item,
|
|
102
|
+
"is_dir": os.path.isdir(item_path),
|
|
103
|
+
"size": os.path.getsize(item_path) if os.path.isfile(item_path) else 0,
|
|
104
|
+
})
|
|
105
|
+
|
|
106
|
+
return {
|
|
107
|
+
"event": "directory_list_response",
|
|
108
|
+
"path": path,
|
|
109
|
+
"items": items,
|
|
110
|
+
}
|
|
111
|
+
except Exception as e:
|
|
112
|
+
raise RuntimeError(f"Failed to list directory: {e}")
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
### 3. Complex Handler with Context Access
|
|
116
|
+
|
|
117
|
+
Handlers can access shared context and services:
|
|
118
|
+
|
|
119
|
+
```python
|
|
120
|
+
from .base import AsyncHandler
|
|
121
|
+
from typing import Any, Dict
|
|
122
|
+
|
|
123
|
+
class ProcessManagementHandler(AsyncHandler):
|
|
124
|
+
"""Handler for managing processes on the device."""
|
|
125
|
+
|
|
126
|
+
@property
|
|
127
|
+
def command_name(self) -> str:
|
|
128
|
+
return "process_management"
|
|
129
|
+
|
|
130
|
+
async def execute(self, message: Dict[str, Any]) -> Dict[str, Any]:
|
|
131
|
+
"""Manage processes with access to session manager."""
|
|
132
|
+
action = message.get("action") # "list", "kill", "start"
|
|
133
|
+
|
|
134
|
+
# Access session manager from context
|
|
135
|
+
session_manager = self.context.get("session_manager")
|
|
136
|
+
if not session_manager:
|
|
137
|
+
raise RuntimeError("Session manager not available")
|
|
138
|
+
|
|
139
|
+
if action == "list":
|
|
140
|
+
import psutil
|
|
141
|
+
processes = []
|
|
142
|
+
for proc in psutil.process_iter(['pid', 'name', 'cpu_percent']):
|
|
143
|
+
try:
|
|
144
|
+
processes.append(proc.info)
|
|
145
|
+
except (psutil.NoSuchProcess, psutil.AccessDenied):
|
|
146
|
+
continue
|
|
147
|
+
|
|
148
|
+
return {
|
|
149
|
+
"event": "process_list_response",
|
|
150
|
+
"processes": processes,
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
elif action == "kill":
|
|
154
|
+
pid = message.get("pid")
|
|
155
|
+
if not pid:
|
|
156
|
+
raise ValueError("pid parameter required for kill action")
|
|
157
|
+
|
|
158
|
+
import psutil
|
|
159
|
+
try:
|
|
160
|
+
process = psutil.Process(pid)
|
|
161
|
+
process.terminate()
|
|
162
|
+
return {
|
|
163
|
+
"event": "process_killed",
|
|
164
|
+
"pid": pid,
|
|
165
|
+
}
|
|
166
|
+
except psutil.NoSuchProcess:
|
|
167
|
+
raise ValueError(f"Process {pid} not found")
|
|
168
|
+
|
|
169
|
+
else:
|
|
170
|
+
raise ValueError(f"Unknown action: {action}")
|
|
171
|
+
```
|
|
172
|
+
|
|
173
|
+
### 4. Handler with Custom Response Handling
|
|
174
|
+
|
|
175
|
+
You can override the `handle` method for custom response behavior:
|
|
176
|
+
|
|
177
|
+
```python
|
|
178
|
+
from .base import BaseHandler
|
|
179
|
+
from typing import Any, Dict, Optional
|
|
180
|
+
|
|
181
|
+
class StreamingHandler(BaseHandler):
|
|
182
|
+
"""Handler that streams responses."""
|
|
183
|
+
|
|
184
|
+
@property
|
|
185
|
+
def command_name(self) -> str:
|
|
186
|
+
return "streaming_data"
|
|
187
|
+
|
|
188
|
+
async def handle(self, message: Dict[str, Any], reply_channel: Optional[str] = None) -> None:
|
|
189
|
+
"""Handle streaming data with custom response handling."""
|
|
190
|
+
try:
|
|
191
|
+
# Send initial response
|
|
192
|
+
await self.send_response({
|
|
193
|
+
"event": "streaming_started",
|
|
194
|
+
"message": "Starting data stream",
|
|
195
|
+
}, reply_channel)
|
|
196
|
+
|
|
197
|
+
# Stream data
|
|
198
|
+
for i in range(10):
|
|
199
|
+
await asyncio.sleep(0.1)
|
|
200
|
+
await self.send_response({
|
|
201
|
+
"event": "streaming_data",
|
|
202
|
+
"chunk": i,
|
|
203
|
+
"data": f"Data chunk {i}",
|
|
204
|
+
}, reply_channel)
|
|
205
|
+
|
|
206
|
+
# Send completion
|
|
207
|
+
await self.send_response({
|
|
208
|
+
"event": "streaming_completed",
|
|
209
|
+
"message": "Stream completed",
|
|
210
|
+
}, reply_channel)
|
|
211
|
+
|
|
212
|
+
except Exception as exc:
|
|
213
|
+
await self.send_error(str(exc), reply_channel)
|
|
214
|
+
```
|
|
215
|
+
|
|
216
|
+
## Registering Handlers
|
|
217
|
+
|
|
218
|
+
### 1. Automatic Registration
|
|
219
|
+
|
|
220
|
+
Add your handler to the `__init__.py` file and register it in `TerminalManager._register_default_handlers()`:
|
|
221
|
+
|
|
222
|
+
```python
|
|
223
|
+
# In __init__.py
|
|
224
|
+
from .file_handlers import FileReadHandler, DirectoryListHandler
|
|
225
|
+
|
|
226
|
+
# In terminal.py _register_default_handlers()
|
|
227
|
+
def _register_default_handlers(self) -> None:
|
|
228
|
+
"""Register the default command handlers."""
|
|
229
|
+
# ... existing handlers ...
|
|
230
|
+
self._command_registry.register(FileReadHandler)
|
|
231
|
+
self._command_registry.register(DirectoryListHandler)
|
|
232
|
+
```
|
|
233
|
+
|
|
234
|
+
### 2. Dynamic Registration
|
|
235
|
+
|
|
236
|
+
Register handlers at runtime:
|
|
237
|
+
|
|
238
|
+
```python
|
|
239
|
+
# In your code
|
|
240
|
+
terminal_manager.register_handler(MyCustomHandler)
|
|
241
|
+
|
|
242
|
+
# List all registered commands
|
|
243
|
+
commands = terminal_manager.list_commands()
|
|
244
|
+
print(f"Available commands: {commands}")
|
|
245
|
+
|
|
246
|
+
# Unregister if needed
|
|
247
|
+
terminal_manager.unregister_handler("my_command")
|
|
248
|
+
```
|
|
249
|
+
|
|
250
|
+
## Handler Guidelines
|
|
251
|
+
|
|
252
|
+
### Best Practices
|
|
253
|
+
|
|
254
|
+
1. **Command Naming**: Use descriptive, action-oriented names (`file_read`, `system_info`)
|
|
255
|
+
2. **Error Handling**: Always validate input parameters and provide meaningful error messages
|
|
256
|
+
3. **Response Format**: Use consistent response formats with `event` field
|
|
257
|
+
4. **Resource Management**: Clean up resources in exception handlers
|
|
258
|
+
5. **Logging**: Use appropriate logging levels for debugging and monitoring
|
|
259
|
+
|
|
260
|
+
### Parameter Validation
|
|
261
|
+
|
|
262
|
+
```python
|
|
263
|
+
def execute(self, message: Dict[str, Any]) -> Dict[str, Any]:
|
|
264
|
+
# Validate required parameters
|
|
265
|
+
required_params = ["param1", "param2"]
|
|
266
|
+
for param in required_params:
|
|
267
|
+
if param not in message:
|
|
268
|
+
raise ValueError(f"Missing required parameter: {param}")
|
|
269
|
+
|
|
270
|
+
# Validate parameter types
|
|
271
|
+
if not isinstance(message.get("count"), int):
|
|
272
|
+
raise ValueError("count parameter must be an integer")
|
|
273
|
+
|
|
274
|
+
# Your logic here...
|
|
275
|
+
```
|
|
276
|
+
|
|
277
|
+
### Error Handling
|
|
278
|
+
|
|
279
|
+
```python
|
|
280
|
+
async def execute(self, message: Dict[str, Any]) -> Dict[str, Any]:
|
|
281
|
+
try:
|
|
282
|
+
# Your logic here
|
|
283
|
+
result = await some_async_operation()
|
|
284
|
+
return {"event": "success", "result": result}
|
|
285
|
+
except FileNotFoundError:
|
|
286
|
+
raise ValueError("File not found")
|
|
287
|
+
except PermissionError:
|
|
288
|
+
raise RuntimeError("Permission denied")
|
|
289
|
+
except Exception as e:
|
|
290
|
+
raise RuntimeError(f"Unexpected error: {e}")
|
|
291
|
+
```
|
|
292
|
+
|
|
293
|
+
## Testing Handlers
|
|
294
|
+
|
|
295
|
+
### Unit Testing
|
|
296
|
+
|
|
297
|
+
```python
|
|
298
|
+
import pytest
|
|
299
|
+
from unittest.mock import Mock, AsyncMock
|
|
300
|
+
from portacode.connection.handlers.your_handler import YourHandler
|
|
301
|
+
|
|
302
|
+
@pytest.mark.asyncio
|
|
303
|
+
async def test_your_handler():
|
|
304
|
+
# Mock control channel
|
|
305
|
+
control_channel = AsyncMock()
|
|
306
|
+
|
|
307
|
+
# Mock context
|
|
308
|
+
context = {"session_manager": Mock()}
|
|
309
|
+
|
|
310
|
+
# Create handler
|
|
311
|
+
handler = YourHandler(control_channel, context)
|
|
312
|
+
|
|
313
|
+
# Test execute
|
|
314
|
+
message = {"param1": "value1"}
|
|
315
|
+
result = await handler.execute(message)
|
|
316
|
+
|
|
317
|
+
assert result["event"] == "expected_event"
|
|
318
|
+
assert result["data"] == "expected_data"
|
|
319
|
+
```
|
|
320
|
+
|
|
321
|
+
### Integration Testing
|
|
322
|
+
|
|
323
|
+
```python
|
|
324
|
+
# Test with real TerminalManager
|
|
325
|
+
terminal_manager = TerminalManager(mock_multiplexer)
|
|
326
|
+
terminal_manager.register_handler(YourHandler)
|
|
327
|
+
|
|
328
|
+
# Simulate command
|
|
329
|
+
message = {"cmd": "your_command", "param1": "value1"}
|
|
330
|
+
# Process through control loop...
|
|
331
|
+
```
|
|
332
|
+
|
|
333
|
+
## Communication Protocol
|
|
334
|
+
|
|
335
|
+
### Command Format
|
|
336
|
+
|
|
337
|
+
Commands from the gateway follow this format:
|
|
338
|
+
|
|
339
|
+
```json
|
|
340
|
+
{
|
|
341
|
+
"cmd": "command_name",
|
|
342
|
+
"param1": "value1",
|
|
343
|
+
"param2": "value2",
|
|
344
|
+
"reply_channel": "optional_reply_channel"
|
|
345
|
+
}
|
|
346
|
+
```
|
|
347
|
+
|
|
348
|
+
### Response Format
|
|
349
|
+
|
|
350
|
+
Responses to the gateway should follow this format:
|
|
351
|
+
|
|
352
|
+
```json
|
|
353
|
+
{
|
|
354
|
+
"event": "event_name",
|
|
355
|
+
"data": "response_data",
|
|
356
|
+
"reply_channel": "reply_channel_if_provided"
|
|
357
|
+
}
|
|
358
|
+
```
|
|
359
|
+
|
|
360
|
+
### Error Format
|
|
361
|
+
|
|
362
|
+
Error responses:
|
|
363
|
+
|
|
364
|
+
```json
|
|
365
|
+
{
|
|
366
|
+
"event": "error",
|
|
367
|
+
"message": "Error description",
|
|
368
|
+
"reply_channel": "reply_channel_if_provided"
|
|
369
|
+
}
|
|
370
|
+
```
|
|
371
|
+
|
|
372
|
+
## Advanced Topics
|
|
373
|
+
|
|
374
|
+
### Custom Context
|
|
375
|
+
|
|
376
|
+
You can extend the context with custom services:
|
|
377
|
+
|
|
378
|
+
```python
|
|
379
|
+
# In TerminalManager._set_mux()
|
|
380
|
+
self._context = {
|
|
381
|
+
"session_manager": self._session_manager,
|
|
382
|
+
"mux": mux,
|
|
383
|
+
"file_manager": FileManager(),
|
|
384
|
+
"process_manager": ProcessManager(),
|
|
385
|
+
}
|
|
386
|
+
```
|
|
387
|
+
|
|
388
|
+
### Handler Chaining
|
|
389
|
+
|
|
390
|
+
Handlers can call other handlers:
|
|
391
|
+
|
|
392
|
+
```python
|
|
393
|
+
async def execute(self, message: Dict[str, Any]) -> Dict[str, Any]:
|
|
394
|
+
# Get another handler
|
|
395
|
+
other_handler = self.context.get("command_registry").get_handler("other_command")
|
|
396
|
+
if other_handler:
|
|
397
|
+
intermediate_result = await other_handler.execute(message)
|
|
398
|
+
# Process intermediate result...
|
|
399
|
+
|
|
400
|
+
return final_result
|
|
401
|
+
```
|
|
402
|
+
|
|
403
|
+
### Middleware Pattern
|
|
404
|
+
|
|
405
|
+
Create middleware handlers that wrap other handlers:
|
|
406
|
+
|
|
407
|
+
```python
|
|
408
|
+
class LoggingMiddleware(BaseHandler):
|
|
409
|
+
def __init__(self, control_channel, context, wrapped_handler):
|
|
410
|
+
super().__init__(control_channel, context)
|
|
411
|
+
self.wrapped_handler = wrapped_handler
|
|
412
|
+
|
|
413
|
+
async def handle(self, message, reply_channel=None):
|
|
414
|
+
logger.info(f"Executing command: {message.get('cmd')}")
|
|
415
|
+
start_time = time.time()
|
|
416
|
+
|
|
417
|
+
try:
|
|
418
|
+
result = await self.wrapped_handler.handle(message, reply_channel)
|
|
419
|
+
duration = time.time() - start_time
|
|
420
|
+
logger.info(f"Command completed in {duration:.2f}s")
|
|
421
|
+
return result
|
|
422
|
+
except Exception as e:
|
|
423
|
+
logger.error(f"Command failed: {e}")
|
|
424
|
+
raise
|
|
425
|
+
```
|
|
426
|
+
|
|
427
|
+
This modular system provides a clean, extensible architecture for adding new functionality to the Portacode client while maintaining backward compatibility and code organization.
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
"""Modular command handler system for Portacode client.
|
|
2
|
+
|
|
3
|
+
This package provides a flexible system for handling commands from the gateway.
|
|
4
|
+
Handlers can be easily added, removed, or modified without touching the main
|
|
5
|
+
terminal manager code.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from .base import BaseHandler, AsyncHandler, SyncHandler
|
|
9
|
+
from .registry import CommandRegistry
|
|
10
|
+
from .terminal_handlers import (
|
|
11
|
+
TerminalStartHandler,
|
|
12
|
+
TerminalSendHandler,
|
|
13
|
+
TerminalStopHandler,
|
|
14
|
+
TerminalListHandler,
|
|
15
|
+
)
|
|
16
|
+
from .system_handlers import SystemInfoHandler
|
|
17
|
+
from .file_handlers import (
|
|
18
|
+
FileReadHandler,
|
|
19
|
+
FileWriteHandler,
|
|
20
|
+
DirectoryListHandler,
|
|
21
|
+
FileInfoHandler,
|
|
22
|
+
FileDeleteHandler,
|
|
23
|
+
)
|
|
24
|
+
|
|
25
|
+
__all__ = [
|
|
26
|
+
"BaseHandler",
|
|
27
|
+
"AsyncHandler",
|
|
28
|
+
"SyncHandler",
|
|
29
|
+
"CommandRegistry",
|
|
30
|
+
"TerminalStartHandler",
|
|
31
|
+
"TerminalSendHandler",
|
|
32
|
+
"TerminalStopHandler",
|
|
33
|
+
"TerminalListHandler",
|
|
34
|
+
"SystemInfoHandler",
|
|
35
|
+
# File operation handlers (optional - register as needed)
|
|
36
|
+
"FileReadHandler",
|
|
37
|
+
"FileWriteHandler",
|
|
38
|
+
"DirectoryListHandler",
|
|
39
|
+
"FileInfoHandler",
|
|
40
|
+
"FileDeleteHandler",
|
|
41
|
+
]
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
"""Base handler classes for command processing."""
|
|
2
|
+
|
|
3
|
+
import asyncio
|
|
4
|
+
import logging
|
|
5
|
+
from abc import ABC, abstractmethod
|
|
6
|
+
from typing import Any, Dict, Optional, TYPE_CHECKING
|
|
7
|
+
|
|
8
|
+
if TYPE_CHECKING:
|
|
9
|
+
from ..multiplex import Channel
|
|
10
|
+
|
|
11
|
+
logger = logging.getLogger(__name__)
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class BaseHandler(ABC):
|
|
15
|
+
"""Base class for all command handlers."""
|
|
16
|
+
|
|
17
|
+
def __init__(self, control_channel: "Channel", context: Dict[str, Any]):
|
|
18
|
+
"""Initialize the handler.
|
|
19
|
+
|
|
20
|
+
Args:
|
|
21
|
+
control_channel: The control channel for sending responses
|
|
22
|
+
context: Shared context containing terminal manager state
|
|
23
|
+
"""
|
|
24
|
+
self.control_channel = control_channel
|
|
25
|
+
self.context = context
|
|
26
|
+
|
|
27
|
+
@property
|
|
28
|
+
@abstractmethod
|
|
29
|
+
def command_name(self) -> str:
|
|
30
|
+
"""Return the command name this handler processes."""
|
|
31
|
+
pass
|
|
32
|
+
|
|
33
|
+
@abstractmethod
|
|
34
|
+
async def handle(self, message: Dict[str, Any], reply_channel: Optional[str] = None) -> None:
|
|
35
|
+
"""Handle the command message.
|
|
36
|
+
|
|
37
|
+
Args:
|
|
38
|
+
message: The command message dict
|
|
39
|
+
reply_channel: Optional reply channel for responses
|
|
40
|
+
"""
|
|
41
|
+
pass
|
|
42
|
+
|
|
43
|
+
async def send_response(self, payload: Dict[str, Any], reply_channel: Optional[str] = None) -> None:
|
|
44
|
+
"""Send a response back to the gateway.
|
|
45
|
+
|
|
46
|
+
Args:
|
|
47
|
+
payload: Response payload
|
|
48
|
+
reply_channel: Optional reply channel
|
|
49
|
+
"""
|
|
50
|
+
if reply_channel:
|
|
51
|
+
payload["reply_channel"] = reply_channel
|
|
52
|
+
await self.control_channel.send(payload)
|
|
53
|
+
|
|
54
|
+
async def send_error(self, message: str, reply_channel: Optional[str] = None) -> None:
|
|
55
|
+
"""Send an error response.
|
|
56
|
+
|
|
57
|
+
Args:
|
|
58
|
+
message: Error message
|
|
59
|
+
reply_channel: Optional reply channel
|
|
60
|
+
"""
|
|
61
|
+
payload = {"event": "error", "message": message}
|
|
62
|
+
if reply_channel:
|
|
63
|
+
payload["reply_channel"] = reply_channel
|
|
64
|
+
await self.control_channel.send(payload)
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
class AsyncHandler(BaseHandler):
|
|
68
|
+
"""Base class for asynchronous command handlers."""
|
|
69
|
+
|
|
70
|
+
@abstractmethod
|
|
71
|
+
async def execute(self, message: Dict[str, Any]) -> Dict[str, Any]:
|
|
72
|
+
"""Execute the command logic asynchronously.
|
|
73
|
+
|
|
74
|
+
Args:
|
|
75
|
+
message: The command message dict
|
|
76
|
+
|
|
77
|
+
Returns:
|
|
78
|
+
Response payload dict
|
|
79
|
+
"""
|
|
80
|
+
pass
|
|
81
|
+
|
|
82
|
+
async def handle(self, message: Dict[str, Any], reply_channel: Optional[str] = None) -> None:
|
|
83
|
+
"""Handle the command by executing it and sending the response."""
|
|
84
|
+
try:
|
|
85
|
+
response = await self.execute(message)
|
|
86
|
+
await self.send_response(response, reply_channel)
|
|
87
|
+
except Exception as exc:
|
|
88
|
+
logger.exception("Error in async handler %s: %s", self.command_name, exc)
|
|
89
|
+
await self.send_error(str(exc), reply_channel)
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
class SyncHandler(BaseHandler):
|
|
93
|
+
"""Base class for synchronous command handlers."""
|
|
94
|
+
|
|
95
|
+
@abstractmethod
|
|
96
|
+
def execute(self, message: Dict[str, Any]) -> Dict[str, Any]:
|
|
97
|
+
"""Execute the command logic synchronously.
|
|
98
|
+
|
|
99
|
+
Args:
|
|
100
|
+
message: The command message dict
|
|
101
|
+
|
|
102
|
+
Returns:
|
|
103
|
+
Response payload dict
|
|
104
|
+
"""
|
|
105
|
+
pass
|
|
106
|
+
|
|
107
|
+
async def handle(self, message: Dict[str, Any], reply_channel: Optional[str] = None) -> None:
|
|
108
|
+
"""Handle the command by executing it in an executor and sending the response."""
|
|
109
|
+
try:
|
|
110
|
+
loop = asyncio.get_running_loop()
|
|
111
|
+
response = await loop.run_in_executor(None, self.execute, message)
|
|
112
|
+
await self.send_response(response, reply_channel)
|
|
113
|
+
except Exception as exc:
|
|
114
|
+
logger.exception("Error in sync handler %s: %s", self.command_name, exc)
|
|
115
|
+
await self.send_error(str(exc), reply_channel)
|