#!/bin/sh # mount_helper.sh # Privilegierter Daemon für Mount-Operationen, kommuniziert über FIFO FIFO="/tmp/mount_cmd" RESULT="/tmp/mount_result" # Aufräumen rm -f "$FIFO" "$RESULT" # FIFO anlegen mkfifo "$FIFO" || { echo "FEHLER: mkfifo gescheitert"; exit 1; } echo "[mount_helper] Bereit auf $FIFO" echo "[mount_helper] Capabilities: $(cat /proc/self/status 2>/dev/null | grep Cap || echo 'n/a')" # Wrapper: try util-linux mount first; if fsconfig() is blocked (HA-OS seccomp), # fall back to busybox mount which uses the old mount(2) syscall. do_mount() { local _OUT _RC _OUT=$(mount "$@" 2>&1) _RC=$? if [ $_RC -ne 0 ] && echo "$_OUT" | grep -q "fsconfig()"; then echo "[mount_helper] fsconfig() blocked — retrying with busybox mount" _OUT=$(busybox mount "$@" 2>&1) _RC=$? fi echo "$_OUT" return $_RC } # Detect the classic "no CAP_SYS_ADMIN" symptom. On Home Assistant this means # Protection Mode is still ENABLED — which makes the Supervisor ignore the # add-on's full_access / privileged / apparmor:false settings, so mount(2) # returns EPERM. is_perm_error() { echo "$1" | grep -qiE "permission denied|operation not permitted|are you root" } # Mount with a specific FS type. For NTFS, prefer the in-kernel ntfs3 driver # (Linux 5.15+, no FUSE required) and fall back to ntfs-3g (FUSE) only if the # kernel driver isn't available. HA-OS add-on containers expose no /dev/fuse, # so ntfs-3g fails there with "fuse device is missing, try 'modprobe fuse'". mount_fs() { _FS="$1"; shift case "$_FS" in ntfs|ntfs-3g|ntfs3|fuseblk) echo "[mount_helper] NTFS: trying kernel driver ntfs3 first" _OUT=$(do_mount -t ntfs3 "$@" 2>&1) _RC=$? if [ $_RC -ne 0 ]; then echo "[mount_helper] ntfs3 failed ($_OUT) — falling back to ntfs-3g (FUSE)" _OUT=$(do_mount -t ntfs-3g "$@" 2>&1) _RC=$? fi ;; ext2|ext3|ext4) # noatime: no metadata write on every read. # errors=remount-ro: on a filesystem error go read-only instead # of silently writing on (the ext4 default is errors=continue). _OUT=$(do_mount -t "$_FS" -o noatime,errors=remount-ro "$@" 2>&1) _RC=$? ;; *) _OUT=$(do_mount -t "$_FS" "$@" 2>&1) _RC=$? ;; esac echo "$_OUT" return $_RC } # Keep the FIFO open persistently on fd 3 so there's no moment between # command-iterations where the FIFO has no reader. Without this, the Python # side (which opens with O_NONBLOCK) hits ENXIO right after each command. # We also open fd 4 as a dummy writer so `read` never sees EOF when the # client side closes — that would otherwise drop us out of the loop. exec 3<"$FIFO" 4>"$FIFO" while true; do if read -r line <&3; then ACTION=$(echo "$line" | cut -d'|' -f1) ARG1=$( echo "$line" | cut -d'|' -f2) ARG2=$( echo "$line" | cut -d'|' -f3) ARG3=$( echo "$line" | cut -d'|' -f4) case "$ACTION" in MOUNT) DEVICE="$ARG1" MOUNTPOINT="$ARG2" FSTYPE="$ARG3" echo "[mount_helper] MOUNT: $DEVICE -> $MOUNTPOINT (fs=$FSTYPE)" mkdir -p "$MOUNTPOINT" 2>/dev/null # Verify device exists if [ ! -b "$DEVICE" ]; then echo "[mount_helper] FEHLER: $DEVICE ist kein Block-Device" printf '1|%s ist kein Block-Device oder nicht gefunden\n' "$DEVICE" > "$RESULT" continue fi # Try mount if [ -n "$FSTYPE" ] && [ "$FSTYPE" != "auto" ]; then echo "[mount_helper] Trying: mount -t $FSTYPE $DEVICE $MOUNTPOINT" OUT=$(mount_fs "$FSTYPE" "$DEVICE" "$MOUNTPOINT" 2>&1) RC=$? else # AUTO: blkid/lsblk sometimes return nothing for by-id # symlinks even though mount -t works fine. Resolve the # symlink first, then escalate through several detectors, # and as a last resort try common FS types directly. REAL_DEV=$(readlink -f "$DEVICE" 2>/dev/null) [ -z "$REAL_DEV" ] && REAL_DEV="$DEVICE" echo "[mount_helper] auto: real device = $REAL_DEV" DETECTED=$(blkid -o value -s TYPE "$REAL_DEV" 2>/dev/null | head -1) [ -z "$DETECTED" ] && DETECTED=$(blkid -p -o value -s TYPE "$REAL_DEV" 2>/dev/null | head -1) [ -z "$DETECTED" ] && DETECTED=$(lsblk -no FSTYPE "$REAL_DEV" 2>/dev/null | head -1 | tr -d ' ') # file -s reads the first bytes directly — works when blkid can't if [ -z "$DETECTED" ] && command -v file >/dev/null 2>&1; then FILE_OUT=$(file -sL "$REAL_DEV" 2>/dev/null) case "$FILE_OUT" in *ext4*) DETECTED=ext4 ;; *ext3*) DETECTED=ext3 ;; *ext2*) DETECTED=ext2 ;; *NTFS*) DETECTED=ntfs ;; *FAT*) DETECTED=vfat ;; *exFAT*) DETECTED=exfat ;; *BTRFS*|*btrfs*) DETECTED=btrfs ;; *XFS*) DETECTED=xfs ;; esac [ -n "$DETECTED" ] && echo "[mount_helper] auto: file -s detected '$DETECTED'" fi RC=1 if [ -n "$DETECTED" ]; then echo "[mount_helper] auto: detected '$DETECTED'" echo "[mount_helper] Trying: mount -t $DETECTED $DEVICE $MOUNTPOINT" OUT=$(mount_fs "$DETECTED" "$DEVICE" "$MOUNTPOINT" 2>&1) RC=$? fi # Last resort: brute-force the common Linux/Windows types. # mount -t on a USB blockdev fails fast and cleanly, # so we can iterate without harm. if [ $RC -ne 0 ]; then if [ -n "$DETECTED" ]; then echo "[mount_helper] auto: '$DETECTED' failed ($OUT) — trying common FS types" else echo "[mount_helper] auto: no FS detected — brute-force common types" fi for TRY_FS in ext4 ext3 ext2 ntfs3 vfat exfat btrfs xfs; do OUT=$(mount_fs "$TRY_FS" "$DEVICE" "$MOUNTPOINT" 2>&1) RC=$? if [ $RC -eq 0 ]; then echo "[mount_helper] auto: brute-force succeeded with -t $TRY_FS" DETECTED="$TRY_FS" break fi done fi # Final fallback: bare mount (relies on kernel auto-probe) if [ $RC -ne 0 ]; then echo "[mount_helper] auto: brute-force failed — last bare mount attempt" OUT=$(do_mount "$DEVICE" "$MOUNTPOINT" 2>&1) RC=$? fi fi # If failed, log debug info if [ $RC -ne 0 ]; then echo "[mount_helper] Mount fehlgeschlagen (rc=$RC): $OUT" echo "[mount_helper] DEBUG: id=$(id)" echo "[mount_helper] DEBUG: ls -la $DEVICE = $(ls -la "$DEVICE" 2>&1)" echo "[mount_helper] DEBUG: blkid $DEVICE = $(blkid "$DEVICE" 2>&1)" echo "[mount_helper] DEBUG: mountinfo (last 5):" tail -5 /proc/self/mountinfo 2>/dev/null # Permission error → almost always Protection Mode still on if is_perm_error "$OUT"; then CAPEFF=$(grep CapEff /proc/self/status 2>/dev/null | awk '{print $2}') echo "[mount_helper] =====================================================" echo "[mount_helper] PERMISSION ERROR — this is NOT a filesystem problem." echo "[mount_helper] CapEff=$CAPEFF (all-zero = NO capabilities granted)" echo "[mount_helper] The add-on has no CAP_SYS_ADMIN, so mount() returns EPERM." echo "[mount_helper] CAUSE: 'Protection mode' is still ENABLED for this add-on." echo "[mount_helper] FIX: Open the add-on page -> Info tab ->" echo "[mount_helper] turn OFF the 'Protection mode' toggle -> Restart." echo "[mount_helper] =====================================================" OUT="Permission denied (no CAP_SYS_ADMIN). Disable 'Protection mode' in the add-on Info tab and restart the add-on. (raw: $OUT)" fi fi printf '%s|%s\n' "$RC" "$OUT" > "$RESULT" echo "[mount_helper] MOUNT result: rc=$RC" ;; UMOUNT) echo "[mount_helper] UMOUNT: $ARG1" OUT=$(umount "$ARG1" 2>&1) RC=$? printf '%s|%s\n' "$RC" "$OUT" > "$RESULT" echo "[mount_helper] UMOUNT result: rc=$RC" ;; UMOUNT_LAZY) echo "[mount_helper] UMOUNT_LAZY: $ARG1" OUT=$(umount -l "$ARG1" 2>&1) RC=$? printf '%s|%s\n' "$RC" "$OUT" > "$RESULT" echo "[mount_helper] UMOUNT_LAZY result: rc=$RC" ;; FUSER) # Show what's holding the mount busy OUT=$(fuser -mv "$ARG1" 2>&1; lsof +D "$ARG1" 2>&1 | head -20) printf '0|%s\n' "$OUT" > "$RESULT" ;; MKDIR) mkdir -p "$ARG1" 2>/dev/null printf '0|\n' > "$RESULT" ;; PARTLIST) # Machine-readable partition layout incl. free-space gaps. # ARG1 = /dev/sdX (whole disk) DISK="$ARG1" OUT=$(parted -m -s "$DISK" unit MiB print free 2>&1) RC=$? # Newlines break our FIFO line protocol → encode as \\n ENC=$(printf '%s' "$OUT" | sed ':a;N;$!ba;s/\n/\\n/g') printf '%s|%s\n' "$RC" "$ENC" > "$RESULT" echo "[mount_helper] PARTLIST $DISK rc=$RC" ;; PARTMKLABEL) # Initialize new partition table. ARG1 = /dev/sdX, ARG2 = gpt|msdos DISK="$ARG1"; LBL="$ARG2" echo "[mount_helper] PARTMKLABEL $DISK $LBL" OUT=$(parted -s "$DISK" mklabel "$LBL" 2>&1) RC=$? partprobe "$DISK" 2>/dev/null printf '%s|%s\n' "$RC" "$OUT" > "$RESULT" ;; PARTADD) # Create partition. ARG1 = /dev/sdX, ARG2 = START (MiB), # ARG3 = END (MiB or "100%") DISK="$ARG1"; PSTART="$ARG2"; PEND="$ARG3" echo "[mount_helper] PARTADD $DISK ${PSTART}MiB → $PEND" OUT=$(parted -s -a optimal "$DISK" mkpart primary "${PSTART}MiB" "$PEND" 2>&1) RC=$? partprobe "$DISK" 2>/dev/null # parted itself sometimes returns 0 even when partprobe needs a moment sleep 1 printf '%s|%s\n' "$RC" "$OUT" > "$RESULT" ;; PARTRM) # Remove partition. ARG1 = /dev/sdX, ARG2 = partition number DISK="$ARG1"; PNUM="$ARG2" echo "[mount_helper] PARTRM $DISK $PNUM" OUT=$(parted -s "$DISK" rm "$PNUM" 2>&1) RC=$? partprobe "$DISK" 2>/dev/null printf '%s|%s\n' "$RC" "$OUT" > "$RESULT" ;; MKFS) # Format partition. ARG1 = /dev/sdXN, ARG2 = fstype, ARG3 = label PART="$ARG1"; FS="$ARG2"; LABEL="$ARG3" echo "[mount_helper] MKFS $PART -t $FS -L '$LABEL'" if [ ! -b "$PART" ]; then printf '1|%s ist kein Block-Device\n' "$PART" > "$RESULT" continue fi # Refuse if currently mounted if grep -q "^$PART " /proc/mounts 2>/dev/null; then printf '1|%s ist eingehängt — bitte zuerst aushängen\n' "$PART" > "$RESULT" continue fi case "$FS" in ext4) OUT=$(mkfs.ext4 -F -L "$LABEL" "$PART" 2>&1) ;; ext3) OUT=$(mkfs.ext3 -F -L "$LABEL" "$PART" 2>&1) ;; ext2) OUT=$(mkfs.ext2 -F -L "$LABEL" "$PART" 2>&1) ;; exfat) OUT=$(mkfs.exfat -n "$LABEL" "$PART" 2>&1) ;; vfat|fat32) OUT=$(mkfs.vfat -F 32 -n "$LABEL" "$PART" 2>&1) ;; ntfs) OUT=$(mkfs.ntfs -f -L "$LABEL" "$PART" 2>&1) ;; *) OUT="Unsupported filesystem: $FS" RC=1 printf '%s|%s\n' "$RC" "$OUT" > "$RESULT" continue ;; esac RC=$? # New filesystem signature isn't always picked up immediately; # ask kernel to re-read & udev to refresh symlinks partprobe "$(echo "$PART" | sed 's/[0-9]*$//')" 2>/dev/null printf '%s|%s\n' "$RC" "$OUT" > "$RESULT" echo "[mount_helper] MKFS rc=$RC" ;; PARTPROBE) partprobe "$ARG1" 2>/dev/null printf '0|\n' > "$RESULT" ;; *) printf '1|Unbekannter Befehl: %s\n' "$ACTION" > "$RESULT" ;; esac fi done