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()
}
Related
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
}
I need to List all updates on Windows that are not installed and write them to a file. But if it takes too long there must be a timeout.
I tried to run a search with a Update Searcher Object as a Job and then continue either when it's completed or the timeout takes place.
Then I check if the job has been completed or not. If it did I pass the job to Receive-Job to get its result and write it to the file.
$session = New-Object -ComObject "Microsoft.Update.Session"
$searcher = $session.CreateUpdateSearcher()
$j = Start-Job -ScriptBlock {
$searcher.Search("IsInstalled=0").Updates | Select-Object Type, Title, IsHidden
} | Wait-Job -Timeout 120
if ($j.State -eq 'Completed') {
Receive-Job $j -Keep | Out-File #out_options
} else {
echo "[TIMEOUT] Script took too long to fetch all missing updates." |
Out-File #out_options
}
#out_options are defined, if you should wonder.
The only thing I receive is following error:
You cannot call a method on a null-valued expression.
+ CategoryInfo : InvalidOperation: (:) [], RuntimeException
+ FullyQualifiedErrorId : InvokeMethodOnNull
+ PSComputerName : localhost
By now I figured out that the error stems from calling Receive-Job. It seems the job is completing before there is a result.
How do I receive Result from my background job?
You've run into a scope issue. The variable $searcher inside your scriptblock is in a different scope (and is thus a different variable) than the variable $searcher in the script scope. Because it wasn't initialized properly the variable in the scriptblock is empty, thus causing the error you observed (you're trying to invoke Search() on an empty value).
The usual fix for this would be either using the using scope modifier
$j = Start-Job -ScriptBlock {
$using:searcher.Search("IsInstalled=0").Updates | ...
} | ...
or passing the variable as an argument
$j = Start-Job -ScriptBlock {
Param($s)
$s.Search("IsInstalled=0").Updates | ...
} -ArgumentList $searcher | ...
However, in your case both approaches don't work, which to my knowledge is because of how COM objects are serialized/deserialized when being passed between different contexts.
To avoid this pitfall create the Update session and searcher inside the scriptblock:
$j = Start-Job -ScriptBlock {
$session = New-Object -ComObject 'Microsoft.Update.Session'
$searcher = $session.CreateUpdateSearcher()
$searcher.Search('IsInstalled=0').Updates | Select-Object Type, Title, IsHidden
} | Wait-Job -Timeout 120
I try to send a module's method/function to a job.
How do I execute the method/function inside the job?
# Create a module with 1 method.
$m = New-Module -ScriptBlock{
function start(){
"started"
};
} -AsCustomObject
# Start a job and send in the function/method.
Start-Job -ScriptBlock{
$func = $args[0]
"<args0:$func>" # The function/method seems to contain what I want.
$func # <------------ How do I call $func/$m.start?
} -ArgumentList $m.start
Running the above and then job 498 | Receive-Job -Keep gives:
PS C:\temp\> job 500 | Receive-Job -Keep
<argsO:System.Object start();>
RunspaceId : 9271e389-cc97-4d2a-9396-5f0ce3f0ae5c
Script :
"started"
OverloadDefinitions : {System.Object start();}
MemberType : ScriptMethod
TypeNameOfValue : System.Object
Value : System.Object start();
Name : start
IsInstance : True
so to all my knowledge I do have the function/method.
start is a scriptmethod. You need to call start as method.
$M | Get-Member
$M.start()
# Create a module with 1 method.
$m = New-Module -ScriptBlock{
function start(){
mkdir 'C:\Vincent\job'
};
} -AsCustomObject
# Start a job and send in the function/method.
Start-Job -ScriptBlock{
$func = $args[0]
"<args0:$func>" # The function/method seems to contain what I want.
$func # <------------ How do I call $func/$m.start?
} -ArgumentList $m.start()
Importing Modules using -AsCustomObject
After posting the question I found:
Start-Job -ScriptBlock{
$func = $args[0]
"<args0:$func>" # The function/method seems to contain what I want.
$script = [scriptblock]::Create($func.Script);
$script.Invoke()
} -ArgumentList $m.start
But as my colleague said: Hmmm who is supposed to understand this?
Isn't there any less convoluted way to run a method?
I've read that objects are serialized when you pass them into a script block called with start-job. This seems to work fine for strings and things, but I'm trying to pass an xml.XmlElement object in. I'm sure that the object is an XMLElement before I invoke the script block, but in the job, I get this error:
Cannot process argument transformation on parameter 'node'. Cannot convert the "[xml.XmlElement]System.Xml.XmlElement"
value of type "System.String" to type "System.Xml.XmlElement".
+ CategoryInfo : InvalidData: (:) [], ParameterBindin...mationException
+ FullyQualifiedErrorId : ParameterArgumentTransformationError
+ PSComputerName : localhost
So, how do I get my XmlElement back. Any ideas?
For what it's worth, this is my code:
$job = start-job -Name $node.LocalName -InitializationScript $DEFS -ScriptBlock {
param (
[xml.XmlElement]$node,
[string]$folder,
[string]$server,
[string]$user,
[string]$pass
)
sleep -s $node.startTime
run-action $node $folder $server $user $pass
} -ArgumentList $node, $folder, $server, $user, $pass
Apparently you can't pass XML nodes into script blocks because you can't serialize them. According to this answer you need to wrap the node into a new XML document object and pass that into the script block. Thus something like this might work:
$wrapper = New-Object System.Xml.XmlDocument
$wrapper.AppendChild($wrapper.ImportNode($node, $true)) | Out-Null
$job = Start-Job -Name $node.LocalName -InitializationScript $DEFS -ScriptBlock {
param (
[xml]$xml,
[string]$folder,
[string]$server,
[string]$user,
[string]$pass
)
$node = $xml.SelectSingleNode('/*')
sleep -s $node.startTime
run-action $node $folder $server $user $pass
} -ArgumentList $wrapper, $folder, $server, $user, $pass
I have the following code.
function createZip
{
Param ([String]$source, [String]$zipfile)
Process { echo "zip: $source`n --> $zipfile" }
}
try {
Start-Job -ScriptBlock { createZip "abd" "acd" }
}
catch {
$_ | fl * -force
}
Get-Job | Wait-Job
Get-Job | receive-job
Get-Job | Remove-Job
However, the script returns the following error.
Id Name State HasMoreData Location Command
-- ---- ----- ----------- -------- -------
309 Job309 Running True localhost createZip "a...
309 Job309 Failed False localhost createZip "a...
Receive-Job : The term 'createZip' 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.
At line:17 char:22
+ Get-Job | receive-job <<<<
+ CategoryInfo : ObjectNotFound: (function:createZip:String) [Receive-Job], CommandNotFoundException
+ FullyQualifiedErrorId : CommandNotFoundException
It seems the function name cannot be recognized inside the script block of start-job. I tried function:createZip too.
Start-Job actually spins up another instance of PowerShell.exe which doesn't have your createZip function. You need to include it all in a script block:
$createZip = {
param ([String]$source, [String]$zipfile)
Process { echo "zip: $source`n --> $zipfile" }
}
Start-Job -ScriptBlock $createZip -ArgumentList "abd", "acd"
An example returning an error message from the background job:
$createZip = {
param ([String] $source, [String] $zipfile)
$output = & zip.exe $source $zipfile 2>&1
if ($LASTEXITCODE -ne 0) {
throw $output
}
}
$job = Start-Job -ScriptBlock $createZip -ArgumentList "abd", "acd"
$job | Wait-Job | Receive-Job
Also note that by using a throw the job object State will be "Failed" so you can get only the jobs which failed: Get-Job -State Failed.
If you are still new to using start-job and receive-job, and want to debug your function more easily, try this form:
$createZip = {
function createzipFunc {
param ([String]$source, [String]$zipfile)
Process { echo "zip: $source`n --> $zipfile" }
}
#other funcs and constants here if wanted...
}
# some secret sauce, this defines the function(s) for us as locals
invoke-expression $createzip
#now test it out without any job plumbing to confuse you
createzipFunc "abd" "acd"
# once debugged, unfortunately this makes calling the function from the job
# slightly harder, but here goes...
Start-Job -initializationScript $createZip -scriptblock {param($a,$b) `
createzipFunc $a $b } -ArgumentList "abc","def"
All not made simpler by the fact I did not define my function as a simple filter as you have, but which I did because I wanted to pass a number of functions into my Job in the end.
Sorry for digging this thread out, but it solved my problem too and so elegantly at that. And so I just had to add this little bit of sauce which I had written while debugging my powershell job.