chromedp/context.go
Daniel Martí 81a48280ef route all communication via the browser
Use a single websocket connection per browser, removing the need for an
extra websocket connection per target.

This is thanks to the Target.sendMessageToTarget command to send
messages to each target, and the Target.receivedMessageFromTarget event
to receive messages back.

The browser handles activity via a single worker goroutine, and the same
technique is used for each target. This means that commands and events
are dealt with in order, and we can do away with some complexity like
mutexes and extra go statements.
2019-04-01 12:18:16 +01:00

128 lines
2.7 KiB
Go

package chromedp
import (
"context"
"encoding/json"
"fmt"
"github.com/chromedp/cdproto/css"
"github.com/chromedp/cdproto/dom"
"github.com/chromedp/cdproto/inspector"
"github.com/chromedp/cdproto/log"
"github.com/chromedp/cdproto/page"
"github.com/chromedp/cdproto/runtime"
"github.com/chromedp/cdproto/target"
)
// Executor
type Executor interface {
Execute(context.Context, string, json.Marshaler, json.Unmarshaler) error
}
// Context
type Context struct {
Allocator Allocator
browser *Browser
sessionID target.SessionID
}
// Wait can be called after cancelling the context containing Context, to block
// until all the underlying resources have been cleaned up.
func (c *Context) Wait() {
if c.Allocator != nil {
c.Allocator.Wait()
}
}
// NewContext creates a browser context using the parent context.
func NewContext(parent context.Context, opts ...ContextOption) (context.Context, context.CancelFunc) {
ctx, cancel := context.WithCancel(parent)
c := &Context{}
if pc := FromContext(parent); pc != nil {
c.Allocator = pc.Allocator
}
for _, o := range opts {
o(c)
}
if c.Allocator == nil {
WithExecAllocator(
NoFirstRun,
NoDefaultBrowserCheck,
Headless,
)(&c.Allocator)
}
ctx = context.WithValue(ctx, contextKey{}, c)
return ctx, cancel
}
type contextKey struct{}
// FromContext creates a new browser context from the provided context.
func FromContext(ctx context.Context) *Context {
c, _ := ctx.Value(contextKey{}).(*Context)
return c
}
// Run runs the action against the provided browser context.
func Run(ctx context.Context, action Action) error {
c := FromContext(ctx)
if c == nil || c.Allocator == nil {
return ErrInvalidContext
}
if c.browser == nil {
browser, err := c.Allocator.Allocate(ctx)
if err != nil {
return err
}
c.browser = browser
}
if c.sessionID == "" {
if err := c.newSession(ctx); err != nil {
return err
}
}
return action.Do(ctx, c.browser.executorForTarget(ctx, c.sessionID))
}
func (c *Context) newSession(ctx context.Context) error {
create := target.CreateTarget("about:blank")
targetID, err := create.Do(ctx, c.browser)
if err != nil {
return err
}
attach := target.AttachToTarget(targetID)
sessionID, err := attach.Do(ctx, c.browser)
if err != nil {
return err
}
target := c.browser.executorForTarget(ctx, sessionID)
// enable domains
for _, enable := range []Action{
log.Enable(),
runtime.Enable(),
//network.Enable(),
inspector.Enable(),
page.Enable(),
dom.Enable(),
css.Enable(),
} {
if err := enable.Do(ctx, target); err != nil {
return fmt.Errorf("unable to execute %T: %v", enable, err)
}
}
c.sessionID = sessionID
return nil
}
// ContextOption
type ContextOption func(*Context)