Использование CefGlue для возврата HTML-страницы из URL

Я пытаюсь написать реализацию для следующего (прототип) метода:

var result = browser.GetHtml(string url);

Причина, по которой я нуждаюсь в этом, заключается в том, что есть несколько страниц, которые помещают в браузер кучу Javascript, а затем Javascript отображает страницу. Единственный способ надежного получения таких страниц - разрешить выполнение Javascript в браузерной среде перед получением результирующего HTML.

Моя текущая попытка использует CefGlue. После загрузки этого проекта и объединения его с кодом в этом ответе я придумал следующий код (включенный здесь для полноты):

using System;
using System.Diagnostics;
using System.Drawing;
using System.Drawing.Imaging;
using System.Drawing.Printing;
using System.IO;
using System.Threading;
using System.Threading.Tasks;
using Xilium.CefGlue;

namespace OffScreenCefGlue
{
    internal class Program
    {
        private static void Main(string[] args)
        {
            // Load CEF. This checks for the correct CEF version.
            CefRuntime.Load();

            // Start the secondary CEF process.
            var cefMainArgs = new CefMainArgs(new string[0]);
            var cefApp = new DemoCefApp();

            // This is where the code path divereges for child processes.
            if (CefRuntime.ExecuteProcess(cefMainArgs, cefApp) != -1)
            {
                Console.Error.WriteLine("CefRuntime could not create the secondary process.");
            }

            // Settings for all of CEF (e.g. process management and control).
            var cefSettings = new CefSettings
            {
                SingleProcess = false,
                MultiThreadedMessageLoop = true
            };

            // Start the browser process (a child process).
            CefRuntime.Initialize(cefMainArgs, cefSettings, cefApp);

            // Instruct CEF to not render to a window at all.
            CefWindowInfo cefWindowInfo = CefWindowInfo.Create();
            cefWindowInfo.SetAsOffScreen(IntPtr.Zero);

            // Settings for the browser window itself (e.g. should JavaScript be enabled?).
            var cefBrowserSettings = new CefBrowserSettings();

            // Initialize some the cust interactions with the browser process.
            // The browser window will be 1280 x 720 (pixels).
            var cefClient = new DemoCefClient(1280, 720);

            // Start up the browser instance.
            string url = "http://www.reddit.com/";
            CefBrowserHost.CreateBrowser(cefWindowInfo, cefClient, cefBrowserSettings, url);

            // Hang, to let the browser do its work.
            Console.Read();

            // Clean up CEF.
            CefRuntime.Shutdown();
        }
    }

    internal class DemoCefApp : CefApp
    {
    }

    internal class DemoCefClient : CefClient
    {
        private readonly DemoCefLoadHandler _loadHandler;
        private readonly DemoCefRenderHandler _renderHandler;

        public DemoCefClient(int windowWidth, int windowHeight)
        {
            _renderHandler = new DemoCefRenderHandler(windowWidth, windowHeight);
            _loadHandler = new DemoCefLoadHandler();
        }

        protected override CefRenderHandler GetRenderHandler()
        {
            return _renderHandler;
        }

        protected override CefLoadHandler GetLoadHandler()
        {
            return _loadHandler;
        }
    }

    internal class DemoCefLoadHandler : CefLoadHandler
    {
        public string Html { get; private set; }

        protected override void OnLoadStart(CefBrowser browser, CefFrame frame)
        {
            // A single CefBrowser instance can handle multiple requests
            //   for a single URL if there are frames (i.e. <FRAME>, <IFRAME>).
            if (frame.IsMain)
            {
                Console.WriteLine("START: {0}", browser.GetMainFrame().Url);
            }
        }

        protected override async void OnLoadEnd(CefBrowser browser, CefFrame frame, int httpStatusCode)
        {
            if (frame.IsMain)
            {
                Html = await browser.GetSourceAsync();
                Console.WriteLine("END: {0}, {1}", browser.GetMainFrame().Url, httpStatusCode);
            }
        }
    }

    internal class DemoCefRenderHandler : CefRenderHandler
    {
        private readonly int _windowHeight;
        private readonly int _windowWidth;

        public DemoCefRenderHandler(int windowWidth, int windowHeight)
        {
            _windowWidth = windowWidth;
            _windowHeight = windowHeight;
        }

        protected override bool GetRootScreenRect(CefBrowser browser, ref CefRectangle rect)
        {
            return GetViewRect(browser, ref rect);
        }

        protected override bool GetScreenPoint(CefBrowser browser, int viewX, int viewY, ref int screenX, ref int screenY)
        {
            screenX = viewX;
            screenY = viewY;
            return true;
        }

        protected override bool GetViewRect(CefBrowser browser, ref CefRectangle rect)
        {
            rect.X = 0;
            rect.Y = 0;
            rect.Width = _windowWidth;
            rect.Height = _windowHeight;
            return true;
        }

        protected override bool GetScreenInfo(CefBrowser browser, CefScreenInfo screenInfo)
        {
            return false;
        }

        protected override void OnPopupSize(CefBrowser browser, CefRectangle rect)
        {
        }

        protected override void OnPaint(CefBrowser browser, CefPaintElementType type, CefRectangle[] dirtyRects, IntPtr buffer, int width, int height)
        {
            // Save the provided buffer (a bitmap image) as a PNG.
            var bitmap = new Bitmap(width, height, width*4, PixelFormat.Format32bppRgb, buffer);
            bitmap.Save("LastOnPaint.png", ImageFormat.Png);
        }

        protected override void OnCursorChange(CefBrowser browser, IntPtr cursorHandle)
        {
        }

        protected override void OnScrollOffsetChanged(CefBrowser browser)
        {
        }
    }

