Files
Sprinter-SDCC/toolchain/sdbg/server.py
T
2026-09-17 23:22:37 +03:00

744 lines
38 KiB
Python

"""Постоянный владелец DebugSession и локальный JSON-RPC для адаптеров."""
from __future__ import annotations
from collections import deque
import json
from pathlib import Path
import socket
import socketserver
import string
import threading
import time
import uuid
from .session import DebugSession, SessionError
from .transport import BridgeError
from .macros import validate_log_message
class SessionController:
CONTROL_TTL = 30.0
def __init__(self, session: DebugSession):
self.session = session
self.lock = threading.RLock()
self.changed = threading.Condition()
self.events = deque(maxlen=1024)
self.dropped_events = 0
self.sequence = 0
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] = {}
self.attached = session.attach()
self.mame_console = bool(self.attached.get('capabilities', {}).get('console_print'))
self._emit('stopped', {'reason': 'entry', 'location': self.attached['location']})
self._install_macro_logs()
self.poller = threading.Thread(target=self._poll, name='sdbg-poller', daemon=True)
self.poller.start()
def _install_macro_logs(self) -> None:
"""Авторские точки принадлежат пакету сборки, не DAP source-набору."""
created = []
try:
for macro in getattr(self.session.model, 'logpoints', []):
if not macro['verified'] or macro['source'] in self.session.model.stale_sources:
self._emit('output', {'category': 'stderr',
'output': 'sdbg: SDBG_LOG ' + macro['tag'] +
' не активирован: ' + str(macro.get('reason') or 'устаревший исходник') + '\n'})
continue
self._validate_log_message(macro['message'])
item = self.session.break_anchor(macro, enabled=False)
created.append(item['id'])
self.breakpoint_info[item['id']] = {
'kind': 'log', 'message': macro['message'],
'condition': macro['condition'], 'module': macro['module'],
'tag': macro['tag'], 'locations': item['locations'], 'hits': 0,
'owner': 'build', 'guard_conditions': item['conditions'],
}
if created:
self.session.activate_breakpoints()
except BaseException:
for identifier in created:
self.breakpoint_info.pop(identifier, None)
try: self.session.clear_breakpoint(identifier)
except BaseException: pass
raise
def _emit(self, name: str, body: dict) -> None:
with self.changed:
if len(self.events) == self.events.maxlen:
self.dropped_events += 1
self.sequence += 1
self.events.append({'seq': self.sequence, 'event': name, 'body': body})
self.changed.notify_all()
def _poll(self) -> None:
next_idle_snapshot = 0.0
while not self.closed:
if not self.running:
delay = next_idle_snapshot - time.monotonic()
if delay > 0:
time.sleep(min(delay, .05))
continue
try:
with self.lock:
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)
elif self._handle_logpoints(location):
self.session.bridge.request('continue')
continue
else:
self.running = False
self._emit('stopped', {'reason': 'breakpoint', 'location': location})
elif snapshot['state'] == 'running' and not self.running:
# Команда из родного окна MAME тоже меняет состояние CPU.
self.running = True
self._emit('continued', {'reason': 'external'})
elif snapshot['state'] == 'invalidated':
self.running = False
self.source_step = None
self.closed = True
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
self.closed = True
self._emit('invalidated', {'reason': str(error)})
time.sleep(.01)
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
def _owner(arguments: dict) -> str:
owner = arguments.get('owner', 'cli')
if not isinstance(owner, str) or not owner or len(owner) > 128:
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
with self.changed:
while self.sequence <= after and not self.closed and time.monotonic() < deadline:
self.changed.wait(deadline - time.monotonic())
return {'events': [event for event in self.events if event['seq'] > after],
'first': self.events[0]['seq'] if self.events else self.sequence + 1,
'last': self.sequence, 'lost': self.dropped_events,
'closed': self.closed}
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_program_memory', 'disassemble_logical',
'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}
if method == 'list_breakpoints':
breakpoints = []
for identifier, info in sorted(self.breakpoint_info.items()):
guards = info.get('guard_conditions', [])
locations = []
for index, location in enumerate(info['locations']):
locations.append({**location,
'bank_guard': guards[index] if index < len(guards)
else ''})
breakpoints.append({'id': identifier,
'owner': info.get('owner', 'dap'),
'kind': info['kind'], 'hits': info['hits'],
'locations': locations,
'tag': info.get('tag')})
return {'breakpoints': breakpoints}
if method == 'snapshot':
return self.session.bridge.request('snapshot')
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')
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':
mapping = self.session.refresh()
return {'generation': mapping.generation, 'registers': mapping.registers,
'bank_pages': mapping.bank_pages}
if method == 'read_memory':
address = int(arguments['address'])
length = int(arguments['length'])
if address < 0 or address > 0xffff or length < 1 or length > 256 or \
address + length > 0x10000:
raise SessionError('Чтение памяти: адрес 0..65535, длина 1..256 без выхода за 64 КБ')
mapping = self.session.refresh()
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 == 'disassemble_logical':
address = int(arguments['address'])
length = int(arguments['length'])
if address < 0 or address > 0xffff or length < 1 or length > 256 or \
address + length > 0x10000:
raise SessionError('Дизассемблирование: адрес 0..0xffff, длина 1..256')
mapping = self.session.refresh()
result = self.session.bridge.request('disassemble_logical',
address=address, length=length)
return {**result, 'bank_pages': mapping.bank_pages}
if method == 'variables':
return self.session.model.variables
if method == 'read_variable':
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'],
'hits': 0, 'owner': owner,
'guard_conditions': item['conditions']}
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'],
'hits': 0, 'owner': owner,
'guard_conditions': item['conditions']}
return item
if method == 'clear_breakpoint':
identifier = int(arguments['id'])
owner = arguments.get('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)
return result
if method == 'clear_owned_breakpoints':
owner = arguments.get('owner')
if not isinstance(owner, str) or not owner:
raise SessionError('Для очистки точек требуется 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)
return {'cleared': identifiers}
if method == 'set_source_breakpoints':
specifications = arguments.get('breakpoints')
if specifications is None:
specifications = [{'line': line} for line in arguments.get('lines', [])]
return self._set_source_breakpoints(arguments['file'], specifications)
if method == 'set_function_breakpoints':
return self._set_function_breakpoints(arguments.get('names', []))
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())
self.running = False
self._emit('stopped', {'reason': 'pause', 'location': location})
return location
if method == 'step':
if self.running:
raise SessionError('CPU уже выполняется; сначала Pause')
count = arguments.get('count', 1)
if type(count) is not int or count < 1 or count > 64:
raise SessionError('Число машинных шагов должно быть 1..64')
self._control(arguments)
self.session.bridge.request('step', count=count)
self.running = True
self._emit('continued', {'reason': 'step'})
location = self.session.where(self.session.bridge.wait_stopped())
self.running = False
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)
@staticmethod
def _source_identity(location: dict) -> frozenset[tuple[str, int]]:
return frozenset((item['file'], int(item['line']))
for item in location.get('sources', []))
def _has_stop_breakpoint(self, location: dict) -> bool:
address = location.get('link_address')
return any(info['kind'] == 'stop' and
any(item.get('link_address') == address for item in info['locations'])
for info in self.breakpoint_info.values())
def _source_step(self, kind: str) -> dict:
commands = {'into': 'step', 'over': 'step_over', 'out': 'step_out'}
if kind not in commands:
raise SessionError('Неизвестный вид source step')
if self.running:
raise SessionError('CPU уже выполняется; сначала Pause')
start = self.session.where()
if start.get('stale_source'):
raise SessionError('Исходник изменён после сборки; пересоберите программу')
initial = self._source_identity(start)
if not initial:
raise SessionError('Текущий PC не имеет проверенной C-позиции')
self.source_step = {
'initial': initial, 'kind': kind, 'command': commands[kind],
'instructions': 1, 'location': start,
}
try:
self.session.bridge.request(commands[kind])
except BaseException:
self.source_step = None
raise
if kind == 'out':
# Первый out выходит из машинного frame. У банкового вызова
# дальше идём через over до первой позиции C вызывающей функции.
self.source_step['command'] = 'step_over'
self.running = True
self._emit('continued', {'reason': 'step'})
return {'accepted': True}
def _source_step_stopped(self, location: dict) -> None:
step = self.source_step
self._handle_logpoints(location)
reason = None
if self._has_stop_breakpoint(location):
reason = 'breakpoint'
elif self._source_identity(location) and self._source_identity(location) != step['initial']:
reason = 'step'
elif step['instructions'] >= 512:
reason = 'step'
self._emit('output', {'category': 'stderr',
'output': 'sdbg: source step достиг лимита; CPU остановлен\n'})
if reason is not None:
self.source_step = None
self.running = False
self._emit('stopped', {'reason': reason, 'location': location})
return
# Машинный over может ждать клавишу сколь угодно долго. Пока CPU
# выполняется, этот автомат не занимает session lock и не ставит
# таймер; пользователь может направить ввод в MAME или нажать Pause.
self.session.bridge.request(step['command'])
step['instructions'] += 1
step['location'] = location
def _set_source_breakpoints(self, filename: str, specifications) -> dict:
filename = str(Path(filename).resolve())
requested = []
for value in specifications:
if not isinstance(value, dict):
raise SessionError('Описание breakpoint должно быть объектом')
line = int(value['line'])
if line <= 0 or any(item['line'] == line for item in requested):
raise SessionError('Номер строки должен быть положительным и уникальным')
if value.get('condition') or value.get('hitCondition'):
raise SessionError('Условия и hitCondition пока не поддержаны')
message = value.get('logMessage')
if message is not None:
self._validate_log_message(message)
requested.append({'line': line, 'logMessage': message})
created = []
results = []
try:
for specification in requested:
line = specification['line']
item = self.session.break_line(filename, line, enabled=False)
created.append(item['id'])
kind = 'log' if specification['logMessage'] is not None else 'stop'
self.breakpoint_info[item['id']] = {
'kind': kind, 'message': specification['logMessage'],
'locations': item['locations'], 'hits': 0,
'owner': 'dap', 'guard_conditions': item['conditions'],
}
results.append({'line': line, 'verified': True,
'logMessage': specification['logMessage'], **item})
except BaseException:
for identifier in created:
self.breakpoint_info.pop(identifier, None)
try:
self.session.clear_breakpoint(identifier)
except BaseException:
pass
raise
previous = self.source_breakpoints.get(filename, [])
for identifier in previous:
self.breakpoint_info.pop(identifier, None)
self.session.clear_breakpoint(identifier)
self.source_breakpoints[filename] = created
self.session.activate_breakpoints()
return {'file': filename, 'breakpoints': results}
def _set_function_breakpoints(self, names) -> dict:
requested = []
for value in names:
name = str(value)
if not name or name in requested:
raise SessionError('Имя функции должно быть непустым и уникальным')
requested.append(name)
created = []
results = []
try:
for name in requested:
item = self.session.break_function(name, enabled=False)
created.append(item['id'])
self.breakpoint_info[item['id']] = {
'kind': 'stop', 'message': None, 'locations': item['locations'],
'hits': 0, 'owner': 'dap',
'guard_conditions': item['conditions']}
results.append({'name': name, 'verified': True, **item})
except BaseException:
for identifier in created:
self.breakpoint_info.pop(identifier, None)
try:
self.session.clear_breakpoint(identifier)
except BaseException:
pass
raise
for identifier in self.function_breakpoints:
self.breakpoint_info.pop(identifier, None)
self.session.clear_breakpoint(identifier)
self.function_breakpoints = created
self.session.activate_breakpoints()
return {'breakpoints': results}
@staticmethod
def _validate_log_message(message: str) -> None:
try:
validate_log_message(message)
except ValueError as error:
raise SessionError(str(error)) from error
def _read_log_variable(self, name: str, module: str | None) -> dict:
if module is None:
return self.session.read_variable(name)
candidates = [item for item in self.session.model.variables
if item['name'] == name and item['module'] in (None, module)]
if len(candidates) != 1:
raise SessionError('Переменная не найдена или имя неоднозначно')
return self.session.read_variable(name, candidates[0]['module'])
def _render_log_message(self, message: str, module: str | None = None) -> str:
output = []
for literal, name, _, _ in string.Formatter().parse(message):
output.append(literal)
if name is not None:
try:
output.append(str(self._read_log_variable(name, module)['value']))
except SessionError as error:
output.append('<unavailable: ' + str(error) + '>')
return ''.join(output)
def _handle_logpoints(self, location: dict) -> bool:
address = location.get('link_address')
matched_logs = []
matched_any_log = False
matched_stop = False
for info in self.breakpoint_info.values():
if not any(item.get('link_address') == address for item in info['locations']):
continue
if info['kind'] == 'stop':
matched_stop = True
else:
matched_any_log = True
condition = info.get('condition')
if condition is not None:
try:
if not self._read_log_variable(condition, info.get('module'))['value']:
continue
except SessionError as error:
if not info.get('condition_warned'):
info['condition_warned'] = True
self._emit('output', {'category': 'stderr',
'output': 'sdbg: SDBG_LOGIF ' + info.get('tag', '') +
': ' + str(error) + '\n'})
continue
info['hits'] += 1
matched_logs.append(info)
for info in matched_logs:
rendered = self._render_log_message(info['message'], info.get('module'))
self._emit('output', {'category': 'console',
'output': rendered + '\n',
'location': location, 'hit': info['hits'],
'tag': info.get('tag')})
if self.mame_console:
try:
self.session.bridge.request('console_print', text=rendered)
except (BridgeError, OSError) as error:
if not info.get('console_warned'):
info['console_warned'] = True
self._emit('output', {'category': 'stderr',
'output': 'sdbg: MAME console: ' + str(error) + '\n'})
return matched_any_log and not matched_stop
class _ThreadedUnixServer(socketserver.ThreadingMixIn, socketserver.UnixStreamServer):
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)
self.controller = controller
self.path.parent.mkdir(parents=True, exist_ok=True)
if self.path.exists():
try:
with socket.socket(socket.AF_UNIX) as probe:
probe.connect(str(self.path))
except OSError:
self.path.unlink()
else:
raise OSError('RPC socket уже занят: ' + str(self.path))
controller_ref = controller
class Handler(socketserver.StreamRequestHandler):
def handle(self):
raw = self.rfile.readline(1_048_577)
response = {'id': None, 'ok': False}
try:
if len(raw) > 1_048_576:
raise ValueError('RPC-запрос слишком велик')
request = json.loads(raw)
if not isinstance(request, dict) or not isinstance(request.get('method'), str):
raise ValueError('Неверная структура RPC-запроса')
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,
'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())
self.server = _ThreadedUnixServer(str(self.path), Handler)
self.path.chmod(0o600)
def serve_forever(self):
try:
self.server.serve_forever(poll_interval=.1)
finally:
self.close()
def close(self):
self.server.server_close()
self.path.unlink(missing_ok=True)
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))
client.sendall((json.dumps(request, ensure_ascii=False) + '\n').encode())
file = client.makefile('rb')
raw = file.readline(1_048_577)
if not raw:
raise SessionError('RPC server закрыл соединение без ответа')
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']