- 将网页输入和外设操作的仿真调度移到状态锁外,覆盖复位、退订和断线释放路径 - 通过真实 GPIO 边沿缓存送丝使能电平,移除快照及激光和波形复位中的反向 GPIO 读取 - 为每个 Renode 会话复制用户配置并设置独立的 Monitor 历史文件,保留原配置及其他设置 - 添加时钟锁和 GPIO 锁回归探针、会话配置与双实例隔离用例,并补充运行说明
154 lines
7.1 KiB
Python
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()
|