cefsharp / CefSharp

.NET (WPF and Windows Forms) bindings for the Chromium Embedded Framework
http://cefsharp.github.io/
Other
9.86k stars 2.92k forks source link

Custom ResourceHandler, UserAgent and specific website causes deadlock on Shutdown #1169

Closed ghost closed 9 years ago

ghost commented 9 years ago

Bear with me here. I've got a very very strange bug and it only happens in the following situation:

Only if all these things are true, it will hang on Cef.Shutdown() on GC::WaitForPendingFinalizers().

The combination of conditions smells like a Javascript bug and livechatinc's Javascript probably chokes on a non-standard user agents. Below I've included the modified Offscreen.Example.

// Copyright © 2010-2014 The CefSharp Authors. All rights reserved.
//
// Use of this source code is governed by a BSD-style license that can be found in the LICENSE file.

using System;
using System.Diagnostics;
using System.Drawing;
using System.IO;
using System.Text;
using System.Threading.Tasks;
using CefSharp.Example;
using System.Net;

namespace CefSharp.OffScreen.Example
{
    public class Program
    {
        private static ChromiumWebBrowser browser;
        private static bool captureFirstRenderedImage = false;

        public static void Main(string[] args)
        {
            const string testUrl = "http://livechatinc.com/";

            Console.WriteLine("This example application will load {0}, take a screenshot, and save it to your desktop.", testUrl);
            Console.WriteLine("You may see a lot of Chromium debugging output, please wait...");
            Console.WriteLine();

            // You need to replace this with your own call to Cef.Initialize();
            //CefExample.Init();
            Cef.Initialize(new CefSettings() { UserAgent = "MyAgent", LogSeverity = LogSeverity.Verbose });

            // Create the offscreen Chromium browser.
            using (browser = new ChromiumWebBrowser(testUrl))
            {
                browser.RequestHandler = new MyBeautifulRequestHandler();
                // An event that is fired when the first page is finished loading.
                // This returns to us from another thread.
                if (captureFirstRenderedImage)
                {
                    browser.ResourceHandlerFactory.RegisterHandler(testUrl, ResourceHandler.FromString("<html><body><h1>CefSharp OffScreen</h1></body></html>"));
                    browser.ScreenshotAsync().ContinueWith(DisplayBitmap);
                }
                else
                {
                    browser.FrameLoadEnd += BrowserFrameLoadEnd;
                }

                // We have to wait for something, otherwise the process will exit too soon.
                Console.ReadKey();
            }

            // Clean up Chromium objects.  You need to call this in your application otherwise
            // you will get a crash when closing.
            Cef.Shutdown();
        }

        private static void BrowserFrameLoadEnd(object sender, FrameLoadEndEventArgs e)
        {
            // Check to ensure it is the main frame which has finished loading
            // (rather than an iframe within the main frame).
            if (e.IsMainFrame)
            {
                // Remove the load event handler, because we only want one snapshot of the initial page.
                browser.FrameLoadEnd -= BrowserFrameLoadEnd;

                // Wait for the screenshot to be taken.
                browser.ScreenshotAsync().ContinueWith(DisplayBitmap);
            }
        }

        private static void DisplayBitmap(Task<Bitmap> task)
        {
            // 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);

            var bitmap = task.Result;

            // Save the Bitmap to the path.
            // The image type is auto-detected via the ".png" extension.
            bitmap.Save(screenshotPath);

            // We no longer need the Bitmap.
            // Dispose it to avoid keeping the memory alive.  Especially important in 32-bit applications.
            bitmap.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.");
        }
    }

    public class MyBeautifulRequestHandler : IRequestHandler
    {
        public bool OnBeforeBrowse(IWebBrowser browser, IRequest request, bool isRedirect, bool isMainFrame)
        {
            return false;
        }

        public bool OnCertificateError(IWebBrowser browser, CefErrorCode errorCode, string requestUrl)
        {
            return true;
        }

