portacode 0.3.11.dev2__tar.gz → 0.3.11.dev3__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.3.11.dev2 → portacode-0.3.11.dev3}/PKG-INFO +1 -1
- {portacode-0.3.11.dev2 → portacode-0.3.11.dev3}/portacode/_version.py +2 -2
- portacode-0.3.11.dev3/portacode/connection/handlers/WEBSOCKET_PROTOCOL.md +423 -0
- {portacode-0.3.11.dev2 → portacode-0.3.11.dev3}/portacode/connection/handlers/base.py +57 -6
- {portacode-0.3.11.dev2 → portacode-0.3.11.dev3}/portacode.egg-info/PKG-INFO +1 -1
- {portacode-0.3.11.dev2 → portacode-0.3.11.dev3}/portacode.egg-info/SOURCES.txt +1 -0
- {portacode-0.3.11.dev2 → portacode-0.3.11.dev3}/.gitignore +0 -0
- {portacode-0.3.11.dev2 → portacode-0.3.11.dev3}/.gitmodules +0 -0
- {portacode-0.3.11.dev2 → portacode-0.3.11.dev3}/LICENSE +0 -0
- {portacode-0.3.11.dev2 → portacode-0.3.11.dev3}/MANIFEST.in +0 -0
- {portacode-0.3.11.dev2 → portacode-0.3.11.dev3}/Makefile +0 -0
- {portacode-0.3.11.dev2 → portacode-0.3.11.dev3}/README.md +0 -0
- {portacode-0.3.11.dev2 → portacode-0.3.11.dev3}/docker-compose.yaml +0 -0
- {portacode-0.3.11.dev2 → portacode-0.3.11.dev3}/portacode/README.md +0 -0
- {portacode-0.3.11.dev2 → portacode-0.3.11.dev3}/portacode/__init__.py +0 -0
- {portacode-0.3.11.dev2 → portacode-0.3.11.dev3}/portacode/__main__.py +0 -0
- {portacode-0.3.11.dev2 → portacode-0.3.11.dev3}/portacode/cli.py +0 -0
- {portacode-0.3.11.dev2 → portacode-0.3.11.dev3}/portacode/connection/README.md +0 -0
- {portacode-0.3.11.dev2 → portacode-0.3.11.dev3}/portacode/connection/__init__.py +0 -0
- {portacode-0.3.11.dev2 → portacode-0.3.11.dev3}/portacode/connection/client.py +0 -0
- {portacode-0.3.11.dev2 → portacode-0.3.11.dev3}/portacode/connection/handlers/README.md +0 -0
- {portacode-0.3.11.dev2 → portacode-0.3.11.dev3}/portacode/connection/handlers/__init__.py +0 -0
- {portacode-0.3.11.dev2 → portacode-0.3.11.dev3}/portacode/connection/handlers/file_handlers.py +0 -0
- {portacode-0.3.11.dev2 → portacode-0.3.11.dev3}/portacode/connection/handlers/registry.py +0 -0
- {portacode-0.3.11.dev2 → portacode-0.3.11.dev3}/portacode/connection/handlers/session.py +0 -0
- {portacode-0.3.11.dev2 → portacode-0.3.11.dev3}/portacode/connection/handlers/system_handlers.py +0 -0
- {portacode-0.3.11.dev2 → portacode-0.3.11.dev3}/portacode/connection/handlers/terminal_handlers.py +0 -0
- {portacode-0.3.11.dev2 → portacode-0.3.11.dev3}/portacode/connection/multiplex.py +0 -0
- {portacode-0.3.11.dev2 → portacode-0.3.11.dev3}/portacode/connection/terminal.py +0 -0
- {portacode-0.3.11.dev2 → portacode-0.3.11.dev3}/portacode/data.py +0 -0
- {portacode-0.3.11.dev2 → portacode-0.3.11.dev3}/portacode/keypair.py +0 -0
- {portacode-0.3.11.dev2 → portacode-0.3.11.dev3}/portacode/service.py +0 -0
- {portacode-0.3.11.dev2 → portacode-0.3.11.dev3}/portacode.egg-info/dependency_links.txt +0 -0
- {portacode-0.3.11.dev2 → portacode-0.3.11.dev3}/portacode.egg-info/entry_points.txt +0 -0
- {portacode-0.3.11.dev2 → portacode-0.3.11.dev3}/portacode.egg-info/requires.txt +0 -0
- {portacode-0.3.11.dev2 → portacode-0.3.11.dev3}/portacode.egg-info/top_level.txt +0 -0
- {portacode-0.3.11.dev2 → portacode-0.3.11.dev3}/pyproject.toml +0 -0
- {portacode-0.3.11.dev2 → portacode-0.3.11.dev3}/setup.cfg +0 -0
- {portacode-0.3.11.dev2 → portacode-0.3.11.dev3}/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.3.11.
|
|
21
|
-
__version_tuple__ = version_tuple = (0, 3, 11, '
|
|
20
|
+
__version__ = version = '0.3.11.dev3'
|
|
21
|
+
__version_tuple__ = version_tuple = (0, 3, 11, 'dev3')
|
|
@@ -0,0 +1,423 @@
|
|
|
1
|
+
# WebSocket Communication Protocol
|
|
2
|
+
|
|
3
|
+
This document outlines the WebSocket communication protocol between the Portacode server and the connected client devices.
|
|
4
|
+
|
|
5
|
+
## Protocol Architecture
|
|
6
|
+
|
|
7
|
+
This protocol uses a **unified message structure** throughout the entire system. No message transformations occur between layers, ensuring consistency and eliminating complexity.
|
|
8
|
+
|
|
9
|
+
## Raw Message Format
|
|
10
|
+
|
|
11
|
+
All communication over the WebSocket is managed by a [multiplexer](./multiplex.py) that wraps every message in a JSON object with a `device_channel` and a `payload`. This allows for multiple virtual communication channels over a single connection.
|
|
12
|
+
|
|
13
|
+
**Raw Message Structure:**
|
|
14
|
+
|
|
15
|
+
```json
|
|
16
|
+
{
|
|
17
|
+
"device_channel": "<channel_id>",
|
|
18
|
+
"payload": {
|
|
19
|
+
// This is where the Action or Event object goes
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
* `device_channel` (string|integer, mandatory): Identifies the virtual channel on the device side. When sending control commands to the device, they should be sent to channel 0. When the device responds to such control commands or sends system events, they will also be sent on channel 0. When a terminal session is created in the device, it is assigned a UUID, and that UUID becomes the device_channel for communicating to that specific terminal.
|
|
25
|
+
* `payload` (object, mandatory): The content of the message, which will be either an [Action](#actions) or an [Event](#events) object.
|
|
26
|
+
|
|
27
|
+
---
|
|
28
|
+
|
|
29
|
+
## Actions
|
|
30
|
+
|
|
31
|
+
Actions are messages sent from the server to the device, placed within the `payload` of a raw message. They instruct the device to perform a specific operation and are handled by the [`BaseHandler`](./base.py) and its subclasses.
|
|
32
|
+
|
|
33
|
+
**Action Structure (inside the `payload`):**
|
|
34
|
+
|
|
35
|
+
```json
|
|
36
|
+
{
|
|
37
|
+
"command": "<command_name>",
|
|
38
|
+
"payload": {
|
|
39
|
+
"arg1": "value1",
|
|
40
|
+
"...": "..."
|
|
41
|
+
},
|
|
42
|
+
"client_channel": "<session_id>"
|
|
43
|
+
}
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
* `command` (string, mandatory): The name of the action to be executed (e.g., `terminal_start`).
|
|
47
|
+
* `payload` (object, mandatory): An object containing the specific arguments for the action.
|
|
48
|
+
* `client_channel` (string, mandatory): A session identifier that the device will echo back in its response. This identifies which client session on the server side is communicating with the device. The device uses this to track active client sessions and ensure messages are only sent to connected clients. This works like a return address - the device_channel specifies which application on the device handles the message, while the client_channel specifies which client session should receive the response.
|
|
49
|
+
|
|
50
|
+
### Session Management and Event Subscriptions
|
|
51
|
+
|
|
52
|
+
**Client Session Tracking**: Devices maintain awareness of active client sessions through explicit session management commands. The server notifies devices when client sessions connect, disconnect, or change their event subscriptions.
|
|
53
|
+
|
|
54
|
+
**Mandatory Client Channel**: All commands sent to devices MUST include a `client_channel` to enable proper session tracking and response routing.
|
|
55
|
+
|
|
56
|
+
**Event Subscription System**: Client sessions can specify which events they want to receive through subscription management. This allows devices to filter events and reduce bandwidth usage by only sending relevant updates to interested clients.
|
|
57
|
+
|
|
58
|
+
---
|
|
59
|
+
|
|
60
|
+
### `terminal_start`
|
|
61
|
+
|
|
62
|
+
Initiates a new terminal session on the device. Handled by [`terminal_start`](./terminal_handlers.py).
|
|
63
|
+
|
|
64
|
+
**Payload Fields:**
|
|
65
|
+
|
|
66
|
+
* `shell` (string, optional): The shell to use (e.g., `/bin/bash`). Defaults to the system's default shell.
|
|
67
|
+
* `cwd` (string, optional): The working directory to start the terminal in. Defaults to the user's home directory.
|
|
68
|
+
* `project_id` (string, optional): The ID of the project this terminal is associated with.
|
|
69
|
+
|
|
70
|
+
**Responses:**
|
|
71
|
+
|
|
72
|
+
* On success, the device will respond with a [`terminal_started`](#terminal_started) event.
|
|
73
|
+
* On error, a generic [`error`](#error) event is sent.
|
|
74
|
+
|
|
75
|
+
### `terminal_send`
|
|
76
|
+
|
|
77
|
+
Sends input data to a running terminal session. Handled by [`terminal_send`](./terminal_handlers.py).
|
|
78
|
+
|
|
79
|
+
**Payload Fields:**
|
|
80
|
+
|
|
81
|
+
* `terminal_id` (string, mandatory): The ID of the terminal session to send data to.
|
|
82
|
+
* `data` (string, mandatory): The data to write to the terminal's standard input.
|
|
83
|
+
|
|
84
|
+
**Responses:**
|
|
85
|
+
|
|
86
|
+
* On success, the device will respond with a [`terminal_send_ack`](#terminal_send_ack) event.
|
|
87
|
+
* On error, a generic [`error`](#error) event is sent.
|
|
88
|
+
|
|
89
|
+
### `terminal_stop`
|
|
90
|
+
|
|
91
|
+
Terminates a running terminal session. Handled by [`terminal_stop`](./terminal_handlers.py).
|
|
92
|
+
|
|
93
|
+
**Payload Fields:**
|
|
94
|
+
|
|
95
|
+
* `terminal_id` (string, mandatory): The ID of the terminal session to stop.
|
|
96
|
+
|
|
97
|
+
**Responses:**
|
|
98
|
+
|
|
99
|
+
* The device immediately responds with a [`terminal_stopped`](#terminal_stopped) event to acknowledge the request.
|
|
100
|
+
* Once the terminal is successfully stopped, a [`terminal_stop_completed`](#terminal_stop_completed) event is sent.
|
|
101
|
+
* If the terminal is not found, a [`terminal_stop_completed`](#terminal_stop_completed) with a "not_found" status is sent.
|
|
102
|
+
|
|
103
|
+
### `terminal_list`
|
|
104
|
+
|
|
105
|
+
Requests a list of all active terminal sessions. Handled by [`terminal_list`](./terminal_handlers.py).
|
|
106
|
+
|
|
107
|
+
**Payload Fields:**
|
|
108
|
+
|
|
109
|
+
* `project_id` (string, optional): If provided, filters terminals by this project ID. If "all", lists all terminals.
|
|
110
|
+
|
|
111
|
+
**Responses:**
|
|
112
|
+
|
|
113
|
+
* On success, the device will respond with a [`terminal_list`](#terminal_list-event) event.
|
|
114
|
+
|
|
115
|
+
### `system_info`
|
|
116
|
+
|
|
117
|
+
Requests system information from the device. Handled by [`system_info`](./system_handlers.py).
|
|
118
|
+
|
|
119
|
+
**Payload Fields:**
|
|
120
|
+
|
|
121
|
+
This action does not require any payload fields.
|
|
122
|
+
|
|
123
|
+
**Responses:**
|
|
124
|
+
|
|
125
|
+
* On success, the device will respond with a [`system_info`](#system_info-event) event.
|
|
126
|
+
|
|
127
|
+
### `file_read`
|
|
128
|
+
|
|
129
|
+
Reads the content of a file. Handled by [`file_read`](./file_handlers.py).
|
|
130
|
+
|
|
131
|
+
**Payload Fields:**
|
|
132
|
+
|
|
133
|
+
* `path` (string, mandatory): The absolute path to the file to read.
|
|
134
|
+
|
|
135
|
+
**Responses:**
|
|
136
|
+
|
|
137
|
+
* On success, the device will respond with a [`file_read_response`](#file_read_response) event.
|
|
138
|
+
* On error, a generic [`error`](#error) event is sent.
|
|
139
|
+
|
|
140
|
+
### `file_write`
|
|
141
|
+
|
|
142
|
+
Writes content to a file. Handled by [`file_write`](./file_handlers.py).
|
|
143
|
+
|
|
144
|
+
**Payload Fields:**
|
|
145
|
+
|
|
146
|
+
* `path` (string, mandatory): The absolute path to the file to write to.
|
|
147
|
+
* `content` (string, mandatory): The content to write to the file.
|
|
148
|
+
|
|
149
|
+
**Responses:**
|
|
150
|
+
|
|
151
|
+
* On success, the device will respond with a [`file_write_response`](#file_write_response) event.
|
|
152
|
+
* On error, a generic [`error`](#error) event is sent.
|
|
153
|
+
|
|
154
|
+
### `directory_list`
|
|
155
|
+
|
|
156
|
+
Lists the contents of a directory. Handled by [`directory_list`](./file_handlers.py).
|
|
157
|
+
|
|
158
|
+
**Payload Fields:**
|
|
159
|
+
|
|
160
|
+
* `path` (string, optional): The path to the directory to list. Defaults to the current directory.
|
|
161
|
+
* `show_hidden` (boolean, optional): Whether to include hidden files in the listing. Defaults to `false`.
|
|
162
|
+
|
|
163
|
+
**Responses:**
|
|
164
|
+
|
|
165
|
+
* On success, the device will respond with a [`directory_list_response`](#directory_list_response) event.
|
|
166
|
+
* On error, a generic [`error`](#error) event is sent.
|
|
167
|
+
|
|
168
|
+
### `file_info`
|
|
169
|
+
|
|
170
|
+
Gets information about a file or directory. Handled by [`file_info`](./file_handlers.py).
|
|
171
|
+
|
|
172
|
+
**Payload Fields:**
|
|
173
|
+
|
|
174
|
+
* `path` (string, mandatory): The path to the file or directory.
|
|
175
|
+
|
|
176
|
+
**Responses:**
|
|
177
|
+
|
|
178
|
+
* On success, the device will respond with a [`file_info_response`](#file_info_response) event.
|
|
179
|
+
|
|
180
|
+
### `file_delete`
|
|
181
|
+
|
|
182
|
+
Deletes a file or directory. Handled by [`file_delete`](./file_handlers.py).
|
|
183
|
+
|
|
184
|
+
**Payload Fields:**
|
|
185
|
+
|
|
186
|
+
* `path` (string, mandatory): The path to the file or directory to delete.
|
|
187
|
+
* `recursive` (boolean, optional): If `true`, recursively deletes a directory and its contents. Defaults to `false`.
|
|
188
|
+
|
|
189
|
+
**Responses:**
|
|
190
|
+
|
|
191
|
+
* On success, the device will respond with a [`file_delete_response`](#file_delete_response) event.
|
|
192
|
+
* On error, a generic [`error`](#error) event is sent.
|
|
193
|
+
|
|
194
|
+
### `session_subscribe`
|
|
195
|
+
|
|
196
|
+
Manages client session subscriptions and notifies the device about active sessions and their event interests. The device stores subscription filters for each active session and uses them to determine which sessions should receive each event.
|
|
197
|
+
|
|
198
|
+
**Payload Fields:**
|
|
199
|
+
|
|
200
|
+
* `client_channel` (string, mandatory): The session identifier being managed.
|
|
201
|
+
* `action` (string, mandatory): The subscription action - "connect", "disconnect", or "update".
|
|
202
|
+
* `filters` (array, optional): Array of filter objects the client is interested in. Required for "connect" and "update" actions.
|
|
203
|
+
|
|
204
|
+
**Filter Object Format:**
|
|
205
|
+
Each filter is a flexible object that can match against any field in event payloads:
|
|
206
|
+
* **Field Matching**: Any field name with string value or "*" for wildcard
|
|
207
|
+
* **Pattern Matching**: String values support wildcards (e.g., "terminal_*")
|
|
208
|
+
* **Multiple Conditions**: All specified fields must match for filter to match
|
|
209
|
+
|
|
210
|
+
**Example Filter Objects:**
|
|
211
|
+
```json
|
|
212
|
+
[
|
|
213
|
+
{"event": "terminal_data", "terminal_id": "uuid-123"},
|
|
214
|
+
{"event": "terminal_*", "project_id": "project-456"},
|
|
215
|
+
{"event": "system_info"},
|
|
216
|
+
{"event": "*", "project_id": "project-789"},
|
|
217
|
+
{"event": "file_*", "path": "/home/user/*"}
|
|
218
|
+
]
|
|
219
|
+
```
|
|
220
|
+
|
|
221
|
+
**Processing Architecture:**
|
|
222
|
+
The device maintains a subscription registry mapping each `client_channel` to its filter objects. When broadcasting events, the device evaluates each active session's filters against the event payload to determine which sessions should receive the event.
|
|
223
|
+
|
|
224
|
+
**Responses:**
|
|
225
|
+
|
|
226
|
+
* On success, the device will respond with a [`session_subscribe_ack`](#session_subscribe_ack) event.
|
|
227
|
+
* On error, a generic [`error`](#error) event is sent.
|
|
228
|
+
|
|
229
|
+
---
|
|
230
|
+
|
|
231
|
+
## Events
|
|
232
|
+
|
|
233
|
+
Events are messages sent from the device to the server, placed within the `payload` of a raw message. They are sent in response to an action or to notify the server of a state change.
|
|
234
|
+
|
|
235
|
+
**Event Structure (inside the `payload`):**
|
|
236
|
+
|
|
237
|
+
```json
|
|
238
|
+
{
|
|
239
|
+
"event": "<event_name>",
|
|
240
|
+
// Event-specific fields...
|
|
241
|
+
"client_channels": ["<session_id1>", "<session_id2>"]
|
|
242
|
+
}
|
|
243
|
+
```
|
|
244
|
+
|
|
245
|
+
* `event` (string, mandatory): The name of the event being sent (e.g., `terminal_started`).
|
|
246
|
+
* <a name="client_channels"></a>`client_channels` (array, optional): List of client session identifiers that should receive this event. For direct responses to commands, this contains the single `client_channel` from the original request. For broadcast events, this contains all client sessions whose subscription filters matched the event. The server uses this to route the event to the correct client sessions.
|
|
247
|
+
|
|
248
|
+
### Event Broadcasting and Session Awareness
|
|
249
|
+
|
|
250
|
+
**Targeted Events**: Events that are direct responses to commands include the original `client_channel` in the `client_channels` array, ensuring they are routed only to the requesting session.
|
|
251
|
+
|
|
252
|
+
**Broadcast Events**: Events that are not direct responses (such as spontaneous terminal exits or system notifications) are processed through subscription filtering. The device evaluates the event payload against each active session's filter objects to determine which sessions should receive the event.
|
|
253
|
+
|
|
254
|
+
**Subscription Filtering Process**:
|
|
255
|
+
1. Device generates an event with its payload fields
|
|
256
|
+
2. For each active client session, device evaluates session's filter objects against event payload
|
|
257
|
+
3. If any filter matches, the session's `client_channel` is added to the `client_channels` array
|
|
258
|
+
4. Event is sent once with all matching `client_channels`, minimizing network traffic
|
|
259
|
+
5. Server routes the single event to all specified client sessions
|
|
260
|
+
|
|
261
|
+
**Filter Matching**: A filter matches an event if all specified fields in the filter have corresponding fields in the event payload with matching values (supporting wildcards like `terminal_*` or `*`).
|
|
262
|
+
|
|
263
|
+
**Efficiency Benefits**:
|
|
264
|
+
- **Single Event Transmission**: Each event is sent once from device to server, regardless of how many client sessions need it
|
|
265
|
+
- **Flexible Filtering**: Clients can filter on any event field without protocol changes
|
|
266
|
+
- **Minimal Bandwidth**: Only events with at least one interested client session are transmitted
|
|
267
|
+
- **Simple Routing**: Server receives pre-filtered events with target client sessions already identified
|
|
268
|
+
|
|
269
|
+
---
|
|
270
|
+
|
|
271
|
+
### <a name="error"></a>`error`
|
|
272
|
+
|
|
273
|
+
A generic event sent when an error occurs during the execution of an action.
|
|
274
|
+
|
|
275
|
+
**Event Fields:**
|
|
276
|
+
|
|
277
|
+
* `message` (string, mandatory): A description of the error that occurred.
|
|
278
|
+
|
|
279
|
+
### <a name="session_subscribe_ack"></a>`session_subscribe_ack`
|
|
280
|
+
|
|
281
|
+
Acknowledges the receipt and processing of a `session_subscribe` action. This confirms that the device has updated its session registry and subscription filters.
|
|
282
|
+
|
|
283
|
+
**Event Fields:**
|
|
284
|
+
|
|
285
|
+
* `action` (string, mandatory): The subscription action that was processed ("connect", "disconnect", or "update").
|
|
286
|
+
* `active_sessions` (integer, mandatory): The total number of currently active client sessions.
|
|
287
|
+
* `filter_count` (integer, mandatory): The number of filter objects registered for the managed session (0 for disconnect action).
|
|
288
|
+
|
|
289
|
+
### <a name="terminal_started"></a>`terminal_started`
|
|
290
|
+
|
|
291
|
+
Confirms that a new terminal session has been successfully started. Triggered by a `terminal_start` action. Handled by [`terminal_start`](./terminal_handlers.py).
|
|
292
|
+
|
|
293
|
+
**Event Fields:**
|
|
294
|
+
|
|
295
|
+
* `terminal_id` (string, mandatory): The unique ID of the newly created terminal session.
|
|
296
|
+
* `device_channel` (string, mandatory): The device channel name for terminal I/O (same as terminal_id).
|
|
297
|
+
* `project_id` (string, optional): The project ID associated with the terminal.
|
|
298
|
+
|
|
299
|
+
### <a name="terminal_exit"></a>`terminal_exit`
|
|
300
|
+
|
|
301
|
+
Notifies the server that a terminal session has terminated. This can be due to the process ending or the session being stopped. Handled by [`terminal_start`](./terminal_handlers.py).
|
|
302
|
+
|
|
303
|
+
**Event Fields:**
|
|
304
|
+
|
|
305
|
+
* `terminal_id` (string, mandatory): The ID of the terminal session that has exited.
|
|
306
|
+
* `returncode` (integer, mandatory): The exit code of the terminal process.
|
|
307
|
+
* `project_id` (string, optional): The project ID associated with the terminal.
|
|
308
|
+
|
|
309
|
+
### <a name="terminal_send_ack"></a>`terminal_send_ack`
|
|
310
|
+
|
|
311
|
+
Acknowledges the receipt of a `terminal_send` action. Handled by [`terminal_send`](./terminal_handlers.py). This event carries no extra fields.
|
|
312
|
+
|
|
313
|
+
### <a name="terminal_stopped"></a>`terminal_stopped`
|
|
314
|
+
|
|
315
|
+
Acknowledges that a `terminal_stop` request has been received and is being processed. Handled by [`terminal_stop`](./terminal_handlers.py).
|
|
316
|
+
|
|
317
|
+
**Event Fields:**
|
|
318
|
+
|
|
319
|
+
* `terminal_id` (string, mandatory): The ID of the terminal being stopped.
|
|
320
|
+
* `status` (string, mandatory): The status of the stop operation (e.g., "stopping", "not_found").
|
|
321
|
+
* `message` (string, mandatory): A descriptive message about the status.
|
|
322
|
+
* `project_id` (string, optional): The project ID associated with the terminal.
|
|
323
|
+
|
|
324
|
+
### <a name="terminal_stop_completed"></a>`terminal_stop_completed`
|
|
325
|
+
|
|
326
|
+
Confirms that a terminal session has been successfully stopped. Handled by [`terminal_stop`](./terminal_handlers.py).
|
|
327
|
+
|
|
328
|
+
**Event Fields:**
|
|
329
|
+
|
|
330
|
+
* `terminal_id` (string, mandatory): The ID of the stopped terminal.
|
|
331
|
+
* `status` (string, mandatory): The final status ("success", "timeout", "error", "not_found").
|
|
332
|
+
* `message` (string, mandatory): A descriptive message.
|
|
333
|
+
* `project_id` (string, optional): The project ID associated with the terminal.
|
|
334
|
+
|
|
335
|
+
### <a name="terminal_list-event"></a>`terminal_list`
|
|
336
|
+
|
|
337
|
+
Provides the list of active terminal sessions in response to a `terminal_list` action. Handled by [`terminal_list`](./terminal_handlers.py).
|
|
338
|
+
|
|
339
|
+
**Event Fields:**
|
|
340
|
+
|
|
341
|
+
* `sessions` (array, mandatory): A list of active terminal session objects.
|
|
342
|
+
* `project_id` (string, optional): The project ID that was used to filter the list.
|
|
343
|
+
|
|
344
|
+
### <a name="system_info-event"></a>`system_info`
|
|
345
|
+
|
|
346
|
+
Provides system information in response to a `system_info` action. Handled by [`system_info`](./system_handlers.py).
|
|
347
|
+
|
|
348
|
+
**Event Fields:**
|
|
349
|
+
|
|
350
|
+
* `info` (object, mandatory): An object containing system information, including:
|
|
351
|
+
* `cpu_percent` (float): CPU usage percentage.
|
|
352
|
+
* `memory` (object): Memory usage statistics.
|
|
353
|
+
* `disk` (object): Disk usage statistics.
|
|
354
|
+
* `os_info` (object): Operating system details, including `os_type`, `os_version`, `architecture`, `default_shell`, and `default_cwd`.
|
|
355
|
+
|
|
356
|
+
### <a name="file_read_response"></a>`file_read_response`
|
|
357
|
+
|
|
358
|
+
Returns the content of a file in response to a `file_read` action. Handled by [`file_read`](./file_handlers.py).
|
|
359
|
+
|
|
360
|
+
**Event Fields:**
|
|
361
|
+
|
|
362
|
+
* `path` (string, mandatory): The path of the file that was read.
|
|
363
|
+
* `content` (string, mandatory): The content of the file.
|
|
364
|
+
* `size` (integer, mandatory): The size of the file in bytes.
|
|
365
|
+
|
|
366
|
+
### <a name="file_write_response"></a>`file_write_response`
|
|
367
|
+
|
|
368
|
+
Confirms that a file has been written successfully in response to a `file_write` action. Handled by [`file_write`](./file_handlers.py).
|
|
369
|
+
|
|
370
|
+
**Event Fields:**
|
|
371
|
+
|
|
372
|
+
* `path` (string, mandatory): The path of the file that was written.
|
|
373
|
+
* `bytes_written` (integer, mandatory): The number of bytes written to the file.
|
|
374
|
+
* `success` (boolean, mandatory): Indicates whether the write operation was successful.
|
|
375
|
+
|
|
376
|
+
### <a name="directory_list_response"></a>`directory_list_response`
|
|
377
|
+
|
|
378
|
+
Returns the contents of a directory in response to a `directory_list` action. Handled by [`directory_list`](./file_handlers.py).
|
|
379
|
+
|
|
380
|
+
**Event Fields:**
|
|
381
|
+
|
|
382
|
+
* `path` (string, mandatory): The path of the directory that was listed.
|
|
383
|
+
* `items` (array, mandatory): A list of objects, each representing a file or directory in the listed directory.
|
|
384
|
+
* `count` (integer, mandatory): The number of items in the `items` list.
|
|
385
|
+
|
|
386
|
+
### <a name="file_info_response"></a>`file_info_response`
|
|
387
|
+
|
|
388
|
+
Returns information about a file or directory in response to a `file_info` action. Handled by [`file_info`](./file_handlers.py).
|
|
389
|
+
|
|
390
|
+
**Event Fields:**
|
|
391
|
+
|
|
392
|
+
* `path` (string, mandatory): The path of the file or directory.
|
|
393
|
+
* `exists` (boolean, mandatory): Indicates whether the file or directory exists.
|
|
394
|
+
* `is_file` (boolean, optional): Indicates if the path is a file.
|
|
395
|
+
* `is_dir` (boolean, optional): Indicates if the path is a directory.
|
|
396
|
+
* `is_symlink` (boolean, optional): Indicates if the path is a symbolic link.
|
|
397
|
+
* `size` (integer, optional): The size of the file in bytes.
|
|
398
|
+
* `modified` (float, optional): The last modification time (timestamp).
|
|
399
|
+
* `accessed` (float, optional): The last access time (timestamp).
|
|
400
|
+
* `created` (float, optional): The creation time (timestamp).
|
|
401
|
+
* `permissions` (string, optional): The file permissions in octal format.
|
|
402
|
+
* `owner_uid` (integer, optional): The user ID of the owner.
|
|
403
|
+
* `group_gid` (integer, optional): The group ID of the owner.
|
|
404
|
+
|
|
405
|
+
### <a name="file_delete_response"></a>`file_delete_response`
|
|
406
|
+
|
|
407
|
+
Confirms that a file or directory has been deleted in response to a `file_delete` action. Handled by [`file_delete`](./file_handlers.py).
|
|
408
|
+
|
|
409
|
+
**Event Fields:**
|
|
410
|
+
|
|
411
|
+
* `path` (string, mandatory): The path of the deleted file or directory.
|
|
412
|
+
* `deleted_type` (string, mandatory): The type of the deleted item ("file" or "directory").
|
|
413
|
+
* `success` (boolean, mandatory): Indicates whether the deletion was successful.
|
|
414
|
+
|
|
415
|
+
---
|
|
416
|
+
|
|
417
|
+
## Future Enhancements
|
|
418
|
+
|
|
419
|
+
### Bandwidth Management
|
|
420
|
+
The protocol will support configurable rate limiting and bandwidth monitoring to prevent resource exhaustion and ensure fair usage across multiple client sessions. This will include per-session bandwidth quotas, throttling mechanisms, and automatic rate limiting enforcement.
|
|
421
|
+
|
|
422
|
+
### Channel Traffic Isolation
|
|
423
|
+
Different device channels (control, terminal sessions, file operations) will have separate traffic shaping to ensure high-priority control commands are not delayed by high-volume terminal output. This includes priority queuing and per-channel bandwidth allocation.
|
|
@@ -40,15 +40,58 @@ class BaseHandler(ABC):
|
|
|
40
40
|
"""
|
|
41
41
|
pass
|
|
42
42
|
|
|
43
|
+
def _normalize_field_names(self, message: Dict[str, Any]) -> Dict[str, Any]:
|
|
44
|
+
"""Normalize field names for backward compatibility.
|
|
45
|
+
|
|
46
|
+
Supports both old and new field names:
|
|
47
|
+
- 'cmd' -> 'command'
|
|
48
|
+
- 'reply_channel' -> 'client_channel'
|
|
49
|
+
|
|
50
|
+
Args:
|
|
51
|
+
message: Original message dict
|
|
52
|
+
|
|
53
|
+
Returns:
|
|
54
|
+
Message dict with normalized field names
|
|
55
|
+
"""
|
|
56
|
+
normalized = message.copy()
|
|
57
|
+
|
|
58
|
+
# Support both 'cmd' and 'command' fields
|
|
59
|
+
if 'cmd' in normalized and 'command' not in normalized:
|
|
60
|
+
normalized['command'] = normalized['cmd']
|
|
61
|
+
logger.debug("Converted 'cmd' to 'command' for backward compatibility")
|
|
62
|
+
|
|
63
|
+
# Support both 'reply_channel' and 'client_channel' fields
|
|
64
|
+
if 'reply_channel' in normalized and 'client_channel' not in normalized:
|
|
65
|
+
normalized['client_channel'] = normalized['reply_channel']
|
|
66
|
+
logger.debug("Converted 'reply_channel' to 'client_channel' for backward compatibility")
|
|
67
|
+
|
|
68
|
+
return normalized
|
|
69
|
+
|
|
70
|
+
def _get_client_channels(self, client_channel: Optional[str] = None) -> Optional[list]:
|
|
71
|
+
"""Convert single client_channel to client_channels array for responses.
|
|
72
|
+
|
|
73
|
+
Args:
|
|
74
|
+
client_channel: Single client channel identifier
|
|
75
|
+
|
|
76
|
+
Returns:
|
|
77
|
+
List with single client channel, or None if no channel provided
|
|
78
|
+
"""
|
|
79
|
+
if client_channel:
|
|
80
|
+
return [client_channel]
|
|
81
|
+
return None
|
|
82
|
+
|
|
43
83
|
async def send_response(self, payload: Dict[str, Any], reply_channel: Optional[str] = None) -> None:
|
|
44
84
|
"""Send a response back to the gateway.
|
|
45
85
|
|
|
46
86
|
Args:
|
|
47
87
|
payload: Response payload
|
|
48
|
-
reply_channel: Optional reply channel
|
|
88
|
+
reply_channel: Optional reply channel (supports both old and new names)
|
|
49
89
|
"""
|
|
90
|
+
# Support both reply_channel (old) and client_channel (new) parameter names
|
|
50
91
|
if reply_channel:
|
|
51
|
-
|
|
92
|
+
# Add both formats for backward compatibility during transition
|
|
93
|
+
payload["reply_channel"] = reply_channel # Old format
|
|
94
|
+
payload["client_channels"] = [reply_channel] # New format
|
|
52
95
|
await self.control_channel.send(payload)
|
|
53
96
|
|
|
54
97
|
async def send_error(self, message: str, reply_channel: Optional[str] = None) -> None:
|
|
@@ -56,11 +99,13 @@ class BaseHandler(ABC):
|
|
|
56
99
|
|
|
57
100
|
Args:
|
|
58
101
|
message: Error message
|
|
59
|
-
reply_channel: Optional reply channel
|
|
102
|
+
reply_channel: Optional reply channel (supports both old and new names)
|
|
60
103
|
"""
|
|
61
104
|
payload = {"event": "error", "message": message}
|
|
62
105
|
if reply_channel:
|
|
63
|
-
|
|
106
|
+
# Add both formats for backward compatibility during transition
|
|
107
|
+
payload["reply_channel"] = reply_channel # Old format
|
|
108
|
+
payload["client_channels"] = [reply_channel] # New format
|
|
64
109
|
await self.control_channel.send(payload)
|
|
65
110
|
|
|
66
111
|
|
|
@@ -81,11 +126,14 @@ class AsyncHandler(BaseHandler):
|
|
|
81
126
|
|
|
82
127
|
async def handle(self, message: Dict[str, Any], reply_channel: Optional[str] = None) -> None:
|
|
83
128
|
"""Handle the command by executing it and sending the response."""
|
|
129
|
+
# Normalize field names for backward compatibility
|
|
130
|
+
normalized_message = self._normalize_field_names(message)
|
|
131
|
+
|
|
84
132
|
logger.info("handler: Processing command %s with reply_channel=%s",
|
|
85
133
|
self.command_name, reply_channel)
|
|
86
134
|
|
|
87
135
|
try:
|
|
88
|
-
response = await self.execute(
|
|
136
|
+
response = await self.execute(normalized_message)
|
|
89
137
|
logger.info("handler: Command %s executed successfully", self.command_name)
|
|
90
138
|
await self.send_response(response, reply_channel)
|
|
91
139
|
except Exception as exc:
|
|
@@ -110,9 +158,12 @@ class SyncHandler(BaseHandler):
|
|
|
110
158
|
|
|
111
159
|
async def handle(self, message: Dict[str, Any], reply_channel: Optional[str] = None) -> None:
|
|
112
160
|
"""Handle the command by executing it in an executor and sending the response."""
|
|
161
|
+
# Normalize field names for backward compatibility
|
|
162
|
+
normalized_message = self._normalize_field_names(message)
|
|
163
|
+
|
|
113
164
|
try:
|
|
114
165
|
loop = asyncio.get_running_loop()
|
|
115
|
-
response = await loop.run_in_executor(None, self.execute,
|
|
166
|
+
response = await loop.run_in_executor(None, self.execute, normalized_message)
|
|
116
167
|
await self.send_response(response, reply_channel)
|
|
117
168
|
except Exception as exc:
|
|
118
169
|
logger.exception("Error in sync handler %s: %s", self.command_name, exc)
|
|
@@ -27,6 +27,7 @@ portacode/connection/client.py
|
|
|
27
27
|
portacode/connection/multiplex.py
|
|
28
28
|
portacode/connection/terminal.py
|
|
29
29
|
portacode/connection/handlers/README.md
|
|
30
|
+
portacode/connection/handlers/WEBSOCKET_PROTOCOL.md
|
|
30
31
|
portacode/connection/handlers/__init__.py
|
|
31
32
|
portacode/connection/handlers/base.py
|
|
32
33
|
portacode/connection/handlers/file_handlers.py
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{portacode-0.3.11.dev2 → portacode-0.3.11.dev3}/portacode/connection/handlers/file_handlers.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{portacode-0.3.11.dev2 → portacode-0.3.11.dev3}/portacode/connection/handlers/system_handlers.py
RENAMED
|
File without changes
|
{portacode-0.3.11.dev2 → portacode-0.3.11.dev3}/portacode/connection/handlers/terminal_handlers.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|