Skip to content

Commit 759a88f

Browse files
Fix the TranscribeOnly bug (take two) (#2026)
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. Co-authored-by: Patrick Meinecke <[email protected]>
1 parent 4ff3078 commit 759a88f

File tree

9 files changed

+97
-52
lines changed

9 files changed

+97
-52
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.Hosting/PowerShellEditorServices.Hosting.csproj

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

9-
<PropertyGroup Condition=" '$(TargetFramework)' == 'net6.0' ">
9+
<PropertyGroup Condition=" '$(TargetFramework)' != 'net462' ">
1010
<DefineConstants>$(DefineConstants);CoreCLR</DefineConstants>
1111
</PropertyGroup>
1212

src/PowerShellEditorServices.VSCode/PowerShellEditorServices.VSCode.csproj

+5-1
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,11 @@
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>
10+
</PropertyGroup>
11+
12+
<PropertyGroup Condition=" '$(TargetFramework)' != 'net462' ">
13+
<DefineConstants>$(DefineConstants);CoreCLR</DefineConstants>
1014
</PropertyGroup>
1115

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

src/PowerShellEditorServices/PowerShellEditorServices.csproj

+4
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,10 @@
99
<Configurations>Debug;Release</Configurations>
1010
</PropertyGroup>
1111

12+
<PropertyGroup Condition=" '$(TargetFramework)' != 'net462' ">
13+
<DefineConstants>$(DefineConstants);CoreCLR</DefineConstants>
14+
</PropertyGroup>
15+
1216
<ItemGroup>
1317
<AssemblyAttribute Include="System.Runtime.CompilerServices.InternalsVisibleTo">
1418
<_Parameter1>Microsoft.PowerShell.EditorServices.Hosting</_Parameter1>

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

+8-4
Original file line numberDiff line numberDiff line change
@@ -105,6 +105,12 @@ private IReadOnlyList<TResult> ExecuteNormally(CancellationToken cancellationTok
105105
if (PowerShellExecutionOptions.WriteOutputToHost)
106106
{
107107
_psCommand.AddOutputCommand();
108+
109+
// Fix the transcription bug!
110+
if (!_pwsh.Runspace.RunspaceIsRemote)
111+
{
112+
_psesHost.DisableTranscribeOnly();
113+
}
108114
}
109115

110116
cancellationToken.Register(CancelNormalExecution);
@@ -148,7 +154,7 @@ private IReadOnlyList<TResult> ExecuteNormally(CancellationToken cancellationTok
148154
if (e is PSRemotingTransportException)
149155
{
150156
_ = System.Threading.Tasks.Task.Run(
151-
() => _psesHost.UnwindCallStack(),
157+
_psesHost.UnwindCallStack,
152158
CancellationToken.None)
153159
.HandleErrorsAsync(_logger);
154160

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

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

196200
PSDataCollection<PSObject> outputCollection = new();
@@ -247,7 +251,7 @@ private IReadOnlyList<TResult> ExecuteInDebugger(CancellationToken cancellationT
247251
if (e is PSRemotingTransportException)
248252
{
249253
_ = System.Threading.Tasks.Task.Run(
250-
() => _psesHost.UnwindCallStack(),
254+
_psesHost.UnwindCallStack,
251255
CancellationToken.None)
252256
.HandleErrorsAsync(_logger);
253257

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

+69-13
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,31 @@ internal class PsesInternalHost : PSHost, IHostSupportsInteractiveSession, IRuns
3939

4040
private static readonly PropertyInfo s_scriptDebuggerTriggerObjectProperty;
4141

42+
#if !CoreCLR
43+
/// <summary>
44+
/// To workaround a horrid bug where the `TranscribeOnly` field of the PSHostUserInterface
45+
/// can accidentally remain true, we have to use a bunch of reflection so that <see
46+
/// cref="DisableTranscribeOnly()" /> can reset it to false. (This was fixed in PowerShell
47+
/// 7.) Note that it must be the internal UI instance, not our own UI instance, otherwise
48+
/// this would be easier. Because of the amount of reflection involved, we contain it to
49+
/// only PowerShell 5.1 at compile-time, and we have to set this up in this class, not <see
50+
/// cref="SynchronousPowerShellTask" /> because that's templated, making statics practically
51+
/// useless. <see cref = "SynchronousPowerShellTask.ExecuteNormally" /> method calls <see
52+
/// cref="DisableTranscribeOnly()" /> when necessary.
53+
/// See: https://github.com/PowerShell/PowerShell/pull/3436
54+
/// </summary>
55+
[ThreadStatic] // Because we can re-use it, but only once per instance of PSES.
56+
private static PSHostUserInterface s_internalPSHostUserInterface;
57+
58+
private static readonly Func<PSHostUserInterface, bool> s_getTranscribeOnlyDelegate;
59+
60+
private static readonly Action<PSHostUserInterface, bool> s_setTranscribeOnlyDelegate;
61+
62+
private static readonly PropertyInfo s_executionContextProperty;
63+
64+
private static readonly PropertyInfo s_internalHostProperty;
65+
#endif
66+
4267
private readonly ILoggerFactory _loggerFactory;
4368

4469
private readonly ILogger _logger;
@@ -104,6 +129,27 @@ static PsesInternalHost()
104129
s_scriptDebuggerTriggerObjectProperty = scriptDebuggerType.GetProperty(
105130
"TriggerObject",
106131
BindingFlags.Instance | BindingFlags.NonPublic);
132+
133+
#if !CoreCLR
134+
PropertyInfo transcribeOnlyProperty = typeof(PSHostUserInterface)
135+
.GetProperty("TranscribeOnly", BindingFlags.NonPublic | BindingFlags.Instance);
136+
137+
MethodInfo transcribeOnlyGetMethod = transcribeOnlyProperty.GetGetMethod(nonPublic: true);
138+
139+
s_getTranscribeOnlyDelegate = (Func<PSHostUserInterface, bool>)Delegate.CreateDelegate(
140+
typeof(Func<PSHostUserInterface, bool>), transcribeOnlyGetMethod);
141+
142+
MethodInfo transcribeOnlySetMethod = transcribeOnlyProperty.GetSetMethod(nonPublic: true);
143+
144+
s_setTranscribeOnlyDelegate = (Action<PSHostUserInterface, bool>)Delegate.CreateDelegate(
145+
typeof(Action<PSHostUserInterface, bool>), transcribeOnlySetMethod);
146+
147+
s_executionContextProperty = typeof(System.Management.Automation.Runspaces.Runspace)
148+
.GetProperty("ExecutionContext", BindingFlags.NonPublic | BindingFlags.Instance);
149+
150+
s_internalHostProperty = s_executionContextProperty.PropertyType
151+
.GetProperty("InternalHost", BindingFlags.NonPublic | BindingFlags.Instance);
152+
#endif
107153
}
108154

109155
public PsesInternalHost(
@@ -476,19 +522,7 @@ public void InvokeDelegate(string representation, ExecutionOptions executionOpti
476522
public IReadOnlyList<TResult> InvokePSCommand<TResult>(PSCommand psCommand, PowerShellExecutionOptions executionOptions, CancellationToken cancellationToken)
477523
{
478524
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-
}
525+
return task.ExecuteAndGetResult(cancellationToken);
492526
}
493527

494528
public void InvokePSCommand(PSCommand psCommand, PowerShellExecutionOptions executionOptions, CancellationToken cancellationToken) => InvokePSCommand<PSObject>(psCommand, executionOptions, cancellationToken);
@@ -507,6 +541,28 @@ public void InvokePSDelegate(string representation, ExecutionOptions executionOp
507541

508542
internal void AddToHistory(string historyEntry) => _readLineProvider.ReadLine.AddToHistory(historyEntry);
509543

544+
// This works around a bug in PowerShell 5.1 (that was later fixed) where a running
545+
// transcription could cause output to disappear since the `TranscribeOnly` property was
546+
// accidentally not reset to false.
547+
#pragma warning disable CA1822 // Warning to make it static when it's empty for CoreCLR.
548+
internal void DisableTranscribeOnly()
549+
#pragma warning restore CA1822
550+
{
551+
#if !CoreCLR
552+
// To fix the TranscribeOnly bug, we have to get the internal UI, which involves a lot
553+
// of reflection since we can't always just use PowerShell to execute `$Host.UI`.
554+
s_internalPSHostUserInterface ??=
555+
(s_internalHostProperty.GetValue(
556+
s_executionContextProperty.GetValue(CurrentPowerShell.Runspace))
557+
as PSHost).UI;
558+
559+
if (s_getTranscribeOnlyDelegate(s_internalPSHostUserInterface))
560+
{
561+
s_setTranscribeOnlyDelegate(s_internalPSHostUserInterface, false);
562+
}
563+
#endif
564+
}
565+
510566
internal Task LoadHostProfilesAsync(CancellationToken cancellationToken)
511567
{
512568
// NOTE: This is a special task run on startup!

test/PowerShellEditorServices.Test.E2E/PowerShellEditorServices.Test.E2E.csproj

+4
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,10 @@
66
<IsPackable>false</IsPackable>
77
</PropertyGroup>
88

9+
<PropertyGroup Condition=" '$(TargetFramework)' != 'net462' ">
10+
<DefineConstants>$(DefineConstants);CoreCLR</DefineConstants>
11+
</PropertyGroup>
12+
913
<ItemGroup>
1014
<PackageReference Include="Microsoft.Extensions.Logging" Version="7.0.0" />
1115
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.6.0" />

test/PowerShellEditorServices.Test/PowerShellEditorServices.Test.csproj

+4-4
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,10 @@
77
<TargetPlatform>x64</TargetPlatform>
88
</PropertyGroup>
99

10+
<PropertyGroup Condition=" '$(TargetFramework)' != 'net462' ">
11+
<DefineConstants>$(DefineConstants);CoreCLR</DefineConstants>
12+
</PropertyGroup>
13+
1014
<ItemGroup>
1115
<ProjectReference Include="..\..\src\PowerShellEditorServices\PowerShellEditorServices.csproj" />
1216
<ProjectReference Include="..\PowerShellEditorServices.Test.Shared\PowerShellEditorServices.Test.Shared.csproj" />
@@ -27,10 +31,6 @@
2731
<PackageReference Include="Microsoft.PowerShell.5.ReferenceAssemblies" Version="1.1.0" />
2832
</ItemGroup>
2933

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

0 commit comments

Comments
 (0)