#!/bin/sh -f
#
# Copyright (c) 2008 - 2016
# Dominic Fandrey <kamikaze@bsdforen.de>
#
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions
# are met:
# 1. Redistributions of source code must retain the above copyright
#    notice, this list of conditions and the following disclaimer.
#
# THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR
# IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
# OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
# IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT,
# INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
# NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
# THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
#

# AMD parameters
amd="/usr/sbin/amd"
amq="/usr/sbin/amq"
a="/var/run/automounter.amd"
c=4
w=2
l="/var/run/automounter.amd.log"
directory="/var/run/automounter.amd.mnt"
map="/var/run/automounter.amd.map"

# The amd map that the dynamic mounts are appended to.
static_map="/etc/amd.map"

# RPC tools, required for AMD operation.
rpcbind="/usr/sbin/rpcbind"
rpcinfo="/usr/bin/rpcinfo"

# Logger command.
logger="/usr/bin/logger -st automounter"

# The location of the devfs.
devfs="/dev"

# A file to remember for which partitions automounter has been configured.
#
# Lines in the file conform to the following format:
#	<device>;<label>
#
# From the glabel detector perspective <device> is the geom consumer and
# <label> the provider.
#
# From the probe detector perspective <device> is the geom provider and
# <label> is a virtual convenience label.
nodes="/var/tmp/automounter.nodes"

# A temporary file for the partitions of the last run.
oldnodes="/tmp/automounter.nodes.old"

# A temporary mountpoint for probing.
probe="/tmp/automounter.probe"

# Used to store a list of device that have already probed and shouldn't be
# reprobed. This is used to prevent the adding of devices that have been
# blacklisted through blacklist_nodes.
probed="/tmp/automounter.probed"

# Used to keep a list of devices and their appearance time to screen devd
# events only occuring due to device probing.
screen="/tmp/automounter.screen"

# The pid of amd.
pidfile="/var/run/automounter.amd.pid"

# This is where the links will be created to access the file systems.
linkdir="/media"

# This is where the folders to mount the file systems into will be created.
mountdir="/var/run/automounter.mnt"

# Lock file.
lock="/var/run/automounter.lock"

# Geli is inactive, set this to anything but 0 to activate it.
geli=0

# A file to remember which geli encrypted providers belong to which node.
#
# The file is in the following format:
# 	<providerName>;<mdDevice>;<keyLabel>
#
# The fields are defined as follows:
# 	providerName	is the filename of the image or device link.
#	mdDevice	is the device node of the file backed memory disk as
#			it was returned by mdconfig or the device that was
#			referenced by a /dev relative symlink.
#	keyLabel	is the label of the key providing device.
# 	
geli_nodes="/var/run/automounter.geli.nodes"

# A temporary file to recognize which nodes were available during the last run.
geli_oldnodes="/tmp/automounter.geli.oldnodes"

# A file to remember where we can find which keys.
#
# The format is:
#	<label>;<keyName>
geli_availablekeys="/var/run/automounter.geli.keys"

# GEOM ELI encrypted images and device link location.
geli_images="/var/geli/images"

# GEOM ELI keys location.
geli_keys=".geli/keys"

# Timeout in seconds for aquiring the lock.
timeout=10

# If set to 1 glabel file system detection is turned on.
detect_glabel=1

# If set to 1 iso9660 hard coding for devices is turned on.
detect_iso9660=1

# If set to 1 probe file system detection is turned on.
detect_probe=1

# The default mount options.
mount_options=rw,noatime,noexec

# Mount isos as cd9660.
iso9660=cd9660
iso9660_options=ro

# Dirty fusefs bug workaround.
evil_fuse=0

# A comma separated list of file system types to probe geom providers for.
probe_types=ufs,msdosfs,iso9660,ntfs,ext2fs

# A comma separated list of device patterns that are hardcoded to iso9660.
iso9660_devs="acd*,cd*"

# Config file.
config="/usr/local/etc/automounter.conf"


# Read config file.
if [ -e "$config" -a ! -d "$config" ]; then
	. "$config"
fi

# devfs must not end with /
devfs=$(realpath $devfs)

# Freeze the current configuration
readonly amd amq a c w l directory map static_map rpcbind rpcinfo logger devfs
readonly nodes oldnodes probe probed screen pidfile linkdir mountdir lock
readonly geli geli_nodes geli_oldnodes geli_availablekeys geli_images geli_keys
readonly timeout detect_glabel detect_iso9660 detect_probe mount_options
readonly evil_fuse probe_types iso9660_devs config

IFS='
'

# Native error return codes.
readonly ERR_CMD_UNKNOWN=1
readonly ERR_NOT_STARTED=2
readonly ERR_MOUNT_FS_MISSING=3
readonly ERR_UMOUNT_ACTIVE=4
readonly ERR_LIST_LOCKED=5
readonly ERR_RPC_FAIL=6
readonly ERR_AMD_FAIL=7

# Locking error return codes.
readonly EX_USAGE=64
readonly EX_SOFTWARE=70
readonly EX_OSERR=71
readonly EX_CANTCREAT=73
readonly EX_TEMPFAIL=75

# Open a file descriptor to the null devices.
exec 3> "$devfs/null"

#
# URL decode, used to make prettier labels under FreeBSD 10.
#
# Does not support "+" decoding.
#
# @param @
#	The strings to decode, if not given stdin is decoded
#
urldecode() {
	if [ $# -gt 0 ]; then
		echo "$@" | urldecode
		return
	fi
	awk '
	# Create a character symbol table
	BEGIN {
		i = -1;
		while (++i < 256) {
			chars[sprintf("%%%02X", i)] = i
			chars[sprintf("%%%02x", i)] = i
		}
	}
	
	# Replace urlencoded characters
	{
		i = 1
		while (i <= length($0)) {
			char = substr($0, i, 3);
			if (char in chars) {
				printf("%c", chars[char])
				i += 3
			} else {
				printf("%.1s", char)
				i++
			}
		}
		printf(ORS)
	}'
}

#
# Produces a list of geom device nodes on stdout.
#
# Only outputs leaf nodes that are not mounted.
#
# NOTE, this breaks for labels containing a pipe '|' or or colon ':'
# character.
#
geomDevices() {
	/usr/sbin/gstat -b -I0 | /usr/bin/awk '
		# Build a device node tree.
		NR > 2 && $10 !~ "/" {
			# Add to list of device nodes.
			nodes[NR] = $10;
			# Build a tree containing device occurrences.
			do {
				tree[$10]++;
				sub("[a-z.]+[0-9]*\$", "", $10);
			} while ($10)
		}
		# Output leaves.
		END {
			for (i in nodes) {
				if (tree[nodes[i]] == 1) {
					print nodes[i];
				}
			}
		}
	' | /usr/bin/grep -Exv "$(
		(/sbin/mount -p; /usr/sbin/swapinfo) | /usr/bin/sed -nE "$(
			geomLabels | /usr/bin/sed 's|.*|s:^/dev/(&)(	+/\| +[0-9]).*:&:p|'
		)"
	)"
}

