jeudi 10 octobre 2019

Canaux Nommes & Form


Dans cet exemple je vais m’intéresser aux problèmes d'attentes rencontré lors de l'utilisation des canaux nommées dans le cadre de communication entre deux processus affichant des formulaires.

Si j’intègre tel quel mon pipe pour communiquer entre 2 Thread contenant chacun leur propre fenêtre indépendante, les fenêtres vont se figer sans arrêt. Ceci tiens à plusieurs problème.

WriteLine() fige la fenêtre :
Ce premier cas est simple à résoudre. Pour éviter que la fenêtre se fige nous allons ajouter un buffer.

$pipeDir = [System.IO.Pipes.PipeDirection]::InOut
$pipeTra = [System.IO.Pipes.PipeTransmissionMode]::Message
$pipeOpt = [System.IO.Pipes.PipeOptions]::Asynchronous
$pipeBuf = 4096   #Buffer Entrée+Sortie.
 $pipe = New-Object System.IO.Pipes.NamedPipeServerStream("\\.\pipe\J12Pipe1",$pipeDir,1,$pipeTra,$pipeOpt,$pipeBuf,$pipeBuf)

Dans ce cas WriteLine() va écrire sans attendre dans le buffer et passer à la ligne suivante. Les données seront ensuite récupérées par ReadLine() dans le seconde processus.

ReadLine() fige la fenêtre :
Le meilleur moyen de ne pas se retrouver avec une fenêtre figé est de faire en sorte de ne pas appeler la commande ReadLine() lorsqu'il n'y a plus de donnée à lire. Il existe bien des propriétés pour connaitre la taille des données en attente, mais elle ne sont pas implémenté dans le cadre des canaux nommées. Il faut donc ruser pour dire au second processus qu'il n'y a plus de donnée à lire.

Les sémaphores au secours des canaux nommées :
Il y a certainement plusieurs moyens pour dire au second processus de ne pas utiliser ReadLine() lorsqu'il n'y a plus de données à lire. Dans cet exemple j'ai choisie d'utiliser les sémaphores pour indiquer aux deux processus le nombre de ligne en attente dans le pipe.

New-Object System.Threading.Semaphore(0, 10000, "Global\J12Sema1")

Je vais déclaré un sémaphore pouvant stocker une valeur de 0 à 10000. Cette valeur correspondra dans mon script au nombre de ligne que j'aurai écrit dans le buffer du pipe. La méthode Release() à la caractéristique bien utile de me renvoyer le nombre de Release positionner dans le sémaphore. Release() va incrémenter ce nombre, tandis que la méthode Waitone() va le diminuer.

Waitone() fige la fenêtre :
La méthode Waitone() a malheureusement aussi la fâcheuse habitude de figer la fenêtre. Dans le cadre d'une utilisation normal du sémaphore c'est bien. Mais comme je détourne son utilisation, ça ne m'arrange pas. Je vais donc utiliser le timeout de la méthode Waitone() pour qu'elle réponde rapidement.

Pour tester ces scriptes, ouvrez deux fois le programme ISE. Copier le premier script dans la premier fenêtre ISE. Et le second script dans la seconde fenêtre ISE. Puis exécutez les deux scripts simultanément. A noter : Les fenêtres ne sont pas code dans cet exemple. La fonction While est utiliser pour simuler ici un Timer.

Premier script :
 
#Création d'un sémaphore.
#Je vais utiliser ici le sémaphore pour envoyer le nombre de ligne à lire.
$sema = New-Object System.Threading.Semaphore(0, 10000, "Global\J12Sema1")

#Création du canal nommé coté client.
$pipe = new-object System.IO.Pipes.NamedPipeClientStream("\\.\pipe\J12Pipe1")

#StreamWriter.
$sw = new-object System.IO.StreamWriter($pipe)

#Connexion avec le serveur.
$pipe.Connect()

#Initialisation.
$base = @("data1","data2","data3","data4")
$sw.AutoFlush = $true
$nb = 10000

While(1)  #Pour simuler un timer.
 {
   #Vérifier le nombre de ligne envoyé dans le pipe.
   try{
        #lit la valeur dans le sémaphore et l'incrémente.
        $nb = $sema.Release(1)     #Incrémente de 1.
       
        #Décrémente immédiatement le sémaphore pour corriger la valeur. 
        $null = $sema.Waitone(1)   #Le timout est de 1ms.
      }
   catch {
           #En cas d'erreur cela indique que le
           #nombre maximal de ligne est atteint.
           $nb = 10000
         }

   #Si tout les lignes sont lu, j’envoie les données.
   if ($nb -eq 0)
     {
       foreach ($ligne in $base)
        {
          #Ecrit dans le Pipe.
          $global:sw.WriteLine($ligne)
         
          #Ecrit dans la Console + pause d'une seconde.
          write-host "envoi de" $ligne; Sleep 1
         
          #Incrémente le nombre de ligne à lire dans le sémaphore.
          $null = $sema.Release(1)
        }
     }
 }

$pipe.Close()
$sema.Close()


Deuxième script :
 
#Création d'un sémaphore.
$sema = New-Object System.Threading.Semaphore(0, 10000, "Global\J12Sema1")

#Création du canal nommé coté serveur.
$pipeDir = [System.IO.Pipes.PipeDirection]::InOut
$pipe = new-object System.IO.Pipes.NamedPipeServerStream("\\.\pipe\J12Pipe1",$pipeDir,1,[System.IO.Pipes.PipeTransmissionMode]::Byte,[System.IO.Pipes.PipeOptions]::Asynchronous,51200,51200)

#StreamReader.
$sr = new-object System.IO.StreamReader($pipe)

#Connexion avec le serveur.
$pipe.WaitForConnection()

#Initialisation.
$nb = 10000

While(1)  #Pour simuler un timer.
 {
#Vérifier le nombre de ligne envoyé dans le pipe.
   try{
        #lit la valeur dans le sémaphore et l'incrémente.
        $nb = $sema.Release(1)     #Incrémente de 1.
       
        #Décrémente immédiatement le sémaphore pour corriger la valeur.
        $null=$sema.Waitone(1)   #Le timout est de 1ms.
       }
   catch
       {
         #En cas d'erreur cela indique que le
         #nombre maximal de ligne est atteint.
         $nb = 10000
       }
      
   #Si il y a des donnée à lire.
   if ($nb -gt 0)
     {
       while ($nb-- -ne 0)
        {
          #Lecture du Pipe.
          $text = $sr.ReadLine()
         
          #Ecrit dans la Console + pause d'une seconde.
          Write-host "Lecture" $text; Sleep 1
         
          #Décrémente le nombre de ligne à lire dans le sémaphore.
          $null = $sema.Waitone(1)
        }
     }
 }

$pipe.Close()
$sema.Close()