        public void OnPluginCrashed(IWebBrowser browser, string pluginPath)
        {
        }

        public CefReturnValue OnBeforeResourceLoad(IWebBrowser browser, IRequest request, bool isMainFrame)
        {
            Console.Write(request.Url);
            WebRequest webRequest = HttpWebRequest.Create(request.Url);
            webRequest.Method = "HEAD";
            webRequest.Proxy = null;

            try
            {
                using (WebResponse webResponse = webRequest.GetResponse())
                {
                    if (webResponse.ContentLength != -1)
                    {
                        Console.Write(":" + webResponse.ContentLength);
                    }
                }
            }
            catch (Exception ex)
            {
                Console.Write(":EX=" + ex.Message);
            }
            Console.WriteLine(":Done");
            return CefReturnValue.Continue;
        }

        public bool GetAuthCredentials(IWebBrowser browser, bool isProxy, string host, int port, string realm, string scheme, ref string username, ref string password)
        {
            return false;
        }

        public bool OnBeforePluginLoad(IWebBrowser browser, string url, string policyUrl, WebPluginInfo info)
        {
            return false;
        }

        public void OnRenderProcessTerminated(IWebBrowser browser, CefTerminationStatus status)
        {
            Console.WriteLine("Terminated: " + status.ToString());
        }
    }
}
amaitland commented 9 years ago

As a general rule we'd prefer you to fork MinimalExample when demonstrating problems like this (https://github.com/cefsharp/CefSharp.MinimalExample) or at a minimum post your cost as a Gist or similar. See https://github.com/cefsharp/CefSharp/blob/master/CONTRIBUTING.md#what-should-i-include-when-creating-an-issue

As a general rule it's not great to completely change the UserAgent, better just to append your own custom tag at the end. Too many websites use UserAgent detection based on the string.

amaitland commented 9 years ago

If you add c# to your code blocks they actually get highlighted properly (you can see I edited your post).

amaitland commented 9 years ago

What does the debug.log file say? (Just an excerpt if anything stands out is fine, ignore anything about sand boxing)

ghost commented 9 years ago

Sorry about not providing the correct example, I shall certainly do that in the future.

