diff --git a/.travis.yml b/.travis.yml index c9961da..1cc2433 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,14 +1,7 @@ language: go + go: - - 1.10.x - - 1.11.x -addons: - apt: - chrome: stable -before_install: - - go get github.com/mattn/goveralls golang.org/x/vgo + - 1.12.x + script: - - export CHROMEDP_TEST_RUNNER=google-chrome-stable - - export CHROMEDP_DISABLE_GPU=true - - vgo test -v -coverprofile=coverage.out - - goveralls -service=travis-ci -coverprofile=coverage.out + - true diff --git a/_example/allocactor.go b/_example/allocactor.go new file mode 100644 index 0000000..8a1fa5d --- /dev/null +++ b/_example/allocactor.go @@ -0,0 +1,26 @@ +package main + +import ( + "context" + "log" + + "github.com/chromedp/chromedp" +) + +func main() { + dockerAllocatorOpts := []chromedp.DockerAllocatorOption{} + + ctxt, cancel := chromedp.NewAllocator(context.Background(), chromedp.WithDockerAllocator(dockerAllocatorOpts...)) + defer cancel() + + task1Context, cancel := chromedp.NewContext(ctxt) + defer cancel() + + if err := chromedp.Run(task1Context, myTask()); err != nil { + log.Fatal(err) + } +} + +func myTask() chromedp.Tasks { + return []chromedp.Action{} +} diff --git a/_example/simple.go b/_example/simple.go new file mode 100644 index 0000000..6c5b403 --- /dev/null +++ b/_example/simple.go @@ -0,0 +1,36 @@ +package main + +import ( + "context" + "fmt" + "log" + + "github.com/chromedp/chromedp" +) + +func main() { + // create a new context + ctx, cancel := chromedp.NewContext(context.Background()) + defer cancel() + + // grab the title + var title string + if err := chromedp.Run(ctx, grabTitle(&title)); err != nil { + log.Fatal(err) + } + + // print it + fmt.Println(title) + + // ensure all resources are cleaned up + cancel() + chromedp.FromContext(ctx).Wait() +} + +func grabTitle(title *string) chromedp.Tasks { + return []chromedp.Action{ + chromedp.Navigate("https://github.com/"), + chromedp.WaitVisible("#start-of-content", chromedp.ByID), + chromedp.Title(title), + } +} diff --git a/_example/tabs.go b/_example/tabs.go new file mode 100644 index 0000000..8d5b954 --- /dev/null +++ b/_example/tabs.go @@ -0,0 +1,31 @@ +package main + +import ( + "context" + "log" + + "github.com/chromedp/chromedp" +) + +func main() { + // first tab + ctx1, cancel := chromedp.NewContext(context.Background()) + defer cancel() + + // create new tab + ctx2, _ := chromedp.NewContext(ctx1) + + // runs in first tab + if err := chromedp.Run(ctx1, myTask()); err != nil { + log.Fatal(err) + } + + // runs in second tab + if err := chromedp.Run(ctx2, myTask()); err != nil { + log.Fatal(err) + } +} + +func myTask() chromedp.Tasks { + return []chromedp.Action{} +} diff --git a/allocate.go b/allocate.go new file mode 100644 index 0000000..5110ba1 --- /dev/null +++ b/allocate.go @@ -0,0 +1,255 @@ +package chromedp + +import ( + "bufio" + "context" + "fmt" + "io/ioutil" + "os" + "os/exec" + "strings" + "sync" +) + +type Allocator interface { + // Allocate creates a new browser from the pool. It can be cancelled via + // the provided context, at which point all the resources used by the + // browser (such as temporary directories) will be cleaned up. + Allocate(context.Context) (*Browser, error) + + // Wait can be called after cancelling a pool's context, to block until + // all the pool's resources have been cleaned up. + Wait() +} + +func NewAllocator(parent context.Context, opts ...AllocatorOption) (context.Context, context.CancelFunc) { + ctx, cancel := context.WithCancel(parent) + c := &Context{} + + for _, o := range opts { + o(&c.Allocator) + } + + ctx = context.WithValue(ctx, contextKey{}, c) + return ctx, cancel +} + +type AllocatorOption func(*Allocator) + +func WithExecAllocator(opts ...ExecAllocatorOption) func(*Allocator) { + return func(p *Allocator) { + ep := &ExecAllocator{ + initFlags: make(map[string]interface{}), + } + for _, o := range opts { + o(ep) + } + if ep.execPath == "" { + ep.execPath = findExecPath() + } + *p = ep + } +} + +type ExecAllocatorOption func(*ExecAllocator) + +type ExecAllocator struct { + execPath string + initFlags map[string]interface{} + + wg sync.WaitGroup +} + +func (p *ExecAllocator) Allocate(ctx context.Context) (*Browser, error) { + removeDir := false + var cmd *exec.Cmd + + // TODO: figure out a nicer way to do this + flags := make(map[string]interface{}) + for name, value := range p.initFlags { + flags[name] = value + } + + dataDir, ok := flags["user-data-dir"].(string) + if !ok { + tempDir, err := ioutil.TempDir("", "chromedp-runner") + if err != nil { + return nil, err + } + flags["user-data-dir"] = tempDir + dataDir = tempDir + removeDir = true + } + + p.wg.Add(1) + go func() { + <-ctx.Done() + // First wait for the process to be finished. + if cmd != nil { + cmd.Wait() + } + // Then delete the temporary user data directory, if needed. + if removeDir { + os.RemoveAll(dataDir) + } + p.wg.Done() + }() + + flags["remote-debugging-port"] = "0" + + args := []string{} + for name, value := range flags { + switch value := value.(type) { + case string: + args = append(args, fmt.Sprintf("--%s=%s", name, value)) + case bool: + if value { + args = append(args, fmt.Sprintf("--%s", name)) + } + default: + return nil, fmt.Errorf("invalid exec pool flag") + } + } + + cmd = exec.CommandContext(ctx, p.execPath, args...) + stderr, err := cmd.StderrPipe() + if err != nil { + return nil, err + } + if err := cmd.Start(); err != nil { + return nil, err + } + + // Pick up the browser's websocket URL from stderr. + wsURL := "" + scanner := bufio.NewScanner(stderr) + prefix := "DevTools listening on" + for scanner.Scan() { + line := scanner.Text() + if s := strings.TrimPrefix(line, prefix); s != line { + wsURL = strings.TrimSpace(s) + break + } + } + if err := scanner.Err(); err != nil { + return nil, err + } + stderr.Close() + + browser, err := NewBrowser(wsURL) + if err != nil { + return nil, err + } + browser.UserDataDir = dataDir + return browser, nil +} + +func (p *ExecAllocator) Wait() { + p.wg.Wait() +} + +func ExecPath(path string) ExecAllocatorOption { + return func(p *ExecAllocator) { + if fullPath, _ := exec.LookPath(path); fullPath != "" { + // Convert to an absolute path if possible, to avoid + // repeated LookPath calls in each Allocate. + path = fullPath + } + p.execPath = path + } +} + +// findExecPath tries to find the Chrome browser somewhere in the current +// system. It performs a rather agressive search, which is the same in all +// systems. That may make it a bit slow, but it will only be run when creating a +// new ExecAllocator. +func findExecPath() string { + for _, path := range [...]string{ + // Unix-like + "headless_shell", + "chromium", + "chromium-browser", + "google-chrome", + "google-chrome-stable", + "google-chrome-beta", + "google-chrome-unstable", + "/usr/bin/google-chrome", + + // Windows + "chrome", + "chrome.exe", // in case PATHEXT is misconfigured + `C:\Program Files (x86)\Google\Chrome\Application\chrome.exe`, + + // Mac + `/Applications/Google Chrome.app/Contents/MacOS/Google Chrome`, + } { + found, err := exec.LookPath(path) + if err == nil { + return found + } + } + // Fall back to something simple and sensible, to give a useful error + // message. + return "google-chrome" +} + +// Flag is a generic command line option to pass a flag to Chrome. If the value +// is a string, it will be passed as --name=value. If it's a boolean, it will be +// passed as --name if value is true. +func Flag(name string, value interface{}) ExecAllocatorOption { + return func(p *ExecAllocator) { + p.initFlags[name] = value + } +} + +// UserDataDir is the command line option to set the user data dir. +// +// Note: set this option to manually set the profile directory used by Chrome. +// When this is not set, then a default path will be created in the /tmp +// directory. +func UserDataDir(dir string) ExecAllocatorOption { + return Flag("user-data-dir", dir) +} + +// ProxyServer is the command line option to set the outbound proxy server. +func ProxyServer(proxy string) ExecAllocatorOption { + return Flag("proxy-server", proxy) +} + +// WindowSize is the command line option to set the initial window size. +func WindowSize(width, height int) ExecAllocatorOption { + return Flag("window-size", fmt.Sprintf("%d,%d", width, height)) +} + +// UserAgent is the command line option to set the default User-Agent +// header. +func UserAgent(userAgent string) ExecAllocatorOption { + return Flag("user-agent", userAgent) +} + +// NoSandbox is the Chrome comamnd line option to disable the sandbox. +func NoSandbox(p *ExecAllocator) { + Flag("no-sandbox", true)(p) +} + +// NoFirstRun is the Chrome comamnd line option to disable the first run +// dialog. +func NoFirstRun(p *ExecAllocator) { + Flag("no-first-run", true)(p) +} + +// NoDefaultBrowserCheck is the Chrome comamnd line option to disable the +// default browser check. +func NoDefaultBrowserCheck(p *ExecAllocator) { + Flag("no-default-browser-check", true)(p) +} + +// Headless is the command line option to run in headless mode. +func Headless(p *ExecAllocator) { + Flag("headless", true)(p) +} + +// DisableGPU is the command line option to disable the GPU process. +func DisableGPU(p *ExecAllocator) { + Flag("disable-gpu", true)(p) +} diff --git a/allocate_test.go b/allocate_test.go new file mode 100644 index 0000000..d3997ff --- /dev/null +++ b/allocate_test.go @@ -0,0 +1,43 @@ +package chromedp + +import ( + "context" + "os" + "testing" +) + +func TestExecAllocator(t *testing.T) { + t.Parallel() + + poolCtx, cancel := NewAllocator(context.Background(), WithExecAllocator(allocOpts...)) + defer cancel() + + // TODO: test that multiple child contexts are run in different + // processes and browsers. + + taskCtx, cancel := NewContext(poolCtx) + defer cancel() + + want := "insert" + var got string + if err := Run(taskCtx, Tasks{ + Navigate(testdataDir + "/form.html"), + Text("#foo", &got, ByID), + }); err != nil { + t.Fatal(err) + } + if got != want { + t.Fatalf("wanted %q, got %q", want, got) + } + + tempDir := FromContext(taskCtx).browser.UserDataDir + pool := FromContext(taskCtx).Allocator + + cancel() + pool.Wait() + + if _, err := os.Lstat(tempDir); os.IsNotExist(err) { + return + } + t.Fatalf("temporary user data dir %q not deleted", tempDir) +} diff --git a/browser.go b/browser.go new file mode 100644 index 0000000..5995af1 --- /dev/null +++ b/browser.go @@ -0,0 +1,122 @@ +// Package chromedp is a high level Chrome DevTools Protocol client that +// simplifies driving browsers for scraping, unit testing, or profiling web +// pages using the CDP. +// +// chromedp requires no third-party dependencies, implementing the async Chrome +// DevTools Protocol entirely in Go. +package chromedp + +import ( + "context" + "log" + "sync/atomic" + + "github.com/chromedp/cdproto" + "github.com/mailru/easyjson" +) + +// Browser is the high-level Chrome DevTools Protocol browser manager, handling +// the browser process runner, WebSocket clients, associated targets, and +// network, page, and DOM events. +type Browser struct { + UserDataDir string + + conn Transport + + // next is the next message id. + next int64 + + // logging funcs + logf func(string, ...interface{}) + errf func(string, ...interface{}) +} + +// NewBrowser creates a new browser. +func NewBrowser(urlstr string, opts ...BrowserOption) (*Browser, error) { + conn, err := Dial(ForceIP(urlstr)) + if err != nil { + return nil, err + } + + b := &Browser{ + conn: conn, + logf: log.Printf, + } + + // apply options + for _, o := range opts { + if err := o(b); err != nil { + return nil, err + } + } + + // ensure errf is set + if b.errf == nil { + b.errf = func(s string, v ...interface{}) { b.logf("ERROR: "+s, v...) } + } + + return b, nil +} + +// Shutdown shuts down the browser. +func (b *Browser) Shutdown() error { + if b.conn != nil { + if err := b.send(cdproto.CommandBrowserClose, nil); err != nil { + b.errf("could not close browser: %v", err) + } + return b.conn.Close() + } + return nil +} + +// send writes the supplied message and params. +func (b *Browser) send(method cdproto.MethodType, params easyjson.RawMessage) error { + msg := &cdproto.Message{ + Method: method, + ID: atomic.AddInt64(&b.next, 1), + Params: params, + } + buf, err := msg.MarshalJSON() + if err != nil { + return err + } + return b.conn.Write(buf) +} + +// sendToTarget writes the supplied message to the target. +func (b *Browser) sendToTarget(targetID string, method cdproto.MethodType, params easyjson.RawMessage) error { + return nil +} + +// CreateContext creates a new browser context. +func (b *Browser) CreateContext() (context.Context, error) { + return nil, nil +} + +// BrowserOption is a browser option. +type BrowserOption func(*Browser) error + +// WithLogf is a browser option to specify a func to receive general logging. +func WithLogf(f func(string, ...interface{})) BrowserOption { + return func(b *Browser) error { + b.logf = f + return nil + } +} + +// WithErrorf is a browser option to specify a func to receive error logging. +func WithErrorf(f func(string, ...interface{})) BrowserOption { + return func(b *Browser) error { + b.errf = f + return nil + } +} + +// WithConsolef is a browser option to specify a func to receive chrome log events. +// +// Note: NOT YET IMPLEMENTED. +func WithConsolef(f func(string, ...interface{})) BrowserOption { + return func(b *Browser) error { + return nil + } +} diff --git a/chromedp.go b/chromedp.go deleted file mode 100644 index 7c2213a..0000000 --- a/chromedp.go +++ /dev/null @@ -1,435 +0,0 @@ -// Package chromedp is a high level Chrome DevTools Protocol client that -// simplifies driving browsers for scraping, unit testing, or profiling web -// pages using the CDP. -// -// chromedp requires no third-party dependencies, implementing the async Chrome -// DevTools Protocol entirely in Go. -package chromedp - -import ( - "context" - "errors" - "fmt" - "log" - "sync" - "time" - - "github.com/chromedp/cdproto/cdp" - - "github.com/chromedp/chromedp/client" - "github.com/chromedp/chromedp/runner" -) - -const ( - // DefaultNewTargetTimeout is the default time to wait for a new target to - // be started. - DefaultNewTargetTimeout = 3 * time.Second - - // DefaultCheckDuration is the default time to sleep between a check. - DefaultCheckDuration = 50 * time.Millisecond - - // DefaultPoolStartPort is the default start port number. - DefaultPoolStartPort = 9000 - - // DefaultPoolEndPort is the default end port number. - DefaultPoolEndPort = 10000 -) - -// CDP is the high-level Chrome DevTools Protocol browser manager, handling the -// browser process runner, WebSocket clients, associated targets, and network, -// page, and DOM events. -type CDP struct { - // r is the chrome runner. - r *runner.Runner - - // opts are command line options to pass to a created runner. - opts []runner.CommandLineOption - - // watch is the channel for new client targets. - watch <-chan client.Target - - // cur is the current active target's handler. - cur cdp.Executor - - // handlers is the active handlers. - handlers []*TargetHandler - - // handlerMap is the map of target IDs to its active handler. - handlerMap map[string]int - - // logging funcs - logf, debugf, errf func(string, ...interface{}) - - sync.RWMutex -} - -// New creates and starts a new CDP instance. -func New(ctxt context.Context, opts ...Option) (*CDP, error) { - c := &CDP{ - handlers: make([]*TargetHandler, 0), - handlerMap: make(map[string]int), - logf: log.Printf, - debugf: func(string, ...interface{}) {}, - errf: func(s string, v ...interface{}) { log.Printf("error: "+s, v...) }, - } - - // apply options - for _, o := range opts { - if err := o(c); err != nil { - return nil, err - } - } - - // check for supplied runner, if none then create one - if c.r == nil && c.watch == nil { - var err error - c.r, err = runner.Run(ctxt, c.opts...) - if err != nil { - return nil, err - } - } - - // watch handlers - if c.watch == nil { - c.watch = c.r.Client().WatchPageTargets(ctxt) - } - - go func() { - for t := range c.watch { - if t == nil { - return - } - go c.AddTarget(ctxt, t) - } - }() - - // TODO: fix this - timeout := time.After(defaultNewTargetTimeout) - - // wait until at least one target active - for { - select { - default: - c.RLock() - exists := c.cur != nil - c.RUnlock() - if exists { - return c, nil - } - - // TODO: fix this - time.Sleep(DefaultCheckDuration) - - case <-ctxt.Done(): - return nil, ctxt.Err() - - case <-timeout: - return nil, errors.New("timeout waiting for initial target") - } - } -} - -// AddTarget adds a target using the supplied context. -func (c *CDP) AddTarget(ctxt context.Context, t client.Target) { - c.Lock() - defer c.Unlock() - - // create target manager - h, err := NewTargetHandler(t, c.logf, c.debugf, c.errf) - if err != nil { - c.errf("could not create handler for %s: %v", t, err) - return - } - - // run - if err := h.Run(ctxt); err != nil { - c.errf("could not start handler for %s: %v", t, err) - return - } - - // add to active handlers - c.handlers = append(c.handlers, h) - c.handlerMap[t.GetID()] = len(c.handlers) - 1 - if c.cur == nil { - c.cur = h - } -} - -// Wait waits for the Chrome runner to terminate. -func (c *CDP) Wait() error { - c.RLock() - r := c.r - c.RUnlock() - - if r != nil { - return r.Wait() - } - - return nil -} - -// Shutdown closes all Chrome page handlers. -func (c *CDP) Shutdown(ctxt context.Context, opts ...client.Option) error { - c.RLock() - defer c.RUnlock() - - if c.r != nil { - return c.r.Shutdown(ctxt, opts...) - } - - return nil -} - -// ListTargets returns the target IDs of the managed targets. -func (c *CDP) ListTargets() []string { - c.RLock() - defer c.RUnlock() - - i, targets := 0, make([]string, len(c.handlers)) - for k := range c.handlerMap { - targets[i] = k - i++ - } - - return targets -} - -// GetHandlerByIndex retrieves the domains manager for the specified index. -func (c *CDP) GetHandlerByIndex(i int) cdp.Executor { - c.RLock() - defer c.RUnlock() - - if i < 0 || i >= len(c.handlers) { - return nil - } - - return c.handlers[i] -} - -// GetHandlerByID retrieves the domains manager for the specified target ID. -func (c *CDP) GetHandlerByID(id string) cdp.Executor { - c.RLock() - defer c.RUnlock() - - if i, ok := c.handlerMap[id]; ok { - return c.handlers[i] - } - - return nil -} - -// SetHandler sets the active handler to the target with the specified index. -func (c *CDP) SetHandler(i int) error { - c.Lock() - defer c.Unlock() - - if i < 0 || i >= len(c.handlers) { - return fmt.Errorf("no handler associated with target index %d", i) - } - - c.cur = c.handlers[i] - - return nil -} - -// SetHandlerByID sets the active target to the target with the specified id. -func (c *CDP) SetHandlerByID(id string) error { - c.Lock() - defer c.Unlock() - - if i, ok := c.handlerMap[id]; ok { - c.cur = c.handlers[i] - return nil - } - - return fmt.Errorf("no handler associated with target id %s", id) -} - -// newTarget creates a new target using supplied context and options, returning -// the id of the created target only after the target has been started for -// monitoring. -func (c *CDP) newTarget(ctxt context.Context, opts ...client.Option) (string, error) { - c.RLock() - cl := c.r.Client(opts...) - c.RUnlock() - - // new page target - t, err := cl.NewPageTarget(ctxt) - if err != nil { - return "", err - } - - timeout := time.After(DefaultNewTargetTimeout) - - for { - select { - default: - var ok bool - id := t.GetID() - c.RLock() - _, ok = c.handlerMap[id] - c.RUnlock() - if ok { - return id, nil - } - - time.Sleep(DefaultCheckDuration) - - case <-ctxt.Done(): - return "", ctxt.Err() - - case <-timeout: - return "", errors.New("timeout waiting for new target to be available") - } - } -} - -// SetTarget is an action that sets the active Chrome handler to the specified -// index i. -func (c *CDP) SetTarget(i int) Action { - return ActionFunc(func(context.Context, cdp.Executor) error { - return c.SetHandler(i) - }) -} - -// SetTargetByID is an action that sets the active Chrome handler to the handler -// associated with the specified id. -func (c *CDP) SetTargetByID(id string) Action { - return ActionFunc(func(context.Context, cdp.Executor) error { - return c.SetHandlerByID(id) - }) -} - -// NewTarget is an action that creates a new Chrome target, and sets it as the -// active target. -func (c *CDP) NewTarget(id *string, opts ...client.Option) Action { - return ActionFunc(func(ctxt context.Context, h cdp.Executor) error { - n, err := c.newTarget(ctxt, opts...) - if err != nil { - return err - } - - if id != nil { - *id = n - } - - return nil - }) -} - -// CloseByIndex closes the Chrome target with specified index i. -func (c *CDP) CloseByIndex(i int) Action { - return ActionFunc(func(ctxt context.Context, h cdp.Executor) error { - return nil - }) -} - -// CloseByID closes the Chrome target with the specified id. -func (c *CDP) CloseByID(id string) Action { - return ActionFunc(func(ctxt context.Context, h cdp.Executor) error { - return nil - }) -} - -// Run executes the action against the current target using the supplied -// context. -func (c *CDP) Run(ctxt context.Context, a Action) error { - c.RLock() - cur := c.cur - c.RUnlock() - - return a.Do(ctxt, cur) -} - -// Option is a Chrome DevTools Protocol option. -type Option func(*CDP) error - -// WithRunner is a CDP option to specify the underlying Chrome runner to -// monitor for page handlers. -func WithRunner(r *runner.Runner) Option { - return func(c *CDP) error { - c.r = r - return nil - } -} - -// WithTargets is a CDP option to specify the incoming targets to monitor for -// page handlers. -func WithTargets(watch <-chan client.Target) Option { - return func(c *CDP) error { - c.watch = watch - return nil - } -} - -// WithClient is a CDP option to use the incoming targets from a client. -func WithClient(ctxt context.Context, cl *client.Client) Option { - return func(c *CDP) error { - return WithTargets(cl.WatchPageTargets(ctxt))(c) - } -} - -// WithURL is a CDP option to use a client with the specified URL. -func WithURL(ctxt context.Context, urlstr string) Option { - return func(c *CDP) error { - return WithClient(ctxt, client.New(client.URL(urlstr)))(c) - } -} - -// WithRunnerOptions is a CDP option to specify the options to pass to a newly -// created Chrome process runner. -func WithRunnerOptions(opts ...runner.CommandLineOption) Option { - return func(c *CDP) error { - c.opts = opts - return nil - } -} - -// WithLogf is a CDP option to specify a func to receive general logging. -func WithLogf(f func(string, ...interface{})) Option { - return func(c *CDP) error { - c.logf = f - return nil - } -} - -// WithDebugf is a CDP option to specify a func to receive debug logging (ie, -// protocol information). -func WithDebugf(f func(string, ...interface{})) Option { - return func(c *CDP) error { - c.debugf = f - return nil - } -} - -// WithErrorf is a CDP option to specify a func to receive error logging. -func WithErrorf(f func(string, ...interface{})) Option { - return func(c *CDP) error { - c.errf = f - return nil - } -} - -// WithLog is a CDP option that sets the logging, debugging, and error funcs to -// f. -func WithLog(f func(string, ...interface{})) Option { - return func(c *CDP) error { - c.logf, c.debugf, c.errf = f, f, f - return nil - } -} - -// WithConsolef is a CDP option to specify a func to receive chrome log events. -// -// Note: NOT YET IMPLEMENTED. -func WithConsolef(f func(string, ...interface{})) Option { - return func(c *CDP) error { - return nil - } -} - -var ( - // defaultNewTargetTimeout is the default target timeout -- used by - // testing. - defaultNewTargetTimeout = DefaultNewTargetTimeout -) diff --git a/chromedp_test.go b/chromedp_test.go index e687f12..018ad26 100644 --- a/chromedp_test.go +++ b/chromedp_test.go @@ -2,117 +2,71 @@ package chromedp import ( "context" - "log" + "fmt" "os" "path" "testing" - "time" - - "github.com/chromedp/chromedp/runner" ) var ( - pool *Pool testdataDir string - defaultContext, defaultCancel = context.WithCancel(context.Background()) + allocCtx context.Context - cliOpts = []runner.CommandLineOption{ - runner.NoDefaultBrowserCheck, - runner.NoFirstRun, + allocOpts = []ExecAllocatorOption{ + NoFirstRun, + NoDefaultBrowserCheck, + Headless, } ) -func testAllocate(t *testing.T, path string) *Res { - c, err := pool.Allocate(defaultContext, cliOpts...) - if err != nil { - t.Fatalf("could not allocate from pool: %v", err) +func testAllocate(t *testing.T, path string) (_ context.Context, cancel func()) { + ctx, cancel := NewContext(allocCtx) + + if err := Run(ctx, Navigate(testdataDir+"/"+path)); err != nil { + t.Fatal(err) } - err = WithLogf(t.Logf)(c.c) - if err != nil { - t.Fatalf("could not set logf: %v", err) - } + //if err := WithLogf(t.Logf)(c.c); err != nil { + // t.Fatalf("could not set logf: %v", err) + //} + //if err := WithDebugf(t.Logf)(c.c); err != nil { + // t.Fatalf("could not set debugf: %v", err) + //} + //if err := WithErrorf(t.Errorf)(c.c); err != nil { + // t.Fatalf("could not set errorf: %v", err) + //} - err = WithDebugf(t.Logf)(c.c) - if err != nil { - t.Fatalf("could not set debugf: %v", err) - } - - err = WithErrorf(t.Errorf)(c.c) - if err != nil { - t.Fatalf("could not set errorf: %v", err) - } - - h := c.c.GetHandlerByIndex(0) - th, ok := h.(*TargetHandler) - if !ok { - t.Fatalf("handler is invalid type") - } - - th.logf, th.debugf = t.Logf, t.Logf - th.errf = func(s string, v ...interface{}) { - t.Logf("TARGET HANDLER ERROR: "+s, v...) - } - - if path != "" { - err = c.Run(defaultContext, Navigate(testdataDir+"/"+path)) - if err != nil { - t.Fatalf("could not navigate to testdata/%s: %v", path, err) - } - } - - return c + return ctx, cancel } func TestMain(m *testing.M) { - var err error - wd, err := os.Getwd() if err != nil { - log.Fatalf("could not get working directory: %v", err) - os.Exit(1) + panic(fmt.Sprintf("could not get working directory: %v", err)) } testdataDir = "file://" + path.Join(wd, "testdata") - // its worth noting that newer versions of chrome (64+) run much faster + // it's worth noting that newer versions of chrome (64+) run much faster // than older ones -- same for headless_shell ... - execPath := os.Getenv("CHROMEDP_TEST_RUNNER") - if execPath == "" { - execPath = runner.LookChromeNames("headless_shell") + if execPath := os.Getenv("CHROMEDP_TEST_RUNNER"); execPath != "" { + allocOpts = append(allocOpts, ExecPath(execPath)) } - cliOpts = append(cliOpts, runner.ExecPath(execPath)) - // not explicitly needed to be set, as this vastly speeds up unit tests if noSandbox := os.Getenv("CHROMEDP_NO_SANDBOX"); noSandbox != "false" { - cliOpts = append(cliOpts, runner.NoSandbox) + allocOpts = append(allocOpts, NoSandbox) } // must be explicitly set, as disabling gpu slows unit tests if disableGPU := os.Getenv("CHROMEDP_DISABLE_GPU"); disableGPU != "" && disableGPU != "false" { - cliOpts = append(cliOpts, runner.DisableGPU) + allocOpts = append(allocOpts, DisableGPU) } - if targetTimeout := os.Getenv("CHROMEDP_TARGET_TIMEOUT"); targetTimeout != "" { - defaultNewTargetTimeout, _ = time.ParseDuration(targetTimeout) - } - if defaultNewTargetTimeout == 0 { - defaultNewTargetTimeout = 30 * time.Second - } - - //pool, err = NewPool(PoolLog(log.Printf, log.Printf, log.Printf)) - pool, err = NewPool() - if err != nil { - log.Fatal(err) - } + ctx, cancel := NewAllocator(context.Background(), WithExecAllocator(allocOpts...)) + allocCtx = ctx code := m.Run() - defaultCancel() - - err = pool.Shutdown() - if err != nil { - log.Fatal(err) - } - + cancel() + FromContext(ctx).Wait() os.Exit(code) } diff --git a/client/chrome.go b/client/chrome.go deleted file mode 100644 index 946ca5a..0000000 --- a/client/chrome.go +++ /dev/null @@ -1,46 +0,0 @@ -package client - -import "fmt" - -//go:generate easyjson -omit_empty -output_filename easyjson.go chrome.go - -// Chrome holds connection information for a Chrome target. -// -//easyjson:json -type Chrome struct { - Description string `json:"description,omitempty"` - DevtoolsURL string `json:"devtoolsFrontendUrl,omitempty"` - ID string `json:"id,omitempty"` - Title string `json:"title,omitempty"` - Type TargetType `json:"type,omitempty"` - URL string `json:"url,omitempty"` - WebsocketURL string `json:"webSocketDebuggerUrl,omitempty"` - FaviconURL string `json:"faviconURL,omitempty"` -} - -// String satisfies the stringer interface. -func (c Chrome) String() string { - return fmt.Sprintf("[%s]: %q", c.ID, c.Title) -} - -// GetID returns the target ID. -func (c *Chrome) GetID() string { - return c.ID -} - -// GetType returns the target type. -func (c *Chrome) GetType() TargetType { - return c.Type -} - -// GetDevtoolsURL returns the devtools frontend target URL, satisfying the -// domains.Target interface. -func (c *Chrome) GetDevtoolsURL() string { - return c.DevtoolsURL -} - -// GetWebsocketURL provides the websocket URL for the target, satisfying the -// domains.Target interface. -func (c *Chrome) GetWebsocketURL() string { - return c.WebsocketURL -} diff --git a/client/client.go b/client/client.go deleted file mode 100644 index 5652724..0000000 --- a/client/client.go +++ /dev/null @@ -1,340 +0,0 @@ -// Package client provides the low level Chrome DevTools Protocol client. -package client - -//go:generate go run gen.go - -import ( - "context" - "encoding/json" - "io/ioutil" - "net" - "net/http" - "regexp" - "strings" - "sync" - "time" - - "github.com/mailru/easyjson" -) - -const ( - // DefaultEndpoint is the default endpoint to connect to. - DefaultEndpoint = "http://localhost:9222/json" - - // DefaultWatchInterval is the default check duration. - DefaultWatchInterval = 100 * time.Millisecond - - // DefaultWatchTimeout is the default watch timeout. - DefaultWatchTimeout = 5 * time.Second -) - -// Error is a client error. -type Error string - -// Error satisfies the error interface. -func (err Error) Error() string { - return string(err) -} - -const ( - // ErrUnsupportedProtocolType is the unsupported protocol type error. - ErrUnsupportedProtocolType Error = "unsupported protocol type" - - // ErrUnsupportedProtocolVersion is the unsupported protocol version error. - ErrUnsupportedProtocolVersion Error = "unsupported protocol version" -) - -// Target is the common interface for a Chrome DevTools Protocol target. -type Target interface { - String() string - GetID() string - GetType() TargetType - GetDevtoolsURL() string - GetWebsocketURL() string -} - -// Client is a Chrome DevTools Protocol client. -type Client struct { - url string - check time.Duration - timeout time.Duration - - ver, typ string - rw sync.RWMutex -} - -// New creates a new Chrome DevTools Protocol client. -func New(opts ...Option) *Client { - c := &Client{ - url: DefaultEndpoint, - check: DefaultWatchInterval, - timeout: DefaultWatchTimeout, - } - - // apply opts - for _, o := range opts { - o(c) - } - - return c -} - -// doReq executes a request. -func (c *Client) doReq(ctxt context.Context, action string, v interface{}) error { - // create request - req, err := http.NewRequest("GET", c.url+"/"+action, nil) - if err != nil { - return err - } - req = req.WithContext(ctxt) - - cl := &http.Client{} - - // execute - res, err := cl.Do(req) - if err != nil { - return err - } - defer res.Body.Close() - - if v != nil { - // load body - body, err := ioutil.ReadAll(res.Body) - if err != nil { - return err - } - - // unmarshal - if z, ok := v.(easyjson.Unmarshaler); ok { - return easyjson.Unmarshal(body, z) - } - - return json.Unmarshal(body, v) - } - - return nil -} - -// ListTargets returns a list of all targets. -func (c *Client) ListTargets(ctxt context.Context) ([]Target, error) { - var err error - - var l []json.RawMessage - if err = c.doReq(ctxt, "list", &l); err != nil { - return nil, err - } - - t := make([]Target, len(l)) - for i, v := range l { - t[i], err = c.newTarget(ctxt, v) - if err != nil { - return nil, err - } - } - - return t, nil -} - -// ListTargetsWithType returns a list of Targets with the specified target -// type. -func (c *Client) ListTargetsWithType(ctxt context.Context, typ TargetType) ([]Target, error) { - var err error - - targets, err := c.ListTargets(ctxt) - if err != nil { - return nil, err - } - - var ret []Target - for _, t := range targets { - if t.GetType() == typ { - ret = append(ret, t) - } - } - - return ret, nil -} - -// ListPageTargets lists the available Page targets. -func (c *Client) ListPageTargets(ctxt context.Context) ([]Target, error) { - return c.ListTargetsWithType(ctxt, Page) -} - -var browserRE = regexp.MustCompile(`(?i)^(chrome|chromium|microsoft edge|safari)`) - -// loadProtocolInfo loads the protocol information from the remote URL. -func (c *Client) loadProtocolInfo(ctxt context.Context) (string, string, error) { - c.rw.Lock() - defer c.rw.Unlock() - - if c.ver == "" || c.typ == "" { - v, err := c.VersionInfo(ctxt) - if err != nil { - return "", "", err - } - - if m := browserRE.FindAllStringSubmatch(v["Browser"], -1); len(m) != 0 { - c.typ = strings.ToLower(m[0][0]) - } - c.ver = v["Protocol-Version"] - } - - return c.ver, c.typ, nil -} - -// newTarget creates a new target. -func (c *Client) newTarget(ctxt context.Context, buf []byte) (Target, error) { - var err error - - ver, typ, err := c.loadProtocolInfo(ctxt) - if err != nil { - return nil, err - } - - if ver != "1.1" && ver != "1.2" && ver != "1.3" { - return nil, ErrUnsupportedProtocolVersion - } - - switch typ { - case "chrome", "chromium", "microsoft edge", "safari", "": - x := new(Chrome) - if buf != nil { - if err = easyjson.Unmarshal(buf, x); err != nil { - return nil, err - } - } - - return x, nil - } - - return nil, ErrUnsupportedProtocolType -} - -// NewPageTargetWithURL creates a new page target with the specified url. -func (c *Client) NewPageTargetWithURL(ctxt context.Context, urlstr string) (Target, error) { - var err error - - t, err := c.newTarget(ctxt, nil) - if err != nil { - return nil, err - } - - u := "new" - if urlstr != "" { - u += "?" + urlstr - } - - if err = c.doReq(ctxt, u, t); err != nil { - return nil, err - } - - return t, nil -} - -// NewPageTarget creates a new page target. -func (c *Client) NewPageTarget(ctxt context.Context) (Target, error) { - return c.NewPageTargetWithURL(ctxt, "") -} - -// ActivateTarget activates a target. -func (c *Client) ActivateTarget(ctxt context.Context, t Target) error { - return c.doReq(ctxt, "activate/"+t.GetID(), nil) -} - -// CloseTarget activates a target. -func (c *Client) CloseTarget(ctxt context.Context, t Target) error { - return c.doReq(ctxt, "close/"+t.GetID(), nil) -} - -// VersionInfo returns information about the remote debugging protocol. -func (c *Client) VersionInfo(ctxt context.Context) (map[string]string, error) { - v := make(map[string]string) - if err := c.doReq(ctxt, "version", &v); err != nil { - return nil, err - } - return v, nil -} - -// WatchPageTargets watches for new page targets. -func (c *Client) WatchPageTargets(ctxt context.Context) <-chan Target { - ch := make(chan Target) - go func() { - defer close(ch) - - encountered := make(map[string]bool) - check := func() error { - targets, err := c.ListPageTargets(ctxt) - if err != nil { - return err - } - - for _, t := range targets { - if !encountered[t.GetID()] { - ch <- t - } - encountered[t.GetID()] = true - } - return nil - } - - var err error - lastGood := time.Now() - for { - err = check() - if err == nil { - lastGood = time.Now() - } else if time.Now().After(lastGood.Add(c.timeout)) { - return - } - - select { - case <-time.After(c.check): - continue - - case <-ctxt.Done(): - return - } - } - }() - - return ch -} - -// Option is a Chrome DevTools Protocol client option. -type Option func(*Client) - -// URL is a client option to specify the remote Chrome DevTools Protocol -// instance to connect to. -func URL(urlstr string) Option { - return func(c *Client) { - // since chrome 66+, dev tools requires the host name to be either an - // IP address, or "localhost" - if strings.HasPrefix(strings.ToLower(urlstr), "http://") { - host, port, path := urlstr[7:], "", "" - if i := strings.Index(host, "/"); i != -1 { - host, path = host[:i], host[i:] - } - if i := strings.Index(host, ":"); i != -1 { - host, port = host[:i], host[i:] - } - if addr, err := net.ResolveIPAddr("ip", host); err == nil { - urlstr = "http://" + addr.IP.String() + port + path - } - } - c.url = urlstr - } -} - -// WatchInterval is a client option that specifies the check interval duration. -func WatchInterval(check time.Duration) Option { - return func(c *Client) { - c.check = check - } -} - -// WatchTimeout is a client option that specifies the watch timeout duration. -func WatchTimeout(timeout time.Duration) Option { - return func(c *Client) { - c.timeout = timeout - } -} diff --git a/client/easyjson.go b/client/easyjson.go deleted file mode 100644 index a0af6d7..0000000 --- a/client/easyjson.go +++ /dev/null @@ -1,174 +0,0 @@ -// Code generated by easyjson for marshaling/unmarshaling. DO NOT EDIT. - -package client - -import ( - json "encoding/json" - easyjson "github.com/mailru/easyjson" - jlexer "github.com/mailru/easyjson/jlexer" - jwriter "github.com/mailru/easyjson/jwriter" -) - -// suppress unused package warning -var ( - _ *json.RawMessage - _ *jlexer.Lexer - _ *jwriter.Writer - _ easyjson.Marshaler -) - -func easyjsonC5a4559bDecodeGithubComChromedpChromedpClient(in *jlexer.Lexer, out *Chrome) { - isTopLevel := in.IsStart() - if in.IsNull() { - if isTopLevel { - in.Consumed() - } - in.Skip() - return - } - in.Delim('{') - for !in.IsDelim('}') { - key := in.UnsafeString() - in.WantColon() - if in.IsNull() { - in.Skip() - in.WantComma() - continue - } - switch key { - case "description": - out.Description = string(in.String()) - case "devtoolsFrontendUrl": - out.DevtoolsURL = string(in.String()) - case "id": - out.ID = string(in.String()) - case "title": - out.Title = string(in.String()) - case "type": - (out.Type).UnmarshalEasyJSON(in) - case "url": - out.URL = string(in.String()) - case "webSocketDebuggerUrl": - out.WebsocketURL = string(in.String()) - case "faviconURL": - out.FaviconURL = string(in.String()) - default: - in.SkipRecursive() - } - in.WantComma() - } - in.Delim('}') - if isTopLevel { - in.Consumed() - } -} -func easyjsonC5a4559bEncodeGithubComChromedpChromedpClient(out *jwriter.Writer, in Chrome) { - out.RawByte('{') - first := true - _ = first - if in.Description != "" { - const prefix string = ",\"description\":" - if first { - first = false - out.RawString(prefix[1:]) - } else { - out.RawString(prefix) - } - out.String(string(in.Description)) - } - if in.DevtoolsURL != "" { - const prefix string = ",\"devtoolsFrontendUrl\":" - if first { - first = false - out.RawString(prefix[1:]) - } else { - out.RawString(prefix) - } - out.String(string(in.DevtoolsURL)) - } - if in.ID != "" { - const prefix string = ",\"id\":" - if first { - first = false - out.RawString(prefix[1:]) - } else { - out.RawString(prefix) - } - out.String(string(in.ID)) - } - if in.Title != "" { - const prefix string = ",\"title\":" - if first { - first = false - out.RawString(prefix[1:]) - } else { - out.RawString(prefix) - } - out.String(string(in.Title)) - } - if in.Type != "" { - const prefix string = ",\"type\":" - if first { - first = false - out.RawString(prefix[1:]) - } else { - out.RawString(prefix) - } - (in.Type).MarshalEasyJSON(out) - } - if in.URL != "" { - const prefix string = ",\"url\":" - if first { - first = false - out.RawString(prefix[1:]) - } else { - out.RawString(prefix) - } - out.String(string(in.URL)) - } - if in.WebsocketURL != "" { - const prefix string = ",\"webSocketDebuggerUrl\":" - if first { - first = false - out.RawString(prefix[1:]) - } else { - out.RawString(prefix) - } - out.String(string(in.WebsocketURL)) - } - if in.FaviconURL != "" { - const prefix string = ",\"faviconURL\":" - if first { - first = false - out.RawString(prefix[1:]) - } else { - out.RawString(prefix) - } - out.String(string(in.FaviconURL)) - } - out.RawByte('}') -} - -// MarshalJSON supports json.Marshaler interface -func (v Chrome) MarshalJSON() ([]byte, error) { - w := jwriter.Writer{} - easyjsonC5a4559bEncodeGithubComChromedpChromedpClient(&w, v) - return w.Buffer.BuildBytes(), w.Error -} - -// MarshalEasyJSON supports easyjson.Marshaler interface -func (v Chrome) MarshalEasyJSON(w *jwriter.Writer) { - easyjsonC5a4559bEncodeGithubComChromedpChromedpClient(w, v) -} - -// UnmarshalJSON supports json.Unmarshaler interface -func (v *Chrome) UnmarshalJSON(data []byte) error { - r := jlexer.Lexer{Data: data} - easyjsonC5a4559bDecodeGithubComChromedpChromedpClient(&r, v) - return r.Error() -} - -// UnmarshalEasyJSON supports easyjson.Unmarshaler interface -func (v *Chrome) UnmarshalEasyJSON(l *jlexer.Lexer) { - easyjsonC5a4559bDecodeGithubComChromedpChromedpClient(l, v) -} diff --git a/client/gen.go b/client/gen.go deleted file mode 100644 index 4cad52e..0000000 --- a/client/gen.go +++ /dev/null @@ -1,145 +0,0 @@ -// +build ignore - -package main - -import ( - "encoding/base64" - "flag" - "fmt" - "io/ioutil" - "log" - "net/http" - "os/exec" - "regexp" - "sort" - - "github.com/knq/snaker" -) - -const ( - // chromiumSrc is the base chromium source repo location - chromiumSrc = "https://chromium.googlesource.com/chromium/src" - - // devtoolsHTTPClientCc contains the target_type names. - devtoolsHTTPClientCc = chromiumSrc + "/+/master/chrome/test/chromedriver/chrome/devtools_http_client.cc?format=TEXT" -) - -var ( - flagOut = flag.String("out", "targettype.go", "out file") -) - -func main() { - flag.Parse() - - if err := run(); err != nil { - log.Fatal(err) - } -} - -var typeAsStringRE = regexp.MustCompile(`type_as_string\s+==\s+"([^"]+)"`) - -// run executes the generator. -func run() error { - var err error - - // grab source - buf, err := grab(devtoolsHTTPClientCc) - if err != nil { - return err - } - - // find names - matches := typeAsStringRE.FindAllStringSubmatch(string(buf), -1) - names := make([]string, len(matches)) - for i, m := range matches { - names[i] = m[1] - } - sort.Strings(names) - - // process names - var constVals, decodeVals string - for _, n := range names { - name := snaker.SnakeToCamelIdentifier(n) - constVals += fmt.Sprintf("%s TargetType = \"%s\"\n", name, n) - decodeVals += fmt.Sprintf("case %s:\n*tt=%s\n", name, name) - } - - if err = ioutil.WriteFile(*flagOut, []byte(fmt.Sprintf(targetTypeSrc, constVals, decodeVals)), 0644); err != nil { - return err - } - - return exec.Command("gofmt", "-w", "-s", *flagOut).Run() -} - -// grab retrieves a file from the chromium source code. -func grab(path string) ([]byte, error) { - res, err := http.Get(path) - if err != nil { - return nil, err - } - defer res.Body.Close() - - body, err := ioutil.ReadAll(res.Body) - if err != nil { - return nil, err - } - - buf, err := base64.StdEncoding.DecodeString(string(body)) - if err != nil { - return nil, err - } - - return buf, nil -} - -const ( - targetTypeSrc = `package client - -// Code generated by gen.go. DO NOT EDIT. - -import ( - easyjson "github.com/mailru/easyjson" - jlexer "github.com/mailru/easyjson/jlexer" - jwriter "github.com/mailru/easyjson/jwriter" -) - -// TargetType are the types of targets available in Chrome. -type TargetType string - -// TargetType values. -const ( -%s -) - -// String satisfies stringer. -func (tt TargetType) String() string { - return string(tt) -} - -// MarshalEasyJSON satisfies easyjson.Marshaler. -func (tt TargetType) MarshalEasyJSON(out *jwriter.Writer) { - out.String(string(tt)) -} - -// MarshalJSON satisfies json.Marshaler. -func (tt TargetType) MarshalJSON() ([]byte, error) { - return easyjson.Marshal(tt) -} - -// UnmarshalEasyJSON satisfies easyjson.Unmarshaler. -func (tt *TargetType) UnmarshalEasyJSON(in *jlexer.Lexer) { - z := TargetType(in.String()) - switch z { -%s - - default: - *tt = z - } -} - -// UnmarshalJSON satisfies json.Unmarshaler. -func (tt *TargetType) UnmarshalJSON(buf []byte) error { - return easyjson.Unmarshal(buf, tt) -} -` -) diff --git a/client/targettype.go b/client/targettype.go deleted file mode 100644 index 50e918e..0000000 --- a/client/targettype.go +++ /dev/null @@ -1,79 +0,0 @@ -package client - -// Code generated by gen.go. DO NOT EDIT. - -import ( - easyjson "github.com/mailru/easyjson" - jlexer "github.com/mailru/easyjson/jlexer" - jwriter "github.com/mailru/easyjson/jwriter" -) - -// TargetType are the types of targets available in Chrome. -type TargetType string - -// TargetType values. -const ( - App TargetType = "app" - BackgroundPage TargetType = "background_page" - Browser TargetType = "browser" - External TargetType = "external" - Iframe TargetType = "iframe" - Other TargetType = "other" - Page TargetType = "page" - ServiceWorker TargetType = "service_worker" - SharedWorker TargetType = "shared_worker" - Webview TargetType = "webview" - Worker TargetType = "worker" -) - -// String satisfies stringer. -func (tt TargetType) String() string { - return string(tt) -} - -// MarshalEasyJSON satisfies easyjson.Marshaler. -func (tt TargetType) MarshalEasyJSON(out *jwriter.Writer) { - out.String(string(tt)) -} - -// MarshalJSON satisfies json.Marshaler. -func (tt TargetType) MarshalJSON() ([]byte, error) { - return easyjson.Marshal(tt) -} - -// UnmarshalEasyJSON satisfies easyjson.Unmarshaler. -func (tt *TargetType) UnmarshalEasyJSON(in *jlexer.Lexer) { - z := TargetType(in.String()) - switch z { - case App: - *tt = App - case BackgroundPage: - *tt = BackgroundPage - case Browser: - *tt = Browser - case External: - *tt = External - case Iframe: - *tt = Iframe - case Other: - *tt = Other - case Page: - *tt = Page - case ServiceWorker: - *tt = ServiceWorker - case SharedWorker: - *tt = SharedWorker - case Webview: - *tt = Webview - case Worker: - *tt = Worker - - default: - *tt = z - } -} - -// UnmarshalJSON satisfies json.Unmarshaler. -func (tt *TargetType) UnmarshalJSON(buf []byte) error { - return easyjson.Unmarshal(buf, tt) -} diff --git a/client/transport.go b/conn.go similarity index 52% rename from client/transport.go rename to conn.go index 50cd15d..8274b0c 100644 --- a/client/transport.go +++ b/conn.go @@ -1,7 +1,9 @@ -package client +package chromedp import ( "io" + "net" + "strings" "github.com/gorilla/websocket" ) @@ -26,7 +28,7 @@ type Conn struct { *websocket.Conn } -// Read reads the next websocket message. +// Read reads the next message. func (c *Conn) Read() ([]byte, error) { _, buf, err := c.ReadMessage() if err != nil { @@ -35,25 +37,18 @@ func (c *Conn) Read() ([]byte, error) { return buf, nil } -// Write writes a websocket message. +// Write writes a message. func (c *Conn) Write(buf []byte) error { return c.WriteMessage(websocket.TextMessage, buf) } -// Dial dials the specified target's websocket URL. -// -// Note: uses gorilla/websocket. -func Dial(urlstr string, opts ...DialOption) (Transport, error) { +// Dial dials the specified websocket URL using gorilla/websocket. +func Dial(urlstr string) (*Conn, error) { d := &websocket.Dialer{ ReadBufferSize: DefaultReadBufferSize, WriteBufferSize: DefaultWriteBufferSize, } - // apply opts - for _, o := range opts { - o(d) - } - // connect conn, _, err := d.Dial(urlstr, nil) if err != nil { @@ -63,5 +58,23 @@ func Dial(urlstr string, opts ...DialOption) (Transport, error) { return &Conn{conn}, nil } -// DialOption is a dial option. -type DialOption func(*websocket.Dialer) +// ForceIP forces the host component in urlstr to be an IP address. +// +// Since Chrome 66+, Chrome DevTools Protocol clients connecting to a browser +// must send the "Host:" header as either an IP address, or "localhost". +func ForceIP(urlstr string) string { + if i := strings.Index(urlstr, "://"); i != -1 { + scheme := urlstr[:i+3] + host, port, path := urlstr[len(scheme)+3:], "", "" + if i := strings.Index(host, "/"); i != -1 { + host, path = host[:i], host[i:] + } + if i := strings.Index(host, ":"); i != -1 { + host, port = host[:i], host[i:] + } + if addr, err := net.ResolveIPAddr("ip", host); err == nil { + urlstr = scheme + addr.IP.String() + port + path + } + } + return urlstr +} diff --git a/context.go b/context.go new file mode 100644 index 0000000..0a2531c --- /dev/null +++ b/context.go @@ -0,0 +1,115 @@ +package chromedp + +import ( + "context" + "encoding/json" + "net/http" +) + +// Executor +type Executor interface { + Execute(context.Context, string, json.Marshaler, json.Unmarshaler) error +} + +// Context +type Context struct { + Allocator Allocator + + browser *Browser + handler *TargetHandler + + logf func(string, ...interface{}) + errf func(string, ...interface{}) +} + +// 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.handler == nil { + if err := c.newHandler(ctx); err != nil { + return err + } + } + return action.Do(ctx, c.handler) +} + +func (c *Context) newHandler(ctx context.Context) error { + // TODO: add RemoteAddr() to the Transport interface? + conn := c.browser.conn.(*Conn).Conn + addr := conn.RemoteAddr() + url := "http://" + addr.String() + "/json/new" + resp, err := http.Get(url) + if err != nil { + return err + } + defer resp.Body.Close() + var wurl withWebsocketURL + if err := json.NewDecoder(resp.Body).Decode(&wurl); err != nil { + return err + } + c.handler, err = NewTargetHandler(wurl.WebsocketURL) + if err != nil { + return err + } + if err := c.handler.Run(ctx); err != nil { + return err + } + return nil +} + +type withWebsocketURL struct { + WebsocketURL string `json:"webSocketDebuggerUrl"` +} + +// ContextOption +type ContextOption func(*Context) diff --git a/contrib/meta.sh b/contrib/meta.sh deleted file mode 100755 index 338ff82..0000000 --- a/contrib/meta.sh +++ /dev/null @@ -1,34 +0,0 @@ -#!/bin/bash - -SRC=$(realpath $(cd -P "$( dirname "${BASH_SOURCE[0]}" )" && pwd )/../) - -pushd $SRC &> /dev/null - -gometalinter \ - --disable=aligncheck \ - --enable=misspell \ - --enable=gofmt \ - --deadline=100s \ - --cyclo-over=25 \ - --sort=path \ - --exclude='\(defer (.+?)\)\) \(errcheck\)$' \ - --exclude='/easyjson\.go.*(passes|copies) lock' \ - --exclude='/easyjson\.go.*ineffectual assignment' \ - --exclude='/easyjson\.go.*unnecessary conversion' \ - --exclude='/easyjson\.go.*this value of key is never used' \ - --exclude='/easyjson\.go.*\((gocyclo|golint|goconst|staticcheck)\)$' \ - --exclude='^cdp/.*Potential hardcoded credentials' \ - --exclude='^cdp/cdp\.go.*UnmarshalEasyJSON.*\(gocyclo\)$' \ - --exclude='^cdp/cdputil/cdputil\.go.*UnmarshalMessage.*\(gocyclo\)$' \ - --exclude='^cmd/chromedp-gen/.*\((gocyclo|interfacer)\)$' \ - --exclude='^cmd/chromedp-proxy/main\.go.*\(gas\)$' \ - --exclude='^cmd/chromedp-gen/fixup/fixup\.go.*\(goconst\)$' \ - --exclude='^cmd/chromedp-gen/internal/enum\.go.*unreachable' \ - --exclude='^cmd/chromedp-gen/(main|domain-gen)\.go.*\(gas\)$' \ - --exclude='^examples/[a-z]+/main\.go.*\(errcheck\)$' \ - --exclude='^kb/gen\.go.*\((gas|vet)\)$' \ - --exclude='^runner/.*\(gas\)$' \ - --exclude='^handler\.go.*cmd can be easyjson\.Marshaler' \ - ./... - -popd &> /dev/null diff --git a/contrib/start.sh b/contrib/start.sh deleted file mode 100755 index 2640e97..0000000 --- a/contrib/start.sh +++ /dev/null @@ -1,10 +0,0 @@ -#!/bin/bash - -TMP=$(mktemp -d /tmp/google-chrome.XXXXX) - -google-chrome \ - --user-data-dir=$TMP \ - --remote-debugging-port=9222 \ - --no-first-run \ - --no-default-browser-check \ - about:blank diff --git a/contrib/stats.sh b/contrib/stats.sh deleted file mode 100755 index eaa6627..0000000 --- a/contrib/stats.sh +++ /dev/null @@ -1,14 +0,0 @@ -#!/bin/bash - -BASE=$(realpath $(cd -P $GOPATH/src/github.com/chromedp && pwd)) - -FILES=$(find $BASE/{chromedp*,goquery,examples} -type f -iname \*.go -not -iname \*.qtpl.go -print0|wc -l --files0-from=-|head -n -1)$'\n' - -AUTOG=$(find $BASE/cdproto/ -type f -iname \*.go -not -iname \*easyjson\* -print0|wc -l --files0-from=-|head -n -1) - -if [ "$1" != "--total" ]; then - echo -e "code:\n$FILES\n\ngenerated:\n$AUTOG" -else - echo "code: $(awk '{s+=$1} END {print s}' <<< "$FILES")" - echo "generated: $(awk '{s+=$1} END {print s}' <<< "$AUTOG")" -fi diff --git a/errors.go b/errors.go index 44e1179..b4bcb9b 100644 --- a/errors.go +++ b/errors.go @@ -39,4 +39,7 @@ const ( // ErrInvalidHandler is the invalid handler error. ErrInvalidHandler Error = "invalid handler" + + // ErrInvalidContext is the invalid context error. + ErrInvalidContext Error = "invalid context" ) diff --git a/handler.go b/handler.go index b3545eb..668bffc 100644 --- a/handler.go +++ b/handler.go @@ -20,13 +20,11 @@ import ( "github.com/chromedp/cdproto/log" "github.com/chromedp/cdproto/page" "github.com/chromedp/cdproto/runtime" - - "github.com/chromedp/chromedp/client" ) // TargetHandler manages a Chrome DevTools Protocol target. type TargetHandler struct { - conn client.Transport + conn Transport // frames is the set of encountered frames. frames map[cdp.FrameID]*cdp.Frame @@ -63,18 +61,22 @@ type TargetHandler struct { } // NewTargetHandler creates a new handler for the specified client target. -func NewTargetHandler(t client.Target, logf, debugf, errf func(string, ...interface{})) (*TargetHandler, error) { - conn, err := client.Dial(t.GetWebsocketURL()) +func NewTargetHandler(urlstr string, opts ...TargetHandlerOption) (*TargetHandler, error) { + conn, err := Dial(urlstr) if err != nil { return nil, err } - return &TargetHandler{ - conn: conn, - logf: logf, - debugf: debugf, - errf: errf, - }, nil + h := &TargetHandler{ + conn: conn, + errf: func(string, ...interface{}) {}, + } + + for _, o := range opts { + o(h) + } + + return h, nil } // Run starts the processing of commands and events of the client target @@ -217,12 +219,11 @@ func (h *TargetHandler) read() (*cdproto.Message, error) { return nil, err } - h.debugf("-> %s", string(buf)) + //h.debugf("-> %s", string(buf)) // unmarshal msg := new(cdproto.Message) - err = json.Unmarshal(buf, msg) - if err != nil { + if err := json.Unmarshal(buf, msg); err != nil { return nil, err } @@ -332,7 +333,7 @@ func (h *TargetHandler) processCommand(cmd *cdproto.Message) error { return err } - h.debugf("<- %s", string(buf)) + //h.debugf("<- %s", string(buf)) return h.conn.Write(buf) } @@ -442,8 +443,6 @@ func (h *TargetHandler) GetRoot(ctxt context.Context) (*cdp.Node, error) { // SetActive sets the currently active frame after a successful navigation. func (h *TargetHandler) SetActive(ctxt context.Context, id cdp.FrameID) error { - var err error - // get frame f, err := h.WaitFrame(ctxt, id) if err != nil { @@ -461,7 +460,7 @@ func (h *TargetHandler) SetActive(ctxt context.Context, id cdp.FrameID) error { // WaitFrame waits for a frame to be loaded using the provided context. func (h *TargetHandler) WaitFrame(ctxt context.Context, id cdp.FrameID) (*cdp.Frame, error) { // TODO: fix this - timeout := time.After(10 * time.Second) + timeout := time.After(time.Second) for { select { @@ -495,7 +494,7 @@ func (h *TargetHandler) WaitFrame(ctxt context.Context, id cdp.FrameID) (*cdp.Fr // WaitNode waits for a node to be loaded using the provided context. func (h *TargetHandler) WaitNode(ctxt context.Context, f *cdp.Frame, id cdp.NodeID) (*cdp.Node, error) { // TODO: fix this - timeout := time.After(10 * time.Second) + timeout := time.After(time.Second) for { select { @@ -672,3 +671,5 @@ func (h *TargetHandler) domEvent(ctxt context.Context, ev interface{}) { op(n) } + +type TargetHandlerOption func(*TargetHandler) diff --git a/input.go b/input.go index aa58a86..b14e160 100644 --- a/input.go +++ b/input.go @@ -58,10 +58,8 @@ func MouseClickXY(x, y int64, opts ...MouseOption) Action { // viewport. func MouseClickNode(n *cdp.Node, opts ...MouseOption) Action { return ActionFunc(func(ctxt context.Context, h cdp.Executor) error { - var err error - var pos []int - err = EvaluateAsDevTools(fmt.Sprintf(scrollIntoViewJS, n.FullXPath()), &pos).Do(ctxt, h) + err := EvaluateAsDevTools(fmt.Sprintf(scrollIntoViewJS, n.FullXPath()), &pos).Do(ctxt, h) if err != nil { return err } @@ -154,12 +152,9 @@ func ClickCount(n int) MouseOption { // of well-known keys. func KeyAction(keys string, opts ...KeyOption) Action { return ActionFunc(func(ctxt context.Context, h cdp.Executor) error { - var err error - for _, r := range keys { for _, k := range kb.Encode(r) { - err = k.Do(ctxt, h) - if err != nil { + if err := k.Do(ctxt, h); err != nil { return err } } diff --git a/input_test.go b/input_test.go index 0f4c15e..3793afe 100644 --- a/input_test.go +++ b/input_test.go @@ -23,13 +23,9 @@ const ( func TestMouseClickXY(t *testing.T) { t.Parallel() - var err error - - c := testAllocate(t, "input.html") - defer c.Release() - - err = c.Run(defaultContext, Sleep(100*time.Millisecond)) - if err != nil { + ctx, cancel := testAllocate(t, "input.html") + defer cancel() + if err := Run(ctx, Sleep(100*time.Millisecond)); err != nil { t.Fatal(err) } @@ -43,18 +39,17 @@ func TestMouseClickXY(t *testing.T) { } for i, test := range tests { - err = c.Run(defaultContext, MouseClickXY(test.x, test.y)) - if err != nil { + if err := Run(ctx, MouseClickXY(test.x, test.y)); err != nil { t.Fatalf("test %d got error: %v", i, err) } time.Sleep(50 * time.Millisecond) var xstr, ystr string - err = c.Run(defaultContext, Value("#input1", &xstr, ByID)) - if err != nil { + if err := Run(ctx, Value("#input1", &xstr, ByID)); err != nil { t.Fatalf("test %d got error: %v", i, err) } + x, err := strconv.ParseInt(xstr, 10, 64) if err != nil { t.Fatalf("test %d got error: %v", i, err) @@ -62,11 +57,10 @@ func TestMouseClickXY(t *testing.T) { if x != test.x { t.Fatalf("test %d expected x to be: %d, got: %d", i, test.x, x) } - - err = c.Run(defaultContext, Value("#input2", &ystr, ByID)) - if err != nil { + if err := Run(ctx, Value("#input2", &ystr, ByID)); err != nil { t.Fatalf("test %d got error: %v", i, err) } + y, err := strconv.ParseInt(ystr, 10, 64) if err != nil { t.Fatalf("test %d got error: %v", i, err) @@ -97,31 +91,28 @@ func TestMouseClickNode(t *testing.T) { t.Run(fmt.Sprintf("test %d", i), func(t *testing.T) { t.Parallel() - c := testAllocate(t, "input.html") - defer c.Release() + ctx, cancel := testAllocate(t, "input.html") + defer cancel() - var err error var nodes []*cdp.Node - err = c.Run(defaultContext, Nodes(test.sel, &nodes, test.by)) - if err != nil { + if err := Run(ctx, Nodes(test.sel, &nodes, test.by)); err != nil { t.Fatalf("got error: %v", err) } + if len(nodes) != 1 { t.Fatalf("expected nodes to have exactly 1 element, got: %d", len(nodes)) } - - err = c.Run(defaultContext, MouseClickNode(nodes[0], test.opt)) - if err != nil { + if err := Run(ctx, MouseClickNode(nodes[0], test.opt)); err != nil { t.Fatalf("got error: %v", err) } time.Sleep(50 * time.Millisecond) var value string - err = c.Run(defaultContext, Value("#input3", &value, ByID)) - if err != nil { + if err := Run(ctx, Value("#input3", &value, ByID)); err != nil { t.Fatalf("got error: %v", err) } + if value != test.exp { t.Fatalf("expected to have value %s, got: %s", test.exp, value) } @@ -146,42 +137,41 @@ func TestMouseClickOffscreenNode(t *testing.T) { t.Run(fmt.Sprintf("test %d", i), func(t *testing.T) { t.Parallel() - c := testAllocate(t, "input.html") - defer c.Release() + ctx, cancel := testAllocate(t, "input.html") + defer cancel() - var err error var nodes []*cdp.Node - err = c.Run(defaultContext, Nodes(test.sel, &nodes, test.by)) - if err != nil { + if err := Run(ctx, Nodes(test.sel, &nodes, test.by)); err != nil { t.Fatalf("got error: %v", err) } + if len(nodes) != 1 { t.Fatalf("expected nodes to have exactly 1 element, got: %d", len(nodes)) } var ok bool - err = c.Run(defaultContext, EvaluateAsDevTools(fmt.Sprintf(inViewportJS, nodes[0].FullXPath()), &ok)) - if err != nil { + if err := Run(ctx, EvaluateAsDevTools(fmt.Sprintf(inViewportJS, nodes[0].FullXPath()), &ok)); err != nil { t.Fatalf("got error: %v", err) } + if ok { t.Fatal("expected node to be offscreen") } for i := test.exp; i > 0; i-- { - err = c.Run(defaultContext, MouseClickNode(nodes[0])) - if err != nil { + if err := Run(ctx, MouseClickNode(nodes[0])); err != nil { t.Fatalf("got error: %v", err) } + } time.Sleep(100 * time.Millisecond) var value int - err = c.Run(defaultContext, Evaluate("window.document.test_i", &value)) - if err != nil { + if err := Run(ctx, Evaluate("window.document.test_i", &value)); err != nil { t.Fatalf("got error: %v", err) } + if value != test.exp { t.Fatalf("expected to have value %d, got: %d", test.exp, value) } @@ -208,34 +198,29 @@ func TestKeyAction(t *testing.T) { t.Run(fmt.Sprintf("test %d", i), func(t *testing.T) { t.Parallel() - c := testAllocate(t, "input.html") - defer c.Release() + ctx, cancel := testAllocate(t, "input.html") + defer cancel() - var err error var nodes []*cdp.Node - err = c.Run(defaultContext, Nodes(test.sel, &nodes, test.by)) - if err != nil { + if err := Run(ctx, Nodes(test.sel, &nodes, test.by)); err != nil { t.Fatalf("got error: %v", err) } + if len(nodes) != 1 { t.Fatalf("expected nodes to have exactly 1 element, got: %d", len(nodes)) } - - err = c.Run(defaultContext, Focus(test.sel, test.by)) - if err != nil { + if err := Run(ctx, Focus(test.sel, test.by)); err != nil { t.Fatalf("got error: %v", err) } - - err = c.Run(defaultContext, KeyAction(test.exp)) - if err != nil { + if err := Run(ctx, KeyAction(test.exp)); err != nil { t.Fatalf("got error: %v", err) } var value string - err = c.Run(defaultContext, Value(test.sel, &value, test.by)) - if err != nil { + if err := Run(ctx, Value(test.sel, &value, test.by)); err != nil { t.Fatalf("got error: %v", err) } + if value != test.exp { t.Fatalf("expected to have value %s, got: %s", test.exp, value) } @@ -262,29 +247,26 @@ func TestKeyActionNode(t *testing.T) { t.Run(fmt.Sprintf("test %d", i), func(t *testing.T) { t.Parallel() - c := testAllocate(t, "input.html") - defer c.Release() + ctx, cancel := testAllocate(t, "input.html") + defer cancel() - var err error var nodes []*cdp.Node - err = c.Run(defaultContext, Nodes(test.sel, &nodes, test.by)) - if err != nil { + if err := Run(ctx, Nodes(test.sel, &nodes, test.by)); err != nil { t.Fatalf("got error: %v", err) } + if len(nodes) != 1 { t.Fatalf("expected nodes to have exactly 1 element, got: %d", len(nodes)) } - - err = c.Run(defaultContext, KeyActionNode(nodes[0], test.exp)) - if err != nil { + if err := Run(ctx, KeyActionNode(nodes[0], test.exp)); err != nil { t.Fatalf("got error: %v", err) } var value string - err = c.Run(defaultContext, Value(test.sel, &value, test.by)) - if err != nil { + if err := Run(ctx, Value(test.sel, &value, test.by)); err != nil { t.Fatalf("got error: %v", err) } + if value != test.exp { t.Fatalf("expected to have value %s, got: %s", test.exp, value) } diff --git a/kb/gen.go b/kb/gen.go index 7b799cc..1a94997 100644 --- a/kb/gen.go +++ b/kb/gen.go @@ -72,8 +72,6 @@ func main() { } func run() error { - var err error - // special characters keys := map[rune]kb.Key{ '\b': {"Backspace", "Backspace", "", "", int64('\b'), int64('\b'), false, false}, @@ -82,8 +80,7 @@ func run() error { } // load keys - err = loadKeys(keys) - if err != nil { + if err := loadKeys(keys); err != nil { return err } @@ -94,24 +91,19 @@ func run() error { } // output - err = ioutil.WriteFile( - *flagOut, + if err := ioutil.WriteFile(*flagOut, []byte(fmt.Sprintf(hdr, *flagPkg, string(constBuf), string(mapBuf))), - 0644, - ) - if err != nil { + 0644); err != nil { return err } // format - err = exec.Command("goimports", "-w", *flagOut).Run() - if err != nil { + if err := exec.Command("goimports", "-w", *flagOut).Run(); err != nil { return err } // format - err = exec.Command("gofmt", "-s", "-w", *flagOut).Run() - if err != nil { + if err := exec.Command("gofmt", "-s", "-w", *flagOut).Run(); err != nil { return err } @@ -120,8 +112,6 @@ func run() error { // loadKeys loads the dom key definitions from the chromium source tree. func loadKeys(keys map[rune]kb.Key) error { - var err error - // load key converter data keycodeConverterMap, err := loadKeycodeConverterData() if err != nil { @@ -444,8 +434,6 @@ var defineRE = regexp.MustCompile(`(?m)^#define\s+(.+?)\s+([0-9A-Fx]+)`) // loadPosixWinKeyboardCodes loads the native and windows keyboard scan codes // mapped to the DOM key. func loadPosixWinKeyboardCodes() (map[string][]int64, error) { - var err error - lookup := map[string]string{ // mac alias "VKEY_LWIN": "0x5B", diff --git a/nav_test.go b/nav_test.go index 325a75f..5a978fb 100644 --- a/nav_test.go +++ b/nav_test.go @@ -11,37 +11,31 @@ import ( func TestNavigate(t *testing.T) { t.Parallel() - var err error - - c := testAllocate(t, "") - defer c.Release() + ctx, cancel := testAllocate(t, "") + defer cancel() expurl, exptitle := testdataDir+"/image.html", "this is title" - - err = c.Run(defaultContext, Navigate(expurl)) - if err != nil { + if err := Run(ctx, Navigate(expurl)); err != nil { t.Fatal(err) } - - err = c.Run(defaultContext, WaitVisible(`#icon-brankas`, ByID)) - if err != nil { + if err := Run(ctx, WaitVisible(`#icon-brankas`, ByID)); err != nil { t.Fatal(err) } var urlstr string - err = c.Run(defaultContext, Location(&urlstr)) - if err != nil { + if err := Run(ctx, Location(&urlstr)); err != nil { t.Fatal(err) } + if !strings.HasPrefix(urlstr, expurl) { t.Errorf("expected to be on image.html, at: %s", urlstr) } var title string - err = c.Run(defaultContext, Title(&title)) - if err != nil { + if err := Run(ctx, Title(&title)); err != nil { t.Fatal(err) } + if title != exptitle { t.Errorf("expected title to contain google, instead title is: %s", title) } @@ -50,10 +44,9 @@ func TestNavigate(t *testing.T) { func TestNavigationEntries(t *testing.T) { t.Parallel() - var err error - - c := testAllocate(t, "") - defer c.Release() + ctx, cancel := testAllocate(t, "") + defer cancel() + time.Sleep(50 * time.Millisecond) tests := []string{ "form.html", @@ -62,38 +55,33 @@ func TestNavigationEntries(t *testing.T) { var entries []*page.NavigationEntry var index int64 - - err = c.Run(defaultContext, NavigationEntries(&index, &entries)) - if err != nil { + if err := Run(ctx, NavigationEntries(&index, &entries)); err != nil { t.Fatal(err) } - if len(entries) != 1 { - t.Errorf("expected to have 1 navigation entry: got %d", len(entries)) + if len(entries) != 2 { + t.Errorf("expected to have 2 navigation entry: got %d", len(entries)) } - if index != 0 { - t.Errorf("expected navigation index is 0, got: %d", index) + if index != 1 { + t.Errorf("expected navigation index is 1, got: %d", index) } - expIdx, expEntries := 1, 2 + expIdx, expEntries := 2, 3 for i, url := range tests { - err = c.Run(defaultContext, Navigate(testdataDir+"/"+url)) - if err != nil { + if err := Run(ctx, Navigate(testdataDir+"/"+url)); err != nil { t.Fatal(err) } time.Sleep(50 * time.Millisecond) - - err = c.Run(defaultContext, NavigationEntries(&index, &entries)) - if err != nil { + if err := Run(ctx, NavigationEntries(&index, &entries)); err != nil { t.Fatal(err) } if len(entries) != expEntries { t.Errorf("test %d expected to have %d navigation entry: got %d", i, expEntries, len(entries)) } - if index != int64(i+1) { - t.Errorf("test %d expected navigation index is %d, got: %d", i, i, index) + if want := int64(i + 2); index != want { + t.Errorf("test %d expected navigation index is %d, got: %d", i, want, index) } expIdx++ @@ -104,44 +92,36 @@ func TestNavigationEntries(t *testing.T) { func TestNavigateToHistoryEntry(t *testing.T) { t.Parallel() - var err error - - c := testAllocate(t, "") - defer c.Release() + ctx, cancel := testAllocate(t, "") + defer cancel() var entries []*page.NavigationEntry var index int64 - err = c.Run(defaultContext, Navigate(testdataDir+"/image.html")) - if err != nil { + if err := Run(ctx, Navigate(testdataDir+"/image.html")); err != nil { t.Fatal(err) } time.Sleep(50 * time.Millisecond) - - err = c.Run(defaultContext, NavigationEntries(&index, &entries)) - if err != nil { + if err := Run(ctx, NavigationEntries(&index, &entries)); err != nil { t.Fatal(err) } - err = c.Run(defaultContext, Navigate(testdataDir+"/form.html")) - if err != nil { + if err := Run(ctx, Navigate(testdataDir+"/form.html")); err != nil { t.Fatal(err) } time.Sleep(50 * time.Millisecond) - - err = c.Run(defaultContext, NavigateToHistoryEntry(entries[index].ID)) - if err != nil { + if err := Run(ctx, NavigateToHistoryEntry(entries[index].ID)); err != nil { t.Fatal(err) } time.Sleep(50 * time.Millisecond) var title string - err = c.Run(defaultContext, Title(&title)) - if err != nil { + if err := Run(ctx, Title(&title)); err != nil { t.Fatal(err) } + if title != entries[index].Title { t.Errorf("expected title to be %s, instead title is: %s", entries[index].Title, title) } @@ -150,43 +130,35 @@ func TestNavigateToHistoryEntry(t *testing.T) { func TestNavigateBack(t *testing.T) { t.Parallel() - var err error - - c := testAllocate(t, "") - defer c.Release() - - err = c.Run(defaultContext, Navigate(testdataDir+"/form.html")) - if err != nil { + ctx, cancel := testAllocate(t, "") + defer cancel() + if err := Run(ctx, Navigate(testdataDir+"/form.html")); err != nil { t.Fatal(err) } time.Sleep(50 * time.Millisecond) var exptitle string - err = c.Run(defaultContext, Title(&exptitle)) - if err != nil { + if err := Run(ctx, Title(&exptitle)); err != nil { t.Fatal(err) } - err = c.Run(defaultContext, Navigate(testdataDir+"/image.html")) - if err != nil { + if err := Run(ctx, Navigate(testdataDir+"/image.html")); err != nil { t.Fatal(err) } time.Sleep(50 * time.Millisecond) - - err = c.Run(defaultContext, NavigateBack()) - if err != nil { + if err := Run(ctx, NavigateBack()); err != nil { t.Fatal(err) } time.Sleep(50 * time.Millisecond) var title string - err = c.Run(defaultContext, Title(&title)) - if err != nil { + if err := Run(ctx, Title(&title)); err != nil { t.Fatal(err) } + if title != exptitle { t.Errorf("expected title to be %s, instead title is: %s", exptitle, title) } @@ -195,50 +167,39 @@ func TestNavigateBack(t *testing.T) { func TestNavigateForward(t *testing.T) { t.Parallel() - var err error - - c := testAllocate(t, "") - defer c.Release() - - err = c.Run(defaultContext, Navigate(testdataDir+"/form.html")) - if err != nil { + ctx, cancel := testAllocate(t, "") + defer cancel() + if err := Run(ctx, Navigate(testdataDir+"/form.html")); err != nil { t.Fatal(err) } time.Sleep(50 * time.Millisecond) - - err = c.Run(defaultContext, Navigate(testdataDir+"/image.html")) - if err != nil { + if err := Run(ctx, Navigate(testdataDir+"/image.html")); err != nil { t.Fatal(err) } time.Sleep(50 * time.Millisecond) var exptitle string - err = c.Run(defaultContext, Title(&exptitle)) - if err != nil { + if err := Run(ctx, Title(&exptitle)); err != nil { t.Fatal(err) } - - err = c.Run(defaultContext, NavigateBack()) - if err != nil { + if err := Run(ctx, NavigateBack()); err != nil { t.Fatal(err) } time.Sleep(50 * time.Millisecond) - - err = c.Run(defaultContext, NavigateForward()) - if err != nil { + if err := Run(ctx, NavigateForward()); err != nil { t.Fatal(err) } time.Sleep(50 * time.Millisecond) var title string - err = c.Run(defaultContext, Title(&title)) - if err != nil { + if err := Run(ctx, Title(&title)); err != nil { t.Fatal(err) } + if title != exptitle { t.Errorf("expected title to be %s, instead title is: %s", exptitle, title) } @@ -247,55 +208,44 @@ func TestNavigateForward(t *testing.T) { func TestStop(t *testing.T) { t.Parallel() - var err error - - c := testAllocate(t, "") - defer c.Release() - - err = c.Run(defaultContext, Navigate(testdataDir+"/form.html")) - if err != nil { + ctx, cancel := testAllocate(t, "") + defer cancel() + if err := Run(ctx, Navigate(testdataDir+"/form.html")); err != nil { + t.Fatal(err) + } + if err := Run(ctx, Stop()); err != nil { t.Fatal(err) } - err = c.Run(defaultContext, Stop()) - if err != nil { - t.Fatal(err) - } } func TestReload(t *testing.T) { t.Parallel() - var err error - - c := testAllocate(t, "") - defer c.Release() - - err = c.Run(defaultContext, Navigate(testdataDir+"/form.html")) - if err != nil { + ctx, cancel := testAllocate(t, "") + defer cancel() + if err := Run(ctx, Navigate(testdataDir+"/form.html")); err != nil { t.Fatal(err) } time.Sleep(50 * time.Millisecond) var exptitle string - err = c.Run(defaultContext, Title(&exptitle)) - if err != nil { + if err := Run(ctx, Title(&exptitle)); err != nil { t.Fatal(err) } - err = c.Run(defaultContext, Reload()) - if err != nil { + if err := Run(ctx, Reload()); err != nil { t.Fatal(err) } time.Sleep(50 * time.Millisecond) var title string - err = c.Run(defaultContext, Title(&title)) - if err != nil { + if err := Run(ctx, Title(&title)); err != nil { t.Fatal(err) } + if title != exptitle { t.Errorf("expected title to be %s, instead title is: %s", exptitle, title) } @@ -304,21 +254,16 @@ func TestReload(t *testing.T) { func TestCaptureScreenshot(t *testing.T) { t.Parallel() - var err error - - c := testAllocate(t, "") - defer c.Release() - - err = c.Run(defaultContext, Navigate(testdataDir+"/image.html")) - if err != nil { + ctx, cancel := testAllocate(t, "") + defer cancel() + if err := Run(ctx, Navigate(testdataDir+"/image.html")); err != nil { t.Fatal(err) } time.Sleep(50 * time.Millisecond) var buf []byte - err = c.Run(defaultContext, CaptureScreenshot(&buf)) - if err != nil { + if err := Run(ctx, CaptureScreenshot(&buf)); err != nil { t.Fatal(err) } @@ -331,18 +276,16 @@ func TestCaptureScreenshot(t *testing.T) { /*func TestAddOnLoadScript(t *testing.T) { t.Parallel() - var err error - - c := testAllocate(t, "") - defer c.Release() + ctx, cancel := testAllocate(t, "") + defer cancel() var scriptID page.ScriptIdentifier - err = c.Run(defaultContext, AddOnLoadScript(`window.alert("TEST")`, &scriptID)) + err = Run(ctx, AddOnLoadScript(`window.alert("TEST")`, &scriptID)) if err != nil { t.Fatal(err) } - err = c.Run(defaultContext, Navigate(testdataDir+"/form.html")) + err = Run(ctx, Navigate(testdataDir+"/form.html")) if err != nil { t.Fatal(err) } @@ -358,13 +301,11 @@ func TestCaptureScreenshot(t *testing.T) { func TestRemoveOnLoadScript(t *testing.T) { t.Parallel() - var err error - - c := testAllocate(t, "") - defer c.Release() + ctx, cancel := testAllocate(t, "") + defer cancel() var scriptID page.ScriptIdentifier - err = c.Run(defaultContext, AddOnLoadScript(`window.alert("TEST")`, &scriptID)) + err = Run(ctx, AddOnLoadScript(`window.alert("TEST")`, &scriptID)) if err != nil { t.Fatal(err) } @@ -373,12 +314,12 @@ func TestRemoveOnLoadScript(t *testing.T) { t.Fatal("got empty script ID") } - err = c.Run(defaultContext, RemoveOnLoadScript(scriptID)) + err = Run(ctx, RemoveOnLoadScript(scriptID)) if err != nil { t.Fatal(err) } - err = c.Run(defaultContext, Navigate(testdataDir+"/form.html")) + err = Run(ctx, Navigate(testdataDir+"/form.html")) if err != nil { t.Fatal(err) } @@ -389,22 +330,18 @@ func TestRemoveOnLoadScript(t *testing.T) { func TestLocation(t *testing.T) { t.Parallel() - var err error expurl := testdataDir + "/form.html" - c := testAllocate(t, "") - defer c.Release() - - err = c.Run(defaultContext, Navigate(expurl)) - if err != nil { + ctx, cancel := testAllocate(t, "") + defer cancel() + if err := Run(ctx, Navigate(expurl)); err != nil { t.Fatal(err) } time.Sleep(50 * time.Millisecond) var urlstr string - err = c.Run(defaultContext, Location(&urlstr)) - if err != nil { + if err := Run(ctx, Location(&urlstr)); err != nil { t.Fatal(err) } @@ -416,22 +353,18 @@ func TestLocation(t *testing.T) { func TestTitle(t *testing.T) { t.Parallel() - var err error expurl, exptitle := testdataDir+"/image.html", "this is title" - c := testAllocate(t, "") - defer c.Release() - - err = c.Run(defaultContext, Navigate(expurl)) - if err != nil { + ctx, cancel := testAllocate(t, "") + defer cancel() + if err := Run(ctx, Navigate(expurl)); err != nil { t.Fatal(err) } time.Sleep(50 * time.Millisecond) var title string - err = c.Run(defaultContext, Title(&title)) - if err != nil { + if err := Run(ctx, Title(&title)); err != nil { t.Fatal(err) } diff --git a/pool.go b/pool.go deleted file mode 100644 index 58e0271..0000000 --- a/pool.go +++ /dev/null @@ -1,219 +0,0 @@ -package chromedp - -import ( - "context" - "fmt" - "log" - "net" - "sync" - - "github.com/chromedp/chromedp/runner" -) - -// Pool manages a pool of running Chrome processes. -type Pool struct { - // start is the start port. - start int - - // end is the end port. - end int - - // res are the running chrome resources. - res map[int]*Res - - // logging funcs - logf, debugf, errf func(string, ...interface{}) - - rw sync.RWMutex -} - -// NewPool creates a new Chrome runner pool. -func NewPool(opts ...PoolOption) (*Pool, error) { - p := &Pool{ - start: DefaultPoolStartPort, - end: DefaultPoolEndPort, - res: make(map[int]*Res), - logf: log.Printf, - debugf: func(string, ...interface{}) {}, - } - - // apply opts - for _, o := range opts { - if err := o(p); err != nil { - return nil, err - } - } - - if p.errf == nil { - p.errf = func(s string, v ...interface{}) { - p.logf("ERROR: "+s, v...) - } - } - - return p, nil -} - -// Shutdown releases all the pool resources. -func (p *Pool) Shutdown() error { - p.rw.Lock() - defer p.rw.Unlock() - - for _, r := range p.res { - r.cancel() - } - - return nil -} - -// Allocate creates a new process runner and returns it. -func (p *Pool) Allocate(ctxt context.Context, opts ...runner.CommandLineOption) (*Res, error) { - var err error - - r := p.next(ctxt) - - // Check if the port is available first. If it's not, Chrome will print - // an "address already in use" error, but it will otherwise keep - // running. This can lead to Allocate succeeding, while the chrome - // process isn't actually listening on the port we need. - l, err := net.Listen("tcp", fmt.Sprintf("localhost:%d", r.port)) - if err != nil { - // we can't use this port, e.g. address already in use - p.errf("pool could not allocate runner on port %d: %v", r.port, err) - return nil, err - } - l.Close() - - p.debugf("pool allocating %d", r.port) - - // create runner - r.r, err = runner.New(append([]runner.CommandLineOption{ - runner.ExecPath(runner.LookChromeNames("headless_shell")), - runner.RemoteDebuggingPort(r.port), - runner.NoDefaultBrowserCheck, - runner.NoFirstRun, - runner.Headless, - }, opts...)...) - if err != nil { - defer r.Release() - p.errf("pool could not allocate runner on port %d: %v", r.port, err) - return nil, err - } - - // start runner - err = r.r.Start(r.ctxt) - if err != nil { - defer r.Release() - p.errf("pool could not start runner on port %d: %v", r.port, err) - return nil, err - } - - // setup cdp - r.c, err = New( - r.ctxt, WithRunner(r.r), - WithLogf(p.logf), WithDebugf(p.debugf), WithErrorf(p.errf), - ) - if err != nil { - defer r.Release() - p.errf("pool could not connect to %d: %v", r.port, err) - return nil, err - } - - return r, nil -} - -// next returns the next available res. -func (p *Pool) next(ctxt context.Context) *Res { - p.rw.Lock() - defer p.rw.Unlock() - - var found bool - var i int - for i = p.start; i < p.end; i++ { - if _, ok := p.res[i]; !ok { - found = true - break - } - } - - if !found { - panic("no ports available") - } - - r := &Res{ - p: p, - port: i, - } - r.ctxt, r.cancel = context.WithCancel(ctxt) - - p.res[i] = r - - return r -} - -// Res is a pool resource. -type Res struct { - p *Pool - ctxt context.Context - cancel func() - port int - r *runner.Runner - c *CDP -} - -// Release releases the pool resource. -func (r *Res) Release() error { - r.cancel() - - var err error - if r.c != nil { - err = r.c.Wait() - } - - defer r.p.debugf("pool released %d", r.port) - - r.p.rw.Lock() - defer r.p.rw.Unlock() - delete(r.p.res, r.port) - - return err -} - -// Port returns the allocated port for the pool resource. -func (r *Res) Port() int { - return r.port -} - -// URL returns a formatted URL for the pool resource. -func (r *Res) URL() string { - return fmt.Sprintf("http://localhost:%d/json", r.port) -} - -// CDP returns the actual CDP instance. -func (r *Res) CDP() *CDP { - return r.c -} - -// Run runs an action. -func (r *Res) Run(ctxt context.Context, a Action) error { - return r.c.Run(ctxt, a) -} - -// PoolOption is a pool option. -type PoolOption func(*Pool) error - -// PortRange is a pool option to set the port range to use. -func PortRange(start, end int) PoolOption { - return func(p *Pool) error { - p.start = start - p.end = end - return nil - } -} - -// PoolLog is a pool option to set the logging to use for the pool. -func PoolLog(logf, debugf, errf func(string, ...interface{})) PoolOption { - return func(p *Pool) error { - p.logf, p.debugf, p.errf = logf, debugf, errf - return nil - } -} diff --git a/pool_test.go b/pool_test.go deleted file mode 100644 index 3ec251d..0000000 --- a/pool_test.go +++ /dev/null @@ -1,47 +0,0 @@ -package chromedp - -import ( - "context" - "net" - "strconv" - "strings" - "testing" -) - -func TestAllocatePortInUse(t *testing.T) { - t.Parallel() - - // take a random available port - l, err := net.Listen("tcp4", "localhost:0") - if err != nil { - t.Fatal(err) - } - defer l.Close() - - ctxt, cancel := context.WithCancel(context.Background()) - defer cancel() - - // make the pool use the port already in use via a port range - _, portStr, _ := net.SplitHostPort(l.Addr().String()) - port, _ := strconv.Atoi(portStr) - pool, err := NewPool( - PortRange(port, port+1), - // skip the error log from the used port - PoolLog(nil, nil, func(string, ...interface{}) {}), - ) - if err != nil { - t.Fatal(err) - } - - c, err := pool.Allocate(ctxt) - if err != nil { - want := "address already in use" - got := err.Error() - if !strings.Contains(got, want) { - t.Fatalf("wanted error to contain %q, but got %q", want, got) - } - } else { - t.Fatal("wanted Allocate to error if port is in use") - c.Release() - } -} diff --git a/query.go b/query.go index c2b51e6..35aefc1 100644 --- a/query.go +++ b/query.go @@ -484,8 +484,7 @@ func Screenshot(sel interface{}, picbuf *[]byte, opts ...QueryOption) Action { // encode var croppedBuf bytes.Buffer - err = png.Encode(&croppedBuf, cropped) - if err != nil { + if err := png.Encode(&croppedBuf, cropped); err != nil { return err } diff --git a/query_test.go b/query_test.go index f681c4c..6b68f3b 100644 --- a/query_test.go +++ b/query_test.go @@ -20,8 +20,8 @@ import ( func TestNodes(t *testing.T) { t.Parallel() - c := testAllocate(t, "table.html") - defer c.Release() + ctx, cancel := testAllocate(t, "table.html") + defer cancel() tests := []struct { sel string @@ -34,13 +34,12 @@ func TestNodes(t *testing.T) { {"#footer", ByID, 1}, } - var err error for i, test := range tests { var nodes []*cdp.Node - err = c.Run(defaultContext, Nodes(test.sel, &nodes, test.by)) - if err != nil { + if err := Run(ctx, Nodes(test.sel, &nodes, test.by)); err != nil { t.Fatalf("test %d got error: %v", i, err) } + if len(nodes) != test.len { t.Errorf("test %d expected to have %d nodes: got %d", i, test.len, len(nodes)) } @@ -50,8 +49,8 @@ func TestNodes(t *testing.T) { func TestNodeIDs(t *testing.T) { t.Parallel() - c := testAllocate(t, "table.html") - defer c.Release() + ctx, cancel := testAllocate(t, "table.html") + defer cancel() tests := []struct { sel string @@ -64,13 +63,12 @@ func TestNodeIDs(t *testing.T) { {"#footer", ByID, 1}, } - var err error for i, test := range tests { var ids []cdp.NodeID - err = c.Run(defaultContext, NodeIDs(test.sel, &ids, test.by)) - if err != nil { + if err := Run(ctx, NodeIDs(test.sel, &ids, test.by)); err != nil { t.Fatal(err) } + if len(ids) != test.len { t.Errorf("test %d expected to have %d node id's: got %d", i, test.len, len(ids)) } @@ -80,8 +78,8 @@ func TestNodeIDs(t *testing.T) { func TestFocusBlur(t *testing.T) { t.Parallel() - c := testAllocate(t, "js.html") - defer c.Release() + ctx, cancel := testAllocate(t, "js.html") + defer cancel() tests := []struct { sel string @@ -93,35 +91,31 @@ func TestFocusBlur(t *testing.T) { {"#input1", ByID}, } - err := c.Run(defaultContext, Click("#input1", ByID)) + err := Run(ctx, Click("#input1", ByID)) if err != nil { t.Fatal(err) } for i, test := range tests { - err = c.Run(defaultContext, Focus(test.sel, test.by)) - if err != nil { + if err := Run(ctx, Focus(test.sel, test.by)); err != nil { t.Fatalf("test %d got error: %v", i, err) } var value string - err = c.Run(defaultContext, Value(test.sel, &value, test.by)) - if err != nil { + if err := Run(ctx, Value(test.sel, &value, test.by)); err != nil { t.Fatalf("test %d got error: %v", i, err) } + if value != "9999" { t.Errorf("test %d expected value is '9999', got: '%s'", i, value) } - - err = c.Run(defaultContext, Blur(test.sel, test.by)) - if err != nil { + if err := Run(ctx, Blur(test.sel, test.by)); err != nil { + t.Fatalf("test %d got error: %v", i, err) + } + if err := Run(ctx, Value(test.sel, &value, test.by)); err != nil { t.Fatalf("test %d got error: %v", i, err) } - err = c.Run(defaultContext, Value(test.sel, &value, test.by)) - if err != nil { - t.Fatalf("test %d got error: %v", i, err) - } if value != "0" { t.Errorf("test %d expected value is '0', got: '%s'", i, value) } @@ -131,8 +125,8 @@ func TestFocusBlur(t *testing.T) { func TestDimensions(t *testing.T) { t.Parallel() - c := testAllocate(t, "image.html") - defer c.Release() + ctx, cancel := testAllocate(t, "image.html") + defer cancel() tests := []struct { sel string @@ -146,13 +140,12 @@ func TestDimensions(t *testing.T) { {"#icon-github", ByID, 120, 120}, } - var err error for i, test := range tests { var model *dom.BoxModel - err = c.Run(defaultContext, Dimensions(test.sel, &model)) - if err != nil { + if err := Run(ctx, Dimensions(test.sel, &model)); err != nil { t.Fatalf("test %d got error: %v", i, err) } + if model.Height != test.height || model.Width != test.width { t.Errorf("test %d expected %dx%d, got: %dx%d", i, test.width, test.height, model.Height, model.Width) } @@ -162,8 +155,8 @@ func TestDimensions(t *testing.T) { func TestText(t *testing.T) { t.Parallel() - c := testAllocate(t, "form.html") - defer c.Release() + ctx, cancel := testAllocate(t, "form.html") + defer cancel() tests := []struct { sel string @@ -176,13 +169,12 @@ func TestText(t *testing.T) { {"/html/body/form/span[2]", BySearch, "keyword"}, } - var err error for i, test := range tests { var text string - err = c.Run(defaultContext, Text(test.sel, &text, test.by)) - if err != nil { + if err := Run(ctx, Text(test.sel, &text, test.by)); err != nil { t.Fatalf("test %d got error: %v", i, err) } + if text != test.exp { t.Errorf("test %d expected `%s`, got: %s", i, test.exp, text) } @@ -217,27 +209,24 @@ func TestClear(t *testing.T) { t.Run(fmt.Sprintf("test %d", i), func(t *testing.T) { t.Parallel() - c := testAllocate(t, "form.html") - defer c.Release() + ctx, cancel := testAllocate(t, "form.html") + defer cancel() var val string - err := c.Run(defaultContext, Value(test.sel, &val, test.by)) + err := Run(ctx, Value(test.sel, &val, test.by)) if err != nil { t.Fatalf("got error: %v", err) } if val == "" { t.Errorf("expected `%s` to have non empty value", test.sel) } - - err = c.Run(defaultContext, Clear(test.sel, test.by)) - if err != nil { + if err := Run(ctx, Clear(test.sel, test.by)); err != nil { + t.Fatalf("got error: %v", err) + } + if err := Run(ctx, Value(test.sel, &val, test.by)); err != nil { t.Fatalf("got error: %v", err) } - err = c.Run(defaultContext, Value(test.sel, &val, test.by)) - if err != nil { - t.Fatalf("got error: %v", err) - } if val != "" { t.Errorf("expected empty value for `%s`, got: %s", test.sel, val) } @@ -264,24 +253,22 @@ func TestReset(t *testing.T) { t.Run(fmt.Sprintf("test %d", i), func(t *testing.T) { t.Parallel() - c := testAllocate(t, "form.html") - defer c.Release() + ctx, cancel := testAllocate(t, "form.html") + defer cancel() - err := c.Run(defaultContext, SetValue(test.sel, test.value, test.by)) + err := Run(ctx, SetValue(test.sel, test.value, test.by)) if err != nil { t.Fatalf("got error: %v", err) } - - err = c.Run(defaultContext, Reset(test.sel, test.by)) - if err != nil { + if err := Run(ctx, Reset(test.sel, test.by)); err != nil { t.Fatalf("got error: %v", err) } var value string - err = c.Run(defaultContext, Value(test.sel, &value, test.by)) - if err != nil { + if err := Run(ctx, Value(test.sel, &value, test.by)); err != nil { t.Fatalf("got error: %v", err) } + if value != test.exp { t.Errorf("expected value after reset is %s, got: '%s'", test.exp, value) } @@ -292,8 +279,8 @@ func TestReset(t *testing.T) { func TestValue(t *testing.T) { t.Parallel() - c := testAllocate(t, "form.html") - defer c.Release() + ctx, cancel := testAllocate(t, "form.html") + defer cancel() tests := []struct { sel string @@ -305,13 +292,12 @@ func TestValue(t *testing.T) { {`#keyword`, ByID}, } - var err error for i, test := range tests { var value string - err = c.Run(defaultContext, Value(test.sel, &value, test.by)) - if err != nil { + if err := Run(ctx, Value(test.sel, &value, test.by)); err != nil { t.Fatalf("test %d got error: %v", i, err) } + if value != "chromedp" { t.Errorf("test %d expected `chromedp`, got: %s", i, value) } @@ -335,19 +321,19 @@ func TestSetValue(t *testing.T) { t.Run(fmt.Sprintf("test %d", i), func(t *testing.T) { t.Parallel() - c := testAllocate(t, "form.html") - defer c.Release() + ctx, cancel := testAllocate(t, "form.html") + defer cancel() - err := c.Run(defaultContext, SetValue(test.sel, "FOOBAR", test.by)) + err := Run(ctx, SetValue(test.sel, "FOOBAR", test.by)) if err != nil { t.Fatalf("got error: %v", err) } var value string - err = c.Run(defaultContext, Value(test.sel, &value, test.by)) - if err != nil { + if err := Run(ctx, Value(test.sel, &value, test.by)); err != nil { t.Fatalf("got error: %v", err) } + if value != "FOOBAR" { t.Errorf("expected `FOOBAR`, got: %s", value) } @@ -358,8 +344,8 @@ func TestSetValue(t *testing.T) { func TestAttributes(t *testing.T) { t.Parallel() - c := testAllocate(t, "image.html") - defer c.Release() + ctx, cancel := testAllocate(t, "image.html") + defer cancel() tests := []struct { sel string @@ -392,11 +378,9 @@ func TestAttributes(t *testing.T) { }}, } - var err error for i, test := range tests { var attrs map[string]string - err = c.Run(defaultContext, Attributes(test.sel, &attrs, test.by)) - if err != nil { + if err := Run(ctx, Attributes(test.sel, &attrs, test.by)); err != nil { t.Fatalf("test %d got error: %v", i, err) } @@ -409,8 +393,8 @@ func TestAttributes(t *testing.T) { func TestAttributesAll(t *testing.T) { t.Parallel() - c := testAllocate(t, "image.html") - defer c.Release() + ctx, cancel := testAllocate(t, "image.html") + defer cancel() tests := []struct { sel string @@ -433,11 +417,9 @@ func TestAttributesAll(t *testing.T) { }, } - var err error for i, test := range tests { var attrs []map[string]string - err = c.Run(defaultContext, AttributesAll(test.sel, &attrs, test.by)) - if err != nil { + if err := Run(ctx, AttributesAll(test.sel, &attrs, test.by)); err != nil { t.Fatalf("test %d got error: %v", i, err) } @@ -495,17 +477,16 @@ func TestSetAttributes(t *testing.T) { t.Run(fmt.Sprintf("test %d", i), func(t *testing.T) { t.Parallel() - c := testAllocate(t, "image.html") - defer c.Release() + ctx, cancel := testAllocate(t, "image.html") + defer cancel() - err := c.Run(defaultContext, SetAttributes(test.sel, test.attrs, test.by)) + err := Run(ctx, SetAttributes(test.sel, test.attrs, test.by)) if err != nil { t.Fatalf("got error: %v", err) } var attrs map[string]string - err = c.Run(defaultContext, Attributes(test.sel, &attrs, test.by)) - if err != nil { + if err := Run(ctx, Attributes(test.sel, &attrs, test.by)); err != nil { t.Fatalf("got error: %v", err) } @@ -519,8 +500,8 @@ func TestSetAttributes(t *testing.T) { func TestAttributeValue(t *testing.T) { t.Parallel() - c := testAllocate(t, "image.html") - defer c.Release() + ctx, cancel := testAllocate(t, "image.html") + defer cancel() tests := []struct { sel string @@ -534,13 +515,10 @@ func TestAttributeValue(t *testing.T) { {"#icon-github", ByID, "alt", "How people build software"}, } - var err error for i, test := range tests { var value string var ok bool - - err = c.Run(defaultContext, AttributeValue(test.sel, test.attr, &value, &ok, test.by)) - if err != nil { + if err := Run(ctx, AttributeValue(test.sel, test.attr, &value, &ok, test.by)); err != nil { t.Fatalf("test %d got error: %v", i, err) } @@ -573,20 +551,20 @@ func TestSetAttributeValue(t *testing.T) { t.Run(fmt.Sprintf("test %d", i), func(t *testing.T) { t.Parallel() - c := testAllocate(t, "form.html") - defer c.Release() + ctx, cancel := testAllocate(t, "form.html") + defer cancel() - err := c.Run(defaultContext, SetAttributeValue(test.sel, test.attr, test.exp, test.by)) + err := Run(ctx, SetAttributeValue(test.sel, test.attr, test.exp, test.by)) if err != nil { t.Fatalf("got error: %v", err) } var value string var ok bool - err = c.Run(defaultContext, AttributeValue(test.sel, test.attr, &value, &ok, test.by)) - if err != nil { + if err := Run(ctx, AttributeValue(test.sel, test.attr, &value, &ok, test.by)); err != nil { t.Fatalf("got error: %v", err) } + if !ok { t.Fatalf("failed to get attribute %s on %s", test.attr, test.sel) } @@ -616,20 +594,20 @@ func TestRemoveAttribute(t *testing.T) { t.Run(fmt.Sprintf("test %d", i), func(t *testing.T) { t.Parallel() - c := testAllocate(t, "image.html") - defer c.Release() + ctx, cancel := testAllocate(t, "image.html") + defer cancel() - err := c.Run(defaultContext, RemoveAttribute(test.sel, test.attr)) + err := Run(ctx, RemoveAttribute(test.sel, test.attr)) if err != nil { t.Fatalf("got error: %v", err) } var value string var ok bool - err = c.Run(defaultContext, AttributeValue(test.sel, test.attr, &value, &ok, test.by)) - if err != nil { + if err := Run(ctx, AttributeValue(test.sel, test.attr, &value, &ok, test.by)); err != nil { t.Fatalf("got error: %v", err) } + if ok || value != "" { t.Fatalf("expected attribute %s removed from element %s", test.attr, test.sel) } @@ -654,24 +632,22 @@ func TestClick(t *testing.T) { t.Run(fmt.Sprintf("test %d", i), func(t *testing.T) { t.Parallel() - c := testAllocate(t, "form.html") - defer c.Release() + ctx, cancel := testAllocate(t, "form.html") + defer cancel() - err := c.Run(defaultContext, Click(test.sel, test.by)) + err := Run(ctx, Click(test.sel, test.by)) if err != nil { t.Fatalf("got error: %v", err) } - - err = c.Run(defaultContext, WaitVisible("#icon-brankas", ByID)) - if err != nil { + if err := Run(ctx, WaitVisible("#icon-brankas", ByID)); err != nil { t.Fatalf("got error: %v", err) } var title string - err = c.Run(defaultContext, Title(&title)) - if err != nil { + if err := Run(ctx, Title(&title)); err != nil { t.Fatalf("got error: %v", err) } + if title != "this is title" { t.Errorf("expected title to be 'chromedp - Google Search', got: '%s'", title) } @@ -696,10 +672,10 @@ func TestDoubleClick(t *testing.T) { t.Run(fmt.Sprintf("test %d", i), func(t *testing.T) { t.Parallel() - c := testAllocate(t, "js.html") - defer c.Release() + ctx, cancel := testAllocate(t, "js.html") + defer cancel() - err := c.Run(defaultContext, DoubleClick(test.sel, test.by)) + err := Run(ctx, DoubleClick(test.sel, test.by)) if err != nil { t.Fatalf("got error: %v", err) } @@ -707,10 +683,10 @@ func TestDoubleClick(t *testing.T) { time.Sleep(50 * time.Millisecond) var value string - err = c.Run(defaultContext, Value("#input1", &value, ByID)) - if err != nil { + if err := Run(ctx, Value("#input1", &value, ByID)); err != nil { t.Fatalf("got error: %v", err) } + if value != "1" { t.Errorf("expected value to be '1', got: '%s'", value) } @@ -739,19 +715,19 @@ func TestSendKeys(t *testing.T) { t.Run(fmt.Sprintf("test %d", i), func(t *testing.T) { t.Parallel() - c := testAllocate(t, "visible.html") - defer c.Release() + ctx, cancel := testAllocate(t, "visible.html") + defer cancel() - err := c.Run(defaultContext, SendKeys(test.sel, test.keys, test.by)) + err := Run(ctx, SendKeys(test.sel, test.keys, test.by)) if err != nil { t.Fatalf("got error: %v", err) } var val string - err = c.Run(defaultContext, Value(test.sel, &val, test.by)) - if err != nil { + if err := Run(ctx, Value(test.sel, &val, test.by)); err != nil { t.Fatalf("got error: %v", err) } + if val != test.exp { t.Errorf("expected value %s, got: %s", test.exp, val) } @@ -762,8 +738,8 @@ func TestSendKeys(t *testing.T) { func TestScreenshot(t *testing.T) { t.Parallel() - c := testAllocate(t, "image.html") - defer c.Release() + ctx, cancel := testAllocate(t, "image.html") + defer cancel() tests := []struct { sel string @@ -775,11 +751,9 @@ func TestScreenshot(t *testing.T) { {"#icon-github", ByID}, } - var err error for i, test := range tests { var buf []byte - err = c.Run(defaultContext, Screenshot(test.sel, &buf)) - if err != nil { + if err := Run(ctx, Screenshot(test.sel, &buf)); err != nil { t.Fatalf("test %d got error: %v", i, err) } @@ -807,24 +781,22 @@ func TestSubmit(t *testing.T) { t.Run(fmt.Sprintf("test %d", i), func(t *testing.T) { t.Parallel() - c := testAllocate(t, "form.html") - defer c.Release() + ctx, cancel := testAllocate(t, "form.html") + defer cancel() - err := c.Run(defaultContext, Submit(test.sel, test.by)) + err := Run(ctx, Submit(test.sel, test.by)) if err != nil { t.Fatalf("got error: %v", err) } - - err = c.Run(defaultContext, WaitVisible("#icon-brankas", ByID)) - if err != nil { + if err := Run(ctx, WaitVisible("#icon-brankas", ByID)); err != nil { t.Fatalf("got error: %v", err) } var title string - err = c.Run(defaultContext, Title(&title)) - if err != nil { + if err := Run(ctx, Title(&title)); err != nil { t.Fatalf("got error: %v", err) } + if title != "this is title" { t.Errorf("expected title to be 'this is title', got: '%s'", title) } @@ -849,13 +821,13 @@ func TestComputedStyle(t *testing.T) { t.Run(fmt.Sprintf("test %d", i), func(t *testing.T) { t.Parallel() - c := testAllocate(t, "js.html") - defer c.Release() + ctx, cancel := testAllocate(t, "js.html") + defer cancel() time.Sleep(50 * time.Millisecond) var styles []*css.ComputedProperty - err := c.Run(defaultContext, ComputedStyle(test.sel, &styles, test.by)) + err := Run(ctx, ComputedStyle(test.sel, &styles, test.by)) if err != nil { t.Fatalf("got error: %v", err) } @@ -867,16 +839,12 @@ func TestComputedStyle(t *testing.T) { } } } - - err = c.Run(defaultContext, Click("#input1", ByID)) - if err != nil { + if err := Run(ctx, Click("#input1", ByID)); err != nil { t.Fatalf("got error: %v", err) } time.Sleep(50 * time.Millisecond) - - err = c.Run(defaultContext, ComputedStyle(test.sel, &styles, test.by)) - if err != nil { + if err := Run(ctx, ComputedStyle(test.sel, &styles, test.by)); err != nil { t.Fatalf("got error: %v", err) } @@ -908,13 +876,13 @@ func TestMatchedStyle(t *testing.T) { t.Run(fmt.Sprintf("test %d", i), func(t *testing.T) { t.Parallel() - c := testAllocate(t, "js.html") - defer c.Release() + ctx, cancel := testAllocate(t, "js.html") + defer cancel() time.Sleep(50 * time.Millisecond) var styles *css.GetMatchedStylesForNodeReturns - err := c.Run(defaultContext, MatchedStyle(test.sel, &styles, test.by)) + err := Run(ctx, MatchedStyle(test.sel, &styles, test.by)) if err != nil { t.Fatalf("got error: %v", err) } @@ -957,10 +925,10 @@ func TestFileUpload(t *testing.T) { t.Fatal(err) } defer os.Remove(tmpfile.Name()) - if _, err = tmpfile.WriteString(uploadHTML); err != nil { + if _, err := tmpfile.WriteString(uploadHTML); err != nil { t.Fatal(err) } - if err = tmpfile.Close(); err != nil { + if err := tmpfile.Close(); err != nil { t.Fatal(err) } @@ -977,19 +945,19 @@ func TestFileUpload(t *testing.T) { // parallel //t.Parallel() - c := testAllocate(t, "") - defer c.Release() + ctx, cancel := testAllocate(t, "") + defer cancel() var result string - err = c.Run(defaultContext, Tasks{ + if err := Run(ctx, Tasks{ Navigate(s.URL), test.a, Click(`input[name="submit"]`), Text(`#result`, &result, ByID, NodeVisible), - }) - if err != nil { + }); err != nil { t.Fatalf("test %d expected no error, got: %v", i, err) } + if result != fmt.Sprintf("%d", len(uploadHTML)) { t.Errorf("test %d expected result to be %d, got: %s", i, len(uploadHTML), result) } @@ -1000,8 +968,8 @@ func TestFileUpload(t *testing.T) { func TestInnerHTML(t *testing.T) { t.Parallel() - c := testAllocate(t, "table.html") - defer c.Release() + ctx, cancel := testAllocate(t, "table.html") + defer cancel() tests := []struct { sel string @@ -1011,13 +979,12 @@ func TestInnerHTML(t *testing.T) { {"thead", ByQueryAll}, {"thead", ByQuery}, } - var err error for i, test := range tests { var html string - err = c.Run(defaultContext, InnerHTML(test.sel, &html)) - if err != nil { + if err := Run(ctx, InnerHTML(test.sel, &html)); err != nil { t.Fatalf("test %d got error: %v", i, err) } + if html == "" { t.Fatalf("test %d: InnerHTML is empty", i) } @@ -1027,8 +994,8 @@ func TestInnerHTML(t *testing.T) { func TestOuterHTML(t *testing.T) { t.Parallel() - c := testAllocate(t, "table.html") - defer c.Release() + ctx, cancel := testAllocate(t, "table.html") + defer cancel() tests := []struct { sel string @@ -1038,13 +1005,12 @@ func TestOuterHTML(t *testing.T) { {"thead tr", ByQueryAll}, {"thead tr", ByQuery}, } - var err error for i, test := range tests { var html string - err = c.Run(defaultContext, OuterHTML(test.sel, &html)) - if err != nil { + if err := Run(ctx, OuterHTML(test.sel, &html)); err != nil { t.Fatalf("test %d got error: %v", i, err) } + if html == "" { t.Fatalf("test %d: OuterHTML is empty", i) } @@ -1054,8 +1020,8 @@ func TestOuterHTML(t *testing.T) { func TestScrollIntoView(t *testing.T) { t.Parallel() - c := testAllocate(t, "image.html") - defer c.Release() + ctx, cancel := testAllocate(t, "image.html") + defer cancel() tests := []struct { sel string @@ -1066,12 +1032,11 @@ func TestScrollIntoView(t *testing.T) { {"img", ByQuery}, {"#icon-github", ByID}, } - var err error for i, test := range tests { - err = c.Run(defaultContext, ScrollIntoView(test.sel, test.by)) - if err != nil { + if err := Run(ctx, ScrollIntoView(test.sel, test.by)); err != nil { t.Fatalf("test %d got error: %v", i, err) } + // TODO test scroll event } } diff --git a/runner/path_darwin.go b/runner/path_darwin.go deleted file mode 100644 index 8bf4446..0000000 --- a/runner/path_darwin.go +++ /dev/null @@ -1,13 +0,0 @@ -// +build darwin - -package runner - -const ( - // DefaultChromePath is the default path to use for Chrome if the - // executable is not in $PATH. - DefaultChromePath = `/Applications/Google Chrome.app/Contents/MacOS/Google Chrome` -) - -// DefaultChromeNames are the default Chrome executable names to look for in -// $PATH. -var DefaultChromeNames []string diff --git a/runner/path_unix.go b/runner/path_unix.go deleted file mode 100644 index 4b1af77..0000000 --- a/runner/path_unix.go +++ /dev/null @@ -1,19 +0,0 @@ -// +build linux freebsd netbsd openbsd - -package runner - -const ( - // DefaultChromePath is the default path to use for Chrome if the - // executable is not in $PATH. - DefaultChromePath = "/usr/bin/google-chrome" -) - -// DefaultChromeNames are the default Chrome executable names to look for in -// $PATH. -var DefaultChromeNames = []string{ - "google-chrome", - "chromium-browser", - "chromium", - "google-chrome-beta", - "google-chrome-unstable", -} diff --git a/runner/path_windows.go b/runner/path_windows.go deleted file mode 100644 index 2a6413a..0000000 --- a/runner/path_windows.go +++ /dev/null @@ -1,13 +0,0 @@ -// +build windows - -package runner - -const ( - // DefaultChromePath is the default path to use for Chrome if the - // executable is not in %PATH%. - DefaultChromePath = `C:\Program Files (x86)\Google\Chrome\Application\chrome.exe` -) - -// DefaultChromeNames are the default Chrome executable names to look for in -// %PATH%. -var DefaultChromeNames = []string{`chrome.exe`} diff --git a/runner/runner.go b/runner/runner.go deleted file mode 100644 index 61ad3db..0000000 --- a/runner/runner.go +++ /dev/null @@ -1,486 +0,0 @@ -// Package runner provides a Chrome process runner. -package runner - -import ( - "context" - "fmt" - "io/ioutil" - "os" - "os/exec" - "regexp" - "runtime" - "sync" - "syscall" - - "github.com/chromedp/chromedp/client" -) - -const ( - // DefaultUserDataDirPrefix is the default user data directory prefix. - DefaultUserDataDirPrefix = "chromedp-runner.%d." -) - -// Error is a runner error. -type Error string - -// Error satisfies the error interface. -func (err Error) Error() string { - return string(err) -} - -// Error values. -const ( - // ErrAlreadyStarted is the already started error. - ErrAlreadyStarted Error = "already started" - - // ErrAlreadyWaiting is the already waiting error. - ErrAlreadyWaiting Error = "already waiting" - - // ErrInvalidURLs is the invalid url-opts error. - ErrInvalidURLOpts Error = "invalid url-opts" - - // ErrInvalidCmdOpts is the invalid cmd-opts error. - ErrInvalidCmdOpts Error = "invalid cmd-opts" - - // ErrInvalidProcessOpts is the invalid process-opts error. - ErrInvalidProcessOpts Error = "invalid process-opts" - - // ErrInvalidExecPath is the invalid exec-path error. - ErrInvalidExecPath Error = "invalid exec-path" -) - -// Runner holds information about a running Chrome process. -type Runner struct { - opts map[string]interface{} - cmd *exec.Cmd - waiting bool - rw sync.RWMutex -} - -// New creates a new Chrome process using the supplied command line options. -func New(opts ...CommandLineOption) (*Runner, error) { - var err error - - cliOpts := make(map[string]interface{}) - - // apply opts - for _, o := range opts { - if err = o(cliOpts); err != nil { - return nil, err - } - } - - // set default Chrome options if exec-path not provided - if _, ok := cliOpts["exec-path"]; !ok { - cliOpts["exec-path"] = LookChromeNames() - for k, v := range map[string]interface{}{ - "no-first-run": true, - "no-default-browser-check": true, - "remote-debugging-port": 9222, - } { - if _, ok := cliOpts[k]; !ok { - cliOpts[k] = v - } - } - } - - // add KillProcessGroup and ForceKill if no other cmd opts provided - if _, ok := cliOpts["cmd-opts"]; !ok { - for _, o := range []CommandLineOption{KillProcessGroup, ForceKill} { - if err = o(cliOpts); err != nil { - return nil, err - } - } - } - - return &Runner{ - opts: cliOpts, - }, nil -} - -// cliOptRE is a regular expression to validate a chrome cli option. -var cliOptRE = regexp.MustCompile(`^[a-z0-9\-]+$`) - -// buildOpts generates the command line options for Chrome. -func (r *Runner) buildOpts() []string { - var opts []string - var urls []string - - // process opts - for k, v := range r.opts { - if !cliOptRE.MatchString(k) || v == nil { - continue - } - - switch k { - case "exec-path", "cmd-opts", "process-opts": - continue - - case "url-opts": - urls = v.([]string) - - default: - switch z := v.(type) { - case bool: - if z { - opts = append(opts, "--"+k) - } - - case string: - opts = append(opts, "--"+k+"="+z) - - default: - opts = append(opts, "--"+k+"="+fmt.Sprintf("%v", v)) - } - } - } - - if urls == nil { - urls = append(urls, "about:blank") - } - - return append(opts, urls...) -} - -// Start starts a Chrome process using the specified context. The Chrome -// process can be terminated by closing the passed context. -func (r *Runner) Start(ctxt context.Context, opts ...string) error { - var err error - var ok bool - - r.rw.RLock() - cmd := r.cmd - r.rw.RUnlock() - - if cmd != nil { - return ErrAlreadyStarted - } - - // set user data dir, if not provided - _, ok = r.opts["user-data-dir"] - if !ok { - r.opts["user-data-dir"], err = ioutil.TempDir( - defaultUserDataTmpDir, fmt.Sprintf(DefaultUserDataDirPrefix, r.Port()), - ) - if err != nil { - return err - } - } - - // get exec path - var execPath string - if p, ok := r.opts["exec-path"]; ok { - execPath, ok = p.(string) - if !ok { - return ErrInvalidExecPath - } - } - - // ensure execPath is valid - if execPath == "" { - return ErrInvalidExecPath - } - - // create cmd - r.cmd = exec.CommandContext(ctxt, execPath, append(r.buildOpts(), opts...)...) - - // apply cmd opts - if cmdOpts, ok := r.opts["cmd-opts"]; ok { - for _, co := range cmdOpts.([]func(*exec.Cmd) error) { - if err = co(r.cmd); err != nil { - return err - } - } - } - - // start process - if err = r.cmd.Start(); err != nil { - return err - } - - // apply process opts - if processOpts, ok := r.opts["process-opts"]; ok { - for _, po := range processOpts.([]func(*os.Process) error) { - if err = po(r.cmd.Process); err != nil { - // TODO: do something better here, as we want to kill - // the child process, do cleanup, etc. - panic(err) - //return err - } - } - } - - return nil -} - -// Shutdown shuts down the Chrome process. Currently only has support for -// SIGTERM in darwin and linux systems -func (r *Runner) Shutdown(ctxt context.Context, opts ...client.Option) error { - var err error - - cl := r.Client(opts...) - - targets, err := cl.ListPageTargets(ctxt) - if err != nil { - return err - } - - var wg sync.WaitGroup - errs := make([]error, len(targets)) - for i, t := range targets { - wg.Add(1) - go func(wg *sync.WaitGroup, i int, t client.Target) { - defer wg.Done() - errs[i] = cl.CloseTarget(ctxt, t) - }(&wg, i, t) - } - wg.Wait() - - for _, e := range errs { - if e != nil { - return e - } - } - - // osx and linux applications do not automatically exit when all windows (ie, tabs) - // closed, so send SIGTERM. - // - // TODO: add other behavior here for more process options on shutdown? - if r.cmd != nil && r.cmd.Process != nil { - switch runtime.GOOS { - case "darwin", "linux": - return r.cmd.Process.Signal(syscall.SIGTERM) - } - } - - return nil -} - -// Wait waits for the previously started Chrome process to terminate, returning -// any encountered error. -func (r *Runner) Wait() error { - r.rw.RLock() - waiting := r.waiting - r.rw.RUnlock() - - if waiting { - return ErrAlreadyWaiting - } - - r.rw.Lock() - r.waiting = true - r.rw.Unlock() - - defer func() { - r.rw.Lock() - r.waiting = false - r.rw.Unlock() - }() - - return r.cmd.Wait() -} - -// Port returns the port the process was launched with. -func (r *Runner) Port() int { - var port interface{} - var ok bool - port, ok = r.opts["remote-debugging-port"] - if !ok { - port, ok = r.opts["port"] - } - if !ok { - panic("expected either remote-debugging-port or port to be specified in command line options") - } - - var p int - p, ok = port.(int) - if !ok { - panic("expected port to be type int") - } - - return p -} - -// Client returns a Chrome DevTools Protocol client for the running Chrome -// process. -func (r *Runner) Client(opts ...client.Option) *client.Client { - return client.New(append(opts, - client.URL(fmt.Sprintf("http://localhost:%d/json", r.Port())), - )...) -} - -// Run starts a new Chrome process runner, using the provided context and -// command line options. -func Run(ctxt context.Context, opts ...CommandLineOption) (*Runner, error) { - var err error - - // create - r, err := New(opts...) - if err != nil { - return nil, err - } - - // start - if err = r.Start(ctxt); err != nil { - return nil, err - } - - return r, nil -} - -// CommandLineOption is a runner command line option. -// -// see: http://peter.sh/experiments/chromium-command-line-switches/ -type CommandLineOption func(map[string]interface{}) error - -// Flag is a generic command line option to pass a name=value flag to -// Chrome. -func Flag(name string, value interface{}) CommandLineOption { - return func(m map[string]interface{}) error { - m[name] = value - return nil - } -} - -// Path sets the path to the Chrome executable and sets default run options for -// Chrome. This will also set the remote debugging port to 9222, and disable -// the first run / default browser check. -// -// Note: use ExecPath if you do not want to set other options. -func Path(path string) CommandLineOption { - return func(m map[string]interface{}) error { - m["exec-path"] = path - m["no-first-run"] = true - m["no-default-browser-check"] = true - m["remote-debugging-port"] = 9222 - return nil - } -} - -// ExecPath is a command line option to set the exec path. -func ExecPath(path string) CommandLineOption { - return Flag("exec-path", path) -} - -// UserDataDir is the command line option to set the user data dir. -// -// Note: set this option to manually set the profile directory used by Chrome. -// When this is not set, then a default path will be created in the /tmp -// directory. -func UserDataDir(dir string) CommandLineOption { - return Flag("user-data-dir", dir) -} - -// ProxyServer is the command line option to set the outbound proxy server. -func ProxyServer(proxy string) CommandLineOption { - return Flag("proxy-server", proxy) -} - -// WindowSize is the command line option to set the initial window size. -func WindowSize(width, height int) CommandLineOption { - return Flag("window-size", fmt.Sprintf("%d,%d", width, height)) -} - -// UserAgent is the command line option to set the default User-Agent -// header. -func UserAgent(userAgent string) CommandLineOption { - return Flag("user-agent", userAgent) -} - -// NoSandbox is the Chrome comamnd line option to disable the sandbox. -func NoSandbox(m map[string]interface{}) error { - return Flag("no-sandbox", true)(m) -} - -// NoFirstRun is the Chrome comamnd line option to disable the first run -// dialog. -func NoFirstRun(m map[string]interface{}) error { - return Flag("no-first-run", true)(m) -} - -// NoDefaultBrowserCheck is the Chrome comamnd line option to disable the -// default browser check. -func NoDefaultBrowserCheck(m map[string]interface{}) error { - return Flag("no-default-browser-check", true)(m) -} - -// RemoteDebuggingPort is the command line option to set the remote -// debugging port. -func RemoteDebuggingPort(port int) CommandLineOption { - return Flag("remote-debugging-port", port) -} - -// Headless is the command line option to run in headless mode. -func Headless(m map[string]interface{}) error { - return Flag("headless", true)(m) -} - -// DisableGPU is the command line option to disable the GPU process. -func DisableGPU(m map[string]interface{}) error { - return Flag("disable-gpu", true)(m) -} - -// URL is the command line option to add a URL to open on process start. -// -// Note: this can be specified multiple times, and each URL will be opened in a -// new tab. -func URL(urlstr string) CommandLineOption { - return func(m map[string]interface{}) error { - var urls []string - if u, ok := m["url-opts"]; ok { - urls, ok = u.([]string) - if !ok { - return ErrInvalidURLOpts - } - } - m["url-opts"] = append(urls, urlstr) - return nil - } -} - -// CmdOpt is a command line option to modify the underlying exec.Cmd -// prior to the call to exec.Cmd.Start in Run. -func CmdOpt(o func(*exec.Cmd) error) CommandLineOption { - return func(m map[string]interface{}) error { - var opts []func(*exec.Cmd) error - if e, ok := m["cmd-opts"]; ok { - opts, ok = e.([]func(*exec.Cmd) error) - if !ok { - return ErrInvalidCmdOpts - } - } - m["cmd-opts"] = append(opts, o) - return nil - } -} - -// ProcessOpt is a command line option to modify the child os.Process -// after the call to exec.Cmd.Start in Run. -func ProcessOpt(o func(*os.Process) error) CommandLineOption { - return func(m map[string]interface{}) error { - var opts []func(*os.Process) error - if e, ok := m["process-opts"]; ok { - opts, ok = e.([]func(*os.Process) error) - if !ok { - return ErrInvalidProcessOpts - } - } - m["process-opts"] = append(opts, o) - return nil - } -} - -// LookChromeNames looks for the platform's DefaultChromeNames and any -// additional names using exec.LookPath, returning the first encountered -// location or the platform's DefaultChromePath if no names are found on the -// path. -func LookChromeNames(additional ...string) string { - for _, p := range append(additional, DefaultChromeNames...) { - path, err := exec.LookPath(p) - if err == nil { - return path - } - } - - return DefaultChromePath -} diff --git a/runner/runner_bsd.go b/runner/runner_bsd.go deleted file mode 100644 index d67e5eb..0000000 --- a/runner/runner_bsd.go +++ /dev/null @@ -1,11 +0,0 @@ -// +build darwin freebsd netbsd openbsd - -package runner - -// ForceKill is a Chrome command line option that forces Chrome to be killed -// when the parent is killed. -// -// Note: sets exec.Cmd.SysProcAttr.Setpgid = true (only for Linux) -func ForceKill(m map[string]interface{}) error { - return nil -} diff --git a/runner/runner_linux.go b/runner/runner_linux.go deleted file mode 100644 index 0e6acae..0000000 --- a/runner/runner_linux.go +++ /dev/null @@ -1,87 +0,0 @@ -// +build linux - -package runner - -import ( - "os" - "os/exec" - "syscall" - "unsafe" -) - -// ByteCount is a type byte count const. -type ByteCount uint64 - -// ByteCount values. -const ( - Byte ByteCount = 1 - Kilobyte ByteCount = 1024 * Byte - Megabyte ByteCount = 1024 * Kilobyte - Gigabyte ByteCount = 1024 * Megabyte -) - -// prlimit invokes the system's prlimit call. Copied from Go source tree. -// -// Note: this needs either the CAP_SYS_RESOURCE capability, or the invoking -// process needs to have the same functional user and group as the pid being -// modified. -// -// see: man 2 prlimit -func prlimit(pid int, res int, newv, old *syscall.Rlimit) error { - _, _, err := syscall.RawSyscall6(syscall.SYS_PRLIMIT64, uintptr(pid), uintptr(res), uintptr(unsafe.Pointer(newv)), uintptr(unsafe.Pointer(old)), 0, 0) - if err != 0 { - return err - } - - return nil -} - -// Rlimit is a Chrome command line option to set the soft rlimit value for res -// on a running Chrome process. -// -// Note: uses Linux prlimit system call, and is invoked after the child process -// has been started. -// -// see: man 2 prlimit -func Rlimit(res int, cur, max uint64) CommandLineOption { - return ProcessOpt(func(p *os.Process) error { - return prlimit(p.Pid, syscall.RLIMIT_AS, &syscall.Rlimit{ - Cur: cur, - Max: max, - }, nil) - }) -} - -// LimitMemory is a Chrome command line option to set the soft memory limit for -// a running Chrome process. -// -// Note: uses Linux prlimit system call, and is invoked after the child -// process has been started. -func LimitMemory(mem ByteCount) CommandLineOption { - return Rlimit(syscall.RLIMIT_AS, uint64(mem), uint64(mem)) -} - -// LimitCoreDump is a Chrome command line option to set the soft core dump -// limit for a running Chrome process. -// -// Note: uses Linux prlimit system call, and is invoked after the child -// process has been started. -func LimitCoreDump(sz ByteCount) CommandLineOption { - return Rlimit(syscall.RLIMIT_CORE, uint64(sz), uint64(sz)) -} - -// ForceKill is a Chrome command line option that forces Chrome to be killed -// when the parent is killed. -// -// Note: sets exec.Cmd.SysProcAttr.Setpgid = true (only for Linux) -func ForceKill(m map[string]interface{}) error { - return CmdOpt(func(c *exec.Cmd) error { - if c.SysProcAttr == nil { - c.SysProcAttr = new(syscall.SysProcAttr) - } - - c.SysProcAttr.Pdeathsig = syscall.SIGKILL - - return nil - })(m) -} diff --git a/runner/runner_unix.go b/runner/runner_unix.go deleted file mode 100644 index 3f0c1d8..0000000 --- a/runner/runner_unix.go +++ /dev/null @@ -1,31 +0,0 @@ -// +build linux darwin freebsd netbsd openbsd - -package runner - -import ( - "os/exec" - "syscall" -) - -var ( - // DefaultUserDataTmpDir is the default directory path for created user - // data directories. - defaultUserDataTmpDir = "/tmp" -) - -// KillProcessGroup is a Chrome command line option that will instruct the -// invoked child Chrome process to terminate when the parent process (ie, the -// Go application) dies. -// -// Note: sets exec.Cmd.SysProcAttr.Setpgid = true and does nothing on Windows. -func KillProcessGroup(m map[string]interface{}) error { - return CmdOpt(func(c *exec.Cmd) error { - if c.SysProcAttr == nil { - c.SysProcAttr = new(syscall.SysProcAttr) - } - - c.SysProcAttr.Setpgid = true - - return nil - })(m) -} diff --git a/runner/runner_windows.go b/runner/runner_windows.go deleted file mode 100644 index 785004b..0000000 --- a/runner/runner_windows.go +++ /dev/null @@ -1,26 +0,0 @@ -// +build windows - -package runner - -import "os" - -var ( - defaultUserDataTmpDir = os.Getenv("USERPROFILE") + `\AppData\Local` -) - -// KillProcessGroup is a Chrome command line option that will instruct the -// invoked child Chrome process to terminate when the parent process (ie, the -// Go application) dies. -// -// Note: sets exec.Cmd.SysProcAttr.Setpgid = true and does nothing on Windows. -func KillProcessGroup(m map[string]interface{}) error { - return nil -} - -// ForceKill is a Chrome command line option that forces Chrome to be killed -// when the parent is killed. -// -// Note: sets exec.Cmd.SysProcAttr.Setpgid = true (only for Linux) -func ForceKill(m map[string]interface{}) error { - return nil -} diff --git a/sel.go b/sel.go index bbfc720..68c0412 100644 --- a/sel.go +++ b/sel.go @@ -107,8 +107,7 @@ func (s *Selector) run(ctxt context.Context, h *TargetHandler) chan error { return } - err = s.after(ctxt, h, nodes...) - if err != nil { + if err := s.after(ctxt, h, nodes...); err != nil { ch <- err } return diff --git a/sel_test.go b/sel_test.go index de95153..0f233cd 100644 --- a/sel_test.go +++ b/sel_test.go @@ -9,132 +9,118 @@ import ( func TestWaitReady(t *testing.T) { t.Parallel() - c := testAllocate(t, "js.html") - defer c.Release() + ctx, cancel := testAllocate(t, "js.html") + defer cancel() var nodeIDs []cdp.NodeID - err := c.Run(defaultContext, NodeIDs("#input2", &nodeIDs, ByID)) + err := Run(ctx, NodeIDs("#input2", &nodeIDs, ByID)) if err != nil { t.Fatalf("got error: %v", err) } if len(nodeIDs) != 1 { t.Errorf("expected to have exactly 1 node id: got %d", len(nodeIDs)) } - - err = c.Run(defaultContext, WaitReady("#input2", ByID)) - if err != nil { + if err := Run(ctx, WaitReady("#input2", ByID)); err != nil { t.Fatalf("got error: %v", err) } var value string - err = c.Run(defaultContext, Value(nodeIDs, &value, ByNodeID)) - if err != nil { + if err := Run(ctx, Value(nodeIDs, &value, ByNodeID)); err != nil { t.Fatalf("got error: %v", err) } + } func TestWaitVisible(t *testing.T) { t.Parallel() - c := testAllocate(t, "js.html") - defer c.Release() + ctx, cancel := testAllocate(t, "js.html") + defer cancel() var nodeIDs []cdp.NodeID - err := c.Run(defaultContext, NodeIDs("#input2", &nodeIDs, ByID)) + err := Run(ctx, NodeIDs("#input2", &nodeIDs, ByID)) if err != nil { t.Fatalf("got error: %v", err) } if len(nodeIDs) != 1 { t.Errorf("expected to have exactly 1 node id: got %d", len(nodeIDs)) } - - err = c.Run(defaultContext, WaitVisible("#input2", ByID)) - if err != nil { + if err := Run(ctx, WaitVisible("#input2", ByID)); err != nil { t.Fatalf("got error: %v", err) } var value string - err = c.Run(defaultContext, Value(nodeIDs, &value, ByNodeID)) - if err != nil { + if err := Run(ctx, Value(nodeIDs, &value, ByNodeID)); err != nil { t.Fatalf("got error: %v", err) } + } func TestWaitNotVisible(t *testing.T) { t.Parallel() - c := testAllocate(t, "js.html") - defer c.Release() + ctx, cancel := testAllocate(t, "js.html") + defer cancel() var nodeIDs []cdp.NodeID - err := c.Run(defaultContext, NodeIDs("#input2", &nodeIDs, ByID)) + err := Run(ctx, NodeIDs("#input2", &nodeIDs, ByID)) if err != nil { t.Fatalf("got error: %v", err) } if len(nodeIDs) != 1 { t.Errorf("expected to have exactly 1 node id: got %d", len(nodeIDs)) } - - err = c.Run(defaultContext, Click("#button2", ByID)) - if err != nil { + if err := Run(ctx, Click("#button2", ByID)); err != nil { t.Fatalf("got error: %v", err) } - - err = c.Run(defaultContext, WaitNotVisible("#input2", ByID)) - if err != nil { + if err := Run(ctx, WaitNotVisible("#input2", ByID)); err != nil { t.Fatalf("got error: %v", err) } var value string - err = c.Run(defaultContext, Value(nodeIDs, &value, ByNodeID)) - if err != nil { + if err := Run(ctx, Value(nodeIDs, &value, ByNodeID)); err != nil { t.Fatalf("got error: %v", err) } + } func TestWaitEnabled(t *testing.T) { t.Parallel() - c := testAllocate(t, "js.html") - defer c.Release() + ctx, cancel := testAllocate(t, "js.html") + defer cancel() var attr string var ok bool - err := c.Run(defaultContext, AttributeValue("#select1", "disabled", &attr, &ok, ByID)) + err := Run(ctx, AttributeValue("#select1", "disabled", &attr, &ok, ByID)) if err != nil { t.Fatalf("got error: %v", err) } if !ok { t.Fatal("expected element to be disabled") } - - err = c.Run(defaultContext, Click("#button3", ByID)) - if err != nil { + if err := Run(ctx, Click("#button3", ByID)); err != nil { + t.Fatalf("got error: %v", err) + } + if err := Run(ctx, WaitEnabled("#select1", ByID)); err != nil { + t.Fatalf("got error: %v", err) + } + if err := Run(ctx, AttributeValue("#select1", "disabled", &attr, &ok, ByID)); err != nil { t.Fatalf("got error: %v", err) } - err = c.Run(defaultContext, WaitEnabled("#select1", ByID)) - if err != nil { - t.Fatalf("got error: %v", err) - } - err = c.Run(defaultContext, AttributeValue("#select1", "disabled", &attr, &ok, ByID)) - if err != nil { - t.Fatalf("got error: %v", err) - } if ok { t.Fatal("expected element to be enabled") } - - err = c.Run(defaultContext, SetAttributeValue(`//*[@id="select1"]/option[1]`, "selected", "true")) - if err != nil { + if err := Run(ctx, SetAttributeValue(`//*[@id="select1"]/option[1]`, "selected", "true")); err != nil { t.Fatalf("got error: %v", err) } var value string - err = c.Run(defaultContext, Value("#select1", &value, ByID)) - if err != nil { + if err := Run(ctx, Value("#select1", &value, ByID)); err != nil { t.Fatalf("got error: %v", err) } + if value != "foo" { t.Fatalf("expected value to be foo, got: %s", value) } @@ -143,43 +129,36 @@ func TestWaitEnabled(t *testing.T) { func TestWaitSelected(t *testing.T) { t.Parallel() - c := testAllocate(t, "js.html") - defer c.Release() + ctx, cancel := testAllocate(t, "js.html") + defer cancel() - err := c.Run(defaultContext, Click("#button3", ByID)) + err := Run(ctx, Click("#button3", ByID)) if err != nil { t.Fatalf("got error: %v", err) } - - err = c.Run(defaultContext, WaitEnabled("#select1", ByID)) - if err != nil { + if err := Run(ctx, WaitEnabled("#select1", ByID)); err != nil { t.Fatalf("got error: %v", err) } var attr string ok := false - err = c.Run(defaultContext, AttributeValue(`//*[@id="select1"]/option[1]`, "selected", &attr, &ok)) - if err != nil { + if err := Run(ctx, AttributeValue(`//*[@id="select1"]/option[1]`, "selected", &attr, &ok)); err != nil { t.Fatalf("got error: %v", err) } + if ok { t.Fatal("expected element to be not selected") } - - err = c.Run(defaultContext, SetAttributeValue(`//*[@id="select1"]/option[1]`, "selected", "true")) - if err != nil { + if err := Run(ctx, SetAttributeValue(`//*[@id="select1"]/option[1]`, "selected", "true")); err != nil { + t.Fatalf("got error: %v", err) + } + if err := Run(ctx, WaitSelected(`//*[@id="select1"]/option[1]`)); err != nil { + t.Fatalf("got error: %v", err) + } + if err := Run(ctx, AttributeValue(`//*[@id="select1"]/option[1]`, "selected", &attr, nil)); err != nil { t.Fatalf("got error: %v", err) } - err = c.Run(defaultContext, WaitSelected(`//*[@id="select1"]/option[1]`)) - if err != nil { - t.Fatalf("got error: %v", err) - } - - err = c.Run(defaultContext, AttributeValue(`//*[@id="select1"]/option[1]`, "selected", &attr, nil)) - if err != nil { - t.Fatalf("got error: %v", err) - } if attr != "true" { t.Fatal("expected element to be selected") } @@ -188,33 +167,30 @@ func TestWaitSelected(t *testing.T) { func TestWaitNotPresent(t *testing.T) { t.Parallel() - c := testAllocate(t, "js.html") - defer c.Release() + ctx, cancel := testAllocate(t, "js.html") + defer cancel() - err := c.Run(defaultContext, WaitVisible("#input3", ByID)) + err := Run(ctx, WaitVisible("#input3", ByID)) if err != nil { t.Fatalf("got error: %v", err) } - - err = c.Run(defaultContext, Click("#button4", ByID)) - if err != nil { + if err := Run(ctx, Click("#button4", ByID)); err != nil { + t.Fatalf("got error: %v", err) + } + if err := Run(ctx, WaitNotPresent("#input3", ByID)); err != nil { t.Fatalf("got error: %v", err) } - err = c.Run(defaultContext, WaitNotPresent("#input3", ByID)) - if err != nil { - t.Fatalf("got error: %v", err) - } } func TestAtLeast(t *testing.T) { t.Parallel() - c := testAllocate(t, "js.html") - defer c.Release() + ctx, cancel := testAllocate(t, "js.html") + defer cancel() var nodes []*cdp.Node - err := c.Run(defaultContext, Nodes("//input", &nodes, AtLeast(3))) + err := Run(ctx, Nodes("//input", &nodes, AtLeast(3))) if err != nil { t.Fatalf("got error: %v", err) } diff --git a/util.go b/util.go index d6337b8..51b5d7a 100644 --- a/util.go +++ b/util.go @@ -1,10 +1,17 @@ package chromedp import ( + "time" + "github.com/chromedp/cdproto" "github.com/chromedp/cdproto/cdp" ) +const ( + // DefaultCheckDuration is the default time to sleep between a check. + DefaultCheckDuration = 50 * time.Millisecond +) + // frameOp is a frame manipulation operation. type frameOp func(*cdp.Frame)