Skip to content
This repository was archived by the owner on Jan 30, 2025. It is now read-only.

Commit 9047580

Browse files
authored
Merge pull request #617 from grafana/fix/614-abort-non-fatal-error
Wait for browser process to exit before returning first error from stderr
2 parents 0b8546d + 6c3449f commit 9047580

File tree

2 files changed

+249
-32
lines changed

2 files changed

+249
-32
lines changed

common/browser_process.go

Lines changed: 62 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -179,41 +179,71 @@ func execute(
179179
return command{cmd, done, stdout, stderr}, nil
180180
}
181181

182-
// parseDevToolsURL grabs the websocket address from chrome's output and returns it.
183-
func parseDevToolsURL(ctx context.Context, cmd command) (wsURL string, _ error) {
184-
type result struct {
185-
devToolsURL string
186-
err error
182+
// parseDevToolsURL grabs the WebSocket address from Chrome's output and returns
183+
// it. If the process ends abruptly, it will return the first error from stderr.
184+
func parseDevToolsURL(ctx context.Context, cmd command) (_ string, err error) {
185+
parser := &devToolsURLParser{
186+
sc: bufio.NewScanner(cmd.stderr),
187187
}
188-
c := make(chan result, 1)
188+
done := make(chan struct{})
189189
go func() {
190-
const urlPrefix = "DevTools listening on "
191-
scanner := bufio.NewScanner(cmd.stderr)
192-
193-
for scanner.Scan() {
194-
line := scanner.Text()
195-
if strings.HasPrefix(line, urlPrefix) {
196-
c <- result{
197-
strings.TrimPrefix(strings.TrimSpace(line), urlPrefix),
198-
nil,
199-
}
200-
return
201-
}
202-
if strings.Contains(line, ":ERROR:") {
203-
if i := strings.Index(line, "] "); i > 0 {
204-
c <- result{"", errors.New(line[i+2:])}
205-
return
206-
}
207-
}
208-
}
209-
if err := scanner.Err(); err != nil {
210-
c <- result{"", err}
190+
for parser.scan() {
211191
}
192+
close(done)
212193
}()
213-
select {
214-
case r := <-c:
215-
return r.devToolsURL, r.err
216-
case <-ctx.Done():
217-
return "", fmt.Errorf("%w", ctx.Err())
194+
for err == nil {
195+
select {
196+
case <-done:
197+
err = parser.err()
198+
case <-ctx.Done():
199+
err = ctx.Err()
200+
case <-cmd.done:
201+
err = errors.New("browser process ended unexpectedly")
202+
}
203+
}
204+
if parser.url != "" {
205+
err = nil
206+
}
207+
208+
return parser.url, err
209+
}
210+
211+
type devToolsURLParser struct {
212+
sc *bufio.Scanner
213+
214+
errs []error
215+
url string
216+
}
217+
218+
func (p *devToolsURLParser) scan() bool {
219+
if !p.sc.Scan() {
220+
return false
221+
}
222+
223+
const urlPrefix = "DevTools listening on "
224+
225+
line := p.sc.Text()
226+
if strings.HasPrefix(line, urlPrefix) {
227+
p.url = strings.TrimPrefix(strings.TrimSpace(line), urlPrefix)
228+
}
229+
if strings.Contains(line, ":ERROR:") {
230+
if i := strings.Index(line, "] "); i > 0 {
231+
p.errs = append(p.errs, errors.New(line[i+2:]))
232+
}
233+
}
234+
235+
return p.url == ""
236+
}
237+
238+
func (p *devToolsURLParser) err() error {
239+
if p.url != "" {
240+
return io.EOF
241+
}
242+
if len(p.errs) > 0 {
243+
return p.errs[0]
244+
}
245+
if err := p.sc.Err(); err != nil {
246+
return fmt.Errorf("%w", err)
218247
}
248+
return nil
219249
}

common/browser_process_test.go

Lines changed: 187 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,187 @@
1+
package common
2+
3+
import (
4+
"context"
5+
"io"
6+
"testing"
7+
"time"
8+
9+
"github.com/stretchr/testify/assert"
10+
"github.com/stretchr/testify/require"
11+
)
12+
13+
type mockCommand struct {
14+
*command
15+
cancelFn context.CancelFunc
16+
}
17+
18+
type mockReader struct {
19+
lines []string
20+
hook func()
21+
err error
22+
}
23+
24+
func (r *mockReader) Read(p []byte) (n int, err error) {
25+
if r.hook != nil {
26+
// Allow some time for the read to be processed
27+
time.AfterFunc(100*time.Millisecond, r.hook)
28+
r.hook = nil // Ensure the hook only runs once
29+
}
30+
if len(r.lines) == 0 {
31+
return 0, io.EOF
32+
}
33+
n = copy(p, []byte(r.lines[0]+"\n"))
34+
r.lines = r.lines[1:]
35+
36+
return n, r.err
37+
}
38+
39+
func TestParseDevToolsURL(t *testing.T) {
40+
t.Parallel()
41+
42+
testCases := []struct {
43+
name string
44+
stderr []string
45+
readErr error
46+
readHook func(c *mockCommand)
47+
assert func(t *testing.T, wsURL string, err error)
48+
}{
49+
{
50+
name: "ok/no_error",
51+
stderr: []string{
52+
`DevTools listening on ws://127.0.0.1:41315/devtools/browser/d1d3f8eb-b362-4f12-9370-bd25778d0da7`,
53+
},
54+
assert: func(t *testing.T, wsURL string, err error) {
55+
t.Helper()
56+
require.NoError(t, err)
57+
assert.Equal(t, "ws://127.0.0.1:41315/devtools/browser/d1d3f8eb-b362-4f12-9370-bd25778d0da7", wsURL)
58+
},
59+
},
60+
{
61+
name: "ok/non-fatal_error",
62+
stderr: []string{
63+
`[23400:23418:1028/115455.877614:ERROR:bus.cc(399)] Failed to ` +
64+
`connect to the bus: Could not parse server address: ` +
65+
`Unknown address type (examples of valid types are "tcp" ` +
66+
`and on UNIX "unix")`,
67+
"",
68+
`DevTools listening on ws://127.0.0.1:41315/devtools/browser/d1d3f8eb-b362-4f12-9370-bd25778d0da7`,
69+
},
70+
assert: func(t *testing.T, wsURL string, err error) {
71+
t.Helper()
72+
require.NoError(t, err)
73+
assert.Equal(t, "ws://127.0.0.1:41315/devtools/browser/d1d3f8eb-b362-4f12-9370-bd25778d0da7", wsURL)
74+
},
75+
},
76+
{
77+
name: "err/fatal-eof",
78+
stderr: []string{
79+
`[6497:6497:1013/103521.932979:ERROR:ozone_platform_x11` +
80+
`.cc(247)] Missing X server or $DISPLAY` + "\n",
81+
},
82+
readErr: io.ErrUnexpectedEOF,
83+
assert: func(t *testing.T, wsURL string, err error) {
84+
t.Helper()
85+
require.Empty(t, wsURL)
86+
assert.EqualError(t, err, "Missing X server or $DISPLAY")
87+
},
88+
},
89+
{
90+
name: "err/fatal-eof-no_stderr",
91+
stderr: []string{""},
92+
readErr: io.ErrUnexpectedEOF,
93+
assert: func(t *testing.T, wsURL string, err error) {
94+
t.Helper()
95+
require.Empty(t, wsURL)
96+
assert.EqualError(t, err, "unexpected EOF")
97+
},
98+
},
99+
{
100+
// Ensure any error found on stderr is returned first.
101+
name: "err/fatal-premature_cmd_done-stderr",
102+
stderr: []string{
103+
`[6497:6497:1013/103521.932979:ERROR:ozone_platform_x11` +
104+
`.cc(247)] Missing X server or $DISPLAY` + "\n",
105+
},
106+
readHook: func(c *mockCommand) { close(c.done) },
107+
assert: func(t *testing.T, wsURL string, err error) {
108+
t.Helper()
109+
require.Empty(t, wsURL)
110+
assert.EqualError(t, err, "Missing X server or $DISPLAY")
111+
},
112+
},
113+
{
114+
// If there's no error on stderr, return a generic error.
115+
name: "err/fatal-premature_cmd_done-no_stderr",
116+
stderr: []string{""},
117+
readHook: func(c *mockCommand) { close(c.done) },
118+
assert: func(t *testing.T, wsURL string, err error) {
119+
t.Helper()
120+
require.Empty(t, wsURL)
121+
assert.EqualError(t, err, "browser process ended unexpectedly")
122+
},
123+
},
124+
{
125+
name: "err/fatal-premature_ctx_cancel-stderr",
126+
stderr: []string{
127+
`[6497:6497:1013/103521.932979:ERROR:ozone_platform_x11` +
128+
`.cc(247)] Missing X server or $DISPLAY` + "\n",
129+
},
130+
readHook: func(c *mockCommand) { c.cancelFn() },
131+
assert: func(t *testing.T, wsURL string, err error) {
132+
t.Helper()
133+
require.Empty(t, wsURL)
134+
assert.EqualError(t, err, "Missing X server or $DISPLAY")
135+
},
136+
},
137+
{
138+
name: "err/fatal-premature_ctx_cancel-no_stderr",
139+
stderr: []string{""},
140+
readHook: func(c *mockCommand) { c.cancelFn() },
141+
assert: func(t *testing.T, wsURL string, err error) {
142+
t.Helper()
143+
require.Empty(t, wsURL)
144+
assert.EqualError(t, err, "context canceled")
145+
},
146+
},
147+
}
148+
149+
for _, tc := range testCases {
150+
tc := tc
151+
t.Run(tc.name, func(t *testing.T) {
152+
t.Parallel()
153+
154+
ctx, cancel := context.WithCancel(context.Background())
155+
t.Cleanup(cancel)
156+
157+
var cmd *mockCommand
158+
mr := mockReader{lines: tc.stderr, err: tc.readErr}
159+
if tc.readHook != nil {
160+
mr.hook = func() { tc.readHook(cmd) }
161+
}
162+
cmd = &mockCommand{&command{done: make(chan struct{}), stderr: &mr}, cancel}
163+
164+
timeout := time.Second
165+
timer := time.NewTimer(timeout)
166+
t.Cleanup(func() { _ = timer.Stop() })
167+
168+
var (
169+
done = make(chan struct{})
170+
wsURL string
171+
err error
172+
)
173+
174+
go func() {
175+
wsURL, err = parseDevToolsURL(ctx, *cmd.command)
176+
close(done)
177+
}()
178+
179+
select {
180+
case <-done:
181+
tc.assert(t, wsURL, err)
182+
case <-timer.C:
183+
t.Errorf("test timed out after %s", timeout)
184+
}
185+
})
186+
}
187+
}

0 commit comments

Comments
 (0)