rewind-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.
- rewind/__init__.py +9 -0
- rewind/cli.py +572 -0
- rewind/diff.py +115 -0
- rewind/tracer.py +192 -0
- rewind_debug-0.1.0.dist-info/METADATA +5 -0
- rewind_debug-0.1.0.dist-info/RECORD +10 -0
- rewind_debug-0.1.0.dist-info/WHEEL +5 -0
- rewind_debug-0.1.0.dist-info/entry_points.txt +2 -0
- rewind_debug-0.1.0.dist-info/licenses/LICENSE +21 -0
- rewind_debug-0.1.0.dist-info/top_level.txt +1 -0
rewind/__init__.py
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Rewind - Deterministic Time-Travel Record & Replay Debugger
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from .tracer import Tracer, step, get_global_tracer
|
|
6
|
+
from .diff import compute_state_diff, serialize_state
|
|
7
|
+
|
|
8
|
+
__version__ = "0.1.0"
|
|
9
|
+
__all__ = ["Tracer", "step", "get_global_tracer", "compute_state_diff", "serialize_state"]
|
rewind/cli.py
ADDED
|
@@ -0,0 +1,572 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Rewind Universal CLI - Multi-Mode Execution Tracer & Web Scrubber with Universal Hot-Code Sandbox
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
import argparse
|
|
6
|
+
import http.client
|
|
7
|
+
import http.server
|
|
8
|
+
import io
|
|
9
|
+
import json
|
|
10
|
+
import os
|
|
11
|
+
import re
|
|
12
|
+
import socket
|
|
13
|
+
import socketserver
|
|
14
|
+
import subprocess
|
|
15
|
+
import sys
|
|
16
|
+
import threading
|
|
17
|
+
import time
|
|
18
|
+
import traceback
|
|
19
|
+
import urllib.parse
|
|
20
|
+
import webbrowser
|
|
21
|
+
from contextlib import redirect_stderr, redirect_stdout
|
|
22
|
+
from pathlib import Path
|
|
23
|
+
|
|
24
|
+
# Allow direct execution from ANY directory
|
|
25
|
+
parent_dir = str(Path(__file__).resolve().parent.parent)
|
|
26
|
+
if parent_dir not in sys.path:
|
|
27
|
+
sys.path.insert(0, parent_dir)
|
|
28
|
+
|
|
29
|
+
try:
|
|
30
|
+
from .diff import compute_state_diff, serialize_state
|
|
31
|
+
from .tracer import Tracer, TraceStep, get_global_tracer
|
|
32
|
+
except (ImportError, ValueError):
|
|
33
|
+
from rewind.diff import compute_state_diff, serialize_state
|
|
34
|
+
from rewind.tracer import Tracer, TraceStep, get_global_tracer
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
REWIND_INTERNAL_DIR = os.path.dirname(os.path.abspath(__file__))
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
# --------------------------------------------------------------------------
|
|
41
|
+
# Stream Interceptor (Prints live to terminal + captures into trace)
|
|
42
|
+
# --------------------------------------------------------------------------
|
|
43
|
+
class LiveTeeStream:
|
|
44
|
+
def __init__(self, original_stream):
|
|
45
|
+
self.orig = original_stream
|
|
46
|
+
self.buf = io.StringIO()
|
|
47
|
+
|
|
48
|
+
def write(self, s):
|
|
49
|
+
self.orig.write(s)
|
|
50
|
+
self.buf.write(s)
|
|
51
|
+
|
|
52
|
+
def flush(self):
|
|
53
|
+
self.orig.flush()
|
|
54
|
+
self.buf.flush()
|
|
55
|
+
|
|
56
|
+
def getvalue(self):
|
|
57
|
+
return self.buf.getvalue()
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
# --------------------------------------------------------------------------
|
|
61
|
+
# Server Management Utilities
|
|
62
|
+
# --------------------------------------------------------------------------
|
|
63
|
+
def is_port_in_use(port: int) -> bool:
|
|
64
|
+
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
|
|
65
|
+
return s.connect_ex(("127.0.0.1", port)) == 0
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def find_free_port(start_port: int = 8765, max_attempts: int = 20) -> int:
|
|
69
|
+
for p in range(start_port, start_port + max_attempts):
|
|
70
|
+
if not is_port_in_use(p):
|
|
71
|
+
return p
|
|
72
|
+
return start_port
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def stop_running_server(port: int = 8765):
|
|
76
|
+
try:
|
|
77
|
+
conn = http.client.HTTPConnection("127.0.0.1", port, timeout=1)
|
|
78
|
+
conn.request("POST", "/api/shutdown")
|
|
79
|
+
res = conn.getresponse()
|
|
80
|
+
if res.status == 200:
|
|
81
|
+
print(f"[Rewind] Server on port {port} shut down cleanly.")
|
|
82
|
+
return True
|
|
83
|
+
except Exception:
|
|
84
|
+
pass
|
|
85
|
+
|
|
86
|
+
try:
|
|
87
|
+
out = subprocess.check_output(["lsof", "-ti", f":{port}"], text=True).strip()
|
|
88
|
+
if out:
|
|
89
|
+
pids = out.split("\n")
|
|
90
|
+
for pid in pids:
|
|
91
|
+
if pid:
|
|
92
|
+
subprocess.run(["kill", "-9", pid], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
|
|
93
|
+
print(f"[Rewind] Stopped server processes on port {port} (PIDs: {', '.join(pids)}).")
|
|
94
|
+
return True
|
|
95
|
+
except Exception:
|
|
96
|
+
pass
|
|
97
|
+
|
|
98
|
+
print(f"[Rewind] No server was running on port {port}.")
|
|
99
|
+
return False
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
def check_server_status(port: int = 8765):
|
|
103
|
+
if is_port_in_use(port):
|
|
104
|
+
print(f"[Rewind] Web Viewer is RUNNING at: http://localhost:{port}")
|
|
105
|
+
else:
|
|
106
|
+
print(f"[Rewind] Web Viewer is STOPPED.")
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
# --------------------------------------------------------------------------
|
|
110
|
+
# Universal Hot-Code Sandbox Replay Engine & Disk Patcher
|
|
111
|
+
# --------------------------------------------------------------------------
|
|
112
|
+
def execute_hot_replay(payload: dict) -> dict:
|
|
113
|
+
code_str = payload.get("code", "")
|
|
114
|
+
filepath = payload.get("filepath", "")
|
|
115
|
+
|
|
116
|
+
out_buf = io.StringIO()
|
|
117
|
+
err_buf = io.StringIO()
|
|
118
|
+
|
|
119
|
+
start_t = time.perf_counter()
|
|
120
|
+
try:
|
|
121
|
+
compiled = compile(code_str, filepath or "<sandbox>", "exec")
|
|
122
|
+
sandbox_globals = {"__name__": "__main__", "__builtins__": __builtins__, "state": {}}
|
|
123
|
+
|
|
124
|
+
with redirect_stdout(out_buf), redirect_stderr(err_buf):
|
|
125
|
+
exec(compiled, sandbox_globals)
|
|
126
|
+
|
|
127
|
+
elapsed = round((time.perf_counter() - start_t) * 1000, 2)
|
|
128
|
+
std_out = out_buf.getvalue()
|
|
129
|
+
std_err = err_buf.getvalue()
|
|
130
|
+
|
|
131
|
+
return {
|
|
132
|
+
"success": True,
|
|
133
|
+
"has_crash": False,
|
|
134
|
+
"stdout": std_out,
|
|
135
|
+
"stderr": std_err,
|
|
136
|
+
"timing_ms": elapsed,
|
|
137
|
+
"message": f"Script executed cleanly with zero errors! Output:\n{std_out.strip() or '(none)'}",
|
|
138
|
+
}
|
|
139
|
+
except Exception as e:
|
|
140
|
+
elapsed = round((time.perf_counter() - start_t) * 1000, 2)
|
|
141
|
+
return {
|
|
142
|
+
"success": False,
|
|
143
|
+
"has_crash": True,
|
|
144
|
+
"error": f"{type(e).__name__}: {str(e)}",
|
|
145
|
+
"traceback": traceback.format_exc(),
|
|
146
|
+
"timing_ms": elapsed,
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
def find_file_in_project(filename: str) -> str:
|
|
151
|
+
if not filename:
|
|
152
|
+
return ""
|
|
153
|
+
if os.path.isabs(filename) and os.path.exists(filename):
|
|
154
|
+
return filename
|
|
155
|
+
|
|
156
|
+
base_name = os.path.basename(filename)
|
|
157
|
+
search_dirs = [os.getcwd(), parent_dir, os.path.join(parent_dir, "tests")]
|
|
158
|
+
|
|
159
|
+
for d in search_dirs:
|
|
160
|
+
candidate = os.path.join(d, filename)
|
|
161
|
+
if os.path.exists(candidate) and os.path.isfile(candidate):
|
|
162
|
+
return os.path.abspath(candidate)
|
|
163
|
+
candidate_base = os.path.join(d, base_name)
|
|
164
|
+
if os.path.exists(candidate_base) and os.path.isfile(candidate_base):
|
|
165
|
+
return os.path.abspath(candidate_base)
|
|
166
|
+
|
|
167
|
+
for root, _, files in os.walk(parent_dir):
|
|
168
|
+
if base_name in files:
|
|
169
|
+
return os.path.abspath(os.path.join(root, base_name))
|
|
170
|
+
|
|
171
|
+
return ""
|
|
172
|
+
|
|
173
|
+
|
|
174
|
+
def apply_patch_to_disk(payload: dict) -> dict:
|
|
175
|
+
raw_filepath = payload.get("filepath", "")
|
|
176
|
+
new_code = payload.get("new_code", "")
|
|
177
|
+
|
|
178
|
+
target_path = find_file_in_project(raw_filepath)
|
|
179
|
+
|
|
180
|
+
if not target_path or not os.path.exists(target_path):
|
|
181
|
+
return {"success": False, "error": f"File not found on disk: '{raw_filepath}'."}
|
|
182
|
+
|
|
183
|
+
backup_path = f"{target_path}.bak"
|
|
184
|
+
try:
|
|
185
|
+
with open(target_path, "r", encoding="utf-8") as src, open(backup_path, "w", encoding="utf-8") as dst:
|
|
186
|
+
dst.write(src.read())
|
|
187
|
+
|
|
188
|
+
with open(target_path, "w", encoding="utf-8") as f:
|
|
189
|
+
f.write(new_code)
|
|
190
|
+
|
|
191
|
+
return {
|
|
192
|
+
"success": True,
|
|
193
|
+
"filepath": target_path,
|
|
194
|
+
"backup": backup_path,
|
|
195
|
+
"message": f"Successfully saved fix to {os.path.basename(target_path)}! (Backup saved at {os.path.basename(backup_path)})",
|
|
196
|
+
}
|
|
197
|
+
except Exception as e:
|
|
198
|
+
return {"success": False, "error": str(e)}
|
|
199
|
+
|
|
200
|
+
|
|
201
|
+
# --------------------------------------------------------------------------
|
|
202
|
+
# Mode 1: Auto Python Script Tracer (Multi-Step Callstack Frame Tracker)
|
|
203
|
+
# --------------------------------------------------------------------------
|
|
204
|
+
def auto_trace_hook(tracer: Tracer):
|
|
205
|
+
last_state = {}
|
|
206
|
+
frame_steps = {}
|
|
207
|
+
|
|
208
|
+
def trace_calls(frame, event, arg):
|
|
209
|
+
nonlocal last_state
|
|
210
|
+
filename = frame.f_code.co_filename
|
|
211
|
+
|
|
212
|
+
# Only ignore Rewind internal files or stdlib
|
|
213
|
+
if filename.startswith(REWIND_INTERNAL_DIR) or filename.startswith("<") or "/lib/python" in filename or "site-packages" in filename:
|
|
214
|
+
return trace_calls
|
|
215
|
+
|
|
216
|
+
func_name = frame.f_code.co_name
|
|
217
|
+
line_no = frame.f_lineno
|
|
218
|
+
frame_id = id(frame)
|
|
219
|
+
|
|
220
|
+
if event == "call":
|
|
221
|
+
if func_name in ("<module>", "main"):
|
|
222
|
+
return trace_calls
|
|
223
|
+
tracer._step_counter += 1
|
|
224
|
+
curr_id = tracer._step_counter
|
|
225
|
+
step_inputs = dict(frame.f_locals)
|
|
226
|
+
if tracer.source_code:
|
|
227
|
+
step_inputs["source_code"] = tracer.source_code
|
|
228
|
+
if tracer.source_filepath:
|
|
229
|
+
step_inputs["filepath"] = tracer.source_filepath
|
|
230
|
+
|
|
231
|
+
step = TraceStep(
|
|
232
|
+
step_id=curr_id,
|
|
233
|
+
name=f"{func_name}()",
|
|
234
|
+
caller_file=os.path.basename(filename),
|
|
235
|
+
caller_line=line_no,
|
|
236
|
+
inputs=serialize_state(step_inputs),
|
|
237
|
+
)
|
|
238
|
+
step.state_before = serialize_state(last_state)
|
|
239
|
+
frame_steps[frame_id] = step
|
|
240
|
+
|
|
241
|
+
elif event == "return":
|
|
242
|
+
step = frame_steps.pop(frame_id, None)
|
|
243
|
+
if step:
|
|
244
|
+
step.output = serialize_state(arg)
|
|
245
|
+
state_now = dict(last_state)
|
|
246
|
+
state_now.update(dict(frame.f_locals))
|
|
247
|
+
if isinstance(arg, dict):
|
|
248
|
+
state_now.update(arg)
|
|
249
|
+
elif arg is not None:
|
|
250
|
+
state_now[f"{func_name}_result"] = arg
|
|
251
|
+
step.state_after = serialize_state(state_now)
|
|
252
|
+
step.diff = compute_state_diff(step.state_before, step.state_after)
|
|
253
|
+
step.status = "SUCCESS"
|
|
254
|
+
last_state = dict(state_now)
|
|
255
|
+
tracer.steps.append(step)
|
|
256
|
+
|
|
257
|
+
elif event == "exception":
|
|
258
|
+
step = frame_steps.get(frame_id)
|
|
259
|
+
if step:
|
|
260
|
+
exc_type, exc_value, _ = arg
|
|
261
|
+
step.status = "FAILED"
|
|
262
|
+
step.error = {
|
|
263
|
+
"type": exc_type.__name__,
|
|
264
|
+
"message": str(exc_value),
|
|
265
|
+
"source_code": tracer.source_code,
|
|
266
|
+
"filepath": tracer.source_filepath,
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
return trace_calls
|
|
270
|
+
|
|
271
|
+
return trace_calls
|
|
272
|
+
|
|
273
|
+
|
|
274
|
+
def run_python_command(args):
|
|
275
|
+
script_path = os.path.abspath(args.script)
|
|
276
|
+
if not os.path.exists(script_path):
|
|
277
|
+
print(f"[Error] File '{args.script}' not found.")
|
|
278
|
+
sys.exit(1)
|
|
279
|
+
|
|
280
|
+
script_dir = os.path.dirname(script_path)
|
|
281
|
+
if script_dir not in sys.path:
|
|
282
|
+
sys.path.insert(0, script_dir)
|
|
283
|
+
|
|
284
|
+
raw_source = ""
|
|
285
|
+
with open(script_path, "r", encoding="utf-8") as f:
|
|
286
|
+
raw_source = f.read()
|
|
287
|
+
|
|
288
|
+
tracer = get_global_tracer()
|
|
289
|
+
tracer.title = f"Auto-Trace: {os.path.basename(script_path)}"
|
|
290
|
+
tracer.source_code = raw_source
|
|
291
|
+
tracer.source_filepath = script_path
|
|
292
|
+
print(f"[Rewind] Auto-tracing Python script: {script_path}")
|
|
293
|
+
|
|
294
|
+
tee_out = LiveTeeStream(sys.stdout)
|
|
295
|
+
tee_err = LiveTeeStream(sys.stderr)
|
|
296
|
+
|
|
297
|
+
sys.settrace(auto_trace_hook(tracer))
|
|
298
|
+
start_time = time.time()
|
|
299
|
+
has_error = False
|
|
300
|
+
try:
|
|
301
|
+
code = compile(raw_source, script_path, "exec")
|
|
302
|
+
with redirect_stdout(tee_out), redirect_stderr(tee_err):
|
|
303
|
+
exec(code, {"__name__": "__main__", "__file__": script_path, "__builtins__": __builtins__})
|
|
304
|
+
except SystemExit as se:
|
|
305
|
+
sys.settrace(None)
|
|
306
|
+
if se.code != 0 and se.code is not None:
|
|
307
|
+
has_error = True
|
|
308
|
+
print(f"\n[Rewind] Process exited with error code: {se.code}")
|
|
309
|
+
step_obj = tracer.record_step_data(
|
|
310
|
+
name=f"SystemExit: {se.code}",
|
|
311
|
+
inputs={"exit_code": se.code, "source_code": raw_source, "filepath": script_path, "stdout": tee_out.getvalue(), "stderr": tee_err.getvalue()},
|
|
312
|
+
state_updates={"crashed": True, "error_type": "SystemExit", "error_msg": f"Exited with code {se.code}", "stdout": tee_out.getvalue()},
|
|
313
|
+
)
|
|
314
|
+
step_obj.caller_file = os.path.basename(script_path)
|
|
315
|
+
step_obj.status = "FAILED"
|
|
316
|
+
step_obj.error = {"type": "SystemExit", "message": f"Exit code {se.code}", "source_code": raw_source, "filepath": script_path}
|
|
317
|
+
except Exception as e:
|
|
318
|
+
sys.settrace(None)
|
|
319
|
+
has_error = True
|
|
320
|
+
tb_str = traceback.format_exc()
|
|
321
|
+
print(f"\n[Rewind] Caught Fatal Execution Exception: {type(e).__name__}: {e}")
|
|
322
|
+
|
|
323
|
+
step_obj = tracer.record_step_data(
|
|
324
|
+
name=f"Fatal Crash: {type(e).__name__}",
|
|
325
|
+
inputs={"error": str(e), "source_code": raw_source, "filepath": script_path, "stdout": tee_out.getvalue(), "stderr": tee_err.getvalue()},
|
|
326
|
+
state_updates={"crashed": True, "error_type": type(e).__name__, "error_msg": str(e), "stdout": tee_out.getvalue()},
|
|
327
|
+
)
|
|
328
|
+
step_obj.caller_file = os.path.basename(script_path)
|
|
329
|
+
step_obj.status = "FAILED"
|
|
330
|
+
step_obj.error = {
|
|
331
|
+
"type": type(e).__name__,
|
|
332
|
+
"message": str(e),
|
|
333
|
+
"traceback": tb_str,
|
|
334
|
+
"source_code": raw_source,
|
|
335
|
+
"filepath": script_path,
|
|
336
|
+
}
|
|
337
|
+
finally:
|
|
338
|
+
sys.settrace(None)
|
|
339
|
+
captured_stdout = tee_out.getvalue()
|
|
340
|
+
captured_stderr = tee_err.getvalue()
|
|
341
|
+
|
|
342
|
+
if not has_error and len(tracer.steps) == 0:
|
|
343
|
+
step_obj = tracer.record_step_data(
|
|
344
|
+
name=f"exec: {os.path.basename(script_path)}",
|
|
345
|
+
inputs={"stdout": captured_stdout, "stderr": captured_stderr, "source_code": raw_source, "filepath": script_path},
|
|
346
|
+
state_updates={"stdout": captured_stdout, "status": "COMPLETED"},
|
|
347
|
+
)
|
|
348
|
+
step_obj.caller_file = os.path.basename(script_path)
|
|
349
|
+
step_obj.status = "SUCCESS"
|
|
350
|
+
|
|
351
|
+
elapsed = round((time.time() - start_time) * 1000, 2)
|
|
352
|
+
out = args.output or "rewind_trace.json"
|
|
353
|
+
tracer.export(out)
|
|
354
|
+
print(f"\n[Rewind] Trace saved: {out} ({len(tracer.steps)} steps, {elapsed} ms)")
|
|
355
|
+
if not args.no_open:
|
|
356
|
+
view_trace(out, port=args.port)
|
|
357
|
+
|
|
358
|
+
|
|
359
|
+
# --------------------------------------------------------------------------
|
|
360
|
+
# Mode 2: Universal Process Wrapper (with Graceful Ctrl+C Handling)
|
|
361
|
+
# --------------------------------------------------------------------------
|
|
362
|
+
def exec_process_command(args):
|
|
363
|
+
cmd = args.command
|
|
364
|
+
if not cmd:
|
|
365
|
+
print("[Error] No command provided to execute.")
|
|
366
|
+
sys.exit(1)
|
|
367
|
+
|
|
368
|
+
tracer = get_global_tracer()
|
|
369
|
+
tracer.title = f"Process Trace: {' '.join(cmd)}"
|
|
370
|
+
print(f"[Rewind] Monitoring process: {' '.join(cmd)}")
|
|
371
|
+
|
|
372
|
+
start_time = time.time()
|
|
373
|
+
try:
|
|
374
|
+
process = subprocess.Popen(
|
|
375
|
+
cmd,
|
|
376
|
+
stdout=subprocess.PIPE,
|
|
377
|
+
stderr=subprocess.PIPE,
|
|
378
|
+
text=True,
|
|
379
|
+
bufsize=1,
|
|
380
|
+
)
|
|
381
|
+
except FileNotFoundError:
|
|
382
|
+
print(f"[Rewind] Error: Command '{cmd[0]}' not found on your system.")
|
|
383
|
+
return
|
|
384
|
+
|
|
385
|
+
step_id = 0
|
|
386
|
+
full_stdout = []
|
|
387
|
+
return_code = 0
|
|
388
|
+
stderr_out = ""
|
|
389
|
+
|
|
390
|
+
try:
|
|
391
|
+
while True:
|
|
392
|
+
line = process.stdout.readline()
|
|
393
|
+
if not line and process.poll() is not None:
|
|
394
|
+
break
|
|
395
|
+
if line:
|
|
396
|
+
step_id += 1
|
|
397
|
+
full_stdout.append(line)
|
|
398
|
+
sys.stdout.write(line)
|
|
399
|
+
tracer.record_step_data(
|
|
400
|
+
name=f"stdout: {line.strip()[:40]}",
|
|
401
|
+
inputs={"raw": line.strip(), "stdout": "".join(full_stdout)},
|
|
402
|
+
state_updates={"last_log": line.strip(), "step_count": step_id, "stdout": "".join(full_stdout)},
|
|
403
|
+
)
|
|
404
|
+
|
|
405
|
+
stderr_out = process.stderr.read()
|
|
406
|
+
return_code = process.poll()
|
|
407
|
+
except KeyboardInterrupt:
|
|
408
|
+
print("\n[Rewind] Process stopped by user (SIGINT).")
|
|
409
|
+
process.terminate()
|
|
410
|
+
return_code = 130
|
|
411
|
+
|
|
412
|
+
if stderr_out or return_code != 0:
|
|
413
|
+
if stderr_out:
|
|
414
|
+
sys.stderr.write(stderr_out)
|
|
415
|
+
|
|
416
|
+
err_type = "ProcessError"
|
|
417
|
+
err_msg = stderr_out.strip() if stderr_out else f"Process exited with code {return_code}"
|
|
418
|
+
|
|
419
|
+
if "ModuleNotFoundError" in stderr_out:
|
|
420
|
+
err_type = "ModuleNotFoundError"
|
|
421
|
+
elif "ImportError" in stderr_out:
|
|
422
|
+
err_type = "ImportError"
|
|
423
|
+
elif "TypeError" in stderr_out:
|
|
424
|
+
err_type = "TypeError"
|
|
425
|
+
elif "SyntaxError" in stderr_out:
|
|
426
|
+
err_type = "SyntaxError"
|
|
427
|
+
elif "npm error" in stderr_out:
|
|
428
|
+
err_type = "NpmScriptError"
|
|
429
|
+
elif return_code == 130:
|
|
430
|
+
err_type = "InterruptedByUser"
|
|
431
|
+
|
|
432
|
+
step_id += 1
|
|
433
|
+
step_obj = tracer.record_step_data(
|
|
434
|
+
name=f"Crash: {err_type}",
|
|
435
|
+
inputs={"stderr": stderr_out.strip(), "exit_code": return_code, "stdout": "".join(full_stdout)},
|
|
436
|
+
state_updates={"has_error": True, "error_type": err_type, "error_output": stderr_out.strip()},
|
|
437
|
+
)
|
|
438
|
+
step_obj.status = "FAILED"
|
|
439
|
+
step_obj.error = {
|
|
440
|
+
"type": err_type,
|
|
441
|
+
"message": err_msg,
|
|
442
|
+
"traceback": stderr_out.strip(),
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
out = args.output or "rewind_trace.json"
|
|
446
|
+
tracer.export(out)
|
|
447
|
+
print(f"\n[Rewind] Process exited with code {return_code}. Trace saved to: {out}")
|
|
448
|
+
if not args.no_open:
|
|
449
|
+
view_trace(out, port=args.port)
|
|
450
|
+
|
|
451
|
+
|
|
452
|
+
# --------------------------------------------------------------------------
|
|
453
|
+
# Mode 3: Robust Web Viewer Server with Hot-Code Sandbox Endpoints
|
|
454
|
+
# --------------------------------------------------------------------------
|
|
455
|
+
def view_trace(trace_path: str = "rewind_trace.json", port: int = 8765):
|
|
456
|
+
viewer_dir = Path(__file__).resolve().parent.parent / "viewer"
|
|
457
|
+
if not viewer_dir.exists():
|
|
458
|
+
viewer_dir = Path("viewer").resolve()
|
|
459
|
+
|
|
460
|
+
if os.path.exists(trace_path):
|
|
461
|
+
dest_path = viewer_dir / "rewind_trace.json"
|
|
462
|
+
with open(trace_path, "r", encoding="utf-8") as src, open(dest_path, "w", encoding="utf-8") as dst:
|
|
463
|
+
dst.write(src.read())
|
|
464
|
+
|
|
465
|
+
active_port = find_free_port(port)
|
|
466
|
+
if active_port != port:
|
|
467
|
+
print(f"[Rewind] Port {port} was busy. Using available port {active_port} instead.")
|
|
468
|
+
|
|
469
|
+
class ManagedHandler(http.server.SimpleHTTPRequestHandler):
|
|
470
|
+
def __init__(self, *args, **kwargs):
|
|
471
|
+
super().__init__(*args, directory=str(viewer_dir), **kwargs)
|
|
472
|
+
|
|
473
|
+
def log_message(self, format, *args): pass
|
|
474
|
+
|
|
475
|
+
def do_POST(self):
|
|
476
|
+
content_len = int(self.headers.get("Content-Length", 0))
|
|
477
|
+
body = self.rfile.read(content_len).decode("utf-8") if content_len > 0 else "{}"
|
|
478
|
+
try:
|
|
479
|
+
payload = json.loads(body)
|
|
480
|
+
except Exception:
|
|
481
|
+
payload = {}
|
|
482
|
+
|
|
483
|
+
if self.path == "/api/shutdown":
|
|
484
|
+
self.send_response(200)
|
|
485
|
+
self.send_header("Content-Type", "application/json")
|
|
486
|
+
self.send_header("Access-Control-Allow-Origin", "*")
|
|
487
|
+
self.end_headers()
|
|
488
|
+
self.wfile.write(b'{"status":"shutting_down"}')
|
|
489
|
+
print("\n[Rewind] Received shutdown command from browser.")
|
|
490
|
+
threading.Thread(target=lambda: (time.sleep(0.3), server.shutdown())).start()
|
|
491
|
+
return
|
|
492
|
+
|
|
493
|
+
elif self.path == "/api/replay":
|
|
494
|
+
result = execute_hot_replay(payload)
|
|
495
|
+
self.send_response(200)
|
|
496
|
+
self.send_header("Content-Type", "application/json")
|
|
497
|
+
self.send_header("Access-Control-Allow-Origin", "*")
|
|
498
|
+
self.end_headers()
|
|
499
|
+
self.wfile.write(json.dumps(result).encode("utf-8"))
|
|
500
|
+
return
|
|
501
|
+
|
|
502
|
+
elif self.path == "/api/patch":
|
|
503
|
+
result = apply_patch_to_disk(payload)
|
|
504
|
+
self.send_response(200)
|
|
505
|
+
self.send_header("Content-Type", "application/json")
|
|
506
|
+
self.send_header("Access-Control-Allow-Origin", "*")
|
|
507
|
+
self.end_headers()
|
|
508
|
+
self.wfile.write(json.dumps(result).encode("utf-8"))
|
|
509
|
+
return
|
|
510
|
+
|
|
511
|
+
self.send_error(404, "Not Found")
|
|
512
|
+
|
|
513
|
+
socketserver.TCPServer.allow_reuse_address = True
|
|
514
|
+
server = socketserver.TCPServer(("", active_port), ManagedHandler)
|
|
515
|
+
url = f"http://localhost:{active_port}"
|
|
516
|
+
|
|
517
|
+
print("=" * 55)
|
|
518
|
+
print(f"[Rewind] Web Scrubber active at: {url}")
|
|
519
|
+
print(f"Press Ctrl+C in terminal or click 'Stop Server' in UI to turn off.")
|
|
520
|
+
print("=" * 55)
|
|
521
|
+
|
|
522
|
+
threading.Thread(target=lambda: (time.sleep(0.4), webbrowser.open(url)), daemon=True).start()
|
|
523
|
+
|
|
524
|
+
try:
|
|
525
|
+
server.serve_forever()
|
|
526
|
+
except KeyboardInterrupt:
|
|
527
|
+
pass
|
|
528
|
+
finally:
|
|
529
|
+
server.server_close()
|
|
530
|
+
print("\n[Rewind] Web server closed successfully.")
|
|
531
|
+
|
|
532
|
+
|
|
533
|
+
# --------------------------------------------------------------------------
|
|
534
|
+
# Main Entry Point
|
|
535
|
+
# --------------------------------------------------------------------------
|
|
536
|
+
def main():
|
|
537
|
+
parser = argparse.ArgumentParser(prog="rewind", description="Rewind - Universal Time-Travel Debugger")
|
|
538
|
+
subparsers = parser.add_subparsers(dest="command", required=True)
|
|
539
|
+
|
|
540
|
+
run_p = subparsers.add_parser("run", help="Auto-trace a Python script with zero code changes")
|
|
541
|
+
run_p.add_argument("-o", "--output", default="rewind_trace.json")
|
|
542
|
+
run_p.add_argument("-p", "--port", type=int, default=8765)
|
|
543
|
+
run_p.add_argument("--no-open", action="store_true")
|
|
544
|
+
run_p.add_argument("script", help="Target Python script")
|
|
545
|
+
run_p.set_defaults(func=run_python_command)
|
|
546
|
+
|
|
547
|
+
exec_p = subparsers.add_parser("exec", help="Trace ANY command/server (Node.js, Go, Docker, etc.)")
|
|
548
|
+
exec_p.add_argument("-o", "--output", default="rewind_trace.json")
|
|
549
|
+
exec_p.add_argument("-p", "--port", type=int, default=8765)
|
|
550
|
+
exec_p.add_argument("--no-open", action="store_true")
|
|
551
|
+
exec_p.add_argument("command", nargs=argparse.REMAINDER, help="Command and args to execute")
|
|
552
|
+
exec_p.set_defaults(func=exec_process_command)
|
|
553
|
+
|
|
554
|
+
view_p = subparsers.add_parser("view", help="Launch the interactive web viewer")
|
|
555
|
+
view_p.add_argument("-p", "--port", type=int, default=8765)
|
|
556
|
+
view_p.add_argument("trace_file", nargs="?", default="rewind_trace.json")
|
|
557
|
+
view_p.set_defaults(func=lambda a: view_trace(a.trace_file, a.port))
|
|
558
|
+
|
|
559
|
+
stop_p = subparsers.add_parser("stop", help="Stop any running Rewind web viewer server")
|
|
560
|
+
stop_p.add_argument("-p", "--port", type=int, default=8765)
|
|
561
|
+
stop_p.set_defaults(func=lambda a: stop_running_server(a.port))
|
|
562
|
+
|
|
563
|
+
status_p = subparsers.add_parser("status", help="Check status of Rewind web viewer")
|
|
564
|
+
status_p.add_argument("-p", "--port", type=int, default=8765)
|
|
565
|
+
status_p.set_defaults(func=lambda a: check_server_status(a.port))
|
|
566
|
+
|
|
567
|
+
args = parser.parse_args()
|
|
568
|
+
args.func(args)
|
|
569
|
+
|
|
570
|
+
|
|
571
|
+
if __name__ == "__main__":
|
|
572
|
+
main()
|
rewind/diff.py
ADDED
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
"""
|
|
2
|
+
High-performance structural diffing and circular-safe object serializer.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from typing import Any, Dict, List, Optional, Set
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def serialize_state(obj: Any, max_depth: int = 6, seen: Optional[Set[int]] = None) -> Any:
|
|
9
|
+
"""Recursively serializes any Python data structure or object into a JSON-serializable structure."""
|
|
10
|
+
if seen is None:
|
|
11
|
+
seen = set()
|
|
12
|
+
|
|
13
|
+
if obj is None or isinstance(obj, (bool, int, float, str)):
|
|
14
|
+
return obj
|
|
15
|
+
|
|
16
|
+
# 1. Prevent infinite loops on circular references
|
|
17
|
+
try:
|
|
18
|
+
obj_id = id(obj)
|
|
19
|
+
if obj_id in seen:
|
|
20
|
+
return f"<CircularRef: {type(obj).__name__}>"
|
|
21
|
+
|
|
22
|
+
if max_depth <= 0:
|
|
23
|
+
return f"<MaxDepthReached: {type(obj).__name__}>"
|
|
24
|
+
|
|
25
|
+
seen.add(obj_id)
|
|
26
|
+
|
|
27
|
+
# 2. Dictionaries
|
|
28
|
+
if isinstance(obj, dict):
|
|
29
|
+
return {str(k): serialize_state(v, max_depth - 1, set(seen)) for k, v in obj.items()}
|
|
30
|
+
|
|
31
|
+
# 3. Lists & Tuples
|
|
32
|
+
if isinstance(obj, (list, tuple)):
|
|
33
|
+
return [serialize_state(item, max_depth - 1, set(seen)) for item in obj]
|
|
34
|
+
|
|
35
|
+
# 4. Sets
|
|
36
|
+
if isinstance(obj, set):
|
|
37
|
+
try:
|
|
38
|
+
sorted_items = sorted(list(obj), key=lambda x: str(x))
|
|
39
|
+
except Exception:
|
|
40
|
+
sorted_items = list(obj)
|
|
41
|
+
return [serialize_state(item, max_depth - 1, set(seen)) for item in sorted_items]
|
|
42
|
+
|
|
43
|
+
# 5. Custom Objects / Classes
|
|
44
|
+
if hasattr(obj, "__dict__"):
|
|
45
|
+
try:
|
|
46
|
+
clean_vars = {k: v for k, v in vars(obj).items() if not k.startswith("_")}
|
|
47
|
+
return {
|
|
48
|
+
"__class__": obj.__class__.__name__,
|
|
49
|
+
"__data__": serialize_state(clean_vars, max_depth - 1, set(seen)),
|
|
50
|
+
}
|
|
51
|
+
except Exception:
|
|
52
|
+
pass
|
|
53
|
+
|
|
54
|
+
# 6. Safe String Fallback
|
|
55
|
+
return repr(obj)
|
|
56
|
+
except Exception as e:
|
|
57
|
+
return f"<Unserializable: {type(obj).__name__} ({str(e)})>"
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def compute_state_diff(prev_state: Any, curr_state: Any, path: str = "root") -> List[Dict[str, Any]]:
|
|
61
|
+
"""Recursively compares two states and computes added (+), removed (-), and mutated (~) diffs."""
|
|
62
|
+
diffs = []
|
|
63
|
+
|
|
64
|
+
# Case 1: Both are Dictionaries
|
|
65
|
+
if isinstance(prev_state, dict) and isinstance(curr_state, dict):
|
|
66
|
+
prev_keys = set(prev_state.keys())
|
|
67
|
+
curr_keys = set(curr_state.keys())
|
|
68
|
+
|
|
69
|
+
# Added Keys
|
|
70
|
+
for k in curr_keys - prev_keys:
|
|
71
|
+
key_path = f"{path}.{k}" if path != "root" else str(k)
|
|
72
|
+
diffs.append({
|
|
73
|
+
"type": "added",
|
|
74
|
+
"path": key_path,
|
|
75
|
+
"value": serialize_state(curr_state[k]),
|
|
76
|
+
})
|
|
77
|
+
|
|
78
|
+
# Removed Keys
|
|
79
|
+
for k in prev_keys - curr_keys:
|
|
80
|
+
key_path = f"{path}.{k}" if path != "root" else str(k)
|
|
81
|
+
diffs.append({
|
|
82
|
+
"type": "removed",
|
|
83
|
+
"path": key_path,
|
|
84
|
+
"prev_value": serialize_state(prev_state[k]),
|
|
85
|
+
})
|
|
86
|
+
|
|
87
|
+
# Common Keys
|
|
88
|
+
for k in prev_keys & curr_keys:
|
|
89
|
+
key_path = f"{path}.{k}" if path != "root" else str(k)
|
|
90
|
+
v_prev = prev_state[k]
|
|
91
|
+
v_curr = curr_state[k]
|
|
92
|
+
|
|
93
|
+
if v_prev != v_curr:
|
|
94
|
+
if isinstance(v_prev, dict) and isinstance(v_curr, dict):
|
|
95
|
+
diffs.extend(compute_state_diff(v_prev, v_curr, key_path))
|
|
96
|
+
else:
|
|
97
|
+
diffs.append({
|
|
98
|
+
"type": "mutated",
|
|
99
|
+
"path": key_path,
|
|
100
|
+
"prev_value": serialize_state(v_prev),
|
|
101
|
+
"new_value": serialize_state(v_curr),
|
|
102
|
+
})
|
|
103
|
+
|
|
104
|
+
return diffs
|
|
105
|
+
|
|
106
|
+
# Case 2: Primitive or Non-Dict Value Comparison
|
|
107
|
+
if prev_state != curr_state:
|
|
108
|
+
diffs.append({
|
|
109
|
+
"type": "mutated",
|
|
110
|
+
"path": path,
|
|
111
|
+
"prev_value": serialize_state(prev_state),
|
|
112
|
+
"new_value": serialize_state(curr_state),
|
|
113
|
+
})
|
|
114
|
+
|
|
115
|
+
return diffs
|
rewind/tracer.py
ADDED
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Core execution tracer and timeline recording engine for Rewind.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
import inspect
|
|
6
|
+
import json
|
|
7
|
+
import os
|
|
8
|
+
import threading
|
|
9
|
+
import time
|
|
10
|
+
import traceback
|
|
11
|
+
from contextlib import contextmanager
|
|
12
|
+
from typing import Any, Dict, List, Optional
|
|
13
|
+
|
|
14
|
+
from .diff import compute_state_diff, serialize_state
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class TraceStep:
|
|
18
|
+
def __init__(
|
|
19
|
+
self,
|
|
20
|
+
step_id: int,
|
|
21
|
+
name: str,
|
|
22
|
+
caller_file: str,
|
|
23
|
+
caller_line: int,
|
|
24
|
+
inputs: Optional[Dict[str, Any]] = None,
|
|
25
|
+
):
|
|
26
|
+
self.step_id = step_id
|
|
27
|
+
self.name = name
|
|
28
|
+
self.caller_file = caller_file
|
|
29
|
+
self.caller_line = caller_line
|
|
30
|
+
self.inputs = inputs or {}
|
|
31
|
+
self.output: Any = None
|
|
32
|
+
self.state_before: Dict[str, Any] = {}
|
|
33
|
+
self.state_after: Dict[str, Any] = {}
|
|
34
|
+
self.diff: List[Dict[str, Any]] = []
|
|
35
|
+
self.duration_us: float = 0.0
|
|
36
|
+
self.timestamp: float = time.time()
|
|
37
|
+
self.status: str = "PENDING"
|
|
38
|
+
self.error: Optional[Dict[str, Any]] = None
|
|
39
|
+
|
|
40
|
+
def to_dict(self) -> Dict[str, Any]:
|
|
41
|
+
"""Converts this step into a JSON-serializable dictionary."""
|
|
42
|
+
return {
|
|
43
|
+
"step_id": self.step_id,
|
|
44
|
+
"name": self.name,
|
|
45
|
+
"caller_file": self.caller_file,
|
|
46
|
+
"caller_line": self.caller_line,
|
|
47
|
+
"inputs": self.inputs,
|
|
48
|
+
"output": self.output,
|
|
49
|
+
"state_before": self.state_before,
|
|
50
|
+
"state_after": self.state_after,
|
|
51
|
+
"diff": self.diff,
|
|
52
|
+
"duration_us": self.duration_us,
|
|
53
|
+
"timestamp": self.timestamp,
|
|
54
|
+
"status": self.status,
|
|
55
|
+
"error": self.error,
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
class Tracer:
|
|
60
|
+
def __init__(self, title: str = "Rewind Execution Trace"):
|
|
61
|
+
self.title = title
|
|
62
|
+
self.source_code = ""
|
|
63
|
+
self.source_filepath = ""
|
|
64
|
+
self.lock = threading.RLock()
|
|
65
|
+
self.steps: List[TraceStep] = []
|
|
66
|
+
self.state: Dict[str, Any] = {}
|
|
67
|
+
self._step_counter = 0
|
|
68
|
+
self.start_time = time.time()
|
|
69
|
+
|
|
70
|
+
def record_step_data(
|
|
71
|
+
self,
|
|
72
|
+
name: str,
|
|
73
|
+
inputs: Optional[Dict[str, Any]] = None,
|
|
74
|
+
metadata: Optional[Dict[str, Any]] = None,
|
|
75
|
+
state_updates: Optional[Dict[str, Any]] = None,
|
|
76
|
+
) -> TraceStep:
|
|
77
|
+
"""Manually records a single atomic step data record."""
|
|
78
|
+
with self.lock:
|
|
79
|
+
self._step_counter += 1
|
|
80
|
+
step_inputs = dict(inputs or {})
|
|
81
|
+
if self.source_code and "source_code" not in step_inputs:
|
|
82
|
+
step_inputs["source_code"] = self.source_code
|
|
83
|
+
if self.source_filepath and "filepath" not in step_inputs:
|
|
84
|
+
step_inputs["filepath"] = self.source_filepath
|
|
85
|
+
|
|
86
|
+
step_obj = TraceStep(
|
|
87
|
+
step_id=self._step_counter,
|
|
88
|
+
name=name,
|
|
89
|
+
caller_file=os.path.basename(self.source_filepath) if self.source_filepath else "cli_runner",
|
|
90
|
+
caller_line=0,
|
|
91
|
+
inputs=serialize_state(step_inputs),
|
|
92
|
+
)
|
|
93
|
+
step_obj.state_before = serialize_state(self.state)
|
|
94
|
+
if state_updates:
|
|
95
|
+
self.state.update(state_updates)
|
|
96
|
+
step_obj.state_after = serialize_state(self.state)
|
|
97
|
+
step_obj.diff = compute_state_diff(step_obj.state_before, step_obj.state_after)
|
|
98
|
+
step_obj.status = "SUCCESS"
|
|
99
|
+
self.steps.append(step_obj)
|
|
100
|
+
return step_obj
|
|
101
|
+
|
|
102
|
+
@contextmanager
|
|
103
|
+
def step(self, name: str, **metadata):
|
|
104
|
+
with self.lock:
|
|
105
|
+
self._step_counter += 1
|
|
106
|
+
curr_id = self._step_counter
|
|
107
|
+
|
|
108
|
+
frame = inspect.currentframe()
|
|
109
|
+
caller_frame = frame.f_back.f_back if frame and frame.f_back else None
|
|
110
|
+
caller_file = os.path.basename(caller_frame.f_code.co_filename) if caller_frame else "unknown"
|
|
111
|
+
caller_line = caller_frame.f_lineno if caller_frame else 0
|
|
112
|
+
|
|
113
|
+
step_inputs = dict(metadata)
|
|
114
|
+
if self.source_code and "source_code" not in step_inputs:
|
|
115
|
+
step_inputs["source_code"] = self.source_code
|
|
116
|
+
if self.source_filepath and "filepath" not in step_inputs:
|
|
117
|
+
step_inputs["filepath"] = self.source_filepath
|
|
118
|
+
|
|
119
|
+
step_obj = TraceStep(
|
|
120
|
+
step_id=curr_id,
|
|
121
|
+
name=name,
|
|
122
|
+
caller_file=caller_file,
|
|
123
|
+
caller_line=caller_line,
|
|
124
|
+
inputs=serialize_state(step_inputs),
|
|
125
|
+
)
|
|
126
|
+
step_obj.state_before = serialize_state(self.state)
|
|
127
|
+
|
|
128
|
+
start_t = time.perf_counter()
|
|
129
|
+
try:
|
|
130
|
+
yield self.state
|
|
131
|
+
step_obj.status = "SUCCESS"
|
|
132
|
+
except BaseException as e:
|
|
133
|
+
if isinstance(e, SystemExit) and (e.code == 0 or e.code is None):
|
|
134
|
+
step_obj.status = "SUCCESS"
|
|
135
|
+
else:
|
|
136
|
+
step_obj.status = "FAILED"
|
|
137
|
+
step_obj.error = {
|
|
138
|
+
"type": type(e).__name__,
|
|
139
|
+
"message": str(e),
|
|
140
|
+
"traceback": traceback.format_exc(),
|
|
141
|
+
"source_code": self.source_code,
|
|
142
|
+
"filepath": self.source_filepath,
|
|
143
|
+
}
|
|
144
|
+
raise
|
|
145
|
+
finally:
|
|
146
|
+
end_t = time.perf_counter()
|
|
147
|
+
step_obj.duration_us = round((end_t - start_t) * 1_000_000, 2)
|
|
148
|
+
with self.lock:
|
|
149
|
+
step_obj.state_after = serialize_state(self.state)
|
|
150
|
+
step_obj.diff = compute_state_diff(step_obj.state_before, step_obj.state_after)
|
|
151
|
+
self.steps.append(step_obj)
|
|
152
|
+
|
|
153
|
+
def to_dict(self) -> Dict[str, Any]:
|
|
154
|
+
"""Serializes the entire timeline into a portable dictionary."""
|
|
155
|
+
with self.lock:
|
|
156
|
+
has_error = any(s.status == "FAILED" for s in self.steps)
|
|
157
|
+
total_duration_ms = (time.time() - self.start_time) * 1000
|
|
158
|
+
|
|
159
|
+
return {
|
|
160
|
+
"schema_version": "1.0.0",
|
|
161
|
+
"title": self.title,
|
|
162
|
+
"source_code": self.source_code,
|
|
163
|
+
"source_filepath": self.source_filepath,
|
|
164
|
+
"created_at": time.strftime("%Y-%m-%d %H:%M:%S"),
|
|
165
|
+
"has_crash": has_error,
|
|
166
|
+
"total_steps": len(self.steps),
|
|
167
|
+
"total_duration_ms": round(total_duration_ms, 2),
|
|
168
|
+
"final_state": serialize_state(self.state),
|
|
169
|
+
"steps": [s.to_dict() for s in self.steps],
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
def export(self, filepath: str = "rewind_trace.json") -> str:
|
|
173
|
+
"""Saves the timeline trace to a JSON file (creating parent directories automatically)."""
|
|
174
|
+
data = self.to_dict()
|
|
175
|
+
dir_name = os.path.dirname(filepath)
|
|
176
|
+
if dir_name:
|
|
177
|
+
os.makedirs(dir_name, exist_ok=True)
|
|
178
|
+
with open(filepath, "w", encoding="utf-8") as f:
|
|
179
|
+
json.dump(data, f, indent=2)
|
|
180
|
+
return filepath
|
|
181
|
+
|
|
182
|
+
|
|
183
|
+
# Global Singleton
|
|
184
|
+
_GLOBAL_TRACER = Tracer()
|
|
185
|
+
|
|
186
|
+
|
|
187
|
+
def get_global_tracer() -> Tracer:
|
|
188
|
+
return _GLOBAL_TRACER
|
|
189
|
+
|
|
190
|
+
|
|
191
|
+
def step(name: str, **metadata):
|
|
192
|
+
return _GLOBAL_TRACER.step(name=name, **metadata)
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
rewind/__init__.py,sha256=a3pB_uTEA2M9YWJHA_VzgcyG4beqUaj6hmLfHCLkboI,289
|
|
2
|
+
rewind/cli.py,sha256=hYKfUkBibTFeSGuYqjrcRKRewebp8laccltggimBbMU,21702
|
|
3
|
+
rewind/diff.py,sha256=xuV-4QLwq0zgkjKaLo8JnUmHNWEUGKN1M9bvrYGPZAs,3962
|
|
4
|
+
rewind/tracer.py,sha256=aB-QaVZ9_nx-cgokZtKluDO5EQWd2cmJizGYN6Cqooc,6984
|
|
5
|
+
rewind_debug-0.1.0.dist-info/licenses/LICENSE,sha256=ULrPVcPD4AeHfYVwvl5cbZVHLMvx8bOi5jCXRDIIhZk,1064
|
|
6
|
+
rewind_debug-0.1.0.dist-info/METADATA,sha256=lYSD3tMGoHqNlZudxR3By5lM9YgpEV-jYUr6TJu11F8,100
|
|
7
|
+
rewind_debug-0.1.0.dist-info/WHEEL,sha256=YVMoNqKzERt-wjUZwJ33xBGAwnFl-4cqbYkTtWa4itE,91
|
|
8
|
+
rewind_debug-0.1.0.dist-info/entry_points.txt,sha256=ObbXyL_jYLu8AuXi-7AA7A4u1prBqGnt_jdiRz84tQs,43
|
|
9
|
+
rewind_debug-0.1.0.dist-info/top_level.txt,sha256=aBIzAIcMbzZQLT7Z6DVBnoLkTZbAxm_gSsLGaionfVs,7
|
|
10
|
+
rewind_debug-0.1.0.dist-info/RECORD,,
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Hrinkar
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
rewind
|