Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 7e8c483eb1 |
@@ -9,6 +9,29 @@ every change carries a descriptive commit message.
|
||||
|
||||
## [Unreleased]
|
||||
|
||||
## [3.32.4] — 2026-10-21
|
||||
|
||||
### Fixed
|
||||
|
||||
- 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.3] — 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 (seen in the first Windows confirmation of v3.32.2). 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.
|
||||
|
||||
## [3.32.2] — 2026-10-21
|
||||
|
||||
### Fixed
|
||||
|
||||
@@ -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** — the auto-equip retries up to 3 times, and **every failed attempt rebuilds the PC/SC transport** before the next one: after a card swap on Windows the old link can stay unable to connect, which used to fail every retry and re-arm forever. 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. 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 не срабатывает после смены карты** — авто-equip повторяется до 3 раз, и **каждая неудачная попытка пересоздаёт транспорт PC/SC** перед следующей: после смены карты в Windows старое соединение может остаться неспособным подключиться, из-за чего раньше все повторы падали, а сторож перезапускал попытки бесконечно. Проглоченная ошибка инициализации (с трассировкой) теперь печатается как `AUTO-EQUIP: init output:`, а «карта есть, но карта не создана» — это ошибка, а не тихое «карта пропала». Сторож повторяет с удвоением задержки (5 с … 60 с), пока карта в ридере; снятие карты полностью очищает состояние, поэтому обработчики отвечают «нет карты», а не шлют 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.2';
|
||||
const SIMPLE_VERSION = '3.32.4';
|
||||
document.getElementById('app-version').textContent = 'v' + SIMPLE_VERSION;
|
||||
|
||||
// ===== view history (the browser Back/Forward buttons) =====
|
||||
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
const CACHE = 'simple-v460';
|
||||
const CACHE = 'simple-v462';
|
||||
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.2"
|
||||
version = "3.32.4"
|
||||
description = "HTTP REST server wrapping pysim for the SIMple PWA"
|
||||
license = { text = "GPL-2.0-or-later" }
|
||||
requires-python = ">=3.9"
|
||||
|
||||
@@ -37,7 +37,7 @@ from osmocom.tlv import BER_TLV_IE
|
||||
from osmocom.utils import rpad
|
||||
|
||||
|
||||
VERSION = '3.32.2'
|
||||
VERSION = '3.32.4'
|
||||
|
||||
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:
|
||||
@@ -5744,8 +5745,15 @@ def _auto_equip_attempt(server):
|
||||
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(lines[start:start + 12]))
|
||||
% '\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
|
||||
@@ -5819,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
|
||||
|
||||
@@ -444,6 +444,28 @@ class AutoEquipTests(unittest.TestCase):
|
||||
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)
|
||||
|
||||
Reference in New Issue
Block a user