portacode 0.3.12.dev7__tar.gz → 0.3.12.dev9__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.12.dev7 → portacode-0.3.12.dev9}/PKG-INFO +1 -1
- {portacode-0.3.12.dev7 → portacode-0.3.12.dev9}/portacode/_version.py +2 -2
- {portacode-0.3.12.dev7 → portacode-0.3.12.dev9}/portacode/connection/handlers/WEBSOCKET_PROTOCOL.md +79 -11
- {portacode-0.3.12.dev7 → portacode-0.3.12.dev9}/portacode/connection/handlers/session.py +116 -20
- {portacode-0.3.12.dev7 → portacode-0.3.12.dev9}/portacode/connection/terminal.py +3 -2
- {portacode-0.3.12.dev7 → portacode-0.3.12.dev9}/portacode.egg-info/PKG-INFO +1 -1
- {portacode-0.3.12.dev7 → portacode-0.3.12.dev9}/.claude/agents/communication-manager.md +0 -0
- {portacode-0.3.12.dev7 → portacode-0.3.12.dev9}/.claude/settings.local.json +0 -0
- {portacode-0.3.12.dev7 → portacode-0.3.12.dev9}/.gitignore +0 -0
- {portacode-0.3.12.dev7 → portacode-0.3.12.dev9}/.gitmodules +0 -0
- {portacode-0.3.12.dev7 → portacode-0.3.12.dev9}/LICENSE +0 -0
- {portacode-0.3.12.dev7 → portacode-0.3.12.dev9}/MANIFEST.in +0 -0
- {portacode-0.3.12.dev7 → portacode-0.3.12.dev9}/Makefile +0 -0
- {portacode-0.3.12.dev7 → portacode-0.3.12.dev9}/README.md +0 -0
- {portacode-0.3.12.dev7 → portacode-0.3.12.dev9}/backup.sh +0 -0
- {portacode-0.3.12.dev7 → portacode-0.3.12.dev9}/docker-compose.yaml +0 -0
- {portacode-0.3.12.dev7 → portacode-0.3.12.dev9}/portacode/README.md +0 -0
- {portacode-0.3.12.dev7 → portacode-0.3.12.dev9}/portacode/__init__.py +0 -0
- {portacode-0.3.12.dev7 → portacode-0.3.12.dev9}/portacode/__main__.py +0 -0
- {portacode-0.3.12.dev7 → portacode-0.3.12.dev9}/portacode/cli.py +0 -0
- {portacode-0.3.12.dev7 → portacode-0.3.12.dev9}/portacode/connection/README.md +0 -0
- {portacode-0.3.12.dev7 → portacode-0.3.12.dev9}/portacode/connection/__init__.py +0 -0
- {portacode-0.3.12.dev7 → portacode-0.3.12.dev9}/portacode/connection/client.py +0 -0
- {portacode-0.3.12.dev7 → portacode-0.3.12.dev9}/portacode/connection/handlers/README.md +0 -0
- {portacode-0.3.12.dev7 → portacode-0.3.12.dev9}/portacode/connection/handlers/__init__.py +0 -0
- {portacode-0.3.12.dev7 → portacode-0.3.12.dev9}/portacode/connection/handlers/base.py +0 -0
- {portacode-0.3.12.dev7 → portacode-0.3.12.dev9}/portacode/connection/handlers/file_handlers.py +0 -0
- {portacode-0.3.12.dev7 → portacode-0.3.12.dev9}/portacode/connection/handlers/registry.py +0 -0
- {portacode-0.3.12.dev7 → portacode-0.3.12.dev9}/portacode/connection/handlers/system_handlers.py +0 -0
- {portacode-0.3.12.dev7 → portacode-0.3.12.dev9}/portacode/connection/handlers/terminal_handlers.py +0 -0
- {portacode-0.3.12.dev7 → portacode-0.3.12.dev9}/portacode/connection/multiplex.py +0 -0
- {portacode-0.3.12.dev7 → portacode-0.3.12.dev9}/portacode/data.py +0 -0
- {portacode-0.3.12.dev7 → portacode-0.3.12.dev9}/portacode/keypair.py +0 -0
- {portacode-0.3.12.dev7 → portacode-0.3.12.dev9}/portacode/service.py +0 -0
- {portacode-0.3.12.dev7 → portacode-0.3.12.dev9}/portacode.egg-info/SOURCES.txt +0 -0
- {portacode-0.3.12.dev7 → portacode-0.3.12.dev9}/portacode.egg-info/dependency_links.txt +0 -0
- {portacode-0.3.12.dev7 → portacode-0.3.12.dev9}/portacode.egg-info/entry_points.txt +0 -0
- {portacode-0.3.12.dev7 → portacode-0.3.12.dev9}/portacode.egg-info/requires.txt +0 -0
- {portacode-0.3.12.dev7 → portacode-0.3.12.dev9}/portacode.egg-info/top_level.txt +0 -0
- {portacode-0.3.12.dev7 → portacode-0.3.12.dev9}/pyproject.toml +0 -0
- {portacode-0.3.12.dev7 → portacode-0.3.12.dev9}/restore.sh +0 -0
- {portacode-0.3.12.dev7 → portacode-0.3.12.dev9}/setup.cfg +0 -0
- {portacode-0.3.12.dev7 → portacode-0.3.12.dev9}/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.12.
|
|
21
|
-
__version_tuple__ = version_tuple = (0, 3, 12, '
|
|
20
|
+
__version__ = version = '0.3.12.dev9'
|
|
21
|
+
__version_tuple__ = version_tuple = (0, 3, 12, 'dev9')
|
{portacode-0.3.12.dev7 → portacode-0.3.12.dev9}/portacode/connection/handlers/WEBSOCKET_PROTOCOL.md
RENAMED
|
@@ -26,6 +26,7 @@ This document outlines the WebSocket communication protocol between the Portacod
|
|
|
26
26
|
- [`error`](#error)
|
|
27
27
|
- [Terminal Events](#terminal-events)
|
|
28
28
|
- [`terminal_started`](#terminal_started)
|
|
29
|
+
- [`terminal_data`](#terminal_data)
|
|
29
30
|
- [`terminal_exit`](#terminal_exit)
|
|
30
31
|
- [`terminal_send_ack`](#terminal_send_ack)
|
|
31
32
|
- [`terminal_stopped`](#terminal_stopped)
|
|
@@ -79,14 +80,14 @@ Actions are messages sent from the server to the device, placed within the `payl
|
|
|
79
80
|
"payload": {
|
|
80
81
|
"arg1": "value1",
|
|
81
82
|
"...": "..."
|
|
82
|
-
}
|
|
83
|
-
"reply_channel": "<channel_name>"
|
|
83
|
+
}
|
|
84
84
|
}
|
|
85
85
|
```
|
|
86
86
|
|
|
87
87
|
* `command` (string, mandatory): The name of the action to be executed (e.g., `terminal_start`).
|
|
88
88
|
* `payload` (object, mandatory): An object containing the specific arguments for the action.
|
|
89
|
-
|
|
89
|
+
|
|
90
|
+
**Note**: Actions do not require targeting information - responses are automatically routed using the client session management system.
|
|
90
91
|
|
|
91
92
|
### `terminal_start`
|
|
92
93
|
|
|
@@ -248,13 +249,22 @@ Events are messages sent from the device to the server, placed within the `paylo
|
|
|
248
249
|
{
|
|
249
250
|
"event": "<event_name>",
|
|
250
251
|
// Event-specific fields...
|
|
251
|
-
"
|
|
252
|
+
"device_id": 123,
|
|
253
|
+
"project_id": "<project_uuid>",
|
|
254
|
+
"client_sessions": ["channel.abc123", "channel.def456"]
|
|
252
255
|
}
|
|
253
256
|
```
|
|
254
257
|
|
|
258
|
+
**Standard Fields (automatically added by the system):**
|
|
259
|
+
|
|
255
260
|
* `event` (string, mandatory): The name of the event being sent (e.g., `terminal_started`).
|
|
256
|
-
*
|
|
257
|
-
* `
|
|
261
|
+
* `device_id` (integer, mandatory): The ID of the authenticated device that generated this event. **Added by the server based on the authenticated connection for security** - devices cannot self-identify.
|
|
262
|
+
* `project_id` (string, optional): The project UUID associated with this event, used for project-scoped filtering. Sent by the device.
|
|
263
|
+
* `client_sessions` (array, optional): Array of client session channel names that should receive this event. **Added by the device's terminal manager** based on interested client sessions. When present, the event is sent only to these specific sessions. When absent, the event is broadcast to all sessions for the device owner.
|
|
264
|
+
|
|
265
|
+
**Event-Specific Fields:**
|
|
266
|
+
|
|
267
|
+
Each event type includes additional fields specific to its purpose, documented in the individual event sections below.
|
|
258
268
|
|
|
259
269
|
### <a name="error"></a>`error`
|
|
260
270
|
|
|
@@ -274,6 +284,41 @@ Confirms that a new terminal session has been successfully started. Triggered by
|
|
|
274
284
|
* `channel` (string, mandatory): The channel name for terminal I/O.
|
|
275
285
|
* `project_id` (string, optional): The project ID associated with the terminal.
|
|
276
286
|
|
|
287
|
+
### <a name="terminal_data"></a>`terminal_data`
|
|
288
|
+
|
|
289
|
+
Streams real-time terminal output data from a running terminal session to connected clients. This event is automatically generated whenever the terminal process produces output (stdout/stderr). Generated by [`TerminalSession`](./session.py) through the terminal manager's session-aware messaging system.
|
|
290
|
+
|
|
291
|
+
**Event-Specific Fields:**
|
|
292
|
+
|
|
293
|
+
* `channel` (string, mandatory): The terminal UUID identifying which terminal session produced this output. This matches the `terminal_id` from the corresponding `terminal_started` event.
|
|
294
|
+
* `data` (string, mandatory): The raw terminal output data. **See detailed description below.**
|
|
295
|
+
|
|
296
|
+
**The `data` Field - Detailed Specification:**
|
|
297
|
+
|
|
298
|
+
The `data` field contains the exact bytes output by the terminal process, decoded as a UTF-8 string with error handling:
|
|
299
|
+
|
|
300
|
+
* **Encoding**: UTF-8 with `errors="ignore"` - invalid UTF-8 sequences are silently dropped
|
|
301
|
+
* **Content**: Raw terminal output including:
|
|
302
|
+
- Regular command output (stdout)
|
|
303
|
+
- Error messages (stderr) - combined with stdout in PTY mode
|
|
304
|
+
- ANSI escape sequences for colors, cursor positioning, screen clearing, etc.
|
|
305
|
+
- Control characters (newlines, tabs, backspace, etc.)
|
|
306
|
+
- Shell prompts and interactive application output
|
|
307
|
+
* **Buffering**: Data is read in 1024-byte chunks from the terminal process and sent immediately (no line buffering)
|
|
308
|
+
* **Binary Safety**: Binary data is handled via UTF-8 decoding with error tolerance
|
|
309
|
+
* **Size**: Individual chunks are typically ≤1024 characters, but can be smaller for real-time responsiveness
|
|
310
|
+
|
|
311
|
+
**Examples of `data` content:**
|
|
312
|
+
```
|
|
313
|
+
"Hello, World!\n" // Simple command output
|
|
314
|
+
"\u001b[32mSuccess\u001b[0m\n" // ANSI colored text
|
|
315
|
+
"user@host:~/project$ " // Shell prompt
|
|
316
|
+
"\u001b[2J\u001b[H" // Clear screen escape sequence
|
|
317
|
+
"Progress: [████████████████████] 100%\r" // Progress bar with carriage return
|
|
318
|
+
```
|
|
319
|
+
|
|
320
|
+
**Security Note**: The `device_id` field is automatically injected by the server based on the authenticated connection - the device cannot and should not specify its own ID. The `project_id` and `client_sessions` fields are added by the device's terminal manager for proper routing and filtering.
|
|
321
|
+
|
|
277
322
|
### <a name="terminal_exit"></a>`terminal_exit`
|
|
278
323
|
|
|
279
324
|
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).
|
|
@@ -402,11 +447,33 @@ This event carries no additional fields.
|
|
|
402
447
|
|
|
403
448
|
### Terminal Data
|
|
404
449
|
|
|
405
|
-
###
|
|
450
|
+
### Terminal I/O Data Formats
|
|
451
|
+
|
|
452
|
+
Terminal I/O data can be sent in two formats depending on the implementation:
|
|
453
|
+
|
|
454
|
+
#### Modern Format (Recommended)
|
|
455
|
+
|
|
456
|
+
Terminal data is sent as a proper [`terminal_data`](#terminal_data) event on the control channel (channel 0) with client session targeting support:
|
|
457
|
+
|
|
458
|
+
```json
|
|
459
|
+
{
|
|
460
|
+
"channel": 0,
|
|
461
|
+
"payload": {
|
|
462
|
+
"event": "terminal_data",
|
|
463
|
+
"channel": "<terminal_uuid>",
|
|
464
|
+
"data": "<terminal_output_string>",
|
|
465
|
+
"device_id": 123,
|
|
466
|
+
"project_id": "<project_uuid>",
|
|
467
|
+
"client_sessions": ["channel.abc123", "channel.def456"]
|
|
468
|
+
}
|
|
469
|
+
}
|
|
470
|
+
```
|
|
471
|
+
|
|
472
|
+
This format follows the standard event structure with automatic system field injection (device_id, project_id, client_sessions) for proper routing and security.
|
|
406
473
|
|
|
407
|
-
|
|
474
|
+
#### Legacy Format (Deprecated)
|
|
408
475
|
|
|
409
|
-
|
|
476
|
+
Terminal data sent directly on terminal channels (not on the control channel):
|
|
410
477
|
|
|
411
478
|
```json
|
|
412
479
|
{
|
|
@@ -416,8 +483,9 @@ Terminal input/output data is sent directly on terminal channels (not on the con
|
|
|
416
483
|
```
|
|
417
484
|
|
|
418
485
|
* Terminal output is sent as raw string data in the payload
|
|
419
|
-
* Input to terminals is sent the same way but in the opposite direction
|
|
420
|
-
* No event wrapper
|
|
486
|
+
* Input to terminals is sent the same way but in the opposite direction
|
|
487
|
+
* No event wrapper or client targeting is used
|
|
488
|
+
* This format broadcasts to all sessions for the device owner
|
|
421
489
|
|
|
422
490
|
### Server-Side Events
|
|
423
491
|
|
|
@@ -4,6 +4,7 @@ import asyncio
|
|
|
4
4
|
import logging
|
|
5
5
|
import os
|
|
6
6
|
import sys
|
|
7
|
+
import time
|
|
7
8
|
import uuid
|
|
8
9
|
from asyncio.subprocess import Process
|
|
9
10
|
from pathlib import Path
|
|
@@ -13,6 +14,10 @@ from collections import deque
|
|
|
13
14
|
if TYPE_CHECKING:
|
|
14
15
|
from ..multiplex import Channel
|
|
15
16
|
|
|
17
|
+
# Terminal data rate limiting configuration
|
|
18
|
+
TERMINAL_DATA_RATE_LIMIT_MS = 200 # Minimum time between terminal_data events (milliseconds)
|
|
19
|
+
TERMINAL_DATA_MAX_WAIT_MS = 1000 # Maximum time to wait before sending accumulated data (milliseconds)
|
|
20
|
+
|
|
16
21
|
logger = logging.getLogger(__name__)
|
|
17
22
|
|
|
18
23
|
_IS_WINDOWS = sys.platform.startswith("win")
|
|
@@ -36,13 +41,19 @@ def _build_child_env() -> Dict[str, str]:
|
|
|
36
41
|
class TerminalSession:
|
|
37
42
|
"""Represents a local shell subprocess bound to a mux channel."""
|
|
38
43
|
|
|
39
|
-
def __init__(self, session_id: str, proc: Process, channel: "Channel", project_id: Optional[str] = None):
|
|
44
|
+
def __init__(self, session_id: str, proc: Process, channel: "Channel", project_id: Optional[str] = None, terminal_manager: Optional["TerminalManager"] = None):
|
|
40
45
|
self.id = session_id
|
|
41
46
|
self.proc = proc
|
|
42
47
|
self.channel = channel
|
|
43
48
|
self.project_id = project_id
|
|
49
|
+
self.terminal_manager = terminal_manager
|
|
44
50
|
self._reader_task: Optional[asyncio.Task[None]] = None
|
|
45
51
|
self._buffer: deque[str] = deque(maxlen=400)
|
|
52
|
+
|
|
53
|
+
# Rate limiting for terminal_data events
|
|
54
|
+
self._last_send_time: float = 0
|
|
55
|
+
self._pending_data: str = ""
|
|
56
|
+
self._debounce_task: Optional[asyncio.Task[None]] = None
|
|
46
57
|
|
|
47
58
|
async def start_io_forwarding(self) -> None:
|
|
48
59
|
"""Spawn background task that copies stdout/stderr to the channel."""
|
|
@@ -56,13 +67,9 @@ class TerminalSession:
|
|
|
56
67
|
break
|
|
57
68
|
text = data.decode(errors="ignore")
|
|
58
69
|
logging.getLogger("portacode.terminal").debug(f"[MUX] Terminal {self.id} output: {text!r}")
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
except Exception as exc:
|
|
63
|
-
logger.warning("Failed to forward terminal output: %s", exc)
|
|
64
|
-
await asyncio.sleep(0.5)
|
|
65
|
-
continue
|
|
70
|
+
|
|
71
|
+
# Use rate-limited sending instead of immediate sending
|
|
72
|
+
await self._handle_terminal_data(text)
|
|
66
73
|
finally:
|
|
67
74
|
if self.proc and self.proc.returncode is None:
|
|
68
75
|
pass # Keep alive across reconnects
|
|
@@ -118,6 +125,20 @@ class TerminalSession:
|
|
|
118
125
|
except asyncio.CancelledError:
|
|
119
126
|
pass
|
|
120
127
|
|
|
128
|
+
# Cancel and flush any pending terminal data
|
|
129
|
+
if self._debounce_task and not self._debounce_task.done():
|
|
130
|
+
logger.info("session.stop: Cancelling debounce task for session %s", self.id)
|
|
131
|
+
self._debounce_task.cancel()
|
|
132
|
+
try:
|
|
133
|
+
await self._debounce_task
|
|
134
|
+
except asyncio.CancelledError:
|
|
135
|
+
pass
|
|
136
|
+
|
|
137
|
+
# Send any remaining pending data
|
|
138
|
+
if self._pending_data:
|
|
139
|
+
logger.info("session.stop: Flushing pending terminal data for session %s", self.id)
|
|
140
|
+
await self._flush_pending_data()
|
|
141
|
+
|
|
121
142
|
# Wait for process to exit
|
|
122
143
|
if self.proc.returncode is None:
|
|
123
144
|
logger.info("session.stop: Waiting for process to exit for session %s", self.id)
|
|
@@ -132,6 +153,70 @@ class TerminalSession:
|
|
|
132
153
|
logger.exception("session.stop: Error stopping session %s: %s", self.id, exc)
|
|
133
154
|
raise
|
|
134
155
|
|
|
156
|
+
async def _send_terminal_data_now(self, data: str) -> None:
|
|
157
|
+
"""Send terminal data immediately and update last send time."""
|
|
158
|
+
self._last_send_time = time.time()
|
|
159
|
+
|
|
160
|
+
# Add to buffer for snapshots
|
|
161
|
+
self._buffer.append(data)
|
|
162
|
+
|
|
163
|
+
try:
|
|
164
|
+
# Send terminal data via control channel with client session targeting
|
|
165
|
+
if self.terminal_manager:
|
|
166
|
+
await self.terminal_manager._send_session_aware({
|
|
167
|
+
"event": "terminal_data",
|
|
168
|
+
"channel": self.id,
|
|
169
|
+
"data": data,
|
|
170
|
+
"project_id": self.project_id
|
|
171
|
+
}, project_id=self.project_id)
|
|
172
|
+
else:
|
|
173
|
+
# Fallback to raw channel for backward compatibility
|
|
174
|
+
await self.channel.send(data)
|
|
175
|
+
except Exception as exc:
|
|
176
|
+
logger.warning("Failed to forward terminal output: %s", exc)
|
|
177
|
+
|
|
178
|
+
async def _flush_pending_data(self) -> None:
|
|
179
|
+
"""Send accumulated pending data and reset pending buffer."""
|
|
180
|
+
if self._pending_data:
|
|
181
|
+
data_to_send = self._pending_data
|
|
182
|
+
self._pending_data = ""
|
|
183
|
+
await self._send_terminal_data_now(data_to_send)
|
|
184
|
+
|
|
185
|
+
# Clear the debounce task
|
|
186
|
+
self._debounce_task = None
|
|
187
|
+
|
|
188
|
+
async def _handle_terminal_data(self, data: str) -> None:
|
|
189
|
+
"""Handle new terminal data with rate limiting and debouncing."""
|
|
190
|
+
current_time = time.time()
|
|
191
|
+
time_since_last_send = (current_time - self._last_send_time) * 1000 # Convert to milliseconds
|
|
192
|
+
|
|
193
|
+
# Add new data to pending buffer
|
|
194
|
+
self._pending_data += data
|
|
195
|
+
|
|
196
|
+
# Cancel existing debounce task if any
|
|
197
|
+
if self._debounce_task and not self._debounce_task.done():
|
|
198
|
+
self._debounce_task.cancel()
|
|
199
|
+
|
|
200
|
+
if time_since_last_send >= TERMINAL_DATA_RATE_LIMIT_MS:
|
|
201
|
+
# Enough time has passed, send immediately
|
|
202
|
+
await self._flush_pending_data()
|
|
203
|
+
else:
|
|
204
|
+
# Too soon, set up debounce timer
|
|
205
|
+
async def _debounce_timer():
|
|
206
|
+
try:
|
|
207
|
+
# Wait for either the rate limit period or max wait time
|
|
208
|
+
wait_time = min(
|
|
209
|
+
(TERMINAL_DATA_RATE_LIMIT_MS - time_since_last_send) / 1000,
|
|
210
|
+
TERMINAL_DATA_MAX_WAIT_MS / 1000
|
|
211
|
+
)
|
|
212
|
+
await asyncio.sleep(wait_time)
|
|
213
|
+
await self._flush_pending_data()
|
|
214
|
+
except asyncio.CancelledError:
|
|
215
|
+
# Timer was cancelled, another data event came in
|
|
216
|
+
pass
|
|
217
|
+
|
|
218
|
+
self._debounce_task = asyncio.create_task(_debounce_timer())
|
|
219
|
+
|
|
135
220
|
def snapshot_buffer(self) -> str:
|
|
136
221
|
"""Return concatenated last buffer contents suitable for UI."""
|
|
137
222
|
return "".join(self._buffer)
|
|
@@ -147,7 +232,7 @@ class TerminalSession:
|
|
|
147
232
|
class WindowsTerminalSession(TerminalSession):
|
|
148
233
|
"""Terminal session backed by a Windows ConPTY."""
|
|
149
234
|
|
|
150
|
-
def __init__(self, session_id: str, pty, channel: "Channel", project_id: Optional[str] = None):
|
|
235
|
+
def __init__(self, session_id: str, pty, channel: "Channel", project_id: Optional[str] = None, terminal_manager: Optional["TerminalManager"] = None):
|
|
151
236
|
# Create a proxy for the PTY process
|
|
152
237
|
class _WinPTYProxy:
|
|
153
238
|
def __init__(self, pty):
|
|
@@ -165,7 +250,7 @@ class WindowsTerminalSession(TerminalSession):
|
|
|
165
250
|
loop = asyncio.get_running_loop()
|
|
166
251
|
await loop.run_in_executor(None, self._pty.wait)
|
|
167
252
|
|
|
168
|
-
super().__init__(session_id, _WinPTYProxy(pty), channel, project_id)
|
|
253
|
+
super().__init__(session_id, _WinPTYProxy(pty), channel, project_id, terminal_manager)
|
|
169
254
|
self._pty = pty
|
|
170
255
|
|
|
171
256
|
async def start_io_forwarding(self) -> None:
|
|
@@ -186,13 +271,9 @@ class WindowsTerminalSession(TerminalSession):
|
|
|
186
271
|
else:
|
|
187
272
|
text = data
|
|
188
273
|
logging.getLogger("portacode.terminal").debug(f"[MUX] Terminal {self.id} output: {text!r}")
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
except Exception as exc:
|
|
193
|
-
logger.warning("Failed to forward terminal output: %s", exc)
|
|
194
|
-
await asyncio.sleep(0.5)
|
|
195
|
-
continue
|
|
274
|
+
|
|
275
|
+
# Use rate-limited sending instead of immediate sending
|
|
276
|
+
await self._handle_terminal_data(text)
|
|
196
277
|
finally:
|
|
197
278
|
if self._pty and self._pty.isalive():
|
|
198
279
|
self._pty.kill()
|
|
@@ -237,6 +318,20 @@ class WindowsTerminalSession(TerminalSession):
|
|
|
237
318
|
except asyncio.CancelledError:
|
|
238
319
|
pass
|
|
239
320
|
|
|
321
|
+
# Cancel and flush any pending terminal data
|
|
322
|
+
if self._debounce_task and not self._debounce_task.done():
|
|
323
|
+
logger.info("session.stop: Cancelling debounce task for Windows session %s", self.id)
|
|
324
|
+
self._debounce_task.cancel()
|
|
325
|
+
try:
|
|
326
|
+
await self._debounce_task
|
|
327
|
+
except asyncio.CancelledError:
|
|
328
|
+
pass
|
|
329
|
+
|
|
330
|
+
# Send any remaining pending data
|
|
331
|
+
if self._pending_data:
|
|
332
|
+
logger.info("session.stop: Flushing pending terminal data for Windows session %s", self.id)
|
|
333
|
+
await self._flush_pending_data()
|
|
334
|
+
|
|
240
335
|
logger.info("session.stop: Successfully stopped Windows session %s", self.id)
|
|
241
336
|
|
|
242
337
|
except Exception as exc:
|
|
@@ -247,8 +342,9 @@ class WindowsTerminalSession(TerminalSession):
|
|
|
247
342
|
class SessionManager:
|
|
248
343
|
"""Manages terminal sessions."""
|
|
249
344
|
|
|
250
|
-
def __init__(self, mux):
|
|
345
|
+
def __init__(self, mux, terminal_manager=None):
|
|
251
346
|
self.mux = mux
|
|
347
|
+
self.terminal_manager = terminal_manager
|
|
252
348
|
self._sessions: Dict[str, TerminalSession] = {}
|
|
253
349
|
|
|
254
350
|
def _allocate_channel_id(self) -> str:
|
|
@@ -287,7 +383,7 @@ class SessionManager:
|
|
|
287
383
|
raise RuntimeError("pywinpty not installed on client")
|
|
288
384
|
|
|
289
385
|
pty_proc = PtyProcess.spawn(shell, cwd=cwd or None, env=_build_child_env())
|
|
290
|
-
session = WindowsTerminalSession(term_id, pty_proc, channel, project_id)
|
|
386
|
+
session = WindowsTerminalSession(term_id, pty_proc, channel, project_id, self.terminal_manager)
|
|
291
387
|
else:
|
|
292
388
|
# Unix: try real PTY for proper TTY semantics
|
|
293
389
|
try:
|
|
@@ -320,7 +416,7 @@ class SessionManager:
|
|
|
320
416
|
cwd=cwd,
|
|
321
417
|
env=_build_child_env(),
|
|
322
418
|
)
|
|
323
|
-
session = TerminalSession(term_id, proc, channel, project_id)
|
|
419
|
+
session = TerminalSession(term_id, proc, channel, project_id, self.terminal_manager)
|
|
324
420
|
|
|
325
421
|
self._sessions[term_id] = session
|
|
326
422
|
await session.start_io_forwarding()
|
|
@@ -167,11 +167,12 @@ class TerminalManager:
|
|
|
167
167
|
|
|
168
168
|
# Only create new session manager on initial setup, preserve existing one on reconnection
|
|
169
169
|
if is_initial or self._session_manager is None:
|
|
170
|
-
self._session_manager = SessionManager(mux)
|
|
170
|
+
self._session_manager = SessionManager(mux, terminal_manager=self)
|
|
171
171
|
logger.info("Created new SessionManager")
|
|
172
172
|
else:
|
|
173
|
-
# Update existing session manager's mux
|
|
173
|
+
# Update existing session manager's mux and terminal_manager references
|
|
174
174
|
self._session_manager.mux = mux
|
|
175
|
+
self._session_manager.terminal_manager = self
|
|
175
176
|
logger.info("Preserved existing SessionManager with %d sessions", len(self._session_manager._sessions))
|
|
176
177
|
|
|
177
178
|
# Create context for handlers
|
|
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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{portacode-0.3.12.dev7 → portacode-0.3.12.dev9}/portacode/connection/handlers/file_handlers.py
RENAMED
|
File without changes
|
|
File without changes
|
{portacode-0.3.12.dev7 → portacode-0.3.12.dev9}/portacode/connection/handlers/system_handlers.py
RENAMED
|
File without changes
|
{portacode-0.3.12.dev7 → portacode-0.3.12.dev9}/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
|
|
File without changes
|