f942056d21
__main__ built a plain HTTPServer (single-threaded): during a RAM install POST every other request waited in the accept queue, including the 2 s /api/status poll that feeds the operation modal - its bar and step headline never moved and the whole result appeared only at the end (live 2026-09-29). _build_http_server() now returns a ThreadingHTTPServer (daemon threads). Card access is unchanged: it is serialized by _CARD_LOCK, not by the server - every POST is blanket-wrapped in do_POST, every /api/ GET except /api/status and /api/test/status is wrapped in do_GET, and the background threads (STATUS poll, _bip_data_available, test runner) take the lock; the audit found no card-access holes. do_GET also lets non-/api (static PWA) paths bypass the lock, so page assets stay answerable during long operations. Regression test tests/test_http_concurrency.py: a fast request must complete while a slow one is in flight (fails on HTTPServer with exactly the reported symptom: 1.01 s vs 0.20 s); test_startup_cardless patches the new factory. sw cache simple-v312; tests: 519 Python / 674 Node.
66 lines
2.3 KiB
Python
66 lines
2.3 KiB
Python
"""The HTTP server must stay answerable while a card operation holds the lock.
|
|
|
|
The RAM modal's live progress comes from /api/status.ram_progress, and the
|
|
static PWA files are card-free: both are served outside _CARD_LOCK by design
|
|
(server.do_GET), which only works when the server dispatches requests
|
|
concurrently. A single-threaded HTTPServer starved every other request until
|
|
the install POST finished, so the progress bar never grew (live 2026-09-29).
|
|
"""
|
|
|
|
import threading
|
|
import time
|
|
import unittest
|
|
import urllib.request
|
|
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
|
|
|
|
from pysim_simple_server.__main__ import _build_http_server
|
|
|
|
|
|
class _StubHandler(BaseHTTPRequestHandler):
|
|
def do_GET(self):
|
|
if self.path == '/slow':
|
|
time.sleep(1.0)
|
|
body = b'ok'
|
|
self.send_response(200)
|
|
self.send_header('Content-Length', str(len(body)))
|
|
self.end_headers()
|
|
self.wfile.write(body)
|
|
|
|
def log_message(self, *args):
|
|
pass
|
|
|
|
|
|
class TestThreadedHttpServer(unittest.TestCase):
|
|
def setUp(self):
|
|
self.server = _build_http_server('127.0.0.1', 0, _StubHandler)
|
|
self.thread = threading.Thread(target=self.server.serve_forever, daemon=True)
|
|
self.thread.start()
|
|
self.port = self.server.server_address[1]
|
|
|
|
def tearDown(self):
|
|
self.server.shutdown()
|
|
self.server.server_close()
|
|
|
|
def test_built_server_is_threaded(self):
|
|
self.assertIsInstance(self.server, ThreadingHTTPServer)
|
|
# a stuck request must not block shutdown
|
|
self.assertTrue(self.server.daemon_threads)
|
|
|
|
def test_cached_request_is_served_while_a_slow_one_runs(self):
|
|
started = time.monotonic()
|
|
slow = threading.Thread(
|
|
target=lambda: urllib.request.urlopen(
|
|
'http://127.0.0.1:%d/slow' % self.port, timeout=5).read())
|
|
slow.start()
|
|
time.sleep(0.15) # let /slow enter its handler
|
|
urllib.request.urlopen('http://127.0.0.1:%d/fast' % self.port, timeout=5).read()
|
|
fast_elapsed = time.monotonic() - started
|
|
slow.join(timeout=5)
|
|
# a single-threaded server would answer only after /slow finished
|
|
self.assertLess(fast_elapsed, 0.6,
|
|
'fast request waited for the slow one: server not threaded')
|
|
|
|
|
|
if __name__ == '__main__':
|
|
unittest.main()
|