Skip to content

Commit 3d8f00c

Browse files
committed
Fix the TranscribeOnly bug (take two)
We were using our own UI, not the byzantine internal UI where it actually needed to be fixed.
1 parent 253422a commit 3d8f00c

File tree

4 files changed

+84
-46
lines changed

4 files changed

+84
-46
lines changed

.editorconfig

+2
Original file line numberDiff line numberDiff line change
@@ -205,6 +205,8 @@ dotnet_diagnostic.IDE0052.severity = error
205205
dotnet_diagnostic.IDE0053.severity = error
206206
# IDE0054: Use compound assignment
207207
dotnet_diagnostic.IDE0054.severity = error
208+
# IDE0059: Unnecessary assignment of a value
209+
dotnet_diagnostic.IDE0059.severity = error
208210
# IDE0063: Use simple 'using' statement
209211
dotnet_diagnostic.IDE0063.severity = error
210212
# IDE0066: Use switch expression

src/PowerShellEditorServices/Services/PowerShell/Execution/SynchronousPowerShellTask.cs

+81-4
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,9 @@
55
using System.Collections.Generic;
66
using System.Collections.ObjectModel;
77
using System.Management.Automation;
8+
using System.Management.Automation.Host;
89
using System.Management.Automation.Remoting;
10+
using System.Reflection;
911
using System.Threading;
1012
using Microsoft.Extensions.Logging;
1113
using Microsoft.PowerShell.EditorServices.Services.PowerShell.Context;
@@ -27,6 +29,55 @@ internal class SynchronousPowerShellTask<TResult> : SynchronousTask<IReadOnlyLis
2729
{
2830
private static readonly PowerShellExecutionOptions s_defaultPowerShellExecutionOptions = new();
2931

32+
#if !CoreCLR
33+
/// <summary>
34+
/// To workaround a bug where the `TranscribeOnly` field of the PSHostUserInterface can
35+
/// accidentally remain true, we have to use a PowerShell pipeline to get the internal
36+
/// instance (saved here) and reflection to get delegates for the field's getter and setter
37+
/// methods, and then reset it to false (see <see cref="ExecuteNormally"/>). Note that it
38+
/// must be the internal instance, not our own UI instance.
39+
/// See https://github.com/PowerShell/PowerShell/pull/3436
40+
/// </summary>
41+
[ThreadStatic] // Because we can re-use it, but only for each PowerShell.
42+
private static PSHostUserInterface s_internalPSHostUserInterface;
43+
44+
private static readonly Func<PSHostUserInterface, bool> s_getTranscribeOnlyDelegate;
45+
46+
private static readonly Action<PSHostUserInterface, bool> s_setTranscribeOnlyDelegate;
47+
48+
private static readonly PropertyInfo s_executionContextProperty;
49+
50+
private static readonly PropertyInfo s_internalHostProperty;
51+
52+
private static readonly PropertyInfo s_internalHostUIProperty;
53+
54+
static SynchronousPowerShellTask()
55+
{
56+
PropertyInfo transcribeOnlyProperty = typeof(PSHostUserInterface)
57+
.GetProperty("TranscribeOnly", BindingFlags.NonPublic | BindingFlags.Instance);
58+
59+
MethodInfo transcribeOnlyGetMethod = transcribeOnlyProperty.GetGetMethod(nonPublic: true);
60+
61+
s_getTranscribeOnlyDelegate = (Func<PSHostUserInterface, bool>)Delegate.CreateDelegate(
62+
typeof(Func<PSHostUserInterface, bool>), transcribeOnlyGetMethod);
63+
64+
MethodInfo transcribeOnlySetMethod = transcribeOnlyProperty.GetSetMethod(nonPublic: true);
65+
66+
s_setTranscribeOnlyDelegate = (Action<PSHostUserInterface, bool>)Delegate.CreateDelegate(
67+
typeof(Action<PSHostUserInterface, bool>), transcribeOnlySetMethod);
68+
69+
s_executionContextProperty = typeof(SMA.Runspaces.Runspace)
70+
.GetProperty("ExecutionContext", BindingFlags.NonPublic | BindingFlags.Instance);
71+
72+
s_internalHostProperty = s_executionContextProperty.PropertyType
73+
.GetProperty("InternalHost", BindingFlags.NonPublic | BindingFlags.Instance);
74+
75+
// It's public but we want the override and reflection confuses me.
76+
s_internalHostUIProperty = s_internalHostProperty.PropertyType
77+
.GetProperty("UI", BindingFlags.Public | BindingFlags.Instance);
78+
}
79+
#endif
80+
3081
private readonly ILogger _logger;
3182

3283
private readonly PsesInternalHost _psesHost;
@@ -105,6 +156,21 @@ private IReadOnlyList<TResult> ExecuteNormally(CancellationToken cancellationTok
105156
if (PowerShellExecutionOptions.WriteOutputToHost)
106157
{
107158
_psCommand.AddOutputCommand();
159+
#if !CoreCLR
160+
// To fix the TranscribeOnly bug, we have to get the internal UI, which involves a
161+
// lot of reflection since we can't always just use PowerShell to execute `$Host.UI`
162+
// (we tried). With that internal UI we can reset its `TranscribeOnly` flag and so
163+
// avoid the disappearing output that happens when a transcription is running.
164+
if (!_pwsh.Runspace.RunspaceIsRemote)
165+
{
166+
s_internalPSHostUserInterface ??=
167+
s_internalHostUIProperty.GetValue(
168+
s_internalHostProperty.GetValue(
169+
s_executionContextProperty.GetValue(_pwsh.Runspace)))
170+
as PSHostUserInterface;
171+
DisableTranscribeOnly();
172+
}
173+
#endif
108174
}
109175

110176
cancellationToken.Register(CancelNormalExecution);
@@ -148,7 +214,7 @@ private IReadOnlyList<TResult> ExecuteNormally(CancellationToken cancellationTok
148214
if (e is PSRemotingTransportException)
149215
{
150216
_ = System.Threading.Tasks.Task.Run(
151-
() => _psesHost.UnwindCallStack(),
217+
_psesHost.UnwindCallStack,
152218
CancellationToken.None)
153219
.HandleErrorsAsync(_logger);
154220

@@ -189,8 +255,6 @@ private IReadOnlyList<TResult> ExecuteNormally(CancellationToken cancellationTok
189255

190256
private IReadOnlyList<TResult> ExecuteInDebugger(CancellationToken cancellationToken)
191257
{
192-
// TODO: How much of this method can we remove now that it only processes PowerShell's
193-
// intrinsic debugger commands?
194258
cancellationToken.Register(CancelDebugExecution);
195259

196260
PSDataCollection<PSObject> outputCollection = new();
@@ -247,7 +311,7 @@ private IReadOnlyList<TResult> ExecuteInDebugger(CancellationToken cancellationT
247311
if (e is PSRemotingTransportException)
248312
{
249313
_ = System.Threading.Tasks.Task.Run(
250-
() => _psesHost.UnwindCallStack(),
314+
_psesHost.UnwindCallStack,
251315
CancellationToken.None)
252316
.HandleErrorsAsync(_logger);
253317

@@ -396,5 +460,18 @@ private void CancelDebugExecution()
396460

397461
_pwsh.Runspace.Debugger.StopProcessCommand();
398462
}
463+
464+
#if !CoreCLR
465+
// This works around a bug in PowerShell 5.1 (that was later fixed) where a running
466+
// transcription could cause output to disappear since the `TranscribeOnly` property was
467+
// accidentally not reset to false.
468+
private static void DisableTranscribeOnly()
469+
{
470+
if (s_getTranscribeOnlyDelegate(s_internalPSHostUserInterface))
471+
{
472+
s_setTranscribeOnlyDelegate(s_internalPSHostUserInterface, false);
473+
}
474+
}
475+
#endif
399476
}
400477
}

src/PowerShellEditorServices/Services/PowerShell/Host/EditorServicesConsolePSHostUserInterface.cs

-29
Original file line numberDiff line numberDiff line change
@@ -7,39 +7,21 @@
77
using System.Collections.ObjectModel;
88
using System.Management.Automation;
99
using System.Management.Automation.Host;
10-
using System.Reflection;
1110
using System.Security;
1211
using Microsoft.Extensions.Logging;
13-
using Microsoft.PowerShell.EditorServices.Utility;
1412

1513
namespace Microsoft.PowerShell.EditorServices.Services.PowerShell.Host
1614
{
1715
internal class EditorServicesConsolePSHostUserInterface : PSHostUserInterface, IHostUISupportsMultipleChoiceSelection
1816
{
1917
private readonly PSHostUserInterface _underlyingHostUI;
2018

21-
private static readonly Action<PSHostUserInterface, bool> s_setTranscribeOnlyDelegate;
22-
2319
/// <summary>
2420
/// We use a ConcurrentDictionary because ConcurrentHashSet does not exist, hence the value
2521
/// is never actually used, and `WriteProgress` must be thread-safe.
2622
/// </summary>
2723
private readonly ConcurrentDictionary<(long, int), object> _currentProgressRecords = new();
2824

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-
4325
public EditorServicesConsolePSHostUserInterface(
4426
ILoggerFactory loggerFactory,
4527
PSHostUserInterface underlyingHostUI)
@@ -105,17 +87,6 @@ internal void ResetProgress()
10587
// TODO: Maybe send the OSC sequence to turn off progress indicator.
10688
}
10789

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-
11990
public override void WriteVerboseLine(string message) => _underlyingHostUI.WriteVerboseLine(message);
12091

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

src/PowerShellEditorServices/Services/PowerShell/Host/PsesInternalHost.cs

+1-13
Original file line numberDiff line numberDiff line change
@@ -476,19 +476,7 @@ public void InvokeDelegate(string representation, ExecutionOptions executionOpti
476476
public IReadOnlyList<TResult> InvokePSCommand<TResult>(PSCommand psCommand, PowerShellExecutionOptions executionOptions, CancellationToken cancellationToken)
477477
{
478478
SynchronousPowerShellTask<TResult> task = new(_logger, this, psCommand, executionOptions, 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-
}
479+
return task.ExecuteAndGetResult(cancellationToken);
492480
}
493481

494482
public void InvokePSCommand(PSCommand psCommand, PowerShellExecutionOptions executionOptions, CancellationToken cancellationToken) => InvokePSCommand<PSObject>(psCommand, executionOptions, cancellationToken);

0 commit comments

Comments
 (0)