xref: /openbsd/usr.sbin/fw_update/fw_update.sh (revision 78b9b1e6)
1#!/bin/ksh
2#	$OpenBSD: fw_update.sh,v 1.62 2024/11/24 21:27:04 afresh1 Exp $
3#
4# Copyright (c) 2021,2023 Andrew Hewus Fresh <afresh1@openbsd.org>
5#
6# Permission to use, copy, modify, and distribute this software for any
7# purpose with or without fee is hereby granted, provided that the above
8# copyright notice and this permission notice appear in all copies.
9#
10# THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
11# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
12# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
13# ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
14# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
15# ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
16# OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
17
18set -o errexit -o pipefail -o nounset -o noclobber -o noglob
19set +o monitor
20export PATH=/usr/bin:/bin:/usr/sbin:/sbin
21
22CFILE=SHA256.sig
23DESTDIR=${DESTDIR:-}
24FWPATTERNS="${DESTDIR}/usr/share/misc/firmware_patterns"
25
26VNAME=${VNAME:-$(sysctl -n kern.osrelease)}
27VERSION=${VERSION:-"${VNAME%.*}${VNAME#*.}"}
28
29HTTP_FWDIR="$VNAME"
30VTYPE=$( sed -n "/^OpenBSD $VNAME\([^ ]*\).*$/s//\1/p" \
31    /var/run/dmesg.boot | sed '$!d' )
32[ "$VTYPE" = -current ] && HTTP_FWDIR=snapshots
33
34FWURL=http://firmware.openbsd.org/firmware/${HTTP_FWDIR}
35FWPUB_KEY=${DESTDIR}/etc/signify/openbsd-${VERSION}-fw.pub
36
37DRYRUN=false
38integer VERBOSE=0
39DELETE=false
40DOWNLOAD=true
41INSTALL=true
42LOCALSRC=
43ENABLE_SPINNER=false
44[ -t 1 ] && ENABLE_SPINNER=true
45
46integer STATUS_FD=1
47integer WARN_FD=2
48FD_DIR=
49
50unset FTPPID
51unset LOCKPID
52unset FWPKGTMP
53REMOVE_LOCALSRC=false
54DROP_PRIVS=true
55
56status() { echo -n "$*" >&"$STATUS_FD"; }
57warn()   { echo    "$*" >&"$WARN_FD"; }
58
59cleanup() {
60	set +o errexit # ignore errors from killing ftp
61
62	if [ -d "$FD_DIR" ]; then
63		echo "" >&"$STATUS_FD"
64		((STATUS_FD == 3)) && exec 3>&-
65		((WARN_FD   == 4)) && exec 4>&-
66
67		[ -s "$FD_DIR/status" ] && cat "$FD_DIR/status"
68		[ -s "$FD_DIR/warn"   ] && cat "$FD_DIR/warn" >&2
69
70		rm -rf "$FD_DIR"
71	fi
72
73	[ "${FTPPID:-}" ] && kill -TERM -"$FTPPID" 2>/dev/null
74	[ "${LOCKPID:-}" ] && kill -TERM -"$LOCKPID" 2>/dev/null
75	[ "${FWPKGTMP:-}" ] && rm -rf "$FWPKGTMP"
76	"$REMOVE_LOCALSRC" && rm -rf "$LOCALSRC"
77	[ -e "$CFILE" ] && [ ! -s "$CFILE" ] && rm -f "$CFILE"
78}
79trap cleanup EXIT
80
81tmpdir() {
82	local _i=1 _dir
83
84	# The installer lacks mktemp(1), do it by hand
85	if [ -x /usr/bin/mktemp ]; then
86		_dir=$( mktemp -d "${1}-XXXXXXXXX" )
87	else
88		until _dir="${1}.$_i.$RANDOM" && mkdir -- "$_dir" 2>/dev/null; do
89		    ((++_i < 10000)) || return 1
90		done
91	fi
92
93	echo "$_dir"
94}
95
96spin() {
97	if ! "$ENABLE_SPINNER"; then
98		sleep 1
99		return 0
100	fi
101
102	{
103		for p in '/' '-' '\\' '|' '/' '-' '\\' '|'; do
104			echo -n "$p"'\b'
105			sleep 0.125
106		done
107		echo -n " "'\b'
108	}>/dev/tty
109}
110
111fetch() {
112	local _src="${FWURL}/${1##*/}" _dst=$1 _user=_file _exit _error=''
113	local _ftp_errors="$FD_DIR/ftp_errors"
114	rm -f "$_ftp_errors"
115
116	# The installer uses a limited doas(1) as a tiny su(1)
117	set -o monitor # make sure ftp gets its own process group
118	(
119	_flags=-vm
120	case "$VERBOSE" in
121		0|1) _flags=-VM ; exec 2>"$_ftp_errors" ;;
122		  2) _flags=-Vm ;;
123	esac
124
125	if ! "$DROP_PRIVS"; then
126		/usr/bin/ftp -N error -D 'Get/Verify' $_flags -o- "$_src" > "$_dst"
127	elif [ -x /usr/bin/su ]; then
128		exec /usr/bin/su -s /bin/ksh "$_user" -c \
129		    "/usr/bin/ftp -N error -D 'Get/Verify' $_flags -o- '$_src'" > "$_dst"
130	else
131		exec /usr/bin/doas -u "$_user" \
132		    /usr/bin/ftp -N error -D 'Get/Verify' $_flags -o- "$_src" > "$_dst"
133	fi
134	) & FTPPID=$!
135	set +o monitor
136
137	SECONDS=0
138	_last=0
139	while kill -0 -"$FTPPID" 2>/dev/null; do
140		if [[ $SECONDS -gt 12 ]]; then
141			set -- $( ls -ln "$_dst" 2>/dev/null )
142			if [[ $_last -ne $5 ]]; then
143				_last=$5
144				SECONDS=0
145				spin
146			else
147				kill -INT -"$FTPPID" 2>/dev/null
148				_error=" (timed out)"
149			fi
150		else
151			spin
152		fi
153	done
154
155	set +o errexit
156	wait "$FTPPID"
157	_exit=$?
158	set -o errexit
159
160	unset FTPPID
161
162	if ((_exit != 0)); then
163		rm -f "$_dst"
164
165		# ftp doesn't provide useful exit codes
166		# so we have to grep its STDERR.
167		# _exit=2 means don't keep trying
168		_exit=2
169
170		# If it was 404, we might succeed at another file
171		if [ -s "$_ftp_errors" ] && \
172		    grep -q "404 Not Found" "$_ftp_errors"; then
173			_exit=1
174			_error=" (404 Not Found)"
175			rm -f "$_ftp_errors"
176		fi
177
178		warn "Cannot fetch $_src$_error"
179	fi
180
181	# If we have ftp errors, print them out,
182	# removing any cntrl characters (like 0x0d),
183	# and any leading blank lines.
184	if [ -s "$_ftp_errors" ]; then
185		sed -e 's/[[:cntrl:]]//g' \
186		    -e '/./,$!d' "$_ftp_errors" >&"$WARN_FD"
187	fi
188
189	return "$_exit"
190}
191
192# If we fail to fetch the CFILE, we don't want to try again
193# but we might be doing this in a subshell so write out
194# a blank file indicating failure.
195check_cfile() {
196	if [ -e "$CFILE" ]; then
197		[ -s "$CFILE" ] || return 2
198		return 0
199	fi
200	if ! fetch_cfile; then
201		echo -n > "$CFILE"
202		return 2
203	fi
204	return 0
205}
206
207fetch_cfile() {
208	if "$DOWNLOAD"; then
209		set +o noclobber # we want to get the latest CFILE
210		fetch "$CFILE" || return 1
211		set -o noclobber
212		signify -qVep "$FWPUB_KEY" -x "$CFILE" -m /dev/null \
213		    2>&"$WARN_FD" || {
214		        warn "Signature check of SHA256.sig failed"
215		        rm -f "$CFILE"
216			return 1
217		    }
218	elif [ ! -e "$CFILE" ]; then
219		warn "${0##*/}: $CFILE: No such file or directory"
220		return 1
221	fi
222
223	return 0
224}
225
226verify() {
227	check_cfile || return $?
228	# The installer sha256 lacks -C, do it by hand
229	if ! grep -Fqx "SHA256 (${1##*/}) = $( /bin/sha256 -qb "$1" )" "$CFILE"
230	then
231		((VERBOSE != 1)) && warn "Checksum test for ${1##*/} failed."
232		return 1
233	fi
234
235	return 0
236}
237
238# When verifying existing files that we are going to re-download
239# if VERBOSE is 0, don't show the checksum failure of an existing file.
240verify_existing() {
241	local _v=$VERBOSE
242	check_cfile || return $?
243
244	((_v == 0)) && "$DOWNLOAD" && _v=1
245	( VERBOSE=$_v verify "$@" )
246}
247
248devices_in_dmesg() {
249	local IFS
250	local _d _m _dmesgtail _last='' _nl='
251'
252
253	# The dmesg can contain multiple boots, only look in the last one
254	_dmesgtail="$( echo ; sed -n 'H;/^OpenBSD/h;${g;p;}' /var/run/dmesg.boot )"
255
256	grep -v '^[[:space:]]*#' "$FWPATTERNS" |
257	    while read -r _d _m; do
258		[ "$_d" = "$_last" ]  && continue
259		[ "$_m" ]             || _m="${_nl}${_d}[0-9] at "
260		[ "$_m" = "${_m#^}" ] || _m="${_nl}${_m#^}"
261
262		IFS='*'
263		set -- $_m
264		unset IFS
265
266		case $# in
267		    1|2|3) [[ $_dmesgtail = *$1*([!$_nl])${2-}*([!$_nl])${3-}* ]] || continue;;
268		    *) warn "${0##*/}: Bad pattern '${_m#$_nl}' in $FWPATTERNS"; exit 1 ;;
269		esac
270
271		echo "$_d"
272		_last="$_d"
273	    done
274}
275
276firmware_filename() {
277	check_cfile || return $?
278	sed -n "s/.*(\($1-firmware-.*\.tgz\)).*/\1/p" "$CFILE" | sed '$!d'
279}
280
281firmware_devicename() {
282	local _d="${1##*/}"
283	_d="${_d%-firmware-*}"
284	echo "$_d"
285}
286
287lock_db() {
288	local _waited
289	[ "${LOCKPID:-}" ] && return 0
290
291	# The installer doesn't have perl, so we can't lock there
292	[ -e /usr/bin/perl ] || return 0
293
294	set -o monitor
295	perl <<-'EOL' |&
296		no lib ('/usr/local/libdata/perl5/site_perl');
297		use v5.36;
298		use OpenBSD::PackageInfo qw< lock_db >;
299
300		$|=1;
301
302		$0 = "fw_update: lock_db";
303		my $waited = 0;
304		package OpenBSD::FwUpdateState {
305			use parent 'OpenBSD::BaseState';
306			sub errprint ($self, @p) {
307				if ($p[0] && $p[0] =~ /already locked/) {
308					$waited++;
309					$p[0] = " " . $p[0]
310					    if !$ENV{VERBOSE};
311				}
312				$self->SUPER::errprint(@p);
313			}
314
315		}
316		lock_db(0, 'OpenBSD::FwUpdateState');
317
318		say "$$ $waited";
319
320		# Wait for STDOUT to be readable, which won't happen
321		# but if our parent exits unexpectedly it will close.
322		my $rin = '';
323		vec($rin, fileno(STDOUT), 1) = 1;
324		select $rin, '', '', undef;
325EOL
326	set +o monitor
327
328	read -rp LOCKPID _waited
329
330	if ((_waited)); then
331		! ((VERBOSE)) && status "${0##*/}:"
332	fi
333
334	return 0
335}
336
337available_firmware() {
338	check_cfile || return $?
339	sed -n 's/.*(\(.*\)-firmware.*/\1/p' "$CFILE"
340}
341
342installed_firmware() {
343	local _pre="$1" _match="$2" _post="$3" _firmware _fw
344	set -sA _firmware -- $(
345	    set +o noglob
346	    grep -Fxl '@option firmware' \
347		"${DESTDIR}/var/db/pkg/"$_pre"$_match"$_post"/+CONTENTS" \
348		2>/dev/null || true
349	    set -o noglob
350	)
351
352	[ "${_firmware[*]:-}" ] || return 0
353	for _fw in "${_firmware[@]}"; do
354		_fw="${_fw%/+CONTENTS}"
355		echo "${_fw##*/}"
356	done
357}
358
359detect_firmware() {
360	local _devices _last='' _d
361
362	set -sA _devices -- $(
363	    devices_in_dmesg
364	    for _d in $( installed_firmware '*' '-firmware-' '*' ); do
365		firmware_devicename "$_d"
366	    done
367	)
368
369	[ "${_devices[*]:-}" ] || return 0
370	for _d in "${_devices[@]}"; do
371		[ "$_last" = "$_d" ] && continue
372		echo "$_d"
373		_last="$_d"
374	done
375}
376
377add_firmware () {
378	local _f="${1##*/}" _m="${2:-Install}"
379	local _pkgdir="${DESTDIR}/var/db/pkg" _pkg
380	FWPKGTMP="$( tmpdir "${DESTDIR}/var/db/pkg/.firmware" )"
381	local _flags=-vm
382	case "$VERBOSE" in
383		0|1) _flags=-VM ;;
384		2|3) _flags=-Vm ;;
385	esac
386
387	ftp -N "${0##/}" -D "$_m" "$_flags" -o- "file:${1}" |
388		tar -s ",^\+,${FWPKGTMP}/+," \
389		    -s ",^firmware,${DESTDIR}/etc/firmware," \
390		    -C / -zxphf - "+*" "firmware/*"
391
392
393	[ -s "${FWPKGTMP}/+CONTENTS" ] &&
394	    _pkg="$( sed -n '/^@name /{s///p;q;}' "${FWPKGTMP}/+CONTENTS" )"
395
396	if [ ! "${_pkg:-}" ]; then
397		warn "Failed to extract name from $1, partial install"
398		rm -rf "$FWPKGTMP"
399		unset FWPKGTMP
400		return 1
401	fi
402
403	if [ -e "$_pkgdir/$_pkg" ]; then
404		warn "Failed to register: $_pkgdir/$_pkg is not firmware"
405		rm -rf "$FWPKGTMP"
406		unset FWPKGTMP
407		return 1
408	fi
409
410	ed -s "${FWPKGTMP}/+CONTENTS" <<EOL
411/^@comment pkgpath/ -1a
412@option manual-installation
413@option firmware
414@comment install-script
415.
416w
417EOL
418
419	chmod 755 "$FWPKGTMP"
420	mv "$FWPKGTMP" "$_pkgdir/$_pkg"
421	unset FWPKGTMP
422}
423
424remove_files() {
425	local _r
426	# Use rm -f, not removing files/dirs is probably not worth failing over
427	for _r in "$@" ; do
428		if [ -d "$_r" ]; then
429			# The installer lacks rmdir,
430			# but we only want to remove empty directories.
431			set +o noglob
432			[ "$_r/*" = "$( echo "$_r"/* )" ] && rm -rf "$_r"
433			set -o noglob
434		else
435			rm -f "$_r"
436		fi
437	done
438}
439
440delete_firmware() {
441	local _cwd _pkg="$1" _pkgdir="${DESTDIR}/var/db/pkg"
442
443	# TODO: Check hash for files before deleting
444	((VERBOSE > 2)) && echo -n "Uninstall $_pkg ..."
445	_cwd="${_pkgdir}/$_pkg"
446
447	if [ ! -e "$_cwd/+CONTENTS" ] ||
448	    ! grep -Fxq '@option firmware' "$_cwd/+CONTENTS"; then
449		warn "${0##*/}: $_pkg does not appear to be firmware"
450		return 2
451	fi
452
453	set -A _remove -- "${_cwd}/+CONTENTS" "${_cwd}"
454
455	while read -r _c _g; do
456		case $_c in
457		@cwd) _cwd="${DESTDIR}$_g"
458		  ;;
459		@*) continue
460		  ;;
461		*) set -A _remove -- "$_cwd/$_c" "${_remove[@]}"
462		  ;;
463		esac
464	done < "${_pkgdir}/${_pkg}/+CONTENTS"
465
466	remove_files "${_remove[@]}"
467
468	((VERBOSE > 2)) && echo " done."
469
470	return 0
471}
472
473unregister_firmware() {
474	local _d="$1" _pkgdir="${DESTDIR}/var/db/pkg" _fw
475
476	set -A installed -- $( installed_firmware '' "$d-firmware-" '*' )
477	if [ "${installed:-}" ]; then
478		for _fw in "${installed[@]}"; do
479			((VERBOSE)) && echo "Unregister $_fw"
480			"$DRYRUN" && continue
481			remove_files \
482			    "$_pkgdir/$_fw/+CONTENTS" \
483			    "$_pkgdir/$_fw/+DESC" \
484			    "$_pkgdir/$_fw/"
485		done
486		return 0
487	fi
488
489	return 1
490}
491
492usage() {
493	echo "usage: ${0##*/} [-adFlnv] [-p path] [driver | file ...]"
494	exit 1
495}
496
497ALL=false
498LIST=false
499while getopts :adFlnp:v name
500do
501	case "$name" in
502	a) ALL=true ;;
503	d) DELETE=true ;;
504	F) INSTALL=false ;;
505	l) LIST=true ;;
506	n) DRYRUN=true ;;
507	p) FWURL="$OPTARG" ;;
508	v) ((++VERBOSE)) ;;
509	:)
510	    warn "${0##*/}: option requires an argument -- -$OPTARG"
511	    usage
512	    ;;
513	?)
514	    warn "${0##*/}: unknown option -- -$OPTARG"
515	    usage
516	    ;;
517	esac
518done
519shift $((OPTIND - 1))
520
521# When listing, provide a clean output
522"$LIST" && VERBOSE=1 ENABLE_SPINNER=false
523
524# Progress bars, not spinner When VERBOSE > 1
525((VERBOSE > 1)) && ENABLE_SPINNER=false
526
527if [[ $FWURL != @(ftp|http?(s))://* ]]; then
528	FWURL="${FWURL#file:}"
529	! [ -d "$FWURL" ] &&
530	    warn "The path must be a URL or an existing directory" &&
531	    exit 1
532	DOWNLOAD=false
533	FWURL="file:$FWURL"
534fi
535
536if [ -x /usr/bin/id ] && [ "$(/usr/bin/id -u)" != 0 ]; then
537	if ! "$INSTALL" || "$LIST"; then
538		# When we aren't in the installer,
539		# allow downloading as the current user.
540		DROP_PRIVS=false
541	else
542		warn "need root privileges"
543		exit 1
544	fi
545fi
546
547set -sA devices -- "$@"
548
549FD_DIR="$( tmpdir "${DESTDIR}/tmp/${0##*/}-fd" )"
550# When being verbose, save the status line for the end.
551if ((VERBOSE)); then
552	exec 3>"${FD_DIR}/status"
553	STATUS_FD=3
554fi
555# Control "warning" messages to avoid the middle of a line.
556# Things that we don't expect to send to STDERR
557# still go there so the output, while it may be ugly, isn't lost
558exec 4>"${FD_DIR}/warn"
559WARN_FD=4
560
561status "${0##*/}:"
562
563if "$DELETE"; then
564	! "$INSTALL" && warn "Cannot use -F and -d" && usage
565	lock_db
566
567	# Show the "Uninstall" message when just deleting not upgrading
568	((VERBOSE)) && VERBOSE=3
569
570	set -A installed
571	if [ "${devices[*]:-}" ]; then
572		"$ALL" && warn "Cannot use -a and devices/files" && usage
573
574		set -A installed -- $(
575		    for d in "${devices[@]}"; do
576			f="${d##*/}"  # only care about the name
577			f="${f%.tgz}" # allow specifying the package name
578			[ "$( firmware_devicename "$f" )" = "$f" ] && f="$f-firmware"
579
580			set -A i -- $( installed_firmware '' "$f-" '*' )
581
582			if [ "${i[*]:-}" ]; then
583				echo "${i[@]}"
584			else
585				warn "No firmware found for '$d'"
586			fi
587		    done
588		)
589	elif "$ALL"; then
590		set -A installed -- $( installed_firmware '*' '-firmware-' '*' )
591	else
592		set -A installed -- $(
593		    set -- $( devices_in_dmesg )
594		    for f in $( installed_firmware '*' -firmware- '*' ); do
595		        n="$( firmware_devicename "$f" )"
596		        for d; do
597		            [ "$d" = "$n" ] && continue 2
598		        done
599		        echo "$f"
600		    done
601		)
602	fi
603
604	status " delete "
605
606	comma=''
607	if [ "${installed:-}" ]; then
608		for fw in "${installed[@]}"; do
609			status "$comma$( firmware_devicename "$fw" )"
610			comma=,
611			if "$DRYRUN"; then
612				((VERBOSE)) && echo "Delete $fw"
613			elif "$LIST"; then
614				echo "$fw"
615			else
616				delete_firmware "$fw" || {
617					status " ($fw failed)"
618					continue
619				}
620			fi
621		done
622	fi
623
624	[ "$comma" ] || status none
625
626	# no status when listing
627	"$LIST" && rm -f "$FD_DIR/status"
628
629	exit
630fi
631
632! "$INSTALL" && ! "$LIST" && LOCALSRC="${LOCALSRC:-.}"
633
634if [ ! "$LOCALSRC" ]; then
635	LOCALSRC="$( tmpdir "${DESTDIR}/tmp/${0##*/}" )"
636	REMOVE_LOCALSRC=true
637fi
638
639CFILE="$LOCALSRC/$CFILE"
640
641if [ "${devices[*]:-}" ]; then
642	"$ALL" && warn "Cannot use -a and devices/files" && usage
643elif "$ALL"; then
644	set -sA devices -- $( available_firmware )
645else
646	((VERBOSE > 1)) && echo -n "Detect firmware ..."
647	set -sA devices -- $( detect_firmware )
648	((VERBOSE > 1)) &&
649	    { [ "${devices[*]:-}" ] && echo " found." || echo " done." ; }
650fi
651
652
653set -A add ''
654set -A update ''
655kept=''
656unregister=''
657
658"$LIST" && ! "$INSTALL" &&
659    echo "$FWURL/${CFILE##*/}"
660
661if [ "${devices[*]:-}" ]; then
662	lock_db
663	for f in "${devices[@]}"; do
664		d="$( firmware_devicename "$f" )"
665
666		if "$LIST" && "$INSTALL"; then
667			echo "$d"
668			continue
669		fi
670
671		verify_existing=true
672		if [ "$f" = "$d" ]; then
673			f=$( firmware_filename "$d" ) || {
674				# Fetching the CFILE here is often the
675				# first attempt to talk to FWURL
676				# If it fails, no point in continuing.
677				if (($? > 1)); then
678					status " failed."
679					exit 1
680				fi
681
682				# otherwise we can try the next firmware
683				continue
684			}
685			if [ ! "$f" ]; then
686				if "$INSTALL" && unregister_firmware "$d"; then
687					unregister="$unregister,$d"
688				else
689					warn "Unable to find firmware for $d"
690				fi
691				continue
692			fi
693		elif ! "$INSTALL" && ! grep -Fq "($f)" "$CFILE" ; then
694			warn "Cannot download local file $f"
695			exit 1
696		else
697			# Don't verify files specified on the command-line
698			verify_existing=false
699		fi
700
701		if "$LIST"; then
702			echo "$FWURL/$f"
703			continue
704		fi
705
706		set -A installed
707		if "$INSTALL"; then
708			set -A installed -- \
709			    $( installed_firmware '' "$d-firmware-" '*' )
710
711			if [ "${installed[*]:-}" ]; then
712				for i in "${installed[@]}"; do
713					if [ "${f##*/}" = "$i.tgz" ]; then
714						((VERBOSE > 2)) \
715						    && echo "Keep $i"
716						kept="$kept,$d"
717						continue 2
718					fi
719				done
720			fi
721		fi
722
723		# Fetch an unqualified file into LOCALSRC
724		# if it doesn't exist in the current directory.
725		if [ "$f" = "${f##/}" ] && [ ! -e "$f" ]; then
726			f="$LOCALSRC/$f"
727		fi
728
729		if "$verify_existing" && [ -e "$f" ]; then
730			pending_status=false
731			if ((VERBOSE == 1)); then
732				echo -n "Verify ${f##*/} ..."
733				pending_status=true
734			elif ((VERBOSE > 1)) && ! "$INSTALL"; then
735				echo "Keep/Verify ${f##*/}"
736			fi
737
738			if "$DRYRUN" || verify_existing "$f"; then
739				"$pending_status" && echo " done."
740				if ! "$INSTALL"; then
741					kept="$kept,$d"
742					continue
743				fi
744			elif "$DOWNLOAD"; then
745				"$pending_status" && echo " failed."
746				((VERBOSE > 1)) && echo "Refetching $f"
747				rm -f "$f"
748			else
749				"$pending_status" && echo " failed."
750				continue
751			fi
752		fi
753
754		if [ "${installed[*]:-}" ]; then
755			set -A update -- "${update[@]}" "$f"
756		else
757			set -A add -- "${add[@]}" "$f"
758		fi
759
760	done
761fi
762
763if "$LIST"; then
764	# No status when listing
765	rm -f "$FD_DIR/status"
766	exit
767fi
768
769if "$INSTALL"; then
770	status " add "
771	action=Install
772else
773	status " download "
774	action=Download
775fi
776
777comma=''
778[ "${add[*]}" ] || status none
779for f in "${add[@]}" _update_ "${update[@]}"; do
780	[ "$f" ] || continue
781	if [ "$f" = _update_ ]; then
782		comma=''
783		"$INSTALL" || continue
784		action=Update
785		status "; update "
786		[ "${update[*]}" ] || status none
787		continue
788	fi
789	d="$( firmware_devicename "$f" )"
790	status "$comma$d"
791	comma=,
792
793	pending_status=false
794	if [ -e "$f" ]; then
795		if "$DRYRUN"; then
796			((VERBOSE)) && echo "$action ${f##*/}"
797		else
798			if ((VERBOSE == 1)); then
799				echo -n "Install ${f##*/} ..."
800				pending_status=true
801			fi
802		fi
803	elif "$DOWNLOAD"; then
804		if "$DRYRUN"; then
805			((VERBOSE)) && echo "Get/Verify ${f##*/}"
806		else
807			if ((VERBOSE == 1)); then
808				echo -n "Get/Verify ${f##*/} ..."
809				pending_status=true
810			fi
811			fetch  "$f" &&
812			verify "$f" || {
813				integer e=$?
814
815				"$pending_status" && echo " failed."
816				status " failed (${f##*/})"
817
818				if ((VERBOSE)) && [ -s "$FD_DIR/warn" ]; then
819					cat "$FD_DIR/warn" >&2
820					rm -f "$FD_DIR/warn"
821				fi
822
823				# Fetch or verify exited > 1
824				# which means we don't keep trying.
825				((e > 1)) && exit 1
826
827				continue
828			}
829		fi
830	elif "$INSTALL"; then
831		warn "Cannot install ${f##*/}, not found"
832		continue
833	fi
834
835	if ! "$INSTALL"; then
836		"$pending_status" && echo " done."
837		continue
838	fi
839
840	if ! "$DRYRUN"; then
841		if [ "$action" = Update ]; then
842			for i in $( installed_firmware '' "$d-firmware-" '*' )
843			do
844				delete_firmware "$i" || {
845					"$pending_status" &&
846					    echo -n " (remove $i failed)"
847					status " (remove $i failed)"
848
849					continue
850				}
851				#status " (removed $i)"
852			done
853		fi
854
855		add_firmware "$f" "$action" || {
856			"$pending_status" && echo " failed."
857			status " failed (${f##*/})"
858			continue
859		}
860	fi
861
862	if "$pending_status"; then
863		if [ "$action" = Install ]; then
864			echo " installed."
865		else
866			echo " updated."
867		fi
868	fi
869done
870
871[ "$unregister" ] && status "; unregister ${unregister:#,}"
872[ "$kept"       ] && status "; keep ${kept:#,}"
873
874exit 0
875