h4-debug 0.1.3__tar.gz → 0.1.4__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.3 → h4_debug-0.1.4}/PKG-INFO +1 -1
- h4_debug-0.1.4/h4_debug/__init__.py +1 -0
- {h4_debug-0.1.3 → h4_debug-0.1.4}/h4_debug/interceptor.py +48 -20
- {h4_debug-0.1.3 → h4_debug-0.1.4}/h4_debug/server.py +15 -13
- {h4_debug-0.1.3 → h4_debug-0.1.4}/h4_debug.egg-info/PKG-INFO +1 -1
- {h4_debug-0.1.3 → h4_debug-0.1.4}/pyproject.toml +1 -1
- h4_debug-0.1.3/h4_debug/__init__.py +0 -1
- {h4_debug-0.1.3 → h4_debug-0.1.4}/README.md +0 -0
- {h4_debug-0.1.3 → h4_debug-0.1.4}/h4_debug/cli.py +0 -0
- {h4_debug-0.1.3 → h4_debug-0.1.4}/h4_debug/static/dashboard.css +0 -0
- {h4_debug-0.1.3 → h4_debug-0.1.4}/h4_debug/static/dashboard.html +0 -0
- {h4_debug-0.1.3 → h4_debug-0.1.4}/h4_debug/static/dashboard.js +0 -0
- {h4_debug-0.1.3 → h4_debug-0.1.4}/h4_debug.egg-info/SOURCES.txt +0 -0
- {h4_debug-0.1.3 → h4_debug-0.1.4}/h4_debug.egg-info/dependency_links.txt +0 -0
- {h4_debug-0.1.3 → h4_debug-0.1.4}/h4_debug.egg-info/entry_points.txt +0 -0
- {h4_debug-0.1.3 → h4_debug-0.1.4}/h4_debug.egg-info/requires.txt +0 -0
- {h4_debug-0.1.3 → h4_debug-0.1.4}/h4_debug.egg-info/top_level.txt +0 -0
- {h4_debug-0.1.3 → h4_debug-0.1.4}/setup.cfg +0 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "0.1.4"
|
|
@@ -3,11 +3,13 @@ import json
|
|
|
3
3
|
import os
|
|
4
4
|
import queue
|
|
5
5
|
import socket
|
|
6
|
-
import
|
|
7
|
-
import threading
|
|
6
|
+
import json
|
|
8
7
|
import time
|
|
9
8
|
import traceback
|
|
10
9
|
import select
|
|
10
|
+
import sys
|
|
11
|
+
import threading
|
|
12
|
+
import sysconfig
|
|
11
13
|
from datetime import datetime
|
|
12
14
|
|
|
13
15
|
# We must keep references to original functions to avoid infinite recursion
|
|
@@ -19,6 +21,7 @@ class TelemetryClient:
|
|
|
19
21
|
def __init__(self, port):
|
|
20
22
|
self.port = port
|
|
21
23
|
self.queue = queue.Queue()
|
|
24
|
+
self.buffer = ""
|
|
22
25
|
self.thread = threading.Thread(target=self._run, daemon=True)
|
|
23
26
|
self.thread.start()
|
|
24
27
|
|
|
@@ -40,15 +43,18 @@ class TelemetryClient:
|
|
|
40
43
|
except queue.Empty:
|
|
41
44
|
break
|
|
42
45
|
|
|
43
|
-
# 2.
|
|
46
|
+
# 2. Process incoming commands
|
|
44
47
|
r, _, _ = select.select([conn], [], [], 0.05)
|
|
45
48
|
if r:
|
|
46
49
|
data = conn.recv(4096)
|
|
47
50
|
if not data:
|
|
48
51
|
break # Connection closed
|
|
49
|
-
|
|
52
|
+
|
|
53
|
+
self.buffer += data.decode("utf-8", errors="replace")
|
|
54
|
+
while "\n" in self.buffer:
|
|
55
|
+
line, self.buffer = self.buffer.split("\n", 1)
|
|
50
56
|
if line.strip():
|
|
51
|
-
self._handle_command(line)
|
|
57
|
+
self._handle_command(line.strip())
|
|
52
58
|
|
|
53
59
|
time.sleep(0.01)
|
|
54
60
|
except Exception:
|
|
@@ -57,16 +63,13 @@ class TelemetryClient:
|
|
|
57
63
|
def _handle_command(self, cmd_data):
|
|
58
64
|
try:
|
|
59
65
|
cmd = json.loads(cmd_data)
|
|
60
|
-
action = cmd.get("action")
|
|
61
66
|
|
|
62
|
-
if action == "set_mode":
|
|
63
|
-
new_mode = cmd.get("mode", "Normal")
|
|
67
|
+
if cmd.get("action") == "set_mode":
|
|
64
68
|
global _mode
|
|
65
|
-
_mode =
|
|
66
|
-
|
|
69
|
+
_mode = cmd.get("mode", "Normal")
|
|
67
70
|
self.send("System", "info", {"text": f"Mode changed to {_mode}"})
|
|
68
71
|
|
|
69
|
-
elif action == "evaluate":
|
|
72
|
+
elif cmd.get("action") == "evaluate":
|
|
70
73
|
code = cmd.get("code", "")
|
|
71
74
|
result = None
|
|
72
75
|
is_error = False
|
|
@@ -196,18 +199,38 @@ class PatchedStream:
|
|
|
196
199
|
|
|
197
200
|
# --- Execution Tracing ---
|
|
198
201
|
|
|
199
|
-
def
|
|
200
|
-
if
|
|
201
|
-
|
|
202
|
+
def is_user_code_path(filename):
|
|
203
|
+
# Check if a filename belongs to the user's project
|
|
204
|
+
cwd = os.getcwd()
|
|
205
|
+
if filename.startswith(cwd):
|
|
206
|
+
return True
|
|
207
|
+
|
|
208
|
+
if "site-packages" in filename:
|
|
209
|
+
return False
|
|
210
|
+
|
|
211
|
+
stdlib = sysconfig.get_path('stdlib')
|
|
212
|
+
if stdlib and filename.startswith(stdlib):
|
|
213
|
+
return False
|
|
202
214
|
|
|
215
|
+
# As a fallback, check if it starts with the python prefix
|
|
216
|
+
if filename.startswith(sys.prefix) or filename.startswith(sys.base_prefix):
|
|
217
|
+
return False
|
|
218
|
+
|
|
219
|
+
return True
|
|
220
|
+
|
|
221
|
+
def trace_calls(frame, event, arg):
|
|
222
|
+
global _mode
|
|
203
223
|
if _mode == "Normal":
|
|
224
|
+
return trace_calls # Always trace so we can hot-swap, but do nothing in Normal
|
|
225
|
+
|
|
226
|
+
if not _telemetry:
|
|
204
227
|
return trace_calls
|
|
205
|
-
|
|
228
|
+
|
|
206
229
|
filename = frame.f_code.co_filename
|
|
207
230
|
if "h4_debug" in filename or "websockets" in filename:
|
|
208
231
|
return trace_calls # Ignore our own debugger code
|
|
209
232
|
|
|
210
|
-
is_user_code =
|
|
233
|
+
is_user_code = is_user_code_path(filename)
|
|
211
234
|
|
|
212
235
|
if _mode == "Trace":
|
|
213
236
|
if not is_user_code:
|
|
@@ -260,6 +283,14 @@ def start_interception(mode="Normal"):
|
|
|
260
283
|
_telemetry = TelemetryClient(port)
|
|
261
284
|
_telemetry_thread_id = _telemetry.thread.ident
|
|
262
285
|
|
|
286
|
+
# Always engage tracing on startup so we can hot-swap modes dynamically
|
|
287
|
+
# It will remain dormant if mode == "Normal"
|
|
288
|
+
sys.settrace(trace_calls)
|
|
289
|
+
threading.settrace(trace_calls)
|
|
290
|
+
|
|
291
|
+
apply_patches()
|
|
292
|
+
|
|
293
|
+
def apply_patches():
|
|
263
294
|
# Patch Disk
|
|
264
295
|
builtins.open = patched_open
|
|
265
296
|
|
|
@@ -269,12 +300,9 @@ def start_interception(mode="Normal"):
|
|
|
269
300
|
# Patch Console
|
|
270
301
|
sys.stdout = PatchedStream(sys.stdout, "stdout")
|
|
271
302
|
sys.stderr = PatchedStream(sys.stderr, "stderr")
|
|
272
|
-
|
|
273
|
-
# ALWAYS enable Tracing so dynamic mode switching works later
|
|
274
|
-
sys.settrace(trace_calls)
|
|
275
303
|
|
|
276
304
|
_telemetry.send("System", "init", {
|
|
277
|
-
"mode":
|
|
305
|
+
"mode": _mode,
|
|
278
306
|
"pid": os.getpid(),
|
|
279
307
|
"language": "python",
|
|
280
308
|
"version": sys.version
|
|
@@ -9,8 +9,9 @@ from fastapi.staticfiles import StaticFiles
|
|
|
9
9
|
app = FastAPI()
|
|
10
10
|
|
|
11
11
|
# Store connected dashboard clients and the active telemetry process
|
|
12
|
-
dashboard_clients = []
|
|
13
|
-
|
|
12
|
+
dashboard_clients: List[WebSocket] = []
|
|
13
|
+
telemetry_clients = set()
|
|
14
|
+
MAX_HISTORY = 1000
|
|
14
15
|
|
|
15
16
|
# For broadcasting logs to WebSockets
|
|
16
17
|
def broadcast_log(log_item_str):
|
|
@@ -39,7 +40,6 @@ def broadcast_log(log_item_str):
|
|
|
39
40
|
|
|
40
41
|
# To persist logs for when a dashboard connects slightly after startup
|
|
41
42
|
log_history = []
|
|
42
|
-
MAX_HISTORY = 5000
|
|
43
43
|
|
|
44
44
|
# Get the path to static files
|
|
45
45
|
STATIC_DIR = os.path.join(os.path.dirname(__file__), "static")
|
|
@@ -59,7 +59,6 @@ async def get_dashboard():
|
|
|
59
59
|
@app.websocket("/ws/dashboard")
|
|
60
60
|
async def websocket_dashboard(websocket: WebSocket):
|
|
61
61
|
"""Endpoint for the web dashboard to receive logs and send commands."""
|
|
62
|
-
global active_telemetry_client
|
|
63
62
|
await websocket.accept()
|
|
64
63
|
|
|
65
64
|
# Send history on connect
|
|
@@ -79,12 +78,17 @@ async def websocket_dashboard(websocket: WebSocket):
|
|
|
79
78
|
if cmd.get("action") == "clear":
|
|
80
79
|
log_history.clear()
|
|
81
80
|
elif cmd.get("action") in ["evaluate", "set_mode"]:
|
|
82
|
-
# Forward commands to
|
|
83
|
-
|
|
81
|
+
# Forward commands to ALL active telemetry processes via TCP
|
|
82
|
+
dead_clients = set()
|
|
83
|
+
for client in telemetry_clients:
|
|
84
84
|
try:
|
|
85
|
-
|
|
85
|
+
client.sendall((json.dumps(cmd) + "\n").encode("utf-8"))
|
|
86
86
|
except Exception:
|
|
87
|
-
|
|
87
|
+
dead_clients.add(client)
|
|
88
|
+
|
|
89
|
+
for dead in dead_clients:
|
|
90
|
+
telemetry_clients.discard(dead)
|
|
91
|
+
|
|
88
92
|
except WebSocketDisconnect:
|
|
89
93
|
if websocket in dashboard_clients:
|
|
90
94
|
dashboard_clients.remove(websocket)
|
|
@@ -93,7 +97,6 @@ import socket
|
|
|
93
97
|
import threading
|
|
94
98
|
|
|
95
99
|
def run_tcp_server(port: int):
|
|
96
|
-
global active_telemetry_client
|
|
97
100
|
server = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
|
98
101
|
server.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
|
|
99
102
|
server.bind(("127.0.0.1", port + 1))
|
|
@@ -101,11 +104,10 @@ def run_tcp_server(port: int):
|
|
|
101
104
|
|
|
102
105
|
while True:
|
|
103
106
|
conn, addr = server.accept()
|
|
104
|
-
active_telemetry_client = conn
|
|
105
107
|
|
|
106
108
|
def handle_client(c):
|
|
107
|
-
global active_telemetry_client
|
|
108
109
|
f = c.makefile("r", encoding="utf-8")
|
|
110
|
+
telemetry_clients.add(c)
|
|
109
111
|
try:
|
|
110
112
|
while True:
|
|
111
113
|
line = f.readline()
|
|
@@ -115,8 +117,8 @@ def run_tcp_server(port: int):
|
|
|
115
117
|
except Exception:
|
|
116
118
|
pass
|
|
117
119
|
finally:
|
|
118
|
-
if
|
|
119
|
-
|
|
120
|
+
if c in telemetry_clients:
|
|
121
|
+
telemetry_clients.remove(c)
|
|
120
122
|
c.close()
|
|
121
123
|
|
|
122
124
|
threading.Thread(target=handle_client, args=(conn,), daemon=True).start()
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
__version__ = "0.1.3"
|
|
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
|