h4-debug 0.1.1__tar.gz → 0.1.3__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.1
3
+ Version: 0.1.3
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.3"
@@ -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
- ws_url = f"ws://127.0.0.1:{self.port}/ws/telemetry"
31
- if not connect:
32
- return
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 connect(ws_url) as websocket:
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
- websocket.send(json.dumps(msg))
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
- try:
49
- # Using recv(timeout) requires catching TimeoutError
50
- cmd_data = websocket.recv(timeout=0.05)
51
- self._handle_command(cmd_data)
52
- except TimeoutError:
53
- pass
54
- except Exception as e:
55
- # If it's not a timeout, might be a real error
56
- if "timed out" not in str(e).lower():
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 as e:
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,43 +56,10 @@ 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."""
62
+ global active_telemetry_client
71
63
  await websocket.accept()
72
64
 
73
65
  # Send history on connect
@@ -87,20 +79,60 @@ async def websocket_dashboard(websocket: WebSocket):
87
79
  if cmd.get("action") == "clear":
88
80
  log_history.clear()
89
81
  elif cmd.get("action") in ["evaluate", "set_mode"]:
90
- # Forward commands to the active telemetry process
82
+ # Forward commands to the active telemetry process via TCP
91
83
  if active_telemetry_client:
92
84
  try:
93
- await active_telemetry_client.send_text(json.dumps(cmd))
85
+ active_telemetry_client.sendall((json.dumps(cmd) + "\n").encode("utf-8"))
94
86
  except Exception:
95
- pass
87
+ active_telemetry_client = None
96
88
  except WebSocketDisconnect:
97
89
  if websocket in dashboard_clients:
98
90
  dashboard_clients.remove(websocket)
99
91
 
92
+ import socket
93
+ import threading
94
+
95
+ def run_tcp_server(port: int):
96
+ global active_telemetry_client
97
+ server = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
98
+ server.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
99
+ server.bind(("127.0.0.1", port + 1))
100
+ server.listen(1)
101
+
102
+ while True:
103
+ conn, addr = server.accept()
104
+ active_telemetry_client = conn
105
+
106
+ def handle_client(c):
107
+ global active_telemetry_client
108
+ f = c.makefile("r", encoding="utf-8")
109
+ try:
110
+ while True:
111
+ line = f.readline()
112
+ if not line:
113
+ break
114
+ broadcast_log(line)
115
+ except Exception:
116
+ pass
117
+ finally:
118
+ if active_telemetry_client == c:
119
+ active_telemetry_client = None
120
+ c.close()
121
+
122
+ threading.Thread(target=handle_client, args=(conn,), daemon=True).start()
123
+
100
124
  def run_server(port: int):
101
125
  # Disable uvicorn access logs to keep terminal clean
102
126
  import logging
103
127
  log = logging.getLogger("uvicorn.access")
104
128
  log.setLevel(logging.WARNING)
105
129
 
106
- uvicorn.run(app, host="127.0.0.1", port=port, log_level="warning")
130
+ global loop
131
+ loop = asyncio.new_event_loop()
132
+ asyncio.set_event_loop(loop)
133
+
134
+ threading.Thread(target=run_tcp_server, args=(port,), daemon=True).start()
135
+
136
+ config = uvicorn.Config(app, host="127.0.0.1", port=port, log_level="warning", loop="asyncio")
137
+ server = uvicorn.Server(config)
138
+ loop.run_until_complete(server.serve())
@@ -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.1
3
+ Version: 0.1.3
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.1"
7
+ version = "0.1.3"
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.1"
File without changes
File without changes
File without changes