【问题标题】:FatalExecutionEngineError in C# / WSC (COM) interopC#/WSC (COM) 互操作中的 FatalExecutionEngineError
【发布时间】:2014-09-05 16:57:52
【问题描述】:

我即将开始为一个用 VBScript 编写的遗留系统的迁移项目。它有一个有趣的结构,其中大部分是通过将各种组件编写为“WSC”文件来隔离的,这实际上是一种以类似 COM 的方式公开 VBScript 代码的方式。从“核心”到这些组件的边界接口相当紧密且众所周知,因此我希望我能够处理编写新核心并重用 WSC,推迟重写。

可以通过添加对“Microsoft.VisualBasic”的引用并调用来加载 WSC

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

其中“controlFilename”是完整的文件路径。 GetObject 返回“System.__ComObject”类型的引用,但可以使用 .net 的“动态”类型访问属性和方法。

这最初似乎工作得很好,但是当相当多的特定情况组合在一起时我遇到了问题 - 我担心这可能会发生在其他情况下,或者更糟糕的是,大部分时间都会发生不好的事情并被蒙面,只是等待在我最意想不到的时候爆炸。

引发的异常是“System.ExecutionEngineException”类型,听起来特别可怕(而且模糊)!

我已经拼凑了我认为是最小重现案例的内容,并希望有人可以对问题可能是什么有所了解。我还发现了一些似乎可以阻止它的调整,但我无法解释原因。

  1. 创建一个名为“WSCErrorExample”的新空“ASP.NET Web 应用程序”(我在 VS 2013 / .net 4.5 和 VS 2010 / .net 4.0 中完成了此操作,没有区别)

  2. 向项目添加对“Microsoft.VisualBasic”的引用

  3. 添加一个名为“Default.aspx”的新“Web 表单”并将以下内容粘贴到“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”中存在问题。

附加信息:运行时遇到致命错误。错误地址位于线程 0x1e10 上的 0x733c3512。错误代码为 0xc0000005。此错误可能是 CLR 中的错​​误或用户代码的不安全或不可验证部分中的错误。此错误的常见来源包括 COM-> interop 或 PInvoke 的用户封送错误,这可能会损坏堆栈。

有时,第二个请求不会导致此异常,但在浏览器窗口中按住 F5 几秒钟将确保它抬起丑陋的脑袋。据我所知,该异常发生在“If IsEmpty”检查中(此重现案例的其他版本有更多的日志记录调用,这表明该行是问题的根源)。

我尝试了各种方法来解决这个问题,我尝试在控制台应用程序中重新创建,但问题没有发生,即使我启动了数百个线程并让它们处理上面的工作。我尝试了一个 ASP.Net MVC Web 应用程序,而不是使用 Web 窗体,并且确实发生了同样的问题。我已经尝试将公寓状态从默认的 MTA 更改为 STA(那时我有点抓紧了稻草!)并且它没有改变行为。我尝试构建一个使用 Microsoft 的 OWIN implementation 的 Web 项目,并且在这种情况下也会出现问题。

我注意到了两件有趣的事情——如果“DataContainer”类没有索引属性(或默认方法/属性,用 [DispId(0)] 属性装饰——在这个例子中没有说明),那么不会发生错误。如果“logger”闭包不包含“FileInfo”引用(如果维护了字符串“logFilePath”,而不是 FileInfo 实例“logFile”),则不会发生错误。我想这听起来像是一种方法是避免做这些事情!但我担心可能有其他方法来触发我目前不知道的这种情况,并且随着代码库的增长,尝试强制执行不做这些事情的规则可能会变得复杂,我可以想象这个错误重新出现,但原因并没有立即显而易见。

在一次运行中(通过 Katana),我得到了额外的调用堆栈信息:

该线程在调用堆栈上只有外部代码帧时停止。外部代码框架通常来自框架代码,但也可以包含在目标进程中加载​​的其他优化模块。

使用外部代码调用堆栈

mscorlib.dll!System.Variant.Variant(object obj) mscorlib.dll!System.OleAutBinder.ChangeType(对象值,System.Type 类型,System.Globalization.CultureInfocultureInfo) mscorlib.dll!System.RuntimeType.TryChangeType(object value, System.Reflection.Binder binder, System.Globalization.CultureInfoculture, bool needsSpecialCast) mscorlib.dll!System.RuntimeType.CheckValue(对象值,System.Reflection.Binder 活页夹,System.Globalization.CultureInfo 文化,System.Reflection.BindingFlags invokeAttr) mscorlib.dll!System.Reflection.MethodBase.CheckArguments(object[] 参数,System.Reflection.Binder binder,System.Reflection.BindingFlags invokeAttr,System.Globalization.CultureInfo 文化,System.Signature sig) [本机到托管转换]

