go-i3/common_test.go

129 lines
3.3 KiB
Go
Raw Permalink Normal View History

2023-10-22 16:19:10 +02:00
package i3
import (
"context"
"fmt"
"io/ioutil"
"os"
"os/exec"
"os/signal"
"path/filepath"
"strconv"
"strings"
"sync"
"syscall"
)
func displayLikelyAvailable(display int) bool {
// The path to this lock is hard-coded to /tmp in the Xorg source code, at
// least in xorg-server-1.19.3. If the path ever changes, thats no big
// deal. Well fall through to starting Xvfb and having Xvfb fail, which is
// only a performance hit, no failure.
b, err := ioutil.ReadFile(fmt.Sprintf("/tmp/.X%d-lock", display))
if err != nil {
if os.IsNotExist(err) {
return true
}
// Maybe a starting process is just replacing the file? The display
// is likely not available.
return false
}
pid, err := strconv.Atoi(strings.TrimSpace(string(b)))
if err != nil {
// No pid inside the lock file, so Xvfb will remove the file.
return true
}
return !pidValid(pid)
}
func launchI3(ctx context.Context, DISPLAY, I3SOCK string) (cleanup func(), _ error) {
abs, err := filepath.Abs("testdata/i3.config")
if err != nil {
return nil, err
}
wm := exec.CommandContext(ctx, "i3", "-c", abs, "-d", "all", fmt.Sprintf("--shmlog-size=%d", 5*1024*1024))
wm.Env = []string{
"DISPLAY=" + DISPLAY,
"PATH=" + os.Getenv("PATH"),
}
if I3SOCK != "" {
wm.Env = append(wm.Env, "I3SOCK="+I3SOCK)
}
wm.Stderr = os.Stderr
if err := wm.Start(); err != nil {
return nil, err
}
return func() { wm.Process.Kill() }, nil
}
var signalMu sync.Mutex
func launchXvfb(ctx context.Context) (xvfb *exec.Cmd, DISPLAY string, _ error) {
// Only one goroutine can wait for Xvfb to start at any point in time, as
// signal handlers are global (per-process, not per-goroutine).
signalMu.Lock()
defer signalMu.Unlock()
var lastErr error
display := 0 // :0 is usually an active session
for attempt := 0; attempt < 100; attempt++ {
display++
if !displayLikelyAvailable(display) {
continue
}
// display likely available, try to start Xvfb
DISPLAY := fmt.Sprintf(":%d", display)
// Indicate we implement Xvfbs readiness notification mechanism.
//
// We ignore SIGUSR1 in a shell wrapper process as there is currently no
// way to ignore signals in a child process, other than ignoring it in
// the parent (using signal.Ignore), which is prone to race conditions
// for this particular use-case:
// https://github.com/golang/go/issues/20479#issuecomment-303791827
ch := make(chan os.Signal, 1)
signal.Notify(ch, syscall.SIGUSR1)
xvfb := exec.CommandContext(ctx,
"sh",
"-c",
"trap '' USR1 && exec Xvfb "+DISPLAY+" -screen 0 1280x800x24")
if attempt == 99 { // last attempt
xvfb.Stderr = os.Stderr
}
if lastErr = xvfb.Start(); lastErr != nil {
continue
}
// The buffer of 1 allows the Wait() goroutine to return.
status := make(chan error, 1)
go func() {
defer signal.Stop(ch)
for range ch {
status <- nil // success
return
}
}()
go func() {
defer func() {
signal.Stop(ch)
close(ch) // avoid leaking the other goroutine
}()
ps, err := xvfb.Process.Wait()
if err != nil {
status <- err
return
}
if ps.Exited() {
status <- fmt.Errorf("Xvfb exited: %v", ps)
return
}
status <- fmt.Errorf("BUG: Wait returned, but !ps.Exited()")
}()
if lastErr = <-status; lastErr == nil {
return xvfb, DISPLAY, nil // Xvfb ready
}
}
return nil, "", lastErr
}