diff --git a/pysim_otaman_server/__main__.py b/pysim_otaman_server/__main__.py index 71fc0c2..2993495 100644 --- a/pysim_otaman_server/__main__.py +++ b/pysim_otaman_server/__main__.py @@ -98,7 +98,12 @@ def main(): scc._tp.proactive_handler = _DefaultProactiveHandler() t_phase = time.time() 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: sl.wait_for_card(3) 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)) if app is not None and opts.fast_init: 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() try: _init_proactive_session() @@ -137,6 +142,8 @@ def main(): _tlog('terminal_profile_drain: %.0fms' % ((time.time() - t_phase) * 1000)) except Exception: 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: # PysimApp.__init__ routes PySimLogger through app.poutput() (app.stdout) # and drops the root level to INFO. Re-route pysim's own APDU trace logging diff --git a/pysim_otaman_server/fastinit.py b/pysim_otaman_server/fastinit.py index eb600f9..531a9eb 100644 --- a/pysim_otaman_server/fastinit.py +++ b/pysim_otaman_server/fastinit.py @@ -15,10 +15,11 @@ keep a real reconnect/physical reset. """ import operator +import sys from pySim.cards import CardBase, SimCardBase, UiccCardBase, card_detect from pySim.commands import SimCardCommands -from pySim.exceptions import SwMatchError +from pySim.exceptions import ProtocolError, SwMatchError from pySim.filesystem import CardApplication, CardModel from pySim.profile import CardProfile from pySim.runtime import RuntimeState @@ -35,7 +36,11 @@ class FastRuntimeState(RuntimeState): of power-cycling the card. Use hard_reset() for an explicit reset.""" 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): 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. ``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) if wait: 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): - """Explicit equip: one real reconnect (wait_for_card) then reset-free init.""" - if app.rs and app.rs.profile: - for cmd_set in app.rs.profile.shell_cmdsets: - app.unregister_command_set(cmd_set) + """Explicit equip: one real reconnect (wait_for_card) then reset-free init. + PysimApp.equip() unregisters the old command sets itself after the new init + succeeds, so a failed init leaves the previous card state intact.""" rs, card = init_card_fast(app.sl, wait=True) app.equip(card, rs) diff --git a/tests/test_fastinit.py b/tests/test_fastinit.py index 2f3a738..8afdecf 100644 --- a/tests/test_fastinit.py +++ b/tests/test_fastinit.py @@ -5,6 +5,7 @@ import sys import types import unittest from pathlib import Path +from unittest import mock PROJECTS = Path(__file__).resolve().parents[2] PY_SIM = PROJECTS / 'pysim' @@ -14,6 +15,7 @@ if str(PY_SIM) not in sys.path: from pySim.exceptions import SwMatchError from pySim.ts_102_221 import CardProfileUICC +import pysim_otaman_server.fastinit as fastinit from pysim_otaman_server.fastinit import ( FastRuntimeState, do_reset_fast, @@ -123,3 +125,88 @@ class TestDoResetFast(unittest.TestCase): if __name__ == '__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, [])