c# .net mstest system.management

c# - NullReferenceException en el finalizador durante MSTest



.net system.management (3)

(Lo sé, esta es una pregunta ridículamente larga. Traté de separar la pregunta de mi investigación hasta ahora, por lo que es un poco más fácil de leer).

Estoy ejecutando mis pruebas unitarias usando MSTest.exe. De vez en cuando, veo este error de prueba:

En el método de prueba de unidad individual: "El proceso del agente se detuvo mientras la prueba se estaba ejecutando".

En toda la prueba de ejecución:

One of the background threads threw exception: System.NullReferenceException: Object reference not set to an instance of an object. at System.Runtime.InteropServices.Marshal.ReleaseComObject(Object o) at System.Management.Instrumentation.MetaDataInfo.Dispose() at System.Management.Instrumentation.MetaDataInfo.Finalize()

Entonces, esto es lo que creo que debo hacer: necesito rastrear qué está causando el error en MetaDataInfo, pero estoy en blanco. El conjunto de pruebas de mi unidad tarda más de media hora en ejecutarse, y el error no se produce todas las veces, por lo que es difícil que se reproduzca.

¿Alguien más ha visto este tipo de falla en la ejecución de pruebas unitarias? ¿Pudiste rastrearlo a un componente específico?

Editar:

El código bajo prueba es una mezcla de C #, C ++ / CLI y un poco de código de C ++ no administrado. El C ++ no administrado se usa solo desde C ++ / CLI, nunca directamente desde las pruebas unitarias. Las pruebas unitarias son todas de C #.

El código bajo prueba se ejecutará en un Servicio de Windows independiente, por lo que no hay complicación en ASP.net ni nada de eso. En el código que se está probando, hay subprocesos que se inician y se detienen, comunicaciones de red y archivos de E / S en el disco duro local.

Mi investigación hasta ahora:

Pasé un poco de tiempo explorando las múltiples versiones del conjunto de System.Management en mi máquina con Windows 7, y encontré la clase MetaDataInfo en System.Management que está en mi directorio de Windows. (La versión que está bajo Archivos de programa / Referencias de referencia es mucho más pequeña y no tiene la clase MetaDataInfo).

Usando Reflector para inspeccionar este ensamblaje, encontré lo que parece ser un error obvio en MetaDataInfo.Dispose ():

// From class System.Management.Instrumentation.MetaDataInfo: public void Dispose() { if (this.importInterface == null) // <---- Should be "!=" { Marshal.ReleaseComObject(this.importInterface); } this.importInterface = null; GC.SuppressFinalize(this); }

Con esta declaración ''if'' al revés, MetaDataInfo perderá el objeto COM si está presente, o lanzará una NullReferenceException si no lo está. He informado de esto en Microsoft Connect: https://connect.microsoft.com/VisualStudio/feedback/details/779328/

Usando el reflector, pude encontrar todos los usos de la clase MetaDataInfo. (Es una clase interna, por lo que solo buscar en el ensamblaje debería ser una lista completa). Solo hay un lugar donde se usa:

public static Guid GetMvid(Assembly assembly) { using (MetaDataInfo info = new MetaDataInfo(assembly)) { return info.Mvid; } }

Dado que todos los usos de MetaDataInfo se desechan correctamente, esto es lo que está sucediendo:

  • Si MetaDataInfo.importInterface no es nulo:
    • Método estático GetMvid devuelve MetaDataInfo.Mvid
    • El using llamadas MetaDataInfo.Dispose
      • Eliminar fugas el objeto COM
      • Eliminar conjuntos importInterface en nulo
      • Disponer llamadas GC.SuppressFinalize
    • Más tarde, cuando el GC recopila MetaDataInfo, se omite el finalizador.
  • .
  • Si MetaDataInfo.importInterface es nulo:
    • método estático GetMvid obtiene una NullReferenceException llamando a MetaDataInfo.Mvid.
    • Antes de que la excepción se propague, el using llamadas MetaDataInfo.Dispose
      • Eliminar llamadas Marshal.ReleaseComObject
        • Marshal.ReleaseComObject lanza una NullReferenceException.
      • Debido a que se lanza una excepción, Dispose no llama a GC.SuppressFinalize
    • La excepción se propaga hasta el llamador de GetMvid.
    • Más tarde, cuando el GC recopila MetaDataInfo, ejecuta el Finalizer
      • Finalizar llamadas
        • Eliminar llamadas Marshal.ReleaseComObject
          • Marshal.ReleaseComObject lanza una NullReferenceException, que se propaga hasta el GC, y la aplicación finaliza.

