@@ -532,7 +532,7 @@ public async Task CanAttachScriptWithPathMappings()
532532
533533 string [ ] logStatements = [ "$PSCommandPath" , "after breakpoint" ] ;
534534
535- await RunWithAttachableProcess ( logStatements , async ( filePath , processId ) =>
535+ await RunWithAttachableProcess ( logStatements , async ( filePath , processId , runspaceId ) =>
536536 {
537537 string localParent = Path . Combine ( Path . GetTempPath ( ) , Path . GetRandomFileName ( ) ) ;
538538 string localScriptPath = Path . Combine ( localParent , Path . GetFileName ( filePath ) ) ;
@@ -541,11 +541,11 @@ await RunWithAttachableProcess(logStatements, async (filePath, processId) =>
541541
542542 Task < StoppedEvent > nextStoppedTask = nextStopped ;
543543
544- _ = await client . Attach (
544+ AttachResponse attachResponse = await client . Attach (
545545 new PsesAttachRequestArguments
546546 {
547547 ProcessId = processId ,
548- RunspaceId = 1 ,
548+ RunspaceId = runspaceId ,
549549 PathMappings = [
550550 new ( )
551551 {
@@ -554,6 +554,7 @@ await RunWithAttachableProcess(logStatements, async (filePath, processId) =>
554554 }
555555 ]
556556 } ) ?? throw new Exception ( "Attach response was null." ) ;
557+ Assert . NotNull ( attachResponse ) ;
557558
558559 SetBreakpointsResponse setBreakpointsResponse = await client . SetBreakpoints ( new SetBreakpointsArguments
559560 {
@@ -585,12 +586,7 @@ await RunWithAttachableProcess(logStatements, async (filePath, processId) =>
585586
586587 // Wait until we hit the breakpoint
587588 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 ) ;
594590
595591 // The code before the breakpoint should have already run
596592 // It will contain the actual script being run
@@ -612,33 +608,26 @@ await RunWithAttachableProcess(logStatements, async (filePath, processId) =>
612608 } ) ;
613609 }
614610
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 )
616612 {
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+ */
631627 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)
639629
640630 $debugRunspaceCmd = Get-Command Debug-Runspace -Module Microsoft.PowerShell.Utility
641- $runspace = [Runspace]::DefaultRunspace
642631 $runspaceBase = [PSObject].Assembly.GetType(
643632 'System.Management.Automation.Runspaces.RunspaceBase')
644633 $availabilityChangedField = $runspaceBase.GetField(
@@ -648,6 +637,18 @@ private async Task RunWithAttachableProcess(string[] logStatements, Func<string,
648637 throw 'Failed to get AvailabilityChanged event field'
649638 }
650639
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+
651652 $start = Get-Date
652653 while ($true) {
653654 $subscribed = $availabilityChangedField.GetValue($runspace) |
@@ -661,10 +662,10 @@ private async Task RunWithAttachableProcess(string[] logStatements, Func<string,
661662 }
662663 }
663664
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+ }
668669
669670 # Keep running until the runner has deleted the test script to
670671 # ensure the process doesn't finish before the test does in
@@ -674,27 +675,8 @@ private async Task RunWithAttachableProcess(string[] logStatements, Func<string,
674675 }
675676 " ;
676677
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 ) ;
698680 string encCommand = Convert . ToBase64String ( Encoding . Unicode . GetBytes ( scriptEntrypoint ) ) ;
699681
700682 ProcessStartInfo psi = new ProcessStartInfo
@@ -708,15 +690,24 @@ private async Task RunWithAttachableProcess(string[] logStatements, Func<string,
708690 } ;
709691 psi . EnvironmentVariables [ "TERM" ] = "dumb" ; // Avoids color/VT sequences in test output.
710692
693+ TaskCompletionSource < int > ridOutput = new ( ) ;
694+
711695 // Task shouldn't take longer than 30 seconds to complete.
712696 using CancellationTokenSource debugTaskCts = new CancellationTokenSource ( TimeSpan . FromSeconds ( 30 ) ) ;
697+ using CancellationTokenRegistration _ = debugTaskCts . Token . Register ( ridOutput . SetCanceled ) ;
713698 using Process psProc = Process . Start ( psi ) ;
714699 try
715700 {
716701 psProc . OutputDataReceived += ( sender , args ) =>
717702 {
718703 if ( ! string . IsNullOrEmpty ( args . Data ) )
719704 {
705+ if ( args . Data . StartsWith ( "RID: " ) )
706+ {
707+ int rid = int . Parse ( args . Data . Substring ( 5 ) ) ;
708+ ridOutput . SetResult ( rid ) ;
709+ }
710+
720711 output . WriteLine ( "STDOUT: {0}" , args . Data ) ;
721712 }
722713 } ;
@@ -732,24 +723,18 @@ private async Task RunWithAttachableProcess(string[] logStatements, Func<string,
732723 psProc . BeginErrorReadLine ( ) ;
733724
734725 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 ;
742727
743728 // 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 ) ;
745730 if ( finishedTask == procExited )
746731 {
747732 await procExited ;
748733 Assert . Fail ( "The attached process exited before the PowerShell entrypoint could start." ) ;
749734 }
750- await waitStart ;
735+ int rid = await waitRid ;
751736
752- Task debugTask = action ( filePath , psProc . Id ) ;
737+ Task debugTask = action ( filePath , psProc . Id , rid ) ;
753738 finishedTask = await Task . WhenAny ( procExited , debugTask ) ;
754739 if ( finishedTask == procExited )
755740 {
@@ -773,5 +758,30 @@ private async Task RunWithAttachableProcess(string[] logStatements, Func<string,
773758 throw ;
774759 }
775760 }
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+ }
776786 }
777787}
0 commit comments