After a lot experimentation I’ve arrived at a scripted answer, with a script that may function in two modes:
The primary is as a daemon, normally run as root
(as launch daemon), which listens on a given socket for instructions figuring out the quantity you need to mount (should be unmounted), adopted by one other to substantiate that you just mounted it (proving you’ll be able to), and a 3rd to then unmount it, power unmount it, or clear the request. The behaviour is a bit simplistic, however ought to fairly set up a consumer had the power to mount the quantity, and subsequently is allowed to request that it then be unmounted, utilizing a easy random credential.
When not run in daemon mode, the script takes a quantity identifier (something supported by diskutil apfs unlockVolume
, UUIDs most well-liked) and makes an attempt to unlock and mount the quantity. That you must have the password for the quantity within the keychain for the person working the script, and can be prompted to permit safety
to entry it. The script usually makes an attempt to unmount a quantity by itself, nevertheless I’ve established that more often than not this may not work, as disk arbitration is normally unloaded earlier than the script makes an attempt to take action (that means diskutil unmount
and umount
each fail), as such if you wish to use this script with a launch agent that may unmount on logout, you have to have a daemon working on the identical system and set the --socket
argument to match.
MountAPFS
Hopefully that is pretty clear in the way it’s supposed for use, because it contains examples and choices are documented. This isn’t meant for anybody that does not have some grasp of Terminal utilization and shell scripting (ZSH particularly) as you might want to customize it to do precisely what you need.
#!/bin/zsh
{
# Examples:
# Standalone: ./MountAPFS 12345678-9012-3456-7890-12345678901234
# (mount): ./MountAPFS --create ~/Library/Volumes/Foo 12345678-9012-3456-7890-12345678901234
#
# Daemon: ./MountAPFS --daemon --socket 61616
# Consumer: ./MountAPFS --socket 61616 12345678-9012-3456-7890-12345678901234
whereas [ $# -gt 0 ]; do
case "$1" in
# Set a listing that must be created (normally the quantity's mount level when a customized mount level is laid out in /and so forth/fstab)
('--create'|'--create-dir'|'--create-directory')
CREATE_DIRECTORY="$2"; shift
case "${CREATE_DIRECTORY:0:1}" in
("https://apple.stackexchange.com/") ;;
('~') CREATE_DIRECTORY="${HOME}${CREATE_DIRECTORY:1}" ;;
(*) CREATE_DIRECTORY="${BASE_DIRECTORY}/${CREATE_DIRECTORY}" ;;
esac
;;
# Runs this script in daemon mount (don't mount any volumes, as an alternative deal with the unmount of registered volumes on behalf of different duties).
('--daemon') DAEMON=1 ;;
# The socket to pay attention on/connect with when working in/with a daemon script
('--socket') SOCKET="$2"; WAIT=1; shift ;;
# The period of time to attend for the quantity to turn into accessible earlier than giving up. This feature can be utilized if there could also be a race situation between this and one other activity earlier than the quantity turns into accessible
('--timeout') TIMEOUT="$2"; shift ;;
# Don't finish as soon as the quantity is mounted, as an alternative watch for a termination sign and try and unmount it
('--wait') WAIT=1 ;;
# Allow verbose output; it will output quantity identifiers and tokens for tracing, however will solely output the final 4 characters of tokens to stop abuse (full tokens are 32 characters in size)
('-v'|'--verbose') VERBOSITY=$(($(echo "0${VERBOSITY}" | sed 's/[^0-9]*//g') + 1)) ;;
# Express finish of arguments
('--') shift; break ;;
(--*) echo "Unknown choice: $1" >&2; exit 2 ;;
# Implicit finish of arguments (first quantity)
(*) break ;;
esac
shift
finished
VERBOSITY=$(echo "0${VERBOSITY}" | sed 's/[^0-9]*//g')
if [[ -n "${SOCKET}" ]]; then
[[ "${SOCKET}" = "$(echo "${SOCKET}" | sed 's/[^0-9]*//g')" ]] || { echo 'Invalid socket:' "${SOCKET}" >&2; exit 2; }
[[ "${SOCKET}" -gt 0 ]] || { echo 'Invalid socket:' "${SOCKET}" >&2; exit 2; }
fi
if [ "${DAEMON}" = 1 ]; then
[[ -n "${SOCKET}" ]] || { echo 'Daemon mode requires a socket' >&2; exit 2; }
# Open netcat on the required socket
coproc nc -kl localhost "${SOCKET}" || { echo 'Unable to open socket' >&2; exit 2; }
entice 'coproc :' EXIT SIGHUP SIGINT SIGTERM
[[ ${VERBOSITY} -gt 0 ]] && echo 'APFS daemon listening on socket:' "${SOCKET}"
declare -A requested=()
declare -A mounted=()
whereas IFS='', learn -rd '' line; do
cmd="${line:0:5}"
worth="${line:5}"
case "${cmd}" in
# Signifies intention to mount a present unmounted quantity (given in worth).
# Returns a token that should be utilized in future instructions
('mount')
if mount=$(diskutil data "${worth}" 2>/dev/null | grep 'Mounted' | sed 's/[^:]*: *//') && [[ "${mount}" = 'No' ]]; then
token=$(echo "${worth}$(head -c 512 &p
[[ ${VERBOSITY} -gt 0 ]] && echo 'Accepted mount request for:' "${worth} assigned token ending with:" "${token: -4}"
else
printf '%spercents ' 'error' 'Quantity not discovered, or is already mounted' >&p
[[ ${VERBOSITY} -gt 0 ]] && echo 'Quantity not discovered or already mounted:' "${worth}" >&2
fi
;;
# Signifies that the beforehand registered quantity is now mounted. Quantity is recognized utilizing the distinctive token returned by the mount command. Now that the quantity has been mounted, it may be unmounted utilizing the unmnt or funmt command.
# Returns the quantity that was examined
('mnted')
quantity=${requested[$value]}
if [ -n "${volume}" ]; then
if mount=$(diskutil data "${quantity}" 2>/dev/null | grep 'Mounted' | sed 's/[^:]*: *//') && [[ "${mount}" != 'No' ]]; then
mounted[${value}]=${quantity}
unset "requested[${token}]"
printf '%spercents ' 'mnted' "${quantity}" >&p
[[ ${VERBOSITY} -gt 0 ]] && echo 'Confirmed mounting of:' "${quantity} utilizing token ending with:" "${worth: -4}"
else
printf '%spercents ' 'error' 'Quantity not discovered, or will not be mounted' >&p
[[ ${VERBOSITY} -gt 0 ]] && echo 'Quantity not discovered or not mounted:' "${quantity}" >&2
fi
else
printf '%spercents ' 'error' 'Unknown token: use the mount command first' >&p
[[ ${VERBOSITY} -gt 0 ]] && echo "Obtained ${cmd} command out of sequence or invalid token ending with: ${token: -4}" >&2
fi
;;
# Requests {that a} beforehand mounted quantity to be unmounted. Quantity is recognized utilizing the distinctive token used within the mnted command.
# The funmt command will try and forcibly unmount the quantity, and may solely be used if the unmnt command beforehand failed.
# Returns the quantity that was unmounted
('unmnt'|'funmt')
quantity=${mounted[$value]}
if [ -n "${volume}" ]; then
if mount=$(diskutil data "${quantity}" 2>/dev/null | grep 'Mounted' | sed 's/[^:]*: *//') && [[ "${mount}" != 'No' ]]; then
[ "${cmd}" = 'funmt' ] && power="power " || power=""
if error=$(diskutil unmount ${power}"${quantity}" 2>&1); then
unset "mounted[${token}]"
printf '%spercents ' "${cmd}" "${quantity}" >&p
[[ ${VERBOSITY} -gt 0 ]] && echo 'Unmounted quantity:' "${quantity} utilizing token ending with:" "${token: -4}"
else
printf '%spercents ' 'error' "Unable to unmount ${quantity}: ${error}" >&p
[[ ${VERBOSITY} -gt 0 ]] && echo 'Unable to mount:' "${quantity}: ${error}" >&2
fi
else
printf '%spercents ' 'error' 'Quantity not discovered, or will not be mounted' >&p
[[ ${VERBOSITY} -gt 0 ]] && echo 'Quantity not discovered:' "${quantity}" >&2
fi
else
printf '%spercents ' 'error' 'Unknown token: use the mnted command first' >&p
[[ ${VERBOSITY} -gt 0 ]] && echo "Obtained ${cmd} command out of sequence: anticipated mnted" >&2
fi
;;
# Clear a token that's now not wanted
('clear')
unset "requested[${value}]"
unset "mounted[${value}]"
printf '%spercents ' 'clear' "${worth}" >&p
[[ ${VERBOSITY} -gt 0 ]] && echo 'Cleared token ending with:' "${worth: -4}"
;;
# Unknown command
(*)
printf '%spercents ' 'error' "Unknown command: ${cmd}" >&p
[[ ${VERBOSITY} -gt 0 ]] && echo 'Obtained unknown command:' "${cmd}" >&2
;;
esac
finished <&p
coproc :
[[ ${VERBOSITY} -gt 0 ]] && echo 'Terminating.'
else
[[ -z "${BASE_DIRECTORY}" ]] && BASE_DIRECTORY="${HOME}/Library/Vaults/"
[[ -d "${BASE_DIRECTORY}" && -w "${BASE_DIRECTORY}" ]] || { echo 'Lacking or unwritable base listing:' "${BASE_DIRECTORY}" >&2; exit 1; }
[[ $# -lt 1 ]] && { echo 'Lacking quantity' >&2; exit 1; }
VOLUME="$1"
# If a timeout was given, wait till the quantity is prepared
TIMEOUT=$(echo "${TIMEOUT}" | sed 's/[^0-9]*//g')
if [[ -n "${TIMEOUT}" ]]; then
whereas [[ "${TIMEOUT}" -gt 0 ]]; do
diskutil data "${VOLUME}" 2>&1 >/dev/null && break
TIMEOUT=$((${TIMEOUT} - 5))
sleep 5
finished
fi
# Be certain the quantity is offered to be unlocked
error=$(diskutil data "${VOLUME}" 2>&1) || { echo 'Quantity not discovered:' "${VOLUME}:" "${error}" >&2; exit 3; }
# If a mount level was given, attempt to create a listing (in any other case quantity will not mount over it)
if [[ -n "${CREATE_DIRECTORY}" ]] && [[ ! -d "${CREATE_DIRECTORY}" ]]; then
error=$(mkdir -m 700 "${CREATE_DIRECTORY}") || { echo 'Unable to create mount level:' "${CREATE_DIRECTORY}:" "${error}" >&2; exit 4; }
fi
# If a socket was given, register our intention to mount the quantity
token=
if [[ "${WAIT}" = 1 && -n "${SOCKET}" ]]; then
socket_cmd() { native cmd="$1"; native worth="$2"
coproc nc localhost "${SOCKET}" || { echo 'Unable to connect with socket' >&2; return 1; }
native response=
printf '%spercents ' "${cmd}" "${worth}" >&p
learn -rd '' response <&p
case "${response:0:5}" in
("${cmd}")
printf '%s' "${response:5}"
coproc :
return 0
;;
('error')
echo "socket_cmd() error: ${response:5}" >&2
coproc :
return 2
;;
(*)
echo 'Unknown/unsupported response:' "${response}" >&2
coproc :
return 3
;;
esac
}
token=$(socket_cmd 'mount' "${VOLUME}") || SOCKET=
fi
if error=$(echo -e "$(safety find-generic-password -wa "${VOLUME}" | sed 's/../x&/g')" | diskutil apfs unlockVolume "${VOLUME}" -stdinpassphrase) || error2=$(diskutil mount "${VOLUME}"); then
if [[ "${WAIT}" = 1 ]]; then
# Verify mounting of quantity to socket (if registered)
[[ -n "${token}" ]] && { volume_confirm=$(socket_cmd "mnted" "${token}") || token=; }
printf '%s' 'Awaiting sign... '
# Lure and wait till activity is ended, then lock the quantity
cleanup_run=0
cleanup() {
[[ ${cleanup_run} = 0 ]] || return 0
cleanup_run=1
echo 'obtained.'
printf '%s' 'Unmounting... '
makes an attempt=5
whereas [[ ${attempts} -gt 0 ]]; do
diskutil apfs lockVolume "${VOLUME}" >/dev/null && echo 'finished.' && break
[[ -n "${CREATE_DIRECTORY}" ]] && umount "${CREATE_DIRECTORY}" && echo 'finished.' && break
[[ -n "${token}" ]] && volume_confirm=$(socket_cmd 'unmnt' "${token}") && token= && echo 'finished.' && break
makes an attempt=$((${makes an attempt} - 1))
sleep 5
finished
if [[ ${attempts} = 0 ]]; then
if diskutil unmount power "${VOLUME}" >/dev/null; then
echo 'pressured.'
else
if [[ -z "${CREATE_DIRECTORY}" ]] || ! umount -f "${CREATE_DIRECTORY}"; then
if [[ -z "${token}" ]] || ! volume_confirm=$(socket_cmd 'funmt' "${token}"); then
echo 'failed.'
echo 'All makes an attempt to unmount failed' >&2
else
token=
echo 'pressured.'
fi
else
echo 'pressured.'
fi
fi
fi
[[ -n "${token}" ]] && socket_cmd 'clear' "${token}"
# Clear all background duties
coproc :
[[ -n "${${(v)jobstates##*:*:}%=*}" ]] && kill ${${(v)jobstates##*:*:}%=*}
}
entice 'cleanup' SIGINT SIGHUP SIGTERM EXIT
whereas true; do
sleep 86400 &
wait $!
finished
fi
else
echo 'Unable to mount quantity:' "${error}" "${error2}" >&2
[[ -n "${token}" ]] && socket_cmd 'clear' "${token}"
fi
fi
}