[PowerShell] Invoke modulbasierte Funktionen, die nicht global zur Verfügung stehen

DPXone

Lieutenant
Registriert
Mai 2009
Beiträge
552
Hallo,

weiß zufällig jemand, wie ich Funktionen im folgenden Kontext ausführen kann, wenn sie nicht installiert bzw. global zur Verfügung stehen?
Es geht um Zeile 12 im Code-Tag -> [void] $PowerShell.AddCommand('<<<<MeineFunktionDieNichtGlobalZurVerfügungSteht>>>>').AddParameters($Parameters)

Hintergrund:
Ich will, dass die Funktion des Moduls sich selbst (im multithreading-Verfahren) aufrufen kann.

Code:
$MaxThreads = 20 
$RunspacePool = [runspacefactory]::CreateRunspacePool(1 , $MaxThreads) 
$RunspacePool.Open() # Open runspace pool to be able to add runspaces    
 
[..someCode..]

$PowerShell = [powershell]::Create() # Create new powershell instance/runspace                                  
$PowerShell.RunSpacePool = $RunspacePool # Assign new instance/runspace to runspace pool  

[..someCode..]

[void] $PowerShell.AddCommand('<<<<MeineFunktionDieNichtGlobalZurVerfügungSteht>>>>').AddParameters($Parameters) 

[..someCode..]

RunSpace = $PowerShell.BeginInvoke() # Invoke ScriptBlock of the runspace

Gibt es irgendeine Möglichkeit die aktuell vom Benutzer geladenen Module (weiterhin) zu verwenden?

Man kann ja leicht global zur Verfügung stehende Funktionen anhängen und ausführen, aber wie sieht es mit Funktionen in Profil-basierten Modulen aus, die nur per Microsoft.PowerShellISE_profile.ps1 und Microsoft.PowerShell_profile.ps1 geladen werden?
Die greifen nämlich nicht. Die Funktion wird einfach nicht gefunden.

Fehler:
Exception calling "EndInvoke" with "1" argument(s): "The term '<<<<MeineFunktion>>>>' 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."


Ich hab es schon mit folgenden zusätzlichen Command probiert:
Code:
[void] $PowerShell.AddCommand("Import-Module $($PSCommandPath) -WarningAction Ignore")
aber das funktioniert auch nicht so wie gewünscht.
 
Jeder Runspace ist "blank", also ohne jede Umgebung. Die muß man erst selber konfigurieren.

Idealerweise baut man sich dafür ein Init-Script, also sowas wie eine $profile-Datei, welche bei jedem [powershell]::create() in deren Umgebung geladen werden kann. Die muß man aber explizit laden, zB mit AddScript($scriptblock).AddParameters($hashtable).

Muß natürlich keine Datei sein. "Hauptsache", man kann das als Scriptblock übergeben. Notfalls definiert man sich eine Variable damit. Die muß ja dann nicht durch das Modul exportiert werden.

Bin grad gar nicht sicher, ob man aus einem Runspace heraus Zugriff auf den startenden Benutzerkontext hat. Müßtest Du mal schauen, ob $profile irgendwas beinhaltet und was $Env:UserProfile sagt.

Würde das auch vermeiden. Am Profil doktert man vielleicht nochmal rum und wundert sich dann, warum das nicht mehr geht. Lieber getrennt halten und die Runspace-Umgebung möglichst sauber halten.

Nicht vergessen, den Runspacepool hinterher wieder zu schließen! Sonst sind Speicherlecks mehr oder weniger garantiert.
 
Langsam geb ich auf.
Habs nun geschafft, dass er meine Funktion schluckt, nach haufenweisen unterschiedlichen und neuen Fehlern.
Aber nun hänge ich bei:
Code:
Cannot convert argument "asyncResult", with value: "@{PowerShell=System.Management.Automation.PowerShell; RunSpace=System.Management.Automation.PowerShellAsyncResult}", for "EndInvoke" to type "System.IAsyncResult": "Cannot convert the 
"@{PowerShell=System.Management.Automation.PowerShell; RunSpace=System.Management.Automation.PowerShellAsyncResult}" value of type "System.Management.Automation.PSCustomObject" to type "System.IAsyncResult"."

