c# asp.net vbscript com wsc

FatalExecutionEngineError en la interoperabilidad C#/ WSC(COM)



asp.net vbscript (1)

Supongo que el error que está viendo está causado por el hecho de que los componentes de script de WSC son objetos COM STA por su naturaleza. Están implementados por el motor VBScript Active Scripting Engine subyacente, que a su vez es un objeto STA COM. Como tal, requieren que se cree un subproceso STA y se acceda a él, y dicho subproceso debe permanecer igual durante el tiempo de vida de cualquier objeto WSC en particular (el objeto requiere afinidad de subprocesos).

Los hilos de ASP.NET no son STA. Son subprocesos de ThreadPool , y se convierten implícitamente en subprocesos de COM MTA cuando comienza a usar objetos COM en ellos (para conocer las diferencias entre STA y MTA, consulte INFO: descripciones y funcionamiento de los modelos de subprocesamiento OLE ). COM luego crea un apartamento de STA implícito separado para sus objetos de WSC y reúne llamadas desde su subproceso de solicitud de ASP.NET. Todo esto puede o no funcionar bien en el entorno ASP.NET.

Idealmente, debería deshacerse de los componentes de script de WSC y reemplazarlos con ensamblados de .NET. Si eso no es factible a corto plazo, le recomendaría que ejecute sus propios hilos STA controlados explícitamente para alojar los componentes WSC. Lo siguiente puede ayudar:

Actualizado , ¿por qué no probarlo? Tu código se vería así:

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

Si eso funciona, puedes mejorar la capacidad de ampliación de la aplicación web mediante el uso de PageAsyncTask y async/await PageAsyncTask lugar del bloqueo Wait() .

Estoy a punto de comenzar un proyecto de migración en el trabajo para un sistema heredado escrito en VBScript. Tiene una estructura interesante porque gran parte de ella se segregó al escribir varios componentes como archivos "WSC", que son efectivamente una forma de exponer el código de VBScript de manera similar a COM. La interfaz de límite entre el "núcleo" y estos componentes es bastante estrecha y bien conocida, así que esperaba poder abordar la escritura de un nuevo núcleo y reutilizar las WSC, posponiendo su reescritura.

Es posible cargar un WSC agregando una referencia a "Microsoft.VisualBasic" y llamando

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

donde "controlFilename" es la ruta completa del archivo. GetObject devuelve una referencia de tipo "System .__ ComObject" pero se puede acceder a las propiedades y métodos utilizando el tipo "dynamic" de .net.

Al principio, esto pareció funcionar bien, pero me he encontrado con problemas cuando se combinan un conjunto bastante específico de circunstancias: mi preocupación es que esto pueda suceder en otros casos o, peor aún, que las cosas malas suceden la mayor parte del tiempo y se enmascaren. , esperando estallar cuando menos lo espero.

La excepción planteada es del tipo "System.ExecutionEngineException", que suena particularmente aterrador (y vago).

He improvisado lo que creo que es el caso de reproducción mínima y esperaba que alguien pudiera arrojar algo de luz sobre cuál podría ser el problema. También identifiqué algunos ajustes que se pueden hacer que parecen prevenirlo, aunque no puedo explicar por qué.

  1. Cree una nueva "Aplicación web ASP.NET vacía" llamada "WSCErrorExample" (He hecho esto en VS 2013 / .net 4.5 y VS 2010 / .net 4.0, no hace ninguna diferencia)

  2. Agregue una referencia a "Microsoft.VisualBasic" al proyecto

  3. Agregue un nuevo "Formulario web" llamado "Default.aspx" y pegue lo siguiente en la parte superior de "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. Agregue un nuevo "Archivo de texto" llamado "TestComponent.wsc", abra su ventana de propiedades y cambie "Copiar al directorio de salida" a "Copiar si es más nuevo" y pegue lo siguiente como su contenido

    <?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>

Ejecutar esto una vez no debería causar ningún problema aparente, el archivo "Log.txt" se escribirá en la carpeta "bin". Actualizar la página, sin embargo, normalmente da como resultado una excepción

