FatalExecutionEngineError в C# / WSC (COM) взаимодействия

Я собираюсь начать миграционный проект на работе для устаревшей системы, написанной на VBScript. Он имеет интересную структуру в том смысле, что большая часть его была разделена путем написания различных компонентов в виде файлов "WSC", которые фактически являются способом представления кода VBScript в стиле COM. Граничный интерфейс между "ядром" и этими компонентами довольно узок и хорошо известен, поэтому я надеялся, что смогу заняться написанием нового ядра и повторно использовать WSC, отложив их переписывание.

Можно загрузить WSC, добавив ссылку на "Microsoft.VisualBasic" и вызвав

var component = (dynamic)Microsoft.VisualBasic.Interaction.GetObject("script:" + controlFilename, null);

где "controlFilename" - полный путь к файлу. GetObject возвращает ссылку типа "System.__ComObject", но к свойствам и методам можно получить доступ, используя "динамический" тип.net.

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

Возбужденное исключение имеет тип "System.ExecutionEngineException", что звучит особенно страшно (и расплывчато)!

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

  1. Создайте новое пустое "Веб-приложение ASP.NET" под названием "WSCErrorExample" (я делал это в VS 2013 / .net 4.5 и VS 2010 / .net 4.0, это не имеет значения)

  2. Добавить ссылку на "Microsoft.VisualBasic" в проект

  3. Добавьте новую "Веб-форму" с именем "Default.aspx" и вставьте следующую строку поверх "Default.aspx.cs".

    using System;
    using System.IO;
    using System.Reflection;
    using System.Runtime.InteropServices;
    using Microsoft.VisualBasic;
    
    namespace WSCErrorExample
    {
        public partial class Default : System.Web.UI.Page
        {
            protected void Page_Load(object sender, EventArgs e)
            {
                var currentFolder = GetCurrentDirectory();
                var logFile = new FileInfo(Path.Combine(currentFolder, "Log.txt"));
                Action<string> logger = message =>
                {
                    // The try..catch is to avoid IO exceptions when reproducing by requesting the page many times
                    try { File.AppendAllText(logFile.FullName, message + Environment.NewLine); }
                    catch { }
                };
    
                var controlFilename = Path.Combine(currentFolder, "TestComponent.wsc");
                var control = (dynamic)Interaction.GetObject("script:" + controlFilename, null);
    
                logger("About to call Go");
                control.Go(new DataProvider(logger));
                logger("Completed");
            }
            private static string GetCurrentDirectory()
            {
                // This is a way to get the working path that works within ASP.Net web projects as well as Console apps
                var path = Path.GetDirectoryName(Assembly.GetExecutingAssembly().GetName().CodeBase);
                if (path.StartsWith(@"file:\", StringComparison.InvariantCultureIgnoreCase))
                    path = path.Substring(6);
                return path;
            }
    
            [ComVisible(true)]
            public class DataProvider
            {
                private readonly Action<string> _logger;
                public DataProvider(Action<string> logger)
                {
                    _logger = logger;
                }
    
                public DataContainer GetDataContainer()
                {
                    return new DataContainer();
                }
    
                public void Log(string content)
                {
                    _logger(content);
                }
            }
    
            [ComVisible(true)]
            public class DataContainer
            {
                public object this[string fieldName]
                {
                    get { return "Item:" + fieldName; }
                }
            }
        }
    }
    
  4. Добавьте новый "Текстовый файл" с именем "TestComponent.wsc", откройте окно его свойств и измените "Копировать в выходной каталог" на "Копировать, если новее", а затем вставьте следующее в качестве его содержимого.

    <?xml version="1.0" ?>
    <?component error="false" debug="false" ?>
    <package>
        <component id="TestComponent">
            <registration progid="TestComponent" description="TestComponent" version="1" />
            <public>
                <method name="Go" />
            </public>
            <script language="VBScript">
                <![CDATA[
                    Function Go(objDataProvider)
                        Dim objDataContainer: Set objDataContainer = objDataProvider.GetDataContainer()
                        If IsEmpty(objDataContainer) Then
                            mDataProvider.Log "No data provided"
                        End If
                    End Function
            ]]>
            </script>
        </component>
    </package>
    

Выполнение этого один раз не должно вызывать никаких явных проблем, файл "Log.txt" будет записан в папку "bin". Обновление страницы, однако, обычно приводит к исключению

Помощник по управляемой отладке "FatalExecutionEngineError" обнаружил проблему в "C:\Program Files (x86)\IIS Express\iisexpress.exe".

Дополнительная информация: во время выполнения произошла фатальная ошибка. Адрес ошибки был 0x733c3512, в потоке 0x1e10. Код ошибки 0xc0000005. Эта ошибка может быть ошибкой в ​​CLR или в небезопасных или не поддающихся проверке частях пользовательского кода. Распространенными источниками этой ошибки являются ошибки пользовательского маршалинга для COM-> interop или PInvoke, которые могут повредить стек.

Иногда второй запрос не приводит к этому исключению, но, удерживая нажатой клавишу F5 в окне браузера в течение нескольких секунд, он сможет поднять свою уродливую голову. Насколько я могу судить, исключение происходит при проверке "If IsEmpty" (в других версиях этого случая воспроизведения было больше вызовов журналирования, что указывало на то, что эта строка является источником проблемы).

Я пробовал разные вещи, чтобы попытаться докопаться до сути, я попытался воссоздать в консольном приложении, и проблема не возникает, даже если я раскручиваю сотни потоков и заставляю их обрабатывать работу выше. Я попробовал веб-приложение ASP.Net MVC, а не веб-форму, и та же проблема возникает. Я попытался изменить состояние квартиры с MTA по умолчанию на STA (в тот момент я немного цеплялся за соломинку!), И это не изменило его поведение. Я попытался создать веб-проект, который использует реализацию Microsoft OWIN, и проблема возникает в этом сценарии.

Я заметил две интересные вещи: если класс "DataContainer" не имеет индексированного свойства (или метода / свойства по умолчанию, украшенных атрибутом [DispId(0)] - в этом примере не показано), то ошибка не происходят. Если замыкание "logger" не содержит ссылку "FileInfo" (если была сохранена строка "logFilePath", а не экземпляр FileInfo "logFile"), то ошибка не возникает. Я полагаю, звучит так, что одним из подходов было бы избежать подобных вещей! Но я был бы обеспокоен тем, что могут быть другие способы инициирования этого сценария, о которых я в данный момент не знаю, и попытка обеспечить соблюдение правила "не делать этих вещей" может усложняться по мере роста базы кода, я могу представить себе это ошибка закрадывается обратно безо всякой очевидной причины.

За один прогон (через Katana) я получил дополнительную информацию о стеке вызовов:

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

Стек вызовов с внешним кодом

mscorlib.dll!System.Variant.Variant(объект obj) mscorlib.dll!System.OleAutBinder.ChangeType(значение объекта, тип System.Type, System.Globalization.CultureInfo cultureInfo) mscorlib.dll!System.RuntimeType.TryChangeType(значение объекта, Связыватель System.Reflection.Binder, культура System.Globalization.CultureInfo, потребности boolSpecialCast) mscorlib.dll!System.RuntimeType.CheckValue(значение объекта, связывание System.Reflection.Binder, культура System.Globalization.CultureInfo, System.Reflection.BindingFlags invokeAttr) mscorlib.dll!System.Reflection.MethodBase.CheckArguments(параметры объекта [], привязка System.Reflection.Binder, System.Reflection.BindingFlags invokeAttr, System.Globalization.CultureInfo culture, System.Signature sig) [Родной для управляемого перехода ]

И последнее замечание: если я создаю оболочку для класса "DataProvider", использую IReflect и сопоставляю вызовы через IDispatch с вызовами базового экземпляра "DataProvider", тогда проблема исчезнет. Но опять же, решение о том, что этот ответ каким-то образом кажется мне опасным, - если мне нужно тщательно следить за тем, чтобы любая ссылка, передаваемая на компоненты, имела такую ​​оболочку, то ошибки могли бы закрадываться, что было бы трудно отследить. Что делать, если ссылка, заключенная в оболочку, реализующую IReflect, возвращает ссылку из вызова метода или свойства, который не упакован таким же образом? Я полагаю, что обертка может попытаться сделать что-то вроде обеспечения того, что она возвращает только "безопасную" ссылку (т. Е. Без индексированных свойств или методов или свойств DispId=0), не заключая их в дальнейшую обертку IReflect... но все это выглядит немного странно,

Я действительно понятия не имею, где дальше идти с этой проблемой, у кого-нибудь есть идеи?

1 ответ

Решение

Я предполагаю, что ошибка, которую вы видите, вызвана тем фактом, что компоненты сценария WSC по своей природе являются объектами COM STA. Они реализованы базовым механизмом активных сценариев VBScript, который сам по себе является COM-объектом STA. Как таковые, они требуют создания потока STA и доступа к нему, и такой поток должен оставаться тем же самым в течение времени жизни любого конкретного объекта WSC (объект требует привязки потока).

Потоки ASP.NET не являются STA. Они ThreadPool потоки, и они неявно становятся потоками COM MTA, когда вы начинаете использовать на них COM-объекты (различия между STA и MTA см. в документе INFO: описание и работа моделей потоков OLE). Затем COM создает отдельную неявную квартиру STA для ваших объектов WSC и выполняет там маршальные вызовы из потока запросов ASP.NET. Все это может или не может пойти хорошо в среде ASP.NET.

В идеале вам следует избавиться от компонентов сценариев WSC и заменить их сборками.NET. Если это невозможно в краткосрочной перспективе, я бы порекомендовал вам запустить свой собственный явно управляемый поток (ы) STA для размещения компонентов WSC. Следующее может помочь:

Обновлено, почему бы не попробовать? Ваш код будет выглядеть так:

// create a global instance of ThreadAffinityTaskScheduler - per web app
public static class GlobalState 
{
    public static ThreadAffinityTaskScheduler TaScheduler { get; private set; }

    public static GlobalState() 
    {
        GlobalState.TaScheduler = new ThreadAffinityTaskScheduler(
            numberOfThreads: 10,
            staThreads: true, 
            waitHelper: WaitHelpers.WaitWithMessageLoop);
    }
}

// ... inside Page_Load

GlobalState.TaScheduler.Run(() => 
{
    var control = (dynamic)Interaction.GetObject("script:" + controlFilename, null);

    logger("About to call Go");
    control.Go(new DataProvider(logger));
    logger("Completed");

}, CancellationToken.None).Wait();

Если это работает, вы можете несколько улучшить масштабируемость веб-приложения, используя PageAsyncTask а также async/await вместо блокировки Wait(),

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