Add scripts/install_services.sh to install the systemd units

The shipped unit files carry the author's user and paths (User=cort,
/home/cort/hblink4), so copying them verbatim produces two services that
fail to start. The docs said so only in a Customization section below the
copy instructions, and told people to edit the repo copies -- which then
conflict on the next git pull.

The installer prompts for the service user and group (defaulting to the
invoking sudo user and their primary group), substitutes them and the
detected installation path into both units, installs to
/etc/systemd/system/, reloads systemd, and optionally enables and starts
the services. It backs up existing units, refuses unknown users/groups,
checks the venv exists, warns when the service user cannot write the
installation directory, and verifies the substitution landed before
leaving anything in place. DESTDIR previews the output without touching
the live system.

The manual instructions now say to copy first and edit the installed
copy, and note that daemon-reload is required after any unit change. Both
unit files carry a header saying the same.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
cortandClaude Opus 5 committed 2026-08-06 21:08:47 -05:00
1 parent 512249c3ed
commit 331fe8814c
6 files changed
+270 -15

No files matched your search

+75 -14
View File
@@ -12,7 +12,40 @@ step, so after a `git pull` you only need to restart the services. If you are
upgrading from a version whose units pointed at `run.py`, re-copy the unit files
below before restarting.
## Installation
## Quick install (recommended)
The shipped unit files contain the author's user and paths (`User=cort`,
`/home/cort/hblink4`), so they will not work as-is on your system. This script
asks for your service user and group, fills in your paths, installs both units
and reloads systemd:
```bash
sudo ./scripts/install_services.sh
```
It offers sensible defaults (the user who invoked `sudo`, and that user's
primary group), backs up any units you already have, checks that your venv
exists and that the service user can write to the installation directory, and
optionally enables and starts the services.
To see what it would produce without touching the live system:
```bash
sudo DESTDIR=/tmp/preview ./scripts/install_services.sh
```
The manual steps below do the same thing by hand.
## Manual installation
> **⚠️ The shipped unit files contain the author's user and paths** —
> `User=cort`, `Group=cort`, and `/home/cort/hblink4` throughout. **They will not
> work as-is on your system.** You must change the user, the group, and every
> path to match your installation.
>
> Edit the *installed* copies in `/etc/systemd/system/`, not the ones in the
> repository — editing the repo copies makes them show up as local changes and
> conflict on the next `git pull`.
### 1. Install the service files
@@ -23,15 +56,39 @@ sudo cp hblink4.service /etc/systemd/system/
sudo cp hblink4-dash.service /etc/systemd/system/
```
### 2. Reload systemd
### 2. Set your user and paths
Tell systemd to reload its configuration:
Four settings in each file need to match your system: `User=`, `Group=`,
`WorkingDirectory=`, and `ExecStart=`. Edit them with your editor of choice:
```bash
sudo nano /etc/systemd/system/hblink4.service
sudo nano /etc/systemd/system/hblink4-dash.service
```
Or, run this from your HBlink4 directory to substitute them automatically:
```bash
sudo sed -i "s|/home/cort/hblink4|$PWD|g; s|^User=cort$|User=$USER|; s|^Group=cort$|Group=$(id -gn)|" \
/etc/systemd/system/hblink4.service /etc/systemd/system/hblink4-dash.service
```
Check the result before continuing — no `cort` or `/home/cort` should remain:
```bash
grep -nE "^(User|Group|WorkingDirectory|ExecStart)=" /etc/systemd/system/hblink4*.service
```
### 3. Reload systemd
Tell systemd to reload its configuration. **This is required after any edit to a
unit file** — systemd will keep using the old version until you do:
```bash
sudo systemctl daemon-reload
```
### 3. Enable services (optional)
### 4. Enable services (optional)
To start the services automatically at boot:
@@ -40,7 +97,7 @@ sudo systemctl enable hblink4
sudo systemctl enable hblink4-dash
```
### 4. Start the services
### 5. Start the services
```bash
sudo systemctl start hblink4
@@ -92,22 +149,26 @@ sudo systemctl disable hblink4-dash
## Configuration
The service files are configured to:
- Run as user `cort` in group `cort`
- Use the Python virtual environment at `/home/cort/hblink4/venv`
As shipped, the service files:
- Run as user `cort` in group `cort` — **change this to your own user and group**
- Use the Python virtual environment at `/home/cort/hblink4/venv` — **change to your path**
- Automatically restart on failure (after 10 seconds)
- Log to systemd journal (view with `journalctl`)
- Start after network is available
- Dashboard starts after and depends on HBlink4
### Customization
### Changing a unit file later
If you need to modify the services (different user, paths, etc.), edit the files before copying them:
Always edit the copy in `/etc/systemd/system/`, then reload and restart:
1. Edit `hblink4.service` and/or `hblink4-dash.service`
2. Change `User=`, `Group=`, `WorkingDirectory=`, or `ExecStart=` as needed
3. Copy to `/etc/systemd/system/`
4. Run `sudo systemctl daemon-reload`
```bash
sudo nano /etc/systemd/system/hblink4.service
sudo systemctl daemon-reload # required, or systemd keeps the old version
sudo systemctl restart hblink4
```
Editing the copy in the repository has no effect on a running service, and
leaves you with local changes that conflict on the next `git pull`.
## Security Features
+4
View File
@@ -1,3 +1,7 @@
# NOTE: this file ships with the author's user and install paths. Change User=,
# Group=, WorkingDirectory= and ExecStart= below to match your system.
# Edit the installed copy in /etc/systemd/system/, not this one, and run
# "sudo systemctl daemon-reload" after any change. See SYSTEMD.md.
[Unit]
Description=HBlink4 Dashboard
After=network.target hblink4.service
+4
View File
@@ -1,3 +1,7 @@
# NOTE: this file ships with the author's user and install paths. Change User=,
# Group=, WorkingDirectory= and ExecStart= below to match your system.
# Edit the installed copy in /etc/systemd/system/, not this one, and run
# "sudo systemctl daemon-reload" after any change. See SYSTEMD.md.
[Unit]
Description=HBlink4 DMR Server
After=network.target
+6 -1
View File
@@ -74,7 +74,12 @@ See the [Configuration Guide](docs/configuration.md) for complete documentation
## Running
### Production (systemd services)
For production deployments with automatic startup, see [SYSTEMD.md](SYSTEMD.md).
```bash
sudo ./scripts/install_services.sh
```
Asks for your service user/group, fills in your paths, installs both units and
reloads systemd. See [SYSTEMD.md](SYSTEMD.md) for manual installation and
service management.
### Development
```bash
+24
View File
@@ -1,5 +1,29 @@
# HBlink4 Scripts
## install_services.sh
Installs the systemd unit files with your user, group and paths filled in — the
shipped units carry the author's (`User=cort`, `/home/cort/hblink4`) and will not
work unmodified.
```bash
sudo ./scripts/install_services.sh
```
Prompts for the service user and group (defaulting to whoever ran `sudo` and
that user's primary group), rewrites both units, installs them to
`/etc/systemd/system/`, runs `systemctl daemon-reload`, and optionally enables
and starts the services. Any existing units are backed up as
`<unit>.bak-<timestamp>` first.
Preview the result without touching the live system:
```bash
sudo DESTDIR=/tmp/preview ./scripts/install_services.sh
```
See [SYSTEMD.md](../SYSTEMD.md) for manual installation and service management.
## filter_user_csv.py
Filters the DMR user database to only include US and Canada entries, reducing file size and memory usage.
+157
View File
@@ -0,0 +1,157 @@
#!/bin/bash
#
# Install the HBlink4 systemd units, with your user, group and paths filled in.
#
# sudo ./scripts/install_services.sh
#
# Prompts for the service user and group (sensible defaults are offered),
# rewrites the shipped unit files accordingly, installs them to
# /etc/systemd/system/, and reloads systemd. Existing units are backed up first.
#
# Set DESTDIR to install somewhere else (useful for inspecting the result
# without touching the live system):
#
# sudo DESTDIR=/tmp/preview ./scripts/install_services.sh
#
set -euo pipefail
RED='\033[0;31m'; GREEN='\033[0;32m'; BLUE='\033[0;34m'; YELLOW='\033[1;33m'; NC='\033[0m'
die() { echo -e "${RED}✗ $*${NC}" >&2; exit 1; }
note() { echo -e "${BLUE}$*${NC}"; }
ok() { echo -e "${GREEN}✓ $*${NC}"; }
warn() { echo -e "${YELLOW}⚠ $*${NC}"; }
REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
UNIT_DIR="${DESTDIR:-/etc/systemd/system}"
UNITS=(hblink4.service hblink4-dash.service)
LIVE_INSTALL=0
[ -z "${DESTDIR:-}" ] && LIVE_INSTALL=1
if [ "$LIVE_INSTALL" = 1 ] && [ "$(id -u)" -ne 0 ]; then
die "This script must be run with sudo: sudo $0"
fi
# ---------------------------------------------------------------- sanity checks
for u in "${UNITS[@]}"; do
[ -f "$REPO_ROOT/$u" ] || die "Missing $REPO_ROOT/$u -- run this from the HBlink4 checkout."
done
[ -x "$REPO_ROOT/venv/bin/python" ] || die \
"No virtual environment at $REPO_ROOT/venv
Create it first:
python3 -m venv venv && source venv/bin/activate && pip install -r requirements.txt"
[ -f "$REPO_ROOT/config/config.json" ] || warn \
"No config/config.json yet -- copy config/config_sample.json and edit it before starting."
echo
note "HBlink4 service installer"
echo " Install directory : $REPO_ROOT"
echo " Unit directory : $UNIT_DIR"
echo
# ------------------------------------------------------------------- user/group
# Default to whoever invoked sudo, falling back to the directory's owner --
# HBlink4 must run as a user that can write logs/ and dashboard/data/.
DEFAULT_USER="${SUDO_USER:-$(stat -c '%U' "$REPO_ROOT")}"
DEFAULT_GROUP="$(id -gn "$DEFAULT_USER" 2>/dev/null || echo "$DEFAULT_USER")"
read -r -p "Service user [$DEFAULT_USER]: " SVC_USER
SVC_USER="${SVC_USER:-$DEFAULT_USER}"
read -r -p "Service group [$DEFAULT_GROUP]: " SVC_GROUP
SVC_GROUP="${SVC_GROUP:-$DEFAULT_GROUP}"
id "$SVC_USER" >/dev/null 2>&1 || die "No such user: $SVC_USER"
getent group "$SVC_GROUP" >/dev/null 2>&1 || die "No such group: $SVC_GROUP"
# The service user must be able to write persistence and logs.
if [ "$(id -u)" -eq 0 ] && ! runuser -u "$SVC_USER" -- test -w "$REPO_ROOT" 2>/dev/null; then
warn "$SVC_USER cannot write to $REPO_ROOT"
warn "The dashboard needs write access for dashboard/data/ and logs/."
fi
echo
note "Will install as:"
echo " User=$SVC_USER"
echo " Group=$SVC_GROUP"
echo " WorkingDirectory=$REPO_ROOT"
echo " ExecStart=$REPO_ROOT/venv/bin/python $REPO_ROOT/hblink4/hblink.py $REPO_ROOT/config/config.json"
echo " ExecStart=$REPO_ROOT/venv/bin/python $REPO_ROOT/dashboard/server.py"
echo
read -r -p "Proceed? [y/N]: " CONFIRM
case "$CONFIRM" in [yY]|[yY][eE][sS]) ;; *) echo "Aborted."; exit 0 ;; esac
# ---------------------------------------------------------------------- install
mkdir -p "$UNIT_DIR"
STAMP="$(date +%Y%m%d-%H%M%S)"
for u in "${UNITS[@]}"; do
if [ -f "$UNIT_DIR/$u" ]; then
cp -p "$UNIT_DIR/$u" "$UNIT_DIR/$u.bak-$STAMP"
echo " backed up existing $u -> $u.bak-$STAMP"
fi
# Substitute only the shipped author values; anchored so nothing else matches.
sed -e "s|/home/cort/hblink4|$REPO_ROOT|g" \
-e "s|^User=cort$|User=$SVC_USER|" \
-e "s|^Group=cort$|Group=$SVC_GROUP|" \
"$REPO_ROOT/$u" > "$UNIT_DIR/$u"
chmod 644 "$UNIT_DIR/$u"
ok "installed $UNIT_DIR/$u"
done
# Confirm the substitution produced what we intended, rather than leaving a
# half-rewritten unit in place. (Checked against the chosen values, not against
# the author's literal strings -- those are legitimate if you happen to use the
# same username or path.)
for u in "${UNITS[@]}"; do
grep -q "^User=$SVC_USER$" "$UNIT_DIR/$u" || die "$u: User= was not set correctly"
grep -q "^Group=$SVC_GROUP$" "$UNIT_DIR/$u" || die "$u: Group= was not set correctly"
grep -q "^WorkingDirectory=$REPO_ROOT$" "$UNIT_DIR/$u" || die "$u: WorkingDirectory= was not set correctly"
grep -q "^ExecStart=$REPO_ROOT/venv/bin/python " "$UNIT_DIR/$u" || die "$u: ExecStart= was not set correctly"
done
if [ "$LIVE_INSTALL" = 0 ]; then
echo
ok "Wrote units to $UNIT_DIR (DESTDIR set -- systemd not touched)."
exit 0
fi
systemctl daemon-reload
ok "systemctl daemon-reload"
# ------------------------------------------------------------- enable and start
echo
read -r -p "Enable services at boot? [y/N]: " DO_ENABLE
case "$DO_ENABLE" in
[yY]|[yY][eE][sS]) systemctl enable "${UNITS[@]}" >/dev/null && ok "enabled at boot" ;;
esac
read -r -p "Start (or restart) the services now? [y/N]: " DO_START
case "$DO_START" in
[yY]|[yY][eE][sS])
systemctl restart "${UNITS[@]}"
sleep 2
echo
for u in "${UNITS[@]}"; do
if systemctl is-active --quiet "$u"; then
ok "$u is running"
else
warn "$u did not start -- journalctl -u ${u%.service} -n 30"
fi
done
;;
esac
echo
note "Done. Useful commands:"
echo " systemctl status hblink4 hblink4-dash"
echo " journalctl -u hblink4 -f"
echo
note "If you edit a unit later, edit $UNIT_DIR/<unit> and run 'systemctl daemon-reload'."