PowerShell-Komponente im Eigenbau

Der designierte Nachfolger der berühmt-berüchtigten DOS-Box ist die Microsoft PowerShell. Selbst zu großen Teilen auf .NET-Code aufbauend ist diese Shell eine ideale Spielwiese bzw. inwzischen auch ein ernsthaftes Arbeitsfeld für System-Entwickler und ambitionierte Admins. codingfreaks zeigt anhand eines Beispieles, wie sich das Gesamtsystem nutzen und anpassen lässt.

Vorbemerkungen zu PowerShell

Dass die gute alte DOS-Box keinen Vergleich mehr zu Konsolen anderer Betriebssysteme besteht, ist beileibe keine Neuigkeit mehr. Interessant war nur die Frage, was Microsoft wohl dagegen unternehmen würde. Mit der ersten Preview der PowerShell vor fast drei Jahren war dann klar, dass MS mit einem vor allem auf Erweiterbarkeit fußenden Konzept auftruumpfen wollte. Inzwischen ist bereits Version 2.0 verfügbar (1.0 reicht aber erstmal aus und ist als stable markiert) und mit der neuen Admin-Plattform „Aspen“ plant MS, die PowerShell als Primär-Kommandozeile in Windows einzuführen und damit DOS-Box und MMC gleichzeitig abzulösen.

Für uns als .NET-Entwickler sind dies gute Neuigkeiten, weil die Befehle der PowerShell (sog. CmdLets = Commandlets) in .NET geschrieben werden und somit die Zeiten der Batch-Programmierung vorbei sein können, wenn man denn nur will. Man schreibt seine eigenen Befehle in C#, VB.NET oder sonst einer managed Sprache, kompiliert und installiert das Ergebnis in der PowerShell und kann sich das erstellen von *.bat-Dateien und Process.Start-Konstrukten in Zukunft schenken.

Noch ist die PowerShell nicht komplett im Windows-System angekommen, aber das ist nur ein Grund mehr, sich jetzt damit zu befassen.

Vorraussetzungen schaffen

Eine Standard-Installation von Windows/Visual Studio reicht leider nicht aus, um sofort mit der Entwicklung der Komponenten beginnen zu können. Zunächst ist es notwendig, die PowerShell selbst zu installieren. Die Setup-Komponenten können über folgende Links bezogen werden:

Die beiden fett markierten Elemente sind für dieses HowTo notwenig, die anderen auf jeden Fall empfehlenswert.

Als guten Einstieg in die Thematik PowerShell bieten sich außerdem folgende Links an:

bezogen werden.

Nach der Installation der Kern-Komponenten steht im Windows-Start-Menu ein neuer Eintrag „Windows Power Shell“ zur Verfügung. Startet man diesen, erscheint ein auf den ersten Blick enttäuchendes Fenster, das der DOS-Box sehr ähnelt (es ist halt auch „nur“ eine Konsole!) und blau hinterlegt ist. Je nach Installation kann es durchaus vorkommen, dass bereits beim Starten rot geschriebene Fehlermeldungen erscheinen, was auf falsche Installationen hinweist, das ist aber meist nicht weiter tragisch und ein beherztes „cls“ :-) schafft wieder Ordnung auf dem Schirm.

CmdLet-Basics

CmdLets sind das Herzstück der PowerShell und sozusagen die neuen Befehle. Neu auch deshalb, weil sie endlich das sog. Piping voll unterstützen, d.h., dass ein CmdLet die Ergebnisse seiner Arbeit nicht anzeigt, sondern per angehängtem „|“ an ein weiteres CmdLet zur Verarbeitung übergibt. Diese Kette kann man beliebig lang gestalten:

get-process | where { $_.WS -gt 10MB }

get-process ist ein CmdLet, das eine Liste aller laufenden Prozesse erstellt und normalerweise ausgibt. Wir übergeben im Beispiel die Liste aber zunächst an die where-Funktion und bestimmen, dass die Eigenschaft „WS“ jedes Prozess-Objektes darauf geprüft werden soll (WS = working space), ob sie größer als (-gt) 10 MByte ist. Nur solche Prozesse werden dann im Ausgabefenster angezeigt.

