From 6b7ca566df1b59f71cc187a0f5cf0658c2867333 Mon Sep 17 00:00:00 2001 From: kisfenyo Date: Wed, 18 Feb 2026 21:30:32 +0100 Subject: [PATCH] fix: clean up stale raw mounts before scanning in attach wizard After an interrupted attach wizard, the raw mount stays behind, causing the device to appear as "mounted" in scan results. Now the scan button calls cancel first, which unmounts any stale raw mounts that have no bind mount in fstab. Co-Authored-By: Claude Opus 4.6 --- controller/internal/storage/attach_linux.go | 48 +++++++++++++++++++ controller/internal/storage/attach_other.go | 3 ++ controller/internal/web/storage_handlers.go | 19 ++++---- .../web/templates/storage_attach.html | 6 ++- 4 files changed, 66 insertions(+), 10 deletions(-) diff --git a/controller/internal/storage/attach_linux.go b/controller/internal/storage/attach_linux.go index 16a5e87..3e54a25 100644 --- a/controller/internal/storage/attach_linux.go +++ b/controller/internal/storage/attach_linux.go @@ -323,6 +323,54 @@ func CleanupRawMount(rawPath string) error { return nil } +// CleanupStaleRawMounts finds and removes raw mounts that have no corresponding +// bind mount — i.e., leftovers from an interrupted attach wizard. +// A raw mount is considered "in use" if fstab has a bind entry sourcing from it. +func CleanupStaleRawMounts() { + data, err := os.ReadFile("/proc/mounts") + if err != nil { + return + } + + // Read fstab to check for bind mount entries + fstabData, _ := os.ReadFile(FstabPath) + fstabLines := strings.Split(string(fstabData), "\n") + + for _, line := range strings.Split(string(data), "\n") { + fields := strings.Fields(line) + if len(fields) < 2 { + continue + } + mountPoint := fields[1] + if !strings.HasPrefix(mountPoint, RawMountBase+"/") { + continue + } + // Only consider direct children of RawMountBase (e.g., /mnt/.felhom-raw/hdd_1) + rel := strings.TrimPrefix(mountPoint, RawMountBase+"/") + if strings.Contains(rel, "/") { + continue + } + + // Check if any fstab bind entry sources from this raw mount path + inUse := false + for _, fl := range fstabLines { + fl = strings.TrimSpace(fl) + if fl == "" || strings.HasPrefix(fl, "#") { + continue + } + if strings.Contains(fl, "bind") && strings.HasPrefix(fl, mountPoint) { + inUse = true + break + } + } + + if !inUse { + _ = exec.Command("umount", mountPoint).Run() + _ = os.Remove(mountPoint) + } + } +} + // --- helpers --- // getBlkidValue runs blkid to get a single value (TYPE, UUID, LABEL) for a device. diff --git a/controller/internal/storage/attach_other.go b/controller/internal/storage/attach_other.go index c87cd22..30b019b 100644 --- a/controller/internal/storage/attach_other.go +++ b/controller/internal/storage/attach_other.go @@ -28,3 +28,6 @@ func FinalizeAttach(req AttachRequest, progress chan<- FormatProgress) (string, func CleanupRawMount(rawPath string) error { return fmt.Errorf("storage attach is only supported on Linux") } + +// CleanupStaleRawMounts is a no-op on non-Linux platforms. +func CleanupStaleRawMounts() {} diff --git a/controller/internal/web/storage_handlers.go b/controller/internal/web/storage_handlers.go index bb26d27..79c7ca8 100644 --- a/controller/internal/web/storage_handlers.go +++ b/controller/internal/web/storage_handlers.go @@ -1071,22 +1071,23 @@ func (s *Server) storageAttachStatusAPIHandler(w http.ResponseWriter, r *http.Re // storageAttachCancelHandler handles POST /api/storage/attach/cancel. // Cleans up the temporary raw mount when the user cancels the wizard. +// Also cleans up any stale raw mounts from interrupted previous sessions. func (s *Server) storageAttachCancelHandler(w http.ResponseWriter, r *http.Request) { s.diskJobMu.Lock() rawMount := s.activeRawMount s.activeRawMount = "" s.diskJobMu.Unlock() - if rawMount == "" { - jsonResponse(w, map[string]interface{}{"ok": true, "msg": "Nincs aktív raw mount"}) - return + if rawMount != "" { + if err := storage.CleanupRawMount(rawMount); err != nil { + s.logger.Printf("[WARN] Failed to cleanup raw mount %s: %v", rawMount, err) + } else { + s.logger.Printf("[INFO] Cleaned up raw mount: %s", rawMount) + } } - if err := storage.CleanupRawMount(rawMount); err != nil { - s.logger.Printf("[WARN] Failed to cleanup raw mount %s: %v", rawMount, err) - } else { - s.logger.Printf("[INFO] Cleaned up raw mount: %s", rawMount) - } + // Also clean up any stale raw mounts from previous interrupted sessions + storage.CleanupStaleRawMounts() - jsonResponse(w, map[string]interface{}{"ok": true, "msg": "Raw mount eltávolítva"}) + jsonResponse(w, map[string]interface{}{"ok": true}) } diff --git a/controller/internal/web/templates/storage_attach.html b/controller/internal/web/templates/storage_attach.html index 35d25dd..4785de6 100644 --- a/controller/internal/web/templates/storage_attach.html +++ b/controller/internal/web/templates/storage_attach.html @@ -167,7 +167,11 @@ function scanDisks() { errEl.style.display = 'none'; resultEl.style.display = 'none'; - fetch('/api/storage/scan', {method:'POST'}) + // Clean up any stale raw mounts from interrupted previous sessions first, + // so the device appears as available in the scan results. + fetch('/api/storage/attach/cancel', {method:'POST'}) + .catch(function(){}) // ignore cancel errors + .then(function() { return fetch('/api/storage/scan', {method:'POST'}); }) .then(function(r){ return r.json(); }) .then(function(data) { btn.textContent = '🔍 Meghajtók keresése';