Files
chuan 771a96fb51 fix: 修复仿真交互锁顺序并隔离会话历史
- 将网页输入和外设操作的仿真调度移到状态锁外,覆盖复位、退订和断线释放路径
- 通过真实 GPIO 边沿缓存送丝使能电平,移除快照及激光和波形复位中的反向 GPIO 读取
- 为每个 Renode 会话复制用户配置并设置独立的 Monitor 历史文件,保留原配置及其他设置
- 添加时钟锁和 GPIO 锁回归探针、会话配置与双实例隔离用例,并补充运行说明
2026-10-26 11:27:04 +08:00

154 lines
7.1 KiB
Python

"""确认实例归属、等待 Monitor 就绪,并通过正常退出流程结束 Renode 子进程"""
import json
import os
from pathlib import Path
import re
import subprocess
import sys
import time
from scripts.environment import RUNTIME
def create_session_config(directory):
"""复制用户 Renode 配置并隔离本会话的历史文件,避免并行 Monitor 写入共享文件而崩溃
只改变历史文件路径,不更改用户原配置;返回配置由调用方通过 --config 传给子进程
"""
directory = Path(directory)
profile = Path(os.environ.get('APPDATA', Path.home() / 'AppData/Roaming')) / 'renode/config'
original = profile.read_bytes() if profile.is_file() else b''
contents = original.decode('utf-8-sig')
newline = '\r\n' if '\r\n' in contents else '\n'
if contents and not contents.endswith(('\n', '\r')):
contents += newline
history = 'history-path = ' + (directory / 'monitor-history').as_posix()
general = re.search(r'(?ims)^[ \t]*\[general\][ \t]*\r?\n(.*?)(?=^[ \t]*\[|\Z)', contents)
if general:
body = general.group(1)
if re.search(r'(?im)^[ \t]*history-path[ \t]*=', body):
body = re.sub(r'(?im)^[ \t]*history-path[ \t]*=[^\r\n]*', lambda _: history, body)
else:
body = history + newline + body
contents = contents[:general.start(1)] + body + contents[general.end(1):]
else:
contents += newline + '[general]' + newline + history + newline
config = directory / 'monitor.config'
# 保留原文件 BOM 和换行,其他设置与注释不经配置解析器重排
config.write_bytes((b'\xef\xbb\xbf' if original.startswith(b'\xef\xbb\xbf') else b'') + contents.encode('utf-8'))
return config
def exit_previous():
"""仅停止运行记录中属于本项目的实例,等待原启动器完成资源清理"""
from scripts.renode_monitor import Monitor
state_path = RUNTIME / 'run.json'
if not state_path.exists():
return False
state = json.loads(state_path.read_text(encoding='utf-8'))
pid = int(state['pid'])
query = f'Get-CimInstance Win32_Process -Filter "ProcessId={pid}" | Select-Object Name,CommandLine | ConvertTo-Json -Compress'
result = subprocess.run(['powershell.exe', '-NoProfile', '-Command', query],
capture_output=True, text=True, timeout=10, check=True)
if not result.stdout.strip():
state_path.unlink(missing_ok=True)
return False
process = json.loads(result.stdout)
# PID 可能被系统复用,必须同时核对进程名与本项目生成的启动脚本目录
command = (process.get('CommandLine') or '').replace('\\', '/').lower()
expected = (RUNTIME / 'tmp').as_posix().lower() + '/'
if process['Name'].lower() != 'renode.exe' or expected not in command:
raise RuntimeError(f'Refusing to stop PID {pid}: it does not match this project')
if state.get('control_break') is True:
request_graceful_exit(pid)
else:
# 旧会话未声明独立进程组,不能假定其 PID 可接收定向组信号
try:
monitor = Monitor(int(state['monitor_port']))
except (OSError, RuntimeError) as error:
raise RuntimeError('This older session requires its monitor to stop; close the interactive monitor connection or enter quit there') from error
try:
monitor.sock.sendall(b'quit\n')
finally:
monitor.sock.close()
# 原启动器在子进程与串口桥接退出后才移除状态文件,不能只等待 Renode PID 消失
deadline = time.monotonic() + 15
while state_path.exists() and time.monotonic() < deadline:
time.sleep(.1)
if state_path.exists():
raise RuntimeError('Previous runner has not finished shutting down; no new instance was started')
print('Previous project instance stopped.', flush=True)
return True
def wait_for_monitor(proc, port, timeout=40):
"""在启动期限内重试 Monitor 握手,子进程提前退出时立即报告错误"""
from scripts.renode_monitor import Monitor
deadline = time.monotonic() + timeout
while time.monotonic() < deadline:
if proc.poll() is not None:
raise RuntimeError(f'Renode exited with code {proc.returncode}; see console output')
try:
return Monitor(port)
except (OSError, RuntimeError):
time.sleep(.1)
raise RuntimeError('Timed out waiting for Renode monitor; see console output')
def request_graceful_exit(pid):
"""向调用方确认归属的独立 Renode 进程组发送 CTRL_BREAK,触发正常退出"""
# 独立辅助进程临时附着目标控制台,避免改变启动器自身的控制台归属
# 组号必须非零,不能将退出事件广播给整个控制台
helper = '''import ctypes, sys, time
kernel = ctypes.WinDLL('kernel32', use_last_error=True)
pid = int(sys.argv[1])
if pid <= 0:
raise ValueError('A nonzero process-group ID is required')
kernel.FreeConsole()
if not kernel.AttachConsole(pid):
raise ctypes.WinError(ctypes.get_last_error())
try:
if not kernel.GenerateConsoleCtrlEvent(1, pid):
raise ctypes.WinError(ctypes.get_last_error())
time.sleep(.1)
finally:
kernel.FreeConsole()
'''
result = subprocess.run([sys.executable, '-c', helper, str(pid)], capture_output=True,
text=True, timeout=5, creationflags=subprocess.CREATE_NO_WINDOW)
if result.returncode:
raise RuntimeError(f'Cannot request normal Renode shutdown: {result.stderr.strip()}')
def stop_process(proc, monitor=None, *, control_break=False):
"""等待正常退出真正完成,失败或重复中断时仍保留监督状态与镜像锁
超时只提示操作者,不强杀 Renode,以免跳过外设 Dispose 和 Flash 持久化
"""
requested = warned = False
deadline = time.monotonic() + 15
while True:
try:
if proc.poll() is not None:
break
if not requested:
requested = True
if control_break:
request_graceful_exit(proc.pid)
elif monitor:
monitor.sock.sendall(b'quit\n')
else:
raise RuntimeError('No normal shutdown channel is available')
proc.wait(timeout=1)
except subprocess.TimeoutExpired:
if not warned and time.monotonic() >= deadline:
warned = True
print('Renode has not finished normal shutdown; keeping this runner and Flash locks. Enter quit in its monitor or retry just stop.', file=sys.stderr, flush=True)
except KeyboardInterrupt:
print('Still waiting for Renode to exit normally; Flash locks remain held. Enter quit in its monitor or retry just stop.', file=sys.stderr, flush=True)
except (OSError, RuntimeError, subprocess.SubprocessError) as error:
warned = True
print(f'Normal shutdown request failed: {error}. Keeping this runner and Flash locks; enter quit in its monitor or retry just stop.', file=sys.stderr, flush=True)
if monitor:
monitor.sock.close()