Wichtig ist hier für uns eigentlich nur folgendes:

  • CmdLets gehorchen einer Namenskonvention. Microsoft hat festgelegt, dass der Name eines CmdLets sets mit einem Verb beginnt und nach einem Bindestrich dann ein Substantiv folgt, das den Objekttyp definiert, auf den die Aktion des Verbs ausgeführt werden soll. Diese Konvention sollte beim Schreiben eigener CmdLets unbedingt eingehalten werden!
  • CmdLets arbeiten mit Objekten. Bevor ein CmdLet etwas in die Ausgabe schreibt, hantiert es tatsächlich mit Objektinstanzen, wie .NET-Programme auch. Dies erleichtert es .NET-Programmierern ungemein, sich in die Welt der CmdLets einzuarbeiten.
  • Über Pipes lassen sich Objekte und Objektmengen an andere CmdLets übergeben.

Das Beispiel-CmdLet

Bei der Suche nach bereits vorhandenen CmdLets fiel uns auf, dass es ein ganz spezielles noch nicht zu geben scheint. Es soll die Aufgabe erfüllen, einen Netzwerk-Rechner per Wake-On-LAN (WOL) einzuschalten. Diese Operation wird mit sog. magic packets durchgeführt. Ist ein PC so konfiguriert, dass er im Standby auf solche Pakete reagiert (WOL im BIOS auf Enabled), sollte er sch bei Zusendung eines magic packets einschalten. Damit ein solcher Rechner „weiß“, dass er gemeint ist, wird dem magic packet die MAC-Adresse des Netzwerk-Adapters mitgegeben, der aufwachen soll.

Ziel ist es also, ein .NET-Programm zu schreiben, dass diese Funktionalität erfüllt und dieses Programm dann als PowerShell-CmdLet zur Verfügung zu stellen.

Programmierung

Der Funktions-Kern

Als waschechte .NET-Freaks wollen wir die Anwendung gleich richtig entwickeln und bauen sie mehrschichtig auf. Schicht 1 soll unsere Anwendungs-Logik werden, also letztlich die Logik zum Erzeugen und Senden des magic packets. Schicht 2 soll das Frontend darstellen, also entweder ein Konsolenprogramm oder eben ein CmdLet.

Als Projekttyp für die Logik-Schicht kommt also nur eine Klassenbibliothek in Frage. Diese enthält eigentlich nur 2 sehr simple Klassen. Die WOLClient-Klasse ist eine Ableitung der Klasse UdpClient, die um die Fähigkeit erweitert wurde, Broadcasts zu senden. Dies ist notwendig, weil man beim Verschicken des magic packet nicht weiß, an welche IP-Adresse das Paket gehen soll und es daher im gesamten lokalen Netz „flutet“. Die entsprechende Adresse laut IPv4 dafür ist 255.255.255.255.

Diese WOLClient-Klasse wiederum wird durch die Klasse WOL genutzt:

/// <summary>
/// Encapsulates logic of sending magic packets.
/// </summary>
public class WOL
{
    /// <summary>
    /// Uses the WOLClass class to generate a UDP
    /// </summary>
    /// <param name="strMAC"></param>
    public void WakeFunction(string strMAC)
    {
        WOLClass wolClient = new WOLClass();
        //255.255.255.255 = broadcast in current subnet
        wolClient.Connect(new IPAddress(0xffffffff),
                          12287);
        wolClient.SetClientToBrodcastMode();
        //set sending bites
        int intCounter = 0;
        //buffer to be send
        byte[] abytData=new byte[1024];
        //first 6 bytes should be 0xFF
        for(int i = 0; i < 6; i++)
            abytData[intCounter++] = 0xFF;
        //now repeate MAC 16 times
        for(int i = 0; i < 16; i++)
        {
            int j = 0;
            for(int k = 0; k < 6; k++)
            {
                abytData[intCounter++]= byte.Parse(strMAC.Substring(j, 2),
                                                   NumberStyles.HexNumber);
                j+=2;
            }
        }
        //now send wake up packet
        int intRet = wolClient.Send(abytData, 1024);
    }
}

Dieser Code ist dem codeproject-Artikel „Wake On Lan Sample for C#“ von maxburow direkt entlehnt und kein so großes Geheimnis :-).

Kommandozeilen-Test

Um gerade den Anfängern einen einfacheren Einstieg zu bieten, möchten wir zunächst das Projekt für erste Tests der Basis-Funktionalität vorstellen. Der Solution wird also ein Projekt vom Typ Konsolenanwendung hinzugefügt. Die einzigen beiden Änderungen sind das Hinzufügen eines Projektverweises auf die Logik-Schicht sowie folgende Anpassungen an der Main-Methode:

static void Main(string[] args)
{
    Console.WriteLine("codingfreaks WOL");
    if (args.Length == 0)
    {
        Console.WriteLine("Usage: wol01.exe [MAC-address]");
    }
    string strAddress = args[0];
    Console.WriteLine("Sending packet to MAC {0} ...", strAddress);
    WOL wolThis = new WOL();
    wolThis.WakeFunction(strAddress);
    Console.WriteLine("Packet sent ... Hit any key");
    Console.ReadKey();
}

