mirror of
https://github.com/MontFerret/ferret.git
synced 2024-12-12 11:15:14 +02:00
Bugfix/#332 nav deadlock (#333)
* Added failing e2e test * Fixed deadlock on navigation * Removed filter for e2e tests * Updated method name in LazyValue struct * Custom atomic value * Fixed linting issue * Updated comments
This commit is contained in:
parent
9756f0dc5a
commit
a5cbdb435c
6
e2e/tests/dynamic/doc/navigate/navigate.fql
Normal file
6
e2e/tests/dynamic/doc/navigate/navigate.fql
Normal file
@ -0,0 +1,6 @@
|
||||
LET url = @dynamic
|
||||
LET p = DOCUMENT("", { driver: "cdp" })
|
||||
|
||||
NAVIGATE(p, url)
|
||||
|
||||
RETURN T::EXPECT(url + '/', p.url)
|
@ -1121,7 +1121,7 @@ func (el *HTMLElement) handleAttrModified(ctx context.Context, message interface
|
||||
return
|
||||
}
|
||||
|
||||
el.attributes.Write(ctx, func(v core.Value, err error) {
|
||||
el.attributes.Mutate(ctx, func(v core.Value, err error) {
|
||||
if err != nil {
|
||||
el.logError(err).Msg("failed to update element")
|
||||
|
||||
@ -1161,7 +1161,7 @@ func (el *HTMLElement) handleAttrRemoved(ctx context.Context, message interface{
|
||||
return
|
||||
}
|
||||
|
||||
el.attributes.Write(ctx, func(v core.Value, err error) {
|
||||
el.attributes.Mutate(ctx, func(v core.Value, err error) {
|
||||
if err != nil {
|
||||
el.logError(err).Msg("failed to update element")
|
||||
|
||||
@ -1250,7 +1250,7 @@ func (el *HTMLElement) handleChildInserted(ctx context.Context, message interfac
|
||||
return
|
||||
}
|
||||
|
||||
el.loadedChildren.Write(ctx, func(v core.Value, _ error) {
|
||||
el.loadedChildren.Mutate(ctx, func(v core.Value, _ error) {
|
||||
loadedArr := v.(*values.Array)
|
||||
loadedEl, err := LoadHTMLElement(ctx, el.logger, el.client, el.events, el.input, el.exec, nextID)
|
||||
|
||||
@ -1302,7 +1302,7 @@ func (el *HTMLElement) handleChildRemoved(ctx context.Context, message interface
|
||||
return
|
||||
}
|
||||
|
||||
el.loadedChildren.Write(ctx, func(v core.Value, err error) {
|
||||
el.loadedChildren.Mutate(ctx, func(v core.Value, err error) {
|
||||
if err != nil {
|
||||
el.logger.Error().
|
||||
Timestamp().
|
||||
|
@ -3,9 +3,6 @@ package cdp
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"hash/fnv"
|
||||
"sync"
|
||||
|
||||
"github.com/mafredri/cdp"
|
||||
"github.com/mafredri/cdp/protocol/emulation"
|
||||
"github.com/mafredri/cdp/protocol/network"
|
||||
@ -13,6 +10,8 @@ import (
|
||||
"github.com/mafredri/cdp/rpcc"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/rs/zerolog"
|
||||
"hash/fnv"
|
||||
"sync"
|
||||
|
||||
"github.com/MontFerret/ferret/pkg/drivers"
|
||||
"github.com/MontFerret/ferret/pkg/drivers/cdp/events"
|
||||
@ -32,7 +31,7 @@ type HTMLPage struct {
|
||||
events *events.EventBroker
|
||||
mouse *input.Mouse
|
||||
keyboard *input.Keyboard
|
||||
document *HTMLDocument
|
||||
document *common.AtomicValue
|
||||
frames *common.LazyValue
|
||||
}
|
||||
|
||||
@ -227,7 +226,7 @@ func NewHTMLPage(
|
||||
p.events = broker
|
||||
p.mouse = mouse
|
||||
p.keyboard = keyboard
|
||||
p.document = document
|
||||
p.document = common.NewAtomicValue(document)
|
||||
p.frames = common.NewLazyValue(p.unfoldFrames)
|
||||
|
||||
broker.AddEventListener(events.EventLoad, p.handlePageLoad)
|
||||
@ -237,10 +236,7 @@ func NewHTMLPage(
|
||||
}
|
||||
|
||||
func (p *HTMLPage) MarshalJSON() ([]byte, error) {
|
||||
p.mu.Lock()
|
||||
defer p.mu.Unlock()
|
||||
|
||||
return p.document.MarshalJSON()
|
||||
return p.getCurrentDocument().MarshalJSON()
|
||||
}
|
||||
|
||||
func (p *HTMLPage) Type() core.Type {
|
||||
@ -248,16 +244,10 @@ func (p *HTMLPage) Type() core.Type {
|
||||
}
|
||||
|
||||
func (p *HTMLPage) String() string {
|
||||
p.mu.Lock()
|
||||
defer p.mu.Unlock()
|
||||
|
||||
return p.document.GetURL().String()
|
||||
return p.getCurrentDocument().GetURL().String()
|
||||
}
|
||||
|
||||
func (p *HTMLPage) Compare(other core.Value) int64 {
|
||||
p.mu.Lock()
|
||||
defer p.mu.Unlock()
|
||||
|
||||
tc := drivers.Compare(p.Type(), other.Type())
|
||||
|
||||
if tc != 0 {
|
||||
@ -270,7 +260,7 @@ func (p *HTMLPage) Compare(other core.Value) int64 {
|
||||
return 1
|
||||
}
|
||||
|
||||
return p.document.GetURL().Compare(cdpPage.GetURL())
|
||||
return p.getCurrentDocument().GetURL().Compare(cdpPage.GetURL())
|
||||
}
|
||||
|
||||
func (p *HTMLPage) Unwrap() interface{} {
|
||||
@ -281,15 +271,12 @@ func (p *HTMLPage) Unwrap() interface{} {
|
||||
}
|
||||
|
||||
func (p *HTMLPage) Hash() uint64 {
|
||||
p.mu.Lock()
|
||||
defer p.mu.Unlock()
|
||||
|
||||
h := fnv.New64a()
|
||||
|
||||
h.Write([]byte("CDP"))
|
||||
h.Write([]byte(p.Type().String()))
|
||||
h.Write([]byte(":"))
|
||||
h.Write([]byte(p.document.GetURL()))
|
||||
h.Write([]byte(p.getCurrentDocument().GetURL()))
|
||||
|
||||
return h.Sum64()
|
||||
}
|
||||
@ -307,17 +294,11 @@ func (p *HTMLPage) SetIn(ctx context.Context, path []core.Value, value core.Valu
|
||||
}
|
||||
|
||||
func (p *HTMLPage) Iterate(ctx context.Context) (core.Iterator, error) {
|
||||
p.mu.Lock()
|
||||
defer p.mu.Unlock()
|
||||
|
||||
return p.document.Iterate(ctx)
|
||||
return p.getCurrentDocument().Iterate(ctx)
|
||||
}
|
||||
|
||||
func (p *HTMLPage) Length() values.Int {
|
||||
p.mu.Lock()
|
||||
defer p.mu.Unlock()
|
||||
|
||||
return p.document.Length()
|
||||
return p.getCurrentDocument().Length()
|
||||
}
|
||||
|
||||
func (p *HTMLPage) Close() error {
|
||||
@ -327,10 +308,12 @@ func (p *HTMLPage) Close() error {
|
||||
p.closed = values.True
|
||||
err := p.events.Stop()
|
||||
|
||||
doc := p.getCurrentDocument()
|
||||
|
||||
if err != nil {
|
||||
p.logger.Warn().
|
||||
Timestamp().
|
||||
Str("url", p.document.GetURL().String()).
|
||||
Str("url", doc.GetURL().String()).
|
||||
Err(err).
|
||||
Msg("failed to stop event events")
|
||||
}
|
||||
@ -340,17 +323,17 @@ func (p *HTMLPage) Close() error {
|
||||
if err != nil {
|
||||
p.logger.Warn().
|
||||
Timestamp().
|
||||
Str("url", p.document.GetURL().String()).
|
||||
Str("url", doc.GetURL().String()).
|
||||
Err(err).
|
||||
Msg("failed to close event events")
|
||||
}
|
||||
|
||||
err = p.document.Close()
|
||||
err = doc.Close()
|
||||
|
||||
if err != nil {
|
||||
p.logger.Warn().
|
||||
Timestamp().
|
||||
Str("url", p.document.GetURL().String()).
|
||||
Str("url", doc.GetURL().String()).
|
||||
Err(err).
|
||||
Msg("failed to close root document")
|
||||
}
|
||||
@ -360,7 +343,7 @@ func (p *HTMLPage) Close() error {
|
||||
if err != nil {
|
||||
p.logger.Warn().
|
||||
Timestamp().
|
||||
Str("url", p.document.GetURL().String()).
|
||||
Str("url", doc.GetURL().String()).
|
||||
Err(err).
|
||||
Msg("failed to close browser page")
|
||||
}
|
||||
@ -376,23 +359,14 @@ func (p *HTMLPage) IsClosed() values.Boolean {
|
||||
}
|
||||
|
||||
func (p *HTMLPage) GetURL() values.String {
|
||||
p.mu.Lock()
|
||||
defer p.mu.Unlock()
|
||||
|
||||
return p.document.GetURL()
|
||||
return p.getCurrentDocument().GetURL()
|
||||
}
|
||||
|
||||
func (p *HTMLPage) GetMainFrame() drivers.HTMLDocument {
|
||||
p.mu.Lock()
|
||||
defer p.mu.Unlock()
|
||||
|
||||
return p.document
|
||||
return p.getCurrentDocument()
|
||||
}
|
||||
|
||||
func (p *HTMLPage) GetFrames(ctx context.Context) (*values.Array, error) {
|
||||
p.mu.Lock()
|
||||
defer p.mu.Unlock()
|
||||
|
||||
res, err := p.frames.Read(ctx)
|
||||
|
||||
if err != nil {
|
||||
@ -449,7 +423,7 @@ func (p *HTMLPage) SetCookies(ctx context.Context, cookies ...drivers.HTTPCookie
|
||||
params := make([]network.CookieParam, 0, len(cookies))
|
||||
|
||||
for _, c := range cookies {
|
||||
params = append(params, fromDriverCookie(p.document.GetURL().String(), c))
|
||||
params = append(params, fromDriverCookie(p.getCurrentDocument().GetURL().String(), c))
|
||||
}
|
||||
|
||||
return p.client.Network.SetCookies(ctx, network.NewSetCookiesArgs(params))
|
||||
@ -466,7 +440,7 @@ func (p *HTMLPage) DeleteCookies(ctx context.Context, cookies ...drivers.HTTPCoo
|
||||
var err error
|
||||
|
||||
for _, c := range cookies {
|
||||
err = p.client.Network.DeleteCookies(ctx, fromDriverCookieDelete(p.document.GetURL().String(), c))
|
||||
err = p.client.Network.DeleteCookies(ctx, fromDriverCookieDelete(p.getCurrentDocument().GetURL().String(), c))
|
||||
|
||||
if err != nil {
|
||||
break
|
||||
@ -722,34 +696,36 @@ func (p *HTMLPage) WaitForNavigation(ctx context.Context) error {
|
||||
}
|
||||
|
||||
func (p *HTMLPage) handlePageLoad(ctx context.Context, _ interface{}) {
|
||||
p.mu.Lock()
|
||||
defer p.mu.Unlock()
|
||||
err := p.document.Write(func(current core.Value) (core.Value, error) {
|
||||
nextDoc, err := LoadRootHTMLDocument(ctx, p.logger, p.client, p.events, p.mouse, p.keyboard)
|
||||
|
||||
nextDoc, err := LoadRootHTMLDocument(ctx, p.logger, p.client, p.events, p.mouse, p.keyboard)
|
||||
if err != nil {
|
||||
return values.None, err
|
||||
}
|
||||
|
||||
// close the prev document
|
||||
currentDoc := current.(*HTMLDocument)
|
||||
err = currentDoc.Close()
|
||||
|
||||
if err != nil {
|
||||
p.logger.Warn().
|
||||
Timestamp().
|
||||
Err(err).
|
||||
Msgf("failed to close root document: %s", currentDoc.GetURL())
|
||||
}
|
||||
|
||||
// reset all loaded frames
|
||||
p.frames.Reset()
|
||||
|
||||
return nextDoc, nil
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
p.logger.Error().
|
||||
p.logger.Warn().
|
||||
Timestamp().
|
||||
Err(err).
|
||||
Msg("failed to load new root document after page load")
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// close the prev document
|
||||
err = p.document.Close()
|
||||
|
||||
if err != nil {
|
||||
p.logger.Error().
|
||||
Timestamp().
|
||||
Err(err).
|
||||
Msgf("failed to close root document: %s", p.document.GetURL())
|
||||
}
|
||||
|
||||
// set the new root document
|
||||
p.document = nextDoc
|
||||
// reset all loaded frames
|
||||
p.frames.Reset()
|
||||
}
|
||||
|
||||
func (p *HTMLPage) handleError(_ context.Context, val interface{}) {
|
||||
@ -765,10 +741,14 @@ func (p *HTMLPage) handleError(_ context.Context, val interface{}) {
|
||||
Msg("unexpected error")
|
||||
}
|
||||
|
||||
func (p *HTMLPage) getCurrentDocument() *HTMLDocument {
|
||||
return p.document.Read().(*HTMLDocument)
|
||||
}
|
||||
|
||||
func (p *HTMLPage) unfoldFrames(ctx context.Context) (core.Value, error) {
|
||||
res := values.NewArray(10)
|
||||
|
||||
err := common.CollectFrames(ctx, res, p.document)
|
||||
err := common.CollectFrames(ctx, res, p.getCurrentDocument())
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
|
52
pkg/drivers/common/atomic.go
Normal file
52
pkg/drivers/common/atomic.go
Normal file
@ -0,0 +1,52 @@
|
||||
package common
|
||||
|
||||
import (
|
||||
"github.com/MontFerret/ferret/pkg/runtime/core"
|
||||
"sync"
|
||||
)
|
||||
|
||||
type (
|
||||
// AtomicValueWriter represents an atomic value writer
|
||||
AtomicValueWriter func(current core.Value) (core.Value, error)
|
||||
|
||||
// AtomicValue represents an atomic value
|
||||
AtomicValue struct {
|
||||
mu sync.Mutex
|
||||
value core.Value
|
||||
}
|
||||
)
|
||||
|
||||
func NewAtomicValue(value core.Value) *AtomicValue {
|
||||
av := new(AtomicValue)
|
||||
av.value = value
|
||||
|
||||
return av
|
||||
}
|
||||
|
||||
// Read returns an underlying value.
|
||||
// @returns (Value) - Underlying value
|
||||
func (av *AtomicValue) Read() core.Value {
|
||||
av.mu.Lock()
|
||||
defer av.mu.Unlock()
|
||||
|
||||
return av.value
|
||||
}
|
||||
|
||||
// Write sets a new underlying value.
|
||||
// If writer fails, the operations gets terminated and an underlying value remains.
|
||||
// @param (AtomicValueWriter) - Writer function that receives a current value and returns new one.
|
||||
// @returns (Error) - Error if write operation failed
|
||||
func (av *AtomicValue) Write(writer AtomicValueWriter) error {
|
||||
av.mu.Lock()
|
||||
defer av.mu.Unlock()
|
||||
|
||||
next, err := writer(av.value)
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
av.value = next
|
||||
|
||||
return nil
|
||||
}
|
@ -9,8 +9,10 @@ import (
|
||||
)
|
||||
|
||||
type (
|
||||
// LazyValueFactory represents a value initializer
|
||||
LazyValueFactory func(ctx context.Context) (core.Value, error)
|
||||
|
||||
// LazyValue represents a value with late initialization
|
||||
LazyValue struct {
|
||||
mu sync.Mutex
|
||||
factory LazyValueFactory
|
||||
@ -29,17 +31,6 @@ func NewLazyValue(factory LazyValueFactory) *LazyValue {
|
||||
return lz
|
||||
}
|
||||
|
||||
func NewVolatileValue(value core.Value, factory LazyValueFactory) *LazyValue {
|
||||
lz := NewLazyValue(factory)
|
||||
|
||||
if value != values.None {
|
||||
lz.ready = true
|
||||
lz.value = value
|
||||
}
|
||||
|
||||
return lz
|
||||
}
|
||||
|
||||
// Ready indicates whether the value is ready.
|
||||
// @returns (Boolean) - Boolean value indicating whether the value is ready.
|
||||
func (lv *LazyValue) Ready() bool {
|
||||
@ -63,10 +54,10 @@ func (lv *LazyValue) Read(ctx context.Context) (core.Value, error) {
|
||||
return lv.value, lv.err
|
||||
}
|
||||
|
||||
// Write safely mutates an underlying value.
|
||||
// Mutate safely mutates an underlying value.
|
||||
// Loads a value if it's not ready.
|
||||
// Thread safe.
|
||||
func (lv *LazyValue) Write(ctx context.Context, writer func(v core.Value, err error)) {
|
||||
func (lv *LazyValue) Mutate(ctx context.Context, mutator func(v core.Value, err error)) {
|
||||
lv.mu.Lock()
|
||||
defer lv.mu.Unlock()
|
||||
|
||||
@ -74,7 +65,26 @@ func (lv *LazyValue) Write(ctx context.Context, writer func(v core.Value, err er
|
||||
lv.load(ctx)
|
||||
}
|
||||
|
||||
writer(lv.value, lv.err)
|
||||
mutator(lv.value, lv.err)
|
||||
}
|
||||
|
||||
// MutateIfReady safely mutates an underlying value only if it's ready.
|
||||
func (lv *LazyValue) MutateIfReady(mutator func(v core.Value, err error)) {
|
||||
lv.mu.Lock()
|
||||
defer lv.mu.Unlock()
|
||||
|
||||
if lv.ready {
|
||||
mutator(lv.value, lv.err)
|
||||
}
|
||||
}
|
||||
|
||||
// Reload resets the storage and loads data.
|
||||
func (lv *LazyValue) Reload(ctx context.Context) {
|
||||
lv.mu.Lock()
|
||||
defer lv.mu.Unlock()
|
||||
|
||||
lv.resetInternal()
|
||||
lv.load(ctx)
|
||||
}
|
||||
|
||||
// Reset resets the storage.
|
||||
@ -83,6 +93,10 @@ func (lv *LazyValue) Reset() {
|
||||
lv.mu.Lock()
|
||||
defer lv.mu.Unlock()
|
||||
|
||||
lv.resetInternal()
|
||||
}
|
||||
|
||||
func (lv *LazyValue) resetInternal() {
|
||||
lv.ready = false
|
||||
lv.value = values.None
|
||||
lv.err = nil
|
||||
|
Loading…
Reference in New Issue
Block a user