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.
@@ -0,0 +1,266 @@
1
+ import sys
2
+ import runpy
3
+ import os
4
+ import argparse
5
+ from typing import Tuple, List
6
+ import retracesoftware.utils as utils
7
+ import retracesoftware.functional as functional
8
+ from retracesoftware.stackdifference import on_stack_difference
9
+ from pathlib import Path
10
+ from retracesoftware.proxy.record import RecordProxySystem
11
+ from retracesoftware.proxy.replay import ReplayProxySystem
12
+ import retracesoftware.stream as stream
13
+ from retracesoftware.proxy.startthread import thread_id
14
+ import datetime
15
+ import json
16
+ from shutil import copy2
17
+ import threading
18
+ import time
19
+ import gc
20
+ import hashlib
21
+
22
+ from retracesoftware.run import install, run_with_retrace, ImmutableTypes, thread_states
23
+
24
+ def expand_recording_path(path):
25
+ return datetime.datetime.now().strftime(path.format(pid = os.getpid()))
26
+
27
+ def load_json(file):
28
+ with open(file, "r", encoding="utf-8") as f:
29
+ return json.load(f)
30
+
31
+ def dump_as_json(path, obj):
32
+ with open(path, 'w') as f:
33
+ json.dump(obj, f, indent=2)
34
+
35
+ vscode_workspace = {
36
+ "folders": [{ 'path': '.' }],
37
+ "settings": {
38
+ "python.defaultInterpreterPath": sys.executable,
39
+ },
40
+ "launch": {
41
+ "version": "0.2.0",
42
+ "configurations": [{
43
+ "name": "replay",
44
+ "type": "debugpy",
45
+ "request": "launch",
46
+
47
+ "python": sys.executable,
48
+ "module": "retracesoftware",
49
+ "args": [
50
+ "--recording", "..",
51
+ "--skip_weakref_callbacks",
52
+ "--read_timeout", "1000"
53
+ ],
54
+
55
+ "cwd": "${workspaceFolder}/run",
56
+ "console": "integratedTerminal",
57
+ "justMyCode": False
58
+ }]
59
+ },
60
+ }
61
+
62
+ def scriptname(argv):
63
+ return argv[1] if argv[0] == "-m" else argv[0]
64
+
65
+ def collector(multiplier):
66
+ collect_gen = utils.CollectPred(multiplier = multiplier)
67
+
68
+ return functional.lazy(functional.sequence(collect_gen, functional.when_not_none(gc.collect)))
69
+
70
+ def file_md5(path):
71
+ return hashlib.md5(path.read_bytes()).hexdigest()
72
+
73
+ def checksum(path):
74
+ return file_md5(path) if path.is_file() else {entry.name: checksum(entry) for entry in path.iterdir() if entry.name != '__pycache__'}
75
+
76
+ def retrace_extension_paths():
77
+ names = ['retracesoftware_utils', 'retracesoftware_functional', 'retracesoftware_stream']
78
+ return {name: Path(sys.modules[name].__file__) for name in names}
79
+
80
+ def retrace_module_paths():
81
+ paths = retrace_extension_paths()
82
+ paths['retracesoftware'] = Path(sys.modules['retracesoftware'].__file__).parent
83
+ return paths
84
+
85
+ def checksums():
86
+ return {name: checksum(path) for name, path in retrace_module_paths().items()}
87
+
88
+ def record(options, args):
89
+
90
+ path = Path(expand_recording_path(options.recording))
91
+ # ensure the path exists
92
+ path.mkdir(parents=True, exist_ok=True)
93
+
94
+ # write various recording files to directory
95
+ dump_as_json(path / 'settings.json', {
96
+ 'argv': args,
97
+ 'magic_markers': options.magic_markers,
98
+ 'trace_inputs': options.trace_inputs,
99
+ 'trace_shutdown': options.trace_shutdown,
100
+ 'env': dict(os.environ),
101
+ 'python_version': sys.version,
102
+ 'md5_checksums': checksums(),
103
+ })
104
+
105
+ rundir = path / 'run'
106
+ rundir.mkdir(exist_ok=True)
107
+
108
+ script = Path(scriptname(args))
109
+ if script.exists():
110
+ copy2(script, rundir)
111
+
112
+ dump_as_json(path / 'replay.code-workspace', vscode_workspace)
113
+
114
+ with stream.writer(path = path / 'trace.bin',
115
+ thread = thread_id,
116
+ verbose = options.verbose,
117
+ stacktraces = options.stacktraces,
118
+ magic_markers = options.magic_markers) as writer:
119
+
120
+ # flusher = threading.Timer(5.0, periodic_task)
121
+ # flusher.start()
122
+
123
+ writer.exclude_from_stacktrace(record)
124
+ writer.exclude_from_stacktrace(main)
125
+
126
+ thread_state = utils.ThreadState(*thread_states)
127
+
128
+ tracing_config = {}
129
+
130
+ multiplier = 2
131
+ gc.set_threshold(*map(lambda x: x * multiplier, gc.get_threshold()))
132
+
133
+ system = RecordProxySystem(
134
+ writer = writer,
135
+ thread_state = thread_state,
136
+ immutable_types = ImmutableTypes(),
137
+ tracing_config = tracing_config,
138
+ maybe_collect = collector(multiplier = multiplier),
139
+ traceargs = options.trace_inputs)
140
+
141
+ # force a full collection
142
+ install(system)
143
+
144
+ gc.collect()
145
+ gc.callbacks.append(system.on_gc_event)
146
+
147
+ run_with_retrace(system, args, options.trace_shutdown)
148
+
149
+ gc.callbacks.remove(system.on_gc_event)
150
+
151
+ def replay(args):
152
+ path = Path(args.recording)
153
+
154
+ if not path.exists():
155
+ raise Exception(f"Recording path: {path} does not exist")
156
+
157
+ settings = load_json(path / "settings.json")
158
+
159
+ if settings['md5_checksums'] != checksums():
160
+ raise Exception("Checksums for Retrace do not match, cannot run replay with different version of retrace to record")
161
+
162
+ if settings['python_version'] != sys.version:
163
+ raise Exception("Python version does not match, cannot run replay with different version of Python to record")
164
+
165
+ os.environ.update(settings['env'])
166
+
167
+ thread_state = utils.ThreadState(*thread_states)
168
+
169
+ # with stream.reader(path = path / 'trace.bin',
170
+ # thread = thread_id,
171
+ # timeout_seconds = args.timeout,
172
+ # verbose = args.verbose,
173
+ # on_stack_difference = thread_state.wrap('disabled', on_stack_difference),
174
+ # magic_markers = settings['magic_markers']) as reader:
175
+
176
+ with stream.reader1(path = path / 'trace.bin',
177
+ read_timeout = args.read_timeout,
178
+ verbose = args.verbose,
179
+ magic_markers = settings['magic_markers']) as reader:
180
+
181
+ tracing_config = {}
182
+
183
+ system = ReplayProxySystem(
184
+ reader = reader,
185
+ thread_state = thread_state,
186
+ immutable_types = ImmutableTypes(),
187
+ tracing_config = tracing_config,
188
+ traceargs = settings['trace_inputs'],
189
+ verbose = args.verbose,
190
+ skip_weakref_callbacks = args.skip_weakref_callbacks)
191
+
192
+ install(system)
193
+
194
+ gc.collect()
195
+ gc.disable()
196
+
197
+ run_with_retrace(system, settings['argv'], settings['trace_shutdown'])
198
+
199
+ def main():
200
+ parser = argparse.ArgumentParser(
201
+ prog="python -m retracesoftware",
202
+ description="Run a Python module with debugging, logging, etc."
203
+ )
204
+
205
+ parser.add_argument(
206
+ '--verbose',
207
+ action='store_true',
208
+ help='Enable verbose output'
209
+ )
210
+
211
+ parser.add_argument(
212
+ '--recording', # or '-r'
213
+ type = str, # ensures it's a string (optional, but safe)
214
+ default = '.', # default value if not provided
215
+ help = 'the directory to place the recording files'
216
+ )
217
+
218
+ if '--' in sys.argv:
219
+ parser.add_argument(
220
+ '--stacktraces',
221
+ action='store_true',
222
+ help='Capture stacktrace for every event'
223
+ )
224
+
225
+ parser.add_argument(
226
+ '--magic_markers',
227
+ action='store_true',
228
+ help='Write magic markers to tracefile, used for debugging'
229
+ )
230
+
231
+ parser.add_argument(
232
+ '--trace_shutdown',
233
+ action='store_true',
234
+ help='Whether to trace system shutdown and cleanup hooks'
235
+ )
236
+
237
+ parser.add_argument(
238
+ '--trace_inputs',
239
+ action='store_true',
240
+ help='Whether to write call parameters, used for debugging'
241
+ )
242
+
243
+ parser.add_argument('rest', nargs = argparse.REMAINDER, help='target application and arguments')
244
+
245
+ args = parser.parse_args()
246
+
247
+ record(args, args.rest[1:])
248
+ else:
249
+
250
+ parser.add_argument(
251
+ '--skip_weakref_callbacks',
252
+ action='store_true',
253
+ help = 'whether to disable retrace in weakref callbacks on replay'
254
+ )
255
+
256
+ parser.add_argument(
257
+ '--read_timeout', # or '-r'
258
+ type = int, # ensures it's a string (optional, but safe)
259
+ default = 1000, # default value if not provided
260
+ help = 'timeout in millseconds for incomplete read of element to timeout'
261
+ )
262
+
263
+ replay(parser.parse_args())
264
+
265
+ if __name__ == "__main__":
266
+ main()
@@ -0,0 +1,53 @@
1
+ if __name__ == "__main__":
2
+ import sysconfig
3
+ import pathlib
4
+
5
+ # 'purelib' is the name of the directory where non-platform-specific modules are installed.
6
+ file = pathlib.Path(sysconfig.get_paths()["purelib"]) / 'retrace.pth'
7
+ file.write_text('import retracesoftware.autoenable;', encoding='utf-8')
8
+
9
+ print(f'Retrace autoinstall enabled by creating: {file}')
10
+ else:
11
+ import os
12
+
13
+ def is_true(name):
14
+ if name in os.environ:
15
+ return os.environ[name].lower() in {'true', '1', 't', 'y', 'yes'}
16
+ else:
17
+ return False
18
+
19
+ def is_running_retrace():
20
+ return sys.orig_argv[1] == '-m' and sys.orig_argv[2].startswith('retracesoftware')
21
+
22
+ # only do anything is the RETRACE env variable is set
23
+ if is_true('RETRACE'):
24
+ import sys
25
+
26
+ if not is_running_retrace():
27
+
28
+ new_argv = [sys.orig_argv[0], '-m', 'retracesoftware']
29
+
30
+ if is_true('RETRACE_VERBOSE'):
31
+ new_argv.append('--verbose')
32
+
33
+ if 'RETRACE_RECORDING_PATH' in os.environ:
34
+ new_argv.append('--recording')
35
+ new_argv.append(os.environ['RETRACE_RECORDING_PATH'])
36
+
37
+ if is_true('RETRACE_STACKTRACES'):
38
+ new_argv.append('--stacktraces')
39
+
40
+ if is_true('RETRACE_SHUTDOWN'):
41
+ new_argv.append('--trace_shutdown')
42
+
43
+ if is_true('RETRACE_MAGIC_MARKERS'):
44
+ new_argv.append('--magic_markers')
45
+
46
+ if is_true('RETRACE_TRACE_INPUTS'):
47
+ new_argv.append('--trace_inputs')
48
+
49
+ new_argv.append('--')
50
+ new_argv.extend(sys.orig_argv[1:])
51
+
52
+ # print(f'Running: {new_argv}')
53
+ os.execv(sys.executable, new_argv)
@@ -4,15 +4,18 @@
4
4
  "internal", {"comment": "Default state when retrace is disabled for a thread"},
