可以通过添加对“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”文件夹.但是,刷新页面通常会导致异常
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.BindingFlags invokeAttr)
mscorlib.dll!System.Reflection.MethodBase.CheckArguments(object[] parameters,System.Reflection.BindingFlags invokeAttr,System.Signature sig)
[Native to Managed Transition]
最后要注意的是:如果我为“DataProvider”类创建一个包装器,使用IReflect并将IDispatch上的调用映射到对底层“DataProvider”实例的调用,那么问题就会消失.但同样,我认为这个答案在某种程度上对我来说似乎很危险 – 如果我必须一丝不苟地确保传递给组件的任何引用都有这样的包装器,那么错误可能会很难追踪到.如果包含在IReflect实现包装器中的IS引用返回的方法或属性调用的引用未以相同的方式包装,该怎么办?我想包装器可以尝试做一些事情,比如确保它只返回“安全”引用(即那些没有索引属性或DispId = 0方法或属性的引用),而不将它们包装在另一个IReflect包装器中……但这一切看起来都有点hacky .
我真的不知道接下来要解决这个问题,有没有人有任何想法?
解决方法
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
// 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()来改善Web应用程序的可伸缩性.