local server added
This commit was merged in pull request #179.
This commit is contained in:
@@ -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){{
|
||||
|
||||
Reference in New Issue
Block a user