fastinit: escalate soft reset to physical on SW mismatch; retry/fallback

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.
This commit is contained in:
2026-09-12 13:02:33 +03:00
parent 25787017e5
commit 7288c22830
3 changed files with 118 additions and 9 deletions
+9 -2
View File
@@ -98,7 +98,12 @@ def main():
scc._tp.proactive_handler = _DefaultProactiveHandler() scc._tp.proactive_handler = _DefaultProactiveHandler()
t_phase = time.time() t_phase = time.time()
if opts.fast_init: if opts.fast_init:
rs, card = fastinit.init_card_fast(sl, opts.skip_card_init, wait=True) try:
rs, card = fastinit.init_card_fast(sl, opts.skip_card_init, wait=True)
except Exception:
print("Warning: fast card initialization failed, falling back to pysim init:", file=sys.stderr)
traceback.print_exc()
rs, card = mod.init_card(sl, opts.skip_card_init)
else: else:
sl.wait_for_card(3) sl.wait_for_card(3)
rs, card = mod.init_card(sl, opts.skip_card_init) rs, card = mod.init_card(sl, opts.skip_card_init)
@@ -118,7 +123,7 @@ def main():
_tlog('pysim_app: %.0fms' % ((time.time() - t_phase) * 1000)) _tlog('pysim_app: %.0fms' % ((time.time() - t_phase) * 1000))
if app is not None and opts.fast_init: if app is not None and opts.fast_init:
fastinit.install(app) fastinit.install(app)
if scc and hasattr(scc, '_tp'): if scc and card is not None and hasattr(scc, '_tp'):
scc._tp.apdu_tracer = _LoggingApduTracer() scc._tp.apdu_tracer = _LoggingApduTracer()
try: try:
_init_proactive_session() _init_proactive_session()
@@ -137,6 +142,8 @@ def main():
_tlog('terminal_profile_drain: %.0fms' % ((time.time() - t_phase) * 1000)) _tlog('terminal_profile_drain: %.0fms' % ((time.time() - t_phase) * 1000))
except Exception: except Exception:
traceback.print_exc(file=sys.stderr) traceback.print_exc(file=sys.stderr)
elif scc is not None:
sys.stderr.write('INIT: card not initialized — use Equip once the card is readable\n')
if app is not None and opts.apdu_trace: if app is not None and opts.apdu_trace:
# PysimApp.__init__ routes PySimLogger through app.poutput() (app.stdout) # PysimApp.__init__ routes PySimLogger through app.poutput() (app.stdout)
# and drops the root level to INFO. Re-route pysim's own APDU trace logging # and drops the root level to INFO. Re-route pysim's own APDU trace logging
+22 -7
View File
@@ -15,10 +15,11 @@ keep a real reconnect/physical reset.
""" """
import operator import operator
import sys
from pySim.cards import CardBase, SimCardBase, UiccCardBase, card_detect from pySim.cards import CardBase, SimCardBase, UiccCardBase, card_detect
from pySim.commands import SimCardCommands from pySim.commands import SimCardCommands
from pySim.exceptions import SwMatchError from pySim.exceptions import ProtocolError, SwMatchError
from pySim.filesystem import CardApplication, CardModel from pySim.filesystem import CardApplication, CardModel
from pySim.profile import CardProfile from pySim.profile import CardProfile
from pySim.runtime import RuntimeState from pySim.runtime import RuntimeState
@@ -35,7 +36,11 @@ class FastRuntimeState(RuntimeState):
of power-cycling the card. Use hard_reset() for an explicit reset.""" of power-cycling the card. Use hard_reset() for an explicit reset."""
def reset(self, cmd_app=None): def reset(self, cmd_app=None):
return self.soft_reset(cmd_app) try:
return self.soft_reset(cmd_app)
except (SwMatchError, ProtocolError) as e:
sys.stderr.write('FAST-RESET: soft reset failed (%s), falling back to physical reset\n' % e)
return self.hard_reset(cmd_app)
def soft_reset(self, cmd_app=None): def soft_reset(self, cmd_app=None):
for lchan_nr in list(self.lchan.keys()): for lchan_nr in list(self.lchan.keys()):
@@ -79,7 +84,18 @@ def init_card_fast(sl, skip_card_init=False, wait=True):
"""Replacement for pySim.app.init_card() that avoids redundant resets. """Replacement for pySim.app.init_card() that avoids redundant resets.
``wait`` performs the single disconnect/connect of this init (explicit ``wait`` performs the single disconnect/connect of this init (explicit
equip passes True; startup already connects via wait_for_card).""" equip passes True; startup already connects via wait_for_card). If probing
leaves the card in a state the software reset cannot clear, retry once
after a physical reset."""
try:
return _init_card_once(sl, skip_card_init, wait)
except (SwMatchError, ProtocolError) as e:
sys.stderr.write('FAST-INIT: %s; retrying after physical reset\n' % e)
sl.reset_card()
return _init_card_once(sl, skip_card_init, wait=False)
def _init_card_once(sl, skip_card_init, wait):
scc = SimCardCommands(transport=sl) scc = SimCardCommands(transport=sl)
if wait: if wait:
sl.wait_for_card(3) sl.wait_for_card(3)
@@ -127,10 +143,9 @@ def init_card_fast(sl, skip_card_init=False, wait=True):
def do_equip_fast(app): def do_equip_fast(app):
"""Explicit equip: one real reconnect (wait_for_card) then reset-free init.""" """Explicit equip: one real reconnect (wait_for_card) then reset-free init.
if app.rs and app.rs.profile: PysimApp.equip() unregisters the old command sets itself after the new init
for cmd_set in app.rs.profile.shell_cmdsets: succeeds, so a failed init leaves the previous card state intact."""
app.unregister_command_set(cmd_set)
rs, card = init_card_fast(app.sl, wait=True) rs, card = init_card_fast(app.sl, wait=True)
app.equip(card, rs) app.equip(card, rs)
+87
View File
@@ -5,6 +5,7 @@ import sys
import types import types
import unittest import unittest
from pathlib import Path from pathlib import Path
from unittest import mock
PROJECTS = Path(__file__).resolve().parents[2] PROJECTS = Path(__file__).resolve().parents[2]
PY_SIM = PROJECTS / 'pysim' PY_SIM = PROJECTS / 'pysim'
@@ -14,6 +15,7 @@ if str(PY_SIM) not in sys.path:
from pySim.exceptions import SwMatchError from pySim.exceptions import SwMatchError
from pySim.ts_102_221 import CardProfileUICC from pySim.ts_102_221 import CardProfileUICC
import pysim_otaman_server.fastinit as fastinit
from pysim_otaman_server.fastinit import ( from pysim_otaman_server.fastinit import (
FastRuntimeState, FastRuntimeState,
do_reset_fast, do_reset_fast,
@@ -123,3 +125,88 @@ class TestDoResetFast(unittest.TestCase):
if __name__ == '__main__': if __name__ == '__main__':
unittest.main() unittest.main()
class FlakyLchan:
"""Lchan whose first MF select fails, as if probing left the card in a
context the software reset cannot clear."""
def __init__(self):
self.scc = types.SimpleNamespace(scp=object())
self.selected_adf = 'SOMETHING'
self.select_calls = 0
self.selected = []
def select(self, path, cmd_app=None):
self.select_calls += 1
if self.select_calls == 1:
raise SwMatchError('6d00', '9000')
self.selected.append(path)
class ResettableCard:
def __init__(self):
self.resets = 0
self._scc = types.SimpleNamespace(get_atr=lambda: 'AABB')
def reset(self):
self.resets += 1
return 'AABB'
class TestFastResetEscalation(unittest.TestCase):
def make_rs(self):
rs = FastRuntimeState.__new__(FastRuntimeState)
rs.lchan = {0: FlakyLchan(), 1: types.SimpleNamespace(scc=types.SimpleNamespace(scp=None))}
rs.adm_verified = True
rs.card = ResettableCard()
rs.identity = {}
return rs
def test_soft_reset_escalates_to_physical(self):
rs = self.make_rs()
atr = rs.reset()
self.assertEqual(atr, 'AABB')
self.assertEqual(rs.card.resets, 1)
self.assertEqual(rs.lchan[0].select_calls, 2)
self.assertEqual(rs.lchan[0].selected, ['MF'])
self.assertFalse(rs.adm_verified)
self.assertNotIn(1, rs.lchan)
class TestInitCardFastRetry(unittest.TestCase):
def test_retries_once_after_physical_reset(self):
calls = []
def once(sl, skip, wait):
calls.append(wait)
if len(calls) == 1:
raise SwMatchError('6d00', '9000')
return ('rs', 'card')
sl = types.SimpleNamespace(resets=0)
def reset_card():
sl.resets += 1
sl.reset_card = reset_card
with mock.patch.object(fastinit, '_init_card_once', side_effect=once):
rs, card = fastinit.init_card_fast(sl, wait=True)
self.assertEqual(calls, [True, False])
self.assertEqual(sl.resets, 1)
self.assertEqual((rs, card), ('rs', 'card'))
class TestDoEquipFastFailure(unittest.TestCase):
def test_failed_equip_keeps_previous_state(self):
calls = []
app = types.SimpleNamespace(
sl=object(),
rs=types.SimpleNamespace(profile=types.SimpleNamespace(shell_cmdsets=[object()])),
unregister_command_set=lambda cs: calls.append('unregister'),
equip=lambda card, rs: calls.append('equip'),
)
with mock.patch.object(fastinit, 'init_card_fast', side_effect=SwMatchError('6d00', '9000')):
with self.assertRaises(SwMatchError):
fastinit.do_equip_fast(app)
self.assertEqual(calls, [])