Question A propos des runspaces sous PS v1

Plus d'informations
il y a 15 ans 3 mois #5155 par Laurent Dardenne
[edit]
La dernière version disponible au travers du projet Add-lib
[/edit]

Voici une adaptation d'un script permettant l'exécution asynchrone d'un scriptblock.
J'ai préféré, pour faciliter quelques tests, ne récupérer qu'un seul objet runspace.

L'entête du script contient 3 exemples, j'en ajouterais d'autres et donnerais qq informations sur le fonctionnement et les qq pb potentiels que le multi-threading peut provoquer.
[code:1]
# New-Runspace.ps1
# Ce script crée un objet Runspace et lui ajoute des membres dynamiques.
# Un Runspace permet l'exécution asynchrone d'un scriptblock.
#
# Le script d'origine crée un tableau de job.
# version 1.0
# Laurent Dardenne.
#
# Adapté de :
# www.leeholmes.com/blog/RealtimeSyntaxHig...werShellConsole.aspx
# Adaptant à son tour le script d'origine suivant :
# jtruher.spaces.live.com/blog/cns!7143DA6E51A2628D!130.entry
#
Function New-Runspace( [scriptblock]$scriptToRun, [switch] $RunInstance, [switch] $InteractWithHost)
{
#
# Object Created - Custom Object (typenames[0] = \"PowerShellJobObject\"«»)
#
# METHODS
#
# void InvokeAsync([string] $script, [array] $input = @())
# Invokes a script asynchronously.
# void Stop([bool] $async = $false) # Stop the pipeline.
#
# PROPERTIES
#
# [System.Management.Automation.Runspaces.LocalPipeline] LastPipeline
# The last pipeline that executed.
# [bool] IsRunning
# Whether the last pipeline is still running.
# [System.Management.Automation.Runspaces.PipelineState] LastPipelineState
# The state of the last pipeline to be created.
# [array] Results
# The output of the last pipeline that was run.
# [array] LastError
# The errors produced by the last pipeline run.
# [object] LastException
# If the pipeline failed, the exception that caused it to fail.
#

# Switchs
#
# InteractWithHost : Configure le runspace afin d'interagir avec un host.
# Autorise l'affichage vers la console.
#
# RunInstance : Le script d'origine démarrait immédiatement l'exécution asynchrone du runspace.
# On laisse l'appelant décidé du moment de son exécution.

# Notes
# ID de runspace : Le script d'origine crée un tableau de job, ici ce n'est plus le cas.
# Chaque runspace posséde un identifiant : $RS1.Runspace.InstanceId
#
# Retrouver tous les runspaces :
# Dir variable:*|Where {($_).value.Psobject.typenames -contains \"PowerShellJobObject\"}
#
# Arrêt du pipeline : $MonRunSpace.Stop($True) # arrêt asynchrone
# Sous réserve que le code exécuté ne soit pas en attente.
# Par exemple le code suivant contenu dans le runspace :
# $evenement = $watcher.WaitForNextEvent()
# doit d'abord se terminer pour que l'appel à StopAsync() se termine à son tour.
# Dans ce cas l'affichage de $MonRunSpace dans la console se figera jusqu'a ce
# l'appel à WaitForNextEvent() se termine (cf. multi-threading).
#
# Déterminer, dans le code du runspace, si l'affichage est possible :
# if ($host.Name -eq \"Default MSH Host\"«») # pas d'affichage possible
#
# Accès aux variables de l'appelant :
# $VarLocaleRunspace==$executionContextProxy.SessionState.PsVariable.Get(\"VariableSessionPowerShell\"«»).Value
# Lee Holmes à ajouter la possibilité d'accèder aux variables et à toute autre information du runspace parent exécutant le script.
#
#
#
# Protection des données :
# Chaque variable de l'appelant utilisée dans le runspace doit exister avant qu'il ne soit exécuté.
# Dans ce cas on se retrouve dans un contexte multi-thread, on doit donc protéger les données partagées :
# $Data = New-Object System.Collections.ArrayList(50)
# $SynchronizedData=[System.Collections.ArrayList]::«»Synchronized($Data)
# Dans le code du runspace on accédera à la variable de la manière suivante :
# $Liste=$executionContextProxy.SessionState.PsVariable.Get(\"SynchronizedData\"«»).Value
# [void]$Liste.Add($MonObjet) # On manipule une référence
#
# Du coup certaines opérations pourront provoquer des erreurs.
# Par exemple l'énumération d'une collection peut provoquer l'exception InvalidOperationException.
# msdn.microsoft.com/fr-fr/library/system....erator.movenext.aspx
# \"Un énumérateur reste valide tant que la collection demeure inchangée.
# Si la collection est modifiée en ajoutant, modifiant ou supprimant des éléments,
# l'énumérateur devient irrévocablement non valide et le prochain appel à MoveNext ou à Reset lève InvalidOperationException.\"
#
# Exécution de Winform : TODO Control.invoke...
#
#Exemples :
#
Exemple 1
# $RS1=New-RunSpace -interact {
# Write-host(\"Début de RS1`r`n\"«»);
# 1..5|% {Write-host \"[Runspace]Element $_\"}
# #sleep -m 500
# Write-host(\"Fin de RS1`r`n\"«»)
# }#RS1
# $RS1
# $RS1.InvokeAsync($RS1.Command); 1..10|% {Write-host \"[Session PowerShell]Element $_\"}#;sleep -m 500 }
# $RS1
# #Libére le runspace
# if (!$Rs1.IsRunning)
# {$RS1.RunSpace.Close();$RS1=$null}
# else
# {
# \"Le runspace est en cours d'exécution\"
# #On peut forcer l'arrêt du pipeline
# #$RS1.Stop()
# #$RS1.RunSpace.Close();$RS1=$null
# }
#
#
Exemple 2
# $RS1=New-RunSpace -interact {
# $c=($input|measure-object).Count
# Write-host \"Count= $c\"
# }# RS1
# $Tab=1..5
# #Envoie,dans le pipeline du runspace, le tableau $TAB élément par élément
# $RS1.InvokeAsync($RS1.Command,$Tab)
# #Envoie le tableau comme un seul élément
# $RS1.InvokeAsync($RS1.Command,@(,$Tab))
#
#
Exemple 2-1
# $RS1=New-RunSpace { $input|sort-object}
# $RS1.InvokeAsync($RS1.Command,\"C\",\"A\",\"B\"«»)
# $RS1.Result -> ABC
#
# Private Fields
#
# [array] _lastOutput # The objects output from the last pipeline run.
# [array] _lastError # The errors output from the last pipeline run.
#
#region Message
$MultiplePipeline = \"A pipeline was already running. \" +
\"Cannot invoke two pipelines concurrently.\"
#
# MAIN
#

# Create a runspace and open it
$config = [Management.Automation.Runspaces.RunspaceConfiguration]::Create()
if ($InteractWithHost)
{
#Accéde au host de PS, permet l'affichage
$runspace = [System.Management.Automation.Runspaces.RunspaceFactory]::CreateRunspace($Host,$config)
}
else
{
#Pas d'affichage possible sur la console de PS
$runspace = [System.Management.Automation.Runspaces.RunspaceFactory]::CreateRunspace($config)
}
$runspace.Open()
#Accéde au contexte d'exécution de l'appelant
#par défaut celui de PowerShell.exe
# Ajout de Lee Holmes
$runspace.SessionStateProxy.SetVariable(\"executionContextProxy\", $executionContext)

# Create the object - we'll use this as the collector for the entire job.
$object = new-object System.Management.Automation.PsObject
# Add the object as a note on the runspace
$object | add-member Noteproperty Runspace $runspace
# Add a field for storing the last pipeline that was run.
$object | add-member Noteproperty LastPipeline $null
# Add an invoke method to the object that takes a script to invoke asynchronously.
$invokeAsyncBody = {
if ($args.Count -lt 1)
{
throw 'Usage: $obj.InvokeAsync([string] $script, [Optional][params][array]$inputObjects)'
}
& {
[string]$script, [array] $inputArray = @($args[0])
$PipelineRunning = [System.Management.Automation.Runspaces.PipelineState]::Running
# Check that there isn't a currently executing pipeline.
# Only one pipeline may run at a time.
if ($this.LastPipeline -eq $null -or
$this.LastPipeline.PipelineStateInfo.State -ne $PipelineRunning )
{
$this.LastPipeline = $this.Runspace.CreatePipeline($script)
# if there's input, write it into the input pipeline.
if ($inputArray.Count -gt 0)
{
#fix
[void]$this.LastPipeline.Input.Write($inputArray, $true)
}
$this.LastPipeline.Input.Close()
# Set the Results and LastError to null.
$this.Results = $null
$this.LastError = $null
# GO!
$this.LastPipeline.InvokeAsync()
}
else
{
# A pipeline was running. Report an error.
throw
}
} $args
}
$object | add-member ScriptMethod InvokeAsync $invokeAsyncBody
# Adds a getter script property that lets you determine whether the runspace is still running.
$get_isRunning = {
$PipelineRunning = [System.Management.Automation.Runspaces.PipelineState]::Running
return -not ($this.LastPipeline -eq $null -or
$this.LastPipeline.PipelineStateInfo.State -ne $PipelineRunning )
}
$object | add-member ScriptProperty IsRunning $get_isRunning
# Add a getter for finding out the state of the last pipeline.
$get_PipelineState = { return $this.LastPipeline.PipelineStateInfo.State }
$object | add-member ScriptProperty LastPipelineState $get_PipelineState
# Add a getter script property that lets you get the last output.
$get_lastOutput = {
if ($this._lastOutput -eq $null -and -not $this.IsRunning)
{
$this._lastOutput = @($this.LastPipeline.Output.ReadToEnd())
}
return $this._lastOutput
}
# ici $_ est égal à args
#Seul l'accés à la propriété Result renseigne la variable _lastOutput
$set_lastOutput = { $this._lastOutput = $_ }
$object | add-member ScriptProperty Results $get_lastOutput $set_lastOutput
$object | add-member Noteproperty _lastOutput $null
# Add a getter for finding out the last exception thrown if any.
$get_lastException = {
if ($this.LastPipelineState -eq \"Failed\" -and -not $this.IsRunning)
{
return $this.LastPipeline.PipelineStateInfo.Reason
}
}
$object | add-member ScriptProperty LastException $get_lastException
# Add a getter script property that lets you get the last errors.
$get_lastError = {
if ($this._lastError -eq $null -and -not $this.IsRunning)
{
$this._lastError = @($this.LastPipeline.Error.ReadToEnd())
}
return $this._lastError
}
$set_lastError = { $this._lastError = $args[0] }
$object | add-member ScriptProperty LastError $get_lastError $set_lastError
$object | add-member Noteproperty _lastError $null
# Add a script method for stopping the execution of the pipeline.
$stopScript = {
if ($args.Count -gt 1)
{
throw 'Too many arguments. Usage: $object.Stop([optional] [bool] $async'
}
if ($args.Count -eq 1 -and [bool] $args[0])
{
$this.LastPipeline.StopAsync()
}
else
{
$this.LastPipeline.Stop()
}
}
$object | add-member ScriptMethod Stop $stopScript
# finally, attach the script to run to the object
$object | add-member Noteproperty Command $scriptToRun
# Ensure that the object has a \"type\" for which we can build a
# formatting file.
$object.Psobject.typenames[0] = \"PowerShellJobObject\"
if ($RunInstance)
{
$object.InvokeAsync($scriptToRun)
}
$object
}#New-RunSpace.ps1[/code:1]

La pièce jointe New_Runspace.ps1 est absente ou indisponible

<br><br>Message édité par: Laurent Dardenne, à: 29/10/09 14:53

Tutoriels PowerShell
Pièces jointes :

Connexion ou Créer un compte pour participer à la conversation.

Plus d'informations
il y a 15 ans 3 mois #5165 par Laurent Dardenne
Correction d'un effet de bord
[code:1] # V1.1
# FIX :
# $set_lastOutput = { $this._lastOutput = $_ }
$set_lastOutput = { $this._lastOutput = $args[0] }[/code:1]

Tutoriels PowerShell

Connexion ou Créer un compte pour participer à la conversation.

Plus d'informations
il y a 15 ans 3 mois #5166 par Laurent Dardenne
Dans cette introduction sur les nouveautés de la version de PowerShell V2 j'avais abordé rapidement les runspaces.
Notez que les liens sur les cmdlets *-Runspace ne sont plus valide, on parle désormais de job et de session, bien que l'usage de runspace par le moteur de PS reste identique voir amélioré.
about_PSSessions ,
about_Job ,
about_Job_Details .

Une exécution asynchrone permet d'exécuter du code PowerShell dans un thread différent de celui de la console, ainsi c'est l'OS qui switchera entre les threads créés.
On parle aussi d'exécution simultanée, enfin pour les machines multi-coeurs/processeurs.
Sous PS v1 la création de thread à l'aide des API dotnet n'est pas possible on doit donc utiliser un runspace (je n'ai pas vérifié pour la v2).
Ce runspace construit une nouvelle session PowerShell \&quot;basique\&quot;, dans le sens où il ne contient pas les cmdlets additionnels ni le code présent dans les profiles et enfin son environnement est séparé de celui de PowerShell.
Il existe donc un cloisonnement entre les 2 sessions.

PowerShell lui même utilise des threads lors de l'exécution de code comme on peut le voir dans la console
(pour rappel un process a au moins un thread, lui-même (le primary thread)) :
[code:1]
[Threading.Thread]::CurrentThread.ManagedThreadId|%{\&quot;ID : $_\&quot;}
[Threading.Thread]::CurrentThread.ManagedThreadId|%{\&quot;ID : $_\&quot;}
[Threading.Thread]::CurrentThread.ManagedThreadId|%{\&quot;ID : $_\&quot;}
[/code:1]
Chaque exécution se fait dans un thread différent.

Pour le code suivant ici aussi le pipeline est exécuté dans un autre thread que celui de PS mais tout au long de l'énumération c'est le même :
[code:1]
1..20|%{[Threading.Thread]::CurrentThread.ManagedThreadId}
[/code:1]
On peut interroger la liste des threads associé à un process :
[code:1]
$RS1=New-RunSpace { sleep 5}
(Get-Process|? {$_.name -match \&quot;powershell\&quot;}).threads.count
$RS1.InvokeAsync($RS1.Command)
(Get-Process|? {$_.name -match \&quot;powershell\&quot;}).threads.count
sleep 5
(Get-Process|? {$_.name -match \&quot;powershell\&quot;}).threads.count
$RS1.runspace.close()
(Get-Process|? {$_.name -match \&quot;powershell\&quot;}).threads.count
[/code:1]
On voit qu'après l'appel de $RS1.InvokeAsync le nombre de thread a augmenté.
Si le code du runspace, $RS1.Command, est terminé la fermeture du runspace n'a pas d'influence sur le nb de thread, car le pipeline est terminé et le thread associé n'existe plus.
Dans le cas contraire, l'appel à Close() termine le pipeline avant de libérer les ressources. On peut aussi utiliser simplement l'affectation de $null qui appelera, à un moment donné, Dispose() qui appellera à son tour la méthode Close() si le runspace est encore \&quot;ouvert\&quot;.
[code:1]
(Get-Process|? {$_.name -match \&quot;powershell\&quot;}).threads.count
$RS1=New-RunSpace { sleep 5}
$RS2=New-RunSpace { sleep 5}
$RS3=New-RunSpace { sleep 5}
(Get-Process|? {$_.name -match \&quot;powershell\&quot;}).threads.count
$RS1.InvokeAsync($RS1.Command)
(Get-Process|? {$_.name -match \&quot;powershell\&quot;}).threads.count
$RS2.InvokeAsync($RS2.Command)
(Get-Process|? {$_.name -match \&quot;powershell\&quot;}).threads.count
$RS3.InvokeAsync($RS3.Command)
(Get-Process|? {$_.name -match \&quot;powershell\&quot;}).threads.count
$RS1.Runspace.close()
$RS2.Runspace.close()
$RS3.Runspace.close()
(Get-Process|? {$_.name -match \&quot;powershell\&quot;}).threads.count
[/code:1]
Comme le montre l'exemple précédent, bien que les pipelines des runspaces ne soient pas terminés, l'appel à close diminue bien le nb de thread.
On ne doit se préoccuper que de la libération des ressources du runspace, celles liées au thread n'est pas ici de notre ressort.

Après ces qq explications basiques voyons de plus prés l'exécution de code dans un runspace.
[code:1]
$RS1=New-RunSpace { Dir c:\}
$RS1.InvokeAsync($RS1.Command)
$Rs1
$RS1.Results
[/code:1]
Pour cet exemple on exécute la commande dir, on constate que le résultat de son exécution n'est pas affiché dans la console.
Notez que l'objet $RS1 que l'on manipule est un objet personnalisé construit autour d'une instance de la classe Runspace.
Son type est
[code:1]
$Rs1.Psobject.typenames[0]
#PowerShellJobObject
[/code:1]
Ajoutons donc l'affichage en modifiant le scriptblock de notre objet runspace, on peut réutiliser un runspace avec un script différent :
[code:1]
$RS1.Command={ Dir c:\|% {Write-host \&quot;$_\&quot;}}
$RS1.InvokeAsync($RS1.Command)
$Rs1
$RS1.Results
$RS1
[/code:1]
Nous avons droit à une exception :

System.Management.Automation.CmdletInvocationException:
Impossible d'appeler cette fonction, car l'hôte actuel ne l'implémente pas.
---&gt; System.Management.Automation.Host.HostException

Ceci est du au fait que par défaut la fonction New-Runspace.ps1 construit un runspace sans lui associer de host, c'est à dire un programme hôte gérant l'affichage.
Cette fois nous devons recréer notre runspace :
[code:1]
$RS1.Runspace.Close()
$RS1=New-RunSpace -InteractWithHost { Dir c:\|% {Write-host \&quot;$_\&quot;}}
$RS1.InvokeAsync($RS1.Command)
$Rs1
$RS1.Results
$RS1
[/code:1]
Le résultat amène quelques commentaires.
Le premier est que l'affichage, selon le contenu du répertoire et la charge processeur, est erratique, ce qui est normal car 2 écrivains utilisent la même zone d'affichage mais sans concertation. Pour l'instant on fera avec.
Le deuxième est que la collection Results est vide, c'est normal puisque notre code, exécuté dans le runspace ne renvoie rien dans le pipeline, du runspace, mais sur la console (de PowerShell).
[code:1]
$RS1.Runspace.Close()
#réémet l'objet dnas le pipeline
$RS1=New-RunSpace -InteractWithHost { Dir c:\|% {Write-host \&quot;$_\&quot;;$_}}
$RS1.InvokeAsync($RS1.Command)
$Rs1
$RS1.Results
$RS1
[/code:1]
La console et le script utilise chacun un pipeline dédié, le runspace ne partage pas le pipeline de PowerShell.
La plupart du temps l'affichage n'est nécessaire que pour la mise au point du script.
Ceci dit l'affichage est une information envoyée par le runspace, si on souhaite contrôler la fin d'exécution du pipeline du runspace on consultera la propriété IsRunning :
[code:1]
$Rs1.IsRunning
$Rs1.LastPipelineState
$RS1.Runspace.Close()
[/code:1]
La propriété LastPipelineState nous informe sur l'état en cours du pipeline, vous trouverez ici les états possibles.
Sous PS v1 natif, on ne peut qu'effectuer du pooling pour contrôler la fin d'exécution du runspace, car on ne dispose pas d'une gestion d'événement (envoi de message entre le runspace et PowerShell).

Un exemple de ping simultané sur plusieurs serveurs :
[code:1]
#affectation multiple
$RS1,$RS2,$RS3=\&quot;127.0.0.1\&quot;,\&quot;127.0.0.0\&quot;,\&quot;214.58.0.9\&quot;|
Foreach-Object {
#On construit le code à exécuter
$Sb=$ExecutionContext.InvokeCommand.NewScriptBlock(\&quot;Get-WmiObject -query `\&quot;SELECT * FROM Win32_PingStatus WHERE Address='$_'`\&quot;\&quot;«»)
#Renvoi un runspace dont l'exécution du pipeline est activé
New-RunSpace $SB -RunInstance
}

$RS1,$RS2,$RS3| Foreach-Object{Write-Host (\&quot;Status du pipeline={0} Résultat du ping={1}\&quot; -F $_.LastPipeLineState,($_.Results).StatusCode)}
$RS1,$RS2,$RS3
[/code:1]
On construit le code du scriptblock car l'appel suivant ne fonctionne pas :
[code:1]
$RS=New-RunSpace \&quot;Get-WmiObject -query `\&quot;SELECT * FROM Win32_PingStatus WHERE Address='$_'`\&quot;\&quot; -RunInstance
[/code:1]
étant donné que la fonction New-Space attend un scriptblock et pas une string. Pour des raisons de sécurité PowerShell n'autorise pas le cast automatique d'une string vers un scriptblock.

Si on ne précise pas le switch RunInstance, on peut exécuter le pipeline de chaque runspace ainsi :
[code:1]
$RS1,$RS2,$RS3|% {$_.InvokeAsync($_.Command)}
$RS1,$RS2,$RS3
[/code:1]
Dans ce cas l'usage d'un tableau de job est préférable, comme le propose le script d'origine...

Tutoriels PowerShell

Connexion ou Créer un compte pour participer à la conversation.

Plus d'informations
il y a 15 ans 3 mois #5187 par Laurent Dardenne
Une petite précision avant de continuer, j'utilise le terme de multithreading par abus de langage, car à aucun moment on ne manipule d'objet représentant un thread, mais juste des runspaces dans lequel est exécuté un pipeline.

A partir du runspace il est possible de créer une variable dans la session de l'appelant :
[code:1]
$RS1.Runspace.Close()
dir variable:TestRSDir
dir variable:TestRS
$RS1=New-RunSpace -InteractWithHost {
$host.ui.WriteLine(\&quot;Création de la variable TestRS depuis le runspace `$RS1\&quot;«»)
$executionContextProxy.SessionState.PsVariable.Set(\&quot;TestRS\&quot;,\&quot;Création de variable\&quot;«»)
$C=dir c:\
$executionContextProxy.SessionState.PsVariable.Set(\&quot;TestRSDir\&quot;,$C)
$C=$null
}# RS1
$RS1.InvokeAsync($RS1.Command);
Sleep -m 100
dir variable:TestRSDir
dir variable:TestRS
#remove-variable TestRSDir;remove-variable TestRS
[/code:1]
La pause laisse le temps aux pipelines de s'exécuter, ainsi les variables seront bien accessibles depuis la session principale de PS.
Note :
A ce propos le code du cmdlet Start-Sleep utilise en interne des routines de gestion de thread.
On \&quot;endort\&quot; le thread principal de PS ainsi l'OS ne lui attribut provisoirement plus de temps d'exécution.

Bien que dans le runspace on affecte $null à la variable $C, la création d'une nouvelle référence via la variable $TestRSDir empêche la variable $C d'être libéré par le garbage collector.
[code:1]
#Force l'appel du garbage collector
[GC]::Collect([GC]::MaxGeneration)
$TestRSDir
[/code:1]
Il est également possible depuis le runspace d'accéder à une variable de la session PowerShell :
[code:1]
remove-variable TestRS
$RS1.Runspace.Close()
$TestRS=\&quot;Variable 'externe' au runspace.\&quot;

$RS1=New-RunSpace -InteractWithHost {
$VariableRS=$executionContextProxy.SessionState.PsVariable.Get(\&quot;TestRS\&quot;«»)
$host.ui.WriteLine(\&quot;Contenu de la variable TestRS depuis le runspace =$VariableRS\&quot;«»)
$host.ui.WriteLine(\&quot;Contenu de la variable TestRS depuis le runspace =`\&quot;$($VariableRS.Value)`\&quot;\&quot;«»)
sleep 1
$VariableRS=\&quot;Modifié dans le runspace\&quot;
}# RS1
$RS1.InvokeAsync($RS1.Command);
$TestRS
sleep 2
$TestRS
[/code:1]
On récupère une variable de la classe System.Management.Automation.PSVariable, on doit donc manipuler son champ Value pour accéder au contenu de l'objet de type String.
Dans l'exemple précédent on manipulait bien, dans la session PS et dans le runspace, des objets de type PSVariable. La différence est que dans la console la manipulation des objets de type PSVariable est transparente.
On s'aperçoit que dans un runspace c'est un peu différent.

Pour le dernier exemple, on pourrait s'attendre à ce que la modification de la variable $VariableRS dans le runspace soit répercutée dans la variable $TestRS de la session PowerShell.
Il n'en est rien car on affecte une nouvelle valeur à notre objet de type PSvariable et pas à l'objet Value, l'objet cible, de type String.
On doit ici aussi passer par le champ Value, modifiez la ligne suivante est ré exécutez le code complet de l'exemple précédent :
[code:1]
$VariableRS.Value=\&quot;Modifié dans le runspace\&quot;
[/code:1]
Cette fois ça fonctionne.

Cette manière de faire est possible grâce à la modification apportée au script d'origine par Lee Holmes, il \&quot;injecte\&quot; dans le runspace la variable globale $executionContext provenant de la session PowerShell :
[code:1]
$runspace.SessionStateProxy.SetVariable(\&quot;executionContextProxy\&quot;, $executionContext)
[/code:1]
Sans cette astuce on doit \&quot;injecter\&quot; chaque variable provenant de la session dans le runspace, certes elle facilite certaines opérations mais j'ai un doute sur le principe...
Bien qu'à l'origine cette modification ait été réalisée dans un but précis, j'ai laissé cette modification.
Peut être faut-il voir cette approche comme l'apport d'un nouvelle sorte de portée : le runspace ?
[code:1]
(dir variable:ExecutionContext).options
#Constant, AllScope
[/code:1]
A moins que soit un cadeau pour le debugger ;-)


Bref, voyons quelques cas intéressants :
[code:1]
#---- Erreur ( se déclenche selon les cas .Après + exécution
$Data = New-Object System.Collections.ArrayList(250)

$RS1=New-RunSpace -InteractWithHost {
$host.ui.WriteLine(\&quot;Début de RS1\&quot;«»)
$Array=$executionContextProxy.SessionState.PsVariable.Get(\&quot;Data\&quot;«»).Value
$nb=$Array.count
$nb..($nb+250)|
Foreach {
[void]$Array.Add($_.ToString())
}
$host.ui.WriteLine(\&quot;Fin de RS1\&quot;«»)
}# RS1

$RS1.InvokeAsync($RS1.Command);$Data|% -begin {\&quot;Début énumération\&quot;} -process{\&quot;élement $_ ($( $data.count))\&quot;}
[/code:1]
Exécutez ce script dans une nouvelle session.
La première exécution ne pose pas de problème, ni la seconde par contre \&quot;à un moment donné\&quot; les suivantes poseront problème.
Elles provoqueront l'exception suivante :
[code:1]
Une erreur s'est produite lors de l'énumération parmi une collection : La collection a été modifiée ; l'opération d'énumération peut ne pas s'exécuter.
[/code:1]
Oups!
C'est une protection du Framework .NET qui interdit de modifier une collection lors de son énumération.
Comme indiqué ici :

Un énumérateur reste valide tant que la collection demeure inchangée.
Si la collection est modifiée en ajoutant, modifiant ou supprimant des éléments,
l'énumérateur devient irrévocablement non valide et le prochain appel à MoveNext ou à Reset lève InvalidOperationException.

Le comportement varie selon que la collection est vide, cas de la première exécution, ou pas. Le nombre de thread \&quot;écrivains\&quot; ne change rien au problème.
Une fois connu, ce problème peut être évité, on peut modifier une collection dans plusieurs thread mais son énumération ne doit se faire qu'une fois tous les runspaces, la manipulant, terminé.
On peut faire plein de choses en même temps avec les runspaces, mais pas n'importe quoi :-)

Enfin une seconde exécution du pipeline du runspace alor que la première n'est pas terminée :
[code:1]
$RS1.InvokeAsync($RS1.Command);$RS1.InvokeAsync($RS1.Command)
[/code:1]
Provoquera une exception
[code:1]
Exception lors de l'appel de « InvokeAsync » avec « 1 » argument(s) : « ScriptHalted »
[/code:1]<br><br>Message édité par: Laurent Dardenne, à: 25/08/09 23:17

Tutoriels PowerShell

Connexion ou Créer un compte pour participer à la conversation.

Plus d'informations
il y a 15 ans 3 mois #5211 par Laurent Dardenne
Un autre cas peut se présenter, la modification d'une même variable par plusieurs threads.
Le scénario suivant, trouvé sur le net, met en évidence le fait que 2 threads peuvent accéder simultanément à une même information.
Dans ce cas il est possible que la variable $val2 puissent être affectée à zéro par un thread alors qu'un autre est en train d'exécuter le code associé au if, puisqu'elle ne vallait pas zéro lors du test :
[code:1]
if ($val2 -ne 0)
{ write-host $\&quot;($val1.value / $val2.value)\&quot; }
[/code:1]
Dans ce cas on obtient une division par zéro :
[code:1]
$Valeur1=10
$Valeur2=2

$RS1=New-RunSpace -InteractWithHost {
$host.ui.WriteLine(\&quot;Début de RS1\&quot;«»)
#Force un décalage dans le temps de l'exécution
sleep -m 50
$Val1=$executionContextProxy.SessionState.PsVariable.Get(\&quot;Valeur1\&quot;«»)
$Val2=$executionContextProxy.SessionState.PsVariable.Get(\&quot;Valeur2\&quot;«»)

if ($val2 -ne 0)
{ write-host $\&quot;($val1.value / $val2.value)\&quot; }
else
{ write-host \&quot;Division impossible dans RS1\&quot; }
$val2.value= 0;
} #RS1

$RS2=New-RunSpace -InteractWithHost {
$host.ui.WriteLine(\&quot;Début de RS2\&quot;«»)
$Val1=$executionContextProxy.SessionState.PsVariable.Get(\&quot;Valeur1\&quot;«»)
$Val2=$executionContextProxy.SessionState.PsVariable.Get(\&quot;Valeur2\&quot;«»)

if ($val2 -ne 0)
{ write-host $\&quot;($val1.value / $val2.value)\&quot; }
else
{ write-host \&quot;Division impossible dans RS2\&quot; }
$val2.value= 0;
}# RS2

cls
$RS1,$RS2|% {$_.InvokeAsync($_.Command)}
sleep -m 50
$RS1,$RS2|% {\&quot;State :{0} Exception : {1}\&quot; -F $_.LastPipelineState,$_.LastException.Message}
[/code:1]
On teste bien le cas d'erreur, mais il ne se produit pas, \&quot;Division impossible\&quot; n'est jamais affiché !

Pour éviter ce problème on doit verrouiller la variable le temps d'effectuer le traitement.
Sans rentrer dans les détails, j'ai choisi d'utiliser des mutex .
Son rôle est identique à un agent de la circulation à un carrefour, il évite les accidents mais peut provoquer des embouteillages ;-).

Le code est verbeux afin de tracer l'exécution :
[code:1]
$Valeur1=10
$Valeur2=2
#Créé une variable référence renseigné par le constructeur du mutex
[ref]$createdNew = $false
#Crée l'objet mutex
$Mutex = New-Object System.Threading.Mutex $false, \&quot;RSMutex\&quot;, $createdNew
if (!$createdNew)
{Write-host \&quot;Le mutex existe déja.\&quot;}
else {
$RS1=New-RunSpace -InteractWithHost {
#On doit impérativement libérer un mutex 'acquis'
trap {$_Mutex.Value.ReleaseMutex();write-host \&quot;EXCEPTION RS1\&quot;;Break}
$host.ui.WriteLine(\&quot;Début de RS1\&quot;«»)
sleep -m 50
$Val1=$executionContextProxy.SessionState.PsVariable.Get(\&quot;Valeur1\&quot;«»)
$Val2=$executionContextProxy.SessionState.PsVariable.Get(\&quot;Valeur2\&quot;«»)
$_Mutex=$executionContextProxy.SessionState.PsVariable.Get(\&quot;Mutex\&quot;«»)
$host.ui.WriteLine(\&quot;RS1 get\&quot;«»)
#acquiert le mutex ou attend indéfiniment qu'il soit disponible
#On peut ajouter un délai avec échec
$result = $_Mutex.Value.WaitOne()
if ($val2.value -ne 0)
{ write-host \&quot;$($val1.value / $val2.value)\&quot; }
else
{ write-host \&quot;Division impossible dans RS1\&quot; }

$val2.value= 0;
#Libére le mutex, un autre thread peut désormais l'acquérir
$_Mutex.Value.ReleaseMutex()
$host.ui.WriteLine(\&quot;RS1 release\&quot;«»)
} #RS1

$RS2=New-RunSpace -InteractWithHost {
trap {$_Mutex.Value.ReleaseMutex();write-host \&quot;EXCEPTION RS2\&quot;;Break}
$host.ui.WriteLine(\&quot;Début de RS2\&quot;«»)
$Val1=$executionContextProxy.SessionState.PsVariable.Get(\&quot;Valeur1\&quot;«»)
$Val2=$executionContextProxy.SessionState.PsVariable.Get(\&quot;Valeur2\&quot;«»)
$_Mutex=$executionContextProxy.SessionState.PsVariable.Get(\&quot;Mutex\&quot;«»)
$host.ui.WriteLine(\&quot;RS2 get\&quot;«»)
$result = $_Mutex.Value.WaitOne()
if ($val2.value -ne 0)
{ write-host \&quot;$($val1.value / $val2.value)\&quot; }
else
{ write-host \&quot;Division impossible dans RS2\&quot; }
$val2.value= 0;
$_Mutex.Value.ReleaseMutex()
$host.ui.WriteLine(\&quot;RS2 release\&quot;«»)
}# RS2

cls
$RS1,$RS2|% {$_.InvokeAsync($_.Command)}
sleep -m 50
$valeur1
$valeur2
$rs1,$rs2|% {\&quot;State :{0} Exception : {1}\&quot; -F $_.LastPipelineState,$_.LastException.Message}

}
#La variable $Mutex utilise des ressources systèmes
$Mutex.Close()
[/code:1]
Le contrôle de l'accès à une variable à l'aide d'un mutex fonctionne.
On doit prévenir un cas probable mais pas certains. Commenter les lignes contenant l'instruction sleep, modifiera le résultat.

Sous PowerShell on manipule souvent des collections d'objets, le framework .NET propose des collections synchronisées pour un usage dit thread-safe, c'est à dire protégé.
On s'assure qu'un et un seul thread accède à une donnée.
On utilise, la méthode Synchronized s'il elle est disponible pour la classe :
[code:1]
#Crée un arraylist
$Data = New-Object System.Collections.ArrayList(250)
#Crée un arraylist thread-safe avec la variable $Data
$SynchronizedData=[System.Collections.ArrayList]::«»Synchronized($Data)
# Le mieux étant
$SynchronizedData=[System.Collections.ArrayList]::«»Synchronized((New-Object System.Collections.ArrayList(250)))
[/code:1]
C'est tout.
Cet objet encapsule les données et les accès à la collection, chaque méthode la modifiant la verrouillera automatiquement.
Sachez que son type est SyncArrayList et qu'il n'est pas \&quot;formaté\&quot; par PS, on doit utiliser le champ psbase pour accéder aux détails de ses membres :
[code:1]
$SynchronizedData.psbase|gm
$SynchronizedData.add(\&quot;test\&quot;«»)
$SynchronizedData
$SynchronizedData.IsSynchronized
[/code:1]
Le fait de protéger ainsi la collection ne résout pas pour autant le problème de l'énumération vu précédement...

La suite, essayer de partager du code et des données entre PS et une WinForm :woohoo:<br><br>Message édité par: Laurent Dardenne, à: 27/08/09 13:29

Tutoriels PowerShell

Connexion ou Créer un compte pour participer à la conversation.

Plus d'informations
il y a 15 ans 3 mois #5214 par Laurent Dardenne
L'affichage d'une winform sous PowerShell pose un petit problème, la console est inaccessible tant que la fenêtre est active.
Comme l'explique Janel dans ce post l'usage de runspace peut régler ce problème.

Puisque qu'un runspace peut prendre en charge un traitement en tâche de fond, essayons de faire en sorte d'avoir
1- une console PS disponible,
2- une winform active,
3- un traitement de peuplement d'une listbox contenue dans la form.

1- une console PS disponible
Placez-vous dans le répertoire contenant le script de création de la winform.
Ce script ne doit pas contenir d'appel à ShowDialog(), ni à Dispose() :
[code:1]
#On crée la fenêtre dans la portée locale
.\&quot;$pwd\TestFrmRunSpace1.ps1\&quot;

$RSShowDialog=New-RunSpace -InteractWithHost {
Write-host \&quot;[RSShowDialog.ID:$(([System.Management.Automation.Runspaces.Runspace]::«»DefaultRunSpace).InstanceId)] Call ShowDialog\&quot;
#Ce runspace appelle la Form déclarée dans la session PS
#cette function est bloquante
$Form1.ShowDialog()
}

$RSShowDialog.Runspace.SessionStateProxy.SetVariable(\&quot;Form1\&quot;, $Form1)

#Affiche la form
$RSShowDialog.InvokeAsync($RSShowDialog.Command)
dir
[/code:1]
Dans le runspace on trace son identifiant, on affiche la fenêtre, puis on injecte la variable $Form1 créée dans la session PS, on utilisera un nom identique dans les 2 threads, veillez à bien injecter les variables que vous utilisez, le debug est assez fastidieux.
Enfin on exécute en tâche de fond le code affichant la fenêtre.

Désormais vous pouvez utiliser la console tout en ayant une winform active en tâche de fond.
Si vous avez utilisé le script joint, vous constaterez qu'un click sur le bouton \&quot;Fermer\&quot; affiche une exception, on doit terminer PowerShell pour sortir de cette situation.

Il se passe que le script de création de la form contient des eventhandlers, le code associé à un événement objet, ici un composant graphique.
[code:1]
function OnClick_btnClose($Sender,$e){
$Form1.Close()
}
[/code:1]
Notre runspace ne connait pas les fonctions crée dans le provider function: de la session PowerShell, c'est le principe d'un runspace, on cloisonne.
Il nous faut donc recréer dans le runspace les fonctions utilisées par le script de création de la forme. Celles qu'il crée et celles qu'il référencerait, déclarées par exemple dans le(s) profile(s) de PS.

On a deux approche, soit on passe en arguments le code de nos fonctions lors de l'appel à InvokeAsync() du runspace cible, soit on utilise les API PowerShell.
J'ai un faible pour les secondes ;-)

Création d'une configuration
Rapidement une configuration de runspace définie son comportement, chaque runspace en possède une.
On peut consulter celle de PowerShell :
[code:1]
([System.Management.Automation.Runspaces.Runspace]::«»DefaultRunspace).RunspaceConfiguration
[/code:1]
La fonction New-RunSpace crée un runspace avec une configuration par défaut. Pour régler notre problème on doit ajouter nos fonctions dans la collection nommée Scripts.
Son type est RunspaceConfigurationEntryCollection`1, c'est à dire une classe générique fermée, et contient uniquement des objets de type ScriptConfigurationEntry :
[code:1]
([System.Management.Automation.Runspaces.Runspace]::«»DefaultRunspace).RunspaceConfiguration.Scripts
.GetType().Fullname
[/code:1]
Cette collection est créée par le runspace, on n'a pas à prendre en charge la création d'un type générique
On crée un objet ScriptConfigurationEntry pour chaque fonction à injecter dans le runspace, on ne récupère que le corps des méthodes :
[code:1]
#
Recopie une fonction existante dans une configuration de Runspace
$ConfigurationRS = [Management.Automation.Runspaces.RunspaceConfiguration]::Create()
#Crée les entrées contenant les fonctions à injecter dans le runspace
$EntryFormClosing=New-object System.Management.Automation.Runspaces.ScriptConfigurationEntry(
\&quot;OnFormClosing_Form1\&quot;,((Get-Item Function:OnFormClosing_Form1).Definition) )
$EntryBtnClose=New-object System.Management.Automation.Runspaces.ScriptConfigurationEntry(
\&quot;OnClick_btnClose\&quot;,((Get-Item Function:OnClick_btnClose).Definition) )
[/code:1]
Ensuite l'ajout doit être suivi d'un appel à la méthode Update() :
[code:1]
$ConfigurationRS.Scripts.Append($EntryFormClosing)
$ConfigurationRS.Scripts.Append($EntryBtnClose)
#Met à jour la configuration
$ConfigurationRS.Scripts.Update()
[/code:1]
Ceci ne peut se faire que si le runspace est dans l'état BeforeOpen. Il n'est donc pas possible de faire ceci :
[code:1]
$RS.Close(),$Col.Insert($Entry),$Col.Update(),$RS.Open()
[/code:1]
Il ne reste plus qu'a modifier le script New-Space en ajoutant un nouveau paramètre :
[code:1]
Function New-Runspace( [scriptblock]$scriptToRun,
[Management.Automation.Runspaces.RunspaceConfiguration] $configuration=$null,
[switch] $RunInstance,
[switch] $InteractWithHost
)
...
# Create a runspace and open it
if ($configuration -eq $null )
#L'appelant fournit la configuration du runspace : cmdlets, scripts,...
#Sinon une fois le runspace ouvert on ne peut pas la mettre à jour
{$configuration = [Management.Automation.Runspaces.RunspaceConfiguration]::Create()}

if ($InteractWithHost)
{
#Accéde au host de PS, permet l'affichage
$runspace = [System.Management.Automation.Runspaces.RunspaceFactory]::CreateRunspace($Host,$configuration)
}
else
{
#Pas d'affichage possible sur la console de PS
$runspace = [System.Management.Automation.Runspaces.RunspaceFactory]::CreateRunspace($configuration)
}
$runspace.Open()
[/code:1]
Le code de création devenant :
[code:1]
#On crée la fenêtre dans la portée locale
.\&quot;$pwd\TestFrmRunSpace1.ps1\&quot;
#
Recopie une fonction existante dans une configuration de Runspace
$ConfigurationRS = [Management.Automation.Runspaces.RunspaceConfiguration]::Create()
#Crée les entrées contenant les fonctions à injecter dans le runspace
$EntryFormClosing=New-object System.Management.Automation.Runspaces.ScriptConfigurationEntry(
\&quot;OnFormClosing_Form1\&quot;,((Get-Item Function:OnFormClosing_Form1).Definition) )
$EntryBtnClose=New-object System.Management.Automation.Runspaces.ScriptConfigurationEntry(
\&quot;OnClick_btnClose\&quot;,((Get-Item Function:OnClick_btnClose).Definition) )
$ConfigurationRS.Scripts.Append($EntryFormClosing)
$ConfigurationRS.Scripts.Append($EntryBtnClose)
#Met à jour la configuration
$ConfigurationRS.Scripts.Update()

$RSShowDialog=New-RunSpace {
Write-host \&quot;[RSShowDialog.ID:$(([System.Management.Automation.Runspaces.Runspace]::«»DefaultRunSpace).InstanceId)] Call ShowDialog\&quot;
#Ce runspace appelle la Form déclarée dans la session PS
#cette function est bloquante
$Form1.ShowDialog()
} -InteractWithHost -configuration $configurationRS

$RSShowDialog.Runspace.SessionStateProxy.SetVariable(\&quot;Form1\&quot;, $Form1)

#Affiche la form
$RSShowDialog.InvokeAsync($RSShowDialog.Command)
dir
[/code:1]
Maintenant si vous cliquez sur le bouton \&quot;Fermer\&quot; la fenêtre se ferme correctement. Reste à récupérer le résultat de l'appel à ShowDialog():
[code:1]
$RSShowDialog.results
[/code:1]
La suite étant de mettre en place un traitement de peuplement d'une listbox contenu dans la forme.
L'approche de Janel fonctionne ici aussi :
[code:1]
$RSShowDialog.InvokeAsync($RSShowDialog.Command)
[void]$lstBxInformations.Items.Add(\&quot;test\&quot;«»)
[/code:1]
Mais, hé oui il y a un mais...

Note:
Le fichier joint contient la nouvelle version du script New-Space et le code de démo.

La pièce jointe New_Space_v1_2___Demo_WinForm.zip est absente ou indisponible


Tutoriels PowerShell

Connexion ou Créer un compte pour participer à la conversation.

Temps de génération de la page : 0.140 secondes
Propulsé par Kunena