1
0
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:
Tim Voronov 2019-07-16 10:34:13 -04:00 committed by GitHub
parent 9756f0dc5a
commit a5cbdb435c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 139 additions and 87 deletions

View File

@ -0,0 +1,6 @@
LET url = @dynamic
LET p = DOCUMENT("", { driver: "cdp" })
NAVIGATE(p, url)
RETURN T::EXPECT(url + '/', p.url)

View File

@ -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().

View File

@ -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

View 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
}

View File

@ -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