mercredi 21 mai 2008

PowerShell et ASP.NET Part 2

Rappel : le code source du site présenté est disponible ici.

Vous avez besoins des éléments suivants pour l'utiliser :

WebDevelopper Express Edition

Ajax controls toolkit et library

Voyons maintenant un peu plus dans le détail l’architecture de ce site web et les différents blocs de code.

WEB.CONFIG

Commençons par le plus simple : le fichier web.config de notre projet.

J’ai apporté ici très peu de modifications : notre premier objectif est de rajouter les références des assemblies PowerShell au projet, afin de pouvoir créer un runspace et un pipeline pour exécuter notre code.

<assemblies> 
    <add assembly="System.Core, Version=3.5.0.0, Culture=neutral, PublicKeyToken=B77A5C561934E089"/> 
    <add assembly="System.Web.Extensions, Version=3.5.0.0, Culture=neutral, PublicKeyToken=31BF3856AD364E35"/> 
    <add assembly="System.Data.DataSetExtensions, Version=3.5.0.0, Culture=neutral, PublicKeyToken=B77A5C561934E089"/> 
    <add assembly="System.Xml.Linq, Version=3.5.0.0, Culture=neutral, PublicKeyToken=B77A5C561934E089"/> 
    <add assembly="System.Management, Version=2.0.0.0, Culture=neutral, PublicKeyToken=B03F5F7F11D50A3A"/> 
    <add assembly="System.Management.Automation, Version=1.0.0.0, Culture=neutral, PublicKeyToken=31BF3856AD364E35"/> 
</assemblies>

Ceci va nous permettre de référencer ces dll dans le code-behind. Bien, passons maintenant a quelque chose de plus consistant : voyons notre interface utilisateur !

DEFAULT.ASPX

Vous pouvez faire votre design en utilisant simplement du glisser/déposer des différents éléments (textbox, combobox, etc…). Cet aspect est assez simple, pour ceux souhaitant apprendre les bases de la création d’interface ASP.NET, je vous propose la lecture des excellents tutos de Didier Danse sur developpez.com, notamment les 2 premiers tomes.

Hormis ces aspects classiques de création de formulaire, voici les particularités de notre site web pour PowerShell :

UpdatePanel

Dans la première partie de ce tutorial, je vous est indiqué que nous utilisons un peu d’Ajax pour :

· Gérer un Timer
· Gérer le rafraichissement de la Textbox de sortie

Pour pouvoir utiliser les extensions AJAX dans notre page web, il faut comme pré-requis tout d’abord placer un ScriptManager dans le document (qui va permettre la prise en charge des autres contrôles AJAX)

Je l’ai placé en début de form, classiquement :

<body>
    <form id="form1" runat="server">
    <asp:ScriptManager ID="ScriptManager1" runat="server">
    </asp:ScriptManager>

Passons à la suite, en faisant abstraction des petites fioritures en haut de la page, le gros de notre interface est ici :

<asp:TextBox ID="TxtPowerShellScript" runat="server" TextMode="MultiLine" Width="597px"
            Height="194px" BackColor="#012456" ForeColor="#EEEDF0" Wrap="False"></asp:TextBox>
        <br />
        <asp:UpdatePanel ID="UpdatePanel1" runat="server">
            <ContentTemplate>
                <asp:Button ID="BtnExecuteScript" runat="server" Text="Launch Script" OnClick="BtnExecuteScript_Click" />
                <br />
                Output :
                <br />
                <asp:TextBox ID="TxtResult" runat="server" Height="199px" TextMode="MultiLine" Width="600px"
                    Wrap="False" BackColor="#012456" ForeColor="#EEEDF0"></asp:TextBox>
                <br />
                <asp:Timer ID="Timer1" runat="server" Enabled="False" Interval="100" OnTick="Timer1_Tick" />
            </ContentTemplate>
        </asp:UpdatePanel>
Le premier élément est notre TextBox pour taper le script :

<asp:TextBox ID="TxtPowerShellScript" runat="server" TextMode="MultiLine" Width="597px"
          Height="194px" BackColor="#012456" ForeColor="#EEEDF0" Wrap="False"></asp:TextBox>

