Skip to content

Commit 9b79c55

Browse files
committed
fix: handle mount lifecycle cleanup
Generated-by: gpt-5
1 parent 1d83449 commit 9b79c55

3 files changed

Lines changed: 115 additions & 17 deletions

File tree

Mounty/ViewModels/VolumeManager.swift

Lines changed: 91 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -454,12 +454,16 @@ final class VolumeManager {
454454
disableAutomount(for: volume)
455455
guard let path = mountPaths[volume.id] else { return }
456456

457-
mountPaths.removeValue(forKey: volume.id)
458457
busyVolumes.insert(volume.id)
459458
log("Disconnecting \(volume.name)")
460459

461460
Task {
462-
await MountService.unmount(path: path)
461+
guard await MountService.unmount(path: path) else {
462+
self.reportUnmountFailure(for: volume, action: "Disconnect")
463+
await self.refreshState()
464+
return
465+
}
466+
self.mountPaths.removeValue(forKey: volume.id)
463467
self.busyVolumes.remove(volume.id)
464468
self.log("Disconnected: \(volume.name)")
465469
await self.refreshState()
@@ -488,33 +492,67 @@ final class VolumeManager {
488492
}
489493

490494
func removeVolume(_ id: UUID) {
491-
if let v = volumes.first(where: { $0.id == id }) {
492-
log("Removed volume: \(v.name)")
495+
guard let volume = volumes.first(where: { $0.id == id }) else { return }
496+
guard let path = mountPaths[id] else {
497+
removeVolumeConfiguration(id: id, name: volume.name)
498+
Task { await refreshState() }
499+
return
500+
}
501+
502+
busyVolumes.insert(id)
503+
log("Removing volume: \(volume.name)")
504+
Task {
505+
guard await MountService.unmount(path: path) else {
506+
self.reportUnmountFailure(for: volume, action: "Remove")
507+
await self.refreshState()
508+
return
509+
}
510+
self.mountPaths.removeValue(forKey: id)
511+
self.busyVolumes.remove(id)
512+
self.removeVolumeConfiguration(id: id, name: volume.name)
513+
await self.refreshState()
493514
}
494-
volumes.removeAll { $0.id == id }
495-
storage.saveVolumes(volumes)
496-
Task { await refreshState() }
497515
}
498516

499517
func editVolume(id: UUID, name: String, serverAddress: String) {
500518
guard let idx = volumes.firstIndex(where: { $0.id == id }) else { return }
501519
let old = volumes[idx]
502520
let addressChanged = old.serverAddress != serverAddress
503521

504-
volumes[idx].name = name
505-
volumes[idx].serverAddress = serverAddress
506-
storage.saveVolumes(volumes)
507-
log("Updated volume: \(name)")
522+
if !addressChanged {
523+
volumes[idx].name = name
524+
storage.saveVolumes(volumes)
525+
log("Updated volume: \(name)")
526+
return
527+
}
508528

509-
guard addressChanged, let oldPath = mountPaths[id] else { return }
529+
guard let oldPath = mountPaths[id] else {
530+
volumes[idx].name = name
531+
volumes[idx].serverAddress = serverAddress
532+
storage.saveVolumes(volumes)
533+
log("Updated volume: \(name)")
534+
return
535+
}
510536

511537
// Unmount the old connection by its recorded path, then remount at the new address.
512538
// Using oldPath (not the new address) ensures we disconnect the right kernel mount
513539
// even if the new address points to a different share entirely.
514-
mountPaths.removeValue(forKey: id)
515540
busyVolumes.insert(id)
516541
Task {
517-
await MountService.unmount(path: oldPath)
542+
guard await MountService.unmount(path: oldPath) else {
543+
self.reportUnmountFailure(for: old, action: "Reconnect")
544+
await self.refreshState()
545+
return
546+
}
547+
self.mountPaths.removeValue(forKey: id)
548+
guard let currentIndex = self.volumes.firstIndex(where: { $0.id == id }) else {
549+
self.busyVolumes.remove(id)
550+
return
551+
}
552+
self.volumes[currentIndex].name = name
553+
self.volumes[currentIndex].serverAddress = serverAddress
554+
self.storage.saveVolumes(self.volumes)
555+
self.log("Updated volume: \(name)")
518556
guard let url = URL(string: serverAddress) else {
519557
self.busyVolumes.remove(id)
520558
return
@@ -530,14 +568,50 @@ final class VolumeManager {
530568
self.showError = true
531569
}
532570
self.busyVolumes.remove(id)
571+
await self.refreshState()
533572
}
534573
}
535574

536575
func clearAllVolumes() {
537-
log("Cleared all volumes")
538-
volumes.removeAll()
576+
let configuredVolumes = volumes
577+
let mountedVolumes = configuredVolumes.compactMap { volume in
578+
mountPaths[volume.id].map { (volume, $0) }
579+
}
580+
for (volume, _) in mountedVolumes { busyVolumes.insert(volume.id) }
581+
582+
Task {
583+
var retainedIDs = Set<UUID>()
584+
for (volume, path) in mountedVolumes {
585+
guard await MountService.unmount(path: path) else {
586+
retainedIDs.insert(volume.id)
587+
self.reportUnmountFailure(for: volume, action: "Clear")
588+
continue
589+
}
590+
self.mountPaths.removeValue(forKey: volume.id)
591+
self.busyVolumes.remove(volume.id)
592+
}
593+
self.volumes.removeAll { !retainedIDs.contains($0.id) }
594+
self.storage.saveVolumes(self.volumes)
595+
if retainedIDs.isEmpty {
596+
self.log("Cleared all volumes")
597+
} else {
598+
self.log("Clear retained \(retainedIDs.count) mounted volume(s)", level: .warning)
599+
}
600+
await self.refreshState()
601+
}
602+
}
603+
604+
private func removeVolumeConfiguration(id: UUID, name: String) {
605+
volumes.removeAll { $0.id == id }
539606
storage.saveVolumes(volumes)
540-
Task { await refreshState() }
607+
log("Removed volume: \(name)")
608+
}
609+
610+
private func reportUnmountFailure(for volume: Volume, action: String) {
611+
busyVolumes.remove(volume.id)
612+
lastError = "Could not disconnect \(volume.name). The share remains mounted."
613+
showError = true
614+
log("\(action) failed; \(volume.name) remains mounted", level: .error)
541615
}
542616

543617
func toggleAutomount(_ id: UUID) {
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
# STORY-009: Mount lifecycle cleanup and failure handling
2+
3+
- Status: CLOSED
4+
- Type: fix
5+
- Date: 2026-08-10
6+
- Commit: _none_
7+
8+
## Intent
9+
10+
Keep SMB shares managed during removal, reset, and edits by unmounting explicitly and preserving configuration when unmounting fails.
11+
12+
## Acceptance criteria
13+
14+
- [x] Removing or resetting a mounted volume unmounts it before deleting its configuration.
15+
- [x] Failed unmounts retain the volume configuration and mounted state, with actionable feedback.
16+
- [x] Editing a mounted volume stops before reconnecting when its old mount cannot be unmounted.
17+
- [x] Manual unmount failure is reported accurately.
18+
19+
## Validation
20+
21+
`swift-format lint --strict Mounty/ViewModels/VolumeManager.swift` and
22+
`xcodebuild -scheme Mounty -destination 'platform=macOS,arch=arm64' test
23+
CODE_SIGNING_ALLOWED=NO` passed. Editor diagnostics reported no errors.

docs/stories/INDEX.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ Newest stories first. Statuses: `OPEN`, `IN_PROGRESS`, `CLOSED`.
44

55
| ID | Type | Story | Status | Date |
66
| --- | --- | --- | --- | --- |
7+
| [009](./009-mount-lifecycle-cleanup.md) | fix | Mount lifecycle cleanup and failure handling | CLOSED | 2026-08-10 |
78
| [008](./008-mount-reliability-and-logging.md) | fix | Reliable mounting and unified logging | CLOSED | 2026-08-10 |
89
| [007](./007-modern-swift-and-mount-state.md) | refactor | Modern Swift and accurate mount state | CLOSED | 2026-08-09 |
910
| [006](./006-concurrency-correctness.md) | fix | Swift concurrency correctness | CLOSED | 2026-08-08 |

0 commit comments

Comments
 (0)