#
# Produces a list of geom labels on stdout.
#
# The labels follow this format:
#	<device>|<label>
#
geomLabels() {
	/sbin/glabel list | /usr/bin/awk '
		/Consumers:/ {name = 1}
		/Geom name:/ {sub("Geom name: ", ""); printf; name = 0}
		! name && /1. Name:/ {sub("1. Name: ", "|");print}
	'
}

#
# Removes a set of media links.
#
# @param 1
#	The current mode of operation (i.e. update or stop)
# @param @
#	Expects a list of labels and devices in the form:
#		[<label> <device>]*
#
rmLinks() {
	local mode label pretty device type

	mode="$1"
	shift

	while [ -n "$1" ]; do
		label="$1"
		device="$2"
		type="${label%%/*}"
		pretty="$(urldecode "$label")"

		# Prepare for the next iteration.
		shift 2

		# Make sure this device is not mounted.
		/sbin/umount -f "$(realpath $mountdir)/$label" >&3 2>&3

		# Remove the stale label.
		echo "$mode: remove <$pretty> [$device]" | eval $logger
		/bin/rm "$linkdir/$pretty"
		/bin/rm "$linkdir/$label" 2>&3 # Pre 1.5 style links
		/bin/rm "$linkdir/${devfs##*/}/$device.$type"
		/bin/rm "$linkdir/${devfs##*/}/$device"
		/bin/rmdir "$linkdir/${devfs##*/}" 2>&3
		/bin/rmdir "$linkdir/$type" 2>&3
		/bin/rmdir "$mountdir/$label"
		/bin/rmdir "$mountdir/$type" 2>&3
	done
}

#
# Creates a set of media links.
#
# @param @
#	Expects a list of labels and devices in the form:
#		[<label> <device>]*
#
createLinks() {
	local label pretty device type

	while [ -n "$1" ]; do
		label="$1"
		device="$2"
		type="${label%%/*}"
		pretty="$(urldecode "$label")"

		# Prepare for the next iteration.
		shift 2
		
		# Create mount hooks.
		echo "update: add <$pretty> [$device]" | eval $logger
		/bin/mkdir -p "$mountdir/$label"

		# Inherit mount node owner from device.
		/usr/sbin/chown "$(
			/usr/bin/stat -f %u:%g "$devfs/$label" 2>&3 \
			|| /usr/bin/stat -f %u:%g "$devfs/$device"
		)" "$mountdir/$label"

		# Inherit permissions from device. Add the executable
		# bit if read or write are permitted.
		/bin/chmod "$(
			(
				/usr/bin/stat -f 0%Lp "$devfs/$label" 2>&3 \
				|| /usr/bin/stat -f 0%Lp "$devfs/$device"
			) | /usr/bin/tr '246' '357'
		)" "$mountdir/$label"

		# Create the links that invoke amd.
		/bin/mkdir -p "$linkdir/$type"
		/bin/mkdir -p "$linkdir/${devfs##*/}"
		/bin/ln -s "$directory/$hash" "$linkdir/$pretty"
		/bin/ln -s "$directory/$hash" "$linkdir/${devfs##*/}/$device.$type"
		/bin/ln -s "$directory/$hash" "$linkdir/${devfs##*/}/$device"
	done
}

#
# This function is forked by geliUpdate() to unmount images that do not longer
# have a key available.
#
# @param 1
#	The label of the key holding file system.
# @param 2
#	The name of the geli provider to unmount.
# @param 3
#	The device the geli provider is available as.
#
geliUnmount() {
	local line label pretty name device
	label="$1"
	name="$2"
	device="$3"
	line="$name;$device;$label"
	pretty="$(urldecode "$label")"
	# Attempt to detach until successful or until the
	# key has returned.
	while true; do
		# Skip if the key to this image has returned.
		/usr/bin/lockf "$lock" /bin/sh -c "
			if /usr/bin/grep -qFx '$label;$name' \
				'$geli_availablekeys'; then
				exit 0
			else
				/sbin/geli detach '$device' \
					2> '$devfs/null' \
					|| exit 1
				/bin/mv '$geli_nodes' \
					'$geli_oldnodes'
				/usr/bin/grep -vFx '$line' \
					'$geli_oldnodes' \
					> '$geli_nodes'
				/bin/rm '$geli_oldnodes'
				exit 2
			fi
		"
		case $? in
		0)
			# The key is back, no more
			# need to detach.
			exit 0
			;;
		2)
			# Detach succeeded.
			echo "geli: remove provider <$name> [$device]" \
				"with key from <$pretty>" | eval $logger
			break
			;;
		esac
		/bin/sleep "${w:-1}"
	done

	# Hang around to remove the device node for the image.
	# Unless it's a device link.
	if [ -e "$geli_images/$name" ]; then
		until /sbin/mdconfig -du "$device"; do
			/bin/sleep "${w:-1}"
		done
	fi
}

#
# This function checks whether the given device or label is blacklisted.
#
# @param 1
#	The device name of the discovered node.
# @param 2
#	The label of the discovered node.
# @param devices_blacklist
#	A list of glob patterns to check the device against.
# @param nodes_blacklist
#	A list glob patterns to check label nodes against.
# @return
#	0 if everything goes right
#	1 if the device is blacklisted
#	2 if the label is blacklisted
#
checkBlacklists() {
	local device label
	device="$1"
	label="$2"

	# Check weather the device is blacklisted.
	if [ -n "$devices_blacklist" ]; then
		eval "
			case '$device' in
			$devices_blacklist)
				return 1
				;;
			esac
		"
	fi

	# Check weather the partition is blacklisted.
	if [ -n "$nodes_blacklist" ]; then
		eval "
			case '$label' in
			$nodes_blacklist)
				return 2
				;;
			esac
		"
	fi
}

#
# This checks whether a revisit after cleanup is needed, because the device
# was previously discovered with a different label.
#
# @param 1
#	The name of the detector.
# @param 2
#	Set this if it is the revisit.
# @param device
#	The name of the device
# @param label
#	The label of the current device
# @param revisit
#	Add the detector if a second update run will be required to track a
#	label change.
# @return
#	0 (true)	if a revisit is required
#	1 (false)	otherwise
#
revisitNeeded() {
	test -n "$2" && return 1

	# If this was alredy discovered with a different label,
	# call for a revisit after the cleanup phase.
	if /usr/bin/grep -q "^$device;" "$oldnodes" \
		&& ! /usr/bin/grep -qFx "$device;$label" "$oldnodes"
	then
		# If not yet in the revisit list, add this detector.
		if [ "$revisit" = "${revisit%$1}" ]; then
			revisit="$revisit${revisit:+$IFS}$1"
		fi
		return 0
	fi

	return 1
}

#
# This function records a node, creates the map file entry and calls
# createLinks. The idea is to separate this code from the device discovery
# code in the update() function.
#
# @param 1
#	The device name of the discovered node.
# @param 2
#	The label of the discovered node.
# @param devices_blacklist
#	A list of glob patterns to check the device against.
# @param nodes_blacklist
#	A list glob patterns to check label nodes against.
# @return
#	0 if everything goes right
#	1 if the device is blacklisted
#	2 if the label is blacklisted
#	3 if the file system type is not known, i.e. the label matches label/*
#
writeNode() {
	local device label type hash
	device="$1"
	label="$2"

	# Check weather device or label are blacklisted.
	checkBlacklists "$device" "$label" || return $?

	type="${label%%/*}"
	mount="$type"
	options="$mount_options"

	# Skip on unknown types.
	test "$type" = "label" && return 3

	# Remember node.
	echo "$device;$label" >> "$nodes"

	# Create node hash for amd.
	hash="$(/sbin/md5 -qs "$device;$label")"

	echo "
# $label
$hash type:=program;fs:=\"$mountdir/$label\";\\
mount:=\"$0 mount mount $hash\";\\
unmount:=\"$0 umount umount $hash\"
" >> "$map"

	# Skip if already present.
	if ! /usr/bin/grep -qFx "$device;$label" "$oldnodes"; then
		# Create the media links.
		createLinks "$label" "$device"
	fi
	return 0
}

#
# Make a read-only mount in the probe directory. Make sure to run probeUnmount
# after succeeding.
#
# @param 1
#	The file system type to use for mounting.
# @param 2
#	The device to mount. This may also be a label.
# @return
#	Whatever the mount command returns, 0 for success,
#	something else otherwise.
#
probeMount() {
	local type mount options device line status
	type="$1"
	device="$2"

	# Create probing directory.
	/bin/mkdir -p "$probe"

	# Deal with devices given as a label.
	line="$(/usr/bin/grep -x ".*;$device" "$nodes")"
	if [ -n "$line" ]; then
		device="${line%%;*}"
	fi

	# Check for config file settings.
	mount="$type"
	options="$mount_options"
	if [ -n "$(eval "echo \"\$$type\"")" ]; then
		mount="$(eval "echo \"\$$type\"")"
	fi
	if [ -n "$(eval "echo \"\$${type}_options\"")" ]; then
		options="$(eval "echo \"\$${type}_options\"")"
	fi

	# Run the mount command.
	/sbin/mount -t "$mount" -o "$options${options:+,}ro" \
	            "$devfs/$device" "$probe" 2>&3
	status=$?
	if [ $status -ne 0 ]; then
		/bin/rmdir "$probe"
	fi
	return $status
}

#
# Force unmount a probe mount.
#
probeUnmount() {
	/sbin/umount -f "$probe" 2>&3
	/bin/rmdir "$probe"
}

#
# Detect already mounted devices and call writeNode().
#
# @param devices_blacklist
#	A list of glob patterns to check the device against.
# @param nodes_blacklist
#	A list glob patterns to check label nodes against.
#
mountedDetect() {
	local line device label

	# Keep mounted nodes.
	for line in $(/bin/cat "$oldnodes"); do
		/usr/bin/grep -qFx "$line" "$nodes" && continue

		device="${line%%;*}"
		label="${line##*;}"

		# Mounted nodes will not be detected by glabelDetect(),
		# because the label provider is removed as soon as the
		# consumer is mounted directly, which is what is done since
		# 1.3.6 to avoid problems with broken labels.
		if /sbin/mount | /usr/bin/grep -qF "on $(realpath $mountdir)/$label ("; then
			writeNode "$device" "$label"
			# Append to the list of already probed devices.
			echo "$device$IFS$label" >> "$probed"
		fi
	done
}

