From 0eeaca4fe98af47f68b66b1ef16850f2b81b0989 Mon Sep 17 00:00:00 2001 From: Robert Hill Date: Thu, 10 Sep 2026 22:17:56 -0400 Subject: [PATCH] fix(mounts): rmdir a stale directory before reaching for unmount The startup sweep unmounted every empty leftover it found, right after establishing it was not mounted, so each one walked the whole escalation. The removal count was also taken from pcall rather than from the result. Closes #31 --- lua/sshfs/lib/mount_point.lua | 11 ++++-- tests/cleanup_stale_spec.lua | 67 +++++++++++++++++++++++++++++++++++ 2 files changed, 75 insertions(+), 3 deletions(-) create mode 100644 tests/cleanup_stale_spec.lua diff --git a/lua/sshfs/lib/mount_point.lua b/lua/sshfs/lib/mount_point.lua index 6e9cd47..d8a8e39 100644 --- a/lua/sshfs/lib/mount_point.lua +++ b/lua/sshfs/lib/mount_point.lua @@ -285,9 +285,14 @@ function MountPoint.cleanup_stale() if vim.fn.isdirectory(file) == 1 then -- Only remove if directory is empty AND not actively mounted if Directory.is_empty(file) and not MountPoint.is_active(file) then - MountPoint.unmount(file) - local success = pcall(vim.fn.delete, file, "d") - if success then removed_count = removed_count + 1 end + -- Plain rmdir first; only a mount the table did not report needs unmounting. + local ok, result = pcall(vim.fn.delete, file, "d") + if not (ok and result == 0) then + MountPoint.unmount(file) + pcall(vim.fn.delete, file, "d") + end + -- unmount() removes the directory itself, so count what is gone, not what returned 0. + if vim.fn.isdirectory(file) == 0 then removed_count = removed_count + 1 end end end end diff --git a/tests/cleanup_stale_spec.lua b/tests/cleanup_stale_spec.lua new file mode 100644 index 0000000..9d1865c --- /dev/null +++ b/tests/cleanup_stale_spec.lua @@ -0,0 +1,67 @@ +-- tests/cleanup_stale_spec.lua +-- Startup sweep of leftover mount directories +-- +-- setup() runs this, so it must not shell out through the unmount escalation +-- for a directory that is merely empty. + +--- Build a base dir holding one empty directory, and load MountPoint against it +--- @return table MountPoint, string base_dir, table unmount_attempts +local function sweep_with(opts) + stub.reload() + local base_dir = vim.fn.tempname() + vim.fn.mkdir(base_dir .. "/leftover", "p") + require("sshfs.config").setup({ mounts = { base_dir = base_dir } }) + + stub.executable({ fusermount = true, umount = true }) + + local attempts = {} + stub.set("system", function(cmd) + table.insert(attempts, table.concat(cmd, " ")) + return { + wait = function() + return { code = 0, stdout = "", stderr = "" } + end, + } + end) + -- An empty mount table means nothing is active. + stub.set("fn.system", function() + return "" + end) + + if opts and opts.delete_fails then + local real_delete = vim.fn.delete + local calls = 0 + stub.set("fn.delete", function(path, flags) + calls = calls + 1 + if calls == 1 then return -1 end + return real_delete(path, flags) + end) + end + + return require("sshfs.lib.mount_point"), base_dir, attempts +end + +describe("MountPoint.cleanup_stale", function() + it("removes an empty leftover without spawning an unmount", function() + local MountPoint, base_dir, attempts = sweep_with() + + local removed = MountPoint.cleanup_stale() + stub.restore_all() + + expect.eq(removed, 1) + expect.eq(#attempts, 0, "an empty directory was never mounted") + expect.eq(vim.fn.isdirectory(base_dir .. "/leftover"), 0) + vim.fn.delete(base_dir, "rf") + end) + + it("falls back to unmounting when the directory will not go away", function() + local MountPoint, base_dir, attempts = sweep_with({ delete_fails = true }) + + local removed = MountPoint.cleanup_stale() + stub.restore_all() + + expect.eq(removed, 1, "the retry after unmounting must count") + expect.truthy(#attempts > 0, "a directory that resists rmdir may still be mounted") + vim.fn.delete(base_dir, "rf") + end) +end)