Search code examples
pythonpowershellerror-handlingexit-code

Catching the correct error code in Python and PowerShell


I am running a Python FastAPI app with 4 different APIs. In the first one, I have implemented an error handling which after some trouble is working somehow fine. So I copied it's structure to the second API and suddenly it throws an error and won't work anymore.

All of my 4 APIs call a PowerShell script because it's easy to work with the Active Directory with PowerShell. After the first ps1 script is called, it calls another ps1 script under a priviledged user so that we get the permission to add groups to a specific OU (organizational unit in AD). So it's like this: Python app -> First ps1 -> 2nd ps1

My problem: Although there is an error at the 2nd script, I only get zero as an exit code / return code within the Python app and the 1st ps1 script.

I learnt that there is an issue within PowerShell encoding: Python - Get command output cannot be decoded So that's why I am trying to call the first ps script with utf-8 encoding. But I am failing still to trigger the 2nd PowerShell script with utf-8 as well.

To test the error handling, I purposefully implemented a division by zero. Within 2nd script:

Start-Transcript -Append .\managead\ADMgmtlog.txt
Try {
  #Add-ADGroupMember -Identity $groupname -Members $techuser;
  Write-Host "PS1 script ADgroupmgmt: let's crash the script "
  $result = 10/0
} catch {

  $errorMessage = $_.Exception.Message
  Write-Host "PS1 script ADgroupmgmt: Error: " + $errorMessage
  Write-Host "PS1 script ADgroupmgmt: The AD Management script has failed"

  $ErrorActionPreference = 'Stop'
  Write-Host "PS1 script ADgroupmgmt: before throw "

  throw $errorMessage
  Write-Host "PS1 script ADgroupmgmt: after throw"
  Stop-Transcript
}  

Then, I within the first ps script, I call this 2nd one and try to catch the error:

try {            

              
           #$enc = [System.Text.Encoding]::UTF8 #not used currently
           $process = Start-Process -NoNewWindow pwsh.exe -Credential $credential -ArgumentList $ArgumentList #-PassThru
           #do{sleep 1}while(Get-Process -Id $process.Id -Ea SilentlyContinue)
           Write-Host "PS1 script ADMgmtInitial: process: " + $process

           #Write-Host "PS1 script ADMgmtInitial: Now lets decode with GetBytes"
           #$encprocess= $enc.GetBytes($process)
           #Write-Host "PS1 script ADMgmtInitial: encprocess: " + $encprocess
           #$exitCode = $encprocess.ExitCode 
           
           $exitCode = $process.ExitCode  
            
           Write-Host "PS1 script ADMgmtInitial: exitCode: " + $exitCode
           Write-Host "PS1 script ADMgmtInitial: ps1 Request success | action = ${action}; groupname = ${groupname}; tech user = ${techuser}; region = ${region}"
           
        } catch {
           <#
           $encprocess= $enc.GetBytes($process)
           Write-Host "PS1 script ADMgmtInitial: encprocess: " + $encprocess
           $exitCode = $encprocess.ExitCode  
           #>
           
           $exitCode = $process.ExitCode  
           Write-Host "PS1 script ADMgmtInitial: exitCode: " + $exitCode
           
           $errorMessage = $_.Exception.Message
           Write-Host "PS1 script ADMgmtInitial: Error: " + $errorMessage
           Write-Host "PS1 script ADMgmtInitial: The AD Management script has failed"
           throw $errorMessage
           $ErrorActionPreference = 'Stop'
        }

By throwing another error within this catch command, the errorMessage should be passed to the Python app. Here, I am triggering the first ps script like this:

result = subprocess.run(
    f'''\
    chcp 65001 >NUL & pwsh.exe -File "./managead/ADMgmtInitial.ps1" -action "{item.action}" -groupname "{providedadgroup}" -techuser "{item.techuser}" -region "{item.region}"
    ''',
    shell=True,              # call via cmd.exe (on Windows)
    text=True,               # decode output as text
    stdout=subprocess.PIPE,  # capture stdout instead of printing to the console
    stderr=subprocess.PIPE   # capture stderr
)

# Print stdout and stderr output for diagnostic purposes.
print("stdout: " + result.stdout)
print("stderr: " + result.stderr)

returncodestring = str(result.returncode)     
print ("returncode in Python: " + returncodestring)

print("AD group changed | Action= " + item.action + "; csname = " + item.csname + "; Tech User = " + item.techuser + "; Region = " + item.region)

#check error code
if result.returncode == 0: #0 stands for no returned error; everything was executed without errors    
    
    stdoutmessage = result.stderr
    print ("stdoutmessage: ")
    print (stdoutmessage)

    #return specific messages depended on the action type add or remove
    if item.action == "add":
        print("User " + item.techuser + " has been added to the AD group " + providedadgroup)
        return {"message": "Success: User " + item.techuser + " has been added to the AD group " + providedadgroup}
    else: #action == "remove"
        print("User " + item.techuser + " has been removed from the AD group " + providedadgroup)
        return {"message": "Success: User " + item.techuser + " has been removed from the AD group " + providedadgroup}
    
