Awesomium WebControl 1.7.5.1 зависает

887
09 декабря 2016, 08:55

Есть программа, которая создает несколько (2-8) Awesomium.Windows.Forms.WebControl в Form'е (используется Awesomium 1.7.5.1)

Необходимо постоянно получать значение InnerHTML каждого WebControl, перезагружать страницу webControl.Source = "https://someurl.com/", а также осуществлять ввод в поля <input> и нажимать на кнопки element.click().

Есть две проблемы, при которых WebControl зависает и с которыми я не могу разобраться:

  1. Спустя некоторое время работы программы метод webControl.ExecuteJavascriptWithResult(script) возвращает значение result.IsUndefined и webControl.GetLastError() выдает значение Awesomium.Core.Error.TimedOut. Я пробовал менять значение webControl.SynchronousMessageTimeout (сделал его = 10000мс), но баг остался. Причем несколько раз после этой ошибки работа продолжается исправно, но в очередной раз webControl зависает, остальные webControl продолжают исправно работать еще в течении нескольких минут, затем зависает вся форма, в которой находятся эти webControl.

  2. Если используемый сайт большой (не могу указать размеры, просто заметил, что, если имеется 10-20 записей некоторого списка на сайте, то все работает, если 50-100, то появляется эта проблема), зависает на методе browser.Source = new Uri(url), причем события LoadingFrameComplete и DocumentReady отрабатывают, но WebControl остается заблокированным (вызов перезагрузки страницы происходит из другого потока по событию таймера, в котором ставится webControl.CmdComplete = false, а при завершении работы webControl.NavigateAsync(url) возвращается обратно webBrowser.CmdComplete = true). При последующих проходах таймера webBrowser.CmdComplete == false долгое время (от нескольких секунд до десятков минут, хотя визуально страница давно загрузилась, работает и события загрузки отработали). И такое происходит только с несколькими webControl, необязательно со всеми. Обычно из четырех webControl один-два работают, остальные виснут (в самом начале). Через какое-то время зависание проходит, затем снова появляется (возможно уже на изначально работающих webControl).

Изначально программа написана с использованием движка Windows.Forms.WebBrowser и она отлично работает без багов. Но появилась необходимость использовать авторизацию на одном и том же сайте одновременно разных пользователей. Для этих целей решил переписать ее под Awesomium. Поэтому проблема скорее всего в особенностях Awesomium, использовании методов навигации и/или исполнения javascript'a, а не в алгоритмах самой программы.

Сразу скажу, что использование браузерного движка необходимо, поэтому предлагать WebRequest не нужно.

webControl создается в классе-обертке AwesomiumWebBrowserControl. Методы класса-обертки:

//...
public AwesomeWebBrowserControl(string id)
{
    //...
    // инициализайция WebControl browser
    // для каждого WebControl используется отдельная сессия и куки
    string sess_path = System.Windows.Forms.Application.StartupPath + "\\sessions";
    string acc_path = "\\" + id;
    if (!System.IO.Directory.Exists(sess_path + acc_path)) System.IO.Directory.CreateDirectory(sess_path + acc_path);
    Awesomium.Core.WebPreferences wp = Awesomium.Core.WebPreferences.Default;
    wp.EnableGPUAcceleration = true;
    Awesomium.Core.WebSession session = Awesomium.Core.WebCore.CreateWebSession(sess_path + acc_path, wp);
    browser = new WebControl() 
    {
        WebSession = session,
        SynchronousMessageTimeout = 10000,
        Name = id
        //...
    };
    //...
}
//...
public Awesomium.Core.JSValue ExecuteJavascriptWithResult(string script)
{
    const string METHOD_NAME = "ExecuteJavascriptWithResult";
    try
    {
        if (IsReady()) return browser.ExecuteJavascriptWithResult(script);
    }
    catch (Exception ex)
    {
        Program.CalcException(CLASS_NAME + "." + METHOD_NAME, ex);
    }
    return Awesomium.Core.JSValue.Null;
}
public string Html()
{
    const string METHOD_NAME = "Html";
    try
    {
        var html_nodes = (Awesomium.Core.JSObject)ExecuteJavascriptWithResult("document.getElementsByTagName('html')");
        if (html_nodes["length"] != 1) return null;
        var html_node = (Awesomium.Core.JSObject)html_nodes.Invoke("item", 0);
        HtmlRAM = html_node["innerHTML"]; // string HtmlRAM - определено в AwesomeWebBrowserControl
        return HtmlRAM;
    }
    catch (Exception ex)
    {
        Program.CalcException(CLASS_NAME + "." + METHOD_NAME, ex);
    }
    return null;
}
public Awesomium.Core.JSValue Document()
{
    const string METHOD_NAME = "Document";
    try
    {
        return ExecuteJavascriptWithResult("document");
    }
    catch (Exception ex)
    {
        Program.CalcException(CLASS_NAME + "." + METHOD_NAME, ex);
    }
    return null;
}
//...
bool loading_frame_complete = true;
bool document_ready = true;
bool acquire_window = true;
public bool IsReady()
{
    if (loading_frame_complete &&
        document_ready &&
        browser.IsDocumentReady &&
        browser.IsLive &&
        !browser.IsLoading &&
        !browser.IsNavigating &&
        !browser.IsCrashed &&
        !browser.IsDisposed)
    {
        Awesomium.Core.JSValue jv_window = AcquireWindow();
        if (jv_window.IsUndefined) acquire_window = false;
        else acquire_window = true;
        return acquire_window;
    }
    else return false;
}
public async Task NavigateAsync(string url)
{
    TaskCompletionSource<bool> tcsNavigation = null;
    TaskCompletionSource<bool> tcsReady = null;
    browser.LoadingFrameComplete += (s, e) =>
    {
        if (tcsNavigation.Task.IsCompleted)
            return;
        if (e.IsMainFrame)
        {
            loading_frame_complete = true;
            tcsNavigation.SetResult(true);
        }
    };
    browser.DocumentReady += (s, e) =>
    {
        if (tcsReady.Task.IsCompleted)
            return;
        document_ready = true;
        tcsReady.SetResult(true);
    };
    tcsNavigation = new TaskCompletionSource<bool>();
    tcsReady = new TaskCompletionSource<bool>();
    loading_frame_complete = false;
    document_ready = false;
    browser.Source = new Uri(url);
    await tcsNavigation.Task;
    await tcsReady.Task;
}
private Awesomium.Core.JSValue AcquireWindow()
{
    // взято с http://answers.awesomium.com/questions/4784/workaround-for-executejavascriptwithresult.html
    // на ход работы никак не влияет, используется для проверки ошибок
    System.Diagnostics.Stopwatch methodStopWatch = System.Diagnostics.Stopwatch.StartNew();
    Awesomium.Core.JSValue windowValue = this.browser.ExecuteJavascriptWithResult("window");
    while (windowValue.IsUndefined && (methodStopWatch.ElapsedMilliseconds < 3000))
    {
        Program.WriteLine("Failed to acquire Javascript global window object. Probably a bug in the Awesomium framwork. Try again for a while");
        Awesomium.Core.Error lastError = browser.GetLastError();
        if (lastError != Awesomium.Core.Error.None)
        {
            switch (lastError)
            {
                case Awesomium.Core.Error.TimedOut:
                    Program.WriteLine("Cause of failure was TimedOut");
                    break;
                case Awesomium.Core.Error.ConnectionGone:
                    Program.WriteLine("Cause of failure was ConnectionGone");
                    break;
                case Awesomium.Core.Error.WebViewGone:
                    Program.WriteLine("Cause of failure was WebViewGone");
                    break;
                case Awesomium.Core.Error.ObjectGone:
                    Program.WriteLine("Cause of failure was ObjectGone, that is object no longer exiet?");
                    break;
                case Awesomium.Core.Error.BadParameters:
                    Program.WriteLine("Cause of failure was BadParameters?");
                    break;
                case Awesomium.Core.Error.Generic:
                    Program.WriteLine("Cause of failure was Generic, we don't know what happened");
                    break;
                default:
                    Program.WriteLine("Cause of failure was unknown error code");
                    break;
            }
        }
        windowValue = this.browser.ExecuteJavascriptWithResult("window");
    }
    if (windowValue.IsUndefined)
    {
        Program.WriteLine("Map has not been properly loaded. Request for interaction with map rejected to avoid crash in Windows Forms");
    }
    return windowValue;
}
//...

