diff --git a/docs/TEMPERATURE_MONITORING.md b/docs/TEMPERATURE_MONITORING.md index 76a793a3a..56e9650cd 100644 --- a/docs/TEMPERATURE_MONITORING.md +++ b/docs/TEMPERATURE_MONITORING.md @@ -77,29 +77,18 @@ ssh -i /path/to/key root@node "cat /sys/class/thermal/thermal_zone0/temp" ## Legacy Cleanup (If Upgrading) -If you still have the old sensor proxy installed from prior releases, remove it from each **Proxmox host** (not the Pulse container): +If you still have the old sensor proxy installed from prior releases, remove it from each **Proxmox host** (not the Pulse container) with the supported cleanup helper: ```bash -# Stop and disable all sensor-proxy systemd units -sudo systemctl disable --now pulse-sensor-proxy pulse-sensor-proxy-selfheal.timer pulse-sensor-proxy-selfheal.service pulse-sensor-cleanup.path pulse-sensor-cleanup.service 2>/dev/null +curl -fsSL https://raw.githubusercontent.com/rcourtman/Pulse/main/scripts/uninstall-sensor-proxy.sh | \ + sudo bash -s -- --uninstall --purge +``` -# Remove systemd unit files -sudo rm -f /etc/systemd/system/pulse-sensor-proxy.service -sudo rm -f /etc/systemd/system/pulse-sensor-proxy-selfheal.timer -sudo rm -f /etc/systemd/system/pulse-sensor-proxy-selfheal.service -sudo rm -f /etc/systemd/system/pulse-sensor-cleanup.service -sudo rm -f /etc/systemd/system/pulse-sensor-cleanup.path -sudo systemctl daemon-reload +If you also want to remove the `pulse-monitor@pam` API user/tokens for a full clean slate before re-adding the node, include `--remove-proxmox-access`: -# Remove sensor-proxy files -sudo rm -rf /opt/pulse/sensor-proxy -sudo rm -rf /etc/pulse-sensor-proxy -sudo rm -rf /var/lib/pulse-sensor-proxy -sudo rm -rf /var/log/pulse/sensor-proxy -sudo rm -rf /run/pulse-sensor-proxy - -# Optional: remove sensor-proxy SSH keys from authorized_keys -sudo sed -i '/# pulse-managed-key$/d;/# pulse-proxy-key$/d' /root/.ssh/authorized_keys +```bash +curl -fsSL https://raw.githubusercontent.com/rcourtman/Pulse/main/scripts/uninstall-sensor-proxy.sh | \ + sudo bash -s -- --uninstall --purge --remove-proxmox-access ``` Reinstalling or upgrading the Pulse container does **not** remove the sensor proxy from the host — they are separate installations. If you skip this cleanup, the selfheal timer will keep running and may generate recurring `TASK ERROR` entries in the Proxmox task log. diff --git a/docs/UPGRADE_v5.md b/docs/UPGRADE_v5.md index a3f89d4b4..a46bd9984 100644 --- a/docs/UPGRADE_v5.md +++ b/docs/UPGRADE_v5.md @@ -58,7 +58,16 @@ If you reset auth (for example by deleting `.env`), Pulse may require a bootstra ### Sensor proxy removal -The `pulse-sensor-proxy` from v4 is no longer needed — temperature monitoring is now handled by the unified agent. If you had the sensor proxy installed on your Proxmox hosts, remove it **on each host** after upgrading. See the [Legacy Cleanup](TEMPERATURE_MONITORING.md#legacy-cleanup-if-upgrading) section in the temperature monitoring docs for the full cleanup commands. +The `pulse-sensor-proxy` from v4 is no longer needed — temperature monitoring is now handled by the unified agent. If you had the sensor proxy installed on your Proxmox hosts, remove it **on each host** after upgrading: + +```bash +curl -fsSL https://raw.githubusercontent.com/rcourtman/Pulse/main/scripts/uninstall-sensor-proxy.sh | \ + sudo bash -s -- --uninstall --purge +``` + +If you deleted the old node from Pulse and want the cleanup to also remove the old `pulse-monitor@pam` API user/tokens before reinstalling, add `--remove-proxmox-access`. + +See the [Legacy Cleanup](TEMPERATURE_MONITORING.md#legacy-cleanup-if-upgrading) section in the temperature monitoring docs for the full cleanup details. Skipping this step will leave a selfheal timer running on the host that generates recurring `TASK ERROR` entries in the Proxmox task log. diff --git a/scripts/tests/test-uninstall-sensor-proxy.sh b/scripts/tests/test-uninstall-sensor-proxy.sh new file mode 100755 index 000000000..bccf35402 --- /dev/null +++ b/scripts/tests/test-uninstall-sensor-proxy.sh @@ -0,0 +1,105 @@ +#!/usr/bin/env bash +# +# Smoke tests for scripts/uninstall-sensor-proxy.sh helper behavior. + +set -euo pipefail + +ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)" +UNINSTALL_SCRIPT="${ROOT_DIR}/scripts/uninstall-sensor-proxy.sh" + +if [[ ! -f "${UNINSTALL_SCRIPT}" ]]; then + echo "uninstall-sensor-proxy.sh not found at ${UNINSTALL_SCRIPT}" >&2 + exit 1 +fi + +# shellcheck disable=SC1090 +source "${UNINSTALL_SCRIPT}" + +failures=0 + +assert_success() { + local desc="$1" + shift + if "$@"; then + echo "[PASS] ${desc}" + return 0 + else + echo "[FAIL] ${desc}" >&2 + ((failures++)) + return 1 + fi +} + +test_remove_managed_keys_from_authorized_keys_file_resolves_symlink_target() { + local tmpdir + tmpdir="$(mktemp -d)" + local real_auth="${tmpdir}/real_authorized_keys" + local linked_auth="${tmpdir}/authorized_keys" + + cat > "${real_auth}" <<'EOF' +ssh-ed25519 AAAAkeep keep +command="sensors -j" ssh-ed25519 AAAAmanaged # pulse-managed-key +command="sensors -j" ssh-ed25519 AAAAproxy # pulse-proxy-key +EOF + ln -s "${real_auth}" "${linked_auth}" + + remove_managed_keys_from_authorized_keys_file "$(resolve_path "${linked_auth}")" + + if [[ "$(cat "${real_auth}")" != "ssh-ed25519 AAAAkeep keep" ]]; then + rm -rf "${tmpdir}" + return 1 + fi + + rm -rf "${tmpdir}" +} + +test_cleanup_sensor_proxy_lines_in_conf_preserves_snapshot_sections() { + local tmpdir + tmpdir="$(mktemp -d)" + local conf="${tmpdir}/101.conf" + + cat > "${conf}" <<'EOF' +arch: amd64 +mp0: /run/pulse-sensor-proxy,mp=/mnt/pulse-proxy +lxc.mount.entry: /run/pulse-sensor-proxy mnt/pulse-proxy none bind,create=dir 0 0 +rootfs: local-lvm:vm-101-disk-0,size=8G +[snapshot] +mp0: /run/pulse-sensor-proxy,mp=/mnt/pulse-proxy +lxc.mount.entry: /run/pulse-sensor-proxy mnt/pulse-proxy none bind,create=dir 0 0 +EOF + + cleanup_sensor_proxy_lines_in_conf "${conf}" "$(main_section_snapshot_line "${conf}")" + + if awk 'BEGIN{snapshot=0} /^\[snapshot\]$/{snapshot=1} !snapshot {print}' "${conf}" | grep -q '^mp0: /run/pulse-sensor-proxy'; then + rm -rf "${tmpdir}" + return 1 + fi + if awk 'BEGIN{snapshot=0} /^\[snapshot\]$/{snapshot=1} !snapshot {print}' "${conf}" | grep -q '^lxc\.mount\.entry: /run/pulse-sensor-proxy'; then + rm -rf "${tmpdir}" + return 1 + fi + if ! awk 'f{print} /^\[snapshot\]$/{f=1}' "${conf}" | grep -q '^mp0: /run/pulse-sensor-proxy'; then + rm -rf "${tmpdir}" + return 1 + fi + if ! awk 'f{print} /^\[snapshot\]$/{f=1}' "${conf}" | grep -q '^lxc\.mount\.entry: /run/pulse-sensor-proxy'; then + rm -rf "${tmpdir}" + return 1 + fi + + rm -rf "${tmpdir}" +} + +main() { + assert_success "managed SSH key cleanup follows symlink targets" test_remove_managed_keys_from_authorized_keys_file_resolves_symlink_target + assert_success "config cleanup keeps snapshot-only sensor-proxy entries intact" test_cleanup_sensor_proxy_lines_in_conf_preserves_snapshot_sections + + if (( failures > 0 )); then + echo "Total failures: ${failures}" >&2 + return 1 + fi + + echo "All uninstall-sensor-proxy smoke tests passed." +} + +main "$@" diff --git a/scripts/uninstall-sensor-proxy.sh b/scripts/uninstall-sensor-proxy.sh new file mode 100755 index 000000000..335264528 --- /dev/null +++ b/scripts/uninstall-sensor-proxy.sh @@ -0,0 +1,494 @@ +#!/usr/bin/env bash +# +# Removes the legacy v4 pulse-sensor-proxy footprint from a Proxmox host. +# Safe to run before migrating to the v5 unified agent. + +set -euo pipefail + +BINARY_PATH="${PULSE_SENSOR_PROXY_BINARY_PATH:-/usr/local/bin/pulse-sensor-proxy}" +INSTALL_ROOT="${PULSE_SENSOR_PROXY_INSTALL_ROOT:-/opt/pulse/sensor-proxy}" +SERVICE_PATH="${PULSE_SENSOR_PROXY_SERVICE_PATH:-/etc/systemd/system/pulse-sensor-proxy.service}" +RUNTIME_DIR="${PULSE_SENSOR_PROXY_RUNTIME_DIR:-/run/pulse-sensor-proxy}" +SOCKET_PATH="${PULSE_SENSOR_PROXY_SOCKET_PATH:-${RUNTIME_DIR}/pulse-sensor-proxy.sock}" +WORK_DIR="${PULSE_SENSOR_PROXY_WORK_DIR:-/var/lib/pulse-sensor-proxy}" +CONFIG_DIR="${PULSE_SENSOR_PROXY_CONFIG_DIR:-/etc/pulse-sensor-proxy}" +LOG_DIR="${PULSE_SENSOR_PROXY_LOG_DIR:-/var/log/pulse/sensor-proxy}" +SERVICE_USER="${PULSE_SENSOR_PROXY_SERVICE_USER:-pulse-sensor-proxy}" +AUTHORIZED_KEYS_PATH="${PULSE_SENSOR_PROXY_AUTHORIZED_KEYS_PATH:-/root/.ssh/authorized_keys}" + +QUIET=false +PURGE=false +REMOVE_PROXMOX_ACCESS=false + +print_info() { + if [[ "${QUIET}" != "true" ]]; then + printf '[INFO] %s\n' "$1" + fi +} + +print_warn() { + printf '[WARN] %s\n' "$1" >&2 +} + +print_success() { + printf '[ OK ] %s\n' "$1" +} + +usage() { + cat <<'EOF' +Usage: uninstall-sensor-proxy.sh [options] + +Removes the legacy pulse-sensor-proxy footprint from a Proxmox host. + +Options: + --uninstall Accepted for compatibility with the old installer. + --purge Remove persisted proxy state, config, logs, and service user/group. + --remove-proxmox-access Remove pulse-monitor@pam API tokens/user after cleanup. + --quiet Reduce informational output. + --help Show this help text. +EOF +} + +resolve_path() { + local path="$1" + + if command -v readlink >/dev/null 2>&1; then + local resolved + resolved=$(readlink -f "$path" 2>/dev/null || true) + if [[ -n "$resolved" ]]; then + printf '%s\n' "$resolved" + return 0 + fi + fi + + if command -v python3 >/dev/null 2>&1; then + python3 - "$path" <<'PY' +import os +import sys + +print(os.path.realpath(sys.argv[1])) +PY + return 0 + fi + + printf '%s\n' "$path" +} + +remove_managed_keys_from_authorized_keys_file() { + local file="$1" + + if [[ ! -f "$file" ]]; then + return 0 + fi + + local tmp_file + tmp_file=$(mktemp) + grep -v -E '# pulse-(managed|proxy)-key$' "$file" >"$tmp_file" 2>/dev/null || true + + if cmp -s "$tmp_file" "$file"; then + rm -f "$tmp_file" + return 0 + fi + + chmod --reference="$file" "$tmp_file" 2>/dev/null || chmod 600 "$tmp_file" 2>/dev/null || true + chown --reference="$file" "$tmp_file" 2>/dev/null || true + mv "$tmp_file" "$file" +} + +cleanup_local_authorized_keys() { + local auth_file + auth_file=$(resolve_path "$AUTHORIZED_KEYS_PATH") + remove_managed_keys_from_authorized_keys_file "$auth_file" + print_success "Removed legacy Pulse SSH key entries from ${auth_file}" +} + +is_local_host() { + local target="$1" + local local_ip="" + local local_ips="" + local local_hostnames="" + + local_ips="$(hostname -I 2>/dev/null || true)" + for local_ip in $local_ips; do + if [[ "$target" == "$local_ip" ]]; then + return 0 + fi + done + + local_hostnames="$(hostname 2>/dev/null || true) $(hostname -f 2>/dev/null || true)" + case " ${local_hostnames} " in + *" ${target} "*) return 0 ;; + esac + + [[ "$target" == "127.0.0.1" || "$target" == "localhost" ]] +} + +discover_cluster_node_addresses() { + local resolved_any=false + local node="" + local node_ip="" + + if command -v pvesh >/dev/null 2>&1 && command -v python3 >/dev/null 2>&1; then + while IFS= read -r node; do + [[ -z "$node" ]] && continue + node_ip=$(getent ahostsv4 "$node" 2>/dev/null | awk 'NR==1 {print $1}') || true + if [[ -z "$node_ip" ]]; then + node_ip=$(getent hosts "$node" 2>/dev/null | awk 'NR==1 {print $1}') || true + fi + if [[ -n "$node_ip" ]]; then + printf '%s\n' "$node_ip" + resolved_any=true + fi + done < <( + pvesh get /nodes --output-format json 2>/dev/null | python3 - <<'PY' +import json +import sys + +try: + data = json.load(sys.stdin) +except Exception: + sys.exit(0) + +if isinstance(data, list): + for item in data: + node = item.get("node") + if node: + print(node) +PY + ) + fi + + if [[ "$resolved_any" == "true" ]]; then + return 0 + fi + + if command -v pvecm >/dev/null 2>&1; then + pvecm status 2>/dev/null | awk ' + /0x[0-9a-f]+/ { + for (i = 1; i <= NF; i++) { + if ($i ~ /^[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+$/) { + print $i + } + } + } + ' + fi +} + +cleanup_remote_authorized_keys() { + local host="$1" + local remote_cmd='set -eu +auth="/root/.ssh/authorized_keys" +if command -v readlink >/dev/null 2>&1; then + resolved=$(readlink -f "$auth" 2>/dev/null || printf "%s" "$auth") +else + resolved="$auth" +fi +if [[ -f "$resolved" ]]; then + sed -i -e "/# pulse-managed-key$/d" -e "/# pulse-proxy-key$/d" "$resolved" +fi' + + if ssh -o StrictHostKeyChecking=no -o BatchMode=yes -o ConnectTimeout=5 root@"$host" "$remote_cmd" >/dev/null 2>&1; then + print_success "Removed legacy Pulse SSH key entries from ${host}" + return 0 + fi + + print_warn "Unable to remove legacy Pulse SSH key entries from ${host}; clean up /root/.ssh/authorized_keys manually if needed" + return 1 +} + +cleanup_cluster_authorized_keys() { + local host="" + local any_nodes=false + local status=0 + + while IFS= read -r host; do + [[ -z "$host" ]] && continue + any_nodes=true + if is_local_host "$host"; then + cleanup_local_authorized_keys + continue + fi + cleanup_remote_authorized_keys "$host" || status=1 + done < <(discover_cluster_node_addresses | sort -u) + + if [[ "$any_nodes" != "true" ]]; then + cleanup_local_authorized_keys + fi + + return "$status" +} + +systemctl_if_available() { + if ! command -v systemctl >/dev/null 2>&1; then + return 0 + fi + + systemctl "$@" >/dev/null 2>&1 || true +} + +disable_legacy_units() { + local unit="" + local -a units=( + pulse-sensor-proxy.service + pulse-sensor-proxy-selfheal.timer + pulse-sensor-proxy-selfheal.service + pulse-sensor-cleanup.path + pulse-sensor-cleanup.service + ) + + for unit in "${units[@]}"; do + systemctl_if_available stop "$unit" + systemctl_if_available disable "$unit" + done + + systemctl_if_available daemon-reload +} + +main_section_snapshot_line() { + local conf="$1" + grep -n '^\[' "$conf" 2>/dev/null | head -1 | cut -d: -f1 +} + +cleanup_sensor_proxy_lines_in_conf() { + local conf="$1" + local snapshot_line="${2:-}" + local tmp_file + tmp_file=$(mktemp) + + if [[ -n "$snapshot_line" ]] && [[ "$snapshot_line" -gt 1 ]]; then + awk -v snapshot_line="$snapshot_line" ' + NR < snapshot_line && ($0 ~ /^mp[0-9]+:.*pulse-sensor-proxy/ || $0 ~ /^lxc\.mount\.entry:.*pulse-sensor-proxy/) { next } + { print } + ' "$conf" >"$tmp_file" + else + awk ' + $0 ~ /^mp[0-9]+:.*pulse-sensor-proxy/ { next } + $0 ~ /^lxc\.mount\.entry:.*pulse-sensor-proxy/ { next } + { print } + ' "$conf" >"$tmp_file" + fi + + chmod --reference="$conf" "$tmp_file" 2>/dev/null || true + chown --reference="$conf" "$tmp_file" 2>/dev/null || true + mv "$tmp_file" "$conf" +} + +cleanup_stale_sensor_proxy_mounts() { + if ! command -v pct >/dev/null 2>&1; then + return 0 + fi + + local ctid="" + local conf="" + local snapshot_line="" + local status="" + local was_running=false + local mp_keys="" + local mp_key="" + local cleaned=0 + + while IFS= read -r ctid; do + [[ -z "$ctid" ]] && continue + conf="/etc/pve/lxc/${ctid}.conf" + [[ -f "$conf" ]] || continue + + if ! grep -q 'pulse-sensor-proxy' "$conf" 2>/dev/null; then + continue + fi + + print_info "Cleaning stale pulse-sensor-proxy mount entries from container ${ctid}" + + status=$(pct status "$ctid" 2>/dev/null | awk '{print $2}') || true + was_running=false + [[ "$status" == "running" ]] && was_running=true + + if [[ "$was_running" == "true" ]]; then + timeout 30 pct stop "$ctid" >/dev/null 2>&1 || true + sleep 2 + fi + + snapshot_line=$(main_section_snapshot_line "$conf") + if [[ -n "$snapshot_line" ]] && [[ "$snapshot_line" -gt 1 ]]; then + mp_keys=$(head -n "$((snapshot_line - 1))" "$conf" 2>/dev/null | grep -E '^mp[0-9]+:.*pulse-sensor-proxy' | sed 's/:.*//') || true + else + mp_keys=$(grep -E '^mp[0-9]+:.*pulse-sensor-proxy' "$conf" 2>/dev/null | sed 's/:.*//') || true + fi + + if [[ -n "$mp_keys" ]]; then + while IFS= read -r mp_key; do + [[ -z "$mp_key" ]] && continue + if ! timeout 15 pct set "$ctid" -delete "$mp_key" >/dev/null 2>&1; then + cleanup_sensor_proxy_lines_in_conf "$conf" "$snapshot_line" + fi + cleaned=$((cleaned + 1)) + done <<<"$mp_keys" + fi + + cleanup_sensor_proxy_lines_in_conf "$conf" "$snapshot_line" + + if [[ "$was_running" == "true" ]]; then + if ! timeout 30 pct start "$ctid" >/dev/null 2>&1; then + print_warn "Container ${ctid} was running before cleanup but could not be restarted automatically" + fi + fi + done < <(pct list 2>/dev/null | tail -n +2 | awk '{print $1}') + + if (( cleaned > 0 )); then + print_success "Removed legacy pulse-sensor-proxy mount entries from Proxmox LXC config(s)" + fi +} + +remove_path_if_present() { + local path="$1" + if [[ -e "$path" || -L "$path" ]]; then + rm -rf "$path" + print_success "Removed ${path}" + fi +} + +remove_legacy_files() { + local -a files=( + "$BINARY_PATH" + /usr/local/bin/pulse-sensor-cleanup.sh + "${INSTALL_ROOT}/bin/pulse-sensor-proxy" + "${INSTALL_ROOT}/bin/pulse-sensor-cleanup.sh" + "${INSTALL_ROOT}/bin/pulse-sensor-proxy-selfheal.sh" + "${INSTALL_ROOT}/bin/pulse-sensor-wrapper.sh" + "${INSTALL_ROOT}/install-sensor-proxy.sh" + "$SERVICE_PATH" + /etc/systemd/system/pulse-sensor-proxy-selfheal.service + /etc/systemd/system/pulse-sensor-proxy-selfheal.timer + /etc/systemd/system/pulse-sensor-cleanup.service + /etc/systemd/system/pulse-sensor-cleanup.path + "$SOCKET_PATH" + ) + local path="" + + for path in "${files[@]}"; do + remove_path_if_present "$path" + done + + remove_path_if_present "$RUNTIME_DIR" + + if [[ "$PURGE" == "true" ]]; then + remove_path_if_present "$INSTALL_ROOT" + remove_path_if_present "$WORK_DIR" + remove_path_if_present "$CONFIG_DIR" + remove_path_if_present "$LOG_DIR" + + if id -u "$SERVICE_USER" >/dev/null 2>&1; then + userdel --remove "$SERVICE_USER" >/dev/null 2>&1 || userdel "$SERVICE_USER" >/dev/null 2>&1 || print_warn "Failed to remove service user ${SERVICE_USER}" + fi + if getent group "$SERVICE_USER" >/dev/null 2>&1; then + groupdel "$SERVICE_USER" >/dev/null 2>&1 || print_warn "Failed to remove service group ${SERVICE_USER}" + fi + else + print_info "Preserving ${WORK_DIR}, ${CONFIG_DIR}, and ${LOG_DIR} (use --purge to remove them)" + fi +} + +remove_proxmox_access() { + if [[ "$REMOVE_PROXMOX_ACCESS" != "true" ]]; then + return 0 + fi + + if ! command -v pveum >/dev/null 2>&1; then + print_warn "pveum is not available; skipping pulse-monitor@pam cleanup" + return 0 + fi + + local token_ids="" + local token_id="" + + if command -v python3 >/dev/null 2>&1; then + token_ids=$(pveum user token list pulse-monitor@pam --output-format json 2>/dev/null | python3 - <<'PY' +import json +import sys + +try: + data = json.load(sys.stdin) +except Exception: + sys.exit(0) + +if isinstance(data, list): + for item in data: + token_id = item.get("tokenid") + if token_id: + print(token_id) +PY + ) || true + fi + + if [[ -z "$token_ids" ]]; then + token_ids=$(pveum user token list pulse-monitor@pam 2>/dev/null | awk ' +{ + line=$0 + gsub(/\r/, "", line) + gsub(/│/, "|", line) + if (index(line, "|") == 0) { + next + } + n = split(line, parts, "|") + if (n < 3) { + next + } + name = parts[2] + gsub(/^[[:space:]]+|[[:space:]]+$/, "", name) + if (name != "" && name != "tokenid") { + print name + } +}') + fi + + while IFS= read -r token_id; do + [[ -z "$token_id" ]] && continue + pveum user token remove pulse-monitor@pam "$token_id" >/dev/null 2>&1 || print_warn "Failed to remove pulse-monitor@pam token ${token_id}" + done <<<"$token_ids" + + pveum user delete pulse-monitor@pam >/dev/null 2>&1 || print_warn "pulse-monitor@pam was not removed; it may already be absent or still in use" +} + +main() { + while [[ $# -gt 0 ]]; do + case "$1" in + --uninstall) + shift + ;; + --purge) + PURGE=true + shift + ;; + --remove-proxmox-access) + REMOVE_PROXMOX_ACCESS=true + shift + ;; + --quiet) + QUIET=true + shift + ;; + --help|-h) + usage + exit 0 + ;; + *) + printf 'Unknown option: %s\n' "$1" >&2 + usage >&2 + exit 1 + ;; + esac + done + + print_info "Starting legacy pulse-sensor-proxy cleanup" + disable_legacy_units + cleanup_cluster_authorized_keys || true + cleanup_stale_sensor_proxy_mounts + remove_legacy_files + remove_proxmox_access + systemctl_if_available daemon-reload + print_success "Legacy pulse-sensor-proxy cleanup complete" +} + +if [[ "${BASH_SOURCE[0]}" == "$0" ]]; then + main "$@" +fi