Skip to content

Commit

Permalink
feat: add idle listener
Browse files Browse the repository at this point in the history
  • Loading branch information
Davincible committed Aug 24, 2023
1 parent 0c1186c commit 8c6b7d0
Show file tree
Hide file tree
Showing 5 changed files with 166 additions and 0 deletions.
1 change: 1 addition & 0 deletions actions.go
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,7 @@ func LoadCookies(cookies []Cookie) chromedp.ActionFunc {
return err
}
}

return nil
})
}
Expand Down
124 changes: 124 additions & 0 deletions events.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
package chromedpundetected

import (
"context"
"time"

"github.com/chromedp/cdproto"
"github.com/chromedp/cdproto/network"
"github.com/chromedp/cdproto/page"
"github.com/chromedp/chromedp"
)

// IdleEvent is sent through the channel returned by NetworkIdleListener when the network is considered idle.
// This event can be used to determine when a page has finished loading.
type IdleEvent struct {
IsIdle bool
}

// NetworkIdlePermanentListener sets up a listener to monitor for network idle events.

Check warning on line 19 in events.go

View workflow job for this annotation

GitHub Actions / Lint

exported: comment on exported function NetworkIdleListener should be of the form "NetworkIdleListener ..." (revive)
//
// This can be used as a proxy to know for e.g. when a page has fully loaded, assuming
// that the page doesn't send any network requests within the networkIdleTimeout period
// after is has finished loading.
//
// This function creates a listener that monitors the target specified by the given context
// for network activity. It triggers a "NETWORK IDLE" event if no network request is sent
// within the provided `networkIdleTimeout` duration after a network idle lifecycle event.
//
// After the network is considered idle, the listener will terminate and the channel will close.
//
// Parameters:
// - ctx: The context for which the listener is set up. Usually, this context is tied to a specific browser tab or page.
// - networkIdleTimeout: The duration to wait for no network activity after a network idle lifecycle event before considering the network to be truly idle.
// - totalTimeout: The duration to wait for the network to be idle before terminating the listener.
//
// Returns:
// 1. A channel of type IdleEvent. When the network is considered idle, an IdleEvent with IsIdle set to true is sent through this channel.
func NetworkIdleListener(ctx context.Context, networkIdleTimeout, totalTimeout time.Duration) chan IdleEvent {
ctx, cancel := context.WithCancel(ctx)

ch := make(chan IdleEvent, 1) // buffer to prevent blocking

var idleTimer *time.Timer

go func() {
<-time.After(totalTimeout)
ch <- IdleEvent{IsIdle: false}

cancel()
close(ch)
}()

listener := newNetworkIdleListener(ch, networkIdleTimeout, idleTimer)

chromedp.ListenTarget(ctx, listener)

return ch
}

// This can be used as a proxy to know for e.g. when a page has fully loaded, assuming

Check warning on line 60 in events.go

View workflow job for this annotation

GitHub Actions / Lint

exported: comment on exported function NetworkIdlePermanentListener should be of the form "NetworkIdlePermanentListener ..." (revive)
// that the page doesn't send any network requests within the networkIdleTimeout period
// after is has finished loading.
//
// This function creates a listener that monitors the target specified by the given context
// for network activity. It triggers a "NETWORK IDLE" event if no network request is sent
// within the provided `networkIdleTimeout` duration after a network idle lifecycle event.
//
// It's designed to run indefinitely, i.e., it doesn't automatically stop listening after
// an idle event or after a certain period. To manually stop listening and to free up
// associated resources, one should call the returned cancel function.
//
// Parameters:
// - ctx: The context for which the listener is set up. Usually, this context is tied to a specific browser tab or page.
// - networkIdleTimeout: The duration to wait for no network activity after a network idle lifecycle event before considering the network to be truly idle.
//
// Returns:
// 1. A channel of type IdleEvent. When the network is considered idle, an IdleEvent
// with IsIdle set to true is sent through this channel.
// 2. A cancel function. This function can be called to terminate the listener and close the channel.
func NetworkIdlePermanentListener(ctx context.Context, networkIdleTimeout time.Duration) (chan IdleEvent, func()) {
ctx, cancel := context.WithCancel(ctx)
ch := make(chan IdleEvent, 1) // buffer to prevent blocking

var idleTimer *time.Timer

listener := newNetworkIdleListener(ch, networkIdleTimeout, idleTimer)

chromedp.ListenTarget(ctx, listener)

cancelFunc := func() {
cancel()
close(ch)
}

return ch, cancelFunc
}

