Cefsharp: Wait for EvaluateScriptAsync to complete

Created on 28 Apr 2015  ·  46Comments  ·  Source: cefsharp/CefSharp

Hi,

I would like to wait for EvaluateScriptAsync to complete but all I get is a deadlock. I think that's because both the script task and the task.Wait() are running in the UI thread.
Is there another way to get this working?

Here's my code: https://gist.github.com/nico87/bf6ba552af8300a06b15

Thank you!

question

Most helpful comment

@shenopkss A simple ContinueWith will greatly simplify your code.

``` c#
browser.EvaluateScriptAsync("1 + 1").ContinueWith(x =>
{
var response = x.Result;

if (response.Success && response.Result != null)
{
    File.AppendAllText("result.txt", response.Result.ToString());
}

});
```

All 46 comments

Are you trying to make EvaluateScriptAsync into a sync function? I'd generally advise against that. Are you using .Net 4.5 e.g. can you use await?

Thank you for your reply.
Exactly. I know it's not a wise choice but I can't do something different. I'm using .net 4.0 and actually I cannot switch to 4.5 so await is not an option. Anyway, even using await won't work because I need that method to return the result value.

Long and the short, you've tried to Wait() on the same thread you've asked the task to continue on. If you really need to keep the function as it is, scrap the ContinueWith() and evaluate task.Result after task.Wait().

I'd suggest if you must have a helper function that you pass in some for of Action/delegate and execute that in the ContineWith() block rather than waiting. That way if you execute on the UI thread, when EvaluateScriptAsync is complete it will attempt to continue execution on that thread.

I've tried your first suggestion but I get an aggregate exception on the Wait instruction. The inner exception says "The communication object, System.ServiceModel.Channels.ServiceChannel, cannot be used for communication because it is in the Faulted state."
Yesterday I tried the delegate/Action solution, asking for a callback to be executed when the script is completed but at the end, I need the call to be synchronized and there I will get the deadlock again.

Your code should look like

``` c#
var task = browser.EvaluateScriptAsync(script, timeout);
task.Wait();

var response = task.Result;
result = response.Success ? (response.Result ?? "null") : response.Message;

return result;
```

If your seeing an exception, then your likely causing the WCF Channel to fault with whatever your executing.

(Waiting on the UI thread is a big no no, you really should investigate other solutions).

This sort of question is best asked on something like stackoverflow as it's not CefSharp specific, more generic .Net Threading/Task.

My code looks exactly like yours.
You're right, I bet write on SO.

Thank you for your time!

Here is a basic working sample https://gist.github.com/amaitland/7a41cc87b88dfcd30e0e

Are you sure the script your evaluating returns a result? You can usually execute your code manually using DevTools

It looks exactly like my code, I don't know why I get the faulted channel exception. If you say that's working for you, I think I have to investigate more on that.

What script are you executing? Can you confirm something simple works e.g. 1 + 1

Can you provide a fully functional GIST? Rather than just an abstract piece of code.

The GIST seems pretty clear the problem is most likely: TaskScheduler.FromCurrentSynchronizationContext()

You want your ContinueWith to run on the thread you call ExecuteScryptAsync from, but you call .Wait() thus causing your deadlock.

If you call the GIST code on your UI thread then you probably want:
.ContinueWith(...., TaskCreationOptions.HideScheduler) or some other variation so your callback gets called on the ThreadPool. Then in the callback if you need to post async result to the UI thread, use some delegate.BeginInvoke to do so.

Thank you rassilon,

Unfortunately I cannot use TaskCreationOptions.HideScheduler because I'm using .NET 4.0

@amaitland I'm preparing the code for you!

ContinueWith has an overload that can accept TaskScheduler.Default which is the ThreadPool on .Net 4.0.

Bill

You're right, I already used it this morning but the issue is still the same.
The point is that if I have to wait for the continuation task, the first won't be never executed because it was scheduled in the current thread. I've also tried to spawn a new thread and then execute EvaluateScriptAsync there but nothing changes, of course.

@amaitland You can find my code here: https://gist.github.com/nico87/ea25a6fd18762b97486c

Oh, EvaludateScriptAsync probably needs to use TaskScheduler.Default for the ContinueWith since it's continuing a TaskCompletionSource.

IsBrowserInitializedChanged is too early to be executing javascript, switch to FrameLoadStarted or NavStateChanged (Checking that IsLoading == false)

On the threading side of things code is being called in CEF's OnAfterCreated method, not specifically sure which one if it's threads that would be, probably the UI (Which is different to the WinForms UI Thread).

http://magpcss.org/ceforum/apidocs3/projects/%28default%29/CefLifeSpanHandler.html#OnAfterCreated%28CefRefPtr%3CCefBrowser%3E%29

Just as an FYI:
WCF Syncronization Context article

