2017-02-09 15:01:40 +00:00
|
|
|
package chromedp
|
|
|
|
|
|
|
|
import (
|
|
|
|
"context"
|
|
|
|
"fmt"
|
2017-02-13 09:00:25 +00:00
|
|
|
"log"
|
pool: error quickly if we find a port in use
Before the fix, the added test would give a Pool.Allocate error like:
pool could not connect to 9000: timeout waiting for initial target
The actual underlying error, which can only be seen if one inspects
chrome's stderr, is that it failed to bind to the debugging protocol
port if it was already in use.
This is of course an issue with the environment that chromedp is being
run under, since it was given a port range that wasn't available.
However, the confusing error can lead to developers wasting their time
instead of spotting the error quickly.
Unfortunately, there doesn't seem to be a way to have Chrome exit
immediately if it can't bind to the given port. So, instead of relying
on it, check if the current process can bind to the port first.
Add a test too, where we grab the first port in the pool range, and
check that we get an error that's not confusing.
Fixes #253.
2018-11-22 12:28:57 +00:00
|
|
|
"net"
|
2017-02-09 15:01:40 +00:00
|
|
|
"sync"
|
|
|
|
|
2017-12-27 02:30:28 +00:00
|
|
|
"github.com/chromedp/chromedp/runner"
|
2017-02-09 15:01:40 +00:00
|
|
|
)
|
|
|
|
|
2017-02-12 04:59:33 +00:00
|
|
|
// Pool manages a pool of running Chrome processes.
|
2017-02-09 15:01:40 +00:00
|
|
|
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
|
|
|
|
|
2017-02-13 09:00:25 +00:00
|
|
|
// logging funcs
|
2018-05-18 22:03:47 +00:00
|
|
|
logf, debugf, errf func(string, ...interface{})
|
2017-02-13 09:00:25 +00:00
|
|
|
|
2017-02-09 15:01:40 +00:00
|
|
|
rw sync.RWMutex
|
|
|
|
}
|
|
|
|
|
|
|
|
// NewPool creates a new Chrome runner pool.
|
|
|
|
func NewPool(opts ...PoolOption) (*Pool, error) {
|
|
|
|
p := &Pool{
|
2017-02-13 09:00:25 +00:00
|
|
|
start: DefaultPoolStartPort,
|
|
|
|
end: DefaultPoolEndPort,
|
|
|
|
res: make(map[int]*Res),
|
|
|
|
logf: log.Printf,
|
|
|
|
debugf: func(string, ...interface{}) {},
|
2017-02-09 15:01:40 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
// apply opts
|
|
|
|
for _, o := range opts {
|
2017-11-25 20:20:52 +00:00
|
|
|
if err := o(p); err != nil {
|
2017-02-09 15:01:40 +00:00
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2018-05-18 22:03:47 +00:00
|
|
|
if p.errf == nil {
|
|
|
|
p.errf = func(s string, v ...interface{}) {
|
|
|
|
p.logf("ERROR: "+s, v...)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2017-11-25 20:20:52 +00:00
|
|
|
return p, nil
|
2017-02-09 15:01:40 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
// 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
|
|
|
|
|
2017-02-18 06:11:46 +00:00
|
|
|
r := p.next(ctxt)
|
2017-02-09 15:01:40 +00:00
|
|
|
|
pool: error quickly if we find a port in use
Before the fix, the added test would give a Pool.Allocate error like:
pool could not connect to 9000: timeout waiting for initial target
The actual underlying error, which can only be seen if one inspects
chrome's stderr, is that it failed to bind to the debugging protocol
port if it was already in use.
This is of course an issue with the environment that chromedp is being
run under, since it was given a port range that wasn't available.
However, the confusing error can lead to developers wasting their time
instead of spotting the error quickly.
Unfortunately, there doesn't seem to be a way to have Chrome exit
immediately if it can't bind to the given port. So, instead of relying
on it, check if the current process can bind to the port first.
Add a test too, where we grab the first port in the pool range, and
check that we get an error that's not confusing.
Fixes #253.
2018-11-22 12:28:57 +00:00
|
|
|
// 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()
|
|
|
|
|
2017-02-18 06:11:46 +00:00
|
|
|
p.debugf("pool allocating %d", r.port)
|
2017-02-09 15:01:40 +00:00
|
|
|
|
|
|
|
// create runner
|
|
|
|
r.r, err = runner.New(append([]runner.CommandLineOption{
|
2018-07-13 03:57:20 +00:00
|
|
|
runner.ExecPath(runner.LookChromeNames("headless_shell")),
|
|
|
|
runner.RemoteDebuggingPort(r.port),
|
|
|
|
runner.NoDefaultBrowserCheck,
|
|
|
|
runner.NoFirstRun,
|
|
|
|
runner.Headless,
|
2017-02-09 15:01:40 +00:00
|
|
|
}, opts...)...)
|
|
|
|
if err != nil {
|
2017-02-18 06:11:46 +00:00
|
|
|
defer r.Release()
|
2018-05-18 22:03:47 +00:00
|
|
|
p.errf("pool could not allocate runner on port %d: %v", r.port, err)
|
2017-02-09 15:01:40 +00:00
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
|
|
|
// start runner
|
2017-02-18 06:11:46 +00:00
|
|
|
err = r.r.Start(r.ctxt)
|
2017-02-09 15:01:40 +00:00
|
|
|
if err != nil {
|
2017-02-18 06:11:46 +00:00
|
|
|
defer r.Release()
|
2018-05-18 22:03:47 +00:00
|
|
|
p.errf("pool could not start runner on port %d: %v", r.port, err)
|
2017-02-09 15:01:40 +00:00
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
|
|
|
// setup cdp
|
2017-02-13 09:00:25 +00:00
|
|
|
r.c, err = New(
|
2017-02-18 06:11:46 +00:00
|
|
|
r.ctxt, WithRunner(r.r),
|
2018-05-18 22:03:47 +00:00
|
|
|
WithLogf(p.logf), WithDebugf(p.debugf), WithErrorf(p.errf),
|
2017-02-13 09:00:25 +00:00
|
|
|
)
|
2017-02-09 15:01:40 +00:00
|
|
|
if err != nil {
|
2017-02-18 06:11:46 +00:00
|
|
|
defer r.Release()
|
2018-05-18 22:03:47 +00:00
|
|
|
p.errf("pool could not connect to %d: %v", r.port, err)
|
2017-02-09 15:01:40 +00:00
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
|
|
|
return r, nil
|
|
|
|
}
|
|
|
|
|
2017-02-18 06:11:46 +00:00
|
|
|
// next returns the next available res.
|
|
|
|
func (p *Pool) next(ctxt context.Context) *Res {
|
2017-02-09 15:01:40 +00:00
|
|
|
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")
|
|
|
|
}
|
|
|
|
|
2017-02-18 06:11:46 +00:00
|
|
|
r := &Res{
|
|
|
|
p: p,
|
|
|
|
port: i,
|
|
|
|
}
|
|
|
|
r.ctxt, r.cancel = context.WithCancel(ctxt)
|
|
|
|
|
|
|
|
p.res[i] = r
|
|
|
|
|
|
|
|
return r
|
2017-02-09 15:01:40 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
// 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()
|
|
|
|
|
2017-02-22 08:27:38 +00:00
|
|
|
var err error
|
|
|
|
if r.c != nil {
|
|
|
|
err = r.c.Wait()
|
|
|
|
}
|
2017-02-18 02:49:41 +00:00
|
|
|
|
2017-02-18 06:11:46 +00:00
|
|
|
defer r.p.debugf("pool released %d", r.port)
|
|
|
|
|
2017-02-09 15:01:40 +00:00
|
|
|
r.p.rw.Lock()
|
|
|
|
defer r.p.rw.Unlock()
|
|
|
|
delete(r.p.res, r.port)
|
|
|
|
|
2017-02-18 02:49:41 +00:00
|
|
|
return err
|
2017-02-09 15:01:40 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
// 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
|
|
|
|
}
|
|
|
|
}
|
2017-02-13 09:00:25 +00:00
|
|
|
|
|
|
|
// PoolLog is a pool option to set the logging to use for the pool.
|
2018-05-18 22:03:47 +00:00
|
|
|
func PoolLog(logf, debugf, errf func(string, ...interface{})) PoolOption {
|
2017-02-13 09:00:25 +00:00
|
|
|
return func(p *Pool) error {
|
2018-05-18 22:03:47 +00:00
|
|
|
p.logf, p.debugf, p.errf = logf, debugf, errf
|
2017-02-13 09:00:25 +00:00
|
|
|
return nil
|
|
|
|
}
|
|
|
|
}
|