fix: keep pySim's command-set bookkeeping in sync in the ES10 selections

After a profile switch the shell was unusable: every file select failed
with 'Attribute already exists: do_decode_hex (ShellCommands)' and even
equipping did not help - only a server restart recovered it.

pySim's equip() unregisters only the command sets of the file that is
selected at that moment, then re-registers everything while selecting
MF/EF.ICCID and MF with the app as cmd_app.  esim._select_isdr() selected
the ISD-R ADF without cmd_app, so MF's sets (ShellCommands, ...) stayed
registered while the selection moved to the ADF; the equip after the
profile switch then died re-registering them (cmd2 raises
CommandSetRegistrationError), leaving the app broken.

- _select_isdr() passes the shell app as cmd_app, so the old file's sets
  are unregistered and the ADF's (none) registered.
- _restore() passes it to soft_reset() too, so the MF re-selection updates
  the bookkeeping on both the success and the failure path.
- _esim_reinit() probes select('MF', app) after the equip and reports
  reinitialized: false on a registration error (a card-level select
  failure, e.g. no active profile, is tolerated).
- tests assert the cmd_app plumbing on the ISD-R select and the restore
  (profiles, chip and both switch paths).
This commit is contained in:
2026-09-22 00:46:14 +03:00
parent f6d62c225c
commit cb63ff286d
3 changed files with 44 additions and 10 deletions
+19 -6
View File
@@ -33,9 +33,11 @@ class FakeLchan:
def __init__(self):
self.scc = SimpleNamespace(name='lchan-scc')
self.selected = None
self.cmd_app = None
def select_file(self, app):
def select_file(self, app, cmd_app=None):
self.selected = app
self.cmd_app = cmd_app
def make_app(profile='Consumer eUICC (SGP.22)', isdr=True):
@@ -46,13 +48,16 @@ def make_app(profile='Consumer eUICC (SGP.22)', isdr=True):
rs = SimpleNamespace(profile=profile,
mf=SimpleNamespace(applications=apps),
lchan=[lchan],
resets=0)
resets=0,
soft_reset_cmd_app=None)
def soft_reset():
def soft_reset(cmd_app=None):
rs.resets += 1
rs.soft_reset_cmd_app = cmd_app
rs.soft_reset = soft_reset
return SimpleNamespace(rs=rs), lchan
app = SimpleNamespace(rs=rs)
return app, lchan
def profile_info(iccid, aid, state=None, nickname=None, cls=None, owner=None,
@@ -128,9 +133,12 @@ class EsimTests(unittest.TestCase):
self.assertEqual(out['profiles'][1]['state'], 'disabled')
self.assertIsNone(out['profiles'][1]['icon'])
self.assertIsNone(out['profiles'][1]['icon_size'])
# ISD-R was selected and the previous selection restored
# ISD-R was selected (with the shell app, so pySim's command-set
# bookkeeping follows) and the previous selection restored the same way
self.assertEqual(lchan.selected.name, 'ISD-R')
self.assertIs(lchan.cmd_app, app)
self.assertEqual(app.rs.resets, 1)
self.assertIs(app.rs.soft_reset_cmd_app, app)
# the request asks for every ProfileInfo tag
self.assertIn('9F70', self.calls[0].to_tlv().hex().upper())
@@ -251,9 +259,12 @@ class EsimTests(unittest.TestCase):
self.assertEqual(out['result'], 'ok')
self.assertFalse(out['refresh_seen'])
self.assertEqual(len(sent), 1)
# the ISD-R was selected for the command and the selection restored
# the ISD-R was selected for the command (with the shell app) and the
# selection restored the same way
self.assertEqual(lchan.selected.name, 'ISD-R')
self.assertIs(lchan.cmd_app, app)
self.assertEqual(app.rs.resets, 1)
self.assertIs(app.rs.soft_reset_cmd_app, app)
def test_switch_profile_treats_91xx_as_ok_and_runs_the_chain(self):
app, lchan = make_app()
@@ -268,7 +279,9 @@ class EsimTests(unittest.TestCase):
self.assertTrue(out['refresh_seen'])
self.assertEqual(chain, ['9111'])
self.assertEqual(lchan.selected.name, 'ISD-R')
self.assertIs(lchan.cmd_app, app)
self.assertEqual(app.rs.resets, 1)
self.assertIs(app.rs.soft_reset_cmd_app, app)
def test_switch_profile_chain_failure_keeps_the_accepted_switch(self):
app, _ = make_app()