Interpret Variable in Powershell { } scriptblock - powershell

I have a shell script which should start a .exe in the background:
$strPath = get-location
$block = {& $strPath"\storage\bin\storage.exe" $args}
start-job -scriptblock $block -argumentlist "-f", $strPath"\storage\conf\storage.conf"
In a preceding Question I found out that I need absolute Paths. However the $strPath variable isn't interpreted, if you look at the command:
PS Q:\mles\etl-i_test> .\iprog.ps1 --start1
Start Storage
Id Name State HasMoreData Location Command
-- ---- ----- ----------- -------- -------
37 Job37 Running True localhost & $strPath"\storage\bi...
How can I fix this?
Edit: I understand I need to pass the path as an argument, and how? Something like:
$block = {& $args[0]"\storage\bin\storage.exe" $args[1] $args[2]}
start-job -scriptblock $block -argumentlist $strPath, "-f", $strPath"\storage\conf\storage.conf"
?

The contents of the script block will be executed in another instance of PowerShell.exe (as the job) which won't have access to your variables. This is why you need to send them in the Start-Job argumentlist. Send all the data the job will need to function as an argument. The full path to storage.exe for example, e.g.
$path = (Get-Location).Path
$block = {& $args[0] $args[1] $args[2]}
start-job -scriptblock $block -argumentlist `
"$path\storage\bin\storage.exe" `
"-f", `
"$path\storage\conf\storage.conf"

Related

Trying to catch the exitcode from PowerShell Invoke-command with a BAT file

I'm trying to catch the exitcode from a PowerShell script that uses a Invoke-Command to run a scriptblock on a remote machine.
First the BAT file:
The BAT file is run with a variable. The script looks like this:
powershell.exe -noninteractive -noprofile -command "& {E:\Scripts\Check-Services_XXX.ps1 %1 }"
EXIT /B %errorlevel%
The PowerShell script looks like this:
param(
[string] $ip #IP address van server
)
$username = "DOMAIN\DOMAIN_USER"
$secpasswdfile = "E:\Location\DOMAINUSER_encrypted_password.txt"
$secpasswd = Get-Content $secpasswdfile | ConvertTo-SecureString
$credentials = New-Object System.Management.Automation.PSCredential ($username, $secpasswd)
$soptions = New-PSSessionOption -SkipCACheck -SkipRevocationCheck -SkipCNCheck
Invoke-Command -ComputerName $ip -UseSSL -SessionOption $soptions -Credential $credentials -ScriptBlock `
{
# Start services
Start-Service -InputObject (Get-Service -Name IAS)
# Check services status
$checkservice = (get-service -Name IAS -ErrorAction SilentlyContinue)
if($checkservice.status -ne "Running"){$host.SetShouldExit(1)}
exit
}
The problem is that the ExitCode is not captured back, so when the BAT file ends, it ends with 0. That would be the case if everything is running. But i deliberately changed the service name in the check service section to something that does not exist for sure, but still it the BAT file ends with Exitcode 0
Done so far: Tried this solution:
catching return code of a command with "invoke-command" - Powershell 2
But didn't work: got the following error "is not equal to Open, you cannot run a command in the session. The session state is Closing"
Apparently, when it exited with a error, the session was closed, thus couldn't get the exitcode
Also tried this one: Capture Write-Host output and exit code from Invoke-Command on a Remote System
But also the same result; no correct exitcode (expected 1 instead of 0 in the BAT file)
SOLUTION!
Thanks to #js2010 and #mklement0 ; it works now like a charm!
This is the BAT file:
powershell.exe -noprofile -File "E:\Scripts\Check-Services_XXX.ps1" "%1" "%2"
EXIT /B %errorlevel%
And here is the PowerShell code that eventually worked out for me:
param(
[string] $ip, #IP address of checked server
[string] $service ) #Service name
$username = "DOMAIN\USER"
$secpasswdfile = "E:\Scripts\Credentials\DOMAIN-USER_encrypted_password.txt"
$secpasswd = Get-Content $secpasswdfile | ConvertTo-SecureString
$credentials = New-Object System.Management.Automation.PSCredential ($username, $secpasswd)
$soptions = New-PSSessionOption -SkipCACheck -SkipRevocationCheck -SkipCNCheck
$session = New-PSSession -ComputerName $ip -UseSSL -SessionOption $soptions -Credential $credentials
# Start services
Invoke-Command -Session $session -ScriptBlock { Start-Service -Name $using:service }
# Check services status
$checkservice = Invoke-Command -Session $session { Get-Service -name $using:service | where status -eq running }
if (! $checkservice) {
write-output ("Error 1, Service '" + $service + "' not running or not found.")
exit 1
}
I had some issues with passing variables to remote commands, this link helped me out (https://powershellexplained.com/2016-08-28-PowerShell-variables-to-remote-commands/)
You would have to run the exit command outside of invoke-command.
# check-service.ps1
$result = invoke-command localhost { get-service appxsvc |
where status -eq running }
if (! $result) {
exit 1
}
Change your invocation of powershell.exe to use the -File CLI parameter:
powershell.exe -NoProfile -File "E:\Scripts\Check-Services_XXX.ps1" "%1"
EXIT /B %errorlevel%
That way, the .ps1 script's exit code is properly relayed as powershell.exe's exit code.
Additionally, as js2010's answer notes, you'll need to use your $host.SetShouldExit(1) call out of the Invoke-Command script block, given that the latter executes remotely. For the reasons explained below, exit 1 is preferable.
Generally speaking:
There's no reason to use the -Command (-c) CLI parameter with "& { ... }" in order to invoke code - just use "..." directly. Older versions of the CLI documentation erroneously suggested that & { ... } is required, but this has since been corrected.
Not only is "& { ... }" unnecessary, it invariably resets the exit code to 0.
As for your use of $host.SetShouldExit(1) to request exiting with an exit code of 1 (leaving aside that in a remote call it isn't effective):
It generally isn't designed to be called from user code, as explained in this answer.
For general information about exit codes in PowerShell, see this answer.

Start-Process and powershell.exe with splatting

I've been trying for a couple of days now to multi-thread a WPF GUI which will run a PS3.0 script once the button has been clicked. I cannot use start-job as that I would have to track (multiple sessions at once), however, I would like to just run the script in a separate process of PS- as if I were to open multiple instances of the script from a shortcut. And be able to just have an open PS window which will track the progress within the script itself.
Expected results would be starting a script in powershell.exe session and passing 3 arguments - 2 strings and 1 boolean value. Which are provided by the user.
So in ISE:
C:\temp\test.ps1 -argumentlist $computername $username $citrixtest
Works fine.
I've spent a few hours scouring through the internet only to find a thread where a start-job was recommended or a way to use a background worker- this is not what I want from the script.
So I would guess the invocation from a button click would be something of the like (some of the things I have tried)
$ComputerName = "testtext1"
$UserName = "testtext2"
$CitrixTest = $True
$command = "c:\temp\test.ps1"
$arg = #{
Computername = "$computername";
Username = "$username";
CitrixTest = "$citrixtest"
}
#$WPFStartButton.Add_Click({
Start-Process powershell -ArgumentList "-noexit -command & {$command} -argumentlist $arg"
#})
Does not pass arguments to test.ps1- it is, however, getting to the "pause" - so the script successfully launches.
Where test.ps1 is
$ComputerName
$UserName
$CitrixTest
pause
Caller:
function Caller {
Param (
$ScriptPath = "c:\temp\test.ps1"
)
$Arguments = #()
$Arguments += "-computername $ComputerName"
$Arguments += "-UserName $UserName"
$Arguments += "-citrixtest $citrixtest"
$StartParams = #{
ArgumentList = "-File ""$ScriptPath""" + $Arguments
}
Start-Process powershell #StartParams
}
Caller
Does not start the script altogether- PS window just closes- possibly a path to .ps1 script not being found.
And a different approach which also nets in the script starts but not passing the arguments
$scriptFile = '"C:\temp\test.ps1"'
[string[]]$argumentList = "-file"
$argumentList += $scriptFile
$argumentlist += $computername
$argumentlist += $UserName
$argumentlist += $CitrixTest
$start_Process_info = New-Object System.Diagnostics.ProcessStartInfo
$start_Process_info.FileName = "$PSHOME\PowerShell.exe"
$start_Process_info.Arguments = $argumentList
$newProcess = New-Object System.Diagnostics.Process
$newProcess.StartInfo = $start_Process_info
$newProcess.Start() | Out-Null
Is there a way to make this work as I want it to? Or should I just dig deeper into runspaces and try with that?
#Bill_Stewart I just realized I did not put the param(args) in my script...
And that's why it would not pull those variables as I would like them to. I will have to check when I'm back in the office if it's just that what I was missing.
Checked on my laptop that's running PS 5.1 and this seems to be working as intended
$testarg = #(
'-File'
"C:\temp\test.ps1"
"$computername"
"$username"
"$citrixtest"
)
Start-Process powershell.exe -ArgumentList $testarg
Where test.ps1 is:
param(
$ComputerName,
$UserName,
$citrixtest
)
$ComputerName
$UserName
$CitrixTest
pause

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

Include a PowerShell file inside a ScriptBlock

I have written the following code
$sb = {
. .\Myfunctions.ps1
$x = MyFunction1
$y = MyFunction2
$x + $y
}
$cred = Get-Credential "domain\user"
Invoke-Command -Computer localhost -Credentials $cred -ScriptBlock $sb
This does not work because it says The term .\MyFunctions.ps1 is not recognized as commandlet
Why can't I include a file inside a script block?
The problem is that the $pwd (current directory) in the script block is different from the actual console path casued this because you are using invoke-command with -computer parameter is like you are do it in a remoting session. Try to put full path to your script to call it or just use ( if locally) & $sb

How to capture the Return Value of a ScriptBlock invoked with Powershell's Invoke-Command

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