]> git.99rst.org Git - openwrt-packages.git/commitdiff
nut: prevent service thrashing on boot or hotplug
authorDaniel F. Dickinson <redacted>
Thu, 27 Aug 2026 10:41:50 +0000 (06:41 -0400)
committerJosef Schlehofer <redacted>
Thu, 10 Sep 2026 08:22:36 +0000 (10:22 +0200)
On boot, hotplug events were causing excessive start and stop action
for the upsd daemon and driver daemons. We fix that with two primary
actions:
  1. Don't restart service daemons on hotplug until after first boot
     has completed.
  2. Use more robust handling of procd instance starts by ensuring
     that the first start starts the nut-server service and all
     others add to the nut-server service (rather than replacing it).

Ignore hotplug events without a DEVNAME.

In addition clean up some logging.

In the process, this fixes #30375 "hotplugging for setting usb access
right[s] doesn't work anymore"

Closes: #30375
Signed-off-by: Daniel F. Dickinson <redacted>
net/nut/Makefile
net/nut/files/libhid-ups.hotplug
net/nut/files/nut-serial.hotplug
net/nut/files/nut-server.init
net/nut/files/nut-service.sh.functions

index 8d294974b6d680d2371f5a96ad07d3dedc22ed5c..e1bf9bcda73a47e4e6740f6b129631b9e958f88b 100644 (file)
@@ -9,7 +9,7 @@ include $(TOPDIR)/rules.mk
 
 PKG_NAME:=nut
 PKG_VERSION:=2.8.5
-PKG_RELEASE:=5
+PKG_RELEASE:=6
 
 PKG_SOURCE:=$(PKG_NAME)-$(PKG_VERSION).tar.gz
 PKG_SOURCE_URL:=https://www.networkupstools.org/source/2.8/