Para lo que vale, aquí está el resto del código relevante de MetaDataInfo:

public MetaDataInfo(string assemblyName) { Guid riid = new Guid(((GuidAttribute) Attribute.GetCustomAttribute(typeof(IMetaDataImportInternalOnly), typeof(GuidAttribute), false)).Value); // The above line retrieves this Guid: "7DAC8207-D3AE-4c75-9B67-92801A497D44" IMetaDataDispenser o = (IMetaDataDispenser) new CorMetaDataDispenser(); this.importInterface = (IMetaDataImportInternalOnly) o.OpenScope(assemblyName, 0, ref riid); Marshal.ReleaseComObject(o); } private void InitNameAndMvid() { if (this.name == null) { uint num; StringBuilder szName = new StringBuilder { Capacity = 0 }; this.importInterface.GetScopeProps(szName, (uint) szName.Capacity, out num, out this.mvid); szName.Capacity = (int) num; this.importInterface.GetScopeProps(szName, (uint) szName.Capacity, out num, out this.mvid); this.name = szName.ToString(); } } public Guid Mvid { get { this.InitNameAndMvid(); return this.mvid; } }

Edición 2:

Pude reproducir el error en la clase MetaDataInfo para Microsoft. Sin embargo, mi reproducción es ligeramente diferente del problema que estoy viendo aquí.

  • Reproducción: Intento crear un objeto MetaDataInfo en un archivo que no es un ensamblaje administrado. Esto importInterface una excepción desde el constructor antes de que se inicialice importInterface .
  • Mi problema con MSTest: MetaDataInfo se construye en algún ensamblaje administrado, y algo sucede para hacer que importInterface nulo, o para salir del constructor antes de que se inicialice importInterface .
    • Sé que MetaDataInfo se crea en un ensamblaje administrado, porque MetaDataInfo es una clase interna, y la única API que lo llama pasa el resultado de Assembly.Location .

Sin embargo, volver a crear el problema en Visual Studio significaba que descargó la fuente en MetaDataInfo para mí. Aquí está el código real, con los comentarios del desarrollador original.

public void Dispose() { // We implement IDisposable on this class because the IMetaDataImport // can be an expensive object to keep in memory. if(importInterface == null) Marshal.ReleaseComObject(importInterface); importInterface = null; GC.SuppressFinalize(this); } ~MetaDataInfo() { Dispose(); }

El código original confirma lo que se vio en el reflector: la instrucción if está al revés, y no deberían acceder al objeto gestionado desde el finalizador.

Dije antes porque nunca estaba llamando a ReleaseComObject , que estaba ReleaseComObject el objeto COM. Leí más sobre el uso de objetos COM en .Net, y si lo entiendo correctamente, fue incorrecto: el objeto COM no se libera cuando se llama a Dispose (), pero se libera cuando el recolector de basura se acerca a recolectando el Runtime Callable Wrapper, que es un objeto administrado. Aunque es un contenedor para un objeto COM no administrado, el RCW sigue siendo un objeto administrado, y la regla sobre "no acceder a los objetos administrados desde el finalizador" todavía debería aplicarse.


¿Estás tratando de arreglar esto para tus pruebas? Si es así, vuelva a escribir su uso. No lo deseche usted mismo, sino que escriba un código para usar la reflexión para acceder a los campos privados y desecharlos correctamente y luego llame a GC.SuppressFinalize para evitar que se ejecute el finalizador.

Como un resumen a un lado (me encantó su investigación por cierto), usted declara que Deshacer llamadas finaliza. Es al revés, Finalice cuando sea invocado por las llamadas de GC Eliminar.


Intente agregar el siguiente código a su definición de clase:

bool _disposing = false // class property public void Dispose() { if( !disposing ) Marshal.ReleaseComObject(importInterface); importInterface = null; GC.SuppressFinalize(this); disposing = true; }


Si MetaDataInfo usa el patrón IDisposable, entonces también debería haber un finalizador (~ MetaDataInfo () en C #). La instrucción de uso se asegurará de llamar a Dispose (), que establece el valor de importInterface en nulo. Luego, cuando el GC está listo para finalizar, se llama a ~ MetaDataInfo (), que normalmente llamaría a Dispose (o más bien a la sobrecarga que toma un bool disposing: Dispose (false)).

Yo diría que este error debería aparecer con bastante frecuencia.