PowerShell 3.0 - Scripting callbacks and Delegates in Managed APIs

by oising 7. April 2012 23:24

A question came up on an MVP mailing list about passing delegates of .NET methods to managed APIs in PowerShell. I covered callbacks to script blocks in a previous post, but I've never covered anything on passing regular .NET methods to APIs so here's a function I wrote to easily create Action, Action<> or Func<> delegates for any given static or instance method. The syntax is pipeline friendly and very flexible. As methods may be overloaded, you must provide a means to select an overload for the delegate. This is done by either providing a specific delegate type you wish to create, or by passing an array of types that should be used to find a compatible overload. The latter technique is more flexible because you only need to provide compatible parameters; the explicit delegate technique needs an exact match for the overload. Here are some examples of the syntax:

# Gets a delegate for a matching overload with string,string parameters.
# It will actually return func<string,object,string> which is the correct 
# signature for invoking string.format with string,string.
$delegate = [string]::format | Get-Delegate string,string

# Gets a delegate for a matching overload with no parameters.
$delegate = [console]::beep | Get-Delegate @()

# Gets a delegate for a matching overload with @(int,int) parameters.
$delegate = [console]::beep | get-delegate int,int

# Gets a delegate for an explicit func[].
$delegate = [string]::format | Get-Delegate -Delegate 'func[string,object,string]'

# Gets a delegate for an explicit action[].
$delegate = [console]::writeline | Get-Delegate -Delegate 'action[int]'

# For a method with no overloads, we will choose the default method and 
# create a corresponding action, action[] or func[].
$delegate = [string]::isnullorempty | get-delegate 

# Gets a delegate to an instance method of a stringbuilder: Append(string)
$sb = new-object text.stringbuilder
$delegate = $sb.append | get-delegate string

Here is the function definition itself. It requires PowerShell 3.0 (in public beta right now) due to some new language features but in theory it could be modified to support PowerShell 2.0. It's also available on PoshCode.

#requires -version 3

