powershelldebuggingverbose

How can I write debug messages to the console and step through my code (debug), when using `ForEach-Object -AsJob -Parallel ...`?


I have optimized my code for performance by utilizing ForEach-Object -AsJob -Parallel ....

However, it comes with major drawbacks, as I can't write debug messages to the console, nor debug my code by stepping through it.

So, every time I want to debug my code, I have to remove -AsJob and -Parallel options, and then e.g. add the debug logging statements, which is something I want to avoid(!). I don't see how this scale - what if I had hundreds of parallel for each loops that all appeared like "black boxes".

How can I write debug messages to the console and step through my code, when using ForEach-Object -AsJob -Parallel ...?

$appPermissionsJob = $servicePrincipals | ForEach-Object -AsJob -ThrottleLimit $ThrottleLimit -Parallel {
  $spApplicationPermissions = $using:spApplicationPermissions
  $servicePrincipalId = $_.Id

  try {
    $applicationPermissions = Get-MgServicePrincipalAppRoleAssignedTo -ServicePrincipalId $servicePrincipalId -All -PageSize 999

    if ($applicationPermissions -ne $null) {
      $applicationPermissions | ForEach-Object {
        Write-Host "Processing service principal ${servicePrincipalId} with app role $($_.ResourceId)"
        if ($_.ResourceId -eq $using:MSGraphServicePrincipalId) {
          $item = New-Object PSObject -Property ([ordered] @{
              ServicePrincipalId     = $_
              ApplicationPermissions = $applicationPermissions
            })
          $spApplicationPermissions.TryAdd($servicePrincipalId, $item)
        }
      }
    }
  }
  catch {
    Write-Verbose "Failed to download delegated permissions for service principal ${servicePrincipalId}: $($_.Exception.Message)"
    $dictFailed.TryAdd($servicePrincipalId, $_.Exception.Message)
  }
}

Solution

  • As for printing verbose / debugging messages:

    Here are minimal examples that provide a workaround:

    # !! The *outer* -Verbose arguably shouldn't be necessary in either case, 
    # !! but is, as of PowerShell 7.4.x
    
    ForEach-Object -Parallel { Write-Verbose hi -Verbose } -Verbose
    
    ForEach-Object -AsJob -Parallel { Write-Verbose hi -Verbose } |
      Receive-Job -Wait -AutoRemoveJob -Verbose
    

    As for breaking into the debugger inside a ForEach-Object -AsJob -Parallel script block:

    The short of it is that debugging such jobs, as well as the technologically closely related thread jobs created with Start-ThreadJob, via the Debug-Job cmdlet, is currently (as of PowerShell 7.4.x) very limited and cumbersome, due to numerous bugs.


    Here's a minimal example that shows what you can currently do (as of PowerShell 7.4.x):

    # Start a sample job with 3 parallel runspaces.
    $job = 1..3 | ForEach-Object -AsJob -Parallel { 
      # Dummy activity that last about two seconds, so that the 
      # debugger has a chance to attach.
      # Note that using `Wait-Debugger` does NOT currently work.
      $rs = [runspace]::DefaultRunspace
      1..20 | ForEach-Object { 
        $i = $_
        "Hi #$i from runspace $($rs.Name)"
        Start-Sleep -Milliseconds 100 
      }
    }
    
    # Wait until the job is ready to be debugged.
    while ($job.State -ne 'Running') { Start-Sleep -Milliseconds 50 }
    
    # !! Due to a bug, 'Running' is reported *too soon*.
    # !! The suboptimal workaround is to sleep a little.
    Start-Sleep -Milliseconds 100
    
    # Invoke the debugger:
    # Debug-Job can only debug one runspace at a time.
    # Each runspace runs in a *child* job. Here, the first one
    # is targeted.
    # !! It is non-deterministic at which statement the debugger will
    # !! break.
    # Submit 'd' or 'c' to exit the debugging session and continue running, 
    # 'h' to display help.
    $job.ChildJobs[0] | Debug-Job
    
    # After exiting the debugger, 
    # receive and output the job's output, then delete the job.
    $job | Receive-Job -Wait -AutoRemoveJob