#
# Detect labeled devices and call writeNode().
#
# @param 1
#	If set this is considered a revisit.
# @param devices_blacklist
#	A list of glob patterns to check the devices against.
# @param nodes_blacklist
#	A list glob patterns to check label nodes against.
# @param revisit
#	Add glabel if a second update run will be required
#	to track a label change.
#
glabelDetect() {
	# Check whether glabel based file system discovery has been disabled.
	test "$detect_glabel" != "1" && return 0

	local line device label

	# Add new mounts.
	for line in $(geomLabels); do
		device="${line%%|*}"
		label="${line#*|}"

		# Use the device name for empty labels.
		test -z "${label#*/}" && label="$label$device"

		# Skip probed labels. Do not check the device node
		# in the list of probed devices to be able to follow
		# label changes.
		# Instead check for the device in the list of already
		# discovered nodes.
		if /usr/bin/grep -qFx "$label" "$probed" \
			|| /usr/bin/grep -q "^$device;" "$nodes"; then
			continue
		fi

		# Do not probe device IDs.
		case "$label" in
		*id/*)
			continue
		;;
		esac

		# Record the device to keep the following detectors from
		# probing.
		echo "$device" >> "$probed"

		# If this was alredy discovered with a different label, e.g.
		# because the label took too long to be discovered or was
		# changed, call for a revisit after the cleanup phase.
		if revisitNeeded glabel "$1"; then
			continue
		fi

		# Write the node.
		writeNode "$device" "$label"

		# Append to the list of already probed devices.
		echo "$label" >> "$probed"
	done
}

#
# Detect iso9660 devices without a disk or label and call writeNode().
#
# @param 1
#	If set this is considered a revisit.
# @param devices_blacklist
#	A list of glob patterns to check the devices against.
# @param nodes_blacklist
#	A list glob patterns to check label nodes against.
# @param revisit
#	Add iso9660 if a second update run will be required to track the
#	disappearance of a label.
#
iso9660Detect() {
	# Check whether iso9660 hard coding has been disabled.
	test "$detect_iso9660" != "1" && return 0

	local device label dev_pattern

	# Change the iso9660_devs format into something that can be used in a
	# case statement.
	dev_pattern="$(
		echo "$iso9660_devs" | /usr/bin/sed -E \
			-e 's/^[[:space:]]*,//' -e 's/,[[:space:]]*$//' \
			-e 's/,/|/g' -e 's,([[:space:]\\]),\\\1,g'
	)"

	# Add new mounts.
	for device in $(geomDevices); do
		# This is in the list of already probed devices.
		if /usr/bin/grep -qFx "$device" "$probed"; then
			continue
		fi

		# Skip if the device is not in the pattern list.
		eval "
			case '$device' in
			$dev_pattern)
				# pass
				;;
			*)
				continue
				;;
			esac
		"

		label="iso9660/$device"

		# If this was previously discovered with a label, call for
		# a revisit after the cleanup phase.
		if revisitNeeded iso9660 "$1"; then
			continue
		fi

		# Write the node.
		writeNode "$device" "$label"

		# Append to the list of already probed devices.
		echo "$device$IFS$label" >> "$probed"
	done
}

#
# Detect labeled devices and call writeNode().
#
# @param 1
#	If set this is considered a revisit.
# @param devices_blacklist
#	A list of glob patterns to check the devices against.
# @param nodes_blacklist
#	A list glob patterns to check label nodes against.
# @param revisit
#	Add probe if a second update run will be required to track the
#	disappearance of a label or a file system type change.
#
probeDetect() {
	# Check whether probing based file system discovery has been disabled.
	test "$detect_probe" != "1" && return 0

	local device label type mount options types

	# Reformat probe_types.
	types="$(echo "$probe_types" | /usr/bin/egrep -o '[^,]+')"

	# Add new mounts.
	for device in $(geomDevices); do
		# This is in the list of already probed devices.
		if /usr/bin/grep -qFx "$device" "$probed"; then
			continue
		fi

		# Try probing every file system type.
		for type in $types; do
			label="$type/$device"
			mount="$type"
			options="$mount_options"

			# Do not probe blacklisted devices.
			if ! checkBlacklists "$device" "$label"; then
				continue
			fi

			# Check for config file settings.
			if [ -n "$(eval "echo \"\$$type\"")" ]; then
				mount="$(eval "echo \"\$$type\"")"
			fi
			if [ -n "$(eval "echo \"\$${type}_options\"")" ]; then
				options="$(eval "echo \"\$${type}_options\"")"
			fi

			# Try to mount the device.
			if probeMount "$type" "$device"; then
				# Unmount the probe.
				probeUnmount

				# If this was previously discovered with a
				# label, or a different file system type, call
				# for a revisit after the cleanup phase.
				if revisitNeeded probe "$1"; then
					# Skip the following probes on this
					# device.
					break
				fi

				# Write the node.
				writeNode "$device" "$label"

				# Append to the list of already probed devices.
				echo "$device" >> "$probed"

				# Skip the following probes on this device.
				break
			fi

			# Add the label to the list of probed devices.
			echo "$label" >> "$probed"
		done
	done
}

#
# Update the list of devices to screen from devd events, because they are
# part of this update run.
#
updateScreen() {
	(geomDevices; geomLabels) \
		| /usr/bin/lockf -k "$screen" /bin/sh -c "/bin/cat > '$screen'"
}

#
# Update the list of managed devices.
#
# Updates the amd.map, the list of partitions and the mount links, if amd
# is running.
#
# @return
#	ERR_NOT_STARTED, if automounter has not been started, otherwise 0.
#
update() {
	local device line partitions label type options
	local hash owner mode key detector revisit
	local devices_blacklist nodes_blacklist

	# Don't do anything if amd is not running.
	# This prevents automounter from starting too early from devd.
	/bin/sync
	test -e "$pidfile" || return $ERR_NOT_STARTED

	# Record the currently present devices for screening devd calls.
	updateScreen

	# Start building a new map by copying the static one.
	/bin/cp "$static_map" "$map"

	/bin/mkdir -p "$a"
	/usr/bin/touch "$nodes"
	/bin/mv "$nodes" "$oldnodes"
	/usr/bin/touch "$nodes"
	echo > "$probed"

	# Change the blacklists' format into something that can be used in a
	# case statement.
	devices_blacklist="$(
		echo "${blacklist_devs}" | /usr/bin/sed -E \
			-e 's/^[[:space:]]*,//' -e 's/,[[:space:]]*$//' \
			-e 's/,/|/g' -e 's,([[:space:]\\]),\\\1,g'
	)"
	nodes_blacklist="$(
		echo "${blacklist_nodes}" | /usr/bin/sed -E \
			-e 's/^[[:space:]]*,//' -e 's/,[[:space:]]*$//' \
			-e 's/,/|/g' -e 's,([[:space:]\\]),\\\1,g'
	)"

	# Run detectors.
	revisit=
	for detector in mounted iso9660 glabel probe mounted; do
		/bin/sync
		${detector}Detect
	done

	# Remove no longer listed nodes.
	for line in $(/usr/bin/grep -vFx "$(/bin/cat "$nodes")" "$oldnodes"); do
		device="${line%%;*}"
		label="${line##*;}"

		# Remove the stale media links.
		rmLinks update "$label" "$device"
	done

	# Revisit detectors requesting it.
	for detector in $revisit; do
		/bin/sync
		${detector}Detect revisit
	done

	# Update geli managed mounts.
	geliUpdate

	# Finally reload amd map.
	eval $amq -f

	# Clean up.
	/bin/rm "$oldnodes" "$probed" 2>&3

	return 0
}

#
# Update geli encrypted providers. Requires the oldnodes file to be intact.
#
geliUpdate() {
	# Do not run if geli features are not activated.
	test "$geli" != "1" && return 0

	local oldkeys keys key label pretty name line device

	# Clean up keys from stale mounts.
	/usr/bin/touch "$oldnodes"
	keys="$(/bin/cat "$geli_availablekeys" 2>&3)"
	oldkeys="$keys"
	for line in $(/bin/cat "$oldnodes"); do
		# Skip still present lines.
		/usr/bin/grep -qFx "$line" "$nodes" && continue
		label="${line##*;}"
		label="${label%;*}"
		pretty="$(urldecode "$label")"

		# Forget keys belonging to this line.
		for key in $(echo "$keys" | /usr/bin/grep -E "^$label;"); do
			echo "geli: remove key <${key##*;}> from <$pretty>" | eval $logger
		done
		keys="$(echo "$keys" | /usr/bin/grep -Ev "^$label;")"
	done

	# Now we check new mounts for keys.
	echo "$keys" | /usr/bin/grep -xv '' > "$geli_availablekeys"

	for line in $(/bin/cat "$nodes"); do
		/usr/bin/grep -qFx "$line" "$oldnodes" && continue
		label="${line#*;}"
		device="${line%%;*}"
		pretty="$(urldecode "$label")"

		if probeMount "${label%%/*}" "$device"; then
			for key in $(/bin/ls "$probe/$geli_keys/" 2>&3); do
				echo "geli: add key <$key> from <$pretty>" | eval $logger
				echo "$label;$key" >> "$geli_availablekeys"
			done

			probeUnmount
		fi
	done

	# Look for stale images.
	/usr/bin/touch "$geli_nodes"
	/bin/mv "$geli_nodes" "$geli_oldnodes"
	for line in $(/bin/cat "$geli_oldnodes"); do
		name="${line%%;*}"
		device="${line#*;}"
		device="${device%;*}"
		label="${line##*;}"

		# This line must remain until the node has been destroyed.
		echo "$line" >> "$geli_nodes"

		# Skip if the key to this image is still around.
		if /usr/bin/grep -qFx "$label;$name" "$geli_availablekeys"; then
	 		continue
		fi

		# This image is stale. Is that a new discovery?
		if echo "$oldkeys" | /usr/bin/grep -qFx "$label;$name"; then
 			# Fork a process that tries to unmount it.
			geliUnmount "$label" "$name" "$device" &
		fi
	done
	/usr/bin/touch "$geli_nodes"
	/bin/rm "$geli_oldnodes"

	# Attach encrypted devices. If necessary create them from images.
	for key in $(/bin/cat "$geli_availablekeys"); do
		label="${key%;*}"
		name="${key##*;}"
		pretty="$(urldecode "$label")"

		# The image with this name is already available as an md device.
		/usr/bin/grep -qE "^$name;" "$geli_nodes" && continue

		# There is no image with this name so there is nothing to do.
		test -e "$geli_images/$name" \
			-o -L "$geli_images/$name" || continue

		# Being here means that this image has not yet been made
		# available. So it is time to give it a try.
		key="$probe/$geli_keys/$name"

		probeMount "${label%%/*}" "$label"

		# Get the device to attach.
		if [ -L "$geli_images/$name" -a ! -e "$geli_images/$name" ]
		then
			# Get the device name from a link.
			device="$(/usr/bin/readlink "$geli_images/$name")"
		else
			# Create a file backed memory disk.
			device="$(/sbin/mdconfig -f "$geli_images/$name")"
			while [ ! -e "$devfs/$device" ]; do
				/bin/sleep 0.1
			done
		fi
		# Attempt to attach (decrypt) file.
		if cd "$(/usr/bin/dirname "$key")" \
			&& /sbin/geli attach -p -k "$key" "$device"
		then
			# Remember success.
			echo "geli: add provider" \
			     "<$name> [$device] with key from <$pretty>" \
			     | eval $logger
			echo "$name;$device;$label" >> "$geli_nodes"
		else
			# Unsuccessful, clean up memory disk.
			/sbin/mdconfig -du "$device"
		fi

		probeUnmount
	done

	return 0
}

#
# Setup amd if not yet running and call update.
#
# @param 1
#	If set the update call is forked into the background.
#
start() {
	local pid

	if [ ! -e "$pidfile" ]; then
		# AMD requires rpcbind
		if ! eval $rpcinfo >&3; then
			eval $logger start: Starting rpcbind ...
			if ! eval $rpcbind; then
				eval $logger start: failed.
				return $ERR_RPC_FAIL
			fi
			eval $logger start: done.
		fi

		# Start amd with a copy of the static map
		/bin/cp "$static_map" "$map"
		eval $logger start: Starting amd ...
		if ! eval $amd -p -a "$a" ${c:+-c "$c"} ${w:+-w "$w"} \
		          ${l:+-l"$l"} "$directory" "$map" > "$pidfile"; then
			eval $logger start: failed.
			return $ERR_AMD_FAIL
		fi
		eval $logger start: done.
	fi

	if [ -n "$1" ]; then
		$0 update &
	else
		update
	fi
}

#
# Kills the amd, unmounts all mounted partitions and cleans up everything.
#
stop() {
	local pid type label line device

	pid="$(/bin/cat "$pidfile" 2>&3)"
	if [ -n "$pid" ]; then
		eval $logger stop: Stopping amd ...
		/bin/kill "$pid"
		/bin/pwait "$pid" 2>&3
		eval $logger stop: done.
	fi
	
	# Clean up stale mounts.
	for line in $(/bin/cat "$nodes" 2>&3); do
		label="${line##*;}"
		device="${line%%;*}"

		rmLinks stop "$label" "$device"
	done

	# Clean up stale geli nodes.
	for line in $(/bin/cat "$geli_nodes" 2>&3); do
		device="${line#*;}"
		device="${device%;*}"

		/sbin/geli detach -f "$device"
		/sbin/mdconfig -du "$device"
	done

	# Clean up temporary folders.
	/bin/rmdir "$a" "$directory" "$mountdir" 2>&3
	/bin/rm "$pidfile" "$map" "$nodes" \
		 "$geli_availablekeys" "$geli_nodes" 2>&3
	/usr/bin/lockf "$screen" true

	return 0
}

#
# List mounted, labels, keys, encrypted providers or one of these categories.
#
# @param 1
#	Either "mounted", "labels", "keys" or "encrypted". If given only this
#	category will be listed.
# @return
#	ERR_LIST_LOCKED if the lock is currently held, 0 otherwise.
#
list() {
	# This is not a reliable way to ensure that everything will go right,
	# but the probabality that bogus output will appear is rather low
	# and not aquiring the lock allows everyone to use the list command.
	if [ -e "$lock" ]; then
		echo "Locked." 1>&2
		return $ERR_LIST_LOCKED
	fi

	local image device hash label line

	# List mounted media.
	if [ -z "$1" -o "$1" = "mounted" ]; then
		for line in $(
			# Format: <device>;<label>
			/sbin/mount | /usr/bin/grep -F " $(realpath $mountdir 2>&3)" | \
				/usr/bin/sed -E "s,$devfs/(.+) on $(realpath $mountdir 2>&3)/(.*) \(.*,\1;\2,1"
		); {
			# If the given devices match a line just print
			# it and we are done. Otherwise look for the label
			# with a different device.
			# This is the case for labeled devices, because the
			# label is used for mounting and for fuse devices,
			# because fuse creates a new device node for each
			# device it mounts.
			device="${line%%;*}"
			if /usr/bin/grep -qFx "$line" "$nodes" 2>&3; then
				echo "mounted: <${line#*;}> [$device]"
			elif line="$(/usr/bin/grep -E ";${line#*;}$" "$nodes" 2>&3)"; then
				if [ "${line#*;}" != "$device" ]; then
					echo "mounted: <${line#*;}> [${line%%;*}] as [$device]"
				else
					echo "mounted: <${line#*;}> [${line%%;*}]"
				fi
			fi
		}
	fi | urldecode

	# Print the labels that are available for mounting.
	if [ -z "$1" -o "$1" = "labels" ]; then
		/usr/bin/sed -E 's/([^;]*);(.*)/label: <\2> [\1]/1' \
			"$nodes" 2>&3
	fi | urldecode

	# List the keys that have been found on mounted devices.
	if [ -z "$1" -o "$1" = "keys" ]; then
		/usr/bin/sed -E 's/(.*);(.*)/key: <\2> from <\1>/1' \
			"$geli_availablekeys" 2>&3
	fi | urldecode

	# List the encrypted providers and their status.
	if [ -z "$1" -o "$1" = "encrypted" -o "$1" = "images" ]; then
		/usr/bin/sed -E 's/(.*);(.*);(.*)/encrypted provider: <\1> [\2] with key from <\3>/1' \
			"$geli_nodes" 2>&3
		for image in $(/bin/ls "$geli_images/" 2>&3); do
			/usr/bin/grep -qx "$image;.*" "$geli_nodes" 2>&3 \
			&& continue
			if [ -e "$geli_images/$image" ]; then
				echo "encrypted provider: <$image>"
			else
				device="$(/usr/bin/readlink \
					"$geli_images/$image")"
				echo "encrypted provider: <$image> [$device]"
			fi
		done
	fi | urldecode

	return 0
}

#
# List data in machine readable form using absolute path names.
#
# @param 1
#	Either "mounted", "llinks" for labeled links or "dlinks" for device
#	links.
# @return
#	ERR_LIST_LOCKED if the lock is currently held, 0 otherwise.
#
mlist() {
	# This is not a reliable way to ensure that everything will go right,
	# but the probabality that bogus output will appear is rather low
	# and not aquiring the lock allows everyone to use the list command.
	if [ -e "$lock" ]; then
		return $ERR_LIST_LOCKED
	fi

	# Print mounted file systems.
	if [ -z "$1" -o "$1" = "mounted" ]; then
		for line in $(
			# Format: <device>;<label>
			/sbin/mount | /usr/bin/grep -F " $(realpath $mountdir 2>&3)" | \
				/usr/bin/sed -E "s,$devfs/(.+) on $mountdir/(.*) \(.*,\1;\2,1"
		); {
			# If the given devices match a line just print
			# it and we are done. Otherwise look for the label
			# with a different device, which can happen with
			# fuse devices.
			if /usr/bin/grep -qFx "$line" "$nodes" 2>&3; then
				echo "$mountdir/${line#*;}"
			elif $(line="$(/usr/bin/grep -E ";${line#*;}$" "$nodes" 2>&3)"); then
				device="${line%%;*}"
				echo "$mountdir/${line#*;}"
			fi
		}
	fi

	# Print the labels that are available for mounting.
	if [ -z "$1" -o "$1" = "llinks" ]; then
		/usr/bin/sed "s,[^;]*;,$linkdir/," "$nodes" 2>&3
	fi | urldecode

	# Print the devices that are available for mounting.
	if [ -z "$1" -o "$1" = "dlinks" ]; then
		/usr/bin/sed -E "s,([^;]*);([^/]*).*,$linkdir/${devfs##*/}/\1\\$IFS$linkdir/${devfs##*/}/\1.\2," "$nodes" 2>&3
	fi

	return 0
}

#
# This calls the apropriate mount command for a given hash.
#
# @param $1
#	The hash over the name of the device node to mount.
# @return
#	ERR_MOUNT_FS_MISSING if the file system to mount cannot be identified,
#	otherwise "exec /sbin/mount" is called.
#
mount() {
	local label labels hash device

	# Go through the list of configured partitions.
	labels="$(/bin/cat "$nodes")"
	for label in $labels; do
		hash="$(/sbin/md5 -qs "$label")"

		# The current partition is the one we're supposed to mount.
		if [ "$hash" = "$1" ]; then
			device="${label%%;*}"
			label="${label#*;}"
			type="${label%%/*}"
			mount="$type"
			options="$mount_options"

			# Check for config file settings.
			if [ -n "$(eval "echo \"\$$type\"")" ]; then
				mount="$(eval "echo \"\$$type\"")"
			fi
			if [ -n "$(eval "echo \"\$${type}_options\"")" ]; then
				options="$(eval "echo \"\$${type}_options\"")"
			fi

			# When a label consumer is mounted, the label provider
			# is destroyed, which creates unnecessary devd noise,
			# i.e. the label disappears and re-appears when
			# mounting and unmounting, causing lots of obsolete
			# update calls through devd.
			# Using the label for mounting avoids this.
			if [ -e "$devfs/$label" ]; then
				device="$label"
			fi

			# Mount the file system, try read-only fallback in
			# case of failure.
			/sbin/mount -t "$mount" -o "$options" \
			            "$devfs/$device" "$mountdir/$label" \
			|| /sbin/mount -t "$mount" -o "$options${options:+,}ro" \
			               "$devfs/$device" "$mountdir/$label"
			return
		fi
	done
	eval $logger mount: Requested file system not found.
	return $ERR_MOUNT_FS_MISSING
}

