EventHandle.WaitOne + WebBrowser = Deadlock when waiting for DocumentComplete

745 views Asked by At

I've got a Problem with the automatisation of WebBrowsing in an C#-Program. I've used the code before for a BHO and there it was working. But within a pure c# Program there seems to be some kind of deadlock. I've instructed my program to click on a search button and then wait (via a manualresetevent) for documentcomplete to signal. But now it seems that the click on the search button isn't processed until the ManualResetEvent signals a timeout. Thereafter the click is made and also the DocumentComplete-Event is fired.

For the structure of that programm: The WebBrowser-Control is part of a WindowsForm. The WebBrowser Control is passed to a Class which runs a Thread. This Class again passed the control to another class where the concrete behaviour according to the loaded webbrowser is programmed.

So in Code this lookes this way:

  1. Setup of the thread

    _runner = new Thread(runner);
    _runner.SetApartmentState(ApartmentState.STA);
    _runner.IsBackground = true;
    _runner.Start();
    
  2. Processing of withingthe Runner-Method

    b.placeTipp(workStructure);
    
  3. The PlaceTipp-Method

    public void placeTipp(ref OverallTippStructure tipp)
        {
            _expectedUrl = String.Empty;
            _betUrl = String.Empty;
            _status = BookieStatusType.CHECKLOGIN;
    
            while (true)
            {
                _mreWaitForAction.Reset();
                checkIETab();
    
                switch (_status)
                {
                    case BookieStatusType.REQUESTWEBSITE:
                        ConsoleWriter.writeToConsole(String.Format("Bookie {0}: Keine IE-Tab vorhanden. Fordere eine an", this.BookieName));
                        //if (RequestIETabEvent != null)
                        //    RequestIETabEvent(this, new EventArgs());
                        _status = BookieStatusType.NAVIGATETOWEBSITE;
                        _mreWaitForAction.Set();
                        break;
                    case BookieStatusType.NAVIGATETOWEBSITE:                                                
                        _webBrowser.Navigate(@"http://www.nordicbet.com");
                        break;
                    case BookieStatusType.CHECKLOGIN:
                        checkLogin();
                        break;
                    case BookieStatusType.LOGINNEEDED:
                        doLogin();
                        break;
                    case BookieStatusType.LOGGEDIN:
                        _status = BookieStatusType.SEARCHTIPP;
                        _mreWaitForAction.Set();
                        break;
                    case BookieStatusType.SEARCHTIPP:
                        searchTipp(tipp);
                        break;
                    case BookieStatusType.NAVTOSB:
                        NavToSB();
                        break;
                    case BookieStatusType.GETMARKET:
                        getMarket(tipp);
                        break;
                    case BookieStatusType.PLACEBET:
                        placeBet(tipp);
                        break;
                    case BookieStatusType.CONFIRMBET:
                        confirmBet();
                        break;
                    case BookieStatusType.EXTRACTBETDATA:
                        extractBetId(ref tipp);
                        break;
                    case BookieStatusType.FINISHED:
                        return;
                }
    
    
                if (!_mreWaitForAction.WaitOne(60000))
                {
                    //Sonderüberpüfung be LoginNeeded
                    if (_status == BookieStatusType.LOGINNEEDED)
                    {
                        //checkLogin();
                        throw new BookieLoginFailedExcpetion();
                    }
                    //TIMEOUT!
                    ConsoleWriter.writeToConsole(String.Format("Bookie {0}: Timeout bei warten auf nächsten Schritt. Status war {1}", this.BookieName, this._status.ToString()));
                    throw new BookieTimeoutExcpetion(String.Format("Bookie {0}: Timeout bei dem Warten auf Ereignis", this.BookieName));
                }
            }
        }
    
  4. The SearchTipp-Method where the Deadlock is happening:

    private void searchTipp(OverallTippStructure tipp)
        {
            if (_webBrowser.InvokeRequired)
            {
                _webBrowser.Invoke(new delegatePlaceBet(searchTipp), new object[] { tipp });
            }
            else
            {
                ConsoleWriter.writeToConsole(String.Format("Bookie {0}: Suche Tipp {1}", this.BookieName, tipp.BookieMatchName));
                _expectedUrl = String.Empty;
                if (!_webBrowser.Url.ToString().StartsWith("https://www.nordicbet.com/eng/sportsbook"))
                {
                    ConsoleWriter.writeToConsole(String.Format("Bookie {0}: Nicht auf Sportsbookseite. Navigiere", this.BookieName));
                    _status = BookieStatusType.NAVTOSB;
                    _mreWaitForAction.Set();
                    return;
                }
                _searchCompleted = false;
                HtmlDocument doc = _webBrowser.Document;
    
                if (doc != null)
                {
    
                    mshtml.IHTMLInputElement elemSearch = (mshtml.IHTMLInputElement)doc.GetElementById("query").DomElement;
                    if (elemSearch != null)
                    {
                        Thread.Sleep(Delayer.delay(2000, 10000));
                        elemSearch.value = tipp.BookieMatchName;
    
                        mshtml.IHTMLElement elemSearchButton = (mshtml.IHTMLElement)doc.GetElementById("search_button").DomElement;
                        if (elemSearchButton != null)
                        {
                            Thread.Sleep(Delayer.delay(900, 4000));
    
                            elemSearchButton.click();
                            //elemSearchButton.InvokeMember("click");
    
                            if (!_mreWaitForAction.WaitOne(10000)) //Here The Deadlock happens
                            {
                                //Now the click event and therefor the search will be executed
                                ConsoleWriter.writeToConsole(String.Format("Bookie {0}: Suche ist auf Timeout gelaufen", this.BookieName));
                                throw new BookieTimeoutExcpetion(String.Format("Bookie {0}: Suche ist auf Timeout gelaufen", this.BookieName));
                            }                                                        
    
                            _mreWaitForAction.Reset();
                            HtmlElement spanResult = doc.GetElementById("total_ocs_count");
                            while (spanResult == null)
                            {
                                Thread.Sleep(500);
                                spanResult = doc.GetElementById("total_ocs_count");
                            }
    
                            int total_ocs_count = 0;
                            if (!Int32.TryParse(spanResult.InnerHtml, out total_ocs_count))
                            {
                                ConsoleWriter.writeToConsole(String.Format("Bookie {0}: Tip {1} nicht gefunden", this.BookieName, tipp.BookieMatchName));
                                throw new BookieTippNotFoundExcpetion(String.Format("Bookie {0}: Tip {1} nicht gefunden", this.BookieName, tipp.BookieMatchName));
                            }
    
                            if (total_ocs_count <= 0)
                            {
                                ConsoleWriter.writeToConsole(String.Format("Bookie {0}: Tip {1} nicht gefunden", this.BookieName, tipp.BookieMatchName));
                                throw new BookieTippNotFoundExcpetion(String.Format("Bookie {0}: Tip {1} nicht gefunden", this.BookieName, tipp.BookieMatchName));
                            }
                            /*
                        else if (total_ocs_count > 1)
                        {
                            throw new BookieMoreThanOneFoundExcpetion(String.Format("Bookie {0}: Tipp {1} nicht eindeutig", this.BookieName, tipp.BookieMatchName));
                        }
                        */
                            ConsoleWriter.writeToConsole(String.Format("Bookie {0}: Tip {1} gefunden", this.BookieName, tipp.BookieMatchName));
                            _status = BookieStatusType.GETMARKET;
                        }
                    }
                }
                _mreWaitForAction.Set();
            }
        }
    