Zeile 8 liest die MAC-Adresse aus dem Eingabestrom und Zeile 10 erstellt eine Instanz unseres Logik-Objektes, um es in Zeile 11 einfach aufzurufen. Das wars auch schon.

Ans Eingemachte: Das CmdLet

ps01_01Um nun diese Funktionalität außer in einer Konsolenanwendung, die ja die alte DOS-Welt repräsentiert, auch in einem PowerShell CmdLet anzubieten, sollten zunächst die PowerShell-Projekt-Templates für das Visual Studio (siehe Link oben) installiert werden. Nachdem dies geschehen ist, stellt das VS eine neue Projekt-Kategorie „Windows Power Shell“ bereit (Screenshot rechts).

Diese Kategorie bietet nur ein Projekt-Template an. Wählt man dieses aus, wird ein Projektgerüst erstellt, auf dem man bei seinen ersten Gehversuchen mit den CmdLets sehr gut aufbauen kann.

PowerShell-Projekte bringen standardmäßig Verweise auf System.Management.Automation mit. Dieser Namespace steht erst nach Installation von Windows Power Shell im GAC bereit!

Als einziges wirkliches Item im Projektbaum findet man zunächst nur eine Datei namens PSSnapin.cs. Diese Datei hat lediglich deklarative Aufgaben und soll Informationen aufnehmen, die das CmdLet innerhalb der PowerShell identifizieren und beschreiben. Viele werden sich hier an die Windows-Dienst-Entwicklung erinnert fühlen und bei näherem Nachdenken ist das auch logisch. Genau wie bei den Diensten wird ein CmdLet später in einer Umgebung gehostet. CmdLets in der PowerShell, Dienste halt über svchost.exe. Egal wie, jedem Host muss irgendwie mitgeteilt werden, dass und wie er etwas hosten soll. Und genau wie bei den Diensten auch, wird später das CmdLet per installutil.exe in der PowerShell installiert und deinstalliert. Aber dazu später mehr.

Das CmdLet hinzufügen

Der erste Schritt sollte das Hinzufügen einer Klasse sein, die die Logik das CmdLets implementiert. Auch hierbei hilft das VS nach Installation der Projektvorlagen. Im Bereich „Neues Element hinzufügen“ findet man wieder unterhalb des Baumes „Windows Power Shell“ den Eintrag „Windows Power Shell CmdLet“. Daneben existiert noch ein Element „Windows Power Shell PSCmdLet“. PSCmdLet ist eine Spezialisierung der Klasse CmdLet. Sie erlaubt weitergehenden Zugriff auf die Umgebung der PowerShell. Man kann mit ihr Skripte starten, die aktuelle Sitzung auslesen und beinflussen usw. Für unsere ersten Gehversuche sollte CmdLet aber völlig ausreichen, sodass wir dem Projekt ein Objekt dieses Typs hinzugefügt haben.

Ins Auge fällt jedem sofort das Attribut über der Klasse. Dieses CmdLetAttribute ist enorm wichtig für die spätere Arbeitsweise des CmdLets. Getreu den Designrichtiglinien von Microsoft legt es z.B. das zu verwendende Verb fest, das Substantiv und eine Option, ob die ShouldProcess-Methode unterstützt wird, die mit der PowerShell 2.0 Einzug gehalten hat.

Bereits der erste Parameter des Attributes ist enorm wichtig und erklärungsbedürftig. Es geht also darum, zu definieren, was für eine Art von CmdLet man hier eigentlich entwirft. Möchte man dem Benutzer Ergebnisse zurück liefern (Get), etwas kopieren lassen (Copy) oder etwas zu erstellen (New)? All diese Optionen bietet die standardmäßig verwendet VerbsCommon-Enumeration. Daneben existieren aber noch andere verwendbare Enumerationen. Folgende Auflistung soll bei diesem Thema ein wenig helfen:

VerbsCommon VerbsCommunications VerbsData VerbsDiagnostic VerbsLifecycle VerbsOther VerbsSecurity
Add Connect Backup Debug Disable Use Block
Clear Disconnect Checkpoint Measure Enable Grant
Copy Read Compare Ping Install Revoke
Get Receive Convert Resolve Restart Unblock
Join Send ConvertFrom Test Resume
Lock Write ConvertTo Trace Start
Move Dismount Stop
New Export Suspend
Remove Import Uninstall
Rename Initialize
Select Limit
Set Merge
Split Mount
Unlock Out
Restore
Update

