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, often run as root
(as launch daemon), which listens on a given socket for instructions figuring out the amount you need to mount (should be unmounted), adopted by one other to verify that you simply mounted it (proving you may), and a 3rd to then unmount it, drive 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 amount, 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 popular) and makes an attempt to unlock and mount the amount. You’ll want to have the password for the amount within the keychain for the consumer working the script, and might 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 often unloaded earlier than the script makes an attempt to take action (which 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, that you must 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 it’s possible you’ll 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
# Shopper: ./MountAPFS --socket 61616 12345678-9012-3456-7890-12345678901234
whereas [ $# -gt 0 ]; do
case "$1" in
# Set a listing that must be created (often the amount's mount level when a customized mount level is laid out in /and many others/fstab)
('--create'|'--create-dir'|'--create-directory')
CREATE_DIRECTORY="$2"; shift
case "${CREATE_DIRECTORY:0:1}" in
(" ;;
('~') 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 hear on/connect with when working in/with a daemon script
('--socket') SOCKET="$2"; WAIT=1; shift ;;
# The period of time to attend for the amount to develop into out there earlier than giving up. This feature can be utilized if there could also be a race situation between this and one other job earlier than the amount turns into out there
('--timeout') TIMEOUT="$2"; shift ;;
# Don't finish as soon as the amount 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 forestall 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 possibility: $1" >&2; exit 2 ;;
# Implicit finish of arguments (first quantity)
(*) break ;;
esac
shift
completed
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 </dev/urandom)" | md5)
requested[${token}]=${worth}
printf '%spercents ' 'mount' "${token}" >&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 amount has been mounted, it may be unmounted utilizing the unmnt or funmt command.
# Returns the amount 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 isn't 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 "Acquired ${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 amount, and may solely be used if the unmnt command beforehand failed.
# Returns the amount 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' ] && drive="drive " || drive=""
if error=$(diskutil unmount ${drive}"${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 isn't 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 "Acquired ${cmd} command out of sequence: anticipated mnted" >&2
fi
;;
# Clear a token that's 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 'Acquired unknown command:' "${cmd}" >&2
;;
esac
completed <&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 amount 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
completed
fi
# Ensure the amount is obtainable 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 amount
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
# Affirm mounting of quantity to socket (if registered)
[[ -n "${token}" ]] && { volume_confirm=$(socket_cmd "mnted" "${token}") || token=; }
printf '%s' 'Awaiting sign... '
# Lure and wait till job is ended, then lock the amount
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 'completed.' && break
[[ -n "${CREATE_DIRECTORY}" ]] && umount "${CREATE_DIRECTORY}" && echo 'completed.' && break
[[ -n "${token}" ]] && volume_confirm=$(socket_cmd 'unmnt' "${token}") && token= && echo 'completed.' && break
makes an attempt=$((${makes an attempt} - 1))
sleep 5
completed
if [[ ${attempts} = 0 ]]; then
if diskutil unmount drive "${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 $!
completed
fi
else
echo 'Unable to mount quantity:' "${error}" "${error2}" >&2
[[ -n "${token}" ]] && socket_cmd 'clear' "${token}"
fi
fi
}