How can I capture the Information output stream of a Job? - powershell

If I start a job, and that job outputs messages to the Information stream (or any stream for that matter), how can I capture that into a variable, or simply output it after I receive the job?
$script = {
[PSCustomObject]#{ "CoolProperty" = "CoolValue" }
Write-Information "Returning a cool object" -InformationAction "Continue"
}
Start-Job -Name "test" -ScriptBlock $script | Out-Null
Wait-Job -Name "test" | Out-Null
$result = Receive-Job -Name "test" 6>&1
Remove-Job -Name "test"
The above code will output
Returning a cool object
to the console, but I would like to capture that and output it to the log file I'm using for the overall script, e.g.:
$JustTheInfoStreamFromJob | Out-File "c:\file.log" -Append
I don't want to log the captured $result, because it also contains the Output stream (i.e. the object). I only want to log the Information stream. So I'm looking for a way to separate that out.
I see that there is an -InformationVariable parameter, but I don't understand how to use it, or if it's relevant for my question. I've tried a few methods of redirection, but I have very little idea what I'm doing when it comes to streams.
This question gives me some hints, but not enough to understand.
This question is very similar but appears to have been asked before the Information stream existed. I'd rather not use Transcripts unless it's necessary; I feel like there should be a better way.
Some answers reference the ChildJobs, Output, and Information properties of a Job object, but I'm having a hard time understanding how to use them, as they are always null in my simple tests.
Thanks for your time.
Solved
Thanks to #Santiago Squarzon and #Steven for two different working solutions:
Remove-Job "test"
$script = {
[PSCustomObject]#{ "CoolProperty" = "CoolValue" }
Write-Information "Returning a cool object"
}
Start-Job -Name "test" -ScriptBlock $script | Out-Null
$job = Get-Job -Name "test" -IncludeChildJob # Steven's solution
$job | Wait-Job | Out-Null
$info = $job.Information # Steven's solution
$result = Receive-Job -Name "test" -InformationVariable info2 # Santiago Squarzon's solution
Remove-Job -Name "test"
$result
"---"
$info
"---"
$info2
This allows me to capture the job's output and the job's Info stream separately (two different ways):
CoolProperty RunspaceId
------------ ----------
CoolValue f49c78bd-eda3-4c47-a6bc-d89a146618e9
---
Returning a cool object
---
Returning a cool object

The different Streams are stored separately in the job object:
State : Completed
HasMoreData : True
StatusMessage :
Location : localhost
Command : Write-Information "something"
JobStateInfo : Completed
Finished : System.Threading.ManualResetEvent
InstanceId : ff5d1155-aca1-40fa-8e4e-dce6b87c709c
Id : 2
Name : test
ChildJobs : {Job3}
PSBeginTime : 4/2/2021 9:41:26 PM
PSEndTime : 4/2/2021 9:41:26 PM
PSJobTypeName : BackgroundJob
Output : {}
Error : {}
Progress : {}
Verbose : {}
Debug : {}
Warning : {}
Information : {}
Notice the child Job, but you can get at the information stream with something like:
(Get-Job -Name test -IncludeChildJob).Information
In the above example it will return "something", reference the command from the job.
Reference here there's some other good information as well.

Related

PowerShell pass a variable to background process [duplicate]

