mirror of
https://github.com/pyMC-dev/pyMC_Repeater.git
synced 2026-08-08 09:52:55 +02:00
Merge remote-tracking branch 'origin/dev' into feat/mqtt_merge
This commit is contained in:
@@ -25,6 +25,9 @@ repeater:
|
||||
# If both identity_file and identity_key are set, identity_key takes precedence
|
||||
# identity_key: null
|
||||
|
||||
# Owner information (shown to clients requesting owner info)
|
||||
owner_info: ""
|
||||
|
||||
# Duplicate packet cache TTL in seconds
|
||||
cache_ttl: 3600
|
||||
|
||||
|
||||
@@ -4,12 +4,69 @@
|
||||
set -e
|
||||
|
||||
INSTALL_DIR="/opt/pymc_repeater"
|
||||
VENV_DIR="$INSTALL_DIR/venv"
|
||||
VENV_PIP="$VENV_DIR/bin/pip"
|
||||
VENV_PYTHON="$VENV_DIR/bin/python"
|
||||
CONFIG_DIR="/etc/pymc_repeater"
|
||||
LOG_DIR="/var/log/pymc_repeater"
|
||||
SERVICE_USER="repeater"
|
||||
SERVICE_NAME="pymc-repeater"
|
||||
SILENT_MODE="${PYMC_SILENT:-${SILENT:-}}"
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Virtual-environment helpers
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
# Create (or re-create) the dedicated venv for pymc_repeater
|
||||
ensure_venv() {
|
||||
if [ ! -x "$VENV_PYTHON" ]; then
|
||||
echo ">>> Creating virtual environment at $VENV_DIR ..."
|
||||
python3 -m venv --system-site-packages "$VENV_DIR"
|
||||
# Upgrade pip inside the venv
|
||||
"$VENV_PIP" install --upgrade pip setuptools wheel >/dev/null 2>&1 || true
|
||||
fi
|
||||
}
|
||||
|
||||
# Migrate an existing system-pip install into the venv.
|
||||
# Idempotent: safe to call on every upgrade.
|
||||
migrate_to_venv() {
|
||||
echo ">>> Checking for legacy system-pip installation..."
|
||||
|
||||
# 1. Ensure the venv exists
|
||||
ensure_venv
|
||||
|
||||
# 2. Remove legacy PYTHONPATH from the service unit
|
||||
local svc_unit="/etc/systemd/system/pymc-repeater.service"
|
||||
if [ -f "$svc_unit" ]; then
|
||||
if grep -q 'PYTHONPATH' "$svc_unit" 2>/dev/null; then
|
||||
sed -i '/^Environment=.*PYTHONPATH/d' "$svc_unit"
|
||||
echo " ✓ Removed legacy PYTHONPATH from service unit"
|
||||
fi
|
||||
# 3. Fix WorkingDirectory if still pointing at old source
|
||||
if grep -q 'WorkingDirectory=/opt/pymc_repeater' "$svc_unit" 2>/dev/null; then
|
||||
sed -i 's|WorkingDirectory=/opt/pymc_repeater|WorkingDirectory=/var/lib/pymc_repeater|' "$svc_unit"
|
||||
echo " ✓ Fixed WorkingDirectory in service unit"
|
||||
fi
|
||||
# 4. Ensure ExecStart uses the venv python
|
||||
if grep -q 'ExecStart=/usr/bin/python3' "$svc_unit" 2>/dev/null; then
|
||||
sed -i "s|ExecStart=/usr/bin/python3|ExecStart=$VENV_PYTHON|" "$svc_unit"
|
||||
echo " ✓ Updated ExecStart to use venv python"
|
||||
fi
|
||||
systemctl daemon-reload
|
||||
fi
|
||||
|
||||
# 5. Remove the package from system python (best-effort)
|
||||
python3 -m pip uninstall -y pymc_repeater 2>/dev/null || true
|
||||
python3 -m pip uninstall -y pymc_core 2>/dev/null || true
|
||||
echo " ✓ Cleaned up system-level packages (if any)"
|
||||
|
||||
# 6. Remove stale source trees that could shadow the venv package
|
||||
if [ -d "$INSTALL_DIR/repeater" ]; then
|
||||
rm -rf "$INSTALL_DIR/repeater"
|
||||
echo " ✓ Removed stale source tree from $INSTALL_DIR/repeater"
|
||||
fi
|
||||
}
|
||||
|
||||
is_silent_flag() {
|
||||
case "${1:-}" in
|
||||
--silent|-y|silent) return 0 ;;
|
||||
@@ -96,9 +153,15 @@ is_enabled() {
|
||||
|
||||
# Function to get current version
|
||||
get_version() {
|
||||
# Read version from the pip-installed package in dist-packages
|
||||
python3 -c "from importlib.metadata import version; print(version('pymc_repeater'))" 2>/dev/null \
|
||||
|| echo "not installed"
|
||||
# Read version from the pip-installed package in the venv
|
||||
if [ -x "$VENV_PYTHON" ]; then
|
||||
"$VENV_PYTHON" -c "from importlib.metadata import version; print(version('pymc_repeater'))" 2>/dev/null \
|
||||
|| echo "not installed"
|
||||
else
|
||||
# Fallback: try system python for pre-migration installs
|
||||
python3 -c "from importlib.metadata import version; print(version('pymc_repeater'))" 2>/dev/null \
|
||||
|| echo "not installed"
|
||||
fi
|
||||
}
|
||||
|
||||
# Function to get service status for display
|
||||
@@ -272,12 +335,16 @@ install_repeater() {
|
||||
|
||||
echo "25"; echo "# Installing system dependencies..."
|
||||
apt-get update -qq
|
||||
DEBIAN_FRONTEND=noninteractive apt-get install -y libffi-dev libusb-1.0-0 sudo jq pip python3-rrdtool wget swig build-essential python3-dev
|
||||
DEBIAN_FRONTEND=noninteractive apt-get install -y libffi-dev libusb-1.0-0 sudo jq pip python3-venv python3-rrdtool wget swig build-essential python3-dev
|
||||
# Install polkit (package name varies by distro version)
|
||||
DEBIAN_FRONTEND=noninteractive apt-get install -y policykit-1 2>/dev/null \
|
||||
|| DEBIAN_FRONTEND=noninteractive apt-get install -y polkitd pkexec 2>/dev/null \
|
||||
|| echo " Warning: Could not install polkit (sudo fallback will be used)"
|
||||
pip install --break-system-packages setuptools_scm >/dev/null 2>&1 || true
|
||||
# setuptools_scm needed for git version detection during build
|
||||
pip install --break-system-packages setuptools_scm >/dev/null 2>&1 || python3 -m pip install --break-system-packages setuptools_scm >/dev/null 2>&1 || true
|
||||
|
||||
echo "28"; echo "# Creating virtual environment..."
|
||||
ensure_venv
|
||||
|
||||
# Install mikefarah yq v4 if not already installed
|
||||
if ! command -v yq &> /dev/null || [[ "$(yq --version 2>&1)" != *"mikefarah/yq"* ]]; then
|
||||
@@ -316,8 +383,12 @@ install_repeater() {
|
||||
fi
|
||||
|
||||
echo "65"; echo "# Setting permissions..."
|
||||
chown -R "$SERVICE_USER:$SERVICE_USER" "$INSTALL_DIR" "$CONFIG_DIR" "$LOG_DIR" /var/lib/pymc_repeater
|
||||
# Venv stays root-owned (pip runs as root); service user only needs read+execute
|
||||
chown -R "$SERVICE_USER:$SERVICE_USER" "$CONFIG_DIR" "$LOG_DIR" /var/lib/pymc_repeater
|
||||
chmod 750 "$CONFIG_DIR" "$LOG_DIR" /var/lib/pymc_repeater
|
||||
# Ensure manage.sh and support files in INSTALL_DIR are accessible
|
||||
chown root:root "$INSTALL_DIR"
|
||||
chmod 755 "$INSTALL_DIR"
|
||||
# Ensure the service user can create subdirectories in their home directory
|
||||
chmod 755 /var/lib/pymc_repeater
|
||||
# Pre-create the .config directory that the service will need
|
||||
@@ -355,35 +426,46 @@ EOF
|
||||
set -e
|
||||
CHANNEL="${1:-main}"
|
||||
PRETEND_VERSION="${2:-}"
|
||||
VENV_DIR="/opt/pymc_repeater/venv"
|
||||
VENV_PIP="$VENV_DIR/bin/pip"
|
||||
VENV_PYTHON="$VENV_DIR/bin/python"
|
||||
# Validate: only allow safe git ref characters
|
||||
if ! [[ "$CHANNEL" =~ ^[a-zA-Z0-9._/-]{1,80}$ ]]; then
|
||||
echo "Invalid channel name: $CHANNEL" >&2
|
||||
exit 1
|
||||
fi
|
||||
export PIP_ROOT_USER_ACTION=ignore
|
||||
# If caller supplied a version string, tell setuptools_scm to use it (sudo
|
||||
# strips env vars so it is passed as a positional argument instead).
|
||||
[ -n "$PRETEND_VERSION" ] && export SETUPTOOLS_SCM_PRETEND_VERSION="$PRETEND_VERSION"
|
||||
# Migration: remove legacy PYTHONPATH from service unit if present.
|
||||
# Old installs set PYTHONPATH=/opt/pymc_repeater which caused the service to
|
||||
# load from a stale source copy instead of the pip-installed dist-packages.
|
||||
# ---- Migration: ensure venv exists (handles upgrades from system-pip era) ----
|
||||
if [ ! -x "$VENV_PYTHON" ]; then
|
||||
echo "[pymc-do-upgrade] Creating venv at $VENV_DIR ..."
|
||||
python3 -m venv --system-site-packages "$VENV_DIR"
|
||||
"$VENV_PIP" install --upgrade pip setuptools wheel >/dev/null 2>&1 || true
|
||||
fi
|
||||
# ---- Migration: clean up legacy service unit issues ----
|
||||
SVC_UNIT=/etc/systemd/system/pymc-repeater.service
|
||||
if grep -q 'PYTHONPATH' "$SVC_UNIT" 2>/dev/null; then
|
||||
sed -i '/^Environment=.*PYTHONPATH/d' "$SVC_UNIT"
|
||||
systemctl daemon-reload
|
||||
fi
|
||||
# Migration: fix WorkingDirectory if it still points at the old source checkout.
|
||||
# /opt/pymc_repeater contains a repeater/ subdirectory which shadows the
|
||||
# pip-installed package, causing updates to have no effect on the running process.
|
||||
if grep -q 'WorkingDirectory=/opt/pymc_repeater' "$SVC_UNIT" 2>/dev/null; then
|
||||
sed -i 's|WorkingDirectory=/opt/pymc_repeater|WorkingDirectory=/var/lib/pymc_repeater|' "$SVC_UNIT"
|
||||
systemctl daemon-reload
|
||||
fi
|
||||
exec python3 -m pip install \
|
||||
--break-system-packages \
|
||||
if grep -q 'ExecStart=/usr/bin/python3' "$SVC_UNIT" 2>/dev/null; then
|
||||
sed -i "s|ExecStart=/usr/bin/python3|ExecStart=$VENV_PYTHON|" "$SVC_UNIT"
|
||||
systemctl daemon-reload
|
||||
fi
|
||||
# ---- Remove stale source trees that shadow the venv package ----
|
||||
[ -d /opt/pymc_repeater/repeater ] && rm -rf /opt/pymc_repeater/repeater
|
||||
# ---- Remove old system-level packages to avoid confusion ----
|
||||
python3 -m pip uninstall -y pymc_repeater 2>/dev/null || true
|
||||
python3 -m pip uninstall -y pymc_core 2>/dev/null || true
|
||||
# ---- Install into the venv ----
|
||||
exec "$VENV_PIP" install \
|
||||
--upgrade \
|
||||
--no-cache-dir \
|
||||
--force-reinstall \
|
||||
--ignore-installed \
|
||||
"pymc_repeater[hardware] @ git+https://github.com/rightup/pyMC_Repeater.git@${CHANNEL}"
|
||||
UPGRADEEOF
|
||||
chmod 0755 /usr/local/bin/pymc-do-upgrade
|
||||
@@ -405,9 +487,6 @@ UPGRADEEOF
|
||||
SCRIPT_DIR="$(dirname "$0")"
|
||||
cd "$SCRIPT_DIR"
|
||||
|
||||
# Suppress pip root user warnings
|
||||
export PIP_ROOT_USER_ACTION=ignore
|
||||
|
||||
# Calculate version from git for setuptools_scm
|
||||
if [ -d .git ]; then
|
||||
git fetch --tags 2>/dev/null || true
|
||||
@@ -423,13 +502,12 @@ UPGRADEEOF
|
||||
echo "Note: Using optimized binary wheels for faster installation"
|
||||
echo ""
|
||||
|
||||
# Remove old pymc_core first so no stale .py/.pyc files linger
|
||||
python3 -m pip uninstall -y pymc_core 2>/dev/null || true
|
||||
# Ensure venv exists
|
||||
ensure_venv
|
||||
|
||||
# Install with --force-reinstall to ensure fresh pymc_core from GitHub
|
||||
# --ignore-installed avoids failures on system-managed packages (e.g. PyYAML)
|
||||
echo "Installing pymc_repeater with fresh dependencies from pyproject.toml..."
|
||||
if python3 -m pip install --break-system-packages --no-cache-dir --force-reinstall --ignore-installed .[hardware]; then
|
||||
# Install into the venv (clean, no system-packages flags needed)
|
||||
echo "Installing pymc_repeater into venv ($VENV_DIR)..."
|
||||
if "$VENV_PIP" install --upgrade --no-cache-dir .[hardware]; then
|
||||
echo ""
|
||||
echo "✓ Python package installation completed successfully!"
|
||||
|
||||
@@ -601,12 +679,12 @@ upgrade_repeater() {
|
||||
echo "[3/9] Updating system dependencies..."
|
||||
apt-get update -qq
|
||||
|
||||
apt-get install -y libffi-dev libusb-1.0-0 sudo jq pip python3-rrdtool wget swig build-essential python3-dev
|
||||
apt-get install -y libffi-dev libusb-1.0-0 sudo jq pip python3-venv python3-rrdtool wget swig build-essential python3-dev
|
||||
# Install polkit (package name varies by distro version)
|
||||
apt-get install -y policykit-1 2>/dev/null \
|
||||
|| apt-get install -y polkitd pkexec 2>/dev/null \
|
||||
|| echo " Warning: Could not install polkit (sudo fallback will be used)"
|
||||
pip install --break-system-packages setuptools_scm >/dev/null 2>&1 || true
|
||||
pip install --break-system-packages setuptools_scm >/dev/null 2>&1 || python3 -m pip install --break-system-packages setuptools_scm >/dev/null 2>&1 || true
|
||||
|
||||
# Install mikefarah yq v4 if not already installed
|
||||
if ! command -v yq &> /dev/null || [[ "$(yq --version 2>&1)" != *"mikefarah/yq"* ]]; then
|
||||
@@ -654,7 +732,10 @@ upgrade_repeater() {
|
||||
echo " ✓ User groups updated"
|
||||
|
||||
echo "[6/9] Fixing permissions..."
|
||||
chown -R "$SERVICE_USER:$SERVICE_USER" "$INSTALL_DIR" "$CONFIG_DIR" "$LOG_DIR" /var/lib/pymc_repeater 2>/dev/null || true
|
||||
# Venv stays root-owned (pip runs as root); service user only needs read+execute
|
||||
chown -R "$SERVICE_USER:$SERVICE_USER" "$CONFIG_DIR" "$LOG_DIR" /var/lib/pymc_repeater 2>/dev/null || true
|
||||
chown root:root "$INSTALL_DIR" 2>/dev/null || true
|
||||
chmod 755 "$INSTALL_DIR" 2>/dev/null || true
|
||||
chmod 750 "$CONFIG_DIR" "$LOG_DIR" 2>/dev/null || true
|
||||
chmod 755 /var/lib/pymc_repeater 2>/dev/null || true
|
||||
# Pre-create the .config directory that the service will need
|
||||
@@ -687,35 +768,46 @@ EOF
|
||||
set -e
|
||||
CHANNEL="${1:-main}"
|
||||
PRETEND_VERSION="${2:-}"
|
||||
VENV_DIR="/opt/pymc_repeater/venv"
|
||||
VENV_PIP="$VENV_DIR/bin/pip"
|
||||
VENV_PYTHON="$VENV_DIR/bin/python"
|
||||
# Validate: only allow safe git ref characters
|
||||
if ! [[ "$CHANNEL" =~ ^[a-zA-Z0-9._/-]{1,80}$ ]]; then
|
||||
echo "Invalid channel name: $CHANNEL" >&2
|
||||
exit 1
|
||||
fi
|
||||
export PIP_ROOT_USER_ACTION=ignore
|
||||
# If caller supplied a version string, tell setuptools_scm to use it (sudo
|
||||
# strips env vars so it is passed as a positional argument instead).
|
||||
[ -n "$PRETEND_VERSION" ] && export SETUPTOOLS_SCM_PRETEND_VERSION="$PRETEND_VERSION"
|
||||
# Migration: remove legacy PYTHONPATH from service unit if present.
|
||||
# Old installs set PYTHONPATH=/opt/pymc_repeater which caused the service to
|
||||
# load from a stale source copy instead of the pip-installed dist-packages.
|
||||
# ---- Migration: ensure venv exists (handles upgrades from system-pip era) ----
|
||||
if [ ! -x "$VENV_PYTHON" ]; then
|
||||
echo "[pymc-do-upgrade] Creating venv at $VENV_DIR ..."
|
||||
python3 -m venv --system-site-packages "$VENV_DIR"
|
||||
"$VENV_PIP" install --upgrade pip setuptools wheel >/dev/null 2>&1 || true
|
||||
fi
|
||||
# ---- Migration: clean up legacy service unit issues ----
|
||||
SVC_UNIT=/etc/systemd/system/pymc-repeater.service
|
||||
if grep -q 'PYTHONPATH' "$SVC_UNIT" 2>/dev/null; then
|
||||
sed -i '/^Environment=.*PYTHONPATH/d' "$SVC_UNIT"
|
||||
systemctl daemon-reload
|
||||
fi
|
||||
# Migration: fix WorkingDirectory if it still points at the old source checkout.
|
||||
# /opt/pymc_repeater contains a repeater/ subdirectory which shadows the
|
||||
# pip-installed package, causing updates to have no effect on the running process.
|
||||
if grep -q 'WorkingDirectory=/opt/pymc_repeater' "$SVC_UNIT" 2>/dev/null; then
|
||||
sed -i 's|WorkingDirectory=/opt/pymc_repeater|WorkingDirectory=/var/lib/pymc_repeater|' "$SVC_UNIT"
|
||||
systemctl daemon-reload
|
||||
fi
|
||||
exec python3 -m pip install \
|
||||
--break-system-packages \
|
||||
if grep -q 'ExecStart=/usr/bin/python3' "$SVC_UNIT" 2>/dev/null; then
|
||||
sed -i "s|ExecStart=/usr/bin/python3|ExecStart=$VENV_PYTHON|" "$SVC_UNIT"
|
||||
systemctl daemon-reload
|
||||
fi
|
||||
# ---- Remove stale source trees that shadow the venv package ----
|
||||
[ -d /opt/pymc_repeater/repeater ] && rm -rf /opt/pymc_repeater/repeater
|
||||
# ---- Remove old system-level packages to avoid confusion ----
|
||||
python3 -m pip uninstall -y pymc_repeater 2>/dev/null || true
|
||||
python3 -m pip uninstall -y pymc_core 2>/dev/null || true
|
||||
# ---- Install into the venv ----
|
||||
exec "$VENV_PIP" install \
|
||||
--upgrade \
|
||||
--no-cache-dir \
|
||||
--force-reinstall \
|
||||
--ignore-installed \
|
||||
"pymc_repeater[hardware] @ git+https://github.com/rightup/pyMC_Repeater.git@${CHANNEL}"
|
||||
UPGRADEEOF
|
||||
chmod 0755 /usr/local/bin/pymc-do-upgrade
|
||||
@@ -735,9 +827,6 @@ UPGRADEEOF
|
||||
SCRIPT_DIR="$(dirname "$0")"
|
||||
cd "$SCRIPT_DIR"
|
||||
|
||||
# Suppress pip root user warnings
|
||||
export PIP_ROOT_USER_ACTION=ignore
|
||||
|
||||
# Calculate version from git for setuptools_scm
|
||||
if [ -d .git ]; then
|
||||
git fetch --tags 2>/dev/null || true
|
||||
@@ -753,13 +842,12 @@ UPGRADEEOF
|
||||
echo "Note: Using optimized binary wheels for faster installation"
|
||||
echo ""
|
||||
|
||||
# Remove old pymc_core first so no stale .py/.pyc files linger
|
||||
python3 -m pip uninstall -y pymc_core 2>/dev/null || true
|
||||
# Migrate from system pip to venv (idempotent)
|
||||
migrate_to_venv
|
||||
|
||||
# Install with --force-reinstall to ensure fresh pymc_core from GitHub
|
||||
# --ignore-installed avoids failures on system-managed packages (e.g. PyYAML)
|
||||
echo "Upgrading pymc_repeater with fresh dependencies from pyproject.toml..."
|
||||
if python3 -m pip install --break-system-packages --no-cache-dir --force-reinstall --ignore-installed .[hardware]; then
|
||||
# Install into the venv (clean, no system-packages flags needed)
|
||||
echo "Upgrading pymc_repeater into venv ($VENV_DIR)..."
|
||||
if "$VENV_PIP" install --upgrade --no-cache-dir .[hardware]; then
|
||||
echo ""
|
||||
echo "✓ Package and dependencies upgraded successfully!"
|
||||
else
|
||||
|
||||
@@ -12,8 +12,8 @@ User=repeater
|
||||
Group=repeater
|
||||
WorkingDirectory=/var/lib/pymc_repeater
|
||||
|
||||
# Start command - use python module directly with proper path
|
||||
ExecStart=/usr/bin/python3 -m repeater.main --config /etc/pymc_repeater/config.yaml
|
||||
# Start command - use venv python to avoid system package conflicts
|
||||
ExecStart=/opt/pymc_repeater/venv/bin/python -m repeater.main --config /etc/pymc_repeater/config.yaml
|
||||
|
||||
# Restart on failure
|
||||
Restart=on-failure
|
||||
|
||||
@@ -33,6 +33,7 @@ class ProtocolRequestHelper:
|
||||
radio=None,
|
||||
engine=None,
|
||||
neighbor_tracker=None,
|
||||
config=None,
|
||||
):
|
||||
|
||||
self.identity_manager = identity_manager
|
||||
@@ -41,6 +42,7 @@ class ProtocolRequestHelper:
|
||||
self.radio = radio
|
||||
self.engine = engine
|
||||
self.neighbor_tracker = neighbor_tracker
|
||||
self.config = config or {}
|
||||
|
||||
# Dictionary of core handlers keyed by dest_hash
|
||||
self.handlers = {}
|
||||
@@ -61,6 +63,9 @@ class ProtocolRequestHelper:
|
||||
# Build request handlers dict
|
||||
request_handlers = {
|
||||
REQ_TYPE_GET_STATUS: self._handle_get_status,
|
||||
REQ_TYPE_GET_ACCESS_LIST: self._make_handle_get_access_list(identity_acl),
|
||||
REQ_TYPE_GET_NEIGHBOURS: self._handle_get_neighbours,
|
||||
REQ_TYPE_GET_OWNER_INFO: self._handle_get_owner_info,
|
||||
}
|
||||
|
||||
# Create core handler
|
||||
@@ -227,3 +232,139 @@ class ProtocolRequestHelper:
|
||||
)
|
||||
|
||||
return stats
|
||||
|
||||
def _make_handle_get_access_list(self, identity_acl):
|
||||
"""Create a closure for GET_ACCESS_LIST bound to a specific identity ACL."""
|
||||
def _handler(client, timestamp: int, req_data: bytes):
|
||||
return self._handle_get_access_list(client, timestamp, req_data, identity_acl)
|
||||
return _handler
|
||||
|
||||
def _handle_get_access_list(self, client, timestamp: int, req_data: bytes, identity_acl):
|
||||
"""Return ACL entries: [pub_key_prefix(6) + permissions(1)] per client.
|
||||
|
||||
Admin-only. Matches C++ simple_repeater handleRequest REQ_TYPE_GET_ACCESS_LIST.
|
||||
"""
|
||||
if not hasattr(client, "is_admin") or not client.is_admin():
|
||||
logger.debug("GET_ACCESS_LIST rejected: client is not admin")
|
||||
return None
|
||||
|
||||
# req_data[0] and req_data[1] are reserved bytes; must both be 0
|
||||
if len(req_data) >= 2 and (req_data[0] != 0 or req_data[1] != 0):
|
||||
logger.debug("GET_ACCESS_LIST: reserved bytes non-zero, ignoring")
|
||||
return None
|
||||
|
||||
result = bytearray()
|
||||
for ci in identity_acl.get_all_clients():
|
||||
if ci.permissions == 0:
|
||||
continue # skip deleted entries
|
||||
pubkey = ci.id.get_public_key()
|
||||
result.extend(pubkey[:6]) # 6-byte pub_key prefix
|
||||
result.append(ci.permissions & 0xFF)
|
||||
|
||||
logger.debug("GET_ACCESS_LIST: returning %d entries", len(result) // 7)
|
||||
return bytes(result)
|
||||
|
||||
def _handle_get_neighbours(self, client, timestamp: int, req_data: bytes):
|
||||
"""Return paginated, sorted neighbour list.
|
||||
|
||||
Matches C++ simple_repeater handleRequest REQ_TYPE_GET_NEIGHBOURS.
|
||||
Request: version(1) + count(1) + offset(2 LE) + order_by(1) + pubkey_prefix_len(1) + random(4)
|
||||
Response: total_count(2 LE) + results_count(2 LE) + entries
|
||||
Each entry: pubkey_prefix(N) + heard_seconds_ago(4 LE) + snr(1 signed)
|
||||
"""
|
||||
if len(req_data) < 7:
|
||||
logger.debug("GET_NEIGHBOURS: req_data too short (%d bytes)", len(req_data))
|
||||
return None
|
||||
|
||||
request_version = req_data[0]
|
||||
if request_version != 0:
|
||||
logger.debug("GET_NEIGHBOURS: unsupported version %d", request_version)
|
||||
return None
|
||||
|
||||
count = req_data[1]
|
||||
offset = struct.unpack_from("<H", req_data, 2)[0]
|
||||
order_by = req_data[4]
|
||||
pubkey_prefix_len = min(req_data[5], 32)
|
||||
|
||||
# Fetch neighbours from storage
|
||||
storage = getattr(self.neighbor_tracker, "storage", None) if self.neighbor_tracker else None
|
||||
if not storage or not hasattr(storage, "get_neighbors"):
|
||||
logger.debug("GET_NEIGHBOURS: no storage available")
|
||||
# Return empty result
|
||||
return struct.pack("<HH", 0, 0)
|
||||
|
||||
raw_neighbors = storage.get_neighbors()
|
||||
now = time.time()
|
||||
|
||||
# Build sortable list: (pubkey_hex, heard_seconds_ago, snr)
|
||||
entries = []
|
||||
for pubkey_hex, info in raw_neighbors.items():
|
||||
last_seen = info.get("last_seen", 0) or 0
|
||||
heard_ago = max(0, int(now - last_seen))
|
||||
snr_raw = info.get("snr", 0) or 0
|
||||
# Store SNR as int8 (firmware stores snr * 4 as int8)
|
||||
snr_int = max(-128, min(127, int(snr_raw * 4)))
|
||||
entries.append((pubkey_hex, heard_ago, snr_int))
|
||||
|
||||
# Sort (matches C++ order_by values)
|
||||
if order_by == 0:
|
||||
entries.sort(key=lambda e: e[1]) # newest first (smallest heard_ago)
|
||||
elif order_by == 1:
|
||||
entries.sort(key=lambda e: e[1], reverse=True) # oldest first
|
||||
elif order_by == 2:
|
||||
entries.sort(key=lambda e: e[2], reverse=True) # strongest SNR first
|
||||
elif order_by == 3:
|
||||
entries.sort(key=lambda e: e[2]) # weakest SNR first
|
||||
|
||||
total_count = len(entries)
|
||||
|
||||
# Paginate
|
||||
entry_size = pubkey_prefix_len + 4 + 1
|
||||
max_results_bytes = 130 # firmware buffer limit
|
||||
results = bytearray()
|
||||
results_count = 0
|
||||
|
||||
for i in range(count):
|
||||
idx = i + offset
|
||||
if idx >= total_count:
|
||||
break
|
||||
if len(results) + entry_size > max_results_bytes:
|
||||
break
|
||||
|
||||
pubkey_hex, heard_ago, snr_int = entries[idx]
|
||||
try:
|
||||
pubkey_bytes = bytes.fromhex(pubkey_hex)
|
||||
except (ValueError, TypeError):
|
||||
continue
|
||||
results.extend(pubkey_bytes[:pubkey_prefix_len])
|
||||
results.extend(struct.pack("<I", heard_ago))
|
||||
results.append(snr_int & 0xFF)
|
||||
results_count += 1
|
||||
|
||||
header = struct.pack("<HH", total_count, results_count)
|
||||
|
||||
logger.debug(
|
||||
"GET_NEIGHBOURS: total=%d, returned=%d, offset=%d, order=%d",
|
||||
total_count, results_count, offset, order_by,
|
||||
)
|
||||
return header + bytes(results)
|
||||
|
||||
def _handle_get_owner_info(self, client, timestamp: int, req_data: bytes):
|
||||
"""Return firmware version, node name, and owner info.
|
||||
|
||||
Matches C++ simple_repeater: sprintf("%s\\n%s\\n%s", FIRMWARE_VERSION, node_name, owner_info)
|
||||
"""
|
||||
repeater_cfg = self.config.get("repeater", {})
|
||||
node_name = repeater_cfg.get("node_name", "pyMC_Repeater")
|
||||
owner_info = repeater_cfg.get("owner_info", "")
|
||||
|
||||
# Version: use package version if available, fallback to "pyMC"
|
||||
try:
|
||||
from importlib.metadata import version as pkg_version
|
||||
fw_version = pkg_version("pymc-repeater")
|
||||
except Exception:
|
||||
fw_version = "pyMC"
|
||||
|
||||
result = f"{fw_version}\n{node_name}\n{owner_info}".encode("utf-8")
|
||||
logger.debug("GET_OWNER_INFO: %s", result.decode("utf-8", errors="replace"))
|
||||
return result
|
||||
|
||||
@@ -311,6 +311,7 @@ class RepeaterDaemon:
|
||||
radio=self.radio,
|
||||
engine=self.repeater_handler,
|
||||
neighbor_tracker=self.advert_helper,
|
||||
config=self.config,
|
||||
)
|
||||
# Register repeater identity for protocol requests
|
||||
self.protocol_request_helper.register_identity(
|
||||
|
||||
@@ -84,6 +84,13 @@ def _get_installed_version() -> str:
|
||||
for p in sys.path:
|
||||
if p and ("site-packages" in p or "dist-packages" in p) and p not in dirs:
|
||||
dirs.append(p)
|
||||
# Explicitly include the dedicated venv's site-packages
|
||||
_venv_site = "/opt/pymc_repeater/venv/lib"
|
||||
if os.path.isdir(_venv_site):
|
||||
for child in os.listdir(_venv_site):
|
||||
sp = os.path.join(_venv_site, child, "site-packages")
|
||||
if os.path.isdir(sp) and sp not in dirs:
|
||||
dirs.append(sp)
|
||||
|
||||
# -- 2. Scan for dist-info METADATA files ------------------------------ #
|
||||
pkg_glob = PACKAGE_NAME.replace("-", "_") + "-*.dist-info"
|
||||
@@ -470,6 +477,13 @@ def _cleanup_stale_dist_info() -> None:
|
||||
dirs.append(_site.getusersitepackages())
|
||||
except AttributeError:
|
||||
pass
|
||||
# Also scan the dedicated venv's site-packages
|
||||
_venv_site = "/opt/pymc_repeater/venv/lib"
|
||||
if os.path.isdir(_venv_site):
|
||||
for child in os.listdir(_venv_site):
|
||||
sp = os.path.join(_venv_site, child, "site-packages")
|
||||
if os.path.isdir(sp) and sp not in dirs:
|
||||
dirs.append(sp)
|
||||
|
||||
pkg_glob = PACKAGE_NAME.replace("-", "_") + "-*.dist-info"
|
||||
|
||||
@@ -655,10 +669,12 @@ def _do_check() -> None:
|
||||
|
||||
|
||||
def _migrate_service_unit() -> None:
|
||||
"""Strip legacy PYTHONPATH and fix WorkingDirectory in the systemd service unit.
|
||||
"""Strip legacy PYTHONPATH, fix WorkingDirectory, and ensure ExecStart
|
||||
uses the venv python in the systemd service unit.
|
||||
"""
|
||||
import subprocess as _sp
|
||||
_SVC_UNIT = "/etc/systemd/system/pymc-repeater.service"
|
||||
_VENV_PYTHON = "/opt/pymc_repeater/venv/bin/python"
|
||||
try:
|
||||
_sp.run(["sed", "-i", "/^Environment=.*PYTHONPATH/d", _SVC_UNIT], check=False)
|
||||
_sp.run(
|
||||
@@ -667,6 +683,12 @@ def _migrate_service_unit() -> None:
|
||||
_SVC_UNIT],
|
||||
check=False,
|
||||
)
|
||||
_sp.run(
|
||||
["sed", "-i",
|
||||
f"s|ExecStart=/usr/bin/python3|ExecStart={_VENV_PYTHON}|",
|
||||
_SVC_UNIT],
|
||||
check=False,
|
||||
)
|
||||
_sp.run(["systemctl", "daemon-reload"], check=False)
|
||||
logger.info("[Update] Service unit migration applied (root path).")
|
||||
except Exception as exc:
|
||||
@@ -702,9 +724,12 @@ def _do_install() -> None:
|
||||
|
||||
import os as _os
|
||||
env = _os.environ.copy()
|
||||
env["PIP_ROOT_USER_ACTION"] = "ignore"
|
||||
env["SETUPTOOLS_SCM_PRETEND_VERSION"] = _state.latest_version or "1.0.0"
|
||||
|
||||
_VENV_DIR = "/opt/pymc_repeater/venv"
|
||||
_VENV_PIP = os.path.join(_VENV_DIR, "bin", "pip")
|
||||
_VENV_PYTHON = os.path.join(_VENV_DIR, "bin", "python")
|
||||
|
||||
_state.append_line(f"[pyMC updater] Installing from channel '{channel}'…")
|
||||
|
||||
_UPGRADE_WRAPPER = "/usr/local/bin/pymc-do-upgrade"
|
||||
@@ -712,22 +737,38 @@ def _do_install() -> None:
|
||||
|
||||
if is_root:
|
||||
_migrate_service_unit()
|
||||
|
||||
# Ensure venv exists (migration from system-pip era)
|
||||
if not os.path.isfile(_VENV_PYTHON):
|
||||
_state.append_line("[pyMC updater] Creating venv (first-time migration)…")
|
||||
_run(["python3", "-m", "venv", "--system-site-packages", _VENV_DIR], env=env)
|
||||
_run([_VENV_PIP, "install", "--upgrade", "pip", "setuptools", "wheel"], env=env)
|
||||
|
||||
# Clean up system-level packages to avoid shadowing
|
||||
_run(["python3", "-m", "pip", "uninstall", "-y", "pymc_repeater"], env=env)
|
||||
_run(["python3", "-m", "pip", "uninstall", "-y", "pymc_core"], env=env)
|
||||
|
||||
# Remove stale source tree that could shadow the venv package
|
||||
stale_src = "/opt/pymc_repeater/repeater"
|
||||
if os.path.isdir(stale_src):
|
||||
_state.append_line("[pyMC updater] Removing stale source tree…")
|
||||
import shutil
|
||||
shutil.rmtree(stale_src, ignore_errors=True)
|
||||
|
||||
install_spec = (
|
||||
f"pymc_repeater[hardware] @ git+https://github.com/{GITHUB_OWNER}/{GITHUB_REPO}.git@{channel}"
|
||||
)
|
||||
_state.append_line(f"[pyMC updater] Running as root – direct pip install")
|
||||
_state.append_line(f"[pyMC updater] Running as root – venv pip install")
|
||||
_state.append_line(f"[pyMC updater] Target: {install_spec}")
|
||||
cmd = [
|
||||
"python3", "-m", "pip", "install",
|
||||
"--break-system-packages",
|
||||
_VENV_PIP, "install",
|
||||
"--upgrade",
|
||||
"--no-cache-dir",
|
||||
"--force-reinstall",
|
||||
install_spec,
|
||||
]
|
||||
elif _os.path.isfile(_UPGRADE_WRAPPER):
|
||||
_state.append_line(f"[pyMC updater] Using sudo wrapper: {_UPGRADE_WRAPPER}")
|
||||
# Pass the target version as $2 so the wrapper can set
|
||||
# SETUPTOOLS_SCM_PRETEND_VERSION (sudo strips our env).
|
||||
# The wrapper handles venv creation/migration internally
|
||||
cmd = ["sudo", _UPGRADE_WRAPPER, channel, _state.latest_version or ""]
|
||||
else:
|
||||
msg = (
|
||||
|
||||
Reference in New Issue
Block a user