Aller au contenu

L'interface

Avalonia depuis PowerShell

La fenêtre est en Avalonia 11, hébergée par PowerShell 7 :

  • le XAML de chaque fenêtre est une chaîne, lue à l'exécution (AvaloniaRuntimeXamlLoader.Parse) : aucune assembly compilée dans le module ;
  • l'application est démarrée une fois par session, et la fenêtre fait tourner sa propre boucle de messages (Dispatcher.UIThread.MainLoop) jusqu'à sa fermeture : l'appel bloque, comme une console modale ;
  • les données des listes et des arbres sont de petites classes PowerShell (AvaloniaTreeNode, AvaloniaHistoryNode, AvaloniaConfigurationRow…) : Avalonia se lie à leurs propriétés.

Le runtime

Resources/AvaloniaPackages.json épingle les 21 paquets NuGet de la fenêtre (Avalonia, Skia, HarfBuzz, leurs fichiers natifs par runtime) avec le SHA-512 du catalogue nuget.org. À la première ouverture d'une fenêtre, la console télécharge les paquets du runtime de l'ordinateur, vérifie chaque empreinte avant d'extraire quoi que ce soit, les prépare, puis les met en place d'un coup : dans le dossier du module quand on peut y écrire, sinon dans le dossier de données de l'utilisateur.

Arbres et détails sans fenêtre

New-PSGPODirectoryTree, New-PSGPOPolicyTree et Get-PSGPODirectoryDetail construisent ce que montre la fenêtre (nœuds, icônes, libellés, détails) sans aucun type Avalonia : ils sont testés sans interface.

Icônes

Fluent UI System Icons (MIT), taille 20, style regular, dans Resources/Icons : la fenêtre lit le tracé de chaque SVG comme une géométrie Avalonia, disponible en XAML sous {StaticResource Icon.<nom>}. Des emoji étaient utilisés avant, mais la plupart des bureaux Linux n'ont pas de police pour les dessiner.

Pièges rencontrés

Les fermetures (closures)

Un gestionnaire créé avec GetNewClosure() tourne dans un nouveau module dynamique : il ne voit plus les fonctions privées du module, ni ses classes. Les gestionnaires capturent d'abord ce qu'ils appellent ($Save = ${function:Save-PSGPODomainConfiguration}, $RowType = [AvaloniaConfigurationRow]).

Le pipeline

La sortie du pipeline enveloppe les objets dans PSObject, auquel Avalonia ne sait pas se lier : les listes sont construites avec List[T] et foreach. if (...) { @(...) } déroule une liste d'un seul élément : les listes sont affectées dans les branches. Select-Object enveloppe aussi ses objets : une liste faite avec lui prend .PSObject.BaseObject de chaque élément.

Le bouton de la vue affichée

La classe accent du thème Fluent ne colore pas un bouton ici : les fenêtres partagent le style Button.active (Get-PSGPOWindowResourceXaml, avec les pinceaux des deux thèmes).

Le code d'une fenêtre

Chaque contrôle nommé d'une fenêtre (Name='...' dans son XAML) est dans une variable de son nom ($treeView, $applySettingButton…), créée à partir du XAML en une boucle (Get-PSGPOAvaloniaNamedControl), et non une ligne par contrôle. La console GPO est découpée par vue : Start-PSGPOManagerWindow construit la fenêtre, puis dot-source chaque vue (Initialize-PSGPOManagerSettingView, ...HistoryView, ...WmiFilterView…), un fichier chacune, dont l'état, les fonctions et les gestionnaires vivent dans la portée de la fenêtre tant qu'elle est ouverte.

Une variable mal orthographiée ne serait que $null à l'exécution : PSGPOTools.WindowCode.tests.ps1 lit le code de la console (son arbre syntaxique) et échoue sur une variable lue qui n'est ni affectée ni un contrôle de son XAML.

Une erreur qu'aucun gestionnaire n'a traitée

Une erreur qui sort d'un gestionnaire (un clic, une minuterie, une action postée) ne ferme pas la fenêtre : la boucle de chaque fenêtre (Invoke-PSGPOWindowLoop -KeepOpenOnError) la reçoit de Dispatcher.UIThread.UnhandledException, l'affiche dans une notification de la fenêtre (en haut à droite, pendant 30 secondes) et l'écrit dans le journal technique (ERROR, avec le script et la ligne où elle s'est produite). La même erreur répétée dans les 5 secondes n'est dite qu'une fois. Les tests de l'interface ouvrent les fenêtres sans elle : une erreur y arrête la boucle et fait échouer le test.

Les opérations longues en arrière-plan

La console GPO lit et écrit des choses qui prennent des secondes ou des minutes : Actualiser (F5) et Changer de contrôleur de domaine..., Détecter maintenant (onglet État), Sauvegarder et Tout sauvegarder (onglet Historique), Enregistrer le rapport.... Elles tournent dans un worker de la console : un runspace à lui, ouvert au démarrage de la console, où HGPM est importé une fois (quelques secondes sous Windows, où le module est analysé ; la première opération l'attend). La fenêtre reste dessinée et répond : un bandeau dit ce qui tourne, l'arbre et les détails du domaine attendent (aucune autre modification ne commence entre-temps), et Annuler arrête une lecture (pas une sauvegarde, qui écrit et n'est pas laissée à moitié).

Le bloc d'une opération tourne dans la portée du module, dans le worker : il trouve les fonctions de HGPM, et ce dont il a besoin de la fenêtre lui est passé en arguments. Les objets de HGPM vont au worker et en reviennent tels quels, ses classes étant les mêmes types dans les deux runspaces ; le worker reçoit la langue et la configuration du domaine de la fenêtre au début de l'opération, et les fichiers ADMX chargés sont des membres statiques, déjà partagés (PSGPOWindowOperation.ps1, Initialize-PSGPOManagerOperationView). Les tests de l'interface exécutent plutôt les opérations sur le fil de la fenêtre, où sont leurs mocks ; le worker a ses propres tests (un résultat, une erreur, une annulation, une seconde opération refusée).

Les classes et les runspaces

Une méthode d'une classe de HGPM qui appelle le module s'exécute dans le runspace qui a défini la classe, celui de la fenêtre, même quand le worker l'appelle (la lecture des fichiers d'une GPO pour un rapport). Ce runspace ne prend un tel appel qu'entre deux commandes de son pipeline, jamais pendant la boucle de la fenêtre : le worker attendait sans fin. Pendant une opération, sa minuterie exécute Start-Sleep -Milliseconds 0 toutes les 5 ms, ce qui permet à la fenêtre de les prendre. Les classes gardent leur affinité : avec [NoRunspaceAffinity()], leurs méthodes chercheraient les fonctions du module dans la session de l'appelant, où une closure ou un script de l'utilisateur ne les a pas. Vérifié sur le labo Samba et le labo Windows : Actualiser, Détecter maintenant et un rapport dans le worker, la fenêtre répondant pendant ce temps.

Tests de la fenêtre

Start-PSGPOManagerWindow -OnShown { param($Ui) ... } (et Start-PSGPOPasswordPolicyWindow pour la console des PSO, sa fenêtre d'un PSO dans $Ui.Editor) ouvre la vraie fenêtre et exécute le bloc une fois qu'elle est affichée, avec ses contrôles par nom : les tests de l'interface cliquent, saisissent et vérifient ce que montre la fenêtre, puis la fenêtre se ferme. Ils tournent sous Windows et dans la session X d'un ordinateur Linux.