diff --git a/cdp/runtime/types.go b/cdp/runtime/types.go index 5380e02..4c1c785 100644 --- a/cdp/runtime/types.go +++ b/cdp/runtime/types.go @@ -2,6 +2,7 @@ package runtime import ( "errors" + "fmt" "github.com/mailru/easyjson" "github.com/mailru/easyjson/jlexer" @@ -181,6 +182,13 @@ type ExceptionDetails struct { ExecutionContextID ExecutionContextID `json:"executionContextId,omitempty"` // Identifier of the context where exception happened. } +// Error satisfies the error interface. +func (e *ExceptionDetails) Error() string { + // TODO: watch script parsed events and match the ExceptionDetails.ScriptID + // to the name/location of the actual code and display here + return fmt.Sprintf("encountered exception '%s' (%d:%d)", e.Text, e.LineNumber, e.ColumnNumber) +} + // CallFrame stack entry for runtime errors and assertions. type CallFrame struct { FunctionName string `json:"functionName,omitempty"` // JavaScript function name. diff --git a/cmd/chromedp-gen/fixup/fixup.go b/cmd/chromedp-gen/fixup/fixup.go index 1f0176e..fd54611 100644 --- a/cmd/chromedp-gen/fixup/fixup.go +++ b/cmd/chromedp-gen/fixup/fixup.go @@ -56,6 +56,7 @@ const ( // - add special unmarshaler to NodeId, BackendNodeId, FrameId to handle values from older (v1.1) protocol versions. -- NOTE: this might need to be applied to more types, such as network.LoaderId // - rename 'Input.GestureSourceType' -> 'Input.GestureType'. // - rename CSS.CSS* types. +// - add Error() method to 'Runtime.ExceptionDetails' type so that it can be used as error. func FixupDomains(domains []*internal.Domain) { // method type methodType := &internal.Type{ @@ -309,9 +310,14 @@ func FixupDomains(domains []*internal.Domain) { case internal.DomainRuntime: var types []*internal.Type for _, t := range d.Types { - if t.ID == "Timestamp" { + switch t.ID { + case "Timestamp": continue + + case "ExceptionDetails": + t.Extra += templates.ExtraExceptionDetailsTemplate() } + types = append(types, t) } d.Types = types diff --git a/cmd/chromedp-gen/templates/domain.qtpl b/cmd/chromedp-gen/templates/domain.qtpl index 9d94f9a..6759800 100644 --- a/cmd/chromedp-gen/templates/domain.qtpl +++ b/cmd/chromedp-gen/templates/domain.qtpl @@ -4,7 +4,7 @@ // DomainTemplate is the template for a single domain. {% func DomainTemplate(d *internal.Domain, domains []*internal.Domain) %} -{%s= FileImportTemplate(map[string]string{ +{%s= FileImportTemplate(map[string]string{ *internal.FlagPkg: "cdp", }) %} {% for _, c := range d.Commands %} diff --git a/cmd/chromedp-gen/templates/extra.qtpl b/cmd/chromedp-gen/templates/extra.qtpl index b84f6ab..3d137f8 100644 --- a/cmd/chromedp-gen/templates/extra.qtpl +++ b/cmd/chromedp-gen/templates/extra.qtpl @@ -195,6 +195,17 @@ func (t *{%s= typ %}) UnmarshalJSON(buf []byte) error { } {% endfunc %} +// ExtraExceptionDetailsTemplate is a special template for the Runtime.ExceptionDetails type that +// defines the standard error interface. +{% func ExtraExceptionDetailsTemplate() %} +// Error satisfies the error interface. +func (e *ExceptionDetails) Error() string { + // TODO: watch script parsed events and match the ExceptionDetails.ScriptID + // to the name/location of the actual code and display here + return fmt.Sprintf("encountered exception '%s' (%d:%d)", e.Text, e.LineNumber, e.ColumnNumber) +} +{% endfunc %} + // ExtraCDPTypes is the template for additional internal type // declarations. {% func ExtraCDPTypes() %} diff --git a/cmd/chromedp-gen/templates/extra.qtpl.go b/cmd/chromedp-gen/templates/extra.qtpl.go index 9302135..8df8d6e 100644 --- a/cmd/chromedp-gen/templates/extra.qtpl.go +++ b/cmd/chromedp-gen/templates/extra.qtpl.go @@ -420,12 +420,55 @@ func ExtraFixStringUnmarshaler(typ, parseFunc, extra string) string { //line templates/extra.qtpl:196 } +// ExtraExceptionDetailsTemplate is a special template for the Runtime.ExceptionDetails type that +// defines the standard error interface. + +//line templates/extra.qtpl:200 +func StreamExtraExceptionDetailsTemplate(qw422016 *qt422016.Writer) { + //line templates/extra.qtpl:200 + qw422016.N().S(` +// Error satisfies the error interface. +func (e *ExceptionDetails) Error() string { + // TODO: watch script parsed events and match the ExceptionDetails.ScriptID + // to the name/location of the actual code and display here + return fmt.Sprintf("encountered exception '%s' (%d:%d)", e.Text, e.LineNumber, e.ColumnNumber) +} +`) +//line templates/extra.qtpl:207 +} + +//line templates/extra.qtpl:207 +func WriteExtraExceptionDetailsTemplate(qq422016 qtio422016.Writer) { + //line templates/extra.qtpl:207 + qw422016 := qt422016.AcquireWriter(qq422016) + //line templates/extra.qtpl:207 + StreamExtraExceptionDetailsTemplate(qw422016) + //line templates/extra.qtpl:207 + qt422016.ReleaseWriter(qw422016) +//line templates/extra.qtpl:207 +} + +//line templates/extra.qtpl:207 +func ExtraExceptionDetailsTemplate() string { + //line templates/extra.qtpl:207 + qb422016 := qt422016.AcquireByteBuffer() + //line templates/extra.qtpl:207 + WriteExtraExceptionDetailsTemplate(qb422016) + //line templates/extra.qtpl:207 + qs422016 := string(qb422016.B) + //line templates/extra.qtpl:207 + qt422016.ReleaseByteBuffer(qb422016) + //line templates/extra.qtpl:207 + return qs422016 +//line templates/extra.qtpl:207 +} + // ExtraCDPTypes is the template for additional internal type // declarations. -//line templates/extra.qtpl:200 +//line templates/extra.qtpl:211 func StreamExtraCDPTypes(qw422016 *qt422016.Writer) { - //line templates/extra.qtpl:200 + //line templates/extra.qtpl:211 qw422016.N().S(` // Error satisfies the error interface. @@ -448,49 +491,49 @@ type FrameHandler interface { // Empty is an empty JSON object message. var Empty = easyjson.RawMessage(`) - //line templates/extra.qtpl:200 + //line templates/extra.qtpl:211 qw422016.N().S("`") - //line templates/extra.qtpl:200 + //line templates/extra.qtpl:211 qw422016.N().S(`{}`) - //line templates/extra.qtpl:200 + //line templates/extra.qtpl:211 qw422016.N().S("`") - //line templates/extra.qtpl:200 + //line templates/extra.qtpl:211 qw422016.N().S(`) `) -//line templates/extra.qtpl:222 +//line templates/extra.qtpl:233 } -//line templates/extra.qtpl:222 +//line templates/extra.qtpl:233 func WriteExtraCDPTypes(qq422016 qtio422016.Writer) { - //line templates/extra.qtpl:222 + //line templates/extra.qtpl:233 qw422016 := qt422016.AcquireWriter(qq422016) - //line templates/extra.qtpl:222 + //line templates/extra.qtpl:233 StreamExtraCDPTypes(qw422016) - //line templates/extra.qtpl:222 + //line templates/extra.qtpl:233 qt422016.ReleaseWriter(qw422016) -//line templates/extra.qtpl:222 +//line templates/extra.qtpl:233 } -//line templates/extra.qtpl:222 +//line templates/extra.qtpl:233 func ExtraCDPTypes() string { - //line templates/extra.qtpl:222 + //line templates/extra.qtpl:233 qb422016 := qt422016.AcquireByteBuffer() - //line templates/extra.qtpl:222 + //line templates/extra.qtpl:233 WriteExtraCDPTypes(qb422016) - //line templates/extra.qtpl:222 + //line templates/extra.qtpl:233 qs422016 := string(qb422016.B) - //line templates/extra.qtpl:222 + //line templates/extra.qtpl:233 qt422016.ReleaseByteBuffer(qb422016) - //line templates/extra.qtpl:222 + //line templates/extra.qtpl:233 return qs422016 -//line templates/extra.qtpl:222 +//line templates/extra.qtpl:233 } // ExtraUtilTemplate generates the decode func for the Message type. -//line templates/extra.qtpl:225 +//line templates/extra.qtpl:236 func StreamExtraUtilTemplate(qw422016 *qt422016.Writer, domains []*internal.Domain) { - //line templates/extra.qtpl:225 + //line templates/extra.qtpl:236 qw422016.N().S(` type empty struct{} var emptyVal = &empty{} @@ -499,66 +542,66 @@ var emptyVal = &empty{} func UnmarshalMessage(msg *cdp.Message) (interface{}, error) { var v easyjson.Unmarshaler switch msg.Method {`) - //line templates/extra.qtpl:232 + //line templates/extra.qtpl:243 for _, d := range domains { - //line templates/extra.qtpl:232 + //line templates/extra.qtpl:243 for _, c := range d.Commands { - //line templates/extra.qtpl:232 + //line templates/extra.qtpl:243 qw422016.N().S(` case cdp.`) - //line templates/extra.qtpl:233 + //line templates/extra.qtpl:244 qw422016.N().S(c.CommandMethodType(d)) - //line templates/extra.qtpl:233 + //line templates/extra.qtpl:244 qw422016.N().S(`:`) - //line templates/extra.qtpl:233 + //line templates/extra.qtpl:244 if len(c.Returns) == 0 { - //line templates/extra.qtpl:233 + //line templates/extra.qtpl:244 qw422016.N().S(` return emptyVal, nil`) - //line templates/extra.qtpl:234 + //line templates/extra.qtpl:245 } else { - //line templates/extra.qtpl:234 + //line templates/extra.qtpl:245 qw422016.N().S(` v = new(`) - //line templates/extra.qtpl:235 + //line templates/extra.qtpl:246 qw422016.N().S(d.PackageRefName()) - //line templates/extra.qtpl:235 + //line templates/extra.qtpl:246 qw422016.N().S(`.`) - //line templates/extra.qtpl:235 + //line templates/extra.qtpl:246 qw422016.N().S(c.CommandReturnsType()) - //line templates/extra.qtpl:235 + //line templates/extra.qtpl:246 qw422016.N().S(`)`) - //line templates/extra.qtpl:235 + //line templates/extra.qtpl:246 } - //line templates/extra.qtpl:235 + //line templates/extra.qtpl:246 qw422016.N().S(` `) - //line templates/extra.qtpl:236 + //line templates/extra.qtpl:247 } - //line templates/extra.qtpl:236 + //line templates/extra.qtpl:247 for _, e := range d.Events { - //line templates/extra.qtpl:236 + //line templates/extra.qtpl:247 qw422016.N().S(` case cdp.`) - //line templates/extra.qtpl:237 + //line templates/extra.qtpl:248 qw422016.N().S(e.EventMethodType(d)) - //line templates/extra.qtpl:237 + //line templates/extra.qtpl:248 qw422016.N().S(`: v = new(`) - //line templates/extra.qtpl:238 + //line templates/extra.qtpl:249 qw422016.N().S(d.PackageRefName()) - //line templates/extra.qtpl:238 + //line templates/extra.qtpl:249 qw422016.N().S(`.`) - //line templates/extra.qtpl:238 + //line templates/extra.qtpl:249 qw422016.N().S(e.EventType()) - //line templates/extra.qtpl:238 + //line templates/extra.qtpl:249 qw422016.N().S(`) `) - //line templates/extra.qtpl:239 + //line templates/extra.qtpl:250 } - //line templates/extra.qtpl:239 + //line templates/extra.qtpl:250 } - //line templates/extra.qtpl:239 + //line templates/extra.qtpl:250 qw422016.N().S(`} var buf easyjson.RawMessage @@ -581,69 +624,69 @@ func UnmarshalMessage(msg *cdp.Message) (interface{}, error) { return v, nil } `) -//line templates/extra.qtpl:260 +//line templates/extra.qtpl:271 } -//line templates/extra.qtpl:260 +//line templates/extra.qtpl:271 func WriteExtraUtilTemplate(qq422016 qtio422016.Writer, domains []*internal.Domain) { - //line templates/extra.qtpl:260 + //line templates/extra.qtpl:271 qw422016 := qt422016.AcquireWriter(qq422016) - //line templates/extra.qtpl:260 + //line templates/extra.qtpl:271 StreamExtraUtilTemplate(qw422016, domains) - //line templates/extra.qtpl:260 + //line templates/extra.qtpl:271 qt422016.ReleaseWriter(qw422016) -//line templates/extra.qtpl:260 +//line templates/extra.qtpl:271 } -//line templates/extra.qtpl:260 +//line templates/extra.qtpl:271 func ExtraUtilTemplate(domains []*internal.Domain) string { - //line templates/extra.qtpl:260 + //line templates/extra.qtpl:271 qb422016 := qt422016.AcquireByteBuffer() - //line templates/extra.qtpl:260 + //line templates/extra.qtpl:271 WriteExtraUtilTemplate(qb422016, domains) - //line templates/extra.qtpl:260 + //line templates/extra.qtpl:271 qs422016 := string(qb422016.B) - //line templates/extra.qtpl:260 + //line templates/extra.qtpl:271 qt422016.ReleaseByteBuffer(qb422016) - //line templates/extra.qtpl:260 + //line templates/extra.qtpl:271 return qs422016 -//line templates/extra.qtpl:260 +//line templates/extra.qtpl:271 } -//line templates/extra.qtpl:262 +//line templates/extra.qtpl:273 func StreamExtraMethodTypeDomainDecoder(qw422016 *qt422016.Writer) { - //line templates/extra.qtpl:262 + //line templates/extra.qtpl:273 qw422016.N().S(` // Domain returns the Chrome Debugging Protocol domain of the event or command. func (t MethodType) Domain() string { return string(t[:strings.IndexByte(string(t), '.')]) } `) -//line templates/extra.qtpl:267 +//line templates/extra.qtpl:278 } -//line templates/extra.qtpl:267 +//line templates/extra.qtpl:278 func WriteExtraMethodTypeDomainDecoder(qq422016 qtio422016.Writer) { - //line templates/extra.qtpl:267 + //line templates/extra.qtpl:278 qw422016 := qt422016.AcquireWriter(qq422016) - //line templates/extra.qtpl:267 + //line templates/extra.qtpl:278 StreamExtraMethodTypeDomainDecoder(qw422016) - //line templates/extra.qtpl:267 + //line templates/extra.qtpl:278 qt422016.ReleaseWriter(qw422016) -//line templates/extra.qtpl:267 +//line templates/extra.qtpl:278 } -//line templates/extra.qtpl:267 +//line templates/extra.qtpl:278 func ExtraMethodTypeDomainDecoder() string { - //line templates/extra.qtpl:267 + //line templates/extra.qtpl:278 qb422016 := qt422016.AcquireByteBuffer() - //line templates/extra.qtpl:267 + //line templates/extra.qtpl:278 WriteExtraMethodTypeDomainDecoder(qb422016) - //line templates/extra.qtpl:267 + //line templates/extra.qtpl:278 qs422016 := string(qb422016.B) - //line templates/extra.qtpl:267 + //line templates/extra.qtpl:278 qt422016.ReleaseByteBuffer(qb422016) - //line templates/extra.qtpl:267 + //line templates/extra.qtpl:278 return qs422016 -//line templates/extra.qtpl:267 +//line templates/extra.qtpl:278 } diff --git a/eval.go b/eval.go new file mode 100644 index 0000000..fc9132c --- /dev/null +++ b/eval.go @@ -0,0 +1,93 @@ +package chromedp + +import ( + "context" + "encoding/json" + + "github.com/knq/chromedp/cdp" + rundom "github.com/knq/chromedp/cdp/runtime" +) + +// Evaluate evaluates the supplied Javascript expression, attempting to +// unmarshal the resulting value into res. +// +// If res is a **chromedp/cdp/runtime.RemoteObject, then it will be set to the +// raw, returned RuntimeObject, Otherwise, the result value be json.Unmarshal'd +// to res. +func Evaluate(expression string, res interface{}, opts ...EvaluateOption) Action { + if res == nil { + panic("res cannot be nil") + } + + return ActionFunc(func(ctxt context.Context, h cdp.FrameHandler) error { + var err error + + // check if we want a 'raw' result + obj, raw := res.(**rundom.RemoteObject) + + // set up parameters + p := rundom.Evaluate(expression) + if !raw { + p = p.WithReturnByValue(true) + } + + // apply opts + for _, o := range opts { + p = o(p) + } + + // evaluate + v, exp, err := p.Do(ctxt, h) + if err != nil { + return err + } + if exp != nil { + return exp + } + + if raw { + *obj = v + return nil + } + + // unmarshal + return json.Unmarshal(v.Value, res) + }) +} + +// EvaluateOption is an Evaluate call option. +type EvaluateOption func(*rundom.EvaluateParams) *rundom.EvaluateParams + +// EvalObjectGroup is a Evaluate option to set the object group. +func EvalObjectGroup(objectGroup string) EvaluateOption { + return func(p *rundom.EvaluateParams) *rundom.EvaluateParams { + return p.WithObjectGroup(objectGroup) + } +} + +// EvalWithCommandLineAPI is an Evaluate option to include the DevTools Command +// Line API. +// +// Note: this should not be used with any untrusted code. +func EvalWithCommandLineAPI(p *rundom.EvaluateParams) *rundom.EvaluateParams { + return p.WithIncludeCommandLineAPI(true) +} + +// EvalSilent is a Evaluate option that will cause script evaluation to ignore +// exceptions. +func EvalSilent(p *rundom.EvaluateParams) *rundom.EvaluateParams { + return p.WithSilent(true) +} + +// EvalAsValue is a Evaluate option that will case the script to encode its +// result as a value. +func EvalAsValue(p *rundom.EvaluateParams) *rundom.EvaluateParams { + return p.WithReturnByValue(true) +} + +// EvaluateAsDevTools evaluates a Javascript expression in the same +// +// Note: this should not be used with any untrusted code. +func EvaluateAsDevTools(expression string) Action { + return Evaluate(expression, EvalObjectGroup("console"), EvalWithCommandLineAPI) +} diff --git a/examples/eval/.gitignore b/examples/eval/.gitignore new file mode 100644 index 0000000..13772a9 --- /dev/null +++ b/examples/eval/.gitignore @@ -0,0 +1,2 @@ +eval +eval.exe diff --git a/examples/eval/main.go b/examples/eval/main.go new file mode 100644 index 0000000..245d2a8 --- /dev/null +++ b/examples/eval/main.go @@ -0,0 +1,47 @@ +package main + +import ( + "context" + "log" + + cdp "github.com/knq/chromedp" +) + +func main() { + var err error + + // create context + ctxt, cancel := context.WithCancel(context.Background()) + defer cancel() + + // create chrome instance + c, err := cdp.New(ctxt) + if err != nil { + log.Fatal(err) + } + + // run task list + var res []string + err = c.Run(ctxt, cdp.Tasks{ + cdp.Navigate(`https://www.brank.as`), + cdp.WaitVisible(`#footer`, cdp.ByID), + cdp.Evaluate(`Object.keys(window);`, &res), + }) + if err != nil { + log.Fatal(err) + } + + // shutdown chrome + err = c.Shutdown(ctxt) + if err != nil { + log.Fatal(err) + } + + // wait for chrome to finish + err = c.Wait() + if err != nil { + log.Fatal(err) + } + + log.Printf("window object keys: %v", res) +} diff --git a/nav.go b/nav.go index 088a217..7b7933c 100644 --- a/nav.go +++ b/nav.go @@ -4,7 +4,6 @@ import ( "context" "errors" "fmt" - "log" "github.com/knq/chromedp/cdp" "github.com/knq/chromedp/cdp/page" @@ -121,25 +120,6 @@ func Stop() Action { return page.StopLoading() } -// Evaluate evaluates a script. -func Evaluate(expression string, res **rundom.RemoteObject) Action { - if res == nil { - panic("res cannot be nil") - } - - return ActionFunc(func(ctxt context.Context, h cdp.FrameHandler) error { - v, exp, err := rundom.Evaluate(expression).Do(ctxt, h) - if err != nil { - return err - } - if exp != nil { - log.Printf(">>> GOT EXECPTION: %v", exp) - } - *res = v - return nil - }) -} - // Location retrieves the URL location. func Location(urlstr *string) Action { if urlstr == nil { @@ -151,7 +131,7 @@ func Location(urlstr *string) Action { return err } if exp != nil { - return fmt.Errorf("got exception evaluating script: %#v", exp) + return exp } if res.Type != rundom.TypeString || len(res.Value) < 2 { return fmt.Errorf("expected string of at least length 2, got %s length %d", res.Subtype, len(res.Value)) diff --git a/query.go b/query.go index 9e19f9c..c45d6fc 100644 --- a/query.go +++ b/query.go @@ -142,7 +142,7 @@ func Value(sel interface{}, value *string, opts ...QueryOption) Action { return err } if exp != nil { - return fmt.Errorf("got exception evaluating script: %#v", exp) + return exp } if res.Type != rundom.TypeString || len(res.Value) < 2 { return fmt.Errorf("expected string of at least length 2, got %s length %d", res.Subtype, len(res.Value)) @@ -170,7 +170,7 @@ func SetValue(sel interface{}, value string, opts ...QueryOption) Action { return err } if exp != nil { - return fmt.Errorf("got exception evaluating script: %#v", exp) + return exp } if res.Type != rundom.TypeString || len(res.Value) < 2 { return fmt.Errorf("expected string of at least length 2, got %s length %d", res.Subtype, len(res.Value)) @@ -199,7 +199,7 @@ func Text(sel interface{}, text *string, opts ...QueryOption) Action { return err } if exp != nil { - return fmt.Errorf("got exception evaluating script: %#v", exp) + return exp } if res.Type != rundom.TypeString || len(res.Value) < 2 { return fmt.Errorf("expected string of at least length 2, got %s length %d", res.Subtype, len(res.Value)) @@ -378,7 +378,7 @@ func Screenshot(sel interface{}, picbuf *[]byte, opts ...QueryOption) Action { return err } if exp != nil { - return fmt.Errorf("got exception evaluating script: %#v", exp) + return exp } if res.Type != rundom.TypeString || len(res.Value) < 2 { return fmt.Errorf("expected string of at least length 2, got %s length %d", res.Subtype, len(res.Value)) @@ -438,7 +438,7 @@ func Submit(sel interface{}, opts ...QueryOption) Action { return err } if exp != nil { - return fmt.Errorf("got exception evaluating script: %#v", exp) + return exp } if res.Type != rundom.TypeString || len(res.Value) < 2 { return fmt.Errorf("expected string of at least length 2, got %s length %d", res.Subtype, len(res.Value))