-
Notifications
You must be signed in to change notification settings - Fork 16
/
common_test.go
128 lines (119 loc) · 3.31 KB
/
common_test.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
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
}