retracesoftware-proxy 0.1.21__py3-none-any.whl → 0.2.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.
@@ -16,16 +16,21 @@ class StubRef:
16
16
  def __init__(self, cls):
17
17
  blacklist = ['__class__', '__dict__', '__module__', '__doc__', '__new__']
18
18
 
19
- methods = []
19
+ self.methods = []
20
+ self.static_methods = []
21
+ self.class_methods = []
20
22
 
21
23
  for key,value in cls.__dict__.items():
22
24
  if key not in blacklist:
23
- if utils.is_method_descriptor(value):
24
- methods.append(key)
25
+ if isinstance(value, classmethod):
26
+ self.class_methods.append(key)
27
+ elif isinstance(value, staticmethod):
28
+ self.class_methods.append(key)
29
+ elif utils.is_method_descriptor(value):
30
+ self.methods.append(key)
25
31
 
26
32
  self.name = cls.__name__
27
33
  self.module = cls.__module__
28
- self.methods = methods
29
34
 
30
35
  # def __init__(self, module, name, methods, members):
31
36
  # self.name = name
@@ -47,6 +52,11 @@ class StubMethodDescriptor(functional.repeatedly):
47
52
  super().__init__(next_result)
48
53
  self.__name__ = name
49
54
 
55
+ # @utils.striptraceback
56
+ # def __call__(self, *args, **kwargs):
57
+ # super().__call__(*args, **kwargs)
58
+
59
+ #@utils.striptraceback
50
60
  def __str__(self):
51
61
  return f"stub - {__name__}"
52
62
 
@@ -55,29 +65,25 @@ class StubMemberDescriptor:
55
65
  self.next_result = next_result
56
66
  self.__name__ = name
57
67
 
68
+ #@utils.striptraceback
58
69
  def __get__(self, instance, owner):
59
70
  if instance is None:
60
71
  return self
61
72
 
62
73
  return self.next_result()
63
74
 
75
+ #@utils.striptraceback
64
76
  def __set__(self, instance, value):
65
77
  return self.next_result()
66
78
 
79
+ #@utils.striptraceback
67
80
  def __delete__(self, instance):
68
81
  return self.next_result()
69
82
 
83
+ #@utils.striptraceback
70
84
  def __str__(self):
71
85
  return f"stub member - {__name__}"
72
86
 
73
- class StubFunction(functional.repeatedly):
74
- def __init__(self, name, next_result):
75
- super().__init__(next_result)
76
- self.__name__ = name
77
-
78
- def __str__(self):
79
- return f"stub function - {__name__}"
80
-
81
87
  class StubFactory:
82
88
 
83
89
  __slots__ = ['next_result', 'thread_state', 'cache']
@@ -106,9 +112,14 @@ class StubFactory:
106
112
  if self.thread_state.value == 'disabled' and name == '__repr__':
107
113
  return f"stub - {name}"
108
114
  else:
109
- print(f'Error trying to call descriptor: {name} {args} {kwargs}, retrace mode: {self.thread_state.value}')
110
- utils.sigtrap(None)
111
- os._exit(1)
115
+ return None
116
+ # print(f'Error trying to call descriptor: {name} {args} {kwargs}, retrace mode: {self.thread_state.value}')
117
+
118
+ # import traceback
119
+ # traceback.print_stack()
120
+
121
+ # utils.sigtrap(None)
122
+ # os._exit(1)
112
123
 
113
124
  next_result = self.thread_state.dispatch(disabled, external = self.next_result)
114
125
 
@@ -119,6 +130,8 @@ class StubFactory:
119
130
 
120
131
  def create_stubtype(self, spec):
121
132
 
