From fa7eb1436911b2957d581333b597170a2a86e836 Mon Sep 17 00:00:00 2001 From: shuki Date: Fri, 6 Mar 2026 06:12:29 +0200 Subject: [PATCH] Fix textual-serve web: robust IP detection for WebSocket URL The landing page wasn't interactive because public_url resolved to localhost, making WebSocket connections fail from remote browsers. - Added multiple IP detection methods (socket, hostname -I, gethostbyname) - Support --port= and --host= flag formats - Print actual serving URL on startup - Switch web start back to textual-serve (TUI in browser) Co-Authored-By: Claude Opus 4.6 --- bin/gniza | 4 +-- etc/gniza-web.service | 4 +-- tui/__main__.py | 69 ++++++++++++++++++++++++++++++++----------- 3 files changed, 55 insertions(+), 22 deletions(-) diff --git a/bin/gniza b/bin/gniza index 77634cf..ca8f5f9 100755 --- a/bin/gniza +++ b/bin/gniza @@ -402,10 +402,10 @@ if [[ "$SUBCOMMAND" == "web" ]]; then _web_port="" _web_host="" _web_port=$(_parse_flag "--port" "${SUBCMD_ARGS[@]+"${SUBCMD_ARGS[@]}"}") || true _web_host=$(_parse_flag "--host" "${SUBCMD_ARGS[@]+"${SUBCMD_ARGS[@]}"}") || true - _web_args=() + _web_args=(--web) [[ -n "$_web_port" ]] && _web_args+=(--port="$_web_port") [[ -n "$_web_host" ]] && _web_args+=(--host="$_web_host") - PYTHONPATH="$GNIZA_DIR:${PYTHONPATH:-}" exec python3 -m web "${_web_args[@]}" + PYTHONPATH="$GNIZA_DIR:${PYTHONPATH:-}" exec python3 -m tui "${_web_args[@]}" ;; install-service) _service_src="$GNIZA_DIR/etc/gniza-web.service" diff --git a/etc/gniza-web.service b/etc/gniza-web.service index ef2b0bf..5b78bf4 100644 --- a/etc/gniza-web.service +++ b/etc/gniza-web.service @@ -4,11 +4,9 @@ After=network.target [Service] Type=simple -ExecStart=/usr/bin/python3 -m web +ExecStart=/usr/bin/python3 -m tui --web --host 0.0.0.0 --port 8080 WorkingDirectory=/usr/local/gniza Environment=GNIZA_DIR=/usr/local/gniza -Environment=GNIZA_CONFIG_DIR=/etc/gniza -Environment=LOG_DIR=/var/log/gniza Environment=PYTHONPATH=/usr/local/gniza Restart=on-failure RestartSec=5 diff --git a/tui/__main__.py b/tui/__main__.py index 4ff2d1a..ad7950d 100644 --- a/tui/__main__.py +++ b/tui/__main__.py @@ -1,5 +1,6 @@ import os import socket +import subprocess import sys from pathlib import Path @@ -11,40 +12,74 @@ _ROOT = os.environ.get("GNIZA_DIR", str(Path(__file__).resolve().parent.parent)) def _get_local_ip() -> str: """Get the machine's LAN IP for public_url.""" + # Method 1: UDP socket trick try: s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) + s.settimeout(2) s.connect(("8.8.8.8", 80)) ip = s.getsockname()[0] s.close() - return ip + if ip and ip != "0.0.0.0": + return ip except Exception: - return "localhost" + pass + # Method 2: hostname -I + try: + result = subprocess.run( + ["hostname", "-I"], capture_output=True, text=True, timeout=5 + ) + if result.returncode == 0: + ip = result.stdout.strip().split()[0] + if ip: + return ip + except Exception: + pass + # Method 3: hostname resolution + try: + return socket.gethostbyname(socket.gethostname()) + except Exception: + return "127.0.0.1" + + +def _parse_web_args(): + """Parse --port and --host from sys.argv.""" + port = 8080 + host = "0.0.0.0" + for i, arg in enumerate(sys.argv): + if arg.startswith("--port="): + port = int(arg.split("=", 1)[1]) + elif arg == "--port" and i + 1 < len(sys.argv): + port = int(sys.argv[i + 1]) + elif arg.startswith("--host="): + host = arg.split("=", 1)[1] + elif arg == "--host" and i + 1 < len(sys.argv): + host = sys.argv[i + 1] + return host, port def main(): if "--web" in sys.argv: from textual_serve.server import Server - port = 8080 - host = "0.0.0.0" - public_host = None - for i, arg in enumerate(sys.argv): - if arg == "--port" and i + 1 < len(sys.argv): - port = int(sys.argv[i + 1]) - elif arg == "--host" and i + 1 < len(sys.argv): - host = sys.argv[i + 1] - public_host = host + + host, port = _parse_web_args() + os.environ["PYTHONPATH"] = f"{_ROOT}:{os.environ.get('PYTHONPATH', '')}" os.environ["GNIZA_DIR"] = _ROOT - # textual-serve uses public_url to build WebSocket URLs. - # If binding to 0.0.0.0, detect the real IP for the browser. - if public_host is None: - public_host = _get_local_ip() if host == "0.0.0.0" else host + + # Determine public URL for WebSocket connections + if host == "0.0.0.0": + public_host = _get_local_ip() + else: + public_host = host + public_url = f"http://{public_host}:{port}" + print(f"GNIZA web: serving TUI at {public_url}") + server = Server( - "python3 -m tui", + f"python3 -m tui", host=host, port=port, - title="gniza", + title="GNIZA Backup", public_url=public_url, ) server.serve()