Search code examples
c#.netpowershellpowershell-7.0add-type

Unable to use .Net class via the Add-Type cmdLet in PowerShell 7


I have a PowerShell script that listens for a key to be pressed before exiting. I accomplish this using a custom TypeDefinition; here's the code:

param(
  [string]$quitKey = "F16"
)

$keyLogger = @"
using System;
using System.IO;
using System.Diagnostics;
using System.Runtime.InteropServices;
using System.Windows.Forms;

namespace KeyLogger{
  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;
    private static int keyCode = 0;

    [DllImport("user32.dll")]
    private static extern IntPtr CallNextHookEx(IntPtr hhk, int nCode, IntPtr wParam, IntPtr lParam);

    [DllImport("user32.dll")]
    private static extern bool UnhookWindowsHookEx(IntPtr hhk);

    [DllImport("user32.dll")]
    private static extern IntPtr SetWindowsHookEx(int idHook, HookProc lpfn, IntPtr hMod, uint dwThreadId);

    [DllImport("kernel32.dll")]
    private static extern IntPtr GetModuleHandle(string lpModuleName);

    public static int WaitForKey() {
        hookId = SetHook(hookProc);
        Application.Run();
        UnhookWindowsHookEx(hookId);
        return keyCode;
    }

    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) {
            keyCode = Marshal.ReadInt32(lParam);
            Application.Exit();
        }

        return CallNextHookEx(hookId, nCode, wParam, lParam);
    }
  }
}
"@

Add-Type -TypeDefinition $keyLogger -ReferencedAssemblies system.Windows.Forms

while ($keyPress -ne $quitKey) {
  $keyPress = [System.Windows.Forms.Keys][KeyLogger.Program]::WaitForKey()
}

Write-Host "`nCaught $quitKey, ending recording..." -ForegroundColor Green

exit

The above executes flawlessly in PowerShell 5.1, the version pre-installed on Windows 11. However, I'm unable to get this script to run in PowerShell 7 (7.3.4 to be specific); I get this error:

InvalidOperation: 
Line |
  62 |    $keyPress = [System.Windows.Forms.Keys][KeyLogger.Program]::WaitFor …
     |                                           ~~~~~~~~~~~~~~~~~~~
     | Unable to find type [KeyLogger.Program].

Searched everywhere online for a similar issue and couldn't find anything. Add-Type seems to work in my other scripts, even in PowerShell 7. But for some reason this specific custom TypeDefiniton fails.


Solution

  • There is a long standing bug in newer versions of PowerShell where when -ReferenceAssemblies is used then the standard assemblies are not included by default, see GitHub issue #9599.

    You can use the same workaround I used in this answer, for your case, the code would look like this to make it compatible with both versions:

    # Top of your code goes this:
    Add-Type -AssemblyName System.Windows.Forms
    
    $refAssemblies = @(
        [System.Windows.Forms.Form].Assembly.Location
    
        if ($IsCoreCLR) {
            $pwshRefAssemblyPattern = [IO.Path]::Combine($PSHOME, 'ref', '*.dll')
            Convert-Path $pwshRefAssemblyPattern
        }
    )
    
    # Definition stays the same:
    $keyLogger = @'
    ...
    ...
    '@ 
    
    # Here you include the assembly you want plus the default ones:
    Add-Type -TypeDefinition $keyLogger -ReferencedAssemblies $refAssemblies