#
# This just exists to work around bugs in fusefs. It simply calls the
# umount command of the OS unless evil_fuse is set to 1.
#
# @param $1
#	The hash over the name of the device node to unmount.
# @return
#	ERR_UMOUNT_ACTIVE if the file system to be unmounted is active,
#	otherwise "exec /sbin/umount" is called.
#
umount() {
	local label pretty device labels status

	# Find the label for the current hash.
	label=
	device=
	labels="$(/bin/cat "$nodes")"
	for label in $labels; do
		if [ "$1" = "$(/sbin/md5 -qs "$label")" ]; then
			device="${label%%;*}"
			label="${label#*;}"
			break
		else
			label=
		fi
	done

	# If the label is unknown, claim the device was unmounted.
	if [ -z "$label" ]; then
		return 0
	fi

	# If the label is not mounted, claim a successful unmount.
	if ! /sbin/mount | /usr/bin/grep -qF "$(realpath $mountdir)/$label ("; then
		return 0
	fi

	# Find out if we are a fuse file system and use dirty bug workaround.
	# This will lead to unexpected results with more than one fuse based
	# file system around. Fuse based file systems will only get unmounted
	# if files are not opened on any of them.
	if [ "$evil_fuse" = "1" ]; then
		local type
		type="$(
			/sbin/mount | /usr/bin/grep -F "$label" | \
				/usr/bin/sed -E "s,.* on $(realpath $mountdir)/$label \(([^,)]*).*,\1,1"
		)"

		# If there are any files opened on ANY fuse based file system
		# then we will NOT unmount.
		if [ "$type" = "fusefs" ]; then
			/usr/bin/fstat | \
				/usr/bin/grep -qE '\?\(fuse\)' && return $ERR_UMOUNT_ACTIVE
		fi
	fi

	# If the device is missing (i.e. it was removed while being mounted),
	# force umount.
	pretty="$(urldecode "$label")"
	if [ ! -e "$devfs/$device" ]; then
		echo  "umount: force umount missing <$pretty> [$device]" | eval $logger
		# Schedule an update.
		/sbin/umount -f "$(realpath $mountdir)/$label" 2>&3
		status=$?
		update
		return $status
	fi

	# Don't attempt umounts for active file systems.
	if /usr/bin/fstat | /usr/bin/grep -qF " $(realpath $mountdir)/$label "; then
		return $ERR_UMOUNT_ACTIVE
	fi

	# Give over to the system unmount command.
	exec /sbin/umount "$(realpath $mountdir)/$label" 2>&3
}

