Skip to content

Commit 14f2be8

Browse files
Merge pull request #2023 from PowerShell/andschwa/fix-output-bug
Fix disappearing output in PowerShell 5.1
2 parents 3401a2d + 9d2a151 commit 14f2be8

File tree

10 files changed

+101
-31
lines changed

10 files changed

+101
-31
lines changed

Diff for: src/PowerShellEditorServices.Hosting/EditorServicesLoader.cs

+3-1
Original file line numberDiff line numberDiff line change
@@ -350,7 +350,9 @@ private void LogHostInformation()
350350
private static string GetPSOutputEncoding()
351351
{
352352
using SMA.PowerShell pwsh = SMA.PowerShell.Create();
353-
return pwsh.AddScript("$OutputEncoding.EncodingName", useLocalScope: true).Invoke<string>()[0];
353+
return pwsh.AddScript(
354+
"[System.Diagnostics.DebuggerHidden()]param() $OutputEncoding.EncodingName",
355+
useLocalScope: true).Invoke<string>()[0];
354356
}
355357

356358
// TODO: Deduplicate this with VersionUtils.

Diff for: src/PowerShellEditorServices/Services/DebugAdapter/DebugService.cs

+3-3
Original file line numberDiff line numberDiff line change
@@ -367,7 +367,7 @@ public async Task<string> SetVariableAsync(int variableContainerReferenceId, str
367367

368368
// Evaluate the expression to get back a PowerShell object from the expression string.
369369
// This may throw, in which case the exception is propagated to the caller
370-
PSCommand evaluateExpressionCommand = new PSCommand().AddScript(value);
370+
PSCommand evaluateExpressionCommand = new PSCommand().AddScript($"[System.Diagnostics.DebuggerHidden()]param() {value}");
371371
IReadOnlyList<object> expressionResults = await _executionService.ExecutePSCommandAsync<object>(evaluateExpressionCommand, CancellationToken.None).ConfigureAwait(false);
372372
if (expressionResults.Count == 0)
373373
{
@@ -500,7 +500,7 @@ public async Task<VariableDetails> EvaluateExpressionAsync(
500500
bool writeResultAsOutput,
501501
CancellationToken cancellationToken)
502502
{
503-
PSCommand command = new PSCommand().AddScript(expressionString);
503+
PSCommand command = new PSCommand().AddScript($"[System.Diagnostics.DebuggerHidden()]param() {expressionString}");
504504
IReadOnlyList<PSObject> results;
505505
try
506506
{
@@ -799,7 +799,7 @@ private async Task FetchStackFramesAsync(string scriptNameOverride)
799799

800800
// PSObject is used here instead of the specific type because we get deserialized
801801
// objects from remote sessions and want a common interface.
802-
PSCommand psCommand = new PSCommand().AddScript($"[Collections.ArrayList]{callStackVarName} = @(); {getPSCallStack}; {returnSerializedIfInRemoteRunspace}");
802+
PSCommand psCommand = new PSCommand().AddScript($"[System.Diagnostics.DebuggerHidden()]param() [Collections.ArrayList]{callStackVarName} = @(); {getPSCallStack}; {returnSerializedIfInRemoteRunspace}");
803803
IReadOnlyList<PSObject> results = await _executionService.ExecutePSCommandAsync<PSObject>(psCommand, CancellationToken.None).ConfigureAwait(false);
804804

805805
IEnumerable callStack = isRemoteRunspace

Diff for: src/PowerShellEditorServices/Services/DebugAdapter/Handlers/DebugEvaluateHandler.cs

+1-1
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,7 @@ public async Task<EvaluateResponseBody> Handle(EvaluateRequestArguments request,
4848
if (isFromRepl)
4949
{
5050
await _executionService.ExecutePSCommandAsync(
51-
new PSCommand().AddScript(request.Expression),
51+
new PSCommand().AddScript($"[System.Diagnostics.DebuggerHidden()]param() {request.Expression}"),
5252
cancellationToken,
5353
new PowerShellExecutionOptions { WriteOutputToHost = true, ThrowOnError = false, AddToHistory = true }).HandleErrorsAsync(_logger).ConfigureAwait(false);
5454
}

Diff for: src/PowerShellEditorServices/Services/PowerShell/Context/PowerShellVersionDetails.cs

+1-1
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,7 @@ public static PowerShellVersionDetails GetVersionDetails(ILogger logger, PowerSh
5555
try
5656
{
5757
Hashtable psVersionTable = pwsh
58-
.AddScript("$PSVersionTable", useLocalScope: true)
58+
.AddScript("[System.Diagnostics.DebuggerHidden()]param() $PSVersionTable", useLocalScope: true)
5959
.InvokeAndClear<Hashtable>()
6060
.FirstOrDefault();
6161

Diff for: src/PowerShellEditorServices/Services/PowerShell/Handlers/ExpandAliasHandler.cs

+1-1
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ public async Task<ExpandAliasResult> Handle(ExpandAliasParams request, Cancellat
3333
{
3434
const string script = @"
3535
function __Expand-Alias {
36-
36+
[System.Diagnostics.DebuggerHidden()]
3737
param($targetScript)
3838
3939
[ref]$errors=$null

Diff for: src/PowerShellEditorServices/Services/PowerShell/Handlers/ShowHelpHandler.cs

+2
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,9 @@ internal class ShowHelpHandler : IShowHelpHandler
2727

2828
public async Task<Unit> Handle(ShowHelpParams request, CancellationToken cancellationToken)
2929
{
30+
// TODO: Refactor to not rerun the function definition every time.
3031
const string CheckHelpScript = @"
32+
[System.Diagnostics.DebuggerHidden()]
3133
[CmdletBinding()]
3234
param (
3335
[String]$CommandName

Diff for: src/PowerShellEditorServices/Services/PowerShell/Host/EditorServicesConsolePSHostUserInterface.cs

+30-1
Original file line numberDiff line numberDiff line change
@@ -7,21 +7,39 @@
77
using System.Collections.ObjectModel;
88
using System.Management.Automation;
99
using System.Management.Automation.Host;
10+
using System.Reflection;
1011
using System.Security;
1112
using Microsoft.Extensions.Logging;
13+
using Microsoft.PowerShell.EditorServices.Utility;
1214

1315
namespace Microsoft.PowerShell.EditorServices.Services.PowerShell.Host
1416
{
1517
internal class EditorServicesConsolePSHostUserInterface : PSHostUserInterface, IHostUISupportsMultipleChoiceSelection
1618
{
1719
private readonly PSHostUserInterface _underlyingHostUI;
1820

21+
private static readonly Action<PSHostUserInterface, bool> s_setTranscribeOnlyDelegate;
22+
1923
/// <summary>
2024
/// We use a ConcurrentDictionary because ConcurrentHashSet does not exist, hence the value
2125
/// is never actually used, and `WriteProgress` must be thread-safe.
2226
/// </summary>
2327
private readonly ConcurrentDictionary<(long, int), object> _currentProgressRecords = new();
2428

29+
static EditorServicesConsolePSHostUserInterface()
30+
{
31+
if (VersionUtils.IsPS5)
32+
{
33+
PropertyInfo transcribeOnlyProperty = typeof(PSHostUserInterface)
34+
.GetProperty("TranscribeOnly", BindingFlags.NonPublic | BindingFlags.Instance);
35+
36+
MethodInfo transcribeOnlySetMethod = transcribeOnlyProperty.GetSetMethod(nonPublic: true);
37+
38+
s_setTranscribeOnlyDelegate = (Action<PSHostUserInterface, bool>)Delegate.CreateDelegate(
39+
typeof(Action<PSHostUserInterface, bool>), transcribeOnlySetMethod);
40+
}
41+
}
42+
2543
public EditorServicesConsolePSHostUserInterface(
2644
ILoggerFactory loggerFactory,
2745
PSHostUserInterface underlyingHostUI)
@@ -70,7 +88,7 @@ public override void WriteProgress(long sourceId, ProgressRecord record)
7088
_underlyingHostUI.WriteProgress(sourceId, record);
7189
}
7290

73-
public void ResetProgress()
91+
internal void ResetProgress()
7492
{
7593
// Mark all processed progress records as completed.
7694
foreach ((long sourceId, int activityId) in _currentProgressRecords.Keys)
@@ -87,6 +105,17 @@ public void ResetProgress()
87105
// TODO: Maybe send the OSC sequence to turn off progress indicator.
88106
}
89107

108+
// This works around a bug in PowerShell 5.1 (that was later fixed) where a running
109+
// transcription could cause output to disappear since the `TranscribeOnly` property was
110+
// accidentally not reset to false.
111+
internal void DisableTranscribeOnly()
112+
{
113+
if (VersionUtils.IsPS5)
114+
{
115+
s_setTranscribeOnlyDelegate(_underlyingHostUI, false);
116+
}
117+
}
118+
90119
public override void WriteVerboseLine(string message) => _underlyingHostUI.WriteVerboseLine(message);
91120

92121
public override void WriteWarningLine(string message) => _underlyingHostUI.WriteWarningLine(message);

Diff for: src/PowerShellEditorServices/Services/PowerShell/Host/PsesInternalHost.cs

+41-8
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,8 @@ internal class PsesInternalHost : PSHost, IHostSupportsInteractiveSession, IRuns
3535
{
3636
internal const string DefaultPrompt = "> ";
3737

38+
private static readonly PSCommand s_promptCommand = new PSCommand().AddCommand("prompt");
39+
3840
private static readonly PropertyInfo s_scriptDebuggerTriggerObjectProperty;
3941

4042
private readonly ILoggerFactory _loggerFactory;
@@ -474,7 +476,19 @@ public void InvokeDelegate(string representation, ExecutionOptions executionOpti
474476
public IReadOnlyList<TResult> InvokePSCommand<TResult>(PSCommand psCommand, PowerShellExecutionOptions executionOptions, CancellationToken cancellationToken)
475477
{
476478
SynchronousPowerShellTask<TResult> task = new(_logger, this, psCommand, executionOptions, cancellationToken);
477-
return task.ExecuteAndGetResult(cancellationToken);
479+
try
480+
{
481+
return task.ExecuteAndGetResult(cancellationToken);
482+
}
483+
finally
484+
{
485+
// At the end of each PowerShell command we need to reset PowerShell 5.1's
486+
// `TranscribeOnly` property to avoid a bug where output disappears.
487+
if (UI is EditorServicesConsolePSHostUserInterface ui)
488+
{
489+
ui.DisableTranscribeOnly();
490+
}
491+
}
478492
}
479493

480494
public void InvokePSCommand(PSCommand psCommand, PowerShellExecutionOptions executionOptions, CancellationToken cancellationToken) => InvokePSCommand<PSObject>(psCommand, executionOptions, cancellationToken);
@@ -1026,10 +1040,8 @@ internal string GetPrompt(CancellationToken cancellationToken)
10261040
string prompt = DefaultPrompt;
10271041
try
10281042
{
1029-
// TODO: Should we cache PSCommands like this as static members?
1030-
PSCommand command = new PSCommand().AddCommand("prompt");
10311043
IReadOnlyList<string> results = InvokePSCommand<string>(
1032-
command,
1044+
s_promptCommand,
10331045
executionOptions: new PowerShellExecutionOptions { ThrowOnError = false },
10341046
cancellationToken);
10351047

@@ -1207,7 +1219,18 @@ private Runspace CreateInitialRunspace(InitialSessionState initialSessionState)
12071219
return runspace;
12081220
}
12091221

1210-
// NOTE: This token is received from PSReadLine, and it _is_ the ReadKey cancellation token!
1222+
/// <summary>
1223+
/// This delegate is handed to PSReadLine and overrides similar logic within its `ReadKey`
1224+
/// method. Essentially we're replacing PowerShell's `OnIdle` handler since the PowerShell
1225+
/// engine isn't idle when we're sitting in PSReadLine's `ReadKey` loop. In our case we also
1226+
/// use this idle time to process queued tasks by executing those that can run in the
1227+
/// background, and canceling the foreground task if a queued tasks requires the foreground.
1228+
/// Finally, if and only if we have to, we run an artificial pipeline to force PowerShell's
1229+
/// own event processing.
1230+
/// </summary>
1231+
/// <param name="idleCancellationToken">
1232+
/// This token is received from PSReadLine, and it is the ReadKey cancellation token!
1233+
/// </param>
12111234
internal void OnPowerShellIdle(CancellationToken idleCancellationToken)
12121235
{
12131236
IReadOnlyList<PSEventSubscriber> eventSubscribers = _mainRunspaceEngineIntrinsics.Events.Subscribers;
@@ -1250,17 +1273,27 @@ internal void OnPowerShellIdle(CancellationToken idleCancellationToken)
12501273

12511274
// If we're executing a PowerShell task, we don't need to run an extra pipeline
12521275
// later for events.
1253-
runPipelineForEventProcessing = task is not ISynchronousPowerShellTask;
1276+
if (task is ISynchronousPowerShellTask)
1277+
{
1278+
// We don't ever want to set this to true here, just skip if it had
1279+
// previously been set true.
1280+
runPipelineForEventProcessing = false;
1281+
}
12541282
ExecuteTaskSynchronously(task, cancellationScope.CancellationToken);
12551283
}
12561284
}
12571285

12581286
// We didn't end up executing anything in the background,
12591287
// so we need to run a small artificial pipeline instead
1260-
// to force event processing
1288+
// to force event processing.
12611289
if (runPipelineForEventProcessing)
12621290
{
1263-
InvokePSCommand(new PSCommand().AddScript("0", useLocalScope: true), executionOptions: null, CancellationToken.None);
1291+
InvokePSCommand(
1292+
new PSCommand().AddScript(
1293+
"[System.Diagnostics.DebuggerHidden()]param() 0",
1294+
useLocalScope: true),
1295+
executionOptions: null,
1296+
CancellationToken.None);
12641297
}
12651298
}
12661299

Diff for: src/PowerShellEditorServices/Services/PowerShell/Runspace/SessionDetails.cs

+1-1
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ public static SessionDetails GetFromPowerShell(PowerShell pwsh)
2727
{
2828
Hashtable detailsObject = pwsh
2929
.AddScript(
30-
$"@{{ '{Property_ComputerName}' = if ([Environment]::MachineName) {{[Environment]::MachineName}} else {{'localhost'}}; '{Property_ProcessId}' = $PID; '{Property_InstanceId}' = $host.InstanceId }}",
30+
$"[System.Diagnostics.DebuggerHidden()]param() @{{ '{Property_ComputerName}' = if ([Environment]::MachineName) {{[Environment]::MachineName}} else {{'localhost'}}; '{Property_ProcessId}' = $PID; '{Property_InstanceId}' = $host.InstanceId }}",
3131
useLocalScope: true)
3232
.InvokeAndClear<Hashtable>()
3333
.FirstOrDefault();

Diff for: src/PowerShellEditorServices/Services/Symbols/SymbolsService.cs

+18-14
Original file line numberDiff line numberDiff line change
@@ -137,11 +137,6 @@ public IEnumerable<SymbolReference> FindSymbolsInFile(ScriptFile scriptFile)
137137
// asserting we should use a giant nested ternary.
138138
private static string[] GetIdentifiers(string symbolName, SymbolType symbolType, CommandHelpers.AliasMap aliases)
139139
{
140-
if (symbolType is not SymbolType.Function)
141-
{
142-
return new[] { symbolName };
143-
}
144-
145140
if (!aliases.CmdletToAliases.TryGetValue(symbolName, out List<string> foundAliasList))
146141
{
147142
return new[] { symbolName };
@@ -165,22 +160,31 @@ public async Task<IEnumerable<SymbolReference>> ScanForReferencesOfSymbolAsync(
165160
return Enumerable.Empty<SymbolReference>();
166161
}
167162

168-
// TODO: Should we handle aliases at a lower level?
169-
CommandHelpers.AliasMap aliases = await CommandHelpers.GetAliasesAsync(
170-
_executionService,
171-
cancellationToken).ConfigureAwait(false);
163+
// We want to handle aliases for functions, but we only want to do the work of getting
164+
// the aliases when we must. We can't cache the alias list on first run else we won't
165+
// support newly defined aliases.
166+
string[] allIdentifiers;
167+
if (symbol.Type is SymbolType.Function)
168+
{
169+
CommandHelpers.AliasMap aliases = await CommandHelpers.GetAliasesAsync(
170+
_executionService,
171+
cancellationToken).ConfigureAwait(false);
172172

173-
string targetName = symbol.Id;
174-
if (symbol.Type is SymbolType.Function
175-
&& aliases.AliasToCmdlets.TryGetValue(symbol.Id, out string aliasDefinition))
173+
string targetName = symbol.Id;
174+
if (aliases.AliasToCmdlets.TryGetValue(symbol.Id, out string aliasDefinition))
175+
{
176+
targetName = aliasDefinition;
177+
}
178+
allIdentifiers = GetIdentifiers(targetName, symbol.Type, aliases);
179+
}
180+
else
176181
{
177-
targetName = aliasDefinition;
182+
allIdentifiers = new[] { symbol.Id };
178183
}
179184

180185
await ScanWorkspacePSFiles(cancellationToken).ConfigureAwait(false);
181186

182187
List<SymbolReference> symbols = new();
183-
string[] allIdentifiers = GetIdentifiers(targetName, symbol.Type, aliases);
184188

185189
foreach (ScriptFile file in _workspaceService.GetOpenedFiles())
186190
{

0 commit comments

Comments
 (0)