WebView2 CallDevToolsProtocolMethodAsync issue with Fetch.continueRequest - c#

I'm using WebView2 in a .net 5 WPF app and have been playing with the devtools protocol as a means of intercepting specific requests for assets. In looking at the Chrome dev docs (https://chromedevtools.github.io/devtools-protocol/), it's possible to intercept requests and then decide whether to continue them, cancel them or satisfy them yourself.
I've been able to successfully intercept the first web request (Eg. https:// www.somedomain.tld), but I've not been able to successfully continue the request (which would presumably trigger any other asset requests made as a result of the parsed html response).
After WebView initialization, I do the following (which works):
// Intercept requests
var receiver = webView.CoreWebView2.GetDevToolsProtocolEventReceiver("Fetch.requestPaused");
receiver.DevToolsProtocolEventReceived += FetchRequestPaused;
await webView.CoreWebView2.CallDevToolsProtocolMethodAsync("Fetch.enable", "{}");
This is my event handler - which doesn't do what I'm expecting it to (although it doesn't deadlock now at least):
private void FetchRequestPaused(object sender, Microsoft.Web.WebView2.Core.CoreWebView2DevToolsProtocolEventReceivedEventArgs e)
{
var doc = JsonDocument.Parse(e.ParameterObjectAsJson);
var id = doc.RootElement.GetProperty("requestId");
var payload = $"{{\"requestId\":\"{id}.0\"}}";
// We can't do this as an async call as it will try to post to the main thread, which is
// busy waiting in this event handler, so we deadlock
//_ = await webView.CoreWebView2.CallDevToolsProtocolMethodAsync("Fetch.continueRequest", payload);
// Exception: Value does not fall within the expected range.
// var result = await webView.CoreWebView2.CallDevToolsProtocolMethodAsync("Fetch.continueRequest", payload).ConfigureAwait(false);
// PROBLEM: This invokes the call on the UI thread OK...
Application.Current.Dispatcher.Invoke(new Action(() =>
{
// ...but it doesn't actually do anything
webView.CoreWebView2.CallDevToolsProtocolMethodAsync("Fetch.continueRequest", payload);
}));
}
Not only does the requested page not finish loading, but the browser is left in an unusual state and so right-clicking on the control and selecting "Refresh" will crash - yielding a COMException:
System.Runtime.InteropServices.COMException: 'The group or resource is not in the correct state to perform the requested operation. (0x8007139F)'
Can anyone see what I'm doing wrong here or am missing??
Thanks!
Additional information
In swapping out the events for the deprecated Network.setRequestInterception / Network.continueInterceptedRequest equivalents, I'm seeing the same behaviour - which at least tells us that it's either a problem with my calling code (most likely) or a bug in WebView2 (possible) rather than Chromium.
Any thoughts?

After some more digging, I realised there were two problems. The first is that my installed version of Edge was slightly behind. The second was that my Action delegate was synchronous. The call should read:
// Somebody forgot some 'async' keywords..!
Application.Current.Dispatcher.Invoke(new Action(async () =>
{
var x = await webView.CoreWebView2.CallDevToolsProtocolMethodAsync("Fetch.continueRequest", payload);
}));

Related

Bot receiving duplicate card click events

I am using the following libraries to connect a bot to a Google Pub/Sub endpoint to perform a simple reply to a card click event.
Google.Apis.HangoutsChat.v1 1.34.0.1233
Google.Cloud.PubSub.V1 1.0.0-beta18
When I construct my card, everything looks normal in the UI, including the button that is supposed to raise the event.
The topic and subscription contain the default settings, following the guide here
I found the following information from the Google documentation about retries here
Responding synchronously
A bot can respond to an event synchronously by returning a
JSON-formatted message payload in the HTTP response. The deadline for
a synchronous response is 30 seconds.
A synchronous response from a bot is always posted in the thread that
generated the event to the bot.
After clicking the button, my subscriber receives 3 duplicate events. The events have the correct response with all of the right metadata, but are exact duplicates of each other, including the id of the message itself.
I don't feel there is a necessarily large delay in the response of the bot (it should happen in <1 second for this test), so I am not sure why these messages are being duplicated.
I've also tried setting the thread id for the card when responding (via the Thread property itself, or the ThreadKey property), but I always seem to get a new thread id when I post a message.
var cardMessage = MessageSender.Spaces.Messages.Create(new Message()
{
Space = new Space()
{
Name = inReplyToThisMessage.Space.Name
},
Thread = new Thread()
{
Name = inReplyToThisMessage.Thread.Name
},
Cards = new List<Card>()
{
card
},
}, inReplyToThisMessage.Space.Name);
var sendCardResult = await cardMessage.ExecuteAsync().ConfigureAwait(false);
//Thread id of sendCardResult does not match inReplyToThisMessage.Thread.Name no matter what
Interestingly enough, trying to create a new message in response to the click event causes the bot to display a "Unable to connect to bot. Try again later", but displays 3 new messages. Also, when specifying an arbitrary thread key, this key is never echoed back in the bot's response.
Make sure you are returning the main event method properly. What looks to be happening is that you are making an asynchronous call to the chat, but then the chat is looking for a response from the actual event method itself. Google will traditionally try three times before giving up (even if it doesn't take thirty seconds)
If you are indeed returning the event call correctly after you made your api request, then there is something in your code that is causing the Google Bot to think it is not getting a response, so it tries three times. Since the issue could be multi-faceted I would need to look at how you are accepting and returning the click response.
This bug has finally been fixed by Google.

CefSharp offscreen - wait for page for render

I have a problem as below. I use the CefSharp offscreen for webpage automation as follows (I open only one and the same page):
1. Open page and wait untill it renders*.
2. With EvaluateScriptAsync I put on value to input form and then with the same method I click the button on webpage.
3. Then there is some JS on this webpage that check result and displays a message.
4. When the message is displayed I make a screenshot. **
However, I have two problems:
* My sulution has to be Internet speed proof. And As I used BrowserLoadingStateChanged event and IsLoading method, even though that the events fired the webpage did not load completly - when I started the EavluateScriptAsync method it gives back error because the page was not completly loaded. Sure, I can put sth like ThreadSleep but it does not always work - it is strongly dependent on Your internet speed.
** When I try to make a screenshot it does not always contain the result message displayed by JS - sometimes there is a loading circle instead of message. And here again I can use THreadSleep but it does not always work.
Do You have any ideas? Thanks in advance.
private static void BrowserLoadingStateChanged(object sender, LoadingStateChangedEventArgs e)
{
// Check to see if loading is complete - this event is called twice, one when loading starts
// second time when it's finished
// (rather than an iframe within the main frame).
if (!e.IsLoading)
{
// Remove the load event handler, because we only want one snapshot of the initial page.
browser.LoadingStateChanged -= BrowserLoadingStateChanged;
Thread.Sleep(1800); // e. g. but it isn't a solution in fact
var scriptTask = browser.EvaluateScriptAsync("document.getElementById('b-7').value = 'something'");
scriptTask = browser.EvaluateScriptAsync("document.getElementById('b-8').click()");
//scriptTask.Wait();
if (browser.IsLoading == false)
{
scriptTask.ContinueWith(t =>
{
//Give the browser a little time to render
//Thread.Sleep(500);
Thread.Sleep(500); // still not a solution
// Wait for the screenshot to be taken.
var task = browser.ScreenshotAsync();
task.ContinueWith(x =>
{
// Make a file to save it to (e.g. C:\Users\jan\Desktop\CefSharp screenshot.png)
var screenshotPath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.Desktop), "CefSharp screenshot.png");
Console.WriteLine();
Console.WriteLine("Screenshot ready. Saving to {0}", screenshotPath);
// Save the Bitmap to the path.
// The image type is auto-detected via the ".png" extension.
task.Result.Save(screenshotPath);
// We no longer need the Bitmap.
// Dispose it to avoid keeping the memory alive. Especially important in 32-bit applications.
task.Result.Dispose();
Console.WriteLine("Screenshot saved. Launching your default image viewer...");
// Tell Windows to launch the saved image.
Process.Start(screenshotPath);
Console.WriteLine("Image viewer launched. Press any key to exit.");
}, TaskScheduler.Default);
});
}
}
}
Ok, so in my case the best sollution was to use javascript to check if element by id exists. If yes then the page is loaded.
I noticed that render time may vary significantly depending on your hardware. It can take up to 5 seconds to render after EvaluateScriptAsync was called. So it always better to do longer delays before calling ScreenshotAsync() if you do not want to get outdated screenshot.
Thread.Sleep(5000);

