PowerShell an IDE

Die PowerShell Editor Services im Überblick
Keine Kommentare

Der noch aus DOS-Zeiten stammende Batch-Interpreter behindert Administratoren. Die von Microsoft als Nachfolger auserkorene PowerShell erfuhr bisher nur wenig Unterstützung. Dieses Projekt möchte Drittanbietern die Integration von PowerShell Intelligence erleichtern, um so die Verfügbarkeit von Tooling zu verbessern.

Sagen wir es offen: Einer der Gründe für den Erfolg von Visual Basic war die damals grenzgeniale Integration zwischen GUI-Design-Werkzeug und Programmierumgebung. Wer einmal eine Symbian-Applikation oder ein Android-Programm zusammengebaut hat, weiß über die Vorteile von Rapid Application Development bestens Bescheid. Ob der vergleichsweise geringen Reichweite der PowerShell – sie ist nun mal nicht sonderlich weit verbreitet – scheuten die Anbieter von Editoren und anderer Entwicklerhilfswerkzeuge bisher den Aufwand für die Integration von PowerShell-Support. Microsoft möchte diesem Problem nun durch eine Untergruppe von Unterstützungsbibliotheken abhelfen.

Die PowerShell Editor Services stehen zwar auch als NuGet-Paket zur Verfügung, werden dort aber nur vergleichsweise selten aktualisiert. Wer mit dem API experimentieren möchte, sollte das bereitstehende GitHub Repository aufrufen und es von dort herunterladen.

Als nächste Aufgabe wollen wir mit der Erzeugung einer Instanz der PowerShellContext-Klasse beginnen. Sie dient als Interface zwischen PowerShell Editor Services (PSES) und der zugrundeliegenden PowerShell-Implementierung. Die folgenden Schritte erfolgen unter Windows 10 und Visual Studio 2015, als Projektvorlage dient WINDOWS | CLASSIC DESKTOP | WPF APPLICATION – laut Kommentaren im GitHub Repository werden die Versionen 3 und 4 der PowerShell manchmal unterstützt. Dazu muss das aus GitHub heruntergeladene Archiv an einen kommoden Ort im Dateisystem extrahiert werden. Für erste Schritte reicht es aus, wenn Sie das Projekt src\PowerShellEditorServices\PowerShellEditorServices.csproj in Ihre Solution laden. Es integriert automatisch eine Gruppe von Unterprojekten. Achten Sie darauf, im Hauptprojekt die notwendigen Referenzen anzulegen.

Nicht unter Universal Windows!

Die im GitHub-Projekt bereitgestellten Assemblies zielen auf .NET Core, während die Universal Windows Platform auf dem .NET Framework basiert. Aus diesem Grund scheitert das Hinzufügen einer Referenz auf den bereitgestellten Code mit einer nur wenig aussagekräftigen Fehlermeldung.

Falls Visual Studio das Anlegen der notwendigen Beziehungen verweigert, liegt das normalerweise an fehlenden Paketen. NuGet könnte diese theoretisch automatisch herstellen, darf dies aber laut den Voreinstellungen in älteren Versionen von Visual Studio nicht. Wechseln Sie dazu in die Einstellungsansicht PACKAGE MANAGER | GENERAL und setzen Sie die Attribute Allow NuGet to download missing packages und Automatically check for missing packages during build in Visual Studio. Sind die betreffenden Checkboxen gesetzt, so müssen Sie zusätzlich die hier beschriebene Prozedur abarbeiten. Im spezifischen muss der versteckte Ordner .nuget gelöscht werden, danach ist eine Anpassung der Datei PowerShellEditorServices.csproj erforderlich. Nach getaner Arbeit präsentiert sich die Projektstruktur wie in Abbildung 1 gezeigt.

Abb. 1: Die Referenzen von PowerShell Editor Services dürfen keine Rufzeichen aufweisen

Abb. 1: Die Referenzen von PowerShell Editor Services dürfen keine Rufzeichen aufweisen

Struktur hoch!

David Wilson baut die von ihm betreuten PowerShell Editor Services nach dem Hub-and-Spoke-Pattern auf: Entwickler erzeugen normalerweise im ersten Schritt eine Instanz der Mutterklasse, um dieser daraufhin einen oder mehrere Unterservices zu entlocken. Fürs Erste wollen wir neben dieser EditorSession auch eine Instanz des ConsoleServices anlegen, der für die Kommunikation zwischen laufendem PSES-Programm und Hintergrundapplikation verantwortlich ist:

