{ lib, stdenv, system, pkgs, jq, coreutils, udisks, toybox, util-linux, writeShellApplication, }: writeShellApplication { # Originally from: https://github.com/scawp/Steam-Deck.Mount-External-Drive/ name = "auto-mount"; runtimeInputs = [jq coreutils udisks util-linux toybox]; text = '' set -euo pipefail # Originally from https://serverfault.com/a/767079 # This script is called from our systemd unit file to mount or unmount # a USB drive. usage() { echo "Usage: $0 {add|remove} device_name (e.g. sdb1)" exit 1 } if [[ $# -ne 2 ]]; then usage fi ACTION=$1 DEVBASE=$2 DEVICE="/dev/''${DEVBASE}" # Shared between this and the auto-mount script to ensure we're not double-triggering nor automounting while formatting # or vice-versa. MOUNT_LOCK="/home/lillian/lock/jupiter-automount-''${DEVBASE//\/_}.lock" # Obtain lock exec 9<>"$MOUNT_LOCK" if ! flock -n 9; then echo "$MOUNT_LOCK is active: ignoring action $ACTION" # Do not return a success exit code: it could end up putting the service in 'started' state without doing the mount # work (further start commands will be ignored after that) exit 1 fi # Wait N seconds for steam wait_steam() { local i=0 local wait=$1 echo "Waiting up to $wait seconds for steam to load" while ! pgrep -x steamwebhelper &>/dev/null && (( i++ < wait )); do sleep 1 done } send_steam_url() { local command command="$1" local arg arg="$2" local encoded encoded=$(urlencode "$arg") if pgrep -x "steam" > /dev/null; then # TODO use -ifrunning and check return value - if there was a steam process and it returns -1, the message wasn't sent # need to retry until either steam process is gone or -ifrunning returns 0, or timeout i guess echo "Sent URL to steam: steam://''${command}/''${arg} (steam://''${command}/''${encoded})" >> /home/lillian/steam.txt systemd-run -M 1000@ --user --collect --wait sh -c "${steam}/bin/steam steam://''${command}/''${encoded@Q}" else echo "Could not send steam URL steam://''${command}/''${arg} (steam://''${command}/''${encoded}) -- steam not running" fi } # From https://gist.github.com/HazCod/da9ec610c3d50ebff7dd5e7cac76de05 urlencode() { [ -z "$1" ] || echo -n "$@" | hexdump -v -e '/1 "%02x"' | sed 's/\(..\)/%\1/g' } do_mount() { declare -i ret # NOTE: these values are ABI, since they are sent to the Steam client # shellcheck disable=SC2034 readonly FSCK_ERROR=1 # shellcheck disable=SC2034 readonly MOUNT_ERROR=2 # Get info for this drive: $ID_FS_LABEL, and $ID_FS_TYPE dev_json=$(lsblk -o PATH,LABEL,FSTYPE --json -- "$DEVICE" | jq '.blockdevices[0]') ID_FS_LABEL=$(jq -r '.label | select(type == "string")' <<< "$dev_json") ID_FS_TYPE=$(jq -r '.fstype | select(type == "string")' <<< "$dev_json") # Global mount options OPTS="rw,noatime" # File system type specific mount options #if [[ ''${ID_FS_TYPE} == "vfat" ]]; then # OPTS+=",users,gid=100,umask=000,shortname=mixed,utf8=1,flush" #fi case "''${ID_FS_TYPE}" in "ntfs") echo "FSType is NTFS" #Extra Opts don't seem necessary anymore? add if required #OPTS+="" ;; "exfat") echo "FSType is exFat" #OPTS+=",users,gid=100,umask=000,shortname=mixed,utf8=1,flush" ;; "btrfs") echo "FSType is btrfs" ;; "ext4") echo "FSType is ext4" #exit 2 ;; *) echo "Error mounting ''${DEVICE}: unsupported fstype: ''${ID_FS_TYPE} - ''${dev_json}" rm "''${MOUNT_LOCK}" exit 2 ;; esac # Prior to talking to udisks, we need all udev hooks (we were started by one) to finish, so we know it has knowledge # of the drive. Our own rule starts us as a service with --no-block, so we can wait for rules to settle here # safely. #if ! udevadm settle; then # echo "Failed to wait for \`udevadm settle\`" # exit 1 #fi # Ask udisks to auto-mount. This needs a version of udisks that supports the 'as-user' option. ret=0 reply=$(busctl call --allow-interactive-authorization=false --expect-reply=true --json=short \ org.freedesktop.UDisks2 \ /org/freedesktop/UDisks2/block_devices/"''${DEVBASE}" \ org.freedesktop.UDisks2.Filesystem \ Mount 'a{sv}' 3 \ as-user s lillian \ auth.no_user_interaction b true \ options s "$OPTS") || ret=$? if (( ret != 0 )); then # send_steam_url "system/devicemountresult" "''${DEVBASE}/''${MOUNT_ERROR}" echo "Error mounting ''${DEVICE} (status = $ret)" exit 1 fi # Expected reply is of the format # {"type":"s","data":["/run/media/lillian/home"]} mount_point=$(jq -r '.data[0] | select(type == "string")' <<< "$reply" || true) if [[ -z $mount_point ]]; then echo "Error when mounting ''${DEVICE}: udisks returned success but could not parse reply:" echo "---"$'\n'"$reply"$'\n'"---" exit 1 fi if [[ ''${ID_FS_TYPE} == "exfat" ]]; then echo "exFat does not support symlinks, do not add library to Steam" exit 0 fi # Create a symlink from /run/media to keep compatibility with apps # that use the older mount point (for SD cards only). case "''${DEVBASE}" in mmcblk0p*) if [[ -z "''${ID_FS_LABEL}" ]]; then old_mount_point="/run/media/''${DEVBASE}" else old_mount_point="/run/media/''${mount_point##*/}" fi if [[ ! -d "''${old_mount_point}" ]]; then rm -f -- "''${old_mount_point}" ln -s -- "''${mount_point}" "''${old_mount_point}" fi ;; esac echo "**** Mounted ''${DEVICE} at ''${mount_point} ****" if [ -f "''${mount_point}/libraryfolder.vdf" ]; then echo " send_steam_url \"addlibraryfolder\" \"''${mount_point}\"" # send_steam_url "addlibraryfolder" "''${mount_point}" else #TODO check permissions are 1000 when creating new SteamLibrary mkdir -p "''${mount_point}/SteamLibrary" chown lillian:users "''${mount_point}/SteamLibrary" # send_steam_url "addlibraryfolder" "''${mount_point}/SteamLibrary" fi } do_unmount() { local mount_point mount_point=$(findmnt -fno TARGET "''${DEVICE}" || true) if [[ -n $mount_point ]]; then # Remove symlink to the mount point that we're unmounting find /run/media -maxdepth 1 -xdev -type l -lname "''${mount_point}" -exec rm -- {} \; else # If we don't know the mount point then remove all broken symlinks find /run/media -maxdepth 1 -xdev -xtype l -exec rm -- {} \; fi } do_retrigger() { local mount_point mount_point=$(findmnt -fno TARGET "''${DEVICE}" || true) [[ -n $mount_point ]] || return 0 # In retrigger mode, we want to wait a bit for steam as the common pattern is starting in parallel with a retrigger wait_steam 10 # This is a truly gnarly way to ensure steam is ready for commands. # TODO literally anything else sleep 6 # send_steam_url "addlibraryfolder" "''${mount_point}" } case "''${ACTION}" in add) do_mount ;; remove) do_unmount ;; retrigger) do_retrigger ;; *) usage ;; esac ''; }