diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md index 920b8d9..a81645e 100644 --- a/.github/copilot-instructions.md +++ b/.github/copilot-instructions.md @@ -50,13 +50,14 @@ except ImportError: ```bash # Development setup python3 -m venv venv && source venv/bin/activate -pip install -e ".[dashboard,dev]" # editable install + console scripts +pip install -r requirements.txt +pip install -r requirements-dashboard.txt # Run server only -hblink4 [config/config.json] # console script from pip install -e . +python run_hblink.py [config/config.json] # or: python hblink4/hblink.py ... # Run dashboard only (bind/port from the 'web' section of dashboard/config.json) -hblink4-dashboard [bind] [port] +python run_dashboard.py [bind] [port] # or: python dashboard/server.py ... # Run both (production-like) ./run_all.sh @@ -105,7 +106,7 @@ target_repeater = user_cache.lookup_user(target_id) - **Core logic**: `hblink4/` package with protocol handling - **Configuration**: `config/config.json` (server), `dashboard/config.json` (web) -- **Entry points**: console scripts `hblink4` -> `hblink4.hblink:main` and `hblink4-dashboard` -> `dashboard.server:run`, declared in `pyproject.toml`; `run_all.sh` runs both for development. Never run the module files by path -- it loads the package twice. +- **Entry points**: `hblink4/hblink.py` and `dashboard/server.py` are directly runnable (what systemd uses). `run_hblink.py` / `run_dashboard.py` in the root are logic-free convenience launchers; `run_all.sh` runs both. No install step required. `pip install -e .` additionally provides `hblink4` / `hblink4-dashboard` commands. - **Deployment**: Systemd services expect user ownership, virtual environment - **Data**: `logs/`, `dashboard/data/` for persistence - **Tests**: `tests/` with pytest, focus on access control and stream tracking diff --git a/SYSTEMD.md b/SYSTEMD.md index 5b33f5c..4cef3a8 100644 --- a/SYSTEMD.md +++ b/SYSTEMD.md @@ -9,30 +9,24 @@ This directory contains systemd service files for running HBlink4 and its dashbo ## Upgrading an existing install -**The launcher scripts `run.py` and `run_dashboard.py` have been removed, and -HBlink4 is now installed as a package with console-script entry points.** Older -unit files point at files that no longer exist and **will fail on the next -restart** — install the package and update the units before restarting anything. +**The units now invoke the modules directly** instead of going through a +launcher script, so there is one less moving part between systemd and the +program. `run_hblink.py` and `run_dashboard.py` still exist in the project root +as convenience launchers for interactive use. There is **no install step** — +everything runs straight from the checkout, so a `git pull` needs nothing beyond +a restart. What changed in the units: | | Old | New | |---|---|---| -| `hblink4` `ExecStart` | `venv/bin/python .../run.py` | `venv/bin/hblink4 /home/cort/hblink4/config/config.json` | -| `hblink4-dash` `ExecStart` | `venv/bin/python .../run_dashboard.py` | `venv/bin/hblink4-dashboard` (bind/port from `dashboard/config.json`) | +| `hblink4` `ExecStart` | `venv/bin/python .../run.py` | `venv/bin/python .../hblink4/hblink.py .../config/config.json` | +| `hblink4-dash` `ExecStart` | `venv/bin/python .../run_dashboard.py` | `venv/bin/python .../dashboard/server.py` (bind/port now from `dashboard/config.json`) | | `hblink4-dash` `WorkingDirectory` | `.../hblink4/dashboard` | `.../hblink4` | | both | `PrivateTmp=true` | *removed* — see [Security Features](#security-features) | -The entry points exist because running the module files by path puts the -package's interior on `sys.path` and loads every module twice under two sets of -names — the dual import trap. Install first, then swap the units: - ```bash cd /home/cort/hblink4 -source venv/bin/activate -pip install -e ".[dashboard]" -ls -l venv/bin/hblink4 venv/bin/hblink4-dashboard # both should exist - sudo cp hblink4.service hblink4-dash.service /etc/systemd/system/ sudo systemctl daemon-reload sudo systemctl restart hblink4 hblink4-dash diff --git a/dashboard/README.md b/dashboard/README.md index cf34f2d..7ec68aa 100644 --- a/dashboard/README.md +++ b/dashboard/README.md @@ -81,7 +81,7 @@ The button appears as the first item in the header status area with a light blue ## Usage -The dashboard is started automatically with `./run_all.sh`, or separately with the `hblink4-dashboard` command (installed by `pip install -e ".[dashboard]"`). It binds the address and port from the `web` section of `dashboard/config.json` (default `0.0.0.0:8080`); passing `[bind] [port]` on the command line overrides that for one-off runs. Access at http://localhost:8080 (or your server IP for remote access). +The dashboard is started automatically with `./run_all.sh`, or separately with `python3 run_dashboard.py` (or `python3 dashboard/server.py`) from the repository root. It binds the address and port from the `web` section of `dashboard/config.json` (default `0.0.0.0:8080`); passing `[bind] [port]` on the command line overrides that for one-off runs. Access at http://localhost:8080 (or your server IP for remote access). ## Dashboard Components diff --git a/dashboard/server.py b/dashboard/server.py index 77e47c7..dacb7e0 100644 --- a/dashboard/server.py +++ b/dashboard/server.py @@ -21,13 +21,16 @@ from fastapi.staticfiles import StaticFiles from pathlib import Path import logging -try: - from .user_db import UserDatabase, compute_next_refresh_seconds, _age_str -except ImportError: - # Running the dashboard directly without package install - import sys as _sys - _sys.path.insert(0, str(Path(__file__).parent)) - from user_db import UserDatabase, compute_next_refresh_seconds, _age_str +# Allow this file to be run directly by path (python dashboard/server.py) as +# well as imported. Establishing the package context means user_db is imported +# once, as dashboard.user_db, rather than a second bare copy under sys.path +# surgery. +if __package__ in (None, ""): + sys.path.insert(0, str(Path(__file__).parent.parent)) + __package__ = "dashboard" + import dashboard # noqa: F401 (parent package must be in sys.modules) + +from .user_db import UserDatabase, compute_next_refresh_seconds, _age_str # Configure logging logging.basicConfig(level=logging.INFO) diff --git a/docs/configuration.md b/docs/configuration.md index 41eebd9..f7da0e8 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -237,7 +237,7 @@ The `web` section is the **browser-facing web UI**: } ``` -`hblink4-dashboard [bind] [port]` overrides these for one-off runs; with no arguments the config values are used. +`python3 run_dashboard.py [bind] [port]` overrides these for one-off runs; with no arguments the config values are used. The `event_receiver` section is the **listening socket for the link from HBlink4**: diff --git a/hblink4-dash.service b/hblink4-dash.service index 4e97327..6da3c34 100644 --- a/hblink4-dash.service +++ b/hblink4-dash.service @@ -8,7 +8,7 @@ Type=simple User=cort Group=cort WorkingDirectory=/home/cort/hblink4 -ExecStart=/home/cort/hblink4/venv/bin/hblink4-dashboard +ExecStart=/home/cort/hblink4/venv/bin/python /home/cort/hblink4/dashboard/server.py Restart=always RestartSec=10 diff --git a/hblink4.service b/hblink4.service index f740723..0ce7131 100644 --- a/hblink4.service +++ b/hblink4.service @@ -7,7 +7,7 @@ Type=simple User=cort Group=cort WorkingDirectory=/home/cort/hblink4 -ExecStart=/home/cort/hblink4/venv/bin/hblink4 /home/cort/hblink4/config/config.json +ExecStart=/home/cort/hblink4/venv/bin/python /home/cort/hblink4/hblink4/hblink.py /home/cort/hblink4/config/config.json Restart=always RestartSec=10 diff --git a/hblink4/hblink.py b/hblink4/hblink.py index 8324660..ca5bad9 100644 --- a/hblink4/hblink.py +++ b/hblink4/hblink.py @@ -32,10 +32,17 @@ LOGGER = logging.getLogger(__name__) import os import sys -# Package-relative imports only. There is deliberately no "except ImportError" -# fallback to bare module names: that fallback let hblink.py be run by file path, -# which loads this package's modules twice under two sets of names (see the note -# in __init__.py). Run the installed console script, or python -m hblink4.hblink. +# Allow this file to be run directly by path (python hblink4/hblink.py) as well +# as imported. Establishing the package context up front means the relative +# imports below resolve to hblink4.* exactly once -- the older approach of +# falling back to bare "from constants import ..." loaded every module a second +# time under a second name (see the note in __init__.py). Cheap: __init__.py is +# metadata only, so importing the parent package pulls in nothing else. +if __package__ in (None, ""): + sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) + __package__ = "hblink4" + import hblink4 # noqa: F401 (parent package must be in sys.modules) + from .constants import ( RPTA, RPTL, RPTK, RPTC, RPTCL, MSTCL, DMRD, MSTNAK, MSTPONG, RPTPING, RPTACK, RPTP, RPTO, DMRA diff --git a/pyproject.toml b/pyproject.toml index 7d4cb99..a7e5b63 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -9,24 +9,13 @@ readme = "readme.md" requires-python = ">=3.9" license = "GPL-3.0-or-later" authors = [{ name = "Cort Buffington, N0MJS" }] -dynamic = ["version"] - -dependencies = [ - "typing-extensions>=4.7.1", - "dmr_utils3>=0.1.29", -] - -[project.optional-dependencies] -# The dashboard is a separate process; install with: pip install -e ".[dashboard]" -dashboard = [ - "fastapi>=0.104.0", - "uvicorn[standard]>=0.24.0", - "websockets>=12.0", -] -dev = [ - "pytest>=7.4.0", - "pytest-cov>=4.1.0", -] +# Installing is OPTIONAL. HBlink4 runs straight from a git checkout: +# pip install -r requirements.txt -r requirements-dashboard.txt +# python hblink.py config/config.json +# This file exists for people who prefer `pip install -e .`, which additionally +# provides `hblink4` / `hblink4-dashboard` commands. Dependencies are read from +# the requirements files so there is only one place to declare them. +dynamic = ["version", "dependencies", "optional-dependencies"] [project.urls] Homepage = "https://github.com/n0mjs710/HBlink4" @@ -39,6 +28,8 @@ hblink4-dashboard = "dashboard.server:run" [tool.setuptools.dynamic] version = { attr = "hblink4.__version__" } +dependencies = { file = ["requirements.txt"] } +optional-dependencies.dashboard = { file = ["requirements-dashboard.txt"] } [tool.setuptools] packages = ["hblink4", "dashboard"] diff --git a/readme.md b/readme.md index 30de880..e0a0726 100644 --- a/readme.md +++ b/readme.md @@ -53,13 +53,14 @@ python3 -m venv venv source venv/bin/activate # On Windows: venv\Scripts\activate ``` -3. Install HBlink4 and its dependencies: +3. Install requirements: ```bash -pip install -e ".[dashboard]" # add ,dev for the test suite +pip install -r requirements.txt +pip install -r requirements-dashboard.txt # if you want the dashboard ``` -This installs the package in editable mode and creates two commands in the -virtual environment: `hblink4` and `hblink4-dashboard`. +HBlink4 runs straight from the checkout — there is no install step, and +nothing to redo after a `git pull`. ## Configuration @@ -80,14 +81,21 @@ For production deployments with automatic startup, see [SYSTEMD.md](SYSTEMD.md). # Start all services together ./run_all.sh -# Or start services separately (with the venv activated): -hblink4 config/config.json # HBlink4 server -hblink4-dashboard # Web dashboard (in another terminal) +# Or start services separately (from the repository root): +python3 run_hblink.py config/config.json # HBlink4 server +python3 run_dashboard.py # Web dashboard (in another terminal) ``` -Both read their configuration from disk: the server's config path defaults to `config/config.json`, and the dashboard takes its bind address and port from the `web` section of `dashboard/config.json`. Each can be overridden on the command line — `hblink4 ` and `hblink4-dashboard [bind] [port]` — for one-off runs. +`run_hblink.py` and `run_dashboard.py` are convenience launchers containing no +logic. The modules they point at run just as well on their own, which is what +the systemd units use — no venv activation needed: -Run the modules by file path and Python loads the package twice under two sets of names; use these commands, or `python3 -m hblink4.hblink`. +```bash +/path/to/hblink4/venv/bin/python /path/to/hblink4/hblink4/hblink.py /path/to/hblink4/config/config.json +/path/to/hblink4/venv/bin/python /path/to/hblink4/dashboard/server.py +``` + +Both read their configuration from disk: the server's config path defaults to `config/config.json`, and the dashboard takes its bind address and port from the `web` section of `dashboard/config.json`. Each accepts the same overrides on the command line for one-off runs. Access the dashboard at http://localhost:8080. See [Dashboard Documentation](dashboard/README.md) for features and configuration. diff --git a/requirements-dashboard.txt b/requirements-dashboard.txt new file mode 100644 index 0000000..49c87f7 --- /dev/null +++ b/requirements-dashboard.txt @@ -0,0 +1,11 @@ +# HBlink4 Dashboard Requirements +# Install with: pip install -r requirements-dashboard.txt + +# Web framework +fastapi>=0.104.0 + +# ASGI server with WebSocket support +uvicorn[standard]>=0.24.0 + +# WebSocket support (included in uvicorn[standard]) +websockets>=12.0 diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..10fa628 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,6 @@ +# asyncio is part of Python 3.7+ standard library (no external dependency needed) +# Removed: twisted>=22.10.0, pyOpenSSL>=23.2.0, service_identity>=23.1.0 +typing-extensions>=4.7.1 # For Python <3.9 compatibility +pytest>=7.4.0 # For running tests +pytest-cov>=4.1.0 # For test coverage reports +dmr_utils3>=0.1.29 # For DMR FEC decoding and LC extraction diff --git a/run_all.sh b/run_all.sh index fc945ec..ce81036 100755 --- a/run_all.sh +++ b/run_all.sh @@ -15,7 +15,7 @@ NC='\033[0m' # No Color # Check if virtual environment exists if [ ! -d "venv" ]; then - echo -e "${YELLOW}Virtual environment not found. Please run: python3 -m venv venv && source venv/bin/activate && pip install -e \".[dashboard]\"${NC}" + echo -e "${YELLOW}Virtual environment not found. Please run: python3 -m venv venv && source venv/bin/activate && pip install -r requirements.txt${NC}" exit 1 fi @@ -23,10 +23,10 @@ fi echo -e "${BLUE}Activating virtual environment...${NC}" source venv/bin/activate -# Check that the package (and its dashboard extra) is installed -if ! command -v hblink4-dashboard >/dev/null 2>&1; then - echo -e "${YELLOW}HBlink4 not installed in the venv. Installing...${NC}" - pip install -e ".[dashboard]" +# Check if dashboard requirements are installed +if ! python3 -c "import fastapi" 2>/dev/null; then + echo -e "${YELLOW}Dashboard dependencies not found. Installing...${NC}" + pip install -r requirements-dashboard.txt fi # Function to cleanup on exit @@ -44,7 +44,7 @@ echo # Start dashboard in background echo -e "${BLUE}Starting Dashboard (bind/port from dashboard/config.json)...${NC}" -hblink4-dashboard & +python3 run_dashboard.py & DASHBOARD_PID=$! # Give dashboard time to start @@ -52,7 +52,7 @@ sleep 2 # Start HBlink4 server echo -e "${BLUE}Starting HBlink4 server...${NC}" -hblink4 config/config.json & +python3 run_hblink.py config/config.json & HBLINK_PID=$! echo diff --git a/run_dashboard.py b/run_dashboard.py new file mode 100755 index 0000000..3c6c24e --- /dev/null +++ b/run_dashboard.py @@ -0,0 +1,20 @@ +#!/usr/bin/env python3 +""" +Convenience launcher for the HBlink4 dashboard. + + python run_dashboard.py [bind] [port] + +Purely a shortcut for interactive use -- it contains no logic beyond locating +dashboard/server.py, which is equally runnable on its own: + + python dashboard/server.py [bind] [port] + +The systemd unit uses that direct path rather than this script. With no +arguments the bind address and port come from the 'web' section of +dashboard/config.json. +""" + +from dashboard.server import run + +if __name__ == '__main__': + run() diff --git a/run_hblink.py b/run_hblink.py new file mode 100755 index 0000000..cd4c399 --- /dev/null +++ b/run_hblink.py @@ -0,0 +1,18 @@ +#!/usr/bin/env python3 +""" +Convenience launcher for the HBlink4 server. + + python run_hblink.py [config/config.json] + +Purely a shortcut for interactive use -- it contains no logic beyond locating +hblink4/hblink.py, which is equally runnable on its own: + + python hblink4/hblink.py [config/config.json] + +The systemd unit uses that direct path rather than this script. +""" + +from hblink4.hblink import main + +if __name__ == '__main__': + main()