mirror of
https://github.com/MontFerret/ferret.git
synced 2025-07-03 00:46:51 +02:00
* Added support of pre-compiled eval expressions * Added unit tests for eval.Function * Added RemoteType and RemoteObjectType enums * Refactored function generation * Refactored Document and Element loading logic * Removed redundant fields from cdp.Page * Exposed eval.Runtime to external callers * Added new eval.RemoteValue interface
329 lines
6.4 KiB
Go
329 lines
6.4 KiB
Go
package dom
|
|
|
|
import (
|
|
"context"
|
|
"sync"
|
|
|
|
"github.com/mafredri/cdp"
|
|
"github.com/mafredri/cdp/protocol/page"
|
|
"github.com/mafredri/cdp/protocol/runtime"
|
|
"github.com/pkg/errors"
|
|
"github.com/rs/zerolog"
|
|
|
|
"github.com/MontFerret/ferret/pkg/drivers/cdp/eval"
|
|
"github.com/MontFerret/ferret/pkg/drivers/cdp/input"
|
|
"github.com/MontFerret/ferret/pkg/drivers/cdp/templates"
|
|
"github.com/MontFerret/ferret/pkg/runtime/core"
|
|
"github.com/MontFerret/ferret/pkg/runtime/logging"
|
|
"github.com/MontFerret/ferret/pkg/runtime/values"
|
|
)
|
|
|
|
type Manager struct {
|
|
mu sync.RWMutex
|
|
logger zerolog.Logger
|
|
client *cdp.Client
|
|
mouse *input.Mouse
|
|
keyboard *input.Keyboard
|
|
mainFrame *AtomicFrameID
|
|
frames *AtomicFrameCollection
|
|
}
|
|
|
|
func New(
|
|
logger zerolog.Logger,
|
|
client *cdp.Client,
|
|
mouse *input.Mouse,
|
|
keyboard *input.Keyboard,
|
|
) (manager *Manager, err error) {
|
|
|
|
manager = new(Manager)
|
|
manager.logger = logging.WithName(logger.With(), "dom_manager").Logger()
|
|
manager.client = client
|
|
manager.mouse = mouse
|
|
manager.keyboard = keyboard
|
|
manager.mainFrame = NewAtomicFrameID()
|
|
manager.frames = NewAtomicFrameCollection()
|
|
|
|
return manager, nil
|
|
}
|
|
|
|
func (m *Manager) Close() error {
|
|
errs := make([]error, 0, m.frames.Length()+1)
|
|
|
|
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) LoadRootDocument(ctx context.Context) (*HTMLDocument, error) {
|
|
ftRepl, err := m.client.Page.GetFrameTree(ctx)
|
|
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return m.LoadDocument(ctx, ftRepl.FrameTree)
|
|
}
|
|
|
|
func (m *Manager) LoadDocument(ctx context.Context, frame page.FrameTree) (*HTMLDocument, error) {
|
|
exec, err := eval.Create(ctx, m.logger, m.client, frame.Frame.ID)
|
|
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
inputs := input.New(m.logger, m.client, exec, m.keyboard, m.mouse)
|
|
|
|
ref, err := exec.EvalRef(ctx, templates.GetDocument())
|
|
|
|
if err != nil {
|
|
return nil, errors.Wrap(err, "failed to load root element")
|
|
}
|
|
|
|
exec.SetLoader(NewNodeLoader(m))
|
|
|
|
rootElement := NewHTMLElement(
|
|
m.logger,
|
|
m.client,
|
|
m,
|
|
inputs,
|
|
exec,
|
|
*ref.ObjectID,
|
|
)
|
|
|
|
return NewHTMLDocument(
|
|
m.logger,
|
|
m.client,
|
|
m,
|
|
inputs,
|
|
exec,
|
|
rootElement,
|
|
frame,
|
|
), nil
|
|
}
|
|
|
|
func (m *Manager) ResolveElement(ctx context.Context, frameID page.FrameID, id runtime.RemoteObjectID) (*HTMLElement, error) {
|
|
doc, err := m.GetFrameNode(ctx, frameID)
|
|
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return NewHTMLElement(
|
|
m.logger,
|
|
m.client,
|
|
m,
|
|
doc.input,
|
|
doc.eval,
|
|
id,
|
|
), 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) 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 frame is not loaded yet
|
|
doc, err := m.LoadDocument(ctx, frame.tree)
|
|
|
|
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)
|
|
}
|