Using await in async methods to prevent next line of code from running

I'm new to using async methods, so I think I'm misunderstanding something. I have a WinForms app with a button, and when the button is clicked, an async method gets called. This must be async, as I need to make javascript calls to a Chromium Web Browser control (using CefSharp). I need to ensure that this javascript has finished running and that the browser has updated before continuing with the next part of the method.
I'm basically trying to capture the entire web page into a single image. My approach was to use javascript to update the scroll position on the page, then take screenshots in each position using Graphics.CopyFromScreen. This mostly works, however occasionally the resulting image will have the wrong 'chunk' of webpage (e.g., the first bitmap is repeated twice). Here is my code:
// Calculate screen sizes, screenshot spacing etc.
for (int i = 0; i < screenshotCount; i++)
{
int scrollSize = i == 0 ? -PageHeight : (int)browserControlHeight;
string script = "(function() { window.scrollBy(0, " + scrollSize.ToString() + ") })();";
await browser.EvaluateScriptAsync(script);
// Take screenshot, add to list of bitmaps
}
// Combine resulting list of bitmaps
If I add the following
await Task.Delay(1000);
after the EvaluateScriptAsync() call, the final image comes out correct every time. I'm working on the assumption that the javascript is being called but doesn't complete before the screenshot begins. If this is the case, even adding a delay may not work (what if the javascript takes longer than a second to run?).
Am I misunderstanding the way that async/await works?
No, the issue is not with await, the issue is that the Task returned from EvaluateScriptAsync is being marked as completed before you're ready to continue. It's going to be marked as completed as soon as the javascript to inform the browser that it should scroll has executed, rather than being marked as completed after the browser has finished re-rendering the screen after being sent the scroll command.

