c# - C#/WSC (COM) 互操作中的 FatalExecutionEngineError

标签 c# asp.net vbscript com wsc

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

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

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

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

这最初似乎工作正常,但是当组合相当多的特定情况时我遇到了问题 - 我担心这可能会发生在其他情况下,或者更糟糕的是,大部分时间都在发生坏事并被掩盖,就在我最意想不到的时候等着爆炸。

引发的异常属于“System.ExecutionEngineException”类型,听起来特别可怕(而且含糊不清)!

我已经拼凑出我认为是最小复制案例的内容,并希望有人能对问题所在有所了解。我还确定了一些似乎可以防止它发生的调整,但我无法解释原因。
  • 创建一个名为“WSCErrorExample”的新的空“ASP.NET Web 应用程序”(我已经在 VS 2013/.net 4.5 和 VS 2010/.net 4.0 中这样做了,没有区别)
  • 向项目添加对“Microsoft.VisualBasic”的引用
  • 添加一个名为“Default.aspx”的新“Web Form”并将以下内容粘贴到“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; }
                }
            }
        }
    }
    
  • 添加一个名为“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”文件夹。然而,刷新页面通常会导致异常

    Managed Debugging Assistant 'FatalExecutionEngineError' has detected a problem in 'C:\Program Files (x86)\IIS Express\iisexpress.exe'.

    Additional information: The runtime has encountered a fatal error. The address of the error was at 0x733c3512, on thread 0x1e10. The error code is 0xc0000005. This error may be a bug in the CLR or in the unsafe or non-verifiable portions of user code. Common sources of this bug include user marshaling errors for COM-> interop or PInvoke, which may corrupt the stack.



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

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

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

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

    This thread is stopped with only external code frames on the call stack. External code frames are typically from framework code but can also include other optimized modules which are loaded in the target process.

    Call stack with external code

    mscorlib.dll!System.Variant.Variant(object obj) mscorlib.dll!System.OleAutBinder.ChangeType(object value, System.Type type, System.Globalization.CultureInfo cultureInfo) mscorlib.dll!System.RuntimeType.TryChangeType(object value, System.Reflection.Binder binder, System.Globalization.CultureInfo culture, bool needsSpecialCast) mscorlib.dll!System.RuntimeType.CheckValue(object value, System.Reflection.Binder binder, System.Globalization.CultureInfo culture, System.Reflection.BindingFlags invokeAttr) mscorlib.dll!System.Reflection.MethodBase.CheckArguments(object[] parameters, System.Reflection.Binder binder, System.Reflection.BindingFlags invokeAttr, System.Globalization.CultureInfo culture, System.Signature sig) [Native to Managed Transition]



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

    我真的不知道接下来要解决这个问题,有人知道吗?

    最佳答案

    我的猜测是,您看到的错误是由 WSC 脚本组件本质上是 COM STA 对象这一事实引起的。它们由底层的 VBScript 事件脚本引擎实现,它本身是一个 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 组件。以下可能有帮助:

  • How to use non-thread-safe async/await APIs and patterns with ASP.NET Web API?
  • StaTaskScheduler and STA thread message pumping

  • 更新 ,何不给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();
    

    如果可行,您可以使用 PageAsyncTask 稍微提高 Web 应用程序的可扩展性。和 async/await而不是阻塞 Wait() .

    关于c# - C#/WSC (COM) 互操作中的 FatalExecutionEngineError,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/24759189/

    相关文章:

    c# - 以编程方式设置文本区域不起作用 C#

    c# - 事务范围因 Oracle : Connection is already part of a local or a distributed transaction 中的 BeginTransaction 而失败

    c# - 使用奇怪的 namespace 序列化 XML?

    c# - 我应该如何在 ASP.NET 中记录异常?

    c# - System.Web.UI.DataVisualization.Charting 中的雷达图选择性标签旋转?

    c# - 没有新 C# 的构造函数

    c# - 下载时生成空白pdf -ItextSharp

    c# - 当接收对象是 C# COM 对象并且函数具有接口(interface)作为参数时,VBScript 抛出异常

    mysql - VBScript 在同一代码中使用两个 For Next

    c# - 无法从 vbscript 调用 c# 代码 - ActiveX 错误