summaryrefslogtreecommitdiffstats
path: root/net/nut/files/libhid-ups.hotplug
blob: aad52cf0bee7261e071c4c0608f1902d777177ab (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
#!/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 -s -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 -s -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
			logger -s -t nut-usb-hotplug "Hotplug restart disabled starting '$ups' when processing '$ACTION' for '$DEVNAME'"
			# 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
			logger -s -t nut-usb-hotplug "Unable to enable access to '$ups' when processing '$ACTION' for '$DEVNAME'"
			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
				logger -s -t nut-usb-hotplug "Failed to start '$ups' processing '$ACTION' for '$DEVNAME'"
				return 0
			fi
		else
			logger -s -t nut-usb-hotplug "Hotplug became disabled while processing '$ACTION' for '$DEVNAME'."
			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" || {
			logger -s -t nut-usb-hotplug "Failed to stop nut-driver instance (UPS) '$ups'."
			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
		logger -s -t nut-usb-hotplug "Configured vendorid for '$ups' is not valid"
		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
		logger -s -t nut-usb-hotplug "Configured productid for '$ups' is not valid"
		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
				logger -s -t nut-usb-hotplug "Hotplug restart disabled starting '$ups' when processing '$ACTION' for '$DEVNAME'"
				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_error "Failed to reload unmatched driver instance (UPS) '$ups' in nut_driver_config" libhid-ups nut-usb-hotplug
					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
				logger -s -t nut-usb-hotplug "NUT server disabled when processing '$ACTION' for '$DEVNAME'."
				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
			logger -s -t nut-usb-hotplug "Hotplug vendor id for '$ups' is not valid"
			nd_driver_config_error=true
			return 0
		fi
		if ! check_valid_hex_number "$pprodid"; then
			logger -s -t nut-usb-hotplug "Hotplug product id for '$ups' is not valid"
			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" || {
		logger -s -t nut-usb-hotplug "Failed to set STATEPATH"
		return 1
	}
	# sets RUNAS
	find_runas "upsd" "nut_server" || {
		logger -s -t nut-usb-hotplug "Failed to set RUNAS"
		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
		logger -s -t nut-usb-hotplug "Hotplug restart disabled when processing '$ACTION' for '$DEVNAME'"
		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
		logger -s -t nut-usb-hotplug "NUT server disabled when processing '$ACTION' for '$DEVNAME'."
		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
### insert libhid-ups.parsed-usermap content here ###
# 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
		logger -s -t nut-usb-hotplug "Hotplug restart disabled when processing '$ACTION' for '$DEVNAME'"
		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
		logger -s -t nut-usb-hotplug "NUT server disabled when processing '$ACTION' for '$DEVNAME'."
		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