Powershell: Is there an alternative to PowerShell.AddScript() that preserves file position information?

Created on 14 Aug 2018  路  13Comments  路  Source: PowerShell/PowerShell

The parameter to .AddScript() is a string, not a scriptblock, so any file position information that might have been present in the ScriptBlock object never makes it into that method. Is there some other way to get a PowerShell object to invoke a ScriptBlock without losing file position information?

Steps to reproduce

try {
    [powershell]::Create().AddScript({
        throw 'message'
    }).Invoke()
}
catch {
    $_.Exception.InnerException.ErrorRecord.ScriptStackTrace
}

Expected behavior

at <ScriptBlock>, <C:\Users\UserName\Desktop\test.ps1>: line 3

Actual behavior

at <ScriptBlock>, <No file>: line 2

Environment data

> $PSVersionTable

Name                           Value
----                           -----
PSVersion                      6.1.0-preview.4
PSEdition                      Core
GitCommitId                    6.1.0-preview.4
OS                             Microsoft Windows 6.3.9600 
Platform                       Win32NT
PSCompatibleVersions           {1.0, 2.0, 3.0, 4.0...}
PSRemotingProtocolVersion      2.3
SerializationVersion           1.1.0.1
WSManStackVersion              3.0
Issue-Question

Most helpful comment

I don't see any immediate downsides to making it public.

Maybe try calling it via reflection in some experiments where you were using $scriptBlock.Ast.GetScriptBlock() to see if there are any problems or if I'm mistaken about the performance impact.

All 13 comments

How about:

try {
    [powershell]::Create().AddCommand('Invoke-Command').AddArgument({
        throw 'message'
    }).Invoke()
} catch {
    $_.Exception.InnerException.ErrorRecord.ScriptStackTrace
}

Your stack will be 1 deeper than you want, but that should be easy to clean up.

If you don't want to use Invoke-Command, you could use AddScript('. $args[0]')

Thanks @lzybkr. I think that will get me there. For posterity, here's what it seems to take to accomplish all of the following:

  • invoke a scriptblock in another runspace
  • pass positional arguments
  • pass input
  • pass named arguments
  • preserve scriptblock position info on error
$inputObject = 'input1','input2'
$argumentList = 'arg1','arg2'
$namedParameters = @{
    Named1 = 'named 1'
    Named2 = 'named 2'
}
$scriptblock = {
    param($Named1,$Named2)
    begin {
        function f { throw 'something' }
    }
    process
    {
        $_
        $PSBoundParameters
        $args
        f
    }
}

$output = [System.Management.Automation.PSDataCollection[psobject]]::new()
try {

    [powershell]::Create().
        AddScript({    
            process {
                $argumentList = $args[1]
                $namedParameters = $args[2]
                ,$_ | . $args[0] @argumentList @namedParameters
            }
        }).
        AddArgument($scriptblock).
        AddArgument($argumentList).
        AddArgument($namedParameters).
        Invoke((,$inputObject),$output)
    $output
}
catch {
    $output
    $_.Exception.InnerException.ErrorRecord.ScriptStackTrace
}

That outputs

input1
input2

Key    Value
---    -----
Named2 named 2
Named1 named 1
arg1
arg2
at f, C:\Users\un1\Desktop\test.ps1: line 10
at <ScriptBlock><Process>, C:\Users\un1\Desktop\test.ps1: line 17
at <ScriptBlock><Process>, <No file>: line 5

@lzybkr I'm seeing crashes and "InvalidOperationException: Stack Empty" using this technique combined with concurrent calls to .BeginInvoke().

Repro

$scriptblock = {
    function fibonacci {
        param([int]$n)
        [bigint]$a=0
        [bigint]$b=1
        foreach ($x in 0..$n)
        {
            $a,$b = $b,($a+$b)
        }
        $b
    }
    fibonacci 100000 | % {'complete'}
}

$invocations = 1..2 |
    % {
        $powershell = [powershell]::Create().
            AddScript({. $args[0]}).
            AddArgument($scriptblock)
        @{
            PowerShell = $powershell
            Invocation = $powershell.BeginInvoke()
        }
    }
$invocations |
    %{ $_.PowerShell.EndInvoke($_.Invocation)}

Expected

complete
complete

Actual

This is the most verbose output I have witnessed. The run ended in a "PowerShell Core 6 has stopped working" message box.

complete
% : Stack empty.
At C:\users\un1\Desktop\test.ps1:26 char:5
+     %{ $_.PowerShell.EndInvoke($_.Invocation)}
+     ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+ CategoryInfo          : NotSpecified: (:) [ForEach-Object], InvalidOperationEx
ception
+ FullyQualifiedErrorId : System.InvalidOperationException,Microsoft.PowerShell.
Commands.ForEachObjectCommand


An error has occurred that was not properly handled. Additional information is s
hown below. The PowerShell process will exit.