within powershell I'd like to learn the best way to call a variable to a start job so I don't have to edit the script for each server as it will be specific based on the client I've placed my script on.
$Servername = 'Server1'
$pingblock = {
pathping $servername | Out-File C:\client\PS\ServerPing.TXT
}
start-job $pingblock
when I run my code above I just get a file with the help as if I forgot the specify the $servername.
Use the -ArgumentList parameter on Start-Job e.g.:
Start-Job -Scriptblock {param($p) "`$p is $p"} -Arg 'Server1'
In your case:
$pingblock = {param($servername) pathping $servername | Out-File C:\...\ServerPing.txt}
Start-Job $pingblock -Arg Server1
To complement Keith Hill's helpful answer with a PSv3+ alternative:
The $using: scope modifier can be used to reference the values of variables in the caller's scope inside the script block passed to Start-Job, as an alternative to passing arguments (by default, a script block executed as a background job does not see any of the caller's variables or other definitions):
$Servername = 'Server1'
Start-Job { "Target: " + $using:ServerName } | Receive-Job -Wait -AutoRemoveJob
The above yields:
Target: Server1
Note:
The same technique can be used with:
Invoke-Command for remote execution - see this question.
Start-ThreadJob, available by default in PowerShell (Core) v6+, installable on demand in Windows PowerShell.
ForEach-Object -Parallel, available in PowerShell (Core) v7+ only.
Note that, as with -ArgumentList (-Args), it is only variable values that are being passed, not the variables themselves; that is, you cannot modify the caller's variables.[1]
[1] However, the thread-based concurrency features - Start-ThreadJob and ForEach-Object Parallel - permit indirect modification, namely if the variable value at hand happens to be an instance of a (mutable) .NET reference type, such as a hashtable, in which case the object that that the variable "points to" can be modified (if it is mutable). Note that taking advantage of that requires additional, nontrivial effort to make the concurrent modifications thread-safe, such as by use of concurrent (synchronized) collections - see this answer - and/or explicit locking of individual objects - see this answer.
Some other ways, $args and $input. This goes for invoke-command too, which I think uses the same mechanism. The $input method works in an unexpected way with arrays.
start-job { $args[0] } -args hi | receive-job -wait -auto
hi
echo hi | start-job { $input } | receive-job -wait -auto
hi
echo hi there | start-job { $input.gettype() } | receive-job -wait -auto
IsPublic IsSerial Name BaseType
-------- -------- ---- --------
False False <GetReadEnumerator>d__20 System.Object
For arrays, it's probably better to use a foreach-object (%) loop instead, so it runs on each array item in parallel. See also start-threadjob or foreach-object -parallel in powershell 7. There's actually no -throttlelimit option to start-job, so use with care.
echo yahoo.com facebook.com |
% { $_ | start-job { test-netconnection $input } } |
receive-job -wait -auto | select * -exclude runspaceid,pssourcejob* | ft
ComputerName RemoteAddress ResolvedAddresses PingSucce
eded
------------ ------------- ----------------- ---------
yahoo.com 74.6.143.25 {74.6.143.25,...} True
facebook.com 31.13.71.36 {31.13.71.36} True

How to Invoke below script block using Invoke-parallel

This Invoke-Parallel is taken from here
Below is my script block
$scriptblock ={
get-service -ComputerName $parameter.computername | where {$_.name -eq $parameter.serviceName }
}
below is a custom object
$pscustomobject = #()
$pscustomobject += [pscustomobject]#{
Computername ='server1'
Servicename ="service1"
}
$pscustomobject += [pscustomobject]#{
Computername ='server2'
Servicename ="service2"
}
I tried using Invoke-Parallel using below method ,but this doesnt work
Invoke-Parallel -ScriptBlock $scriptblock -Parameter $pscustomobject
"server1","server2" | Invoke-Parallel -ScriptBlock $scriptblock -Parameter $pscustomobject
Few services exist in few servers and on few others,they dont,so created a custom object which tightly maps services to servers.
Any ideas would be greatly helpfull
The -Parameter switch makes the properties available to each item in the script block. I think if you make a small modification like this, then it should work:
$scriptblock ={
$thisServer = $_
$thisServerParams = $parameter.Where({$_.Computername -eq $thisServer})
get-service -ComputerName $thisServerParams.computername | where {$_.name -eq $thisServerParams.serviceName }
}
We can use this code above to find the right properties we should use for each server. With the code before, we were effectively using Invoke-Parallel to run the same command on all servers each time. And the logic wouldn't work for matching services in the Where block because it would compute to
$pscustomobject.ServiceName
service1
service2
PS C:\Users\Stephen> $pscustomobject.ServiceName.GetType()
IsPublic IsSerial Name BaseType
-------- -------- ---- --------
True True Object[] System.Array
An individual service coming back from Get-Service would never equal to System.Array
Thanks to Foxdeploy,found one more way to achieve what i need,below method is more flexible for me,since..when i pass two services,one service may take a lot of time to start or stop and may hold data from another service as well
$pscustomobject | Invoke-Parallel -ScriptBlock {
$computername = $_.computername
$servicename = $_.servicename
get-service -ComputerName $($computername) | where {$_.name -eq $($serviceName) }
}
I might be wrong but I believe this would be faster, using builtin Powershell cmdlet and also looks cleaner imo:
$servers=#(
'server1'
'server2'
)
$scriptblock={
switch($env:COMPUTERNAME)
{
'server1'{Get-Service service1}
'server2'{Get-Service service2}
}
}
$result=Invoke-Command -ComputerName $servers -ScriptBlock $scriptblock
Edit:
For the sake of demonstration, here I'm waiting 10 seconds and then resolving a easy mathematical equation on 7 servers depending on their name 3 pairs of them start with the same name. If this was a linear invocation it should've taken 10+10+10+10 seconds yet it only took 15 seconds for all of them. This demonstrates that you can execute different commands on different remote computers at the same time.

