Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 7e8c483eb1 |
@@ -9,6 +9,29 @@ every change carries a descriptive commit message.
|
|||||||
|
|
||||||
## [Unreleased]
|
## [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
|
## [3.32.2] — 2026-10-21
|
||||||
|
|
||||||
### Fixed
|
### 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`.
|
- **"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.
|
- **"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.
|
- **"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
|
### 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`.
|
- **"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`. Сервер всё равно стартует без ридера.
|
- **"device file /dev/ttyUSB0 does not exist"** — нет serial-ридера; подключите USB-ридер или укажите `-d`. Сервер всё равно стартует без ридера.
|
||||||
- **"Service not available" (0x8010001D) / ридер пропал после перезапуска `pcscd`** — сервер восстанавливается сам: сторож перезапускает монитор присутствия pyscard, а следующий equip (автоматически после повторной вставки карты или кнопкой **Equip**) пересоздаёт транспорт PC/SC (сначала освобождая старый линк). Перезапуск сервера не нужен.
|
- **"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
|
### Справочник API
|
||||||
|
|
||||||
|
|||||||
+1
-1
@@ -1943,7 +1943,7 @@
|
|||||||
// ===== Version =====
|
// ===== Version =====
|
||||||
// Single source of truth for the PWA version: shown in the header and used
|
// Single source of truth for the PWA version: shown in the header and used
|
||||||
// by the server version check in pysimConnect().
|
// 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;
|
document.getElementById('app-version').textContent = 'v' + SIMPLE_VERSION;
|
||||||
|
|
||||||
// ===== view history (the browser Back/Forward buttons) =====
|
// ===== view history (the browser Back/Forward buttons) =====
|
||||||
|
|||||||
+1
-1
@@ -1,4 +1,4 @@
|
|||||||
const CACHE = 'simple-v460';
|
const CACHE = 'simple-v462';
|
||||||
const URLS = [
|
const URLS = [
|
||||||
'index.html',
|
'index.html',
|
||||||
'help.html',
|
'help.html',
|
||||||
|
|||||||
+1
-1
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|||||||
|
|
||||||
[project]
|
[project]
|
||||||
name = "pysim-simple-server"
|
name = "pysim-simple-server"
|
||||||
version = "3.32.2"
|
version = "3.32.4"
|
||||||
description = "HTTP REST server wrapping pysim for the SIMple PWA"
|
description = "HTTP REST server wrapping pysim for the SIMple PWA"
|
||||||
license = { text = "GPL-2.0-or-later" }
|
license = { text = "GPL-2.0-or-later" }
|
||||||
requires-python = ">=3.9"
|
requires-python = ">=3.9"
|
||||||
|
|||||||
@@ -37,7 +37,7 @@ from osmocom.tlv import BER_TLV_IE
|
|||||||
from osmocom.utils import rpad
|
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
|
MAX_ENVELOPE_SEGMENTS = 5 # max SMS segments for outgoing C-APDU in ENVELOPE
|
||||||
|
|
||||||
@@ -5426,7 +5426,8 @@ def _ensure_transport(server):
|
|||||||
try:
|
try:
|
||||||
old.disconnect()
|
old.disconnect()
|
||||||
except Exception as e:
|
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:
|
try:
|
||||||
sl = factory()
|
sl = factory()
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
@@ -5744,8 +5745,15 @@ def _auto_equip_attempt(server):
|
|||||||
lines = captured.splitlines()
|
lines = captured.splitlines()
|
||||||
start = next(i for i, l in enumerate(lines)
|
start = next(i for i, l in enumerate(lines)
|
||||||
if 'Traceback (most recent call last)' in l)
|
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'
|
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
|
# ANY failed attempt leaves the PC/SC card handle suspect: after a
|
||||||
# card swap on Windows the link can stay unable to connect, so
|
# card swap on Windows the link can stay unable to connect, so
|
||||||
# every retry failed and the watchdog re-armed forever (live
|
# every retry failed and the watchdog re-armed forever (live
|
||||||
@@ -5819,6 +5827,15 @@ class _CardPresenceObserver(CardObserver):
|
|||||||
with _CARD_LOCK:
|
with _CARD_LOCK:
|
||||||
if _server_ref:
|
if _server_ref:
|
||||||
_server_ref.card_present = True
|
_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
|
trigger_auto = True
|
||||||
if trigger_auto and _AUTO_EQUIP:
|
if trigger_auto and _AUTO_EQUIP:
|
||||||
_auto_equip_trigger()
|
_auto_equip_trigger()
|
||||||
|
|||||||
@@ -35,8 +35,11 @@ class TestCardPresenceObserver(unittest.TestCase):
|
|||||||
self.trigger_mock = self.trigger.start()
|
self.trigger_mock = self.trigger.start()
|
||||||
self.saved_auto = S._AUTO_EQUIP
|
self.saved_auto = S._AUTO_EQUIP
|
||||||
S._AUTO_EQUIP = True
|
S._AUTO_EQUIP = True
|
||||||
|
self.saved_stale = S._TRANSPORT_STALE
|
||||||
|
S._TRANSPORT_STALE = False
|
||||||
|
|
||||||
def tearDown(self):
|
def tearDown(self):
|
||||||
|
S._TRANSPORT_STALE = self.saved_stale
|
||||||
S._AUTO_EQUIP = self.saved_auto
|
S._AUTO_EQUIP = self.saved_auto
|
||||||
self.trigger.stop()
|
self.trigger.stop()
|
||||||
self.patcher.stop()
|
self.patcher.stop()
|
||||||
@@ -61,6 +64,16 @@ class TestCardPresenceObserver(unittest.TestCase):
|
|||||||
self.assertEqual(self.disconnects, [])
|
self.assertEqual(self.disconnects, [])
|
||||||
self.trigger_mock.assert_called_once()
|
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):
|
def test_insertion_does_not_trigger_when_disabled(self):
|
||||||
S._AUTO_EQUIP = False
|
S._AUTO_EQUIP = False
|
||||||
self.server.card_present = False
|
self.server.card_present = False
|
||||||
|
|||||||
@@ -444,6 +444,28 @@ class AutoEquipTests(unittest.TestCase):
|
|||||||
self.assertIn('AUTO-EQUIP: init output:', out)
|
self.assertIn('AUTO-EQUIP: init output:', out)
|
||||||
self.assertIn('ProtocolError: Connection failed', 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):
|
def test_failed_attempt_unequips_the_half_initialized_shell(self):
|
||||||
calls = []
|
calls = []
|
||||||
srv = self.make_server(None)
|
srv = self.make_server(None)
|
||||||
|
|||||||
Reference in New Issue
Block a user