Compare commits
4 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 1174a340db | |||
| f92eb8f131 | |||
| 7e8c483eb1 | |||
| 8a08f642d4 |
@@ -9,6 +9,47 @@ every change carries a descriptive commit message.
|
||||
|
||||
## [Unreleased]
|
||||
|
||||
## [3.32.5] — 2026-10-21
|
||||
|
||||
### Fixed
|
||||
|
||||
- A card removed during the **startup initialization** no longer leaves the server
|
||||
reporting a phantom equipped card. The init caught the PC/SC error (and printed a
|
||||
traceback) but kept the card object it had already built, so `card_present` and
|
||||
`_CARD_CONNECTED` stayed True — and the auto-equip then skipped every later insertion
|
||||
(its `_CARD_CONNECTED` early return), leaving only a manual Equip or a restart. A
|
||||
PC/SC error now prints one line (`INIT: card removed during initialization …`), drops
|
||||
the half-initialized session and lets the presence monitor equip the next card.
|
||||
|
||||
## [3.32.4] — 2026-10-21
|
||||
|
||||
### Fixed
|
||||
|
||||
- The auto-equip's captured traceback now always includes the **exception line**: cmd2
|
||||
renders a rich-box traceback longer than the logged head, so the actual error was cut
|
||||
off. The tail is appended after a `...` marker; the "releasing the old link failed"
|
||||
message now says it continues with a fresh link (a removed card's dead PC/SC handle
|
||||
always fails to disconnect).
|
||||
- A card insertion now marks the PC/SC link for a rebuild, so the **first** auto-equip
|
||||
attempt already uses a fresh link. On Windows the old link cannot connect after a
|
||||
swap — v3.32.2 recovered it, but only on the second attempt, after a failed init, a
|
||||
2–4 s delay and a traceback in the log. A link created at a cardless startup has
|
||||
never connected either, which the same mark covers. The rebuild still runs in the
|
||||
equip worker (never in the pyscard monitor thread) and `_ensure_transport` releases
|
||||
the old link first.
|
||||
|
||||
## [3.32.2] — 2026-10-21
|
||||
|
||||
### Fixed
|
||||
|
||||
- **The auto-equip loop after a card swap on Windows.** A failed initialization with
|
||||
the card still present was reported as "card gone during initialization" and treated
|
||||
as a no-op success — so the three retries never ran and the watchdog re-armed every
|
||||
few seconds forever. It is a failure now, and **every failed attempt marks the
|
||||
PC/SC transport for a rebuild**, so the retry starts from a fresh link (the release
|
||||
of the old link happens first, so it cannot inherit a stale card handle). The error
|
||||
pySim swallowed is printed as `AUTO-EQUIP: init output:` with its traceback.
|
||||
|
||||
## [3.32.1] — 2026-10-21
|
||||
|
||||
### Added
|
||||
|
||||
@@ -799,7 +799,7 @@ its MSVC build chain) is not pulled in.
|
||||
- **"Failed to establish context: Access denied"** — `pcscd` isn't running or the user lacks permission: `sudo systemctl enable --now pcscd && sudo usermod -a -G pcscd $USER`.
|
||||
- **"device file /dev/ttyUSB0 does not exist"** — no serial reader; connect a USB reader or pass `-d` explicitly. The server still starts without a reader.
|
||||
- **"Service not available" (0x8010001D) / reader gone after a `pcscd` restart** — the server recovers by itself: a watchdog revives pyscard's presence monitor and the next equip (automatic after re-insertion, or the **Equip** button) recreates the PC/SC transport (releasing the old link first). No server restart needed.
|
||||
- **"Card was removed" errors / auto-equip fails after a card swap** — a normal card swap reconnects on the existing transport (card-level PC/SC errors no longer force a transport rebuild), a failed auto-equip is retried up to 3 times, and the watchdog re-arms it every 5 s while a card is present. A removed card also clears the card state completely, so handlers answer "no card" instead of transmitting over the dead card.
|
||||
- **"Card was removed" errors / auto-equip fails after a card swap** — a card insertion marks the PC/SC link for a rebuild (so the **first** attempt already uses a fresh link — on Windows the old one cannot connect after a swap), the auto-equip retries up to 3 times, and **every failed attempt rebuilds the transport** before the next one. A card removed while the server initializes at startup leaves it cardless with one line (no traceback), and the next insertion equips normally. The swallowed init error (with its traceback) is now printed as `AUTO-EQUIP: init output:`, and a card that is present but produced no card is a failure, not a silent "card gone". The watchdog re-arms with a doubling delay (5 s up to 60 s) while a card is present; a removed card clears the card state completely, so handlers answer "no card" instead of transmitting over the dead card.
|
||||
|
||||
### API reference
|
||||
|
||||
|
||||
+1
-1
@@ -783,7 +783,7 @@ pysim-simple-server --http-port 8080
|
||||
- **"Failed to establish context: Access denied"** — `pcscd` не запущен или нет прав: `sudo systemctl enable --now pcscd && sudo usermod -a -G pcscd $USER`.
|
||||
- **"device file /dev/ttyUSB0 does not exist"** — нет serial-ридера; подключите USB-ридер или укажите `-d`. Сервер всё равно стартует без ридера.
|
||||
- **"Service not available" (0x8010001D) / ридер пропал после перезапуска `pcscd`** — сервер восстанавливается сам: сторож перезапускает монитор присутствия pyscard, а следующий equip (автоматически после повторной вставки карты или кнопкой **Equip**) пересоздаёт транспорт PC/SC (сначала освобождая старый линк). Перезапуск сервера не нужен.
|
||||
- **Ошибки "Card was removed" / equip не срабатывает после смены карты** — обычная смена карты переподключается на существующем транспорте (карточные ошибки PC/SC больше не вызывают пересоздание транспорта), неудавшийся авто-equip повторяется до 3 раз, а сторож перезапускает его каждые 5 с, пока карта в ридере. Снятие карты теперь полностью очищает состояние, поэтому обработчики отвечают «нет карты», а не шлют APDU мёртвой карте.
|
||||
- **Ошибки "Card was removed" / equip не срабатывает после смены карты** — вставка карты помечает соединение PC/SC для пересоздания (поэтому **первая** попытка уже использует свежее соединение — в Windows старое после смены карты подключиться не может), авто-equip повторяется до 3 раз, и **каждая неудачная попытка пересоздаёт транспорт** перед следующей. Карта, снятая во время инициализации при старте сервера, оставляет его без карты с одной строкой (без трассировки), а следующая вставка инициализируется обычным образом. Проглоченная ошибка инициализации (с трассировкой) теперь печатается как `AUTO-EQUIP: init output:`, а «карта есть, но карта не создана» — это ошибка, а не тихое «карта пропала». Сторож повторяет с удвоением задержки (5 с … 60 с), пока карта в ридере; снятие карты полностью очищает состояние, поэтому обработчики отвечают «нет карты», а не шлют APDU мёртвой карте.
|
||||
|
||||
### Справочник API
|
||||
|
||||
|
||||
+1
-1
@@ -1943,7 +1943,7 @@
|
||||
// ===== Version =====
|
||||
// Single source of truth for the PWA version: shown in the header and used
|
||||
// by the server version check in pysimConnect().
|
||||
const SIMPLE_VERSION = '3.32.1';
|
||||
const SIMPLE_VERSION = '3.32.5';
|
||||
document.getElementById('app-version').textContent = 'v' + SIMPLE_VERSION;
|
||||
|
||||
// ===== view history (the browser Back/Forward buttons) =====
|
||||
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
const CACHE = 'simple-v459';
|
||||
const CACHE = 'simple-v463';
|
||||
const URLS = [
|
||||
'index.html',
|
||||
'help.html',
|
||||
|
||||
+1
-1
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
||||
|
||||
[project]
|
||||
name = "pysim-simple-server"
|
||||
version = "3.32.1"
|
||||
version = "3.32.5"
|
||||
description = "HTTP REST server wrapping pysim for the SIMple PWA"
|
||||
license = { text = "GPL-2.0-or-later" }
|
||||
requires-python = ">=3.9"
|
||||
|
||||
@@ -18,7 +18,7 @@ from . import presets
|
||||
from . import test_scripts
|
||||
from . import test_suites
|
||||
from . import testscript
|
||||
from .server import PysimHandler, StderrApduTracer, _LoggingApduTracer, VERSION, _send_terminal_profile, _DefaultProactiveHandler, _handle_proactive_chain, _send_status, _init_proactive_session, _timing_on, _tlog, _set_menu_timeout, start_card_monitor, set_auto_equip, _read_iccid, _read_arr, _read_pbr, _netstate_read, _netstate_install, _LineFilter, _test_command_type, migrate_scripts_to_suites
|
||||
from .server import PysimHandler, StderrApduTracer, _LoggingApduTracer, VERSION, _send_terminal_profile, _DefaultProactiveHandler, _handle_proactive_chain, _send_status, _init_proactive_session, _timing_on, _tlog, _set_menu_timeout, start_card_monitor, set_auto_equip, _read_iccid, _read_arr, _read_pbr, _netstate_read, _netstate_install, _LineFilter, _test_command_type, migrate_scripts_to_suites, _is_pcsc_error, _clear_app_card_state
|
||||
|
||||
|
||||
_server_start = 0
|
||||
@@ -295,7 +295,25 @@ def main():
|
||||
break
|
||||
_handle_proactive_chain(scc, st_sw)
|
||||
_tlog('terminal_profile_drain: %.0fms' % ((time.time() - t_phase) * 1000))
|
||||
except Exception:
|
||||
except Exception as e:
|
||||
if _is_pcsc_error(e):
|
||||
# The card (or the reader) went away during the startup init -
|
||||
# a normal state, not a crash. Drop the half-initialized
|
||||
# session: keeping the card object would leave the server
|
||||
# reporting a phantom equipped card, and the auto-equip would
|
||||
# then skip every later insertion (_CARD_CONNECTED is True).
|
||||
# The presence monitor equips the next card (live 2026-10-21).
|
||||
sys.stderr.write('INIT: card removed during initialization - server ready; '
|
||||
'insert a card or press Equip\n')
|
||||
iccid = None
|
||||
netstate_files = None
|
||||
arr_files = None
|
||||
pbr_files = None
|
||||
card = None
|
||||
rs = None
|
||||
scc = None
|
||||
_clear_app_card_state(app)
|
||||
else:
|
||||
traceback.print_exc(file=sys.stderr)
|
||||
if app is not None and tracer is not None:
|
||||
if opts.apdu_trace:
|
||||
|
||||
@@ -37,7 +37,7 @@ from osmocom.tlv import BER_TLV_IE
|
||||
from osmocom.utils import rpad
|
||||
|
||||
|
||||
VERSION = '3.32.1'
|
||||
VERSION = '3.32.5'
|
||||
|
||||
MAX_ENVELOPE_SEGMENTS = 5 # max SMS segments for outgoing C-APDU in ENVELOPE
|
||||
|
||||
@@ -5426,7 +5426,8 @@ def _ensure_transport(server):
|
||||
try:
|
||||
old.disconnect()
|
||||
except Exception as e:
|
||||
sys.stderr.write('TRANSPORT: releasing the old link failed: %s\n' % e)
|
||||
sys.stderr.write('TRANSPORT: releasing the old link failed '
|
||||
'(continuing with a fresh one): %s\n' % e)
|
||||
try:
|
||||
sl = factory()
|
||||
except Exception as e:
|
||||
@@ -5701,8 +5702,8 @@ def _auto_equip_attempt(server):
|
||||
if not _ensure_transport(server):
|
||||
return False
|
||||
server.equipping = True
|
||||
try:
|
||||
out = StringIO()
|
||||
try:
|
||||
old_stdout, old_stderr = app.stdout, sys.stderr
|
||||
old_debug = getattr(app, 'debug', None)
|
||||
app.stdout = out
|
||||
@@ -5716,9 +5717,17 @@ def _auto_equip_attempt(server):
|
||||
app.debug = old_debug
|
||||
app.stdout = old_stdout
|
||||
sys.stderr = old_stderr
|
||||
if not getattr(server, 'card_present', False) or server.app.card is None:
|
||||
if not getattr(server, 'card_present', False):
|
||||
# A real removal: _handle_card_disconnect already cleared the
|
||||
# session, nothing to retry.
|
||||
sys.stderr.write('AUTO-EQUIP: card gone during initialization\n')
|
||||
return True
|
||||
if server.app.card is None:
|
||||
# The presence monitor still reports the card, so this is a
|
||||
# failed initialization, not a swap - never a silent no-op
|
||||
# (a Windows card swap showed an endless "card gone"
|
||||
# re-arm loop because this returned True: 2026-10-21).
|
||||
raise RuntimeError('the card is present but the init produced no card')
|
||||
if 'Traceback (most recent call last)' in out.getvalue():
|
||||
raise RuntimeError('equip reported an error (see the captured output)')
|
||||
if not _app_equip_complete(app):
|
||||
@@ -5729,12 +5738,31 @@ def _auto_equip_attempt(server):
|
||||
return True
|
||||
except Exception as e:
|
||||
sys.stderr.write('AUTO-EQUIP failed: %s\n' % e)
|
||||
# A dead transport must be rebuilt before the next attempt;
|
||||
# card-level failures reconnect on the existing link. Unequip the
|
||||
# half-initialized shell as well: a failed init can leave command
|
||||
# sets registered that the next equip would refuse to re-register.
|
||||
if _is_transport_fatal(e):
|
||||
# Show what pySim swallowed (cmd2 printed it into the capture):
|
||||
# without it the failure is undiagnosable in the run log.
|
||||
captured = out.getvalue().strip()
|
||||
if 'Traceback (most recent call last)' in captured:
|
||||
lines = captured.splitlines()
|
||||
start = next(i for i, l in enumerate(lines)
|
||||
if 'Traceback (most recent call last)' in l)
|
||||
body = lines[start:start + 10]
|
||||
tail = [l for l in lines[start:] if l.strip()][-3:]
|
||||
if tail and tail[-1] not in body:
|
||||
# The exception itself is the last line of the traceback;
|
||||
# a rich-box traceback can be longer than the head slice.
|
||||
body.append('...')
|
||||
body.extend(tail)
|
||||
sys.stderr.write('AUTO-EQUIP: init output:\n%s\n'
|
||||
% '\n'.join(body))
|
||||
# ANY failed attempt leaves the PC/SC card handle suspect: after a
|
||||
# card swap on Windows the link can stay unable to connect, so
|
||||
# every retry failed and the watchdog re-armed forever (live
|
||||
# 2026-10-21). Mark the transport stale so the retry rebuilds it -
|
||||
# _ensure_transport releases the old link first, so the new one
|
||||
# never inherits a stale handle (the 2026-09-24 bug).
|
||||
_handle_card_disconnect(stale=True)
|
||||
# Unequip the half-initialized shell too: a failed init can leave
|
||||
# command sets registered that the next equip would refuse.
|
||||
_clear_app_card_state(app)
|
||||
return False
|
||||
finally:
|
||||
@@ -5799,6 +5827,15 @@ class _CardPresenceObserver(CardObserver):
|
||||
with _CARD_LOCK:
|
||||
if _server_ref:
|
||||
_server_ref.card_present = True
|
||||
# A freshly inserted card gets a fresh PC/SC link: after a
|
||||
# swap the old link can stay unable to connect (seen on
|
||||
# Windows: the first init failed and only the retry's
|
||||
# rebuild recovered it), and a link created at a cardless
|
||||
# startup has never connected. Marking it here (under the
|
||||
# lock) makes the FIRST attempt rebuild - the rebuild
|
||||
# itself runs in the equip worker, never in this thread.
|
||||
global _TRANSPORT_STALE
|
||||
_TRANSPORT_STALE = True
|
||||
trigger_auto = True
|
||||
if trigger_auto and _AUTO_EQUIP:
|
||||
_auto_equip_trigger()
|
||||
|
||||
@@ -35,8 +35,11 @@ class TestCardPresenceObserver(unittest.TestCase):
|
||||
self.trigger_mock = self.trigger.start()
|
||||
self.saved_auto = S._AUTO_EQUIP
|
||||
S._AUTO_EQUIP = True
|
||||
self.saved_stale = S._TRANSPORT_STALE
|
||||
S._TRANSPORT_STALE = False
|
||||
|
||||
def tearDown(self):
|
||||
S._TRANSPORT_STALE = self.saved_stale
|
||||
S._AUTO_EQUIP = self.saved_auto
|
||||
self.trigger.stop()
|
||||
self.patcher.stop()
|
||||
@@ -61,6 +64,16 @@ class TestCardPresenceObserver(unittest.TestCase):
|
||||
self.assertEqual(self.disconnects, [])
|
||||
self.trigger_mock.assert_called_once()
|
||||
|
||||
def test_insertion_marks_the_transport_for_a_rebuild(self):
|
||||
# The first equip attempt must already use a fresh link: after a swap
|
||||
# the old one cannot connect on Windows, and a cardless-startup link
|
||||
# never connected (v3.32.4).
|
||||
S._TRANSPORT_STALE = False
|
||||
self.server.card_present = False
|
||||
self.observer.update(None, ([FakeCard('Test Reader 00 00')], []))
|
||||
self.assertTrue(S._TRANSPORT_STALE)
|
||||
self.trigger_mock.assert_called_once()
|
||||
|
||||
def test_insertion_does_not_trigger_when_disabled(self):
|
||||
S._AUTO_EQUIP = False
|
||||
self.server.card_present = False
|
||||
|
||||
+111
-3
@@ -269,9 +269,50 @@ class AutoEquipTests(unittest.TestCase):
|
||||
self.assertTrue(server._auto_equip_attempts(srv, 3, lambda s: None))
|
||||
self.assertEqual(state['calls'], 2)
|
||||
self.assertEqual(applied, [srv])
|
||||
# the failed attempt left the handle suspect; the retry's
|
||||
# _ensure_transport (patched here) would rebuild and clear the flag
|
||||
self.assertTrue(server._TRANSPORT_STALE)
|
||||
|
||||
def test_a_failed_init_rebuilds_the_transport_before_the_retry(self):
|
||||
# The Windows swap end to end: the first init fails with the card
|
||||
# present, the retry starts from a fresh PC/SC link and succeeds -
|
||||
# and the old link is released before the new one is built.
|
||||
srv = self.make_server(None)
|
||||
released = []
|
||||
built = []
|
||||
|
||||
class Link:
|
||||
def disconnect(self):
|
||||
released.append(self)
|
||||
|
||||
old = Link()
|
||||
srv.sl = old
|
||||
srv.transport_factory = lambda: built.append(Link()) or built[-1]
|
||||
calls = {'n': 0}
|
||||
|
||||
def onecmd(cmd):
|
||||
calls['n'] += 1
|
||||
if calls['n'] == 1:
|
||||
raise RuntimeError('ProtocolError: Connection failed')
|
||||
# a completed equip installs both (the failure unequipped the shell)
|
||||
srv.app.card = SimpleNamespace(_scc='scc')
|
||||
srv.app.lchan = 'lchan'
|
||||
|
||||
srv.app.onecmd_plus_hooks = onecmd
|
||||
with mock.patch.object(server, '_apply_equipped_card', lambda s: None):
|
||||
self.assertTrue(server._auto_equip_attempts(srv, 3, lambda s: None))
|
||||
self.assertEqual(calls['n'], 2)
|
||||
self.assertEqual(len(built), 1)
|
||||
self.assertEqual(released, [old])
|
||||
self.assertIs(srv.sl, built[0])
|
||||
self.assertFalse(server._TRANSPORT_STALE)
|
||||
|
||||
def test_card_level_failure_keeps_the_transport(self):
|
||||
def test_a_failed_attempt_marks_the_transport_for_the_rebuild(self):
|
||||
# A card-level error used to keep the link: after a Windows card swap
|
||||
# the link stayed unable to connect, every retry failed and the
|
||||
# watchdog re-armed forever (live 2026-10-21). Any failed attempt now
|
||||
# marks the transport stale so the retry rebuilds it (which releases
|
||||
# the old link first - the 2026-09-24 bug).
|
||||
srv = self.make_server(None)
|
||||
|
||||
def onecmd(cmd):
|
||||
@@ -282,7 +323,7 @@ class AutoEquipTests(unittest.TestCase):
|
||||
srv.app.onecmd_plus_hooks = onecmd
|
||||
with mock.patch.object(server, '_ensure_transport', lambda s: True):
|
||||
self.assertFalse(server._auto_equip_attempt(srv))
|
||||
self.assertFalse(server._TRANSPORT_STALE)
|
||||
self.assertTrue(server._TRANSPORT_STALE)
|
||||
|
||||
def test_transport_fatal_failure_marks_the_transport(self):
|
||||
srv = self.make_server(None)
|
||||
@@ -341,7 +382,7 @@ class AutoEquipTests(unittest.TestCase):
|
||||
mock.patch.object(server, '_apply_equipped_card',
|
||||
lambda s: self.fail('_apply_equipped_card must not run')):
|
||||
self.assertFalse(server._auto_equip_attempt(srv))
|
||||
self.assertFalse(server._TRANSPORT_STALE)
|
||||
self.assertTrue(server._TRANSPORT_STALE)
|
||||
|
||||
def test_captured_equip_traceback_is_a_failure(self):
|
||||
# cmd2 prints swallowed exceptions (with cmd2 debug: a traceback) to
|
||||
@@ -358,6 +399,73 @@ class AutoEquipTests(unittest.TestCase):
|
||||
lambda s: self.fail('_apply_equipped_card must not run')):
|
||||
self.assertFalse(server._auto_equip_attempt(srv))
|
||||
|
||||
def test_a_card_present_init_without_a_card_is_a_failure(self):
|
||||
# The Windows loop: the presence monitor still reports the card, the
|
||||
# init produced nothing - it must retry (and rebuild), never report a
|
||||
# silent "card gone" success that re-arms forever (2026-10-21).
|
||||
srv = self.make_server(None)
|
||||
|
||||
def onecmd(cmd):
|
||||
pass # cmd2 swallowed the real error: no card built
|
||||
|
||||
srv.app.onecmd_plus_hooks = onecmd
|
||||
with mock.patch.object(server, '_ensure_transport', lambda s: True):
|
||||
self.assertFalse(server._auto_equip_attempt(srv))
|
||||
self.assertTrue(server._TRANSPORT_STALE)
|
||||
|
||||
def test_a_card_removed_during_the_init_is_still_a_silent_no_op(self):
|
||||
srv = self.make_server(None)
|
||||
|
||||
def onecmd(cmd):
|
||||
srv.card_present = False # the monitor saw the removal mid-init
|
||||
|
||||
srv.app.onecmd_plus_hooks = onecmd
|
||||
with mock.patch.object(server, '_ensure_transport', lambda s: True), \
|
||||
mock.patch('sys.stderr', new=StringIO()) as err:
|
||||
self.assertTrue(server._auto_equip_attempt(srv))
|
||||
self.assertIn('card gone during initialization', err.getvalue())
|
||||
self.assertFalse(server._TRANSPORT_STALE)
|
||||
|
||||
def test_the_swallowed_traceback_reaches_the_log(self):
|
||||
srv = self.make_server(None)
|
||||
|
||||
def onecmd(cmd):
|
||||
srv.app.card = SimpleNamespace(_scc='scc')
|
||||
sys.stderr.write('Traceback (most recent call last):\n'
|
||||
' File "x", line 1, in equip\n'
|
||||
'ProtocolError: Connection failed\n')
|
||||
|
||||
srv.app.onecmd_plus_hooks = onecmd
|
||||
with mock.patch.object(server, '_ensure_transport', lambda s: True), \
|
||||
mock.patch.object(server, '_apply_equipped_card', lambda s: None), \
|
||||
mock.patch('sys.stderr', new=StringIO()) as err:
|
||||
self.assertFalse(server._auto_equip_attempt(srv))
|
||||
out = err.getvalue()
|
||||
self.assertIn('AUTO-EQUIP: init output:', out)
|
||||
self.assertIn('ProtocolError: Connection failed', out)
|
||||
|
||||
def test_a_long_traceback_keeps_its_exception_line(self):
|
||||
# cmd2 renders a rich-box traceback; a fixed head slice cut the
|
||||
# exception itself off (seen live on Windows 2026-10-21), so the tail
|
||||
# is always included.
|
||||
srv = self.make_server(None)
|
||||
|
||||
def onecmd(cmd):
|
||||
srv.app.card = SimpleNamespace(_scc='scc')
|
||||
sys.stderr.write('Traceback (most recent call last):\n')
|
||||
for i in range(30):
|
||||
sys.stderr.write(' File "f%d", line %d, in step%d\n' % (i, i, i))
|
||||
sys.stderr.write('CardConnectionException: No card is inserted\n')
|
||||
|
||||
srv.app.onecmd_plus_hooks = onecmd
|
||||
with mock.patch.object(server, '_ensure_transport', lambda s: True), \
|
||||
mock.patch.object(server, '_apply_equipped_card', lambda s: None), \
|
||||
mock.patch('sys.stderr', new=StringIO()) as err:
|
||||
self.assertFalse(server._auto_equip_attempt(srv))
|
||||
out = err.getvalue()
|
||||
self.assertIn('AUTO-EQUIP: init output:', out)
|
||||
self.assertIn('CardConnectionException: No card is inserted', out)
|
||||
|
||||
def test_failed_attempt_unequips_the_half_initialized_shell(self):
|
||||
calls = []
|
||||
srv = self.make_server(None)
|
||||
|
||||
@@ -8,6 +8,7 @@ line and no traceback, and the pySim-internal 'Waiting for card...' /
|
||||
failure keeps the stock-pysim fallback and its traceback.
|
||||
"""
|
||||
|
||||
import inspect
|
||||
import io
|
||||
import sys
|
||||
import unittest
|
||||
@@ -158,3 +159,28 @@ def _parser():
|
||||
|
||||
if __name__ == '__main__':
|
||||
unittest.main()
|
||||
|
||||
class StartupCardLostTests(unittest.TestCase):
|
||||
"""A card removed during the startup init leaves the server cardless.
|
||||
|
||||
The startup code used to keep the card object it had already built:
|
||||
``card_present`` and ``_CARD_CONNECTED`` stayed True, so the server
|
||||
reported a phantom equipped card and the auto-equip skipped every later
|
||||
insertion (its ``_CARD_CONNECTED`` early return). The PC/SC error must
|
||||
drop the half-initialized session instead - and not print a traceback for
|
||||
a normal removal."""
|
||||
|
||||
def test_a_pcsc_interruption_drops_the_session(self):
|
||||
src = inspect.getsource(srv_main.main)
|
||||
self.assertIn('_is_pcsc_error(e)', src)
|
||||
self.assertIn('card removed during initialization', src)
|
||||
self.assertIn('_clear_app_card_state(app)', src)
|
||||
# the locals that feed card_present / _CARD_CONNECTED are cleared
|
||||
for line in ('iccid = None', 'card = None', 'rs = None', 'scc = None'):
|
||||
self.assertIn(line, src)
|
||||
|
||||
def test_the_session_state_is_derived_from_the_cleared_card(self):
|
||||
# the assignments that would otherwise report a phantom card
|
||||
src = inspect.getsource(srv_main.main)
|
||||
self.assertIn('server.card_present = card is not None', src)
|
||||
self.assertIn('_CARD_CONNECTED = card is not None', src)
|
||||
|
||||
Reference in New Issue
Block a user