PowerShell / PowerShell/PowerShell

Provide a public way of preserving ErrorRecord.InvocationInfo when writing errors from jobs

Open
#24,984 4 comments 2 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

Issue-Enhancement Needs-Triage WG-Engine WG-Remoting
Dominant language
C#
Stars
55.5k
Forks
8.5k
Avg merge
1d 2h
Merged PRs (30d)
88

Description

Summary of the new feature / enhancement

As an author of compiled Cmdlets that manage jobs I would like the ability to show errors from jobs that accurately show the position in user code where that error originated.

The problem is set out in detail this stackoverflow question:

Errors from thread jobs written to the error stream by Receive-Job show the PositionMessage for the originating error.

Consider, for example

$job =
   Start-ThreadJob {
       1/0 # error line
   }
$job | Wait-Job | Out-Null
$job | Receive-Job

which outputs

RuntimeException:
Line |
  2 |          1/0 # error line
    |          ~~~
    | Attempted to divide by zero.

Note that the original error line is shown. The line number output is incorrect (which is a different matter altogether) but at least the impugned code shown in the position message is correct.

I also would like to write errors from a thread job from my own compiled Cmdlet like Receive-Job does. However, writing the error with Cmdlet.WriteError() shows the position of the Cmdlet not the position of the original error:

Add-Type @'
using System.Management.Automation;

[Cmdlet(VerbsCommunications.Receive,"JobFancy")]
public class ReceiveJobFancy : PSCmdlet {
   [Parameter(Mandatory = true,ValueFromPipeline = true)]
   public Job Job {get; set;}
   protected override void ProcessRecord() {
       foreach (var e in Job.Error) WriteError(e);
   }
}
'@ -PassThru   |
   % Assembly |
   Import-Module

$job =
   Start-ThreadJob {
       1/0 # error line
   }
$job | Wait-Job | Out-Null
$job | Receive-JobFancy

outputs

Line |
 22 |  $job | Receive-JobFancy
    |         ~~~~~~~~~~~~~~~~
    | Attempted to divide by zero.

Showing all the errors in $job with the same receiving command's position makes diagnostics more difficult. Get-Error does reveal the ErrorRecord with the original error line at $.Exception.ErrorRecord. But using Get-Error to see all errors from a job is awkward. I'd prefer to mimick the behavior of Receive-Job so that the original error is shown.

Santiago Squarzon determined in his answer to that question that Receive-Job accomplishes this with the line

e.PreserveInvocationInfoOnce = true;

PreserveInvocationInfoOnce is, unfortunately, internal:

internal bool PreserveInvocationInfoOnce { get; set; }

But setting PreserveInvocationInfoOnce indeed achieves the desired result.

demo code

Adapted from this answer

Add-Type @'
using System.Management.Automation;
using System.Reflection;

[Cmdlet(VerbsCommunications.Receive, "JobFancy")]
public class ReceiveJobFancy : PSCmdlet
{
    private PropertyInfo _propertyInfo;

    [Parameter(Mandatory = true, ValueFromPipeline = true)]
    public Job Job { get; set; }

    protected override void BeginProcessing()
    {
        _propertyInfo = typeof(ErrorRecord).GetProperty(
            "PreserveInvocationInfoOnce",
            BindingFlags.NonPublic | BindingFlags.Instance);
    }

    protected override void ProcessRecord()
    {
        foreach (var e in Job.Error)
        {
            _propertyInfo.SetValue(e, true);
            WriteError(e);
        }
    }
}
'@ -PassThru   |
   % Assembly |
   Import-Module

$job =
   Start-ThreadJob {
       1/0 # error line
   }
$job | Wait-Job | Out-Null
$job | Receive-JobFancy

outputs

RuntimeException:
Line |
   2 |         1/0 # error line
     |         ~~~
     | Attempted to divide by zero.

In other words, this seems to already be possible with a private API. I don't see a way of preserving the position message of the ErrorRecord the goal without accessing this private API, though.

Enabling community workarounds to issues affecting Receive-Job, ForEach-Object -Parallel etc

For what its worth, this relatively small change to the PowerShell project itself, would enable community implementations of commands that write errors from jobs to the pipeline. Receive-Job and ForEach-Object -Parallel are the obvious such commands. I have reimplemented each of those to workaround a variety of their shortcomings. Those commands lie at the intersection of a substantial variety of concerns, so it's no surprise that their original implementations resulted in some shortcomings. (#24575, #21332, #5433, #21549, #24901, and #24016 are just a few of issues I have personally worked around in my own implementations.) Those workarounds, however, each ultimate rely on using the non-public API PreserveInvocationInfoOnce to avert obscuring the location of errors in user code arising within jobs. I'm reluctant to publish or promote workarounds that rely on private APIs and I have seen that other command authors are reluctant as well. Providing a public API as requested here, would enable many of those workarounds to be implemented and validated in community modules.

Proposed technical implementation details

The obvious solution to this is to change PreserveInvocationInfoOnce from internal to public.

Another option would be to provide an overload of Cmdlet.WriteError that accepts an argument corresponding to PreserveInvocationInfoOnce:

public void WriteError (System.Management.Automation.ErrorRecord errorRecord, bool preserveInvocationInfo);

Contributor guide

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Research direction

Start by reading ErrorPackage.cs at PreserveInvocationInfoOnce and ReceiveJob.cs at the existing use described in the issue. Compare the proposed public property with a Cmdlet.WriteError overload, then inspect nearby error-handling behavior. Done means compiled cmdlets can preserve a job error's original invocation position without private reflection, with the existing Receive-Job behavior retained.

Written by the indexing model from the issue text.

Assessment

Tech stack
csharp, powershell
Domain
backend, cli
Issue type
Feature
Difficulty
4/5
Estimated time
3-5 days
Activity status
Stale
Clarity
Mostly clear
Newbie friendliness
35/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.