h4-debug 0.1.1__tar.gz → 0.1.2__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.
- {h4_debug-0.1.1 → h4_debug-0.1.2}/PKG-INFO +1 -1
- h4_debug-0.1.2/h4_debug/__init__.py +1 -0
- {h4_debug-0.1.1 → h4_debug-0.1.2}/h4_debug/interceptor.py +18 -24
- {h4_debug-0.1.1 → h4_debug-0.1.2}/h4_debug/server.py +70 -39
- {h4_debug-0.1.1 → h4_debug-0.1.2}/h4_debug.egg-info/PKG-INFO +1 -1
- {h4_debug-0.1.1 → h4_debug-0.1.2}/pyproject.toml +1 -1
- h4_debug-0.1.1/h4_debug/__init__.py +0 -1
- {h4_debug-0.1.1 → h4_debug-0.1.2}/README.md +0 -0
- {h4_debug-0.1.1 → h4_debug-0.1.2}/h4_debug/cli.py +0 -0
- {h4_debug-0.1.1 → h4_debug-0.1.2}/h4_debug/static/dashboard.css +0 -0
- {h4_debug-0.1.1 → h4_debug-0.1.2}/h4_debug/static/dashboard.html +0 -0
- {h4_debug-0.1.1 → h4_debug-0.1.2}/h4_debug/static/dashboard.js +0 -0
- {h4_debug-0.1.1 → h4_debug-0.1.2}/h4_debug.egg-info/SOURCES.txt +0 -0
- {h4_debug-0.1.1 → h4_debug-0.1.2}/h4_debug.egg-info/dependency_links.txt +0 -0
- {h4_debug-0.1.1 → h4_debug-0.1.2}/h4_debug.egg-info/entry_points.txt +0 -0
- {h4_debug-0.1.1 → h4_debug-0.1.2}/h4_debug.egg-info/requires.txt +0 -0
- {h4_debug-0.1.1 → h4_debug-0.1.2}/h4_debug.egg-info/top_level.txt +0 -0
- {h4_debug-0.1.1 → h4_debug-0.1.2}/setup.cfg +0 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "0.1.2"
|
|
@@ -7,13 +7,9 @@ import sys
|
|
|
7
7
|
import threading
|
|
8
8
|
import time
|
|
9
9
|
import traceback
|
|
10
|
+
import select
|
|
10
11
|
from datetime import datetime
|
|
11
12
|
|
|
12
|
-
try:
|
|
13
|
-
from websockets.sync.client import connect
|
|
14
|
-
except ImportError:
|
|
15
|
-
connect = None
|
|
16
|
-
|
|
17
13
|
# We must keep references to original functions to avoid infinite recursion
|
|
18
14
|
_original_open = builtins.open
|
|
19
15
|
_original_socket = socket.socket
|
|
@@ -27,37 +23,35 @@ class TelemetryClient:
|
|
|
27
23
|
self.thread.start()
|
|
28
24
|
|
|
29
25
|
def _run(self):
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
26
|
+
# Connect to the local TCP server spawned by h4-debug
|
|
27
|
+
server_port = self.port + 1
|
|
28
|
+
|
|
34
29
|
while True:
|
|
35
30
|
try:
|
|
36
|
-
with
|
|
31
|
+
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as conn:
|
|
32
|
+
conn.connect(("127.0.0.1", server_port))
|
|
33
|
+
|
|
37
34
|
while True:
|
|
38
35
|
# 1. Send all queued outgoing telemetry
|
|
39
36
|
while not self.queue.empty():
|
|
40
37
|
try:
|
|
41
38
|
msg = self.queue.get_nowait()
|
|
42
|
-
|
|
43
|
-
self.queue.task_done()
|
|
39
|
+
conn.sendall((json.dumps(msg) + "\n").encode("utf-8"))
|
|
44
40
|
except queue.Empty:
|
|
45
41
|
break
|
|
46
42
|
|
|
47
43
|
# 2. Check for incoming commands (with short timeout)
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
raise
|
|
58
|
-
|
|
44
|
+
r, _, _ = select.select([conn], [], [], 0.05)
|
|
45
|
+
if r:
|
|
46
|
+
data = conn.recv(4096)
|
|
47
|
+
if not data:
|
|
48
|
+
break # Connection closed
|
|
49
|
+
for line in data.decode("utf-8").split("\n"):
|
|
50
|
+
if line.strip():
|
|
51
|
+
self._handle_command(line)
|
|
52
|
+
|
|
59
53
|
time.sleep(0.01)
|
|
60
|
-
except Exception
|
|
54
|
+
except Exception:
|
|
61
55
|
time.sleep(1)
|
|
62
56
|
|
|
63
57
|
def _handle_command(self, cmd_data):
|
|
@@ -10,7 +10,32 @@ app = FastAPI()
|
|
|
10
10
|
|
|
11
11
|
# Store connected dashboard clients and the active telemetry process
|
|
12
12
|
dashboard_clients = []
|
|
13
|
-
active_telemetry_client = None
|
|
13
|
+
active_telemetry_client = None # This will now be a TCP socket
|
|
14
|
+
|
|
15
|
+
# For broadcasting logs to WebSockets
|
|
16
|
+
def broadcast_log(log_item_str):
|
|
17
|
+
try:
|
|
18
|
+
log_item = json.loads(log_item_str)
|
|
19
|
+
log_history.append(log_item)
|
|
20
|
+
if len(log_history) > MAX_HISTORY:
|
|
21
|
+
log_history.pop(0)
|
|
22
|
+
except Exception:
|
|
23
|
+
pass
|
|
24
|
+
|
|
25
|
+
disconnected_clients = []
|
|
26
|
+
# Needs to be scheduled on the asyncio loop
|
|
27
|
+
async def _send(client):
|
|
28
|
+
try:
|
|
29
|
+
await client.send_text(log_item_str)
|
|
30
|
+
except Exception:
|
|
31
|
+
disconnected_clients.append(client)
|
|
32
|
+
|
|
33
|
+
for client in dashboard_clients.copy():
|
|
34
|
+
asyncio.run_coroutine_threadsafe(_send(client), loop)
|
|
35
|
+
|
|
36
|
+
for client in disconnected_clients:
|
|
37
|
+
if client in dashboard_clients:
|
|
38
|
+
dashboard_clients.remove(client)
|
|
14
39
|
|
|
15
40
|
# To persist logs for when a dashboard connects slightly after startup
|
|
16
41
|
log_history = []
|
|
@@ -31,40 +56,6 @@ async def get_dashboard():
|
|
|
31
56
|
return HTMLResponse(f.read())
|
|
32
57
|
return HTMLResponse("<h1>Dashboard not found</h1>")
|
|
33
58
|
|
|
34
|
-
@app.websocket("/ws/telemetry")
|
|
35
|
-
async def websocket_telemetry(websocket: WebSocket):
|
|
36
|
-
"""Endpoint for the intercepted process to push logs and receive commands."""
|
|
37
|
-
global active_telemetry_client
|
|
38
|
-
await websocket.accept()
|
|
39
|
-
active_telemetry_client = websocket
|
|
40
|
-
|
|
41
|
-
try:
|
|
42
|
-
while True:
|
|
43
|
-
data = await websocket.receive_text()
|
|
44
|
-
|
|
45
|
-
try:
|
|
46
|
-
log_item = json.loads(data)
|
|
47
|
-
log_history.append(log_item)
|
|
48
|
-
if len(log_history) > MAX_HISTORY:
|
|
49
|
-
log_history.pop(0)
|
|
50
|
-
except Exception:
|
|
51
|
-
pass
|
|
52
|
-
|
|
53
|
-
# Broadcast to all dashboards
|
|
54
|
-
disconnected_clients = []
|
|
55
|
-
for client in dashboard_clients:
|
|
56
|
-
try:
|
|
57
|
-
await client.send_text(data)
|
|
58
|
-
except Exception:
|
|
59
|
-
disconnected_clients.append(client)
|
|
60
|
-
|
|
61
|
-
for client in disconnected_clients:
|
|
62
|
-
if client in dashboard_clients:
|
|
63
|
-
dashboard_clients.remove(client)
|
|
64
|
-
except WebSocketDisconnect:
|
|
65
|
-
if active_telemetry_client == websocket:
|
|
66
|
-
active_telemetry_client = None
|
|
67
|
-
|
|
68
59
|
@app.websocket("/ws/dashboard")
|
|
69
60
|
async def websocket_dashboard(websocket: WebSocket):
|
|
70
61
|
"""Endpoint for the web dashboard to receive logs and send commands."""
|
|
@@ -87,20 +78,60 @@ async def websocket_dashboard(websocket: WebSocket):
|
|
|
87
78
|
if cmd.get("action") == "clear":
|
|
88
79
|
log_history.clear()
|
|
89
80
|
elif cmd.get("action") in ["evaluate", "set_mode"]:
|
|
90
|
-
# Forward commands to the active telemetry process
|
|
81
|
+
# Forward commands to the active telemetry process via TCP
|
|
91
82
|
if active_telemetry_client:
|
|
92
83
|
try:
|
|
93
|
-
|
|
84
|
+
active_telemetry_client.sendall((json.dumps(cmd) + "\n").encode("utf-8"))
|
|
94
85
|
except Exception:
|
|
95
|
-
|
|
86
|
+
active_telemetry_client = None
|
|
96
87
|
except WebSocketDisconnect:
|
|
97
88
|
if websocket in dashboard_clients:
|
|
98
89
|
dashboard_clients.remove(websocket)
|
|
99
90
|
|
|
91
|
+
import socket
|
|
92
|
+
import threading
|
|
93
|
+
|
|
94
|
+
def run_tcp_server(port: int):
|
|
95
|
+
global active_telemetry_client
|
|
96
|
+
server = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
|
97
|
+
server.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
|
|
98
|
+
server.bind(("127.0.0.1", port + 1))
|
|
99
|
+
server.listen(1)
|
|
100
|
+
|
|
101
|
+
while True:
|
|
102
|
+
conn, addr = server.accept()
|
|
103
|
+
active_telemetry_client = conn
|
|
104
|
+
|
|
105
|
+
def handle_client(c):
|
|
106
|
+
global active_telemetry_client
|
|
107
|
+
f = c.makefile("r", encoding="utf-8")
|
|
108
|
+
try:
|
|
109
|
+
while True:
|
|
110
|
+
line = f.readline()
|
|
111
|
+
if not line:
|
|
112
|
+
break
|
|
113
|
+
broadcast_log(line)
|
|
114
|
+
except Exception:
|
|
115
|
+
pass
|
|
116
|
+
finally:
|
|
117
|
+
if active_telemetry_client == c:
|
|
118
|
+
active_telemetry_client = None
|
|
119
|
+
c.close()
|
|
120
|
+
|
|
121
|
+
threading.Thread(target=handle_client, args=(conn,), daemon=True).start()
|
|
122
|
+
|
|
100
123
|
def run_server(port: int):
|
|
101
124
|
# Disable uvicorn access logs to keep terminal clean
|
|
102
125
|
import logging
|
|
103
126
|
log = logging.getLogger("uvicorn.access")
|
|
104
127
|
log.setLevel(logging.WARNING)
|
|
105
128
|
|
|
106
|
-
|
|
129
|
+
global loop
|
|
130
|
+
loop = asyncio.new_event_loop()
|
|
131
|
+
asyncio.set_event_loop(loop)
|
|
132
|
+
|
|
133
|
+
threading.Thread(target=run_tcp_server, args=(port,), daemon=True).start()
|
|
134
|
+
|
|
135
|
+
config = uvicorn.Config(app, host="127.0.0.1", port=port, log_level="warning", loop="asyncio")
|
|
136
|
+
server = uvicorn.Server(config)
|
|
137
|
+
loop.run_until_complete(server.serve())
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
__version__ = "0.1.1"
|
|
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
|