2015年2月27日金曜日

How to call .NET and Win32 methods from PowerShell

Every once in a while I run into a problem that I just don't know how to solve using PowerShell directly. Fortunately, PowerShell has great interoperability with other .NET code, so if you're already familiar with VB.NET or C# you'll never be lost.
One of my "bad habits" is to use .NET methods instead of native PowerShell cmdlets. Even now I still find myself using [System.IO.File]::Exists() instead of Test-Path; it's just what pops into my head when I think about that task. I call this a bad habit because some of the folks I interact with aren't C# developers and don't intuitively understand what's going on when there is inline .NET calls all over the place. Here's an example of what I'm talking about. Both these functions do the same thing; depending on your background you may gravitate towards one or the other. Where possible, I recommend using the native PowerShell cmdlet as it'll help other PowerShell users understand what's going on.
# Native PowerShell version
if ((test-path c:\windows\notepad.exe) -eq $true)
{
# do something interesting
$PSFileInfo = Get-ItemProperty c:\windows\notepad.exe
echo "PSFileInfo"
$PSFileInfo
}
# .NET Version
if ([System.IO.File]::Exists("C:\windows\notepad.exe") -eq $true)
{
# do something interesting
$NETFileInfo = New-Object System.IO.FileInfo -ArgumentList "C:\windows\notepad.exe"
echo "NETFileInfo"
$NETFileInfo
}
This style of inline .NET call is pretty convenient when you only need one or two static methods, but what about something more complex? What if you need to call a Win32 API? Well, you're in luck, because PowerShell provides a method to get you on your way. You can use the PowerShell SDK to build a custom cmdlet, but is there another way? Yes, there is!
Let's say that you're building a Windows Troubleshooting Pack that's designed to resolve problems with your device. One of the manual troubleshooting steps you're likely to perform is to check the device in Device Manager for errors, then uninstall and reinstall the device. In order to do this in your troubleshooter you need to be able to:
Identify the device
Get its state (ConfigManagerErrorCode for those familiar with accessing devices using WMI)
Uninstall the device
Reinstall the device
In most cases you'll be able to do the first two tasks directly from PowerShell by using WMI. Uninstalling and reinstalling the device is a bit more complicated, you'll need to call some Win32 APIs to get that done. So let's take a look at how that works in PowerShell and the Windows Troubleshooting Platform. Conceptually, the process looks like this:
Identify the Win32 APIs you need to perform this task
Create a C#/VB.NET class that wraps the Win32 APIs
Bring your wrapper class into PowerShell
Call the methods defined in your wrapper class from PowerShell
Best Practice: While you can do all the steps in this example in Notepad or the PowerShell ISE, I strongly recommend using Visual Studio to create your C#/VB.NET class when doing this for real. Visual Studio will give you access to IntelliSense and all sorts of other features that'll make your life easier. If you don't have Visual Studio you can download the Express edition of your favorite language for free.
I'm going to skip the uninstall step and focus on the reinstall. The concept related to accessing Win32 APIs is identical, but uninstall requires us to do steps one and two first. For the purposes of this example we can manually do steps 1-3 and code only the reinstall.
Step 1: Identify the Win32 APIs you need to perform this task
MSDN is your friend here. In this case we need 3 Win32 APIs:
CM_Locate_DevNode_Ex
CM_Reenumerate_DevNode_Ex
CMP_WaitNoPendingInstallEvents
Step 2: Create a C#/VB.NET class that wraps the Win32 APIs
This is where you'll spend most of your time and why I'd recommend doing this work inside Visual Studio where you have great debugging tools. To save time I've included the entire wrapper source below in C#. Paste this into Visual Studio, save, and do a quick build to make sure there aren't any errors.
using System.Runtime.InteropServices;
using System;

namespace mattbie.examples.devices
{
// These are the native win32 methods that we require
internal static class NativeMethods
{
[DllImport("cfgmgr32.dll", SetLastError = true, EntryPoint = "CM_Locate_DevNode_Ex", CharSet = CharSet.Auto)]
public static extern UInt32 CM_Locate_DevNode_Ex(ref UInt32 DevInst, IntPtr DeviceID, UInt64 Flags, IntPtr Machine);

[DllImport("cfgmgr32.dll", SetLastError = true, EntryPoint = "CM_Reenumerate_DevNode_Ex", CharSet = CharSet.Auto)]
public static extern UInt32 CM_Reenumerate_DevNode_Ex(UInt32 DevInst, UInt64 Flags, IntPtr Machine);

[DllImport("cfgmgr32.dll", SetLastError = true, EntryPoint = "CMP_WaitNoPendingInstallEvents", CharSet = CharSet.Auto)]
public static extern UInt32 CMP_WaitNoPendingInstallEvents(UInt32 TimeOut);
}

// This class houses the public methods that we'll use from powershell
public static class StaticMethods
{

public const UInt32 CR_SUCCESS = 0;
public const UInt64 CM_REENUMERATE_SYNCHRONOUS = 1;
public const UInt64 CM_LOCATE_DEVNODE_NORMAL = 0;

public static UInt32 RescanAllDevices()
{
//only connect to local device nodes
UInt32 ResultCode = 0;
IntPtr LocalMachineInstance = IntPtr.Zero;
UInt32 DeviceInstance = 0;
UInt32 PendingTime = 30000;

ResultCode = NativeMethods.CM_Locate_DevNode_Ex(ref DeviceInstance, IntPtr.Zero, CM_LOCATE_DEVNODE_NORMAL, LocalMachineInstance);
if (CR_SUCCESS == ResultCode)
{
ResultCode = NativeMethods.CM_Reenumerate_DevNode_Ex(DeviceInstance, CM_REENUMERATE_SYNCHRONOUS, LocalMachineInstance);
ResultCode = NativeMethods.CMP_WaitNoPendingInstallEvents(PendingTime);
}
return ResultCode;
}
}
}

Step 3: Bring your wrapper class into PowerShell
Now it's time to get our wrapper into PowerShell. You have a few options here; include as an assembly, or include as source; your scenario and business needs may lend itself to one method more than the other. For this example we're going to include our wrapper as source. Obviously, including our wrapper as source code means that it's readable by anyone who opens the PowerShell script in a text editor. Including as source works particularly well for short snippets like this. Regardless of the method, you'll be using the Add-Type cmdlet, which will do all the heavy lifting for us.
I've created a new function in my PowerShell script called InitDeviceClass that'll bring our wrapper into play. You can see this function in the completed PowerShell script at the end of this post, it looks complicated, but in reality it only has two statements. The first statement defines a string variable named $SourceCode which contains the entire source for our wrapper. Just copy/paste the whole source from Visual Studio into the PowerShell ISE. The second statement does all the work, so let's take a closer look at that.
Add-Type -TypeDefinition $SourceCode
Add-Type is a new cmdlet introduce with PowerShell 2.0 that lets you define a .NET Framework class in your Windows PowerShell session. For a complete description check out the MSDN reference. The -TypeDefinition parameter is where we tell Add-Type how to find the type definition that we want to add. In this case, we're just passing it the entire C# source file, which it'll compile and make available to us. If your C# has errors in it, Add-Type will return an error letting you know that there were compile-time errors with your code.
That's it. The wrapper is now loaded and ready to use, so let's see how that works.
Step 4: Call the methods defined in your wrapper class from PowerShell
Our wrapper defines one public static method, which makes things pretty easy. All we need to do is call our init function and then the wrapper is ready to use. The syntax is exactly the same as my old [System.IO.File]::Exists() crutch.
InitDevice Class
[mattbie.examples.devices.staticmethods]::RescanAllDevices()
And there you have it. Win32, wrapped in .NET, then wrapped in PowerShell, ready to be added to your Troubleshooting Pack resolver script. Here is the complete PowerShell script. Enjoy!
# Resolver Script - This script fixes the root cause. It only runs if the Troubleshooter detects the root cause.
# Key cmdlets:
# -- get-diaginput invokes an interactions and returns the response
# -- write-diagprogress displays a progress string to the user

# Your logic to fix the root cause here


# Create a function that loads our managed code into powershell
function InitDeviceClass
{

# This is the C# source code. I've tested this in visual studio and then copy/pasted it here
# You could also load the .CS file from the disk, if you prefer

[string]$SourceCode = @"
using System.Runtime.InteropServices;
using System;

namespace mattbie.examples.devices
{
// These are the native win32 methods that we require
internal static class NativeMethods
{
[DllImport("cfgmgr32.dll", SetLastError = true, EntryPoint = "CM_Locate_DevNode_Ex", CharSet = CharSet.Auto)]
public static extern UInt32 CM_Locate_DevNode_Ex(ref UInt32 DevInst, IntPtr DeviceID, UInt64 Flags, IntPtr Machine);

[DllImport("cfgmgr32.dll", SetLastError = true, EntryPoint = "CM_Reenumerate_DevNode_Ex", CharSet = CharSet.Auto)]
public static extern UInt32 CM_Reenumerate_DevNode_Ex(UInt32 DevInst, UInt64 Flags, IntPtr Machine);

[DllImport("cfgmgr32.dll", SetLastError = true, EntryPoint = "CMP_WaitNoPendingInstallEvents", CharSet = CharSet.Auto)]
public static extern UInt32 CMP_WaitNoPendingInstallEvents(UInt32 TimeOut);
}

// This class houses the public methods that we'll use from powershell
public static class StaticMethods
{

public const UInt32 CR_SUCCESS = 0;
public const UInt64 CM_REENUMERATE_SYNCHRONOUS = 1;
public const UInt64 CM_LOCATE_DEVNODE_NORMAL = 0;

public static UInt32 RescanAllDevices()
{
//only connect to local device nodes
UInt32 ResultCode = 0;
IntPtr LocalMachineInstance = IntPtr.Zero;
UInt32 DeviceInstance = 0;
UInt32 PendingTime = 30000;

ResultCode = NativeMethods.CM_Locate_DevNode_Ex(ref DeviceInstance, IntPtr.Zero, CM_LOCATE_DEVNODE_NORMAL, LocalMachineInstance);
if (CR_SUCCESS == ResultCode)
{
ResultCode = NativeMethods.CM_Reenumerate_DevNode_Ex(DeviceInstance, CM_REENUMERATE_SYNCHRONOUS, LocalMachineInstance);
ResultCode = NativeMethods.CMP_WaitNoPendingInstallEvents(PendingTime);
}
return ResultCode;
}
}
}
"@

# use the powershell 2.0 add-type cmdlet to compile the source and make it available to our powershell session
add-type -TypeDefinition $SourceCode

}

0 件のコメント:

コメントを投稿