arkaos 3.71.0 → 3.71.1
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.
- package/VERSION +1 -1
- package/core/terminal/__init__.py +2 -0
- package/core/terminal/__pycache__/__init__.cpython-313.pyc +0 -0
- package/core/terminal/__pycache__/connections.cpython-313.pyc +0 -0
- package/core/terminal/__pycache__/session.cpython-313.pyc +0 -0
- package/core/terminal/connections.py +46 -0
- package/dashboard/package.json +3 -1
- package/package.json +1 -1
- package/pyproject.toml +1 -1
- package/scripts/__pycache__/dashboard-api.cpython-313.pyc +0 -0
- package/scripts/dashboard-api.py +23 -4
package/VERSION
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
3.71.
|
|
1
|
+
3.71.1
|
|
@@ -16,6 +16,7 @@ from core.terminal.session import (
|
|
|
16
16
|
)
|
|
17
17
|
from core.terminal.audit import log_end, log_start
|
|
18
18
|
from core.terminal.token import current_token
|
|
19
|
+
from core.terminal.connections import ConnectionRegistry
|
|
19
20
|
|
|
20
21
|
__all__ = [
|
|
21
22
|
"TerminalSession",
|
|
@@ -24,4 +25,5 @@ __all__ = [
|
|
|
24
25
|
"log_start",
|
|
25
26
|
"log_end",
|
|
26
27
|
"current_token",
|
|
28
|
+
"ConnectionRegistry",
|
|
27
29
|
]
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
"""Single active WebSocket connection per terminal session (v3.71.1).
|
|
2
|
+
|
|
3
|
+
asyncio allows only one reader per fd, so two concurrent WebSockets on the
|
|
4
|
+
same session would fight over the PTY master fd and the scrollback replay
|
|
5
|
+
could duplicate output. This registry enforces "latest wins": a new
|
|
6
|
+
connection supersedes the previous one (which the endpoint then closes),
|
|
7
|
+
and release is guarded so a superseded connection's teardown cannot evict
|
|
8
|
+
its replacement.
|
|
9
|
+
|
|
10
|
+
Pure bookkeeping — no FastAPI import, so it's unit-testable standalone.
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
from __future__ import annotations
|
|
14
|
+
|
|
15
|
+
from typing import Any, Optional
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class ConnectionRegistry:
|
|
19
|
+
"""Tracks the one live connection per session id."""
|
|
20
|
+
|
|
21
|
+
def __init__(self) -> None:
|
|
22
|
+
self._active: dict[str, Any] = {}
|
|
23
|
+
|
|
24
|
+
def acquire(self, session_id: str, conn: Any) -> Optional[Any]:
|
|
25
|
+
"""Make ``conn`` the active connection for ``session_id``.
|
|
26
|
+
|
|
27
|
+
Returns the connection it superseded (for the caller to close), or
|
|
28
|
+
``None`` when there was none / it was already active.
|
|
29
|
+
"""
|
|
30
|
+
old = self._active.get(session_id)
|
|
31
|
+
self._active[session_id] = conn
|
|
32
|
+
return old if old is not conn else None
|
|
33
|
+
|
|
34
|
+
def release(self, session_id: str, conn: Any) -> bool:
|
|
35
|
+
"""Drop ``conn`` iff it is still the active connection.
|
|
36
|
+
|
|
37
|
+
Returns whether it was — the caller should only tear down shared
|
|
38
|
+
resources (the fd reader) when this is ``True``.
|
|
39
|
+
"""
|
|
40
|
+
if self._active.get(session_id) is conn:
|
|
41
|
+
del self._active[session_id]
|
|
42
|
+
return True
|
|
43
|
+
return False
|
|
44
|
+
|
|
45
|
+
def is_active(self, session_id: str, conn: Any) -> bool:
|
|
46
|
+
return self._active.get(session_id) is conn
|
package/dashboard/package.json
CHANGED
|
@@ -8,7 +8,8 @@
|
|
|
8
8
|
"preview": "nuxt preview",
|
|
9
9
|
"postinstall": "nuxt prepare",
|
|
10
10
|
"lint": "eslint .",
|
|
11
|
-
"typecheck": "nuxt typecheck"
|
|
11
|
+
"typecheck": "nuxt typecheck",
|
|
12
|
+
"test:e2e": "playwright test"
|
|
12
13
|
},
|
|
13
14
|
"dependencies": {
|
|
14
15
|
"@iconify-json/lucide": "^1.2.100",
|
|
@@ -34,6 +35,7 @@
|
|
|
34
35
|
},
|
|
35
36
|
"devDependencies": {
|
|
36
37
|
"@nuxt/eslint": "^1.15.2",
|
|
38
|
+
"@playwright/test": "^1.60.0",
|
|
37
39
|
"eslint": "^10.1.0",
|
|
38
40
|
"typescript": "^6.0.2",
|
|
39
41
|
"vue-tsc": "^3.2.6"
|
package/package.json
CHANGED
package/pyproject.toml
CHANGED
|
Binary file
|
package/scripts/dashboard-api.py
CHANGED
|
@@ -2282,6 +2282,12 @@ def _terminal_origin_ok(origin: str) -> bool:
|
|
|
2282
2282
|
)
|
|
2283
2283
|
|
|
2284
2284
|
|
|
2285
|
+
# v3.71.1 — enforce a single live WebSocket per session (latest wins), so a
|
|
2286
|
+
# reload or a second tab can't fight over the one PTY fd reader.
|
|
2287
|
+
from core.terminal.connections import ConnectionRegistry as _TerminalConnRegistry
|
|
2288
|
+
_terminal_conns = _TerminalConnRegistry()
|
|
2289
|
+
|
|
2290
|
+
|
|
2285
2291
|
@app.get("/api/terminal/token")
|
|
2286
2292
|
def terminal_token_endpoint():
|
|
2287
2293
|
"""Return the per-process bearer token used in WS handshakes.
|
|
@@ -2366,6 +2372,16 @@ async def ws_terminal(ws: WebSocket, session_id: str, token: str = Query("")):
|
|
|
2366
2372
|
|
|
2367
2373
|
await ws.accept()
|
|
2368
2374
|
|
|
2375
|
+
# v3.71.1 — single live connection per session: a newer connection
|
|
2376
|
+
# (a reload, or a second tab) supersedes the previous one, which we
|
|
2377
|
+
# close so it stops pumping the shared PTY fd.
|
|
2378
|
+
superseded = _terminal_conns.acquire(session_id, ws)
|
|
2379
|
+
if superseded is not None:
|
|
2380
|
+
try:
|
|
2381
|
+
await superseded.close(code=4409, reason="superseded by a newer connection")
|
|
2382
|
+
except Exception:
|
|
2383
|
+
pass
|
|
2384
|
+
|
|
2369
2385
|
# v3.71.0 — replay recent scrollback so a client reconnecting after
|
|
2370
2386
|
# the operator navigated away / reloaded restores its session as it
|
|
2371
2387
|
# left it. Sent before the live reader is attached, so the historical
|
|
@@ -2437,10 +2453,13 @@ async def ws_terminal(ws: WebSocket, session_id: str, token: str = Query("")):
|
|
|
2437
2453
|
pass
|
|
2438
2454
|
finally:
|
|
2439
2455
|
pump_task.cancel()
|
|
2440
|
-
|
|
2441
|
-
|
|
2442
|
-
|
|
2443
|
-
|
|
2456
|
+
# Only the still-active connection owns the fd reader — a superseded
|
|
2457
|
+
# connection tearing down must not remove its replacement's reader.
|
|
2458
|
+
if _terminal_conns.release(session_id, ws):
|
|
2459
|
+
try:
|
|
2460
|
+
loop.remove_reader(session.master_fd)
|
|
2461
|
+
except (ValueError, OSError):
|
|
2462
|
+
pass
|
|
2444
2463
|
|
|
2445
2464
|
|
|
2446
2465
|
@app.on_event("startup")
|