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

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
}

Related

Using Invoke-Command with Variables in ScriptBlock

New to PowerShell and learning through writing random scripts using the help info. I've tried the following 3 ways to properly get variables into the ScriptBlock(along with way too many small variations to list) with listed error message wrapped in **:
do
{
try {
[ValidateRange(1,7)][int]$days = Read-Host "Let's pull up some Warning event logs. How many days back would you like to go back? (1-7)"
} catch {}
} until ($?)
do
{
try {
[ValidateSet('desktop','documents',IgnoreCase)]$location = Read-Host "Would you like me to save the log on your Desktop or in your Documents?"
} catch {}
} until ($?)
$filename = Read-Host "What would you like to name the file?"
$DaysAgo = [datetime]::Now.AddDays(-$days)
Invoke-Command -AsJob -Jobname JobEventLog -ScriptBlock {Get-EventLog -logname system | Where-Object EntryType -eq Warning | where TimeGenerated -ge $DaysAgo | Out-File $HOME\$location\$filename.txt}
Invoke-Command : Parameter set cannot be resolved using the specified named parameters.
do
{
try {
[ValidateRange(1,7)][int]$days = Read-Host "Let's pull up some Warning event logs. How many days back would you like to go back? (1-7)"
} catch {}
} until ($?)
do
{
try {
[ValidateSet('desktop','documents',IgnoreCase)]$location = Read-Host "Would you like me to save the log on your Desktop or in your Documents?"
} catch {}
} until ($?)
$filename = Read-Host "What would you like to name the file?"
$DaysAgo = [datetime]::Now.AddDays(-$days)
Invoke-Command -AsJob -Jobname JobEventLog -ScriptBlock {Get-EventLog -logname system | Where-Object EntryType -eq Warning | where TimeGenerated -ge $Using:$DaysAgo | Out-File $Using:HOME\$Using:location\$Using:filename.txt}
Invoke-Command : Parameter set cannot be resolved using the specified named parameters.
do
{
try {
[ValidateRange(1,7)][int]$days = Read-Host "Let's pull up some Warning event logs. How many days back would you like to go back? (1-7)"
} catch {}
} until ($?)
do
{
try {
[ValidateSet('desktop','documents',IgnoreCase)]$location = Read-Host "Would you like me to save the log on your Desktop or in your Documents?"
} catch {}
} until ($?)
$filename = Read-Host "What would you like to name the file?"
Write-Host "Processing..."
$DaysAgo = [datetime]::Now.AddDays(-$days)
$parameters = #{
ScriptBlock = { Param ($Arg1,$Arg2,$Arg3) Invoke-Command -AsJob -Jobname JobEventLog -ScriptBlock {Get-EventLog -logname system | Where-Object source -eq DCOM | where TimeGenerated -ge $Arg1 | Out-File "$HOME\$Arg2\$Arg3.txt"}}
JobName = "DCOM"
ArgumentList = ($DaysAgo,$location,$filename)
}
Invoke-Command #parameters
Invoke-Command : Cannot validate argument on parameter 'ScriptBlock'. The argument is null. Provide a valid value for the argument, and then try running the command again.
I'm just looking to have user input how far back they want to view Event Logs, where to save it, and what to name it. I've been able to work my way through everything so far until I hit the Invoke-Command line and haven't been able to get through it. I prefer the one line style of 1 and 2 over the parameters style, however after spending way too much time using the help_Invoke-Command-full and googling I'm throwing in the towel over what I'm sure is a simple error on my syntax.
You can use $args inside the scriptblock, see an example:
$DaysAgo = [datetime]::Now.AddDays(-$days)
Invoke-Command -AsJob -Jobname JobEventLog -ScriptBlock {
Get-EventLog -logname system | Where-Object EntryType -eq Warning |
where TimeGenerated -ge $args[0] |
Out-File $HOME\$location\$filename.txt
} -ArgumentList $DaysAgo
Add the arguments at the end of the Invoke-Command like in the example and use $args[0] for the first argument, $args[1] for the second and so on...
This works for me. The computer is localhost as a test, at an elevated prompt, which you would need for the system log anyway. Level 3 is warning. If it was for the same computer you wouldn't need invoke-command at all.
$location = 'foo'
$filename = 'myfile'
$date = get-date
$daysago = $date.adddays(-1)
invoke-command localhost { param($daysago, $location, $filename)
get-winevent #{logname = 'system'; level = 3; starttime = $daysago} |
out-file $home\$location\$filename.txt } -args $daysago,$location,$filename
In order to use Invoke-Command's -AsJob switch, you must execute code remotely, such as by targeting a different computer with the -ComputerName or -Session arguments.
In the absence of such arguments, your command would run locally, but it fails due to the syntactic restriction described above.
If you want to run a job locally, use Start-Job directly:
$job = Start-Job -Name JobEventLog -ScriptBlock {
Get-EventLog -logname system |
Where-Object EntryType -eq Warning |
Where-Object TimeGenerated -ge $using:DaysAgo |
Out-File $HOME\$using:location\$using:filename.txt
}
Note: Since your background script block references variables from the caller's scope, they must be referenced via the $using: scope (as you've also done in your last Invoke-Command-based attempt). This requirement also applies to script blocks executed remotely, such as via Invoke-Command -ComputerName - see this answer for background information. The alternative is to pass arguments to the script block, via the -ArgumentList (-Args) parameter, though the $using: approach is usually simpler.
Start-Job returns a job-information object (System.Management.Automation.Job), which you can use to monitor the progress of and obtain output from the background job, using the various *-Job cmdlets, notably Wait-Job and Receive-Job - see the about_Jobs conceptual help topic.
Generally, using Invoke-Command for local code execution, while technically supported, is rarely necessary.
For direct, synchronous invocation (not as a job) of a command or script block, use &, the call operator (not needed for single commands, as long as the command name isn't quoted or specified via a variable), or, for execution directly in the caller's scope, ., the dot-sourcing operator (. { ... }).
You have a couple of options. Since you're only running this on your local machine, you can use Start-Job instead of Invoke-Command.
That being said, the problem that you're running into is 2-fold. First, if you're running the Invoke-Command cmdlet, you'll need to specify the ComputerName parameter. Even though it's an optional parameter, you'll need to use it to tell Powershell which parameter set you're using, otherwise it's going to get confused.
Secondly, you'll need to pass the arguments into the scriptblock. This is because Start-Job and Invoke-Command are part of PSRemoting and will actually look for environment variables on the specified computer instead of variables that you've declared in your script.
Here's what worked for me:
Invoke-Command -ComputerName $(hostname) -AsJob -JobName "TestJob" -ScriptBlock { Get-EventLog -logname system | Where-Object EntryType -eq Warning | Where-Object -property TimeGenerated -ge $args[0] | Out-File "$HOME\$($args[1])\$($args[2]).txt" } -ArgumentList $DaysAgo, $location, $filename
The Invoke-Command option is powerful if you're wanting to get this information from other devices on your network.
And here's the Start-Job version:
Start-Job -Name "TestJob2" -ScriptBlock { Get-EventLog -logname system | Where-Object EntryType -eq Warning | Where-Object -property TimeGenerated -ge $args[0] | Out-File "$HOME\$($args[1])\$($args[2]).txt" } -ArgumentList $DaysAgo, $location, $filename

How to pass object to Get-Job subprocess as reference

I need to write a powershell script that asynchronously reads from and writes to a System.IO.Ports.SerialPort object. However when writing the code to simply read from the object using Start-Job I'm getting an error. Here's my code so far:
$func = {
function CheckPort
{
param (
[parameter(Mandatory=$true,ValueFromPipeline=$true)]
[System.IO.Ports.SerialPort]$port
)
Write-Output $port.ReadLine()
}
}
$port = new-Object System.IO.Ports.SerialPort COM4, 9600, None, 8, one
$port.Open()
Start-Job -ScriptBlock {CheckPort $args[0]} -ArgumentList $port -Name “$computerName” -InitializationScript $func
When I run the code above, after I use Receive-Object to check the output of the subprocess, I see an error. It seems that instead of the $port object being passed as-is, it is first serialized then unserialized:
Error: "Cannot convert the "System.IO.Ports.SerialPort" value of type "Deserialized.System.IO.Ports.SerialPort" to type "System.IO.Ports.SerialPort"."
+ CategoryInfo : InvalidData: (:) [CheckPort], ParameterBindin...mationException
+ FullyQualifiedErrorId : ParameterArgumentTransformationError,CheckPort
+ PSComputerName : localhost
Is there anyway to pass the an argument by reference using Start-Job? $port.ReadLine() is blocking and there isn't an alternative method that can just check whether there is something to read, and I need to occasionally write to the port, so I definitely need asynchronous execution here.
I get the same error if I try $using:
$port = new-Object System.IO.Ports.SerialPort COM4, 9600, None, 8, one
$port.Open()
Start-Job -ScriptBlock {
$myPort = $using:port
Write-Output $myPort.ReadLine()
}
These two methods don't serialize the objects. You need PS 7 for foreach-object -parallel, but you can download start-threadjob in PS 5.
Start-ThreadJob {
$myPort = $using:port
$myPort.ReadLine()
} | receive-job -wait -auto
foreach-object -parallel {
$myPort = $using:port
$myPort.ReadLine()
}

Redirecting .bat output when using Invoke-Command

I am trying to redirect the output of a .bat script to a file. The script is run on another machine.
The commented line works. The t.txt file is produced in the expected location. I cannot convince PowerShell to produce the output file when the ScriptBlock is used.
The current result is that the $sb text is printed to the PowerShell console running this script. No file is produced on SERVER2. What do I need to get the output written to the file specified in the scriptblock?
$cn = 'SERVER2'
$Logfile = "D:\DBA\Scripts\monlogs\monlog_$(Get-Date -Format 'yyyy-MM-ddTHH-mm-ss').txt"
$sb = [scriptblock]::Create("{ & cmd.exe /C D:\DBA\Scripts\mon_test_001.bat >`"$Logfile`" }")
### Invoke-Command -ComputerName $cn -ScriptBlock { & D:\DBA\Scripts\mon_test_001.bat >D:\DBA\Scripts\monlogs\t.txt 2>&1 }
Invoke-Command -ComputerName $cn -ScriptBlock $sb
EDIT
After BenH's comment, I found the following to work as expected. Note that the parameter needed to have the $ escaped.
$sb = [scriptblock]::Create("param(`$Logfile) & cmd.exe /C D:\DBA\Scripts\mon_test_001.bat >`"$Logfile`"")
Rather than class create method, maybe casting would work? Then because you're running the scriptblock on a remote machine, use the "$using:" scope on the local variable. (PSv3+ onwards)
$cn = 'SERVER2'
$Logfile = "c:\temp\$(Get-Date -Format 'yyyy-MM-ddTHH-mm-ss').txt"
[scriptblock]$sb = { & cmd.exe /C c:\temp\test.bat > "$using:Logfile" }
Invoke-Command -ComputerName $cn -ScriptBlock $sb
Otherwise for earlier versions, you will need to use a param block and -ArgumentList:
[scriptblock]$sb = {param($logpath) & cmd.exe /C c:\temp\test.bat > "$logpath" }
Invoke-Command -ComputerName $cn -ScriptBlock $sb -ArgumentList $Logfile

