From 559ebadc2781fef1a6996e2f0d4027bf829853d4 Mon Sep 17 00:00:00 2001 From: Isaac Dawson Date: Tue, 13 Oct 2015 19:38:40 -0700 Subject: [PATCH] add chrome termination and tab crash handling --- autogcd.go | 15 ++++++++-- autogcd_test.go | 29 ++++++++++++++++-- element.go | 29 ++++++++++++++++++ tab.go | 69 +++++++++++++++++++++++++++++-------------- tab_subscribers.go | 18 ++++++++++++ tab_test.go | 73 +++++++++++++++++++++++++++++++++++++++------- 6 files changed, 196 insertions(+), 37 deletions(-) diff --git a/autogcd.go b/autogcd.go index de7dc91..2d2318f 100644 --- a/autogcd.go +++ b/autogcd.go @@ -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 @@ -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) } @@ -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) diff --git a/autogcd_test.go b/autogcd_test.go index bb9e037..5ddb413 100644 --- a/autogcd_test.go +++ b/autogcd_test.go @@ -9,6 +9,7 @@ import ( "os" "runtime" "testing" + "time" ) var ( @@ -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) { @@ -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) { @@ -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) @@ -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 } diff --git a/element.go b/element.go index 0665943..d892e6f 100644 --- a/element.go +++ b/element.go @@ -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 @@ -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) { @@ -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. diff --git a/tab.go b/tab.go index 9fcff0d..9e06441 100644 --- a/tab.go +++ b/tab.go @@ -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) @@ -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 @@ -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 @@ -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 } @@ -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 @@ -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) }() @@ -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: @@ -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) + } } } } diff --git a/tab_subscribers.go b/tab_subscribers.go index af90464..cb80b07 100644 --- a/tab_subscribers.go +++ b/tab_subscribers.go @@ -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) { diff --git a/tab_test.go b/tab_test.go index 554d1f3..e6b1baa 100644 --- a/tab_test.go +++ b/tab_test.go @@ -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) {