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

 

No comments: