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.
- retracesoftware/__main__.py +266 -0
- retracesoftware/autoenable.py +53 -0
- retracesoftware/config.json +19 -295
- retracesoftware/install/config.py +6 -0
- retracesoftware/install/edgecases.py +23 -1
- retracesoftware/install/patcher.py +93 -649
- retracesoftware/install/patchfindspec.py +117 -0
- retracesoftware/install/phases.py +338 -0
- retracesoftware/install/record.py +97 -37
- retracesoftware/install/replace.py +28 -0
- retracesoftware/install/replay.py +55 -11
- retracesoftware/install/tracer.py +87 -77
- retracesoftware/modules.toml +384 -0
- retracesoftware/proxy/messagestream.py +204 -0
- retracesoftware/proxy/proxysystem.py +283 -64
- retracesoftware/proxy/proxytype.py +34 -10
- retracesoftware/proxy/record.py +97 -64
- retracesoftware/proxy/replay.py +62 -219
- retracesoftware/proxy/serializer.py +28 -0
- retracesoftware/proxy/startthread.py +40 -0
- retracesoftware/proxy/stubfactory.py +42 -19
- retracesoftware/replay.py +104 -0
- retracesoftware/run.py +373 -0
- retracesoftware/stackdifference.py +133 -0
- {retracesoftware_proxy-0.1.21.dist-info → retracesoftware_proxy-0.2.0.dist-info}/METADATA +2 -1
- retracesoftware_proxy-0.2.0.dist-info/RECORD +41 -0
- retracesoftware_proxy-0.1.21.dist-info/RECORD +0 -29
- {retracesoftware_proxy-0.1.21.dist-info → retracesoftware_proxy-0.2.0.dist-info}/WHEEL +0 -0
- {retracesoftware_proxy-0.1.21.dist-info → retracesoftware_proxy-0.2.0.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
from functools import wraps
|
|
2
|
+
import os
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
import shutil
|
|
5
|
+
import sys
|
|
6
|
+
|
|
7
|
+
# Set to track copied modules
|
|
8
|
+
copied_modules = set()
|
|
9
|
+
|
|
10
|
+
def get_relative_path(module_path: Path, sys_path: list) -> Path:
|
|
11
|
+
"""Compute the relative path of module_path relative to sys.path entries."""
|
|
12
|
+
module_path = module_path.resolve()
|
|
13
|
+
for base in sys_path:
|
|
14
|
+
base_path = Path(base).resolve()
|
|
15
|
+
try:
|
|
16
|
+
if module_path.is_relative_to(base_path):
|
|
17
|
+
return module_path.relative_to(base_path)
|
|
18
|
+
except ValueError:
|
|
19
|
+
continue
|
|
20
|
+
return Path(module_path.name)
|
|
21
|
+
|
|
22
|
+
class patch_find_spec:
|
|
23
|
+
def __init__(self, cwd, run_path, python_path):
|
|
24
|
+
self.cwd = cwd
|
|
25
|
+
self.run_path = run_path
|
|
26
|
+
self.python_path = python_path
|
|
27
|
+
|
|
28
|
+
def __call__(self, spec):
|
|
29
|
+
if spec is not None and spec.origin and spec.origin != "built-in" and os.path.isfile(spec.origin):
|
|
30
|
+
module_name = spec.name
|
|
31
|
+
if module_name not in copied_modules:
|
|
32
|
+
module_path = Path(spec.origin)
|
|
33
|
+
|
|
34
|
+
dest_path = None
|
|
35
|
+
|
|
36
|
+
if module_path.is_relative_to(self.cwd):
|
|
37
|
+
dest_path = self.run_path / module_path.relative_to(self.cwd)
|
|
38
|
+
elif self.python_path:
|
|
39
|
+
relative_path = get_relative_path(module_path, sys.path)
|
|
40
|
+
dest_path = self.python_path / relative_path
|
|
41
|
+
|
|
42
|
+
if dest_path:
|
|
43
|
+
dest_path.parent.mkdir(parents=True, exist_ok=True)
|
|
44
|
+
shutil.copy2(module_path, dest_path)
|
|
45
|
+
copied_modules.add(module_name)
|
|
46
|
+
|
|
47
|
+
# def __call__(self, fullname, path, target=None):
|
|
48
|
+
# spec = self.original_find_spec(fullname, path, target)
|
|
49
|
+
|
|
50
|
+
# if spec is not None and spec.origin and spec.origin != "built-in" and os.path.isfile(spec.origin):
|
|
51
|
+
# module_name = spec.name
|
|
52
|
+
# if module_name not in copied_modules:
|
|
53
|
+
# module_path = Path(spec.origin)
|
|
54
|
+
|
|
55
|
+
# dest_path = None
|
|
56
|
+
|
|
57
|
+
# if module_path.is_relative_to(self.cwd):
|
|
58
|
+
# dest_path = self.run_path / module_path.relative_to(self.cwd)
|
|
59
|
+
# elif self.python_path:
|
|
60
|
+
# relative_path = get_relative_path(module_path, sys.path)
|
|
61
|
+
# dest_path = self.python_path / relative_path
|
|
62
|
+
|
|
63
|
+
# if dest_path:
|
|
64
|
+
# dest_path.parent.mkdir(parents=True, exist_ok=True)
|
|
65
|
+
# shutil.copy2(module_path, dest_path)
|
|
66
|
+
# copied_modules.add(module_name)
|
|
67
|
+
|
|
68
|
+
# return spec
|
|
69
|
+
|
|
70
|
+
# def patch_find_spec(cwd, run_path, python_path, original_find_spec):
|
|
71
|
+
# """Create a patched version of find_spec that copies .py files."""
|
|
72
|
+
# @wraps(original_find_spec)
|
|
73
|
+
# def patched_find_spec(fullname, path, target=None):
|
|
74
|
+
# spec = original_find_spec(fullname, path, target)
|
|
75
|
+
|
|
76
|
+
# if spec is not None and spec.origin and spec.origin != "built-in" and os.path.isfile(spec.origin):
|
|
77
|
+
# module_name = spec.name
|
|
78
|
+
# if module_name not in copied_modules:
|
|
79
|
+
# module_path = Path(spec.origin)
|
|
80
|
+
|
|
81
|
+
# dest_path = None
|
|
82
|
+
|
|
83
|
+
# if module_path.is_relative_to(cwd):
|
|
84
|
+
# dest_path = run_path / module_path.relative_to(cwd)
|
|
85
|
+
# elif python_path:
|
|
86
|
+
# relative_path = get_relative_path(module_path, sys.path)
|
|
87
|
+
# dest_path = python_path / relative_path
|
|
88
|
+
|
|
89
|
+
# if dest_path:
|
|
90
|
+
# dest_path.parent.mkdir(parents=True, exist_ok=True)
|
|
91
|
+
# shutil.copy2(module_path, dest_path)
|
|
92
|
+
# copied_modules.add(module_name)
|
|
93
|
+
|
|
94
|
+
# # elif python_path:
|
|
95
|
+
# # relative_path = get_relative_path(module_path, sys.path)
|
|
96
|
+
# # dest_path = python_path / relative_path
|
|
97
|
+
# # dest_path.parent.mkdir(parents=True, exist_ok=True)
|
|
98
|
+
|
|
99
|
+
# # shutil.copy2(module_path, dest_path)
|
|
100
|
+
# # copied_modules.add(module_name)
|
|
101
|
+
|
|
102
|
+
# # if module_path.suffix == '.py':
|
|
103
|
+
# # elif module_path.suffix == '.py':
|
|
104
|
+
# # relative_path = get_relative_path(module_path, sys.path)
|
|
105
|
+
# # dest_path = path / relative_path
|
|
106
|
+
# # dest_path.parent.mkdir(parents=True, exist_ok=True)
|
|
107
|
+
# # try:
|
|
108
|
+
# # shutil.copy2(module_path, dest_path)
|
|
109
|
+
# # print(f"Copied {module_path} to {dest_path} (relative: {relative_path})")
|
|
110
|
+
# # copied_modules.add(module_name)
|
|
111
|
+
# # except Exception as e:
|
|
112
|
+
# # print(f"Failed to copy {module_path}: {e}")
|
|
113
|
+
# # else:
|
|
114
|
+
# # print(f"Skipped copying {module_path} (not a .py file)")
|
|
115
|
+
|
|
116
|
+
# return spec
|
|
117
|
+
# return patched_find_spec
|
|
@@ -0,0 +1,338 @@
|
|
|
1
|
+
import retracesoftware.functional as functional
|
|
2
|
+
import retracesoftware.utils as utils
|
|
3
|
+
from retracesoftware.proxy.thread import start_new_thread_wrapper
|
|
4
|
+
import threading
|
|
5
|
+
import importlib
|
|
6
|
+
from retracesoftware.install.typeutils import modify, WithFlags, WithoutFlags
|
|
7
|
+
import enum
|
|
8
|
+
|
|
9
|
+
from retracesoftware.install.typeutils import modify
|
|
10
|
+
from abc import ABC, abstractmethod
|
|
11
|
+
from typing import List, Dict, Any, Callable
|
|
12
|
+
import functools
|
|
13
|
+
|
|
14
|
+
def simple_phase(names, func):
|
|
15
|
+
"""
|
|
16
|
+
Simplest phase type
|
|
17
|
+
"""
|
|
18
|
+
return {name: func for name in names}
|
|
19
|
+
|
|
20
|
+
def simple_patch_phase(names, func):
|
|
21
|
+
"""
|
|
22
|
+
Simplest phase type
|
|
23
|
+
"""
|
|
24
|
+
def side_effect(obj):
|
|
25
|
+
func(obj)
|
|
26
|
+
return obj
|
|
27
|
+
|
|
28
|
+
return {name: side_effect for name in names}
|
|
29
|
+
|
|
30
|
+
Transform = Callable[[Any], Any]
|
|
31
|
+
Config = Any
|
|
32
|
+
|
|
33
|
+
Updater = Dict[str, Transform]
|
|
34
|
+
|
|
35
|
+
Phase = Callable[[Config], Updater]
|
|
36
|
+
|
|
37
|
+
Patcher = Dict[str, Phase]
|
|
38
|
+
|
|
39
|
+
def update_class(cls : type, actions : Updater):
|
|
40
|
+
...
|
|
41
|
+
|
|
42
|
+
def type_attributes(patcher, config):
|
|
43
|
+
result = {}
|
|
44
|
+
|
|
45
|
+
for typename, action_attrs in config.items():
|
|
46
|
+
|
|
47
|
+
def patch_type(cls):
|
|
48
|
+
with modify(cls):
|
|
49
|
+
for action, attrs in action_attrs.items():
|
|
50
|
+
update_class(cls, patcher[action](attrs))
|
|
51
|
+
return cls
|
|
52
|
+
|
|
53
|
+
result[typename] = patch_type
|
|
54
|
+
|
|
55
|
+
return result
|
|
56
|
+
|
|
57
|
+
def create_patcher(system) -> Patcher:
|
|
58
|
+
patcher = {}
|
|
59
|
+
|
|
60
|
+
def simple_patcher(func): lambda config: {name: func for name in config}
|
|
61
|
+
|
|
62
|
+
patcher.update({
|
|
63
|
+
'type_attributes': functools.partial(type_attributes, patcher),
|
|
64
|
+
'disable': simple_patcher(system.disable),
|
|
65
|
+
'patch_types': simple_patcher(system.patch_type),
|
|
66
|
+
|
|
67
|
+
})
|
|
68
|
+
return patcher
|
|
69
|
+
|
|
70
|
+
class TypeAttributesPhase(Phase):
|
|
71
|
+
|
|
72
|
+
def __init__(self, patcher):
|
|
73
|
+
super().__init__('type_attributes')
|
|
74
|
+
self.patcher = patcher
|
|
75
|
+
|
|
76
|
+
def patch_with_config(self, config, cls):
|
|
77
|
+
# print(f'TypeAttributesPhase: {config} {cls}')
|
|
78
|
+
if not isinstance(cls, type):
|
|
79
|
+
raise Exception("TODO")
|
|
80
|
+
|
|
81
|
+
with modify(cls):
|
|
82
|
+
for phase_name,values in config.items():
|
|
83
|
+
for attribute_name,func in self.patcher.find_phase(phase_name)(values).items():
|
|
84
|
+
utils.update(cls, attribute_name, func)
|
|
85
|
+
|
|
86
|
+
return cls
|
|
87
|
+
|
|
88
|
+
class Phase:
|
|
89
|
+
def __init__(self, system):
|
|
90
|
+
self.system = system
|
|
91
|
+
|
|
92
|
+
def simple_phase(func): return lambda names: {name: func for name in names}
|
|
93
|
+
|
|
94
|
+
self.phases = {
|
|
95
|
+
'disable': simple_phase(system.disable),
|
|
96
|
+
'type_attributes': lambda config: simple_phase(config, system.disable)
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
def type_attributes(self, config, cls):
|
|
100
|
+
with modify(cls):
|
|
101
|
+
for action_name, attribute_names in config.items():
|
|
102
|
+
for attribute_name, func in self(action_name, attribute_names).items():
|
|
103
|
+
utils.update(cls, attribute_name, func)
|
|
104
|
+
|
|
105
|
+
return cls
|
|
106
|
+
|
|
107
|
+
def __call__(self, name, config):
|
|
108
|
+
match name:
|
|
109
|
+
case 'disable':
|
|
110
|
+
return {name: self.system.disable for name in config}
|
|
111
|
+
case 'type_attributes':
|
|
112
|
+
return {cls: partial(self.type_attributes, actions) for cls, actions in config.items()}
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
phases = {
|
|
116
|
+
'disable': lambda system, config: simple_phase(config, system.disable),
|
|
117
|
+
'type_attributes': lambda system, config: simple_phase(config, system.disable)
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
class Phase(ABC):
|
|
121
|
+
"""
|
|
122
|
+
A phase is function when called with system and config yields map of name->func
|
|
123
|
+
A patch phase
|
|
124
|
+
A phase has... keys... and action
|
|
125
|
+
"""
|
|
126
|
+
def __init__(self, name):
|
|
127
|
+
self.name = name
|
|
128
|
+
|
|
129
|
+
def patch(self, obj):
|
|
130
|
+
return obj
|
|
131
|
+
|
|
132
|
+
def patch_with_config(self, config, obj):
|
|
133
|
+
return obj
|
|
134
|
+
|
|
135
|
+
@abstractmethod
|
|
136
|
+
def apply(self, name : str, value : Any) -> Any:
|
|
137
|
+
pass
|
|
138
|
+
|
|
139
|
+
@abstractmethod
|
|
140
|
+
def __call__(self, namespace : Dict[str, Any]) -> None:
|
|
141
|
+
for name, value in namespace.items():
|
|
142
|
+
if
|
|
143
|
+
|
|
144
|
+
def __call__(self, config):
|
|
145
|
+
if isinstance(config, str):
|
|
146
|
+
return {config: self.patch}
|
|
147
|
+
elif isinstance(config, list):
|
|
148
|
+
return {key: self.patch for key in config}
|
|
149
|
+
elif isinstance(config, dict):
|
|
150
|
+
return {key: lambda obj: self.patch_with_config(value, obj) for key,value in config.items()}
|
|
151
|
+
else:
|
|
152
|
+
raise Exception(f'Unhandled config type: {config}')
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
class Proxy(Phase):
|
|
157
|
+
|
|
158
|
+
def proxy()
|
|
159
|
+
def __call__(self, name : str, value : Any) -> None:
|
|
160
|
+
if name in self.targets
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
class Patch(Phase):
|
|
164
|
+
|
|
165
|
+
def __init__(self, targets, action):
|
|
166
|
+
self.targets = targets
|
|
167
|
+
|
|
168
|
+
def select(self, name : str) -> bool:
|
|
169
|
+
pass
|
|
170
|
+
|
|
171
|
+
def __call__(self, name : str, value : Any) -> None:
|
|
172
|
+
if name in self.targets
|
|
173
|
+
|
|
174
|
+
class SimplePhase(Phase):
|
|
175
|
+
def __init__(self, name, patch):
|
|
176
|
+
super().__init__(name)
|
|
177
|
+
self.patch = patch
|
|
178
|
+
|
|
179
|
+
class DisablePhase(Phase):
|
|
180
|
+
def __init__(self, thread_state):
|
|
181
|
+
super().__init__('disable')
|
|
182
|
+
self.thread_state = thread_state
|
|
183
|
+
|
|
184
|
+
def patch(self, obj):
|
|
185
|
+
print(f'Disabling: {obj}')
|
|
186
|
+
return self.thread_state.wrap('disabled', obj)
|
|
187
|
+
|
|
188
|
+
class TryPatchPhase(Phase):
|
|
189
|
+
def __init__(self, patcher):
|
|
190
|
+
super().__init__('try_patch')
|
|
191
|
+
self.patcher = patcher
|
|
192
|
+
|
|
193
|
+
def patch(self, obj):
|
|
194
|
+
try:
|
|
195
|
+
return self.patcher(obj)
|
|
196
|
+
except:
|
|
197
|
+
return obj
|
|
198
|
+
|
|
199
|
+
class PatchTypesPhase(Phase):
|
|
200
|
+
def __init__(self, patcher):
|
|
201
|
+
super().__init__('patch_types')
|
|
202
|
+
self.patcher = patcher
|
|
203
|
+
|
|
204
|
+
def patch(self, obj):
|
|
205
|
+
if not isinstance(obj, type):
|
|
206
|
+
raise Exception("TODO")
|
|
207
|
+
|
|
208
|
+
self.patcher(obj)
|
|
209
|
+
return obj
|
|
210
|
+
|
|
211
|
+
class BindPhase(Phase):
|
|
212
|
+
def __init__(self, bind):
|
|
213
|
+
super().__init__('bind')
|
|
214
|
+
self.bind = bind
|
|
215
|
+
|
|
216
|
+
def patch(self, obj):
|
|
217
|
+
# print(f'In BindPhase: {obj} {isinstance(obj, type)}')
|
|
218
|
+
if issubclass(obj, enum.Enum):
|
|
219
|
+
for member in obj:
|
|
220
|
+
# print(f'binding member: {member}')
|
|
221
|
+
# utils.sigtrap(member)
|
|
222
|
+
self.bind(member)
|
|
223
|
+
else:
|
|
224
|
+
self.bind(obj)
|
|
225
|
+
|
|
226
|
+
return obj
|
|
227
|
+
|
|
228
|
+
class ImmutableTypePhase(Phase):
|
|
229
|
+
|
|
230
|
+
def __init__(self, types):
|
|
231
|
+
super().__init__('immutable')
|
|
232
|
+
self.types = types
|
|
233
|
+
|
|
234
|
+
def patch(self, obj):
|
|
235
|
+
if not isinstance(obj, type):
|
|
236
|
+
raise Exception("TODO")
|
|
237
|
+
|
|
238
|
+
self.types.add(obj)
|
|
239
|
+
return obj
|
|
240
|
+
|
|
241
|
+
class PatchThreadPhase(Phase):
|
|
242
|
+
def __init__(self, thread_state, on_exit):
|
|
243
|
+
super().__init__('patch_start_new_thread')
|
|
244
|
+
self.thread_state = thread_state
|
|
245
|
+
self.on_exit = on_exit
|
|
246
|
+
|
|
247
|
+
def patch(self, obj):
|
|
248
|
+
return start_new_thread_wrapper(thread_state = self.thread_state,
|
|
249
|
+
on_exit = self.on_exit,
|
|
250
|
+
start_new_thread = obj)
|
|
251
|
+
|
|
252
|
+
def resolve(path):
|
|
253
|
+
module, sep, name = path.rpartition('.')
|
|
254
|
+
if module == None: module = 'builtins'
|
|
255
|
+
|
|
256
|
+
return getattr(importlib.import_module(module), name)
|
|
257
|
+
|
|
258
|
+
class WrapPhase(Phase):
|
|
259
|
+
def __init__(self):
|
|
260
|
+
super().__init__('wrap')
|
|
261
|
+
|
|
262
|
+
def patch_with_config(self, wrapper, obj):
|
|
263
|
+
return resolve(wrapper)(obj)
|
|
264
|
+
|
|
265
|
+
class PatchClassPhase(Phase):
|
|
266
|
+
def __init__(self):
|
|
267
|
+
super().__init__('patch_class')
|
|
268
|
+
|
|
269
|
+
def patch_with_config(self, config, cls):
|
|
270
|
+
|
|
271
|
+
patchers = utils.map_values(resolve, config)
|
|
272
|
+
|
|
273
|
+
assert cls is not None
|
|
274
|
+
|
|
275
|
+
with WithoutFlags(cls, "Py_TPFLAGS_IMMUTABLETYPE"):
|
|
276
|
+
for name,func in patchers.items():
|
|
277
|
+
utils.update(cls, name, func)
|
|
278
|
+
|
|
279
|
+
return cls
|
|
280
|
+
|
|
281
|
+
class ProxyWrapPhase(Phase):
|
|
282
|
+
def __init__(self):
|
|
283
|
+
super().__init__('wrap_proxy')
|
|
284
|
+
|
|
285
|
+
def patch_with_config(self, config, cls):
|
|
286
|
+
|
|
287
|
+
patchers = utils.map_values(resolve, config)
|
|
288
|
+
|
|
289
|
+
def patch(proxytype):
|
|
290
|
+
for name,func in patchers.items():
|
|
291
|
+
utils.update(proxytype, name, func)
|
|
292
|
+
|
|
293
|
+
cls.__retrace_patch_proxy__ = patch
|
|
294
|
+
|
|
295
|
+
return cls
|
|
296
|
+
# return resolve(wrapper)(obj)
|
|
297
|
+
|
|
298
|
+
class TypeAttributesPhase(Phase):
|
|
299
|
+
|
|
300
|
+
def __init__(self, patcher):
|
|
301
|
+
super().__init__('type_attributes')
|
|
302
|
+
self.patcher = patcher
|
|
303
|
+
|
|
304
|
+
def patch_with_config(self, config, cls):
|
|
305
|
+
# print(f'TypeAttributesPhase: {config} {cls}')
|
|
306
|
+
if not isinstance(cls, type):
|
|
307
|
+
raise Exception("TODO")
|
|
308
|
+
|
|
309
|
+
with modify(cls):
|
|
310
|
+
for phase_name,values in config.items():
|
|
311
|
+
for attribute_name,func in self.patcher.find_phase(phase_name)(values).items():
|
|
312
|
+
utils.update(cls, attribute_name, func)
|
|
313
|
+
|
|
314
|
+
return cls
|
|
315
|
+
|
|
316
|
+
class PerThread(threading.local):
|
|
317
|
+
def __init__(self):
|
|
318
|
+
self.internal = utils.counter()
|
|
319
|
+
self.external = utils.counter()
|
|
320
|
+
|
|
321
|
+
class PatchHashPhase(Phase):
|
|
322
|
+
|
|
323
|
+
def __init__(self, thread_state):
|
|
324
|
+
super().__init__('patch_hash')
|
|
325
|
+
|
|
326
|
+
per_thread = PerThread()
|
|
327
|
+
|
|
328
|
+
self.hashfunc = thread_state.dispatch(
|
|
329
|
+
functional.constantly(None),
|
|
330
|
+
internal = functional.repeatedly(functional.partial(getattr, per_thread, 'internal')),
|
|
331
|
+
external = functional.repeatedly(functional.partial(getattr, per_thread, 'external')))
|
|
332
|
+
|
|
333
|
+
def patch(self, obj):
|
|
334
|
+
if not isinstance(obj, type):
|
|
335
|
+
raise Exception("TODO")
|
|
336
|
+
|
|
337
|
+
utils.patch_hash(cls = obj, hashfunc = self.hashfunc)
|
|
338
|
+
return obj
|
|
@@ -6,23 +6,34 @@ import retracesoftware.stream as stream
|
|
|
6
6
|
|
|
7
7
|
from retracesoftware.install.tracer import Tracer
|
|
8
8
|
from retracesoftware.install import globals
|
|
9
|
+
from retracesoftware.install.config import env_truthy
|
|
10
|
+
from retracesoftware.install.patchfindspec import patch_find_spec
|
|
9
11
|
|
|
10
12
|
import os
|
|
11
13
|
import sys
|
|
12
14
|
from datetime import datetime
|
|
13
15
|
import json
|
|
14
16
|
from pathlib import Path
|
|
17
|
+
import shutil
|
|
15
18
|
|
|
16
19
|
# class ThreadSwitch:
|
|
17
20
|
# def __init__(self, id):
|
|
18
21
|
# self.id = id
|
|
19
|
-
|
|
22
|
+
|
|
20
23
|
# def __repr__(self):
|
|
21
24
|
# return f'ThreadSwitch<{self.id}>'
|
|
22
25
|
|
|
23
26
|
# def __str__(self):
|
|
24
27
|
# return f'ThreadSwitch<{self.id}>'
|
|
25
28
|
|
|
29
|
+
def code_workspace():
|
|
30
|
+
return {
|
|
31
|
+
'folders': [
|
|
32
|
+
{'path': '../..', 'name': 'Application'},
|
|
33
|
+
{'path': '.', 'name': 'Recording'}
|
|
34
|
+
]
|
|
35
|
+
}
|
|
36
|
+
|
|
26
37
|
def write_files(recording_path):
|
|
27
38
|
with open(recording_path / 'env', 'w') as f:
|
|
28
39
|
json.dump(dict(os.environ), f, indent=2)
|
|
@@ -36,6 +47,9 @@ def write_files(recording_path):
|
|
|
36
47
|
with open(recording_path / 'cmd', 'w') as f:
|
|
37
48
|
json.dump(sys.orig_argv, f, indent=2)
|
|
38
49
|
|
|
50
|
+
with open(recording_path / 'replay.code-workspace', 'w') as f:
|
|
51
|
+
json.dump(code_workspace(), f, indent=2)
|
|
52
|
+
|
|
39
53
|
def create_recording_path(path):
|
|
40
54
|
expanded = datetime.now().strftime(path.format(pid = os.getpid()))
|
|
41
55
|
os.environ['RETRACE_RECORDING_PATH'] = expanded
|
|
@@ -55,10 +69,14 @@ def merge_config(base, override):
|
|
|
55
69
|
return override
|
|
56
70
|
|
|
57
71
|
|
|
72
|
+
|
|
73
|
+
def dump_as_json(path, obj):
|
|
74
|
+
with open(path, 'w') as f:
|
|
75
|
+
json.dump(obj, f, indent=2)
|
|
76
|
+
|
|
58
77
|
def record_system(thread_state, immutable_types, config):
|
|
59
78
|
|
|
60
79
|
recording_path = create_recording_path(config['recording_path'])
|
|
61
|
-
|
|
62
80
|
recording_path.mkdir(parents=True, exist_ok=True)
|
|
63
81
|
|
|
64
82
|
globals.recording_path = globals.RecordingPath(recording_path)
|
|
@@ -67,48 +85,90 @@ def record_system(thread_state, immutable_types, config):
|
|
|
67
85
|
|
|
68
86
|
tracing_config = config['tracing_levels'].get(tracing_level(config), {})
|
|
69
87
|
|
|
70
|
-
|
|
71
|
-
json.dump(tracing_config, f, indent=2)
|
|
88
|
+
dump_as_json(path = recording_path / 'tracing_config.json', obj = tracing_config)
|
|
72
89
|
|
|
73
90
|
def write_main_path(path):
|
|
74
91
|
with open(recording_path / 'mainscript', 'w') as f:
|
|
75
92
|
f.write(path)
|
|
76
93
|
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
#
|
|
89
|
-
|
|
90
|
-
#
|
|
91
|
-
#
|
|
92
|
-
#
|
|
93
|
-
|
|
94
|
-
#
|
|
95
|
-
#
|
|
96
|
-
#
|
|
97
|
-
|
|
98
|
-
#
|
|
99
|
-
#
|
|
100
|
-
#
|
|
101
|
-
|
|
102
|
-
#
|
|
103
|
-
#
|
|
104
|
-
#
|
|
105
|
-
|
|
106
|
-
#
|
|
107
|
-
|
|
108
|
-
#
|
|
94
|
+
run_path = recording_path / 'run'
|
|
95
|
+
run_path.mkdir(parents=True, exist_ok=False)
|
|
96
|
+
|
|
97
|
+
shutil.copy2(sys.argv[0], run_path)
|
|
98
|
+
|
|
99
|
+
python_path = recording_path / 'pythonpath'
|
|
100
|
+
python_path.mkdir(parents=True, exist_ok=False)
|
|
101
|
+
|
|
102
|
+
vscode = recording_path / '.vscode'
|
|
103
|
+
vscode.mkdir(parents=True, exist_ok=False)
|
|
104
|
+
|
|
105
|
+
# launch_json = {
|
|
106
|
+
# # // Use IntelliSense to learn about possible attributes.
|
|
107
|
+
# # // Hover to view descriptions of existing attributes.
|
|
108
|
+
# # // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
|
|
109
|
+
# "version": "0.2.0",
|
|
110
|
+
# "configurations": [
|
|
111
|
+
# {
|
|
112
|
+
# "name": "Python Debugger: Python File",
|
|
113
|
+
# "type": "debugpy",
|
|
114
|
+
# "request": "launch",
|
|
115
|
+
# "cwd": "${workspaceFolder}/run",
|
|
116
|
+
# "env": {
|
|
117
|
+
# "RETRACE_MODE": "replay"
|
|
118
|
+
# },
|
|
119
|
+
# "python": sys.executable,
|
|
120
|
+
# "program": sys.argv[0]
|
|
121
|
+
# }
|
|
122
|
+
# ]
|
|
123
|
+
# }
|
|
124
|
+
|
|
125
|
+
# dump_as_json(vscode / 'launch.json', launch_json)
|
|
126
|
+
|
|
127
|
+
workspace = {
|
|
128
|
+
"folders": [{ 'path': '.' }],
|
|
129
|
+
"settings": {
|
|
130
|
+
# This is the one that works reliably in .code-workspace files
|
|
131
|
+
# "python.defaultInterpreterPath":
|
|
132
|
+
"python.defaultInterpreterPath": sys.executable,
|
|
133
|
+
# "python.interpreterInfo": {
|
|
134
|
+
# "run": {"path": sys.executable}
|
|
135
|
+
# }
|
|
136
|
+
},
|
|
137
|
+
"launch": {
|
|
138
|
+
"version": "0.2.0",
|
|
139
|
+
"configurations": [
|
|
140
|
+
{
|
|
141
|
+
"justMyCode": False,
|
|
142
|
+
"name": "Python Debugger: Python File",
|
|
143
|
+
"type": "debugpy",
|
|
144
|
+
"request": "launch",
|
|
145
|
+
"cwd": "${workspaceFolder}/run",
|
|
146
|
+
"env": {
|
|
147
|
+
"RETRACE_RECORDING_PATH": "..",
|
|
148
|
+
"RETRACE_MODE": "replay"
|
|
149
|
+
},
|
|
150
|
+
# "python": ["${command:python.interpreterPath}"],
|
|
151
|
+
"python": sys.executable,
|
|
152
|
+
# "python": sys.executable,
|
|
153
|
+
"program": sys.argv[0]
|
|
154
|
+
}
|
|
155
|
+
]
|
|
156
|
+
},
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
dump_as_json(recording_path / 'replay.code-workspace', workspace)
|
|
160
|
+
|
|
161
|
+
copy_source = thread_state.wrap('disabled', patch_find_spec(cwd = Path(os.getcwd()), run_path = run_path, python_path = python_path))
|
|
162
|
+
|
|
163
|
+
for finder in sys.meta_path:
|
|
164
|
+
finder.find_spec = functional.sequence(finder.find_spec, functional.side_effect(copy_source))
|
|
109
165
|
|
|
110
166
|
return RecordProxySystem(thread_state = thread_state,
|
|
111
167
|
immutable_types = immutable_types,
|
|
112
168
|
tracing_config = tracing_config,
|
|
113
169
|
write_main_path = write_main_path,
|
|
114
|
-
path = recording_path / 'trace.bin'
|
|
170
|
+
path = recording_path / 'trace.bin',
|
|
171
|
+
tracecalls = env_truthy('RETRACE_ALL', False),
|
|
172
|
+
verbose = env_truthy('RETRACE_VERBOSE', False),
|
|
173
|
+
stacktraces = env_truthy('RETRACE_STACKTRACES', False),
|
|
174
|
+
magic_markers = env_truthy('RETRACE_MAGIC_MARKERS', False))
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import gc
|
|
2
|
+
|
|
3
|
+
def container_replace(container, old, new):
|
|
4
|
+
if isinstance(container, dict):
|
|
5
|
+
if old in container:
|
|
6
|
+
elem = container.pop(old)
|
|
7
|
+
container[new] = elem
|
|
8
|
+
container_replace(container, old, new)
|
|
9
|
+
else:
|
|
10
|
+
for key,value in container.items():
|
|
11
|
+
if key != '__retrace_unproxied__' and value is old:
|
|
12
|
+
container[key] = new
|
|
13
|
+
return True
|
|
14
|
+
elif isinstance(container, list):
|
|
15
|
+
for i,value in enumerate(container):
|
|
16
|
+
if value is old:
|
|
17
|
+
container[i] = new
|
|
18
|
+
return True
|
|
19
|
+
elif isinstance(container, set):
|
|
20
|
+
container.remove(old)
|
|
21
|
+
container.add(new)
|
|
22
|
+
return True
|
|
23
|
+
else:
|
|
24
|
+
return False
|
|
25
|
+
|
|
26
|
+
def update(old, new):
|
|
27
|
+
for ref in gc.get_referrers(old):
|
|
28
|
+
container_replace(container = ref, old = old, new = new)
|