Smail/test/tools/runtime_fault_calibration.py
smail 079ebee495
Some checks are pending
Security Checks / security (push) Waiting to run
补充SQL切换联合故障验收工具并校准系统调用边界
2026-09-22 00:58:02 +08:00

110 lines
4.6 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""在独立微型进程中校准系统调用延迟,不附加正式或测试应用。"""
import hashlib
import json
import os
from pathlib import Path
import signal
import subprocess
import sys
import time
CHILD = r'''
import json, os, sys, time
from pathlib import Path
root = Path(sys.argv[1])
while not (root / 'gate').exists():
time.sleep(0.01)
fd = os.open(root, os.O_RDONLY | os.O_DIRECTORY)
start = time.monotonic()
os.rename('unrelated.tmp', 'unrelated.json', src_dir_fd=fd, dst_dir_fd=fd)
(root / 'unrelated-result.json').write_text(json.dumps({'seconds': time.monotonic() - start}))
(root / 'ready').touch()
start = time.monotonic()
os.rename('candidate.tmp', 'config.json', src_dir_fd=fd, dst_dir_fd=fd)
(root / 'done.json').write_text(json.dumps({'seconds': time.monotonic() - start}))
os.close(fd)
'''
def wait_until(predicate, timeout=10):
deadline = time.monotonic() + timeout
while time.monotonic() < deadline:
if predicate():
return
time.sleep(0.02)
raise AssertionError('校准等待超时')
def tracer_pid(pid):
lines = Path('/proc/' + str(pid) + '/status').read_text().splitlines()
return int(next(line.split(':')[1] for line in lines if line.startswith('TracerPid:')))
def calibrate(root, phase):
assert phase in ('enter', 'exit')
root.mkdir(mode=0o700)
for name, value in [('config.json', 'old'), ('candidate.tmp', 'new'), ('unrelated.tmp', 'other')]:
(root / name).write_text(value)
child = subprocess.Popen([sys.executable, '-c', CHILD, str(root)],
stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
trace = None
details = {'phase': phase, 'passed': False}
try:
with (root / 'trace.stderr').open('w') as errors:
trace = subprocess.Popen(['strace', '-f', '-ttt', '-T', '-yy',
'-e', 'trace=rename,renameat,renameat2', '-P', 'config.json',
'-e', 'inject=rename,renameat,renameat2:delay_' + phase + '=3s',
'-o', str(root / 'trace.log'), '-p', str(child.pid)],
stdout=subprocess.DEVNULL, stderr=errors)
wait_until(lambda: tracer_pid(child.pid) == trace.pid)
wait_until(lambda: 'attached' in (root / 'trace.stderr').read_text())
(root / 'gate').touch()
wait_until(lambda: (root / 'ready').exists())
unrelated = json.loads((root / 'unrelated-result.json').read_text())
assert unrelated['seconds'] < 1, '路径过滤没有排除无关替换'
time.sleep(0.4)
observed = (root / 'config.json').read_text()
assert observed == ('old' if phase == 'enter' else 'new'), '延迟边界不符合预期'
assert 'renameat(' in (root / 'trace.log').read_text(), '延迟期间没有可观测的调用记录'
assert not (root / 'done.json').exists(), '替换调用未处于延迟窗口'
assert child.wait(timeout=10) == 0
assert trace.wait(timeout=5) == 0
duration = json.loads((root / 'done.json').read_text())['seconds']
assert 2.5 <= duration <= 8
assert (root / 'config.json').read_text() == 'new'
details.update(passed=True, unrelated_seconds=unrelated['seconds'],
target_seconds=duration, observed_during_delay=observed,
child_exited=True, tracer_exited=True,
trace_sha256=hashlib.sha256((root / 'trace.log').read_bytes()).hexdigest())
finally:
# 只回收本函数亲自创建的子进程,不接收外部 PID,也不删除任何证据。
if child.poll() is None:
child.kill()
child.wait(timeout=10)
if trace is not None and trace.poll() is None:
trace.send_signal(signal.SIGTERM)
trace.wait(timeout=10)
(root / 'result.json').write_text(json.dumps(details, indent=2), encoding='utf-8')
return details
def main():
assert os.geteuid() == 0
root = Path('/var/tmp') / ('smail-review-fault-calibration-' + time.strftime('%Y%m%d-%H%M%S'))
root.mkdir(mode=0o700)
result = {'root': str(root), 'passed': False}
try:
result['strace_version'] = subprocess.check_output(['strace', '-V'], text=True).splitlines()[0]
result['ptrace_scope'] = Path('/proc/sys/kernel/yama/ptrace_scope').read_text().strip()
result['cases'] = [calibrate(root / phase, phase) for phase in ('enter', 'exit')]
result['passed'] = True
finally:
(root / 'result.json').write_text(json.dumps(result, indent=2), encoding='utf-8')
print(json.dumps(result, indent=2), flush=True)
if __name__ == '__main__':
main()