WebView2Feedback icon indicating copy to clipboard operation
WebView2Feedback copied to clipboard

ExecuteScript synchronous

Open omegakode opened this issue 5 years ago • 22 comments

I think it would make sense, if you need to work with the return values it has to be synchronous, otherwise is very messy.

AB#25510588

omegakode avatar Mar 09 '20 23:03 omegakode

Thanks for the feedback!

To run script we must go cross process to the renderer process and run the script and so ExecuteScript really is asynchronous. A synchronous version of ExecuteScript would be a wrapper that would call the asynchronous method and then block until it completes. We could block while waiting for the async execution but our partners vary on what should happen while waiting (process just windows messages, process window messages & COM calls, some other set of CoWaitForMultipleHandles flags). Additionally our control is intended to run on the UI thread and we generally don't want to encourage blocking the UI thread or associated reentrancy issues. Accordingly, we've avoided making synchronous versions of our asynchronous functions.

How would you want to deal with reentrancy and what sort of things would you still want to process while synchronously waiting for the async operation to complete?

david-risney avatar Mar 10 '20 00:03 david-risney

A solution is to use WinRT's IAsyncOperation, but it may not compatible with Windows 7.

ysc3839 avatar Mar 10 '20 03:03 ysc3839

How would you want to deal with reentrancy and what sort of things would you still want to process while synchronously waiting for the async operation to complete?

It should be intended for small js function calls so i guess i should behave like any other function of the main thread, but i understand the complexibility. Currently i launch a message loop that exits when i post a custom message in the event handler.

omegakode avatar Mar 10 '20 14:03 omegakode

Thanks @champnic , I think its absolutely sensible to have a synchronous way of executing web-code as I explained in #2741 , when consumers of webView2 already have code running sync.

I don't want to get in depth of what you're doing, but why not have a wrapper that uses the same hack suggested by some people by using the DispatcherFrame? I don't think you should block the UI thread, as animations/etc could be running.

ahmed-salem-me avatar Sep 08 '22 12:09 ahmed-salem-me

@david-risney @champnic , I just noticed this has been open for 2 years! What does Microsoft suggest as a reliable work around? I now realized the hack that uses the DispatcherFrame which seemed to work, seems to terminate the process when the call originated from JS using a host object. same as this question: https://stackoverflow.com/questions/73038854/issue-in-calling-script-synchronously-from-webview2-in-wpf