Has anybody an Idea what's happens here?

Thanks

lichtbringer

1

There are 1 answers

2
noseratio On BEST ANSWER

By doing _webBrowser.Invoke(new delegatePlaceBet(searchTipp), new object[] { tipp }), you make the searchTipp method be executed synchronously on the main UI thread. This is understood, as you cannot access WebBrowser API from any thread other than the original thread the control was created on.

However, by doing so, the _mreWaitForAction.WaitOne(10000) call will be too executed on the main UI thread, effectively blocking the message loop (which was started by Application.Run). WebBrowser needs a functional message loop, which is constantly pumping Windows messages, otherwise DocumentCompleted doesn't get fired, and you are getting a deadlock.

My understanding is, you only create another thread here to organize the workflow of your automation scenario. You really don't need another thread for that. Here's an example of how it can be done on the main UI thread asynchronously, using async/await, and here's an example of using WebBrowser in a console app.

Alternatively, as a workaround, you can use WaitWithDoEvents from here, like this: _mreWaitForAction.WaitWithDoEvents(10000). It waits for a handle while still pumping messages. You should be aware of potential implications of creating a nested message loop with Application.DoEvents().

Note, you still don't need a separate thread if you use a nested message loop. It will give you the linear code workflow on your main UI thread. Example:

private void buttonStart_Click(object sender, EventArgs e)
{
    using (var mre = new ManualResetEvent(false))
    {
        WebBrowserDocumentCompletedEventHandler handler = (s, args) => 
            mre.Set();
        this.webBrowser.DocumentCompleted += handler;
        try
        {
            this.webBrowser.Navigate("http://www.example.com");
            mre.WaitWithDoEvents(10000);
        }
        finally
        {
            this.webBrowser.DocumentCompleted -= handler;
        }
    }
    MessageBox.Show(this.webBrowser.Document.Body.OuterHtml);
}

Although as I've mentioned above, you can achieve the same more naturally and without nested message loops, using async/await, which is a preferred way of doing this, IMO.