Sunday 11 December 2011

Compilar y ejecutar C# desde PowerShell

Hola a todos,

Estoy encantado con PowerShell, como al principio de mi vida profesional pasé cierto tiempo utilizando bash en entornos linux siempre había echado en falta un lenguaje de scripting tan ágil en los entornos Microsoft (es posible que por esa frase se me echen encima los amantes de vbscript y wsh pero es una afirmación basada en mi experiencia personal).

Supongo que, como llevo muchos años desarrollando en C#, este "cariño" que le estoy cogiendo a PowerShell viene relacionado con que esté tan ligado a .Net. La única pega que le podría poner es que parece que es bastante más lento, sobre todo cuando realizas llamadas a las librerías de .Net, sin embargo todavía no he hecho ninguna prueba en profundidad para afirmar esto completamente, así que esto quedará para otro post. Lo que sí que puedo afirmar, que es el objetivo de este post, es que es enormemente potente ya que nos permite, no solo llamar a funciones en objetos COM o en ensamblados .Net sino incluso "compilar" código C# "on the fly" y, acto seguido, llamar a dichas funciones.  A continuación os dejo un script que muestra cómo hacerlo que descubrí en este post.

function Compile-Csharp (

 [string] $code, $FrameworkVersion='v2.0.50727', [Array]$References)

 {

      $framework = 'c:\windows\Microsoft.NET\Framework\$FrameWorkVersion';

      $cp = new-object Microsoft.CSharp.CSharpCodeProvider;

      $refs = new-object Collections.ArrayList;

      $refs.AddRange(

        @('c:\windows\Microsoft.NET\Framework\v2.0.50727\System.dll',

        'c:\windows\Microsoft.NET\Framework\v2.0.50727\system.windows.forms.dll',

        'c:\windows\Microsoft.NET\Framework\v2.0.50727\System.data.dll'));

      if ($References.Count -ge 1)     {         $refs.AddRange($References);     }

      $cpar = new-object System.CodeDom.Compiler.CompilerParameters;

      $cpar.GenerateInMemory = $true;

      $cpar.GenerateExecutable = $false;

      $cpar.OutputAssembly = 'custom';

      $cpar.ReferencedAssemblies.AddRange($refs);

      $cr = $cp.CompileAssemblyFromSource($cpar, $code);

      write-host 'Found ' $cr.Errors.Count ' errors';

      if ($cr.Errors.Count)

      {

               $codeLines = $code.Split('`n');

               foreach ($ce in $cr.Errors)

               {

                  write-host 'Line:' $ce.Line;

                  write-host 'Column:' $ce.Column;

                  write-host 'Error:' $ce.ErrorText;

               }

               Throw 'INVALID DATA: Errors encountered while compiling code';        

      }

}

Desde PowerShell 2.0 aún es más sencillo con la adición de la función Add-Type:

Add-Type -ReferencedAssemblies $Assem -TypeDefinition $Source -Language CSharp 

Tened en cuenta las referencias que se añaden para poder hacer la compilación del código.

Una vez descubierta esta funcionalidad de PowerShell, me pregunté ¿si podemos llamar a .Net no podríamos llamar también a las API de Windows utilizando P/Invoke? así que me lancé a poner un par de DllImports en el código C# dentro de PowerShell y "voilà" en pocos minutos estaba haciendo llamadas al API de Windows a través de código C# que se compilaba desde PowerShell. Con lo que por ejemplo, en un simple script ya podía cambiar la configuración de la barra de tareas de Windows, las opciones de accesibilidad, o cualquier otra tarea que de no ser así habría supuesto mucho más trabajo. A continuación os dejo un ejemplo que hace que se muestre el subrayado en las letras de acceso de los elementos de menú en Windows:

$code = 'using System; using System.Runtime.InteropServices; namespace test { public class Testclass { [DllImport(\"user32.dll\")]public static extern int SystemParametersInfo(int uAction, int uParam, int lpvParam, int fuWinIni); } }';

Compile-Csharp  $code;

[Test.TestClass]::SystemParametersInfo(4107,0,1,2)

La función SystemParametersInfo nos puede servir para establecer una gran variedad de parámetros del sistema operativo.

Ni que decir tiene, que desde el punto de vista de administración de SharePoint 2010 esto tiene muchas aplicaciones ya que podemos atacar a un servidor sin necesidad de desplegar nuevos ensamblados, solo con un simple script.