Insgesamt 56 Verben können also derzeit über diese Enumerationen spezifiziert werden. Das sollte zwar vorerst reichen, doch gerade der Bereich VerbsOther könnte in Zukunft Erweiterungen erfahren.

Nachdem das richtige Verb ausgewählt ist (in unserem Fall VerbsCommunications.Send), sollte dem zweiten Parameter ein möglichst prägnanter Name übergeben werden. In unserem Beispiel ist dies „MagicPacket“, da ein solches versendet werden soll:

[Cmdlet(VerbsCommunications.Send, "MagicPacket", SupportsShouldProcess = true)]
public class SendMagicPacket : Cmdlet
{
	...
}

Der Klassenname selbst spielt hier eine untergeordnete Rolle, da er in der PowerShell selbst nicht auftauchen wird.

Viele CmdLets (wie unseres auch) benötigen zum Funktionieren Eingabeparameter. Diese müssen dem Benutzer natürlich bekannt gemacht werden. Ein erster Punkt, dies zu erledigen ist die Definition von öffentlichen Eigenschaften, die ein Parameter-Attribut bekommen. Unser CmdLet soll eine MAC-Adresse entgegen nehmen, also brauchen wir eine entsprechende Property:

/// <summary>
/// The MAC address of the NIC which should receive the magic packet.
/// </summary>
[Parameter(Position = 0,
           Mandatory = false,
           ValueFromPipelineByPropertyName = true,
           HelpMessage = "The MAC address of the NIC which should receive the magic packet."),
 Alias("MAC")]]
[ValidateNotNullOrEmpty]
public string MACAddress
{
    get { return m_strMAC; }
    set { m_strMAC = value; }
}

Der Parameter wird für die Verwendung im Visual Studio wie immer mit einer übergeordneten Beschreibung versehen. Entscheidend ist allerdings alles innerhalb des [Parameter…]-Blocks. Es wird definiert, dass der Parameter an erster Stelle erscheint. Mandantory bestimmt, ob das CmdLet durch eine Eingabeaufforderung nach dem Paramterwert fragen soll (true) oder nicht (false).

ValueFromPipelineByPropertyName stellt sicher, dass, wenn dieses CmdLet von einem anderen per Pipeline aufgerufen wird, dessen Ergebnis-Objekt nach einer Eigenschaft „MACAddress“ oder „MAC“ durchsucht wird. Wird eine solche Eigenschaft gefunden, wird sie als Eingabeparameter verwendet.

Das Alias-Attribut erlaubt es, beliebig viele Aliase für den Parameternamen zu verwenden. So, wie wir unser CmdLet deklariert haben, können wir es mit folgenden Schreibweisen aufrufen:

Send-MagicPacket 123456789012
Send-MagicPacket -MACAddress 123456789012
Send-MagicPacket -MAC 123456789012

Das Flag ValidateNotNullOrEmpty stellt sicher, dass das CmdLet einen Fehler wirft, wenn der MAC-Parameter nicht übergeben wird.

Der Rest der Klasse besteht nun noch aus der Funktion an sich:

/// <summary>
/// The function of the CmdLet.
/// </summary>
protected override void ProcessRecord()
{
    try
    {
        if (!string.IsNullOrEmpty(m_strMAC))
        {
            if (m_strMAC.Length == 12)
            {
                // MAC is given
                WriteProgress(new ProgressRecord(1, "Sending magic packet", "Starting"));
                WOL wolThis = new WOL();
                wolThis.WakeFunction(m_strMAC);
                WriteProgress(new ProgressRecord(1, "Sending magic packet", "Finished"));
            }
            else
            {
                // error during sending
                WriteError(new ErrorRecord(new ApplicationException("MAC-address has to be 12 digits long!"),
                                           "ERR03",
                                           ErrorCategory.InvalidArgument,
                                           null));
            }
        }
        else
        {
            // error during sending
            WriteError(new ErrorRecord(new ApplicationException("MAC-address not specified"),
                                       "ERR02",
                                       ErrorCategory.InvalidArgument,
                                       null));
        }
    }
    catch (Exception excThis)
    {
        // unknown exception
        WriteError(new ErrorRecord(excThis,
                                   "ERR01",
                                   ErrorCategory.NotSpecified,
                                   null));
    }
}

Die Methode zum Senden des Paketes hatten wir ja schon behandelt. Neu ist hier nun, dass Protokoll- und Fehlermeldungen erzeugt werden. Die Methoden WriteProgress() und WriteError() sind Bestandteil der CmdLet-Basisklasse und kümmern sich über die Parameter um das entsprechende Ausgeben von Protokoll- und Fehlermeldungen.

