Exception handling in powershell when it's invoked from batch script - powershell

Unfortunately, my host application can't directly execute PowerShell scripts, so I have written a batch script which calls PowerShell script with content
#echo off
echo calling upgrade product with argument %2
if [%1] == [] (
powershell -ExecutionPolicy UnRestricted -command "%~dp0Product1.ps1 "ProductConfig.xml" -verbose; exit $LASTEXITCODE"
) else (
cd %1
powershell -ExecutionPolicy UnRestricted -command "%1\UpgradeProduct.ps1 %2 -verbose; exit $LASTEXITCODE"
)
And in my powershell script i have code like
$ErrorActionPreference="Stop"
try{
Copy-Item -Path $source -Destination $dest
}catch{
Write-Warning "Some Error"
}
This executes fine when i execute the script from the PowerShell window(if $source is not found it will throw a terminating error and prints Some Error). But when executed from batch script if $source is not found Copy-Item throws a non terminating error and continues(Dosen't print Some Error).
How can i make the Copy-Item to throw a terminating error if $Source is not found?

You have nothing that stop in your catch block.
I presume Your Write-Warning is triggered, but code after your block is runned.
You must return something in your catch block, like:
$ErrorActionPreference="Stop"
try{
Copy-Item -Path $source -Destination $dest
}catch{
Write-Warning "Some Error"
#$LASTEXITCODE is a special variable in powershell
$LASTEXITCODE = 1
exit $LASTEXITCODE
}
Note $LASTEXITCODE variable, it is a special variable, the equivalent to %errorlevel%, that is used by commands like "cmd calls" by PowerShell.
You can test it by using this command in PowerShell:
cmd.exe /c exit 5
$LASTEXITCODE
I suggest you to first do a check if path exists:
try{
if(Test-Path $source){
Copy-Item -Path $source -Destination $dest -ErrorAction Stop
}else{
Write-Warning "$source not found."
$LASTEXITCODE = 1
exit $LASTEXITCODE
}
}catch{
Write-Error "Exception during copy: $($_)"
$LASTEXITCODE = 1
exit $LASTEXITCODE
}
Point of interest:
ErrorAction is used at Function level. Set it at script level will include this settings for many commands.
Exception will be thrown only if copy fail for something else: bad ACL, overwrite etc.

Related

How To Capture Output from Non-elevated Process Run from Elevated Powershell

I'm trying to update an elevated PowerShell script that's using StartProcess on a BAT file that runs RunAs on PowerShell.exe to run another PowerShell script without elevation in order to clone a git repository so that the directory is created in a way that a normal non-elevated user will be able to use.
Elevated PS1: Start-Process
=> Elevated .BAT: RunAs /trustlevel:0x20000
=> Non-elevated PS1
This is failing in some environments and I can't figure out why so I'm trying to figure out how to capture stdout and stderr from all levels of this process, but I'm not seeing the error or any output. I can capture it down to the BAT file level, but I can't seem to see anything that's happening within the inner-most Powershell script.
This seems like an awful lot of work just to programmatically clone a Git repository from an elevated process. Is there a way to make this work or is there an easier way?
EDIT: Just learned that this solution was broken as of Windows 11 Update 22H2: https://superuser.com/questions/1749696/parameter-is-incorrect-when-using-runas-with-trustlevel-after-windows-11-22h2
but the workaround is to use the /machine switch when running RunAs.
I suggest simplifying your approach as follows:
Use synchronous invocation of runas.exe, via Start-Process -Wait, which obviates the need for an intermediate batch file, and the need for a named pipe (System.IO.Pipes.NamedPipeClientStream)
Let the runas.exe-launched PowerShell child process that runs test2.ps1 capture that script's output in a temporary file, which you can read after the Start-Process -Wait call returns.
test2.ps1 can then just produce output normally - no need for System.IO.Pipes.NamedPipeClientStream
Elevated PowerShell Script (test.ps1):
function IsAdmin{
$currentPrincipal = New-Object Security.Principal.WindowsPrincipal([Security.Principal.WindowsIdentity]::GetCurrent())
$Is64 = [Environment]::Is64BitOperatingSystem
if ($currentPrincipal.IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator)) {
Write-Output "Running with elevated privileges. (64-bit=$Is64)"
} else {
Write-Output "Running without elevated privileges. (64-bit=$Is64)"
}
}
IsAdmin
# Create a temporary file in which to capture the output from the
# PowerShell child process launched by runas.exe.
$outFile = New-TemporaryFile
# Use Start-Process -Wait to directly invoke runas.exe,
# which doesn't just wait for runas.exe ITSELF to exit, but also
# waits for its CHILD processes.
# This ensures that execution is blocked until the other PowerShell script exits too.
Start-Process -Wait runas.exe #"
/machine:amd64 /trustlevel:0x20000 "powershell -c & \"$PSScriptRoot\test2.ps1\" -drive C:\ *> \"$outFile\""
"#
# Now $outFile contains all output produced by the other PowerShell script.
Write-Verbose -Verbose "Output from the runas.exe-launched PowerShell script:"
Get-Content -LiteralPath $outFile
$outFile | Remove-Item # Clean up.
Non-Elevated PowerShell Script (test2.ps1):
param([string]$drive)
function IsAdmin{
$currentPrincipal = New-Object Security.Principal.WindowsPrincipal([Security.Principal.WindowsIdentity]::GetCurrent())
$Is64 = [Environment]::Is64BitOperatingSystem
if ($currentPrincipal.IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator)) {
Write-Output "Running with elevated privileges. (64-bit=$Is64)"
} else {
Write-Output "Running without elevated privileges. (64-bit=$Is64)"
}
}
function Setup-Test{
Write-Output "Testing Powershell with Parameter Drive=$drive"
git config --global user.name
cd bob
Write-Error "Error Line 1
Error Line 2"
Write-Error "Error Line 3"
$d = 3/0
Write-Output "Done Testing Powershell"
}
IsAdmin
Setup-Test
This can be solved with a named pipe.
Elevated PowerShell Script (test.ps1)
function IsAdmin{
$currentPrincipal = New-Object Security.Principal.WindowsPrincipal([Security.Principal.WindowsIdentity]::GetCurrent())
$Is64 = [Environment]::Is64BitOperatingSystem
if ($currentPrincipal.IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator)) {
Write-Output "Running with elevated privileges. (64-bit=$Is64)"
} else {
Write-Output "Running without elevated privileges. (64-bit=$Is64)"
}
}
IsAdmin
Write-Output "Running $PSScriptRoot\test.bat"
Start-Process -FilePath "$PSScriptRoot\test.bat" -ArgumentList "C:\" -NoNewWindow
$np = new-object System.IO.Pipes.NamedPipeClientStream('.','SAMPipe', [System.IO.Pipes.PipeDirection]::In,[System.IO.Pipes.PipeOptions]::None,[System.Security.Principal.TokenImpersonationLevel]::Impersonation)
$np.Connect()
$sr = new-object System.IO.StreamReader($np)
while ($l=$sr.ReadLine()) {
Write-Output $l
}
$sr.Close()
$np.Close()
BAT file in the middle to de-elevate (test.bat)
runas /machine:amd64 /trustlevel:0x20000 "powershell -command %~dp0test2.ps1 -drive %1 >dummy.txt"
Non-Elevated PowerShell Script (test2.ps1)
param([string]$drive)
function IsAdmin{
$currentPrincipal = New-Object Security.Principal.WindowsPrincipal([Security.Principal.WindowsIdentity]::GetCurrent())
$Is64 = [Environment]::Is64BitOperatingSystem
if ($currentPrincipal.IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator)) {
Write-Output "Running with elevated privileges. (64-bit=$Is64)"
} else {
Write-Output "Running without elevated privileges. (64-bit=$Is64)"
}
}
function Setup-Test{
Write-Output "Testing Powershell with Parameter Drive=$drive"
git config --global user.name
cd bob
Write-Error "Error Line 1
Error Line 2"
Write-Error "Error Line 3"
$d = 3/0
Write-Output "Done Testing Powershell"
}
$np = New-Object System.IO.Pipes.NamedPipeServerStream('SAMPipe',[System.IO.Pipes.PipeDirection]::Out)
$np.WaitForConnection()
$sw = New-Object System.IO.StreamWriter($np)
$sw.WriteLine('Begin Non-Elevated Process Pipe')
Invoke-Command -ScriptBlock {
try {
IsAdmin
Setup-Test
} catch {
Write-Error $_
}
} -ErrorVariable errVar -OutVariable out
foreach ($line in $out){
$sw.WriteLine($line)
}
foreach ($line in $errVar) {
$sw.WriteLine($line)
}
$sw.WriteLine('End Non-Elevated Process Pipe')
$sw.Close()
$np.Close()
Output
Running with elevated privileges. (64-bit=True)
Running C:\Users\bmarty\source\PowerShellTest\test.bat
C:\Users\bmarty\source\PowerShellTest>runas /machine:amd64 /trustlevel:0x20000 "powershell -command C:\Users\bmarty\source\PowerShellTest\test2.ps1 -drive C:\ >dummy.txt"
Begin Non-Elevated Process Pipe
Running without elevated privileges. (64-bit=True)
Testing Powershell with Parameter Drive=C:\
Ben Marty
Cannot find path 'C:\Users\bmarty\source\PowerShellTest\bob' because it does not exist.
Error Line 1
Error Line 2
Error Line 3
Attempted to divide by zero.
System.Management.Automation.RuntimeException: Attempted to divide by zero. ---> System.DivideByZeroException: Attempted to divide by zero.
--- End of inner exception stack trace ---
at System.Management.Automation.ExceptionHandlingOps.CheckActionPreference(FunctionContext funcContext, Exception exception)
at System.Management.Automation.Interpreter.ActionCallInstruction`2.Run(InterpretedFrame frame)
at System.Management.Automation.Interpreter.EnterTryCatchFinallyInstruction.Run(InterpretedFrame frame)
at System.Management.Automation.Interpreter.EnterTryCatchFinallyInstruction.Run(InterpretedFrame frame)
Attempted to divide by zero.
Attempted to divide by zero.
End Non-Elevated Process Pipe
Done running
I don't understand why the output of git config only appears in the output if I include >dummy.txt in the BAT file.
Try working with ACLs instead. You can set that up on the parent directory so you don't even need to run the script in an elevated context.
Just set up a "gitclone" account that can write into the repository parent directory and then add the rest of the users as read+execute.
The rest will come automagically through inheritance.
Then run script as that "gitclone" user.

Start-Process powershell cannot find specified

Edit5: Adam's code works unless there are spaces in the path. That solution is at Powershell Opening File Path with Spaces
Edit4: Simplified further with a test for the path. Same Error.
If ($args[0] -eq "admin")
{
$TheScriptPath = "C:\Users\root\Desktop\script.ps1"
Test-Path ($TheScriptPath)
Start-Process "powershell -noexit" $TheScriptPath
}
Else { Write-Host "OK" }
Output when I call "powershell .\script.ps1 admin" is:
True
Start-Process : This command cannot be run due to the error: The system cannot find the file specified.
At C:\Users\root\Desktop\script.ps1:11 char:2
Edit3: Nevermind. Previous solution stopped working. Script is:
if ($args[0] -eq "admin)
{
$TheScriptPath = $myInvocation.MyCommand.Definition
Start-Process powershell -Verb runAs -Workingdirectory $PSScriptroot $TheScriptPath
exit
}
Write-Host "Ok"
Error when I call "powershell .\script.ps1 admin" is:
Start-Process : This command cannot be run due to the error: The system cannot find the file specified.
At C:\Users\root\Desktop\script.ps1:11 char:2
It's not even working when I hard-code the script path now, even with "-Verb runAs" removed.
Edit2: This is solved, I just can't accept my own answer for two days. Hopefully I remember to do that in case someone else comes along with this question.
Edit1: My script now reads:
If ($args[0] -eq "go")
{
$ThePath = $myInvocation.MyCommand.Definition
Start-Process powershell -Verb runAs $ThePath
Exit
}
Write-Host "OK"
It fails with the error below. However, if I hard-code the script path and write the script as:
If ($args[0] -eq "go")
{
Start-Process powershell -Verb runAs C:\Users\root\Desktop\script.ps1
Exit
}
Write-Host "OK"
It succeeds. I've also tried ""$myInvocation.MyCommand.Definition"" to no avail.
Original:
I have a powershell script that, at least in Windows 7, elevated the user and then ran the rest of the script. In Windows 10, however, it's giving me:
Exception calling "start" with "1" argument(s): "The system cannot find hte file specified"
At C:\Users\root\desktop\script.ps1:15 char:2
If ($True)
{
# We are not running "as Administrator" - so relaunch as administrator
# Create a new process object that starts PowerShell
$newProcess = new-object System.Diagnostics.ProcessStartInfo "PowerShell";
# Specify the current script path and name as a parameter
$newProcess.Arguments = $myInvocation.MyCommand.Definition;
# Indicate that the process should be elevated
$newProcess.Verb = "runas";
# Start the new process
[System.Diagnostics.Process]::Start($newProcess);
# Exit from the current, unelevated, process
exit
}
Write-Host "Ok"
The script exists at this path, as it actually attempting to invoke itself. I'm at a loss here.
I'm running Powershell v5.1.15063.1155 on Windows 10 (v10.0.15063 Build 15063). If I run the following:
$context = [Security.Principal.WindowsPrincipal] [Security.Principal.WindowsIdentity]::GetCurrent()
if (-not $context.IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator)) {
Start-Process powershell -Verb runAs -ArgumentList $myInvocation.MyCommand.Definition
exit
}
Get-Date
Sleep -Seconds 4
You can try this as a workaround as it works for me.
To your question, I'd think something is wrong with the the ProcessStartInfo object you created ($newProcess). I've seen that error when the executable name supplied as a parameter can't be found. For example, if I run the following:
$newProcess = new-object System.Diagnostics.ProcessStartInfo "cocain";
$newProcess.Arguments = $myInvocation.MyCommand.Definition;
$newProcess.Verb = "runas";
[System.Diagnostics.Process]::Start($newProcess);
I get the error you described:
You're sure Powershell is in your path (where.exe powershell)? I know its a reach.

file check script doesn't raise critical flag when file doesn't exist

I write a little script in PowerShell for Nagios that check if file exists.
If it exists the status should be "ok", and if not it should be "critical".
The problem is when the file does not exist the status is not "critical", it shows in Nagios as "unknown".
$path = "c:\test\test.txt"
$critical = 2
$ok = 0
if (-not (Test-Path $path)) {
Write-Host "file not exists"
exit $critical
} else {
Write-Host "file exists"
exit $ok
}
There's nothing wrong with your code, although I'd probably streamline it like this:
$path = "c:\test\test.txt"
$fileMissing = -not (Test-Path -LiteralPath $path)
$msg = if ($fileMissing) {'file does not exist'} else {'file exists'}
Write-Host $msg
exit ([int]$fileMissing * 2)
Your problem is most likely with the way you're executing the script. If you run the script using the -Command parameter, like this:
powershell.exe -Command "&{& 'C:\path\to\your.ps1'}"
or like this:
cmd /c echo C:\path\to\your.ps1 | powershell.exe -Command -
the return value is 1 if an error occured, or 0 otherwise, regardless of what exitcode you set.
To have PowerShell return the correct exit code you need to add an exit $LASTEXITCODE to the command string:
powershell.exe -Command "&{& 'C:\path\to\your.ps1'; exit $LASTEXITCODE}"
or call the script using the -File parameter:
powershell.exe -File "C:\path\to\your.ps1"

Ignoring powershell script failure for a particular line

I have a powershell script that defines $ErrorActionPreference = "Stop"
But i also have a start-process call that target a process that returns a non-standard exit code on success (1 instead of 0).
As a result of this, the script is failing even when the start-process is ok.
I tried to append the -ErrorAction "Continue" parameter in the start-process call but it didn't solve the problem.
The problematic line looks like this:
$ErrorActionPreference = "Stop"
...
start-process "binary.exe" -Wait -ErrorAction "Continue"
if ($LastExitCode -ne 1)
{
echo "The executable failed to execute properly."
exit -1
}
...
How could I prevent start-process from making the whole script fail.
Start-Process doesn't update $LASTEXITCODE. Run Start-Process with the -PassThru parameter to get the process object, and evaluate that object's ExitCode property:
$ErrorActionPreference = "Stop"
...
$p = Start-Process "binary.exe" -Wait -PassThru
if ($p.ExitCode -ne 1) {
echo "The executable failed to execute properly."
exit -1
}

PowerShell doesn't return error running executable [duplicate]

I'm new to powershell, but I'm trying to output some simple logging in a ps I'm writing to create scheduled tasks. My code is below. It seems that it doesn't throw an exception when you get an error with schtasks. Another SO question mentioned this with fileIO actions and suggested doing "-ea stop" but that doesn't work with schtasks.
#create log file
$log = "\\servername\!incoming\Deploy\log.txt"
Clear-Content $log
#get input file list
$txtServerList = Gc "\\servername\!incoming\Deploy\serverlist.txt"
#loop through each server
ForEach($strServername In $txtServerList)
{
try
{
#install task from XML template
schtasks /create /s $strServername /tn InventoryServer /XML "\\servername\!incoming\Deploy\TaskTemplate.xml"
#run the task immediately
schtasks /run /s $strServername /tn InventoryServer
}
catch [exception]
{
Add-Content -path $log -value $strServername
#Add-Content -path $log -value $_.Exception
#Add-Content -path $log -value $_.Exception.Message
#Add-Content -path $log -value ""
}
}
I verified that 'Add-Content -path "\servername!incoming\Deploy\log.txt" -value "test"'works, so like I said I'm fairly sure it's just not throwing an exception.
In order for a Try/Catch to work, PowerShell needs a terminating exception. When running a cmdlet in a Try block you can make that happen by using -erroraction Stop (or use the -ea alias). As you already realize SCHTASKS.EXE can't do this. Without a terminating exception, the code in the Catch block will never run.
What you have to do is step out side the box, so to speak, and independently check if Schtasks failed. If so, they you can use Write-Error in your Try block.
One thing you might try is using Start-Process and look at the exit code. Anything other than 0 should be an error.
Try {
get-date
$p=Start-Process schtasks.exe -ArgumentList "/Create foo" -wait -passthru
if ($p.exitcode -ne 0) {
write-error "I failed with error $($p.exitcode)"
}
}
Catch {
"oops"
$_.Exception
}