diff --git a/src/PowerShellEditorServices.Hosting/EditorServicesLoader.cs b/src/PowerShellEditorServices.Hosting/EditorServicesLoader.cs index d5772bb59..5a7166eef 100644 --- a/src/PowerShellEditorServices.Hosting/EditorServicesLoader.cs +++ b/src/PowerShellEditorServices.Hosting/EditorServicesLoader.cs @@ -350,7 +350,9 @@ private void LogHostInformation() private static string GetPSOutputEncoding() { using SMA.PowerShell pwsh = SMA.PowerShell.Create(); - return pwsh.AddScript("$OutputEncoding.EncodingName", useLocalScope: true).Invoke()[0]; + return pwsh.AddScript( + "[System.Diagnostics.DebuggerHidden()]param() $OutputEncoding.EncodingName", + useLocalScope: true).Invoke()[0]; } // TODO: Deduplicate this with VersionUtils. diff --git a/src/PowerShellEditorServices/Services/DebugAdapter/DebugService.cs b/src/PowerShellEditorServices/Services/DebugAdapter/DebugService.cs index a92f05eed..0439f4484 100644 --- a/src/PowerShellEditorServices/Services/DebugAdapter/DebugService.cs +++ b/src/PowerShellEditorServices/Services/DebugAdapter/DebugService.cs @@ -367,7 +367,7 @@ public async Task SetVariableAsync(int variableContainerReferenceId, str // Evaluate the expression to get back a PowerShell object from the expression string. // This may throw, in which case the exception is propagated to the caller - PSCommand evaluateExpressionCommand = new PSCommand().AddScript(value); + PSCommand evaluateExpressionCommand = new PSCommand().AddScript($"[System.Diagnostics.DebuggerHidden()]param() {value}"); IReadOnlyList expressionResults = await _executionService.ExecutePSCommandAsync(evaluateExpressionCommand, CancellationToken.None).ConfigureAwait(false); if (expressionResults.Count == 0) { @@ -500,7 +500,7 @@ public async Task EvaluateExpressionAsync( bool writeResultAsOutput, CancellationToken cancellationToken) { - PSCommand command = new PSCommand().AddScript(expressionString); + PSCommand command = new PSCommand().AddScript($"[System.Diagnostics.DebuggerHidden()]param() {expressionString}"); IReadOnlyList results; try { @@ -799,7 +799,7 @@ private async Task FetchStackFramesAsync(string scriptNameOverride) // PSObject is used here instead of the specific type because we get deserialized // objects from remote sessions and want a common interface. - PSCommand psCommand = new PSCommand().AddScript($"[Collections.ArrayList]{callStackVarName} = @(); {getPSCallStack}; {returnSerializedIfInRemoteRunspace}"); + PSCommand psCommand = new PSCommand().AddScript($"[System.Diagnostics.DebuggerHidden()]param() [Collections.ArrayList]{callStackVarName} = @(); {getPSCallStack}; {returnSerializedIfInRemoteRunspace}"); IReadOnlyList results = await _executionService.ExecutePSCommandAsync(psCommand, CancellationToken.None).ConfigureAwait(false); IEnumerable callStack = isRemoteRunspace diff --git a/src/PowerShellEditorServices/Services/DebugAdapter/Handlers/DebugEvaluateHandler.cs b/src/PowerShellEditorServices/Services/DebugAdapter/Handlers/DebugEvaluateHandler.cs index 2c6c47364..cd91809fd 100644 --- a/src/PowerShellEditorServices/Services/DebugAdapter/Handlers/DebugEvaluateHandler.cs +++ b/src/PowerShellEditorServices/Services/DebugAdapter/Handlers/DebugEvaluateHandler.cs @@ -48,7 +48,7 @@ public async Task Handle(EvaluateRequestArguments request, if (isFromRepl) { await _executionService.ExecutePSCommandAsync( - new PSCommand().AddScript(request.Expression), + new PSCommand().AddScript($"[System.Diagnostics.DebuggerHidden()]param() {request.Expression}"), cancellationToken, new PowerShellExecutionOptions { WriteOutputToHost = true, ThrowOnError = false, AddToHistory = true }).HandleErrorsAsync(_logger).ConfigureAwait(false); } diff --git a/src/PowerShellEditorServices/Services/PowerShell/Context/PowerShellVersionDetails.cs b/src/PowerShellEditorServices/Services/PowerShell/Context/PowerShellVersionDetails.cs index ec5b8711f..d236f2327 100644 --- a/src/PowerShellEditorServices/Services/PowerShell/Context/PowerShellVersionDetails.cs +++ b/src/PowerShellEditorServices/Services/PowerShell/Context/PowerShellVersionDetails.cs @@ -55,7 +55,7 @@ public static PowerShellVersionDetails GetVersionDetails(ILogger logger, PowerSh try { Hashtable psVersionTable = pwsh - .AddScript("$PSVersionTable", useLocalScope: true) + .AddScript("[System.Diagnostics.DebuggerHidden()]param() $PSVersionTable", useLocalScope: true) .InvokeAndClear() .FirstOrDefault(); diff --git a/src/PowerShellEditorServices/Services/PowerShell/Handlers/ExpandAliasHandler.cs b/src/PowerShellEditorServices/Services/PowerShell/Handlers/ExpandAliasHandler.cs index 69c39450d..8390bbdf5 100644 --- a/src/PowerShellEditorServices/Services/PowerShell/Handlers/ExpandAliasHandler.cs +++ b/src/PowerShellEditorServices/Services/PowerShell/Handlers/ExpandAliasHandler.cs @@ -33,7 +33,7 @@ public async Task Handle(ExpandAliasParams request, Cancellat { const string script = @" function __Expand-Alias { - + [System.Diagnostics.DebuggerHidden()] param($targetScript) [ref]$errors=$null diff --git a/src/PowerShellEditorServices/Services/PowerShell/Handlers/ShowHelpHandler.cs b/src/PowerShellEditorServices/Services/PowerShell/Handlers/ShowHelpHandler.cs index ea18de73c..43fcdfca2 100644 --- a/src/PowerShellEditorServices/Services/PowerShell/Handlers/ShowHelpHandler.cs +++ b/src/PowerShellEditorServices/Services/PowerShell/Handlers/ShowHelpHandler.cs @@ -27,7 +27,9 @@ internal class ShowHelpHandler : IShowHelpHandler public async Task Handle(ShowHelpParams request, CancellationToken cancellationToken) { + // TODO: Refactor to not rerun the function definition every time. const string CheckHelpScript = @" + [System.Diagnostics.DebuggerHidden()] [CmdletBinding()] param ( [String]$CommandName diff --git a/src/PowerShellEditorServices/Services/PowerShell/Host/EditorServicesConsolePSHostUserInterface.cs b/src/PowerShellEditorServices/Services/PowerShell/Host/EditorServicesConsolePSHostUserInterface.cs index fb6d2c861..badcaef6b 100644 --- a/src/PowerShellEditorServices/Services/PowerShell/Host/EditorServicesConsolePSHostUserInterface.cs +++ b/src/PowerShellEditorServices/Services/PowerShell/Host/EditorServicesConsolePSHostUserInterface.cs @@ -7,8 +7,10 @@ using System.Collections.ObjectModel; using System.Management.Automation; using System.Management.Automation.Host; +using System.Reflection; using System.Security; using Microsoft.Extensions.Logging; +using Microsoft.PowerShell.EditorServices.Utility; namespace Microsoft.PowerShell.EditorServices.Services.PowerShell.Host { @@ -16,12 +18,28 @@ internal class EditorServicesConsolePSHostUserInterface : PSHostUserInterface, I { private readonly PSHostUserInterface _underlyingHostUI; + private static readonly Action s_setTranscribeOnlyDelegate; + /// /// We use a ConcurrentDictionary because ConcurrentHashSet does not exist, hence the value /// is never actually used, and `WriteProgress` must be thread-safe. /// private readonly ConcurrentDictionary<(long, int), object> _currentProgressRecords = new(); + static EditorServicesConsolePSHostUserInterface() + { + if (VersionUtils.IsPS5) + { + PropertyInfo transcribeOnlyProperty = typeof(PSHostUserInterface) + .GetProperty("TranscribeOnly", BindingFlags.NonPublic | BindingFlags.Instance); + + MethodInfo transcribeOnlySetMethod = transcribeOnlyProperty.GetSetMethod(nonPublic: true); + + s_setTranscribeOnlyDelegate = (Action)Delegate.CreateDelegate( + typeof(Action), transcribeOnlySetMethod); + } + } + public EditorServicesConsolePSHostUserInterface( ILoggerFactory loggerFactory, PSHostUserInterface underlyingHostUI) @@ -70,7 +88,7 @@ public override void WriteProgress(long sourceId, ProgressRecord record) _underlyingHostUI.WriteProgress(sourceId, record); } - public void ResetProgress() + internal void ResetProgress() { // Mark all processed progress records as completed. foreach ((long sourceId, int activityId) in _currentProgressRecords.Keys) @@ -87,6 +105,17 @@ public void ResetProgress() // TODO: Maybe send the OSC sequence to turn off progress indicator. } + // This works around a bug in PowerShell 5.1 (that was later fixed) where a running + // transcription could cause output to disappear since the `TranscribeOnly` property was + // accidentally not reset to false. + internal void DisableTranscribeOnly() + { + if (VersionUtils.IsPS5) + { + s_setTranscribeOnlyDelegate(_underlyingHostUI, false); + } + } + public override void WriteVerboseLine(string message) => _underlyingHostUI.WriteVerboseLine(message); public override void WriteWarningLine(string message) => _underlyingHostUI.WriteWarningLine(message); diff --git a/src/PowerShellEditorServices/Services/PowerShell/Host/PsesInternalHost.cs b/src/PowerShellEditorServices/Services/PowerShell/Host/PsesInternalHost.cs index 419aebf8c..32a728291 100644 --- a/src/PowerShellEditorServices/Services/PowerShell/Host/PsesInternalHost.cs +++ b/src/PowerShellEditorServices/Services/PowerShell/Host/PsesInternalHost.cs @@ -35,6 +35,8 @@ internal class PsesInternalHost : PSHost, IHostSupportsInteractiveSession, IRuns { internal const string DefaultPrompt = "> "; + private static readonly PSCommand s_promptCommand = new PSCommand().AddCommand("prompt"); + private static readonly PropertyInfo s_scriptDebuggerTriggerObjectProperty; private readonly ILoggerFactory _loggerFactory; @@ -474,7 +476,19 @@ public void InvokeDelegate(string representation, ExecutionOptions executionOpti public IReadOnlyList InvokePSCommand(PSCommand psCommand, PowerShellExecutionOptions executionOptions, CancellationToken cancellationToken) { SynchronousPowerShellTask task = new(_logger, this, psCommand, executionOptions, cancellationToken); - return task.ExecuteAndGetResult(cancellationToken); + try + { + return task.ExecuteAndGetResult(cancellationToken); + } + finally + { + // At the end of each PowerShell command we need to reset PowerShell 5.1's + // `TranscribeOnly` property to avoid a bug where output disappears. + if (UI is EditorServicesConsolePSHostUserInterface ui) + { + ui.DisableTranscribeOnly(); + } + } } public void InvokePSCommand(PSCommand psCommand, PowerShellExecutionOptions executionOptions, CancellationToken cancellationToken) => InvokePSCommand(psCommand, executionOptions, cancellationToken); @@ -1026,10 +1040,8 @@ internal string GetPrompt(CancellationToken cancellationToken) string prompt = DefaultPrompt; try { - // TODO: Should we cache PSCommands like this as static members? - PSCommand command = new PSCommand().AddCommand("prompt"); IReadOnlyList results = InvokePSCommand( - command, + s_promptCommand, executionOptions: new PowerShellExecutionOptions { ThrowOnError = false }, cancellationToken); @@ -1207,7 +1219,18 @@ private Runspace CreateInitialRunspace(InitialSessionState initialSessionState) return runspace; } - // NOTE: This token is received from PSReadLine, and it _is_ the ReadKey cancellation token! + /// + /// This delegate is handed to PSReadLine and overrides similar logic within its `ReadKey` + /// method. Essentially we're replacing PowerShell's `OnIdle` handler since the PowerShell + /// engine isn't idle when we're sitting in PSReadLine's `ReadKey` loop. In our case we also + /// use this idle time to process queued tasks by executing those that can run in the + /// background, and canceling the foreground task if a queued tasks requires the foreground. + /// Finally, if and only if we have to, we run an artificial pipeline to force PowerShell's + /// own event processing. + /// + /// + /// This token is received from PSReadLine, and it is the ReadKey cancellation token! + /// internal void OnPowerShellIdle(CancellationToken idleCancellationToken) { IReadOnlyList eventSubscribers = _mainRunspaceEngineIntrinsics.Events.Subscribers; @@ -1250,17 +1273,27 @@ internal void OnPowerShellIdle(CancellationToken idleCancellationToken) // If we're executing a PowerShell task, we don't need to run an extra pipeline // later for events. - runPipelineForEventProcessing = task is not ISynchronousPowerShellTask; + if (task is ISynchronousPowerShellTask) + { + // We don't ever want to set this to true here, just skip if it had + // previously been set true. + runPipelineForEventProcessing = false; + } ExecuteTaskSynchronously(task, cancellationScope.CancellationToken); } } // We didn't end up executing anything in the background, // so we need to run a small artificial pipeline instead - // to force event processing + // to force event processing. if (runPipelineForEventProcessing) { - InvokePSCommand(new PSCommand().AddScript("0", useLocalScope: true), executionOptions: null, CancellationToken.None); + InvokePSCommand( + new PSCommand().AddScript( + "[System.Diagnostics.DebuggerHidden()]param() 0", + useLocalScope: true), + executionOptions: null, + CancellationToken.None); } } diff --git a/src/PowerShellEditorServices/Services/PowerShell/Runspace/SessionDetails.cs b/src/PowerShellEditorServices/Services/PowerShell/Runspace/SessionDetails.cs index 4eeaf165c..53be32e13 100644 --- a/src/PowerShellEditorServices/Services/PowerShell/Runspace/SessionDetails.cs +++ b/src/PowerShellEditorServices/Services/PowerShell/Runspace/SessionDetails.cs @@ -27,7 +27,7 @@ public static SessionDetails GetFromPowerShell(PowerShell pwsh) { Hashtable detailsObject = pwsh .AddScript( - $"@{{ '{Property_ComputerName}' = if ([Environment]::MachineName) {{[Environment]::MachineName}} else {{'localhost'}}; '{Property_ProcessId}' = $PID; '{Property_InstanceId}' = $host.InstanceId }}", + $"[System.Diagnostics.DebuggerHidden()]param() @{{ '{Property_ComputerName}' = if ([Environment]::MachineName) {{[Environment]::MachineName}} else {{'localhost'}}; '{Property_ProcessId}' = $PID; '{Property_InstanceId}' = $host.InstanceId }}", useLocalScope: true) .InvokeAndClear() .FirstOrDefault(); diff --git a/src/PowerShellEditorServices/Services/Symbols/SymbolsService.cs b/src/PowerShellEditorServices/Services/Symbols/SymbolsService.cs index 457b1d651..99f8bbbc8 100644 --- a/src/PowerShellEditorServices/Services/Symbols/SymbolsService.cs +++ b/src/PowerShellEditorServices/Services/Symbols/SymbolsService.cs @@ -137,11 +137,6 @@ public IEnumerable FindSymbolsInFile(ScriptFile scriptFile) // asserting we should use a giant nested ternary. private static string[] GetIdentifiers(string symbolName, SymbolType symbolType, CommandHelpers.AliasMap aliases) { - if (symbolType is not SymbolType.Function) - { - return new[] { symbolName }; - } - if (!aliases.CmdletToAliases.TryGetValue(symbolName, out List foundAliasList)) { return new[] { symbolName }; @@ -165,22 +160,31 @@ public async Task> ScanForReferencesOfSymbolAsync( return Enumerable.Empty(); } - // TODO: Should we handle aliases at a lower level? - CommandHelpers.AliasMap aliases = await CommandHelpers.GetAliasesAsync( - _executionService, - cancellationToken).ConfigureAwait(false); + // We want to handle aliases for functions, but we only want to do the work of getting + // the aliases when we must. We can't cache the alias list on first run else we won't + // support newly defined aliases. + string[] allIdentifiers; + if (symbol.Type is SymbolType.Function) + { + CommandHelpers.AliasMap aliases = await CommandHelpers.GetAliasesAsync( + _executionService, + cancellationToken).ConfigureAwait(false); - string targetName = symbol.Id; - if (symbol.Type is SymbolType.Function - && aliases.AliasToCmdlets.TryGetValue(symbol.Id, out string aliasDefinition)) + string targetName = symbol.Id; + if (aliases.AliasToCmdlets.TryGetValue(symbol.Id, out string aliasDefinition)) + { + targetName = aliasDefinition; + } + allIdentifiers = GetIdentifiers(targetName, symbol.Type, aliases); + } + else { - targetName = aliasDefinition; + allIdentifiers = new[] { symbol.Id }; } await ScanWorkspacePSFiles(cancellationToken).ConfigureAwait(false); List symbols = new(); - string[] allIdentifiers = GetIdentifiers(targetName, symbol.Type, aliases); foreach (ScriptFile file in _workspaceService.GetOpenedFiles()) {