From f92eb8f1310837d9cb48e2afdc3382fd9ac983c0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=90=D0=BD=D1=82=D0=BE=D0=BD=20=D0=A2=D1=80=D0=BE=D1=88?= =?UTF-8?q?=D0=B8=D0=BD?= Date: Wed, 21 Oct 2026 14:52:12 +0300 Subject: [PATCH] v3.32.5: a card removed during the startup init leaves it cardless Live log: a card was pulled while the server started and the TERMINAL PROFILE send raised CardConnectionException 0x80100016 (SCARD_E_NOT_TRANSACTED). The init's except caught it (no crash), but the code then continued with the card object it had already built: card_present and _CARD_CONNECTED became True - a phantom equipped card - and the auto-equip's _CARD_CONNECTED early return then skipped every later insertion, leaving only a manual Equip or a restart. A PC/SC error in the startup init now prints one line (a traceback is noise for a normal removal), clears the locals that feed those flags and calls _clear_app_card_state(app); the presence monitor equips the next card. Non-PC/SC errors keep their traceback. Pinned in tests/test_startup_cardless.py (the branch and the flag sources). 1016 Python + 953 frontend green; simple-v463. --- CHANGELOG.md | 12 ++++++++++++ README.md | 2 +- README_RUS.md | 2 +- frontend/index.html | 2 +- frontend/sw.js | 2 +- pyproject.toml | 2 +- pysim_simple_server/__main__.py | 24 +++++++++++++++++++++--- pysim_simple_server/server.py | 2 +- tests/test_startup_cardless.py | 26 ++++++++++++++++++++++++++ 9 files changed, 65 insertions(+), 9 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 51ef04a..d9d71a2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,18 @@ 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 diff --git a/README.md b/README.md index fbfc521..90cecf2 100644 --- a/README.md +++ b/README.md @@ -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 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. 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. +- **"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 diff --git a/README_RUS.md b/README_RUS.md index b65923c..1811e93 100644 --- a/README_RUS.md +++ b/README_RUS.md @@ -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 для пересоздания (поэтому **первая** попытка уже использует свежее соединение — в Windows старое после смены карты подключиться не может), авто-equip повторяется до 3 раз, и **каждая неудачная попытка пересоздаёт транспорт** перед следующей. Проглоченная ошибка инициализации (с трассировкой) теперь печатается как `AUTO-EQUIP: init output:`, а «карта есть, но карта не создана» — это ошибка, а не тихое «карта пропала». Сторож повторяет с удвоением задержки (5 с … 60 с), пока карта в ридере; снятие карты полностью очищает состояние, поэтому обработчики отвечают «нет карты», а не шлют APDU мёртвой карте. +- **Ошибки "Card was removed" / equip не срабатывает после смены карты** — вставка карты помечает соединение PC/SC для пересоздания (поэтому **первая** попытка уже использует свежее соединение — в Windows старое после смены карты подключиться не может), авто-equip повторяется до 3 раз, и **каждая неудачная попытка пересоздаёт транспорт** перед следующей. Карта, снятая во время инициализации при старте сервера, оставляет его без карты с одной строкой (без трассировки), а следующая вставка инициализируется обычным образом. Проглоченная ошибка инициализации (с трассировкой) теперь печатается как `AUTO-EQUIP: init output:`, а «карта есть, но карта не создана» — это ошибка, а не тихое «карта пропала». Сторож повторяет с удвоением задержки (5 с … 60 с), пока карта в ридере; снятие карты полностью очищает состояние, поэтому обработчики отвечают «нет карты», а не шлют APDU мёртвой карте. ### Справочник API diff --git a/frontend/index.html b/frontend/index.html index 040328c..ef3c129 100644 --- a/frontend/index.html +++ b/frontend/index.html @@ -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.4'; +const SIMPLE_VERSION = '3.32.5'; document.getElementById('app-version').textContent = 'v' + SIMPLE_VERSION; // ===== view history (the browser Back/Forward buttons) ===== diff --git a/frontend/sw.js b/frontend/sw.js index 887da9e..191d388 100644 --- a/frontend/sw.js +++ b/frontend/sw.js @@ -1,4 +1,4 @@ -const CACHE = 'simple-v462'; +const CACHE = 'simple-v463'; const URLS = [ 'index.html', 'help.html', diff --git a/pyproject.toml b/pyproject.toml index 8855c0c..6da310d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "pysim-simple-server" -version = "3.32.4" +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" diff --git a/pysim_simple_server/__main__.py b/pysim_simple_server/__main__.py index eeb2032..7dcab22 100644 --- a/pysim_simple_server/__main__.py +++ b/pysim_simple_server/__main__.py @@ -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,8 +295,26 @@ def main(): break _handle_proactive_chain(scc, st_sw) _tlog('terminal_profile_drain: %.0fms' % ((time.time() - t_phase) * 1000)) - except Exception: - traceback.print_exc(file=sys.stderr) + 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: # PysimApp.__init__ routes PySimLogger through app.poutput() (app.stdout) diff --git a/pysim_simple_server/server.py b/pysim_simple_server/server.py index 5297507..ba78627 100644 --- a/pysim_simple_server/server.py +++ b/pysim_simple_server/server.py @@ -37,7 +37,7 @@ from osmocom.tlv import BER_TLV_IE from osmocom.utils import rpad -VERSION = '3.32.4' +VERSION = '3.32.5' MAX_ENVELOPE_SEGMENTS = 5 # max SMS segments for outgoing C-APDU in ENVELOPE diff --git a/tests/test_startup_cardless.py b/tests/test_startup_cardless.py index d6aeeb8..8168551 100644 --- a/tests/test_startup_cardless.py +++ b/tests/test_startup_cardless.py @@ -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)