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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: h4-debug
3
- Version: 0.1.2
3
+ Version: 0.1.4
4
4
  Summary: Advanced application proxy debugger and F12-style web console
5
5
  Author-email: h4 <h4@example.com>
6
6
  Requires-Python: >=3.8
@@ -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 sys
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. Check for incoming commands (with short timeout)
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
- for line in data.decode("utf-8").split("\n"):
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 = new_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 trace_calls(frame, event, arg):
200
- if _is_telemetry_thread() or not _telemetry:
201
- return trace_calls
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 = "site-packages" not in filename and "lib" not in filename.lower()
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": 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
- active_telemetry_client = None # This will now be a TCP socket
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 the active telemetry process via TCP
82
- if active_telemetry_client:
81
+ # Forward commands to ALL active telemetry processes via TCP
82
+ dead_clients = set()
83
+ for client in telemetry_clients:
83
84
  try:
84
- active_telemetry_client.sendall((json.dumps(cmd) + "\n").encode("utf-8"))
85
+ client.sendall((json.dumps(cmd) + "\n").encode("utf-8"))
85
86
  except Exception:
86
- active_telemetry_client = None
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 active_telemetry_client == c:
118
- active_telemetry_client = None
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(--bg-primary);
130
- color: var(--text-primary);
131
- border: 1px solid var(--border-color);
132
- padding: 8px 12px;
133
- cursor: pointer;
129
+ background-color: var(--accent);
130
+ color: #fff;
131
+ border: none;
134
132
  border-radius: 4px;
135
- transition: background 0.2s;
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: var(--bg-hover);
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
- <label><input type="checkbox" id="toggle-info" checked> Info</label>
51
- <label><input type="checkbox" id="toggle-warn" checked> Warn</label>
52
- <label><input type="checkbox" id="toggle-error" checked> Error</label>
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,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: h4-debug
3
- Version: 0.1.2
3
+ Version: 0.1.4
4
4
  Summary: Advanced application proxy debugger and F12-style web console
5
5
  Author-email: h4 <h4@example.com>
6
6
  Requires-Python: >=3.8
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "h4-debug"
7
- version = "0.1.2"
7
+ version = "0.1.4"
8
8
  description = "Advanced application proxy debugger and F12-style web console"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.8"
@@ -1 +0,0 @@
1
- __version__ = "0.1.2"
File without changes
File without changes
File without changes