Equip was broken after a card swap: auto-equip (and manual Equip) failed with
"Failed to transmit with protocol T0. Card was removed. (0x80100069)" until
the server was restarted.
Chain (v3.1.2 regression):
- _handle_card_disconnect() cleared server.card/scc but not pySim's
app.card/app.rs/app.lchan, so the removed card - and with it the old PC/SC
link and its exclusive card handle - stayed referenced; handlers using
app.rs (e.g. the PWA's /api/tree poll) kept transmitting over the dead card.
- those errors carry hresult=0x80100069 (SCARD_W_REMOVED_CARD), but
_is_pcsc_error() treated any PC/SC error as a dead service and set
_TRANSPORT_STALE; the next auto-equip then called _ensure_transport(),
which built a second PcscSimLink while the old one was still connected -
the new link inherited the removed card's handle and failed on its first
APDU. No retry existed, so every later equip repeated the failure.
Fixes:
- _is_transport_fatal(): only service/context hresults rebuild the transport
(E_NO_SERVICE, E_SERVICE_STOPPED, E_NO_READERS_AVAILABLE, E_INVALID_HANDLE,
...); card-level states (W_REMOVED_CARD, E_NO_SMARTCARD, W_RESET_CARD,
W_UNRESPONSIVE_CARD, W_UNPOWERED_CARD) reconnect on the existing link.
All 8 disconnect call sites pass the new verdict.
- _clear_app_card_state(): unequip through pySim's own equip(None, None)
before clearing app.card/app.rs/app.lchan. Nulling them alone would make
the next equip abort with "CommandSet ... is already installed"
(PysimApp.equip() unregisters the previous profile's command sets from
self.rs), which left /api/tree broken after a swap. A failed auto-equip
attempt unequips the half-initialized shell as well. Handlers now answer
"no card" instead of transmitting over the dead card, and the old link
becomes collectable.
- _ensure_transport(): disconnects the old link before building the new one
(restoring it if the factory fails) - the rebuild path is now safe for the
real pcscd-restart case.
- auto-equip: _auto_equip_attempt()/_auto_equip_attempts() retry up to 3
times, 1 s apart (fatal failures mark the transport so the retry rebuilds);
the watchdog re-arms it (_auto_equip_rearm) every 5 s while a card is
present and the session is down, with exponential backoff to 60 s for a
card that cannot be initialized at all.
- fastinit.init_card_fast(): also retries once after a PC/SC link error
(CardConnectionException/NoCardException), not only after SW mismatches.
Tests: transport-fatal classification, app-state clearing, shell unequip,
old-link release, auto-equip retry/backoff/re-arm, fastinit link retry.
547 frontend / 410 Python green; version 3.5.2; sw cache simple-v254.
With an eUICC in the reader the fast init raised SwMatchError twice and
fell back to the stock pysim init: pick_profile_no_reset() disables the
physical resets that CardProfile.match_with_card() normally performs, so
the successful SGP.22 probe leaves the ISD-R ADF selected. RuntimeState
then selects MF by FID (00 A4 00 04 02 3F00) from within the ADF, which
this card answers with 6A82.
- _restore_mf_after_probe() re-selects MF after the profile pick: the
cheap select keeps the reset-free path for normal cards, a physical
reset covers cards that refuse the MF select from an ADF.
- The EID read restores with rs.reset() (soft reset, escalating to a
physical reset) instead of rs.soft_reset(), which had the same trap.
- tests: FakeScc mf_select_error mode + two _restore_mf_after_probe
cases (no reset / exactly one physical reset).
A probe can leave a card in a context where CLA-00 file access returns
6d00, so the software MF select fails (stock pysim init survives only
because it resets physically after probing). Fast init now recovers the
same way, on demand:
- FastRuntimeState.reset() falls back to hard_reset() on
SwMatchError/ProtocolError (logged as FAST-RESET)
- init_card_fast() retries once after sl.reset_card() (FAST-INIT)
- __main__ falls back to stock pysim init once and skips
TERMINAL PROFILE/drain when no card was initialized (no more
6d00/6985 noise after a failed init)
- do_equip_fast() no longer pre-unregisters command sets; PysimApp.equip
does that after a successful init, so a failed equip keeps the
previous card/rs instead of leaving the app unequipped
Tests for the escalation, the retry and failed-equip state retention.
No card-model special cases.
pysim's init_card() resets the card once per profile candidate in
CardProfile.pick(), once in RuntimeState.__init__ and again in
PysimApp.equip(); on typical readers each reconnect costs ~1.3s and a
normal init/equip does 7-8 of them (measured: equip 9.85s, startup
~14.8s).
fastinit.py mirrors pySim.app.init_card() with the resets removed:
pick_profile_no_reset() runs all profile probes back-to-back on one
connection, FastRuntimeState.reset() is a software reset (select MF,
clear selected_adf/scp, ATR from the transport) and the equip/reset
commands are routed through do_equip_fast (one reconnect via
wait_for_card) and do_reset_fast (always a physical reset). Enabled
with --fast-init; stock behavior remains the default.
Tests for the reset-free pick, the soft reset and the explicit reset
paths. Docs updated; SW cache v88 -> v89.