Skip to content

Commit

Permalink
add chrome termination and tab crash handling
Browse files Browse the repository at this point in the history
  • Loading branch information
Isaac Dawson authored and Isaac Dawson committed Oct 14, 2015
1 parent de6e838 commit 559ebad
Show file tree
Hide file tree
Showing 6 changed files with 196 additions and 37 deletions.
15 changes: 12 additions & 3 deletions autogcd.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,13 +23,12 @@ or rewriting links etc.
package autogcd

import (
"fmt"
"github.com/wirepair/gcd"
"os"
"sync"
)

var Debug = false

type AutoGcd struct {
debugger *gcd.Gcd
settings *Settings
Expand All @@ -43,7 +42,7 @@ func NewAutoGcd(settings *Settings) *AutoGcd {
auto.tabLock = &sync.RWMutex{}
auto.tabs = make(map[string]*Tab)
auto.debugger = gcd.NewChromeDebugger()

auto.debugger.SetTerminationHandler(auto.defaultTerminationHandler)
if len(settings.extensions) > 0 {
auto.debugger.AddFlags(settings.extensions)
}
Expand All @@ -59,6 +58,16 @@ func NewAutoGcd(settings *Settings) *AutoGcd {
return auto
}

// Default termination handling is to log, override with SetTerminationHandler
func (auto *AutoGcd) defaultTerminationHandler(reason string) {
panic(fmt.Sprintf("chrome was terminated: %s\n", reason))
}

// Allow callers to handle chrome terminating.
func (auto *AutoGcd) SetTerminationHandler(handler gcd.TerminatedHandler) {
auto.debugger.SetTerminationHandler(handler)
}

// Starts Google Chrome with debugging enabled.
func (auto *AutoGcd) Start() error {
auto.debugger.StartProcess(auto.settings.chromePath, auto.settings.userDir, auto.settings.chromePort)
Expand Down
29 changes: 27 additions & 2 deletions autogcd_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import (
"os"
"runtime"
"testing"
"time"
)

var (
Expand Down Expand Up @@ -55,7 +56,7 @@ func TestStart(t *testing.T) {
if err := auto.Start(); err != nil {
t.Fatalf("failed to start chrome: %s\n", err)
}

auto.SetTerminationHandler(nil)
}

func TestGetTab(t *testing.T) {
Expand All @@ -72,7 +73,6 @@ func TestGetTab(t *testing.T) {
if tab.Target.Type != "page" {
t.Fatalf("Got tab but wasn't of type Page")
}

}

func TestNewTab(t *testing.T) {
Expand Down Expand Up @@ -118,6 +118,30 @@ func TestCloseTab(t *testing.T) {
}
}

func TestChromeTermination(t *testing.T) {
auto := testDefaultStartup(t)
doneCh := make(chan struct{})
shutdown := time.NewTimer(time.Second * 4)
timeout := time.NewTimer(time.Second * 10)
terminatedHandler := func(reason string) {
t.Logf("reason: %s\n", reason)
doneCh <- struct{}{}
}

auto.SetTerminationHandler(terminatedHandler)
for {
select {
case <-doneCh:
goto DONE
case <-shutdown.C:
auto.Shutdown()
case <-timeout.C:
t.Fatalf("timed out waiting for termination")
}
}
DONE:
}

func testDefaultStartup(t *testing.T) *AutoGcd {
s := NewSettings(testPath, testRandomDir(t))
s.RemoveUserDir(true)
Expand All @@ -127,6 +151,7 @@ func testDefaultStartup(t *testing.T) *AutoGcd {
if err := auto.Start(); err != nil {
t.Fatalf("failed to start chrome: %s\n", err)
}
auto.SetTerminationHandler(nil) // do not want our tests to panic
return auto
}

Expand Down
29 changes: 29 additions & 0 deletions element.go
Original file line number Diff line number Diff line change
Expand Up @@ -307,6 +307,7 @@ func (e *Element) removeChild(removedNode *gcdapi.DOMNode) {
if e.node == nil || e.node.Children == nil {
return
}

for idx, child = range e.node.Children {
if child.NodeId == removedNode.NodeId {
childIdx = idx
Expand Down Expand Up @@ -383,6 +384,22 @@ func (e *Element) IsEnabled() (bool, error) {
return true, nil
}

// Simulate WebDrivers checked propertyname check
func (e *Element) IsSelected() (bool, error) {
e.lock.RLock()
defer e.lock.RUnlock()

if !e.ready {
return false, &ElementNotReadyErr{}
}

checked, ok := e.attributes["checked"]
if ok == true && checked != "false" {
return true, nil
}
return false, nil
}

// Returns the CSS Style Text of the element, returns the inline style first
// and the attribute style second, or error.
func (e *Element) GetCssInlineStyleText() (string, string, error) {
Expand Down Expand Up @@ -438,6 +455,18 @@ func (e *Element) GetAttribute(name string) string {
return attr[name]
}

// Similar to above but works for boolean properties (checked, async etc)
// Returns true if the attribute is set in our known list of attributes
// for this element.
func (e *Element) HasAttribute(name string) bool {
attr, err := e.GetAttributes()
if err != nil {
return false
}
_, exists := attr[name]
return exists
}

// Works like WebDriver's clear(), simply sets the attribute value for input
// or clears the value for textarea. This element must be ready so we can
// properly read the nodeName value.
Expand Down
69 changes: 47 additions & 22 deletions tab.go
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,9 @@ func (e *TimeoutErr) Error() string {

type GcdResponseFunc func(target *gcd.ChromeTarget, payload []byte)

// Called when the tab crashes or the inspector was disconnected
type TabDisconnectedHandler func(tab *Tab, reason string)

// A function to handle javascript dialog prompts as they occur, pass to SetJavaScriptPromptHandler
// Internally this should call tab.Page.HandleJavaScriptDialog(accept bool, promptText string)
type PromptHandlerFunc func(tab *Tab, message, promptType string)
Expand Down Expand Up @@ -88,23 +91,25 @@ type ConditionalFunc func(tab *Tab) bool

// Our tab object for driving a specific tab and gathering elements.
type Tab struct {
*gcd.ChromeTarget // underlying chrometarget
eleMutex *sync.RWMutex // locks our elements when added/removed.
elements map[int]*Element // our map of elements for this tab
topNodeId atomic.Value // the nodeId of the current top level #document
topFrameId atomic.Value // the frameId of the current top level #document
isNavigatingFlag atomic.Value // are we currently navigating (between Page.Navigate -> page.loadEventFired)
isTransitioningFlag atomic.Value // has navigation occurred on the top frame (not due to Navigate() being called)
debug bool // for debug printing
nodeChange chan *NodeChangeEvent // for receiving node change events from tab_subscribers
navigationCh chan int // for receiving navigation complete messages while isNavigating is true
docUpdateCh chan struct{} // for receiving document update completion while isNavigating is true
navigationTimeout time.Duration // amount of time to wait before failing navigation
elementTimeout time.Duration // amount of time to wait for element readiness
stabilityTimeout time.Duration // amount of time to give up waiting for stability
stableAfter time.Duration // amount of time of no activity to consider the DOM stable
lastNodeChangeTimeVal atomic.Value // timestamp of when the last node change occurred atomic because multiple go routines will modify
domChangeHandler DomChangeHandlerFunc // allows the caller to be notified of DOM change events.
*gcd.ChromeTarget // underlying chrometarget
eleMutex *sync.RWMutex // locks our elements when added/removed.
elements map[int]*Element // our map of elements for this tab
topNodeId atomic.Value // the nodeId of the current top level #document
topFrameId atomic.Value // the frameId of the current top level #document
isNavigatingFlag atomic.Value // are we currently navigating (between Page.Navigate -> page.loadEventFired)
isTransitioningFlag atomic.Value // has navigation occurred on the top frame (not due to Navigate() being called)
debug bool // for debug printing
nodeChange chan *NodeChangeEvent // for receiving node change events from tab_subscribers
navigationCh chan int // for receiving navigation complete messages while isNavigating is true
docUpdateCh chan struct{} // for receiving document update completion while isNavigating is true
crashedCh chan string // the chrome tab crashed with a reason
disconnectedHandler TabDisconnectedHandler // called with reason the chrome tab was disconnected from the debugger service
navigationTimeout time.Duration // amount of time to wait before failing navigation
elementTimeout time.Duration // amount of time to wait for element readiness
stabilityTimeout time.Duration // amount of time to give up waiting for stability
stableAfter time.Duration // amount of time of no activity to consider the DOM stable
lastNodeChangeTimeVal atomic.Value // timestamp of when the last node change occurred atomic because multiple go routines will modify
domChangeHandler DomChangeHandlerFunc // allows the caller to be notified of DOM change events.
}

// Creates a new tab using the underlying ChromeTarget
Expand All @@ -115,6 +120,7 @@ func NewTab(target *gcd.ChromeTarget) *Tab {
t.nodeChange = make(chan *NodeChangeEvent)
t.navigationCh = make(chan int, 1) // for signaling navigation complete
t.docUpdateCh = make(chan struct{}) // wait for documentUpdate to be called during navigation
t.crashedCh = make(chan string) // reason the tab crashed/was disconnected.
t.navigationTimeout = 30 * time.Second // default 30 seconds for timeout
t.elementTimeout = 5 * time.Second // default 5 seconds for waiting for element.
t.stabilityTimeout = 2 * time.Second // default 2 seconds before we give up waiting for stability
Expand All @@ -124,8 +130,9 @@ func NewTab(target *gcd.ChromeTarget) *Tab {
t.DOM.Enable()
t.Console.Enable()
t.Debugger.Enable()
t.disconnectedHandler = t.defaultDisconnectedHandler
t.subscribeEvents()
go t.listenDOMChanges()
go t.listenDebuggerEvents()
return t
}

Expand All @@ -134,6 +141,14 @@ func (t *Tab) Debug(enabled bool) {
t.debug = enabled
}

func (t *Tab) SetDisconnectedHandler(handlerFn TabDisconnectedHandler) {
t.disconnectedHandler = handlerFn
}

func (t *Tab) defaultDisconnectedHandler(tab *Tab, reason string) {
t.debugf("tab %s tabId: %s", reason, tab.ChromeTarget.Target.Id)
}

// How long to wait in seconds for navigations before giving up, default is 30 seconds
func (t *Tab) SetNavigationTimeout(timeout time.Duration) {
t.navigationTimeout = timeout
Expand Down Expand Up @@ -211,6 +226,8 @@ func (t *Tab) Navigate(url string) (string, error) {
return "", &InvalidNavigationErr{Message: "Unable to navigate, already navigating."}
}
t.setIsNavigating(true)
t.debugf("navigating to %s", url)

defer func() {
t.setIsNavigating(false)
}()
Expand Down Expand Up @@ -952,12 +969,16 @@ func (t *Tab) subscribeEvents() {
t.subscribeLoadEvent()
t.subscribeFrameLoadingEvent()
t.subscribeFrameFinishedEvent()

// Crash related
t.subscribeTargetCrashed()
t.subscribeTargetDetached()
}

// listens for NodeChangeEvents and dispatches them accordingly. Calls the user
// defined domChangeHandler if bound. Updates the lastNodeChangeTime to the current
// time.
func (t *Tab) listenDOMChanges() {
// listens for NodeChangeEvents and crash events and dispatches them accordingly.
// Calls the user defined domChangeHandler if bound. Updates the lastNodeChangeTime
// to the current time. If the target crashes or is detached, call the disconnectedHandler.
func (t *Tab) listenDebuggerEvents() {
for {
select {
case nodeChangeEvent := <-t.nodeChange:
Expand All @@ -968,6 +989,10 @@ func (t *Tab) listenDOMChanges() {
t.domChangeHandler(t, nodeChangeEvent)
}
t.lastNodeChangeTimeVal.Store(time.Now())
case reason := <-t.crashedCh:
if t.disconnectedHandler != nil {
go t.disconnectedHandler(t, reason)
}
}
}
}
Expand Down
18 changes: 18 additions & 0 deletions tab_subscribers.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,24 @@ import (
"github.com/wirepair/gcd/gcdapi"
)

func (t *Tab) subscribeTargetCrashed() {
t.Subscribe("Inspector.targetCrashed", func(target *gcd.ChromeTarget, payload []byte) {
t.crashedCh <- "crashed"
})
}

func (t *Tab) subscribeTargetDetached() {
t.Subscribe("Inspector.detached", func(target *gcd.ChromeTarget, payload []byte) {
header := &gcdapi.InspectorDetachedEvent{}
err := json.Unmarshal(payload, header)
if err == nil {
t.crashedCh <- header.Params.Reason
} else {
t.crashedCh <- "detached"
}
})
}

// our default loadFiredEvent handler, returns a response to resp channel to navigate once complete.
func (t *Tab) subscribeLoadEvent() {
t.Subscribe("Page.loadEventFired", func(target *gcd.ChromeTarget, payload []byte) {
Expand Down
73 changes: 63 additions & 10 deletions tab_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -587,21 +587,74 @@ func TestTabSslError(t *testing.T) {
t.Fatalf("error getting tab")
}
tab.Debug(true)
//tab.ChromeTarget.DebugEvents(true)
//tab.Network.Enable()
if _, err := tab.Navigate("https://173.194.123.39/"); err != nil {
// Test expired SSL certificate
// "--test-type", "--ignore-certificate-errors", should not return any errors
if _, err := tab.Navigate("https://expired.identrustssl.com/"); err != nil {
t.Fatalf("error opening first window: %s\n", err)
}
//tab.WaitStable()
url, _ := tab.GetPageSource(0)
t.Logf("url: %s\n", url)

ret, failText := tab.DidNavigationFail()
if ret == false {
t.Fatalf("navigation should have failed but got false back\n")
} else {
t.Logf("nav error: %s\n", failText)
t.Logf("ret: %t failText: %s\n", ret, failText)
if ret == true {
t.Fatalf("navigation should have succeeded but got failed back: %s\n", failText)
}
// Test invalid CN name
// example.com ip https://93.184.216.34/
// "--test-type", "--ignore-certificate-errors", should not return any errors
if _, err := tab.Navigate("https://93.184.216.34/"); err != nil {
t.Fatalf("error opening invalid cn host: %s\n", err)
}

ret, failText = tab.DidNavigationFail()
t.Logf("ret: %t failText: %s\n", ret, failText)
if ret == true {
t.Fatalf("navigation should have succeeded but got failed back: %s\n", failText)
}
}

func TestTabChromeTabCrash(t *testing.T) {
testAuto := testDefaultStartup(t)
defer testAuto.Shutdown()
tab, err := testAuto.GetTab()
if err != nil {
t.Fatalf("error getting tab")
}
timeout := time.NewTimer(time.Second * 10)
doneCh := make(chan struct{})
handlerFn := func(tab *Tab, reason string) {
doneCh <- struct{}{}
}

go func() {
<-timeout.C
t.Fatalf("timed out waiting for termination event")
}()

tab.SetDisconnectedHandler(handlerFn)
if _, err := tab.Navigate("chrome://crash"); err == nil {
t.Fatalf("crash window did not cause error\n")
}
<-doneCh
}

func TestTabChromeUnhandledCrash(t *testing.T) {
testAuto := testDefaultStartup(t)
defer testAuto.Shutdown()
tab, err := testAuto.GetTab()
if err != nil {
t.Fatalf("error getting tab")
}
timeout := time.NewTimer(time.Second * 10)
tab.Debug(true)
go func() {
<-timeout.C
t.Fatalf("timed out waiting for termination event")
}()

tab.SetNavigationTimeout(10 * time.Second)
if _, err := tab.Navigate("chrome://crash"); err == nil {
t.Fatalf("error opening crash did not return error\n")
}
}

func testMultiNavigateSendKeys(t *testing.T, wg *sync.WaitGroup, tab *Tab) {
Expand Down

0 comments on commit 559ebad

Please sign in to comment.