@@ -26,7 +26,6 @@ import (
26
26
"strings"
27
27
"sync"
28
28
"sync/atomic"
29
- "time"
30
29
31
30
"github.com/chromedp/cdproto"
32
31
cdpbrowser "github.com/chromedp/cdproto/browser"
@@ -59,10 +58,15 @@ type Browser struct {
59
58
browserProc * BrowserProcess
60
59
launchOpts * LaunchOptions
61
60
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
66
70
67
71
contextsMu sync.RWMutex
68
72
contexts map [cdp.BrowserContextID ]* BrowserContext
@@ -82,9 +86,18 @@ type Browser struct {
82
86
logger * Logger
83
87
}
84
88
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 {
88
101
BaseEventEmitter : NewBaseEventEmitter (ctx ),
89
102
ctx : ctx ,
90
103
cancelFn : cancelFn ,
@@ -96,26 +109,25 @@ func NewBrowser(ctx context.Context, cancelFn context.CancelFunc, browserProc *B
96
109
sessionIDtoTargetID : make (map [target.SessionID ]target.ID ),
97
110
logger : logger ,
98
111
}
99
- if err := b .connect (); err != nil {
100
- return nil , err
101
- }
102
- return & b , nil
103
112
}
104
113
105
114
func (b * Browser ) connect () error {
106
115
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 )
109
117
if err != nil {
110
118
return fmt .Errorf ("unable to connect to browser WS URL: %w" , err )
111
119
}
112
120
113
- b .connMu .Lock ()
121
+ b .conn = conn
122
+ b .connEvents = conn
123
+ b .connSessions = conn
124
+
125
+ b .connectedMu .Lock ()
114
126
b .connected = true
115
- b .connMu .Unlock ()
127
+ b .connectedMu .Unlock ()
116
128
117
129
// 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 )
119
131
120
132
return b .initEvents ()
121
133
}
@@ -150,7 +162,7 @@ func (b *Browser) initEvents() error {
150
162
cancelCtx , b .evCancelFn = context .WithCancel (b .ctx )
151
163
chHandler := make (chan Event )
152
164
153
- b .conn .on (cancelCtx , []string {
165
+ b .connEvents .on (cancelCtx , []string {
154
166
cdproto .EventTargetAttachedToTarget ,
155
167
cdproto .EventTargetDetachedFromTarget ,
156
168
EventConnectionClose ,
@@ -171,9 +183,9 @@ func (b *Browser) initEvents() error {
171
183
} else if event .typ == EventConnectionClose {
172
184
b .logger .Debugf ("Browser:initEvents:EventConnectionClose" , "" )
173
185
174
- b .connMu .Lock ()
186
+ b .connectedMu .Lock ()
175
187
b .connected = false
176
- b .connMu .Unlock ()
188
+ b .connectedMu .Unlock ()
177
189
b .browserProc .didLoseConnection ()
178
190
b .cancelFn ()
179
191
}
@@ -220,7 +232,7 @@ func (b *Browser) onAttachedToTarget(ev *target.EventAttachedToTarget) {
220
232
221
233
switch evti .Type {
222
234
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 )
224
236
if err != nil {
225
237
isRunning := atomic .LoadInt64 (& b .state ) == BrowserStateOpen && b .IsConnected () //b.conn.isConnected()
226
238
if _ , ok := err .(* websocket.CloseError ); ! ok && ! isRunning {
@@ -229,7 +241,15 @@ func (b *Browser) onAttachedToTarget(ev *target.EventAttachedToTarget) {
229
241
ev .SessionID , evti .TargetID , err )
230
242
return
231
243
}
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
+ }
233
253
}
234
254
235
255
b .pagesMu .Lock ()
@@ -252,15 +272,23 @@ func (b *Browser) onAttachedToTarget(ev *target.EventAttachedToTarget) {
252
272
253
273
b .logger .Debugf ("Browser:onAttachedToTarget:page" , "sid:%v tid:%v opener nil:%t" , ev .SessionID , evti .TargetID , opener == nil )
254
274
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 )
256
276
if err != nil {
257
277
isRunning := atomic .LoadInt64 (& b .state ) == BrowserStateOpen && b .IsConnected () //b.conn.isConnected()
258
278
if _ , ok := err .(* websocket.CloseError ); ! ok && ! isRunning {
259
279
// If we're no longer connected to browser, then ignore WebSocket errors
260
280
b .logger .Debugf ("Browser:onAttachedToTarget:page:return" , "sid:%v tid:%v websocket err:" , ev .SessionID , evti .TargetID )
261
281
return
262
282
}
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
+ }
264
292
}
265
293
266
294
b .pagesMu .Lock ()
@@ -292,7 +320,8 @@ func (b *Browser) onDetachedFromTarget(ev *target.EventDetachedFromTarget) {
292
320
293
321
b .sessionIDtoTargetIDMu .RUnlock ()
294
322
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.
296
325
return
297
326
}
298
327
@@ -311,51 +340,52 @@ func (b *Browser) newPageInContext(id cdp.BrowserContextID) (*Page, error) {
311
340
browserCtx , ok := b .contexts [id ]
312
341
b .contextsMu .RUnlock ()
313
342
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 )
315
344
}
316
345
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 ()
321
348
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
331
365
},
332
366
)
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
345
378
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 ()
355
387
}
356
- b .pagesMu .RLock ()
357
- defer b .pagesMu .RUnlock ()
358
- return b .pages [targetID ], nil
388
+ return page , err
359
389
}
360
390
361
391
// Close shuts down the browser
@@ -397,8 +427,8 @@ func (b *Browser) Contexts() []api.BrowserContext {
397
427
}
398
428
399
429
func (b * Browser ) IsConnected () bool {
400
- b .connMu .RLock ()
401
- defer b .connMu .RUnlock ()
430
+ b .connectedMu .RLock ()
431
+ defer b .connectedMu .RUnlock ()
402
432
403
433
return b .connected
404
434
}
@@ -419,7 +449,7 @@ func (b *Browser) NewContext(opts goja.Value) api.BrowserContext {
419
449
420
450
b .contextsMu .Lock ()
421
451
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 )
423
453
b .contexts [browserContextID ] = browserCtx
424
454
425
455
return browserCtx
0 commit comments