Por ejemplo, añadir un elemento:

$assembly = ("Microsoft.SharePoint, Version=14.0.0.0, Culture=neutral, PublicKeyToken=71e9bce111e9429c");

$code = @"

using System;

using Microsoft.SharePoint;

namespace SP.Script

{

    public static class SampleClass

    {

        public static void Add()

        {

            using (SPSite site = new SPSite("http://SPSite/"))

            {

                using (SPWeb web = site.OpenWeb())

                {

                    web.AllowUnsafeUpdates = true;

                    SPList list=web.Lists["SPList"];

                    SPListItem item = list.Items.Add();

                    item["Title"] = "Test";

                    item.Update();

                }

            }

        }

    }

}

"@

 

Add-Type -ReferencedAssemblies $assembly -TypeDefinition $code -Language CSharp

 

[SP.Script.SampleClass]::Add()

 

Más posibles usos los dejo a vuestra imaginación.

Más info:

Add-Type

http://technet.microsoft.com/es-es/library/dd315241.aspx

Compile-Csharp

http://monadblog.blogspot.com/2005/12/calling-win32-api-functions-through.html

SystemParametersInfo

http://msdn.microsoft.com/en-us/library/windows/desktop/ms724947(v=vs.85).aspx

 

Tuesday 25 October 2011

Troubleshooting WMI

Hola a todos,
como prometí en el post anterior, en este post intentaré explicar un poco más por qué parece que WMI falla tanto y, lo más importante, os hablaré de un par de herramientas que podemos utilizar para comprobar el estado de este servicio y sus proveedores. Por último, finalizaré explicando algunas acciones que podemos tomar para reparar WMI y evitar lanzar el ordenador por la ventana.
Desde el punto de vista de desarrolladores .Net, es posible que tengamos una visión sesgada de WMI, ya que queda encapsulada de manera más o menos afortunada en el espacio de nombres System.Managament. En el post anterior ya intenté explicar un poco la arquitectura de WMI pero como dicen que una imagen vale más que mil palabras, seguramente quedará más claro con el siguiente diagrama.
clip_image002[1]

¿Por qué parece que WMI falla tanto?

Se podría decir que WMI está hasta en la sopa porque es una tecnología que se utiliza un la gran mayoría de componentes del sistema operativo. Como se puede ver en el diagrama anterior, WMI utiliza proveedores con los que interactúa para recuperar información o ejecutar acciones. Estos proveedores pueden ser de lo más variado, pueden acceder a la red, al directorio activo, al sistema de ficheros o a objetos del núcleo de Windows. Si por cualquier tontería, uno de estos componentes falla, WMI devolverá un error, sin embargo, aunque es posible, este error no significa necesariamente que se haya roto WMI.
Lo primero que tenemos que hacer cuando WMI nos devuelva un error es averiguar si es un error nativo de WMI o está originado por un componente externo. Por ejemplo, aquí muestro unos códigos de error extraídos de la documentación de la utilidad WMIDiag de la que hablaré más adelante.
• 0x800410xx y 0x800440xx: Indican errores de operaciones no específicas de WMI. Pueden ser debidos a privilegios insuficientes para realizar la operación, a una manera incorrecta de ejecutar la operación, o por un problema de infraestructura de WMI, como por ejemplo un registro de COM incorrecto de algún proveedor de WMI.
• 0x8007xxxx son errores internos del sistema operativo. WMI puede devolver este tipo de errores por ejemplo por tener privilegios insuficientes para acceder a DCOM.
• 0x80040xxx son errores de DCOM. WMI puede devolver este tipo de error por ejemplo si DCOM está mal configurado.
• 0x80005xxx errores ADSI (LDAP). WMI puede devolver este tipo de errores debido a algún problema al acceder al directorio activo.

¿Cómo puedo diagnosticar WMI?

