local server added
Read the Docs Deploy Trigger / trigger-rtd-webhook (push) Successful in 1s
CI for csaxs_bec / test (push) Successful in 2m0s

This commit was merged in pull request #179.
This commit is contained in:
x12sa
2026-03-30 13:07:46 +02:00
committed by holler
parent 80de9724d4
commit cceedc947a
2 changed files with 102 additions and 6 deletions
@@ -1319,6 +1319,7 @@ class Flomni(
output_dir="~/data/raw/webpage/",
#upload_url="http://s1090968537.online.de/upload.php", # optional
upload_url="https://v1p0zyg2w9n2k9c1.myfritz.net/upload.php",
local_port=8080
)
self._webpage_gen.start()
@@ -5,10 +5,14 @@ Background thread that reads tomo progress from the BEC global variable store
and writes status.json (every cycle) + status.html (once at startup) to a
staging directory. An optional HttpUploader sends those files to a web host
after every cycle, running in a separate daemon thread so uploads never block
the generator cycle.
the generator cycle. A built-in LocalHttpServer always serves the output
directory locally (default port 8080) so the page can be accessed on the
lab network without any extra setup.
Architecture
------------
LocalHttpServer -- built-in HTTP server; serves output_dir on port 8080.
Always started at _launch(); URL printed to console.
HttpUploader -- non-blocking HTTP uploader (fire-and-forget thread).
Tracks file mtimes; only uploads changed files.
Sends a cleanup request to the server when the
@@ -31,19 +35,24 @@ Integration (inside Flomni.__init__, after self._progress_proxy.reset()):
bec_client=client,
output_dir="~/data/raw/webpage/",
upload_url="http://omny.online/upload.php", # optional
local_port=8080, # optional, default 8080
)
self._webpage_gen.start()
# On start(), the console prints:
# ➜ Status page: http://hostname:8080/status.html
Interactive helpers (optional, in the iPython session):
-------------------------------------------------------
flomni._webpage_gen.status() # print current status
flomni._webpage_gen.status() # print current status + local URL
flomni._webpage_gen.verbosity = 2 # VERBOSE: one-line summary per cycle
flomni._webpage_gen.verbosity = 3 # DEBUG: full JSON per cycle
flomni._webpage_gen.stop() # release lock
flomni._webpage_gen.stop() # release lock, stop local server
flomni._webpage_gen.start() # restart after stop()
"""
import datetime
import functools
import http.server
import json
import os
import shutil
@@ -364,6 +373,73 @@ class HttpUploader:
self._warn("cleanup", f"HttpUploader cleanup failed: {exc}")
# ---------------------------------------------------------------------------
# Local HTTP server (serves output_dir over http://hostname:port/)
# ---------------------------------------------------------------------------
class LocalHttpServer:
"""
Serves the generator's output directory over plain HTTP in a daemon thread.
Uses Python's built-in http.server — no extra dependencies.
Request logging is suppressed so the BEC console stays clean.
The server survives stop()/start() cycles: _launch() creates a fresh
instance each time start() is called.
Usage:
srv = LocalHttpServer(output_dir, port=8080)
srv.start()
print(srv.url) # http://hostname:8080/status.html
srv.stop()
"""
def __init__(self, directory: Path, port: int = 8080):
self._directory = Path(directory)
self._port = port
self._server = None
self._thread = None
# ── silence the per-request log lines in the iPython console ──────────
class _QuietHandler(http.server.SimpleHTTPRequestHandler):
def log_message(self, *args):
pass
def start(self) -> None:
Handler = functools.partial(
self._QuietHandler,
directory=str(self._directory),
)
try:
self._server = http.server.HTTPServer(("", self._port), Handler)
except OSError as exc:
raise RuntimeError(
f"LocalHttpServer: cannot bind port {self._port}: {exc}"
) from exc
self._thread = threading.Thread(
target=self._server.serve_forever,
name="LocalHttpServer",
daemon=True,
)
self._thread.start()
def stop(self) -> None:
if self._server is not None:
self._server.shutdown() # blocks until serve_forever() returns
self._server = None
def is_alive(self) -> bool:
return self._thread is not None and self._thread.is_alive()
@property
def port(self) -> int:
return self._port
@property
def url(self) -> str:
"""Best-guess URL for printing. Uses the machine's hostname."""
return f"http://{socket.gethostname()}:{self._port}/status.html"
# ---------------------------------------------------------------------------
# Base generator
# ---------------------------------------------------------------------------
@@ -382,12 +458,15 @@ class WebpageGeneratorBase:
cycle_interval: float = _CYCLE_INTERVAL_S,
verbosity: int = VERBOSITY_NORMAL,
upload_url: str = None,
local_port: int = 8080,
):
self._bec = bec_client
self._output_dir = Path(output_dir).expanduser().resolve()
self._cycle_interval = cycle_interval
self._verbosity = verbosity
self._uploader = HttpUploader(upload_url) if upload_url else None
self._local_port = local_port
self._local_server = None # created fresh each _launch()
self._thread = None
self._stop_event = threading.Event()
@@ -437,6 +516,17 @@ class WebpageGeneratorBase:
self._copy_logo()
(self._output_dir / "status.html").write_text(_render_html(_PHONE_NUMBERS))
# Start local HTTP server (always on; a fresh instance per _launch).
if self._local_server is not None and self._local_server.is_alive():
self._local_server.stop()
self._local_server = LocalHttpServer(self._output_dir, self._local_port)
try:
self._local_server.start()
local_url_msg = f" local={self._local_server.url}"
except RuntimeError as exc:
local_url_msg = f" local=ERROR({exc})"
self._log(VERBOSITY_NORMAL, str(exc), level="warning")
# Upload static files (html + logo) once at startup
if self._uploader is not None:
self._uploader.upload_dir_async(self._output_dir)
@@ -449,13 +539,16 @@ class WebpageGeneratorBase:
self._log(VERBOSITY_NORMAL,
f"WebpageGenerator started owner={self._owner_id} "
f"output={self._output_dir} interval={self._cycle_interval}s"
+ (f" upload={self._uploader._url}" if self._uploader else " upload=disabled"))
+ (f" upload={self._uploader._url}" if self._uploader else " upload=disabled")
+ f"\n ➜ Status page:{local_url_msg}")
def stop(self) -> None:
"""Stop the generator thread and release the singleton lock."""
"""Stop the generator thread, local HTTP server, and release the singleton lock."""
self._stop_event.set()
if self._thread is not None:
self._thread.join(timeout=self._cycle_interval + 5)
if self._local_server is not None:
self._local_server.stop()
self._release_lock()
self._log(VERBOSITY_NORMAL, "WebpageGenerator stopped.")
@@ -472,12 +565,14 @@ class WebpageGeneratorBase:
"""Print a human-readable status summary to the console."""
lock = self._read_lock()
running = self._thread is not None and self._thread.is_alive()
local = self._local_server.url if (self._local_server and self._local_server.is_alive()) else "stopped"
print(
f"WebpageGenerator\n"
f" This session running : {running}\n"
f" Lock owner : {lock.get('owner_id', 'none')}\n"
f" Lock heartbeat : {lock.get('heartbeat', 'never')}\n"
f" Output dir : {self._output_dir}\n"
f" Local URL : {local}\n"
f" Cycle interval : {self._cycle_interval}s\n"
f" Upload URL : {self._uploader._url if self._uploader else 'disabled'}\n"
f" Verbosity : {self._verbosity}\n"
@@ -1982,7 +2077,7 @@ function render(d){{
async function poll(){{
try{{
const r=await fetch(STATUS_JSON+'?t='+Date.now());
const r=await fetch(STATUS_JSON, {{cache:'no-store'}});
if(!r.ok) throw new Error('HTTP '+r.status);
render(await r.json());
}}catch(e){{