commit 97c6c0e274f04ac1d0d58dfdae5870b60aa50c86
Author: Jens Kristoffersson <jens.kristoffersson.se@gmail.com>
AuthorDate: Tue Sep 8 12:57:32 2026 +0200
Commit: Jens Kristoffersson <jens.kristoffersson.se@gmail.com>
CommitDate: Tue Sep 8 12:57:32 2026 +0200
run_webui: startkontroll - vagra om porten redan anvands
python run_webui.py vagrar nu starta om 127.0.0.1:8765 redan anvands (visar
PID). --force dodar den korande instansen och tar over, --port N byter port.
---
README.md | 1 +
TODO.md | 3 ++
run_webui.py | 99 +++++++++++++++++++++++++++++++++++++++++++++++--
tests/test_run_webui.py | 24 ++++++++++++
4 files changed, 123 insertions(+), 4 deletions(-)
diff --git a/README.md b/README.md
index c4c7668..ef62e8a 100644
--- a/README.md
+++ b/README.md
@@ -43,6 +43,7 @@ powershell -ExecutionPolicy Bypass -File .\install_win.ps1
```powershell
.venv\Scripts\Activate.ps1
python run_webui.py # webui:t på http://127.0.0.1:8765
+python run_webui.py --force # om en gammal instans redan kör: ta över porten
python main.py # fristående 1177-exempel
```
diff --git a/TODO.md b/TODO.md
index 5d15cb9..9a1d9bb 100644
--- a/TODO.md
+++ b/TODO.md
@@ -43,6 +43,9 @@
`get_block`); `rpa/desktop_inspect.py` refaktorerad till `list_windows()`/
`inspect_window()` (+ `/api/desktop/windows|inspect`); dra steg till canvasen
eller in i loop/if-kropp (`stepops.appendToContainer`).
+- **~~Startkontroll i run_webui.py~~ – klar.** Vägrar starta om porten redan
+ används (visar PID), `--force` dödar den gamla instansen och tar över,
+ `--port N`.
- **Äkta grafmotor / fri kabeldragning i UI:t** – sömmen finns i
`webui/frontend/src/lib/graph.ts` (se kommentaren där).
- **Fler desktop-steg vid behov** – t.ex. `desktop_drag`, `desktop_menu_select`,
diff --git a/run_webui.py b/run_webui.py
index d08720c..f88120f 100644
--- a/run_webui.py
+++ b/run_webui.py
@@ -1,6 +1,97 @@
-import uvicorn
+"""Startar flödesbyggarens webui på http://127.0.0.1:8765.
+
+ python run_webui.py # startar; vägrar om porten redan används
+ python run_webui.py --force # dödar en redan körande instans och tar över
+ python run_webui.py --port 9000
+
+Endast lokal åtkomst (127.0.0.1) - webui:t styr en riktig webbläsare mot din
+inloggade session och ska inte exponeras på nätverket.
+"""
+
+import argparse
+import socket
+import subprocess
+import sys
+import time
+
+HOST = "127.0.0.1"
+
+
+def _port_in_use(port: int) -> bool:
+ with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
+ s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 0)
+ try:
+ s.bind((HOST, port))
+ return False
+ except OSError:
+ return True
+
+
+def _pids_on_port(port: int) -> list[int]:
+ """PID:erna som lyssnar på porten. Bäst effort, plattformsspecifikt."""
+ pids: set[int] = set()
+ try:
+ if sys.platform == "win32":
+ out = subprocess.run(
+ ["netstat", "-ano", "-p", "tcp"], capture_output=True, text=True, timeout=5
+ ).stdout
+ for line in out.splitlines():
+ parts = line.split()
+ if len(parts) >= 5 and parts[0].upper() == "TCP" and parts[1].endswith(f":{port}") \
+ and parts[3].upper() == "LISTENING":
+ pids.add(int(parts[4]))
+ else:
+ out = subprocess.run(
+ ["lsof", "-ti", f"tcp:{port}", "-sTCP:LISTEN"], capture_output=True, text=True, timeout=5
+ ).stdout
+ pids.update(int(x) for x in out.split())
+ except Exception: # noqa: BLE001
+ pass
+ return sorted(pids)
+
+
+def _kill(pids: list[int]) -> None:
+ for pid in pids:
+ try:
+ if sys.platform == "win32":
+ subprocess.run(["taskkill", "/F", "/PID", str(pid)], capture_output=True, timeout=5)
+ else:
+ subprocess.run(["kill", "-9", str(pid)], capture_output=True, timeout=5)
+ print(f"Stängde process {pid}.")
+ except Exception as exc: # noqa: BLE001
+ print(f"Kunde inte stänga {pid}: {exc}", file=sys.stderr)
+
+
+def main() -> None:
+ parser = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter)
+ parser.add_argument("--port", type=int, default=8765)
+ parser.add_argument("--force", action="store_true",
+ help="Döda en redan körande instans på porten och ta över")
+ args = parser.parse_args()
+
+ if _port_in_use(args.port):
+ pids = _pids_on_port(args.port)
+ who = f" (PID {', '.join(map(str, pids))})" if pids else ""
+ if not args.force:
+ print(
+ f"Port {args.port} används redan{who}.\n"
+ f" - webui:t kör troligen redan: öppna http://{HOST}:{args.port}\n"
+ f" - annars: kör 'python run_webui.py --force' för att ta över, "
+ f"eller '--port <annan port>'.",
+ file=sys.stderr,
+ )
+ sys.exit(1)
+ if pids:
+ _kill(pids)
+ time.sleep(1)
+ if _port_in_use(args.port):
+ print(f"Port {args.port} är fortfarande upptagen. Avbryter.", file=sys.stderr)
+ sys.exit(1)
+
+ import uvicorn
+
+ uvicorn.run("webui.app:app", host=HOST, port=args.port, reload=False)
+
if __name__ == "__main__":
- # Endast lokal åtkomst (127.0.0.1) - detta styr en riktig webbläsare mot
- # din inloggade 1177-session och ska inte exponeras på nätverket.
- uvicorn.run("webui.app:app", host="127.0.0.1", port=8765, reload=False)
+ main()
diff --git a/tests/test_run_webui.py b/tests/test_run_webui.py
new file mode 100644
index 0000000..ca1fddd
--- /dev/null
+++ b/tests/test_run_webui.py
@@ -0,0 +1,24 @@
+"""Startkontrollen i run_webui.py (vägrar om porten redan används)."""
+
+import socket
+
+import run_webui
+
+
+def test_port_in_use_detection():
+ # en ledig hög port ska rapporteras fri
+ with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as probe:
+ probe.bind(("127.0.0.1", 0))
+ free_port = probe.getsockname()[1]
+ assert run_webui._port_in_use(free_port) is False
+
+ # medan vi håller en lyssnande socket ska porten rapporteras upptagen
+ srv = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
+ srv.bind(("127.0.0.1", 0))
+ srv.listen(1)
+ port = srv.getsockname()[1]
+ try:
+ assert run_webui._port_in_use(port) is True
+ assert isinstance(run_webui._pids_on_port(port), list)
+ finally:
+ srv.close()