server+ui: auto-equip on card insertion; reset card views on session change
- _apply_equipped_card() centralizes the post-equip refresh + TERMINAL PROFILE (shared by the /api/command equip branch and auto-equip) - server tracks card_session (bumped on equip and disconnect) and equipping; /api/status exposes connected, card_present, card_session, equipping, auto_equip and is exempt from _CARD_LOCK (pure cached state) - auto-equip is on by default (--no-auto-equip; off with --no-card-init): the presence observer spawns a one-shot worker after insertion, which runs equip under _CARD_LOCK and applies the same refresh; the monitor starts after the startup init so pyscard's initial 'already present' event does not re-equip a fresh session - UI: /api/status polls every 2s (other views stay at 5s); when card_session changes it runs pysimResetCardData() (STK overlay, file tree, events, proactive log, PLI, status) — the same reset as a manual Equip; messages: initializing / press Equip / no card Tests for the observer and auto-equip rules, session bumps, _apply_equipped_card, and the UI state machine. SW cache v91 -> v92.
This commit is contained in:
@@ -12,7 +12,7 @@ from pySim.cards import UiccCardBase
|
||||
|
||||
from .shell import load_pysim_app
|
||||
from . import fastinit
|
||||
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
|
||||
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
|
||||
|
||||
|
||||
_server_start = 0
|
||||
@@ -55,6 +55,8 @@ def main():
|
||||
help="Use pysim's stock init_card/equip (multiple physical card resets) instead of the default reset-free fast init")
|
||||
parser.add_argument('--menu-timeout', type=int, default=60, metavar='SECS',
|
||||
help='Auto-send a timeout TERMINAL RESPONSE if a paused STK command is not answered (default: 60, 0 disables)')
|
||||
parser.add_argument('--no-auto-equip', action='store_true', default=False,
|
||||
help='Do not automatically initialize a card right after it is inserted (default: auto-equip on)')
|
||||
|
||||
opts = parser.parse_args()
|
||||
opts.skip_card_init = opts.no_card_init
|
||||
@@ -63,6 +65,7 @@ def main():
|
||||
_timing_on()
|
||||
if opts.menu_timeout is not None:
|
||||
_set_menu_timeout(opts.menu_timeout)
|
||||
set_auto_equip(not opts.no_auto_equip and not opts.skip_card_init)
|
||||
sl = None
|
||||
scc = None
|
||||
card = None
|
||||
@@ -93,8 +96,6 @@ def main():
|
||||
t_phase = time.time()
|
||||
sl = mod.init_reader(opts, **kwargs)
|
||||
_tlog('init_reader: %.0fms' % ((time.time() - t_phase) * 1000))
|
||||
if getattr(sl, '_reader', None) is not None:
|
||||
start_card_monitor(str(sl._reader))
|
||||
scc = SimCardCommands(sl)
|
||||
scc.cat_cla = '80' # UICC CLA default; overridden for SIM after init_card
|
||||
scc._tp.proactive_handler = _DefaultProactiveHandler()
|
||||
@@ -182,6 +183,8 @@ def main():
|
||||
server.menu_active = False
|
||||
server.stk_pending = None
|
||||
server.card_present = card is not None
|
||||
server.card_session = 1 if card is not None else 0
|
||||
server.equipping = False
|
||||
# Set server reference for polling timer and mark the card session state
|
||||
import pysim_otaman_server.server
|
||||
pysim_otaman_server.server._server_ref = server
|
||||
@@ -191,6 +194,12 @@ def main():
|
||||
# Auto-enable polling if card initialized successfully (unless interval is 0)
|
||||
if server.scc and server.card and opts.poll_interval != 0:
|
||||
pysim_otaman_server.server._poll_enable()
|
||||
# Start presence monitoring only after the startup init: pyscard reports an
|
||||
# already-present card as "added" on the first pass, and we must not
|
||||
# auto-equip over a session we just initialized. If startup init failed,
|
||||
# that event triggers auto-equip instead — the desired retry.
|
||||
if sl is not None and getattr(sl, '_reader', None) is not None:
|
||||
start_card_monitor(str(sl._reader))
|
||||
print("─" * 70)
|
||||
print(" pysim-otaman-server v%s listening on http://%s:%s" % (VERSION, opts.http_host, opts.http_port))
|
||||
print(" Open http://%s:%s in your browser for the OTAMan UI (served by this server)."
|
||||
|
||||
@@ -1032,9 +1032,82 @@ def _handle_card_disconnect():
|
||||
_server_ref.menu_active = False
|
||||
_server_ref.event_list = None
|
||||
_server_ref.sim_menu = None
|
||||
_server_ref.equipping = False
|
||||
_server_ref.card_session = getattr(_server_ref, 'card_session', 0) + 1
|
||||
_reset_proactive_log()
|
||||
|
||||
|
||||
def _apply_equipped_card(server):
|
||||
"""Common post-equip state refresh + TERMINAL PROFILE, shared by the
|
||||
/api/command equip branch and the auto-equip worker."""
|
||||
global _CARD_CONNECTED
|
||||
server.stk_pending = None
|
||||
server.menu_active = False
|
||||
_cancel_menu_timeout()
|
||||
server.event_list = None
|
||||
_reset_proactive_log()
|
||||
server.card = server.app.card
|
||||
server.scc = server.app.card._scc
|
||||
server.scc.cat_cla = '80' if isinstance(server.card, UiccCardBase) else 'a0'
|
||||
_CARD_CONNECTED = True
|
||||
server.card_present = True
|
||||
server.card_session = getattr(server, 'card_session', 0) + 1
|
||||
_poll_enable()
|
||||
sm, el = _send_terminal_profile(server.scc, server.terminal_profile)
|
||||
server.sim_menu = sm
|
||||
server.event_list = el
|
||||
_tlog('equip: terminal profile done')
|
||||
|
||||
|
||||
_AUTO_EQUIP = True
|
||||
_AUTO_EQUIP_BUSY = False
|
||||
|
||||
def set_auto_equip(enabled):
|
||||
global _AUTO_EQUIP
|
||||
_AUTO_EQUIP = bool(enabled)
|
||||
|
||||
def _auto_equip_trigger():
|
||||
"""Spawn a one-shot worker; never run equip in the pyscard monitor thread."""
|
||||
global _AUTO_EQUIP_BUSY
|
||||
if not _AUTO_EQUIP or _AUTO_EQUIP_BUSY:
|
||||
return
|
||||
_AUTO_EQUIP_BUSY = True
|
||||
threading.Thread(target=_auto_equip_worker, name='auto-equip', daemon=True).start()
|
||||
|
||||
def _auto_equip_worker():
|
||||
global _AUTO_EQUIP_BUSY
|
||||
try:
|
||||
with _CARD_LOCK:
|
||||
server = _server_ref
|
||||
if not server or _CARD_CONNECTED or not getattr(server, 'card_present', False):
|
||||
return
|
||||
app = server.app
|
||||
if app is None or not getattr(server, 'terminal_profile', None):
|
||||
return
|
||||
server.equipping = True
|
||||
try:
|
||||
sys.stderr.write('AUTO-EQUIP: card inserted, initializing\n')
|
||||
old_stdout, old_stderr = app.stdout, sys.stderr
|
||||
app.stdout = StringIO()
|
||||
sys.stderr = app.stdout
|
||||
try:
|
||||
app.onecmd_plus_hooks('equip')
|
||||
finally:
|
||||
app.stdout = old_stdout
|
||||
sys.stderr = old_stderr
|
||||
if not getattr(server, 'card_present', False) or server.app.card is None:
|
||||
sys.stderr.write('AUTO-EQUIP: card gone during initialization\n')
|
||||
return
|
||||
_apply_equipped_card(server)
|
||||
sys.stderr.write('AUTO-EQUIP: done\n')
|
||||
except Exception as e:
|
||||
sys.stderr.write('AUTO-EQUIP failed: %s\n' % e)
|
||||
finally:
|
||||
server.equipping = False
|
||||
finally:
|
||||
_AUTO_EQUIP_BUSY = False
|
||||
|
||||
|
||||
class _CardPresenceObserver(CardObserver):
|
||||
"""Passive PC/SC presence watcher: pyscard's CardMonitor only polls
|
||||
SCardGetStatusChange (no connection, no APDUs), so it can never interleave
|
||||
@@ -1046,6 +1119,7 @@ class _CardPresenceObserver(CardObserver):
|
||||
def update(self, observable, handlers):
|
||||
addedcards, removedcards = handlers
|
||||
try:
|
||||
trigger_auto = False
|
||||
for card in removedcards:
|
||||
if str(getattr(card, 'reader', '')) == self.reader_name:
|
||||
sys.stderr.write('CARD-WATCH: card removed from %s\n' % self.reader_name)
|
||||
@@ -1055,10 +1129,13 @@ class _CardPresenceObserver(CardObserver):
|
||||
_handle_card_disconnect()
|
||||
for card in addedcards:
|
||||
if str(getattr(card, 'reader', '')) == self.reader_name:
|
||||
sys.stderr.write('CARD-WATCH: card inserted into %s (press Equip)\n' % self.reader_name)
|
||||
sys.stderr.write('CARD-WATCH: card inserted into %s\n' % self.reader_name)
|
||||
with _CARD_LOCK:
|
||||
if _server_ref:
|
||||
_server_ref.card_present = True
|
||||
trigger_auto = True
|
||||
if trigger_auto and _AUTO_EQUIP:
|
||||
_auto_equip_trigger()
|
||||
except Exception as e:
|
||||
sys.stderr.write('CARD-WATCH error: %s\n' % e)
|
||||
|
||||
@@ -1551,6 +1628,13 @@ class PysimHandler(BaseHTTPRequestHandler):
|
||||
self.wfile.write(data)
|
||||
|
||||
def do_GET(self):
|
||||
# /api/status is pure cached state (no card I/O); keeping it out of the
|
||||
# lock lets the UI report 'initializing' while a long equip holds the
|
||||
# card lock. Result-shaping masks everything card-derived when the
|
||||
# session is not connected.
|
||||
if self.path == '/api/status':
|
||||
self._do_GET()
|
||||
return
|
||||
# Serialize all card access: the background STATUS poll runs in its own
|
||||
# thread and must never interleave with a FETCH/TERMINAL RESPONSE pair.
|
||||
with _CARD_LOCK:
|
||||
@@ -1581,6 +1665,9 @@ class PysimHandler(BaseHTTPRequestHandler):
|
||||
'reader': str(self.server.sl) if self.server.sl else None,
|
||||
'connected': connected,
|
||||
'card_present': bool(getattr(self.server, 'card_present', False)),
|
||||
'card_session': int(getattr(self.server, 'card_session', 0)),
|
||||
'equipping': bool(getattr(self.server, 'equipping', False)),
|
||||
'auto_equip': bool(_AUTO_EQUIP),
|
||||
'card': card.name if card else None,
|
||||
'profile': str(rs.profile) if rs and rs.profile else None,
|
||||
'app_ready': app is not None,
|
||||
@@ -1715,22 +1802,7 @@ class PysimHandler(BaseHTTPRequestHandler):
|
||||
if is_equip:
|
||||
_tlog('equip: onecmd_plus_hooks %dms' % elapsed)
|
||||
if is_equip and self.server.app and self.server.app.card and self.server.terminal_profile:
|
||||
global _CARD_CONNECTED
|
||||
self.server.stk_pending = None
|
||||
self.server.menu_active = False
|
||||
_cancel_menu_timeout()
|
||||
self.server.event_list = None
|
||||
_reset_proactive_log()
|
||||
self.server.card = self.server.app.card
|
||||
self.server.scc = self.server.app.card._scc
|
||||
self.server.scc.cat_cla = '80' if isinstance(self.server.card, UiccCardBase) else 'a0'
|
||||
_CARD_CONNECTED = True
|
||||
self.server.card_present = True
|
||||
_poll_enable()
|
||||
sm, el = _send_terminal_profile(self.server.scc, self.server.terminal_profile)
|
||||
self.server.sim_menu = sm
|
||||
self.server.event_list = el
|
||||
_tlog('equip: terminal profile done')
|
||||
_apply_equipped_card(self.server)
|
||||
sys.stderr.write("CMD: %s → %s (%dms)\n" % (cmd, status, elapsed))
|
||||
resp = {'output': output, 'stop': bool(stop)}
|
||||
self._send_json(resp)
|
||||
|
||||
Reference in New Issue
Block a user