I've already put a lot of effort and time to replace the web control we use with WebView2, and now it seems I can't proceed! :( that's a real shame

ahmed-salem-me avatar Sep 08 '22 15:09 ahmed-salem-me

We have big framework called OpenSilver http://github.com/opensilver/opensilver and we have what we call a simulator, that allows it to run within a desktop app using a web browser control

ahmed-salem-me avatar Sep 08 '22 15:09 ahmed-salem-me

Yes the issue is there's no good solution that will work for everyone.

  1. If you run WebView2 on the same UI thread as the rest of your app code you'll need to run a message pump so that the script execution completion window message can be delivered to the WebView2 and the WebView2 can complete the async call. Running a message loop like this has potential problems like reentrancy or out of order messages. Though depending on your app perhaps this isn't a problem.

  2. Or if you can run the code that is calling WebView2.ExecuteScriptAsync from a different thread than the WebView2 UI thread, you can use normal sort of multi-threading options to block on the caller thread waiting for the async call on the WebView2 UI thread to complete and signal the calling thread.

(2) is more reliable but the constraint of running code on a different thread isn't practical for most folks. (1) may work if you don't run into the issues I described above.

david-risney avatar Sep 08 '22 19:09 david-risney

Thanks @david-risney, Yes I just tried the second option, but unfortunately it doesn't solve the issue when the call to ExecuteScriptAsync originated from JS runtime through a host object, cause that happens on the UI thread similar to this issue https://stackoverflow.com/questions/73038854/issue-in-calling-script-synchronously-from-webview2-in-wpf

Our entire framework runs by talking to browser control, so it should work fine on a different thread except for what I just explained.

I don't really understand the first option probably lack of deeper knowledge on my side. and you already say it has potential problems. you say "if you run webview2 on the UI thread", is there a way to run it on a different thread? is that by creating the entire window and opening on a different thread and will that solve this problem?

ahmed-salem-me avatar Sep 09 '22 12:09 ahmed-salem-me

an example: class ABC { public GetBoxText() { return webView2.ExecuteScriptAsync("box.value");} } webView2.CoreWebView2.AddHostObjectToScript("abcObj", new ABC() from JS anotherBox.value = chrome.webview.hostObjects.abcObj.GetBoxText();

using a DispatcherFrame in this scenario seems to terminate the process

ahmed-salem-me avatar Sep 09 '22 14:09 ahmed-salem-me

In the above you have anotherBox.value = chrome.webview.hostObjects.abcObj.GetBoxText(); but that seems to be mixing async proxy and sync proxy semantics. Do you actually mean one of the following: a. anotherBox.value = await chrome.webview.hostObjects.abcObj.GetBoxText(); b. anotherBox.value = chrome.webview.hostObjects.sync.abcObj.GetBoxText();

(b) won't work because AddHostObjectToScript synchronous proxies don't support reentrancy. When in JS you call hostObjects.abcObj.GetBoxText(), the JS engine blocks execution to wait for GetBoxText to complete. While the JS engine is blocked waiting for GetBoxText it cannot run additional JavaScript.

(a) I would think should work but I don't know if we've explicitly tested that.

david-risney avatar Sep 09 '22 16:09 david-risney

this doesn't work image

ahmed-salem-me avatar Sep 09 '22 17:09 ahmed-salem-me

Hi All,

Regarding the thread safety and blocking the UI thread, webview2 document has mentioned that we cannot use waitforsingleobject or Task.Result. https://docs.microsoft.com/en-us/microsoft-edge/webview2/concepts/threading-model#block-the-ui-thread

Instead, an example given to use await for ExecuteScript in C#, which doesn't blocks the main thread execution and message pump continues. How can we achieve the same in Win32/C++ so that we can have ExecuteScript call in Synchronous manner?

raroraca avatar Sep 13 '22 08:09 raroraca

In my (2) above about calling from a different thread I meant you could have the calling thread communicate to the WebView2 UI thread to call ExecuteScript and then wait on an event to block the calling thread. When the WebView2 UI thread finishes running ExecuteScript it can signal the event to wake up the calling thread. The WebView2 has to be on a UI thread (or at least a thread that has a message loop) but it doesn't have to be the app's main UI thread. Alternatively the WebView2 could be on the app's main UI thread and the calling thread could be a different thread. Requiring the WebView2 to not be on the app's main UI thread or requiring the calling code to not run on the app's main UI thread is often not a reasonable restriction so this is probably not a good solution for most folks.

@raroraca, my descriptions of synchronous ExecuteScript described as (1) & (2) above also apply to Win32/C++ and with the same problems. Unfortunately there is no good general purpose solution. If you want to try (1) in Win32 you might try using CoWaitForMultipleHandles with the COWAIT_DISPATCH_WINDOW_MESSAGES parameter to ensure that WebView2's window messages used to communicate async method completion are dispatched.

david-risney avatar Sep 13 '22 16:09 david-risney

Thanks @david-risney , but come on guys, give me a workaround that works, other embedded web browsers out there do this out of the box. Give me some solution that works, it doesn't seem like I can run my whole app on a different thread without a lot of issues. Also, if I do, I have heaps of callbacks from JS runtime that calls a host object method even if my whole app code runs in a different thread, this callbacks through the host object still happens on the UI thread. What can I do? and plz I'm not a c++ developer, and kinda isolated from all the plumbing work of system messaging! You say your partners not united on what to do, so you end up having no solutions at all!!

ahmed-salem-me avatar Sep 13 '22 20:09 ahmed-salem-me

Hi @david-risney Thank you for the update. Tried to use the same recommendation, but seems did not worked for us. Following is the snip

HANDLE hEvent;
std::wstring resultValue = L"";

HRESULT _OnScriptExecuteCompleted(HRESULT error, LPCWSTR result)
{
	resultValue = result;
	SetEvent(hEvent);
	return S_OK;
}

HRESULT ExecuteScriptSync(ICoreWebView2* sender)
{
	// CoInitialize
	HRESULT hr = ::CoInitializeEx(NULL, COINIT_APARTMENTTHREADED);

	hEvent = CreateEvent(NULL, true, false, L"sample");
	std::wstring scriptToRun = L"document.getElementById('DialogIcon').value";
	 hr = sender->ExecuteScript(scriptToRun.c_str(), 
		Callback<ICoreWebView2ExecuteScriptCompletedHandler>(_OnScriptExecuteCompleted).Get());

	// This wait on the primary thread halts the message pump 
	// Completion events are not pumped and wait never ends...
	DWORD dwIndex;
	if(hEvent)
		hr = ::CoWaitForMultipleHandles(COWAIT_DISPATCH_WINDOW_MESSAGES, INFINITE, 1, &hEvent, &dwIndex);

	return hr;
}

In the above snip, CoWaitForMultipleHandles returns "0x80004021 : The operation attempted is not supported." Can you please review and recommend if something is not right? Note: This is the same main UI thread and message loop works fine in Async mode here.

raroraca avatar Sep 15 '22 10:09 raroraca

@raroraca Try change COWAIT_DISPATCH_WINDOW_MESSAGES to COWAIT_DISPATCH_WINDOW_MESSAGES | COWAIT_DISPATCH_CALLS | COWAIT_INPUTAVAILABLE? Ref: https://stackoverflow.com/questions/21226600 https://github.com/Boscop/web-view/blob/e87e08cab3a2d500d54068ec9c7aaa055a711465/webview-sys/webview_edge.cpp#L426 https://github.com/tauri-apps/tauri/issues/212#issuecomment-573791689

ysc3839 avatar Sep 15 '22 10:09 ysc3839

@ysc3839 Thank for for sharing different flag options, But did not worked in the above case. When we use COWAIT_DISPATCH_CALLS, it blocks the call and breaks after sometime. Callback does not gets completed in this case. @david-risney Do you have any further suggestion on this?

raroraca avatar Sep 16 '22 04:09 raroraca

I've tried the following in our WebView2 API Sample app and it seems to be working OK. This would be an example of (1) above. But again CoWait could be processing window messages or other calls that lead to weird ordering issues or reentrancy or other problems depending on your app.

        wil::unique_handle asyncMethodCompleteEvent(CreateEvent(nullptr, false, false, nullptr));
        m_webView->ExecuteScript(dialog.input.c_str(),
            Callback<ICoreWebView2ExecuteScriptCompletedHandler>(
                [asyncMethodCompleteEventHandle = asyncMethodCompleteEvent.get()](HRESULT error, PCWSTR result) -> HRESULT
                {
                    SetEvent(asyncMethodCompleteEventHandle);
                    return S_OK;
                }).Get());

        DWORD handleIndex = 0;
        CoWaitForMultipleHandles(COWAIT_DISPATCH_WINDOW_MESSAGES
            | COWAIT_DISPATCH_CALLS | COWAIT_INPUTAVAILABLE,
            INFINITE, 1, asyncMethodCompleteEvent.addressof(), &handleIndex);

david-risney avatar Sep 16 '22 17:09 david-risney

Okay, I finally got things to work for me with wpf+c# . (here is how, incase it help others)

Yes, using another thread to wait on (block) while your ExecuteScripAsync works. The problem was having huge calls from JS on a host object method, these calls happen on the main UI thread, so it can't call ExecuteScriptAsync ex, AddHostObject("abc", new ABC()) , later abc.DoSomething()

option1 : use a backgroundWorker to start our code then every time DoSomething() is called is to run a Task.Run(do whatever u need to and ExecuteScriptAsync).

This consumes many threads and most importantly disrupted our code that were assuming to run on the same thread.

options2: (that worked) create a second UI thread, which is just a thread with an SetApartmentState(ApartmentState.STA); to start our code then every time DoSomething() is called use this second UI thread.Dispatcher.InvokeAsync(do whatever u need to and ExecuteScriptAsync).

ahmed-salem-me avatar Sep 17 '22 22:09 ahmed-salem-me

Hi @david-risney, Thank you for verifying. The above code seems to be running in the example webview sample from Microsoft in only case of Inject Script. If we use the above snip in the InjectScript function from webview2 sample and ExecuteScript, it works well. But when we try to ExecuteScript in any callback completion eg NavigationCompleted or DOMContentLoaded, it does not works and hits a breakpoint after sometime. When we kept the snip of code in either ScenarioDOMContentLoaded or while NavigationComplete with AppWindow, it did not worked. Do we have any particular reason why it behaves like this? Is it okay to ExecuteScript with the above recommendation during any callback completion from webview2?

raroraca avatar Sep 19 '22 20:09 raroraca

Has anyone found a solution that works in winforms and avoids calling Application.DoEvents()? The reentrancy issues DoEvents creates make this approach unusable and afaik its not possible to host webview2 in a different thread in winforms (webview2 is a usercontrol in my case).

While i understand that blocking the UI is not best practice, there are situations where this is critical and a working solution is needed.

movedoa avatar Jun 02 '23 09:06 movedoa

Hi,

Perhaps I'm late to the party here, but I thought I would share our current workaround for this problem.

First I would also like to emphasize the need for guidance how to do synchronous wait.

When integrating the webview into an existing GUI framework you have to obey the programming model of that framework, which at least in our case is synchronous. Just processing all messages during the wait for completion is a very dangerous solution since it could/will introduce arbitrary reentrance problems on the callstack where the wait occurs. I would argue it is impossible to reason about the implications of this - but expect that bad things will happen eventually.

Our current workaround still process messages, but only a special message that appears to be used by the webview to notify completion events to the GUI thread - so they can be executed on that thread. This is obviously only an observed behaviour which I (and many other developers) have seen while debugging.

The code that does the waiting (and also implements a simple timeout mechanism) looks like this (depends on Win32 only) :

bool __fastcall WaitForCallbackHelper(LONG& Completed, DWORD Timeout)
{
  // Since we may wait multiple times before completion, we may need to handle
  // the caller Timeout manually by splitting it in smaller parts. The resolution
  // of this wait is selected to 100 ms which is good enough for our needs
  DWORD StartTick = GetTickCount();
  DWORD WaitTimeout = (Timeout == 0 || Timeout == INFINITE) ? Timeout : 100UL;

  MSG msg;
  while (true)
  {
    if ( Completed )
      return true;  // Never wait if already done

    DWORD waitResult = MsgWaitForMultipleObjectsEx(
                                  0, NULL,          // We don't wait for any handles
                                  WaitTimeout,      // May be partial wait of caller Timeout
                                  QS_POSTMESSAGE, 0 // Wait only for new posted messages
                                );
    switch ( waitResult )
    {
      case WAIT_OBJECT_0: // A posted message has arrived
      {
        // We only process posted messages used by WebView2 for completion events (WM_USER + 1 = 1025).
        //  - This knowledge is not officially documented so it is subject of change
        //  - We may get here for other posted messages which we ignore by not processing them.
        //    These messages will be processed later by the main message pump
        while (PeekMessage(&msg, NULL, 1025, 1025, PM_REMOVE))
          DispatchMessage(&msg);  // Will trigger webview2 callback in this thread (this will bump Completed)

        if ( Completed )
          return true;  // Some dispatched message completed *this* wait scope

        // If we reached here we possibly made some progress elsewhere given we dispatched
        // messages -or- we woke up because some other posted message arrived we don't care about

        // Fall through and also check for timeout before we wait again.
      }
      case WAIT_TIMEOUT:
        if ( WaitTimeout == 0 || (WaitTimeout != INFINITE && GetElapsedTick(StartTick, GetTickCount()) > Timeout) )
          return false;  // timeout occurred
        break;           // continue the wait

      default:
        return false;    // unexpected wait result occurred
    }
  }
}

The referred Completed flag is unique per waiting operation.

The little helper function GetElapsedTick looks like this

static DWORD GetElapsedTick(DWORD StartTick, DWORD CurrentTick)
{
  DWORD ElapsedTick;
  if ( StartTick <= CurrentTick )
    ElapsedTick = CurrentTick - StartTick;
  else
    ElapsedTick = ((DWORD)0) - StartTick + CurrentTick; // Handle the 49.7 day rollover case
  return ElapsedTick;
}

This code appears to work at least in our use cases:

  • It uses a simple flag Completed since both the wait and completion code (which modifies the flag) are running on the GUI thread (so an event is not really needed)
  • A more controlled reentrancy could still happen I guess - if the event handler (called by DispatchMessage) triggers more operations that wait this way

However, the code assumes a lot of things:

  • It assumes that only the message WM_USER + 1 needs to be processed
  • It assumes that the sending thread uses PostMessage (or possibly SendMessageTimeout) to put the message into the message queue of the GUI thread
  • It assumes that this undocumented behaviour does not change at anytime.
  • It may still pump messages for other controls which also posts WM_USER + 1 to do other things

Can anybody with knowledge of the internals confirm the validity of code like this?

danjagell-tietoevrycom avatar Mar 31 '25 12:03 danjagell-tietoevrycom