I'm trying to use Invoke-Command to run a command in PowerShell remotely on a number of machines, and capture their output from it, but I'm not getting any output from it. I suspect it's from how I'm using Start-Process, but I'm not sure.
$RunCommand = {
Start-Process "$env:ProgramFiles\Some Program\someprogram.exe" -ArgumentList "-SignatureUpdate"
}
$comp_list = #(Get-Content "c:\temp\comp_list.txt")
$cred = Get-Credential
$jobs = Invoke-Command -Credential $cred -Computer $comp_list -ScriptBlock $RunCommand -AsJob
Wait-Job $jobs
$r = Receive-Job $jobs
$r | % { $_ > c:\temp\$($_.PScomputerName).output }
Is there a better way to invoke a command using environment variables like that?
I've found that Start-Process is probably not the best way to capture the output from a binary unless the binary itself passes on .NET objects. What I did find that works better for this is the call operator:
$RunCommand = {
$exe = "$env:ProgramFiles\Some Program\someprogram.exe"
& $exe -SignatureUpdate
}
I was still not getting the output I was expecting. More specifically, I was only getting the last line of output from the command instead of the entire thing. Eventually it dawned on me that all the previous lines were being overwritten, and so I changed the last line to this:
$r | % { $_ >> c:\temp\$($_.PScomputerName).output }
Note: I changed > to >> for appending to the file.
Related
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
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
I have a script located on a remote system.
On the server "web12" under C:\tmp\hgttg.ps1:
Write-Host "Don't Panic"
exit 42
I invoke this script from my local box (both systems running v4.0) using Invoke-Command:
$svr = "web12"
$cmd = "C:\tmp\hgttg.ps1"
$result = Invoke-Command -ComputerName $svr -ScriptBlock {& $using:cmd}
This causes the following output when executed:
> $result = Invoke-Command -ComputerName $svr -ScriptBlock {& $using:cmd}
Don't Panic
> $result
>
($result is not set, output goes straight to the console, no good!) After much web searching and troubleshooting, I have come to some improvement:
> $result = Invoke-Command -ComputerName $svr -ScriptBlock {& $using:cmd; $LASTEXITCODE}
Don't Panic
> $result
42
>
Now I'm able to capture the return code, but the output still goes straight to the console. This is as far as I can get. On the long list of things I have already attempted, none of the following have worked:
Instead of '&' used 'Invoke-Expression' with -OutVariable out; New-Object -TypeName PSCustomObject -Property #{code=$LASTEXITCODE; output=$out}
This produces the output:
> $result
code : 42
output :
PSComputerName : web12
RunspaceId : aaa00a00-d1fa-4dd6-123b-aa00a00000000
>
Attempted both iterations above with '4>&1' and '*>&1' on the end of the inner and outer commands, no change.
Attempted each of:
"& $using:cmd | Tee-Object -Variable out; $out"
"& $using:cmd >$out; $out"
"$out = & $using:cmd; $out"
(Discarding the return code just to get output) Again, no change.
Also attempted: '& $using:cmd >> C:\tmp\output.log; $LASTEXITCODE'. The generated file was empty, and the text was still output to the local terminal.
I'm sure there's something obvious that I'm missing, but so far all I've hit are dead ends. Any suggestions?
On PSv4- (versions 4.x or below), you simply cannot capture Write-Host output - it invariably goes straight to the console.
In PSv5+, Write-Host (too) writes to the newly introduced information stream that the newly introduced Write-Information cmdlet is designed to write to; its number is 6.
Thus, if your target host runs PSv5+, you can use the following; note how *> captures all output streams by redirecting (&) them to the success stream (1), but you can use 6> to capture the information stream selectively):
$result = Invoke-Command -ComputerName $svr -ScriptBlock {& $using:cmd *>&1; $LASTEXITCODE}
$output = $result[0..($result.Count-2)]
$exitCode = $result[-1]
Basically I want to make the following concise using the pipeline:
{Remove-Job 27}, {Remove-Job 29} | % { Invoke-Command -Session $s -ScriptBlock $_; };
To something like (conceptually)
#(27, 29) | % {Remove-Job $_;} | % { Invoke-Command -Session $s -ScriptBlock $_; };
What would be a good way to do this?
Your second example isn't quite right, because you have to emit a ScriptBlock in order to call Invoke-Command on it. Therefore, rather than actually calling Remove-Job on the local machine, you'd want to pass it as a ScriptBlock to the remote computer. Here is the most concise way I can think of at the moment, to achieve what you're after:
27, 29 | % { Invoke-Command -Session $s -ScriptBlock { Remove-Job -Id $args[0]; } -ArgumentList $_; };
Even though you didn't explicitly come out and display the code, it's obvious that you are pre-creating the PowerShell Session (aka. PSSession) object, prior to calling Invoke-Command. You can also simplify things by not pre-creating the PSSession, and simply using Invoke-Command with the -ComputerName parameter. Here is an example:
$ComputerList = #('server01.contoso.com', 'server02.contoso.com', 'server03.contoso.com');
Invoke-Command -ComputerName $ComputerList -ScriptBlock { Remove-Job -Id $args[0]; } -ArgumentList 27,29;
Note: I also moved the Job IDs directly into -ArgumentList, rather than piping them in. I generally try to avoid using the PowerShell pipeline, unless it really makes sense (eg. taking advantage of Where-Object, Get-Member, or Select-Object). Everyone has a different approach.
Any reason this wouldn't work?
Invoke-Command -Session $s -ScriptBlock { 27,29 |% {Remove-Job -Id $_ } }
Another approach would be to use variable expansion, and the Create() method on the ScriptBlock type.
27, 29 | % { Invoke-Command -Session $s -ScriptBlock $([ScriptBlock]::Create("Remove-Job -Id $_")) };
I'm not sure that is any more concise than Trevor's approach.
The Id parameter of the Remove-Job cmdlet accepts an array of ID's so you could speicfy them like so:
Invoke-Command -Session $s -ScriptBlock { Remove-Job -Id 27,29 }
My question is very similar to this one, except I'm trying to capture the return code of a ScriptBlock using Invoke-Command (so I can't use the -FilePath option). Here's my code:
Invoke-Command -computername $server {\\fileserver\script.cmd $args} -ArgumentList $args
exit $LASTEXITCODE
The problem is that Invoke-Command doesn't capture the return code of script.cmd, so I have no way of knowing if it failed or not. I need to be able to know if script.cmd failed.
I tried using a New-PSSession as well (which lets me see script.cmd's return code on the remote server) but I can't find any way to pass it back to my calling Powershell script to actually DO anything about the failure.
$remotesession = new-pssession -computername localhost
invoke-command -ScriptBlock { cmd /c exit 2} -Session $remotesession
$remotelastexitcode = invoke-command -ScriptBlock { $lastexitcode} -Session $remotesession
$remotelastexitcode # will return 2 in this example
Create a new session using new-pssession
Invoke your scripblock in this session
Fetch the lastexitcode from this session
$script = {
# Call exe and combine all output streams so nothing is missed
$output = ping badhostname *>&1
# Save lastexitcode right after call to exe completes
$exitCode = $LASTEXITCODE
# Return the output and the exitcode using a hashtable
New-Object -TypeName PSCustomObject -Property #{Host=$env:computername; Output=$output; ExitCode=$exitCode}
}
# Capture the results from the remote computers
$results = Invoke-Command -ComputerName host1, host2 -ScriptBlock $script
$results | select Host, Output, ExitCode | Format-List
Host : HOST1
Output : Ping request could not find host badhostname. Please check the name and try again
ExitCode : 1
Host : HOST2
Output : Ping request could not find host badhostname. Please check the name and try again.
ExitCode : 1
I have been using another method lately to solve this problem. The various outputs that come from the script running on the remote computer are an array.
$result = Invoke-Command -ComputerName SERVER01 -ScriptBlock {
ping BADHOSTNAME
$lastexitcode
}
exit $result | Select-Object -Last 1
The $result variable will contain an array of the ping output message and the $lastexitcode. If the exit code from the remote script is output last then it can be fetched from the complete result without parsing.
To get the rest of the output before the exit code it's just:
$result | Select-Object -First $(result.Count-1)
#jon Z's answer is good, but this is simpler:
$remotelastexitcode = invoke-command -computername localhost -ScriptBlock {
cmd /c exit 2; $lastexitcode}
Of course if your command produces output you'll have to suppress it or parse it to get the exit code, in which case #jon Z's answer may be better.
It is better to use return instead of exit.
For example:
$result = Invoke-Command -ComputerName SERVER01 -ScriptBlock {
return "SERVER01"
}
$result