Hier mal nun mein Modul (ob es direkt als Script klappt hab ich noch nicht getestet.)
Was meine Funktion können soll..... Schnell suchen, im Gegensatz zu Get-ChildItem (welches alle Attribute abfragt und ewig braucht, wenn es sehr viele Files betrifft)
Sobald das Grundgerüst steht und funktioniert, kann ich auch endlich weitere Optionen einbauen.

Beispiel zur Ausführung:
Code:
Find-File 'D:\' '*.mp3'

Code:
Function Find-File { 
	[CmdletBinding()] 
	Param (
		[Parameter(Position = 0)] 
		[string] $RootPath , 
		[Parameter(Position = 1)] 
		[string] $SearchPattern 
	) 
	
	Begin { 
		$SearchOption = [System.IO.SearchOption]::TopDirectoryOnly # Do not change!              
		$Result = @() 
		Try { 
			$RootDir = [System.IO.DirectoryInfo]::new($RootPath) 
		} Catch { 
			$null 
		} 
		[System.Collections.ArrayList] $RunspaceCollection = @() 
		$MaxThreads = 30
		$InitialSessionState = [initialsessionstate]::CreateDefault() 
		$InitialSessionState.ImportPSModule($PSCommandPath) 
		
		$RunspacePool = [runspacefactory]::CreateRunspacePool(1 , $MaxThreads , $InitialSessionState , $Host) 
		$RunspacePool.Open() # Open runspace pool to be able to add runspaces          
	} 
	
	Process { 
		Try { 
			$RootDirFolders = [System.IO.Directory]::EnumerateDirectories($RootDir.FullName , '*' , $SearchOption) 
			[System.IO.Directory]::EnumerateFiles($RootDir.FullName , $SearchPattern , $SearchOption) 
			
			Foreach ($RootDirFolder In $RootDirFolders) { 
				$PowerShell = [powershell]::Create($InitialSessionState) # Create new powershell instance/runspace                                   
				$PowerShell.RunSpacePool = $RunspacePool # Assign new instance/runspace to runspace pool        
				
				$Parameters = @{ # Define parameters for the runspace                     
					RootPath = $RootDirFolder 
					SearchPattern = $SearchPattern 
				} 
				
				#[void] $PowerShell.AddCommand("Import-Module",$false).AddParameter("Name",$PSCommandPath).Invoke()  
				#[void] $PowerShell.AddCommand('Import-Module').AddParameters(@{$PSCommandPath).BeginInvoke()  
				[void] $PowerShell.AddCommand("Find-File").AddParameter($Parameters) 
				
				
				$RunspaceCollection+= New-Object PSObject -Property @{ 
					PowerShell = $PowerShell 
					RunSpace = $PowerShell.BeginInvoke() # Invoke ScriptBlock of the runspace                                  
				} 
				
			} 
		} Catch { 
			write-host($_) -ForegroundColor Red 
		} 
	} 
	
	End { 
		While ($RunspaceCollection) { # While collection is not $null/$empty                              
			$RunspacesNotYetReturned = $RunspaceCollection | ? { $_.Runspace.IsCompleted -eq $true } # Filter by already completed runspaces    
			Foreach ($Runspace In $RunspacesNotYetReturned) { 
				$Runspace.PowerShell.EndInvoke($Runspace) # Return result of the runspace = ScriptBlock result                                  
				$Runspace.PowerShell.Dispose() # Dispose runspace                              
				$RunspaceCollection.Remove($Runspace) # Remove runspace from the collection                           
			} 
		} 
	} 
}
 
Zuletzt bearbeitet:
Na jetzt mußte ich aber erstmal gut durchschütteln. ;)