133
+ assert self.thread_state.value == 'disabled'
134
+
122
135
  slots = {
123
136
  '__module__': spec.module,
124
137
  '__qualname__': spec.name,
@@ -129,7 +142,14 @@ class StubFactory:
129
142
  slots[method] = self.create_method(method)
130
143
  assert utils.is_method_descriptor(slots[method])
131
144
 
145
+ def on_disabled(name):
146
+ print(f'Error trying to get/set attribute: {name}, when retrace mode: {self.thread_state.value} was not external')
147
+ utils.sigtrap(None)
148
+ os._exit(1)
149
+
150
+ #@utils.striptraceback
132
151
  def getattr(instance, name):
152
+ print('In stub getattr!!!')
133
153
  if self.thread_state.value == 'external':
134
154
  return self.next_result()
135
155
  else:
@@ -137,6 +157,7 @@ class StubFactory:
137
157
  utils.sigtrap(None)
138
158
  os._exit(1)
139
159
 
160
+ #@utils.striptraceback
140
161
  def setattr(instance, name, value):
141
162
  if self.thread_state.value == 'external':
142
163
  return self.next_result()
@@ -145,13 +166,15 @@ class StubFactory:
145
166
  utils.sigtrap(None)
146
167
  os._exit(1)
147
168
 
148
- slots['__getattr__'] = getattr
149
- slots['__setattr__'] = setattr
169
+ slots['__getattr__'] = self.thread_state.method_dispatch(on_disabled, external = self.next_result)
170
+ # slots['__getattr__'] = getattr
171
+ slots['__setattr__'] = self.thread_state.method_dispatch(on_disabled, external = self.next_result)
150
172
 
151
173
  resolved = resolve(spec.module, spec.name)
152
174
 
153
- if isinstance(resolved, type):
154
- slots['__class__'] = property(functional.repeatedly(resolved))
175
+ # if isinstance(resolved, type):
176
+ # slots['__class__'] = property(functional.repeatedly(resolved))
177
+
155
178
  # else:
156
179
  # utils.sigtrap(f'{spec.module}.{spec.name}')
157
180
 
@@ -0,0 +1,104 @@
1
+ import retracesoftware.stream as stream
2
+ from retracesoftware.run import install, run_with_retrace, ImmutableTypes, thread_states
3
+ import argparse
4
+ import sys
5
+ from pathlib import Path
6
+ from retracesoftware.proxy.thread import thread_id
7
+ from retracesoftware.proxy.replay import ReplayProxySystem
8
+ import retracesoftware.utils as utils
9
+ import json
10
+ from retracesoftware.stackdifference import on_stack_difference
11
+
12
+ def parse_args():
13
+ parser = argparse.ArgumentParser(
14
+ prog=f"python -m {sys.argv[0]}",
15
+ description="Run a Python module with debugging, logging, etc."
16
+ )
17
+
18
+ parser.add_argument(
19
+ '--verbose',
20
+ action='store_true',
21
+ help='Enable verbose output'
22
+ )
23
+
24
+ parser.add_argument(
25
+ '--timeout', # or '-r'
26
+ type = int, # ensures it's a string (optional, but safe)
27
+ default = 60, # default value if not provided
28
+ help = 'the directory to place the recording files'
29
+ )
30
+
31
+ parser.add_argument(
32
+ '--recording', # or '-r'
33
+ type = str, # ensures it's a string (optional, but safe)
34
+ default = '.', # default value if not provided
35
+ help = 'the directory to place the recording files'
36
+ )
37
+
38
+ return parser.parse_args()
39
+
40
+ def load_json(file):
41
+ with open(file, "r", encoding="utf-8") as f:
42
+ return json.load(f)
43
+
44
+ def main():
45
+ # import debugpy
46
+
47
+ # port = 1977
48
+ # debugpy.listen(("127.0.0.1", port))
49
+ # debugpy.wait_for_client()
50
+
51
+ # self.reader = stream.reader(path,
52
+ # thread = thread_id,
53
+ # timeout_seconds = 60,
54
+ # verbose = verbose,
55
+ # on_stack_difference = thread_state.wrap('disabled', on_stack_difference),
56
+ # magic_markers = magic_markers)
57
+
58
+ # return ReplayProxySystem(thread_state = thread_state,
59
+ # immutable_types = immutable_types,
60
+ # tracing_config = tracing_config,
61
+ # mainscript = mainscript,
62
+ # path = recording_path / 'trace.bin',
63
+ # tracecalls = env_truthy('RETRACE_ALL', False),
64
+ # verbose = verbose,
65
+ # magic_markers = env_truthy('RETRACE_MAGIC_MARKERS', False))
66
+
67
+ args = parse_args()
68
+ path = Path(args.recording)
69
+
70
+ if not path.exists():
71
+ raise Exception(f"Recording path: {path} does not exist")
72
+
73
+ settings = load_json(path / "settings.json")
74
+
75
+ thread_state = utils.ThreadState(*thread_states)
76
+
77
+ with stream.reader(path = path / 'trace.bin',
78
+ thread = thread_id,
79
+ timeout_seconds = args.timeout,
80
+ verbose = args.verbose,
81
+ on_stack_difference = thread_state.wrap('disabled', on_stack_difference),
82
+ magic_markers = settings['magic_markers']) as reader:
83
+
84
+ tracing_config = {}
85
+
86
+ system = ReplayProxySystem(
87
+ reader = reader,
88
+ thread_state = thread_state,
89
+ immutable_types = ImmutableTypes(),
90
+ tracing_config = tracing_config,
91
+ tracecalls = settings['trace_inputs'])
92
+
93
+ install(system)
94
+
95
+ run_with_retrace(system, settings['argv'])
96
+
97
+ # install(system)
98
+
99
+ # run_with_retrace(system, args.rest[1:])
100
+
101
+ # runpy.run_module('foo', run_name="__main__", alter_sys=False)
102
+
103
+ if __name__ == "__main__":
104
+ main()
retracesoftware/run.py ADDED
@@ -0,0 +1,373 @@
1
+ import sys
2
+ import os
3
+ import runpy
4
+ from pathlib import Path
5
+ # from retracesoftware.install.phases import *
6
+ import pkgutil
7
+ import tomllib
8
+ import retracesoftware.functional as functional
9
+ import builtins
10
+ import importlib
11
+ import _imp
12
+ import importlib._bootstrap_external as _bootstrap_external
13
+ import atexit
14
+ import threading
15
+ import _signal
16
+ import retracesoftware.utils as utils
17
+ from retracesoftware.install.patcher import patch_module, create_patcher, patch_imported_module
18
+ from retracesoftware.proxy.startthread import patch_thread_start
19
+ from retracesoftware.install.replace import update
20
+
21
+ thread_states = [
22
+ "disabled", # Default state when retrace is disabled for a thread
23
+ "internal", # Default state when retrace is disabled for a thread
24
+ "external", # When target thread is running outside the python system to be recorded
25
+ "retrace", # When target thread is running outside the retrace system
26
+ "importing", # When target thread is running outside the retrace system
27
+ "gc", # When the target thread is running inside the pyton garbage collector
28
+ ]
29
+
30
+ class ImmutableTypes(set):
31
+ def __init__(self, *args, **kwargs):
32
+ super().__init__(*args, **kwargs)
33
+
34
+ def __contains__(self, item):
35
+ assert isinstance(item, type)
36
+
37
+ if super().__contains__(item):
38
+ return True
39
+
40
+ for elem in self:
41
+ if issubclass(item, elem):
42
+ self.add(item)
43
+ return True
44
+
45
+ return False
46
+
47
+ def load_module_config(filename):
48
+ data = pkgutil.get_data("retracesoftware", filename)
49
+ assert data is not None
50
+ return tomllib.loads(data.decode("utf-8"))
51
+
52
+ def wait_for_non_daemon_threads(timeout=None):
53
+ """
54
+ Wait for all non-daemon threads to finish, just like Python does on exit.
55
+
56
+ Args:
57
+ timeout (float, optional): Max seconds to wait. None = wait forever.
58
+
59
+ Returns:
60
+ bool: True if all threads finished, False if timeout exceeded.
61
+ """
62
+ import threading
63
+ import time
64
+
65
+ start_time = time.time()
66
+ main_thread = threading.main_thread()
67
+
68
+ while True:
69
+ # Get all active threads
70
+ active = threading.enumerate()
71
+
72
+ # Filter: non-daemon and not the main thread
73
+ non_daemon_threads = [
74
+ t for t in active
75
+ if t is not main_thread and not t.daemon
76
+ ]
77
+
78
+ if not non_daemon_threads:
79
+ return True # All done!
80
+
81
+ # Check timeout
82
+ if timeout is not None:
83
+ elapsed = time.time() - start_time
84
+ if elapsed >= timeout:
85
+ print(f"Timeout: {len(non_daemon_threads)} thread(s) still alive")
86
+ return False
87
+
88
+ # Sleep briefly to avoid busy-wait
89
+ time.sleep(0.1)
90
+
91
+ def run_python_command(argv):
92
+ """
93
+ Run a Python app from a command list using runpy.
94
+
95
+ Supports:
96
+ ['-m', 'module', 'arg1', ...] → like `python -m module arg1 ...`
97
+ ['script.py', 'arg1', ...] → like `python script.py arg1 ...`
98
+
99
+ Args:
100
+ argv: List of command-line arguments (first item is either '-m' or script path)
101
+
102
+ Returns:
103
+ Exit code (0 on success, 1+ on error)
104
+ """
105
+ if not argv:
106
+ print("Error: No command provided", file=sys.stderr)
107
+ return 1
108
+
109
+ original_argv = sys.argv[:]
110
+ original_cwd = os.getcwd()
111
+
112
+ try:
113
+ if argv[0] == '-m':
114
+ if len(argv) < 2:
115
+ print("Error: -m requires a module name", file=sys.stderr)
116
+ return 1
117
+ module_name = argv[1]
118
+ module_args = argv[2:]
119
+ sys.argv = ['-m', module_name] + module_args
120
+ runpy.run_module(module_name, run_name="__main__")
121
+ return 0
122
+
123
+ else:
124
+ script_path = argv[0]
125
+ script_args = argv[1:]
126
+ path = Path(script_path)
127
+
128
+ if not path.exists():
129
+ print(f"Error: Script not found: {script_path}", file=sys.stderr)
130
+ return 1
131
+
132
+ if path.suffix != ".py":
133
+ print(f"Error: Not a Python script: {script_path}", file=sys.stderr)
134
+ return 1
135
+
136
+ full_path = str(path.resolve())
137
+ sys.argv = [full_path] + script_args
138
+
139
+ # Change to script's directory
140
+ os.chdir(path.parent)
141
+
142
+ runpy.run_path(full_path, run_name="__main__")
143
+ return 0
144
+
145
+ except ModuleNotFoundError:
146
+ print(f"Error: No module named '{argv[1]}'", file=sys.stderr)
147
+ return 1
148
+ except Exception as e:
149
+ print(f"Error: {e}", file=sys.stderr)
150
+ return 1
151
+ finally:
152
+ sys.argv = original_argv
153
+ os.chdir(original_cwd)
154
+
155
+ def patch_import(thread_state, patcher, sync, checkpoint):
156
+ # builtins.__import__ = thread_state.dispatch(builtins.__import__, internal = sync(thread_state.wrap('importing', builtins.__import__)))
157
+ # bi = builtins.__import__
158
+ # def foo(*args, **kwargs):
159
+ # print(f'in patched __import__: {thread_state.value} {args[0]}')
160
+ # if thread_state.value == 'internal':
161
+ # with thread_state.select('importing'):
162
+ # return bi(*args, **kwargs)
163
+ # else:
164
+ # return bi(*args, **kwargs)
165
+
166
+ # builtins.__import__ = foo
167
+
168
+ builtins.__import__ = thread_state.dispatch(builtins.__import__, internal = thread_state.wrap('importing', builtins.__import__))
169
+
170
+ def exec(source, globals = None, locals = None):
171
+ checkpoint(f"exec module: {globals.get('__name__', 'unknown')}")
172
+ res = builtins.exec(source, globals, locals)
173
+ patcher(globals, False)
174
+ return res
175
+
176
+ def patch(module):
177
+ patcher(module.__dict__, False)
178
+ return module
179
+
180
+ _imp.exec_dynamic = thread_state.dispatch(_imp.exec_dynamic,
181
+ importing = functional.juxt(
182
+ thread_state.wrap('internal', _imp.exec_dynamic),
183
+ patch))
184
+ # _imp.exec_dynamic = lambda mod: utils.sigtrap(f'{thread_state.value} - {mod}')
185
+ # _imp.exec_builtin = lambda mod: utils.sigtrap(f'{thread_state.value} - {mod}')
186
+
187
+ _imp.exec_builtin = thread_state.dispatch(_imp.exec_builtin,
188
+ importing = functional.juxt(
189
+ thread_state.wrap('internal', _imp.exec_builtin),
190
+ patch))
191
+
192
+ # def runpy_exec(source, globals = None, locals = None):
193
+ # print(f'In runpy exec!!!!!')
194
+ # return builtins.exec(source, globals, locals)
195
+
196
+ # utils.update(runpy, "_run_code",
197
+ # utils.wrap_func_with_overrides,
198
+ # exec = runpy_exec)
199
+
200
+ utils.update(_bootstrap_external._LoaderBasics, "exec_module",
201
+ utils.wrap_func_with_overrides,
202
+ exec = thread_state.dispatch(builtins.exec, importing = thread_state.wrap('internal', sync(exec))))
203
+
204
+ preload = [
205
+ "logging",
206
+ "pathlib",
207
+ "_signal",
208
+ "_posixsubprocess",
209
+ "socket",
210
+ "select",
211
+ "ssl",
212
+ "random",
213
+ "email",
214
+ "email.errors",
215
+ "http.client",
216
+
217
+ "json",
218
+ "typing",
219
+ "queue",
220
+ "mimetypes",
221
+ "tempfile",
222
+ "zipfile",
223
+ "importlib.resources",
224
+ "importlib.metadata",
225
+ "encodings.idna"
226
+
227
+ # "http.client",
228
+ # "queue",
229
+ # "mimetypes",
230
+ # "encodings.idna",
231
+ # "hmac",
232
+ # "ipaddress",
233
+ # "tempfile",
234
+ # "zipfile",
235
+ # "importlib.resources",
236
+ # "importlib.metadata",
237
+ # "atexit",
238
+ # "weakref"
239
+ ]
240
+
241
+ # def debugger_is_active():
242
+ # return sys.gettrace() and 'debugpy' in sys.modules
243
+
244
+ def init_weakref():
245
+ import weakref
246
+ def dummy_callback():
247
+ pass
248
+
249
+ class DummyTarget:
250
+ pass
251
+
252
+ f = weakref.finalize(DummyTarget(), dummy_callback)
253
+ f.detach()
254
+
255
+ def wrapped_weakref(ref, thread_state, wrap_callback):
256
+ orig_new = ref.__new__
257
+
258
+ def __new__(cls, ob, callback=None, **kwargs):
259
+ return orig_new(cls, ob, wrap_callback(callback) if callback else None)
260
+
261
+ return type('ref', (ref, ), {'__new__': thread_state.dispatch(orig_new, internal = __new__)})
262
+
263
+ def patch_weakref(thread_state, wrap_callback):
264
+ import _weakref
265
+
266
+ update(_weakref.ref, wrapped_weakref(_weakref.ref, thread_state, wrap_callback))
267
+ # _weakref.ref = wrapped_weakref(_weakref.ref)
268
+
269
+ # def patch_signal(thread_state, wrap_callback):
270
+
271
+ # def wrap_handler(handler):
272
+ # return utils.observer(on_call = ..., function = handler)
273
+
274
+ # _signal.signal =
275
+ # update(_signal.signal, thread_state.dispatch(_signal.signal, internal = thread_state.wrap('internal', _signal.signal)))
276
+
277
+
278
+
279
+ def install(system):
280
+
281
+ patch_weakref(thread_state = system.thread_state, wrap_callback = system.wrap_weakref_callback)
282
+
283
+ init_weakref()
284
+
285
+ for name in preload:
286
+ importlib.import_module(name)
287
+
288
+ # if 'pydevd' in sys.modules:
289
+ # utils.update(sys.modules['pydevd'].PyDB, 'enable_tracing', system.disable_for)
290
+ # utils.update(sys.modules['pydevd'].PyDB, 'set_suspend', system.disable_for)
291
+ # utils.update(sys.modules['pydevd'].PyDB, 'do_wait_suspend', system.disable_for)
292
+
293
+ # if '_pydevd_bundle.pydevd_trace_dispatch_regular' in sys.modules:
294
+ # mod = sys.modules['_pydevd_bundle.pydevd_trace_dispatch_regular']
295
+ # utils.update(mod.ThreadTracer, '__call__', system.disable_for)
296
+
297
+ for function in utils.stack_functions():
298
+ system.exclude_from_stacktrace(function)
299
+
300
+ def recursive_disable(func):
301
+ if not callable(func):
302
+ return func
303
+
304
+ def wrapped(*args, **kwargs):
305
+ with system.thread_state.select('disabled'):
306
+ return recursive_disable(func(*args, **kwargs))
307
+
308
+ return wrapped
309
+
310
+ sys.settrace = functional.sequence(recursive_disable, sys.settrace)
311
+ sys.setprofile = functional.sequence(recursive_disable, sys.setprofile)
312
+ threading.settrace = functional.sequence(recursive_disable, threading.settrace)
313
+
314
+ sys.settrace(sys.gettrace())
315
+ sys.setprofile(sys.getprofile())
316
+
317
+ system.checkpoint('About to install retrace system')
318
+
319
+ module_config = load_module_config('modules.toml')
320
+
321
+ patch_loaded = functional.partial(patch_module, create_patcher(system), module_config)
322
+ patch_imported = functional.partial(patch_imported_module, create_patcher(system), system.checkpoint, module_config)
323
+
324
+ system.checkpoint('Started installing system 1')
325
+
326
+ for modname in module_config.keys():
327
+ if modname in sys.modules:
328
+ patch_loaded(sys.modules[modname].__dict__, True)
329
+
330
+ system.checkpoint('About to patch threading')
331
+
332
+ patch_thread_start(system.thread_state)
333
+ threading.current_thread().__retrace__ = system
334
+
335
+ system.checkpoint('About to patch import')
336
+ patch_import(thread_state = system.thread_state,
337
+ patcher = patch_imported,
338
+ sync = system.sync,
339
+ checkpoint = system.checkpoint)
340
+
341
+ # print(f'MODULES: {list(sys.modules.keys())}')
342
+
343
+ importlib.import_module = \
344
+ system.thread_state.dispatch(system.disable_for(importlib.import_module),
345
+ internal = system.thread_state.wrap('importing', importlib.import_module))
346
+
347
+
348
+ system.checkpoint('About to patch preload libraries')
349
+
350
+ system.checkpoint('system patched...')
351
+
352
+ def run_with_retrace(system, argv, trace_shutdown = False):
353
+
354
+ def runpy_exec(source, globals = None, locals = None):
355
+ with system.thread_state.select('internal'):
356
+ return builtins.exec(source, globals, locals)
357
+
358
+ utils.update(runpy, "_run_code",
359
+ utils.wrap_func_with_overrides,
360
+ exec = runpy_exec)
361
+
362
+ try:
363
+ run_python_command(argv)
364
+ finally:
365
+ wait_for_non_daemon_threads()
366
+ try:
367
+ if trace_shutdown:
368
+ with system.thread_state.select('internal'):
369
+ atexit._run_exitfuncs()
370
+ else:
371
+ atexit._run_exitfuncs()
372
+ except Exception as e:
373
+ print(f"Error in atexit hook: {e}", file=sys.stderr)