Archiv für den Monat: März 2015

Löschen der Application und Services-Logs

Nach diesem Tipp habe ich eine Weile suchen müssen. Die PowerShell besitzt bekanntlich ein Get-WinEvent-Cmdlet für das Abfragen von Einträgen aus den „Application und Services“-Logdateien (die mit den langen Namen), aber kein Cmdlet, um die Einträge aus einem Protokoll zu löschen. Ein Clear-EventLog funktioniert nicht. Die Lösung ist der Aufruf einer statischen .NET-Methode von einer Klasse, die ich bislang nicht kannte (was generell nichts heißen muss;).

Der folgende Befehl löscht alle Einträge aus dem dem Log „Microsoft-Windows-ManagementOdataService/Operational“:

$LogName = "Microsoft-Windows-ManagementOdataService/Operational"
[System.Diagnostics.Eventing.Reader.EventLogSession]::GlobalSession.ClearLog($LogName)

Index-Eigenschaft bei Select-Object hinzufügen

Manchmal möchte man eine Ausgabe um eine Eigenschaft erweitern, die eine Art „Zählindex“ darstellt und mit jedem Objekt erhöht wird. Eher per Zufall (ein Kursteilnehmer hatte mich auf die Idee gebracht), bin ich auf eine einfache und praktische Lösung gestoßen. Sie basiert auf den eher selten verwendeten OutVariable-Parameter, auf den der Name (ohne $-Zeichen) folgt, in die der Pipeline-Inhalt abgelegt wird

Der folgende Befehl gibt z.B. die Namen aller Prozesse zusammen mit einer Index-Spalte aus:

Get-Process -OutVariable output | Select-Object -Property Name, @{n="Index";e={$Output.Count}} | Format-Table -Property Name, Index -AutoSize
Name                                  Index
----                                  -----
conhost                                   1
csrss                                     2
csrss                                     3
dfsrs                                     4
dfssvc                                    5
usw.

Mir ist allerdings immer noch nicht klar, warum Select-Object immer die Reihenfolge der Eigenschaften vertauscht.

Datenaustausch zwischen Jobs über Events per Forward-Parameter

Backgorundjobs sind bezüglich ihrer Möglichkeit auf Variablen außerhalb ihres Bereichs zugreifen zu können leider stark eingeschränkt. Da ein Backgroundjob im Rahmen einer eigenen Remoting-Session ausgeführt wird, gelten dieselben Einschränkungen wie allgemein bei PowerShell-Remoting. Soll ein Scriptblock, der als Backgroundjob ausgeführt wird, Mitteilungen an das Skript senden, von dem er aus gestartet wurde, geht dies am einfachsten über Events, konkret die Cmdlets Register-EngineEvent und New-Event. Zuerst wird per Register-EngineEvent ein Event im Scriptblock registriert. Dabei kommt es auf den Forward-Parameter an. Anschließend wird während der weiteren Ausführung des Scriptblocks per New-Event ein Event ausgelöst und automatisch weitergeleitet. Die zu versendenden Daten werden dem MessageData-Parameter übergeben. Außerhalb des Scriptblocks hat man mehrere Möglichkeiten, auf das Event zu reagieren:

  • Man schaut per Get-Event-Cmdlet nach
  • Man wartet auf das Event per Wait-Event-Cmdlet
  • Man registriert es erneut, verbindet es über den Action-Parameter von Register-EngineEvent dieses Mal mit einem Scriptblock, dessen Befehle ausgeführt werden, wenn das Event in der Remote-Session eingetreten ist

Das folgende Beispiel verwendet diese Technik.

<#
 .Synopsis
 Backgroundjobs-Datenaustausch per Event
#>

$SBJobA = {
    Register-EngineEvent -SourceIdentifier JobA -Forward
    1..100 | Foreach-Object {
        $z = 1..10 | Get-Random
        if ($z -gt 7) 
        {
            $Message = New-Object -TypeName PSObject @{Text="Wert liegt im kritischen Bereich ($z)";Wert=$z}
            New-Event -SourceIdentifier JobA -MessageData $Message
        }
        Start-Sleep -Seconds 2
    }
    Unregister-Event -SourceIdentifier JobA
}

