h4-debug 0.1.4__tar.gz → 0.2.0__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.4
3
+ Version: 0.2.0
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
@@ -8,6 +8,7 @@ Description-Content-Type: text/markdown
8
8
  Requires-Dist: fastapi>=0.100
9
9
  Requires-Dist: uvicorn[standard]>=0.20
10
10
  Requires-Dist: websockets>=11.0
11
+ Requires-Dist: Pillow>=10.0.0
11
12
 
12
13
  # h4-debug
13
14
 
@@ -0,0 +1 @@
1
+ __version__ = "0.1.5"
@@ -0,0 +1,71 @@
1
+ import argparse
2
+ import os
3
+ import sys
4
+ import threading
5
+ import time
6
+ import webbrowser
7
+
8
+ from . import server
9
+ from . import handlers
10
+
11
+ def main():
12
+ parser = argparse.ArgumentParser(description="h4-debug: Advanced application proxy debugger", usage="h4-debug [--mode MODE] command ...")
13
+ parser.add_argument("--mode", choices=["Normal", "Trace", "Full"], default="Normal", help="Debugging mode")
14
+ parser.add_argument("--port", type=int, default=8008, help="Port for the web dashboard")
15
+ parser.add_argument("command", nargs=argparse.REMAINDER, help="Command to run")
16
+
17
+ args = parser.parse_args()
18
+
19
+ if not args.command:
20
+ parser.print_help()
21
+ sys.exit(1)
22
+
23
+ command = args.command
24
+ if command[0] == "--":
25
+ command = command[1:]
26
+
27
+ target = command[0]
28
+
29
+ # 1. Start the web server in a background thread
30
+ print(f"[h4-debug] Starting dashboard on http://localhost:{args.port} ...")
31
+ server_thread = threading.Thread(target=server.run_server, args=(args.port,), daemon=True)
32
+ server_thread.start()
33
+
34
+ time.sleep(1.0)
35
+ webbrowser.open(f"http://localhost:{args.port}")
36
+
37
+ # 2. Setup interception environment
38
+ env = os.environ.copy()
39
+
40
+ # 3. Dispatch to handler based on file extension
41
+ ext = ""
42
+ if os.path.isfile(target):
43
+ ext = os.path.splitext(target)[1].lower()
44
+
45
+ if ext == ".py":
46
+ handlers.handle_python(target, command, env, args)
47
+ elif ext in [".bat", ".cmd"]:
48
+ handlers.handle_batch(target, command, env, args)
49
+ elif ext in [".png", ".jpg", ".jpeg", ".gif", ".bmp", ".tiff", ".webp"]:
50
+ handlers.handle_image(target, command, env, args)
51
+ elif ext in [".js", ".ts", ".mjs", ".cjs"] or target.lower() in ["node", "npm", "npx", "node.exe", "npm.cmd", "npx.cmd"]:
52
+ handlers.handle_node(target, command, env, args)
53
+ elif ext == ".exe":
54
+ handlers.handle_exe(target, command, env, args)
55
+ else:
56
+ # Check if it's implicitly a python script via "python script.py"
57
+ if target.lower().endswith("python") or target.lower().endswith("python.exe"):
58
+ handlers.handle_python(target, command, env, args)
59
+ else:
60
+ handlers.handle_generic(target, command, env, args)
61
+
62
+ print("[h4-debug] Process finished. Dashboard will remain open until you exit (Ctrl+C).")
63
+ try:
64
+ while True:
65
+ time.sleep(1)
66
+ except KeyboardInterrupt:
67
+ print("[h4-debug] Shutting down.")
68
+
69
+ if __name__ == "__main__":
70
+ main()
71
+
@@ -0,0 +1,252 @@
1
+ import os
2
+ import subprocess
3
+ import tempfile
4
+ import sys
5
+ import time
6
+
7
+ from .interceptor import TelemetryClient
8
+
9
+ def _create_sitecustomize(mode, temp_dir):
10
+ sitecustomize_path = os.path.join(temp_dir, "sitecustomize.py")
11
+ script = f"""
12
+ import sys
13
+ import os
14
+
15
+ try:
16
+ sys.path.remove(r"{temp_dir}")
17
+ except ValueError:
18
+ pass
19
+
20
+ try:
21
+ import h4_debug.interceptor
22
+ h4_debug.interceptor.start_interception(mode="{mode}")
23
+ except ImportError as e:
24
+ print(f"h4-debug: Failed to load interceptor: {{e}}", file=sys.stderr)
25
+ """
26
+ with open(sitecustomize_path, "w", encoding="utf-8") as f:
27
+ f.write(script)
28
+
29
+ def handle_python(target, command, env, args):
30
+ print(f"[h4-debug] Handling as Python script")
31
+ temp_dir = tempfile.mkdtemp(prefix="h4_debug_")
32
+ _create_sitecustomize(args.mode, temp_dir)
33
+
34
+ if "PYTHONPATH" in env:
35
+ env["PYTHONPATH"] = f"{temp_dir}{os.pathsep}{env['PYTHONPATH']}"
36
+ else:
37
+ env["PYTHONPATH"] = temp_dir
38
+
39
+ env["H4_DEBUG_MODE"] = args.mode
40
+ env["H4_DEBUG_PORT"] = str(args.port)
41
+
42
+ print(f"[h4-debug] Launching process: {' '.join(command)}")
43
+ process = None
44
+ try:
45
+ process = subprocess.Popen(command, env=env)
46
+ process.wait()
47
+ except KeyboardInterrupt:
48
+ print("\n[h4-debug] Interrupted by user. Terminating process...")
49
+ if process:
50
+ process.terminate()
51
+ process.wait()
52
+ except Exception as e:
53
+ print(f"[h4-debug] Error running command: {e}")
54
+
55
+ def handle_batch(target, command, env, args):
56
+ print(f"[h4-debug] Handling as Batch script")
57
+ client = TelemetryClient(args.port)
58
+ time.sleep(0.5) # Wait for connection
59
+
60
+ # Read batch file and send content
61
+ try:
62
+ with open(target, 'r', encoding='utf-8', errors='replace') as f:
63
+ content = f.read()
64
+ client.send("File", "batch_content", {"file": target, "content": content})
65
+ except Exception as e:
66
+ client.send("System", "error", {"text": f"Failed to read batch file: {e}"})
67
+
68
+ # Prepare env for inner python calls
69
+ temp_dir = tempfile.mkdtemp(prefix="h4_debug_")
70
+ _create_sitecustomize(args.mode, temp_dir)
71
+ if "PYTHONPATH" in env:
72
+ env["PYTHONPATH"] = f"{temp_dir}{os.pathsep}{env['PYTHONPATH']}"
73
+ else:
74
+ env["PYTHONPATH"] = temp_dir
75
+ env["H4_DEBUG_MODE"] = args.mode
76
+ env["H4_DEBUG_PORT"] = str(args.port)
77
+
78
+ print(f"[h4-debug] Launching batch process: {' '.join(command)}")
79
+ process = None
80
+ try:
81
+ # Wrap command with cmd.exe /c if not already
82
+ if not command[0].lower().endswith('cmd.exe'):
83
+ cmd_args = ["cmd.exe", "/c"] + command
84
+ else:
85
+ cmd_args = command
86
+
87
+ process = subprocess.Popen(cmd_args, env=env, stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True, bufsize=1)
88
+
89
+ # Stream stdout and stderr
90
+ import threading
91
+ def stream_output(pipe, is_err):
92
+ for line in pipe:
93
+ client.send("Console", "stderr" if is_err else "stdout", {"text": line.rstrip()})
94
+ print(line, end='', file=sys.stderr if is_err else sys.stdout)
95
+
96
+ t1 = threading.Thread(target=stream_output, args=(process.stdout, False), daemon=True)
97
+ t2 = threading.Thread(target=stream_output, args=(process.stderr, True), daemon=True)
98
+ t1.start()
99
+ t2.start()
100
+
101
+ process.wait()
102
+ t1.join()
103
+ t2.join()
104
+
105
+ client.send("System", "info", {"text": f"Batch process finished with code {process.returncode}"})
106
+ except KeyboardInterrupt:
107
+ print("\n[h4-debug] Interrupted by user.")
108
+ if process:
109
+ process.terminate()
110
+ process.wait()
111
+ except Exception as e:
112
+ print(f"[h4-debug] Error running command: {e}")
113
+
114
+ def handle_image(target, command, env, args):
115
+ print(f"[h4-debug] Handling as Image file")
116
+
117
+ try:
118
+ from PIL import Image
119
+ except ImportError:
120
+ print("[h4-debug] Pillow is not installed. EXIF extraction unavailable.")
121
+ handle_generic(target, command, env, args)
122
+ return
123
+
124
+ def cmd_handler(cmd):
125
+ if cmd.get("action") == "strip_exif":
126
+ try:
127
+ img = Image.open(target)
128
+ data = list(img.getdata())
129
+ image_without_exif = Image.new(img.mode, img.size)
130
+ image_without_exif.putdata(data)
131
+
132
+ name, ext = os.path.splitext(target)
133
+ new_target = f"{name}_stripped{ext}"
134
+ image_without_exif.save(new_target)
135
+ client.send("System", "info", {"text": f"Image metadata stripped and saved to: {new_target}"})
136
+ print(f"[h4-debug] Image stripped successfully: {new_target}")
137
+ except Exception as e:
138
+ client.send("System", "error", {"text": f"Failed to strip EXIF: {e}"})
139
+ return True
140
+ return False
141
+
142
+ client = TelemetryClient(args.port, command_callback=cmd_handler)
143
+ time.sleep(0.5)
144
+
145
+ try:
146
+ img = Image.open(target)
147
+ exif = img.getexif()
148
+ metadata = {
149
+ "format": img.format,
150
+ "mode": img.mode,
151
+ "size": img.size,
152
+ "exif": {k: str(v) for k, v in (exif.items() if exif else [])},
153
+ "file": target
154
+ }
155
+ client.send("Data", "image_metadata", metadata)
156
+ print(f"[h4-debug] Extracted metadata for {target}. Sent to dashboard.")
157
+ except Exception as e:
158
+ client.send("System", "error", {"text": f"Failed to read image metadata: {e}"})
159
+ print(f"[h4-debug] Error reading image: {e}")
160
+
161
+ print("[h4-debug] Image handler running. Dashboard is active. (Press Ctrl+C to stop)")
162
+ try:
163
+ while True:
164
+ time.sleep(1)
165
+ except KeyboardInterrupt:
166
+ print("\n[h4-debug] Exiting image handler.")
167
+
168
+ def handle_generic(target, command, env, args):
169
+ print(f"[h4-debug] Handling as generic command")
170
+ client = TelemetryClient(args.port)
171
+ time.sleep(0.5)
172
+
173
+ try:
174
+ size = os.path.getsize(target)
175
+ client.send("System", "info", {"text": f"Generic file: {target} (Size: {size} bytes)"})
176
+ except Exception:
177
+ pass
178
+
179
+ process = None
180
+ try:
181
+ process = subprocess.Popen(command, env=env, stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True, bufsize=1)
182
+
183
+ import threading
184
+ def stream_output(pipe, is_err):
185
+ for line in pipe:
186
+ client.send("Console", "stderr" if is_err else "stdout", {"text": line.rstrip()})
187
+ print(line, end='', file=sys.stderr if is_err else sys.stdout)
188
+
189
+ t1 = threading.Thread(target=stream_output, args=(process.stdout, False), daemon=True)
190
+ t2 = threading.Thread(target=stream_output, args=(process.stderr, True), daemon=True)
191
+ t1.start()
192
+ t2.start()
193
+
194
+ process.wait()
195
+ t1.join()
196
+ t2.join()
197
+
198
+ client.send("System", "info", {"text": f"Generic process finished with code {process.returncode}"})
199
+ except KeyboardInterrupt:
200
+ if process:
201
+ process.terminate()
202
+ process.wait()
203
+ except Exception as e:
204
+ print(f"[h4-debug] Error running generic command: {e}")
205
+
206
+ def handle_node(target, command, env, args):
207
+ print(f"[h4-debug] Handling as Node.js script")
208
+
209
+ # Locate the interceptor script
210
+ current_dir = os.path.dirname(os.path.abspath(__file__))
211
+ interceptor_path = os.path.join(current_dir, "node_interceptor.js").replace("\\", "/")
212
+
213
+ # Format NODE_OPTIONS
214
+ node_opts = env.get("NODE_OPTIONS", "")
215
+ new_opt = f'--require "{interceptor_path}"'
216
+ env["NODE_OPTIONS"] = f"{new_opt} {node_opts}".strip()
217
+
218
+ env["H4_DEBUG_MODE"] = args.mode
219
+ env["H4_DEBUG_PORT"] = str(args.port)
220
+
221
+ process = None
222
+ try:
223
+ # If target is .js but command doesn't start with node, prepend node
224
+ if target.endswith('.js') and command[0] == target:
225
+ command = ["node"] + command
226
+
227
+ process = subprocess.Popen(command, env=env)
228
+ process.wait()
229
+ except KeyboardInterrupt:
230
+ if process:
231
+ process.terminate()
232
+ process.wait()
233
+ except Exception as e:
234
+ print(f"[h4-debug] Error running node command: {e}")
235
+
236
+ def handle_exe(target, command, env, args):
237
+ print(f"[h4-debug] Handling as Native Executable (using ctypes Windows Debugger)")
238
+ client = TelemetryClient(args.port)
239
+ time.sleep(0.5)
240
+
241
+ env["H4_DEBUG_MODE"] = args.mode
242
+ env["H4_DEBUG_PORT"] = str(args.port)
243
+
244
+ try:
245
+ from h4_debug.win_debugger import debug_process
246
+ client.send("System", "info", {"text": f"Starting native debug wrapper for {target}"})
247
+ debug_process(command, client)
248
+ except KeyboardInterrupt:
249
+ pass
250
+ except Exception as e:
251
+ print(f"[h4-debug] Error running EXE command: {e}")
252
+
@@ -18,8 +18,9 @@ _original_socket = socket.socket
18
18
  _original_print = builtins.print