Wie Du schon sehr richtig erkannt hattest, ist die gesuchte Methode zum Parameter-Hinzufügen-als-Liste .AddParameters($hashtable). Aber beim Auskommentieren ist Dir das s hinten abhanden gekommen und .AddParameter-OHNE-das-s erwartet (string Name, objectWert) als Signatur. Da hat er dann per auto-convert die Zeichenfolge "system.collections.hashtable" draus gemacht - Standard-toString()-Methode aus Object, wenn es keinen Override gibt.

Irgendwo ist allerdings immer noch der Wurm drin: Die Pipeline terminiert irgendwo während der Ausführung.


Und, aber das nur als Überlegung und YMMV:

- Für mich sieht das so aus, als würde für jeden rekursiven Aufruf ein neuer Runspacepool aufgemacht werden. Muß nicht sein.
- Schlage vor, rekursive Aufrufe durch iterative Aufrufe zu ersetzen. Die einzelnen Threads können ja auch EnumerateFiles() / EnumerateDirectories() zur Ausführung übergeben bekommen... wobei ich das persönlich zumindest fürs Erste auch auf ein flaches Modell einstampfen würde (rein iterativ, nicht hierarchisch rekursiv). Ist immerhin für jedes Verzeichnis ein Thread/Runspace. Wenn jetzt aus dem per Parameter übergebenen Suchpfad sämtliche Unterverzeichnisse parallel durchsucht werden (ohne weitere Rekursion) sollte das auch schon schneller gehen.


- Außerdem als gutgemeinter Rat: ich glaub Du mißbrauchst da die BEGIN/PROCESS/END Architektur ein bißchen. Begin und End sind NUR für die Initialisierung/Deinitialisierung da. Übergebene Objekte müssen atomisch behandelt werden können - das geht nicht, wenn Du Programmlogik nach END verschiebst, wo sie zB wegen Abbruch oder irgendeines terminating errors in PROCESS erst gar nicht ausgeführt werden würde.

Prozeßlogik daher in Process() belassen.


Und architektonisch, naja zugegeben, ich stelle fest ich neig in letzter Zeit dazu, anderen sagen zu wollen, daß sies so machen sollen wie ich -- ist natürlich Käse, dafür schon mal vorbeugend sorry. Dennoch als Vorschlag:
- Komponenten nach Semantik zuordnen. Also zB zwei Funktionen Start-Pool und End-Pool, wobei erstere irgendwie einen Pool zurückgeben muß und zweiteres irgendwie diesen Pool zum Schließen kriegen muß.
- Dann zwei Funktionen Start-Runspace und Stop-Runspace (ich hab das für mich Start-Thread und Stop-Thread genannt wegen allzugroßer Konfusionierung ^_^ ). Die müssen dann wirklich einen Objekttypen oä. zurückliefern, welcher einer PS-Instanz den zugehörigen Thread/Runspace zuordnet -- denn nur die Instanz kann den Runspace schließen.

- Die Stop-Funktion würde dieses Objekt dann kriegen und zB das Ergebnis von EndInvoke() zurückgeben. Also das, was man "blank" ohne Verwendung von Runspaces zurückbekommen hätte. Wenn man das dann pipelinetauglich macht, würde also sowas wie Start-Runspace "dir" | Stop-Runspace identisch sein zu "dir" allein.

- Start müßte natürlich den erstellten Runspace mitbekommen.

- Ziel ist, daß die Chose möglichst übersichtlich wird. Multitask ist alles außer deterministisch, da sieht man sehr schnell nicht mehr durch, und wer grad was macht, weiß man im Normalfall eh nicht. Man muß also schauen, daß man sich das nicht selbst noch zusätzlich undurchsichtig macht.

- Wie man an dieser leckeren "Pipeline terminated" Ausnahme schon sieht. "Normal" hätte man die relativ gleich. Aber hier muß man buddeln, die einzelnen Ausgabestreams der PowerShell-Instanzen anschauen und sich einbilden, daß man so zum Pudels Kern kommt.


Anbei noch eine geringfügig überarbeitete Variante mit ein bissel mehr Typisierung - hilft bei der Fehlersuche. Okay, meistens. Diesmal war's Intellisense, was mir für AddParameter nicht das Erwartete angezeigt hatte.