index dc80df23007ceba419d3d3ede7ab25cd44778d21..e3d3546914088637fc57dd48ae2435234fc5a8bb 100644 (file)
@@ -202,7 +202,8 @@ nut_driver_config() {
                        return 0
                fi
                if [ "$(printf "%04x" 0x"$pvendid")" = "$cfg_vendorid" ] &&
-                       [ "$(printf "%04x" 0x"$pprodid")" = "$cfg_productid" ]; then
+                       [ "$(printf "%04x" 0x"$pprodid")" = "$cfg_productid" ] &&
+                       [ -n "$DEVNAME" ] && [ -n "$ups" ]; then
                        # Round 1: If we have a match for hotplug event and the UCI config
                        # start this UPS and record that fact, otherwise skip this UCI UPS
                        # for this round
index d7e300525c98943d34edb6e9f2276a8183570fff..c8e0f368d034d5e9cc18f591505312bfa9796d59 100644 (file)
@@ -162,8 +162,12 @@ nut_on_hotplug_add() {
 # Do not do any hotplug actions if hotplug is disabled. Not an error but log
 # for information
 if ! allow_hotplug_restart; then
-       # Informational and not an error
-       log_msg "Did not set permissions on serial port hotplug as NUT hotplug is disabled" nut-serial nut-serial-hotplug notice
+       # Don't spam logs on initial boot
+       # (before NUT_HOTPLUG_BOOT_COMPLETE_PATH exists)
+       if [ -f "$NUT_HOTPLUG_BOOT_COMPLETE_PATH" ]; then
+               # Informational and not an error
+               log_msg "Did not set permissions on serial port hotplug as NUT hotplug is disabled" nut-serial nut-serial-hotplug notice
+       fi
        exit 0
 fi
 
index 7293627f46dbd7a14826289baceb83f3035af339..b33e8f51c87118f62ecbd084ca47c14f7060b71e 100644 (file)
 START=70
 STOP=30
 
-USE_PROCD=1
+# We use the script functions and variables definitions from
+# /etc/rc.common that are added by USE_PROCD except with a modified
+# start and rc_procd. We cannot simply override USE_PROCD's
+# implementation of start and rc_procd as rc.common sources this
+# initscript before the definitions are applied by USE_PROCD
+#
+# The modified rc_procd uses "set" for procd_close_service, which
+# registers the service with procd, if the service for this
+# initscript (nut-server) is not registered with procd (active) at all,
+# otherwise it uses "add" which adds an instance to the registered
+# (active) service.
+#
+# USE_PROCD=1
 
 # IPKG_INSTROOT is intentionally only set when building an image and
 # is intentionally empty on a live OpenWrt device
@@ -73,15 +85,91 @@ USE_PROCD=1
        exit 1
 }
 
+# shellcheck source=/dev/null
+. "${IPKG_INSTROOT}"/lib/functions/procd.sh || {
+       log_source_error "procd.sh" "nut-server" "nut-server"
+       exit 1
+}
+
 restore_umask_on_exit
 
+extra_command "running" "Check if service is running"
+extra_command "status" "Service status"
+extra_command "trace" "Start with syscall trace"
+extra_command "info" "Dump procd service info"
+
+basescript=$(readlink "$initscript")
+
+has_running_driver() {
+       local instances instance
+
+       instances="$(list_running_instances "nut-server")"
+       [ -n "$instances" ] || return 0
+       set -f
+       for instance in $instances; do
+               if [ "$instance" = "upsd" ]; then
+                       have_upsd_instance="true"
+                       continue
+               fi
+               have_driver_instance="true"
+               break
+       done
+       set +f
+}
+
+start_server_service() {
+       procd_open_instance upsd
+       procd_set_param respawn
+       procd_set_param stderr 1
+       procd_set_param stdout 1
+       procd_set_param env NUT_DEBUG_SYSLOG="stderr"
+       procd_set_param env NUT_QUIET_INIT_UPSNOTIFY=true
+       procd_set_param env NUT_STATEPATH="$STATEPATH"
+       procd_set_param command /usr/sbin/upsd
+       procd_append_param command -FF
+       procd_append_param command -u "$RUNAS"
+       procd_close_instance
+}
+
+# Start upsd instance
+start_server_instance() {
+       if [ -f "$NUT_KILLPOWER" ]; then
+               return 1
+       fi
+
+       local have_driver_instance="false"
+       local have_upsd_instance="false"
+       has_running_driver
+
+       # upsd will crash if started with no available UPS, so only start if
+       # we have one, and we do not already have a upsd instance
+       if [ "$have_driver_instance" = "true" ] && [ "$have_upsd_instance" = "false" ]; then
+               rc_procd start_server_service || return 1
+       fi
+}
+
+start_ups_service() {
+       procd_open_instance "$ups"
+       procd_set_param respawn
+       procd_set_param stderr 1
+       procd_set_param stdout 1
+       procd_set_param env NUT_DEBUG_SYSLOG="stderr"
+       procd_set_param env NUT_QUIET_INIT_UPSNOTIFY=true
+       procd_set_param env NUT_STATEPATH="${STATEPATH}"
+       procd_set_param command "/usr/libexec/nut/${driver}"
+       procd_append_param command -FF -a "$ups"
+       procd_append_param command -u "$RUNAS"
+       procd_close_instance
+}
+
 # Start a ups driver instance
 start_ups_driver() {
        local ups="$1"
        local requested="$2"
        local driver=""
 
-       # If wanting a specific instance, only start that instance
+       # If wanting a specific instance, only start that instance; this is a normal
+       # operating event, so do not log and do not error
        if [ -n "$requested" ] && [ "$requested" != "$ups" ]; then
                return 0
        fi
@@ -91,7 +179,12 @@ start_ups_driver() {
        # driver instance (UPS) match a USB device in sysfs, set the permissions
        # on the USB device node in /dev/bus/usb/... to allow NUT to access and
        # control the device.
-       ensure_usb_ups_access "$ups" || return 1
+       ensure_usb_ups_access "$ups" || {
+               usb_ups_access_error="true"
+               log_error "Error enabling USB access for UPS '$ups'" "nut-server" "nut-server"
+               return 1
+       }
+
        config_get driver "$ups" driver
 
        [ -n "$driver" ] || {
@@ -99,33 +192,10 @@ start_ups_driver() {
                return 1
        }
 
-       procd_open_instance "$ups"
-       procd_set_param respawn
-       procd_set_param stderr 1
-       procd_set_param stdout 1
-       procd_set_param env NUT_DEBUG_SYSLOG="stderr"
-       procd_set_param env NUT_QUIET_INIT_UPSNOTIFY=true
-       procd_set_param env NUT_STATEPATH="${STATEPATH}"
-       procd_set_param command "/usr/libexec/nut/${driver}"
-       procd_append_param command -FF -a "$ups"
-       procd_append_param command -u "$RUNAS"
-       procd_close_instance
-
-       # If upsd is running, reload it to pick up new driver
-       if procd_running "nut-server" "upsd" >/dev/null 2>&1; then
-               signal_instance "upsd" "upsd" "reload" "HUP" "${STATEPATH}/upsd.pid" "" "nut-server"
-       else
-               start_server_instance
-       fi
-}
-
-# On reload, we need to use rpc_procd to add an instance to an already running
-# service, as procd_open_service is not called by default when doing a
-# reload vs start
-add_ups_driver() {
-       local ups="$1"
-       local requested="$2"
-       rc_procd start_ups_driver "$ups" "$requested"
+       rc_procd start_ups_service "$@" || {
+               return 1
+       }
+       return 0
 }
 
 have_upsd_section() {
@@ -170,58 +240,52 @@ common_preconditions() {
 stop_no_longer_configured_instances() {
        local have_driver_instance=false
        local have_upsd_instance=false
+       local had_running_instance="false"
        local instance instances
 
        # Stop any driver instances which are no longer configured
        # We can only reliably do this for instances managed by procd
-       set -f
        instances="$(list_running_instances "nut-server")"
        [ -n "$instances" ] || return 0
+       set -f
        for instance in $instances; do
                if [ "$instance" = "upsd" ]; then
                        continue
                fi
+               had_running_instance="true"
                config_get driver "$instance" driver
                # Only stop not configured but running instances
                if [ -z "$driver" ] && [ -n "$instance" ] && procd_running "nut-server" "$instance" >/dev/null 2>&1; then
-                       procd_kill "nut-server" "$instance" 2>&1 | logger -t nut-server
+                       log_msg "Stopping no longer configured ups '$instance'" "nut-server" "nut-server" notice
+                       procd_kill "nut-server" "$instance"
                fi
        done
        set +f
 
        # Need to stop drivers before stopping upsd
-       # The second loop is required in case some instances failed to stop in the
-       # first loop (so we cannot just track instances in the above loop).
-       # In addition, it is possible for hotplug triggered drivers start to start
+       # The has_running_driver check is required in case some instances failed to
+       # stop in the first loop (so we cannot just track instances in the above loop).
+       # In addition, it is possible for hotplug triggered drivers starting to start
        # instances between the stop above, and this loop.
-       set -f
-       instances="$(list_running_instances "nut-server")"
-       [ -n "$instances" ] || return 0
-       for instance in $instances; do
-               if [ "$instance" = "upsd" ]; then
-                       have_upsd_instance="true"
-                       continue
-               fi
-               have_driver_instance="true"
-               break
-       done
-       set +f
+       has_running_driver
 
        # If we have no UPS instances we must stop upsd or it will crash
        # The "nut-server" service remains active and will 'see' configuration
        # changes and execute reload_service when they are detected
-       if [ "$have_upsd_instance" = "true" ] && [ "$have_driver_instance" = "false" ]; then
-               # stop_server_instance
+       if [ "$have_upsd_instance" = "true" ] && [ "$have_driver_instance" = "false" ] && [ "$had_running_instance" = "true" ]; then
+               log_msg "Stopping upsd because no driver instances configured" "nut-server" "nut-server" "notice"
                signal_instance "upsd" "upsd" "stop" "TERM" "${STATEPATH}/upsd.pid" "" "nut-server" "procd_kill" "nut-server" "upsd"
+       elif [ "$have_upsd_instance" = "false" ] && [ "$have_driver_instance" = "true" ]; then
+               start_server_instance
        fi
 }
 
 stop_all_instances() {
        local instance instances
 
-       set -f
        instances="$(list_running_instances "nut-server")"
        [ -n "$instances" ] || return 0
+       set -f
        for instance in $instances; do
                # stop upsd last
                if [ "$instance" = "upsd" ]; then
@@ -305,25 +369,6 @@ service_preconditions() {
        return 0
 }
 
-# Start upsd instance
-start_server_instance() {
-       if [ -f "$NUT_KILLPOWER" ]; then
-               return 1
-       fi
-
-       procd_open_instance upsd
-       procd_set_param respawn
-       procd_set_param stderr 1
-       procd_set_param stdout 1
-       procd_set_param env NUT_DEBUG_SYSLOG="stderr"
-       procd_set_param env NUT_QUIET_INIT_UPSNOTIFY=true
-       procd_set_param env NUT_STATEPATH="$STATEPATH"
-       procd_set_param command /usr/sbin/upsd
-       procd_append_param command -FF
-       procd_append_param command -u "$RUNAS"
-       procd_close_instance
-}
-
 remove_var_config() {
        for rm_file in "$USERS_C" "$UPS_C" "$UPSD_C" \
                "$USERS_C.new" "$UPS_C.new" "$UPSD_C.new"; do
@@ -390,8 +435,12 @@ reload_ups_driver() {
        # If the driver instance (UPS) was terminated, or stopped 'naturally',
        # start it up again
        if ! procd_running "nut-server" "$ups" >/dev/null 2>&1; then
-               add_ups_driver "$ups" "$ups"
+               start_ups_driver "$ups" "$ups"
        fi
+
+       # If the server instance (upsd) was terminated, or stopped 'naturally',
+       # start it up again
+       start_server_instance
        return 0
 }
 
@@ -412,14 +461,13 @@ manage_instances() {
        case "$action" in
        start)
                config_foreach start_ups_driver driver
+               start_server_instance
                ;;
        reload)
                if service_active_no_instances "nut-server"; then
                        log_msg "nut-server active with no instances. Reloading." "nut-server" "nut-server" "info"
                fi
-               if procd_running "nut-server" upsd >/dev/null 2>&1; then
-                       signal_instance "upsd" "upsd" "reload" "HUP" "${STATEPATH}/upsd.pid" "" "nut-server" "procd_kill" "nut-server" "upsd"
-               fi
+
                config_foreach reload_ups_driver driver
                stop_no_longer_configured_instances
                ;;
@@ -444,16 +492,14 @@ manage_service() {
        # instances, we act only on one instance).
        local instance="$1"
        local ret=0
+       local service_active
 
        case "$action" in
        start)
+               service_preconditions "$action"
+               ret=$?
                case "$instance" in
                "")
-                       service_preconditions "$action"
-                       ret=$?
-
-                       stop_no_longer_configured_instances
-
                        if [ "$ret" = "0" ]; then
                                manage_instances start
                        else
@@ -462,27 +508,20 @@ manage_service() {
                        ;;
                # We only start one service (upsd or one driver) from a given invocation
                upsd)
-                       service_preconditions "$action"
-                       ret=$?
                        if [ "$ret" = "0" ]; then
                                start_server_instance
                        fi
-                       stop_no_longer_configured_instances
-
-                       return $ret
                        ;;
                # We only start one service (upsd or one driver) from a given invocation
                *)
-                       service_preconditions "$action"
-                       ret=$?
                        if [ "$ret" = "0" ]; then
                                config_foreach start_ups_driver driver "$instance"
+                               start_server_instance
                        fi
-                       stop_no_longer_configured_instances
-
-                       return $ret
                        ;;
                esac
+               stop_no_longer_configured_instances
+               return $ret
                ;;
        reload)
                service_preconditions "$action"
@@ -492,8 +531,6 @@ manage_service() {
                        manage_instances reload
                fi
 
-               stop_no_longer_configured_instances
-
                if [ "$ret" = "1" ]; then
                        return 1
                fi
@@ -502,17 +539,27 @@ manage_service() {
                case "$instance" in
                "")
                        manage_instances stop
+                       ret=$?
+
+                       service_active_no_instances "$(basename "${basescript:-$initscript}")"
+                       service_active=$?
+
+                       if [ "$service_active" -eq 1 ]; then
+                               # Prevent NUT hotplug events when service is not running
+                               rm -f "$NUT_HOTPLUG_BOOT_COMPLETE_PATH"
+                       elif [ "$service_active" -eq 3 ]; then
+                               return 1
+                       fi
+                       return $ret
                        ;;
                upsd)
                        if procd_running "nut-server" upsd >/dev/null 2>&1; then
                                signal_instance "upsd" "upsd" "stop" "TERM" "${STATEPATH}/upsd.pid" "" "nut-server" "procd_kill" "nut-server" "upsd"
                        fi
-                       stop_service_if_no_instances
                        ;;
                *)
                        # We only handle the first parameter, so do not pass in all parameters
                        config_foreach stop_ups_driver driver "$instance"
-                       stop_service_if_no_instances
                        ;;
                esac
                ;;
@@ -535,14 +582,61 @@ manage_service() {
        esac
 }
 
+allow_hotplug() {
+       local ret="$1"
+
+       [ "$ret" -eq 0 ] || return "$ret"
+
+       # Allow hotplug events to (re)start drivers/upsd only if starting
+       # nut-server service was successful
+       touch "$NUT_HOTPLUG_BOOT_COMPLETE_PATH"
+
+       # Ensure drivers/upsd started once hotplug has been allowed
+       local usb_ups_access_error="false"
+       config_foreach start_ups_driver driver
+       if [ "$usb_ups_access_error" = "false" ]; then
+               start_server_instance
+               ret=$?
+       else
+               ret=1
+       fi
+
+       # Re-disable hotplug if starting drivers and/or upsd failed after
+       # enabling hotplug.
+       if [ "$ret" -ne 0 ]; then
+               rm -f "$NUT_HOTPLUG_BOOT_COMPLETE_PATH"
+       fi
+
+       return "$ret"
+}
+
 # Start NUT drivers and upsd (NUT server)
 start_service() {
+       local ret
        manage_service start "$@"
+       ret=$?
+
+       # Only change allowing hotplug events for the
+       # start all instances case
+       if [ -z "$1" ]; then
+               allow_hotplug "$ret"
+       fi
 }
 
 # Reload NUT drivers and upsd (NUT server)
 reload_service() {
+       local ret
+
        manage_service reload "$@"
+       ret=$?
+
+       # Only change allowing hotplug events for the
+       # reload all instances case
+       if [ -z "$1" ]; then
+               # Allow hotplug events to (re)start drivers/upsd when reloading
+               # (successfully) after a failed start
+               allow_hotplug "$ret"
+       fi
 }
 
 # Stop NUT drivers and upsd (NUT server)
@@ -568,3 +662,90 @@ service_triggers() {
        }
        procd_add_reload_trigger "nut_server"
 }
+
+rc_procd() {
+       local method service_active
+
+       service_active_no_instances "$(basename "${basescript:-$initscript}")"
+       service_active=$?
+
+       # Always use set if nut-server service is not active, even if this
+       # function is executed from reload or restart. In the default
+       # implementation from USE_PROCD=1, rc_procd is only called from
+       # start, so set is only used on a start action. In addition, the
+       # default rc_procd only uses add when called with a second parameter
+       if [ "$service_active" -eq 1 ]; then
+               log_msg "$(basename "${basescript:-$initscript}") was not active, so start it" "nut-server" "nut-server" "debug"
+               method="set"
+       elif [ "$service_active" -eq 3 ]; then
+               # log error message was already issued in service_active_no_instances
+               return 1
+       elif [ "$service_active" -eq 0 ] || [ "$service_active" -eq 2 ]; then
+               method="add"
+       else
+               log_error "Unexpected return value from 'service_active_no_instances'" "nut-server" "nut-server"
+               return 1
+       fi
+       procd_open_service "$(basename "${basescript:-$initscript}")" "$initscript"
+       "$@"
+       procd_close_service "$method"
+
+       return 0
+}
+
+# Define start in a way that avoids nesting rc_procd
+start() {
+       # Ensure start actions occur one at a time
+       procd_lock
+
+       # Ensure service is started even if no instances are started
+       if rc_procd :; then
+               start_service "$@"
+       fi
+}
+
+boot() {
+       start
+}
+
+trace() {
+       TRACE_SYSCALLS=1
+       start "$@"
+}
+
+info() {
+       json_init
+       json_add_string name "$(basename "${basescript:-$initscript}")"
+       json_add_boolean verbose "1"
+       _procd_ubus_call list
+}
+
+stop() {
+       procd_lock
+       stop_service "$@"
+       procd_kill "$(basename "${basescript:-$initscript}")" "$1"
+       if eval "type service_stopped" 2>/dev/null >/dev/null; then
+               service_stopped
+       fi
+}
+
+reload() {
+       if eval "type reload_service" 2>/dev/null >/dev/null; then
+               procd_lock
+               reload_service "$@"
+       else
+               start
+       fi
+}
+
+running() {
+       service_running "$@"
+}
+
+status() {
+       if eval "type status_service" 2>/dev/null >/dev/null; then
+               status_service "$@"
+       else
+               _procd_status "$(basename "${basescript:-$initscript}")" "$1"
+       fi
+}
index b8a4ded76cec54d6a4cfdcf68c84301a0fdac3a7..4c81a76e1fa5a6a291f6a0f815e7bd96a88fe18e 100644 (file)
@@ -21,6 +21,8 @@ NUT_KILLPOWER="/var/run/killpower"
 
 # Disable NUT hotplug path
 NUT_DISABLE_HOTPLUG_PATH="/var/run/nut/disable-hotplug"
+# Must be present to allow hotplug
+NUT_HOTPLUG_BOOT_COMPLETE_PATH="/var/run/nut-hotplug-boot-complete"
 
 # Fallback STATEPATH setting
 NUT_BASE_STATEPATH="/var/run/nut"
@@ -48,6 +50,9 @@ allow_hotplug_restart() {
        # or nutshutdown
        [ ! -f "$NUT_DISABLE_HOTPLUG_PATH" ] || return 1
 
+       # Initial boot complete; NUT hotplug allowed sentinel
+       # Created by nut-server initscript
+       [ -f "$NUT_HOTPLUG_BOOT_COMPLETE_PATH" ] || return 1
        return 0
 }
 
@@ -211,14 +216,23 @@ find_runas() {
        ) || return $?
 }
 