Using Get-Job to test-connection, but quite different [duplicate]

This question already has answers here:
If using Test-Connection on multiple computers with -Quiet how do I know which result is for which computer?
(2 answers)
Closed 2 years ago.
It's my first post here, I'm tring to write scripts on PS on my own, now my target is to write script that checks if computer is online at network, for example: test-Connection 192.168.0.1, 2, 3 etc. Doing this one by one on loop for takes some time if some computers are offline, I've found some tutorials on this site to use -AsJob param, but I'm not really Sure how could it work. I mean I'd like to output every checked PC to excel, so i need if operator. eg:
if (Job1 completed successfull (computer pings)){
do smth}...
I need to get output from Job boolean (true/false), but one by one. I'm taking my first steps in PS, I've made program that checks it one by one in for loop, but as i said it take some time till my excel file fill...
I can see, that AsJob makes working more effective and I think it's important to understand it
Thanks and sorry for bad text formatting, by the time I'll go on with this!
In your example, in the Start-Job scriptblock you are trying to access $_ which is not available in the codeblock scope. If you replace $_ with $args[0] it should work since you are passing in the $ip value as an argument
Your Example
$ipki = Get-Content 'C:\Users\pchor\Desktop\ipki.txt'
foreach ($ip in $ipki) {
Start-Job -Name "$ip" -ScriptBlock {
Test-Connection $_ -Count 1 # <---- replace $_ with $args[0]
} -ArgumentList $_ # <----- change $_ to $ip
}
You'll probably also want to wait for all the jobs to finish. I recommend something like this
$computers = #(
'www.google.com'
'www.yahoo.com'
)
$jobs = $computers |
ForEach-Object {
Start-Job -ScriptBlock {
[pscustomobject]#{
Computer = $using:_
Alive = Test-Connection $using:_ -Count 1 -Quiet
}
}
}
# Loop until all jobs have stopped running
While ($jobs |
Where-Object { $_.state -eq 'Running' }) {
"# of jobs still running $( ($jobs | Where-Object {$_.state -eq 'Running'}).Count )";
Start-Sleep -Seconds 2
}
$results = $jobs | Receive-Job | Select-Object Computer, Alive
$results | Format-Table
Output
Computer Alive
-------- -----
www.google.com True
www.yahoo.com True
To modify the properties to what you want there are different ways of doing this. Easiest in this case is probably to use a calculated property
$newResults = $results |
Select-Object Computer,
#{Label = 'State'; Expression = { if ($_.Alive) { 'Online' } else { 'Offline' } } }
Objects will now look like this (I added another fake address to illustrate offline state)
Computer State
-------- -----
www.google.com Online
www.yahoo.com Online
xxx.NotAValidAddress.xxx Offline
You can then export the objects to csv using Export-csv
$newResults | Export-Csv -Path c:\temp\output.csv

Apply conditional to output of a commandlet

I want to output the result of the commandlet (Invoke-Command) on success and add a custom message if the result is null. The code as shown below produces the desired results except in the event of a null response, it simply outputs nothing on that line.
I can not pipe directly to an if statement, nor can I output on 2 opposing conditions (True & False). Is it possible to get a custom response on $null while not suppressing the normal output on success?
Invoke-Command -ComputerName PC1, PC2, PC3 -Scriptblock {get-eventlog system | where-object {$_.eventid -eq 129} | select MachineName, EventID, TimeGenerated, Message -last 1}
If you run the example code block assuming that PC1 and PC3 have the event ID but PC2 does not, the output will simply skip PC2.
I want to output something like "Event Not found" in that case.
Placing the entire thing in a loop and then running the results through another conditional loops destroys performance so that is not an ideal solution.
I would create a new object for returning from Invoke-Command. So you are sure you will receive from every host something even the event log is not present. And might you can change get-eventlog to Get-WinEvent. Get-WinEvent was for my tasks the most time faster than get-eventlog.
[System.Management.Automation.ScriptBlock]$Scriptblock = {
[System.Collections.Hashtable]$Hashtable = #{
WinEvent = Get-WinEvent -FilterHashtable #{ LogName = 'System'; Id = 129 } -MaxEvents 1 -ErrorAction SilentlyContinue #-ErrorAction SilentlyContinue --> otherwise there is an error if no event is available
}
return (New-Object -TypeName PSCustomObject -Property $Hashtable)
}
Invoke-Command -ComputerName 'PC1', 'PC2', 'PC3' -Scriptblock $Scriptblock

PowerShell's Write-Debug won't output arrays, but Write-Output does. Is this on purpose?