Unhandled Exception: System.InvalidOperationException: Stack empty.
   at System.Collections.Generic.Stack`1.ThrowForEmptyStack()
   at System.Collections.Generic.Stack`1.Pop()
   at System.Management.Automation.DlrScriptCommandProcessor.OnRestorePreviousSc
ope()
   at System.Management.Automation.CommandProcessorBase.DoComplete()
   at System.Management.Automation.Internal.PipelineProcessor.DoCompleteCore(Com
mandProcessorBase commandRequestingUpstreamCommandsToStop)
   at System.Management.Automation.Internal.PipelineProcessor.SynchronousExecute
Enumerate(Object input)
   at System.Management.Automation.Runspaces.LocalPipeline.InvokeHelper()
   at System.Management.Automation.Runspaces.LocalPipeline.InvokeThreadProc()
   at System.Management.Automation.Runspaces.PipelineThread.WorkerProc()
   at System.Threading.Thread.ThreadMain_ThreadStart()
   at System.Threading.ExecutionContext.RunInternal(ExecutionContext executionCo
ntext, ContextCallback callback, Object state)
--- End of stack trace from previous location where exception was thrown ---
   at System.Runtime.ExceptionServices.ExceptionDispatchInfo.Throw()

That looks like a potentially serious bug, maybe open a new issue?

This looks like the stack to save and restore variables like $_ when dot sourcing. If the stack was empty, then the wrong stack may have been used, probably from another runspace.

@alx9r Might be due to PowerShell attempting to send the scriptblock back to it's origin runspace. Try AddArgument($scriptBlock.Ast.GetScriptBlock())

@SeeminglyScience - $scriptBlock.Clone() should work as well and should be faster - it would avoid recompiling the script block.

Also - if that workaround works, I still think this issue should not be ignored - if my assumption is correct, it might also manifest silently and do something unexpected.

@lzybkr It's internal unfortunately :/

I don't see any immediate downsides to making it public.

Maybe try calling it via reflection in some experiments where you were using $scriptBlock.Ast.GetScriptBlock() to see if there are any problems or if I'm mistaken about the performance impact.

That looks like a potentially serious bug, maybe open a new issue?

Done. The new issue is #7626.

Thanks for the help @lzybkr and @SeeminglyScience.

It seems like the peformance of .Clone() and .Ast.GetScriptBlock() are similar:

function Clone {
    param($scriptblock)
    [scriptblock].GetMethod(
        'Clone',
        [System.Reflection.BindingFlags]'Instance,NonPublic'
    ).
        Invoke($scriptblock,$null)
}

function GetFromAst {
    param($scriptblock)
    $scriptblock.Ast.GetScriptBlock()
}

$scriptblock = {
    function fibonacci {
        param([int]$n)
        [bigint]$a=0
        [bigint]$b=1
        foreach ($x in 0..$n)
        {
            $a,$b = $b,($a+$b)
        }
        $b
    }
    fibonacci 100000 | % {'complete'}
}

'GetFromAst','Clone' |
    % {
        $commandName = $_
        [pscustomobject]@{
            CommandName = $commandName
            'Time(s)   ' = Measure-Command { 
                1..10000 | % {& $commandName $scriptblock} 
            } |
                % { [System.Math]::Round($_.TotalSeconds,2) }
        }
    }

One typical result of this run on my computer is:

CommandName Time(s)
----------- ----------
GetFromAst        1.25
Clone             1.36

FWIW I have settled (for now) on the following for creating scriptblock clone from C#:

static class Extensions
{
    public static ScriptBlock Clone(this ScriptBlock scriptBlock)
    {
        return ((ScriptBlockAst)scriptBlock.Ast).GetScriptBlock();
    }
}

This is interesting! Any non reported progress since 2018? Thanks

@animasc I just checked. It looks like I settled on the following for my general-purpose runspace invoker:

```C#
var powershell = PowerShell.Create();
powershell
.AddScript(@"
process {
$argumentList = $args[1]
$namedArgs = $args[2]
Set-Variable __PSStreams $args[3] -Option Constant,AllScope
Set-Variable __PSOutput $args[4] -Option Constant,AllScope
,$_ | . $args[0] @argumentList @namedArgs
}
")
.AddArgument(input.ScriptBlock.Clone())
.AddArgument(input.ArgumentList)
.AddArgument(input.NamedArgs)
.AddArgument(powershell.Streams)
.AddArgument(output);


and 

```C#
    static class ScriptBlockExtensions
    {
        internal static ScriptBlock Clone(this ScriptBlock scriptBlock)
        {
            // see also PowerShell/PowerShell#7530
            return ((ScriptBlockAst)scriptBlock.Ast).GetScriptBlock();
        }
    }    
Was this page helpful?
0 / 5 - 0 ratings