#!/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
			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" ]; 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 "Error starting 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
	# defines STATEPATH
	find_statepath "upsd" "nut_server" || {
		log_error "Failed to set STATEPATH" "libhid-ups" nut-usb-hotplug
		return 1
	}
	# sets RUNAS
	find_runas "upsd" "nut_server" || {
		log_error "Failed to set RUNAS" "libhid-ups" nut-usb-hotplug
		return 1
	}

	# 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 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.
}

# 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.
# This confuses shellcheck and reviewers, hence the disables below.
# shellcheck disable=SC1073,SC1072
case "$PRODUCT" in
1/0/* | \
3f0/1/* | \
3f0/1f01/* | \
3f0/1f02/* | \
3f0/1f06/* | \
3f0/1f08/* | \
3f0/1f09/* | \
3f0/1f0a/* | \
3f0/1fe0/* | \
3f0/1fe1/* | \
3f0/1fe2/* | \
3f0/1fe3/* | \
3f0/1fe5/* | \
3f0/1fe6/* | \
3f0/1fe7/* | \
3f0/1fe8/* | \
463/1/* | \
463/ffff/* | \
47c/ffff/* | \
483/35/* | \
483/a113/* | \
483/a430/* | \
4b3/1/* | \
4b4/5500/* | \
4d8/d004/* | \
4d8/d005/* | \
50d/375/* | \
50d/551/* | \
50d/750/* | \
50d/751/* | \
50d/900/* | \
50d/910/* | \
50d/912/* | \
50d/980/* | \
50d/f51/* | \
50d/1100/* | \
51d/0/* | \
51d/2/* | \
51d/3/* | \
51d/4/* | \
51d/5/* | \
592/2/* | \
592/4/* | \
5b8/0/* | \
5dd/41b/* | \
5dd/a011/* | \
5dd/a0a0/* | \
665/5161/* | \
6da/2/* | \
6da/3/* | \
6da/4/* | \
6da/5/* | \
6da/201/* | \
6da/601/* | \
6da/ffff/* | \
75d/300/* | \
764/5/* | \
764/501/* | \
764/601/* | \
925/1234/* | \
9ae/1/* | \
9ae/1003/* | \
9ae/1007/* | \
9ae/1008/* | \
9ae/1009/* | \
9ae/1010/* | \
9ae/1330/* | \
9ae/2005/* | \
9ae/2007/* | \
9ae/2008/* | \
9ae/2009/* | \
9ae/2010/* | \
9ae/2011/* | \
9ae/2012/* | \
9ae/2013/* | \
9ae/2014/* | \
9ae/3008/* | \
9ae/3009/* | \
9ae/3010/* | \
9ae/3011/* | \
9ae/3012/* | \
9ae/3013/* | \
9ae/3014/* | \
9ae/3015/* | \
9ae/3016/* | \
9ae/3024/* | \
9ae/4001/* | \
9ae/4002/* | \
9ae/4003/* | \
9ae/4004/* | \
9ae/4005/* | \
9ae/4006/* | \
9ae/4007/* | \
9ae/4008/* | \
9d6/1/* | \
d9f/1/* | \
d9f/4/* | \
d9f/a2/* | \
d9f/a3/* | \
d9f/a4/* | \
d9f/a5/* | \
d9f/a6/* | \
f03/1/* | \
10af/0/* | \
10af/1/* | \
10af/2/* | \
10af/4/* | \
10af/8/* | \
14f0/c9/* | \
1a86/7523/* | \
1cb0/32/* | \
1cb0/35/* | \
1cb0/38/* | \
2341/36/* | \
2341/8036/* | \
2a03/36/* | \
2a03/40/* | \
2a03/8036/* | \
2a03/8040/* | \
2b2d/ffff/* | \
2e51/0/* | \
2e51/ffff/* | \
2e66/101/* | \
2e66/201/* | \
2e66/202/* | \
2e66/203/* | \
2e66/300/* | \
2e66/302/* | \
3746/ffff/* | \
4234/1/* | \
4234/2/* | \
ffff/0/* | \
# The result is cases of the form
# "vendorid/productid/other" ) | \
# to be matched against the PRODUCT from the calling hotplug event
# and does NOT use ';;' (so falls through to code below).
# The empty string matches when PRODUCT is empty, but does not match
# when PRODUCT is neither empty nor one of the case targets, above

"")
	# 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