Code:
Function Find-File { 
  [CmdletBinding()] 
  Param
  (
    [Parameter(Position = 0,Mandatory)] 
    [validatescript({ $d = $_|Get-Item; $d.Exists})]
    [string] $RootPath , 
    [Parameter(Position = 1,Mandatory)] 
    [string] $SearchPattern 
  ) 
	
  Begin { 
    # Remember ErrorActionPreference, then switch to STOP mode
    [System.Management.Automation.ActionPreference] $eaP = $ErrorActionPreference
    #$ErrorActionPreference = [System.Management.Automation.ActionPreference]::Stop
    
  
    [System.IO.SearchOption] $SearchOption = [System.IO.SearchOption]::TopDirectoryOnly # Do not change!              
    [object[]] $Result = @() 
    Try 
    { 
      $RootDir = [System.IO.DirectoryInfo]::new($RootPath) 
    }
    Catch # "Eigentlich" sollte es dank Parametervalidierung hier nichts mehr geben, aber...
    { 
      write-warning -Message ('{0}' -f $_.exception.message)
    } 
    [System.Collections.ArrayList] $RunspaceCollection = [System.Collections.ArrayList]::new()
    $MaxThreads = 30
    [initialsessionstate] $InitialSessionState = [initialsessionstate]::CreateDefault() 
    $InitialSessionState.ImportPSModule($PSCommandPath) 
		
    [System.Management.Automation.Runspaces.RunspacePool]$RunspacePool = [runspacefactory]::CreateRunspacePool(1 , $MaxThreads , $InitialSessionState , $Host) 
    $RunspacePool.Open() # Open runspace pool to be able to add runspaces          
  } 
	
  Process { 
    Try { 
      [System.Collections.Generic.IEnumerable[string]]$RootDirFolders = [System.IO.Directory]::EnumerateDirectories($RootDir.FullName , '*' , $SearchOption) 
      [System.IO.Directory]::EnumerateFiles($RootDir.FullName , $SearchPattern , $SearchOption) 
			
      Foreach ($RootDirFolder In $RootDirFolders) { 
        [powershell]$PowerShell = [powershell]::Create($InitialSessionState) # Create new powershell instance/runspace                                   
        $PowerShell.RunSpacePool = $RunspacePool # Assign new instance/runspace to runspace pool        
				
        [hashtable]$Parameters = @{ # Define parameters for the runspace                     
          RootPath = $RootDirFolder 
          SearchPattern = $SearchPattern 
        } 
				
        #[void] $PowerShell.AddCommand("Import-Module",$false).AddParameter("Name",$PSCommandPath).Invoke()  
        #[void] $PowerShell.AddCommand('Import-Module').AddParameters(@{$PSCommandPath).BeginInvoke()  
        [void] $PowerShell.AddCommand("Find-File").AddParameters($Parameters) 
				
        [System.IAsyncResult] $Thread = $PowerShell.BeginInvoke()
				
        $RunspaceCollection+= New-Object PSObject -Property @{ 
          PowerShell = $PowerShell 
          RunSpace = $Thread # Invoke ScriptBlock of the runspace                                  
        } 
				
      } 
    } Catch { 
      write-warning -message ('{0}' -f$_.exception.message) 
    } 
        
  } # Process
	
  End { 
    While ($RunspaceCollection) { # While collection is not $null/$empty                              
      $RunspacesNotYetReturned = $RunspaceCollection | ? { $_.Runspace.IsCompleted } # Filter by already completed runspaces    
      Foreach ($Runspace In $RunspacesNotYetReturned) { 

        [System.IAsyncResult] $Thread = $Runspace.Runspace
        
        
        $Runspace.PowerShell.EndInvoke($Thread) # Return result of the runspace = ScriptBlock result                                  
        $Runspace.PowerShell.Dispose() # Dispose runspace                              
        $RunspaceCollection.Remove($Runspace) # Remove runspace from the collection                           
      } 
      
      $RunspacePool.Close()
    } 
  } # end 
} #find-file
 
RalphS schrieb:
Na jetzt mußte ich aber erstmal gut durchschütteln. ;)