5
5
  "external", {"comment": "When target thread is running outside the python system to be recorded"},
6
6
  "retrace", {"comment": "When target thread is running outside the retrace system"},
7
+ "importing", {"comment": "When target thread is running outside the retrace system"},
7
8
  "gc", {"comment": "When the target thread is running inside the pyton garbage collector"}
8
9
  ],
9
10
 
11
+ "trace_calls": false,
12
+
10
13
  "recording_path": "recordings/%Y%m%d_%H%M%S_%f",
11
14
 
12
15
  "tracing_levels": {
13
16
  "none": [],
14
17
  "all": [
15
- "tracecalls",
18
+ "tracecalls.call",
16
19
  "proxy.ext.disabled.call",
17
20
  "proxy.int.disabled.call",
18
21
  "proxy.wrapping.new",
@@ -36,6 +39,7 @@
36
39
  "install.module.phase.results"
37
40
  ],
38
41
  "debug": [
42
+ "tracecalls.call",
39
43
  "proxy.wrapping.new",
40
44
  "proxy.wrapping.new.method",
41
45
  "proxy.int_to_ext.stack",
@@ -55,7 +59,8 @@
55
59
  "install.module",
56
60
  "install.module.phase",
57
61
  "install.module.phase.results"
58
- ]
62
+ ],
63
+ "release": []
59
64
  },