I'm not sure what the code for the WCF proxy looks like for Begin/End pairs. I think that is most likely where any problem is likely to be occurring, since I agree with you the odds of a SyncContext problem from OnAfterCreated do seem low.

I'd still argue that using a Continuation to retrieve some simple values from the response is overly complicated and unnecessary.

I think we have two problems here

  • Deadlock, taking the original code and modifying the Continuation to use TaskScheduler.Default works as expected
  • Need to wait for the page to at least start rendering before executing javascript

https://gist.github.com/amaitland/7a41cc87b88dfcd30e0e

IsBrowserInitializedChanged is really a horrible name, and possible misleading. It should probably be BrowserCreated better matching the underling CEF method OnAfterCreated

The example in the FAQ using TaskScheduler.FromCurrentSynchronizationContext() still works as expected in the context of the WPF Example when called from the UI thread. I've not tried in the WinForms example, though I'd imagine it would work just fine.

https://github.com/cefsharp/CefSharp/wiki/Frequently-asked-questions#2-how-do-you-call-a-javascript-method-that-return-a-result

IsBrowserInitializedChanged is really a horrible name, and possible misleading. It should probably be BrowserCreated better matching the underling CEF method OnAfterCreated

Works for me.

Thank you all for your help.
I still didn't sort this out. I've tried to apply all the suggestions you made but I always get a deadlock. To me it looks like the WinForms implementation is broken.
Even if I remove the complete.Wait(); line, after a while I get faulted tasks results (and no deadlocks of course).

I know that trying to sync the EvaluateScriptAsync method looks dumb but I have my own reasons.
When we first started working on our software, the only available browser was the internal WebBrowser control of .NET.
Then we switched to CefSharp1 that we wrapped around the IWebBrowser interface (to avoid changes to the rest of the application).
Now we would like to switch to CefSharp3 but since we are still using the old sync EvaluateScript of the old CefSharp1 interface in the entire software, we would like to make things work in the same way with the new CefSharp version.
I think we'll have to ditch CefSharp3 for now.

In the coming days I hope to get some other spare time to continue investigating this.
Eventually I will report here my successes.

Thank you for your time.

Guys, you know CefSharp better than me. Do you think this may be a CefSharp3 Winforms related issue?
I may be able to contribute and solve this problem.

Thank you!

Are you still trying to call EvaluateScriptAsync in IsBrowserInitializedChanged?

Nope. I'm calling it from NavStateChanged checking that IsLoading == false.

Can you fork the MinimalExample to recreate the problem? The complexity comes with the different threads that CEF implements, so it's likely not a bug per-say just a subtlety of how you interact with the browser.

https://github.com/cefsharp/CefSharp.MinimalExample

Hello amaitland,

I'm sorry for being so late. I've finally managed to get some spare time to recreate the issue on the Minimal Example Project. Here's my fork: https://github.com/nico87/CefSharp.MinimalExample

What I do in the example is a simulation of what I need in my real world usage of CefSharp:

  1. When the browser is ready, JS runs a script that call an object's method that I've previously registered (in my implementation that call is made every time the user clicks on the page).
  2. The called method performs some internal operations and then executes one or more EvaluateScriptAsync calls that I need to be sync.
  3. At the previous point the software hangs indefinitely.

JSB calls are executed in a sync fashion, so your call to showMessage() blocks the UI. For long running methods you'd typically return immediately and use a callback.

To use CefSharp3 I think you'll have to rewrite your code to properly support async (Your UI will likely benefit from this).

I agree with you but there's a point in my software were I have to wait for the JS result before going on.
In that case the deadlock is ensured, as far as I can see. I have no clue on how solving this problem, right now.

In the following way, this issue has been solved!

``` c#

public BrowserForm()
{
InitializeComponent();

chromiumWebBrowser = new ChromiumWebBrowser("http://google.com") { Dock = DockStyle.Fill };
this.Controls.Add(chromiumWebBrowser);
chromiumWebBrowser.LoadingStateChanged += ChromiumWebBrowser_LoadingStateChanged1;

}

private void ChromiumWebBrowser_LoadingStateChanged1(object sender, LoadingStateChangedEventArgs e)
{
if (e.IsLoading == false) {
Task response = chromiumWebBrowser.EvaluateScriptAsync("document.getElementById('id').innerHTML");

    Func<Task<CefSharp.JavascriptResponse>> func = () =>
    {
        response.Wait();
        return response;
    };
    func.BeginInvoke((ar) =>
    {
        var result = func.EndInvoke(ar);
        if (result.Result.Result != null)
        {
            File.AppendAllText("result.txt", result.Result.Result.ToString());
        }      
    },
    null);
}

}
```

@shenopkss A simple ContinueWith will greatly simplify your code.

