v3.32.2: the Windows card-swap auto-equip loop
release / release (push) Waiting to run

After a card swap (or the first insertion after startup) on Windows the
auto-equip looped forever: "card inserted, initializing" / "card gone
during initialization", with no "retry 2/3" lines.

An equip with the card still present produced no card (app.card None),
and that branch reported "card gone" and returned True - a no-op success
- so the three retries never ran and the watchdog re-armed every ~5 s.

- The card-gone no-op is now only for a real removal (card_present
  False); a card-present init that produced no card raises a failure.
- Any failed attempt marks the transport stale, so the next attempt
  rebuilds the PC/SC link (a Windows swap can leave the old link unable
  to connect; _ensure_transport releases the old link before building
  the new one, so the 2026-09-24 stale-handle bug cannot recur).
- The error pySim swallowed (cmd2) is printed as "AUTO-EQUIP: init
  output:" with its traceback - the failure is diagnosable from the log.
- Docs: the READMEs' troubleshooting bullet.
- Version strings + the PWA cache bumped together (simple-v460).

Tests: a card-present init without a card is a failure, a mid-init
removal stays a silent no-op, any failure marks the transport, the
stale-handle traceback reaches the log, and an end-to-end retry: a
failed init, the old link released, a fresh one built, the retry
succeeds.  1012 Python + 953 frontend green.
This commit is contained in:
2026-10-21 14:35:44 +03:00
parent cdd3820064
commit 8a08f642d4
8 changed files with 135 additions and 17 deletions
+29 -9
View File
@@ -37,7 +37,7 @@ from osmocom.tlv import BER_TLV_IE
from osmocom.utils import rpad
VERSION = '3.32.1'
VERSION = '3.32.2'
MAX_ENVELOPE_SEGMENTS = 5 # max SMS segments for outgoing C-APDU in ENVELOPE
@@ -5701,8 +5701,8 @@ def _auto_equip_attempt(server):
if not _ensure_transport(server):
return False
server.equipping = True
out = StringIO()
try:
out = StringIO()
old_stdout, old_stderr = app.stdout, sys.stderr
old_debug = getattr(app, 'debug', None)
app.stdout = out
@@ -5716,9 +5716,17 @@ def _auto_equip_attempt(server):
app.debug = old_debug
app.stdout = old_stdout
sys.stderr = old_stderr
if not getattr(server, 'card_present', False) or server.app.card is None:
if not getattr(server, 'card_present', False):
# A real removal: _handle_card_disconnect already cleared the
# session, nothing to retry.
sys.stderr.write('AUTO-EQUIP: card gone during initialization\n')
return True
if server.app.card is None:
# The presence monitor still reports the card, so this is a
# failed initialization, not a swap - never a silent no-op
# (a Windows card swap showed an endless "card gone"
# re-arm loop because this returned True: 2026-10-21).
raise RuntimeError('the card is present but the init produced no card')
if 'Traceback (most recent call last)' in out.getvalue():
raise RuntimeError('equip reported an error (see the captured output)')
if not _app_equip_complete(app):
@@ -5729,12 +5737,24 @@ def _auto_equip_attempt(server):
return True
except Exception as e:
sys.stderr.write('AUTO-EQUIP failed: %s\n' % e)
# A dead transport must be rebuilt before the next attempt;
# card-level failures reconnect on the existing link. Unequip the
# half-initialized shell as well: a failed init can leave command
# sets registered that the next equip would refuse to re-register.
if _is_transport_fatal(e):
_handle_card_disconnect(stale=True)
# Show what pySim swallowed (cmd2 printed it into the capture):
# without it the failure is undiagnosable in the run log.
captured = out.getvalue().strip()
if 'Traceback (most recent call last)' in captured:
lines = captured.splitlines()
start = next(i for i, l in enumerate(lines)
if 'Traceback (most recent call last)' in l)
sys.stderr.write('AUTO-EQUIP: init output:\n%s\n'
% '\n'.join(lines[start:start + 12]))
# ANY failed attempt leaves the PC/SC card handle suspect: after a
# card swap on Windows the link can stay unable to connect, so
# every retry failed and the watchdog re-armed forever (live
# 2026-10-21). Mark the transport stale so the retry rebuilds it -
# _ensure_transport releases the old link first, so the new one
# never inherits a stale handle (the 2026-09-24 bug).
_handle_card_disconnect(stale=True)
# Unequip the half-initialized shell too: a failed init can leave
# command sets registered that the next equip would refuse.
_clear_app_card_state(app)
return False
finally: