Cleaning up runner API prior to package rewrite

This commit is contained in:
Kenneth Shaw 2018-07-13 10:57:20 +07:00
parent e051c4a982
commit 622c90c82c
6 changed files with 116 additions and 143 deletions

View File

@ -90,7 +90,7 @@ func New(ctxt context.Context, opts ...Option) (*CDP, error) {
// watch handlers // watch handlers
if c.watch == nil { if c.watch == nil {
c.watch = c.r.WatchPageTargets(ctxt) c.watch = c.r.Client().WatchPageTargets(ctxt)
} }
go func() { go func() {

View File

@ -74,7 +74,11 @@ func (p *Pool) Allocate(ctxt context.Context, opts ...runner.CommandLineOption)
// create runner // create runner
r.r, err = runner.New(append([]runner.CommandLineOption{ r.r, err = runner.New(append([]runner.CommandLineOption{
runner.HeadlessPathPort("", r.port), runner.ExecPath(runner.LookChromeNames("headless_shell")),
runner.RemoteDebuggingPort(r.port),
runner.NoDefaultBrowserCheck,
runner.NoFirstRun,
runner.Headless,
}, opts...)...) }, opts...)...)
if err != nil { if err != nil {
defer r.Release() defer r.Release()

View File

@ -3,10 +3,11 @@
package runner package runner
const ( const (
// DefaultChromePath is the default path to the Chrome application. // 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` DefaultChromePath = `/Applications/Google Chrome.app/Contents/MacOS/Google Chrome`
) )
func findChromePath() string { // DefaultChromeNames are the default Chrome executable names to look for in
return DefaultChromePath // $PATH.
} var DefaultChromeNames []string

View File

@ -2,30 +2,18 @@
package runner package runner
import "os/exec"
const ( const (
// DefaultChromePath is the default path to the google-chrome executable if // DefaultChromePath is the default path to use for Chrome if the
// a variant cannot be found on $PATH. // executable is not in $PATH.
DefaultChromePath = "/usr/bin/google-chrome" DefaultChromePath = "/usr/bin/google-chrome"
) )
// chromeNames are the Chrome executable names to search for in the path. // DefaultChromeNames are the default Chrome executable names to look for in
var chromeNames = []string{ // $PATH.
var DefaultChromeNames = []string{
"google-chrome", "google-chrome",
"chromium-browser", "chromium-browser",
"chromium", "chromium",
"google-chrome-beta", "google-chrome-beta",
"google-chrome-unstable", "google-chrome-unstable",
} }
func findChromePath() string {
for _, p := range chromeNames {
path, err := exec.LookPath(p)
if err == nil {
return path
}
}
return DefaultChromePath
}

View File

@ -2,32 +2,12 @@
package runner package runner
import "os/exec"
const ( const (
// DefaultChromePath is the default path to use for Google Chrome if the // DefaultChromePath is the default path to use for Chrome if the
// executable is not in %PATH%. // executable is not in %PATH%.
DefaultChromePath = `C:\Program Files (x86)\Google\Chrome\Application\chrome.exe` DefaultChromePath = `C:\Program Files (x86)\Google\Chrome\Application\chrome.exe`
// DefaultEdgeDiagnosticsAdapterPath is the default path to use for the
// Microsoft Edge Diagnostics Adapter if the executable is not in %PATH%.
DefaultEdgeDiagnosticsAdapterPath = `c:\Edge\EdgeDiagnosticsAdapter\x64\EdgeDiagnosticsAdapter.exe`
) )
func findChromePath() string { // DefaultChromeNames are the default Chrome executable names to look for in
path, err := exec.LookPath(`chrome.exe`) // %PATH%.
if err == nil { var DefaultChromeNames = []string{`chrome.exe`}
return path
}
return DefaultChromePath
}
func findEdgePath() string {
path, err := exec.LookPath(`EdgeDiagnosticsAdapter.exe`)
if err == nil {
return path
}
return DefaultEdgeDiagnosticsAdapterPath
}

View File

@ -5,7 +5,6 @@ import (
"context" "context"
"fmt" "fmt"
"io/ioutil" "io/ioutil"
"net/url"
"os" "os"
"os/exec" "os/exec"
"regexp" "regexp"
@ -16,6 +15,11 @@ import (
"github.com/chromedp/chromedp/client" "github.com/chromedp/chromedp/client"
) )
const (
// DefaultUserDataDirPrefix is the default user data directory prefix.
DefaultUserDataDirPrefix = "chromedp-runner.%d."
)
// Error is a runner error. // Error is a runner error.
type Error string type Error string
@ -32,19 +36,17 @@ const (
// ErrAlreadyWaiting is the already waiting error. // ErrAlreadyWaiting is the already waiting error.
ErrAlreadyWaiting Error = "already waiting" ErrAlreadyWaiting Error = "already waiting"
// ErrInvalidURLs is the invalid url-opts error.
ErrInvalidURLOpts Error = "invalid url-opts"
// ErrInvalidCmdOpts is the invalid cmd-opts error. // ErrInvalidCmdOpts is the invalid cmd-opts error.
ErrInvalidCmdOpts Error = "invalid cmd-opts" ErrInvalidCmdOpts Error = "invalid cmd-opts"
// ErrInvalidProcessOpts is the invalid process-opts error. // ErrInvalidProcessOpts is the invalid process-opts error.
ErrInvalidProcessOpts Error = "invalid process-opts" ErrInvalidProcessOpts Error = "invalid process-opts"
// ErrExecPathNotSet is the exec-path not set set error. // ErrInvalidExecPath is the invalid exec-path error.
ErrExecPathNotSet Error = "exec-path command line option not set, or chrome executable not found in PATH" ErrInvalidExecPath Error = "invalid exec-path"
)
const (
// DefaultUserDataDirPrefix is the default user data directory prefix.
DefaultUserDataDirPrefix = "chromedp-runner.%d."
) )
// Runner holds information about a running Chrome process. // Runner holds information about a running Chrome process.
@ -59,19 +61,18 @@ type Runner struct {
func New(opts ...CommandLineOption) (*Runner, error) { func New(opts ...CommandLineOption) (*Runner, error) {
var err error var err error
cliOpts := map[string]interface{}{} cliOpts := make(map[string]interface{})
// apply opts // apply opts
for _, o := range opts { for _, o := range opts {
err = o(cliOpts) if err = o(cliOpts); err != nil {
if err != nil {
return nil, err return nil, err
} }
} }
// set default Chrome options if exec-path not provided // set default Chrome options if exec-path not provided
if _, ok := cliOpts["exec-path"]; !ok { if _, ok := cliOpts["exec-path"]; !ok {
cliOpts["exec-path"] = findChromePath() cliOpts["exec-path"] = LookChromeNames()
for k, v := range map[string]interface{}{ for k, v := range map[string]interface{}{
"no-first-run": true, "no-first-run": true,
"no-default-browser-check": true, "no-default-browser-check": true,
@ -86,8 +87,7 @@ func New(opts ...CommandLineOption) (*Runner, error) {
// add KillProcessGroup and ForceKill if no other cmd opts provided // add KillProcessGroup and ForceKill if no other cmd opts provided
if _, ok := cliOpts["cmd-opts"]; !ok { if _, ok := cliOpts["cmd-opts"]; !ok {
for _, o := range []CommandLineOption{KillProcessGroup, ForceKill} { for _, o := range []CommandLineOption{KillProcessGroup, ForceKill} {
err = o(cliOpts) if err = o(cliOpts); err != nil {
if err != nil {
return nil, err return nil, err
} }
} }
@ -104,9 +104,9 @@ var cliOptRE = regexp.MustCompile(`^[a-z0-9\-]+$`)
// buildOpts generates the command line options for Chrome. // buildOpts generates the command line options for Chrome.
func (r *Runner) buildOpts() []string { func (r *Runner) buildOpts() []string {
var opts []string var opts []string
var urls []string
// process options // process opts
var urlstr string
for k, v := range r.opts { for k, v := range r.opts {
if !cliOptRE.MatchString(k) || v == nil { if !cliOptRE.MatchString(k) || v == nil {
continue continue
@ -116,8 +116,8 @@ func (r *Runner) buildOpts() []string {
case "exec-path", "cmd-opts", "process-opts": case "exec-path", "cmd-opts", "process-opts":
continue continue
case "start-url": case "url-opts":
urlstr = v.(string) urls = v.([]string)
default: default:
switch z := v.(type) { switch z := v.(type) {
@ -135,16 +135,16 @@ func (r *Runner) buildOpts() []string {
} }
} }
if urlstr == "" { if urls == nil {
urlstr = "about:blank" urls = append(urls, "about:blank")
} }
return append(opts, urlstr) return append(opts, urls...)
} }
// Start starts a Chrome process using the specified context. The Chrome // Start starts a Chrome process using the specified context. The Chrome
// process can be terminated by closing the passed context. // process can be terminated by closing the passed context.
func (r *Runner) Start(ctxt context.Context) error { func (r *Runner) Start(ctxt context.Context, opts ...string) error {
var err error var err error
var ok bool var ok bool
@ -156,11 +156,6 @@ func (r *Runner) Start(ctxt context.Context) error {
return ErrAlreadyStarted return ErrAlreadyStarted
} }
// setup context
if ctxt == nil {
ctxt = context.Background()
}
// set user data dir, if not provided // set user data dir, if not provided
_, ok = r.opts["user-data-dir"] _, ok = r.opts["user-data-dir"]
if !ok { if !ok {
@ -172,36 +167,41 @@ func (r *Runner) Start(ctxt context.Context) error {
} }
} }
// ensure exec-path set // get exec path
execPath, ok := r.opts["exec-path"] var execPath string
if p, ok := r.opts["exec-path"]; ok {
execPath, ok = p.(string)
if !ok { if !ok {
return ErrExecPathNotSet return ErrInvalidExecPath
}
}
// ensure execPath is valid
if execPath == "" {
return ErrInvalidExecPath
} }
// create cmd // create cmd
r.cmd = exec.CommandContext(ctxt, execPath.(string), r.buildOpts()...) r.cmd = exec.CommandContext(ctxt, execPath, append(r.buildOpts(), opts...)...)
// apply cmd opts // apply cmd opts
if cmdOpts, ok := r.opts["cmd-opts"]; ok { if cmdOpts, ok := r.opts["cmd-opts"]; ok {
for _, co := range cmdOpts.([]func(*exec.Cmd) error) { for _, co := range cmdOpts.([]func(*exec.Cmd) error) {
err = co(r.cmd) if err = co(r.cmd); err != nil {
if err != nil {
return err return err
} }
} }
} }
// start process // start process
err = r.cmd.Start() if err = r.cmd.Start(); err != nil {
if err != nil {
return err return err
} }
// apply process opts // apply process opts
if processOpts, ok := r.opts["process-opts"]; ok { if processOpts, ok := r.opts["process-opts"]; ok {
for _, po := range processOpts.([]func(*os.Process) error) { for _, po := range processOpts.([]func(*os.Process) error) {
err = po(r.cmd.Process) if err = po(r.cmd.Process); err != nil {
if err != nil {
// TODO: do something better here, as we want to kill // TODO: do something better here, as we want to kill
// the child process, do cleanup, etc. // the child process, do cleanup, etc.
panic(err) panic(err)
@ -305,12 +305,6 @@ func (r *Runner) Client(opts ...client.Option) *client.Client {
)...) )...)
} }
// WatchPageTargets returns a channel that will receive new page targets as
// they are created.
func (r *Runner) WatchPageTargets(ctxt context.Context, opts ...client.Option) <-chan client.Target {
return r.Client(opts...).WatchPageTargets(ctxt)
}
// Run starts a new Chrome process runner, using the provided context and // Run starts a new Chrome process runner, using the provided context and
// command line options. // command line options.
func Run(ctxt context.Context, opts ...CommandLineOption) (*Runner, error) { func Run(ctxt context.Context, opts ...CommandLineOption) (*Runner, error) {
@ -323,20 +317,19 @@ func Run(ctxt context.Context, opts ...CommandLineOption) (*Runner, error) {
} }
// start // start
err = r.Start(ctxt) if err = r.Start(ctxt); err != nil {
if err != nil {
return nil, err return nil, err
} }
return r, nil return r, nil
} }
// CommandLineOption is a Chrome command line option. // CommandLineOption is a runner command line option.
// //
// see: http://peter.sh/experiments/chromium-command-line-switches/ // see: http://peter.sh/experiments/chromium-command-line-switches/
type CommandLineOption func(map[string]interface{}) error type CommandLineOption func(map[string]interface{}) error
// Flag is a generic Chrome command line option to pass a name=value flag to // Flag is a generic command line option to pass a name=value flag to
// Chrome. // Chrome.
func Flag(name string, value interface{}) CommandLineOption { func Flag(name string, value interface{}) CommandLineOption {
return func(m map[string]interface{}) error { return func(m map[string]interface{}) error {
@ -360,33 +353,12 @@ func Path(path string) CommandLineOption {
} }
} }
// HeadlessPathPort is the Chrome command line option to set the default // ExecPath is a command line option to set the exec path.
// settings for running the headless_shell executable. If path is empty, then
// an attempt will be made to find headless_shell on the path.
func HeadlessPathPort(path string, port int) CommandLineOption {
if path == "" {
path, _ = exec.LookPath("headless_shell")
}
return func(m map[string]interface{}) error {
m["exec-path"] = path
m["remote-debugging-port"] = port
m["headless"] = true
return nil
}
}
// ExecPath is a Chrome command line option to set the exec path.
func ExecPath(path string) CommandLineOption { func ExecPath(path string) CommandLineOption {
return Flag("exec-path", path) return Flag("exec-path", path)
} }
// Port is the Chrome command line option to set the remote debugging port. // UserDataDir is the command line option to set the user data dir.
func Port(port int) CommandLineOption {
return Flag("remote-debugging-port", port)
}
// UserDataDir is the Chrome command line option to set the user data dir.
// //
// Note: set this option to manually set the profile directory used by Chrome. // 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 // When this is not set, then a default path will be created in the /tmp
@ -395,27 +367,17 @@ func UserDataDir(dir string) CommandLineOption {
return Flag("user-data-dir", dir) return Flag("user-data-dir", dir)
} }
// StartURL is the Chrome command line option to set the initial URL. // ProxyServer is the command line option to set the outbound proxy server.
func StartURL(urlstr string) CommandLineOption { func ProxyServer(proxy string) CommandLineOption {
return Flag("start-url", urlstr)
}
// Proxy is the Chrome command line option to set the outbound proxy.
func Proxy(proxy string) CommandLineOption {
return Flag("proxy-server", proxy) return Flag("proxy-server", proxy)
} }
// ProxyPacURL is the Chrome command line option to set the URL of a proxy PAC file. // WindowSize is the command line option to set the initial window size.
func ProxyPacURL(pacURL url.URL) CommandLineOption {
return Flag("proxy-pac-url", pacURL.String())
}
// WindowSize is the Chrome command line option to set the initial window size.
func WindowSize(width, height int) CommandLineOption { func WindowSize(width, height int) CommandLineOption {
return Flag("window-size", fmt.Sprintf("%d,%d", width, height)) return Flag("window-size", fmt.Sprintf("%d,%d", width, height))
} }
// UserAgent is the Chrome command line option to set the default User-Agent // UserAgent is the command line option to set the default User-Agent
// header. // header.
func UserAgent(userAgent string) CommandLineOption { func UserAgent(userAgent string) CommandLineOption {
return Flag("user-agent", userAgent) return Flag("user-agent", userAgent)
@ -438,45 +400,83 @@ func NoDefaultBrowserCheck(m map[string]interface{}) error {
return Flag("no-default-browser-check", true)(m) return Flag("no-default-browser-check", true)(m)
} }
// DisableGPU is the Chrome command line option to disable the GPU process. // 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 { func DisableGPU(m map[string]interface{}) error {
return Flag("disable-gpu", true)(m) return Flag("disable-gpu", true)(m)
} }
// CmdOpt is a Chrome command line option to modify the underlying exec.Cmd // URL is the command line option to add a URL to open on process start.
// prior to invocation. //
// 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 { func CmdOpt(o func(*exec.Cmd) error) CommandLineOption {
return func(m map[string]interface{}) error { return func(m map[string]interface{}) error {
var opts []func(*exec.Cmd) error var opts []func(*exec.Cmd) error
if e, ok := m["cmd-opts"]; ok { if e, ok := m["cmd-opts"]; ok {
opts, ok = e.([]func(*exec.Cmd) error) opts, ok = e.([]func(*exec.Cmd) error)
if !ok { if !ok {
return ErrInvalidCmdOpts return ErrInvalidCmdOpts
} }
} }
m["cmd-opts"] = append(opts, o) m["cmd-opts"] = append(opts, o)
return nil return nil
} }
} }
// ProcessOpt is a Chrome command line option to modify the child os.Process // ProcessOpt is a command line option to modify the child os.Process
// after started exec.Cmd.Start. // after the call to exec.Cmd.Start in Run.
func ProcessOpt(o func(*os.Process) error) CommandLineOption { func ProcessOpt(o func(*os.Process) error) CommandLineOption {
return func(m map[string]interface{}) error { return func(m map[string]interface{}) error {
var opts []func(*os.Process) error var opts []func(*os.Process) error
if e, ok := m["process-opts"]; ok { if e, ok := m["process-opts"]; ok {
opts, ok = e.([]func(*os.Process) error) opts, ok = e.([]func(*os.Process) error)
if !ok { if !ok {
return ErrInvalidProcessOpts return ErrInvalidProcessOpts
} }
} }
m["process-opts"] = append(opts, o) m["process-opts"] = append(opts, o)
return nil 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
}