public partial class MainWindow : Window
{
  EditorSession mySession;
  ConsoleService myCService;

Im Konstruktor des Hauptformulars erstellen wir im ersten Schritt eine neue Instanz der EditorSession. Diese stellt im Moment einen reinen Platzhalter dar, der vor der Nutzung Verbindung zum Rest der PowerShell aufnehmen muss:

public MainWindow()
{
   InitializeComponent();
   mySession = new EditorSession();

Zur Bevölkerung einer neuen EditorSession sind zwei Parameterobjekte erforderlich. HostDetails liefert Daten zur Arbeitsumgebung – wer alle drei Parameter auf null setzt, bekommt ein normal parametriertes HostDetails-Objekt. ProfilePaths wird von Seiten Microsofts nicht mit Default-Werten ausgestattet, weshalb wir zwei Pfade aus GetFolderPath übergeben:

HostDetails myHD = new HostDetails(null, null, null);
ProfilePaths myP = new ProfilePaths("myHost", Environment.GetFolderPath(Environment.SpecialFolder.CommonDocuments), Environment.GetFolderPath(Environment.SpecialFolder.UserProfile));

Die eigentliche Einrichtung der EditorSession erfolgt dann durch Aufruf von StartSession. Ab diesem Zeitpunkt stellt das Objekt diverse Member bereit, über die Sie auf die eigentlichen Dienste zugreifen können – wir beschaffen einen ConsoleService, der im nächsten Schritt einen Event Handler eingeschrieben bekommt:

mySession.StartSession(myHD, myP);
myCService = mySession.ConsoleService;
myCService.OutputWritten += MyCService_OutputWritten;

Damit müssen wir nur noch ein Kommando an die PowerShell senden. Ein interessanter Anwendungsfall für die PSES ist das Beschaffen von Systeminformationen. Wir wollen hier die Order Get-Service aufrufen, die die PowerShell zum Auswerfen aller auf der Workstation befindlichen Dienste animiert:

myCService.ExecuteCommand("Get-Service", false);
}

Schon aus Gründen der Bequemlichkeit soll die Anzeige in einem Textblock erfolgen, der sich mit minimalem Aufwand im CodeBehind anlegen lässt:

<Grid>
 <TextBlock x:Name="textBlock" HorizontalAlignment="Left" Margin="10,10,0,0" TextWrapping="Wrap" Text="TextBlock" VerticalAlignment="Top" Height="382" Width="599"/>
</Grid>

Der GUI-Stack der WPF wirft uns an dieser Stelle ein kleines Hindernis vor die Füße: Die PowerShell arbeitet normalerweise nicht im Thread der Applikation. Aus diesem Grund wird der Event Handler oft aus einem fremden Thread aufgerufen und kann somit nicht direkt auf die Steuerelemente zugreifen. Zur Lösung dieses Problems bietet sich die Nutzung des Dispatchers an:

private void MyCService_OutputWritten(object sender, OutputWrittenEventArgs e){
  Dispatcher.Invoke(() =>
  {
    textBlock.Text += e.OutputText;
    if (e.IncludeNewLine) textBlock.Text += "\n";
  });
}

Die PowerShell Editor Services helfen Entwicklern bei der Verarbeitung der eingehenden Daten durch das Bereitstellen diverser Flags: IncludeNewLine informiert den Klienten beispielsweise darüber, ob nach dem angelieferten Text ein Carriage Return erforderlich ist.

Damit ist die Arbeit am ersten Programmbeispiel abgeschlossen. Abbildung 2 zeigt, dass alles in bester Ordnung funktioniert.

Abb. 2: Auch virtuelle Maschinen bringen den einen oder anderen Dienst mit

Abb. 2: Auch virtuelle Maschinen bringen den einen oder anderen Dienst mit

Navigieren im Code

IntelliSense ist einer der Gründe für den immensen Erfolg von Visual Studio: Die Intelligenz der Autovorschläge spart Entwicklern Zeit. Leider ist das Entwickeln eines Parsers für eine neue Programmiersprache alles andere als einfach – ob man für seinen Editor ausgerechnet PowerShell-Unterstützung implementiert, ist fraglich.

Die PowerShell Editor Services bieten mit dem Language-Service eine Gruppe von Hilfsklassen an, die bei der Realisierung von Autocomplete und dem Anspringen von Zielen assistieren. Für die folgenden Schritte ist ein Testfile erforderlich, das auf der Workstation des Autors im Verzeichnis C:\Users\TAMHAN\Desktop\psspace unterkommt. Erzeugen Sie dann eine neue Datei namens firsttest.ps1, die mit dem Inhalt aus Listing 1 auszustatten ist.

. .\emptyFirst.ps1
function testThis($howOften)
{
  $i=1;
  while($i -le $howOften)
  {
    Write-Output "This"
  }
}

testThis 30

emptyFirst.ps1 ist ein leeres File, das nur als „Futter“ für den Syntaxparser dient und sonst keine Relevanz aufweist. Der Rest des Programms realisiert eine Funktion, die periodisch aufgerufen wird – der eigentliche Sinn dahinter ist, dass wir in einem weiteren Beispielprogramm einen Breakpoint in das Innere der Schleife setzen.

Zudem ermöglicht uns der zweistufige Aufbau das Suchen der Deklaration der Funktion. Da die PowerShell Editor Services stark zeilen- und spaltenorientiert arbeiten, zeigt Abbildung 3 das Shellskript in der PowerShell ISE.

Abb. 3: Die Zeilenzahlen sind für die korrekte Funktion des Programms wichtig

Abb. 3: Die Zeilenzahlen sind für die korrekte Funktion des Programms wichtig

Zur Arbeit mit dem Language-Service ist eine Helferklasse erforderlich, die die Inhalte des zu diskutierenden Skripts abbildet. Die betreffende ScriptFile-Instanz wird idealerweise als Member des Formulars angelegt:

public partial class MainWindow : Window {
  ScriptFile myFile;

Der Konstruktor der Klasse ist vom Aufbau her etwas seltsam: Neben der Version der in der Session zu verwendenden PowerShell (diese Information ist zur Parametrisierung des Syntaxparsers erforderlich) fragt er auch zwei Pfade ab, die die Skriptdatei beschreiben.

Interessanterweise erfolgt das eigentliche Einlesen der Inhalte nicht durch Dateioperationen: Der Entwickler des Editors kann die Inhalte entweder über einen Textstream oder über einen String anliefern. In beiden Fällen erfolgt das Parsing im Rahmen der Initialisierung (Listing 2).

public MainWindow()
{
  . . .
  StreamReader sr = new StreamReader("C:\\Users\\TAMHAN\\Desktop\\psspace\\firsttest.ps1");
  myFile =new ScriptFile("C:\\Users\\TAMHAN\\Desktop\\psspace\\firsttest.ps1", "C:\\Users\\TAMHAN\\Desktop\\psspace\\firsttest.ps1", sr ,mySession.PowerShellContext.PowerShellVersion);
  myFile = myFile;
}

Fertig geladene ScriptFile-Instanzen stellen eine Vielzahl von Elementen bereit, die Informationen über die geöffnete Datei anbieten. So enthält das Feld ReferencedFiles eine Liste aller Dateien, die aus der jeweiligen Skriptdatei inkludiert werden – das ist zum Beispiel zum Realisieren einer Springe-nach-Funktion geeignet.

ScriptTokens enthält derweil ein Array von Elementen, die zum Beispiel zur Realisierung von Autocomplete hilfreich sind. Achten Sie dabei allerdings darauf, dass das Array vergleichsweise viele leere Elemente enthält und auch Klammern und sonstige Sonderzeichen mitnimmt (Abb. 4).

Abb. 4: Der Inhalt von „ScriptTokens“ könnte etwas Ordnung gut gebrauchen

Abb. 4: Der Inhalt von „ScriptTokens“ könnte etwas Ordnung gut gebrauchen

Während das Laden einer ScriptFile-Datei statische Informationen über das gesamte Skript zeigt, liefert eine Instanz des Language-Service auf Anfrage Informationen über die Elemente, die sich an einer bestimmten Stelle der Datei befinden (Listing 3).

public partial class MainWindow : Window {
  LanguageService myLService;