Rien de particulier ici, je l’ai nommé “TxtPowerShellScript” pour respecter un peu de normalisation, je l’ai définit en multiline et mis des couleurs proches de la console PowerShell pour le fun.

Ensuite, vous pouvez voir que mon bouton et la deuxième TextBox sont encapsulés dans un contrôle UpdatePanel. C’est ceci qui me permet de ne rafraichir que le contenu encapsulé et non la page entière.

On y retrouve aussi notre Timer :

<
asp:Timer ID="Timer1" runat="server" Enabled="False" Interval="100" OnTick="Timer1_Tick" />




L’intervalle (Interval="100") defini le temps de rafraichissement du timer (en milliseconde), et le OnTick (OnTick="Timer1_Tick") la fonction appelée dans le Code-Behind. J’ai désactivé le Timer par défaut (Enabled="False")afin de ne pas faire de charge inutile. Il sera activé qu’au click sur notre bouton.

Voilà pour l’interface. Vous voyez qu’hormis la question de l’UpdatePanel (qui est après tout un contrôle comme les autres), nous ne sortons résolument pas des sentiers battus. Voyons maintenant ce qui se passe en coulisse dans notre Code-Behind, qui est le gros du morceau.

DEFAULT.ASPX.CS

Nous arrivons maintenant dans le code C# qui va gérer notre invocation de PowerShell et le rafraichissement de la TextBox de sortie.

Pour commencer par le commencement, vous pouvez voir en début de code que j’appel en plus des instances par défaut du site asp.net généré par Visual 3 dll :


using System.Management.Automation;

using System.Management.Automation.Runspaces;

using System.IO;


System.Management.Automation et .Runspaces sont nécessaires pour faire créer le Runspace et le Pipeline. System.IO va nous servir pour la conversion du contenu de la TextBox de script comme nous allons le voir plus loin.

Passons maintenant au code proprement dit. Nous allons tout d’abord créer les objets Runspace et Pipeline pour qu’il soit exposé sur l’ensemble de notre classe.

Runspace runspace = RunspaceFactory.CreateRunspace();
Pipeline pipe;

Nous allons maintenant analyser le code en partant de l’action de l’utilisateur : le click sur le bouton. La fonction (que vous trouvez ligne 85) est la suivante :

protected void BtnExecuteScript_Click(object sender, EventArgs e)
{
    string strCurrentId = System.Security.Principal.WindowsIdentity.GetCurrent().Name;
    // Enable timer and disable button, clear TxtResult textbox
    this.Timer1.Enabled = true;
    this.BtnExecuteScript.Enabled = false;
    this.TxtResult.Text = "";

    // put the username at the beginning of the output (optional)
    Session["PowerTrace"] = "Initiateur de la demande : " + strCurrentID + "\r\n";

     // Gather script from the TxtPowerShellScript and convert it from html to clean text
    // then call executePowerShellCode function with the result
      string strContent = TxtPowerShellScript.Text;
    StringWriter writer = new StringWriter();
     Server.HtmlDecode(strContent, writer);
    this.executePowerShellCode(writer.ToString());
}

Voyons un peu ce qu’il se passe. Pour commencer, je crée une string strCurrentID qui va récupérer le nom de l’utilisateur en cours. Ceci n’est pas obligatoire dans notre exemple, mais nous verrons par la suite qu’il est intéressant pour nous de connaitre cette information. En effet, dans le cadre du déploiement de ce site ASP.NET, c’est un compte de service qui sera utilisé pour exécuter le code côté serveur.

Or, comme nous souhaitons (si ce n’est pas le cas, vous devriez !) tracer l’activité du site, il est important de connaitre l’utilisateur à l’origine de l’action, histoire par exemple de savoir à qui tirer les oreilles en cas d’utilisation abusive de notre belle page.

System.Security.Principal.WindowsIdentity.GetCurrent().Name nous donne cette information en récupérant le Login de l’utilisateur.

Ensuite, nous allons activer le Timer :

this.Timer1.Enabled = true;



Désactiver le bouton (pour éviter le lancement par erreur du script alors qu’un script est déjà en exécution)

this.BtnExecuteScript.Enabled = false;


et vider la TextBox de sortie :

this.TxtResult.Text = "";