Terminating Thread Running an Event

I wrote an API that automates a certain website. However, on the testing stage, I noticed that (not very sure), my thread is not being terminated correctly.
I am using the WebBrowser object to navigate inside a thread, so that it works synchronously with my program:
private void NavigateThroughTread(string url)
{
Console.WriteLine("Defining thread...");
var th = new Thread(() =>
{
_wb = new WebBrowser();
_wb.DocumentCompleted += PageLoaded;
_wb.Visible = true;
_wb.Navigate(url);
Console.WriteLine("Web browser navigated.");
Application.Run();
});
Console.WriteLine("Thread defined.");
th.SetApartmentState(ApartmentState.STA);
Console.WriteLine("Before thread start...");
th.Start();
Console.WriteLine("Thread started.");
while (th.IsAlive) { }
Console.WriteLine("Journey ends.");
}
private void PageLoaded(object sender, WebBrowserDocumentCompletedEventArgs e)
{
Console.WriteLine("Pages loads...");
.
.
.
switch (_action)
{
.
.
.
case ENUM.FarmActions.Idle:
_wb.Navigate(new Uri("about:blank"));
_action = ENUM.FarmActions.Exit;
return;
case ENUM.FarmActions.Exit:
Console.WriteLine("Disposing wb...");
_wb.DocumentCompleted -= PageLoaded;
_wb.Dispose();
break;
}
Application.ExitThread(); // Stops the thread
}
Here is how I call this function:
public int Attack(int x, int y, ArmyBuilder army)
{
// instruct to attack the village
_action = ENUM.FarmActions.Attack;
//get the army and coordinates
_army = army;
_enemyCoordinates[X] = x;
_enemyCoordinates[Y] = y;
//Place the attack command
_errorFlag = true; // the action is not complated, the flag will set as false once action is complete
_attackFlag = false; // attack is not made yet
Console.WriteLine("Journey starts");
NavigateThroughTread(_url.GetUrl(ENUM.Screens.RallyPoint));
return _errorFlag ? -1 : CalculateDistance();
}
So the problem is, when I call the Attack function, couple times like this:
_command.Attack(509, 355, new ArmyBuilder(testArmy_lc));
_command.Attack(509, 354, new ArmyBuilder(testArmy_lc));
_command.Attack(505, 356, new ArmyBuilder(testArmy_lc));
_command.Attack(504, 356, new ArmyBuilder(testArmy_lc));
_command.Attack(504, 359, new ArmyBuilder(testArmy_lc));
_command.Attack(505, 356, new ArmyBuilder(testArmy_lc));
_command.Attack(504, 356, new ArmyBuilder(testArmy_lc));
_command.Attack(504, 359, new ArmyBuilder(testArmy_lc));
My application most of the times, gets stuck in one of these function (usually happens after the 4th or 5th). When it gets stuck the last log that I see is
Web browser navigated.
I assume it is something to do with termination of my thread. Can someone show me how I can run a thread which runs the DocumentCompleted event ?
I don't see any obvious reason for deadlock, nor did it reproduce at all when testing the code. There are a number of flaws in the code but nothing that yells "here!" loudly. I can only make recommendations:
Consider that you do not need a thread at all. The while (th.IsAlive) { } hot loop blocks your main thread while you wait for the browser code to finish the job. That is not a useful way to use a thread, you might as well use your main thread. This instantly eliminates a large number of potential hang causes.
The state logic in PageLoaded is risky. We cannot see all of it but one glaring issue is that you dispose the WebBrowser twice. If you have a case where you use return without a Navigate() call then you'll hang as described. No need to unsubscribe the event but same story, if you do unsubscribe but don't all Application.Exit() then you'll hang as described. State machines can be hard to debug, thorough logging is necessary. Minimize the risk by moving the Dispose() call and unsubscribing the event out of the logic, it doesn't belong there. And you need to test what happens when any Navigate() call ends up in failure, redirecting to a page you did not expect.
The _wb.Dispose() call is risky. Note that you destroy the WebBrowser while its DocumentCompleted event is in flight. Technically that can return code execution to code that is no longer alive or present. That can trip a race condition in the browser. As well as in the debugger, there is a dedicated MDA that checks for this problem. It is trivially avoided by moving the Dispose() call after the Application.Run() call where it belongs.
The while-loop burns 100% core, potentially starving the worker thread. Not a good enough reason to explain deadlock, but certainly unnecessary. Use Thread.Join() instead.
You create a lot of WebBrowser objects in this code. It is a very heavy object, as you can imagine, you need to keep an eye on memory usage in your program. Especially the unmanaged kind. If the browser leaks, like they so often do, you could technically create a scenario where the WB initializes okay but does not have enough memory left to load the page. Strongly favor using only one WB.
You need to consider that this might well be an environmental problem. On the top of that list is forever anti-malware and firewall, they always have a very good reason to treat a browser specially since that is the most common malware injection vector. You'll need to run your test with anti-malware and firewall disabled to ensure that it is not the cause of the hang.
Another environmental problem is one I noticed while testing this code, Google got sulky about me hitting it so often and started to throttle the requests, greatly slowing down the code. Talk to the web site owner and ask if he's got similar blocking or throttling counter-measures in place, most do. You need to test your state logic to verify that it still works properly when the browser redirects to an error page.
Yet another environmental issue is the WB will display a dialog itself in certain cases. This can deadlock in 3rd party code, very hard to diagnose. You should at least set the WebBrower.ScriptErrorsSuppressed to true but beware of Javascript code in the web page you load that itself creates new windows or displays alert dialogs. Using one WB is the workaround.
Keep in mind that your program can only be as reliable as your Internet connection and the web page server. That's not a terribly good place to be of course, both are quite out of your reach and you don't get nice exceptions to help you diagnose such a failure. And consider that you probably have not yet tested your program well enough yet to check if it can survive such a failure, it doesn't happen enough.
Quite a laundry list, focus first on eliminating the unnecessary thread and temporarily suppressing anti-malware. That's quick, focus next on using only one WebBrowser.
Hans thank you, I was able to fix this issue with one of your ideas. As you spent your time giving me a long answer, I wanted respond in same manner.
2 - I built the state machine structure carefully and with a lot logs (you can see it from my git account) also did a lot of debugs. I am sure that after I'm done navigating, I use Application.ExitThread() and wb.Dispose() only once.
3 - I tried placing the wb.Dispose() outside the event, however I couldn't find any other place where the Thread is still alive. If I try disposing WebBrowser outside the thread which is created inside the thread, the application gives me an error.
4 - I changed the code while (th.IsAlive) { } with th.Join(2000) this is absolutely a better idea but did not change anything. It optimized the code and as you mentioned, it prevented burning 100% core of my CPU.
5 - I tried using a single WebBrowser object which is instantiated in the constructor. However when I tried to navigate inside the thread, the application wouldnt even fire the events anymore. For some reason, I couldn't make it running whit a single WB object.
6,7 - I tested my application with different PC's and diffrent networks(with firewall and non-firewall protection). I changed windows firewall options as well but no travail. On my original code I do have _wb.ScriptErrorsSuppressed = true; so this shouldn't also be the issue.
8,9 - If these are the reasons, I can't do anything about it. But I doubt the real problem is caused because of them.
1 - This one was a good suggestion. I tried implementing my code without using a thread and it is now working fine. Here is how it looks like (still needs a lot optimization)
// Constructer
public FarmActions(string token)
{
// set the urls using the token
_url = new URL(token);
// define web browser properties
_wb = new WebBrowser();
_wb.DocumentCompleted += PageLoaded;
_wb.Visible = true;
_wb.AllowNavigation = true;
_wb.ScriptErrorsSuppressed = true;
}
public int Attack(int x, int y, ArmyBuilder army)
{
// instruct to attack the village
_action = ENUM.FarmActions.Attack;
//get the army and coordinates
_army = army;
_enemyCoordinates[X] = x;
_enemyCoordinates[Y] = y;
//Place the attack command
_errorFlag = true; // the action is not complated, the flag will set as false once action is complete
_attackFlag = false; // attack is not made yet
_isAlive = true;
Console.WriteLine("-------------------------");
Console.WriteLine("Journey starts");
NavigateThroughTread(_url.GetUrl(ENUM.Screens.RallyPoint));
return _errorFlag ? -1 : CalculateDistance();
}
private void NavigateThroughTread(string url)
{
Console.WriteLine("Defining thread...");
_wb.Navigate(url);
while (_isAlive) Application.DoEvents();
}
private void PageLoaded(object sender, WebBrowserDocumentCompletedEventArgs e)
{
Console.WriteLine("Pages loads...");
.
.
.
switch (_action)
{
.
.
.
case ENUM.FarmActions.Idle:
_wb.Navigate(new Uri("about:blank"));
_action = ENUM.FarmActions.Exit;
return;
case ENUM.FarmActions.Exit:
break;
}
_isAlive = false;
}
This is how I was able to wait without using a thread.
The main problem was probably as you mentioned in number 3 or 5. But I wasn't able to fix the problem as I spent couple of hours.
Anyway thanks for your help it works.