  public MainWindow() {
    . . .
    myLService = mySession.LanguageService;
    SymbolReference myRef = myLService.FindSymbolAtLocation(myFile, 12, 2);
    myRef = myRef;

Zum Beschaffen der LanguageService-Instanz greifen wir wie gewohnt auf die Session zurück. Das relevante Objekt findet sich in der Property LanguageService. Die Funktion FindSymbolAtLocation übernimmt zwei Integer: Der erste beschreibt die Zeile, der zweite die Spalte des Zielelements.

In der zurückgelieferten SymbolReference-Klasse ist das SymbolType-Attribut von besonderer Relevanz. Es handelt sich dabei um eine Enumeration, die die Art des anvisierten Symbols beschreibt. Zum Zeitpunkt der Drucklegung kennt PSES die folgenden Spielarten:

public enum SymbolType {
  Unknown = 0,
  Variable,
  Function,
  Parameter,
  Configuration,
  Workflow,
}

SymbolReference-Instanzen stellen nur einen Teil der verfügbaren Gesamtinformationen bereit. Der Language Service kann weitere Daten bereitstellen und sogar den Ort der Definition des jeweiligen Elements finden. Das ist immer dann besonders interessant, wenn man eine Springe-zu-Funktion realisieren möchte: IDEs wie Visual Studio sparen ihren Nutzern damit Unmengen von Zeit. Aus technischer Sicht ist die Aufgabe einfach: Übergeben Sie die SymbolReference-Klasse des betreffenden Elements einfach an die Methode GetDefinitionOfSymbol.

public MainWindow() {
  myLService = mySession.LanguageService;
  SymbolReference myRef = myLService.FindSymbolAtLocation(myFile, 12, 2);
  runner(myRef);
}

Aufgrund des potenziell hohen Berechnungsaufwands ist die Nutzung asynchroner Methoden hier verpflichtend. Wir umgehen das durch die Einführung eines Runners, der die von C# gestellten Bedingungen erfüllt. Angemerkt sei, dass es in Tests des Autors kein einziges Mal zu messbaren Verzögerungen kam:

async void runner(SymbolReference myRef) {
  GetDefinitionResult myRes = await myLService.GetDefinitionOfSymbol(myFile, myRef, new Workspace(mySession.PowerShellContext.PowerShellVersion));
  myRes = myRes;
}

Der Lohn der Mühen ist das Zurückliefern eines ScriptRegion-Objekts, das Informationen über die Deklarationsstelle und die Größe des für die Deklaration zuständigen Bereichs anliefert. Eine Reflexion der Klasse ergibt folgende Struktur:

public sealed class ScriptRegion {
  public string File { get; set; }
  public string Text { get; set; }
  public int StartLineNumber { get; set; }
  public int StartColumnNumber { get; set; }
  public int StartOffset { get; set; }
  public int EndLineNumber { get; set; }
  public int EndColumnNumber { get; set; }
  public int EndOffset { get; set; }

Von besonderer Bedeutung ist an dieser Stelle das File– und das StartLine-Attribut: Die beiden Felder beschreiben gemeinsam den Aufenthaltsort der Deklaration des jeweiligen Elements.

Wer mehr über die Möglichkeiten dieses APIs erfahren möchte, kann die bereitstehende Referenz der Klasse konsultieren. Achten Sie allerdings darauf, dass die in der Dokumentation befindlichen Informationen teilweise stark veraltet sind. Immerhin bringt ein Rechtsklick die Option Go to Definition, mit der man die Implementierung auf den Bildschirm holen kann.

Auf der Fehlerjagd

PowerShell-Skripte sind wegen der höheren Leistungsfähigkeit der Runtime komplizierter als ihre klassischen Vorgänger. Das zeigt sich unter anderem darin, dass Microsoft die PowerShell-Laufzeitumgebung mit einem vollständigen Debugger ausstattet, der bei der Suche nach Fehlern in Skripten hilft. Wer den Debugger nutzen möchte, muss die Initialisierung der Session statt durch StartSession durch die Methode StartDebugSession abwickeln. Im Falle unseres Beispiels sähe das so aus:

public MainWindow()
{
  . . .
  mySession.StartDebugSession(myHD, myP);

Im nächsten Schritt muss der Entwickler eine Instanz der DebuggerService-Klasse bereitstellen: Dabei handelt es sich um eine Art Adapter, der für die Kommunikation zwischen Debugger-Engine und Ausführungsumgebung zuständig ist. Achten Sie darauf, dass der DebuggerService selbst keine Beziehung zum ausgeführten Skript hat:

DebugService myDService;

public MainWindow() {
  . . .
  myDService=mySession.DebugService;
  myDService.DebuggerStopped += MyDService_DebuggerStopped;
  runner();
}

Das Anlegen von Breakpoints erfolgt über die Methode SetLineBreakpoints. Sie ist insofern interessant, weil die Definition der als Parameterquelle dienenden BreakpointDetails-Objekte etwas seltsam ist:

async void runner() {
  BreakpointDetails[] myBreakpoints = new BreakpointDetails[1];
  myBreakpoints[0] = BreakpointDetails.Create("Ruhe im Assert!", 8);
  await myDService.SetLineBreakpoints(myFile, myBreakpoints, true); 
  await mySession.PowerShellContext.ExecuteScriptAtPath(myFile.FilePath);
}

Die Erzeugung neuer Breakpoints erfolgt über die Methode BreakpointDetails.Create, die neben einem Integer mit Informationen über den Aufenthalt des Breakpoints auch einen String entgegennimmt. Dieser ist eine reine Beschreibung, der zum Zeitpunkt der Drucklegung nur auf Vorhandensein geprüft wird – das Übergeben eines beliebigen Texts ist also kein Problem.

SetLineBreakpoints kann das Breakpoint-Array auf zwei Arten verarbeiten: Steht die flag auf true, so stellen die übergebenen Breakpoints die Gesamtheit aller in der Datei anzulegenden Breakpoints dar. Wer stattdessen false übergibt, lässt die schon in der Ausführungsumgebung befindlichen Breakpoints unangetastet – das ist zum schnellen Hinzufügen weiterer Unterbrechungspunkte ideal geeignet.

Die eigentliche Ausführung des Skripts erfolgt danach wie gewohnt durch den von der Session zurückgegebenen PowerShell-Kontext. Da diese Methode – analog zu SetLineBreakpoints – asynchron abläuft, brauchen wir auch an dieser Stelle einen Runner().

Falls die Kompilation des Projekts mit einem Verweis auf die Assembly System.Management.Automation fehlschlägt, müssen Sie den NuGet-Paketmanager öffnen und die in Abbildung 5 hervorgehobene Assembly aus dem Repository herunterladen. Achten Sie darauf, nicht versehentlich eine andere Variante zu erwischen – der Compiler scheitert bei allen Assemblies außer System.Management.Automation.dll mit einem Versions-Mismatch.

Abb. 5: Dieses Modul bringt die fehlenden Features mit

Abb. 5: Dieses Modul bringt die fehlenden Features mit

Das Entgegennehmen von Debugger-Ereignissen erfolgt im Event Handler MyDService_DebuggerStopped. Der Debugger-Service dient dabei nur als Schnittstelle zur Ausführungsumgebung – wer die Programmausführung fortsetzen möchte, kann dies durch Aufrufen der Methode Continue() tun:

private void MyDService_DebuggerStopped(object sender, System.Management.Automation.DebuggerStopEventArgs e) {
  myDService.Continue();
}

Das erfolgreiche Funktionieren des Programms lässt sich durch Platzieren eines Breakpoints in MyDService_DebuggerStopped überprüfen – es sind einige Klicks erforderlich, bis das betreffende Formular am Bildschirm erscheint.

Im Debugger ablaufende PowerShell-Skripte sind aber trotzdem gewöhnliche Skripte: Ihre Ausgabe lässt sich wie bei normaler Abarbeitung einsammeln. Der einfachste Weg dazu ist das Nutzen des weiter oben vorgestellten ConsoleServices – übernehmen Sie den Event Handler einfach aus dem ersten Programmbeispiel:

public MainWindow() {
  . . .
  myDService.DebuggerStopped += MyDService_DebuggerStopped;
  mySession.ConsoleService.OutputWritten += MyCService_OutputWritten;
  runner();
}

Damit ist auch dieses Programm einsatzbereit. Abbildung 6 zeigt, dass die Ausgabe der .ps-Datei trotz Ausführung im Debugger im Textfeld ankommt. Da emptyFirst.ps1 im Moment keine Inhalte enthält, wird vor der eigentlichen Skriptausführung ein Fehler ausgeworfen.

Abb. 6: Die von verschiedenen Services angebotenen Dienstleistungen lassen sich nach Belieben kombinieren

Abb. 6: Die von verschiedenen Services angebotenen Dienstleistungen lassen sich nach Belieben kombinieren

Wie im Fall der vorhergehenden Dienste gilt auch hier, dass der DebuggerService eine Vielzahl von gut dokumentierten Hilfsmethoden bereitstellt. Diese erlauben die Interaktion mit dem gerade geladenen Kontext, um so fortgeschrittene Debugging-Szenarien zu realisieren.

Fazit

Microsofts PowerShell Editor Services sind mit Sicherheit kein Werkzeug für jedermann: Wenn Sie die PowerShell nur als Administrator nutzen, profitieren Sie nur wenig davon. Wer in seinem Unternehmen Werkzeuge für Entwickler anbietet und sich in die PowerShell integrieren möchte, sollte sich mit dem derzeit nur auf GitHub verfügbaren Produkt näher auseinandersetzen. Dass die Dokumentation im Moment alles andere als vollständig ist, sei an dieser Stelle allerdings angemerkt.

Unsere Redaktion empfiehlt:

Relevante Beiträge

Hinterlasse einen Kommentar

Hinterlasse den ersten Kommentar!

avatar
400
  Subscribe  
Benachrichtige mich zu:
X
- Gib Deinen Standort ein -
- or -