2017-02-09 15:01:40 +00:00
|
|
|
package chromedp
|
|
|
|
|
|
|
|
import (
|
|
|
|
"context"
|
|
|
|
"fmt"
|
2017-02-13 09:00:25 +00:00
|
|
|
"log"
|
2017-02-09 15:01:40 +00:00
|
|
|
"sync"
|
|
|
|
|
|
|
|
"github.com/knq/chromedp/runner"
|
|
|
|
)
|
|
|
|
|
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
|
|
|
|
logf, debugf, errorf LogFunc
|
|
|
|
|
2017-02-09 15:01:40 +00:00
|
|
|
rw sync.RWMutex
|
|
|
|
}
|
|
|
|
|
|
|
|
// NewPool creates a new Chrome runner pool.
|
|
|
|
func NewPool(opts ...PoolOption) (*Pool, error) {
|
|
|
|
var err 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{}) {},
|
|
|
|
errorf: func(s string, v ...interface{}) { log.Printf("error: "+s, v...) },
|
2017-02-09 15:01:40 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
// apply opts
|
|
|
|
for _, o := range opts {
|
|
|
|
err = o(p)
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return p, err
|
|
|
|
}
|
|
|
|
|
|
|
|
// 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
|
|
|
|
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{
|
|
|
|
runner.Headless("", r.port),
|
|
|
|
}, opts...)...)
|
|
|
|
if err != nil {
|
2017-02-18 06:11:46 +00:00
|
|
|
defer r.Release()
|
|
|
|
p.errorf("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()
|
|
|
|
p.errorf("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),
|
2017-02-13 09:00:25 +00:00
|
|
|
WithLogf(p.logf), WithDebugf(p.debugf), WithErrorf(p.errorf),
|
|
|
|
)
|
2017-02-09 15:01:40 +00:00
|
|
|
if err != nil {
|
2017-02-18 06:11:46 +00:00
|
|
|
defer r.Release()
|
|
|
|
p.errorf("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-18 02:49:41 +00:00
|
|
|
err := r.c.Wait()
|
|
|
|
|
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.
|
|
|
|
func PoolLog(logf, debugf, errorf LogFunc) PoolOption {
|
|
|
|
return func(p *Pool) error {
|
|
|
|
p.logf = logf
|
|
|
|
p.debugf = debugf
|
|
|
|
p.errorf = errorf
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
}
|