    public class TaskStringVisitor : CefStringVisitor
    {
        private readonly TaskCompletionSource<string> taskCompletionSource;

        public TaskStringVisitor()
        {
            taskCompletionSource = new TaskCompletionSource<string>();
        }

        protected override void Visit(string value)
        {
            taskCompletionSource.SetResult(value);
        }

        public Task<string> Task
        {
            get { return taskCompletionSource.Task; }
        }
    }

    public static class CEFExtensions
    {
        public static Task<string> GetSourceAsync(this CefBrowser browser)
        {
            TaskStringVisitor taskStringVisitor = new TaskStringVisitor();
            browser.GetMainFrame().GetSource(taskStringVisitor);
            return taskStringVisitor.Task;
        }
    }
}

Соответствующий фрагмент кода здесь:

protected override async void OnLoadEnd(CefBrowser browser, CefFrame frame, int httpStatusCode)
{
    if (frame.IsMain)
    {
        Html = await browser.GetSourceAsync();
        Console.WriteLine("END: {0}, {1}", browser.GetMainFrame().Url, httpStatusCode);
    }
}

Это на самом деле, кажется, работает; Вы можете проверить переменную Html с помощью отладчика, и там есть HTML-страница. Проблема в том, что переменная Html не помогает мне в этом методе обратного вызова; он похоронен на трех уровнях глубоко в иерархии классов, и мне нужно вернуть его в методе, который я пытаюсь написать, не создавая Schroedinbug.

(пытаясь получить результат от этого string Html свойство, в том числе попытка просмотреть его с помощью визуализатора Html в отладчике, по-видимому, вызывает тупик, чего я действительно хотел бы избежать, тем более что этот код будет выполняться на сервере).

Как мне достичь моего var result = browser.GetHtml(string url); безопасно и надежно?

Дополнительный вопрос: могут ли механизмы обратного вызова в приведенном выше коде быть преобразованы в задачи с использованием этой техники? Как это будет выглядеть?

1 ответ

Имейте в виду, что текущие версии CefGlue не обеспечивали какой-либо из контекстов синхронизации, поэтому большую часть времени вам не следует использовать async/await в обратных вызовах, если вы не уверены в том, что делаете.

"Надежный" код должен быть асинхронным, поскольку большинство вызовов CEF являются асинхронными (с предоставлением обратных вызовов или без них). Async/await значительно упрощает эту задачу, поэтому я предполагаю, что этот вопрос можно упростить до: "Как правильно написать метод GetSourceAsync?". Это также зависит от вашего бонусного вопроса, и простого ответа, конечно, нет, и эту технику следует считать вредной, поскольку без знания базового кода это приводит к различным последствиям.

Таким образом, независимо от метода GetSourceAsync, и в особенности TaskStringVisitor, я только предлагаю вам никогда не выполнять методы TaskCompletionSource напрямую, поскольку он выполняет продолжения синхронно (в.NET 4.6 есть возможность выполнять продолжения асинхронно, но я лично не проверял, как это сделано в 4.6 внутренне). Это необходимо, чтобы как можно скорее освободить один из потоков CEF. В противном случае вы можете получить большое дерево продолжения, цикл или ожидание, что на самом деле навсегда блокирует поток браузера. Также обратите внимание, что расширения такого рода также вредны, потому что у них были те же проблемы, что описаны выше - единственный выбор, с которым нужно иметь дело, - это иметь истинное асинхронное продолжение.

protected override void Visit(string value)
{
    System.Threading.Tasks.Task.Run(() => taskCompletionSource.SetResult(value));
}

Некоторые CEF API являются гибридными: они ставят задачу в очередь нужному потоку, если мы уже не в требуемом потоке, или выполняем синхронно. В этом случае обработка должна быть упрощена, и в этом случае лучше избегать асинхронных операций. Опять же, просто чтобы избежать синхронных продолжений, потому что они могут привести к проблемам с повторным входом и / или просто к получению ненужных стековых кадров (с надеждой, что только на короткий промежуток времени и код не застрянет где-то).

Один из самых простых примеров, но это также верно для некоторых других вызовов API:

internal static class CefTaskHelper
{
    public static Task RunAsync(CefThreadId threadId, Action action)
    {
        if (CefRuntime.CurrentlyOn(threadId))
        {
            action();
            return TaskHelpers.Completed();
        }
        else
        {
            var tcs = new TaskCompletionSource<FakeVoid>();
            StartNew(threadId, () =>
            {
                try
                {
                    action();
                    tcs.SetResultAsync(default(FakeVoid));
                }
                catch (Exception e)
                {
                    tcs.SetExceptionAsync(e);
                }
            });
            return tcs.Task;
        }
    }

    public static void StartNew(CefThreadId threadId, Action action)
    {
        CefRuntime.PostTask(threadId, new CefActionTask(action));
    }
}

ОБНОВИТЬ:

Это на самом деле, кажется, работает; Вы можете проверить переменную Html с помощью отладчика, и там есть HTML-страница. Проблема в том, что переменная Html не помогает мне в этом методе обратного вызова; он похоронен на трех уровнях глубоко в иерархии классов, и мне нужно вернуть его в методе, который я пытаюсь написать, не создавая Schroedinbug.

Вам просто нужно реализовать CefLifeSpanHandler, и тогда вы сможете получить прямой доступ к CefBrowser, как только он будет создан (он создан асинхронно). Существует вызов CreateBrowserSync, но он не является предпочтительным способом.

PS: я на пути к CefGlue следующего поколения, но сейчас ничего не готово к использованию. Лучшая интеграция async/await запланирована. Лично я интенсивно использую асинхронные / ожидающие вещи, именно на стороне сервера.

Другие вопросы по тегам