Merge pull request 'mounts: list reads the root mount's unit (issue 374)' (#194) from fix/374-mounts-list-reads-the-root-mounts-unit into main

This commit was merged in pull request #194.
This commit is contained in:
2026-10-11 00:13:58 +00:00
2 changed files with 73 additions and 2 deletions
+4 -1
View File
@@ -304,7 +304,10 @@ func (m *Machine) List(onlyNetwork bool, match string) MountsReport {
r.Notes = append(r.Notes, fmt.Sprintf("the service manager's units were not read: %v", err))
}
if names := ParseListUnits(listed); len(names) > 0 {
args := append([]string{"show", "-p", ShowProperties, "--no-pager"}, names...)
// The names go after "--": the root mount's unit is -.mount, and systemctl reads an argument
// that starts with a dash as an option and refuses the whole call, so every mount lost its
// unit state (novox/hq issue 374).
args := append([]string{"show", "-p", ShowProperties, "--no-pager", "--"}, names...)
shown, err := m.Run(ctx, "systemctl", args...)
if err != nil && shown == "" {
r.Notes = append(r.Notes, fmt.Sprintf("the units' properties were not read: %v", err))
+69 -1
View File
@@ -40,6 +40,11 @@ func (f *fake) machine() *Machine {
Root: f.root,
Run: func(_ context.Context, name string, args ...string) (string, error) {
line := strings.TrimSpace(name + " " + strings.Join(args, " "))
if name == "systemctl" {
if err := systemctlRefuses(args); err != nil {
return "", err
}
}
if out, ok := f.answers[line]; ok {
return out, nil
}
@@ -49,6 +54,26 @@ func (f *fake) machine() *Machine {
}
}
// systemctlRefuses is what systemctl itself does with an argument that starts with a dash before
// "--": it reads it as an option, so the root mount's unit -.mount is refused. The message is the
// one systemctl gave on the desktop (novox/hq issue 374); a fake that took -.mount as a unit name
// agreed with the mistake.
func systemctlRefuses(args []string) error {
for i := 0; i < len(args); i++ {
a := args[i]
switch {
case a == "--":
return nil
case a == "-p":
i++ // its value is the property list
case strings.HasPrefix(a, "--"):
case strings.HasPrefix(a, "-") && len(a) > 1:
return fmt.Errorf("exit status 1: systemctl: invalid option -- '%c'\nHint: to specify units starting with a dash, use \"--\":\n systemctl [OPTIONS…] COMMAND -- %s …", a[1], a)
}
}
return nil
}
func TestRedactOptions(t *testing.T) {
cases := []struct{ in, want string }{
{"", ""},
@@ -156,7 +181,7 @@ func listFake(t *testing.T) *fake {
f.file("/proc/self/mountinfo", sampleMountinfo)
f.answers["systemctl list-units --type=mount,automount --all --no-legend --plain --no-pager"] = sampleListUnits
names := strings.Join(ParseListUnits(sampleListUnits), " ")
f.answers["systemctl show -p "+ShowProperties+" --no-pager "+names] = sampleShow
f.answers["systemctl show -p "+ShowProperties+" --no-pager -- "+names] = sampleShow
return f
}
@@ -190,6 +215,49 @@ func TestList(t *testing.T) {
}
}
// The desktop's own mount units, as its service manager listed them on 2026-10-11 (novox/hq issue
// 374): the root mount's unit is -.mount, and systemctl reads a name that starts with a dash as an
// option unless it comes after "--". Every mount then lost its unit state, not only the root's.
const desktopListUnits = `-.mount loaded active mounted Root Mount
boot.mount loaded active mounted /boot
home.mount loaded active mounted /home
mnt-hdd_2tb.mount loaded active mounted /mnt/hdd_2tb
mnt-recalbox.automount loaded active waiting recalbox, mounted by the mesh at first use
mnt-recalbox.mount loaded inactive dead recalbox (//192.0.2.176/Share), mounted by the mesh
storage-media.automount loaded active waiting media, mounted by the mesh at first use
storage-media.mount loaded inactive dead media (192.0.2.10:/storage/media), mounted by the mesh
`
func TestListReadsTheRootMountsUnit(t *testing.T) {
f := newFake(t)
f.file("/etc/fstab", "UUID=aaaa / ext4 rw,relatime 0 1\n")
f.file("/proc/self/mountinfo", "22 1 259:3 / / rw,relatime shared:1 - ext4 /dev/nvme0n1p3 rw\n")
f.answers["systemctl list-units --type=mount,automount --all --no-legend --plain --no-pager"] = desktopListUnits
names := strings.Join(ParseListUnits(desktopListUnits), " ")
if !strings.HasPrefix(names, "-.mount ") {
t.Fatalf("the root mount's unit was not listed: %q", names)
}
f.answers["systemctl show -p "+ShowProperties+" --no-pager -- "+names] =
"Id=-.mount\nWhere=/\nWhat=/dev/nvme0n1p3\nType=ext4\nOptions=rw\nResult=success\nActiveState=active\nSubState=mounted\n" +
"FragmentPath=/run/systemd/generator/-.mount\nSourcePath=/etc/fstab\nUnitFileState=generated\n\n" +
"Id=storage-media.automount\nWhere=/storage/media\nResult=success\nActiveState=active\nSubState=waiting\n" +
"FragmentPath=/etc/systemd/system/storage-media.automount\nUnitFileState=enabled\n"
r := f.machine().List(false, "")
if len(r.Notes) != 0 {
t.Fatalf("the units' properties must be read, the root mount's included: %q", r.Notes)
}
got := map[string]Mount{}
for _, mt := range r.Mounts {
got[mt.Mountpoint] = mt
}
if root := got["/"]; root.MountUnit == nil || root.MountUnit.Unit != "-.mount" {
t.Errorf("/ must carry its unit -.mount: %+v", root.MountUnit)
}
if media := got["/storage/media"]; media.Automount == nil {
t.Errorf("/storage/media must carry its automount: %+v", media)
}
}
func TestListNarrowed(t *testing.T) {
m := listFake(t).machine()
if r := m.List(true, ""); len(r.Mounts) != 3 {