最后一点:如果我为“DataProvider”类创建一个包装器,使用IReflect 并将 IDispatch 上的调用映射到对底层“DataProvider”实例的调用,那么问题就会消失。但同样,决定这是某种答案对我来说似乎很危险——如果我必须小心谨慎地确保传递给组件的任何引用都具有这样的包装器,那么错误可能会蔓延到难以追踪的地方。如果封装在 IReflect 实现包装器中的引用返回来自方法或属性调用的引用,该引用未以相同方式包装怎么办?我想包装器可以尝试做一些事情,比如确保它只返回“安全”引用(即那些没有索引属性或 DispId=0 方法或属性的引用)而不将 它们 包装在另一个 IReflect 包装器中。但这一切似乎有点老套。

我真的不知道下一步该去哪里解决这个问题,有人知道吗?

【问题讨论】:

  • 我认为您可能需要确保一次只能从单个线程访问 WSC 代码。您可能需要在使用该代码时添加lock 语句。我怀疑它是为了在像 ASP.NET 这样的多线程环境中运行。
  • 我尝试将静态锁对象放入 Default.aspx.cs 并使用它将工作包装在锁中,但仍然没有乐趣。如果它有所作为,我会感到惊讶,因为当第一个请求完全终止并且第二个(非并发)请求进入时发生错误。但值得一试,所以谢谢! :)

标签: c# asp.net vbscript com wsc


【解决方案1】:

我的猜测是,您看到的错误是由于 WSC 脚本组件本质上是 COM STA 对象这一事实引起的。它们由底层的 VBScript Active Scripting Engine 实现,它本身就是一个 STA COM 对象。因此,它们需要创建和访问 STA 线程,并且此类线程应在任何特定 WSC 对象的生命周期内保持不变(该对象需要线程亲和性)。

ASP.NET 线程不是 STA。它们是ThreadPool 线程,当您开始在它们上使用COM 对象时,它们隐含地成为COM MTA 线程(有关STA 和MTA 之间的差异,请参阅INFO: Descriptions and Workings of OLE Threading Models)。 COM 然后为您的 WSC 对象创建一个单独的隐式 STA 单元,并从您的 ASP.NET 请求线程中编组调用。整个事情在 ASP.NET 环境中可能会也可能不会顺利。

理想情况下,您应该摆脱 WSC 脚本组件并将其替换为 .NET 程序集。如果这在短期内不可行,我建议您运行自己的明确控制的 STA 线程来托管 WSC 组件。以下可能会有所帮助:

更新,何不试试this?您的代码如下所示:

// 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();

如果可行,您可以通过使用PageAsyncTaskasync/await 而不是阻止Wait(),在一定程度上提高网络应用的可扩展性。

【讨论】:

  • 感谢您的回复!我曾尝试将 AspCompat="true" 添加到 Page 声明中,据我所知,它告诉 Web 窗体在 STA 模式下的线程中运行。将“About to call Go”记录器调用更改为 logger("About to call Go - Hosting Thread has ApartmentState: " + System.Threading.Thread.CurrentThread.GetApartmentState()) 似乎确认这已经奏效,但是问题仍然发生。这是你的意思吗?将旧代码重写为 .net 绝对是理想的(也是长期计划),但我认为一次尝试全部完成可能会太费劲。
  • @DanDanDan, AspCompat="true" 确实应该缓解 MTA 问题,但显然没有。时间允许时,我会使用您的代码。您是否尝试过在 ASP.NET 之外执行它,使用单元测试,或者只是一个简单的控制台应用程序(在 STA 线程上)?
  • 我确实使用控制台应用程序尝试过,并认为我无法重现该问题。然而,我又从头开始,得到了一个启动数百个线程并尝试执行工作的应用程序。如果我没有将线程上的公寓状态设置为 STA,那么 大多数时候 它会运行,应用程序会崩溃(没有有用的异常信息)。将公寓状态设置为 STA,它似乎在大多数情况下都可以工作:gist.github.com/anonymous/80a2fa7f8589ee8cf92e。我仍然不知道为什么它在 ASP.Net 中以兼容(支持 STA)模式失败。
  • 这看起来很有希望!在初步检查时,我似乎无法重现该错误。我只是想多玩一点,然后如果它看起来还不错,就接受你的回答。这真是个好消息,非常感谢! :) 猜猜我将不得不深入研究并弄清楚这一切意味着什么(从您之前链接的答案开始)。非常小的一点:在您的示例代码中,“GlobalState”构造函数需要括号作为“公共”访问器删除,然后一切编译正常。
  • 我已经对此进行了更彻底的测试,并且在我尝试过的各种情况下都无法破解它 - 所以我认为这是成功的!再次感谢您的帮助,我肯定会阅读您链接到的所有信息以尝试了解解决方案。非常高兴地接受了答案!
猜你喜欢
  • 1970-01-01
  • 2010-12-14
  • 2014-09-03
  • 2011-07-02
  • 2010-12-24
  • 2012-06-26
  • 2012-02-27
  • 2010-12-27
  • 1970-01-01
相关资源
最近更新 更多