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

Dan*_*rts 7 c# asp.net com vbscript wsc

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

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

var component = (dynamic)Microsoft.VisualBasic.Interaction.GetObject("script:" + controlFilename, null);
Run Code Online (Sandbox Code Playgroud)

其中"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; }
                }
            }
        }
    }
    
    Run Code Online (Sandbox Code Playgroud)
  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>
    
    Run Code Online (Sandbox Code Playgroud)

运行一次应该没有明显的问题,"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 Web应用程序,而不是使用Web表单,并且会出现同样的问题.我已经尝试将公寓状态从默认的MTA更改为STA(此时我抓住了一点稻草!)并且它没有改变行为.我已经尝试构建一个使用Microsoft的OWIN实现的Web项目,该问题也出现在该场景中.

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

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

仅在调用堆栈上使用外部代码帧停止此线程.外部代码帧通常来自框架代码,但也可以包括在目标进程中加载​​的其他优化模块.

使用外部代码调用堆栈

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实现包装器中的IS引用返回的方法或属性调用的引用未以相同的方式包装,该怎么办?我想包装器可以尝试做一些事情,比如确保它只返回"安全"引用(即那些没有索引属性或DispId = 0方法或属性的引用),而不将它们包装在另一个IReflect包装器中......但这一切看起来都有点hacky .

我真的不知道接下来要解决这个问题,有没有人有任何想法?

nos*_*tio 2

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

ASP.NET 线程不是 STA。它们是ThreadPool线程,当您开始在它们上使用 COM 对象时,它们会隐式地成为 COM MTA 线程(有关 STA 和 MTA 之间的差异,请参阅信息:OLE 线程模型的描述和工作原理)。然后,COM 为您的 WSC 对象创建一个单独的隐式 STA 单元,并从您的 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();
Run Code Online (Sandbox Code Playgroud)

如果有效,您可以通过使用PageAsyncTaskasync/await而不是阻塞来在一定程度上提高 Web 应用程序的可扩展性Wait()

  • 感谢您的答复!我曾尝试将 AspCompat="true" 添加到页面声明中,据我了解,这告诉 Web 窗体以 STA 模式在线程中运行。将“About to call Go”记录器调用更改为 logger("About to call Go - Hosting Thread has ApartmentState: " + System.Threading.Thread.CurrentThread.GetApartmentState()) 似乎确认这已经有效,但问题仍然发生。这是你的意思吗?将旧代码重写到 .net 中绝对是理想的(也是更长期的计划),但我认为尝试一次性完成这一切可能会耗费太多精力。 (2认同)