Skip to content

Commit 7ba4620

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. Whole lot of reflection. Also had to fix our `CoreCLR` compiler constant.
1 parent 253422a commit 7ba4620

File tree

8 files changed

+91
-55
lines changed

8 files changed

+91
-55
lines changed

Diff for: .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

Diff for: PowerShellEditorServices.Common.props

+3
Original file line numberDiff line numberDiff line change
@@ -18,4 +18,7 @@
1818
<GenerateDocumentationFile>true</GenerateDocumentationFile>
1919
<!-- TODO: Enable <AnalysisMode>All</AnalysisMode> -->
2020
</PropertyGroup>
21+
<PropertyGroup Condition=" '$(TargetFramework)' != 'net462' ">
22+
<DefineConstants>$(DefineConstants);CoreCLR</DefineConstants>
23+
</PropertyGroup>
2124
</Project>

Diff for: src/PowerShellEditorServices.Hosting/PowerShellEditorServices.Hosting.csproj

-4
Original file line numberDiff line numberDiff line change
@@ -6,10 +6,6 @@
66
<AssemblyName>Microsoft.PowerShell.EditorServices.Hosting</AssemblyName>
77
</PropertyGroup>
88

9-
<PropertyGroup Condition=" '$(TargetFramework)' == 'net6.0' ">
10-
<DefineConstants>$(DefineConstants);CoreCLR</DefineConstants>
11-
</PropertyGroup>
12-
139
<ItemGroup>
1410
<PackageReference Include="NETStandard.Library" Version="2.0.3" />
1511
<PackageReference Include="PowerShellStandard.Library" Version="5.1.1" PrivateAssets="all" />

Diff for: src/PowerShellEditorServices.VSCode/PowerShellEditorServices.VSCode.csproj

+1-1
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
<Description>Provides added functionality to PowerShell Editor Services for the Visual Studio Code editor.</Description>
77
<TargetFrameworks>netstandard2.0</TargetFrameworks>
88
<AssemblyName>Microsoft.PowerShell.EditorServices.VSCode</AssemblyName>
9-
<Configurations>Debug;Release;CoreCLR</Configurations>
9+
<Configurations>Debug;Release</Configurations>
1010
</PropertyGroup>
1111

1212
<!-- Fail the release build if there are missing public API documentation comments -->

Diff for: src/PowerShellEditorServices/Services/PowerShell/Execution/SynchronousPowerShellTask.cs

+84-4
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,11 @@
1414
using Microsoft.PowerShell.EditorServices.Utility;
1515
using SMA = System.Management.Automation;
1616

17+
#if !CoreCLR
18+
using System.Management.Automation.Host;
19+
using System.Reflection;
20+
#endif
21+
1722
namespace Microsoft.PowerShell.EditorServices.Services.PowerShell.Execution
1823
{
1924
internal interface ISynchronousPowerShellTask
@@ -27,6 +32,55 @@ internal class SynchronousPowerShellTask<TResult> : SynchronousTask<IReadOnlyLis
2732
{
2833
private static readonly PowerShellExecutionOptions s_defaultPowerShellExecutionOptions = new();
2934

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

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

110179
cancellationToken.Register(CancelNormalExecution);
@@ -148,7 +217,7 @@ private IReadOnlyList<TResult> ExecuteNormally(CancellationToken cancellationTok
148217
if (e is PSRemotingTransportException)
149218
{
150219
_ = System.Threading.Tasks.Task.Run(
151-
() => _psesHost.UnwindCallStack(),
220+
_psesHost.UnwindCallStack,
152221
CancellationToken.None)
153222
.HandleErrorsAsync(_logger);
154223

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

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

196263
PSDataCollection<PSObject> outputCollection = new();
@@ -247,7 +314,7 @@ private IReadOnlyList<TResult> ExecuteInDebugger(CancellationToken cancellationT
247314
if (e is PSRemotingTransportException)
248315
{
249316
_ = System.Threading.Tasks.Task.Run(
250-
() => _psesHost.UnwindCallStack(),
317+
_psesHost.UnwindCallStack,
251318
CancellationToken.None)
252319
.HandleErrorsAsync(_logger);
253320

@@ -396,5 +463,18 @@ private void CancelDebugExecution()
396463

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

Diff for: 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);

Diff for: 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);

Diff for: test/PowerShellEditorServices.Test/PowerShellEditorServices.Test.csproj

-4
Original file line numberDiff line numberDiff line change
@@ -27,10 +27,6 @@
2727
<PackageReference Include="Microsoft.PowerShell.5.ReferenceAssemblies" Version="1.1.0" />
2828
</ItemGroup>
2929

30-
<PropertyGroup Condition=" '$(TargetFramework)' != 'net462' ">
31-
<DefineConstants>$(DefineConstants);CoreCLR</DefineConstants>
32-
</PropertyGroup>
33-
3430
<ItemGroup>
3531
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.5.0" />
3632
<PackageReference Include="xunit" Version="2.4.2" />

0 commit comments

Comments
 (0)