Background-Job run Scripts without hardcoding them - powershell

I am running a PowerShell GUI that calls different scripts. I'm currently using hardcoded paths such as:
function start-jobhere([scriptblock]$block) {
Start-Job -ArgumentList (Get-Location),$block {
Set-Location $args[0];
Invoke-Expression $args[1]
}
}
$handler_button1_Click = {
$job1 = start-jobhere {& K:\Uploader\Import\DataUploader.ps1} -Name "Uploader"
}
I tried to avoid it using:
$LocalPath = ($MyInvocation.MyCommand.Path).ToLower().Replace("datauploader.ps1", "")
$handler_button1_Click = {
$job1 = start-jobhere {& $LocalPath\DataUploader.ps1} -Name "Uploader"
}
But it doesn't seem to work. I have some scripts in the same folder and some are on a different harddrive. Is there a way to avoid the hardcoded paths?

You're passing a scriptblock to the function. The code inside the scriptblock doesn't know anything about variables in the rest of the script unless you make them known via the using scope modifier:
$job1 = start-jobhere {& "$using:LocalPath\DataUploader.ps1"} -Name "Uploader"
With that said, if you want to run scripts anyway, why aren't you passing their path as a string to your function?
function start-jobhere([string]$Name, [string]$Script) {
Start-Job -Name $Name -ScriptBlock {
Set-Location $args[0]
& $args[1]
} -ArgumentList (Get-Location), $Script
}
$handler_button1_Click = {
$job1 = start-jobhere "$LocalPath\DataUploader.ps1" -Name "Uploader"
}

Related

Getting an error when executing a nested ScriptBlock from Invoke-Command

I'm looking for method to create a wrapper around Invoke-Command that restores the current directory that I'm using on the remote machine before invoking my command. Here's what I tried to do:
function nice_invoke {
param(
[string]$Computer,
[scriptblock]$ScriptBlock
)
Set-PSDebug -Trace 0
$cwd = (Get-Location).Path
write-host "cmd: $cwd"
$wrapper = {
$target = $using:cwd
if (-not (Test-Path "$target")) {
write-host "ERROR: Directory doesn't exist on remote"
exit 1
}
else {
Set-Location $target
}
$sb = $using:ScriptBlock
$sb.Invoke() | out-host
}
# Execute Command on remote computer in Same Directory as Local Machine
Invoke-Command -Computer pv3039 -ScriptBlock $wrapper
}
Command Line:
PS> nice_invoke -Computer pv3039 -ScriptBlock {get-location |out-host; get-ChildItem | out-host }
Error Message:
Method invocation failed because [System.String]
does not contain a method named 'Invoke'.
+ CategoryInfo : InvalidOperation: (:) [], RuntimeException
+ FullyQualifiedErrorId : MethodNotFound
+ PSComputerName : pv3039
You can't pass a ScriptBlock like this with the $using: scope, it will get rendered to a string-literal first. Use the [ScriptBlock]::Create(string) method instead within your $wrapper block to create a ScriptBlock from a String:
$sb = [ScriptBlock]::Create($using:ScriptBlock)
$sb.Invoke() | Out-Host
Alternatively, you could also use Invoke-Command -ArgumentList $ScriptBlock, but you still have the same issue with the ScriptBlock getting rendered as a string. Nonetheless, here is an example for this case as well:
# Call `Invoke-Command -ArgumentList $ScriptBlock`
# $args[0] is the first argument passed into the `Invoke-Command` block
$sb = [ScriptBlock]::Create($args[0])
$sb.Invoke() | Out-Host
Note: While I kept the format here in the way you were attempting to run the ScriptBlock in your original code, the idiomatic way to run ScriptBlocks locally (from the perspective the nested ScriptBlock it is a local execution on the remote machine) is to use the Call Operator like & $sb rather than using $sb.Invoke().
With either approach, the nested ScriptBlock will execute for you from the nested block now. This limitation is similar to how some other types are incompatible with shipping across remote connections or will not survive serialization with Export/Import-CliXml; it is simply a limitation of the ScriptBlock type.
Worthy to note, this limitation persists whether using Invoke-Command or another cmdlet that initiates execution via a child PowerShell session such as Start-Job. So the solution will be the same either way.
function nice_invoke {
param(
[string]$Computer,
[scriptblock]$ScriptBlock
)
Set-PSDebug -Trace 0
$cwd = (Get-Location).Path
write-host "cmd: $cwd"
$wrapper = {
$target = $using:cwd
if (-not (Test-Path "$target")) {
write-host "ERROR: Directory doesn't exist on remote"
exit 1
}
else {
Set-Location $using:cwd
}
$sb = [scriptblock]::Create($using:ScriptBlock)
$sb.Invoke()
}
# Execute Command on remote computer in Same Directory as Local Machine
Invoke-Command -Computer pv3039 -ScriptBlock $wrapper
}
nice_invoke -Computer pv3039 -ScriptBlock {
hostname
get-location
#dir
}

Specify NON-hardcoded path and filename in a scriptblock?

Ok I feel spechul for even asking, but I have tried several iterations of this and nothing has worked except hard coding the scriptname in the Scriptblock statement, which is unacceptable.
Here is the code that works, hard coded, unacceptable....
$Scriptblock = { C:\Scripts\Path1\ScriptName.ps1 -arguement0 $args[0] -arguement1 $args[1] }
Start-Job -ScriptBlock $Scriptblock -ArgumentList $argue0, $argue1 | Out-Null
Ive tried this, and it doesn't work...
$loc = (Get-Location).Path
Set-Location -Path $loc
And this....
$rootpath = $MyInvocation.MyCommand.Path.Substring(0, ($MyInvocation.MyCommand.Path).LastIndexOf("\"))
Set-Location -Path $rootpath
And this....
$rootpath = $MyInvocation.MyCommand.Path.Substring(0, ($MyInvocation.MyCommand.Path).LastIndexOf("\"))
$scriptFilename = $([string]::Format("{0}\ScriptName.ps1", $rootpath))
$sb = $([string]::Format("{0} -arguement0 $args[0] -arguement1 $args[1]", $scriptFilename))
$Scriptblock = { $sb }
Start-Job -ScriptBlock $Scriptblock -ArgumentList $argue0, $argue1 | Out-Null
Nothing else has worked except the first code above with hardcoded path and script name - I know it has to be something stupid I am missing - help me fix stoopid please! ;-)
In your last example, this line:
$ScriptBlock = { $sb }
simply creates a scriptblock with a string inside it. Change it to:
$ScriptBlock = [scriptblock]::Create($sb)

PowerShell Splatting the Argumentlist on Invoke-Command

How is it possible to use the parameters collected in a hash table for use with ArgumentList on Invoke-Command?
$CopyParams = #{
Source = 'E:\DEPARTMENTS\CBR\SHARE\Target'
Destination = 'E:\DEPARTMENTS\CBR\SHARE\Target 2'
Structure = 'yyyy-MM-dd'
}
Invoke-Command -Credential $Cred -ComputerName 'SERVER' -ScriptBlock ${Function:Copy-FilesHC} -ArgumentList #CopyParams
Whatever I try, it's always complaining about the 'Source':
Cannot validate argument on parameter 'Source'. The "Test-Path $_" validation script for the argument with
value "System.Collections.Hashtable" did not return true. Determine why the validation script failed
This blog talks about a similar problem, but I can't get it to work.
The same is true for a simple Copy-Item within Invoke-Command, example:
Invoke-Command -Credential $Cred -ComputerName 'SERVER' -ScriptBlock {Copy-Item} -ArgumentList #CopyParams
Invoke-Command : Missing an argument for parameter 'ArgumentList'. Specify a parameter of type 'System.Obj
ect[]' and try again.
At line:11 char:89
+ ... ck {Copy-Item} -ArgumentList #CopyParams
Thank you for your help.
One-liner, to convert a remote script to accept named parameters from a hash.
Given a scriptblock which you wish to call like this:
$Options = #{
Parameter1 = "foo"
Parameter2 = "bar"
}
Invoke-Command -ComputerName REMOTESERVER -ArgumentList $Options -ScriptBlock {
param(
$Parameter1,
$Parameter2
)
#Script goes here, this is just a sample
"ComputerName: $ENV:COMPUTERNAME"
"Parameter1: $Parameter1"
"Parameter2: $Parameter2"
}
You can convert it like so
Invoke-Command -Computername REMOTESERVER -ArgumentList $Options -ScriptBlock {param($Options)&{
param(
$Parameter1,
$Parameter2
)
#Script goes here, this is just a sample
"ComputerName: $ENV:COMPUTERNAME"
"Parameter1: $Parameter1"
"Parameter2: $Parameter2"
} #Options}
What's going on? Essentially we've wrapped the original script block like so:
{param($Options)& <# Original script block (including {} braces)#> #options }
This makes the original script block an anonymous function, and creates the outer script block which has a parameter $Options, which does nothing but call the inner script block, passing #options to splat the hash.
Here's one way to approach passing named parameters:
function Copy-FilesHC
{
param ($Source,$Destination,$Structure)
"Source is $Source"
"Desintation is $Destination"
"Structure is $Structure"
}
$CopyParams = #{
Source = 'E:\DEPARTMENTS\CBR\SHARE\Target'
Destination = "'E:\DEPARTMENTS\CBR\SHARE\Target 2'" #Nested quotes required due to embedded space in value.
Structure = 'yyyy-MM-dd'
}
$SB = [scriptblock]::Create(".{${Function:Copy-FilesHC}} $(&{$args}#CopyParams)")
Invoke-Command -Credential $Cred -ComputerName 'SERVER' -ScriptBlock $SB
Basically, you create a new script block from your invoked script, with the parameters splatted to that from the hash table. Everything is already in the script block with the values expanded, so there's no argument list to pass.
I found a workaround, but you have to make sure that your Advanced function which is located in your module file is loaded up front in the local session. So it can be used in the remote session. I wrote a small helper function for this.
Function Add-FunctionHC {
[CmdletBinding(SupportsShouldProcess=$True)]
Param(
[String]$Name
)
Process {
Try {
$Module = (Get-Command $Name -EA Stop).ModuleName
}
Catch {
Write-Error "Add-FunctionHC: Function '$Name' doesn't exist in any module"
$Global:Error.RemoveAt('1')
Break
}
if (-not (Get-Module -Name $Module)) {
Import-Module -Name $Module
}
}
}
# Load funtion for remoting
Add-FunctionHC -Name 'Copy-FilesHC'
$CopyParams = #{
Source = 'E:\DEPARTMENTS\CBR\SHARE\Target\De file.txt'
Destination = 'E:\DEPARTMENTS\CBR\SHARE\Target 2'
}
$RemoteFunctions = "function Copy-FilesHC {${function:Copy-FilesHC}}" #';' seperated to add more
Invoke-Command -ArgumentList $RemoteFunctions -ComputerName 'SERVER' -Credential $Cred -ScriptBlock {
Param (
$RemoteFunctions
)
. ([ScriptBlock]::Create($RemoteFunctions))
$CopyParams = $using:CopyParams
Copy-FilesHC #CopyParams
}
The big advantage is that you don't need to copy your complete function in the script and it can stay in the module. So when you change something in the module to the function it will also be available in the remote session, without the need to update your script.
I recently experienced a similar problem and solved it by building the hash (or rebuilding the hash) inside the invoke by leveraging the $using variable scope (more on that here)
it looks something like this:
$Source = 'E:\DEPARTMENTS\CBR\SHARE\Target'
$Destination = 'E:\DEPARTMENTS\CBR\SHARE\Target 2'
$Structure = 'yyyy-MM-dd'
Invoke-Command -Credential $Cred -ComputerName 'SERVER' -ScriptBlock {
$CopyParms= #{
'Source'=$Using:Source
'Destination'=$Using:Destination
'Structure'=$Using:Structure
}
Function:Copy-FilesHC #CopyParms
}
This is what works for me:
$hash = #{
PARAM1="meaning of life"
PARAM2=42
PARAM3=$true
}
$params = foreach($x in $hash.GetEnumerator()) {"$($x.Name)=""$($x.Value)"""}
I know this is late, but I ran into the same problem and found a solution that worked for me. Assigning it to a variable within the scriptblock and then using that variable to splat didn't show any problems.
Here's an example:
$param=#{"parameter","value"}
invoke-command -asjob -session $session -ScriptBlock {$a=$args[0];cmdlet #a } -ArgumentList $param

Calling Another Powershell Script With A Timeout

I have this function:
function getHDriveSize($usersHomeDirectory)
{
$timeOutSeconds = 600
$code =
{
$hDriveSize = powershell.exe $script:getHDriveSizePath - path $usersDirectory
return $hDriveSize
}
$job = Start-Job -ScriptBlock $code
if (Wait-Job $job -Timeout $timeOutSeconds)
{
$receivedJob = Receive-Job $job
return $receivedJob
}
else
{
return "Timed Out"
}
}
When I call it, I get a CommandNotFoundException:
-path : The term '-path' is not recognized as the name of a cmdlet, function, script file, or operable program. Check the spelling of the name, or if a path was included, verify that the path is correct and try again.
However, the line:
$hDriveSize = powershell.exe $script:getHDriveSizePath - path $usersDirectory
by itself works fine.
How can I call the powershell script within the $code variable successfully?
Variables and functions defined outside the scriptblock are not available inside the scriptblock. Because of this, both $script:getHDriveSizePath and $usersDirectory inside the scriptblock are $null, so that you're actually trying to run the statement powershell.exe -Path, which produces the error you observed. You need to pass variables as parameters into the scriptblock:
function getHDriveSize($usersHomeDirectory) {
$timeOutSeconds = 600
$code = {
& powershell.exe $args[0] -Path $args[1]
}
$job = Start-Job -ScriptBlock $code -ArgumentList $script:getHDriveSizePath, $usersHomeDirectory
if (Wait-Job $job -Timeout $timeOutSeconds) {
Receive-Job $job
} else {
'Timed Out'
}
}

Invoke-Command with dynamic function name

I found this awesome post: Using Invoke-Command -ScriptBlock on a function with arguments
I'm trying to make the function call (${function:Foo}) dynamic, as in I want to pass the function name.
I tried this:
$name = "Foo"
Invoke-Command -ScriptBlock ${function:$name}
but that fails. I also tried various escape sequences, but just can't get the function name to be dynamic.
EDIT: For clarity I am adding a small test script. Of course the desired result is to call the ExternalFunction.
Function ExternalFunction()
{
write-host "I was called externally"
}
Function InternalFunction()
{
Param ([parameter(Mandatory=$true)][string]$FunctionName)
#working: Invoke-Command -ScriptBlock ${function:ExternalFunction}
#not working: Invoke-Command -ScriptBlock ${invoke-expression $FunctionName}
if (Test-Path Function:\$FunctionName) {
#working,but how to use it in ScriptBlock?
}
}
InternalFunction -FunctionName "ExternalFunction"
Alternate solution:
function foo {'I am foo!'}
$name = 'foo'
$sb = (get-command $name -CommandType Function).ScriptBlock
invoke-command -scriptblock $sb
I am foo!
as simple as :
invoke-expression $name
or if you want to keep invoke-commande for remoting for example
Invoke-Command -ScriptBlock { invoke-expression $name}
You could try the following. It tests if the name specified is a valid function before it attempts to run it:
$myfuncnamevar = "Foo"
Invoke-Command -ScriptBlock {
param($name)
if (Test-Path Function:\$name) {
#Function exists = run it
& $name
}
} -ArgumentList $myfuncnamevar