El asistente de depuración administrado ''FatalExecutionEngineError'' detectó un problema en ''C: / Archivos de programa (x86) / IIS Express / iisexpress.exe''.

Información adicional: el tiempo de ejecución ha encontrado un error fatal. La dirección del error estaba en 0x733c3512, en el hilo 0x1e10. El código de error es 0xc0000005. Este error puede ser un error en el CLR o en las partes inseguras o no verificables del código de usuario. Las fuentes comunes de este error incluyen errores de clasificación de usuarios para COM-> interop o PInvoke, que pueden dañar la pila.

Ocasionalmente, la segunda solicitud no da como resultado esta excepción, pero si mantiene presionado F5 en la ventana del navegador durante unos segundos, se asegurará de que asoma su horrible cabeza. La excepción, que yo sepa, ocurre en la comprobación "If Is Empty" (otras versiones de este caso de reproducción tuvieron más llamadas de registro, lo que indica que esa es la fuente del problema).

He intentado varias cosas para tratar de llegar al fondo de esto, he intentado recrear en una aplicación de consola y el problema no ocurre, incluso si hago girar cientos de subprocesos y hago que procesen el trabajo anterior. Probé una aplicación web ASP.Net MVC, en lugar de utilizar un formulario web y ocurre el mismo problema. Intenté cambiar el estado del apartamento del MTA predeterminado a STA (¡estaba agarrando un poco las pajitas en ese momento!) Y no modificó el comportamiento. Intenté construir un proyecto web que utilizara la implementación de OWIN de Microsoft y el problema también ocurre en ese escenario.

Dos cosas interesantes que he notado: si la clase "DataContainer" no tiene una propiedad indexada (o un método / propiedad predeterminado, decorado con un atributo [DispId (0)] - no ilustrado en este ejemplo), entonces el error no ocurrir. Si el cierre "logger" no contiene una referencia "FileInfo" (si se mantuvo una cadena "logFilePath", en lugar de la instancia FileInfo "logFile"), entonces el error no ocurre. ¡Supongo que parece que un enfoque sería evitar hacer estas cosas! Pero me preocuparía que podría haber otras formas de desencadenar este escenario que actualmente no conozco y tratar de hacer cumplir la regla de no hacer estas cosas podría complicarse a medida que crezca la base de códigos, me puedo imaginar esto error volviendo a entrar sin que sea inmediatamente obvio por qué.

En una carrera (a través de Katana), obtuve información adicional de la pila de llamadas:

Este hilo se detiene solo con marcos de código externos en la pila de llamadas. Las tramas de código externo son típicamente del código de la infraestructura pero también pueden incluir otros módulos optimizados que se cargan en el proceso objetivo.

Pila de llamadas con código externo

mscorlib.dll! System.Variant.Variant (object obj) mscorlib.dll! System.OleAutBinder.ChangeType (valor del objeto, tipo de System.Type, System.Globalization.CultureInfo cultureInfo) mscorlib.dll! System.RuntimeType.TryChangeType (valor del objeto) , System.Reflection.Binder Binder, System.Globalization.CultureInfo culture, bool needsSpecialCast) mscorlib.dll! System.RuntimeType.CheckValue (valor del objeto, 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 ]

Una nota final: si creo un contenedor para la clase "DataProvider", usando IReflect y asignando las llamadas a través de IDispatch a las llamadas a la instancia "DataProvider" subyacente, el problema desaparece. Pero una vez más, al decidir que esto es de alguna manera, la respuesta me parece peligrosa: si tengo que ser meticuloso para asegurar que cualquier referencia pasada a los componentes tenga tal envoltorio, podrían aparecer errores que podrían ser difíciles de rastrear. ¿Qué sucede si una referencia que está encerrada en un contenedor implementador de IReflect devuelve una referencia de un método o llamada de propiedad que no está completa de la misma manera? Supongo que el contenedor podría intentar hacer algo así como asegurar que solo devuelva una referencia "segura" (es decir, aquellos sin propiedades indexadas o métodos o propiedades DispId = 0) sin envolverlos en un envoltorio IReflect adicional ... pero todo parece un poco hacky .

Realmente no tengo ni idea de a dónde ir con este problema, ¿alguien tiene alguna idea?