Skip to content

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

Closed

Description

@alx9r

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

Activity

  1. lzybkr commented on Aug 14, 2018

    @lzybkr
    Contributor

    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]')

  2. added
    Issue-Questionideally support can be provided via other mechanisms, but sometimes folks do open an issue to get a
    on Aug 16, 2018
  3. alx9r commented on Aug 21, 2018

    @alx9r
    Author

    Thanks Jason Shirk (@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
    
  4. alx9r commented on Aug 21, 2018

    @alx9r
    Author

    Jason Shirk (@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()
    
  5. reopened this on Aug 21, 2018
  6. lzybkr commented on Aug 21, 2018

    @lzybkr
    Contributor

    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.

  7. SeeminglyScience commented on Aug 22, 2018

    @SeeminglyScience
    Contributor

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

  8. lzybkr commented on Aug 22, 2018

    @lzybkr
    Contributor

    Patrick Meinecke (@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.

  9. SeeminglyScience commented on Aug 22, 2018

    @SeeminglyScience
    Contributor

    Jason Shirk (@lzybkr) It's internal unfortunately :/

  10. lzybkr commented on Aug 22, 2018

    @lzybkr
    Contributor

    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.

  11. alx9r commented on Aug 23, 2018

    @alx9r
    Author

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

    Done. The new issue is #7626.

  12. alx9r commented on Aug 24, 2018

    @alx9r
    Author

    Thanks for the help Jason Shirk (@lzybkr) and Patrick Meinecke (@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
    
  13. alx9r commented on Aug 24, 2018

    @alx9r
    Author

    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();
        }
    }
  14. animasc commented on May 8, 2020

    @animasc

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

  15. alx9r commented on May 8, 2020

    @alx9r
    Author

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

                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

        static class ScriptBlockExtensions
        {
            internal static ScriptBlock Clone(this ScriptBlock scriptBlock)
            {
                // see also PowerShell/PowerShell#7530
                return ((ScriptBlockAst)scriptBlock.Ast).GetScriptBlock();
            }
        }    
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    Issue-Questionideally support can be provided via other mechanisms, but sometimes folks do open an issue to get a

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions