Use local functions in PowerShell Job - powershell

I am writing custom cmdlets to perform a task. One of the cmdlets depends on the other ones and is taking a really long time, so I wanted to perform that task inside a job. Well guess what, you can't because the other cmdlets are not available inside that Job's scope. Why? The other languages out there like C++, Java, C# allow you to use variables, objects, functions from whitin the same scope, why isn't this available in PowerShell? Why is it so decoupled? I feel like it makes it harder for developers. Maybe I don't get the topic, but I would like to do something like this:
function Write-Yes {
Write-Host "yes"
}
function Write-No {
Write-Host "no"
}
function Write-Random {
$result = #($true, $false) | Get-Random
if ($result) {
Write-Yes
}
else {
Write-No
}
}
Start-Job -ScriptBlock { Write-Random }
This is not possible. You have to do some hacks like providing the scriptblock of the function as the argument and call it using the call operator or something like this. Or even, use Import-Module to reimport the same file that you are working in. This feels overly complicated. The only module that I saw that is able to do something like this is PoshRSJob that allows you to name the cmdlets that will be used inside the job and it will create them dynamically for you, with some lexical parsing and again, overly complicated things.
Why are the things like they are and is there any way to do what I'm trying in the example in an elegat way?

Start-Job is very limited and slow. It was implemented very poorly imho and I never use it. You can use runspaces for fast and lightweight "background jobs", and import functions and variables from your current session.
Example:
function Write-Yes { "yes" }
function Write-No { "no" }
function Write-Random {
if ($true, $false | Get-Random) {
Write-Yes
}
else {
Write-No
}
}
# setup session and import functions
$session = [System.Management.Automation.Runspaces.InitialSessionState]::CreateDefault()
"Write-Yes", "Write-No", "Write-Random" | foreach {
$session.Commands.Add((
New-Object System.Management.Automation.Runspaces.SessionStateFunctionEntry $_, (Get-Content "Function:\$_")
))
}
# setup separate powershell instance
$job = [Powershell]::Create($session)
[void]$job.AddScript({ Write-Random })
# start async
$asyncResult = $job.BeginInvoke()
# do stuff ...
# wait for completion
$job.EndInvoke($asyncResult)
$job.Dispose()
But in general, Powershell is not made for complex parallel processing. In general, it's best to put everything inside a script file and run that, as a task or background job etc.

Related

Can a powershell module call functions in its importer's scope?

Is it possible for a powershell module to call functions that live in its importer's scope?
For example, say I have module.psm1, and script.ps1. Inside script.ps1 I import module.psm1, and define a function called Execute-Payload. Is there any way for the code in module.psm1 to call Execute-Payload?
If I understood correctly what you're trying to do, there are 2 commonly used ways to load custom functions to your main script. I'll give you a few examples, since, I'm not sure if there is a best practice for this.
The script being executed will always be main.ps1 on all given examples.
Example 1: All functions are stored on one file
Folder structure
../path/to/script/main.ps1
../path/to/script/Functions/functions.ps1
Functions.ps1
function myCustomFunction1 {
....
}
function myCustomFunction2 {
....
}
function myCustomFunction3 {
....
}
main.ps1
On the first lines of code you could add something like this:
$ErrorActionPreference = 'Stop'
# Option 1: Using Import-Module.
try
{
Import-Module "$PSScriptRoot\Functions\functions.ps1"
}
catch
{
"Failed to load dependency Functions.`nError: $_"
}
# Option 2: Dot Sourcing
try
{
. "$PSScriptRoot\Functions\functions.ps1"
}
catch
{
"Failed to load dependency Functions.`nError: $_"
}
Note: both options will load ALL functions.
Example 2: Many functions stored on different files. This is used when you have lots of complex and/or lengthy functions.
Folder structure
../path/to/script/main.ps1
../path/to/script/Functions/myCustomFunction1.ps1
../path/to/script/Functions/myCustomFunction2.ps1
../path/to/script/Functions/myCustomFunction3.ps1
myCustomFunction1.ps1
function myCustomFunction1 {
....
}
myCustomFunction2.ps1
function myCustomFunction2 {
....
}
myCustomFunction3.ps1
function myCustomFunction3 {
....
}
main.ps1
On the first lines of code you could add something like this:
$ErrorActionPreference = 'Stop'
# Option 1: Using Import-Module.
try
{
Get-ChildItem "$PSScriptRoot\Functions\*.ps1" | Import-Module
}
catch
{
"Failed to load dependency Functions.`nError: $_"
}
# Option 2: Dot Sourcing
try
{
Get-ChildItem "$PSScriptRoot\Functions\*.ps1" | ForEach-Object {
. $_.FullName
}
}
catch
{
"Failed to load dependency Functions.`nError: $_"
}
From inside an (advanced) function in your module, you can use $PSCmdlet.InvokeCommand.InvokeScript() with argument $PSCmdlet.SessionState and a script block created from a string to execute arbitrary code in the caller's scope.
The technique was gratefully adapted from this comment on GitHub.
For brevity, the following code demonstrates the technique with a dynamic module created with New-Module, but it equally applies to regular, persisted modules:
# The function to call from the module.
function Execute-Payload {
"Inside Execute-Payload in the script's scope."
}
# Create (and implicitly import) a dynamic module.
$null = New-Module {
# Define the advanced module function that calls `Execute-Payload`
# in its caller's scope.
function Invoke-Something {
[CmdletBinding()]
param()
$PSCmdlet.InvokeCommand.InvokeScript(
$PSCmdlet.SessionState,
# Pass a *string* with arbitrary code to execute in the caller's scope to
# [scriptblock]::Create().
# !! It is imperative that [scriptblock]::Create() be used, with
# !! a *string*, as it creates an *unbound* script block that then
# !! runs in the specified session.
# !! If needed, use string expansion to "bake" module-local variable
# !! values into the string, or pass parameters as additional arguments.
# !! However, if a script block is passed *by the caller*,
# !! bound to *its* context, it *can* be used as-is.
[scriptblock]::Create(' Execute-Payload ')
)
}
}
# Now invoke the function defined in the module.
Invoke-Something
The above outputs Inside Execute-Payload in the script's scope., proving that the script's function was called.

PowerShell scriptblock scope weirdness

I am using Invoke-Command on the local (not remote) computer to run a scriptblock that includes a function. Some of the variables defined at the "root" of the scriptblock are available in the function, and some require me to scope them as Global:.
So in quasi-code, this fails:
{
function test() {
if (test-path $filepath.trim('t')) {
$taskFolder.gettasks(1) |out-file '$env:temp\tasks.txt'
}
}
$filepath = '$env:temp\test.txt'
$TaskService = new-object -ComObject('Schedule.Service')
$TaskService.connect()
$TaskFolder = $TaskService.GetFolder('\')
test
}
with a "You cannot call a method on a null-valued expression." (i.e. $filepath -eq $null).
However, this works:
{
function test() {
if (test-path $Global:filepath.trim('t')) {
$taskFolder.gettasks(1) |out-file '$env:temp\tasks.txt'
}
}
$Global:filepath = '$env:temp\test.txt'
$TaskService = new-object -ComObject('Schedule.Service')
$TaskService.connect()
$TaskFolder = $TaskService.GetFolder('\')
test
}
even though $TaskFolder is not Global.
I can't seem to replicate the problem with short, simple code like this here, but the effect of ONLY globally scoping a couple variables makes all the difference.
I initially noticed the issue when I was attempting to use the variables with Script: scope and that was failing. I assumed that was because this was a script block and not an actual script. Changing to Global: resolved the problem and then I noticed the non-scoped $taskfolder variable.
I am not asking about scheduled tasks! I only included it in the example because that is the variable that is working fine without being assigned a global scope.
Obviously, I'm not understanding something about scoping, invoking, or maybe the Schedule Service.
Can someone enlighten me?

How to memoize a function in PowerShell?

I have a function string --> string in PowerShell that is quite slow to execute, and I would like to memoize it, that to preserve all the input/output pairs to speed-up an execution that calls this function over and over. I can think of many complicated ways of achieving this. Would anyone have anything not too convoluted to propose?
Well, here's a shot at it. I definitely cannot claim this is a good or efficient method; in fact, even coming up with something that works was tricky and a PowerShell expert might do better.
This is massive overkill in a scripting language, by the way (a global variable is far simpler), so this is definitely more of an intellectual exercise.
function Memoize($func) {
$cachedResults = #{}
{
if (-not $cachedResults.ContainsKey("$args")) {
echo "Remembering $args..." #for illustration
$cachedResults.Add("$args", $func.invoke($args))
}
$cachedResults["$args"]
}.getnewclosure()
}
function add($a, $b) {
return $a + $b;
}
$add = Memoize ${function:add};
&$add 5 4
&$add 5 4
&$add 1 2
I don't know if writing a .NET PowerShell module is practical in your case. If it is, you could use the similar technique as I did with one of my projects.
Steps:
Rewrite your function as a cmdlet
Have a static class with a static hash table where you store the results
Add a -Cache flag to the cmdlet so you can run it with and without the cache (if needed)
My scenario is a little different as I am not using a hash.
My cache: https://github.com/Swoogan/Octopus-Cmdlets/blob/master/Octopus.Extensions/Cache.cs
Usage: https://github.com/Swoogan/Octopus-Cmdlets/blob/master/Octopus.Cmdlets/GetEnvironment.cs
I thought of something pretty simple, like the following:
$array = #{}
function memoized
{
param ([string] $str)
if ($array[$str] -eq $null)
{
$array["$str"] = "something complicated goes on here"
}
return $array[$str]
}
memoized "testing"

Calling a powershell function with a global hotkey

I want to use a function from my Powershell script by triggering a global hotkey (the combination Ctrl+Shift+F12) which is registered and unregistered by my script. I need to access a .NET object created by my script. In pseudo code:
$object_i_need = New-Object SomeClass
register_hotkey "Ctrl+Shift+F12" hotkey_func
function hotkey_func { do_something_with $object_i_need }
wait_for_keypress
unregister_hotkey
Is this possible somehow?
If the .Net object supports Add_Keydown (like System.Windows.Forms.Form) you can do something like this...
$objWhatever.KeyPreview = $True
$objWhatever.Add_KeyDown({
if ($_.KeyCode -eq "Enter"){
hotkey_func{}
}
})

How does name lookup work in Powershell script blocks?

User cashfoley has posted what appears to be a fairly elegant set of code at codeplex for a "module" called PSClass.
When I dot-source the psclass code into some code of my own, I am able to write code like:
$Animal = New-PSClass Animal {
constructor {
param( $name, $legs )
# ...
}
method -override ToString {
"A $($this.Class.ClassName) named $($this.name) with $($this.Legs) Legs"
}
}
When I tried to create a module out of the PSClass code, however, I started getting errors. The constructor and method names are no longer recognized.
Looking at the actual implementation, what I see is that constructor, method, etc. are actually nested functions inside the New-PSClass function.
Thus, it seems to me that when I dot-source the PSClass.ps1 file, my script-blocks are allowed to contain references to functions nested inside other local functions. But when the PSClass code becomes a module, with the New-PSClass function exported (I tried both using a manifest and using Export-ModuleMember), the names are no longer visible.
Can someone explain to me how the script blocks, scoping rules, and visibility rules for nested functions work together?
Also, kind of separately, is there a better class definition protocol for pure Powershell scripting? (Specifically, one that does not involve "just write it in C# and then do this...")
The variables in your script blocks don't get evaluated until they are executed. If the variables in the script block don't exist in the current scope when the block is executed, the variables won't have any values. Script blocks aren't closures: they don't capture the context at instantiation time.
Remove-variable FooBar
function New-ScriptBlock
{
$FooBar = 1
$scriptBlock = {
Write-Host "FooBar: $FooBar"
}
$FooBar = 2
& $scriptBlock # Outputs FooBar: 2 because $FooBar was set to 2 before invocation
return $scriptBlock
}
function Invoke-ScriptBlock
{
param(
$ScriptBlock
)
& $ScriptBlock
}
$scriptBlock = New-ScriptBlock
& $scriptBlock # Prints nothing since $FooBar doesn't exist in this scope
$FooBar = 3
Invoke-ScriptBlock $scriptBlock # Prints $FooBar: 3 since FooBar set to 3