La utilidad más básica para consultar el estado del servicio WMI es su consola de administración (WMI Control), podemos acceder a ella ejecutando el comando wmimgmt.msc. Si no indica ningún error es posible que WMI no esté totalmente roto y se pueda recuperar, si indica algún error ya tendremos una pista de por dónde empezar. A continuación muestro una captura de pantalla de esta consola, en ella se puede ver que desde aquí podemos administrar también la seguridad de los espacios de nombres o realizar una copia de seguridad.
clip_image003
Otra de las primeras cosas que debemos mirar es si se está ejecutando el servicio Windows Management Instrumentation y está bien configurado. Cosa que podemos hacer desde la consola de administración de los servicios de Windows (services.msc).
Si aun así, no encontramos la fuente del error, aún nos quedan dos ases en la manga. El primero es el script wmidiag.vbs que se puede descargar desde aquí: http://www.microsoft.com/download/en/details.aspx?id=7684
Al ejecutar este script nos genera un informe de gran ayuda a la hora de detectar cualquier error ya que revisa todos los componentes de WMI incluidos posibles problemas por modulos MOF sin compilar o por falta de credenciales para acceder a DCOM.
La otra alternativa es la utilidad WBEMTest, que tiene una interfaz gráfica de la que se muestra a continuación una captura de pantalla. Quizás esta utilidad sea más avanzada que WMIDiag pero recomiendo utilizar ambas ya que con WMIDiag realizaremos un análisis automático de las funcionalidades principales y más tarde con WBEMTest podremos probar la funcionalidad de los espacios de nombres o los aspectos específicos que nos estén dando problemas.
clip_image004

Reparar WMI

Estas acciones pueden ser muy peligrosas y solo deben ejecutarse como último recurso ya que podrían estropearse otras partes del componente u otros componentes que necesitaran indirectamente de WMI para funcionar, así que ejecutadlas bajo vuestra propia responsabilidad.
Después de haber descartado los errores por factores externos, uno de los problemas más comunes que puede presentar WMI es la corrupción de su repositorio, para intentar repararlo podemos intentar actualizarlo y para ello debemos ejecutar el siguiente comando:
rundll32 WbemUpgd, UpgradeRepository
Tras realizar la actualización es posible que no encuentre los archivos de localización de algunos componentes, por ejemplo al ejecutar WMIC se puede quejar de que no encuentra el archivo c:\Windows\MUI\Fallback\0403\CLIEGALI.MFL
Una solución es copiarlo de otra carpeta, por ejemplo de c:\Windows\MUI\Fallback y después compilarlo utilizando la utilidad MOFCOMP.
mofcomp c:\Windows\MUI\Fallback\0403\CLIEGALI.MFL
Hablando de mofcomp otro de los errores comunes es que el archivo MOF de alguno de los proveedores WMI no se haya compilado correctamente, podemos intentar volverlo a recompilar utilizando el comando mofcomp de la siguiente manera:
mofcomp C:\WINDOWS\system32\wbem\Cli.mof
Si no nos funciona ni siquiera la utilidad mofcomp, es posible que tengamos errores más graves, un indicativo de ésto sería que retornara un error del tipo:
An error occurred while opening the namespace for object x defined on lines yyy
0X80041002 Class, instance, or property '' was not found.
Compiler returned error 0x80041002
Para solucionarlo podemos probar a re-registrar WMI en COM y sincronizar los contadores de rendimiento, para ello podemos ejecutar los siguientes comandos:
regsvr32.exe WBEMPROX.DLL
regsvr32.exe FASTPROX.DLL
winmgmt.exe /regserver
winmgmt.exe /clearadap
winmgmt.exe /resyncperf
Más info en:
WMI Architecture
http://msdn.microsoft.com/en-us/library/aa394553%28v=vs.85%29.aspx
WMI Command-line Tool is not Completely Localized in Std 2009
http://blogs.msdn.com/b/embedded/archive/2009/10/29/wmi-command-line-tool-is-not-completely-localized-in-std-2009.aspx
KX9CPP5ENM8F

Saturday 3 September 2011

Optimizando consultas WMI

Hola a todos,

como he estado de vacaciones la semana pasada no posteé nada, intentaré compensar con dos entradas dedicadas a WMI. Cada vez que cuento las excelencias de WMI a otros desarrolladores, los dos principales inconvenientes que me comentan que tienen al intentar utilizarlo son su lentitud y lo fácil que parece algunas veces que deje de funcionar. Estoy casi completamente de acuerdo con ellos pero no del todo :) En este post me centraré en la primera parte, la lentitud. Os daré una explicación a "grosso modo" de por qué puede llegar a ser extremadamente lento e intentaré ofreceros algunos consejos para mejorar el rendimiento. En el siguiente hablaremos de como enfrentarnos a algunos errores.

 

