Expand shared Sprinter MCP with managed launch, raw reads and key input
This commit is contained in:
@@ -223,10 +223,13 @@ class SessionController:
|
||||
self._emit('control', {'owner': None})
|
||||
return {'released': owner}
|
||||
if self.running and method in ('where', 'registers', 'read_memory',
|
||||
'read_variable', 'break_line', 'break_function'):
|
||||
'read_program_memory', 'read_variable',
|
||||
'break_line', 'break_function'):
|
||||
raise SessionError('Операция требует остановленного CPU; сначала Pause')
|
||||
if method == 'status':
|
||||
return {**self.attached, 'running': self.running,
|
||||
'generation': getattr(self.session.bridge, 'generation',
|
||||
self.attached.get('generation')),
|
||||
'event_sequence': self.sequence, 'session_id': self.session_id,
|
||||
'control_owner': self.control_owner if
|
||||
self.control_deadline > time.monotonic() else None}
|
||||
@@ -235,6 +238,41 @@ class SessionController:
|
||||
if method == 'mame_console_tail':
|
||||
return self.session.bridge.request(
|
||||
'console_tail', count=int(arguments.get('count', 40)))
|
||||
if method == 'list_ports':
|
||||
return self.session.bridge.request('list_ports')
|
||||
if method == 'list_shares':
|
||||
return self.session.bridge.request('list_shares')
|
||||
if method in ('read_share', 'read_vram'):
|
||||
address = int(arguments['address'])
|
||||
length = int(arguments['length'])
|
||||
if address < 0 or address > 0x10000000 or length < 1 or length > 4096:
|
||||
raise SessionError('Share: адрес 0..0x10000000, длина 1..4096')
|
||||
if method == 'read_share':
|
||||
tag = arguments['tag']
|
||||
if not isinstance(tag, str) or not tag or len(tag) > 128:
|
||||
raise SessionError('Нужен точный tag share до 128 символов')
|
||||
return self.session.bridge.request('read_share', tag=tag,
|
||||
address=address, length=length)
|
||||
return self.session.bridge.request('read_vram', address=address,
|
||||
length=length)
|
||||
if method == 'read_screen_pixels':
|
||||
x, y = int(arguments['x']), int(arguments['y'])
|
||||
width, height = int(arguments['width']), int(arguments['height'])
|
||||
if x < 0 or y < 0 or x > 4095 or y > 4095 or \
|
||||
width < 1 or height < 1 or width > 512 or height > 512 or \
|
||||
width * height > 8192:
|
||||
raise SessionError('Экран: координаты 0..4095, размер 1..512, максимум 8192 пикселей')
|
||||
return self.session.bridge.request('screen_pixels', x=x, y=y,
|
||||
width=width, height=height)
|
||||
if method == 'screenshot':
|
||||
result = self.session.bridge.request('screen_snapshot')
|
||||
root = (Path(self.session.bridge.directory).parent / 'snapshot').resolve()
|
||||
path = Path(result['path']).resolve()
|
||||
if path.parent != root or path.suffix.lower() != '.png' or \
|
||||
not path.is_file() or path.stat().st_size != result['size'] or \
|
||||
result['size'] > 8 * 1024 * 1024:
|
||||
raise SessionError('Снимок MAME вне каталога сессии или повреждён')
|
||||
return result
|
||||
if method == 'input_key':
|
||||
if not self.running and arguments['down'] is not False:
|
||||
raise SessionError('Нажатие возможно только при running CPU')
|
||||
@@ -264,6 +302,16 @@ class SessionController:
|
||||
data = self.session.bridge.request('memory', address=address, length=length)
|
||||
return {'address': address, 'length': length, 'hex': data['hex'],
|
||||
'generation': mapping.generation, 'bank_pages': mapping.bank_pages}
|
||||
if method == 'read_program_memory':
|
||||
address = int(arguments['address'])
|
||||
length = int(arguments['length'])
|
||||
if address < 0 or address > 0x3ffff or length < 1 or length > 4096 or \
|
||||
address + length > 0x40000:
|
||||
raise SessionError('Raw program: адрес 0..0x3ffff, длина 1..4096')
|
||||
mapping = self.session.refresh()
|
||||
result = self.session.bridge.request('program_memory',
|
||||
address=address, length=length)
|
||||
return {**result, 'bank_pages': mapping.bank_pages}
|
||||
if method == 'variables':
|
||||
return self.session.model.variables
|
||||
if method == 'read_variable':
|
||||
@@ -556,6 +604,17 @@ class _ThreadedUnixServer(socketserver.ThreadingMixIn, socketserver.UnixStreamSe
|
||||
daemon_threads = True
|
||||
|
||||
|
||||
RPC_MUTATIONS = frozenset({
|
||||
'claim_control', 'renew_control', 'release_control', 'input_key',
|
||||
'break_line', 'break_function', 'clear_breakpoint', 'clear_owned_breakpoints',
|
||||
'set_source_breakpoints', 'set_function_breakpoints',
|
||||
'continue', 'pause', 'step', 'source_step',
|
||||
})
|
||||
RPC_GENERATION_MUTATIONS = RPC_MUTATIONS - {
|
||||
'claim_control', 'renew_control', 'release_control',
|
||||
}
|
||||
|
||||
|
||||
class SessionRpcServer:
|
||||
def __init__(self, path, controller: SessionController):
|
||||
self.path = Path(path)
|
||||
@@ -584,8 +643,31 @@ class SessionRpcServer:
|
||||
arguments = request.get('arguments', {})
|
||||
if not isinstance(arguments, dict):
|
||||
raise ValueError('RPC arguments должен быть объектом')
|
||||
method = request['method']
|
||||
with controller_ref.lock:
|
||||
if method != 'unknown' and (method != 'status' or
|
||||
request.get('session_id') is not None or
|
||||
request.get('build_id') is not None):
|
||||
if request.get('session_id') != controller_ref.session_id or \
|
||||
request.get('build_id') != controller_ref.attached['build_id']:
|
||||
raise SessionError('Устаревшая RPC-сессия или build ID; переподключитесь')
|
||||
if method in RPC_GENERATION_MUTATIONS and \
|
||||
request.get('generation') != \
|
||||
getattr(controller_ref.session.bridge, 'generation', None):
|
||||
raise SessionError('Устаревшая generation; обновите статус сессии')
|
||||
if method != 'events':
|
||||
result = controller_ref.call(method, arguments)
|
||||
result_generation = getattr(controller_ref.session.bridge,
|
||||
'generation', None)
|
||||
if method == 'events':
|
||||
result = controller_ref.call(method, arguments)
|
||||
result_generation = getattr(controller_ref.session.bridge,
|
||||
'generation', None)
|
||||
response = {'id': request.get('id'), 'ok': True,
|
||||
'result': controller_ref.call(request['method'], arguments)}
|
||||
'session_id': controller_ref.session_id,
|
||||
'build_id': controller_ref.attached['build_id'],
|
||||
'generation': result_generation,
|
||||
'result': result}
|
||||
except (BridgeError, SessionError, ValueError, TypeError, OSError, KeyError) as error:
|
||||
response.update(error=str(error))
|
||||
self.wfile.write((json.dumps(response, ensure_ascii=False) + '\n').encode())
|
||||
@@ -604,8 +686,11 @@ class SessionRpcServer:
|
||||
self.path.unlink(missing_ok=True)
|
||||
|
||||
|
||||
def rpc_call(path, method: str, arguments=None, timeout=10):
|
||||
request = {'id': 1, 'method': method, 'arguments': arguments or {}}
|
||||
def rpc_call(path, method: str, arguments=None, timeout=10, *,
|
||||
session_id=None, build_id=None, generation=None):
|
||||
request = {'id': 1, 'method': method, 'arguments': arguments or {},
|
||||
'session_id': session_id, 'build_id': build_id,
|
||||
'generation': generation}
|
||||
with socket.socket(socket.AF_UNIX) as client:
|
||||
client.settimeout(timeout)
|
||||
client.connect(str(path))
|
||||
@@ -617,4 +702,7 @@ def rpc_call(path, method: str, arguments=None, timeout=10):
|
||||
response = json.loads(raw)
|
||||
if not response.get('ok'):
|
||||
raise SessionError(response.get('error', 'Неизвестная RPC-ошибка'))
|
||||
if method != 'status' and (response.get('session_id') != session_id or
|
||||
response.get('build_id') != build_id):
|
||||
raise SessionError('RPC-ответ от другой сессии или сборки')
|
||||
return response['result']
|
||||
|
||||
Reference in New Issue
Block a user