Ensuite nous initierions notre fameuse variable de Session que j’ai appelé « PowerTrace » avec le nom de la personne exécutant la commande

Session["PowerTrace"] = "Initiateur de la demande : " + strCurrentID + "\r\n";


Un peu d’explication s’impose sur ce choix. Toute la subtilité ici est de pouvoir transmettre à notre page asp.net ce que récupère le Code-Behind quand nous rafraichissons la page. Comme les 2 mondes sont isolés (chacun s’exécute dans son contexte : le page web côté client et le code-behind côté serveur), il nous faut trouver un moyen pour transmettre des informations de l’un à l’autre (ici en l’occurrence, passer la sortie de notre script à la TextBox).


Il existe plusieurs solutions plus ou moins élégantes (créer un fichier texte temporaire, rajouter la sortie dans l’URL…) mais la variable de session est dans notre contexte la meilleure des méthodes. Cela permet de mettre à disposition des informations lisible du côté de l’interface qui se conserve pendant l’ensemble de la session de l’utilisateur. Sinon, par défaut, au rafraîchissement toutes les informations récoltées côté code-behind disparaîtraient. Comme notre sortie de scripts ne prendra jamais une place démesurée en mémoire et qu’il s’agit d’une utilisation en intranet relativement restreinte, c’est ici un bon choix facile à mettre en place.

Le contenu de cette variable est donc exploitable pendant toute la durée de la session de l’utilisateur. Ceci étant fait, nous allons maintenant récupérer le contenu de la TextBox contenant le script :

string strContent = TxtPowerShellScript.Text;



Problème ici : nous sommes dans un contexte HTML, le contenu de cette TextBox est donc au format HTML. Ceci ne nous permet pas d’exécuter le code tels que, car des informations se glissent dans le texte (retour chariot notamment, qui se font avec la syntaxe \r\n). Le code est donc inutilisable par PowerShell tels quel.

Heureusement pour nous, il existe une méthode qui nous permet de décoder l’HTML pour obtenir du texte classique : HtmlDecode

C’est elle que j’utilise ici : je créé une instance d’un StringWriter (qui permet de créer une string), et j’utilise cette méthode pour inscrire le texte « décodé » dans cette string.

StringWriter writer = new StringWriter()
Server.HtmlDecode(strContent, writer);



Je fais appel ensuite à ma fonction d’exécution de code PowerShell avec le résultat :

this.executePowerShellCode(writer.ToString());


Passons donc à l’analyse de cette fonction « executePowerShellCode »

private void executePowerShellCode(string code)
{
    runspace.Open();
    pipe = runspace.CreatePipeline(code);
    pipe.Input.Close();

    // Call output_DataReady when data arrived in the pipe
    pipe.Output.DataReady += new EventHandler(Output_DataReady);
    // Call pipe_StateChanged 
    pipe.StateChanged += new EventHandler<PipelineStateEventArgs>(pipe_StateChanged);
    pipe.InvokeAsync();
}

Je n’est pas réinventé la roue, j’utilise la méthode la plus connue que vous trouverez dans pas mal d’exemple sur le net :

Je commence par ouvrir le runspace

runspace.Open();



Ensuite je créé un pipeline dans ce runspace avec le code retourné précédemment

pipe = runspace.CreatePipeline(code);



Je ferme le pipeline en écriture

pipe.Input.Close();



Ensuite nous créons un gestionnaire d’évènement, qui va tout simplement appeler la fonction « Output_DataReady » quand quelque chose se présente dans la sortie du pipeline

pipe.Output.DataReady += new EventHandler(Output_DataReady);

Nous créons ensuite un autre gestionnaire d’évènement, qui lui va appelé la fonction “pipe_StateChanged” quand le pipeline change d’état. En l’occurrence ici, nous allons tester si l’état du pipeline est « Terminé », qui signifie la fin du script et donc des actions à effectuer tels que la réactivation du bouton Executer comme nous le verrons plus loin.

Ensuite nous appelons la méthode « InvokeAsync() » qui nous permet d’exécuter le pipeline en asynchrone.

pipe.InvokeAsync();


Ok maintenant, notre code est en attente d’activité côté pipeline. Voyons a présent ce qu’il se passe quand des données arrivent dans la sortie du pipeline avec notre fonction Output_DataReady