Shown below, an array works fine as input for Write-Output but not for Write-Debug (I expected them to be more similar than that).
PS C:\> [string[]]$test = #("test1", "test2", "test3")
PS C:\> Write-Output $test
test1
test2
test3
PS C:\> $DebugPreference = "Inquire"
PS C:\> Write-Debug $test
Write-Debug : Cannot convert 'System.String[]' to the type 'System.String' required by parameter 'Message'. Specified method is not supported.
At line:1 char:12
+ Write-Debug <<<< $test
+ CategoryInfo : InvalidArgument: (:) [Write-Debug], ParameterBindingException
+ FullyQualifiedErrorId : CannotConvertArgument,Microsoft.PowerShell.Commands.WriteDebugCommand
PS C:\>
I'm thinking this is just an unfortunate design, but hoping for a sensible explanation. Am I using Write-Debug correctly? If so, anyone have a favorite simple workaround?
I kept having the same problem, and none of the solutions I found above or anywhere else would work in the general case.
For example, the first answer above works only because the array is an array of strings. If it's an array of anything else, that solution breaks, and Write-Debug will output the object type, and not its value as one would expect.
Finally I found a general solution: The key point is to first convert the input object to a string using the Out-String command. Once everything is a string, the above solution works. Using "Out-String -stream" improves the output alignment.
Example:
PS C:\> gwmi win32_bios
SMBIOSBIOSVersion : 786F3 v01.34
Manufacturer : Hewlett-Packard
Name : Default System BIOS
SerialNumber : CZC8196Q8S
Version : HPQOEM - 20120709
PS C:\> gwmi win32_bios | ft -auto
SMBIOSBIOSVersion Manufacturer Name SerialNumber Version
----------------- ------------ ---- ------------ -------
786F3 v01.34 Hewlett-Packard Default System BIOS CZC8196Q8S HPQOEM - ...
PS C:\> $DebugPreference = "Continue"
PS C:\> gwmi win32_bios | ft -auto | Write-Debug
DEBUG: Microsoft.PowerShell.Commands.Internal.Format.FormatStartData
DEBUG: Microsoft.PowerShell.Commands.Internal.Format.GroupStartData
DEBUG: Microsoft.PowerShell.Commands.Internal.Format.FormatEntryData
DEBUG: Microsoft.PowerShell.Commands.Internal.Format.GroupEndData
PS C:\> gwmi win32_bios | ft -auto | Out-String | Write-Debug
DEBUG: SMBIOSBIOSVersion Manufacturer Name SerialNumber Version
----------------- ------------ ---- ------------ -------
786F3 v01.34 Hewlett-Packard Default System BIOS CZC8196Q8S HPQOEM - ...
PS C:\> gwmi win32_bios | ft | Out-String -stream | Write-Debug
DEBUG:
DEBUG: SMBIOSBIOSVersi Manufacturer Name SerialNumber Version
DEBUG: on
DEBUG: --------------- ------------ ---- ------------ -------
DEBUG: 786F3 v01.34 Hewlett-Packard Default Syst... CZC8196Q8S HPQOEM - 201...
DEBUG:
DEBUG:PS C:\>
If you want write-debug to handle each one separately:
[string[]]$test = #("test1", "test2", "test3")
Write-Output $test
test1
test2
test3
$DebugPreference = "Inquire"
$test | Write-Debug
DEBUG: test1
DEBUG: test2
DEBUG: test3
Write-Debug is designed for outputting simple messages when debug preferences are set in a particular way. It takes only a string, not just anything like Write-Host does (and magically formats). You will have to format your output yourself into a single string.
You could combine Write-Host and Write-Debug if you have extra info to output before prompting the user:
if ($DebugPreference -ne 'SilentlyContinue') {
Write-Host 'such-and-such array:' $array
}
Write-Debug 'such-and-such array dumped'
Write-Host is used because it will always write to the console host, rather than to the script's output, as Write-Output does. If you where redirecting standard output of the script to a file, Write-Output would end up in the file, while Write-Host would still be seen in the console.
You could also try doing something like this if your array is of simply enough types that an automatic call to ToString() on them (if they're not strings already) gets you what you want:
$array = 'Alice','Bob','Charlie'
Write-Debug ([String]::Join("`n", $array))
Write-Debug:
Write-Debug [-Message] <string> [<CommonParameters>]
It expects a string. It is unable to convert a string array to a string as the error says. The reason why it expects a string is because it writes debug messages to the console from a script or command.
Note that Write-Output and Write-Host take an object:
Write-Output [-InputObject] <PSObject[]> [<CommonParameters>]
and
Write-Host [[-Object] <Object>] ...