Home > Software engineering >  Verify a function in PowerShell has run succesfully
Verify a function in PowerShell has run succesfully

Time:01-06

I'm writing a script to backup existing bit locker keys to the associated device in Azure AD, I've created a function which goes through the bit locker enabled volumes and backs up the key to Azure however would like to know how I can check that the function has completed successfully without any errors. Here is my code. I've added a try and catch into the function to catch any errors in the function itself however how can I check that the Function has completed succesfully - currently I have an IF statement checking that the last command has run "$? - is this correct or how can I verify please?

    function Invoke-BackupBDEKeys {

        ##Get all current Bit Locker volumes - this will ensure keys are backed up for devices which may have additional data drives
        $BitLockerVolumes = Get-BitLockerVolume | select-object MountPoint
        foreach ($BDEMountPoint in $BitLockerVolumes.mountpoint) {

            try {
            #Get key protectors for each of the BDE mount points on the device
            $BDEKeyProtector = Get-BitLockerVolume -MountPoint $BDEMountPoint | select-object -ExpandProperty keyprotector
            #Get the Recovery Password protector - this will be what is backed up to AAD and used to recover access to the drive if needed
            $KeyId = $BDEKeyProtector | Where-Object {$_.KeyProtectorType -eq 'RecoveryPassword'}
            #Backup the recovery password to the device in AAD
            BackupToAAD-BitLockerKeyProtector -MountPoint $BDEMountPoint -KeyProtectorId $KeyId.KeyProtectorId
            }
             catch {
                 Write-Host "An error has occured" $Error[0] 
            }
        }
    }     

#Run function
    Invoke-BackupBDEKeys

if ($? -eq $true) {

    $ErrorActionPreference = "Continue"
    #No errors ocurred running the last command - reg key can be set as keys have been backed up succesfully
    $RegKeyPath = 'custom path'
    $Name = 'custom name'
    New-ItemProperty -Path $RegKeyPath -Name $Name -Value 1 -Force
    Exit
}
 else {
    Write-Host "The backup of BDE keys were not succesful"
    #Exit
}

CodePudding user response:

  • Unfortunately, as of PowerShell 7.2.1, the automatic $? variable has no meaningful value after calling a written-in-PowerShell function (as opposed to a binary cmdlet) - unless the function call is aborted by a terminating error. (More immediately, even inside the function, $? only reflects $false at the very start of the catch block, as Mathias notes).

    • If PowerShell functions had feature parity with binary cmdlets, then emitting at least one non-terminating error with Write-Error would set $? in the caller's scope to $false, but that is currently not the case.

    • You can work around this limitation by using $PSCmdlet.WriteError() from an advanced function or script, but that is quite cumbersome. The same applies to $PSCmdlet.ThrowTerminatingError(), which is the only way to create a statement-terminating error from PowerShell code. (By contrast, the throw statement generates a script-terminating error, i.e. terminates the entire script and its callers).

    • See this answer for more information and links to relevant GitHub issues.

  • As a workaround, I suggest:

    • Make your function relay caught errors via Write-Error.

    • Make your function an advanced one, so as to enable support for the common -ErrorVariable parameter - it allows you to collect all non-terminating errors emitted by the function in a self-chosen variable.

      • You can combine this with the common -ErrorAction parameter to initially silence the errors (-ErrorAction SilentlyContinue), so you can emit them later on demand.
    • After invocation, check if any errors were collected, to determine whether at least one key wasn't backed up successfully.

Here's the outline of this approach:

function Invoke-BackupBDEKeys {
  # Make the function an *advanced* function, to enable
  # support for -ErrorVariable (and -ErrorAction)
  [CmdletBinding()]
  param()

  # ...
  foreach ($BDEMountPoint in $BitLockerVolumes.mountpoint) {

    try {
      # ... statements that may cause terminating errors.
      # Note: Non-terminating errors are NOT caught and are emitted
      #       to the error stream - unless $ErrorActionPreference = 'Stop'
      #       is in effect or -ErrorAction Stop was passed.
    }
    catch {
      # Convert the caught terminating error to a non-terminating one
      # and cotinue the loop.      
      $_ | Write-Error
    }
  }
}     

# Call the function and collect any
# non-terminating errors in variable $errs.
Invoke-BackupBDEKeys -ErrorAction SilentlyContinue -ErrorVariable errs

# If $errs is an empty collection, no errors occurred.
if (-not $errs) {

  "No errors occurred"
  # ... 
}
else {
  "At least one error occurred during the backup of BDE keys:`n$errs"
  # ...
}

CodePudding user response:

As stated elsewhere, the try/catch you're using is what is preventing the relay of the error condition. That is by design and the very intentional reason for using try/catch.

What I would do in your case is either create a variable or a file to capture the error info. My apologies to anyone named 'Bob'. It's the variable name that I always use for quick stuff.

Here is a basic sample that works:

$bob = (1,2,"blue",4,"notit",7)

$bobout = @{}                               #create a hashtable for errors

foreach ($tempbob in $bob) {
   $tempbob
   try {
      $tempbob - 2                          #this will fail for a string
   } catch {
      $bobout.Add($tempbob,"not a number")  #store a key/value pair (current,msg)
   }
}

$bobout                                     #output the errors

Here we created an array just to use a foreach. Think of it like your $BDEMountPoint variable.

Go through each one, do what you want. In the }catch{}, you just want to say "not a number" when it fails. Here's the output of that:

-1
0
2
5

Name                           Value
----                           -----
notit                          not a number
blue                           not a number

All the numbers worked (you can obvious surpress output, this is just for demo). More importantly, we stored custom text on failure.

Now, you might want a more informative error. You can grab the actual error that happened like this:

$bob = (1,2,"blue",4,"notit",7)

$bobout = @{}                               #create a hashtable for errors

foreach ($tempbob in $bob) {
   $tempbob
   try {
      $tempbob - 2                          #this will fail for a string
   } catch {
      $bobout.Add($tempbob,$PSItem)         #store a key/value pair (current,error)
   }
}

$bobout

Here we used the current variable under inspection $PSItem, also commonly referenced as $_.

-1
0
2
5

Name                           Value
----                           -----
notit                          Cannot convert value "notit" to type "System.Int32". Error: "Input string was not in ...
blue                           Cannot convert value "blue" to type "System.Int32". Error: "Input string was not in a...

You can also parse the actual error and take action based on it or store custom messages. But that's outside the scope of this answer. :)

  •  Tags:  
  • Related