Function not accessible in a ScriptBlock

I have a script that has some functions and then multiple jobs in the very same script that use those functions. When I start a new job they don't seem to be accessible in the [ScriptBlock] that I have for my jobs.
Here's a minimal example demonstrating this:
# A simple test function
function Test([string] $string)
{
Write-Output "I'm a $string"
}
# My test job
[ScriptBlock] $test =
{
Test "test function"
}
# Start the test job
Start-Job -ScriptBlock $test -Name "Test" | Out-Null
# Wait for jobs to complete and print their output
#(Get-Job).ForEach({
Wait-Job -Job $_ |Out-Null
Receive-Job -Job $_ | Write-Host
})
# Remove the completed jobs
Remove-Job -State Completed
The error that I get in PowerShell ISE is:
The term 'Test' 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.
+ CategoryInfo : ObjectNotFound: (Test:String) [], CommandNotFoundException
+ FullyQualifiedErrorId : CommandNotFoundException
+ PSComputerName : localhost
Start-Job run jobs in separate PowerShell processes. So that, jobs do not have access to session state of calling PowerShell session. You need to define functions, which get used by jobs, in every job. An easy way to do that without duplicating the code would be using of -InitializationScript parameter, where all common functions can be defined.
$IS = {
function CommonFunction1 {
'Do something'
}
function CommonFunction2 {
'Do something else'
}
}
$SB1 = {
CommonFunction1
CommonFunction2
}
$SB2 = {
CommonFunction2
CommonFunction1
}
$Job1 = Start-Job -InitializationScript $IS -ScriptBlock $SB1
$Job2 = Start-Job -InitializationScript $IS -ScriptBlock $SB2
Receive-Job $Job1,$Job2 -Wait -AutoRemoveJob
Just extending PetSerAl's answer, you can use Runspaces for this, if you want faster code and a little bit more organised. Check out this question:
39180266
So when you run something in different runspace, you need to import functions in both of them. So finished structure would look like:
Module: functions.ps1 - you store here functions to share with both scopes.
Main script: script.ps1 - it's basically your script, with runspaces, but without functions from functions.ps1.
And in beginning of your script.ps1, just simply call Import-module .\functions.ps1, to get access to your functions. Remember that runscape has different scope, and in their scriptblock, you have to call import-module once again. Full example:
#file functions.ps1
function add($inp) {
return $inp + 2
}
#file script.ps1
Import-module .\functions.ps1 #or you can use "dot call": . .\function.ps1
Import-module .\invoke-parallel.ps1 #it's extern module
$argument = 10 #it may be any object, even your custom class
$results = $argument | Invoke-Parallel -ScriptBlock {
import-module .\functions.ps1 #you may have to use here absolute path, because in a new runspace PSScriptRoot may be different/undefined
return (add $_) # $_ is simply passed object from "parent" scope, in fact, the relationship between scopes is not child-parent
}
echo $result # it's 12
echo (add 5) # it's 7