func newNetworkIdleListener(ch chan IdleEvent, networkIdleTimeout time.Duration, idleTimer *time.Timer) func(interface{}) {
return func(ev interface{}) {
// Check if the event is a standard protocol message
if _, ok := ev.(*cdproto.Message); ok {
return
}

// Reset the timer every time a request is sent
if _, ok := ev.(*network.EventRequestWillBeSent); ok {
if idleTimer != nil {
idleTimer.Stop()
idleTimer = nil
}
}

// Start or check the timer when the network is idle
if ev, ok := ev.(*page.EventLifecycleEvent); ok && ev.Name == "networkIdle" {
if idleTimer == nil {
idleTimer = time.AfterFunc(networkIdleTimeout, func() {
ch <- IdleEvent{IsIdle: true}
})
} else {
idleTimer.Reset(networkIdleTimeout)
}
}
}
}
35 changes: 35 additions & 0 deletions events_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
package chromedpundetected

import (
"context"
"fmt"
"testing"
"time"

"github.com/chromedp/chromedp"
)

func TestNetworkIdleListener(t *testing.T) {
testRun(t,
n,
NewConfig(
WithTimeout(20*time.Second),
WithHeadless(),
),
func(ctx context.Context) error {
idleListener := NetworkIdleListener(ctx, time.Second, time.Second*10)

if err := chromedp.Run(ctx,
chromedp.Navigate("https://nowsecure.nl"),
); err != nil {
return err
}

if event := <-idleListener; !event.IsIdle {
return fmt.Errorf("expected idle event, got %v", event)
}

return nil
},
)
}
1 change: 1 addition & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ require (
github.com/google/uuid v1.3.0
github.com/hashicorp/go-multierror v1.1.1
github.com/mailru/easyjson v0.7.7
github.com/sanity-io/litter v1.5.5
github.com/stretchr/testify v1.8.1
golang.org/x/exp v0.0.0-20221217163422-3c43f8badb15
)
Expand Down
5 changes: 5 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ github.com/chromedp/chromedp v0.9.1 h1:CC7cC5p1BeLiiS2gfNNPwp3OaUxtRMBjfiw3E3k6d
github.com/chromedp/chromedp v0.9.1/go.mod h1:DUgZWRvYoEfgi66CgZ/9Yv+psgi+Sksy5DTScENWjaQ=
github.com/chromedp/sysutil v1.0.0 h1:+ZxhTpfpZlmchB58ih/LBHX52ky7w2VhQVKQMucy3Ic=
github.com/chromedp/sysutil v1.0.0/go.mod h1:kgWmDdq8fTzXYcKIBqIYvRRTnYb9aNS9moAV0xufSww=
github.com/davecgh/go-spew v0.0.0-20161028175848-04cdfd42973b/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
Expand Down Expand Up @@ -33,15 +34,19 @@ github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0
github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc=
github.com/orisano/pixelmatch v0.0.0-20220722002657-fb0b55479cde h1:x0TT0RDC7UhAVbbWWBzr41ElhJx5tXPWkIHA2HWPRuw=
github.com/orisano/pixelmatch v0.0.0-20220722002657-fb0b55479cde/go.mod h1:nZgzbfBr3hhjoZnS66nKrHmduYNpc34ny7RK4z5/HM0=
github.com/pmezard/go-difflib v0.0.0-20151028094244-d8ed2627bdf0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/sanity-io/litter v1.5.5 h1:iE+sBxPBzoK6uaEP5Lt3fHNgpKcHXc/A2HGETy0uJQo=
github.com/sanity-io/litter v1.5.5/go.mod h1:9gzJgR2i4ZpjZHsKvUXIRQVk7P+yM3e+jAF7bU2UI5U=
github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d h1:zE9ykElWQ6/NYmHa3jpm/yHnI4xSofP+UP6SpjHcSeM=
github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc=
github.com/smartystreets/goconvey v1.6.7 h1:I6tZjLXD2Q1kjvNbIzB1wvQBsXmKXiVrhpRE8ZjP5jY=
github.com/smartystreets/goconvey v1.6.7/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
github.com/stretchr/testify v0.0.0-20161117074351-18a02ba4a312/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
Expand Down

0 comments on commit 8c6b7d0

Please sign in to comment.