From 99df6b999bad77c51dfdd13470cf57b1f38e8c24 Mon Sep 17 00:00:00 2001 From: jochen Date: Sun, 11 Oct 2026 02:02:18 +0200 Subject: [PATCH] mounts: pass unit names to systemctl show after --, so the root mount's -.mount is not read as an option systemctl refused the whole call on -.mount, and list answered every mount without its unit state (novox/hq issue 374). The test fake now refuses a dash-led argument as systemctl does, so it no longer agrees with the mistake. --- modules/mounts/cmd/mounts/mounts.go | 5 +- modules/mounts/cmd/mounts/mounts_test.go | 70 +++++++++++++++++++++++- 2 files changed, 73 insertions(+), 2 deletions(-) diff --git a/modules/mounts/cmd/mounts/mounts.go b/modules/mounts/cmd/mounts/mounts.go index f96992aa..46569135 100644 --- a/modules/mounts/cmd/mounts/mounts.go +++ b/modules/mounts/cmd/mounts/mounts.go @@ -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)) diff --git a/modules/mounts/cmd/mounts/mounts_test.go b/modules/mounts/cmd/mounts/mounts_test.go index d342029d..52f9858e 100644 --- a/modules/mounts/cmd/mounts/mounts_test.go +++ b/modules/mounts/cmd/mounts/mounts_test.go @@ -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 {