function Get-Delegate {
<#
.SYNOPSIS
Create an action[] or func[] delegate for a psmethod reference.
.DESCRIPTION
Create an action[] or func[] delegate for a psmethod reference.
.PARAMETER Method
A PSMethod reference to create a delegate for. This parameter accepts pipeline input.
.PARAMETER ParameterType
An array of types to use for method overload resolution. If there are no overloaded methods
then this array will be ignored but a warning will be omitted if the desired parameters were
not compatible.
.PARAMETER DelegateType
The delegate to create for the corresponding method. Example: [string]::format | get-delegate -delegatetype func[int,string]
.INPUTS System.Management.Automation.PSMethod, System.Type[]
.EXAMPLE
$delegate = [string]::format | Get-Delegate string,string

Gets a delegate for a matching overload with string,string parameters.
It will actually return func which is the correct 
signature for invoking string.format with string,string.
.EXAMPLE
$delegate = [console]::beep | Get-Delegate @()

Gets a delegate for a matching overload with no parameters.
.EXAMPLE
$delegate = [console]::beep | get-delegate int,int

Gets a delegate for a matching overload with @(int,int) parameters.
.EXAMPLE
$delegate = [string]::format | Get-Delegate -Delegate 'func[string,object,string]'

Gets a delegate for an explicit func[].
.EXAMPLE
$delegate = [console]::writeline | Get-Delegate -Delegate 'action[int]'

Gets a delegate for an explicit action[].
.EXAMPLE
$delegate = [string]::isnullorempty | get-delegate 

For a method with no overloads, we will choose the default method and create a corresponding action/action[] or func[].
#>
    [CmdletBinding(DefaultParameterSetName="FromParameterType")]
    [outputtype('System.Action','System.Action[]','System.Func[]')]
    param(
        [parameter(mandatory=$true, valuefrompipeline=$true)]
        [system.management.automation.psmethod]$Method,

        [parameter(position=0, valuefromremainingarguments=$true, parametersetname="FromParameterType")]
        [validatenotnull()]
        [allowemptycollection()]
        [Alias("types")]
        [type[]]$ParameterType = @(),

        [parameter(mandatory=$true, parametersetname="FromDelegate")]
        [validatenotnull()]
        [validatescript({ ([delegate].isassignablefrom($_)) })]
        [type]$DelegateType
    )

    $base = $method.GetType().GetField("baseObject","nonpublic,instance").GetValue($method)    
    
    if ($base -is [type]) {
        [type]$baseType = $base
        [reflection.bindingflags]$flags = "Public,Static"
    } else {
        [type]$baseType = $base.GetType()
        [reflection.bindingflags]$flags = "Public,Instance"
    }

    if ($pscmdlet.ParameterSetName -eq "FromDelegate") {
        write-verbose "Inferring from delegate."

        if ($DelegateType -eq [action]) {
            # void action        
            $ParameterType = [type[]]@()
        
        } elseif ($DelegateType.IsGenericType) {
            # get type name
            $name = $DelegateType.Name

            # is it [action[]] ?
            if ($name.StartsWith("Action``")) {
    
                $ParameterType = @($DelegateType.GetGenericArguments())    
            
            } elseif ($name.StartsWith("Func``")) {
    
                # it's a [func[]]
                $ParameterType = @($DelegateType.GetGenericArguments())
                $ParameterType = $ParameterType[0..$($ParameterType.length - 2)] # trim last element (TReturn)
            } else {
                throw "Unsupported delegate type: Use Action<> or Func<>."
            }
        }
    }

    [reflection.methodinfo]$methodInfo = $null

    if ($Method.OverloadDefinitions.Count -gt 1) {
        # find best match overload
        write-verbose "$($method.name) has multiple overloads; finding best match."

        $finder = [type].getmethod("GetMethodImpl", [reflection.bindingflags]"NonPublic,Instance")

        write-verbose "base is $($base.gettype())"

        $methodInfo = $finder.invoke(
            $baseType,
             @(
                  $method.Name,
                  $flags,
                  $null,
                  $null,
                  [type[]]$ParameterType,
                  $null
             )
        ) # end invoke
    } else {
        # method not overloaded
        Write-Verbose "$($method.name) is not overloaded."
        if ($base -is [type]) {
            $methodInfo = $base.getmethod($method.name, $flags)
        } else {
            $methodInfo = $base.gettype().GetMethod($method.name, $flags)
        }

        # if parametertype is $null, fill it out; if it's not $null,
        # override it to correct it if needed, and warn user.
        if ($pscmdlet.ParameterSetName -eq "FromParameterType") {           
            if ($ParameterType -and ((compare-object $parametertype $methodinfo.GetParameters().parametertype))) {
                write-warning "Method not overloaded: Ignoring provided parameter type(s)."
            }
            $ParameterType = $methodInfo.GetParameters().parametertype
            write-verbose ("Set default parameters to: {0}" -f ($ParameterType -join ","))
        }
    }

    if (-not $methodInfo) {
        write-warning "Could not find matching signature for $($method.Name) with $($parametertype.count) parameter(s)."
    } else {
        
        write-verbose "MethodInfo: $methodInfo"

        # it's important here to use the actual MethodInfo's parameter types,
        # not the desired types ($parametertype) because they may not match,
        # e.g. asked for method(int) but match is method(object).

        if ($pscmdlet.ParameterSetName -eq "FromParameterType") {            
            
            if ($methodInfo.GetParameters().count -gt 0) {
                $ParameterType = $methodInfo.GetParameters().ParameterType
            }
            
            # need to create corresponding [action[]] or [func[]]
            if ($methodInfo.ReturnType -eq [void]) {
                if ($ParameterType.Length -eq 0) {
                    $DelegateType = [action]
                } else {
                    # action<...>
                    
                    # replace desired with matching overload parameter types
                    #$ParameterType = $methodInfo.GetParameters().ParameterType
                    $DelegateType = ("action[{0}]" -f ($ParameterType -join ",")) -as [type]
                }
            } else {
                # func<...>

                # replace desired with matching overload parameter types
                #$ParameterType = $methodInfo.GetParameters().ParameterType
                $DelegateType = ("func[{0}]" -f (($ParameterType + $methodInfo.ReturnType) -join ",")) -as [type]
            }                        
        }
        Write-Verbose $DelegateType

        if ($flags -band [reflection.bindingflags]::Instance) {
            $methodInfo.createdelegate($DelegateType, $base)
        } else {
            $methodInfo.createdelegate($DelegateType)
        }
    }
}

Here are some tests to demonstrate the various cases covered. They were pretty much essential while I was tweaking the script.

# general test function
function Assert-True {
    param(
        [parameter(position=0, mandatory=$true)]
        [validatenotnull()]
        [scriptblock]$Script,

        [parameter(position=1)]
        [validatenotnullorempty()]
        [string]$Name = "Assert-True"
    )    
    $eap = $ErrorActionPreference
    Write-Host -NoNewline "Assert-True [ $Name ] "
    try {
        $erroractionpreference = "stop"
        if ((& $script) -eq $true) {
            write-host -ForegroundColor Green "[PASS]"
            return
        }
        $reason = "Assert failed."
    }
    catch {
        $reason = "Error: $_"
    }
    finally {
        $ErrorActionPreference = $eap
    }
    write-host -ForegroundColor Red "[FAIL] " -NoNewline
    write-host "Reason: '$reason'"
}

