fix: drop stale STK dialogs, finish pending on TP re-send, mute client-gone (v3.6.35)

The live log showed the "stuck" state: after the menu watchdog (60 s)
answered a pending command, the PWA kept the panel; the next click got
{'error': 'no pending command'}, 400 and that reply was fed to the dialog
handler as if it were a card answer.  A page reload then cancelled in-flight
polls, flooding the log with BrokenPipeError tracebacks.

- stkMenuRespond detects the already-answered reply (stkNoPendingError) and
  closes the panel (stkMenuPanelReset) instead of treating it as an answer;
  the 5 s status poll drops the panel whenever the server reports
  pending: false, even when the Phone tab is hidden.
- /api/terminal-profile (POST) and /api/rescue finish a pending command
  with a cancel TR first, so re-sending the TP can never shadow an
  unanswered FETCH.
- _send_json and _serve_static swallow BrokenPipe/ConnectionReset/
  ConnectionAborted with one "CLIENT GONE" line (a reload cancels polls);
  the handlers' state changes still stand.

Tests: stkNoPendingError + the already-answered respond path; version
3.6.35; sw simple-v308; stylesheet rebuilt.
This commit is contained in:
2026-09-29 00:37:56 +03:00
parent 0b7b6b8157
commit bd5dc69c4e
5 changed files with 67 additions and 8 deletions
+16 -3
View File
@@ -31,7 +31,7 @@ from osmocom.tlv import BER_TLV_IE
from osmocom.utils import rpad
VERSION = '3.6.34'
VERSION = '3.6.35'
MAX_ENVELOPE_SEGMENTS = 5 # max SMS segments for outgoing C-APDU in ENVELOPE
@@ -5033,7 +5033,13 @@ class PysimHandler(BaseHTTPRequestHandler):
self.send_header('Content-Type', 'application/json')
self.send_header('Access-Control-Allow-Origin', '*')
self.end_headers()
self.wfile.write(json.dumps(data, ensure_ascii=False).encode())
try:
self.wfile.write(json.dumps(data, ensure_ascii=False).encode())
except (BrokenPipeError, ConnectionResetError, ConnectionAbortedError):
# The browser navigated away or reloaded while the response was in
# flight (a cancelled poll): the request is done and the client is
# gone - one line, no traceback (v3.6.35).
sys.stderr.write('CLIENT GONE: %s %s\n' % (self.command, self.path))
def _read_body(self):
length = int(self.headers.get('Content-Length', 0))
@@ -5104,7 +5110,10 @@ class PysimHandler(BaseHTTPRequestHandler):
if last_modified:
self.send_header('Last-Modified', last_modified)
self.end_headers()
self.wfile.write(data)
try:
self.wfile.write(data)
except (BrokenPipeError, ConnectionResetError, ConnectionAbortedError):
sys.stderr.write('CLIENT GONE: GET %s\n' % rel)
def do_GET(self):
if _test_request_blocked(self.path):
@@ -5698,6 +5707,7 @@ class PysimHandler(BaseHTTPRequestHandler):
self._send_json({'error': 'no terminal profile configured'}, 400)
self._log_resp({'error': 'no terminal profile configured'})
return
_finish_pending_menu(self.server, scc)
sys.stderr.write('RESCUE: re-sending TERMINAL PROFILE\n')
resp = _resend_terminal_profile(self.server, scc)
self._send_json(resp)
@@ -5722,6 +5732,9 @@ class PysimHandler(BaseHTTPRequestHandler):
self._send_json({'error': 'no terminal profile configured'}, 400)
self._log_resp({'error': 'no terminal profile configured'})
return
# Never re-send the TP while a FETCHed command awaits its
# TERMINAL RESPONSE: answer it with a cancel TR first (v3.6.35).
_finish_pending_menu(self.server, scc)
sys.stderr.write('TERMINAL-PROFILE: re-sending %s\n' % self.server.terminal_profile)
try:
resp = _resend_terminal_profile(self.server, scc)