jeudi 23 juin 2016

PowerShell, CSharp et Global Hook

Dans cet exemple, j'utilise un script en cSharp pour récupérer les frappes clavier sur les touches F1 à F12. Puis je passe l'information au script Windows PowerShell. Cette technique me permet de déclencher une action sans avoir le focus sur l'application PowerShell (Global Hook)

Dans cet exemple, j'utilise un programme déjà publié un peu partout, mais adapté ici pour récupère que les touches de fonctions de F1 à F12. Ce programme se décomposer en deux parties. Un programme en CShape que j’intègre directement dans le script PowerShell et qui est lancé dans un RunSpace afin de ne pas bloquer mon script. Et un programme principale en PowerShell qui lance l'action lorsque la frappe est détecté. La communication entre les deux programmes n'est pas ce qu'il y a de plus simple à traiter. Après plusieurs essai, j'ai actuellement opté pour un socket réseau afin de pouvoir échanger des données entre le programme en CShape et le programme principale en Powershell. Ce n'est pas forcement l'idéal, mais c'est la solution qui m'a posé le moins de problèmes.

Exemple


 
##################################################################
# Script C# :
# Récupère les frappes aux claviers (quelques soit l'application),
# et en informe le programme en écrit en Powershell.
##################################################################
# Script PowerShell :
# Lance le script en C# dans un nouveau thread, et écoute les
# informations transmissent par le script en C#.
##################################################################

#Assembly.
[Reflection.Assembly]::LoadWithPartialName("System.Windows.Forms")

##################################################################
# Script C#
# Récupère les frappes aux claviers (F1 à F12),
# et on informe le script PS1.
##################################################################

$csharp1 = {
  Add-Type -TypeDefinition @"
  using System;
  using System.IO;
  using System.Net;
  using System.Text;
  using System.Net.Sockets;
  using System.Diagnostics;
  using System.Windows.Forms;
  using System.Runtime.InteropServices;
  namespace KL {
  
    public static class Program {
      private const int WH_KEYBOARD_LL = 13;
      private const int WM_KEYDOWN = 0x0100;
   
      private static HookProc hookProc = HookCallback;
      private static IntPtr hookId = IntPtr.Zero;
   
      public static void Main() {
                                  hookId = SetHook(hookProc);
                                  Application.Run();
                                  UnhookWindowsHookEx(hookId);
                                }
      private static IntPtr SetHook(HookProc hookProc) {
        IntPtr moduleHandle = GetModuleHandle(Process.GetCurrentProcess().MainModule.ModuleName);
        return SetWindowsHookEx(WH_KEYBOARD_LL, hookProc, moduleHandle, 0);
                                                       }
      private delegate IntPtr HookProc(int nCode, IntPtr wParam, IntPtr lParam);
      private static IntPtr HookCallback(int nCode, IntPtr wParam, IntPtr lParam) {
                                                                                    if (nCode >= 0 && wParam == (IntPtr)WM_KEYDOWN)
                                                                                      {
                                                                                        int vkCode = Marshal.ReadInt32(lParam);
                                                                                        if ((vkCode > 111) && (vkCode < 128))
                                                                                          {
          string text = "";
          Socket sock = new Socket(AddressFamily.InterNetwork, SocketType.Dgram,ProtocolType.Udp);
          IPAddress serverAddr = IPAddress.Parse("127.0.0.1");
          IPEndPoint endPoint = new IPEndPoint(serverAddr, 12345);
          KeysConverter kc = new KeysConverter();
          text = kc.ConvertToString(vkCode);
          byte[] send_buffer = Encoding.ASCII.GetBytes(text);
          sock.SendTo(send_buffer , endPoint);
          sock.Close();
          if (vkCode == 127) { Application.ExitThread(); }
                                                                                          }
                                                                                      }
                                                                                    return CallNextHookEx(hookId, nCode, wParam, lParam);
                                                                                  }
      [DllImport("user32.dll")]
      private static extern IntPtr SetWindowsHookEx(int idHook, HookProc lpfn, IntPtr hMod, uint dwThreadId);
      [DllImport("user32.dll")]
      private static extern bool UnhookWindowsHookEx(IntPtr hhk);
      [DllImport("user32.dll")]
      private static extern IntPtr CallNextHookEx(IntPtr hhk, int nCode, IntPtr wParam, IntPtr lParam);
      [DllImport("kernel32.dll")]
      private static extern IntPtr GetModuleHandle(string lpModuleName);
    }
  }
"@ -ReferencedAssemblies System.Windows.Forms
#Lancement du programme en CShape.
[KL.Program]::Main()
}

##################################################################
# Ouverture d'un canal de communication réseau en UDP.
# Communication entre le script C# et PowerShell.
##################################################################

$global:udpport = 12345
$global:endpoint = new-object System.Net.IPEndPoint ([IPAddress]::Any,$global:udpport)
$global:udpclient = new-Object System.Net.Sockets.UdpClient $global:udpport

##################################################################
# Lance l'application C# dans un Runspace.
##################################################################

#Création du runspace.
$runspace1 = [System.Management.Automation.Runspaces.RunspaceFactory]::CreateRunspace()
$runspace1.Open()
$pipeline1 = $runspace1.CreatePipeline()
$pipeline1.Commands.AddScript($csharp1)
$pipeline1.InvokeAsync()
$running = [System.Management.Automation.Runspaces.PipelineState]::Running
if ($pipeline1.PipelineStateInfo.State -ne $running) { Exit }

##################################################################
# Timer pour écouter les retours de l'application C#
##################################################################
               
$timer1 = new-object System.Windows.forms.timer
$timer1.Interval = 250
$timer1.Add_Tick({
                   while ($global:udpclient.Available)
                    {
                      $content = $global:udpclient.Receive([ref]$global:endpoint)
                      $fxx = [Text.Encoding]::ASCII.GetString($content)
                      switch ($fxx){
                                     F1      { write-host "F1" ; break }
                                     #...
                                     F12     { write-host "F12"; break }
                                     default { write-host $fxx ; break }
                                   }
                    }
                })

##################################################################
# Script Powershell.
##################################################################

$global:formA = New-Object Windows.Forms.Form
$global:formA.text = "GlobalHook Keyboard."            
$global:formA.Size = New-Object System.Drawing.Size(250,110)
$global:formA.Add_KeyDown({
                            if ($_.KeyCode -eq "Escape")
                              { $global:formA.Close() }
                          })
$labelA1 = New-Object Windows.Forms.Label
$labelA1.Location = New-Object Drawing.Point 20,30
$labelA1.Size = New-Object Drawing.Point 210,16
$labelA1.Text = "Appuyer sur un touche de F1 à F12."
$formA.controls.add($labelA1)
$timer1.Start()
$global:formA.ShowDialog()

##################################################################
# Fermeture des ressources.
##################################################################

$timer1.Stop()                                       #Stop Timer.
[System.Windows.Forms.SendKeys]::SendWait("{F16}")   #Stop C# App.
$runspace1.Close()                                   #Stop Runspace.
$udpclient.Close()                                   #Stop Socket.


Pour éviter qu'une partie du programme continue de tourner tout seul dans son coin, il est important de bien fermer tout ce petit monde dans l'ordre. Tout d'abord il faut demander au programme CSharp de se fermer. Pour cela je simule dans mon programme PowerShell une frappe sur la touche F16. Le programme CShape est configuré pour s'arrêter lorsqu’il capture une frappe sur la touche F16. Ceci fait, je ferme mon RunSpace, puis je libérer mes ressources réseaux.