-# Detect if service is running under procd, with no instances
-# (e.g. upsd with no UPS drivers running)
+# Detect if a service is active with procd, and if it has
+# any instances.
+#
+# Returns:
+#  0 - service is registered (active) but has no instances
+#  1 - service is not registered (active) at all
+#  2 - service is registered (active) and has instances
+#  3 - error condition encountered
 service_active_no_instances() {
        local service="$1"
        local active_instances
        local active_var service_filter
 
-       check_safe_uci_name "$service" || return 1
+       check_safe_uci_name "$service" || {
+               log_error "Invalid service name '$service' in service_active_no_instances" nut-service.sh nut-service
+               return 3
+       }
 
        service_filter="@['""${service}""']"
        active_instances="$(_procd_ubus_call list "{\"name\":\"$service\"}" | jsonfilter -l 1 -e active_var="$service_filter")"
@@ -232,14 +246,16 @@ service_active_no_instances() {
                ;;
        *)
                log_error "Unexpected value '$active_instances' for 'active_instances' from jsonfilter in service_active_no_instances" nut-service.sh nut-service
-               return 1
+               return 3
                ;;
        esac
 
        # active_var is only empty, but present, if there are no instances for the service
        [ -z "$active_var" ] && return 0
 
-       return 1
+       # otherwise there are instances, in any state, for the service; to check if
+       # any are running use procd_running
+       return 2
 }
 
 # Setup triggers in procd for changes to specified network interfaces
git clone https://git.99rst.org/PROJECT