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

Commit ffe719e

Browse files
authored
Merge pull request #188 from grafana/fix/187-waitfornavigation-nil-pointer
Fix WaitForFrameNavigation nil pointer bug
2 parents 54cd293 + 2951829 commit ffe719e

8 files changed

+298
-93
lines changed

common/browser.go

Lines changed: 96 additions & 66 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,6 @@ import (
2626
"strings"
2727
"sync"
2828
"sync/atomic"
29-
"time"
3029

3130
"github.com/chromedp/cdproto"
3231
cdpbrowser "github.com/chromedp/cdproto/browser"
@@ -59,10 +58,15 @@ type Browser struct {
5958
browserProc *BrowserProcess
6059
launchOpts *LaunchOptions
6160

62-
// Connection to browser to talk CDP protocol
63-
conn *Connection
64-
connMu sync.RWMutex
65-
connected bool
61+
// Connection to browser to talk CDP protocol.
62+
// A *Connection is saved to this field, see: connect().
63+
conn cdp.Executor
64+
connEvents EventEmitter
65+
connSessions interface {
66+
getSession(target.SessionID) *Session
67+
}
68+
connectedMu sync.RWMutex
69+
connected bool
6670

6771
contextsMu sync.RWMutex
6872
contexts map[cdp.BrowserContextID]*BrowserContext
@@ -82,9 +86,18 @@ type Browser struct {
8286
logger *Logger
8387
}
8488

85-
// NewBrowser creates a new browser
86-
func NewBrowser(ctx context.Context, cancelFn context.CancelFunc, browserProc *BrowserProcess, launchOpts *LaunchOptions, logger *Logger) (*Browser, error) {
87-
b := Browser{
89+
// NewBrowser creates a new browser, connects to it, then returns it.
90+
func NewBrowser(ctx context.Context, cancel context.CancelFunc, browserProc *BrowserProcess, launchOpts *LaunchOptions, logger *Logger) (*Browser, error) {
91+
b := newBrowser(ctx, cancel, browserProc, launchOpts, logger)
92+
if err := b.connect(); err != nil {
93+
return nil, err
94+
}
95+
return b, nil
96+
}
97+
98+
// newBrowser returns a ready to use Browser without connecting to an actual browser.
99+
func newBrowser(ctx context.Context, cancelFn context.CancelFunc, browserProc *BrowserProcess, launchOpts *LaunchOptions, logger *Logger) *Browser {
100+
return &Browser{
88101
BaseEventEmitter: NewBaseEventEmitter(ctx),
89102
ctx: ctx,
90103
cancelFn: cancelFn,
@@ -96,26 +109,25 @@ func NewBrowser(ctx context.Context, cancelFn context.CancelFunc, browserProc *B
96109
sessionIDtoTargetID: make(map[target.SessionID]target.ID),
97110
logger: logger,
98111
}
99-
if err := b.connect(); err != nil {
100-
return nil, err
101-
}
102-
return &b, nil
103112
}
104113

105114
func (b *Browser) connect() error {
106115
b.logger.Debugf("Browser:connect", "wsURL:%q", b.browserProc.WsURL())
107-
var err error
108-
b.conn, err = NewConnection(b.ctx, b.browserProc.WsURL(), b.logger)
116+
conn, err := NewConnection(b.ctx, b.browserProc.WsURL(), b.logger)
109117
if err != nil {
110118
return fmt.Errorf("unable to connect to browser WS URL: %w", err)
111119
}
112120

113-
b.connMu.Lock()
121+
b.conn = conn
122+
b.connEvents = conn
123+
b.connSessions = conn
124+
125+
b.connectedMu.Lock()
114126
b.connected = true
115-
b.connMu.Unlock()
127+
b.connectedMu.Unlock()
116128

117129
// We don't need to lock this because `connect()` is called only in NewBrowser
118-
b.defaultContext = NewBrowserContext(b.ctx, b.conn, b, "", NewBrowserContextOptions(), b.logger)
130+
b.defaultContext = NewBrowserContext(b.ctx, b, "", NewBrowserContextOptions(), b.logger)
119131

120132
return b.initEvents()
121133
}
@@ -150,7 +162,7 @@ func (b *Browser) initEvents() error {
150162
cancelCtx, b.evCancelFn = context.WithCancel(b.ctx)
151163
chHandler := make(chan Event)
152164

153-
b.conn.on(cancelCtx, []string{
165+
b.connEvents.on(cancelCtx, []string{
154166
cdproto.EventTargetAttachedToTarget,
155167
cdproto.EventTargetDetachedFromTarget,
156168
EventConnectionClose,
@@ -171,9 +183,9 @@ func (b *Browser) initEvents() error {
171183
} else if event.typ == EventConnectionClose {
172184
b.logger.Debugf("Browser:initEvents:EventConnectionClose", "")
173185

174-
b.connMu.Lock()
186+
b.connectedMu.Lock()
175187
b.connected = false
176-
b.connMu.Unlock()
188+
b.connectedMu.Unlock()
177189
b.browserProc.didLoseConnection()
178190
b.cancelFn()
179191
}
@@ -220,7 +232,7 @@ func (b *Browser) onAttachedToTarget(ev *target.EventAttachedToTarget) {
220232

221233
switch evti.Type {
222234
case "background_page":
223-
p, err := NewPage(b.ctx, b.conn.getSession(ev.SessionID), browserCtx, evti.TargetID, nil, false, b.logger)
235+
p, err := NewPage(b.ctx, b.connSessions.getSession(ev.SessionID), browserCtx, evti.TargetID, nil, false, b.logger)
224236
if err != nil {
225237
isRunning := atomic.LoadInt64(&b.state) == BrowserStateOpen && b.IsConnected() //b.conn.isConnected()
226238
if _, ok := err.(*websocket.CloseError); !ok && !isRunning {
@@ -229,7 +241,15 @@ func (b *Browser) onAttachedToTarget(ev *target.EventAttachedToTarget) {
229241
ev.SessionID, evti.TargetID, err)
230242
return
231243
}
232-
k6Throw(b.ctx, "cannot create NewPage for background_page event: %w", err)
244+
select {
245+
case <-b.ctx.Done():
246+
b.logger.Debugf("Browser:onAttachedToTarget:background_page:return:<-ctx.Done",
247+
"sid:%v tid:%v err:%v",
248+
ev.SessionID, evti.TargetID, b.ctx.Err())
249+
return // ignore
250+
default:
251+
k6Throw(b.ctx, "cannot create NewPage for background_page event: %w", err)
252+
}
233253
}
234254

235255
b.pagesMu.Lock()
@@ -252,15 +272,23 @@ func (b *Browser) onAttachedToTarget(ev *target.EventAttachedToTarget) {
252272

253273
b.logger.Debugf("Browser:onAttachedToTarget:page", "sid:%v tid:%v opener nil:%t", ev.SessionID, evti.TargetID, opener == nil)
254274

255-
p, err := NewPage(b.ctx, b.conn.getSession(ev.SessionID), browserCtx, evti.TargetID, opener, true, b.logger)
275+
p, err := NewPage(b.ctx, b.connSessions.getSession(ev.SessionID), browserCtx, evti.TargetID, opener, true, b.logger)
256276
if err != nil {
257277
isRunning := atomic.LoadInt64(&b.state) == BrowserStateOpen && b.IsConnected() //b.conn.isConnected()
258278
if _, ok := err.(*websocket.CloseError); !ok && !isRunning {
259279
// If we're no longer connected to browser, then ignore WebSocket errors
260280
b.logger.Debugf("Browser:onAttachedToTarget:page:return", "sid:%v tid:%v websocket err:", ev.SessionID, evti.TargetID)
261281
return
262282
}
263-
k6Throw(b.ctx, "cannot create NewPage for page event: %w", err)
283+
select {
284+
case <-b.ctx.Done():
285+
b.logger.Debugf("Browser:onAttachedToTarget:page:return:<-ctx.Done",
286+
"sid:%v tid:%v err:%v",
287+
ev.SessionID, evti.TargetID, b.ctx.Err())
288+
return // ignore
289+
default:
290+
k6Throw(b.ctx, "cannot create NewPage for page event: %w", err)
291+
}
264292
}
265293

266294
b.pagesMu.Lock()
@@ -292,7 +320,8 @@ func (b *Browser) onDetachedFromTarget(ev *target.EventDetachedFromTarget) {
292320

293321
b.sessionIDtoTargetIDMu.RUnlock()
294322
if !ok {
295-
// We don't track targets of type "browser", "other" and "devtools", so ignore if we don't recognize target.
323+
// We don't track targets of type "browser", "other" and "devtools",
324+
// so ignore if we don't recognize target.
296325
return
297326
}
298327

@@ -311,51 +340,52 @@ func (b *Browser) newPageInContext(id cdp.BrowserContextID) (*Page, error) {
311340
browserCtx, ok := b.contexts[id]
312341
b.contextsMu.RUnlock()
313342
if !ok {
314-
return nil, fmt.Errorf("no browser context with ID %s exists", id)
343+
return nil, fmt.Errorf("missing browser context: %s", id)
315344
}
316345

317-
var (
318-
mu sync.RWMutex // protects targetID
319-
targetID target.ID
320-
localTargetID target.ID // sync free access for logging
346+
ctx, cancel := context.WithTimeout(b.ctx, b.launchOpts.Timeout)
347+
defer cancel()
321348

322-
err error
323-
)
324-
ch, evCancelFn := createWaitForEventHandler(
325-
b.ctx, browserCtx, []string{EventBrowserContextPage},
326-
func(data interface{}) bool {
327-
mu.RLock()
328-
defer mu.RUnlock()
329-
b.logger.Debugf("Browser:newPageInContext:createWaitForEventHandler", "tid:%v bctxid:%v", targetID, id)
330-
return data.(*Page).targetID == targetID
349+
// buffer of one is for sending the target ID whether an event handler
350+
// exists or not.
351+
targetID := make(chan target.ID, 1)
352+
353+
waitForPage, removeEventHandler := createWaitForEventHandler(
354+
ctx,
355+
browserCtx, // browser context will emit the following event:
356+
[]string{EventBrowserContextPage},
357+
func(e interface{}) bool {
358+
tid := <-targetID
359+
360+
b.logger.Debugf("Browser:newPageInContext:createWaitForEventHandler",
361+
"tid:%v ptid:%v bctxid:%v", tid, e.(*Page).targetID, id)
362+
363+
// we are only interested in the new page.
364+
return e.(*Page).targetID == tid
331365
},
332366
)
333-
defer evCancelFn() // Remove event handler
334-
errCh := make(chan error)
335-
func() {
336-
action := target.CreateTarget("about:blank").WithBrowserContextID(id)
337-
mu.Lock()
338-
defer mu.Unlock()
339-
localTargetID = targetID
340-
b.logger.Debugf("Browser:newPageInContext:CreateTargetBlank", "tid:%v bctxid:%v", localTargetID, id)
341-
if targetID, err = action.Do(cdp.WithExecutor(b.ctx, b.conn)); err != nil {
342-
errCh <- fmt.Errorf("unable to execute %T: %w", action, err)
343-
}
344-
}()
367+
defer removeEventHandler()
368+
369+
// create a new page.
370+
action := target.CreateTarget("about:blank").WithBrowserContextID(id)
371+
tid, err := action.Do(cdp.WithExecutor(ctx, b.conn))
372+
if err != nil {
373+
return nil, fmt.Errorf("%T: %w", action, err)
374+
}
375+
// let the event handler know about the new page.
376+
targetID <- tid
377+
var page *Page
345378
select {
346-
case <-b.ctx.Done():
347-
b.logger.Debugf("Browser:newPageInContext:<-b.ctx.Done", "tid:%v bctxid:%v", localTargetID, id)
348-
case <-time.After(b.launchOpts.Timeout):
349-
b.logger.Debugf("Browser:newPageInContext:timeout", "tid:%v bctxid:%v timeout:%s", localTargetID, id, b.launchOpts.Timeout)
350-
case c := <-ch:
351-
b.logger.Debugf("Browser:newPageInContext:<-ch", "tid:%v bctxid:%v, c:%v", localTargetID, id, c)
352-
case err := <-errCh:
353-
b.logger.Debugf("Browser:newPageInContext:<-errCh", "tid:%v bctxid:%v, err:%v", localTargetID, id, err)
354-
return nil, err
379+
case <-waitForPage:
380+
b.logger.Debugf("Browser:newPageInContext:<-waitForPage", "tid:%v bctxid:%v", tid, id)
381+
b.pagesMu.RLock()
382+
page = b.pages[tid]
383+
b.pagesMu.RUnlock()
384+
case <-ctx.Done():
385+
b.logger.Debugf("Browser:newPageInContext:<-ctx.Done", "tid:%v bctxid:%v err:%v", tid, id, ctx.Err())
386+
err = ctx.Err()
355387
}
356-
b.pagesMu.RLock()
357-
defer b.pagesMu.RUnlock()
358-
return b.pages[targetID], nil
388+
return page, err
359389
}
360390

361391
// Close shuts down the browser
@@ -397,8 +427,8 @@ func (b *Browser) Contexts() []api.BrowserContext {
397427
}
398428

399429
func (b *Browser) IsConnected() bool {
400-
b.connMu.RLock()
401-
defer b.connMu.RUnlock()
430+
b.connectedMu.RLock()
431+
defer b.connectedMu.RUnlock()
402432

403433
return b.connected
404434
}
@@ -419,7 +449,7 @@ func (b *Browser) NewContext(opts goja.Value) api.BrowserContext {
419449

420450
b.contextsMu.Lock()
421451
defer b.contextsMu.Unlock()
422-
browserCtx := NewBrowserContext(b.ctx, b.conn, b, browserContextID, browserCtxOpts, b.logger)
452+
browserCtx := NewBrowserContext(b.ctx, b, browserContextID, browserCtxOpts, b.logger)
423453
b.contexts[browserContextID] = browserCtx
424454

425455
return browserCtx

common/browser_context.go

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,6 @@ type BrowserContext struct {
4747
BaseEventEmitter
4848

4949
ctx context.Context
50-
conn *Connection
5150
browser *Browser
5251
id cdp.BrowserContextID
5352
opts *BrowserContextOptions
@@ -58,11 +57,10 @@ type BrowserContext struct {
5857
}
5958

6059
// NewBrowserContext creates a new browser context.
61-
func NewBrowserContext(ctx context.Context, conn *Connection, browser *Browser, id cdp.BrowserContextID, opts *BrowserContextOptions, logger *Logger) *BrowserContext {
60+
func NewBrowserContext(ctx context.Context, browser *Browser, id cdp.BrowserContextID, opts *BrowserContextOptions, logger *Logger) *BrowserContext {
6261
b := BrowserContext{
6362
BaseEventEmitter: NewBaseEventEmitter(ctx),
6463
ctx: ctx,
65-
conn: conn,
6664
browser: browser,
6765
id: id,
6866
opts: opts,
@@ -219,7 +217,7 @@ func (b *BrowserContext) NewPage() api.Page {
219217

220218
p, err := b.browser.newPageInContext(b.id)
221219
if err != nil {
222-
k6Throw(b.ctx, "cannot create a new page: %w", err)
220+
k6Throw(b.ctx, "newPageInContext: %w", err)
223221
}
224222

225223
var (
@@ -408,3 +406,7 @@ func (b *BrowserContext) WaitForEvent(event string, optsOrPredicate goja.Value)
408406
b.logger.Debugf("BrowserContext:WaitForEvent:return nil", "bctxid:%v event:%q", b.id, event)
409407
return nil
410408
}
409+
410+
func (b *BrowserContext) getSession(id target.SessionID) *Session {
411+
return b.browser.connSessions.getSession(id)
412+
}

0 commit comments

Comments
 (0)