void Output_DataReady(object sender, EventArgs e)
{
    PipelineReader<PSObject> reader = (PipelineReader<PSObject>)sender;
    String strPowershellTrace = reader.Read().ToString();
    Session["PowerTrace"] += strPowershellTrace + "\r\n";
}

Ici nous allons lire la sortie du pipeline que nous inscrivons dans l’objet reader :

PipelineReader<PSObject> reader = (PipelineReader<PSObject>)sender;



Ensuite, nous définissons une variable de type string qui récupère cette sortie sous forme de… string !

String strPowershellTrace = reader.Read().ToString();


Il ne nous reste plus qu’a rajouter cette variable à notre variable de session :

Session["PowerTrace"] += strPowershellTrace + "\r\n";



Notez que je rajoute “\r\n” derrière cette variable pour gérer le retour chariot. Vous voyez, c’est plutôt simple à comprendre. Pour finir sur le processus d’exécution de code PowerShell, voyons la dernière fonction qui gère le changement d’état du pipeline :

Ici, je pose une condition sur l’état du pipeline : si il a le statut « Completed », nous allons agir, sinon nous ne faisons rien.

if (pipe.PipelineStateInfo.State == PipelineState.Completed)



Alors, que fait-on quand le statut est Completed ? Premièrement nous allons fermer le runspace

Ensuite, tant que la Variable de Sessions n’est pas nulle et qu’elle contient quelque chose (nombre de caractère superieur à 0), nous n’y touchons pas. Ceci afin de laisser le temps au Timer d’inscrire son contenu dans la TextBox de Sortie. Nous verrons cela tout de suite après en analysant la fonction du Timer.

while ((Session["PowerTrace"] != null) && (Session["PowerTrace"].ToString().Length > 0))
{
}
runspace.Close();

Il ne nous reste plus ensuite qu’à supprimer cette variable de session dans l’attente d’un nouveau script :

Session.Remove("PowerTrace");



Voyons maintenant notre fameux Timer :

protected void Timer1_Tick(object sender, EventArgs e)
    {
        if (Session["PowerTrace"] == null)
        {
            this.BtnExecuteScript.Enabled = true;
            Timer1.Enabled = false;
            this.TxtResult.Text += "Fin du script";
        }
        else
        {
            String strPoshTrace = Session["PowerTrace"].ToString();
              this.TxtResult.Text += strPoshTrace;
            Session["PowerTrace"] = "";
        }
    }
Indépendamment des autres fonctions, celle-ci est appelé par notre Timer à intervalles réguliés (dans notre cas toute les 100 ms, on peut bien sûr élargir un peu ce temps)

Ici nous faisons la chose suivante :

Si la Variable de Session est Null (donc directement après sa suppression par la fonction pipe_StateChanged), nous allons :

· Réactiver le bouton
· Désactiver le Timer
· Inscrire « Fin du script » dans la TextBox de sortie

this.BtnExecuteScript.Enabled = true;
Timer1.Enabled = false;
this.TxtResult.Text += "Fin du script";

Sinon, on récupère le contenu de la variable de session dans une string

String strPoshTrace = Session["PowerTrace"].ToString();



On ajoute celle-ci à la TextBox de sortie

this.TxtResult.Text += strPoshTrace;



et on vide la variable de session de son contenu

Session["PowerTrace"] = "";



Notez que nous ne supprimons pas ici la variable, ce qui fait que notre condition dans la fonction pipe_StateChanged est valide :

while ((Session["PowerTrace"] != null) && (Session["PowerTrace"].ToString().Length > 0))

Voilà pour notre tour du propriétaire de ce premier projet. Nous verrons dans la troisième partie de ce tutorial comment piloter un script à partir d’un formulaire, et comment déployer ce site web sur un serveur IIS avec la sécurité adéquate.

2 commentaires:

pi a dit…

ma petite caille je vois que t'es tjs aussi fort !!! c pascal ex rte.eds.installshield if u remember me !

Antoine Habert a dit…

Bien sûr mon grand, par quel heureux hasard t'es tu rendus sur ce tutorial ? ça y est tu te mets à PowerShell ? ;)