Skip to content

Commit b1aa091

Browse files
committed
Fix up test for WinPS
1 parent a8cc3b2 commit b1aa091

File tree

2 files changed

+78
-68
lines changed

2 files changed

+78
-68
lines changed

src/PowerShellEditorServices/Services/DebugAdapter/Handlers/LaunchAndAttachHandler.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -427,7 +427,7 @@ await _executionService.ExecutePSCommandAsync(
427427
}
428428
catch (Exception e)
429429
{
430-
string msg = $"Could not attach to process with ID: {processId} - {e.Message}\n{e.StackTrace}";
430+
string msg = $"Could not attach to process with ID: {processId}";
431431
_logger.LogError(e, msg);
432432
throw new RpcErrorException(0, null, msg);
433433
}

test/PowerShellEditorServices.Test.E2E/DebugAdapterProtocolMessageTests.cs

Lines changed: 77 additions & 67 deletions
Original file line numberDiff line numberDiff line change
@@ -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

Comments
 (0)