alx9r
alx9r

Reputation: 4263

In a compiled Cmdlet, how can I write an error from a thread job without burying the PositionMessage?

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.

How can I write an error from a job so that it shows the original position message like Receive-Job does?

Upvotes: 3

Views: 64

Answers (1)

Santiago Squarzon
Santiago Squarzon

Reputation: 60838

First, a notable mention, using your original cmdlet, you can still see where in the scriptblock the error originated by looking at the ScriptStackTrace property:

try {
    Start-ThreadJob { 1 / 0 } |
        Wait-Job |
        Receive-JobFancy -ErrorAction Stop
}
catch {
    $_.ScriptStackTrace # at <ScriptBlock>, <No file>: line 1
}

Now, to answer the question, they use a property you have no access to, see ReceiveJob.cs#L815-L816. So if you use reflection you get the same result (wouldn't advise it...):

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);
        }
    }
}

If you want to create a setter delegate you can also do:

typeof(ErrorRecord).GetProperty(
    "PreserveInvocationInfoOnce",
    BindingFlags.NonPublic | BindingFlags.Instance)?
    .GetSetMethod(true)?
    .CreateDelegate<Action<ErrorRecord, bool>>();

See also in ErrorPackage.cs#L1576-L1577:

// 2005/07/14-913791 "write-error output is confusing and misleading"
internal bool PreserveInvocationInfoOnce { get; set; }

Not sure what 913791 is, but if it is a GitHub issue you might find more details about this property.

Upvotes: 3

Related Questions