[verified] feat: complete Ralph task workstreams
This commit is contained in:
@@ -3,6 +3,7 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import base64
|
||||
import http.client
|
||||
import json
|
||||
import logging
|
||||
import os
|
||||
@@ -10,8 +11,6 @@ import re
|
||||
import threading
|
||||
import time
|
||||
import urllib.parse
|
||||
import urllib.error
|
||||
import urllib.request
|
||||
from concurrent.futures import ThreadPoolExecutor
|
||||
from dataclasses import dataclass
|
||||
|
||||
@@ -44,8 +43,14 @@ BINARY_FRAME_MAGIC = b"MRF1"
|
||||
|
||||
|
||||
def encode_binary_frame(header: dict, body: bytes) -> bytes:
|
||||
"""Build one request-owned binary frame without base64 expansion.
|
||||
|
||||
``join`` makes one owned output frame rather than creating intermediate
|
||||
concatenation frames. The layout is intentionally unchanged because the
|
||||
relay ships an independent copy of this codec.
|
||||
"""
|
||||
header_bytes = json.dumps(header, separators=(",", ":")).encode()
|
||||
return BINARY_FRAME_MAGIC + len(header_bytes).to_bytes(4, "big") + header_bytes + body
|
||||
return b"".join((BINARY_FRAME_MAGIC, len(header_bytes).to_bytes(4, "big"), header_bytes, body))
|
||||
|
||||
|
||||
def decode_binary_frame(frame: bytes) -> tuple[dict, bytes]:
|
||||
@@ -53,7 +58,9 @@ def decode_binary_frame(frame: bytes) -> tuple[dict, bytes]:
|
||||
raise ValueError("not a meshnet binary relay frame")
|
||||
header_len = int.from_bytes(frame[4:8], "big")
|
||||
header = json.loads(frame[8:8 + header_len].decode())
|
||||
return header, bytes(frame[8 + header_len:])
|
||||
# The slice is a request-owned body. It cannot retain the enclosing relay
|
||||
# frame after callers finish processing it.
|
||||
return header, frame[8 + header_len:]
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
@@ -82,6 +89,62 @@ def _max_concurrency_from_env() -> int:
|
||||
return max(1, value)
|
||||
|
||||
|
||||
class _LoopbackHttpClientPool:
|
||||
"""Bounded worker-local HTTP/1.1 clients for relay loopback forwarding."""
|
||||
|
||||
def __init__(self, base_url: str, timeout: float = 300.0) -> None:
|
||||
parsed = urllib.parse.urlsplit(base_url.rstrip("/"))
|
||||
if parsed.scheme not in {"http", "https"} or not parsed.hostname:
|
||||
raise ValueError(f"invalid local bridge URL: {base_url!r}")
|
||||
self._scheme = parsed.scheme
|
||||
self._host = parsed.hostname
|
||||
self._port = parsed.port
|
||||
self._base_path = parsed.path.rstrip("/")
|
||||
self._timeout = timeout
|
||||
self._local = threading.local()
|
||||
self._lock = threading.Lock()
|
||||
self._clients: set[http.client.HTTPConnection] = set()
|
||||
|
||||
def _connection(self) -> http.client.HTTPConnection:
|
||||
connection = getattr(self._local, "connection", None)
|
||||
if connection is None:
|
||||
kind = http.client.HTTPSConnection if self._scheme == "https" else http.client.HTTPConnection
|
||||
connection = kind(self._host, self._port, timeout=self._timeout)
|
||||
self._local.connection = connection
|
||||
with self._lock:
|
||||
self._clients.add(connection)
|
||||
return connection
|
||||
|
||||
def request(self, method: str, path: str, body: bytes, headers: dict):
|
||||
request_path = f"{self._base_path}{path if path.startswith('/') else '/' + path}"
|
||||
connection = self._connection()
|
||||
try:
|
||||
connection.request(method, request_path, body=body, headers=headers)
|
||||
return connection.getresponse()
|
||||
except Exception:
|
||||
self.discard()
|
||||
raise
|
||||
|
||||
def discard(self) -> None:
|
||||
connection = getattr(self._local, "connection", None)
|
||||
if connection is None:
|
||||
return
|
||||
try:
|
||||
connection.close()
|
||||
finally:
|
||||
self._local.connection = None
|
||||
with self._lock:
|
||||
self._clients.discard(connection)
|
||||
|
||||
def close(self) -> None:
|
||||
with self._lock:
|
||||
clients = tuple(self._clients)
|
||||
self._clients.clear()
|
||||
for connection in clients:
|
||||
connection.close()
|
||||
self._local.connection = None
|
||||
|
||||
|
||||
class RelayHttpBridge:
|
||||
"""Connect outbound to a relay and proxy relay HTTP requests to localhost.
|
||||
|
||||
@@ -115,6 +178,7 @@ class RelayHttpBridge:
|
||||
self._decode_log_lock = threading.Lock()
|
||||
self._decode_steps: dict[str, int] = {}
|
||||
self._ws = None
|
||||
self._loopback_clients = _LoopbackHttpClientPool(self.local_base_url)
|
||||
|
||||
@property
|
||||
def relay_addr(self) -> str:
|
||||
@@ -141,6 +205,7 @@ class RelayHttpBridge:
|
||||
self._thread.join(timeout=3.0)
|
||||
if self._executor is not None:
|
||||
self._executor.shutdown(wait=False)
|
||||
self._loopback_clients.close()
|
||||
|
||||
def _run(self) -> None:
|
||||
import websockets.sync.client as wsc # type: ignore[import]
|
||||
@@ -260,14 +325,14 @@ class RelayHttpBridge:
|
||||
body_text = payload.get("body") or ""
|
||||
data = body_text.encode() if isinstance(body_text, str) else bytes(body_text)
|
||||
|
||||
url = f"{self.local_base_url}{path}"
|
||||
req = urllib.request.Request(url, data=data, headers=headers, method=method)
|
||||
try:
|
||||
with urllib.request.urlopen(req, timeout=300.0) as resp:
|
||||
resp = self._loopback_clients.request(method, path, data, headers)
|
||||
try:
|
||||
resp_headers = dict(resp.headers)
|
||||
content_type = resp.headers.get("Content-Type", "")
|
||||
if "text/event-stream" in content_type:
|
||||
self._stream_response(request_id, resp, resp_headers)
|
||||
if not self._stream_response(request_id, resp, resp_headers):
|
||||
self._loopback_clients.discard()
|
||||
return
|
||||
resp_bytes = resp.read()
|
||||
# Forward all X-Meshnet-* headers so the caller can reconstruct the activation.
|
||||
@@ -289,14 +354,18 @@ class RelayHttpBridge:
|
||||
else:
|
||||
result["body"] = resp_bytes.decode(errors="replace")
|
||||
self._send_response_frame(result)
|
||||
except urllib.error.HTTPError as exc:
|
||||
finally:
|
||||
resp.close()
|
||||
except http.client.HTTPException as exc:
|
||||
self._loopback_clients.discard()
|
||||
self._send_response_frame({
|
||||
"request_id": request_id,
|
||||
"status": exc.code,
|
||||
"headers": {"Content-Type": exc.headers.get("Content-Type", "application/json")},
|
||||
"body": exc.read().decode(errors="replace"),
|
||||
"status": 503,
|
||||
"headers": {"Content-Type": "application/json"},
|
||||
"body": json.dumps({"error": f"relay bridge local request failed: {exc}"}),
|
||||
})
|
||||
except Exception as exc:
|
||||
self._loopback_clients.discard()
|
||||
self._send_response_frame({
|
||||
"request_id": request_id,
|
||||
"status": 503,
|
||||
@@ -304,7 +373,7 @@ class RelayHttpBridge:
|
||||
"body": json.dumps({"error": f"relay bridge local request failed: {exc}"}),
|
||||
})
|
||||
|
||||
def _stream_response(self, request_id: str, resp, resp_headers: dict) -> None:
|
||||
def _stream_response(self, request_id: str, resp, resp_headers: dict) -> bool:
|
||||
"""Forward an SSE response as chunk frames, one per complete SSE event.
|
||||
|
||||
Frame order: header frame (status + headers), chunk frames, done frame.
|
||||
@@ -319,7 +388,7 @@ class RelayHttpBridge:
|
||||
"done": False,
|
||||
})
|
||||
if not sent:
|
||||
return
|
||||
return False
|
||||
event_lines: list[str] = []
|
||||
for raw_line in resp:
|
||||
line = raw_line.decode(errors="replace")
|
||||
@@ -333,7 +402,7 @@ class RelayHttpBridge:
|
||||
"chunk": "".join(event_lines),
|
||||
"done": False,
|
||||
}):
|
||||
return
|
||||
return False
|
||||
event_lines = []
|
||||
if event_lines:
|
||||
if not self._send_response_frame({
|
||||
@@ -342,8 +411,8 @@ class RelayHttpBridge:
|
||||
"chunk": "".join(event_lines),
|
||||
"done": False,
|
||||
}):
|
||||
return
|
||||
self._send_response_frame({
|
||||
return False
|
||||
return self._send_response_frame({
|
||||
"request_id": request_id,
|
||||
"stream": True,
|
||||
"done": True,
|
||||
|
||||
Reference in New Issue
Block a user