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:
@@ -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
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|
||||||
|
|||||||
@@ -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, [])
|
||||||
|
|||||||
Reference in New Issue
Block a user