99using System . Text ;
1010using System . Threading ;
1111using System . Threading . Tasks ;
12+ using System . Xml . Linq ;
1213using Microsoft . PowerShell . EditorServices . Handlers ;
1314using Nerdbank . Streams ;
1415using OmniSharp . Extensions . DebugAdapter . Client ;
@@ -608,6 +609,13 @@ await RunWithAttachableProcess(logStatements, async (filePath, processId) =>
608609
609610 private async Task RunWithAttachableProcess ( string [ ] logStatements , Func < string , int , Task > action )
610611 {
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+
611619 // PowerShell has no public API for waiting when Debug-Runspace
612620 // has attached itself to a Runspace. We use this reflection
613621 // hackery to wait until the AvailabilityChanged event is
@@ -616,8 +624,14 @@ private async Task RunWithAttachableProcess(string[] logStatements, Func<string,
616624 // Use https://github.com/PowerShell/PowerShell/pull/25788 once it
617625 // is available.
618626 string scriptEntrypoint = @"
627+ param([string]$StartMarker, [string]$TestScript)
628+
619629 $ErrorActionPreference = 'Stop'
620630
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+
621635 $debugRunspaceCmd = Get-Command Debug-Runspace -Module Microsoft.PowerShell.Utility
622636 $runspace = [Runspace]::DefaultRunspace
623637 $runspaceBase = [PSObject].Assembly.GetType(
@@ -644,22 +658,44 @@ private async Task RunWithAttachableProcess(string[] logStatements, Func<string,
644658
645659 # Needed to sync breakpoints on WinPS 5.1
646660 Wait-Debugger
647- " ;
648661
649- string filePath = NewTestFile ( GenerateLoggingScript ( logStatements ) ) ;
650- scriptEntrypoint += Environment . NewLine + $ "& '{ filePath } '";
662+ & $TestScript
651663
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+ " ;
656671
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 ) ) ;
657693 string encCommand = Convert . ToBase64String ( Encoding . Unicode . GetBytes ( scriptEntrypoint ) ) ;
658694
659695 ProcessStartInfo psi = new ProcessStartInfo
660696 {
661697 FileName = PsesStdioLanguageServerProcessHost . PwshExe ,
662- Arguments = $ "-NoLogo -NoProfile -EncodedCommand { encCommand } ",
698+ Arguments = $ "-NoLogo -NoProfile -EncodedCommand { encCommand } -EncodedArguments { encArgs } ",
663699 RedirectStandardOutput = true ,
664700 RedirectStandardError = true ,
665701 UseShellExecute = false ,
@@ -691,13 +727,29 @@ private async Task RunWithAttachableProcess(string[] logStatements, Func<string,
691727 psProc . BeginErrorReadLine ( ) ;
692728
693729 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 ;
695746
696- Task finishedTask = await Task . WhenAny ( procExited , debugTask ) ;
747+ Task debugTask = action ( filePath , psProc . Id ) ;
748+ finishedTask = await Task . WhenAny ( procExited , debugTask ) ;
697749 if ( finishedTask == procExited )
698750 {
699751 await procExited ;
700- Assert . Fail ( "Attached process exited before the debug task completed ." ) ;
752+ Assert . Fail ( "Attached process exited before the script could start ." ) ;
701753 }
702754
703755 await debugTask ;
0 commit comments