From c27e6dcf7ef09b8ceff6db3d74f2396967d914d3 Mon Sep 17 00:00:00 2001 From: Ramil Valitov Date: Thu, 10 Sep 2026 21:12:35 +0300 Subject: [PATCH 1/3] fix(telegram): install bot and autostart services on OpenRC hosts setup_telegram_service() wrote a systemd unit only, with no else branch, so on a host without systemd (Alpine/OpenRC) `mtproxymax telegram setup` reported success while installing nothing. The bot stayed dead, and because the bot also carries the "proxy down" alerts, the entire alert channel was lost with no signal to the user. setup_autostart() had the identical no-else bug, so "start on boot" was silently broken on the same hosts. Add detect_init_system() and branch on it: - OpenRC hosts get /etc/init.d/mtproxymax-telegram supervised by supervise-daemon (respawn_delay=10, respawn_max=0), mirroring the systemd unit's Restart=on-failure / RestartSec=10, plus /etc/init.d/mtproxymax for autostart (plain start/stop, mirroring Type=oneshot + RemainAfterExit=yes). - Hosts with neither init system now warn loudly and return nonzero instead of failing silently, matching what setup_replication_service already does. - A failed start is reported instead of logging "service started" regardless. Route the remaining service calls (update restart, telegram disable/remove, menu toggle, uninstall) through the same detector, and make `telegram status` report whether the service is actually running rather than echoing the settings flag. Unit paths become injectable via SYSTEMD_DIR/INITD_DIR, following the existing ${VAR:-default} idiom, so the service installers are testable without root. Adds tests/test_telegram_service_openrc.sh covering all three init systems. --- README.md | 2 +- mtproxymax.sh | 264 +++++++++++++++++++++---- tests/test_telegram_service_openrc.sh | 266 ++++++++++++++++++++++++++ 3 files changed, 493 insertions(+), 39 deletions(-) create mode 100644 tests/test_telegram_service_openrc.sh diff --git a/README.md b/README.md index 5c7ca1d..2d09dcb 100644 --- a/README.md +++ b/README.md @@ -1036,7 +1036,7 @@ Master-Slave Replication (optional): |-----------|------| | **mtproxymax.sh** | Single bash script: CLI, TUI, config manager | | **telemt** | Rust MTProto engine running inside Docker | -| **Telegram bot service** | Independent systemd service polling Bot API | +| **Telegram bot service** | Independent service polling Bot API (systemd or OpenRC) | | **Replication sync service** | systemd timer pushing config to slave servers | | **Prometheus endpoint** | `/metrics` on port 9090 (localhost only) | diff --git a/mtproxymax.sh b/mtproxymax.sh index c398b7f..44fe19b 100644 --- a/mtproxymax.sh +++ b/mtproxymax.sh @@ -35,6 +35,8 @@ FLEET_DATA_DIR="${FLEET_DATA_DIR:-${INSTALL_DIR}/fleet_data}" SSL_CONF_FILE="${SSL_CONF_FILE:-${INSTALL_DIR}/ssl.conf}" SSL_DIR="${SSL_DIR:-${INSTALL_DIR}/ssl}" CLOUD_BACKUP_FILE="${CLOUD_BACKUP_FILE:-${INSTALL_DIR}/cloud_backup.conf}" +SYSTEMD_DIR="${SYSTEMD_DIR:-/etc/systemd/system}" +INITD_DIR="${INITD_DIR:-/etc/init.d}" SCANNER_SHIELD_SET="mtp_scanners" CONTAINER_NAME="mtproxymax" DOCKER_IMAGE_BASE="mtproxymax-telemt" @@ -522,6 +524,17 @@ detect_os() { fi } +# Detect the host init system: systemd | openrc | none +detect_init_system() { + if command -v systemctl &>/dev/null; then + echo "systemd" + elif [ -x /sbin/openrc-run ] || command -v rc-service &>/dev/null; then + echo "openrc" + else + echo "none" + fi +} + # Check dependencies check_dependencies() { local missing=() @@ -10039,11 +10052,10 @@ self_update() { # Always regenerate and restart Telegram bot service to apply latest daemon code if [ "${TELEGRAM_ENABLED:-}" = "true" ]; then telegram_generate_service_script - if command -v systemctl &>/dev/null && [ -f /etc/systemd/system/mtproxymax-telegram.service ]; then - log_info "Restarting Telegram bot service..." - systemctl restart mtproxymax-telegram.service 2>/dev/null \ - && log_success "Telegram bot service restarted" \ - || log_warn "Telegram restart failed — run: systemctl restart mtproxymax-telegram.service" + if telegram_restart_service; then + log_success "Telegram bot service restarted" + else + log_warn "Telegram bot service is not installed — run: mtproxymax telegram setup" fi fi @@ -11097,8 +11109,12 @@ telegram_setup_wizard() { # Send proxy links telegram_notify_proxy_started &>/dev/null & - # Setup systemd service for bot polling - setup_telegram_service + # Setup the bot polling service (systemd or OpenRC) + if ! setup_telegram_service; then + echo "" + log_warn "The bot is configured, but its background service is NOT running." + log_warn "Bot commands and alerts will not work until it is started (see the hint above)." + fi press_any_key } @@ -12120,12 +12136,66 @@ TELEGRAM_SCRIPT chmod +x "$script_path" } +# Stop the Telegram bot service on whichever init system is present +telegram_stop_service() { + case "$(detect_init_system)" in + systemd) systemctl stop mtproxymax-telegram.service 2>/dev/null || true ;; + openrc) rc-service mtproxymax-telegram stop 2>/dev/null || true ;; + esac + return 0 +} + +# Restart the Telegram bot service; nonzero when it is not installed or fails +telegram_restart_service() { + case "$(detect_init_system)" in + systemd) + [ -f "${SYSTEMD_DIR}/mtproxymax-telegram.service" ] || return 1 + systemctl restart mtproxymax-telegram.service 2>/dev/null + ;; + openrc) + [ -f "${INITD_DIR}/mtproxymax-telegram" ] || return 1 + rc-service mtproxymax-telegram restart 2>/dev/null + ;; + *) return 1 ;; + esac +} + +# Remove the Telegram bot service definition from the host init system +telegram_remove_service() { + case "$(detect_init_system)" in + systemd) + systemctl stop mtproxymax-telegram.service 2>/dev/null || true + systemctl disable mtproxymax-telegram.service 2>/dev/null || true + rm -f "${SYSTEMD_DIR}/mtproxymax-telegram.service" + systemctl daemon-reload 2>/dev/null || true + ;; + openrc) + rc-service mtproxymax-telegram stop 2>/dev/null || true + rc-update del mtproxymax-telegram default 2>/dev/null || true + rm -f "${INITD_DIR}/mtproxymax-telegram" + ;; + esac + return 0 +} + +# True when the Telegram bot service is actually running (not merely enabled) +telegram_service_running() { + case "$(detect_init_system)" in + systemd) systemctl is-active --quiet mtproxymax-telegram.service 2>/dev/null ;; + openrc) rc-service mtproxymax-telegram status >/dev/null 2>&1 ;; + *) return 1 ;; + esac +} + setup_telegram_service() { telegram_generate_service_script - # Create systemd service - if command -v systemctl &>/dev/null; then - cat > /etc/systemd/system/mtproxymax-telegram.service << 'SERVICE_EOF' + local init_system + init_system=$(detect_init_system) + + case "$init_system" in + systemd) + cat > "${SYSTEMD_DIR}/mtproxymax-telegram.service" << 'SERVICE_EOF' [Unit] Description=MTProxyMax Telegram Bot Service After=network-online.target docker.service @@ -12145,10 +12215,65 @@ SERVICE_EOF systemctl daemon-reload systemctl enable mtproxymax-telegram.service 2>/dev/null - systemctl restart mtproxymax-telegram.service 2>/dev/null - log_success "Telegram bot service started" + if systemctl restart mtproxymax-telegram.service 2>/dev/null; then + log_success "Telegram bot service started (systemd)" + else + log_warn "Telegram bot service failed to start — check: journalctl -u mtproxymax-telegram.service" + return 1 + fi + ;; + openrc) + # supervise-daemon keeps the bot alive across crashes, mirroring the + # systemd unit's Restart=on-failure / RestartSec=10. + cat > "${INITD_DIR}/mtproxymax-telegram" << OPENRC_EOF +#!/sbin/openrc-run +# MTProxyMax Telegram Bot Service +# Auto-generated — do not edit manually + +name="mtproxymax-telegram" +description="MTProxyMax Telegram Bot Service" + +supervisor=supervise-daemon +command="/bin/bash" +command_args="${INSTALL_DIR}/mtproxymax-telegram.sh" +respawn_delay=10 +respawn_max=0 + +pidfile="/run/\${RC_SVCNAME}.pid" +output_log="/var/log/mtproxymax-telegram.log" +error_log="/var/log/mtproxymax-telegram.log" + +depend() { + need net + use docker +} + +start_pre() { + if [ ! -x "${INSTALL_DIR}/mtproxymax-telegram.sh" ]; then + eerror "Bot daemon not found at ${INSTALL_DIR}/mtproxymax-telegram.sh" + eerror "Run 'mtproxymax telegram setup' first." + return 1 fi } +OPENRC_EOF + + chmod +x "${INITD_DIR}/mtproxymax-telegram" + rc-update add mtproxymax-telegram default 2>/dev/null || true + if rc-service mtproxymax-telegram restart 2>/dev/null; then + log_success "Telegram bot service started (OpenRC)" + else + log_warn "Telegram bot service failed to start — check: rc-service mtproxymax-telegram status" + return 1 + fi + ;; + *) + log_warn "No supported init system found (systemd or OpenRC)." + log_warn "The bot daemon was generated at ${INSTALL_DIR}/mtproxymax-telegram.sh but is NOT running." + echo -e " ${DIM}Start it manually: nohup ${INSTALL_DIR}/mtproxymax-telegram.sh >/var/log/mtproxymax-telegram.log 2>&1 &${NC}" + return 1 + ;; + esac +} # ── Section 14b: Replication / HA ──────────────────────────── @@ -12558,7 +12683,7 @@ setup_replication_service() { return 1 fi - cat > /etc/systemd/system/mtproxymax-sync.service << 'REPL_SERVICE_EOF' + cat > "${SYSTEMD_DIR}/mtproxymax-sync.service" << 'REPL_SERVICE_EOF' [Unit] Description=MTProxyMax Replication Sync After=network-online.target docker.service @@ -12571,7 +12696,7 @@ StandardOutput=journal StandardError=journal REPL_SERVICE_EOF - cat > /etc/systemd/system/mtproxymax-sync.timer << REPL_TIMER_EOF + cat > "${SYSTEMD_DIR}/mtproxymax-sync.timer" << REPL_TIMER_EOF [Unit] Description=MTProxyMax Replication Sync Timer @@ -12600,8 +12725,8 @@ stop_replication_service() { remove_replication_service() { stop_replication_service - rm -f /etc/systemd/system/mtproxymax-sync.service - rm -f /etc/systemd/system/mtproxymax-sync.timer + rm -f "${SYSTEMD_DIR}/mtproxymax-sync.service" + rm -f "${SYSTEMD_DIR}/mtproxymax-sync.timer" rm -f "${INSTALL_DIR}/mtproxymax-sync.sh" command -v systemctl &>/dev/null && systemctl daemon-reload 2>/dev/null || true } @@ -13203,7 +13328,7 @@ run_installer() { } # Setup autostart - setup_autostart + setup_autostart || true # Telegram setup offer echo "" @@ -13228,9 +13353,28 @@ run_installer() { show_main_menu } +# Remove the main autostart service definition from the host init system +main_service_remove() { + case "$(detect_init_system)" in + systemd) + systemctl stop mtproxymax.service 2>/dev/null || true + systemctl disable mtproxymax.service 2>/dev/null || true + rm -f "${SYSTEMD_DIR}/mtproxymax.service" + systemctl daemon-reload 2>/dev/null || true + ;; + openrc) + rc-service mtproxymax stop 2>/dev/null || true + rc-update del mtproxymax default 2>/dev/null || true + rm -f "${INITD_DIR}/mtproxymax" + ;; + esac + return 0 +} + setup_autostart() { - if command -v systemctl &>/dev/null; then - cat > /etc/systemd/system/mtproxymax.service << 'AUTOSTART_EOF' + case "$(detect_init_system)" in + systemd) + cat > "${SYSTEMD_DIR}/mtproxymax.service" << 'AUTOSTART_EOF' [Unit] Description=MTProxyMax Telegram Proxy After=network-online.target docker.service @@ -13250,7 +13394,51 @@ AUTOSTART_EOF systemctl daemon-reload systemctl enable mtproxymax.service 2>/dev/null log_success "Auto-start enabled (systemd)" - fi + ;; + openrc) + # Type=oneshot + RemainAfterExit=yes wraps the manager's own start/stop, + # so a plain start/stop script is the faithful equivalent (no supervisor). + cat > "${INITD_DIR}/mtproxymax" << OPENRC_EOF +#!/sbin/openrc-run +# MTProxyMax Telegram Proxy +# Auto-generated — do not edit manually + +name="mtproxymax" +description="MTProxyMax Telegram Proxy" + +depend() { + need net + need docker +} + +start() { + ebegin "Starting MTProxyMax" + /usr/local/bin/mtproxymax start + eend \$? +} + +stop() { + ebegin "Stopping MTProxyMax" + /usr/local/bin/mtproxymax stop + eend \$? +} + +status() { + /usr/local/bin/mtproxymax status +} +OPENRC_EOF + + chmod +x "${INITD_DIR}/mtproxymax" + rc-update add mtproxymax default 2>/dev/null || true + log_success "Auto-start enabled (OpenRC)" + ;; + *) + log_warn "No supported init system found (systemd or OpenRC)." + log_warn "Auto-start on boot is NOT enabled." + echo -e " ${DIM}Add it to your init system manually, or start at boot with: ${INSTALL_DIR}/mtproxymax start${NC}" + return 1 + ;; + esac } show_install_summary() { @@ -13352,15 +13540,8 @@ uninstall() { echo "" log_info "Removing services..." - systemctl stop mtproxymax-telegram.service 2>/dev/null || true - systemctl disable mtproxymax-telegram.service 2>/dev/null || true - rm -f /etc/systemd/system/mtproxymax-telegram.service - - systemctl stop mtproxymax.service 2>/dev/null || true - systemctl disable mtproxymax.service 2>/dev/null || true - rm -f /etc/systemd/system/mtproxymax.service - - systemctl daemon-reload 2>/dev/null || true + telegram_remove_service + main_service_remove log_info "Removing geo-blocking rules..." geoblock_remove_all @@ -15478,11 +15659,15 @@ cli_main() { setup) check_root; telegram_setup_wizard ;; test) telegram_test_message ;; status|"") - if [ "$TELEGRAM_ENABLED" = "true" ]; then + if [ "$TELEGRAM_ENABLED" != "true" ]; then + echo -e " ${BOLD}Telegram:${NC} $(draw_status disabled 'Disabled')" + elif telegram_service_running; then echo -e " ${BOLD}Telegram:${NC} $(draw_status running 'Enabled')" echo -e " ${DIM}Interval: every ${TELEGRAM_INTERVAL}h | Alerts: ${TELEGRAM_ALERTS_ENABLED} | Label: ${TELEGRAM_SERVER_LABEL}${NC}" else - echo -e " ${BOLD}Telegram:${NC} $(draw_status disabled 'Disabled')" + echo -e " ${BOLD}Telegram:${NC} $(draw_status warning 'Enabled (service not running)')" + echo -e " ${DIM}Interval: every ${TELEGRAM_INTERVAL}h | Alerts: ${TELEGRAM_ALERTS_ENABLED} | Label: ${TELEGRAM_SERVER_LABEL}${NC}" + echo -e " ${DIM}The bot service is configured but not running — run: mtproxymax telegram setup${NC}" fi ;; interval) @@ -15550,7 +15735,7 @@ cli_main() { check_root TELEGRAM_ENABLED="false" save_settings - systemctl stop mtproxymax-telegram.service 2>/dev/null || true + telegram_stop_service log_success "Telegram disabled" ;; remove) @@ -15559,8 +15744,7 @@ cli_main() { TELEGRAM_BOT_TOKEN="" TELEGRAM_CHAT_ID="" save_settings - systemctl stop mtproxymax-telegram.service 2>/dev/null || true - systemctl disable mtproxymax-telegram.service 2>/dev/null || true + telegram_remove_service log_success "Telegram bot removed" ;; *) log_error "Usage: mtproxymax telegram [setup|test|status|interval|label|alerts|disable|remove]"; return 1 ;; @@ -17236,13 +17420,16 @@ show_telegram_menu() { 4) if [ "$TELEGRAM_ENABLED" = "true" ]; then TELEGRAM_ENABLED="false" - systemctl stop mtproxymax-telegram.service 2>/dev/null || true + telegram_stop_service log_success "Telegram disabled" else if [ -n "$TELEGRAM_BOT_TOKEN" ] && [ -n "$TELEGRAM_CHAT_ID" ]; then TELEGRAM_ENABLED="true" - setup_telegram_service - log_success "Telegram enabled" + if setup_telegram_service; then + log_success "Telegram enabled" + else + log_warn "Telegram bot service could not be started" + fi else log_warn "Run setup wizard first" fi @@ -18055,7 +18242,8 @@ show_info_telegram() { echo "" echo -e " ${BOLD}What does the bot do?${NC}" echo -e " Control your proxy from your phone via Telegram. The bot runs" - echo -e " as a separate systemd service and responds to commands." + echo -e " as a separate background service (systemd or OpenRC) and responds" + echo -e " to commands." echo "" echo -e " ${BOLD}Available commands:${NC}" echo -e " /mp_status Check proxy status, uptime, traffic" diff --git a/tests/test_telegram_service_openrc.sh b/tests/test_telegram_service_openrc.sh new file mode 100644 index 0000000..55f28ed --- /dev/null +++ b/tests/test_telegram_service_openrc.sh @@ -0,0 +1,266 @@ +#!/bin/bash +# Regression tests for Telegram bot / autostart service installation across init systems. +# +# Covers the non-systemd path added for Alpine/OpenRC: the Telegram bot service and the +# main autostart service must be installed via OpenRC instead of silently doing nothing, +# and a host with neither init system must fail loudly rather than reporting success. +set -o pipefail + +if [ "${BASH_VERSINFO[0]:-0}" -lt 4 ]; then + echo "SKIP: bash 4+ required (got ${BASH_VERSION:-unknown})" >&2 + exit 0 +fi + +TEST_TMPDIR=$(mktemp -d 2>/dev/null || mktemp -d -t 'mtp_test_XXXXXX') +INSTALL_DIR="$TEST_TMPDIR/install" +SETTINGS_FILE="$INSTALL_DIR/settings.conf" +INITD_DIR="$TEST_TMPDIR/etc/init.d" +SYSTEMD_DIR="$TEST_TMPDIR/etc/systemd/system" +mkdir -p "$INSTALL_DIR" "$INITD_DIR" "$SYSTEMD_DIR" + +MTPROXYMAX_SOURCE_ONLY=true source "$(dirname "${BASH_SOURCE[0]}")/../mtproxymax.sh" +set +e +trap 'rm -rf "$TEST_TMPDIR"' EXIT + +TESTS_RUN=0 +TESTS_FAILED=0 + +CMD_LOG="$TEST_TMPDIR/cmd.log" +WARN_LOG="$TEST_TMPDIR/warn.log" +: > "$CMD_LOG" +: > "$WARN_LOG" + +check_root() { :; } +log_success() { :; } +log_info() { :; } +log_warn() { echo "$*" >> "$WARN_LOG"; } +log_error() { echo "$*" >> "$WARN_LOG"; } + +# Shadow the init-system binaries so nothing touches the host. +RC_SERVICE_STATUS=0 +SYSTEMCTL_STATUS=0 +systemctl() { echo "systemctl $*" >> "$CMD_LOG"; return "$SYSTEMCTL_STATUS"; } +rc-update() { echo "rc-update $*" >> "$CMD_LOG"; return 0; } +rc-service() { echo "rc-service $*" >> "$CMD_LOG"; return "$RC_SERVICE_STATUS"; } + +FAKE_INIT="openrc" +detect_init_system() { echo "$FAKE_INIT"; } + +assert_eq() { + local name="$1" want="$2" got="$3" + TESTS_RUN=$((TESTS_RUN + 1)) + if [ "$got" = "$want" ]; then + printf ' PASS %s\n' "$name" + else + TESTS_FAILED=$((TESTS_FAILED + 1)) + printf ' FAIL %s (got=%q want=%q)\n' "$name" "$got" "$want" + fi +} + +assert_file_contains() { + local name="$1" needle="$2" file="$3" + TESTS_RUN=$((TESTS_RUN + 1)) + if grep -qF -- "$needle" "$file" 2>/dev/null; then + printf ' PASS %s\n' "$name" + else + TESTS_FAILED=$((TESTS_FAILED + 1)) + printf ' FAIL %s (missing %q in %s)\n' "$name" "$needle" "$file" + fi +} + +assert_log_contains() { + local name="$1" needle="$2" + TESTS_RUN=$((TESTS_RUN + 1)) + if grep -qF -- "$needle" "$CMD_LOG" 2>/dev/null; then + printf ' PASS %s\n' "$name" + else + TESTS_FAILED=$((TESTS_FAILED + 1)) + printf ' FAIL %s (missing %q in command log)\n' "$name" "$needle" + fi +} + +assert_warn_contains() { + local name="$1" needle="$2" + TESTS_RUN=$((TESTS_RUN + 1)) + if grep -qF -- "$needle" "$WARN_LOG" 2>/dev/null; then + printf ' PASS %s\n' "$name" + else + TESTS_FAILED=$((TESTS_FAILED + 1)) + printf ' FAIL %s (missing %q in warning log)\n' "$name" "$needle" + fi +} + +reset_state() { + FAKE_INIT="${1:-openrc}" + RC_SERVICE_STATUS=0 + SYSTEMCTL_STATUS=0 + : > "$CMD_LOG" + : > "$WARN_LOG" + rm -f "$INITD_DIR/mtproxymax" "$INITD_DIR/mtproxymax-telegram" + rm -f "$SYSTEMD_DIR/mtproxymax.service" "$SYSTEMD_DIR/mtproxymax-telegram.service" +} + +TELEGRAM_INIT="$INITD_DIR/mtproxymax-telegram" +MAIN_INIT="$INITD_DIR/mtproxymax" +TELEGRAM_UNIT="$SYSTEMD_DIR/mtproxymax-telegram.service" +MAIN_UNIT="$SYSTEMD_DIR/mtproxymax.service" + +echo "Telegram service init-system tests" + +# ── OpenRC: telegram bot service ───────────────────────────── +reset_state openrc +setup_telegram_service +assert_eq "openrc setup succeeds" "0" "$?" + +assert_file_contains "init script declares supervise-daemon" "supervisor=supervise-daemon" "$TELEGRAM_INIT" +assert_file_contains "init script respawns after 10s" "respawn_delay=10" "$TELEGRAM_INIT" +assert_file_contains "init script respawns indefinitely" "respawn_max=0" "$TELEGRAM_INIT" +assert_file_contains "init script needs net" "need net" "$TELEGRAM_INIT" +assert_file_contains "init script uses docker softly" "use docker" "$TELEGRAM_INIT" +assert_file_contains "init script points at generated daemon" \ + "command_args=\"${INSTALL_DIR}/mtproxymax-telegram.sh\"" "$TELEGRAM_INIT" + +if [ -x "$TELEGRAM_INIT" ]; then + assert_eq "init script is executable" "yes" "yes" +else + assert_eq "init script is executable" "yes" "no" +fi + +# Runtime variables must survive generation unexpanded. +assert_file_contains "RC_SVCNAME stays literal" 'pidfile="/run/${RC_SVCNAME}.pid"' "$TELEGRAM_INIT" +if grep -qF 'pidfile="/run/.pid"' "$TELEGRAM_INIT" 2>/dev/null; then + assert_eq "RC_SVCNAME not expanded at generation time" "literal" "expanded" +else + assert_eq "RC_SVCNAME not expanded at generation time" "literal" "literal" +fi + +assert_log_contains "service added to default runlevel" "rc-update add mtproxymax-telegram default" +assert_log_contains "service restarted via rc-service" "rc-service mtproxymax-telegram restart" + +# ── OpenRC: restart / stop / remove / status helpers ───────── +reset_state openrc +setup_telegram_service >/dev/null +: > "$CMD_LOG" + +telegram_restart_service +assert_eq "restart succeeds when unit installed" "0" "$?" +assert_log_contains "restart uses rc-service" "rc-service mtproxymax-telegram restart" + +telegram_stop_service +assert_log_contains "stop uses rc-service" "rc-service mtproxymax-telegram stop" + +telegram_remove_service +assert_log_contains "remove stops the service" "rc-service mtproxymax-telegram stop" +assert_log_contains "remove deletes from runlevel" "rc-update del mtproxymax-telegram default" +if [ -f "$TELEGRAM_INIT" ]; then + assert_eq "remove deletes init script" "deleted" "kept" +else + assert_eq "remove deletes init script" "deleted" "deleted" +fi + +# restart must report failure once the service definition is gone. +telegram_restart_service +assert_eq "restart fails when unit absent" "1" "$?" + +reset_state openrc +setup_telegram_service >/dev/null +RC_SERVICE_STATUS=0 +if telegram_service_running; then + assert_eq "status true when rc-service succeeds" "true" "true" +else + assert_eq "status true when rc-service succeeds" "true" "false" +fi +RC_SERVICE_STATUS=1 +if telegram_service_running; then + assert_eq "status false when rc-service fails" "false" "true" +else + assert_eq "status false when rc-service fails" "false" "false" +fi + +# ── OpenRC: start failure must not be reported as success ──── +reset_state openrc +RC_SERVICE_STATUS=1 +setup_telegram_service +assert_eq "openrc start failure returns nonzero" "1" "$?" +assert_warn_contains "openrc start failure warns" "rc-service mtproxymax-telegram status" + +# ── No init system: loud failure, no silent no-op ──────────── +reset_state none +setup_telegram_service +assert_eq "no-init-system setup returns nonzero" "1" "$?" +if [ -f "$TELEGRAM_INIT" ]; then + assert_eq "no-init-system writes no init script" "absent" "present" +else + assert_eq "no-init-system writes no init script" "absent" "absent" +fi +assert_warn_contains "no-init-system warns it is not running" "is NOT running" + +reset_state none +if telegram_service_running; then + assert_eq "status false with no init system" "false" "true" +else + assert_eq "status false with no init system" "false" "false" +fi + +# ── systemd behaviour unchanged, but now path-injectable ───── +reset_state systemd +setup_telegram_service +assert_eq "systemd setup succeeds" "0" "$?" +assert_file_contains "systemd unit written to SYSTEMD_DIR" "MTProxyMax Telegram Bot Service" "$TELEGRAM_UNIT" +assert_file_contains "systemd unit restarts on failure" "Restart=on-failure" "$TELEGRAM_UNIT" +assert_log_contains "systemd enables the unit" "systemctl enable mtproxymax-telegram.service" +assert_log_contains "systemd restarts the unit" "systemctl restart mtproxymax-telegram.service" + +reset_state systemd +setup_telegram_service >/dev/null +: > "$CMD_LOG" +telegram_remove_service +assert_log_contains "systemd remove disables the unit" "systemctl disable mtproxymax-telegram.service" +if [ -f "$TELEGRAM_UNIT" ]; then + assert_eq "systemd remove deletes unit file" "deleted" "kept" +else + assert_eq "systemd remove deletes unit file" "deleted" "deleted" +fi + +# ── Autostart service ──────────────────────────────────────── +reset_state openrc +setup_autostart +assert_eq "autostart openrc succeeds" "0" "$?" +assert_file_contains "autostart script starts the manager" "/usr/local/bin/mtproxymax start" "$MAIN_INIT" +assert_file_contains "autostart script stops the manager" "/usr/local/bin/mtproxymax stop" "$MAIN_INIT" +assert_file_contains "autostart script requires docker" "need docker" "$MAIN_INIT" +assert_log_contains "autostart added to default runlevel" "rc-update add mtproxymax default" +if [ -x "$MAIN_INIT" ]; then + assert_eq "autostart script is executable" "yes" "yes" +else + assert_eq "autostart script is executable" "yes" "no" +fi + +reset_state none +setup_autostart +assert_eq "autostart with no init system returns nonzero" "1" "$?" +if [ -f "$MAIN_INIT" ]; then + assert_eq "autostart writes no init script when unsupported" "absent" "present" +else + assert_eq "autostart writes no init script when unsupported" "absent" "absent" +fi + +reset_state systemd +setup_autostart +assert_eq "autostart systemd succeeds" "0" "$?" +assert_file_contains "autostart unit written to SYSTEMD_DIR" "MTProxyMax Telegram Proxy" "$MAIN_UNIT" +assert_log_contains "autostart unit enabled" "systemctl enable mtproxymax.service" + +reset_state systemd +setup_autostart >/dev/null +: > "$CMD_LOG" +main_service_remove +assert_log_contains "autostart remove disables the unit" "systemctl disable mtproxymax.service" +if [ -f "$MAIN_UNIT" ]; then + assert_eq "autostart remove deletes unit file" "deleted" "kept" +else + assert_eq "autostart remove deletes unit file" "deleted" "deleted" +fi + +printf '\n%d tests, %d failures\n' "$TESTS_RUN" "$TESTS_FAILED" +[ "$TESTS_FAILED" -eq 0 ] From ca87a7c1febebb6815928259a131b870d8b54fe6 Mon Sep 17 00:00:00 2001 From: Ramil Valitov Date: Thu, 10 Sep 2026 21:22:52 +0300 Subject: [PATCH 2/3] fix(telegram): adopt OpenRC unit semantics validated on Alpine MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Reconcile the generated init script with the unit running in production on an Alpine LXC: - `need net docker` rather than `use docker`. The daemon drives the proxy container and is the only scheduler for the periodic tasks (quota/expiry enforcement, sweep, proxy auto-restart), so a hard docker dependency fails loudly instead of starting a bot that can only emit bogus "proxy down" alerts. - Separate error_log (.err) from output_log so stderr does not interleave. - start_pre tests ${command_args} via -f — the daemon is invoked through bash so it need not be executable — and names the exact command to run. Also fixes a generation bug this surfaced: in the unquoted heredoc ${command_args} was expanded at generation time, to empty, so the guard would have been written as `if [ ! -f "" ]`. Now escaped, with a test assertion pinning it. --- mtproxymax.sh | 22 +++++++++++++++------- tests/test_telegram_service_openrc.sh | 7 +++++-- 2 files changed, 20 insertions(+), 9 deletions(-) diff --git a/mtproxymax.sh b/mtproxymax.sh index 44fe19b..a0188cf 100644 --- a/mtproxymax.sh +++ b/mtproxymax.sh @@ -12224,7 +12224,14 @@ SERVICE_EOF ;; openrc) # supervise-daemon keeps the bot alive across crashes, mirroring the - # systemd unit's Restart=on-failure / RestartSec=10. + # systemd unit's Restart=on-failure / RestartSec=10. The daemon runs its + # own foreground poll loop — and is the only scheduler for the periodic + # tasks (quota/expiry enforcement, sweep, proxy auto-restart) — so it can + # be supervised directly instead of backgrounding itself. + # + # `need docker` is deliberate: the daemon drives the proxy container, so + # starting it without docker would only emit bogus "proxy down" alerts. + # `use` is soft — it orders dns/logger only when those services exist. cat > "${INITD_DIR}/mtproxymax-telegram" << OPENRC_EOF #!/sbin/openrc-run # MTProxyMax Telegram Bot Service @@ -12241,17 +12248,18 @@ respawn_max=0 pidfile="/run/\${RC_SVCNAME}.pid" output_log="/var/log/mtproxymax-telegram.log" -error_log="/var/log/mtproxymax-telegram.log" +error_log="/var/log/mtproxymax-telegram.err" depend() { - need net - use docker + need net docker + use dns logger } start_pre() { - if [ ! -x "${INSTALL_DIR}/mtproxymax-telegram.sh" ]; then - eerror "Bot daemon not found at ${INSTALL_DIR}/mtproxymax-telegram.sh" - eerror "Run 'mtproxymax telegram setup' first." + # Generated by 'mtproxymax telegram setup'; absent until that wizard runs. + if [ ! -f "\${command_args}" ]; then + eerror "\${command_args} not found." + eerror "Run: ${INSTALL_DIR}/mtproxymax telegram setup" return 1 fi } diff --git a/tests/test_telegram_service_openrc.sh b/tests/test_telegram_service_openrc.sh index 55f28ed..74b34d3 100644 --- a/tests/test_telegram_service_openrc.sh +++ b/tests/test_telegram_service_openrc.sh @@ -115,10 +115,13 @@ assert_eq "openrc setup succeeds" "0" "$?" assert_file_contains "init script declares supervise-daemon" "supervisor=supervise-daemon" "$TELEGRAM_INIT" assert_file_contains "init script respawns after 10s" "respawn_delay=10" "$TELEGRAM_INIT" assert_file_contains "init script respawns indefinitely" "respawn_max=0" "$TELEGRAM_INIT" -assert_file_contains "init script needs net" "need net" "$TELEGRAM_INIT" -assert_file_contains "init script uses docker softly" "use docker" "$TELEGRAM_INIT" +assert_file_contains "init script needs net and docker" "need net docker" "$TELEGRAM_INIT" assert_file_contains "init script points at generated daemon" \ "command_args=\"${INSTALL_DIR}/mtproxymax-telegram.sh\"" "$TELEGRAM_INIT" +assert_file_contains "init script separates stderr" \ + "error_log=\"/var/log/mtproxymax-telegram.err\"" "$TELEGRAM_INIT" +assert_file_contains "start_pre guards on the generated daemon" \ + "if [ ! -f \"\${command_args}\" ]; then" "$TELEGRAM_INIT" if [ -x "$TELEGRAM_INIT" ]; then assert_eq "init script is executable" "yes" "yes" From 4942e75379f75894352def9e3b33815c92b334a0 Mon Sep 17 00:00:00 2001 From: Ramil Valitov Date: Thu, 10 Sep 2026 22:20:24 +0300 Subject: [PATCH 3/3] fix(telegram): don't report boot-enable success when rc-update failed MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Review on a live Alpine LXC surfaced three issues. 1. `rc-update add ... || true` swallowed failures and then logged success, so a user could be told boot-autostart was enabled when it was not — the same class of silent lie this branch set out to fix. Adds openrc_enable_service(), which verifies the service actually landed in the runlevel instead of trusting rc-update's exit status (which is also ambiguous when the service is already registered). setup_autostart now returns nonzero when it fails, since enabling boot-autostart is the whole point of that function. 2. Two pid files exist with different meanings: /run/.pid belongs to the supervision layer while the bot daemon writes its own PID under INSTALL_DIR. Killing the first stops the supervisor, not the bot. Documented in the generated unit. 3. respawn_max=0 restarts on any exit, whereas systemd's Restart=on-failure does not restart after a clean exit. Kept the behaviour — the daemon only exits on error or a signal — but documented the divergence. Also switches the runlevel check to -e rather than -L so the decision logic stays verifiable on hosts that cannot create symlinks. --- mtproxymax.sh | 34 +++++++++++++++-- tests/test_telegram_service_openrc.sh | 55 +++++++++++++++++++++++++-- 2 files changed, 83 insertions(+), 6 deletions(-) diff --git a/mtproxymax.sh b/mtproxymax.sh index a0188cf..bad095a 100644 --- a/mtproxymax.sh +++ b/mtproxymax.sh @@ -37,6 +37,7 @@ SSL_DIR="${SSL_DIR:-${INSTALL_DIR}/ssl}" CLOUD_BACKUP_FILE="${CLOUD_BACKUP_FILE:-${INSTALL_DIR}/cloud_backup.conf}" SYSTEMD_DIR="${SYSTEMD_DIR:-/etc/systemd/system}" INITD_DIR="${INITD_DIR:-/etc/init.d}" +RUNLEVELS_DIR="${RUNLEVELS_DIR:-/etc/runlevels}" SCANNER_SHIELD_SET="mtp_scanners" CONTAINER_NAME="mtproxymax" DOCKER_IMAGE_BASE="mtproxymax-telemt" @@ -535,6 +536,18 @@ detect_init_system() { fi } +# Register an OpenRC service in a runlevel, returning 0 only if it really landed +# there. `rc-update add` is not trusted on its own: its exit status is swallowed +# and, more importantly, a failure must never be reported to the user as success. +# Tested with -e rather than -L: rc-update creates a symlink to the init script we +# just wrote, so -e is true exactly when the service is genuinely runnable, and it +# stays verifiable on hosts that cannot create symlinks. +openrc_enable_service() { + local svc="$1" runlevel="${2:-default}" + rc-update add "$svc" "$runlevel" 2>/dev/null || true + [ -e "${RUNLEVELS_DIR}/${runlevel}/${svc}" ] +} + # Check dependencies check_dependencies() { local missing=() @@ -12232,6 +12245,11 @@ SERVICE_EOF # `need docker` is deliberate: the daemon drives the proxy container, so # starting it without docker would only emit bogus "proxy down" alerts. # `use` is soft — it orders dns/logger only when those services exist. + # + # respawn_max=0 means "always respawn", which is slightly broader than the + # systemd unit's Restart=on-failure: supervise-daemon restarts even after a + # clean exit. The daemon only exits on error or a signal, so this is the + # behaviour we want, but it is a real difference between the two backends. cat > "${INITD_DIR}/mtproxymax-telegram" << OPENRC_EOF #!/sbin/openrc-run # MTProxyMax Telegram Bot Service @@ -12246,6 +12264,10 @@ command_args="${INSTALL_DIR}/mtproxymax-telegram.sh" respawn_delay=10 respawn_max=0 +# NOTE: two pid files exist and they are NOT interchangeable. This one belongs to +# the supervision layer; the bot daemon separately writes its own PID to +# ${INSTALL_DIR}/mtproxymax-telegram.pid. Killing the PID in this file stops the +# supervisor, not the bot. pidfile="/run/\${RC_SVCNAME}.pid" output_log="/var/log/mtproxymax-telegram.log" error_log="/var/log/mtproxymax-telegram.err" @@ -12266,7 +12288,9 @@ start_pre() { OPENRC_EOF chmod +x "${INITD_DIR}/mtproxymax-telegram" - rc-update add mtproxymax-telegram default 2>/dev/null || true + if ! openrc_enable_service mtproxymax-telegram default; then + log_warn "Could not enable the bot service for boot — run: rc-update add mtproxymax-telegram default" + fi if rc-service mtproxymax-telegram restart 2>/dev/null; then log_success "Telegram bot service started (OpenRC)" else @@ -13437,8 +13461,12 @@ status() { OPENRC_EOF chmod +x "${INITD_DIR}/mtproxymax" - rc-update add mtproxymax default 2>/dev/null || true - log_success "Auto-start enabled (OpenRC)" + if openrc_enable_service mtproxymax default; then + log_success "Auto-start enabled (OpenRC)" + else + log_warn "Could not enable auto-start — run: rc-update add mtproxymax default" + return 1 + fi ;; *) log_warn "No supported init system found (systemd or OpenRC)." diff --git a/tests/test_telegram_service_openrc.sh b/tests/test_telegram_service_openrc.sh index 74b34d3..c3ce0ae 100644 --- a/tests/test_telegram_service_openrc.sh +++ b/tests/test_telegram_service_openrc.sh @@ -16,7 +16,8 @@ INSTALL_DIR="$TEST_TMPDIR/install" SETTINGS_FILE="$INSTALL_DIR/settings.conf" INITD_DIR="$TEST_TMPDIR/etc/init.d" SYSTEMD_DIR="$TEST_TMPDIR/etc/systemd/system" -mkdir -p "$INSTALL_DIR" "$INITD_DIR" "$SYSTEMD_DIR" +RUNLEVELS_DIR="$TEST_TMPDIR/etc/runlevels" +mkdir -p "$INSTALL_DIR" "$INITD_DIR" "$SYSTEMD_DIR" "$RUNLEVELS_DIR" MTPROXYMAX_SOURCE_ONLY=true source "$(dirname "${BASH_SOURCE[0]}")/../mtproxymax.sh" set +e @@ -27,11 +28,13 @@ TESTS_FAILED=0 CMD_LOG="$TEST_TMPDIR/cmd.log" WARN_LOG="$TEST_TMPDIR/warn.log" +SUCCESS_LOG="$TEST_TMPDIR/success.log" : > "$CMD_LOG" : > "$WARN_LOG" +: > "$SUCCESS_LOG" check_root() { :; } -log_success() { :; } +log_success() { echo "$*" >> "$SUCCESS_LOG"; } log_info() { :; } log_warn() { echo "$*" >> "$WARN_LOG"; } log_error() { echo "$*" >> "$WARN_LOG"; } @@ -39,8 +42,18 @@ log_error() { echo "$*" >> "$WARN_LOG"; } # Shadow the init-system binaries so nothing touches the host. RC_SERVICE_STATUS=0 SYSTEMCTL_STATUS=0 +# When 0, `rc-update add` silently fails to create the runlevel symlink, which is +# how a genuine boot-enable failure presents. Success must not be reported then. +RC_UPDATE_LINKS=1 systemctl() { echo "systemctl $*" >> "$CMD_LOG"; return "$SYSTEMCTL_STATUS"; } -rc-update() { echo "rc-update $*" >> "$CMD_LOG"; return 0; } +rc-update() { + echo "rc-update $*" >> "$CMD_LOG" + if [ "$1" = "add" ] && [ "$RC_UPDATE_LINKS" = "1" ]; then + mkdir -p "$RUNLEVELS_DIR/$3" + ln -sfn "$INITD_DIR/$2" "$RUNLEVELS_DIR/$3/$2" + fi + return 0 +} rc-service() { echo "rc-service $*" >> "$CMD_LOG"; return "$RC_SERVICE_STATUS"; } FAKE_INIT="openrc" @@ -90,14 +103,28 @@ assert_warn_contains() { fi } +assert_success_contains() { + local name="$1" needle="$2" + TESTS_RUN=$((TESTS_RUN + 1)) + if grep -qF -- "$needle" "$SUCCESS_LOG" 2>/dev/null; then + printf ' PASS %s\n' "$name" + else + TESTS_FAILED=$((TESTS_FAILED + 1)) + printf ' FAIL %s (missing %q in success log)\n' "$name" "$needle" + fi +} + reset_state() { FAKE_INIT="${1:-openrc}" RC_SERVICE_STATUS=0 SYSTEMCTL_STATUS=0 + RC_UPDATE_LINKS=1 : > "$CMD_LOG" : > "$WARN_LOG" + : > "$SUCCESS_LOG" rm -f "$INITD_DIR/mtproxymax" "$INITD_DIR/mtproxymax-telegram" rm -f "$SYSTEMD_DIR/mtproxymax.service" "$SYSTEMD_DIR/mtproxymax-telegram.service" + rm -rf "$RUNLEVELS_DIR" } TELEGRAM_INIT="$INITD_DIR/mtproxymax-telegram" @@ -187,6 +214,22 @@ setup_telegram_service assert_eq "openrc start failure returns nonzero" "1" "$?" assert_warn_contains "openrc start failure warns" "rc-service mtproxymax-telegram status" +# ── OpenRC: boot-enable failure must not be reported as success ── +reset_state openrc +setup_telegram_service +assert_success_contains "setup reports the bot as started" "Telegram bot service started (OpenRC)" +if [ -e "$RUNLEVELS_DIR/default/mtproxymax-telegram" ]; then + assert_eq "boot-enable registers the service in the runlevel" "yes" "yes" +else + assert_eq "boot-enable registers the service in the runlevel" "yes" "no" +fi + +reset_state openrc +RC_UPDATE_LINKS=0 +setup_telegram_service +assert_eq "boot-enable failure still starts the bot" "0" "$?" +assert_warn_contains "boot-enable failure warns" "Could not enable the bot service for boot" + # ── No init system: loud failure, no silent no-op ──────────── reset_state none setup_telegram_service @@ -239,6 +282,12 @@ else assert_eq "autostart script is executable" "yes" "no" fi +reset_state openrc +RC_UPDATE_LINKS=0 +setup_autostart +assert_eq "autostart boot-enable failure returns nonzero" "1" "$?" +assert_warn_contains "autostart boot-enable failure warns" "Could not enable auto-start" + reset_state none setup_autostart assert_eq "autostart with no init system returns nonzero" "1" "$?"