#
# For devd events, screens update calls for the usual timeout period unless
# a new device appears.
#
devd() {

	local devs

	devs="$(geomDevices; geomLabels)"
	/usr/bin/lockf -k "$screen" /bin/sh -c "
		# Pass and update, so that only the first one passes
		pass() {
			echo '$devs' > '$screen'
			exit 0
		}

		# Check age
		passed=\$((\$(/bin/date +%s) - \$(/usr/bin/stat -f %m -t %s '$screen')))
		test \$passed -gt $timeout && pass

		screened=\"\$(/bin/cat '$screen')\"

		# Check whether new devices appeared
		test -n \"\$(echo '$devs' | /usr/bin/grep -vFx \"\$screened\")\" && pass

		# Check whether devices disappeared
		test -n \"\$(echo \"\$screened\" | /usr/bin/grep -vFx '$devs')\" && pass

		# Do not pass
		exit 1
	"
}

#
# This function initializes the formatting strings and variables for the
# monitor list.
#
# The last terminal character is never used, because some terminals do not
# support printing it without starting a new line.
#
# @param co
#	Set to the number of terminal character columns
# @param li
#	Set to the number of terminal lines
# @param hformat
#	Set to the printf list heading formatting string
# @param lformat
#	Set to the printf list formatting string
# @param clrformat
#	Set to a string that clears a line of output
#
monitorFormats() {
	local wd wl

	/usr/bin/tput cl

	co=$(/usr/bin/tput co)
	li=$(/usr/bin/tput li)

	wd=$(((co - 12) / 2))
	wl=$((co - 12 - wd))
	hformat="%-$wl.${wl}s %-$wd.${wd}s %4.4s %4.4s\r"

	lformat="%-$wl.${wl}s %-$wd.${wd}s %3.3s  %3.3s\r"
	clrformat="%$((co - 1))s\r"
}

#
# Idles for the desired cycle.
#
# This works by waiting for a previously issued sleep call. Once waiting
# is complete (or was interrupted by a signal), a new sleep call is issued
# and $sleepPID is updated.
#
# @note
#	Requires an initial call of monitorIdleInit() to start the first
#	sleep cycle
# @param idleCycle
#	The idle time
# @param idleSleepPID
#	The PID of the last sleep call
#
monitorIdle() {
	wait $idleSleepPID >&3 2>&3
	(
		set -T
		trap 'kill $(jobs -p)' winch info
		/bin/sleep $idleCycle
	) >&- 2>&- &
	idleSleepPID=$!
}

#
# Initializes the first idle cycle for monitorIdle().
#
# It also checks whether $idleCycle is set up properly.
#
# @param idleCycle
#	The idle time
# @param idleSleepPID
#	The PID of the last sleep call
#
monitorIdleInit() {
	idleCycle=${idleCycle%%$IFS*}
	if ! echo $idleCycle | /usr/bin/grep -Eqx '([0-9]+\.?[0-9]*|\.[0-9]+)'; then
		return 1
	fi
	(
		set -T
		trap 'kill $(jobs -p)' winch info
		/bin/sleep $idleCycle
	) 1>&3 2>&3 &
	idleSleepPID=$!
}

#
# This function draws a list heading.
#
# @param @
#	The headers to draw
# @param i
#	Set to the first line behind the heading and expected to contain
#	the line to draw the heading to. Note that a heading is only
#	drawn if there are 3 more lines of space (an empty line, the
#	heading and space for a line of content
# @param hformat
#	The formatting string for the line
# @param clrformat
#	The formatting string for the clear line
# @param li
#	The number of terminal lines
# @retval 0
#	The heading was drawn
# @retval 1
#	Insufficient number of lines left
#
monitorListHeading() {
	# Check for sufficient space
	if [ $((i + 2)) -ge $li ]; then
		return 1
	fi

	# Clear the line
	/usr/bin/tput cm 0 $i
	printf $clrformat
	i=$((i + 1))
	# Print the headings
	/usr/bin/tput cm 0 $i
	printf $hformat "$@"
	i=$((i + 1))
}

#
# Prints a list row.
#
# @param @
#	The list column entries
# @param i
#	Set to the line behind the new row, expected to point to the line
#	to draw to
# @param lformat
#	The formatting string for the list entry row
# @param li
#	The number of terminal lines
# @retval 0
#	Printing completed
# @retval 1
#	There was no space to print the row
#
monitorListRow() {
	# No space left to print the row
	if [ $i -ge $li ]; then
		return 1
	fi

	# Print row
	/usr/bin/tput cm 0 $i
	printf "$lformat" "$@"
	i=$((i + 1))
}

#
# Cleans up list rows that are no longer in use, because the list got
# shorter since the last call.
#
# @param i
#	Expected to point to the first line to clear
# @param tailLine
#	Set to the end of the tail
# @param li
#	The number of terminal lines
#
monitorListTail() {
	local line
	line=$i
	while [ $i -lt ${tailLine:-0} ]; do
		test $i -ge $li && break
		/usr/bin/tput cm 0 $i
		printf $clrformat
		i=$((i + 1))
	done
	tailLine=$line
}

#
# Draws the amd/automounter state heading.
#
# @param 1
#	The number of labels
# @param 2
#	The number of mounted devices
# @param 3
#	The number of encrypted images/devices
# @param 4
#	The number of keys
# @param i
#	Set to the line behind the heading
# @param co
#	The number of available terminal character columns
#
monitorHeading() {
	local header amdPID state
	# Get the amd and automounter states.
	amdPID=$(/bin/cat "$pidfile" 2>&3)
	state=up
	test -z "$amdPID" && state=down
	test -e "$lock" && state=LOCKED

	# Print the header lines.
	header=$(printf "amd pid: %-5.5s   automounter: %-6.6s" ${amdPID:--} $state)
	/usr/bin/tput cm 0 0
	printf "%-$((co - 10)).$((co - 10))s %s\r" $header $(/bin/date +%H:%M:%S)

 	header=$(printf "state: %3.3s labels,%3.3s mounted,%3.3s encrypted,%3.3s keys" "$@")
	/usr/bin/tput cm 0 1
	printf "%-$((co - 1)).$((co - 1))s\r" $header

	# Point the current line counter behind the headers.
	i=2
}

