From 520763e98dc36fc83c65b4ad714925298e1c436c Mon Sep 17 00:00:00 2001 From: jochen Date: Sun, 11 Oct 2026 02:17:47 +0200 Subject: [PATCH] mounts: list says an armed automount waits, not mounted, as the look already judges it (issue 437) The autofs line an armed automount leaves in mountinfo made List say "mounted" while nothing was mounted. One judgement, Mounted.IsAutomount, now serves List, the look and the checks before a mount. --- modules/mounts/cmd/mounts/apply.go | 2 +- modules/mounts/cmd/mounts/data.go | 4 ++-- modules/mounts/cmd/mounts/mounts.go | 10 +++++++- modules/mounts/cmd/mounts/mounts_test.go | 29 ++++++++++++++++++++++++ 4 files changed, 41 insertions(+), 4 deletions(-) diff --git a/modules/mounts/cmd/mounts/apply.go b/modules/mounts/cmd/mounts/apply.go index 97cb80da..678efadc 100644 --- a/modules/mounts/cmd/mounts/apply.go +++ b/modules/mounts/cmd/mounts/apply.go @@ -593,7 +593,7 @@ func (a *Applier) judge(p Pair, states map[string]map[string]string, mounted map } mnt, auto := states[p.MountUnit], states[p.AutoUnit] cur, isMounted := mounted[p.Mountpoint] - isMounted = isMounted && cur.Type != "autofs" + isMounted = isMounted && !cur.IsAutomount() switch { case !there && p.Occasional: return StateAbsent, "not present: " + tried.Error() diff --git a/modules/mounts/cmd/mounts/data.go b/modules/mounts/cmd/mounts/data.go index 4550ed81..803a1a84 100644 --- a/modules/mounts/cmd/mounts/data.go +++ b/modules/mounts/cmd/mounts/data.go @@ -158,7 +158,7 @@ func kindOf(what, fstype string) string { // there, or nothing when it is, or nothing is mounted there but an automount. func (m *Machine) notOwnSource(kind, what, point string, mounted map[string]Mounted) error { mt, there := mounted[point] - if !there || mt.Type == "autofs" { + if !there || mt.IsAutomount() { return nil } if kind != "" && m.sourceIs(kind, what, mt) { @@ -172,7 +172,7 @@ func (m *Machine) notOwnSource(kind, what, point string, mounted map[string]Moun func (m *Machine) mountpointFree(p Pair, own map[string]bool, mounted map[string]Mounted) error { ours := own[p.MountUnit] && own[p.AutoUnit] if mt, there := mounted[p.Mountpoint]; there { - if mt.Type == "autofs" { + if mt.IsAutomount() { if ours { return nil } diff --git a/modules/mounts/cmd/mounts/mounts.go b/modules/mounts/cmd/mounts/mounts.go index 46569135..94bbd1ab 100644 --- a/modules/mounts/cmd/mounts/mounts.go +++ b/modules/mounts/cmd/mounts/mounts.go @@ -135,6 +135,14 @@ type Mounted struct { Options string `json:"options,omitempty"` } +// IsAutomount says whether this line is only the autofs mount an armed automount leaves at its point. +// Nothing is mounted there yet: the share is mounted over it at first use, and that line then wins in +// ParseMountinfo. Every judgement of "mounted" asks this one question, so List, the look and the checks +// before a mount cannot disagree (novox/hq issue 437). +func (mt Mounted) IsAutomount() bool { + return mt.Type == "autofs" +} + // ParseMountinfo reads /proc/self/mountinfo: per line the mount point (field 5), and after the " - " // separator the type, the source and the superblock options. The last mount at a point wins, as it // is the one a path resolves to. @@ -400,7 +408,7 @@ func (mt *Mount) say() string { if mt.MountUnit.Result != "" && mt.MountUnit.Result != "success" { state += ", " + mt.MountUnit.Result } - case mt.MountedNow != nil: + case mt.MountedNow != nil && !mt.MountedNow.IsAutomount(): state = "mounted" case mt.Automount != nil && mt.Automount.Active == "active": state = "not mounted, waits for its first use (automount " + mt.Automount.Sub + ")" diff --git a/modules/mounts/cmd/mounts/mounts_test.go b/modules/mounts/cmd/mounts/mounts_test.go index 52f9858e..538472d4 100644 --- a/modules/mounts/cmd/mounts/mounts_test.go +++ b/modules/mounts/cmd/mounts/mounts_test.go @@ -258,6 +258,35 @@ func TestListReadsTheRootMountsUnit(t *testing.T) { } } +// The desktop's recalbox share as it stood on 2026-10-11 (novox/hq issue 437): its automount armed +// (active, waiting), its mount unit inactive, and in mountinfo only the autofs line the armed automount +// leaves at the point, with the superblock options the desktop showed. Nothing is mounted there, so it +// must be said as waiting for its first use, never as mounted. /mnt/backup in TestList keeps the other +// half: an nfs4 line over the autofs line is mounted. +func TestListSaysAnArmedAutomountIsNotMounted(t *testing.T) { + f := newFake(t) + f.file("/etc/fstab", "//192.0.2.176/Share /mnt/recalbox cifs username=root,password=s3cret,iocharset=utf8,sec=ntlmssp 0 0\n") + f.file("/proc/self/mountinfo", "22 1 259:3 / / rw,relatime shared:1 - ext4 /dev/nvme0n1p3 rw\n"+ + "40 22 0:50 / /mnt/recalbox rw,relatime shared:30 - autofs systemd-1 rw,fd=116,pgrp=1,timeout=600,minproto=5,maxproto=5,direct,pipe_ino=68285996\n") + f.answers["systemctl list-units --type=mount,automount --all --no-legend --plain --no-pager"] = desktopListUnits + names := strings.Join(ParseListUnits(desktopListUnits), " ") + f.answers["systemctl show -p "+ShowProperties+" --no-pager -- "+names] = + "Id=mnt-recalbox.automount\nWhere=/mnt/recalbox\nResult=success\nActiveState=active\nSubState=waiting\n" + + "FragmentPath=/etc/systemd/system/mnt-recalbox.automount\nUnitFileState=enabled\n\n" + + "Id=mnt-recalbox.mount\nWhere=/mnt/recalbox\nWhat=//192.0.2.176/Share\nType=cifs\n" + + "Options=credentials=/var/lib/mounts/smb-recalbox.credentials,iocharset=utf8,_netdev,nofail,uid=1000,gid=1000\n" + + "Result=success\nActiveState=inactive\nSubState=dead\nFragmentPath=/etc/systemd/system/mnt-recalbox.mount\n" + + "TimeoutUSec=10s\nUnitFileState=enabled-runtime\n" + r := f.machine().List(false, "recalbox") + if len(r.Said) != 1 { + t.Fatalf("said %q, want one line for /mnt/recalbox", r.Said) + } + want := "/mnt/recalbox (cifs from //192.0.2.176/Share): not mounted, waits for its first use (automount waiting); from /etc/fstab line 1, no nofail" + if r.Said[0] != want { + t.Errorf("said %q,\nwant %q", r.Said[0], want) + } +} + func TestListNarrowed(t *testing.T) { m := listFake(t).machine() if r := m.List(true, ""); len(r.Mounts) != 3 {