Verteilen und Nutzen

Das Deployment von CmdLets ist ein wenig hakelig aber bei etwas Übung reine Nervensache :-). Zunächst muss eine PowerShell geöffnet werden. In ihr sind dann folgende Schritte notwendig:

Schritt 1 Erstellen eines Aliases für den Befehl installutil

Installutil.exe ist das Programm, das benötigt wird, um das CmdLet in die PowerShell zu integrieren. Da es i.d.R. an einem eher schlecht zugänglichen Dateiort versteckt ist, werden wir einen Alias setzen, um nicht immer den vollen Pfad angeben zu müssen. Die Datei findet man immer im Ordner „{WINDIR}\Microsoft.NET\Framework\v2.0.50727\„. Man sollte sie bei Bedarf ruhig in den Release-Ordner kopieren. Innerhalb der PowerShell ist der erste Befehl dann:

1
set-alias installutil $env:windir\Microsoft.NET\Framework\v2.0.50727\installutil

Wir haben hier gleich eine Version verwendet, die die PowerShell-Systemvariable $env:windir verwendet, um das Windows-Verzeichnis zu ermitteln.

Schritt 2 Installieren des CmdLets in der PowerShell

Ein Aufruf von

1
installutil {ReleaseOrdner}\CmdLet.dll

macht das CmdLet der PowerShell bekannt. Man kann den Erfolg dieses Befehles mit

1
get-PSsnapin -registered

überprüen. Das CmdLet sollte hier erscheinen. Achten Sie darauf, dass das CmdLet mit dem Namen der DLL angezeigt wird. In unserem Sample ist dies WOLCmdLet.

Schritt 3 CmdLet registrieren

Die PowerShell „weiß“ zwar bereits von dem CmdLet, es wurde aber dem Befehlsraum noch nicht hinzugefügt. Dies erledigt

1
add-pssnapin WOLCmdLet
Schritt 4 (OPTIONAL) PowerShell-Konsole speichern

Wer, wie ich, spätestens nach dem 13. mal keine Lust mehr auf diesen Schreibkram hat, der kann über

1
export-console ConsoleWithWOL

Eine Datei *.pscd1-Datei im Ordner Eigene Dateien erzeugen, die Schritt 3 zukünftig selbstständig durchführt.

Ab sofort steht einem die Welt der Magic Packets offen und ein beherztes

Send-MagicPacket {MAC}

führt die gewünschte Funktion aus (Groß- und Kleinschreibung sind übrigens Nebensache in der PowerShell).

Dokumentation

Dem Sample-Projekt liegt eine Datei mit Namen „WOLCmdLet.dll-Help.xml“ bei. Diese XML-Datei enthält eine Dokumentations-Struktur, die von der PowerShell interpretiert werden kann. Jedes CmdLet kann mit

Get-Help {CmdLet-Name}

auf Herz und Nieren geprüft werden. Dem Get-Help-CmdLet können die Parameter „-detailed“ oder „-full“ angehängt werden.

Damit die PowerShell hier etwas anzeigen kann, muss man ihr die entsprechende „-Help.xml“-Datei im Verteilungsordner zur Verfügung stellen. Diese können auch internationalisiert verteilt werden (Unterverzeichnisse pro Locale-ID erstellen). Im Link-Abschnitt am Anfang des HowTos findet sich ein Verweis auf den „CmdLet Help Editor“. Er erlaubt das komfortable Eingeben der Parameter. Dazu sollte das CmdLet mindestens einmal der lokalen PowerShell hinzugefügt worden sein, damit man es innerhalb des Programmes als Ziel-Objekt auswählen kann.

Resumé

Wir sehen keinen erkennbaren Grund, der alten DOS-Welt auch nur eine Träne nachzuweinen. CmdLets bieten mit .NET im Rücken einen mächtigen Funktionsumfang. Dieses HowTo konnte nur einen kleinen Teil der PowerShell-Welt abdecken. SQL Server und Exchange Server machen mit eigenen sog. PowerShell-Providern vor, was man noch so alles veranstalten kann und wahrscheinlich auch sollte. All das wird allerdings auch in Zukunft vornehmlich von einigen Spezialisten genutzt werden, die ständig wiederkehrende Aufgaben in einer Shell bequem automatisieren möchten. Die große Masse wird wohl noch lange brauchen, um sich mit dem CmdLet-Konzept anzufreunden.

Schreibe einen Kommentar

Deine E-Mail-Adresse wird nicht veröffentlicht. Erforderliche Felder sind mit * markiert.