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, that’s no big // deal. We’ll 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 Xvfb’s 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 }