Infraestructura

Para entender por qué las consultas pueden llegar a ser tan lentas es necesario conocer un poco la infraestructura. La arquitectura de WMI básicamente se compone del servicio WMI y el repositorio WMI. Una aplicación o script pide información al servicio WMI a través de las API en COM, el servicio se encarga de recuperarla o bien del repositorio o bien de pedir la información a los proveedores WMI también a través de COM (un proveedor no es más que un objeto COM y un archivo MOF). Por lo tanto la velocidad de WMI dependerá directamente del proveedor WMI que estemos consultando. Más aún, como solo la información estática de los objetos se almacena en el repositorio, el resto de información debe recuperarse dinámicamente del proveedor al momento de pedirla el cliente, lo que ocasiona un mayor retardo. Así que ya sabemos que aunque utilicemos WQL, un lenguaje parecido a SQL para recuperar información, no estamos leyendo de una base de datos si no que alguna de la información se puede estar generando al vuelo.

 

Cachear datos

Algunas de las optimizaciones que podemos aplicar son las mismas que aplicaríamos si tuviéramos que acceder a cualquier otro recurso lento. Por ejemplo cachear los resultados de la consulta si sabemos que se tendrá que repetir y no nos importa que no se reflejen las últimas actualizaciones. Esta técnica es especialmente útil si lo que queremos hacer son consultas y no actualizaciones o ejecución de métodos. Además, ganaremos espacio en memoria si extraemos la información del objeto ManagementObject y la almacenamos en las propiedades de un objeto POCO que sea el que finalmente guardaremos en la cache.

 

Consultas sobre la red

Uno de los usos más populares de WMI es acceder a la información de ordenadores remotos así que sí ya sabemos que las consultas WQL son generalmente lentas (ahora sabemos que porque normalmente deben crearse los datos al vuelo) este rendimiento puede empeorar todavía más si tenemos que añadir la latencia de la red. En el caso de recuperar datos de ordenadores remotos o referentes a la red es muy difícil optimizar las consultas. En estos caso hemos de intentar aglutinar todos los datos a recuperar de un ordenador remoto en una única consulta, de manera que se minimicen los viajes a través de la red. Por otra parte, también es aconsejable si queremos trabajar con un ordenador remoto intentar hacer ping primero y analizar el tiempo de respuesta de manera que sepamos cual es la causa de la lentitud o podamos aplazar el proceso para cuando la red esté menos congestionada. En cualquier caso, siempre es muy importante reducir al mínimo indispensable los datos a recuperar.

 

Campos indexados y enumeraciones

Ahora vamos a entrar más en detalle en como implementar una consulta en .Net para intentar optimizarla lo máximo posible. Para ello vamos a tener en cuenta lo que hemos comentado previamente de que los datos pueden estar siendo calculados en el momento de realizar la consulta, con lo que tendremos que evitar sentencias del tipo "SELECT *" y minimizar los datos recuperados, puesto que cada campo que añadamos puede estar generándose en ese instante. Una consulta tipo "SELECT *" es especialmente peligrosa en WMI ya que las clases suelen tener una gran cantidad de campos con lo que por ejemplo una consulta a Win32_Process con un "SELECT *" nos puede retornar una gran cantidad de información del procesador cuando solo queríamos , por ejemplo, la arquitectura.

 

Por otra parte, también quería mencionar que hay algunos proveedores que proporcionan campos optimizados para poder filtrar por ellos, mejorando el rendimiento, con lo cual, dentro de lo posible, debemos siempre consultar la documentación del proveedor para filtrar por ellos en la clausula WHERE. En cualquier caso, un buen filtro siempre mejorará el rendimiento de nuestra consulta pero en el caso de los campos no optimizados es el motor WMI quien se encarga de realizar un filtrado "a posteriori" una vez calculados los datos por el proveedor. Un ejemplo de propiedades optimizadas son Drive y Path de la clase CIM_DataFile.

 

Otra forma de mejorar el rendimiento es reutilizar las conexiones WMI que establezcamos, sobre todo si es contra ordenadores remotos. No obstante, es muy importante no olvidarse de hacer un Dispose de los objetos innecesarios para ahorrar memoria.

 

