Verify visible hello MCP run and add shared control lease
This commit is contained in:
+105
-5
@@ -9,6 +9,7 @@ import socketserver
|
||||
import string
|
||||
import threading
|
||||
import time
|
||||
import uuid
|
||||
|
||||
from .session import DebugSession, SessionError
|
||||
from .transport import BridgeError
|
||||
@@ -16,6 +17,8 @@ from .macros import validate_log_message
|
||||
|
||||
|
||||
class SessionController:
|
||||
CONTROL_TTL = 30.0
|
||||
|
||||
def __init__(self, session: DebugSession):
|
||||
self.session = session
|
||||
self.lock = threading.RLock()
|
||||
@@ -26,6 +29,11 @@ class SessionController:
|
||||
self.running = False
|
||||
self.source_step = None
|
||||
self.closed = False
|
||||
self.session_id = uuid.uuid4().hex
|
||||
self.control_owner: str | None = None
|
||||
self.control_deadline = 0.0
|
||||
self.owner_deadlines: dict[str, float] = {}
|
||||
self.held_inputs: dict[str, set[tuple[str, int]]] = {}
|
||||
self.source_breakpoints: dict[str, list[int]] = {}
|
||||
self.function_breakpoints: list[int] = []
|
||||
self.breakpoint_info: dict[int, dict] = {}
|
||||
@@ -84,6 +92,7 @@ class SessionController:
|
||||
snapshot = self.session.bridge.request('snapshot')
|
||||
if snapshot['state'] == 'stopped':
|
||||
if self.running:
|
||||
self._release_all_inputs()
|
||||
location = self.session.where(snapshot)
|
||||
if self.source_step is not None:
|
||||
self._source_step_stopped(location)
|
||||
@@ -104,6 +113,7 @@ class SessionController:
|
||||
self._emit('invalidated', {'reason': 'reset_or_load'})
|
||||
if not self.running:
|
||||
next_idle_snapshot = time.monotonic() + .5
|
||||
self._reap_owners()
|
||||
except (BridgeError, SessionError, ValueError, OSError) as error:
|
||||
self.running = False
|
||||
self.source_step = None
|
||||
@@ -114,6 +124,11 @@ class SessionController:
|
||||
def close(self) -> None:
|
||||
self.closed = True
|
||||
self.poller.join(timeout=1)
|
||||
with self.lock:
|
||||
try:
|
||||
self._release_all_inputs()
|
||||
except (BridgeError, SessionError, OSError):
|
||||
pass
|
||||
self.session.bridge.close()
|
||||
|
||||
@staticmethod
|
||||
@@ -123,8 +138,59 @@ class SessionController:
|
||||
raise SessionError('owner должен быть непустой строкой до 128 символов')
|
||||
return owner
|
||||
|
||||
def _control(self, arguments: dict) -> dict:
|
||||
"""Захватить или продлить управление; чужой живой lease не отбирать."""
|
||||
owner = self._owner(arguments)
|
||||
self._touch_owner(owner)
|
||||
now = time.monotonic()
|
||||
if self.control_owner is not None and self.control_deadline <= now:
|
||||
self.control_owner = None
|
||||
if self.control_owner not in (None, owner):
|
||||
raise SessionError(f'CPU управляет {self.control_owner}; дождитесь release или истечения lease')
|
||||
if self.control_owner != owner:
|
||||
self.control_owner = owner
|
||||
self._emit('control', {'owner': owner})
|
||||
self.control_deadline = now + self.CONTROL_TTL
|
||||
return {'owner': owner, 'expires_in': self.CONTROL_TTL}
|
||||
|
||||
def _touch_owner(self, owner: str) -> None:
|
||||
if owner.startswith('mcp:'):
|
||||
self.owner_deadlines[owner] = time.monotonic() + self.CONTROL_TTL
|
||||
|
||||
def _release_inputs(self, owner: str) -> None:
|
||||
for tag, mask in self.held_inputs.pop(owner, set()):
|
||||
self.session.bridge.request('key', tag=tag, mask=mask, down=False)
|
||||
|
||||
def _release_all_inputs(self) -> None:
|
||||
for owner in list(self.held_inputs):
|
||||
self._release_inputs(owner)
|
||||
|
||||
def _reap_owners(self) -> None:
|
||||
now = time.monotonic()
|
||||
for owner, deadline in list(self.owner_deadlines.items()):
|
||||
if deadline > now:
|
||||
continue
|
||||
self._release_inputs(owner)
|
||||
identifiers = [identifier for identifier, info in self.breakpoint_info.items()
|
||||
if info.get('owner') == owner]
|
||||
for identifier in identifiers:
|
||||
self.session.clear_breakpoint(identifier)
|
||||
self.breakpoint_info.pop(identifier, None)
|
||||
if self.control_owner == owner:
|
||||
self.control_owner = None
|
||||
self.control_deadline = 0.0
|
||||
self._emit('control', {'owner': None})
|
||||
self.owner_deadlines.pop(owner, None)
|
||||
self._emit('owner_expired', {'owner': owner, 'cleared_breakpoints': identifiers})
|
||||
|
||||
def call(self, method: str, arguments: dict) -> object:
|
||||
if method == 'events':
|
||||
owner = arguments.get('owner')
|
||||
if owner is not None:
|
||||
with self.lock:
|
||||
if self.control_owner == self._owner(arguments) and \
|
||||
self.control_deadline > time.monotonic():
|
||||
self.control_deadline = time.monotonic() + self.CONTROL_TTL
|
||||
after = int(arguments.get('after', 0))
|
||||
timeout = min(max(float(arguments.get('timeout', 0)), 0), 30)
|
||||
deadline = time.monotonic() + timeout
|
||||
@@ -138,12 +204,32 @@ class SessionController:
|
||||
with self.lock:
|
||||
if self.closed:
|
||||
raise SessionError('Сессия закрыта или инвалидирована')
|
||||
if method == 'claim_control':
|
||||
return self._control(arguments)
|
||||
if method == 'renew_control':
|
||||
owner = self._owner(arguments)
|
||||
self._touch_owner(owner)
|
||||
if self.control_owner == owner and self.control_deadline > time.monotonic():
|
||||
self.control_deadline = time.monotonic() + self.CONTROL_TTL
|
||||
return {'owner': owner, 'has_control': True,
|
||||
'expires_in': self.CONTROL_TTL}
|
||||
return {'owner': owner, 'has_control': False}
|
||||
if method == 'release_control':
|
||||
owner = self._owner(arguments)
|
||||
self._release_inputs(owner)
|
||||
if self.control_owner == owner:
|
||||
self.control_owner = None
|
||||
self.control_deadline = 0.0
|
||||
self._emit('control', {'owner': None})
|
||||
return {'released': owner}
|
||||
if self.running and method in ('where', 'registers', 'read_memory',
|
||||
'read_variable', 'break_line', 'break_function'):
|
||||
raise SessionError('Операция требует остановленного CPU; сначала Pause')
|
||||
if method == 'status':
|
||||
return {**self.attached, 'running': self.running,
|
||||
'event_sequence': self.sequence}
|
||||
'event_sequence': self.sequence, 'session_id': self.session_id,
|
||||
'control_owner': self.control_owner if
|
||||
self.control_deadline > time.monotonic() else None}
|
||||
if method == 'snapshot':
|
||||
return self.session.bridge.request('snapshot')
|
||||
if method == 'mame_console_tail':
|
||||
@@ -152,9 +238,16 @@ class SessionController:
|
||||
if method == 'input_key':
|
||||
if not self.running and arguments['down'] is not False:
|
||||
raise SessionError('Нажатие возможно только при running CPU')
|
||||
return self.session.bridge.request(
|
||||
'key', tag=arguments['tag'], mask=int(arguments['mask']),
|
||||
down=arguments['down'])
|
||||
self._control(arguments)
|
||||
tag, mask = arguments['tag'], int(arguments['mask'])
|
||||
result = self.session.bridge.request('key', tag=tag, mask=mask,
|
||||
down=arguments['down'])
|
||||
held = self.held_inputs.setdefault(self._owner(arguments), set())
|
||||
if arguments['down']:
|
||||
held.add((tag, mask))
|
||||
else:
|
||||
held.discard((tag, mask))
|
||||
return result
|
||||
if method == 'where':
|
||||
return self.session.where()
|
||||
if method == 'registers':
|
||||
@@ -177,6 +270,7 @@ class SessionController:
|
||||
return self.session.read_variable(arguments['name'], arguments.get('module'))
|
||||
if method == 'break_line':
|
||||
owner = self._owner(arguments)
|
||||
self._touch_owner(owner)
|
||||
item = self.session.break_line(arguments['file'], int(arguments['line']))
|
||||
self.breakpoint_info[item['id']] = {
|
||||
'kind': 'stop', 'message': None, 'locations': item['locations'],
|
||||
@@ -184,6 +278,7 @@ class SessionController:
|
||||
return item
|
||||
if method == 'break_function':
|
||||
owner = self._owner(arguments)
|
||||
self._touch_owner(owner)
|
||||
item = self.session.break_function(arguments['name'])
|
||||
self.breakpoint_info[item['id']] = {
|
||||
'kind': 'stop', 'message': None, 'locations': item['locations'],
|
||||
@@ -192,7 +287,8 @@ class SessionController:
|
||||
if method == 'clear_breakpoint':
|
||||
identifier = int(arguments['id'])
|
||||
owner = arguments.get('owner')
|
||||
if owner is not None and self.breakpoint_info.get(identifier, {}).get('owner') != owner:
|
||||
if identifier in self.breakpoint_info and owner is not None and \
|
||||
self.breakpoint_info[identifier].get('owner') != owner:
|
||||
raise SessionError('Точка не принадлежит этому клиенту')
|
||||
result = self.session.clear_breakpoint(identifier)
|
||||
self.breakpoint_info.pop(identifier, None)
|
||||
@@ -217,11 +313,13 @@ class SessionController:
|
||||
if method == 'continue':
|
||||
if self.running:
|
||||
raise SessionError('CPU уже выполняется; сначала Pause')
|
||||
self._control(arguments)
|
||||
result = self.session.bridge.request('continue')
|
||||
self.running = True
|
||||
self._emit('continued', {})
|
||||
return result
|
||||
if method == 'pause':
|
||||
self._control(arguments)
|
||||
self.source_step = None
|
||||
self.session.bridge.request('pause')
|
||||
location = self.session.where(self.session.bridge.wait_stopped())
|
||||
@@ -231,6 +329,7 @@ class SessionController:
|
||||
if method == 'step':
|
||||
if self.running:
|
||||
raise SessionError('CPU уже выполняется; сначала Pause')
|
||||
self._control(arguments)
|
||||
self.session.bridge.request('step')
|
||||
self.running = True
|
||||
self._emit('continued', {'reason': 'step'})
|
||||
@@ -239,6 +338,7 @@ class SessionController:
|
||||
self._emit('stopped', {'reason': 'step', 'location': location})
|
||||
return location
|
||||
if method == 'source_step':
|
||||
self._control(arguments)
|
||||
return self._source_step(arguments.get('kind', 'into'))
|
||||
raise SessionError('Неизвестный RPC-метод: ' + method)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user