feat: enhance WebSocket error handling and cleanup logic in simulation and ESP32 libraries

pull/47/head
David Montero Crespo 2026-03-17 02:28:08 -03:00
parent f5323aa557
commit fdbc37b69b
3 changed files with 50 additions and 10 deletions

View File

@ -40,8 +40,14 @@ async def simulation_websocket(websocket: WebSocket, client_id: str):
logger.info('[%s] system event: %s', client_id, data.get('event'))
elif event_type == 'error':
logger.error('[%s] error: %s', client_id, data.get('message'))
elif event_type == 'serial_output':
text = data.get('data', '')
logger.info('[%s] serial_output uart=%s len=%d: %r', client_id, data.get('uart', 0), len(text), text[:80])
payload = json.dumps({'type': event_type, 'data': data})
await manager.send(client_id, payload)
try:
await manager.send(client_id, payload)
except Exception as _send_exc:
logger.debug('[%s] qemu_callback send failed (%s): %s', client_id, event_type, _send_exc)
def _use_lib() -> bool:
return esp_lib_manager.is_available()
@ -177,13 +183,19 @@ async def simulation_websocket(websocket: WebSocket, client_id: str):
)
except WebSocketDisconnect:
manager.disconnect(client_id)
qemu_manager.stop_instance(client_id)
await esp_lib_manager.stop_instance(client_id)
esp_qemu_manager.stop_instance(client_id)
# Guard: only clean up if this coroutine still owns the connection for client_id.
# A newer simulation_websocket may have already connected and replaced us.
if manager.active_connections.get(client_id) is websocket:
manager.disconnect(client_id)
qemu_manager.stop_instance(client_id)
await esp_lib_manager.stop_instance(client_id)
esp_qemu_manager.stop_instance(client_id)
else:
logger.info('[%s] old WS session ended; newer session is active — skipping cleanup', client_id)
except Exception as exc:
logger.error('WebSocket error for %s: %s', client_id, exc)
manager.disconnect(client_id)
qemu_manager.stop_instance(client_id)
await esp_lib_manager.stop_instance(client_id)
esp_qemu_manager.stop_instance(client_id)
if manager.active_connections.get(client_id) is websocket:
manager.disconnect(client_id)
qemu_manager.stop_instance(client_id)
await esp_lib_manager.stop_instance(client_id)
esp_qemu_manager.stop_instance(client_id)

View File

@ -160,7 +160,10 @@ class EspLibManager:
logger.info('Launching esp32_worker for %s (machine=%s, script=%s, python=%s)',
client_id, machine, _WORKER_SCRIPT, sys.executable)
await callback('system', {'event': 'booting'})
try:
await callback('system', {'event': 'booting'})
except Exception as exc:
logger.warning('start_instance %s: booting event delivery failed: %s', client_id, exc)
try:
proc = subprocess.Popen(
@ -362,6 +365,17 @@ class EspLibManager:
raw = raw.strip()
if not raw:
continue
# With -nographic, serial0 is connected to the stdio mux so
# qemu_chr_fe_write() writes the raw UART byte to fd 1 just
# before picsimlab_uart_tx_event emits the JSON line. Strip
# any prefix bytes before the JSON object marker.
idx = raw.find(b'{"type":')
if idx > 0:
raw = raw[idx:]
elif idx < 0:
logger.debug('[%s] ignoring non-JSON worker line: %s',
client_id, raw[:200])
continue
try:
event = json.loads(raw)
except Exception:

View File

@ -307,6 +307,13 @@ export const useSimulatorStore = create<SimulatorState>((set, get) => {
bridge.onCrash = () => {
set({ esp32CrashBoardId: id });
};
bridge.onDisconnected = () => {
set((s) => {
const boards = s.boards.map((b) => b.id === id ? { ...b, running: false } : b);
const isActive = s.activeBoardId === id;
return { boards, ...(isActive ? { running: false } : {}) };
});
};
bridge.onLedcUpdate = (update) => {
// Route LEDC duty cycles to PinManager as PWM (0.01.0).
// If gpio is known (from GPIO out_sel sync), use the actual GPIO pin;
@ -619,6 +626,13 @@ export const useSimulatorStore = create<SimulatorState>((set, get) => {
if (boardPm) boardPm.triggerPinChange(gpioPin, state);
};
bridge.onCrash = () => { set({ esp32CrashBoardId: boardId }); };
bridge.onDisconnected = () => {
set((s) => {
const boards = s.boards.map((b) => b.id === boardId ? { ...b, running: false } : b);
const isActive = s.activeBoardId === boardId;
return { boards, ...(isActive ? { running: false } : {}) };
});
};
bridge.onLedcUpdate = (update) => {
const boardPm = pinManagerMap.get(boardId);
if (boardPm && typeof boardPm.updatePwm === 'function') {