Merge pull request 'The clipboard service waits for an older daemon's lock, and the history sees the daemon (hq issues 860, 862)' (#323) from fix/860-862-the-clipboard-takes-over-quietly into main
This commit was merged in pull request #323.
This commit is contained in:
@@ -225,3 +225,22 @@ func TestWithoutAStoreTheChangesSayWhy(t *testing.T) {
|
||||
t.Fatalf("an empty history: %+v, %v", h, err)
|
||||
}
|
||||
}
|
||||
|
||||
// clipmenud is a bash script: its process is named "bash" and only its command line names clipmenud
|
||||
// (issue 860: collecting was false while the daemon ran).
|
||||
func TestCollectingSeesClipmenudRunAsABashScript(t *testing.T) {
|
||||
store(t, map[string]string{"first": "first"}, "first")
|
||||
fakeProcess(t, nobody, "bash")
|
||||
cmdline := filepath.Join(procRoot, strconv.Itoa(nobody), "cmdline")
|
||||
if err := os.WriteFile(cmdline, []byte("bash\x00/usr/bin/clipmenud\x00"), 0o644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if h, err := History(10); err != nil || !h.Collecting {
|
||||
t.Fatalf("clipmenud runs as a bash script: %+v %v", h, err)
|
||||
}
|
||||
// another bash script is not the daemon
|
||||
os.WriteFile(cmdline, []byte("bash\x00/usr/bin/other\x00"), 0o644)
|
||||
if h, _ := History(10); h.Collecting {
|
||||
t.Fatalf("a bash script that is not clipmenud counted: %+v", h)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,6 +7,8 @@ import (
|
||||
"os/exec"
|
||||
"reflect"
|
||||
"strings"
|
||||
"sync"
|
||||
"syscall"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
@@ -145,3 +147,78 @@ func TestTheLauncherUsesTheManagersDisplayAndWaitsOtherwise(t *testing.T) {
|
||||
t.Fatalf("the display that arrived while waiting: %q %v", out, ok)
|
||||
}
|
||||
}
|
||||
|
||||
// Run for real: while another clipmenud of the account holds the session lock the launcher waits without
|
||||
// a word, and starts clipmenud once the lock is free (issue 862).
|
||||
func TestTheLauncherWaitsQuietlyWhileAnotherClipmenudHoldsTheLock(t *testing.T) {
|
||||
if _, err := exec.LookPath("flock"); err != nil {
|
||||
t.Skip("flock is missing here")
|
||||
}
|
||||
m := readManifest(t)
|
||||
script, _ := m.resource(t, "launcher")["content"].(string)
|
||||
dir := t.TempDir()
|
||||
body := strings.Replace(script, "exec clipmenud", `echo "started"`, 1)
|
||||
file := dir + "/launcher"
|
||||
if err := os.WriteFile(file, []byte(body), 0o755); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
store := dir + "/cm/clipmenu.6.op"
|
||||
os.MkdirAll(store, 0o700)
|
||||
lock := store + "/session_lock"
|
||||
// the older clipmenud: it holds the lock the way clipmenud does, until it is killed
|
||||
holder := exec.Command("flock", "-x", "-n", lock, "sleep", "60")
|
||||
holder.SysProcAttr = &syscall.SysProcAttr{Setpgid: true}
|
||||
if err := holder.Start(); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
// flock's child shares the lock, so the whole group goes
|
||||
release := func() { syscall.Kill(-holder.Process.Pid, syscall.SIGKILL); holder.Wait() }
|
||||
defer release()
|
||||
for i := 0; i < 50; i++ {
|
||||
if probe := exec.Command("flock", "-n", lock, "true"); probe.Run() != nil {
|
||||
break
|
||||
}
|
||||
time.Sleep(50 * time.Millisecond)
|
||||
}
|
||||
var out syncBuffer
|
||||
cmd := exec.Command("sh", file)
|
||||
cmd.Env = []string{"PATH=" + os.Getenv("PATH"), "HOME=" + dir, "DISPLAY=:7", "USER=op", "CM_DIR=" + dir + "/cm"}
|
||||
cmd.Stdout, cmd.Stderr = &out, &out
|
||||
if err := cmd.Start(); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
done := make(chan error, 1)
|
||||
go func() { done <- cmd.Wait() }()
|
||||
select {
|
||||
case <-done:
|
||||
t.Fatalf("the launcher went on while the lock was held: %q", out.String())
|
||||
case <-time.After(2500 * time.Millisecond):
|
||||
}
|
||||
if out.Len() != 0 {
|
||||
t.Fatalf("the launcher spoke while it waited: %q", out.String())
|
||||
}
|
||||
release()
|
||||
select {
|
||||
case <-done:
|
||||
case <-time.After(5 * time.Second):
|
||||
cmd.Process.Kill()
|
||||
t.Fatal("the launcher did not start clipmenud after the lock was freed")
|
||||
}
|
||||
if got := strings.TrimSpace(out.String()); got != "started" {
|
||||
t.Fatalf("after the lock was freed: %q", got)
|
||||
}
|
||||
}
|
||||
|
||||
type syncBuffer struct {
|
||||
mu sync.Mutex
|
||||
b strings.Builder
|
||||
}
|
||||
|
||||
func (s *syncBuffer) Write(p []byte) (int, error) {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
return s.b.Write(p)
|
||||
}
|
||||
|
||||
func (s *syncBuffer) Len() int { s.mu.Lock(); defer s.mu.Unlock(); return s.b.Len() }
|
||||
func (s *syncBuffer) String() string { s.mu.Lock(); defer s.mu.Unlock(); return s.b.String() }
|
||||
|
||||
@@ -403,7 +403,7 @@ func processesOf(comm string) []int {
|
||||
if owner, ok := ownerOf(dir); !ok || owner != uid {
|
||||
continue
|
||||
}
|
||||
if readTrimmed(filepath.Join(dir, "comm")) == comm {
|
||||
if readTrimmed(filepath.Join(dir, "comm")) == comm || runsScript(dir, comm) {
|
||||
pids = append(pids, pid)
|
||||
}
|
||||
}
|
||||
@@ -411,6 +411,24 @@ func processesOf(comm string) []int {
|
||||
return pids
|
||||
}
|
||||
|
||||
// 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 {
|
||||
var reached []int
|
||||
|
||||
@@ -75,7 +75,7 @@
|
||||
"type": "file",
|
||||
"path": "/usr/local/lib/mesh-clipmenu/clipmenud-session",
|
||||
"mode": "0755",
|
||||
"content": "#!/bin/sh\n# clipmenud as the operator's user unit starts it, written by the mesh (module clipmenu). Replaced at every push.\n#\n# The account's service manager starts before the X server does, and has a DISPLAY only once the session's\n# xinitrc has handed it over (module xorg). So this waits, quietly, until the display is known, and exits\n# only when the session ends with it: a quick exit would run the unit into its start limit. DISPLAY, when\n# the manager has it, wins. Without it, exactly one X socket names the display; none or several (a\n# second server beside the session) wait for the session's own to arrive.\n# A running process does not see what the session hands the manager later, so each pass asks the manager.\nuntil [ -n \"$DISPLAY\" ]; do\n\tfor pair in $(systemctl --user show-environment 2>/dev/null | grep -E '^(DISPLAY|XAUTHORITY)='); do\n\t\texport \"$pair\"\n\tdone\n\t[ -n \"$DISPLAY\" ] && break\n\tset -- /tmp/.X11-unix/X*\n\tif [ $# -eq 1 ] && [ -S \"$1\" ]; then\n\t\tDISPLAY=\":${1##*/X}\"\n\t\texport DISPLAY\n\telse\n\t\tsleep 1\n\tfi\ndone\nif [ -z \"$XAUTHORITY\" ] && [ -r \"$HOME/.Xauthority\" ]; then\n\tXAUTHORITY=\"$HOME/.Xauthority\"\n\texport XAUTHORITY\nfi\nPATH=\"/usr/local/lib/mesh-clipmenu:$PATH\"\nexport PATH\nexec clipmenud\n"
|
||||
"content": "#!/bin/sh\n# clipmenud as the operator's user unit starts it, written by the mesh (module clipmenu). Replaced at every push.\n#\n# The account's service manager starts before the X server does, and has a DISPLAY only once the session's\n# xinitrc has handed it over (module xorg). So this waits, quietly, until the display is known, and exits\n# only when the session ends with it: a quick exit would run the unit into its start limit. DISPLAY, when\n# the manager has it, wins. Without it, exactly one X socket names the display; none or several (a\n# second server beside the session) wait for the session's own to arrive.\n# A running process does not see what the session hands the manager later, so each pass asks the manager.\nuntil [ -n \"$DISPLAY\" ]; do\n\tfor pair in $(systemctl --user show-environment 2>/dev/null | grep -E '^(DISPLAY|XAUTHORITY)='); do\n\t\texport \"$pair\"\n\tdone\n\t[ -n \"$DISPLAY\" ] && break\n\tset -- /tmp/.X11-unix/X*\n\tif [ $# -eq 1 ] && [ -S \"$1\" ]; then\n\t\tDISPLAY=\":${1##*/X}\"\n\t\texport DISPLAY\n\telse\n\t\tsleep 1\n\tfi\ndone\nif [ -z \"$XAUTHORITY\" ] && [ -r \"$HOME/.Xauthority\" ]; then\n\tXAUTHORITY=\"$HOME/.Xauthority\"\n\texport XAUTHORITY\nfi\n# clipmenud allows one daemon per account: it takes an exclusive flock on its session_lock file and exits\n# with status 2 when that is held. After a delivery without a new login, the session's older clipmenud (from\n# its xinitrc) still holds it, and a start would run the unit round its restart loop. So this waits, quietly,\n# until the lock is free: it looks at the same file clipmenud locks (the directory is clipmenud's own rule)\n# and lets go at once. Exit status 1 is \"held\" for util-linux and BusyBox flock alike (no -E: BusyBox lacks it); any other outcome starts clipmenud, which says its own\n# error. Another daemon winning the gap between look and start costs one restart, not a loop.\nlock=\"${CM_DIR:-${XDG_RUNTIME_DIR-${TMPDIR-/tmp}}}/clipmenu.6.$USER/session_lock\"\nwhile [ -e \"$lock\" ]; do\n\tflock -n \"$lock\" true\n\t[ $? -eq 1 ] || break\n\tsleep 2\ndone\nPATH=\"/usr/local/lib/mesh-clipmenu:$PATH\"\nexport PATH\nexec clipmenud\n"
|
||||
},
|
||||
{
|
||||
"id": "daemon",
|
||||
|
||||
Reference in New Issue
Block a user