我将为使用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窗体”,并将以下内容粘贴到“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”文件夹。但是,刷新页面通常会导致异常



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

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

    我注意到的两件有趣的事情-如果“DataContainer”类没有索引属性(或默认方法/属性,装饰有[DispId(0)]属性-在此示例中未说明),则错误不会发生。如果“记录器”闭包不包含“FileInfo”引用(如果保留了字符串“logFilePath”,而不是FileInfo实例“logFile”),则不会发生该错误。我想这听起来像是一种避免做这些事情的方法!但是我担心,还有其他方式可以触发我目前尚不了解的这种情况,并尝试执行不执行的规则,随着代码库的增长,这些事情可能会变得复杂,我可以想象这错误逐渐消失,而没有立即显而易见的原因。

    一次运行(通过Katana),我得到了更多的调用堆栈信息:



    最后一点:如果我使用IReflect为“DataProvider”类创建一个包装器,并将IDispatch上的调用映射到对基础“DataProvider”实例的调用,则问题将消失。但是,再次以某种方式确定这对我来说似乎是危险的答案-如果我必须谨慎确保传递给组件的任何引用都具有这样的包装,那么错误可能会蔓延开来,这可能很难追查。如果包含在IReflect-implementing包装器中的引用返回的方法或属性调用的引用不是以相同的方式包装的,该怎么办?我想包装器可以尝试做一些事情,例如确保其仅返回“安全”引用(即那些没有索引属性或DispId = 0方法或属性的引用),而不将它们包装在进一步的IReflect包装器中。 。

    我真的不知道下一步该怎么做,有人知道吗?

    最佳答案

    我的猜测是,您看到的错误是由WSC脚本组件本质上是COM STA对象引起的。它们由基础的VBScript Active脚本引擎实现,该引擎本身是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();
    

    如果可行,则可以使用PageAsyncTaskasync/await而不是阻止Wait()来稍微改善Web应用程序的可伸缩性。

    10-06 04:55