SignalR WPF StateChange Issues

I am writing a little WPF application to test some SignalR code. Everything works from what I have written, but I have stumbled across something which I am unsure of.
I have created an event handler for the HubConnection StateChanged event;
_hub.StateChanged += (change) =>
{
Console.WriteLine("hubConnection.StateChanged {0} => {1}", change.OldState, change.NewState);
if (change.NewState == ConnectionState.Connecting)
{
statusCallBack callBack = new statusCallBack(UpdateStatus);
this.Dispatcher.Invoke(callBack, "hubConnection.StateChanged");
}
if (change.NewState == ConnectionState.Connected)
{
Console.WriteLine("hello");
statusCallBack callBack = new statusCallBack(UpdateStatus);
this.Dispatcher.Invoke(callBack, "hubConnection.StateChanged");
}
};
With my Delegate method statusCallBack and method being;
delegate void statusCallBack(string msg);
private void UpdateStatus(string msg)
{
if (this.Dispatcher.CheckAccess() == true)
{
this.tbStatus.AppendText(Environment.NewLine + DateTime.Now.ToLongTimeString() + " --- " + msg);
this.tbStatus.CaretIndex = this.tbStatus.Text.Length;
this.tbStatus.ScrollToEnd();
}
}
Now I am probably missing something really obvious here, but when in the StateChanged handler I check for ConnectionState.Connecting and output the message to my Label it works fine.
Then when the SignalR HubConnection state then changes to ConnectionState.Connected and I try to Invoke the delegate, the WPF app just locks up.
It will output to the console fine, and check to see if change.NewState == ConnectionState.Connected, then will output "hello" to the console, but then just freezes.
If I debug the app, when it gets to within the Connected if statement, the object change.NewState and change.OldState have the error message below.
View larger image here.
I am lost as to why it works within the first if statement, but not the second. Also why it is able to output the correct values to the console?
Even if I comment out the initial if statement to check for Connecting it will still freeze when Connected.
This is a common deadlock problem when calling back to the UI from an event.
Try using Dispatcher.BeginInvoke rather than Invoke.
As quoted in this thread:
When you use Dispatcher.BeginInvoke it means that it schedules the
given action for execution in the UI thread at a later point in time,
and then returns control to allow the current thread to continue
executing. Invoke blocks the caller until the scheduled action
finishes.
As a side-note: MVVM Light has a very useful helper called DispatcherHelper that is not only compatible with virtually everything (WPF, WinRT, SL, etc.), but very simple to use. It helps you automatically dispatch calls back to the UI. If nothing else, it may be helpful to use the NUGet package JUST for this feature.

Categories

Resources