#!/bin/sh /etc/rc.common
# Copyright © 2012-2026 OpenWrt.org
#
# Startup script for NUT (UPS) server
#
# This is free software, licensed under the GNU General Public License v2.
# See /LICENSE for more information.
#

# In recent (relevant) versions of shellcheck busybox is a valid shell type
# shellcheck shell=busybox

# shellcheck disable=SC2034
START=70
STOP=30

USE_PROCD=1

# 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-server[$$]" "FATAL: Unable to source 'functions.sh' in 'nut-server'"
	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-server[$$]" "FATAL: Unable to source 'nut-common.sh' in 'nut-server'"
	exit 1
}

# shellcheck source=/dev/null
. "${IPKG_INSTROOT}"/lib/functions/network.sh || {
	log_source_error "network.sh" "nut-server" "nut-server"
	exit 1
}

# shellcheck source=net/nut/files/nut-server-config.sh.functions
. "${IPKG_INSTROOT}"/lib/functions/nut/nut-server-config.sh || {
	log_source_error "nut-server-config.sh" "nut-server" "nut-server"
	exit 1
}

# shellcheck source=/dev/null
. "${IPKG_INSTROOT}"/usr/share/libubox/jshn.sh || {
	log_source_error "jshn.sh" "nut-server" "nut-server"
	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" "nut-server" "nut-server"
	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" "nut-server" "nut-server"
	exit 1
}

restore_umask_on_exit

# 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 [ -n "$requested" ] && [ "$requested" != "$ups" ]; then
		return 0
	fi

	# For non-USB devices this is effectively a no-op
	# For USB devices, if the USB productid and USB vendorid of the configured
	# 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
	config_get driver "$ups" driver

	[ -n "$driver" ] || {
		log_error "Missing driver when starting UPS '$ups'" "nut-server" "nut-server"
		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"
}

have_upsd_section() {
	have_upsd_section="true"
}

common_preconditions() {
	local ret

	# No config, or error loading config is fatal
	config_load nut_server || {
		log_config_load_error "nut_server" "nut-server" "nut-server"
		exit 1
	}

	# Only find statepath and runas once per service start or reload
	if have_section_named "upsd" "upsd"; then
		STATEPATH="$(find_statepath "upsd" "nut_server")" || {
			log_error_exit "Failed to determine STATEPATH" "nut-server" "nut-server"
		}
		RUNAS="$(find_runas "upsd" "nut_server")" || {
			log_error_exit "Failed to determine RUNAS" "nut-server" "nut-server"
		}
	else
		# If there is no 'upsd' section in the nut_server config file, use
		# default settings
		STATEPATH=/var/run/nut
		RUNAS=nut
	fi

	# NUT_KILLPOWER is an external sentinel that indicates a forced shutdown
	if [ -f "$NUT_KILLPOWER" ]; then
		return 1
	fi

	build_config
	ret=$?

	return $ret
}

stop_no_longer_configured_instances() {
	local have_driver_instance=false
	local have_upsd_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
	for instance in $instances; do
		if [ "$instance" = "upsd" ]; then
			continue
		fi
		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
		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
	# 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

	# 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
		signal_instance "upsd" "upsd" "stop" "TERM" "${STATEPATH}/upsd.pid" "" "nut-server" "procd_kill" "nut-server" "upsd"
	fi
}

stop_all_instances() {
	local instance instances

	set -f
	instances="$(list_running_instances "nut-server")"
	[ -n "$instances" ] || return 0
	for instance in $instances; do
		# stop upsd last
		if [ "$instance" = "upsd" ]; then
			continue
		fi
		config_get driver "$instance" driver
		[ -n "$driver" ] || continue

		signal_instance "$instance" "/usr/libexec/nut/${driver}" "stop" "TERM" \
			"${STATEPATH}/${driver}-${instance}.pid" "" "nut-server" "procd_kill" "nut-server" "$instance"
	done
	set +f
	# stop_server_instance
	signal_instance "upsd" "upsd" "stop" "TERM" "${STATEPATH}/upsd.pid" "" "nut-server" "procd_kill" "nut-server" "upsd"
	return 0
}

service_preconditions() {
	local action="$1"
	local should_stop_srv=false
	local should_start_srv=true
	local ret=0

	case "$action" in
	start)
		common_preconditions
		ret=$?

		case $ret in
		0)
			check_interface_up "upsd" || should_start_srv=false
			;;
		*)
			should_start_srv=false
			;;
		esac
		;;
	reload)
		common_preconditions
		ret=$?

		case $ret in
		0 | 2)
			check_interface_up "upsd" || should_stop_srv=true
			;;
		*)
			should_stop_srv=true
			;;
		esac
		;;
	*)
		log_error "Unknown action '$action' in service_preconditions" "nut-server" "nut-server"
		ret=1
		;;
	esac

	# If server not configured or no interfaces are up, diagnostic should already have been logged.
	# Do not spam log more, but remove config and exit.
	if [ "$should_start_srv" = "false" ]; then
		remove_var_config
		return 1
	fi

	# Stop all instances, remove config, and exit with error, if should_stop_srv is true
	if [ "$should_stop_srv" = "true" ]; then
		stop_all_instances || true
		remove_var_config
		return $ret
	fi

	# NUT_KILLPOWER is an external sentinel that may be set between now and
	# the previous check
	if [ -f "$NUT_KILLPOWER" ]; then
		return 1
	fi

	# If we're here we should have good new config; activate it
	mv -f "$USERS_C.new" "$USERS_C" || return 1
	mv -f "$UPS_C.new" "$UPS_C" || return 1
	mv -f "$UPSD_C.new" "$UPSD_C" || return 1
	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
		rm -f "$rm_file"
	done
}