Calling function from remote script block

My powershell script is as below. I try to zip a folder at remote machine. I don't want to put Zip function inside ScriptBlock because it will be used in other parts of the script.
function Zip{
param([string]$sourceFolder, [string]$targetFile)
#zipping
}
$backupScript = {
param([string]$appPath,[string]$backupFile)
If (Test-Path $backupFile){ Remove-Item $backupFile }
#do other tasks
$function:Zip $appPath $backupFile
}
Invoke-Command -ComputerName $machineName -ScriptBlock $backupScript -Args $appPath,$backupFile
In $backupScript, it is giving error in $function:Zip line:
+ $function:Zip $appPath $backupFile
+ ~~~~~~~~
Unexpected token '$appPath' in expression or statement.
You have to refer to arguments in a scriptblock like:
$backupScript = {
param([string]$appPath,[string]$backupFile)
If (Test-Path $backupFile){ Remove-Item $backupFile }
#do other tasks
$function:Zip $args[0] $args[1]
}
Invoke-Command -ComputerName $machineName -ScriptBlock $backupScript -Args $appPath,$backupFile
Also, the function will not be known by the target machine, you'll have to define it within the script-block or pass it to the machine.
Here is an example:
How do I include a locally defined function when using PowerShell's Invoke-Command for remoting?
This example puts it in your perspective:
PowerShell ScriptBlock and multiple functions
I would find some way of getting your shared functions onto your server. We have a standard share on all our servers where we deploy common code. When we run code remotely, that code can then reference and use the shared code.