60
65
 
61
66
  "default_tracing_level": "debug",
@@ -68,6 +73,18 @@
68
73
  ],
69
74
 
70
75
  "preload": [
76
+ "logging",
77
+ "http.client",
78
+ "queue",
79
+ "mimetypes",
80
+ "encodings.idna",
81
+ "hmac",
82
+ "ipaddress",
83
+ "tempfile",
84
+ "zipfile",
85
+ "importlib.resources",
86
+ "atexit",
87
+ "weakref"
71
88
  ],
72
89
 
73
90
  "predicates": {
@@ -154,298 +171,5 @@
154
171
  }
155
172
  ]
156
173
  }
157
- },
158
-
159
- "modules": {
160
- "_frozen_importlib_external": {
161
- "comment": {
162
- "with_state": {
163
- "disabled": ["_path_stat"]
164
- }
165
- }
166
- },
167
- "_imp": {
168
- "patch_extension_exec": ["exec_dynamic", "exec_builtin"],
169
- "comment": {
170
- "with_state": {
171
- "internal": ["create_dynamic"]
172
- }
173
- }
174
- },
175
- "bdb": {
176
- "methods_with_state": {
177
- "disabled": {
178
- "Bdb": ["trace_dispatch"]
179
- }
180
- },
181
- "with_state": {
182
- "disabled": ["Bdb.trace_dispatch"]
183
- }
184
- },
185
-
186
- "sys": {
187
- "with_state": {
188
- "disabled": ["excepthook"]
189
- },
190
- "comment": {
191
- "with_state_recursive": {
192
- "disabled": ["settrace", "setprofile"]
193
- }
194
- }
195
- },
196
-
197
- "importlib": {
198
- "with_state": {
199
- "disabled": ["import_module"]
200
- }
201
- },
202
-
203
- "importlib._bootstrap": {
204
- "comment": {
205
- "with_state": {
206
- "internal": [
207
- "_load_unlocked",
208
- "_find_spec",
209
- "_lock_unlock_module",
210
- "_get_module_lock"]
211
- }
212
- }
213
- },
214
-
215
- "encodings": {
216
- "with_state": {
217
- "disabled": ["search_function"]
218
- }
219
- },
220
- "_retrace_utils": {
221
- "immutable_types": [
222
- "PyCFunctionProxy"
223
- ]
224
- },
225
- "enum": {
226
- "immutable_types": [
227
- "Enum"
228
- ]
229
- },
230
- "os": {
231
- "immutable_types": [
232
- "stat_result",
233
- "terminal_size",
234
- "statvfs_result"
235
- ]
236
- },
237
- "mmap": {
238
- "proxy": [
239
- "mmapXXX"
240
- ]
241
- },
242
- "types": {
243
- "patch_hash": ["FunctionType"],
244
- "immutable_types": [
245
- "TracebackType",
246
- "ModuleType"
247
- ]
248
- },
249
- "builtins": {
250
- "patch_hash": ["object"],
251
-
252
- "comment": {
253
- "with_state": {
254
- "disabled": ["__import__"]
255
- }
256
- },
257
-
258
- "immutable_types": [
259
- "BaseException",
260
- "memoryview",
261
- "int",
262
- "float",
263
- "complex",
264
- "str",
265
- "bytes",
266
- "bool",
267
- "bytearray",
268
- "type",
269
- "slice"],
270
-
271
- "patch_exec": "exec"
272
- },
273
- "_thread": {
274
- "proxy": [
275
- "allocate",
276
- "allocate_lock",
277
- "RLock"
278
- ],
279
- "patch_start_new_thread": ["start_new_thread", "start_new"]
280
- },
281
- "_datetime": {
282
- "immutable_types": [
283
- "datetime",
284
- "tzinfo",
285
- "timezone"
286
- ],
287
- "proxy_type_attributes": {
288
- "datetime": [
289
- "now",
290
- "utcnow",
291
- "today"
292
- ]
293
- }
294
- },
295
- "time": {
296
- "immutable_types": [
297
- "struct_time"
298
- ],
299
- "proxy": [
300
- "perf_counter",
301
- "time",
302
- "gmtime",
303
- "localtime",
304
- "monotonic",
305
- "monotonic_ns",
306
- "time_ns"
307
- ]
308
- },
309
- "io": {
310
- "comment": {
311
- "proxy": ["open_code", "open"]
312
- },
313
- "path_predicates": {
314
- "open_code": "path",
315
- "open": "file"
316
- },
317
- "patchtype": {
318
- "FileIO": {
319
- "readinfo": "retracesoftware.install.edgecases.readinto"
320
- },
321
- "BufferedReader": {
322
- "readinfo": "retracesoftware.install.edgecases.readinto"
323
- },
324
- "BufferedRandom": {
325
- "readinfo": "retracesoftware.install.edgecases.readinto"
326
- }
327
- }
328
- },
329
- "pathlib": {
330
- "immutable_types": [
331
- "PosixPath"
332
- ]
333
- },
334
- "posix": {
335
- "immutable_types": [
336
- "times_result",
337
- "statvfs_result",
338
- "uname_result",
339
- "stat_result",
340
- "terminal_size"
341
- ],
342
-
343
- "proxy_all_except": [
344
- "fork",
345
- "register_at_fork",
346
- "basename",
347
- "readlink",
348
- "strerror",
349
- "listdir",
350
- "_path_normpath",
351
- "DirEntry"
352
- ],
353
-
354
- "with_state_recursive0": {
355
- "disabled": ["register_at_fork"]
356
- },
357
-
358
- "wrappers": {
359
- "fork_exec": "retracesoftware.install.edgecases.fork_exec",
360
- "posix_spawn": "retracesoftware.install.edgecases.posix_spawn"
361
- }
362
- },
363
- "_posixsubprocess": {
364
- "proxy_all_except": []
365
- },
366
- "fcntl": {
367
- "proxy_all_except": []
368
- },
369
- "_signal": {
370
- "proxy_all_except": []
371
- },
372
- "_socket": {
373
- "proxy_all_except": [
374
- "CAPI"
375
- ],
376
- "immutable_types": [
377
- "error",
378
- "herror",
379
- "gaierror",
380
- "timeout"
381
- ],
382
- "patchtype": {
383
- "socket": {
384
- "recvfrom_into": "retracesoftware.install.edgecases.recvfrom_into",
385
- "recv_into": "retracesoftware.install.edgecases.recv_into",
386
- "recvmsg_into": "retracesoftware.install.edgecases.recvmsg_into"
387
- }
388
- }
389
- },
390
- "select": {
391
- "proxy_all_except": []
392
- },
393
- "_sqlite3": {
394
- "proxy_all_except": []
395
- },
396
- "_ssl": {
397
- "proxy_all_except": [],
398
-
399
- "proxy_functions": ".*",
400
- "comment": {
401
- "proxy_types": [
402
- "MemoryBIO",
403
- "_SSLSocket",
404
- "_SSLContext"
405
- ],
406
- "wrap": {
407
- "_SSLSocket.write": "write"
408
- }
409
- },
410
- "patchtype": {
411
- "_SSLSocket": {
412
- "read": "retracesoftware.install.edgecases.read"
413
- }
414
- }
415
- },
416
- "_random": {
417
- "proxy_all_except": [],
418
- "proxy_types": [
419
- "Random"
420
- ],
421
- "proxy_functions": ".*"
422
- },
423
- "_multiprocessing": {
424
- "proxy_functions": ".*"
425
- },
426
- "multiprocessing.context": {
427
- "proxy": ["_default_context"]
428
- },
429
-
430
- "PIL._imaging": {
431
- "proxy_all_except": ["map_buffer"]
432
- },
433
- "psycopg2._psycopg": {
434
- "proxy_all_except": ["Error"]
435
- },
436
-
437
- "_weakrefset": {
438
- "sync_types": ["WeakSet", "_IterationGuard"]
439
- },
440
- "_collections": {
441
- "sync_types": [
442
- "deque"
443
- ]
444
- },
445
- "_queue": {
446
- "sync_types": [
447
- "SimpleQueue"
448
- ]
449
- }
450
174
  }
451
175
  }
@@ -2,6 +2,7 @@ import pkgutil
2
2
  import json
3
3
  import os
4
4
  import datetime
5
+ import tomllib
5
6
 
6
7
  from pathlib import Path
7
8
 
@@ -36,6 +37,11 @@ def recording_path(config):
36
37
  os.environ['RETRACE_RECORDING_PATH'] = str(recording_path)
37
38
  return recording_path
38
39
 
40
+ def load_module_config(filename):
41
+ data = pkgutil.get_data("retracesoftware", filename)
42
+ assert data is not None
43
+ return tomllib.loads(data.decode("utf-8"))
44
+
39
45
  def load_config(filename):
40
46
 
41
47
  data = pkgutil.get_data("retracesoftware", filename)