# Stop NUT drivers and upsd (NUT server)
# We do not error exit (only log issues) because we do not want to prevent
# processing of further drivers.
stop_ups_driver() {
	local ups="$1" # The ups (driver instance)
	local requested="$2"
	local driver

	# If wanting a specific instance, only stop it
	if [ -n "$requested" ] && [ "$requested" != "$ups" ]; then
		return 0
	fi

	config_get driver "$ups" driver

	if [ -z "$driver" ]; then
		log_error "No driver specified for UPS '$ups'. Attempting procd_kill." "nut-server" "nut-server"
		procd_kill "nut-server" "$ups" || log_error "No driver specified for UPS '$ups' and procd_kill fallback failed" "nut-server" "nut-server"
		return 0
	fi

	# If there is a running driver instance for this UPS
	if procd_running "nut-server" "$ups" >/dev/null 2>&1; then
		signal_instance "$ups" "/usr/libexec/nut/${driver}" exit "TERM" "${STATEPATH}/${driver}-${ups}.pid" "${ups}" "nut-server"
	fi
	return 0
}

# Reload NUT drivers and upsd (NUT server)
# We do not error exit (only log issues) because we do not want to prevent
# processing of further drivers.
reload_ups_driver() {
	local ups="$1"
	local requested="$2"
	local driver

	# If wanting a specific instance, only reload that instance
	if [ -n "$requested" ] && [ "$requested" != "$ups" ]; then
		return 0
	fi

	if [ -f "$NUT_KILLPOWER" ]; then
		stop_ups_driver "$ups"
		return 0
	fi

	config_get driver "$ups" driver

	# Try to reload, otherwise exit politely
	if procd_running "nut-server" "$ups"; then
		signal_instance "$ups" "/usr/libexec/nut/${driver}" "reload-or-exit" "HUP" "${STATEPATH}/${driver}-${ups}.pid" "${ups}" "nut-server"
		# Fall through to start driver, if it exited due to 'reload-or-exit'
		# finding a configuration condition which requires a stop and start
		# for the configuration to be applied.
	fi

	# 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"
	fi
	return 0
}

stop_service_if_no_instances() {
	if service_active_no_instances "nut-server"; then
		log_msg "nut-server active with no instances" nut-server "nut-server" notice
		# If "nut-server" is active with no instances
		# We don't care about the exit code of procd_kill, and logging its
		# stderr can aid debugging.
		procd_kill "nut-server" 2>&1 | logger -t "nut-server"
	fi
}

# Stop or reload active instances
manage_instances() {
	local action="$1"

	case "$action" in
	start)
		config_foreach start_ups_driver driver
		;;
	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
		;;
	stop)
		stop_all_instances
		stop_service_if_no_instances
		remove_var_config
		;;
	stop-instances)
		stop_all_instances
		remove_var_config
		;;
	esac
}

# Manage NUT drivers and upsd (NUT server) services
manage_service() {
	local action="$1"
	# If we have no second parameter, we use the "") case and affect all instances
	shift || true
	# We only consume a single additional parameter (if not acting on all
	# instances, we act only on one instance).
	local instance="$1"
	local ret=0

	case "$action" in
	start)
		case "$instance" in
		"")
			service_preconditions "$action"
			ret=$?

			stop_no_longer_configured_instances

			if [ "$ret" = "0" ]; then
				manage_instances start
			else
				return 1
			fi
			;;
		# 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"
			fi
			stop_no_longer_configured_instances

			return $ret
			;;
		esac
		;;
	reload)
		service_preconditions "$action"
		ret=$?

		if [ "$ret" != "1" ]; then
			manage_instances reload
		fi

		stop_no_longer_configured_instances

		if [ "$ret" = "1" ]; then
			return 1
		fi
		;;
	stop)
		case "$instance" in
		"")
			manage_instances stop
			;;
		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
		;;
	stop-instances)
		case "$instance" in
		"")
			manage_instances stop-instances
			;;
		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
			;;
		*)
			# We only handle the first parameter, so do not pass in all parameters
			config_foreach stop_ups_driver driver "$instance"
			;;
		esac
		;;
	esac
}

# Start NUT drivers and upsd (NUT server)
start_service() {
	manage_service start "$@"
}

# Reload NUT drivers and upsd (NUT server)
reload_service() {
	manage_service reload "$@"
}

# Stop NUT drivers and upsd (NUT server)
stop_service() {
	manage_service stop "$@"
}

service_triggers() {
	# No config, or error loading config is fatal
	config_load nut_server || {
		log_config_load_error "nut_server" "nut-server" "nut-server"
		exit 1
	}

	config_get interface_reload_delay upsd interface_reload_delay $DEFAULT_PROCD_INTERFACE_RELOAD_DELAY
	if ! check_unsigned_int "$interface_reload_delay" || [ -z "$interface_reload_delay" ]; then
		log_msg "interface_reload_delay must be an unsigned integer" nut-server nut-server warn
		interface_reload_delay="$DEFAULT_PROCD_INTERFACE_RELOAD_DELAY"
	fi

	add_interface_triggers "upsd" "$interface_reload_delay" "nut-server" "upsd" || {
		log_error "Failed to add interface triggers" nut-server nut-server
	}
	procd_add_reload_trigger "nut_server"
}