19
19
 
20
20
  class TelemetryClient:
21
- def __init__(self, port):
21
+ def __init__(self, port, command_callback=None):
22
22
  self.port = port
23
+ self.command_callback = command_callback
23
24
  self.queue = queue.Queue()
24
25
  self.buffer = ""
25
26
  self.thread = threading.Thread(target=self._run, daemon=True)
@@ -64,6 +65,9 @@ class TelemetryClient:
64
65
  try:
65
66
  cmd = json.loads(cmd_data)
66
67
 
68
+ if self.command_callback and self.command_callback(cmd):
69
+ return
70
+
67
71
  if cmd.get("action") == "set_mode":
68
72
  global _mode
69
73
  _mode = cmd.get("mode", "Normal")
@@ -0,0 +1,126 @@
1
+ const net = require('net');
2
+ const http = require('http');
3
+ const https = require('https');
4
+ const fs = require('fs');
5
+
6
+ const mode = process.env.H4_DEBUG_MODE || 'Normal';
7
+ const port = parseInt(process.env.H4_DEBUG_PORT || '8008', 10) + 1;
8
+ const isTelemetryThread = false;
9
+
10
+ const messageQueue = [];
11
+ let client = null;
12
+ let connected = false;
13
+
14
+ function connectTelemetry() {
15
+ client = net.createConnection({ port: port, host: '127.0.0.1' }, () => {
16
+ connected = true;
17
+ sendQueue();
18
+ });
19
+
20
+ client.on('error', () => {
21
+ connected = false;
22
+ setTimeout(connectTelemetry, 1000);
23
+ });
24
+
25
+ client.on('close', () => {
26
+ connected = false;
27
+ setTimeout(connectTelemetry, 1000);
28
+ });
29
+ }
30
+
31
+ function sendQueue() {
32
+ if (!connected || !client) return;
33
+ while (messageQueue.length > 0) {
34
+ const msg = messageQueue.shift();
35
+ try {
36
+ client.write(JSON.stringify(msg) + '\n');
37
+ } catch (e) {
38
+ messageQueue.unshift(msg);
39
+ break;
40
+ }
41
+ }
42
+ }
43
+
44
+ function sendEvent(module, type, data) {
45
+ if (mode === 'Normal' && module !== 'System') return; // Do not send if normal, unless system init
46
+ const msg = {
47
+ timestamp: new Date().toISOString(),
48
+ module: module,
49
+ type: type,
50
+ data: data,
51
+ thread: 'Main'
52
+ };
53
+ messageQueue.push(msg);
54
+ sendQueue();
55
+ }
56
+
57
+ // Start connection
58
+ connectTelemetry();
59
+
60
+ sendEvent('System', 'init', {
61
+ mode: mode,
62
+ pid: process.pid,
63
+ language: 'node.js',
64
+ version: process.version
65
+ });
66
+
67
+ // Patch Console
68
+ const originalLog = console.log;
69
+ const originalError = console.error;
70
+ const originalWarn = console.warn;
71
+
72
+ console.log = function(...args) {
73
+ sendEvent('Console', 'stdout', { text: args.join(' ') });
74
+ originalLog.apply(console, args);
75
+ };
76
+ console.error = function(...args) {
77
+ sendEvent('Console', 'stderr', { text: args.join(' ') });
78
+ originalError.apply(console, args);
79
+ };
80
+ console.warn = function(...args) {
81
+ sendEvent('Console', 'stdout', { text: '[WARN] ' + args.join(' ') });
82
+ originalWarn.apply(console, args);
83
+ };
84
+
85
+ // Patch HTTP
86
+ const originalHttpRequest = http.request;
87
+ http.request = function(...args) {
88
+ let url = typeof args[0] === 'string' ? args[0] : (args[0] && args[0].href) ? args[0].href : JSON.stringify(args[0]);
89
+ sendEvent('Network', 'connect', { address: url });
90
+ return originalHttpRequest.apply(http, args);
91
+ };
92
+
93
+ const originalHttpsRequest = https.request;
94
+ https.request = function(...args) {
95
+ let url = typeof args[0] === 'string' ? args[0] : (args[0] && args[0].href) ? args[0].href : JSON.stringify(args[0]);
96
+ sendEvent('Network', 'connect', { address: url });
97
+ return originalHttpsRequest.apply(https, args);
98
+ };
99
+
100
+ // Patch File System (read sync as example)
101
+ const originalReadFileSync = fs.readFileSync;
102
+ fs.readFileSync = function(...args) {
103
+ sendEvent('Disk', 'open', { file: args[0], mode: 'r' });
104
+ return originalReadFileSync.apply(fs, args);
105
+ };
106
+
107
+ // Unhandled Rejection
108
+ process.on('unhandledRejection', (reason, promise) => {
109
+ sendEvent('Execution', 'exception', {
110
+ type: 'UnhandledRejection',
111
+ value: String(reason),
112
+ file: 'unknown',
113
+ line: 0
114
+ });
115
+ });
116
+
117
+ process.on('uncaughtException', (err) => {
118
+ sendEvent('Execution', 'exception', {
119
+ type: err.name,
120
+ value: err.message,
121
+ file: err.stack,
122
+ line: 0
123
+ });
124
+ // Rethrow to maintain standard behavior
125
+ throw err;
126
+ });
@@ -77,6 +77,10 @@ document.addEventListener('DOMContentLoaded', () => {
77
77
  renderBasicLog(containers.disk, log, timeStr);
78
78
  } else if (log.module === 'Execution') {
79
79
  renderBasicLog(containers.execution, log, timeStr);
80
+ } else if (log.module === 'Data' && log.type === 'image_metadata') {
81
+ renderImageMetadata(containers.disk, log, timeStr);
82
+ // Also show a brief info in console
83
+ renderBasicLog(containers.console, {module: 'System', type: 'info', data: {text: `Image metadata loaded for ${log.data.file}. Check Disk tab.`}}, timeStr, 'system');
80
84
  } else if (log.module === 'Console') {
81
85
  if (log.type === 'eval_result') {
82
86
  renderEvalResult(containers.console, log, timeStr);
@@ -94,6 +98,43 @@ document.addEventListener('DOMContentLoaded', () => {
94
98
  }
95
99
  }
96
100
 
101
+ function renderImageMetadata(container, log, timeStr) {
102
+ const el = document.createElement('div');
103
+ el.className = 'log-entry image-metadata';
104
+
105
+ let html = `<h3>Image: ${log.data.file}</h3>`;
106
+ html += `<div><strong>Format:</strong> ${log.data.format} | <strong>Size:</strong> ${log.data.size[0]}x${log.data.size[1]} | <strong>Mode:</strong> ${log.data.mode}</div>`;
107
+
108
+ const exifCount = Object.keys(log.data.exif).length;
109
+ if (exifCount > 0) {
110
+ html += `<h4>EXIF Data (${exifCount} entries)</h4>`;
111
+ html += `<pre style="max-height: 200px; overflow-y: auto; background: var(--bg-dark); padding: 5px; font-size: 0.9em;">`;
112
+ for (let [k, v] of Object.entries(log.data.exif)) {
113
+ html += `${k}: ${v}\n`;
114
+ }
115
+ html += `</pre>`;
116
+ html += `<button class="btn-strip-exif" style="margin-top: 10px; padding: 5px 10px; background: #e74c3c; color: white; border: none; border-radius: 4px; cursor: pointer;">Strip EXIF Metadata</button>`;
117
+ } else {
118
+ html += `<h4>No EXIF Data found.</h4>`;
119
+ }
120
+
121
+ el.innerHTML = html;
122
+
123
+ const btn = el.querySelector('.btn-strip-exif');
124
+ if (btn) {
125
+ btn.addEventListener('click', () => {
126
+ if(ws && ws.readyState === WebSocket.OPEN) {
127
+ ws.send(JSON.stringify({action: 'strip_exif', file: log.data.file}));
128
+ btn.textContent = "Stripping...";
129
+ btn.disabled = true;
130
+ }
131
+ });
132
+ }
133
+
134
+ container.appendChild(el);
135
+ scrollToBottom(container);
136
+ }
137
+
97
138
  function renderBasicLog(container, log, timeStr, customClass='') {
98
139
  const el = document.createElement('div');
99
140
  el.className = `log-entry ${customClass}`;
@@ -0,0 +1,227 @@
1
+ import ctypes
2
+ from ctypes import wintypes
3
+ import time
4
+ import struct
5
+ import sys
6
+ import os
7
+
8
+ kernel32 = ctypes.WinDLL('kernel32', use_last_error=True)
9
+
10
+ DEBUG_PROCESS = 0x00000001
11
+ DEBUG_ONLY_THIS_PROCESS = 0x00000002
12
+ DBG_CONTINUE = 0x00010002
13
+ DBG_EXCEPTION_NOT_HANDLED = 0x80010001
14
+
15
+ # Event Codes
16
+ EXCEPTION_DEBUG_EVENT = 1
17
+ CREATE_THREAD_DEBUG_EVENT = 2
18
+ CREATE_PROCESS_DEBUG_EVENT = 3
19
+ EXIT_THREAD_DEBUG_EVENT = 4
20
+ EXIT_PROCESS_DEBUG_EVENT = 5
21
+ LOAD_DLL_DEBUG_EVENT = 6
22
+ UNLOAD_DLL_DEBUG_EVENT = 7
23
+ OUTPUT_DEBUG_STRING_EVENT = 8
24
+ RIP_EVENT = 9
25
+
26
+ class STARTUPINFO(ctypes.Structure):
27
+ _fields_ = [
28
+ ("cb", wintypes.DWORD),
29
+ ("lpReserved", wintypes.LPWSTR),
30
+ ("lpDesktop", wintypes.LPWSTR),
31
+ ("lpTitle", wintypes.LPWSTR),
32
+ ("dwX", wintypes.DWORD),
33
+ ("dwY", wintypes.DWORD),
34
+ ("dwXSize", wintypes.DWORD),
35
+ ("dwYSize", wintypes.DWORD),
36
+ ("dwXCountChars", wintypes.DWORD),
37
+ ("dwYCountChars", wintypes.DWORD),
38
+ ("dwFillAttribute", wintypes.DWORD),
39
+ ("dwFlags", wintypes.DWORD),
40
+ ("wShowWindow", wintypes.WORD),
41
+ ("cbReserved2", wintypes.WORD),
42
+ ("lpReserved2", ctypes.POINTER(wintypes.BYTE)),
43
+ ("hStdInput", wintypes.HANDLE),
44
+ ("hStdOutput", wintypes.HANDLE),
45
+ ("hStdError", wintypes.HANDLE),
46
+ ]
47
+
48
+ class PROCESS_INFORMATION(ctypes.Structure):
49
+ _fields_ = [
50
+ ("hProcess", wintypes.HANDLE),
51
+ ("hThread", wintypes.HANDLE),
52
+ ("dwProcessId", wintypes.DWORD),
53
+ ("dwThreadId", wintypes.DWORD),
54
+ ]
55
+
56
+ class EXCEPTION_RECORD(ctypes.Structure):
57
+ pass
58
+ EXCEPTION_RECORD._fields_ = [
59
+ ("ExceptionCode", wintypes.DWORD),
60
+ ("ExceptionFlags", wintypes.DWORD),
61
+ ("ExceptionRecord", ctypes.POINTER(EXCEPTION_RECORD)),
62
+ ("ExceptionAddress", ctypes.c_void_p),
63
+ ("NumberParameters", wintypes.DWORD),
64
+ ("ExceptionInformation", ctypes.c_void_p * 15),
65
+ ]
66
+
67
+ class EXCEPTION_DEBUG_INFO(ctypes.Structure):
68
+ _fields_ = [
69
+ ("ExceptionRecord", EXCEPTION_RECORD),
70
+ ("dwFirstChance", wintypes.DWORD),
71
+ ]
72
+
73
+ class CREATE_THREAD_DEBUG_INFO(ctypes.Structure):
74
+ _fields_ = [
75
+ ("hThread", wintypes.HANDLE),
76
+ ("lpThreadLocalBase", ctypes.c_void_p),
77
+ ("lpStartAddress", ctypes.c_void_p),
78
+ ]
79
+
80
+ class CREATE_PROCESS_DEBUG_INFO(ctypes.Structure):
81
+ _fields_ = [
82
+ ("hFile", wintypes.HANDLE),
83
+ ("hProcess", wintypes.HANDLE),
84
+ ("hThread", wintypes.HANDLE),
85
+ ("lpBaseOfImage", ctypes.c_void_p),
86
+ ("dwDebugInfoFileOffset", wintypes.DWORD),
87
+ ("nDebugInfoSize", wintypes.DWORD),
88
+ ("lpThreadLocalBase", ctypes.c_void_p),
89
+ ("lpStartAddress", ctypes.c_void_p),
90
+ ("lpImageName", ctypes.c_void_p),
91
+ ("fUnicode", wintypes.WORD),
92
+ ]
93
+
94
+ class EXIT_THREAD_DEBUG_INFO(ctypes.Structure):
95
+ _fields_ = [
96
+ ("dwExitCode", wintypes.DWORD),
97
+ ]
98
+
99
+ class EXIT_PROCESS_DEBUG_INFO(ctypes.Structure):
100
+ _fields_ = [
101
+ ("dwExitCode", wintypes.DWORD),
102
+ ]
103
+
104
+ class LOAD_DLL_DEBUG_INFO(ctypes.Structure):
105
+ _fields_ = [
106
+ ("hFile", wintypes.HANDLE),
107
+ ("lpBaseOfDll", ctypes.c_void_p),
108
+ ("dwDebugInfoFileOffset", wintypes.DWORD),
109
+ ("nDebugInfoSize", wintypes.DWORD),
110
+ ("lpImageName", ctypes.c_void_p),
111
+ ("fUnicode", wintypes.WORD),
112
+ ]
113
+
114
+ class UNLOAD_DLL_DEBUG_INFO(ctypes.Structure):
115
+ _fields_ = [
116
+ ("lpBaseOfDll", ctypes.c_void_p),
117
+ ]
118
+
119
+ class OUTPUT_DEBUG_STRING_INFO(ctypes.Structure):
120
+ _fields_ = [
121
+ ("lpDebugStringData", ctypes.c_void_p),
122
+ ("fUnicode", wintypes.WORD),
123
+ ("nDebugStringLength", wintypes.WORD),
124
+ ]
125
+
126
+ class RIP_INFO(ctypes.Structure):
127
+ _fields_ = [
128
+ ("dwError", wintypes.DWORD),
129
+ ("dwType", wintypes.DWORD),
130
+ ]
131
+
132
+ class DEBUG_EVENT_UNION(ctypes.Union):
133
+ _fields_ = [
134
+ ("Exception", EXCEPTION_DEBUG_INFO),
135
+ ("CreateThread", CREATE_THREAD_DEBUG_INFO),
136
+ ("CreateProcessInfo", CREATE_PROCESS_DEBUG_INFO),
137
+ ("ExitThread", EXIT_THREAD_DEBUG_INFO),
138
+ ("ExitProcess", EXIT_PROCESS_DEBUG_INFO),
139
+ ("LoadDll", LOAD_DLL_DEBUG_INFO),
140
+ ("UnloadDll", UNLOAD_DLL_DEBUG_INFO),
141
+ ("DebugString", OUTPUT_DEBUG_STRING_INFO),
142
+ ("RipInfo", RIP_INFO),
143
+ ]
144
+
145
+ class DEBUG_EVENT(ctypes.Structure):
146
+ _fields_ = [
147
+ ("dwDebugEventCode", wintypes.DWORD),
148
+ ("dwProcessId", wintypes.DWORD),
149
+ ("dwThreadId", wintypes.DWORD),
150
+ ("u", DEBUG_EVENT_UNION),
151
+ ]
152
+
153
+ def read_process_memory(hProcess, address, size):
154
+ buffer = ctypes.create_string_buffer(size)
155
+ bytesRead = ctypes.c_size_t()
156
+ if kernel32.ReadProcessMemory(hProcess, ctypes.c_void_p(address), buffer, size, ctypes.byref(bytesRead)):
157
+ return buffer.raw[:bytesRead.value]
158
+ return b""
159
+
160
+ def debug_process(command, client):
161
+ si = STARTUPINFO()
162
+ si.cb = ctypes.sizeof(si)
163
+ pi = PROCESS_INFORMATION()
164
+
165
+ creation_flags = DEBUG_PROCESS
166
+
167
+ # Convert command list to string for Windows
168
+ cmd_str = " ".join(f'"{c}"' if " " in c else c for c in command)
169
+
170
+ if not kernel32.CreateProcessW(
171
+ None, ctypes.c_wchar_p(cmd_str), None, None, False,
172
+ creation_flags, None, None, ctypes.byref(si), ctypes.byref(pi)):
173
+ client.send("System", "error", {"text": f"Failed to CreateProcess: {ctypes.GetLastError()}"})
174
+ return
175
+
176
+ process_handle = None
177
+ debug_event = DEBUG_EVENT()
178
+
179
+ while True:
180
+ if not kernel32.WaitForDebugEvent(ctypes.byref(debug_event), 1000): # 1 sec timeout to yield
181
+ # Check if process exited
182
+ exit_code = wintypes.DWORD()
183
+ if kernel32.GetExitCodeProcess(pi.hProcess, ctypes.byref(exit_code)) and exit_code.value != 259: # STILL_ACTIVE
184
+ break
185
+ continue
186
+
187
+ continue_status = DBG_CONTINUE
188
+
189
+ if debug_event.dwDebugEventCode == CREATE_PROCESS_DEBUG_EVENT:
190
+ process_handle = debug_event.u.CreateProcessInfo.hProcess
191
+ client.send("System", "info", {"text": f"Process Created: PID {debug_event.dwProcessId}"})
192
+ if debug_event.u.CreateProcessInfo.hFile:
193
+ kernel32.CloseHandle(debug_event.u.CreateProcessInfo.hFile)
194
+
195
+ elif debug_event.dwDebugEventCode == LOAD_DLL_DEBUG_EVENT:
196
+ if debug_event.u.LoadDll.hFile:
197
+ kernel32.CloseHandle(debug_event.u.LoadDll.hFile)
198
+ # Address resolution of DLL names can be complex in Win32, so we just log the event natively.
199
+ # Real debuggers use GetMappedFileName on the lpBaseOfDll.
200
+ pass # Keep output clean unless we resolve name
201
+
202
+ elif debug_event.dwDebugEventCode == OUTPUT_DEBUG_STRING_EVENT:
203
+ if process_handle:
204
+ info = debug_event.u.DebugString
205
+ data = read_process_memory(process_handle, info.lpDebugStringData, info.nDebugStringLength * (2 if info.fUnicode else 1))
206
+ if data:
207
+ text = data.decode('utf-16' if info.fUnicode else 'ansi', 'replace').strip('\x00\r\n')
208
+ if text:
209
+ client.send("Console", "stdout", {"text": f"[DEBUG] {text}"})
210
+
211
+ elif debug_event.dwDebugEventCode == EXCEPTION_DEBUG_EVENT:
212
+ exc = debug_event.u.Exception.ExceptionRecord.ExceptionCode
213
+ # Ignore standard breakpoint exceptions (0x80000003)
214
+ if exc != 0x80000003:
215
+ # Log exception
216
+ continue_status = DBG_EXCEPTION_NOT_HANDLED
217
+
218
+ elif debug_event.dwDebugEventCode == EXIT_PROCESS_DEBUG_EVENT:
219
+ client.send("System", "info", {"text": f"Process Exited with code {debug_event.u.ExitProcess.dwExitCode}"})
220
+ kernel32.ContinueDebugEvent(debug_event.dwProcessId, debug_event.dwThreadId, continue_status)
221
+ break
222
+
223
+ kernel32.ContinueDebugEvent(debug_event.dwProcessId, debug_event.dwThreadId, continue_status)
224
+
225
+ if process_handle:
226
+ kernel32.CloseHandle(pi.hThread)
227
+ kernel32.CloseHandle(pi.hProcess)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: h4-debug
3
- Version: 0.1.4
3
+ Version: 0.2.0
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
@@ -8,6 +8,7 @@ Description-Content-Type: text/markdown
8
8
  Requires-Dist: fastapi>=0.100
9
9
  Requires-Dist: uvicorn[standard]>=0.20
10
10
  Requires-Dist: websockets>=11.0
11
+ Requires-Dist: Pillow>=10.0.0
11
12
 
12
13
  # h4-debug
13
14
 
@@ -2,8 +2,11 @@ README.md
2
2
  pyproject.toml
3
3
  h4_debug/__init__.py
4
4
  h4_debug/cli.py
5
+ h4_debug/handlers.py
5
6
  h4_debug/interceptor.py
7
+ h4_debug/node_interceptor.js
6
8
  h4_debug/server.py
9
+ h4_debug/win_debugger.py
7
10
  h4_debug.egg-info/PKG-INFO
8
11
  h4_debug.egg-info/SOURCES.txt
9
12
  h4_debug.egg-info/dependency_links.txt
@@ -1,3 +1,4 @@
1
1
  fastapi>=0.100
2
2
  uvicorn[standard]>=0.20
3
3
  websockets>=11.0
4
+ Pillow>=10.0.0
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "h4-debug"
7
- version = "0.1.4"
7
+ version = "0.2.0"
8
8
  description = "Advanced application proxy debugger and F12-style web console"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.8"
@@ -15,10 +15,11 @@ dependencies = [
15
15
  "fastapi>=0.100",
16
16
  "uvicorn[standard]>=0.20",
17
17
  "websockets>=11.0",
18
+ "Pillow>=10.0.0",
18
19
  ]
19
20
 
20
21
  [project.scripts]
21
22
  h4-debug = "h4_debug.cli:main"
22
23
 
23
24
  [tool.setuptools.package-data]
24
- h4_debug = ["static/*"]
25
+ "h4_debug" = ["static/*", "*.js"]
@@ -1 +0,0 @@
1
- __version__ = "0.1.4"
@@ -1,103 +0,0 @@
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()
File without changes
File without changes
File without changes