#
# Sets up file descriptors, traps and the terminal cursor for operation.
#
# The terminal size update trap sets winch=1.
#
# @param winch
#	Is set to 1 by the winch trap to announce that a window change
#	happened
#
monitorSetup() {
	trap 'monitorFormats; winch=1' winch info
	trap 'exit 0' int term
	trap '/usr/bin/tput ve; /usr/bin/tput cl' EXIT
	/usr/bin/tput vi
}

#
# Provides a top-like display of the available labels, encrypted devices and
# keys.
#
# No list updates are performed while the automounter is in locked state. The
# monitor itself does not acquire the lock, so there is a small chance of
# displaying corrupted data, which heals itself during the next cycle.
#
# @note
#	This function is a dead end, the intended way to terminate it is
#	a SIGTERM or SIGINT, which result in a call of "exit 0".
# @note
#	SIGINFO causes a redraw of the current display.
# @param 1
#	The (optional) update cycle time
#
monitor() {
	# For monitorSetup
	local winch
	# Populated by monitorFormats()
	local co li lformat hformat clrformat
	# For monitorIdle()
	local idleCycle idleSleepPID
	# For monitorHeading() and monitorList*()
	local i
	# For monitorListTail()
	local tailLine

	# Initialise traps, cursor and the file descriptor 3 for litter.
	monitorSetup

	idleCycle=${1:-2}
	if ! monitorIdleInit; then
		echo "The cycle time $idleCycle is not a valid number of seconds." 1>&2
		exit 1
	fi

	# Initialise list formatting strings.
	monitorFormats

	# Local variables.
	local list label pretty device mounts
	local dir mount mode keys nkeys key
	local gnodes image encrypted

	while true; do
		# Reset the window change trap notify.
		winch=

		# Get the number of encrypted images/devices
		encrypted=$(($(/bin/ls $geli_images 2>&3 | /usr/bin/wc -l)))

		# Draw the heading.
		monitorHeading ${list:-0} ${mounts:-0} $encrypted ${keys:-0}

		test -e "$lock" && continue

		# The list of managed nodes.
		list=$(/bin/cat $nodes 2>&3 | /usr/bin/sort -t\; -k2)
		# The 'real' mountdir.
		dir=$(realpath $mountdir 2>&3 || echo $mountdir)
		# The list of available keys.
		keys=$(/bin/cat $geli_availablekeys 2>&3)
		# The list of mounted labels.
		mounts=$(/sbin/mount | /usr/bin/grep -F " $dir")
		# The list of encrypted, unlocked images/devices.
		gnodes=$(/bin/cat $geli_nodes 2>&3)

		# Print the list of labels.
		test -n "$list" \
		&& monitorListHeading LABEL DEVICE KEYS MODE \
		&& for label in $list; do
			device="${label%%;*}"
			label="${label#*;}"
			pretty="$(urldecode "$label")"

			# Get the number of keys.
			nkeys=$(($(echo "$keys" | /usr/bin/grep -F "$label;" | /usr/bin/wc -l)))
			test 0 -eq $nkeys && nkeys=-

			# Is the fs mounted and if so in ro or rw mode?
			mode=-
			mount=$(echo "$mounts" | /usr/bin/grep -F " $dir/$label") \
			&& mode=rw
			echo $mount | /usr/bin/grep -qF ', read-only' && mode=ro

			monitorListRow $pretty $device $nkeys $mode || break
		done
		# Calculate the number of labels.
		list=${list:+$(($(echo "$list" | /usr/bin/wc -l)))}
		list=${list:-0}
		# Calculate the number of mounted labels.
		mounts=${mounts:+$(($(echo "$mounts" | /usr/bin/wc -l)))}
		mounts=${mounts:-0}

		# Print the list of encrypted images/devices.
		test -n "$(/bin/ls $geli_images 2>&3)" \
		&& monitorListHeading ENCRYPTED DEVICE \
		&& for image in $(/bin/ls $geli_images 2>&3); do
			label=$(echo $gnodes | /usr/bin/grep "^$image;")
			label=${label#*;}
			device=${label%;*}
			label=${label#*;}

			monitorListRow $image $device || break
		done

		# Print the list of available keys.
		test -n "$keys" \
		&& monitorListHeading KEY ORIGIN \
		&& for key in $keys; do
			pretty=$(urldecode "${key%%;*}")
			key=${key#*;}

			monitorListRow $key $pretty || break
		done
		# Calculate the number of available keys.
		keys=${keys:+$(($(echo "$keys" | /usr/bin/wc -l)))}
		keys=${keys:-0}

		# Overwrite trailing rows.
		monitorListTail

		# Skip idle interval if a window change trap was executed.
		test -n "$winch" && continue

		# Sleep.
		monitorIdle
	done
}

# Ensure the lock is aquired. And run the requested command.
case "$1" in
locked)
	case "$2" in
	start | update | stop | mount | umount)
		$2 "$3"
		return
		;;
	esac
	;;
list | mlist | monitor)
	$1 "$2"
	return
	;;
umount)
	# Unmounts should not happen during updates.
	exec /usr/bin/lockf -st 0 "$lock" $0 locked "$@"
	;;
mount)
	exec /usr/bin/lockf -st "$timeout" "$lock" $0 locked "$@"
	;;
devd)
	if devd; then
		shift
		# Execute in background.
		/usr/bin/lockf -st "$timeout" "$lock" $0 locked "$@" &
		return
	fi
	;;
start | update | stop)
	/usr/bin/lockf -st "$timeout" "$lock" $0 locked "$@"
	status=$?
	case $status in
	$EX_USAGE | $EX_SOFTWARE | $EX_OSERR | $EX_CANTCREAT | $EX_TEMPFAIL)
		echo "automounter: Lock could not be aquired:" "$@" 1>&2
		;;
	esac
	return $status
	;;
?*)
	echo "automounter: unknown directive '$1'." 1>&2
	;&
'')
	/bin/cat << HERE-DOC
Usage:	automounter (start | update | list | mlist | monitor | stop)
	automounter list [mounted | labels | keys | encrypted]
	automounter mlist [mounted | llinks | dlinks]
	automounter monitor [interval]
HERE-DOC
	return $ERR_CMD_UNKNOWN
	;;
esac

