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