PS Runspace
Wenn ich eine PowerShell starte, dann habe ich auch immer eine "Laufzeitumgebung", in der ich mich bewege. Wir wissen alle, dass wir auch weitere PowerShell-Fenster öffnen können oder mit Start-Job oder Start-Thread auch Befehle in den Hintergrund senden. Ich kann aber auch parallel weitere Runspaces instanzieren und Code dort parallel und im Hintergrund ausführen lassen.
Anstatt nun viel Text und Beispielcode zu lesen, können Sie sich das kurze Video von Adam Driscol anschauen, welches sehr gut die Nutzung von PowerShell Runspaces erklärt.
Adan Driscol: Advanced PowerShell -
Runspaces
https://www.youtube.com/watch?app=desktop&v=WvxUuru_vhk
Mit PowerShell Core gibt es mit Start-ThreadJob auch eine neue Option zum Start von Prozessen im Hintergrund.
PowerShell und parallele Verarbeitung
Einfacher Runspace mit Exchange
Eine ganz einfache Variante zeigt, was damit möglich ist und was nicht. Hier ein Code, der einfach ein "Get-Recipient" ausführen soll. Er wurde in einer Exchange PowerShell gestartet.
$PowerShell = [powershell]::Create() [void]$PowerShell.AddScript({ Get-Recipient -resultsize 3 }) $PowerShell.Invoke()
Sie können mit AddScript auch mehrere Skripte addieren. Die werden dann einfach nacheinander ausgeführt.
Der "$powershell.invoke()" startet den Code und wartet, bis er fertig ist. Das geht hier schnell und das "$powershell"-Objekt hat folgende Eigenschaften:
[PS] C:\>$PowerShell Commands : System.Management.Automation.PSCommand Streams : System.Management.Automation.PSDataStreams InstanceId : 5966f660-b993-4176-b5cd-a73d5a6b3ec7 InvocationStateInfo : System.Management.Automation.PSInvocationStateInfo IsNested : False HadErrors : True Runspace : System.Management.Automation.Runspaces.LocalRunspace RunspacePool : IsRunspaceOwner : True HistoryString :
Hier sehen Sie die verschiedenen Properties. Da sind "Commands" und "Streams" interessant:
[PS] C:\>$PowerShell.Commands | fl * Commands : { Get-recipient -resultsize 3 } [PS] C:\>$PowerShell.Streams Error : {The term 'Get-recipient' is not recognized as the name of a cmdlet, function, script file, or operable program. Check the spelling of the name, or if a path was included, verify that the path is correct and try again.} Progress : {parent = -1 id = 0 act = Preparing modules for first use. stat = cur = pct = -1 sec = -1 type = Completed} Verbose : {} Debug : {} Warning : {} Information : {}
Im Property "Commands" stehen die Befehle, die ich übergeben hatte und auch immer wieder per "Invoke" aufrufen könnte. Das Skript ist aber nicht gelaufen, weswegen hier im ERROR-Stream der Fehlercode steht.
Damit sehe ich aber auch, dass der Runspace eine eigene Shell ist und nicht die Module und Einstellungen der aufrufenden Shell vererbt bekommt.
Die Exchange Commandlets funktionieren so einfach nicht. Ich muss also in dem Runspace ggfls. meine erforderlich Umgebung noch einmal neu schaffen.
Etwas Parallel
Um die Befehle nun parallel auszuführen, kann ich "BeginInvoke()" nutzen. Damit wird das aufrufende Programm nicht angehalten.
# Code definieren $code = { 1..100 | % { $(get-date) start-sleep -seconds 1 } } #Objekt System.Management.Automation.PowerShell vorbereiten $newPowerShell = [PowerShell]::Create().AddScript($code) # Job starten $job = $newPowerShell.BeginInvoke() #Warten auf das Ende #While (-Not $job.IsCompleted) { # write-host " #} #Ergebnisse abholen. Wartet allerdings bis das Skript beendet ist. Notfalls endlos $newPowerShell.EndInvoke($job) # Aufraeumen $newPowerShell.Dispose()
Die so gestartet Jobs finden Sie nicht mit "Get-Job". Sie sind also gut beraten das Ergebnis des "BeginInvoke" irgendwo zu hinterlegen. Soweit ich gesehen habe, werden aber alle Ausgaben erst mit dem "EndInvoke()" zurück gegeben und auch erst dann, wenn der Code terminiert. Das Beispiel wird also erst nach 100 Sekunden beendet und dann die Ausgabe eingesammelt. Das ist zwar "schnell parallel" aber eine Weiterverarbeitung der Zwischenergebnisse ist so nicht möglich. Auch scheint die Laufzeitumgebung sich nicht zu vererben wenngleich es keinen weiteren "Powershell.exe"-Prozess im Taskmanager gibt. Es sieht wirklich nach einem eigenen Thread aus.
Parameterübergabe
Wer ein PowerShell-Skript startet, weiß um die Funktion der Parameter.
$Global:Runspace1 = [PowerShell]::Create() $Runspace1.Runspace.AddArgument("argument1") $Runspace1.AddScript({ param($arg1) Write-host "Runspace1:Variable1 $($arg1)" }) $Runspace1´.AddArgument(arg1,"Wert1") $jobHandle = $Runspace1.BeginInvoke()
Die lassen sich natürlich einfach übergeben.
- AddArgument
https://learn.microsoft.com/en-us/dotnet/api/system.management.automation.powershell.addargument?view=powershellsdk-7.4.0 - PowerShell.AddParameter Method
https://learn.microsoft.com/en-us/dotnet/api/system.management.automation.powershell.addparameter?view=powershellsdk-7.4.0 - Beginning Use of PowerShell Runspaces: Part 2
https://devblogs.microsoft.com/scripting/beginning-use-of-powershell-runspaces-part-2/
Übergabe mit SetVariable
Ein paar PowerShell-Befehle an einen anderen Prozess zur Ausführung zu senden und am Ende die Ausgabe abzurufen, ist ja einfach. Interessanter ist hier, wie man z.B. bestimmte Informationen schon vorab füllt und am Ende wieder abruft. Auch hierfür gibt es Wege, z.B. kann ich Variablen direkt am Anfang setzen:
$Global:Runspace1 = [PowerShell]::Create() $Runspace1.Runspace.SessionStateProxy.SetVariable("var1","1") $Runspace1.AddScript({ start-transcript ".\runspace1.txt" 1..10 | %{ Write-host "Runspace1:Variable1 $($var1)" $var1++ start-sleep -seconds 2 } stop-transcript }) $jobHandle = $Runspace1.BeginInvoke() start-sleep -seconds 5 Write-host "Host:Variable1 $($Runspace1.Runspace.SessionStateProxy.GetVariable("var1"))" $Runspace1.Runspace.SessionStateProxy.SetVariable("var1",100) Write-host "Host:Variable1 $($Runspace1.Runspace.SessionStateProxy.GetVariable("var1"))" start-sleep -seconds 5 Write-host "Host:Variable1 $($Runspace1.Runspace.SessionStateProxy.GetVariable("var1"))"
Bei dem Skript habe ich zwei Fehler bzw. Verhaltensweisen erkennen können:
- Start-Transcript ist Global
Alle Ausgaben des Hosts wurden protokolliert aber nicht die des Runspace. Ein "Transcript" im Runspace funktioniert nicht - Keine
Variablen bei laufendem Runspace
Wenn der Runspace mit "BeginInvoke()" im Hintergrund läuft sind die Properties ReadOnly
Ich kann also nur vorher die Variablen füllen und hinterher die verschiedenen Pipelines auswerten
- SessionStateProxy.SetVariable(String)
Method (System.Management.Automation.Runspaces)
https://learn.microsoft.com/en-us/dotnet/api/system.management.automation.runspaces.sessionstateproxy.setvariable
Rückgabe mit GetVariable
Wenn das Skript im Runspace zum Ende gekommen ist, dann wird es zwar nicht mehr ausgeführt aber der PowerShell Host ist weiterhin vorhanden und auch alle Variablen sind noch instanziert und belegen Speicher. Diese Variablen kann ich auch auslesen
$Global:Runspace1 = [PowerShell]::Create() $Runspace1.Runspace.SessionStateProxy.SetVariable("var1","1") $Runspace1.AddScript({ Write-host "Runspace1:Variable1 $($var1)" $Var1=2 }) $jobHandle = $Runspace1.BeginInvoke() Start-Sleep -seconds 5 Write-host "Host:Variable1 $($Runspace1.Runspace.SessionStateProxy.GetVariable("var1"))" 2
Das Beispiel setzt erst die Variabel vor dem Start des Runspace auf "1", das Script im Runspace setzt die Variable auf 2 und nach dem Ende des Runspace kann ich den Wert auch auslesen.
- SessionStateProxy.GetVariable(String)
Method (System.Management.Automation.Runspaces)
https://learn.microsoft.com/en-us/dotnet/api/system.management.automation.runspaces.sessionstateproxy.getvariable?view=powershellsdk-7.4.0
Rückgaben über Pipeline
Ein zweiter Weg sind natürlich die Pipelines. Beim normalen "Invoke" kommen die Daten direkt über die Pipeline. Beim asynchronen Aufruf mit BeginInvoke muss ich die Variablen mit übergeben.
- PowerShell.Invoke Method
https://learn.microsoft.com/en-us/dotnet/api/system.management.automation.powershell.invoke?view=powershellsdk-7.4.0 - BeginInvoke<TInput,TOutput>(PSDataCollection<TInput>,
PSDataCollection<TOutput>)
https://learn.microsoft.com/en-us/dotnet/api/system.management.automation.powershell.begininvoke?view=powershellsdk-7.4.0#system-management-automation-powershell-begininvoke-2(system-management-automation-psdatacollection((-0))-system-management-automation-psdatacollection((-1)))
Ich sende erst einmal keine Eingabe aber erwarte eine Ausgabe
$Global:Runspace1 = [PowerShell]::Create() $Runspace1.Runspace.SessionStateProxy.SetVariable("var1","1") $null= $Runspace1.AddScript({ 1..5 | foreach { $Var1=$_ Write-Output "Runspace1:Variable1 $($var1)" Start-Sleep -seconds 1 } }) $jobHandle = $Runspace1.BeginInvoke() While (!$jobHandle.IsCompleted) { Write-host "Host: Wait for completion" Start-Sleep -Seconds 1 } $outputStream = $Runspace1.EndInvoke($jobHandle) $outputstream
Sie sollten sehen, dass das Hauptprogramm wartet und wenn der Hintergrundprozess zu Ende ist, dann hole ich die Output-Pipeline ab.
- RunSpace09 Code Sample
https://learn.microsoft.com/en-us/powershell/scripting/developer/prog-guide/runspace09-code-sample
This sample application creates and opens a runspace, creates and asynchronously invokes a pipeline, and then uses pipeline events to process the script asynchronously. The script that is run by this application creates the integers 1 through 10 in 0.5-second intervals (500 ms) - Another Way to Get Output From a
PowerShell Runspace
https://learn-powershell.net/2016/02/14/another-way-to-get-output-from-a-powershell-runspace/ - Yet Another Way to Get Output from a
Runspace, Reflection Edition
https://learn-powershell.net/2016/11/28/yet-another-way-to-get-output-from-a-runspace-reflection-edition/
EXIT-Code?
Eigentlich alle Skripte können am Ende einen Fehlercode zurückgeben. Das habe ich mit einem Runspace auch versucht.
$PowerShell = [powershell]::Create() $PowerShell.AddScript({Write-Verbose "Start" -verbose ; exit 1}) $PowerShell.Invoke()
Allerdings habe ich keinen Weg gefunden, dieses "EXIT 1" auch abzufragen. Der Aufruf von "$PowerShell.Invoke()" liefert immer "$null". Nur beim Aufruf von "BeginInvoke()" erhalte ich einen Handle zu dem Thread und auch das "PowerShell"-Objekt kennt zwar Streams, und hier auch die Ausgabe von Write-Verbose, aber der Error-Stream ist leer und auch "InvocationStateInfo" liefert nichts mit.
Ich habe mir dann damit geholfen, dass ich den Exit-Code einfach in eine Variable geschrieben habe
$PowerShell = [powershell]::Create() $PowerShell.AddScript({Write-Verbose "Start" -Verbose ; $exit=1}) $PowerShell.Invoke() $PowerShell.Runspace.SessionStateProxy.GetVariable("EXIT") 1
Schön ist das nicht aber $LASTEXITCODE konnte ich nicht schreiben und nicht auslesen. Eine andere Option wäre es gewesen, den Exitcode einfach and STDOUT oder STRERR zu schreiben und dort abzurufen
Wenn sie hier eine Lösung zum sauberen lesen des "EXIT <code" kennen, dann gerne her damit.
- about_Automatic_Variables -
$LastExitCode
https://learn.microsoft.com/de-de/powershell/module/microsoft.powershell.core/about/about_automatic_variables?view=powershell-7.5#lastexitcode - RunspaceStateInfo Class
https://learn.microsoft.com/en-us/dotnet/api/system.management.automation.runspaces.runspacestateinfo?view=powershellsdk-7.4.0&redirectedfrom=MSDN - PSHost.SetShouldExit(Int32) Method
https://learn.microsoft.com/en-us/dotnet/api/system.management.automation.host.pshost.setshouldexit
Sonstige Kommunikation
Das das Setzen und Lesen von Variablen geht natürlich vor dem Start oder nach dem Ende des PowerShell Runspace. Die Kommunikation mit Pipelines in der einfachen Form funktioniert auch nicht während der Laufzeit. Das geht dann über entsprechende Handler, die bei neuen Ausgaben aufgerufen werden.
Wer ansonsten eine Kommunikation zwischen dem Host und dem Runspace benötigt, kann natürlich eine klassische Kommunikation zwischen Prozessen verwenden, z.B. per TCP/IP, Sockets, Windows Pipelines oder das Dateisystem.
Runspace beenden
Ein gutes Programm endet, nachdem es seinen Dienste getan hat. Das gilt so zwar nicht für das eigentliche Hauptprogramm aber ein Runspace hat meist eine begrenzte Lebensdauer. Dennoch kann es passieren, dass ein Runspace länger läuft als erwartet oder sogar festklemmt. Das steuernde Hauptprogramm sollte sich also nicht nur merken, welchen Runspace es aktiviert hat, sondern auch wann das passiert ist und vielleicht Langläufer behandeln. Das geht mit einem "dispose()" recht einfache
$Global:Runspace1 = [PowerShell]::Create() $null= $Runspace1.AddScript({ Write-Verbose "ThreadID: $([appdomain]::GetCurrentThreadId())" -Verbose start-sleep -seconds 10000 }) $jobHandle = $Runspace1.BeginInvoke() Start-Sleep -Seconds 10 $runspace1.dispose()
Die bisherigen Ausgaben, z.B. durch Write-Verbose können wir natürlich immer noch abrufen
- Runspace.Dispose Method
https://learn.microsoft.com/en-us/dotnet/api/system.management.automation.runspaces.runspace.dispose?view=powershellsdk-7.4.0 - Runspace.Close Method
https://learn.microsoft.com/en-us/dotnet/api/system.management.automation.runspaces.runspace.close?view=powershellsdk-7.4.0
Runspace Pool
Die ganze Zeit habe ich eine PowerShell-Konsole genutzt, in der ich genau einen Runspace im Hintergrund aktiviert habe. Wenn ich nun mehrere Runspaces parallel laufen lassen will, dann kann ich diese nacheinander instanzieren und z.B. in einem Array oder Hashtable o.ä. speicher. Ich werde ich nicht nicht die Variablen von Hand und statisch in "$runspace1, $runspace2, $runspace3" durchnummerieren.
Allerdings bringt hier PowerShell oder das .NET Framework eine eigene Klasse "RunspaceFactory" mit, über die elegant mehrere Runspaces koordinieren können. Der große Vorteil dabei ist, dass ich mich nicht ums "Throttling" kümmern muss. Ich kann mehr Threads einstellen während die RunspaceFactory dafür sorgt, dass nur die angegebene Anzahl parallel läuft. Natürlich könnte ich das auch selbst koordinieren aber so ist es einfacher.
$pool = [RunspaceFactory]::CreateRunspacePool(1, [int]$env:NUMBER_OF_PROCESSORS + 1) $pool.ApartmentState = "MTA" $pool.Open() $runspaces = New-Object System.Collections.ArrayList 1..10 | foreach { $runspace = [PowerShell]::Create(); $null = $runspace.AddScript({ Write-Verbose "ThreadID: $([appdomain]::GetCurrentThreadId())" -Verbose 1..100 | % {Write-Verbose $_; Start-Sleep -seconds 1} }); # Wir setzen dann das Property "RunspacePool" des Runspace auf den Pool $runspace.RunspacePool = $pool $runspaces += [PSCustomObject]@{ Pipe = $runspace; handle = $runspace.BeginInvoke() } } while ($Runspaces.Handle.IsCompleted){} # hier dann die Ergebnisse einsammeln
In der Liste "Runspaces" sind dann alle Instanzen über das Property "handle" erreichbar.
Ich kann problemlos mehr Prozesse einstellen und starten aber es laufen maximal die angegeben Anzahl an Prozessen gleichzeitig. Wie viele Runspaces noch verfügbar sind, erhalten wir mit:
$pool.GetAvailableRunspaces()
Zudem gibt es einen "Cleanup"-Prozess, der ungenutzte Runspaces wegwirft, was natürlich Speicher spart
- RunspacePool.CleanupInterval Property
https://learn.microsoft.com/en-us/dotnet/api/system.management.automation.runspaces.runspacepool.cleanupinterval?view=powershellsdk-7.4.0
Im Windows Taskmanager sehe ich übrigens nur einen PWSH-Prozess. Erst mit dem SysInternals ProcessExplorer finde ich bei dem Prozess die ganzen Threads
- RunspaceFactory Class
https://learn.microsoft.com/en-us/dotnet/api/system.management.automation.runspaces.runspacefactory - Runspaces Simplified (as much as
possible)
https://blog.netnerds.net/2016/12/runspaces-simplified/
Weitere Links
- PowerShell und parallele Verarbeitung
- PowerShell Mutex
- Start-ThreadJob
- PowerShell Streams
- PS Job
- PS Interprozesskommunikation
- Power​Shell Class
https://docs.microsoft.com/en-us/dotnet/api/system.management.automation.powershell?view=powershellsdk-1.1.0 - Beginning Use of PowerShell Runspaces:
Part 1:https://devblogs.microsoft.com/scripting/beginning-use-of-powershell-runspaces-part-1/
Part 2:https://devblogs.microsoft.com/scripting/beginning-use-of-powershell-runspaces-part-2/
Part 3:https://devblogs.microsoft.com/scripting/beginning-use-of-powershell-runspaces-part-3/ - Weekend Scripter: A Look at the
PoshRSJob Module
https://devblogs.microsoft.com/scripting/weekend-scripter-a-look-at-the-poshrsjob-module/?hide_banner=true - Runspace01 Code Samples
https://learn.microsoft.com/en-us/powershell/scripting/developer/prog-guide/runspace01-code-samples - RunSpace09 Code Sample
https://learn.microsoft.com/en-us/powershell/scripting/developer/prog-guide/runspace09-code-sample
This sample application creates and opens a runspace, creates and asynchronously invokes a pipeline, and then uses pipeline events to process the script asynchronously. The script that is run by this application creates the integers 1 through 10 in 0.5-second intervals (500 ms) - True Multithreading in PowerShell
PowerShell Add comments.
http://www.get-blog.com/?p=189 - Invoke-All
https://aka.ms/invokeall - Easily Multi thread Powershell commands
and scripts
https://blogs.technet.microsoft.com/santhse/invokeall/ - Adan Driscol: Advanced PowerShell -
Runspaces
https://www.youtube.com/watch?app=desktop&v=WvxUuru_vhk - Using Background Runspaces Instead of
PSJobs For Better Performance
https://learn-powershell.net/2012/05/13/using-background-runspaces-instead-of-psjobs-for-better-performance/ - Multithread Your PowerShell Commands
Using Runspaces with PoshRSJob
https://mcpmag.com/articles/2015/08/06/multithread-your-commands.aspx - Beginning Use of PowerShell Runspaces:
Part1: https://devblogs.microsoft.com/scripting/beginning-use-of-powershell-runspaces-part-1/
Part2: https://devblogs.microsoft.com/scripting/beginning-use-of-powershell-runspaces-part-2/
Part3; https://devblogs.microsoft.com/scripting/beginning-use-of-powershell-runspaces-part-3/ - Weekend Scripter: A Look at the
PoshRSJob Module
https://devblogs.microsoft.com/scripting/weekend-scripter-a-look-at-the-poshrsjob-module/ - Using Background Runspaces Instead of
PSJobs For Better Performance
https://learn-powershell.net/2012/05/13/using-background-runspaces-instead-of-psjobs-for-better-performance/ - Yet Another Way to Get Output from a
Runspace, Reflection Edition
https://learn-powershell.net/2016/11/28/yet-another-way-to-get-output-from-a-runspace-reflection-edition/ - Split-Pipeline
https://github.com/nightroman/SplitPipeline - Powershell parallelisieren mit Runspaces
https://netz-weise-it.training/weisheiten/powershell-parallisieren-mit-runspaces - PowerShell - Background Jobs, Runspace
Jobs, and Thread Jobs
https://randombrainworks.com/2018/01/28/powershell-background-jobs-runspace-jobs-thread-jobs/