Question The powers hell ou la puissance de l'enfer

Plus d'informations
il y a 11 ans 10 mois #2882 par Laurent Dardenne
J'ai trouvé ce script sur le blog d'un développeur japonais.
Ce script implémente les closures , +- autorise la persistence des variables locales entre plusieurs appels de fonction.

On peut y voir, difficilement :woohoo:, que le PS peut être un vrai casse-tête.
Je propose aux courageux une petite séance de reverse, en gros c'est comment que ça marche :blink:
[code:1]
function global:closure
{ #From csharper.blog57.fc2.com/blog-entry-218.html

param ([ScriptBlock]$private:«»script)
trap { break; }

#Vérification de l'argument
if ($null -eq $script)
{ throw 'The script is null.' }
#La variable ClosureStore, de type hash table, maintient la liste des closures
if ($null -eq $global:ClosureStore)
{
Set-Variable 'ClosureStore' @{} -Scope 'global' -Option 'Constant, AllScope';
}
#Supprime dans la hastable les éléments qui ont été collectés par le GC
($ClosureStore.GetEnumerator() | ? { !$_.Value.IsAlive; }) | ? { $() -ne $_; } | % { $ClosureStore.Remove($_.Key); };

#Mémorisation de l'environnement de la portée enfant (toutes les variables sauf les variables automatiques)
$autoVariableNames =
@(
'$', '?', '^', '_', 'args', 'ConfirmPreference', 'ConsoleFileName', 'DebugPreference', 'Error', 'ErrorActionPreference',
'ErrorView', 'ExecutionContext', 'false', 'FormatEnumerationLimit', 'foreach', 'HOME', 'Host', 'input', 'LASTEXITCODE', 'lastWord',
'line', 'Matches', 'MaximumAliasCount', 'MaximumDriveCount', 'MaximumErrorCount', 'MaximumFunctionCount', 'MaximumHistoryCount', 'MaximumVariableCount', 'MyInvocation', 'NestedPromptLevel',
'null', 'OutputEncoding', 'PID', 'PROFILE', 'ProgressPreference', 'PSHOME', 'PWD', 'ReportErrorShowExceptionClass', 'ReportErrorShowInnerException', 'ReportErrorShowSource',
'ReportErrorShowStackTrace', 'ShellId', 'StackTrace', 'switch', 'true', 'VerbosePreference', 'WarningPreference', 'WhatIfPreference'
);
$private:environment = & { return Get-Variable | ? { $autoVariableNames -notcontains $_.Name }; };

#Création de l'objet Closure, combinant le scriptbloc reçu en paramétre et l'environnement.
$private:closure =
New-Object 'PSObject' |
Add-Member 'Script' $script -MemberType 'NoteProperty' -PassThru |
Add-Member 'Environment' $environment -MemberType 'NoteProperty' -PassThru;
# Mémorisation de la fermeture dans le magasin. Le GUID est la clé qui désigne la fermeture, passée en tant une référence faible (weak reference)
$private:closureId = [Guid]::NewGuid();
$ClosureStore.Add($closureId, [WeakReference]$closure);

#Construit l'appel de la fermeture, on y insére le GUID dynamiquement
$private:invokerText = \"InvokeClosureScript `\"$closureId`\" `$Args;\";

#On convertit le texte en un script, tout en castant le résultat dans le type PSObject
#Ce script block exécutera le traitement réel contenu dans la fermeture
$private:invoker = [PSObject](Invoke-Expression \"{ $invokerText }\"«»);

#La durée de vie du script et de l'environnement est dépendant du fait que l'environnement est attaché au scriptblock
Add-Member -InputObject $invoker -Name 'Closure' -Value $closure -MemberType 'NoteProperty';
return $invoker;
}

function global:InvokeClosureScript
{
param ([Guid]$private:closureId, [Array]$private:Args_)

#On récupére la référence faible sur la fermeture hébergé dans le magasin ($ClosureStore)
$private:closure = $ClosureStore[$closureId].Target;
if ($null -eq $closure)
{ throw 'The closure which in ID which it appoints relation is attached does not exist.'; }

#L'appel de fonction est construit dynamiquement
$private:invokerText = 'param ([ScriptBlock]$private:«»script, [Array]$private:Args_) .$script';

#On ajoute les possibles paramètres
for ($private:i = 0; $i -lt $Args_.Length; $i++) { $invokerText += \" `$Args_[$i]\"; }

#On convertit le texte en un script,
$private:invoker = Invoke-Expression \"{ $invokerText }\";

#On charge l'environnement et on exécute la fermeture dans la portée d'enfant
$private:result =
&{
#Charge l'environnement
$Args[1].Environment | % { trap { continue; } $ExecutionContext.SessionState.PSVariable.Set($_); };
#Execute la fermeture
return .$Args[0] $Args[1].Script $Args[2];
} $invoker $closure $Args_;
return $result;
}
[/code:1]
Un exemple de fonction
[code:1]
function NewCounter
{
$i = 0;
return closure {
$i++;
return $i;
};
}

$counter = NewCounter;
$counter
&$counter; # renvoie 1
&$counter; # renvoie 2
&$counter; # renvoie 3
[/code:1]
Un exemple de fonction utilisant un paramètre
[code:1]
function NewDecorator
{
param ([string]$decoration)
return closure {
param ([string]$text)
return $decoration + $text + $decoration;
};
}

$sharpDecorator = NewDecorator '#';
$sharpDecorator
&$sharpDecorator \"hoge\"; # renvoie #hoge#
&$sharpDecorator \"fuga\"; # renvoie #fuga#
&$sharpDecorator \"piyo\"; # renvoie #piyo#
[/code:1]
J'oubliais l'aspirine n'est pas fournie :-)
L'objectif est certes différent de celui de l' Obfuscated PowerShell mais pas loin !

A propos des références faibles sous .NET:
merlin.developpez.com/cours/dotnet/WeakReference/

Tutoriels PowerShell

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

Plus d'informations
il y a 11 ans 10 mois #2891 par Laurent Dardenne
Voci quelques informations au sujet de ces 2 fonctions.

Les acteurs de l'avant-plan
[code:1]
function NewCounter
{
$i = 0
return closure {
$i++;
return $i
};
}

$counter = NewCounter
$counter
&$counter; # 1
[/code:1]
La fonction NewCounter est une fonction quelconque utilisant une closure.
$i est une variable locale à la fonction NewCounter.
Les lignes suivantes :
[code:1]
return closure {
$i++;
return $i
};
[/code:1]
sont constituées :
-d'une instruction return,
-d'une création de closure (appel à function:Closure). On lui passe en paramètre un scriptblock qui incrémente la variable locale $i puis renvoi le résultat.
Notez qu'ici l'auteur crée en quelque sorte une nouvelle fonctionnalité du langage, cf. DSL((Domain-Specific Language).

L'affectation suivante :
[code:1]
$counter = NewCounter
[/code:1]
appelle la fonction NewCounter qui initialise la variable locale $i et la closure.
$Counter affiche le contenu de la variable $counter qui est de type scriptblock :
[code:1]
InvokeClosureScript \"5cd56fbb-86a8-44d9-a7eb-bcbf8770760e\" $Args;
[/code:1]
Cette variable contient un membre synthétique nommé Closure
[code:1]
$counter.Closure
[/code:1]
Il est constitué des 2 membres suivants :
[code:1]
$counter.Closure.Script
$counter.Closure.Environment
[/code:1]
$counter.Closure.Script est un scriptblock qui contient {$i++;return $i};
$counter.Closure.Environment est un tableau de variables qui contient les variables locales déclarées dans la fonction NewCounter et utilisées dans Closure.Script. Pour rappel $i n'est pas accessible hors de la fonction NewCounter. On doit donc mémoriser le \"contexte d'exécution\" pour retrouver les variables dans l'état où elles étaient lors du dernier appel. On ne mémorise pas les variables automatiques.
Le scriptblock Closure.Script est en quelque sorte le corps de la méthode de la fonction NewCounter.
Son fonctionnement ressemble, de loin, à celui du code suivant :
[code:1]
$global:$i = 0
function NewCounter
{
$global:$i++;
return $global:$i
}
[/code:1]
Tout en sachant que dans ce cas la variable $i est accessible dans toutes les portées, console, script,etc.

La fonction closure crée une variable globale constante nommée $ClosureStore de type hash table.
[code:1]
Set-Variable 'ClosureStore' @{} -Scope 'global' -Option 'Constant, AllScope';
[/code:1]
Cette variable globale mémorise le scriptblock à exécuter (Closure.Script) et l'environnement dans lequel l'exécuter (Closure.Environment). L'environnement référence toutes les variables, sauf les variables automatiques, existantes au moment de l'appel à la fonction Closure.

Les acteurs d'arrière-plan :
TODO

Tutoriels PowerShell

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

Plus d'informations
il y a 11 ans 10 mois #2894 par Laurent Dardenne
Les acteurs d'arrière-plan :
1)La fonction globale Closure
Cette fonction est appelée à chaque fois que l'on crée une closure (fermeture) :
[code:1]
$counter = NewCounter
$counter2 = NewCounter #Crée une deuxiéme closure
[/code:1]
pour le vérifier :
[code:1]
$ClosureStore
#Name Value
#----
#730713d6-127a-4350-b74c-746... System.WeakReference
#5cd56fbb-86a8-44d9-a7eb-bcb... System.WeakReference
[/code:1]
Enfin plus exactement c'est la ligne d'instructions suivante, présente dans le code de la fonction NewCounter, qui crée la fermeture :
[code:1]
return closure {
[/code:1]
le code suivant :
[code:1]
&$Counter
[/code:1]
Ne crée pas de fermeture mais l'exécute, le & commerciale exécutant un scriptblock, on a vu précédement que son contenu était :
[code:1]
InvokeClosureScript \"5cd56fbb-86a8-44d9-a7eb-bcbf8770760e\" $Args;
[/code:1]
voyons le détail de cette ligne :
-Nous aborderons plus avant la fonction InvokeClosureScript
-\"5cd56fbb-86a8-44d9-a7eb-bcbf8770760e\" est un GUID, donnée pseudo-aléatoire. Ce GUID est la clé de la fermeture mémorisée dans la hastable $ClosureStore.
[code:1]
$private:closureId = [Guid]::NewGuid();
[/code:1]
-$Args est un tableau contenant les arguments optionnels utilisés par le script de la fermeture.


Les références faibles
Pour plus de détails sur le sujet vous pouvez consulter ce tutoriel : merlin.developpez.com/cours/dotnet/WeakReference/
Rapidement les références faibles vont permettre de faciliter le cycle de vie des objets inactifs(détruit) dans la hashtable $ClosureStore.

Le probléme qui se pose est le suivant :
Un même objet, notre fermeture, est référencé par un scriptbloc enrichi d'un membre synthétique de type NoteProperty ($Counter.Closure) et par le magasin $ClosureStore.
Tant qu'un objet est référencé par une variable il ne peut être supprimé par le garbage collector. Dans ce cas on parle de références fortes.
[code:1]
#Création de l'objet Closure, combinant le scriptbloc reçu en paramétre et l'environnement d'exécution. $private:closure =
New-Object 'PSObject' |
Add-Member 'Script' $script -MemberType 'NoteProperty' -PassThru |
Add-Member 'Environment' $environment -MemberType 'NoteProperty' -PassThru;

# Mémorisation dans le magasin dédié de la fermeture.
#Le GUID est la clé qui désigne la fermeture, passée en tant une référence faible (weak reference)
$private:closureId = [Guid]::NewGuid();
$ClosureStore.Add($closureId, $closure);

Add-Member -InputObject $invoker -Name 'Closure' -Value $closure -MemberType 'NoteProperty';
[/code:1]
Avec ce code, modifié[/u] pour l'explication, si on supprime la fermeture :
[code:1]
$Counter=$null
[/code:1]
on ne supprime pas l'entrée correspondante dans la hashtable $ClosureStore. Et surtout il n'est pas possible de savoir si une référence est active ou non, c'est à dire si l'objet a été détruit.
De plus pour chaque suppression de fermeture il nous faudrait supprimer l'entrée correspondante dans le magasin. Tout en sachant qu'on ne connaît pas directement l'entrée associée dans le magasin.

Si tel était le cas le nombre d'entrée dans la hashtable $ClosureStore augmenterait sans cesse tout en contenant des informations périmées/inutilisées.

Avec l'usage de la référence faible, c'est à dire avec le code suivant,
[code:1]
$ClosureStore.Add($closureId, [WeakReference]$closure);
[/code:1]
notre objet fermeture peut être supprimé automatiquement car une fois la variable $Counter supprimée il ne reste plus, dans le magasin, aucune référence forte sur cet objet mais seulement une référence faible.
La référence faible permettra :
- au GC de libérer rééllement notre fermeture($Counter),
- et à notre fonction globale Closure de supprimer les entrées inactives (IsAlive=False) dans le magasin :
[code:1]
#Supprime dans la hastable les éléments qui ont été collectés par le GC
($ClosureStore.GetEnumerator() | ? { !$_.Value.IsAlive; }) | ? { $() -ne $_; } | % { $ClosureStore.Remove($_.Key); };
[/code:1]
L'instruction [WeakReference]$closure encapsule l'objet $closure au sein d'une instance de la classe WeakReference.

Dit autrement dans le SDK :

Le mécanisme de garbage collection du Common Language Runtime récupère des objets inaccessibles de la mémoire. Un objet devient inaccessible si toutes les références directes et indirectes à cet objet ne sont plus valides, par exemple si ces références ont la valeur référence Null (Nothing en Visual Basic). Une référence à un objet accessible est appelée référence forte.

Une référence faible fait également référence à un objet accessible, appelé la cible. Un utilisateur crée une référence forte à la cible en assignant la valeur de la propriété Target à une variable. Toutefois, s'il n'y a pas de référence forte à la cible, celle-ci est susceptible d'être traitée par le garbage collection, même s'il existe une référence faible à l'objet.


Affichons quelques informations à propos de la gestion de la durée de vie des entrées de la table $ClosureStore.
Ouvrez une nouvelle session de PowerShell, exécutez le script Closure.ps1:
[code:1]
. .\Closure.ps1
[/code:1]
puis créeons ces deux fonctions :
[code:1]
function NewCounter
{
$i = 0;
return closure {
$i++;
return $i;
};
}

function NewDecorator
{
param ([string]$decoration)
return closure {
param ([string]$text)
return $decoration + $text + $decoration;
};
}
[/code:1]
Ensuite créons 2 fermetures et affichons le contenu du magasin :
[code:1]
$counter = NewCounter
$sharpDecorator = NewDecorator '#';

$ClosureStore.GetEnumerator() | ? { $_.Value.IsAlive; }
#Name Value
#----
#248b8272-76ce-4054-9051-d28... System.WeakReference
#89e00708-d51a-4416-abb1-d6e... System.WeakReference

$ClosureStore.GetEnumerator() | % {$_.value }
# IsAlive TrackResurrection Target
#


# True False @{Script=$i++;...
# True False @{Script=param([string]$text) return...
[/code:1]
Le champ IsAlive est un membre de la classe WeakReference et nous indique si la référence, c'est à dire une entrée de notre hashtable, est toujours accessible (actif).
Créons une troisiéme fermeture mais en utilisant une variable existante, ici $Counter:
[code:1]
$counter = NewCounter
$ClosureStore.GetEnumerator() | % {$_.value }
# IsAlive TrackResurrection Target
#


# True False @{Script=$i++;...
# True False @{Script=param([string]$text) return...
# True False @{Script=$i++;...
[/code:1]
dans cette table nous avons 3 entrées pour 2 closures actives. Forçons la collecte du garbage collector (GC) :
[code:1]
[GC]::Collect([GC]::MaxGeneration)
$ClosureStore.GetEnumerator() | % {$_.value }
# IsAlive TrackResurrection Target
#


# False False
# True False @{Script=param([string]$text) return...
# True False @{Script=$i++;...

[/code:1]
La référence de la première fermeture a été supprimée automatiquement par le garbage collector. Mais notre référence faible existe encore dans le magasin.
C'est la méthode Closure qui se charge de supprimer les entrées inaccessibles présente dans la hastable car le garbage collector collecte automatiquement et à intervalle régulier les objets détruits :

Créeons une nouvelle fermeture dans une nouvelle variable :
[code:1]
function InitArray
{
$Instance= $null;
return closure {
if ($Instance -eq $null)
{ $Instance=@(1,2,3)}
return $Instance;
};
}

$Init=InitArray; $Array=&$Init; \"$array\"
#1 2 3
$array+=0; $Array+=&$Init; [string]::Join(',',$array)
#1,2,3,0,1,2,3
$ClosureStore.GetEnumerator() | % {$_.value }
# IsAlive TrackResurrection Target
#


# True False @{Script=param([string]$text) return...
# True False @ {Script=if ($Instance -eq $null)...
# True False @{Script=$i++;...
[/code:1]
On voit que nous avons désormais 3 entrées pour 3 closures actives et que l'entrée inaccessible, la référence faible, a été supprimée lors de l'appel suivant :
[code:1]
$Init=InitArray; # Appel en interne la fonction closure
[/code:1]

2)La fonction globale InvokeClosureScript
TODO

Tutoriels PowerShell

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

Plus d'informations
il y a 11 ans 10 mois #2903 par Laurent Dardenne
2)La fonction globale InvokeClosureScript

Cette fonction est relativement complexe à saisir car elle utilise pleinement le dynamisme de PowerShell.
Elle recoit en paramètre le GUID de la fermeture concernée et un tableau d'objets.
Le paramètre GUID permet de récupérer la fermeture dans le magasin ($ClosureStore).
[code:1]
$private:closure = $ClosureStore[$closureId].Target;
[/code:1]
On récupére une référence forte sur la fermeture via le membre Target.
Ensuite on crée, selon le nombre d'éléments dans le tableau Args, un des codes suivants :
[code:1]
param([ScriptBlock]$private:«»script, [Array]$private:Args_) .$script
#ou
param([ScriptBlock]$private:«»script, [Array]$private:Args_) .$script $Args_[0]
#ou encore
param([ScriptBlock]$private:«»script, [Array]$private:Args_) .$script $Args_[0] $Args_[n]
[/code:1]
Ensuite, et comme dirait Christian C., c'est là que ça se corse !

Dans un premier temps on construit un scriptblock
[code:1]
{
param([ScriptBlock]$private:«»script, [Array]$private:Args_)
.$script $Args_[0] $Args_[n]
}
[/code:1]
puis viens ceci :
[code:1]
$private:result =
&{
#Charge l'environnement
$Args[1].Environment | % { trap { continue; } $ExecutionContext.SessionState.PSVariable.Set($_); };
#Execute la fermeture
return .$Args[0] $Args[1].Script $Args[2];
} $invoker $closure $Args_
return $result;
[/code:1]
Décortiquons l'affectation de la variable $Result tout en simplifiant le code, que vous pouvez exécuter directement dans la console:
[code:1]
$result =
&{
Echo \"$Args\"
} \"`$invoker\" \"`$closure\" \"`$Args_\"
&$result
[/code:1]
A première vue ce code semble assez complexe et une des questions qui vient à l'esprit est pourquoi procéder ainsi ?
La fermeture doit retrouver son \"contexte d'exécution\" c'est à dire les variables locales sur lesquelles elle intervient, i.e. son environnement lexical.
[code:1]
$ExecutionContext.SessionState.PSVariable.Set($_)
[/code:1]
Cette instruction \"injecte\" une variable dans le contexte d'exécution courant.

La présence du scriptblock $Result permet de créer une portée enfant autorisant leur restauration sans effet de bord sur la portée courante.
Reprenont,
Niveau 0 : La fonction InvokeClosureScript
Niveau 1 : La variable $Result contient le résultat de l'exécution d'un scriptblock avec 3 arguments.
Niveau 2 : Son code imbrique l'exécution d'un scriptblock, $Invoker, prenant en paramétre 2 arguments.
Niveau 3 : Et enfin le code de ce scriptblock imbriqué exécute à son tour le code du scriptbloc de la closure qui lui attend 0 ou n arguments.

Le second appel de scriptblock imbriqué, .$Args[0], simule les portées du code d'origine, par exemple :
[code:1]
function NewCounter result =
{ &{
$i = 0 $ExecutionContext.SessionState.PSVariable.Set($i)
return closure { return &{param([ScriptBlock]$private:«»script, [Array]$private:Args_)
$i++; .{$i++;return $i } # pas de paramétre, $Args_ est inutilisé dans ce cas présent
return $i
}; }
}
[/code:1]
Tout compte fait cette construction propage le scriptbloc de la closure et ses arguments jusqu'a la portée adéquate.
A noter une dernière remarque, sur ce code
[code:1]
return .$Args[0] $Args[1].Script $Args[2];
[/code:1]
L'usage du dotsourcing sur un scriptbloc l'exécute dans la portée globale alors que l'usage du & l'exécuterais dans une portée locale.
C'est ce qui fait que le code de la closure modifie ces variables dans la portée adéquate. Une fois ceci expliqué cette portion de code est déjà plus facile à aborder, enfin j'espère.

Reste la sauvegarde des variables modifiées dans le scriptblock de la fermeture. Puisque l'on récupère, pour chaque variable injectée dans la portée parente, une référence forte sur nos variables mémorisées dans le magasin, leur modification se fait sur le même objet. Il n'est donc pas nécessaire de mettre à jour le magasin.

Par l'étude de ce script on peut voir que PowerShell offre des fonctionnalités étendues qui associées à une connaissance des principes de base de .NET. permet des traitements assez poussés, certes au détriment parfois de la relecture.
Bien évidemmment ce n'est pas tous les jours qu'on utilise ce genre de mécanismes, heureusement d'ailleurs, mais dans certains cas de savoir qu'ils existent peut être utile.

PowerShell c'est d'enfer mais attention au retour de flamme ;-)

Lien :
Portée dynamique : fr.wikipedia.org/wiki/Port%C3%A9e_(informatique)
PS est un langage à portée dynamique
[code:1]
function get{echo $i}
get
#ras
$i=10
get
#10
function get2{$i=15;get}
get2
#15
get
#10
function get2{$i=15;get;&{$i=84;get}}
get2
#15
#84
[/code:1]

Fermetures lexicales en C : ftp://ftp.linux-kheops.com/pub/traduc.or...005/112/lg112-H.html
La syntaxe de PowerShell étant proche de celle du C, la connaissance de ce langage n'est pas nécessaire pour aborder la première partie de ce texte.

Définitions: fermeture (closure en anglais) et environnement lexical sous .NET : www.dotnetguru.org/articles/dossiers/ano..._FR.htm#_Toc83201319

PS
Amélioration possible :
Il reste un point à vérifier, ce code mémorise un nombre important de variables sans que l'on sache si cela est vraiment nécessaire.
Comme PowerShell est un langage à portée dynamique je ne sais pas si l'environnement lexical concerne les seules variables manipulées dans le code de la closure ou toutes les variables du contexte comme le fait le code d'origine.

Une solution possible serait d'étendre le principe des DSL comme ceci :
[code:1]
function NewCounter
{
Var i
$i = 0;
return closure {
$i++;
return $i;
};
}
[/code:1]
Var[/i] est une fonction globale utilisée pour mémoriser uniquement les variables locale de la fermeture.
Dans ce cas la gestion des variables dans le code de la fonction Closure ressemblerait à ceci :
[code:1]
$private:environment = $__ClosureVarStore|% {Get-Variable -name $_}
[/code:1]
A suivre...

Tutoriels PowerShell

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

Plus d'informations
il y a 11 ans 10 mois #2904 par Arnaud
Bonsoir Laurent,

Ce n'est pas du Japonais, mais c'est pas si loin... ;-)

Pourrais tu nous dire à quoi peuvent servir les closures ? J'ai regardé attentivement le lien Wikipedia que tu as donné mais je vois pas trop quelle est la finalité de la chose.:blink:

Merci

Arnaud

Créateur du forum de la communauté PowerShell Francophone

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

Plus d'informations
il y a 11 ans 10 mois #2907 par Laurent Dardenne
Salut Arnaud
Arnaud écrit:

Pourrais tu nous dire à quoi peuvent servir les closures ?

Donne moi un peu de temps pour te répondre, je ne suis pas très à l'aise pour les expliquer simplement et rapidement.
Arnaud écrit:

Ce n'est pas du Japonais, mais c'est pas si loin...

Qu'est-ce qui te pose pb, autre que celui exposé ?

Tutoriels PowerShell

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

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