``` c#
browser.EvaluateScriptAsync("1 + 1").ContinueWith(x =>
{
var response = x.Result;

if (response.Success && response.Result != null)
{
    File.AppendAllText("result.txt", response.Result.ToString());
}

});
```

@amaitland 赞!nice!

@shenopkss, that's not a solution. Your code is running asynchronously while the issue is related to a synchronous execution of EvaluateScript.
Btw, I was able to make things working with the following code:

public object EvaluateScript(string script, object defaultValue, TimeSpan timeout) {
    var result = defaultValue;
    if (_browser.IsBrowserInitialized && !_browser.IsDisposed && !_browser.Disposing) {
       try {
         var task = _browser.EvaluateScriptAsync(script, timeout);
         var completed = task.ContinueWith(res => {
            if (!res.IsFaulted) {
                 var response = res.Result;
                 result = response.Success ? (response.Result ?? "null") : response.Message;
            }
            else {
               Console.WriteLine("JS Thread is faulted");
            }
         });
         completed.Wait();
      }
      catch (Exception e) {
         Console.WriteLine(e.InnerException.Message);
      }
   }
   return result;
}

Unfortunately, with releases newer than 39.0.2 this code is not working anymore. Therefore I'm still looking for a real solution.

PS: Is this example supposed to work? https://github.com/cefsharp/CefSharp/blob/master/CefSharp.WinForms.Example/BrowserTabUserControl.cs#L200

Probably not, don't think it's ever called.

Thanks for your efforts.
I'll continue to look for a way to sync an async code. If I'll find a solution, I post it here!

I finally managed to solve my synchronization problems with v41+ builds.
I had the change to upgrade to .Net 4.6 so I used async/await.
Here's the code:

public class MyBrowser {

    private ChromiumWebBrowser _browser;

    // Some initialization/constructor code here
    // ...

    // Sync the async script execution
    public async Task<object> EvaluateScript(string script, object defaultValue, TimeSpan timeout) {
        object result = defaultValue;
        if (_browser.IsBrowserInitialized && !_browser.IsDisposed && !_browser.Disposing) {
            try {
                var task = _browser.EvaluateScriptAsync(script, timeout);
                await task.ContinueWith(res => {
                    if (!res.IsFaulted) {
                        var response = res.Result;
                        result = response.Success ? (response.Result ?? "null") : response.Message;
                    }
                }).ConfigureAwait(false); // <-- This makes the task to synchronize on a different context
            }
            catch (Exception e) {
                Console.WriteLine(e.InnerException.Message);
            }
        }
        return result;
    }
}

I can use it like this:

object result = myBrowser.EvaluateScript("2+2", 0, TimeSpan.FromSeconds(1)).GetAwaiter().GetResult();

Hi @nico87

I'd also like to use the "await" but using your code i get the following error:
An object reference is required for the non-static field, method, or property 'weeBrowserForm.MyBrowser.EvaluateScript(string, object, TimeSpan)'

Am I missing something?

Hi @Anthonidas

Is your browser correctly initialized? You should be able to get more information from the exception's stack trace.

Hi @nico87
Yes the browser was inizialized correctly. I really do not know what the problem was, but in the meantime i resolved my problem executing all script parts in a "big" function, so that i have to wait just for a single function to execute.

Are you trying to make EvaluateScriptAsync into a sync function? I'd generally advise against that. Are you using .Net 4.5 e.g. can you use await?

I'm trying to make EvaluateScriptAsync into a sync function. And it causes deadlock. This is my code:
```c#
public JavascriptResponse EvaluateScript(string script)
{
var response = Task.Run(() => EvaluateScriptAsync(script)).GetAwaiter().GetResult();
if (response.Success)
return response;

 throw new EvaluateJSScriptException(response.Message);

}

private async Task EvaluateScriptAsync(string script)
{
return await _browser.GetMainFrame().EvaluateScriptAsync(script).ConfigureAwait(false);
}
`` It causes deadlock whenEvaluateScriptfunction called very often. I can't use async await here because I useEvaluateScript` to handle Events (passing information like mouse clicks and keyboard to browser).

I'm trying to make EvaluateScriptAsync into a sync function

EvaluateScriptAsync is an async function and calling it in a sync fashion is not supported.

I can't use async await here because I use EvaluateScript to handle Events (passing information like mouse clicks and keyboard to browser).

.Net supports asynchronous event handlers. There's no need for sync calls.

In terms of keyboard/mouse events there are methods for sending those directly to the browser http://cefsharp.github.io/api/85.3.x/html/Methods_T_CefSharp_IBrowserHost.htm

You could potentially use DevTools to send input events also

This thread has been inactive for over four years, please use one of the options listed at https://github.com/cefsharp/CefSharp#contact to ask any follow up questions.

Was this page helpful?
0 / 5 - 0 ratings