1
0
mirror of https://github.com/MontFerret/ferret.git synced 2025-01-26 03:51:57 +02:00
ferret/pkg/drivers/cdp/dom/manager.go
2020-08-25 01:25:07 -04:00

453 lines
9.7 KiB
Go

package dom
import (
"context"
"github.com/MontFerret/ferret/pkg/drivers/cdp/events"
"github.com/MontFerret/ferret/pkg/drivers/cdp/input"
"github.com/MontFerret/ferret/pkg/drivers/common"
"github.com/MontFerret/ferret/pkg/runtime/core"
"github.com/MontFerret/ferret/pkg/runtime/values"
"github.com/mafredri/cdp"
"github.com/mafredri/cdp/protocol/dom"
"github.com/mafredri/cdp/protocol/page"
"github.com/mafredri/cdp/rpcc"
"github.com/pkg/errors"
"github.com/rs/zerolog"
"io"
"sync"
)
var (
eventDocumentUpdated = events.New("doc_updated")
eventChildNodeInserted = events.New("child_inserted")
eventChildNodeRemoved = events.New("child_removed")
)
type (
DocumentUpdatedListener func(ctx context.Context)
AttrModifiedListener func(ctx context.Context, nodeID dom.NodeID, name, value string)
AttrRemovedListener func(ctx context.Context, nodeID dom.NodeID, name string)
ChildNodeCountUpdatedListener func(ctx context.Context, nodeID dom.NodeID, count int)
ChildNodeInsertedListener func(ctx context.Context, nodeID, previousNodeID dom.NodeID, node dom.Node)
ChildNodeRemovedListener func(ctx context.Context, nodeID, previousNodeID dom.NodeID)
Manager struct {
mu sync.RWMutex
logger *zerolog.Logger
client *cdp.Client
events *events.Loop
mouse *input.Mouse
keyboard *input.Keyboard
mainFrame *AtomicFrameID
frames *AtomicFrameCollection
cancel context.CancelFunc
}
)
// a dirty workaround to let pass the vet test
func createContext() (context.Context, context.CancelFunc) {
return context.WithCancel(context.Background())
}
func New(
logger *zerolog.Logger,
client *cdp.Client,
mouse *input.Mouse,
keyboard *input.Keyboard,
) (manager *Manager, err error) {
ctx, cancel := createContext()
closers := make([]io.Closer, 0, 10)
defer func() {
if err != nil {
common.CloseAll(logger, closers, "failed to close a DOM event stream")
}
}()
onContentReady, err := client.Page.DOMContentEventFired(ctx)
if err != nil {
return nil, err
}
closers = append(closers, onContentReady)
onDocUpdated, err := client.DOM.DocumentUpdated(ctx)
if err != nil {
return nil, err
}
closers = append(closers, onDocUpdated)
onAttrModified, err := client.DOM.AttributeModified(ctx)
if err != nil {
return nil, err
}
closers = append(closers, onAttrModified)
onAttrRemoved, err := client.DOM.AttributeRemoved(ctx)
if err != nil {
return nil, err
}
closers = append(closers, onAttrRemoved)
onChildCountUpdated, err := client.DOM.ChildNodeCountUpdated(ctx)
if err != nil {
return nil, err
}
closers = append(closers, onChildCountUpdated)
onChildNodeInserted, err := client.DOM.ChildNodeInserted(ctx)
if err != nil {
return nil, err
}
closers = append(closers, onChildNodeInserted)
onChildNodeRemoved, err := client.DOM.ChildNodeRemoved(ctx)
if err != nil {
return nil, err
}
closers = append(closers, onChildNodeRemoved)
eventLoop := events.NewLoop()
eventLoop.AddSource(events.NewSource(eventDocumentUpdated, onDocUpdated, func(stream rpcc.Stream) (i interface{}, e error) {
return stream.(dom.DocumentUpdatedClient).Recv()
}))
eventLoop.AddSource(events.NewSource(eventChildNodeInserted, onChildNodeInserted, func(stream rpcc.Stream) (i interface{}, e error) {
return stream.(dom.ChildNodeInsertedClient).Recv()
}))
eventLoop.AddSource(events.NewSource(eventChildNodeRemoved, onChildNodeRemoved, func(stream rpcc.Stream) (i interface{}, e error) {
return stream.(dom.ChildNodeRemovedClient).Recv()
}))
manager = new(Manager)
manager.logger = logger
manager.client = client
manager.events = eventLoop
manager.mouse = mouse
manager.keyboard = keyboard
manager.mainFrame = NewAtomicFrameID()
manager.frames = NewAtomicFrameCollection()
manager.cancel = cancel
eventLoop.Run(ctx)
return manager, nil
}
func (m *Manager) Close() error {
errs := make([]error, 0, m.frames.Length()+1)
if m.cancel != nil {
m.cancel()
m.cancel = nil
}
m.frames.ForEach(func(f Frame, key page.FrameID) bool {
// if initialized
if f.node != nil {
if err := f.node.Close(); err != nil {
errs = append(errs, err)
}
}
return true
})
if len(errs) > 0 {
return core.Errors(errs...)
}
return nil
}
func (m *Manager) GetMainFrame() *HTMLDocument {
m.mu.RLock()
defer m.mu.RUnlock()
mainFrameID := m.mainFrame.Get()
if mainFrameID == "" {
return nil
}
mainFrame, exists := m.frames.Get(mainFrameID)
if exists {
return mainFrame.node
}
return nil
}
func (m *Manager) SetMainFrame(doc *HTMLDocument) {
m.mu.Lock()
defer m.mu.Unlock()
mainFrameID := m.mainFrame.Get()
if mainFrameID != "" {
if err := m.removeFrameRecursivelyInternal(mainFrameID); err != nil {
m.logger.Error().Err(err).Msg("failed to close previous main frame")
}
}
m.mainFrame.Set(doc.frameTree.Frame.ID)
m.addPreloadedFrame(doc)
}
func (m *Manager) AddFrame(frame page.FrameTree) {
m.mu.RLock()
defer m.mu.RUnlock()
m.addFrameInternal(frame)
}
func (m *Manager) RemoveFrame(frameID page.FrameID) error {
m.mu.RLock()
defer m.mu.RUnlock()
return m.removeFrameInternal(frameID)
}
func (m *Manager) RemoveFrameRecursively(frameID page.FrameID) error {
m.mu.RLock()
defer m.mu.RUnlock()
return m.removeFrameRecursivelyInternal(frameID)
}
func (m *Manager) RemoveFramesByParentID(parentFrameID page.FrameID) error {
m.mu.RLock()
defer m.mu.RUnlock()
frame, found := m.frames.Get(parentFrameID)
if !found {
return errors.New("frame not found")
}
for _, child := range frame.tree.ChildFrames {
if err := m.removeFrameRecursivelyInternal(child.Frame.ID); err != nil {
return err
}
}
return nil
}
func (m *Manager) GetFrameNode(ctx context.Context, frameID page.FrameID) (*HTMLDocument, error) {
return m.getFrameInternal(ctx, frameID)
}
func (m *Manager) GetFrameTree(_ context.Context, frameID page.FrameID) (page.FrameTree, error) {
m.mu.RLock()
defer m.mu.RUnlock()
frame, found := m.frames.Get(frameID)
if !found {
return page.FrameTree{}, core.ErrNotFound
}
return frame.tree, nil
}
func (m *Manager) GetFrameNodes(ctx context.Context) (*values.Array, error) {
m.mu.RLock()
defer m.mu.RUnlock()
arr := values.NewArray(m.frames.Length())
for _, f := range m.frames.ToSlice() {
doc, err := m.getFrameInternal(ctx, f.tree.Frame.ID)
if err != nil {
return nil, err
}
arr.Push(doc)
}
return arr, nil
}
func (m *Manager) AddDocumentUpdatedListener(listener DocumentUpdatedListener) events.ListenerID {
m.mu.RLock()
defer m.mu.RUnlock()
return m.events.AddListener(eventDocumentUpdated, func(ctx context.Context, _ interface{}) bool {
listener(ctx)
return true
})
}
func (m *Manager) RemoveReloadListener(listenerID events.ListenerID) {
m.mu.RLock()
defer m.mu.RUnlock()
m.events.RemoveListener(eventDocumentUpdated, listenerID)
}
func (m *Manager) AddChildNodeInsertedListener(listener ChildNodeInsertedListener) events.ListenerID {
m.mu.RLock()
defer m.mu.RUnlock()
return m.events.AddListener(eventChildNodeInserted, func(ctx context.Context, message interface{}) bool {
reply := message.(*dom.ChildNodeInsertedReply)
listener(ctx, reply.ParentNodeID, reply.PreviousNodeID, reply.Node)
return true
})
}
func (m *Manager) RemoveChildNodeInsertedListener(listenerID events.ListenerID) {
m.mu.RLock()
defer m.mu.RUnlock()
m.events.RemoveListener(eventChildNodeInserted, listenerID)
}
func (m *Manager) AddChildNodeRemovedListener(listener ChildNodeRemovedListener) events.ListenerID {
m.mu.RLock()
defer m.mu.RUnlock()
return m.events.AddListener(eventChildNodeRemoved, func(ctx context.Context, message interface{}) bool {
reply := message.(*dom.ChildNodeRemovedReply)
listener(ctx, reply.ParentNodeID, reply.NodeID)
return true
})
}
func (m *Manager) RemoveChildNodeRemovedListener(listenerID events.ListenerID) {
m.mu.RLock()
defer m.mu.RUnlock()
m.events.RemoveListener(eventChildNodeRemoved, listenerID)
}
func (m *Manager) addFrameInternal(frame page.FrameTree) {
m.frames.Set(frame.Frame.ID, Frame{
tree: frame,
node: nil,
})
for _, child := range frame.ChildFrames {
m.addFrameInternal(child)
}
}
func (m *Manager) addPreloadedFrame(doc *HTMLDocument) {
m.frames.Set(doc.frameTree.Frame.ID, Frame{
tree: doc.frameTree,
node: doc,
})
for _, child := range doc.frameTree.ChildFrames {
m.addFrameInternal(child)
}
}
func (m *Manager) getFrameInternal(ctx context.Context, frameID page.FrameID) (*HTMLDocument, error) {
frame, found := m.frames.Get(frameID)
if !found {
return nil, core.ErrNotFound
}
// frame is initialized
if frame.node != nil {
return frame.node, nil
}
// the frames is not loaded yet
node, execID, err := resolveFrame(ctx, m.client, frameID)
if err != nil {
return nil, errors.Wrapf(err, "failed to resolve frame node: %s", frameID)
}
doc, err := LoadHTMLDocument(
ctx,
m.logger,
m.client,
m,
m.mouse,
m.keyboard,
node,
frame.tree,
execID,
)
if err != nil {
return nil, errors.Wrap(err, "failed to load frame document")
}
frame.node = doc
return doc, nil
}
func (m *Manager) removeFrameInternal(frameID page.FrameID) error {
current, exists := m.frames.Get(frameID)
if !exists {
return core.Error(core.ErrNotFound, "frame")
}
m.frames.Remove(frameID)
mainFrameID := m.mainFrame.Get()
if frameID == mainFrameID {
m.mainFrame.Reset()
}
if current.node == nil {
return nil
}
return current.node.Close()
}
func (m *Manager) removeFrameRecursivelyInternal(frameID page.FrameID) error {
parent, exists := m.frames.Get(frameID)
if !exists {
return core.Error(core.ErrNotFound, "frame")
}
for _, child := range parent.tree.ChildFrames {
if err := m.removeFrameRecursivelyInternal(child.Frame.ID); err != nil {
return err
}
}
return m.removeFrameInternal(frameID)
}