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
|
# 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`
|
## 2026-05-04 – Detect mounts via `/proc/self/mountinfo` instead of `stat`
|
||||||
- **Who**: Dom, ClaudeCode
|
- **Who**: Dom, ClaudeCode
|
||||||
- **Why**: `mountinfo.Mounted(path)` from `github.com/moby/sys/mountinfo` works
|
- **Why**: `mountinfo.Mounted(path)` from `github.com/moby/sys/mountinfo` works
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ install -Dm755 sshfsc /usr/local/bin/sshfsc
|
|||||||
# Dependencies
|
# Dependencies
|
||||||
|
|
||||||
- [SSHFS](https://wiki.archlinux.org/title/SSHFS)
|
- [SSHFS](https://wiki.archlinux.org/title/SSHFS)
|
||||||
|
- `findmnt` from `util-linux` (for `-l`)
|
||||||
- [Go](https://wiki.archlinux.org/title/Go) >= 1.25 (build-time)
|
- [Go](https://wiki.archlinux.org/title/Go) >= 1.25 (build-time)
|
||||||
|
|
||||||
# Usage
|
# Usage
|
||||||
@@ -39,7 +40,7 @@ sshfsc -l # list active mounts
|
|||||||
| `-e` | open mountpoint in your editor |
|
| `-e` | open mountpoint in your editor |
|
||||||
| `-v` | verbose: print resolved ssh_config fields (HostName, User, Port, IdentityFile) |
|
| `-v` | verbose: print resolved ssh_config fields (HostName, User, Port, IdentityFile) |
|
||||||
| `-r`, `--remote-dir <path>` | remote directory to mount (default: remote home) |
|
| `-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`) |
|
| `-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
|
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 {
|
func list_mounts(out io.Writer) error {
|
||||||
base, err := mount_base(false)
|
cmd := exec.Command("findmnt", "-t", "fuse.sshfs")
|
||||||
if err != nil {
|
cmd.Stdout = out
|
||||||
if errors.Is(err, fs.ErrNotExist) {
|
cmd.Stderr = os.Stderr
|
||||||
|
err := cmd.Run()
|
||||||
|
if err == nil {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
return err
|
// findmnt exits 1 when no matching mount exists — not an error here.
|
||||||
}
|
var ee *exec.ExitError
|
||||||
mounts, err := mountinfo.GetMounts(mountinfo.PrefixFilter(base))
|
if errors.As(err, &ee) && ee.ExitCode() == 1 {
|
||||||
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)
|
|
||||||
}
|
|
||||||
return nil
|
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 {
|
func run_fusermount(flag, mount string) error {
|
||||||
|
|||||||
@@ -4,7 +4,6 @@
|
|||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"strings"
|
"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) {
|
func TestUnmountRejectsBadAlias(t *testing.T) {
|
||||||
runtime := t.TempDir()
|
runtime := t.TempDir()
|
||||||
t.Setenv("XDG_RUNTIME_DIR", runtime)
|
t.Setenv("XDG_RUNTIME_DIR", runtime)
|
||||||
|
|||||||
Reference in New Issue
Block a user