refactor: delegate -l output to findmnt
Update PKGBUILD version / update-pkgver (push) Successful in 2s
Update PKGBUILD version / update-pkgver (push) Successful in 2s
The custom one-alias-per-line output was useless — no mountpoint, no source, no options. Reinventing a table format when findmnt from util-linux already produces a familiar fuse.sshfs view was the wrong call. -l now shells out to findmnt -t fuse.sshfs.
This commit is contained in:
@@ -1,5 +1,35 @@
|
||||
# Decisions
|
||||
|
||||
## 2026-05-04 – Delegate `-l` output to `findmnt -t fuse.sshfs`
|
||||
- **Who**: Dom, ClaudeCode
|
||||
- **Why**: The custom `-l` output ("just the alias, one per line") was
|
||||
uselessly minimal — no mountpoint, no source, no options. Reinventing a
|
||||
table format for a single command was the wrong call when `findmnt` from
|
||||
`util-linux` already produces a familiar, well-formatted view of fuse.sshfs
|
||||
mounts.
|
||||
- **Tradeoffs**:
|
||||
- External dependency on `findmnt` (part of `util-linux`, present on every
|
||||
Arch and most other Linux systems by default — no real adoption cost).
|
||||
- Output is mountpoint-first, not alias-first. The alias is only visible as
|
||||
the last path segment under the SOURCE column / TARGET basename.
|
||||
- Lists *all* fuse.sshfs mounts on the system, not just sshfsc-managed
|
||||
ones under `$XDG_RUNTIME_DIR/sshfs/`. In practice this is what the user
|
||||
wants ("show me all sshfs mounts"); a future `-r` (restrict) flag could
|
||||
narrow it if needed.
|
||||
- `findmnt` exits 1 when nothing matches its filter — we treat that as
|
||||
success-with-empty-output, not a failure.
|
||||
- **How**:
|
||||
- `list_mounts` rewritten as a thin wrapper around `exec.Command("findmnt",
|
||||
"-t", "fuse.sshfs")` with stdout piped to the caller's writer.
|
||||
- Exit-code 1 from `findmnt` is swallowed via `errors.As(err, *exec.ExitError)`
|
||||
+ `ExitCode() == 1`.
|
||||
- `errors.Is(err, exec.ErrNotFound)` produces a clear "install util-linux"
|
||||
message instead of a cryptic exec error.
|
||||
- Tests `TestListMountsMissingBase` and `TestListMountsFiltersUnmounted`
|
||||
removed: they covered the old in-Go enumeration logic that no longer
|
||||
exists. Mocking `findmnt` would be alibi-testing — the wrapper is two
|
||||
branches over `cmd.Run()`'s error.
|
||||
|
||||
## 2026-05-04 – Detect mounts via `/proc/self/mountinfo` instead of `stat`
|
||||
- **Who**: Dom, ClaudeCode
|
||||
- **Why**: `mountinfo.Mounted(path)` from `github.com/moby/sys/mountinfo` works
|
||||
|
||||
@@ -22,6 +22,7 @@ install -Dm755 sshfsc /usr/local/bin/sshfsc
|
||||
# Dependencies
|
||||
|
||||
- [SSHFS](https://wiki.archlinux.org/title/SSHFS)
|
||||
- `findmnt` from `util-linux` (for `-l`)
|
||||
- [Go](https://wiki.archlinux.org/title/Go) >= 1.25 (build-time)
|
||||
|
||||
# Usage
|
||||
@@ -39,7 +40,7 @@ sshfsc -l # list active mounts
|
||||
| `-e` | open mountpoint in your editor |
|
||||
| `-v` | verbose: print resolved ssh_config fields (HostName, User, Port, IdentityFile) |
|
||||
| `-r`, `--remote-dir <path>` | remote directory to mount (default: remote home) |
|
||||
| `-l` | list active mounts under `$XDG_RUNTIME_DIR/sshfs/` and exit |
|
||||
| `-l` | list active fuse.sshfs mounts via `findmnt` and exit |
|
||||
| `-u` | unmount the given `<Host>` and exit (mutually exclusive with `-l`) |
|
||||
|
||||
By default only the resolved mount path is printed. Use `-v` for the full
|
||||
|
||||
@@ -281,26 +281,22 @@ func is_mounted_at(path string) (bool, error) {
|
||||
}
|
||||
|
||||
func list_mounts(out io.Writer) error {
|
||||
base, err := mount_base(false)
|
||||
if err != nil {
|
||||
if errors.Is(err, fs.ErrNotExist) {
|
||||
cmd := exec.Command("findmnt", "-t", "fuse.sshfs")
|
||||
cmd.Stdout = out
|
||||
cmd.Stderr = os.Stderr
|
||||
err := cmd.Run()
|
||||
if err == nil {
|
||||
return nil
|
||||
}
|
||||
return err
|
||||
}
|
||||
mounts, err := mountinfo.GetMounts(mountinfo.PrefixFilter(base))
|
||||
if err != nil {
|
||||
return fmt.Errorf("read mountinfo: %w", err)
|
||||
}
|
||||
prefix := base + string(os.PathSeparator)
|
||||
for _, m := range mounts {
|
||||
rel := strings.TrimPrefix(m.Mountpoint, prefix)
|
||||
if rel == "" || strings.Contains(rel, string(os.PathSeparator)) {
|
||||
continue
|
||||
}
|
||||
fmt.Fprintln(out, rel)
|
||||
}
|
||||
// findmnt exits 1 when no matching mount exists — not an error here.
|
||||
var ee *exec.ExitError
|
||||
if errors.As(err, &ee) && ee.ExitCode() == 1 {
|
||||
return nil
|
||||
}
|
||||
if errors.Is(err, exec.ErrNotFound) {
|
||||
return fmt.Errorf("findmnt not found (install util-linux)")
|
||||
}
|
||||
return fmt.Errorf("findmnt: %w", err)
|
||||
}
|
||||
|
||||
func run_fusermount(flag, mount string) error {
|
||||
|
||||
@@ -4,7 +4,6 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
@@ -140,36 +139,6 @@ func TestVerifyMountDirRejectsSymlink(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestListMountsMissingBase(t *testing.T) {
|
||||
runtime := t.TempDir()
|
||||
t.Setenv("XDG_RUNTIME_DIR", runtime)
|
||||
var buf bytes.Buffer
|
||||
if err := list_mounts(&buf); err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
if buf.Len() != 0 {
|
||||
t.Fatalf("want empty output, got %q", buf.String())
|
||||
}
|
||||
}
|
||||
|
||||
func TestListMountsFiltersUnmounted(t *testing.T) {
|
||||
runtime := t.TempDir()
|
||||
t.Setenv("XDG_RUNTIME_DIR", runtime)
|
||||
base := filepath.Join(runtime, "sshfs")
|
||||
for _, name := range []string{"stale1", "stale2"} {
|
||||
if err := os.MkdirAll(filepath.Join(base, name), 0700); err != nil {
|
||||
t.Fatalf("setup stale dir: %v", err)
|
||||
}
|
||||
}
|
||||
var buf bytes.Buffer
|
||||
if err := list_mounts(&buf); err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
if buf.Len() != 0 {
|
||||
t.Fatalf("want stale dirs filtered, got %q", buf.String())
|
||||
}
|
||||
}
|
||||
|
||||
func TestUnmountRejectsBadAlias(t *testing.T) {
|
||||
runtime := t.TempDir()
|
||||
t.Setenv("XDG_RUNTIME_DIR", runtime)
|
||||
|
||||
Reference in New Issue
Block a user