chromedp/handler.go
2017-01-26 14:28:34 +07:00

660 lines
14 KiB
Go

package chromedp
import (
"context"
"fmt"
"log"
"reflect"
"runtime"
"strings"
"sync"
"time"
"github.com/mailru/easyjson"
"github.com/knq/chromedp/cdp"
"github.com/knq/chromedp/cdp/css"
"github.com/knq/chromedp/cdp/dom"
"github.com/knq/chromedp/cdp/inspector"
logdom "github.com/knq/chromedp/cdp/log"
"github.com/knq/chromedp/cdp/page"
rundom "github.com/knq/chromedp/cdp/runtime"
"github.com/knq/chromedp/client"
)
var (
_ cdp.FrameHandler = &TargetHandler{}
)
// TargetHandler manages a Chrome Debugging Protocol target.
type TargetHandler struct {
conn client.Transport
// frames is the set of encountered frames.
frames map[cdp.FrameID]*cdp.Frame
// cur is the current top level frame.
cur *cdp.Frame
// qcmd is the outgoing message queue.
qcmd chan *cdp.Message
// qres is the incoming command result queue.
qres chan *cdp.Message
// qevents is the incoming event queue.
qevents chan *cdp.Message
// detached is closed when the detached event is received.
detached chan *inspector.EventDetached
pageWaitGroup, domWaitGroup *sync.WaitGroup
// last is the last sent message identifier.
last int64
lastm sync.Mutex
// res is the id->result channel map.
res map[int64]chan interface{}
resrw sync.RWMutex
sync.RWMutex
}
// NewTargetHandler creates a new manager for the specified client target.
func NewTargetHandler(t client.Target) (*TargetHandler, error) {
conn, err := client.Dial(t)
if err != nil {
return nil, err
}
return &TargetHandler{conn: conn}, nil
}
// Run starts the processing of commands and events to the client target
// provided to NewTargetHandler.
//
// Callers can stop Run by closing the passed context.
func (h *TargetHandler) Run(ctxt context.Context) error {
var err error
// reset
h.Lock()
h.frames = make(map[cdp.FrameID]*cdp.Frame)
h.qcmd = make(chan *cdp.Message)
h.qres = make(chan *cdp.Message)
h.qevents = make(chan *cdp.Message)
h.res = make(map[int64]chan interface{})
h.detached = make(chan *inspector.EventDetached)
h.pageWaitGroup = new(sync.WaitGroup)
h.domWaitGroup = new(sync.WaitGroup)
h.Unlock()
// run
go h.run(ctxt)
// enable domains
for _, a := range []Action{
logdom.Enable(),
rundom.Enable(),
//network.Enable(),
inspector.Enable(),
page.Enable(),
dom.Enable(),
css.Enable(),
} {
err = a.Do(ctxt, h)
if err != nil {
return fmt.Errorf("unable to execute %s, got: %v", reflect.TypeOf(a), err)
}
}
h.Lock()
// get page resources
tree, err := page.GetResourceTree().Do(ctxt, h)
if err != nil {
return fmt.Errorf("unable to get resource tree, got: %v", err)
}
h.frames[tree.Frame.ID] = tree.Frame
h.cur = tree.Frame
for _, c := range tree.ChildFrames {
h.frames[c.Frame.ID] = c.Frame
}
h.Unlock()
h.documentUpdated(ctxt)
return nil
}
// run handles the actual message processing to / from the web socket connection.
func (h *TargetHandler) run(ctxt context.Context) {
defer h.conn.Close()
// add cancel to context
ctxt, cancel := context.WithCancel(ctxt)
defer cancel()
go func() {
defer cancel()
for {
select {
default:
msg, err := h.read()
if err != nil {
return
}
switch {
case msg.Method != "":
h.qevents <- msg
case msg.ID != 0:
h.qres <- msg
default:
log.Printf("ignoring malformed incoming message (missing id or method): %#v", msg)
}
case <-h.detached:
// FIXME: should log when detached, and reason
return
case <-ctxt.Done():
return
}
}
}()
var err error
// process queues
for {
select {
case ev := <-h.qevents:
err = h.processEvent(ctxt, ev)
if err != nil {
log.Printf("could not process event, got: %v", err)
}
case res := <-h.qres:
err = h.processResult(res)
if err != nil {
log.Printf("could not process command result, got: %v", err)
}
case cmd := <-h.qcmd:
err = h.processCommand(cmd)
if err != nil {
log.Printf("could not process command, got: %v", err)
}
case <-ctxt.Done():
return
}
}
}
// read reads a message from the client connection.
func (h *TargetHandler) read() (*cdp.Message, error) {
// read
buf, err := h.conn.Read()
if err != nil {
return nil, err
}
log.Printf("-> %s", string(buf))
// unmarshal
msg := new(cdp.Message)
err = easyjson.Unmarshal(buf, msg)
if err != nil {
return nil, err
}
return msg, nil
}
// processEvent processes an incoming event.
func (h *TargetHandler) processEvent(ctxt context.Context, msg *cdp.Message) error {
if msg == nil {
return cdp.ErrChannelClosed
}
// unmarshal
ev, err := UnmarshalMessage(msg)
if err != nil {
return err
}
switch e := ev.(type) {
case *inspector.EventDetached:
h.Lock()
defer h.Unlock()
h.detached <- e
return nil
case *dom.EventDocumentUpdated:
h.domWaitGroup.Wait()
go h.documentUpdated(ctxt)
return nil
}
d := msg.Method.Domain()
if d != "Page" && d != "DOM" {
return nil
}
switch d {
case "Page":
h.pageWaitGroup.Add(1)
go h.pageEvent(ctxt, ev)
case "DOM":
h.domWaitGroup.Add(1)
go h.domEvent(ctxt, ev)
}
return nil
}
// documentUpdated handles the document updated event, retrieving the document
// root for the root frame.
func (h *TargetHandler) documentUpdated(ctxt context.Context) {
f, err := h.WaitFrame(ctxt, emptyFrameID)
if err != nil {
log.Printf("could not get current frame, got: %v", err)
return
}
f.Lock()
defer f.Unlock()
// invalidate nodes
if f.Root != nil {
close(f.Root.Invalidated)
}
f.Nodes = make(map[cdp.NodeID]*cdp.Node)
f.Root, err = dom.GetDocument().WithPierce(true).Do(ctxt, h)
if err != nil {
log.Printf("error could not retrieve document root for %s, got: %v", f.ID, err)
return
}
f.Root.Invalidated = make(chan struct{})
walk(f.Nodes, f.Root)
}
// processResult processes an incoming command result.
func (h *TargetHandler) processResult(msg *cdp.Message) error {
h.resrw.Lock()
defer h.resrw.Unlock()
res, ok := h.res[msg.ID]
if !ok {
panic(fmt.Sprintf("expected result to be present for message id %d", msg.ID))
}
if msg.Error != nil {
res <- msg.Error
} else {
res <- msg.Result
}
delete(h.res, msg.ID)
return nil
}
// processCommand writes a command to the client connection.
func (h *TargetHandler) processCommand(cmd *cdp.Message) error {
// FIXME: there are two possible error conditions here, check and
// do some kind of logging ...
buf, err := easyjson.Marshal(cmd)
if err != nil {
return err
}
log.Printf("<- %s", string(buf))
// write
return h.conn.Write(buf)
}
// Execute executes commandType against the endpoint passed to Run, using the
// provided context and the raw JSON encoded params.
//
// Returns a result channel that will receive AT MOST ONE result. A result is
// either the command's result value (as a raw JSON encoded value), or any
// error encountered during operation. After the result (or an error) is passed
// to the returned channel, the channel will be closed.
//
// Note: the returned channel will be closed after the result is read. If the
// passed context finishes prior to receiving the command result, then
// ErrContextDone will be sent to the channel.
func (h *TargetHandler) Execute(ctxt context.Context, commandType cdp.MethodType, params easyjson.RawMessage) <-chan interface{} {
ch := make(chan interface{}, 1)
go func() {
defer close(ch)
res := make(chan interface{}, 1)
defer close(res)
// get next id
h.lastm.Lock()
h.last++
id := h.last
h.lastm.Unlock()
// save channel
h.resrw.Lock()
h.res[id] = res
h.resrw.Unlock()
h.qcmd <- &cdp.Message{
ID: id,
Method: commandType,
Params: params,
}
select {
case v := <-res:
if v != nil {
ch <- v
} else {
ch <- cdp.ErrChannelClosed
}
case <-ctxt.Done():
ch <- cdp.ErrContextDone
}
}()
return ch
}
// Listen adds a listener for the specified event types to the appropriate
// domain.
func (h *TargetHandler) Listen(eventTypes ...cdp.MethodType) <-chan interface{} {
return nil
}
// GetRoot returns the current top level frame's root document node.
func (h *TargetHandler) GetRoot(ctxt context.Context) (*cdp.Node, error) {
// TODO: fix this
ctxt, cancel := context.WithTimeout(ctxt, 10*time.Second)
defer cancel()
var root *cdp.Node
loop:
for {
var cur *cdp.Frame
select {
default:
h.RLock()
cur = h.cur
if cur != nil {
cur.RLock()
root = cur.Root
cur.RUnlock()
}
h.RUnlock()
if cur != nil && root != nil {
break loop
}
time.Sleep(DefaultCheckDuration)
case <-ctxt.Done():
return nil, ctxt.Err()
}
}
return root, nil
}
// 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 {
return err
}
h.Lock()
defer h.Unlock()
h.cur = f
return nil
}
// 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)
loop:
for {
select {
default:
var f *cdp.Frame
var ok bool
h.RLock()
if id == emptyFrameID {
f, ok = h.cur, h.cur != nil
} else {
f, ok = h.frames[id]
}
h.RUnlock()
if ok {
return f, nil
}
time.Sleep(DefaultCheckDuration)
case <-ctxt.Done():
return nil, cdp.ErrContextDone
case <-timeout:
break loop
}
}
return nil, fmt.Errorf("timeout waiting for frame `%s`", id)
}
// 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)
loop:
for {
select {
default:
var n *cdp.Node
var ok bool
f.RLock()
n, ok = f.Nodes[id]
f.RUnlock()
if n != nil && ok {
return n, nil
}
time.Sleep(DefaultCheckDuration)
case <-ctxt.Done():
return nil, cdp.ErrContextDone
case <-timeout:
break loop
}
}
return nil, fmt.Errorf("timeout waiting for node `%d`", id)
}
// pageEvent handles incoming page events.
func (h *TargetHandler) pageEvent(ctxt context.Context, ev interface{}) {
defer h.pageWaitGroup.Done()
var id cdp.FrameID
var op FrameOp
switch e := ev.(type) {
case *page.EventFrameNavigated:
h.Lock()
h.frames[e.Frame.ID] = e.Frame
h.Unlock()
return
case *page.EventFrameAttached:
id, op = e.FrameID, frameAttached(e.ParentFrameID)
case *page.EventFrameDetached:
id, op = e.FrameID, frameDetached
case *page.EventFrameStartedLoading:
id, op = e.FrameID, frameStartedLoading
case *page.EventFrameStoppedLoading:
id, op = e.FrameID, frameStoppedLoading
case *page.EventFrameScheduledNavigation:
id, op = e.FrameID, frameScheduledNavigation
case *page.EventFrameClearedScheduledNavigation:
id, op = e.FrameID, frameClearedScheduledNavigation
case *page.EventDomContentEventFired:
return
case *page.EventLoadEventFired:
return
case *page.EventFrameResized:
return
default:
panic(fmt.Sprintf("unhandled page event %s", reflect.TypeOf(ev)))
}
f, err := h.WaitFrame(ctxt, id)
if err != nil {
log.Printf("error could not get frame %s, got: %v", id, err)
return
}
h.Lock()
defer h.Unlock()
f.Lock()
defer f.Unlock()
op(f)
}
// domEvent handles incoming DOM events.
func (h *TargetHandler) domEvent(ctxt context.Context, ev interface{}) {
defer h.domWaitGroup.Done()
// wait current frame
f, err := h.WaitFrame(ctxt, emptyFrameID)
if err != nil {
log.Printf("error processing DOM event %s: error waiting for frame, got: %v", reflect.TypeOf(ev), err)
return
}
var id cdp.NodeID
var op NodeOp
switch e := ev.(type) {
case *dom.EventSetChildNodes:
id, op = e.ParentID, setChildNodes(f.Nodes, e.Nodes)
case *dom.EventAttributeModified:
id, op = e.NodeID, attributeModified(e.Name, e.Value)
case *dom.EventAttributeRemoved:
id, op = e.NodeID, attributeRemoved(e.Name)
case *dom.EventInlineStyleInvalidated:
id, op = e.NodeIds[0], inlineStyleInvalidated(e.NodeIds[1:])
case *dom.EventCharacterDataModified:
id, op = e.NodeID, characterDataModified(e.CharacterData)
case *dom.EventChildNodeCountUpdated:
id, op = e.NodeID, childNodeCountUpdated(e.ChildNodeCount)
case *dom.EventChildNodeInserted:
if e.PreviousNodeID != emptyNodeID {
_, err = h.WaitNode(ctxt, f, e.PreviousNodeID)
if err != nil {
return
}
}
id, op = e.ParentNodeID, childNodeInserted(f.Nodes, e.PreviousNodeID, e.Node)
case *dom.EventChildNodeRemoved:
id, op = e.ParentNodeID, childNodeRemoved(f.Nodes, e.NodeID)
case *dom.EventShadowRootPushed:
id, op = e.HostID, shadowRootPushed(f.Nodes, e.Root)
case *dom.EventShadowRootPopped:
id, op = e.HostID, shadowRootPopped(f.Nodes, e.RootID)
case *dom.EventPseudoElementAdded:
id, op = e.ParentID, pseudoElementAdded(f.Nodes, e.PseudoElement)
case *dom.EventPseudoElementRemoved:
id, op = e.ParentID, pseudoElementRemoved(f.Nodes, e.PseudoElementID)
case *dom.EventDistributedNodesUpdated:
id, op = e.InsertionPointID, distributedNodesUpdated(e.DistributedNodes)
case *dom.EventNodeHighlightRequested:
id, op = e.NodeID, nodeHighlightRequested
case *dom.EventInspectNodeRequested:
return
default:
panic(fmt.Sprintf("unhandled node event %s", reflect.TypeOf(ev)))
}
s := strings.TrimPrefix(strings.TrimSuffix(runtime.FuncForPC(reflect.ValueOf(op).Pointer()).Name(), ".func1"), "github.com/knq/chromedp.")
// retrieve node
n, err := h.WaitNode(ctxt, f, id)
if err != nil {
log.Printf("error could not perform (%s) operation on node %d (wait node error), got: %v", s, id, err)
return
}
h.Lock()
defer h.Unlock()
f.Lock()
defer f.Unlock()
n.Lock()
defer n.Unlock()
op(n)
}