codesuture 0.5.1__tar.gz → 0.6.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {codesuture-0.5.1 → codesuture-0.6.0}/.gitignore +1 -0
- {codesuture-0.5.1 → codesuture-0.6.0}/CHANGELOG.md +15 -0
- {codesuture-0.5.1 → codesuture-0.6.0}/PKG-INFO +2 -2
- {codesuture-0.5.1 → codesuture-0.6.0}/README.md +1 -1
- codesuture-0.6.0/codesuture/__init__.py +2 -0
- {codesuture-0.5.1 → codesuture-0.6.0}/codesuture/cli.py +3 -3
- {codesuture-0.5.1 → codesuture-0.6.0}/codesuture/guard_synthesizer.py +92 -1
- {codesuture-0.5.1 → codesuture-0.6.0}/codesuture/tracer.py +42 -11
- {codesuture-0.5.1 → codesuture-0.6.0}/codesuture.egg-info/PKG-INFO +2 -2
- {codesuture-0.5.1 → codesuture-0.6.0}/pyproject.toml +2 -1
- codesuture-0.5.1/codesuture/__init__.py +0 -1
- {codesuture-0.5.1 → codesuture-0.6.0}/LICENSE +0 -0
- {codesuture-0.5.1 → codesuture-0.6.0}/MANIFEST.in +0 -0
- {codesuture-0.5.1 → codesuture-0.6.0}/codesuture/__main__.py +0 -0
- {codesuture-0.5.1 → codesuture-0.6.0}/codesuture/_eval_fix.py +0 -0
- {codesuture-0.5.1 → codesuture-0.6.0}/codesuture/audit.py +0 -0
- {codesuture-0.5.1 → codesuture-0.6.0}/codesuture/code_replacer.py +0 -0
- {codesuture-0.5.1 → codesuture-0.6.0}/codesuture/codesuture_fix.py +0 -0
- {codesuture-0.5.1 → codesuture-0.6.0}/codesuture/debuggee.py +0 -0
- {codesuture-0.5.1 → codesuture-0.6.0}/codesuture/diff_guard.py +0 -0
- {codesuture-0.5.1 → codesuture-0.6.0}/codesuture/explain.py +0 -0
- {codesuture-0.5.1 → codesuture-0.6.0}/codesuture/fingerprint.py +0 -0
- {codesuture-0.5.1 → codesuture-0.6.0}/codesuture/knowledge.py +0 -0
- {codesuture-0.5.1 → codesuture-0.6.0}/codesuture/middleware.py +0 -0
- {codesuture-0.5.1 → codesuture-0.6.0}/codesuture/pattern_matcher.py +0 -0
- {codesuture-0.5.1 → codesuture-0.6.0}/codesuture/persistence.py +0 -0
- {codesuture-0.5.1 → codesuture-0.6.0}/codesuture/plugins/__init__.py +0 -0
- {codesuture-0.5.1 → codesuture-0.6.0}/codesuture/plugins/autonomous.py +0 -0
- {codesuture-0.5.1 → codesuture-0.6.0}/codesuture/rewind.py +0 -0
- {codesuture-0.5.1 → codesuture-0.6.0}/codesuture/rollback.py +0 -0
- {codesuture-0.5.1 → codesuture-0.6.0}/codesuture/sandbox.py +0 -0
- {codesuture-0.5.1 → codesuture-0.6.0}/codesuture/shadow.py +0 -0
- {codesuture-0.5.1 → codesuture-0.6.0}/codesuture/watcher.py +0 -0
- {codesuture-0.5.1 → codesuture-0.6.0}/codesuture.egg-info/SOURCES.txt +0 -0
- {codesuture-0.5.1 → codesuture-0.6.0}/codesuture.egg-info/dependency_links.txt +0 -0
- {codesuture-0.5.1 → codesuture-0.6.0}/codesuture.egg-info/entry_points.txt +0 -0
- {codesuture-0.5.1 → codesuture-0.6.0}/codesuture.egg-info/requires.txt +0 -0
- {codesuture-0.5.1 → codesuture-0.6.0}/codesuture.egg-info/top_level.txt +0 -0
- {codesuture-0.5.1 → codesuture-0.6.0}/setup.cfg +0 -0
- {codesuture-0.5.1 → codesuture-0.6.0}/setup.py +0 -0
- {codesuture-0.5.1 → codesuture-0.6.0}/tests/__init__.py +0 -0
- {codesuture-0.5.1 → codesuture-0.6.0}/tests/closure_test.py +0 -0
- {codesuture-0.5.1 → codesuture-0.6.0}/tests/debug_gc.py +0 -0
- {codesuture-0.5.1 → codesuture-0.6.0}/tests/harness3.py +0 -0
- {codesuture-0.5.1 → codesuture-0.6.0}/tests/test_codesuture_debuggee.py +0 -0
- {codesuture-0.5.1 → codesuture-0.6.0}/tests/test_e2e.py +0 -0
- {codesuture-0.5.1 → codesuture-0.6.0}/tests/test_guard_synthesizer.py +0 -0
- {codesuture-0.5.1 → codesuture-0.6.0}/tests/test_harness.py +0 -0
- {codesuture-0.5.1 → codesuture-0.6.0}/tests/test_harness2.py +0 -0
- {codesuture-0.5.1 → codesuture-0.6.0}/tests/test_new_guards.py +0 -0
- {codesuture-0.5.1 → codesuture-0.6.0}/tests/test_pattern_matcher.py +0 -0
- {codesuture-0.5.1 → codesuture-0.6.0}/tests/test_unknown_bug.py +0 -0
|
@@ -5,6 +5,21 @@ All notable changes to this project will be documented in this file.
|
|
|
5
5
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
|
6
6
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
7
|
|
|
8
|
+
## [0.6.0] - 2026-05-12
|
|
9
|
+
|
|
10
|
+
### Fixed
|
|
11
|
+
- PEP 659: Force de-specialization after `__code__` swap via
|
|
12
|
+
`ctypes.pythonapi.PyFunction_SetCode` — prevents CPython 3.11+
|
|
13
|
+
adaptive bytecode cache from ignoring injected patches
|
|
14
|
+
- Thread blindness: Install trace hook on all threads via
|
|
15
|
+
`threading.settrace` at startup, with `_install_trace_on_all_threads`
|
|
16
|
+
helper covering existing and future threads; added `threading.Lock`
|
|
17
|
+
for thread-safe patch store writes
|
|
18
|
+
- Exception table corruption: Guard injection now detects try/except
|
|
19
|
+
scope via `TryBegin`/`TryEnd` markers and redirects to function
|
|
20
|
+
entry-point injection to avoid corrupting `co_exceptiontable` offsets
|
|
21
|
+
in CPython 3.11+
|
|
22
|
+
|
|
8
23
|
## [0.5.1] - 2026-05-11
|
|
9
24
|
|
|
10
25
|
### Fixed
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: codesuture
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.6.0
|
|
4
4
|
Summary: Runtime Python bytecode patcher with guard knowledge base, persistence, and self-healing re-execution
|
|
5
5
|
License-Expression: MIT
|
|
6
6
|
Project-URL: Source, https://github.com/codesuture-py/codesuture
|
|
@@ -158,7 +158,7 @@ Beyond basic patching, CodeSuture includes a set of higher-order behaviors that
|
|
|
158
158
|
|
|
159
159
|
**Single-process scope.** Patches apply per-process. Multi-process applications need a CodeSuture instance per worker. The `.codesuture_store/` directory is shared on disk, so patches load correctly on restart.
|
|
160
160
|
|
|
161
|
-
**
|
|
161
|
+
**Web server support requires Python 3.11+ with `threading.settrace` active.** Tested with `socketserver` and `threading.Thread`. ASGI frameworks (FastAPI, Starlette) require the ASGI middleware wrapper.
|
|
162
162
|
|
|
163
163
|
---
|
|
164
164
|
|
|
@@ -137,7 +137,7 @@ Beyond basic patching, CodeSuture includes a set of higher-order behaviors that
|
|
|
137
137
|
|
|
138
138
|
**Single-process scope.** Patches apply per-process. Multi-process applications need a CodeSuture instance per worker. The `.codesuture_store/` directory is shared on disk, so patches load correctly on restart.
|
|
139
139
|
|
|
140
|
-
**
|
|
140
|
+
**Web server support requires Python 3.11+ with `threading.settrace` active.** Tested with `socketserver` and `threading.Thread`. ASGI frameworks (FastAPI, Starlette) require the ASGI middleware wrapper.
|
|
141
141
|
|
|
142
142
|
---
|
|
143
143
|
|
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
import sys
|
|
2
2
|
import argparse
|
|
3
|
-
from codesuture.tracer import install, uninstall
|
|
3
|
+
from codesuture.tracer import install, uninstall, _install_trace_on_all_threads
|
|
4
4
|
|
|
5
5
|
def main():
|
|
6
6
|
parser = argparse.ArgumentParser(prog='codesuture',
|
|
7
7
|
description='Runtime Python bytecode patcher with self-healing re-execution')
|
|
8
|
-
parser.add_argument('--version', action='version', version='codesuture 0.
|
|
8
|
+
parser.add_argument('--version', action='version', version='codesuture 0.6.0')
|
|
9
9
|
sub = parser.add_subparsers(dest='command', required=True)
|
|
10
10
|
|
|
11
11
|
run_parser = sub.add_parser('run', help='Run a script with live patching')
|
|
@@ -102,7 +102,7 @@ def main():
|
|
|
102
102
|
patched_before = tracer.stats['patched']
|
|
103
103
|
tracer._handled_exc_ids.clear()
|
|
104
104
|
try:
|
|
105
|
-
|
|
105
|
+
_install_trace_on_all_threads(tracer)
|
|
106
106
|
globs = make_persisted_patch_globals(
|
|
107
107
|
"__main__",
|
|
108
108
|
{'__name__': '__main__', '__file__': args.script},
|
|
@@ -1,9 +1,30 @@
|
|
|
1
1
|
"""
|
|
2
2
|
Synthesises guard + original bytecode for all deterministic strategies.
|
|
3
3
|
"""
|
|
4
|
+
import ctypes
|
|
4
5
|
from bytecode import Bytecode, Instr, Label, Compare
|
|
5
6
|
from codesuture.pattern_matcher import PatchSpec
|
|
6
7
|
|
|
8
|
+
def _force_despecialize(func):
|
|
9
|
+
"""
|
|
10
|
+
Force CPython 3.11+ to abandon its adaptive
|
|
11
|
+
instruction cache for this function.
|
|
12
|
+
After __code__ replacement, the interpreter must
|
|
13
|
+
re-read the new bytecode from scratch.
|
|
14
|
+
"""
|
|
15
|
+
try:
|
|
16
|
+
# PyFunction_SetCode forces de-specialization
|
|
17
|
+
# by going through the official C API path
|
|
18
|
+
# rather than the Python attribute setter.
|
|
19
|
+
ctypes.pythonapi.PyFunction_SetCode(
|
|
20
|
+
ctypes.py_object(func),
|
|
21
|
+
ctypes.py_object(func.__code__)
|
|
22
|
+
)
|
|
23
|
+
except Exception:
|
|
24
|
+
pass # Non-fatal: patch still applied, may not
|
|
25
|
+
# take effect until next function cold-start
|
|
26
|
+
|
|
27
|
+
|
|
7
28
|
class PatchValidationError(Exception):
|
|
8
29
|
pass
|
|
9
30
|
|
|
@@ -51,21 +72,91 @@ def propagate_patch(original_func, patched_code) -> int:
|
|
|
51
72
|
if hasattr(ref, '__func__') and hasattr(ref.__func__, '__code__'):
|
|
52
73
|
if ref.__func__.__code__ is original_code:
|
|
53
74
|
ref.__func__.__code__ = patched_code
|
|
75
|
+
_force_despecialize(ref.__func__)
|
|
54
76
|
propagated += 1
|
|
55
77
|
|
|
56
78
|
elif hasattr(ref, '__code__') and ref.__code__ is original_code:
|
|
57
79
|
ref.__code__ = patched_code
|
|
80
|
+
_force_despecialize(ref)
|
|
58
81
|
propagated += 1
|
|
59
82
|
|
|
60
83
|
original_func.__code__ = patched_code
|
|
84
|
+
_force_despecialize(original_func)
|
|
61
85
|
|
|
62
86
|
if propagated > 0:
|
|
63
87
|
print(f"[CodeSuture] Propagated patch to {propagated} additional "
|
|
64
88
|
f"live reference(s) of {original_func.__qualname__}.")
|
|
65
89
|
return propagated
|
|
66
90
|
|
|
91
|
+
def _is_inside_try_block(code):
|
|
92
|
+
"""Return True if any BINARY_SUBSCR or crash-relevant opcode
|
|
93
|
+
falls inside an exception handler range (TryBegin/TryEnd).
|
|
94
|
+
Uses the bytecode library's TryBegin/TryEnd markers."""
|
|
95
|
+
import sys
|
|
96
|
+
if sys.version_info < (3, 11):
|
|
97
|
+
return False
|
|
98
|
+
try:
|
|
99
|
+
from bytecode import TryBegin, TryEnd
|
|
100
|
+
bc = Bytecode.from_code(code)
|
|
101
|
+
depth = 0
|
|
102
|
+
has_subscr_in_try = False
|
|
103
|
+
for item in bc:
|
|
104
|
+
if isinstance(item, TryBegin):
|
|
105
|
+
depth += 1
|
|
106
|
+
elif isinstance(item, TryEnd):
|
|
107
|
+
depth = max(0, depth - 1)
|
|
108
|
+
elif depth > 0 and isinstance(item, Instr):
|
|
109
|
+
if item.name in ('BINARY_SUBSCR', 'LOAD_ATTR', 'LOAD_METHOD',
|
|
110
|
+
'BINARY_OP', 'BINARY_TRUE_DIVIDE'):
|
|
111
|
+
has_subscr_in_try = True
|
|
112
|
+
break
|
|
113
|
+
return has_subscr_in_try
|
|
114
|
+
except Exception:
|
|
115
|
+
return False
|
|
116
|
+
|
|
117
|
+
def _build_entry_point_null_guard(original_code, var_name, default):
|
|
118
|
+
"""Build a guard injected at the function entry point (after RESUME).
|
|
119
|
+
Checks if var_name is None and replaces it with default.
|
|
120
|
+
Safe for use when the crash site is inside a try block."""
|
|
121
|
+
bc = Bytecode.from_code(original_code)
|
|
122
|
+
skip = Label()
|
|
123
|
+
patch = [
|
|
124
|
+
Instr('LOAD_FAST', var_name),
|
|
125
|
+
Instr('LOAD_CONST', None),
|
|
126
|
+
Instr('IS_OP', 0),
|
|
127
|
+
Instr('POP_JUMP_FORWARD_IF_FALSE', skip),
|
|
128
|
+
Instr('LOAD_CONST', default),
|
|
129
|
+
Instr('RETURN_VALUE'),
|
|
130
|
+
skip
|
|
131
|
+
]
|
|
132
|
+
idx = 0
|
|
133
|
+
for i, instr in enumerate(bc):
|
|
134
|
+
if isinstance(instr, Instr) and instr.name == 'RESUME':
|
|
135
|
+
idx = i + 1
|
|
136
|
+
break
|
|
137
|
+
for instr in reversed(patch):
|
|
138
|
+
bc.insert(idx, instr)
|
|
139
|
+
return bc
|
|
140
|
+
|
|
141
|
+
# Strategies that inject inline at the crash site (replace BINARY_SUBSCR etc.)
|
|
142
|
+
# These are the ones that can corrupt exception tables when inside try blocks.
|
|
143
|
+
_INLINE_STRATEGIES = frozenset({
|
|
144
|
+
'subscript_guard', 'key_guard', 'dict_get_guard',
|
|
145
|
+
'chain_subscript_guard', 'division_guard',
|
|
146
|
+
})
|
|
147
|
+
|
|
67
148
|
def synthesize_guarded_code(original_code, spec: PatchSpec) -> Bytecode:
|
|
68
|
-
|
|
149
|
+
# Bug 3 fix: If the crash site is inside a try/except block and we
|
|
150
|
+
# would normally inject inline, redirect to an entry-point guard
|
|
151
|
+
# to avoid corrupting co_exceptiontable offsets on CPython 3.11+.
|
|
152
|
+
if spec.strategy in _INLINE_STRATEGIES and _is_inside_try_block(original_code):
|
|
153
|
+
import logging
|
|
154
|
+
logging.getLogger(__name__).debug(
|
|
155
|
+
"[CodeSuture] Crash inside try block — redirecting %s to entry-point guard",
|
|
156
|
+
spec.strategy
|
|
157
|
+
)
|
|
158
|
+
res = _build_entry_point_null_guard(original_code, spec.var_name, spec.default_value)
|
|
159
|
+
elif spec.strategy in ('subscript_guard', 'key_guard', 'dict_get_guard'):
|
|
69
160
|
res = _build_subscript_guarded_code(original_code, spec.var_name, spec.key_name, spec.default_value)
|
|
70
161
|
elif spec.strategy == 'chain_subscript_guard':
|
|
71
162
|
res = _build_chain_subscript_guarded_code(original_code, spec.var_name, spec.key_name, spec.default_value)
|
|
@@ -3,7 +3,7 @@ import os
|
|
|
3
3
|
import json
|
|
4
4
|
from datetime import datetime
|
|
5
5
|
from codesuture.pattern_matcher import analyze_exception
|
|
6
|
-
from codesuture.guard_synthesizer import synthesize_guarded_code
|
|
6
|
+
from codesuture.guard_synthesizer import synthesize_guarded_code, _force_despecialize
|
|
7
7
|
from codesuture.code_replacer import replace_function_code, get_function_from_frame
|
|
8
8
|
from codesuture.rewind import rewind_frame_to_start
|
|
9
9
|
|
|
@@ -27,6 +27,7 @@ def _is_internal_frame(frame):
|
|
|
27
27
|
|
|
28
28
|
class CodeSutureTracer:
|
|
29
29
|
def __init__(self, dry_run=False, log_file=None, max_retries=3, autonomous=False, script_path=None, verbose=False, shadow=False, ttl=7):
|
|
30
|
+
import threading
|
|
30
31
|
self.dry_run = dry_run
|
|
31
32
|
self.log_file = log_file
|
|
32
33
|
self.max_retries = max_retries
|
|
@@ -44,6 +45,7 @@ class CodeSutureTracer:
|
|
|
44
45
|
}
|
|
45
46
|
self.patched_signatures = {}
|
|
46
47
|
self._handled_exc_ids = set()
|
|
48
|
+
self._patch_lock = threading.Lock()
|
|
47
49
|
|
|
48
50
|
def __call__(self, frame, event, arg):
|
|
49
51
|
if event == 'return' and self.shadow_mode and frame.f_code in self._patched_codes:
|
|
@@ -271,22 +273,24 @@ class CodeSutureTracer:
|
|
|
271
273
|
if getattr(spec, 'target_func', None):
|
|
272
274
|
assert spec.target_func.__code__ is new_code, "Property fget code replacement failed"
|
|
273
275
|
|
|
274
|
-
|
|
275
|
-
|
|
276
|
+
with self._patch_lock:
|
|
277
|
+
from codesuture.persistence import save_patch
|
|
278
|
+
save_patch(func, new_code, spec, self.ttl)
|
|
276
279
|
|
|
277
|
-
|
|
278
|
-
|
|
280
|
+
if self.shadow_mode:
|
|
281
|
+
self._patched_codes[new_code] = spec.strategy
|
|
279
282
|
|
|
280
283
|
if self.verbose:
|
|
281
284
|
from codesuture.diff_guard import semantic_diff
|
|
282
285
|
diff = semantic_diff(old_code, new_code, spec.strategy)
|
|
283
286
|
print(f"[CodeSuture DEBUG] Diff: +{diff.added} -{diff.removed} instructions (allowed <= {diff.allowed})")
|
|
284
287
|
|
|
285
|
-
|
|
286
|
-
|
|
288
|
+
with self._patch_lock:
|
|
289
|
+
if not is_reuse:
|
|
290
|
+
self.patched_signatures[sig] = spec
|
|
287
291
|
|
|
288
|
-
|
|
289
|
-
|
|
292
|
+
if not cached:
|
|
293
|
+
record(fp, spec.strategy, spec.var_name, getattr(func, '__name__', 'unknown'), exc_type.__name__, spec.default_value, spec.key_name)
|
|
290
294
|
|
|
291
295
|
entry["action"] = "applied"
|
|
292
296
|
self._log(entry)
|
|
@@ -347,6 +351,7 @@ class CodeSutureTracer:
|
|
|
347
351
|
new_bc = synthesize_guarded_code(internal_frame.f_code, spec)
|
|
348
352
|
new_code = new_bc.to_code()
|
|
349
353
|
replace_function_code(func, new_code)
|
|
354
|
+
_force_despecialize(func)
|
|
350
355
|
self.stats["patched"] += 1
|
|
351
356
|
print(f"[CodeSuture] Self-healed {func.__name__}().")
|
|
352
357
|
|
|
@@ -369,6 +374,7 @@ class CodeSutureTracer:
|
|
|
369
374
|
if hasattr(ref, "__code__") and getattr(ref, "__code__", None) is old_code:
|
|
370
375
|
try:
|
|
371
376
|
ref.__code__ = new_code
|
|
377
|
+
_force_despecialize(ref)
|
|
372
378
|
replaced = True
|
|
373
379
|
propagated_count += 1
|
|
374
380
|
except Exception:
|
|
@@ -378,6 +384,7 @@ class CodeSutureTracer:
|
|
|
378
384
|
if hasattr(fn, "__code__") and getattr(fn, "__code__", None) is old_code:
|
|
379
385
|
try:
|
|
380
386
|
fn.__code__ = new_code
|
|
387
|
+
_force_despecialize(fn)
|
|
381
388
|
replaced = True
|
|
382
389
|
propagated_count += 1
|
|
383
390
|
except Exception:
|
|
@@ -404,6 +411,7 @@ class CodeSutureTracer:
|
|
|
404
411
|
|
|
405
412
|
if func and hasattr(func, "__code__") and getattr(func, "__code__", None) is old_code:
|
|
406
413
|
func.__code__ = new_code
|
|
414
|
+
_force_despecialize(func)
|
|
407
415
|
print(f"[CodeSuture] In-memory propagated patch applied to {func.__name__}().")
|
|
408
416
|
else:
|
|
409
417
|
print("[CodeSuture] Could not find code object in memory to persist.")
|
|
@@ -428,17 +436,40 @@ def _codesuture_excepthook(tracer, exc_type, exc_value, exc_tb):
|
|
|
428
436
|
if exc_tb:
|
|
429
437
|
tracer._handle_exception(exc_tb.tb_frame, exc_type, exc_value, exc_tb, thread=threading.current_thread())
|
|
430
438
|
|
|
439
|
+
# If CodeSuture handled and patched this exception, suppress the
|
|
440
|
+
# default traceback — the patch was applied and re-execution will
|
|
441
|
+
# handle the rest.
|
|
442
|
+
if id(exc_value) in tracer._handled_exc_ids:
|
|
443
|
+
return
|
|
444
|
+
|
|
431
445
|
if _original_excepthook:
|
|
432
446
|
_original_excepthook(exc_type, exc_value, exc_tb)
|
|
433
447
|
else:
|
|
434
448
|
sys.__excepthook__(exc_type, exc_value, exc_tb)
|
|
435
449
|
|
|
450
|
+
def _install_trace_on_all_threads(trace_fn):
|
|
451
|
+
"""Install trace hook on main thread and
|
|
452
|
+
all currently running threads."""
|
|
453
|
+
import threading
|
|
454
|
+
sys.settrace(trace_fn)
|
|
455
|
+
threading.settrace(trace_fn)
|
|
456
|
+
# For threads already running:
|
|
457
|
+
for thread in threading.enumerate():
|
|
458
|
+
if thread is not threading.current_thread():
|
|
459
|
+
try:
|
|
460
|
+
import ctypes
|
|
461
|
+
ctypes.pythonapi.PyThreadState_SetAsyncExc(
|
|
462
|
+
ctypes.c_ulong(thread.ident),
|
|
463
|
+
None # does not raise, just wakes thread
|
|
464
|
+
)
|
|
465
|
+
except Exception:
|
|
466
|
+
pass
|
|
467
|
+
|
|
436
468
|
def install(dry_run=False, log_file=None, max_retries=3, autonomous=False, script_path=None, verbose=False, shadow=False, ttl=7):
|
|
437
469
|
global _original_excepthook
|
|
438
470
|
import threading
|
|
439
471
|
tracer = CodeSutureTracer(dry_run, log_file, max_retries, autonomous, script_path, verbose, shadow, ttl)
|
|
440
|
-
|
|
441
|
-
threading.settrace(tracer)
|
|
472
|
+
_install_trace_on_all_threads(tracer)
|
|
442
473
|
|
|
443
474
|
if getattr(threading, 'excepthook', None) is not None:
|
|
444
475
|
if threading.excepthook != getattr(threading, '__excepthook__', None):
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: codesuture
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.6.0
|
|
4
4
|
Summary: Runtime Python bytecode patcher with guard knowledge base, persistence, and self-healing re-execution
|
|
5
5
|
License-Expression: MIT
|
|
6
6
|
Project-URL: Source, https://github.com/codesuture-py/codesuture
|
|
@@ -158,7 +158,7 @@ Beyond basic patching, CodeSuture includes a set of higher-order behaviors that
|
|
|
158
158
|
|
|
159
159
|
**Single-process scope.** Patches apply per-process. Multi-process applications need a CodeSuture instance per worker. The `.codesuture_store/` directory is shared on disk, so patches load correctly on restart.
|
|
160
160
|
|
|
161
|
-
**
|
|
161
|
+
**Web server support requires Python 3.11+ with `threading.settrace` active.** Tested with `socketserver` and `threading.Thread`. ASGI frameworks (FastAPI, Starlette) require the ASGI middleware wrapper.
|
|
162
162
|
|
|
163
163
|
---
|
|
164
164
|
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "codesuture"
|
|
7
|
-
version = "0.
|
|
7
|
+
version = "0.6.0"
|
|
8
8
|
description = "Runtime Python bytecode patcher with guard knowledge base, persistence, and self-healing re-execution"
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
license = "MIT"
|
|
@@ -35,3 +35,4 @@ codesuture = "codesuture.cli:main"
|
|
|
35
35
|
[tool.setuptools.packages.find]
|
|
36
36
|
include = ["codesuture*"]
|
|
37
37
|
|
|
38
|
+
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
__version__ = "0.5.1"
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|