h4-debug 0.1.0__py3-none-any.whl

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/__init__.py ADDED
@@ -0,0 +1 @@
1
+ __version__ = "0.1.0"
h4_debug/cli.py ADDED
@@ -0,0 +1,103 @@
1
+ import argparse
2
+ import os
3
+ import subprocess
4
+ import sys
5
+ import tempfile
6
+ import threading
7
+ import time
8
+ import webbrowser
9
+ import pathlib
10
+
11
+ from . import server
12
+
13
+ def create_sitecustomize(mode):
14
+ temp_dir = tempfile.mkdtemp(prefix="h4_debug_")
15
+ sitecustomize_path = os.path.join(temp_dir, "sitecustomize.py")
16
+
17
+ # We will write a sitecustomize.py that imports our interceptor
18
+ # and initializes it with the selected mode.
19
+ script = f"""
20
+ import sys
21
+ import os
22
+
23
+ # Remove this temp dir from sys.path so we don't mess with other imports too much
24
+ try:
25
+ sys.path.remove(r"{temp_dir}")
26
+ except ValueError:
27
+ pass
28
+
29
+ try:
30
+ import h4_debug.interceptor
31
+ h4_debug.interceptor.start_interception(mode="{mode}")
32
+ except ImportError as e:
33
+ print(f"h4-debug: Failed to load interceptor: {{e}}", file=sys.stderr)
34
+ """
35
+ with open(sitecustomize_path, "w", encoding="utf-8") as f:
36
+ f.write(script)
37
+
38
+ return temp_dir
39
+
40
+ def main():
41
+ parser = argparse.ArgumentParser(description="h4-debug: Advanced application proxy debugger", usage="h4-debug [--mode MODE] command ...")
42
+ parser.add_argument("--mode", choices=["Normal", "Trace", "Full"], default="Normal", help="Debugging mode")
43
+ parser.add_argument("--port", type=int, default=8008, help="Port for the web dashboard")
44
+ parser.add_argument("command", nargs=argparse.REMAINDER, help="Command to run")
45
+
46
+ args = parser.parse_args()
47
+
48
+ if not args.command:
49
+ parser.print_help()
50
+ sys.exit(1)
51
+
52
+ command = args.command
53
+ # argparse puts '--' in the remainder if it's there
54
+ if command[0] == "--":
55
+ command = command[1:]
56
+
57
+ # 1. Start the web server in a background thread
58
+ print(f"[h4-debug] Starting dashboard on http://localhost:{args.port} ...")
59
+ server_thread = threading.Thread(target=server.run_server, args=(args.port,), daemon=True)
60
+ server_thread.start()
61
+
62
+ # Give server a moment to start
63
+ time.sleep(1.0)
64
+
65
+ # Open dashboard in browser
66
+ webbrowser.open(f"http://localhost:{args.port}")
67
+
68
+ # 2. Setup interception environment
69
+ env = os.environ.copy()
70
+
71
+ # Python interception via sitecustomize
72
+ sitecustomize_dir = create_sitecustomize(args.mode)
73
+
74
+ if "PYTHONPATH" in env:
75
+ env["PYTHONPATH"] = f"{sitecustomize_dir}{os.pathsep}{env['PYTHONPATH']}"
76
+ else:
77
+ env["PYTHONPATH"] = sitecustomize_dir
78
+
79
+ env["H4_DEBUG_MODE"] = args.mode
80
+ env["H4_DEBUG_PORT"] = str(args.port)
81
+
82
+ # 3. Launch the target process
83
+ print(f"[h4-debug] Launching process: {' '.join(command)}")
84
+ try:
85
+ process = subprocess.Popen(command, env=env)
86
+ process.wait()
87
+ except KeyboardInterrupt:
88
+ print("\n[h4-debug] Interrupted by user. Terminating process...")
89
+ if process:
90
+ process.terminate()
91
+ process.wait()
92
+ except Exception as e:
93
+ print(f"[h4-debug] Error running command: {e}")
94
+ finally:
95
+ print("[h4-debug] Process finished. Dashboard will remain open until you exit (Ctrl+C).")
96
+ try:
97
+ while True:
98
+ time.sleep(1)
99
+ except KeyboardInterrupt:
100
+ print("[h4-debug] Shutting down.")
101
+
102
+ if __name__ == "__main__":
103
+ main()
@@ -0,0 +1,197 @@
1
+ import builtins
2
+ import json
3
+ import os
4
+ import queue
5
+ import socket
6
+ import sys
7
+ import threading
8
+ import time
9
+ import traceback
10
+ from datetime import datetime
11
+
12
+ try:
13
+ from websockets.sync.client import connect
14
+ except ImportError:
15
+ connect = None
16
+
17
+ # We must keep references to original functions to avoid infinite recursion
18
+ _original_open = builtins.open
19
+ _original_socket = socket.socket
20
+ _original_print = builtins.print
21
+
22
+ class TelemetryClient:
23
+ def __init__(self, port):
24
+ self.port = port
25
+ self.queue = queue.Queue()
26
+ self.thread = threading.Thread(target=self._run, daemon=True)
27
+ self.thread.start()
28
+
29
+ def _run(self):
30
+ ws_url = f"ws://127.0.0.1:{self.port}/ws/telemetry"
31
+ if not connect:
32
+ return
33
+
34
+ while True:
35
+ try:
36
+ # Use original socket context if websockets internally creates sockets
37
+ # This might be tricky because websockets uses asyncio or sync wrappers
38
+ # which use the monkeypatched socket. We must be very careful.
39
+ # A safer approach for the telemetry thread is to unpatch socket temporarily or
40
+ # ensure our hooks bypass if thread == telemetry_thread
41
+ with connect(ws_url) as websocket:
42
+ while True:
43
+ msg = self.queue.get()
44
+ websocket.send(json.dumps(msg))
45
+ self.queue.task_done()
46
+ except Exception as e:
47
+ time.sleep(1)
48
+
49
+ def send(self, module, event_type, data):
50
+ msg = {
51
+ "timestamp": datetime.now().isoformat(),
52
+ "module": module,
53
+ "type": event_type,
54
+ "data": data,
55
+ "thread": threading.current_thread().name
56
+ }
57
+ self.queue.put(msg)
58
+
59
+
60
+ _telemetry = None
61
+ _mode = "Normal"
62
+ _telemetry_thread_id = None
63
+
64
+ def _is_telemetry_thread():
65
+ return threading.get_ident() == _telemetry_thread_id
66
+
67
+ # --- Disk Interception ---
68
+
69
+ def patched_open(file, mode='r', buffering=-1, encoding=None, errors=None, newline=None, closefd=True, opener=None):
70
+ if not _is_telemetry_thread() and _telemetry:
71
+ _telemetry.send("Disk", "open", {
72
+ "file": str(file),
73
+ "mode": mode
74
+ })
75
+ return _original_open(file, mode, buffering, encoding, errors, newline, closefd, opener)
76
+
77
+ # --- Network Interception ---
78
+
79
+ class PatchedSocket(_original_socket):
80
+ def __init__(self, family=socket.AF_INET, type=socket.SOCK_STREAM, proto=0, fileno=None):
81
+ super().__init__(family, type, proto, fileno)
82
+ self._h4_peer = None
83
+
84
+ def connect(self, address):
85
+ if not _is_telemetry_thread() and _telemetry:
86
+ self._h4_peer = address
87
+ _telemetry.send("Network", "connect", {
88
+ "address": str(address)
89
+ })
90
+ return super().connect(address)
91
+
92
+ def send(self, data, flags=0):
93
+ if not _is_telemetry_thread() and _telemetry:
94
+ payload = data.hex() if _mode == "Full" else (data[:100].hex() + "..." if len(data)>100 else data.hex())
95
+ # try to decode as text
96
+ try:
97
+ text_payload = data.decode('utf-8', errors='ignore')
98
+ except Exception:
99
+ text_payload = None
100
+
101
+ _telemetry.send("Network", "send", {
102
+ "address": str(self._h4_peer),
103
+ "bytes": len(data),
104
+ "payload_hex": payload,
105
+ "payload_text": text_payload
106
+ })
107
+ return super().send(data, flags)
108
+
109
+ def recv(self, bufsize, flags=0):
110
+ data = super().recv(bufsize, flags)
111
+ if not _is_telemetry_thread() and _telemetry:
112
+ payload = data.hex() if _mode == "Full" else (data[:100].hex() + "..." if len(data)>100 else data.hex())
113
+ try:
114
+ text_payload = data.decode('utf-8', errors='ignore')
115
+ except Exception:
116
+ text_payload = None
117
+
118
+ _telemetry.send("Network", "recv", {
119
+ "address": str(self._h4_peer),
120
+ "bytes": len(data),
121
+ "payload_hex": payload,
122
+ "payload_text": text_payload
123
+ })
124
+ return data
125
+
126
+ # --- Stdout/Stderr Interception ---
127
+
128
+ class PatchedStream:
129
+ def __init__(self, original_stream, stream_name):
130
+ self.original_stream = original_stream
131
+ self.stream_name = stream_name
132
+
133
+ def write(self, text):
134
+ if not _is_telemetry_thread() and _telemetry and text.strip():
135
+ _telemetry.send("Console", self.stream_name, {"text": text})
136
+ return self.original_stream.write(text)
137
+
138
+ def flush(self):
139
+ return self.original_stream.flush()
140
+
141
+ def __getattr__(self, name):
142
+ return getattr(self.original_stream, name)
143
+
144
+
145
+ # --- Execution Tracing ---
146
+
147
+ def trace_calls(frame, event, arg):
148
+ if _is_telemetry_thread() or not _telemetry:
149
+ return trace_calls
150
+
151
+ if event == "call":
152
+ func_name = frame.f_code.co_name
153
+ filename = frame.f_code.co_filename
154
+ # Ignore our own files and standard library to avoid noise, unless Full Debug
155
+ if "h4_debug" not in filename and ("site-packages" not in filename or _mode == "Full"):
156
+ _telemetry.send("Execution", "call", {
157
+ "function": func_name,
158
+ "file": filename,
159
+ "line": frame.f_lineno
160
+ })
161
+ elif event == "exception" and _mode in ("Trace", "Full"):
162
+ exc_type, exc_value, exc_traceback = arg
163
+ filename = frame.f_code.co_filename
164
+ if "h4_debug" not in filename:
165
+ _telemetry.send("Execution", "exception", {
166
+ "type": exc_type.__name__,
167
+ "value": str(exc_value),
168
+ "file": filename,
169
+ "line": frame.f_lineno
170
+ })
171
+
172
+ return trace_calls
173
+
174
+ def start_interception(mode="Normal"):
175
+ global _telemetry, _mode, _telemetry_thread_id
176
+ _mode = mode
177
+
178
+ port = int(os.environ.get("H4_DEBUG_PORT", 8008))
179
+ _telemetry = TelemetryClient(port)
180
+ _telemetry_thread_id = _telemetry.thread.ident
181
+
182
+ # Patch Disk
183
+ builtins.open = patched_open
184
+
185
+ # Patch Network
186
+ socket.socket = PatchedSocket
187
+
188
+ # Patch Console
189
+ sys.stdout = PatchedStream(sys.stdout, "stdout")
190
+ sys.stderr = PatchedStream(sys.stderr, "stderr")
191
+
192
+ # Enable Tracing if required
193
+ if mode in ("Trace", "Full"):
194
+ sys.settrace(trace_calls)
195
+
196
+ _telemetry.send("System", "init", {"mode": mode, "pid": os.getpid()})
197
+
h4_debug/server.py ADDED
@@ -0,0 +1,94 @@
1
+ import asyncio
2
+ import json
3
+ import os
4
+ import uvicorn
5
+ from fastapi import FastAPI, WebSocket, WebSocketDisconnect
6
+ from fastapi.responses import HTMLResponse
7
+ from fastapi.staticfiles import StaticFiles
8
+
9
+ app = FastAPI()
10
+
11
+ # Store connected dashboard clients
12
+ dashboard_clients = []
13
+
14
+ # To persist logs for when a dashboard connects slightly after startup
15
+ log_history = []
16
+ MAX_HISTORY = 5000
17
+
18
+ # Get the path to static files
19
+ STATIC_DIR = os.path.join(os.path.dirname(__file__), "static")
20
+ if not os.path.exists(STATIC_DIR):
21
+ os.makedirs(STATIC_DIR)
22
+
23
+ app.mount("/static", StaticFiles(directory=STATIC_DIR), name="static")
24
+
25
+ @app.get("/")
26
+ async def get_dashboard():
27
+ dashboard_path = os.path.join(STATIC_DIR, "dashboard.html")
28
+ if os.path.exists(dashboard_path):
29
+ with open(dashboard_path, "r", encoding="utf-8") as f:
30
+ return HTMLResponse(f.read())
31
+ return HTMLResponse("<h1>Dashboard not found</h1>")
32
+
33
+ @app.websocket("/ws/telemetry")
34
+ async def websocket_telemetry(websocket: WebSocket):
35
+ """Endpoint for the intercepted process to push logs."""
36
+ await websocket.accept()
37
+ try:
38
+ while True:
39
+ data = await websocket.receive_text()
40
+
41
+ try:
42
+ log_item = json.loads(data)
43
+ log_history.append(log_item)
44
+ if len(log_history) > MAX_HISTORY:
45
+ log_history.pop(0)
46
+ except Exception:
47
+ pass
48
+
49
+ # Broadcast to all dashboards
50
+ disconnected_clients = []
51
+ for client in dashboard_clients:
52
+ try:
53
+ await client.send_text(data)
54
+ except Exception:
55
+ disconnected_clients.append(client)
56
+
57
+ for client in disconnected_clients:
58
+ if client in dashboard_clients:
59
+ dashboard_clients.remove(client)
60
+ except WebSocketDisconnect:
61
+ pass
62
+
63
+ @app.websocket("/ws/dashboard")
64
+ async def websocket_dashboard(websocket: WebSocket):
65
+ """Endpoint for the web dashboard to receive logs."""
66
+ await websocket.accept()
67
+
68
+ # Send history on connect
69
+ for log_item in log_history:
70
+ try:
71
+ await websocket.send_text(json.dumps(log_item))
72
+ except Exception:
73
+ break
74
+
75
+ dashboard_clients.append(websocket)
76
+ try:
77
+ while True:
78
+ # Keep alive and receive commands from dashboard if needed
79
+ data = await websocket.receive_text()
80
+ cmd = json.loads(data)
81
+ if cmd.get("action") == "clear":
82
+ log_history.clear()
83
+ except WebSocketDisconnect:
84
+ if websocket in dashboard_clients:
85
+ dashboard_clients.remove(websocket)
86
+
87
+ def run_server(port: int):
88
+ # Disable uvicorn access logs to keep terminal clean
89
+ import logging
90
+ log = logging.getLogger("uvicorn.access")
91
+ log.setLevel(logging.WARNING)
92
+
93
+ uvicorn.run(app, host="127.0.0.1", port=port, log_level="warning")
94
+
@@ -0,0 +1,21 @@
1
+ Metadata-Version: 2.4
2
+ Name: h4-debug
3
+ Version: 0.1.0
4
+ Summary: Advanced application proxy debugger and F12-style web console
5
+ Author-email: h4 <h4@example.com>
6
+ Requires-Python: >=3.8
7
+ Description-Content-Type: text/markdown
8
+ Requires-Dist: fastapi>=0.100
9
+ Requires-Dist: uvicorn[standard]>=0.20
10
+ Requires-Dist: websockets>=11.0
11
+
12
+ # h4-debug
13
+
14
+ Advanced application proxy debugger and F12-style web console.
15
+
16
+ ## Usage
17
+
18
+ ```bash
19
+ pip install h4-debug
20
+ h4-debug python main.py
21
+ ```
@@ -0,0 +1,9 @@
1
+ h4_debug/__init__.py,sha256=kUR5RAFc7HCeiqdlX36dZOHkUI5wI6V_43RpEcD8b-0,22
2
+ h4_debug/cli.py,sha256=DoGUYNFxmesQFDqtVBW1XhdK35ks55-JSK4SuUAdePU,3208
3
+ h4_debug/interceptor.py,sha256=ZFWhzmjKSLGJbHVZxTjthnR22jqtGWatyULCk49O7OI,6609
4
+ h4_debug/server.py,sha256=YVxF8C3QoVj4riddJ-qGRr8ReqdESwY8SykcRuh4T8E,3005
5
+ h4_debug-0.1.0.dist-info/METADATA,sha256=gk3rGtBV4gHHzGwxfk5AlzA0IakJIMwOS6evYF8mdPY,486
6
+ h4_debug-0.1.0.dist-info/WHEEL,sha256=K260EYznzXsJYBQGqmI8VTxEdiZYNvDZwW9cBh9-_MA,91
7
+ h4_debug-0.1.0.dist-info/entry_points.txt,sha256=xZWjVvTnVZdESrvUUl0BEwVHT3q8-5GhA58oXvK-d2g,47
8
+ h4_debug-0.1.0.dist-info/top_level.txt,sha256=2fWNW-Vus5YAjroqCTsKMS2I4RT9lfBkSE3aKzlWE70,9
9
+ h4_debug-0.1.0.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (83.0.0)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ h4-debug = h4_debug.cli:main
@@ -0,0 +1 @@
1
+ h4_debug