Por último quería hacer hincapié en que se configuren las opciones de enumeración de una consulta, ya que se pueden conseguir grandes mejoras de rendimiento. Para configurar estas opciones se debe utilizar la clase EnumerationOptions que se asigna a la propiedad Options de la clase ManagementObjectSearcher o bien se le pasa en el constructor. Por defecto, la consulta permite navegar hacia delante y atrás en los datos recuperados pero es muy importante que si lo que deseamos es recuperar datos lo más rápidamente posible, se utilice un modo de enumeración solo hacia delante, solo lectura y semi-síncrona. Para ello estableceremos las propiedades ReturnImmediately a true y Rewindable a false. No obstante, estas configuraciones no suelen reflejarse en una mejora importante a menos que se usen para enumerar una clase con un número de entradas considerable. Además es importante recordar que es incompatible utilizar Rewindable a false y utilizar la propiedad Count. Con el modo semi-síncrono lo que logramos es que la llamada retorne inmediatamente y que los objetos sean recuperados en segundo plano y retornados bajo demanda, una vez se hayan creado.

 

Y ahora por fin algo de código:

  1. EnumerationOptions optionsQuery = new EnumerationOptions();
  2. optionsQuery.DirectRead = true;
  3. optionsQuery.EnumerateDeep = false;
  4. optionsQuery.ReturnImmediately = true;
  5. optionsQuery.Rewindable = false;            
  6. StringBuilder units = new StringBuilder(200); // DriveType 4 is a network drive
  7. WqlObjectQuery query = new WqlObjectQuery("SELECT Name, ProviderName FROM Win32_LogicalDisk WHERE DriveType=4");
  8. using (ManagementObjectSearcher diskSearch = new ManagementObjectSearcher(null, query, optionsQuery))
  9. {
  10.     foreach (ManagementObject disk in diskSearch.Get())
  11.     {
  12.         units.Append(disk["Name"].ToString());
  13.         units.Append("\t");
  14.         units.Append(disk["ProviderName"].ToString());
  15.         units.Append("\n");
  16.     }
  17. }

Cambio de enfoque

A veces el error está en el enfoque, ¿quizás es mejor utilizar eventos que consultas? WMI también nos permite instalar “eventwatchers”. Por ejemplo, el siguiente fragmento informa de desconexiones de la red.

 

  1. static void Test()
  2. {
  3.     ManagementEventWatcher w = null;
  4.     ManagementOperationObserver observer = new ManagementOperationObserver();
  5.     // Bind to local machine
  6.     ManagementScope scope = new ManagementScope("root\\wmi");
  7.     scope.Options.EnablePrivileges = true; //sets required privilege
  8.     try
  9.     {
  10.         WqlEventQuery q = new WqlEventQuery();
  11.         q.EventClassName = "MSNdis_StatusMediaDisConnect";
  12.         w = new ManagementEventWatcher(scope, q);
  13.         w.EventArrived += new EventArrivedEventHandler(MediaEventArrived);
  14.         w.Start();
  15.         Console.ReadLine(); // block main thread for test purposes
  16.     }
  17.     catch (Exception e)
  18.     {
  19.         Console.WriteLine(e.Message);
  20.     }
  21.  
  22.     finally
  23.     {
  24.         w.Stop();
  25.     }
  26. }
  27.  
  28. static void MediaEventArrived(object sender, EventArrivedEventArgs e)
  29. {
  30.     Console.WriteLine("Event arrived");
  31.     Console.WriteLine(e.NewEvent.Properties["InstanceName"].Value);
  32.     //Get the Event object and display it
  33.     Console.WriteLine(Convert.ToBoolean(e.NewEvent.Properties["Active"].Value) ? "Active" : "Inactive");
  34. }

 

Conclusión

Así pues en conclusión los puntos que tenemos que revisar principalmente son:

  • Evitar "SELECT *"

  • Usar campos indexados en la WHERE

  • Configurar EnumerationSettings

  • Tener especial cuidado con las consultas sobre la red

  • Cachear los resultados

  • Analizar si el enfoque utilizado para recuperar información es el correcto

Más info en:

Secrets of Windows Management Instrumentation - Troubleshooting and Tips

Optimizing WMI query performances - avoid the nasty ‘select *’

How to make forward-only, read-only WMI queries in C#?

Calling a Method - Semisynchronous