Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Wait for EvaluateScriptAsync to complete #983

Closed
nico87 opened this issue Apr 28, 2015 · 46 comments
Closed

Wait for EvaluateScriptAsync to complete #983

nico87 opened this issue Apr 28, 2015 · 46 comments
Labels

Comments

@nico87
Copy link

nico87 commented Apr 28, 2015

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!

@amaitland
Copy link
Member

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?

@nico87
Copy link
Author

nico87 commented Apr 28, 2015

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.

@amaitland
Copy link
Member

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.

@nico87
Copy link
Author

nico87 commented Apr 28, 2015

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.

@amaitland
Copy link
Member

Your code should look like

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.

@amaitland
Copy link
Member

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

@amaitland
Copy link
Member

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

@nico87
Copy link
Author

nico87 commented Apr 28, 2015

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

Thank you for your time!

@nico87 nico87 closed this as completed Apr 28, 2015
@amaitland
Copy link
Member

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

@nico87
Copy link
Author

nico87 commented Apr 28, 2015

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.

@nico87 nico87 reopened this Apr 28, 2015
@amaitland
Copy link
Member

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

@amaitland
Copy link
Member

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

@rassilon
Copy link
Contributor

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.

@nico87
Copy link
Author

nico87 commented Apr 28, 2015

Thank you rassilon,

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

@amaitland I'm preparing the code for you!

@rassilon
Copy link
Contributor

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

Bill

@nico87
Copy link
Author

nico87 commented Apr 28, 2015

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

@rassilon
Copy link
Contributor

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

@amaitland
Copy link
Member

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

@rassilon
Copy link
Contributor

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.

@amaitland
Copy link
Member

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

@amaitland
Copy link
Member

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

@amaitland
Copy link
Member

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

@amaitland
Copy link
Member

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

@rassilon
Copy link
Contributor

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

Works for me.

@nico87
Copy link
Author

nico87 commented Apr 29, 2015

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.

@nico87
Copy link
Author

nico87 commented May 8, 2015

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!

@amaitland
Copy link
Member

Are you still trying to call EvaluateScriptAsync in IsBrowserInitializedChanged?

@nico87
Copy link
Author

nico87 commented May 8, 2015

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

@amaitland
Copy link
Member

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

@nico87
Copy link
Author

nico87 commented May 18, 2015

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.

@amaitland
Copy link
Member

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).

@nico87
Copy link
Author

nico87 commented May 22, 2015

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.

@shenopkss
Copy link

In the following way, this issue has been solved!

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<CefSharp.JavascriptResponse> 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);
    }
}

@amaitland
Copy link
Member

@shenopkss A simple ContinueWith will greatly simplify your code.

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

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

@shenopkss
Copy link

@amaitland 赞!nice!

@nico87
Copy link
Author

nico87 commented Feb 24, 2016

@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.

@nico87
Copy link
Author

nico87 commented Feb 24, 2016

@amaitland
Copy link
Member

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.

@amaitland
Copy link
Member

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

That code has been removed with 8de2257

@nico87
Copy link
Author

nico87 commented Feb 25, 2016

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!

@nico87
Copy link
Author

nico87 commented Feb 26, 2016

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();

@Anthonidas
Copy link

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?

@nico87
Copy link
Author

nico87 commented Apr 27, 2016

Hi @Anthonidas

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

@Anthonidas
Copy link

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.

@Astimus
Copy link

Astimus commented Oct 27, 2020

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:

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<JavascriptResponse> EvaluateScriptAsync(string script)
{
    return await _browser.GetMainFrame().EvaluateScriptAsync(script).ConfigureAwait(false);
}

It causes deadlock when EvaluateScript function called very often. I can't use async await here because I use EvaluateScript to handle Events (passing information like mouse clicks and keyboard to browser).

@amaitland
Copy link
Member

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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

No branches or pull requests

6 participants