Count clipmenud when it runs as a bash script (issue 860)
mesh/merge-gate head 21ebbffed469 merged into trunk 8c2d7e5f97f1 — pass: builds clipmenu → g14, shanks; no bus step; every machine composes with the change…
mesh/repo-check head 21ebbffed469 merged into trunk 8c2d7e5f97f1 — fail: its merge-check.sh failed: --- FAIL: TestTheLauncherWaitsQuietlyWhileAnotherClipme…
mesh/delivery superseded: a newer head of the same pull request

This commit is contained in:
2026-10-15 23:18:22 +02:00
parent cf304d8629
commit 21ebbffed4
2 changed files with 34 additions and 41 deletions
@@ -5,7 +5,6 @@ import (
"errors" "errors"
"os" "os"
"os/exec" "os/exec"
"os/user"
"path/filepath" "path/filepath"
"strconv" "strconv"
"strings" "strings"
@@ -227,26 +226,21 @@ func TestWithoutAStoreTheChangesSayWhy(t *testing.T) {
} }
} }
// The tools can run as another account than the operator's; the daemon that counts is the operator's // clipmenud is a bash script: its process is named "bash" and only its command line names clipmenud
// (issue 860). Here the operator is the test's own account and the tool runs as a different uid. // (issue 860: collecting was false while the daemon ran).
func TestCollectingIsTheOperatorsDaemonNotThatOfTheAccountTheToolRunsAs(t *testing.T) { func TestCollectingSeesClipmenudRunAsABashScript(t *testing.T) {
store(t, map[string]string{"first": "first"}, "first") store(t, map[string]string{"first": "first"}, "first")
fakeProcess(t, nobody, "clipmenud") // owned by the test's uid: the operator's fakeProcess(t, nobody, "bash")
u, err := user.Current() cmdline := filepath.Join(procRoot, strconv.Itoa(nobody), "cmdline")
if err != nil { if err := os.WriteFile(cmdline, []byte("bash\x00/usr/bin/clipmenud\x00"), 0o644); err != nil {
t.Skip(err) t.Fatal(err)
} }
t.Setenv("MESH_OPERATOR_ACCOUNT", u.Username) if h, err := History(10); err != nil || !h.Collecting {
old := toolUID t.Fatalf("clipmenud runs as a bash script: %+v %v", h, err)
toolUID = func() int { return os.Getuid() + 1 }
t.Cleanup(func() { toolUID = old })
h, err := History(10)
if err != nil || !h.Collecting {
t.Fatalf("the operator's clipmenud runs: %+v %v", h, err)
} }
// without the operator named, the tool's own uid stands, and that account has no daemon // another bash script is not the daemon
t.Setenv("MESH_OPERATOR_ACCOUNT", "") os.WriteFile(cmdline, []byte("bash\x00/usr/bin/other\x00"), 0o644)
if h, _ := History(10); h.Collecting { if h, _ := History(10); h.Collecting {
t.Fatalf("another account's daemon counted: %+v", h) t.Fatalf("a bash script that is not clipmenud counted: %+v", h)
} }
} }
+22 -23
View File
@@ -21,7 +21,6 @@ import (
"fmt" "fmt"
"os" "os"
"os/exec" "os/exec"
"os/user"
"path/filepath" "path/filepath"
"sort" "sort"
"strconv" "strconv"
@@ -387,31 +386,13 @@ func (c *capped) Write(p []byte) (int, error) {
return c.Buffer.Write(p) return c.Buffer.Write(p)
} }
// toolUID is the uid the tool runs as; a test replaces it to stand for a tool that runs as another // processesOf are the pids of this uid's processes whose command name is comm, oldest first.
// account than the operator's.
var toolUID = os.Getuid
// operatorUID is the uid of the operator's account, whose clipmenud and store these tools look at.
// The runtime serves the tools as an account that need not be the operator's and names the operator
// in MESH_OPERATOR_ACCOUNT; without it (or when it is unknown here) the tool's own uid stands.
func operatorUID() int {
if name := strings.TrimSpace(os.Getenv("MESH_OPERATOR_ACCOUNT")); name != "" {
if u, err := user.Lookup(name); err == nil {
if id, err := strconv.Atoi(u.Uid); err == nil {
return id
}
}
}
return toolUID()
}
// processesOf are the pids of the operator account's processes whose command name is comm, oldest first.
func processesOf(comm string) []int { func processesOf(comm string) []int {
entries, err := os.ReadDir(procRoot) entries, err := os.ReadDir(procRoot)
if err != nil { if err != nil {
return nil return nil
} }
uid := operatorUID() uid := os.Getuid()
var pids []int var pids []int
for _, e := range entries { for _, e := range entries {
pid, err := strconv.Atoi(e.Name()) pid, err := strconv.Atoi(e.Name())
@@ -422,7 +403,7 @@ func processesOf(comm string) []int {
if owner, ok := ownerOf(dir); !ok || owner != uid { if owner, ok := ownerOf(dir); !ok || owner != uid {
continue continue
} }
if readTrimmed(filepath.Join(dir, "comm")) == comm { if readTrimmed(filepath.Join(dir, "comm")) == comm || runsScript(dir, comm) {
pids = append(pids, pid) pids = append(pids, pid)
} }
} }
@@ -430,7 +411,25 @@ func processesOf(comm string) []int {
return pids return pids
} }
// signalAll sends sig to every process of the operator account named comm, and answers the pids it reached. // runsScript is whether the process is an interpreter running the script named name: clipmenud is a
// bash script, so its comm is "bash" and only its command line, "bash /usr/bin/clipmenud", names it.
func runsScript(procDir, name string) bool {
raw, err := os.ReadFile(filepath.Join(procDir, "cmdline"))
if err != nil {
return false
}
args := strings.Split(strings.TrimRight(string(raw), "\x00"), "\x00")
if len(args) < 2 {
return false
}
switch filepath.Base(args[0]) {
case "bash", "sh", "dash", "zsh", "env":
return filepath.Base(args[1]) == name
}
return false
}
// signalAll sends sig to every process of this uid named comm, and answers the pids it reached.
func signalAll(comm string, sig syscall.Signal) []int { func signalAll(comm string, sig syscall.Signal) []int {
var reached []int var reached []int
for _, pid := range processesOf(comm) { for _, pid := range processesOf(comm) {