@@ -532,7 +532,7 @@ public async Task CanAttachScriptWithPathMappings()
532
532
533
533
string [ ] logStatements = [ "$PSCommandPath" , "after breakpoint" ] ;
534
534
535
- await RunWithAttachableProcess ( logStatements , async ( filePath , processId ) =>
535
+ await RunWithAttachableProcess ( logStatements , async ( filePath , processId , runspaceId ) =>
536
536
{
537
537
string localParent = Path . Combine ( Path . GetTempPath ( ) , Path . GetRandomFileName ( ) ) ;
538
538
string localScriptPath = Path . Combine ( localParent , Path . GetFileName ( filePath ) ) ;
@@ -541,11 +541,11 @@ await RunWithAttachableProcess(logStatements, async (filePath, processId) =>
541
541
542
542
Task < StoppedEvent > nextStoppedTask = nextStopped ;
543
543
544
- _ = await client . Attach (
544
+ AttachResponse attachResponse = await client . Attach (
545
545
new PsesAttachRequestArguments
546
546
{
547
547
ProcessId = processId ,
548
- RunspaceId = 1 ,
548
+ RunspaceId = runspaceId ,
549
549
PathMappings = [
550
550
new ( )
551
551
{
@@ -554,6 +554,7 @@ await RunWithAttachableProcess(logStatements, async (filePath, processId) =>
554
554
}
555
555
]
556
556
} ) ?? throw new Exception ( "Attach response was null." ) ;
557
+ Assert . NotNull ( attachResponse ) ;
557
558
558
559
SetBreakpointsResponse setBreakpointsResponse = await client . SetBreakpoints ( new SetBreakpointsArguments
559
560
{
@@ -585,12 +586,7 @@ await RunWithAttachableProcess(logStatements, async (filePath, processId) =>
585
586
586
587
// Wait until we hit the breakpoint
587
588
stoppedEvent = await nextStoppedTask ;
588
-
589
- // WinPS has a bug on attach where it doesn't send the breakpoints on the stop event.
590
- string expectedReason = PsesStdioLanguageServerProcessHost . IsWindowsPowerShell
591
- ? "step"
592
- : "breakpoint" ;
593
- Assert . Equal ( expectedReason , stoppedEvent . Reason ) ;
589
+ Assert . Equal ( "breakpoint" , stoppedEvent . Reason ) ;
594
590
595
591
// The code before the breakpoint should have already run
596
592
// It will contain the actual script being run
@@ -612,33 +608,26 @@ await RunWithAttachableProcess(logStatements, async (filePath, processId) =>
612
608
} ) ;
613
609
}
614
610
615
- private async Task RunWithAttachableProcess ( string [ ] logStatements , Func < string , int , Task > action )
611
+ private async Task RunWithAttachableProcess ( string [ ] logStatements , Func < string , int , int , Task > action )
616
612
{
617
- // Ensures that we don't try and attach until the PowerShell proc
618
- // has fully started and is ready to accept the attach request.
619
- string startMarker = Path . Combine ( Path . GetTempPath ( ) , Path . GetRandomFileName ( ) ) ;
620
- File . WriteAllText ( startMarker , "" ) ;
621
-
622
- string filePath = NewTestFile ( GenerateLoggingScript ( logStatements ) ) ;
623
-
624
- // PowerShell has no public API for waiting when Debug-Runspace
625
- // has attached itself to a Runspace. We use this reflection
626
- // hackery to wait until the AvailabilityChanged event is
627
- // subscribed to by Debug-Runspace as a marker that it is ready to
628
- // continue.
629
- // Use https://github.com/PowerShell/PowerShell/pull/25788 once it
630
- // is available.
613
+ /*
614
+ There is no public API in pwsh to wait for an attach event. We
615
+ use reflection to wait until the AvailabilityChanged event is
616
+ subscribed to by Debug-Runspace as a marker that it is ready to
617
+ continue.
618
+
619
+ We also run the test script in another runspace as WinPS'
620
+ Debug-Runspace will break on the first statement after the
621
+ attach and we want that to be the Wait-Debugger call.
622
+
623
+ We can use https://github.com/PowerShell/PowerShell/pull/25788
624
+ once that is merged and we are running against that version but
625
+ WinPS will always need this.
626
+ */
631
627
string scriptEntrypoint = @"
632
- param([string]$StartMarker, [string]$TestScript)
633
-
634
- $ErrorActionPreference = 'Stop'
635
-
636
- # Removing this file tells the runner that the script has
637
- # started and pwsh is ready to accept the attach request.
638
- Remove-Item -LiteralPath $StartMarker -Force
628
+ param([string]$TestScript)
639
629
640
630
$debugRunspaceCmd = Get-Command Debug-Runspace -Module Microsoft.PowerShell.Utility
641
- $runspace = [Runspace]::DefaultRunspace
642
631
$runspaceBase = [PSObject].Assembly.GetType(
643
632
'System.Management.Automation.Runspaces.RunspaceBase')
644
633
$availabilityChangedField = $runspaceBase.GetField(
@@ -648,6 +637,18 @@ private async Task RunWithAttachableProcess(string[] logStatements, Func<string,
648
637
throw 'Failed to get AvailabilityChanged event field'
649
638
}
650
639
640
+ $ps = [PowerShell]::Create()
641
+ $runspace = $ps.Runspace
642
+
643
+ # Wait-Debugger is needed in WinPS to sync breakpoints before
644
+ # running the script.
645
+ $null = $ps.AddCommand('Wait-Debugger').AddStatement()
646
+ $null = $ps.AddCommand($TestScript)
647
+
648
+ # Let the runner know what Runspace to attach to and that it
649
+ # is ready to run.
650
+ 'RID: {0}' -f $runspace.Id
651
+
651
652
$start = Get-Date
652
653
while ($true) {
653
654
$subscribed = $availabilityChangedField.GetValue($runspace) |
@@ -661,10 +662,10 @@ private async Task RunWithAttachableProcess(string[] logStatements, Func<string,
661
662
}
662
663
}
663
664
664
- # Needed to sync breakpoints on WinPS 5.1
665
- Wait-Debugger
666
-
667
- & $TestScript
665
+ $ps.Invoke()
666
+ foreach ($e in $ps.Streams.Error) {
667
+ Write-Error -ErrorRecord $e
668
+ }
668
669
669
670
# Keep running until the runner has deleted the test script to
670
671
# ensure the process doesn't finish before the test does in
@@ -674,27 +675,8 @@ private async Task RunWithAttachableProcess(string[] logStatements, Func<string,
674
675
}
675
676
" ;
676
677
677
- // Only way to pass args to -EncodedCommand is to use CLIXML with
678
- // -EncodedArguments. Not pretty but the structure isn't too
679
- // complex.
680
- string clixmlNamespace = "http://schemas.microsoft.com/powershell/2004/04" ;
681
- string clixmlArg = new XDocument (
682
- new XDeclaration ( "1.0" , "utf-16" , "yes" ) ,
683
- new XElement ( XName . Get ( "Objs" , clixmlNamespace ) ,
684
- new XAttribute ( "Version" , "1.1.0.1" ) ,
685
- new XElement ( XName . Get ( "Obj" , clixmlNamespace ) ,
686
- new XAttribute ( "RefId" , "0" ) ,
687
- new XElement ( XName . Get ( "TN" , clixmlNamespace ) ,
688
- new XAttribute ( "RefId" , "0" ) ,
689
- new XElement ( XName . Get ( "T" , clixmlNamespace ) , "System.Collections.ArrayList" ) ,
690
- new XElement ( XName . Get ( "T" , clixmlNamespace ) , "System.Object" )
691
- ) ,
692
- new XElement ( XName . Get ( "LST" , clixmlNamespace ) ,
693
- new XElement ( XName . Get ( "S" , clixmlNamespace ) , startMarker ) ,
694
- new XElement ( XName . Get ( "S" , clixmlNamespace ) , filePath )
695
- )
696
- ) ) ) . ToString ( SaveOptions . DisableFormatting ) ;
697
- string encArgs = Convert . ToBase64String ( Encoding . Unicode . GetBytes ( clixmlArg ) ) ;
678
+ string filePath = NewTestFile ( GenerateLoggingScript ( logStatements ) ) ;
679
+ string encArgs = CreatePwshEncodedArgs ( filePath ) ;
698
680
string encCommand = Convert . ToBase64String ( Encoding . Unicode . GetBytes ( scriptEntrypoint ) ) ;
699
681
700
682
ProcessStartInfo psi = new ProcessStartInfo
@@ -708,15 +690,24 @@ private async Task RunWithAttachableProcess(string[] logStatements, Func<string,
708
690
} ;
709
691
psi . EnvironmentVariables [ "TERM" ] = "dumb" ; // Avoids color/VT sequences in test output.
710
692
693
+ TaskCompletionSource < int > ridOutput = new ( ) ;
694
+
711
695
// Task shouldn't take longer than 30 seconds to complete.
712
696
using CancellationTokenSource debugTaskCts = new CancellationTokenSource ( TimeSpan . FromSeconds ( 30 ) ) ;
697
+ using CancellationTokenRegistration _ = debugTaskCts . Token . Register ( ridOutput . SetCanceled ) ;
713
698
using Process psProc = Process . Start ( psi ) ;
714
699
try
715
700
{
716
701
psProc . OutputDataReceived += ( sender , args ) =>
717
702
{
718
703
if ( ! string . IsNullOrEmpty ( args . Data ) )
719
704
{
705
+ if ( args . Data . StartsWith ( "RID: " ) )
706
+ {
707
+ int rid = int . Parse ( args . Data . Substring ( 5 ) ) ;
708
+ ridOutput . SetResult ( rid ) ;
709
+ }
710
+
720
711
output . WriteLine ( "STDOUT: {0}" , args . Data ) ;
721
712
}
722
713
} ;
@@ -732,24 +723,18 @@ private async Task RunWithAttachableProcess(string[] logStatements, Func<string,
732
723
psProc . BeginErrorReadLine ( ) ;
733
724
734
725
Task procExited = psProc . WaitForExitAsync ( debugTaskCts . Token ) ;
735
- Task waitStart = Task . Run ( async ( ) =>
736
- {
737
- while ( File . Exists ( startMarker ) )
738
- {
739
- await Task . Delay ( 100 , debugTaskCts . Token ) ;
740
- }
741
- } ) ;
726
+ Task < int > waitRid = ridOutput . Task ;
742
727
743
728
// Wait for the process to fail or the script to start.
744
- Task finishedTask = await Task . WhenAny ( waitStart , procExited ) ;
729
+ Task finishedTask = await Task . WhenAny ( waitRid , procExited ) ;
745
730
if ( finishedTask == procExited )
746
731
{
747
732
await procExited ;
748
733
Assert . Fail ( "The attached process exited before the PowerShell entrypoint could start." ) ;
749
734
}
750
- await waitStart ;
735
+ int rid = await waitRid ;
751
736
752
- Task debugTask = action ( filePath , psProc . Id ) ;
737
+ Task debugTask = action ( filePath , psProc . Id , rid ) ;
753
738
finishedTask = await Task . WhenAny ( procExited , debugTask ) ;
754
739
if ( finishedTask == procExited )
755
740
{
@@ -773,5 +758,30 @@ private async Task RunWithAttachableProcess(string[] logStatements, Func<string,
773
758
throw ;
774
759
}
775
760
}
761
+
762
+ private static string CreatePwshEncodedArgs ( params string [ ] args )
763
+ {
764
+ // Only way to pass args to -EncodedCommand is to use CLIXML with
765
+ // -EncodedArguments. Not pretty but the structure isn't too
766
+ // complex and saves us trying to embed/escape strings in a script.
767
+ string clixmlNamespace = "http://schemas.microsoft.com/powershell/2004/04" ;
768
+ string clixml = new XDocument (
769
+ new XDeclaration ( "1.0" , "utf-16" , "yes" ) ,
770
+ new XElement ( XName . Get ( "Objs" , clixmlNamespace ) ,
771
+ new XAttribute ( "Version" , "1.1.0.1" ) ,
772
+ new XElement ( XName . Get ( "Obj" , clixmlNamespace ) ,
773
+ new XAttribute ( "RefId" , "0" ) ,
774
+ new XElement ( XName . Get ( "TN" , clixmlNamespace ) ,
775
+ new XAttribute ( "RefId" , "0" ) ,
776
+ new XElement ( XName . Get ( "T" , clixmlNamespace ) , "System.Collections.ArrayList" ) ,
777
+ new XElement ( XName . Get ( "T" , clixmlNamespace ) , "System.Object" )
778
+ ) ,
779
+ new XElement ( XName . Get ( "LST" , clixmlNamespace ) ,
780
+ args . Select ( s => new XElement ( XName . Get ( "S" , clixmlNamespace ) , s ) )
781
+ )
782
+ ) ) ) . ToString ( SaveOptions . DisableFormatting ) ;
783
+
784
+ return Convert . ToBase64String ( Encoding . Unicode . GetBytes ( clixml ) ) ;
785
+ }
776
786
}
777
787
}
0 commit comments