Wie Du schon sehr richtig erkannt hattest, ist die gesuchte Methode zum Parameter-Hinzufügen-als-Liste .AddParameters($hashtable). Aber beim Auskommentieren ist Dir das s hinten abhanden gekommen und .AddParameter-OHNE-das-s erwartet (string Name, objectWert) als Signatur. Da hat er dann per auto-convert die Zeichenfolge "system.collections.hashtable" draus gemacht - Standard-toString()-Methode aus Object, wenn es keinen Override gibt.
Werd ich nochmals prüfen

RalphS schrieb:
- Außerdem als gutgemeinter Rat: ich glaub Du mißbrauchst da die BEGIN/PROCESS/END Architektur ein bißchen. Begin und End sind NUR für die Initialisierung/Deinitialisierung da. Übergebene Objekte müssen atomisch behandelt werden können - das geht nicht, wenn Du Programmlogik nach END verschiebst, wo sie zB wegen Abbruch oder irgendeines terminating errors in PROCESS erst gar nicht ausgeführt werden würde.
Ja. Das ist mir beim Schreiben des Codes selbst schon ein Dorn im Auge gewesen.
Wusste aber echt nicht, wie ich die Auswertung der Collection im PROCESS realisieren soll ( aber wie schon gesagt, die Optimierung erfolgt, sobald es mal ohne Fehler läuft; Ohne Multithreading-Methoden funktionierts ja)
Der PROCESS läuft ja schließlich bei 1-Dimensionalen-Übergaben, durch die Pipeline, jedes mal ab. Heißt: 1 Dimension | 20 Items = 20 Prozesse
Da kommt das ForEach ja nicht mal zum Gebrauch. Wird ja nur bei Mehr-Dimensionalen-Übergaben genutzt.

Danke auf jeden Fall mal (wie zur Zeit öfter ) für die Hilfe und Feedback ;)
Ich schau mir das Alles nochmal an.
Vor allem das mit den Runspace-Pools.
War mir in meinem tiefen-Wahn garnicht mehr aufgefallen.
Werde da noch einen Übergabe-Parameter einbauen.


PS:
Die Variablen-Deklaration war bei dieser Aktion erstmal nachrangig.
Hab zwar bei ziemlich jeden Fehler probiert, erstmal die Variable zu deklarieren. Hat mich aber in der Try&Error Phase auch nicht weitergebracht.
Fürs finale Design natürlich wichtig, aber da bin ich noch etwas entfernt.
 
Zuletzt bearbeitet:
Bei mir isses genau andersherum, in der Entwicklung will ich das so streng wie möglich haben, damit mir ja nicht noch so blöde Fehler unterlaufen wie "ja wie jetzt, wo kommt denn der Integer da her? Das ist doch ein Array, dacht ich?" :)

Ich bau meine MT-Routinen bisher dreistufig:

- Start
Hier werden Teilaufgaben auf die Threads verteilt und diese gestartet (ich nenn das einfach mal so, ob das jetzt echt Threads sind oder Runspaces, oder was weiß ich ist ja glaub ich egal). Je nach Aufgabe könnte man das in "Start" zerlegen lassen; ich will sowas immer möglichst modular haben und Aufgabenteilung ist nichts, was man pauschal machen kann, ergo der Vorschlag, an Start bereits eine Liste von Teilaufgaben zu übergeben.
Bei Abschluß von "start" sind alle Aufgaben an den Runspacepool übergeben worden (note: das heißt NICHT, daß die alle gestartet wurden!).
"start" selber ist synchron.

- Sync
Grundlegend eine While-Schleife, die solange läuft, bis alle vorgesehenen Threads beendet wurden (Ergebnis noch egal). Da sich dort der Runspace drum kümmert, bleibt effektiv an dieser Stelle nur die Rückinfo für den Anwender (zB in Form von "x von y Aufgaben erledigt" als Progressbar).
Nach Abschluß von "sync" gibt es keine Threads mehr mit isCompleted -eq $False.
"sync" ist asynchron dahingehend, daß "vorne" synchronisiert wird und "hinten" die Aufgaben erledigt werden. Terminiert wird mit Abschluß der letzten Aufgabe.

