Files
packages/net/nut/files/libhid-ups.hotplug
Daniel F. Dickinson 2537757a3b nut: improve documentation
Include more inline comments to document the nut-server initscript and
usb hotplug.

Also trim an extraneous space in a comment.

Signed-off-by: Daniel F. Dickinson <dfdpublic@wildtechgarden.ca>
2026-09-10 10:22:36 +02:00

371 lines
13 KiB
Bash

#!/bin/sh
# In recent (relevant) versions of shellcheck busybox is a valid shell type
# shellcheck shell=busybox
# IPKG_INSTROOT is intentionally only set when building an image and
# is intentionally empty on a live OpenWrt device
# The shellcheck source directives are used during development by external
# tools to find the source files on the development system. They do not
# affect runtime behaviour.
# Also the filenames in the shellcheck source directives may differ from the
# production names that are sourced on live OpenWrt devices.
# shellcheck source=net/nut/files/functions.sh.functions
. "${IPKG_INSTROOT}"/lib/functions.sh || {
# Before our sourcing error logging definitions are sourced
logger -t nut-usb-hotplug[$$] "Unable to source 'functions.sh' in 'libhid-ups' hotplug. Bailing"
exit 1
}
# 'shellcheck' complains about nut-common.sh due to a case pattern it does not understand, even
# though it is correctly POSIX compliant
# shellcheck disable=SC1094
# shellcheck source=net/nut/files/nut-common.sh.functions
. "${IPKG_INSTROOT}"/lib/functions/nut/nut-common.sh || {
# Before our sourcing error logging definitions are sourced
logger -t nut-usb-hotplug[$$] "Unable to source 'nut-common.sh' in 'libhid-ups' hotplug. Bailing"
exit 1
}
# shellcheck source=net/nut/files/nut-service.sh.functions
. "${IPKG_INSTROOT}"/lib/functions/nut/nut-service.sh || {
log_source_error "nut-service.sh" "libhid-ups" nut-usb-hotplug
exit 1
}
# shellcheck source=net/nut/files/nut-server-service.sh.functions
. "${IPKG_INSTROOT}"/lib/functions/nut/nut-server-service.sh || {
log_source_error "nut-server-service.sh" "libhid-ups" nut-usb-hotplug
exit 1
}
config_load nut_server || {
log_config_load_error "nut_server" "libhid-ups" nut-usb-hotplug
exit 1
}
# shellcheck disable=SC2329,SC2317
start_stop_driver() {
local ups="$1"
local known_devname="$2"
# ACTION and DEVNAME come from the calling hotplug event
# shellcheck disable=SC2153
case "$ACTION" in
add)
# Skip hotplug if it is disabled or NUT is shutting down
if ! allow_hotplug_restart; then
log_msg "Hotplug restart disabled starting '$ups' when processing '$ACTION' for '$DEVNAME'" "libhid-ups" nut-usb-hotplug info
# This is a configuration not an error
return 0
fi
# Set permissions to allow the UPS device to start
# In some cases this may be enough for the UPS to start working
if ! ensure_usb_ups_access "$ups" "$known_devname"; then
log_error "Unable to enable access to '$ups' when processing '$ACTION' for '$DEVNAME'" "libhid-ups" nut-usb-hotplug
return 0
fi
# Start the newly hotplugged UPS
# The initscript is effectively a noop if the UPS is already started
# (does not stop and restart, in that case).
# We check enabled again here, in case of a race condition since
# our last check.
if /etc/init.d/nut-server enabled; then
# As the driver start goes through the initscript's start which has a
# procd_lock, the driver should not be started twice, even if hotplug
# fires as soon as hotplugging is allowed in initscript's boot()
if ! /etc/init.d/nut-server start "$ups"; then
log_error "Failed to start '$ups' processing '$ACTION' for '$DEVNAME'" "libhid-ups" nut-usb-hotplug
return 0
fi
else
log_error "Hotplug became disabled while processing '$ACTION' for '$DEVNAME'." "libhid-ups" nut-usb-hotplug
return 0
fi
;;
remove)
# Stop the newly removed UPS, even if hotplug is disabled.
# We stop only the removed UPS as we want the NUT service and other
# UPS (if any) to remain up.
/etc/init.d/nut-server stop "$ups" || {
log_error "Failed to stop nut-driver instance (UPS) '$ups'." "libhid-ups" nut-usb-hotplug
return 0
}
;;
*)
# Ignore all other hotplug ACTION types
return 0
;;
esac
# We do not return 0 here as it is unnecessary (all case branches, including
# catch-all have a return) and shellcheck will complain if we do.
}
# We do not exit with error as that would abort processing for any other
# drivers/devices
# shellcheck disable=SC2329,SC2317
nut_driver_config() {
local ups="$1"
local try_match="$2"
local cfg_vendorid cfg_productid
# Configuration might not have a vendorid. This is allowed, and in that case
# we keep going as we will restart this UPS with no match.
config_get cfg_vendorid "$ups" vendorid
if [ -n "$cfg_vendorid" ] && ! check_valid_hex_number "${cfg_vendorid/0x/}"; then
log_error "Configured vendorid for '$ups' is not valid" "libhid-ups" nut-usb-hotplug
nd_driver_config_error=true
return 0
fi
case "$cfg_vendorid" in
'')
# Configuration might not have a vendorid. This is allowed, and in that
# case we keep going as we may restart this UPS with no match.
;;
0x*)
cfg_vendorid="$(printf "%04x" "$cfg_vendorid")"
;;
*)
cfg_vendorid="$(printf "%04x" "0x$cfg_vendorid")"
;;
esac
# Configuration might not have a productid. This is allowed, and in that case
# we keep going as we will restart this UPS with no match.
config_get cfg_productid "$ups" productid
if [ -n "$cfg_productid" ] && ! check_valid_hex_number "${cfg_productid/0x/}"; then
log_error "Configured productid for '$ups' is not valid" "libhid-ups" nut-usb-hotplug
nd_driver_config_error=true
return 0
fi
case "$cfg_productid" in
'')
# Configuration might not have a productid. This is allowed, and in that
# case we keep going as we may restart this UPS with no match.
;;
0x*)
cfg_productid="$(printf "%04x" "$cfg_productid")"
;;
*)
cfg_productid="$(printf "%04x" "0x$cfg_productid")"
;;
esac
# ACTION and DEVNAME come from the calling hotplug event
# If we could not match a device in the previous round, or the configuration
# for the UPS does not have a vendorid and productid, start this UPS
# without checking for a match
if [ "$try_match" = "no" ]; then
if [ "$ACTION" = "add" ]; then
if ! allow_hotplug_restart; then
log_msg "Hotplug restart disabled starting '$ups' when processing '$ACTION' for '$DEVNAME'" "libhid-ups" nut-usb-hotplug notice
return 0
fi
# We check enabled again here, in case of a race condition since
# our last check.
if /etc/init.d/nut-server enabled; then
# If we don't have a specific match, attempt a reload rather
# than restart of '$ups'. The nut-server initscript will do a
# start if the reload fails.
/etc/init.d/nut-server reload "$ups" || {
log_msg "Failed to reload unmatched driver instance (UPS) '$ups' in nut_driver_config" libhid-ups nut-usb-hotplug notice
nd_driver_config_error=true
return 0
}
else
# Initscript should have its own logging, and we do not exit with
# error as that would abort processing other devices
log_msg "NUT server disabled when processing '$ACTION' for '$DEVNAME'." "libhid-ups" nut-usb-hotplug notice
return 0
fi
else
# We do not stop running UPS that do not match on vendorid
# and productid on a removal event as that would mean all UPS would
# be stopped if one nomatch UPS were to be removed. This would
# be bad.
return 0
fi
elif [ -n "$cfg_vendorid" ] && [ -n "$cfg_productid" ] && [ -n "$pvendid" ] && [ -n "$pprodid" ]; then
if ! check_valid_hex_number "$pvendid"; then
log_error "Hotplug vendor id for '$ups' is not valid" "libhid-ups" nut-usb-hotplug
nd_driver_config_error=true
return 0
fi
if ! check_valid_hex_number "$pprodid"; then
log_error "Hotplug product id for '$ups' is not valid" "libhid-ups" nut-usb-hotplug
nd_driver_config_error=true
return 0
fi
if [ "$(printf "%04x" 0x"$pvendid")" = "$cfg_vendorid" ] &&
[ "$(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
# Initscript should have its own logging
if ! start_stop_driver "$ups" "/dev/$DEVNAME"; then
log_error "Failed to start/stop matched driver instance (UPS) in nut_driver_config" libhid-ups nut-usb-hotplug
nd_driver_config_error=true
return 0
fi
# shellcheck disable=SC2317
nd_found=true
return 0
fi
fi
# We do not return 0 here as it is unnecessary (all if branches have a
# return) and shellcheck will complain if we do.
}
perform_libhid_action() {
# Variable to indicate NUT driver instance (UPS) section found
local nd_found=false
local nd_driver_config_error=false
local pvendid pprodid
# Only find statepath and runas once per event
STATEPATH="$(find_statepath "upsd" "nut_server")" || {
log_error "Failed to set STATEPATH" "libhid-ups" nut-usb-hotplug
return 1
}
[ -n "$STATEPATH" ] || {
log_error "Failed to set STATEPATH" "libhid-ups" nut-usb-hotplug
return 1
}
export STATEPATH
RUNAS="$(find_runas "upsd" "nut_server")" || {
log_error "Failed to set RUNAS" "libhid-ups" nut-usb-hotplug
return 1
}
[ -n "$RUNAS" ] || {
log_error "Failed to set RUNAS" "libhid-ups" nut-usb-hotplug
return 1
}
export RUNAS
# PRODUCT comes from the calling hotplug event, and
# is of the form vendorid/productid/other
# The code below uses shell parameter expansion to split out
# vendorid as pvendid and productid as pprodid
# We disable the check for possible misspelling as the variables names
# are similar, but PRODUCT, pvendid, and pprodid are correct.
# shellcheck disable=SC2153
# PRODUCT from hotplug has the form aaaa/bbbb/cccc, where aaaa, bbbb,
# cccc are 1-4 hex digits.
# From PRODUCT, strip final / and everything after it (e.g. /cccc)
pvendid=${PRODUCT%/*}
# pvendid is now of form aaaa/bbbb
# Strip final / and anything after (e.g. /bbbb), leaving aaaa in pvendid
pvendid=${pvendid%/*}
# From PRODUCT, strip final / and everything after it (e.g. /cccc)
pprodid=${PRODUCT%/*}
# pprodid is now of form aaaa/bbbb
# Strip everything before and including the remaining / (e.g. aaaa/)
# This leaves bbbb in pprodid
pprodid=${pprodid##*/}
# Attempt to find and (re)start a UPS with a match for the
# pvendid and pprodid we have parsed.
config_foreach nut_driver_config driver yes
[ "$nd_driver_config_error" = "false" ] || return 1
# Only do a second search that does not need to match a known device
# when ACTION is "add". We don't want to stop all UPSes on the removal
# of only one.
[ "$ACTION" = "add" ] || return 0
# Only if we cannot find a matching UPS (driver instance) configuration
# do we try again, this time restarting all UPS (driver) instances
# This is for the event the device matching configuration for NUT does
# not use USB vendorid and productid (and possibly serial number), which
# is all we have to work with in this script (and sourced scripts)
if [ "$nd_found" = "false" ]; then
config_foreach nut_driver_config driver no
fi
[ "$nd_driver_config_error" = "false" ] || return 1
# Calls nut-server start for upsd, which has checks for the existence of a
# ups driver and upsd configuration and will not start if configuration
# is missing. In addition any UPS for which configuration has been removed
# and not already stopped, will be stopped. This is preferable to doing
# nothing on removals.
if ! allow_hotplug_restart; then
log_msg "Hotplug restart disabled when processing '$ACTION' for '$DEVNAME'" "libhid-ups" nut-usb-hotplug notice
return 0
fi
# We check enabled again here, in case of a race condition since
# our last check.
if /etc/init.d/nut-server enabled; then
# Initscript should have its own logging
if ! /etc/init.d/nut-server start upsd; then
return 1
fi
return 0
else
log_msg "NUT server disabled when processing '$ACTION' for '$DEVNAME'." "libhid-ups" nut-usb-hotplug notice
return 0
fi
# We do not return 0 here as it is unnecessary (all if branches have a
# return) and shellcheck will complain if we do.
}
# Ignore incomplete (for our purposes) events
[ -n "$DEVNAME" ] || exit 0
[ -n "$ACTION" ] || exit 0
# PRODUCT comes from the calling hotplug event, and
# is of the form vendorid/productid/other
# This is an incomplete script which gets filled by libhid-ups.parsed-usermap.
# The result is cases of the form
# "vendorid/productid/other" ) | \
# to be matched against the PRODUCT from the calling hotplug event
# The empty string matches when PRODUCT is empty, but does not match
# when PRODUCT is neither empty nor one of the case targets, above
# This confuses shellcheck and reviewers, hence the disables below.
# shellcheck disable=SC1073,SC1072
case "$PRODUCT" in
### insert libhid-ups.parsed-usermap content here ###
"")
# Skip hotplug actions if NUT hotplug is disabled
# Uses NUT_DISABLE_HOTPLUG_PATH, defined in nut-service.sh and sourced
# NUT_DISABLE_HOTPLUG_PATH is an external sentinel used to indicate NUT
# hotplug is disabled.
if ! allow_hotplug_restart; then
log_msg "Hotplug restart disabled when processing '$ACTION' for '$DEVNAME'" "libhid-ups" nut-usb-hotplug notice
exit 0
fi
# We check enabled again here, in case of a race condition since
# our last check.
if ! /etc/init.d/nut-server enabled; then
log_msg "NUT server disabled when processing '$ACTION' for '$DEVNAME'." "libhid-ups" nut-usb-hotplug notice
exit 0
fi
if ! perform_libhid_action; then
exit 1
fi
exit 0
;;
*)
# We intentionally do nothing when PRODUCT is neither empty, nor matches
# the libhid-ups.parsed-usermap content cases. In that case the PRODUCT is
# for a device other than a UPS supported by NUT's libhid-ups.
exit 0
;;
esac