I agree with you on the User Agent, but even one of the standard ones (Mozilla/5.0 (Windows NT 6.1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/41.0.2228.0 Safari/537.36) causes it.

Below is the last part of the log. After the first line, I press a button (causing Shutdown to be called) and it freezes there.

[0804/120026:VERBOSE1:resource_loader.cc(778)] ResponseCompleted: http://cdn.livechatinc.com/sounds/message.ogg
[0804/120026:ERROR:audio_low_latency_output_win.cc(181)] Bailing out due to non-perfect timing.  Buffer size of 480 is not an even divisor of 1056
[0804/120027:VERBOSE1:ipc_sync_channel.cc(386)] Canceling pending sends
[0804/120027:VERBOSE1:ipc_sync_channel.cc(386)] Canceling pending sends
[0804/120027:VERBOSE1:ipc_sync_channel.cc(386)] Canceling pending sends
[0804/120027:VERBOSE1:ipc_sync_channel.cc(386)] Canceling pending sends
[0804/120027:VERBOSE1:ipc_sync_channel.cc(386)] Canceling pending sends
[0804/120027:VERBOSE1:ipc_sync_channel.cc(386)] Canceling pending sends
[0804/120027:VERBOSE1:ipc_sync_channel.cc(386)] Canceling pending sends
[0804/120027:VERBOSE1:ipc_sync_channel.cc(386)] Canceling pending sends
[0804/120027:VERBOSE1:ipc_sync_channel.cc(386)] Canceling pending sends
[0804/120027:VERBOSE1:ipc_sync_channel.cc(386)] Canceling pending sends
[0804/120027:VERBOSE1:ipc_sync_channel.cc(386)] Canceling pending sends
[0804/120027:VERBOSE1:ipc_sync_channel.cc(386)] Canceling pending sends
[0804/120027:VERBOSE1:ipc_sync_channel.cc(386)] Canceling pending sends
[0804/120027:VERBOSE1:ipc_sync_channel.cc(386)] Canceling pending sends
[0804/120027:VERBOSE1:ipc_sync_channel.cc(386)] Canceling pending sends
[0804/120027:VERBOSE1:ipc_sync_channel.cc(386)] Canceling pending sends
[0804/120027:VERBOSE1:ipc_sync_channel.cc(386)] Canceling pending sends
[0804/120027:VERBOSE1:ipc_sync_channel.cc(386)] Canceling pending sends
[0804/120027:VERBOSE1:ipc_sync_channel.cc(386)] Canceling pending sends
[0804/120027:VERBOSE1:ipc_sync_channel.cc(386)] Canceling pending sends
[0804/120056:VERBOSE1:ipc_sync_channel.cc(386)] Canceling pending sends
[0804/120056:VERBOSE1:ipc_sync_channel.cc(386)] Canceling pending sends
[0804/120056:VERBOSE1:ipc_sync_channel.cc(386)] Canceling pending sends
[0804/120056:VERBOSE1:ipc_sync_channel.cc(386)] Canceling pending sends
[0804/120056:VERBOSE1:ipc_sync_channel.cc(386)] Canceling pending sends
[0804/120056:VERBOSE1:ipc_sync_channel.cc(386)] Canceling pending sends
[0804/120056:VERBOSE1:ipc_sync_channel.cc(386)] Canceling pending sends
[0804/120056:VERBOSE1:statistics_recorder.cc(294)] Collections of all histograms
Histogram: GPU.CollectContextGraphicsInfo recorded 1 samples, average = 0.0 (flags = 0x1)

At the end it has some histograms, doesn't seem that relevant to me (I could be wrong!)

amaitland commented 9 years ago

You can get console messages as well https://github.com/cefsharp/CefSharp/blob/cefsharp/41/CefSharp/IWebBrowser.cs#L16

Even though your in OffScreen you can likely still open DevTools (just remember to close it before disposing of the browser). So you can likely see what's going on under the hood.

ghost commented 9 years ago

Seems related to https://bitbucket.org/chromiumembedded/cef/issues/1115/cefshutdown-crash

amaitland commented 9 years ago

Sorry about not providing the correct example, I shall certainly do that in the future.

It's fine, just makes our helping you a lot easier if we can simply download a ready to run example :+1:

amaitland commented 9 years ago

Seems related to https://bitbucket.org/chromiumembedded/cef/issues/1115/cefshutdown-crash

Possibly, that references a very old version of CEF.

Can you test again with master? It's a newer version of CEF again, plus a major rewrite (some of the methods have changed, so it won't just be a copy and paste job exactly, shouldn't be too much effort though).

ghost commented 9 years ago

Sure! master's NuGet still picks up CEF 3.2272.32 though. I can build it against a higher version by manually downloading latest CEF :)

amaitland commented 9 years ago

NuGet still picks up CEF 3.2272.32 though

Reference? It should be pointing to 3.2357.1274

https://github.com/cefsharp/CefSharp/blob/master/build.ps1#L10

amaitland commented 9 years ago

Does your WebRequest finish before you attempt to Shutdown? Most handlers including OnBeforeResourceLoad actually execute on a CEF thread, so it's best not to block them or you'll get issues. Can you spawn your processing off into a Task?

ghost commented 9 years ago

I tried spawning them in a Task, same thing happened. Even blocked the event thread by Thread.Sleep(1000) took a while but still exited cleanly. Ill try with master, seem to have mixed up the versions.

ghost commented 9 years ago

Just did a quick test with a timeout on the WebRequest, so they should all complete. None timed out I think and it stll froze.

ghost commented 9 years ago

Ok, got master working now, give me a minute while I retry my tests.

ghost commented 9 years ago

Still happens on master, here's the example again:

// Copyright © 2010-2015 The CefSharp Authors. All rights reserved.
//
// Use of this source code is governed by a BSD-style license that can be found in the LICENSE file.

using System;
using System.Diagnostics;
using System.Drawing;
using System.IO;
using System.Threading.Tasks;
using CefSharp.Example;
using System.Net;

namespace CefSharp.OffScreen.Example
{
    public class Program
    {
        private const string TestUrl = "http://livechatinc.com/";

        public static void Main(string[] args)
        {
            Console.WriteLine("This example application will load {0}, take a screenshot, and save it to your desktop.", TestUrl);
            Console.WriteLine("You may see a lot of Chromium debugging output, please wait...");
            Console.WriteLine();

            // You need to replace this with your own call to Cef.Initialize();
            //CefExample.Init();
            Cef.Initialize(new CefSettings() { UserAgent = "MyBeautifulUserAgent", LogSeverity = LogSeverity.Disable });

            MainAsync("cachePath1", 1.0);
            //Demo showing Zoom Level of 3.0
            //Using seperate request contexts allows the urls from the same domain to have independent zoom levels
            //otherwise they would be the same - default behaviour of Chromium
            //MainAsync("cachePath2", 3.0);

            // We have to wait for something, otherwise the process will exit too soon.
            Console.ReadKey();

            // Clean up Chromium objects.  You need to call this in your application otherwise
            // you will get a crash when closing.
            Cef.Shutdown();
        }

        private static async void MainAsync(string cachePath, double zoomLevel)
        {
            var browserSettings = new BrowserSettings();
            var requestContextSettings = new RequestContextSettings { CachePath = cachePath };

            // RequestContext can be shared between browser instances and allows for custom settings
            // e.g. CachePath
            using(var requestContext = new RequestContext(requestContextSettings))
            using (var browser = new ChromiumWebBrowser(TestUrl, browserSettings, requestContext))
            {
                browser.RequestHandler = new MyBeautifulRequestHandler();
                browser.ConsoleMessage += browser_ConsoleMessage;
                browser.StatusMessage += browser_StatusMessage;
                if (zoomLevel > 1)
                {
                    browser.FrameLoadStart += (s, argsi) =>
                    {
                        var b = (ChromiumWebBrowser)s;
                        if (argsi.IsMainFrame)
                        {
                            b.SetZoomLevel(zoomLevel);
                        }
                    };
                }
                await LoadPageAsync(browser);

                // Wait for the screenshot to be taken.
                await browser.ScreenshotAsync().ContinueWith(DisplayBitmap);

                //await LoadPageAsync(browser, "http://github.com");

                // Wait for the screenshot to be taken.
                //await browser.ScreenshotAsync().ContinueWith(DisplayBitmap);
            }
        }

        static void browser_ConsoleMessage(object sender, ConsoleMessageEventArgs e)
        {
            Console.WriteLine("============================================================================");
            Console.WriteLine("[" + e.Source + "][" + e.Line + "]" + e.Message);
            Console.WriteLine("============================================================================");
        }

        static void browser_StatusMessage(object sender, StatusMessageEventArgs e)
        {
            Console.WriteLine("============================================================================");
            Console.WriteLine(e.Value);
            Console.WriteLine("============================================================================");
        }

        public static Task LoadPageAsync(IWebBrowser browser, string address = null)
        {
            var tcs = new TaskCompletionSource<bool>();

            EventHandler<LoadingStateChangedEventArgs> handler = null;
            handler = (sender, args) =>
            {
                //Wait for while page to finish loading not just the first frame
                if (!args.IsLoading)
                {
                    browser.LoadingStateChanged -= handler;
                    tcs.TrySetResult(true);
                }
            };

            browser.LoadingStateChanged += handler;

            if (!string.IsNullOrEmpty(address))
            {
                browser.Load(address);
            }
            return tcs.Task;
        }

        private static void DisplayBitmap(Task<Bitmap> task)
        {
            // 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" + DateTime.Now.Ticks + ".png");

            Console.WriteLine();
            Console.WriteLine("Screenshot ready. Saving to {0}", screenshotPath);

            var bitmap = task.Result;

            // Save the Bitmap to the path.
            // The image type is auto-detected via the ".png" extension.
            bitmap.Save(screenshotPath);

            // We no longer need the Bitmap.
            // Dispose it to avoid keeping the memory alive.  Especially important in 32-bit applications.
            bitmap.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.");
        }
    }

    public class MyBeautifulRequestHandler : IRequestHandler
    {

        public bool OnBeforeBrowse(IWebBrowser browserControl, IBrowser browser, IFrame frame, IRequest request, bool isRedirect)
        {
            return false;
        }

        public bool OnCertificateError(IWebBrowser browserControl, IBrowser browser, CefErrorCode errorCode, string requestUrl, IRequestCallback callback)
        {
            return true;
        }

        public void OnPluginCrashed(IWebBrowser browserControl, IBrowser browser, string pluginPath)
        {
            Console.WriteLine("Not interested in this one");
        }

        public CefReturnValue OnBeforeResourceLoad(IWebBrowser browserControl, IBrowser browser, IFrame frame, IRequest request, IRequestCallback callback)
        {
            Console.Write(request.Url);
            WebRequest webRequest = HttpWebRequest.Create(request.Url);
            webRequest.Method = "HEAD";
            webRequest.Proxy = null;
            webRequest.Timeout = 5000;

            try
            {
                using (WebResponse webResponse = webRequest.GetResponse())
                {
                    if (webResponse.ContentLength != -1)
                    {
                        Console.Write(":" + webResponse.ContentLength);
                    }
                }
            }
            catch (Exception ex)
            {
                Console.Write(":EX=" + ex.Message);
            }
            Console.WriteLine(":Done");
            callback.Continue(true);
            return CefReturnValue.Continue;
        }

        public bool GetAuthCredentials(IWebBrowser browserControl, IBrowser browser, IFrame frame, bool isProxy, string host, int port, string realm, string scheme, IAuthCallback callback)
        {
            return false;
        }

        public bool OnBeforePluginLoad(IWebBrowser browserControl, IBrowser browser, string url, string policyUrl, WebPluginInfo info)
        {
            return false;
        }

        public void OnRenderProcessTerminated(IWebBrowser browserControl, IBrowser browser, CefTerminationStatus status)
        {
            Console.WriteLine("Render process terminated");
        }

        public bool OnQuotaRequest(IWebBrowser browserControl, IBrowser browser, string originUrl, long newSize, IRequestCallback callback)
        {
            callback.Continue(true);
            return false;
        }

        public void OnResourceRedirect(IWebBrowser browserControl, IBrowser browser, IFrame frame, IRequest request, ref string newUrl)
        {
            Console.WriteLine("OnResourceRedirect");
        }

        public bool OnProtocolExecution(IWebBrowser browserControl, IBrowser browser, string url)
        {
            return true;
        }

        public void OnFaviconUrlChange(IWebBrowser browserControl, IBrowser browser, System.Collections.Generic.IList<string> urls)
        {
        }
    }
}

Debug log looks about the same as it did before. Still hangs on GC::WaitForPendingFinalizers(). After testing this I disabled the logging so I could see potential javascript and status messages (none showed)

amaitland commented 9 years ago

For master at least you shouldn't perform long running tasks in the OnBeforeResourceLoad method it's self. Try https://gist.github.com/amaitland/7cb77867e45db4aff00b#file-program-cs-L168

ghost commented 9 years ago

That did it for some reason. Already tried that on the older version and for some reason still isn't working there. I'll just switch over to the latest version to remedy the issue :+1:

amaitland commented 9 years ago

master has much better support for async operations. Previously versions hide much of the implementation detail which can cause problems, like this one.