$SBJobB = {
    Register-EngineEvent -SourceIdentifier JobB -Forward
    1..100 | Foreach-Object {
        $z = 1..10 | Get-Random
        if ($z -gt 7) 
        {
            $Message = New-Object -TypeName PSObject @{Text="Wert liegt im kritischen Bereich ($z)";Wert=$z}
            New-Event -SourceIdentifier JobB -MessageData $Message
        }
        Start-Sleep -Seconds 2
    }
    Unregister-Event -SourceIdentifier JobB
}


$SBEvent = {
    $JobName = $Event.SourceIdentifier
    $Color = 8..15 | Get-Random
    Write-Host -Fore $Color -Object ("Job {0} meldet den Wert {1}" -f $JobName, $Event.MessageData.Wert)
}

Register-EngineEvent -SourceIdentifier JobA -Action $SBEvent 
Register-EngineEvent -SourceIdentifier JobB -Action $SBEvent 

#Unregister-Event -SourceIdentifier JobA -Force
#Unregister-Event -SourceIdentifier JobB -Force

Start-Job -ScriptBlock $SBJobA
Start-Job -ScriptBlock $SBJobB

Arrays an ein per Powershell.exe gestartetes Skript übergeben

Vor kurzem kam in einer PowerShell-Schulung eine Frage auf, die eigentlich ganz einfach zu lösen sein sollte. Wie kann einem Skript ein Array mit Namen als Argument übergeben werden?

Eigentlich ganz einfach bedeutet, dass dazu lediglich der Parameter als Array deklariert werden muss. Der Ausgangspunkt für das folgende Beispiel ist, dass ein Skript den Status von Diensten, deren Name beim Aufruf übergeben wird, per Get-Service prüfen und in eine Datei schreiben soll. Neben den Namen der Dienste soll daher auch der Pfad einer Datei übergeben werden. Die folgende Parameterdeklaration führt zu dem gewünschten Resultat:

[CmdletBinding()]
param([String]$Path,
      [String[]]$Dienste)
"Anzahl Dienste: {0}" -f $Dienste.Count
$Dienste | Get-Service -ErrorAction SilentlyContinue | Select-Object -Property Name, Status |  Tee-Object -FilePath $Path

Das Problem: Die Übergabe funktioniert auf einmal nicht mehr, wenn das Skript außerhalb der PowerShell durch den Aufruf von Powershell.exe gestartet wird. Ein

powershell -file .SkriptMitArgumenten.ps1 -Path Dienste.txt -Dienste Winmgmt, AudioSrv

führt nun zu einer Fehlermeldung, die besagt, dass kein Positionsparameter gefunden wurde, der „AudioSrv“ als Argument akzeptiert.

Eine Lösung besteht darin (es gibt sicher noch andere), die selten verwendete Eigenschaft ValueFromRemainingArguments beim Parameter-Attribut zu verwenden:

[CmdletBinding()]
param([String]$Path,
      [Parameter(ValueFromRemainingArguments=$true)][String[]]$Dienste)
"Anzahl Dienste: {0}" -f $Dienste.Count
$Dienste | Get-Service -ErrorAction SilentlyContinue | Select-Object -Property Name, Status | Tee-Object -FilePath $Path

Damit ist der folgende Aufruf möglich:

powershell -file .SkriptMitArgumenten.ps1 -Path Dienste.txt Winmgmt, AudioSrv

Wer genau aufgepasst hat wird festgestellt haben, dass der Dienste-Parameter fehlt. Dies ist die Voraussetzung dafür, dass es funktioniert. Ansonsten ist erneut eine Fehlermeldung die Folge (außerdem muss auf das erste (!) Komma ein Leerzeichen folgen). Wenn ein Skript über die Eingabeaufforderung oder als Verknüpfung aufgerufen wird, kann man damit natürlich leben.