pySim's scc.read_binary/read_record call select_path(ef) first, so the ICCID, network-state, EF.ARR and EF.PBR readers - which had already made a parent-scoped selection and hold its FCP - sent a second SELECT per file and per record. _read_selected_ef() issues READ BINARY (chunked at max_cmd_len from the FCP file size) / READ RECORD (b2<rec>04<len> at the FCP record length) directly; the pySim helpers stay as the fallback when the FCP lacks the value. The startup TERMINAL PROFILE drain now breaks on the chain's idle SW instead of polling STATUS once more. Measured on the last startup: ~3.4 s traced init (netstate/ARR/PBR 271 APDUs / 1.26 s incl. 169 SELECTs; the card's own TP processing 1.58 s); the change removes ~0.7-0.8 s of the read phase without touching any data or the TP. v3.38.1; tests: tests/test_read_selected.py (direct reads, chunking, the fallback and the drain source pin) and the ARR/PBR/ICCID/netstate fakes. 1067 Python + 1075 frontend. sw cache simple-v482.
441 lines
22 KiB
Python
441 lines
22 KiB
Python
import argparse
|
|
import logging
|
|
import os
|
|
import sys
|
|
import time
|
|
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.log import PySimLogger
|
|
from pySim.cards import UiccCardBase
|
|
|
|
from .shell import load_pysim_app
|
|
from . import fastinit
|
|
from . import gsmtap
|
|
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, _is_pcsc_error, _clear_app_card_state
|
|
|
|
|
|
_server_start = 0
|
|
|
|
def _log_stdout(msg):
|
|
elapsed = time.time() - _server_start
|
|
os.write(1, ('[%8.3f] %s\n' % (elapsed, msg)).encode())
|
|
|
|
|
|
def _guard_console_streams():
|
|
"""Never let a non-encodable character kill the server.
|
|
|
|
On Windows the launcher pipes stdout/stderr into PowerShell (see start.bat),
|
|
so Python encodes with the ANSI code page instead of the wide console API -
|
|
cp1251 cannot encode the banner's box-drawing rule (U+2500) or the APDU
|
|
trace arrows (U+2192), and one such character raised UnicodeEncodeError and
|
|
killed the process. errors="replace" keeps every message, whatever the code
|
|
page (a character the code page lacks prints as '?')."""
|
|
for stream in (sys.stdout, sys.stderr):
|
|
try:
|
|
stream.reconfigure(errors="replace")
|
|
except Exception:
|
|
pass
|
|
|
|
|
|
def _console_rule(n=70):
|
|
"""The banner rule in the widest character the output stream can encode.
|
|
|
|
Under start.bat stdout is a pipe (not a console), so Python encodes with the
|
|
ANSI code page - cp1251 (Russian Windows) cannot encode the box-drawing
|
|
U+2500 and falls back to a plain ASCII rule (cp437 has the box-drawing
|
|
characters and keeps the pretty one)."""
|
|
enc = getattr(sys.stdout, "encoding", None) or "ascii"
|
|
try:
|
|
"\u2500".encode(enc)
|
|
return "\u2500" * n
|
|
except (UnicodeEncodeError, LookupError):
|
|
return "-" * n
|
|
|
|
|
|
def _default_web_dir():
|
|
# <repo>/frontend, whether run from source or an editable install.
|
|
return os.path.join(os.path.dirname(os.path.dirname(os.path.abspath(__file__))), 'frontend')
|
|
|
|
|
|
def _default_mcc_mnc_list():
|
|
# Bundled default: the MIT-licensed operator list ships with the package
|
|
# (data/mcc-mnc-list.json, from pbakondy/mcc-mnc-list); --mcc-mnc-list
|
|
# overrides it and the endpoint reports available=false when missing.
|
|
pkg = os.path.dirname(os.path.abspath(__file__))
|
|
return os.path.join(pkg, 'data', 'mcc-mnc-list.json')
|
|
|
|
|
|
def _build_http_server(host, port, handler):
|
|
"""HTTP server for the PWA + API.
|
|
|
|
Threaded on purpose: card access is serialized by ``_CARD_LOCK`` (all
|
|
POSTs, the card-touching GETs and the background threads take it), so
|
|
while a long operation holds the lock the cached endpoints and static
|
|
files must still be answerable - a single-threaded server starved the
|
|
RAM modal's live progress poll (`/api/status.ram_progress`) and showed
|
|
the whole install result only at the end. Daemon threads keep a stuck
|
|
request from blocking shutdown."""
|
|
return ThreadingHTTPServer((host, port), handler)
|
|
|
|
|
|
def main():
|
|
global _server_start
|
|
_guard_console_streams()
|
|
_server_start = time.time()
|
|
mod = load_pysim_app()
|
|
parser = mod.option_parser
|
|
parser.description = 'pysim-simple-server — HTTP API for pysim'
|
|
parser.add_argument('--http-host', default='127.0.0.1', help='Bind address (default: 127.0.0.1)')
|
|
parser.add_argument('--http-port', type=int, default=8080, help='Bind port (default: 8080)')
|
|
parser.add_argument('--web-dir', default=_default_web_dir(), metavar='PATH',
|
|
help='Directory with the SIMple PWA static files to serve (default: <repo>/frontend)')
|
|
parser.add_argument('--mcc-mnc-list', default=_default_mcc_mnc_list(), metavar='PATH',
|
|
help='Worldwide MCC/MNC operator list (JSON) for the network-simulation operator picker (default: the bundled data/mcc-mnc-list.json)')
|
|
parser.add_argument('--log-requests', action='store_true', default=False, help='Log request/response payloads to stderr')
|
|
parser.add_argument('--card-presets', default=None, metavar='PATH',
|
|
help='Card preset store (default: ~/.pysim-simple-server/card_presets.json)')
|
|
parser.add_argument('--test-scripts', default=None, metavar='PATH',
|
|
help='Test script store (default: ~/.pysim-simple-server/test_scripts.json)')
|
|
parser.add_argument('--test-suites', default=None, metavar='PATH',
|
|
help='Test suite store (default: ~/.pysim-simple-server/test_suites.json)')
|
|
parser.add_argument('--sms-oa', default='12345', metavar='DIGITS',
|
|
help='TP-Originating-Address (SMSC number) for the SMS-DELIVER TPDU (default: 12345)')
|
|
parser.add_argument('--sms-sm-sc', default='12345678912', metavar='DIGITS',
|
|
help='SM-SC address for SMS-SUBMIT routing in PoR-in-submit mode (default: 12345678912)')
|
|
parser.add_argument('--terminal-profile',
|
|
default='FFFFFFFF7F9F00DFFF03021FE2000000C3FB000704117800710100000038428003',
|
|
metavar='HEX',
|
|
help='TERMINAL PROFILE payload (default: the 33-byte profile of a real BIP-capable handset - the live card only starts HTTP OTA when BIP events/commands are advertised)')
|
|
parser.add_argument('--poll-interval', type=int, default=30, metavar='SECS',
|
|
help='Idle interval before automatic STATUS polling (1-255 seconds, default: 30). Disable with --poll-interval 0')
|
|
parser.add_argument('--no-card-init', action='store_true', default=False,
|
|
help='Skip pysim card initialization (preserve CAT session — no file manager)')
|
|
parser.add_argument('--timing', action='store_true', default=False,
|
|
help='Log phase durations, card resets and APDU counters with elapsed timestamps')
|
|
parser.add_argument('--fast-init', action='store_true', help=argparse.SUPPRESS)
|
|
parser.add_argument('--full-pysim-init', action='store_true', default=False,
|
|
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)')
|
|
parser.add_argument('--gsmtap', nargs='?', const=gsmtap.DEFAULT_TARGET, default=None,
|
|
metavar='HOST[:PORT]',
|
|
help='Stream every APDU (and the card ATR) as GSMTAP-SIM UDP packets for '
|
|
'Wireshark / SIMtrace Analyser (default target: %s)' % gsmtap.DEFAULT_TARGET)
|
|
|
|
opts = parser.parse_args()
|
|
opts.skip_card_init = opts.no_card_init
|
|
opts.fast_init = not opts.full_pysim_init
|
|
if opts.timing:
|
|
_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)
|
|
# APDU tracers: --apdu-trace (stderr) and --gsmtap (GSMTAP-SIM UDP) can be
|
|
# combined; the fan-out keeps pySim's single-tracer interface.
|
|
gsmtap_sender = None
|
|
tracers = []
|
|
if opts.apdu_trace:
|
|
tracers.append(_LoggingApduTracer())
|
|
if opts.gsmtap is not None:
|
|
try:
|
|
gsmtap_host, gsmtap_port = gsmtap.parse_target(opts.gsmtap)
|
|
gsmtap_sender = gsmtap.GsmtapSender(gsmtap_host, gsmtap_port)
|
|
tracers.append(gsmtap.GsmtapApduTracer(gsmtap_sender))
|
|
sys.stderr.write('GSMTAP: streaming APDUs to %s\n' % gsmtap_sender.target)
|
|
except (ValueError, OSError) as e:
|
|
sys.stderr.write('GSMTAP: disabled (%s)\n' % e)
|
|
tracer = None
|
|
if len(tracers) == 1:
|
|
tracer = tracers[0]
|
|
elif tracers:
|
|
tracer = gsmtap.FanoutApduTracer(tracers)
|
|
sl = None
|
|
scc = None
|
|
card = None
|
|
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:
|
|
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
|
|
|
|
try:
|
|
kwargs = {}
|
|
if tracer is not None:
|
|
kwargs['apdu_tracer'] = tracer
|
|
t_phase = time.time()
|
|
sl = mod.init_reader(opts, **kwargs)
|
|
_tlog('init_reader: %.0fms' % ((time.time() - t_phase) * 1000))
|
|
scc = SimCardCommands(sl)
|
|
scc.cat_cla = '80' # UICC CLA default; overridden for SIM after init_card
|
|
scc._tp.proactive_handler = _DefaultProactiveHandler()
|
|
t_phase = time.time()
|
|
if opts.fast_init:
|
|
try:
|
|
rs, card = fastinit.init_card_fast(sl, opts.skip_card_init, wait=True)
|
|
except NoCardError:
|
|
# Normal cardless start: there was no card in the reader (the
|
|
# 3s wait timed out). The presence monitor auto-equips once a
|
|
# card appears; nothing to recover, no traceback.
|
|
sys.stderr.write('INIT: no card in the reader - server ready; insert a card or press Equip\n')
|
|
except Exception:
|
|
print("Warning: fast card initialization failed, falling back to pysim init:", file=sys.stderr)
|
|
traceback.print_exc()
|
|
try:
|
|
rs, card = mod.init_card(sl, opts.skip_card_init)
|
|
except NoCardError:
|
|
# The fallback retried the cardless wait; still a normal
|
|
# cardless start, not an initialization failure.
|
|
sys.stderr.write('INIT: no card in the reader - server ready; insert a card or press Equip\n')
|
|
else:
|
|
try:
|
|
sl.wait_for_card(3)
|
|
rs, card = mod.init_card(sl, opts.skip_card_init)
|
|
except NoCardError:
|
|
sys.stderr.write('INIT: no card in the reader - server ready; insert a card or press Equip\n')
|
|
_tlog('card_init: %.0fms' % ((time.time() - t_phase) * 1000))
|
|
if card is not None:
|
|
# Fast init builds the card on its own SimCardCommands instance;
|
|
# adopt it so server.scc carries the card's cla_byte/sel_ctrl (the
|
|
# placeholder created above is a different object left at the SIM
|
|
# 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:
|
|
print("Warning: reader/card initialization failed:", file=sys.stderr)
|
|
traceback.print_exc()
|
|
ch = CardHandler(sl) if sl else None
|
|
t_phase = time.time()
|
|
try:
|
|
if card is not None:
|
|
app = mod.PysimApp(verbose=opts.verbose, card=card, rs=rs, sl=sl, ch=ch)
|
|
else:
|
|
# Cardless start: pySim logs 'Waiting for card...' (its own retry
|
|
# path) and pySim-shell prints 'pySim-shell not equipped!'; we
|
|
# report both cases with our own single line above. A PysimApp
|
|
# without a card would also retry the cardless wait, so install
|
|
# the filter before constructing it and drop the two internals.
|
|
saved_stdout = sys.stdout
|
|
sys.stdout = _LineFilter(saved_stdout, ('Waiting for card...', 'pySim-shell not equipped!'))
|
|
try:
|
|
app = mod.PysimApp(verbose=opts.verbose, card=None, rs=None, sl=sl, ch=ch)
|
|
finally:
|
|
sys.stdout = saved_stdout
|
|
except Exception:
|
|
print("Warning: PysimApp creation failed:", file=sys.stderr)
|
|
traceback.print_exc()
|
|
app = None
|
|
_tlog('pysim_app: %.0fms' % ((time.time() - t_phase) * 1000))
|
|
if app is not None and opts.fast_init:
|
|
fastinit.install(app)
|
|
iccid = None
|
|
netstate_files = None
|
|
arr_files = None
|
|
pbr_files = None
|
|
if scc and card is not None and hasattr(scc, '_tp'):
|
|
scc._tp.apdu_tracer = _LoggingApduTracer()
|
|
try:
|
|
_init_proactive_session()
|
|
# Read EF.ICCID before the TERMINAL PROFILE starts the CAT session
|
|
# (the PWA auto-selects the matching card preset from it).
|
|
iccid = _read_iccid(app)
|
|
if iccid:
|
|
sys.stderr.write('INIT: ICCID %s\n' % iccid)
|
|
# Network state monitor: read the network-related EFs in the
|
|
# same CAT-free window (skipped without a readable ICCID).
|
|
try:
|
|
netstate_files = _netstate_read(app)
|
|
except Exception:
|
|
netstate_files = None
|
|
# EF.ARR (the FCP '8B' references): read here too - a card
|
|
# present at server start never runs _apply_equipped_card,
|
|
# which owns this read on a later equip.
|
|
arr_files = _read_arr(app)
|
|
if arr_files:
|
|
sys.stderr.write('INIT: EF.ARR %s\n' % ', '.join(
|
|
'%s (%d rec)' % (a['fid'], a['num_records']) for a in arr_files))
|
|
# EF.PBR (the phonebook reference record): same reason - the
|
|
# startup init never runs _apply_equipped_card.
|
|
pbr_files = _read_pbr(app)
|
|
if pbr_files:
|
|
sys.stderr.write('INIT: EF.PBR %s\n' % ', '.join(
|
|
'%s (%d rec)' % (p['fid'], p['num_records']) for p in pbr_files))
|
|
t_phase = time.time()
|
|
sys.stderr.write('INIT: sending TERMINAL PROFILE %s (CLA=%s)\n' % (opts.terminal_profile, scc.cat_cla))
|
|
sm, el = _send_terminal_profile(scc, opts.terminal_profile)
|
|
sys.stderr.write('INIT: TP done, menu=%s events=%s\n' % ('yes' if sm else 'no', 'yes' if el else 'no'))
|
|
sim_menu = sm or sim_menu
|
|
event_list = el or event_list
|
|
for _ in range(3):
|
|
st_data, st_sw = _send_status(scc)
|
|
sys.stderr.write('INIT: drain STATUS -> %s\n' % st_sw)
|
|
if not st_sw.startswith('91'):
|
|
break
|
|
chain_sw = _handle_proactive_chain(scc, st_sw)
|
|
# the chain's trailing STATUS already proved the card idle;
|
|
# polling once more only costs an APDU (v3.38.1)
|
|
if chain_sw and not chain_sw.startswith('91'):
|
|
break
|
|
_tlog('terminal_profile_drain: %.0fms' % ((time.time() - t_phase) * 1000))
|
|
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)
|
|
# and drops the root level to INFO. Re-route pysim's own APDU trace logging
|
|
# directly to fd 1 so it survives the app.stdout/StringIO redirection in the
|
|
# HTTP handlers and the INFO level suppression.
|
|
PySimLogger.setup(print_callback=_log_stdout)
|
|
PySimLogger.set_level(logging.DEBUG)
|
|
# PysimApp.__init__ and every `equip` wipe the transport apdu_tracer
|
|
# (_onchange_apdu_trace sets it to None). Re-attach our tracer(s) and make
|
|
# sure they stay attached across equip/re-equip.
|
|
def _reattach_tracer():
|
|
if app.card:
|
|
app.card._scc._tp.apdu_tracer = tracer
|
|
_reattach_tracer()
|
|
orig_onchange = app._onchange_apdu_trace
|
|
def _onchange_apdu_trace(param_name, old, new):
|
|
orig_onchange(param_name, old, new)
|
|
_reattach_tracer()
|
|
app._onchange_apdu_trace = _onchange_apdu_trace
|
|
server = _build_http_server(opts.http_host, opts.http_port, PysimHandler)
|
|
server.sl = sl
|
|
server.scc = scc
|
|
server.card = card
|
|
server.rs = rs
|
|
server.app = app
|
|
# PC/SC recovery: recreate the transport after a service failure (pcscd
|
|
# restart / reader re-enumeration); _ensure_transport() calls this.
|
|
def _new_transport():
|
|
return mod.init_reader(opts, **kwargs)
|
|
server.transport_factory = _new_transport
|
|
server.sms_oa = opts.sms_oa
|
|
server.sms_sc = opts.sms_sm_sc
|
|
server.log_requests = opts.log_requests
|
|
server.terminal_profile = opts.terminal_profile
|
|
server.cli_terminal_profile = opts.terminal_profile
|
|
server.web_dir = opts.web_dir
|
|
server.mcc_mnc_path = opts.mcc_mnc_list
|
|
# Card presets live server-side (the SCP80 counter is card state the server
|
|
# persists with the operation that consumed it); --card-presets overrides
|
|
# the default store in the user's home.
|
|
server.card_presets = presets.PresetStore(opts.card_presets)
|
|
sys.stderr.write('PRESETS: %s (%d presets)\n'
|
|
% (server.card_presets.path, len(server.card_presets.list())))
|
|
# Test scripts live server-side too (only the server can run them):
|
|
# --test-scripts overrides the default store, and the store validates a
|
|
# script with the same engine the runner uses, so a stored script can
|
|
# never fail at run start for a validation reason.
|
|
server.test_scripts = test_scripts.TestScriptStore(
|
|
opts.test_scripts,
|
|
validator=lambda raw: testscript.normalise_script(raw, _test_command_type))
|
|
sys.stderr.write('TESTSCRIPTS: %s (%d scripts)\n'
|
|
% (server.test_scripts.path, len(server.test_scripts.list())))
|
|
# Test suites group the scripts (v3.22.0): a script belongs to exactly one
|
|
# suite, so a pre-suite store's scripts are attached to an auto-created
|
|
# "Imported scripts" suite once (idempotent - no orphans, nothing to do).
|
|
server.test_suites = test_suites.TestSuiteStore(opts.test_suites)
|
|
sys.stderr.write('TESTSUITES: %s (%d suites)\n'
|
|
% (server.test_suites.path, len(server.test_suites.list())))
|
|
migrate_scripts_to_suites(server.test_scripts, server.test_suites)
|
|
server.sim_menu = sim_menu
|
|
server.event_list = event_list
|
|
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
|
|
# The startup init inlines the post-equip work (it never runs
|
|
# _apply_equipped_card): the batch worker's ready marker must be set
|
|
# here too (v3.28.0 audit).
|
|
server.card_ready_session = server.card_session
|
|
server.iccid = iccid
|
|
# EF.ARR cache (v3.24.0): read by the startup init above (a card present
|
|
# at server start); a later equip refreshes it in _apply_equipped_card.
|
|
server.arr = arr_files
|
|
# EF.PBR cache (TS 31.102 4.4.2.1): read by the startup init above; a
|
|
# later equip refreshes it in _apply_equipped_card.
|
|
server.pbr = pbr_files
|
|
# Network state monitor: install the state read during the startup init
|
|
# (right after the ICCID, before the TERMINAL PROFILE). No readable
|
|
# ICCID means the card is considered unusable - give up.
|
|
try:
|
|
_netstate_install(server, netstate_files if iccid else None)
|
|
except Exception as e:
|
|
server.net_state = None
|
|
sys.stderr.write('INIT: network state failed: %s\n' % e)
|
|
server.equipping = False
|
|
# Set server reference for polling timer and mark the card session state
|
|
import pysim_simple_server.server
|
|
pysim_simple_server.server._server_ref = server
|
|
pysim_simple_server.server._CARD_CONNECTED = card is not None
|
|
if opts.poll_interval is not None:
|
|
pysim_simple_server.server._set_poll_interval(opts.poll_interval)
|
|
# Background STATUS polling is on by default: a CAT terminal polls during
|
|
# idle (TS 102 221 14.6.2). --poll-interval 0 disables it, and a card
|
|
# that wants no polling suspends it with POLLING OFF (TS 102 223 6.4.14)
|
|
# until a new POLL INTERVAL.
|
|
if opts.poll_interval == 0:
|
|
pysim_simple_server.server._poll_disable()
|
|
else:
|
|
pysim_simple_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(_console_rule())
|
|
print(" pysim-simple-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 SIMple UI (served by this server)."
|
|
% (opts.http_host, opts.http_port))
|
|
print(_console_rule())
|
|
try:
|
|
server.serve_forever()
|
|
except KeyboardInterrupt:
|
|
print("\nShutting down...")
|
|
server.shutdown()
|
|
|
|
|
|
if __name__ == '__main__':
|
|
main() |