вызов перезагрузки страницы и исполнения скриптов осуществляется из обработчика таймера потока Form с WebControl'ами. Ниже показаны типовые примеры использования навигации и исполнения javascript:

await whc.NavigateAsync(url);
public void FocusIntern(bool must_focus = false)
{
    const string MethodName = "FocusIntern";
    if ((AutoFocus && (DateTime.Now - lastFocus).TotalSeconds >= secFocus) || must_focus)
    {
        using (dynamic wdoc = Document())
        {
            if (wdoc != null) wdoc.focus();
        }
        lastFocus = DateTime.Now;
    }
}
[HandleProcessCorruptedStateExceptions]
public static async Task<RetVal<bool>> AuthInternAsync(AwesomeWebBrowserControl whc, string[] auth_data)
{
    const string MethodName = "AuthInternAsync";
    try
    {
        using ( dynamic result = (JSObject)whc.ExecuteJavascriptWithResult(Js.Auth(auth_data[0], auth_data[1])))
        {
            if (result.error) return RetVal<bool>.ErrorMessage(Name + "." + MethodName + ": <" + result.message + ">");
        }
        return new RetVal<bool>(true);
    }
    catch (Exception e)
    {
        Program.CalcException(Name + "." + MethodName, e);
    }
    return RetVal<bool>.ErrorMessage(Name + "." + MethodName + ": <Exception>");
}
// из класса Js
public static string Auth(string name, string password)
{
    // метод генерации скрипта авторизации
    const string MethodName = "Auth";
    return @"(
        function() {
            var res = {};
            res.error = true;
            res.message = 'undefined';
            try {
                // здесь javascript код авторизации на сайте
                // если авторизация удалась 
                    res.error = false;
                // иначе 
                {
                    res.error = true;
                    res.message = 'some message';
                }
            }
            catch (e) {
                res.message = '" + ClassName + "." + MethodName + @": <' + e.name + ':' + e.message + '>';
            }
            return res;
        })();";
}

есть и другие методы (LogoutInternAsync и пр.), но они все построены по типу AuthInternAsync.

Вывод перед зависанием формы:

Любые предположения и предложения буду проверять и комментировать, я очень заинтересован в скорейшем решении своего вопроса.

Если нужно больше кода, пишите - добавлю.

Answer 1

Как всегда, сам пошутил - сам посмеялся. Конечно решение не очень красивое, но зато теперь все работает. К сути

Причина зависания:

Поток awesomium_process (поток движка awesomium) потребляет все больше и больше памяти (причем сам движок винить не стоит, на некоторых сайтах все работает хорошо, в то время как другие съедают какое то непомерное количество памяти (1.5гб за 10 минут)). Т.к. движок написан под x86, при достижении предела адресации awesomium_process крашится (но не крашится сама форма приложения), в следствии чего наблюдается зависание без каких-либо явных ошибок.

Решение (костыль):

Пишется простенькая программа - лаунчер, в которой запускаются приложения-браузеры. В лаунчере нужно поставить флаг получения событий от процесса и подписаться на событие Exit:

Process.EnableRaisingEvents = true;
Process.Exited += Process_Exited;

И, если событие происходит, перезапускать приложение-браузер. В самом приложении-браузере я сделал что-то типа watchdog-таймера, в котором проверяется работоспособность браузера. И если в течении, например, 10 секунд таймер не был сброшен - приложение вырубает само себя.

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

[ServiceContract]
public interface ICloser
{
    [OperationContract]
    void Shutdown();
}

Теперь вся эта система теперь работает 24/7 без ошибок.

READ ALSO
Tasks и локальный DataTable

Tasks и локальный DataTable

Есть такой вот метод

361
Магнит в Unity - как уменьшить количество GetComponent?

Магнит в Unity - как уменьшить количество GetComponent?

Делаю в Unity магнит - через PhysicsOverlap находим, ищем rigidbody и притягиваем к себеКод

552
Как улучшить глобальный хук на языке C# в Microsoft Visual Studio?

Как улучшить глобальный хук на языке C# в Microsoft Visual Studio?

В форме у меня есть следующий код

375