startup: report PC/SC access denial instead of a silent serial fallback

When no reader was named and readers() failed (e.g. SCARD_E_NO_ACCESS -
the user may not use pcscd), the auto-detect swallowed the error and pySim
fell back to the Serial transport, failing on /dev/ttyUSB0 while the
server still printed "listening".  The startup now probes PC/SC,
classifies the failure by hresult (access/service/reader), reports the
cause with the pcscd group/polkit and pcsc_scan guidance and exits 2; the
python -m guard uses sys.exit(main()).  An explicit -d/--device keeps the
serial path; a reader without a card stays a normal cardless start; a
readerless start exits with its own message.

start.sh runs the same access pre-flight before launching (readers() with
a 3x retry for socket activation; -d is detected and skips it).  READMEs,
CHANGELOG (folded into the unreleased 3.38.1) and AGENTS updated.

Tests: tests/test_pcsc_startup.py (16: the error classifier, the probe,
the report guidance, the serial-arg detection and the source pins).
1083 Python + 1075 frontend.
This commit is contained in:
2026-10-25 00:29:26 +03:00
parent 0231df905c
commit a269f8bd2e
6 changed files with 333 additions and 31 deletions
+123 -18
View File
@@ -7,7 +7,7 @@ import traceback
from http.server import ThreadingHTTPServer
from pySim.card_handler import CardHandler
from pySim.commands import SimCardCommands
from pySim.exceptions import NoCardError
from pySim.exceptions import NoCardError, ReaderError
from pySim.log import PySimLogger
from pySim.cards import UiccCardBase
@@ -23,6 +23,107 @@ from .server import PysimHandler, StderrApduTracer, _LoggingApduTracer, VERSION,
_server_start = 0
# PC/SC hresults (pyscard's exceptions carry them; the messages are often
# empty, so the hresult is the reliable signal).
_PCSC_E_UNKNOWN_READER = 0x80100009
_PCSC_E_NO_SERVICE = 0x8010001D
_PCSC_E_SERVICE_STOPPED = 0x8010001E
_PCSC_E_NO_ACCESS = 0x80100027
_PCSC_E_NO_READERS_AVAILABLE = 0x8010002E
class PcscUnavailable(RuntimeError):
"""The PC/SC stack cannot be used (kind: access/service/reader)."""
def __init__(self, kind, cause=None):
super().__init__(kind)
self.kind = kind
self.cause = cause
def _pcsc_error_kind(exc):
"""Classify a pyscard failure: 'access', 'service', 'reader' or None.
``SCARD_E_NO_ACCESS`` is the "Access denied" a user without permission to
pcscd gets; the message is a fallback because some pyscard paths raise a
CardConnectionException with an empty message and only the hresult."""
hr = getattr(exc, 'hresult', None)
msg = str(exc or '').lower()
if hr == _PCSC_E_NO_ACCESS or 'access denied' in msg or 'access is denied' in msg:
return 'access'
if hr in (_PCSC_E_NO_SERVICE, _PCSC_E_SERVICE_STOPPED) or \
'resource manager is not running' in msg or 'service not available' in msg:
return 'service'
if hr in (_PCSC_E_UNKNOWN_READER, _PCSC_E_NO_READERS_AVAILABLE) or \
'no smart card readers' in msg:
return 'reader'
return None
def _report_pcsc_unavailable(kind, exc=None):
"""Print an actionable reason instead of starting a readerless server."""
if kind == 'access':
user = os.environ.get('USER') or os.environ.get('LOGNAME') or '$USER'
sys.stderr.write('INIT: PC/SC access denied - the current user may not use pcscd.\n')
sys.stderr.write(' Add the user to the pcscd group: sudo usermod -aG pcscd %s\n' % user)
sys.stderr.write(' (then log out and back in), or configure the polkit rule that\n')
sys.stderr.write(' allows the user to use pcscd. `pcsc_scan` shows the same\n')
sys.stderr.write(' "SCardEstablishContext: Access denied" error.\n')
elif kind == 'service':
sys.stderr.write('INIT: pcscd is not running or not reachable.\n')
sys.stderr.write(' Start it (sudo systemctl start pcscd) and restart the server.\n')
else:
sys.stderr.write('INIT: no PC/SC reader found.\n')
sys.stderr.write(' Connect a reader (pcsc_scan lists them) and restart the server.\n')
if exc is not None:
hr = getattr(exc, 'hresult', None)
detail = str(exc) or type(exc).__name__
if hr not in (None, -1):
detail += ' (hresult=0x%08X)' % hr
sys.stderr.write(' (%s)\n' % detail)
def _probe_pcsc_reader(retries=3, sleep_fn=time.sleep, readers_fn=None):
"""Return the first PC/SC reader name or raise PcscUnavailable.
``SCARD_E_NO_ACCESS``/service failures are raised immediately (retrying
cannot fix permissions or a stopped service); an empty list is retried
(USB enumeration) and reported as 'reader' after the last attempt."""
if readers_fn is None:
from smartcard.System import readers as readers_fn
last = None
for attempt in range(retries):
try:
found = readers_fn()
except Exception as exc:
last = exc
kind = _pcsc_error_kind(exc)
if kind in ('access', 'service'):
raise PcscUnavailable(kind, exc) from exc
else:
if found:
return str(found[0])
last = None
if attempt + 1 < retries:
sys.stderr.write('INIT: no PC/SC reader yet, retrying in 2s...\n')
sleep_fn(2)
raise PcscUnavailable('reader', last)
def _serial_arg_given(argv=None):
"""True when the operator explicitly chose the serial reader (-d/--device).
The default (-d is /dev/ttyUSB0 in pySim's parser) must not masquerade as
an explicit choice: only a flag on the command line counts."""
argv = sys.argv[1:] if argv is None else argv
for arg in argv:
if arg in ('-d', '--device') or arg.startswith('--device='):
return True
if arg.startswith('-d') and not arg.startswith('--') and len(arg) > 2:
return True
return False
def _log_stdout(msg):
elapsed = time.time() - _server_start
os.write(1, ('[%8.3f] %s\n' % (elapsed, msg)).encode())
@@ -164,22 +265,18 @@ def main():
rs = None
sim_menu = None
event_list = None
# Auto-detect PC/SC reader if none was explicitly specified.
# Handles late pcscd startup and USB enumeration delays.
if opts.pcsc_dev is None and opts.pcsc_regex is None:
# Auto-detect the PC/SC reader unless one was named explicitly or the
# operator chose the serial transport (-d). A PC/SC access denial or a
# missing service must be reported - silently falling back to the serial
# reader left the server "listening" without a usable card link (v3.38.1).
if opts.pcsc_dev is None and opts.pcsc_regex is None and not _serial_arg_given():
try:
from smartcard.System import readers
for attempt in range(3):
r = readers()
if r:
sys.stderr.write('INIT: PC/SC reader detected: %s\n' % r[0])
opts.pcsc_dev = 0
break
if attempt < 2:
sys.stderr.write('INIT: no PC/SC readers found, retrying in 2s...\n')
time.sleep(2)
except Exception:
pass # smartcard module not available or pcscd unreachable
reader = _probe_pcsc_reader()
except PcscUnavailable as e:
_report_pcsc_unavailable(e.kind, e.cause)
return 2
sys.stderr.write('INIT: PC/SC reader detected: %s\n' % reader)
opts.pcsc_dev = 0
try:
kwargs = {}
@@ -223,7 +320,15 @@ def main():
# defaults, which made VERIFY go out with CLA A0 on a UICC).
scc = card._scc
scc.cat_cla = '80' if isinstance(card, UiccCardBase) else 'a0'
except Exception:
except ReaderError as e:
sys.stderr.write('INIT: no PC/SC reader found: %s\n' % e)
sys.stderr.write(' Connect the reader (pcsc_scan lists them) and restart the server.\n')
return 2
except Exception as e:
kind = _pcsc_error_kind(e)
if kind:
_report_pcsc_unavailable(kind, e)
return 2
print("Warning: reader/card initialization failed:", file=sys.stderr)
traceback.print_exc()
ch = CardHandler(sl) if sl else None
@@ -438,4 +543,4 @@ def main():
if __name__ == '__main__':
main()
sys.exit(main())