h4-debug 0.1.2__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.2 → h4_debug-0.1.4}/PKG-INFO +1 -1
- h4_debug-0.1.4/h4_debug/__init__.py +1 -0
- {h4_debug-0.1.2 → h4_debug-0.1.4}/h4_debug/interceptor.py +48 -20
- {h4_debug-0.1.2 → h4_debug-0.1.4}/h4_debug/server.py +15 -12
- {h4_debug-0.1.2 → h4_debug-0.1.4}/h4_debug/static/dashboard.css +70 -8
- {h4_debug-0.1.2 → h4_debug-0.1.4}/h4_debug/static/dashboard.html +17 -3
- {h4_debug-0.1.2 → h4_debug-0.1.4}/h4_debug.egg-info/PKG-INFO +1 -1
- {h4_debug-0.1.2 → h4_debug-0.1.4}/pyproject.toml +1 -1
- h4_debug-0.1.2/h4_debug/__init__.py +0 -1
- {h4_debug-0.1.2 → h4_debug-0.1.4}/README.md +0 -0
- {h4_debug-0.1.2 → h4_debug-0.1.4}/h4_debug/cli.py +0 -0
- {h4_debug-0.1.2 → h4_debug-0.1.4}/h4_debug/static/dashboard.js +0 -0
- {h4_debug-0.1.2 → h4_debug-0.1.4}/h4_debug.egg-info/SOURCES.txt +0 -0
- {h4_debug-0.1.2 → h4_debug-0.1.4}/h4_debug.egg-info/dependency_links.txt +0 -0
- {h4_debug-0.1.2 → h4_debug-0.1.4}/h4_debug.egg-info/entry_points.txt +0 -0
- {h4_debug-0.1.2 → h4_debug-0.1.4}/h4_debug.egg-info/requires.txt +0 -0
- {h4_debug-0.1.2 → h4_debug-0.1.4}/h4_debug.egg-info/top_level.txt +0 -0
- {h4_debug-0.1.2 → 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")
|
|
@@ -78,12 +78,17 @@ async def websocket_dashboard(websocket: WebSocket):
|
|
|
78
78
|
if cmd.get("action") == "clear":
|
|
79
79
|
log_history.clear()
|
|
80
80
|
elif cmd.get("action") in ["evaluate", "set_mode"]:
|
|
81
|
-
# Forward commands to
|
|
82
|
-
|
|
81
|
+
# Forward commands to ALL active telemetry processes via TCP
|
|
82
|
+
dead_clients = set()
|
|
83
|
+
for client in telemetry_clients:
|
|
83
84
|
try:
|
|
84
|
-
|
|
85
|
+
client.sendall((json.dumps(cmd) + "\n").encode("utf-8"))
|
|
85
86
|
except Exception:
|
|
86
|
-
|
|
87
|
+
dead_clients.add(client)
|
|
88
|
+
|
|
89
|
+
for dead in dead_clients:
|
|
90
|
+
telemetry_clients.discard(dead)
|
|
91
|
+
|
|
87
92
|
except WebSocketDisconnect:
|
|
88
93
|
if websocket in dashboard_clients:
|
|
89
94
|
dashboard_clients.remove(websocket)
|
|
@@ -92,7 +97,6 @@ import socket
|
|
|
92
97
|
import threading
|
|
93
98
|
|
|
94
99
|
def run_tcp_server(port: int):
|
|
95
|
-
global active_telemetry_client
|
|
96
100
|
server = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
|
97
101
|
server.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
|
|
98
102
|
server.bind(("127.0.0.1", port + 1))
|
|
@@ -100,11 +104,10 @@ def run_tcp_server(port: int):
|
|
|
100
104
|
|
|
101
105
|
while True:
|
|
102
106
|
conn, addr = server.accept()
|
|
103
|
-
active_telemetry_client = conn
|
|
104
107
|
|
|
105
108
|
def handle_client(c):
|
|
106
|
-
global active_telemetry_client
|
|
107
109
|
f = c.makefile("r", encoding="utf-8")
|
|
110
|
+
telemetry_clients.add(c)
|
|
108
111
|
try:
|
|
109
112
|
while True:
|
|
110
113
|
line = f.readline()
|
|
@@ -114,8 +117,8 @@ def run_tcp_server(port: int):
|
|
|
114
117
|
except Exception:
|
|
115
118
|
pass
|
|
116
119
|
finally:
|
|
117
|
-
if
|
|
118
|
-
|
|
120
|
+
if c in telemetry_clients:
|
|
121
|
+
telemetry_clients.remove(c)
|
|
119
122
|
c.close()
|
|
120
123
|
|
|
121
124
|
threading.Thread(target=handle_client, args=(conn,), daemon=True).start()
|
|
@@ -126,17 +126,79 @@ body {
|
|
|
126
126
|
}
|
|
127
127
|
|
|
128
128
|
button {
|
|
129
|
-
background-color: var(--
|
|
130
|
-
color:
|
|
131
|
-
border:
|
|
132
|
-
padding: 8px 12px;
|
|
133
|
-
cursor: pointer;
|
|
129
|
+
background-color: var(--accent);
|
|
130
|
+
color: #fff;
|
|
131
|
+
border: none;
|
|
134
132
|
border-radius: 4px;
|
|
135
|
-
|
|
133
|
+
padding: 4px 10px;
|
|
134
|
+
cursor: pointer;
|
|
135
|
+
font-size: 0.85rem;
|
|
136
|
+
transition: background-color 0.2s;
|
|
136
137
|
}
|
|
137
138
|
|
|
138
|
-
button:hover {
|
|
139
|
-
background-color:
|
|
139
|
+
.toolbar button:hover {
|
|
140
|
+
background-color: #005a9e;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/* Toggle Switch Styles */
|
|
144
|
+
.toggle-group {
|
|
145
|
+
display: flex;
|
|
146
|
+
gap: 12px;
|
|
147
|
+
align-items: center;
|
|
148
|
+
margin-left: 10px;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
.switch-container {
|
|
152
|
+
display: flex;
|
|
153
|
+
align-items: center;
|
|
154
|
+
cursor: pointer;
|
|
155
|
+
font-size: 0.85rem;
|
|
156
|
+
gap: 6px;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
.switch-container input {
|
|
160
|
+
opacity: 0;
|
|
161
|
+
width: 0;
|
|
162
|
+
height: 0;
|
|
163
|
+
position: absolute;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
.switch-slider {
|
|
167
|
+
position: relative;
|
|
168
|
+
display: inline-block;
|
|
169
|
+
width: 28px;
|
|
170
|
+
height: 16px;
|
|
171
|
+
background-color: var(--border-color);
|
|
172
|
+
border-radius: 16px;
|
|
173
|
+
transition: .4s;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
.switch-slider:before {
|
|
177
|
+
position: absolute;
|
|
178
|
+
content: "";
|
|
179
|
+
height: 12px;
|
|
180
|
+
width: 12px;
|
|
181
|
+
left: 2px;
|
|
182
|
+
bottom: 2px;
|
|
183
|
+
background-color: white;
|
|
184
|
+
border-radius: 50%;
|
|
185
|
+
transition: .4s;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
.switch-container input:checked + .switch-slider {
|
|
189
|
+
background-color: var(--accent);
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
.switch-container input:checked + .switch-slider:before {
|
|
193
|
+
transform: translateX(12px);
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
.switch-label {
|
|
197
|
+
color: var(--text-secondary);
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
.switch-container input:checked ~ .switch-label {
|
|
201
|
+
color: var(--text-primary);
|
|
140
202
|
}
|
|
141
203
|
|
|
142
204
|
/* Main Content */
|
|
@@ -47,9 +47,23 @@
|
|
|
47
47
|
<option value="light">Light</option>
|
|
48
48
|
</select>
|
|
49
49
|
</label>
|
|
50
|
-
<
|
|
51
|
-
|
|
52
|
-
|
|
50
|
+
<div class="toggle-group">
|
|
51
|
+
<label class="switch-container">
|
|
52
|
+
<input type="checkbox" id="toggle-info" checked>
|
|
53
|
+
<span class="switch-slider"></span>
|
|
54
|
+
<span class="switch-label">Info</span>
|
|
55
|
+
</label>
|
|
56
|
+
<label class="switch-container">
|
|
57
|
+
<input type="checkbox" id="toggle-warn" checked>
|
|
58
|
+
<span class="switch-slider"></span>
|
|
59
|
+
<span class="switch-label">Warn</span>
|
|
60
|
+
</label>
|
|
61
|
+
<label class="switch-container">
|
|
62
|
+
<input type="checkbox" id="toggle-error" checked>
|
|
63
|
+
<span class="switch-slider"></span>
|
|
64
|
+
<span class="switch-label">Error</span>
|
|
65
|
+
</label>
|
|
66
|
+
</div>
|
|
53
67
|
</div>
|
|
54
68
|
</div>
|
|
55
69
|
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
__version__ = "0.1.2"
|
|
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
|