129 lines
3.3 KiB
Go
129 lines
3.3 KiB
Go
|
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
|
|||
|
}
|