else: # !=0 stands for an error; something went wrong
    returncodestring = str(result.returncode)
    print ("returncode in Python: " + returncodestring)
    errormessagestring= result.stderr.strip()
    print ("Python Error | errormessagestring: " + errormessagestring)

    #return specific messages depended on the action type add or remove
    if item.action == "add":
        print("Error: User " + item.techuser + " could not be added to the AD group " + providedadgroup)
        raise HTTPException(
            status_code=500,
            detail="Failed to add user " + item.techuser + " to the AD group " + providedadgroup +". Details: " + errormessagestring,
            headers={"Error": "Could not add user " + item.techuser + " to the AD group " + providedadgroup},
        )
    else: #action == "remove"
        print("Error: User " + item.techuser + " could not be removed from the AD group " + providedadgroup)
        raise HTTPException(
            status_code=500,
            detail="Failed to remove user " + item.techuser + " from the AD group " + providedadgroup +". Details: " + errormessagestring,
            headers={"Failed to remove user " + item.techuser + " from the AD group " + providedadgroup},
        )

Now, although there is an error thrown, I get exit code == 0 within the first ps script. This code is passed to the Python app where the result.returncode is zero as well. It's really weird because the error is actually regonized when I print it. This is the part of my console:

Action successful: E415_myOU_ as a prefix has been added. Result: E415_myOU_mygroup
stdout: Transcript started, output file is .\managead\ADInitiallog.txt
PS1 script ADMgmtInitial: ps1 Request received | action = add; groupname = E415_myOU_mygroup;         techuser = myuserid; region = EMEA
PS1 script ADMgmtInitial: ArgumentList :-noprofile -file ".\managead\ADgroupmgmt.ps1" -action add -groupname E415_myOU_mygroup -techuser myuserid -region EMEA
PS1 script ADMgmtInitial: Execution started for region EMEA
PS1 script ADMgmtInitial: Calling the ADgroupmgmt.ps1 script to add myuserid to the group     E415_myOU_mygroup
PS1 script ADMgmtInitial: AD Group E415_myOU_mygroup is available:     CN=E415_myOU_mygroup,OU=Groups,OU=myOU,OU=notshown,DC=emea,DC=notshown,DC=net
PS1 script ADMgmtInitial: techuser is a user. Trying to find the user.
PS1 script ADMgmtInitial: AD user myuserid is available: CN=myname\, myname(123),OU=Users,OU=_GlobalResources,OU=notshown,DC=emea,DC=notshown,DC=net
PS1 script ADMgmtInitial: process:  +
PS1 script ADMgmtInitial: exitCode:  +
PS1 script ADMgmtInitial: ps1 Request success | action = add; groupname = E415_myOU_mygroup;     tech user = myuserid; region = EMEA
Transcript stopped, output file is .\managead\ADInitiallog.txt
Transcript started, output file is .\managead\ADMgmtlog.txt
PS1 script ADgroupmgmt: ps1 Request received | action = add; groupname = E415_myOU_mygroup; techuser = myuserid; region = EMEA
PS1 script ADgroupmgmt: ps1 Request | Starting to add myuserid to E415_myOU_mygroup
PS1 script ADgroupmgmt: let's crash the script
PS1 script ADgroupmgmt: Error:  + Attempted to divide by zero.
PS1 script ADgroupmgmt: The AD Management script has failed
PS1 script ADgroupmgmt: before throw

stderr: The specified drive root "C:\Users\myuserid\AppData\Local\Temp\" either does not exist, or it is not a folder. #comment added afterwards: no idea why this error shows up
Exception: .\managead\ADgroupmgmt.ps1:41:7
Line |
  41 |        throw $errorMessage
     |        ~~~~~~~~~~~~~~~~~~~
     | Attempted to divide by zero.

returncode in Python: 0
AD group changed | Action= add; csname = sourcename; Tech User = myuserid; Region = EMEA
stdoutmessage:
The specified drive root "C:\Users\myuserid\AppData\Local\Temp\" either does not exist, or it is not a folder. 
Exception: .\managead\ADgroupmgmt.ps1:41:7
Line |
  41 |        throw $errorMessage
     |        ~~~~~~~~~~~~~~~~~~~
     | Attempted to divide by zero.

User myuserid has been added to the AD group E415_myOU_mygroup #should not be shown because if in-built error
INFO:     127.0.0.1:55917 - "POST /managead HTTP/1.1" 200 OK

So, the API should never return http code 200! I need to prevent this and return the correct 400 or 500 http code.

Do you have any idea why I am not getting the correct exit codes?


Solution

  • You will not be able to catch a PowerShell exception in Python. So there is no need to throw in your catch block again. Instead, write your error message to the error stream and exit PowerShell with your desired exit code. Example:

    Write-Error $errorMessage -ErrorAction Continue
    exit $exitCode
    

    In Python, you can then read your error message from stderr and the return code will equal your $exitCode from PowerShell.


    If you want to catch a PowerShell exception from another PowerShell script in PowerShell, you have to execute it in the same context by directly calling it or by dot sourcing it. As you use the nested PowerShell invocation to run it elevated, you cannot call your second script directly. You will have to stick to your approach with Start-Process. But you can apply the same logic as you do in your first script:

    Propagate your exit code with exit. To get stdout/stderr, have a look at this Q&A.