9
9
using System . Text ;
10
10
using System . Threading ;
11
11
using System . Threading . Tasks ;
12
+ using System . Xml . Linq ;
12
13
using Microsoft . PowerShell . EditorServices . Handlers ;
13
14
using Nerdbank . Streams ;
14
15
using OmniSharp . Extensions . DebugAdapter . Client ;
@@ -608,6 +609,13 @@ await RunWithAttachableProcess(logStatements, async (filePath, processId) =>
608
609
609
610
private async Task RunWithAttachableProcess ( string [ ] logStatements , Func < string , int , Task > action )
610
611
{
612
+ // Ensures that we don't try and attach until the PowerShell proc
613
+ // has fully started and is ready to accept the attach request.
614
+ string startMarker = Path . Combine ( Path . GetTempPath ( ) , Path . GetRandomFileName ( ) ) ;
615
+ File . WriteAllText ( startMarker , "" ) ;
616
+
617
+ string filePath = NewTestFile ( GenerateLoggingScript ( logStatements ) ) ;
618
+
611
619
// PowerShell has no public API for waiting when Debug-Runspace
612
620
// has attached itself to a Runspace. We use this reflection
613
621
// hackery to wait until the AvailabilityChanged event is
@@ -616,8 +624,14 @@ private async Task RunWithAttachableProcess(string[] logStatements, Func<string,
616
624
// Use https://github.com/PowerShell/PowerShell/pull/25788 once it
617
625
// is available.
618
626
string scriptEntrypoint = @"
627
+ param([string]$StartMarker, [string]$TestScript)
628
+
619
629
$ErrorActionPreference = 'Stop'
620
630
631
+ # Removing this file tells the runner that the script has
632
+ # started and pwsh is ready to accept the attach request.
633
+ Remove-Item -LiteralPath $StartMarker -Force
634
+
621
635
$debugRunspaceCmd = Get-Command Debug-Runspace -Module Microsoft.PowerShell.Utility
622
636
$runspace = [Runspace]::DefaultRunspace
623
637
$runspaceBase = [PSObject].Assembly.GetType(
@@ -644,22 +658,44 @@ private async Task RunWithAttachableProcess(string[] logStatements, Func<string,
644
658
645
659
# Needed to sync breakpoints on WinPS 5.1
646
660
Wait-Debugger
647
- " ;
648
661
649
- string filePath = NewTestFile ( GenerateLoggingScript ( logStatements ) ) ;
650
- scriptEntrypoint += Environment . NewLine + $ "& '{ filePath } '";
662
+ & $TestScript
651
663
652
- // This allows us to ensure the process won't end after the script
653
- // has run. Without this we run the risk that the process ends
654
- // before the debug task completes causing a test failure.
655
- scriptEntrypoint += Environment . NewLine + $ "while (Test-Path -Path '{ filePath } ') {{ Start-Sleep -Seconds 1 }}";
664
+ # Keep running until the runner has deleted the test script to
665
+ # ensure the process doesn't finish before the test does in
666
+ # normal circumstances.
667
+ while (Test-Path -Path $TestScript) {
668
+ Start-Sleep -Seconds 1
669
+ }
670
+ " ;
656
671
672
+ // Only way to pass args to -EncodedCommand is to use CLIXML with
673
+ // -EncodedArguments. Not pretty but the structure isn't too
674
+ // complex.
675
+ string clixmlNamespace = "http://schemas.microsoft.com/powershell/2004/04" ;
676
+ string clixmlArg = new XDocument (
677
+ new XDeclaration ( "1.0" , "utf-16" , "yes" ) ,
678
+ new XElement ( XName . Get ( "Objs" , clixmlNamespace ) ,
679
+ new XAttribute ( "Version" , "1.1.0.1" ) ,
680
+ new XElement ( XName . Get ( "Obj" , clixmlNamespace ) ,
681
+ new XAttribute ( "RefId" , "0" ) ,
682
+ new XElement ( XName . Get ( "TN" , clixmlNamespace ) ,
683
+ new XAttribute ( "RefId" , "0" ) ,
684
+ new XElement ( XName . Get ( "T" , clixmlNamespace ) , "System.Collections.ArrayList" ) ,
685
+ new XElement ( XName . Get ( "T" , clixmlNamespace ) , "System.Object" )
686
+ ) ,
687
+ new XElement ( XName . Get ( "LST" , clixmlNamespace ) ,
688
+ new XElement ( XName . Get ( "S" , clixmlNamespace ) , startMarker ) ,
689
+ new XElement ( XName . Get ( "S" , clixmlNamespace ) , filePath )
690
+ )
691
+ ) ) ) . ToString ( SaveOptions . DisableFormatting ) ;
692
+ string encArgs = Convert . ToBase64String ( Encoding . Unicode . GetBytes ( clixmlArg ) ) ;
657
693
string encCommand = Convert . ToBase64String ( Encoding . Unicode . GetBytes ( scriptEntrypoint ) ) ;
658
694
659
695
ProcessStartInfo psi = new ProcessStartInfo
660
696
{
661
697
FileName = PsesStdioLanguageServerProcessHost . PwshExe ,
662
- Arguments = $ "-NoLogo -NoProfile -EncodedCommand { encCommand } ",
698
+ Arguments = $ "-NoLogo -NoProfile -EncodedCommand { encCommand } -EncodedArguments { encArgs } ",
663
699
RedirectStandardOutput = true ,
664
700
RedirectStandardError = true ,
665
701
UseShellExecute = false ,
@@ -691,13 +727,29 @@ private async Task RunWithAttachableProcess(string[] logStatements, Func<string,
691
727
psProc . BeginErrorReadLine ( ) ;
692
728
693
729
Task procExited = psProc . WaitForExitAsync ( debugTaskCts . Token ) ;
694
- Task debugTask = action ( filePath , psProc . Id ) ;
730
+ Task waitStart = Task . Run ( async ( ) =>
731
+ {
732
+ while ( File . Exists ( startMarker ) )
733
+ {
734
+ await Task . Delay ( 100 , debugTaskCts . Token ) ;
735
+ }
736
+ } ) ;
737
+
738
+ // Wait for the process to fail or the script to start.
739
+ Task finishedTask = await Task . WhenAny ( waitStart , procExited ) ;
740
+ if ( finishedTask == procExited )
741
+ {
742
+ await procExited ;
743
+ Assert . Fail ( "The attached process exited before the PowerShell entrypoint could start." ) ;
744
+ }
745
+ await waitStart ;
695
746
696
- Task finishedTask = await Task . WhenAny ( procExited , debugTask ) ;
747
+ Task debugTask = action ( filePath , psProc . Id ) ;
748
+ finishedTask = await Task . WhenAny ( procExited , debugTask ) ;
697
749
if ( finishedTask == procExited )
698
750
{
699
751
await procExited ;
700
- Assert . Fail ( "Attached process exited before the debug task completed ." ) ;
752
+ Assert . Fail ( "Attached process exited before the script could start ." ) ;
701
753
}
702
754
703
755
await debugTask ;
0 commit comments