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?
try {
[powershell]::Create().AddScript({
throw 'message'
}).Invoke()
}
catch {
$_.Exception.InnerException.ErrorRecord.ScriptStackTrace
}
at <ScriptBlock>, <C:\Users\UserName\Desktop\test.ps1>: line 3
at <ScriptBlock>, <No file>: line 2
> $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
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:
$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()
.
$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)}
complete
complete
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();
}
}
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.