#
# static methods
#

assert-true {
    $delegate = [string]::format | Get-Delegate -Delegate 'func[string,object,string]'
    $delegate.invoke("hello, {0}", "world") -eq "hello, world"
} -name "[string]::format | get-delegate -delegate 'func[string,object,string]'"

assert-true {
    $delegate = [console]::writeline | Get-Delegate -Delegate 'action[int]'
    $delegate -is [action[int]]
} -name "[console]::writeline | get-delegate -delegate 'action[int]'"

assert-true {
    $delegate = [string]::format | Get-Delegate string,string
    $delegate.invoke("hello, {0}", "world") -eq "hello, world"
} -name "[string]::format | get-delegate string,string"

assert-true {
    $delegate = [console]::beep | Get-Delegate @()
    $delegate -is [action]
} -name "[console]::beep | get-delegate @()"

assert-true {
    $delegate = [console]::beep | Get-Delegate -DelegateType action
    $delegate -is [action]
} -name "[console]::beep | Get-Delegate -DelegateType action"

assert-true {
    $delegate = [string]::IsNullOrEmpty | get-delegate
    $delegate -is [func[string,bool]]
} -name "[string]::IsNullOrEmpty | get-delegate # single overload"

assert-true {
    $delegate = [string]::IsNullOrEmpty | get-delegate string
    $delegate -is [func[string,bool]]
} -name "[string]::IsNullOrEmpty | get-delegate string # single overload"

#
# instance methods
#

assert-true {
    $sb = new-object text.stringbuilder
    $delegate = $sb.Append | get-delegate string
    $delegate -is [System.Func[string,System.Text.StringBuilder]]
} -name "`$sb.Append | get-delegate string"

assert-true {
    $sb = new-object text.stringbuilder
    $delegate = $sb.AppendFormat | get-delegate string, int, int
    $delegate -is [System.Func[string,object,object,System.Text.StringBuilder]]
} -name "`$sb.AppendFormat | get-delegate string, int, int"

Tags:

.NET | PowerShell 3.0 | PowerShell | Reflection

PowerShell 3.0–Now with Property Unrolling!

by oising 16. March 2012 22:37

There are many new improvements to the language and parser in v3 (some of which I hope to cover over the next few posts) but one of my favourites is what Microsoft are calling singleton/array enumeration (or something equally obtuse.) I am hereby christening it “Property Unrolling” as it works similarly to how PowerShell does automatic collection unrolling when piping an enumerable (list, collection, array.)

This powershell 3.0 technique is where you can take a variable that contains an array (or collection, list or anything else that is enumerable) like $myarray and if you want to access a property on each element in that array, you no longer need to use foreach-object with $_.propertyName to access it. Instead, you can simply type $myarray.propertyName and powershell will return that property from each element in the array, but only if the array itself does not have that property. For example if you had an array of strings, asking for $arr.length would return the length of the array, and not the length of each string. The best way to show this is with some examples:

Array of files

# the older way (still works)
dir | foreach-object { $_.lastwritetime } | sort

# now, here's the shortcut way for v3
(dir).lastwritetime | sort

XML

Here's an example on how working with XML just got ten times easier. Here's some XML:

    
                     
             42
        
                     
             43
        
                     
             44
        
    

Here's a script that dumps the prop value in each element:

# the older way
$xml = [xml]" ... "
$xml.root.element | foreach-object { $_.prop }

# the v3 way ;)
$xml.root.element.prop

This is such a time saver. Thank you Microsoft!

Tags:

PowerShell | PowerShell 3.0 | XML | Monad | Beta

Bypassing Restricted Execution Policy in Code or in Script

by oising 10. February 2012 23:34

Many businesses are averse to moving away from a restricted execution policy because they don't really understand it. As Microsoft will tell you, It's not a security boundary - it's just an extra hoop to jump through so you don't shoot yourself in the foot by running something you shouldn’t. If you want to run ps1 scripts in your own application, simply host your own Runspace and use the base authorization manager which pays no heed to system execution policy, even if it’s controlled by group policy and immune to powershell –bypass and set-executiopolicy:

Bypassing in Code

InitialSessionState initial = InitialSessionState.CreateDefault(); 
 
// Replace PSAuthorizationManager with a null manager
// which ignores execution policy 
initial.AuthorizationManager = new 
      System.Management.Automation.AuthorizationManager("MyShellId"); 
 
