macos – Unmount Quantity on Consumer Logout

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.


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.


# 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_DIRECTORY="$2"; shift
            case "${CREATE_DIRECTORY:0:1}" in
                ("   ;;
                ('~')   CREATE_DIRECTORY="${HOME}${CREATE_DIRECTORY:1}"   ;;
        # 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 ;;

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; }

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
        case "${cmd}" in
            # Signifies intention to mount a present unmounted quantity (given in worth).
            # Returns a token that should be utilized in future instructions
                if mount=$(diskutil data "${worth}" 2>/dev/null | grep 'Mounted' | sed 's/[^:]*: *//') && [[ "${mount}" = 'No' ]]; then
                    token=$(echo "${worth}$(head -c 512 </dev/urandom)" | md5)
                    printf '%spercents' 'mount' "${token}" >&p

                    [[ ${VERBOSITY} -gt 0 ]] && echo 'Accepted mount request for:' "${worth} assigned token ending with:" "${token: -4}"
                    printf '%spercents' 'error' 'Quantity not discovered, or is already mounted' >&p
                    [[ ${VERBOSITY} -gt 0 ]] && echo 'Quantity not discovered or already mounted:' "${worth}" >&2
            # 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
                if [ -n "${volume}" ]; then
                    if mount=$(diskutil data "${quantity}" 2>/dev/null | grep 'Mounted' | sed 's/[^:]*: *//') && [[ "${mount}" != 'No' ]]; then
                        unset "requested[${token}]"
                        printf '%spercents' 'mnted' "${quantity}" >&p

                        [[ ${VERBOSITY} -gt 0 ]] && echo 'Confirmed mounting of:' "${quantity} utilizing token ending with:" "${worth: -4}"
                        printf '%spercents' 'error' 'Quantity not discovered, or isn't mounted' >&p
                        [[ ${VERBOSITY} -gt 0 ]] && echo 'Quantity not discovered or not mounted:' "${quantity}" >&2
                    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
            # 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
                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}"
                            printf '%spercents' 'error' "Unable to unmount ${quantity}: ${error}" >&p
                            [[ ${VERBOSITY} -gt 0 ]] && echo 'Unable to mount:' "${quantity}: ${error}" >&2
                        printf '%spercents' 'error' 'Quantity not discovered, or isn't mounted' >&p
                        [[ ${VERBOSITY} -gt 0 ]] && echo 'Quantity not discovered:' "${quantity}" >&2
                    printf '%spercents' 'error' 'Unknown token: use the mnted command first' >&p
                    [[ ${VERBOSITY} -gt 0 ]] && echo "Acquired ${cmd} command out of sequence: anticipated mnted" >&2
            # Clear a token that's not wanted
                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
    completed <&p

    coproc :
    [[ ${VERBOSITY} -gt 0 ]] && echo 'Terminating.'
    [[ -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; }

    # 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

    # 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; }
    # If a socket was given, register our intention to mount the amount
    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
                    printf '%s' "${response:5}"
                    coproc :
                    return 0
                    echo "socket_cmd() error: ${response:5}" >&2
                    coproc :
                    return 2
                    echo 'Unknown/unsupported response:' "${response}" >&2
                    coproc :
                    return 3
        token=$(socket_cmd 'mount' "${VOLUME}") || SOCKET=

    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() {
                [[ ${cleanup_run} = 0 ]] || return 0
                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
                if [[ ${attempts} = 0 ]]; then
                    if diskutil unmount drive "${VOLUME}" >/dev/null; then
                        echo 'pressured.'
                        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
                                echo 'pressured.'
                            echo 'pressured.'
                [[ -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 $!
        echo 'Unable to mount quantity:' "${error}" "${error2}" >&2
        [[ -n "${token}" ]] && socket_cmd 'clear' "${token}"


Related Articles


Please enter your comment!
Please enter your name here

Latest Articles