- End
Zusammenbauen des (Gesamt-)Ergebnisses. Oft, aber nicht notwendigerweise, einfach ein Array mit allen Teilergebnissen. Kann aber zB auch die Summe über alle Ergebnisse sein (oder sonst was Arithmetisches). Oder sonst "irgendetwas".
Am Ende von "end" ist die Aufgabe abgeschlossen. Eventuell aufgetretene Ausnahmen werden durchgereicht, soweit sie nicht mit der MT-Geschichte selber zu tun hatten; Ziel ist, die MT-Variante vor der restlichen Programmlogik zu verstecken, damit die möglichst implementierungsunabhängig bleiben kann.

Die kommen bei mir auch alle in process{}, einfach deswegen, damit ich das über die Pipeline übergeben kann. Start-Thread -Task X | Stop-Thread muß das Ergebnis von X liefern, und zwar so, daß es nicht von der blanken Ausführung von X zu unterscheiden ist.

Für multi-input könnte man das dann in drei "Befehlen" machen (hier in Form hoffentlich anschaulicher Namen):
Code:
$subtasks = $problem | split-subtasks
 #zerlegt das Gesamtproblem in Teilaufgaben => split-subtasks ist notwendigerweise anwendungsspezifisch

$taskCollection = $subtasks | start-thread
 # $subtasks ist irgendwas IEnumerable, damit start-Thread für jeden einzelnen Subtasks in PROCESS{} was zu tun bekommt; $taskCollection hält dann die Sammlung von (PowerShell; RunSpace)-Zuordnungen

 sync-Thread -TaskCollection $taskCollection
# sync-thread verändert die TaskCollection nicht, sondern wartet nur darauf, daß alle Tasks abgeschlossen werden - Statusinfo in Sync-Thread optional. Das ist der Punkt, wo man das nicht an die Pipeline verfüttern sollte; der einzelne Task ist uninteressant, es interessiert nur, daß am Ende keiner mehr läuft und individuelle Abarbeitung per Pipeline würde nur stören
$results = $taskCollection | End-Thread
# $taskCollection ist nach wie vor die Sammlung aller Tasks; entsprechend wird PROCESS{} wieder einen Task nach dem anderen abarbeiten und sein individuelles Ergebnis zurückliefern

$finalResult = $results | Join-Subtasks
# Funktion, welche die Teilergebnisse zusammensetzt (optional); das wäre dann wieder anwendungsspezifisch, genau wie Split-Tasks oben.

Ob man das jetzt als Modul baut und dort drin einen einzelnen Runspacepool in eine Modul-interne Variable steckt oder ob man das exportiert und dann an den entsprechenden Stellen -Pool X mitgibt... ist glaub ich Ansichtssache bzw hängt vom konkreten Bedarf hab.

Für recursive-threaded würde ich glaub ich erstmal mit irgendeiner sort-Implementierung anfangen. Das geht dann nämlich schnell, bis das fertig ist, und man hat eine ganz genaue Vorstellung, was hinten rauskommen MUß. Dann, wenn das steht, kann man das ja entsprechend parametrisieren und die "sort"-Spezifika durch irgendwas Generisches ersetzen, damit man so eine multi-level rekursive Dateisuche bekommt (wie oben), wenn man nur die richtigen Parameter übergibt.

Oder alternativ die Fibonacci-Folge. Dann aber wahrscheinlich sinnvoll begrenzt, damit Dir nicht CPU-Zeit, RAM und Lebenszeit verlorengehen. :) Die hat aber den Vorteil, daß man ebenfalls weiß, was rauskommen muß, UND man hat rekursive Abhängigkeiten, die man ja irgendwie auch auseinandernehmen muß (sprich, will ich Element N haben, muß ich erst Element N-1 und Element N-2 ermitteln - ggf gleichzeitig -- bevor ich beide zu Element N zusammensetzen kann).
 
Zurück
Oben