// Extract psm1 from resource, save locally 
// ... 
 
// load my extracted module with my commands 
initial.ImportPSModule(new[] { <path_to_psm1> }); 
 
// open runspace 
Runspace runspace = RunspaceFactory.CreateRunspace(initial); 
runspace.Open(); 
 
RunspaceInvoke invoker = new RunspaceInvoke(runspace); 
 
// execute a command from my module 
Collection<PSObject> results = invoker.Invoke("my-command"); 
 
// or run a ps1 script     
Collection<PSObject> results = invoker.Invoke(@"c:\program files\myapp\my.ps1");

By using this null authorization manager, execution policy is completed ignored. Remember - this is not some "hack" because execution policy is something for protecting users against themselves. It's not for protecting against malicious third parties, like some kind of script firewall. Whatever could be put in a script, could be run by hand in a dozen different ways using invoke-expression, and even file based scripts can be executed this way: invoke-expression (get-content .\foo.ps1).

Bypassing in Script

Now this is a little more hackish because it involves manipulating powershell.exe internals at runtime. This is a useful one-liner (if you can memorise it) when you find yourself in one of those clients who has GPO controlled execution policy. It’s pushing it for a one-liner, I know, but hey:

function Disable-ExecutionPolicy {
    ($ctx = $executioncontext.gettype().getfield(
        "_context","nonpublic,instance").getvalue(
            $executioncontext)).gettype().getfield(
                "_authorizationManager","nonpublic,instance").setvalue(
        $ctx, (new-object System.Management.Automation.AuthorizationManager
                  "Microsoft.PowerShell"))
}

This function will swap out the powershell host’s AuthorizationManager implementation (PSAuthorizationManager) with the null, policy-ignoring version. Execution policy will be effectively unrestricted, regardless of enterprise, machine or user level attempts to set it to restricted. This is an in-memory bypass only – when powershell.exe is closed and restarted, it’s back to business (or lack thereof.)

Have fun!

Tags:

.NET | Developer | PowerShell 2.0 | PowerShell 3.0 | PowerShell | Reflection

PowerShell – Module Installation Best Practices

by oising 16. December 2011 11:47

I’m seeing a few errant companies have their installers throw their modules into ${env:systemroot}\WindowsPowerShell\1.0\Modules but this is not the right place. The only things that should go there are core operating system modules from Microsoft. So, where should you install them?

How to: Install a module for all users

  1. Create the folder ${env:programfiles}\YourProduct\PowerShell\Modules\
  2. Place your module (or modules) under this folder
  3. Add the folder from step 1 to the system scoped environment variable PSModulePath; consider embedding %ProgramFiles% to keep the environment string as short as possible
  4. Profit.

How to: Install a module for the current user

  1. Test for, and create if necessary the folder which is the result of this call (or equivalent in managed code): join-path ([environment]::GetFolderPath("MyDocuments")) WindowsPowerShell\Modules
  2. Copy your module or (modules) to folder at above
  3. Profit.

It’s as easy as that.

Tags:

Modules | PowerShell | PowerShell 2.0 | PowerShell 3.0 | PowerShell ISE | Best Practice

PowerShell 3.0–Now with a legible registry provider!

by oising 9. October 2011 13:21

I don’t need to say anything to accompany these pictures. A screenshot or three is worth a thousand words:

PS C:\> gi hklm:\software\microsoft\windows\currentversion

    Hive: Registry::HKEY_LOCAL_MACHINE\software\microsoft\windows

Name                           Property
----                           --------
currentversion                 SM_GamesName             : Games
                               SM_ConfigureProgramsName : Set Program Access and Defaults
                               CommonFilesDir           : C:\Program Files\Common Files
                               CommonFilesDir (x86)     : C:\Program Files (x86)\Common Files
                               CommonW6432Dir           : C:\Program Files\Common Files
                               DevicePath               : C:\windows\inf
                               MediaPathUnexpanded      : C:\windows\Media
                               ProgramFilesDir          : C:\Program Files
                               ProgramFilesDir (x86)    : C:\Program Files (x86)
                               ProgramFilesPath         : C:\Program Files
                               ProgramW6432Dir          : C:\Program Files
                               SM_AccessoriesName       : Accessories
                               PF_AccessoriesName       : Accessories

Now here’s the corresponding view in the Registry Editor:

image

Awesomesauce!

Tags:

PowerShell 3.0 | PowerShell | Monad

About the author

Oisin Grehan lives in Montreal, Canada and builds all sorts of crap for all sorts of people.

Month List

Page List