From a19bfcf0cd9ef772b9e26812484c53a09da70a73 Mon Sep 17 00:00:00 2001 From: Tyler Leonhardt Date: Wed, 19 Jun 2019 18:09:10 -0700 Subject: [PATCH 1/6] initial runspacesynchronizer --- .../Language/AstOperations.cs | 103 ++++--- .../Language/RunspaceSychronizer.cs | 281 ++++++++++++++++++ 2 files changed, 344 insertions(+), 40 deletions(-) create mode 100644 src/PowerShellEditorServices/Language/RunspaceSychronizer.cs diff --git a/src/PowerShellEditorServices/Language/AstOperations.cs b/src/PowerShellEditorServices/Language/AstOperations.cs index 354e5f188..c0290fd7c 100644 --- a/src/PowerShellEditorServices/Language/AstOperations.cs +++ b/src/PowerShellEditorServices/Language/AstOperations.cs @@ -32,6 +32,8 @@ internal static class AstOperations private static readonly SemaphoreSlim s_completionHandle = AsyncUtils.CreateSimpleLockingSemaphore(); + private static PowerShell pwsh = PowerShell.Create(); + /// /// Gets completions for the symbol found in the Ast at /// the given file offset. @@ -69,6 +71,11 @@ static public async Task GetCompletionsAsync( return null; } + if (!RunspaceSynchronizer.IsReadyForEvents) + { + RunspaceSynchronizer.InitializeRunspaces(powerShellContext.CurrentRunspace.Runspace, pwsh.Runspace); + } + try { IScriptPosition cursorPosition = (IScriptPosition)s_extentCloneWithNewOffset.Invoke( @@ -90,49 +97,65 @@ static public async Task GetCompletionsAsync( var stopwatch = new Stopwatch(); + stopwatch.Start(); + + try + { + return CommandCompletion.CompleteInput( + scriptAst, + currentTokens, + cursorPosition, + options: null, + powershell: pwsh); + } + finally + { + stopwatch.Stop(); + logger.Write(LogLevel.Verbose, $"IntelliSense completed in {stopwatch.ElapsedMilliseconds}ms."); + } // If the current runspace is out of process we can use // CommandCompletion.CompleteInput because PSReadLine won't be taking up the // main runspace. - if (powerShellContext.IsCurrentRunspaceOutOfProcess()) - { - using (RunspaceHandle runspaceHandle = await powerShellContext.GetRunspaceHandleAsync(cancellationToken)) - using (PowerShell powerShell = PowerShell.Create()) - { - powerShell.Runspace = runspaceHandle.Runspace; - stopwatch.Start(); - try - { - return CommandCompletion.CompleteInput( - scriptAst, - currentTokens, - cursorPosition, - options: null, - powershell: powerShell); - } - finally - { - stopwatch.Stop(); - logger.Write(LogLevel.Verbose, $"IntelliSense completed in {stopwatch.ElapsedMilliseconds}ms."); - } - } - } - - CommandCompletion commandCompletion = null; - await powerShellContext.InvokeOnPipelineThreadAsync( - pwsh => - { - stopwatch.Start(); - commandCompletion = CommandCompletion.CompleteInput( - scriptAst, - currentTokens, - cursorPosition, - options: null, - powershell: pwsh); - }); - stopwatch.Stop(); - logger.Write(LogLevel.Verbose, $"IntelliSense completed in {stopwatch.ElapsedMilliseconds}ms."); - - return commandCompletion; + // if (powerShellContext.IsCurrentRunspaceOutOfProcess()) + // { + // using (RunspaceHandle runspaceHandle = await powerShellContext.GetRunspaceHandleAsync(cancellationToken)) + // using (PowerShell powerShell = PowerShell.Create()) + // { + // powerShell.Runspace = runspaceHandle.Runspace; + // stopwatch.Start(); + // try + // { + // return CommandCompletion.CompleteInput( + // scriptAst, + // currentTokens, + // cursorPosition, + // options: null, + // powershell: powerShell); + // } + // finally + // { + // stopwatch.Stop(); + // logger.Write(LogLevel.Verbose, $"IntelliSense completed in {stopwatch.ElapsedMilliseconds}ms."); + // } + // } + // } + + // CommandCompletion commandCompletion = null; + // await powerShellContext.InvokeOnPipelineThreadAsync( + // pwsh => + // { + // stopwatch.Start(); + // commandCompletion = CommandCompletion.CompleteInput( + // scriptAst, + // currentTokens, + // cursorPosition, + // options: null, + // powershell: pwsh); + // }); + // stopwatch.Stop(); + // logger.Write(LogLevel.Verbose, $"IntelliSense completed in {stopwatch.ElapsedMilliseconds}ms."); + + // return commandCompletion; } finally { diff --git a/src/PowerShellEditorServices/Language/RunspaceSychronizer.cs b/src/PowerShellEditorServices/Language/RunspaceSychronizer.cs new file mode 100644 index 000000000..fdd64d265 --- /dev/null +++ b/src/PowerShellEditorServices/Language/RunspaceSychronizer.cs @@ -0,0 +1,281 @@ +// +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +// + +using System; +using System.Collections.Generic; +using System.Collections.ObjectModel; +using System.Management.Automation.Runspaces; +using System.Reflection; + +namespace Microsoft.PowerShell.EditorServices +{ + using System.Management.Automation; + /// + /// Does a thing + /// + public class RunspaceSynchronizer + { + /// + /// Does a thing + /// + private static bool SourceActionEnabled = false; + + // 'moduleCache' keeps track of all modules imported in the source Runspace. + // when there is a `Import-Module -Force`, the new module object would be a + // different instance with different hashcode, so we can tell if there is a + // force loading of an already loaded module. + private static HashSet moduleCache = new HashSet(); + + // 'variableCache' keeps all global scope variable names and their value type. + // As long as the value type doesn't change, we don't need to update the variable + // in the target Runspace, because all tab completion needs is the type information. + private static Dictionary variableCache = new Dictionary(StringComparer.OrdinalIgnoreCase); + + private static List moduleToImport = new List(); + private static List variablesToSet = new List(); + + private static Runspace sourceRunspace; + private static Runspace targetRunspace; + private static EngineIntrinsics sourceEngineIntrinsics; + private static EngineIntrinsics targetEngineIntrinsics; + + private static object syncObj = new object(); + + /// + /// Does a thing + /// + public static bool IsReadyForEvents { get; private set; } + + private static void HandleRunspaceStateChange(object sender, PSEventArgs args) + { + if (!SourceActionEnabled) + { + return; + } + + SourceActionEnabled = false; + + try + { + // Maybe also track the latest history item id ($h = Get-History -Count 1; $h.Id) + // to make sure we do the collection only if there was actually any input. + + var newOrChangedModules = new List(); + List modules = ReflectionUtils.GetModules(sourceRunspace); + foreach (PSModuleInfo module in modules) + { + if (moduleCache.Add(module)) + { + newOrChangedModules.Add(module); + } + } + + + var newOrChangedVars = new List(); + + var variables = sourceEngineIntrinsics.GetVariables(); + foreach (var variable in variables) + { + // TODO: first filter out the built-in variables. + if(!variableCache.TryGetValue(variable.Name, out Type value) || value != variable.Value?.GetType()) + { + variableCache[variable.Name] = variable.Value?.GetType(); + + newOrChangedVars.Add(variable); + } + } + + if (newOrChangedModules.Count == 0 && newOrChangedVars.Count == 0) + { + return; + } + + lock (syncObj) + { + moduleToImport.AddRange(newOrChangedModules); + variablesToSet.AddRange(newOrChangedVars); + } + + // Enable the action in target Runspace + UpdateTargetRunspaceState(); + } catch (Exception ex) { + System.Console.WriteLine(ex.Message); + System.Console.WriteLine(ex.StackTrace); + } + } + + private static void UpdateTargetRunspaceState() + { + List newOrChangedModules; + List newOrChangedVars; + + lock (syncObj) + { + newOrChangedModules = new List(moduleToImport); + newOrChangedVars = new List(variablesToSet); + + moduleToImport.Clear(); + variablesToSet.Clear(); + } + + if (newOrChangedModules.Count > 0) + { + // Import the modules with -Force + using (PowerShell pwsh = PowerShell.Create()) + { + pwsh.Runspace = targetRunspace; + + foreach (PSModuleInfo moduleInfo in newOrChangedModules) + { + if(moduleInfo.Path != null) + { + pwsh.AddCommand("Import-Module") + .AddParameter("Name", moduleInfo.Path) + .AddParameter("Force") + .AddStatement(); + } + } + + pwsh.Invoke(); + } + } + + if (newOrChangedVars.Count > 0) + { + // Set or update the variables. + foreach (PSVariable variable in newOrChangedVars) + { + targetEngineIntrinsics.SetVariable(variable); + } + } + } + + /// + /// Does a thing + /// + public static void InitializeRunspaces(Runspace runspaceSource, Runspace runspaceTarget) + { + sourceRunspace = runspaceSource; + sourceEngineIntrinsics = ReflectionUtils.GetEngineIntrinsics(sourceRunspace); + IsReadyForEvents = true; + + targetRunspace = runspaceTarget; + targetEngineIntrinsics = ReflectionUtils.GetEngineIntrinsics(runspaceTarget); + + if(sourceEngineIntrinsics != null) + { + sourceEngineIntrinsics.Events.SubscribeEvent( + source: null, + eventName: null, + sourceIdentifier: PSEngineEvent.OnIdle.ToString(), + data: null, + handlerDelegate: HandleRunspaceStateChange, + supportEvent: true, + forwardEvent: false); + } + + Activate(); + // Trigger events + HandleRunspaceStateChange(sender: null, args: null); + } + + /// + /// Does a thing + /// + public static void Activate() + { + SourceActionEnabled = true; + } + + internal class ReflectionUtils + { + private static BindingFlags bindingFlags = BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.Default; + + internal static List GetModules(Runspace runspace) + { + var executionContext = typeof(Runspace) + .GetProperty("ExecutionContext", bindingFlags) + .GetValue(runspace); + var ModuleIntrinsics = executionContext.GetType() + .GetProperty("Modules", bindingFlags) + .GetValue(executionContext); + var modules = ModuleIntrinsics.GetType() + .GetMethod("GetModules", bindingFlags, null, new Type[] { typeof(string[]), typeof(bool) }, null) + .Invoke(ModuleIntrinsics, new object[] { new string[] { "*" }, false }) as List; + return modules; + } + + internal static EngineIntrinsics GetEngineIntrinsics(Runspace runspace) + { + var executionContext = typeof(Runspace) + .GetProperty("ExecutionContext", bindingFlags) + .GetValue(runspace); + var engineIntrinsics = executionContext.GetType() + .GetProperty("EngineIntrinsics", bindingFlags) + .GetValue(executionContext) as EngineIntrinsics; + return engineIntrinsics; + } + } + } + + internal static class EngineIntrinsicsExtensions + { + internal static List GetVariables(this EngineIntrinsics engineIntrinsics) + { + List variables = new List(); + foreach (PSObject psobject in engineIntrinsics.GetItems(ItemType.Variable)) + { + var variable = (PSVariable) psobject.BaseObject; + variables.Add(variable); + } + return variables; + } + + internal static void SetVariable(this EngineIntrinsics engineIntrinsics, PSVariable variable) + { + engineIntrinsics.SetItem(ItemType.Variable, variable.Name, variable.Value); + } + + private static Collection GetItems(this EngineIntrinsics engineIntrinsics, ItemType itemType) + { + for (int i = 0; i < 3; i++) + { + try + { + return engineIntrinsics.InvokeProvider.Item.Get($@"{itemType.ToString()}:\*"); + } + catch(Exception) + { + // InvokeProvider.Item.Get is not threadsafe so let's try a couple times + // to get results from it. + } + } + return new Collection(); + } + + private static void SetItem(this EngineIntrinsics engineIntrinsics, ItemType itemType, string name, object value) + { + for (int i = 0; i < 3; i++) + { + try + { + engineIntrinsics.InvokeProvider.Item.Set($@"{itemType}:\{name}", value); + return; + } + catch (Exception) + { + // InvokeProvider.Item.Set is not threadsafe so let's try a couple times to set. + } + } + } + + private enum ItemType + { + Variable, + Function, + Alias + } + } +} From 35ed20b62bc320f01a37baea11bc936e6787e4bf Mon Sep 17 00:00:00 2001 From: Tyler Leonhardt Date: Thu, 20 Jun 2019 15:24:40 -0700 Subject: [PATCH 2/6] refactor and hook up setting to the key handler --- .../Language/AstOperations.cs | 112 +++++----- ...Sychronizer.cs => RunspaceSynchronizer.cs} | 203 +++++++++--------- .../Session/PowerShellContext.cs | 26 ++- 3 files changed, 178 insertions(+), 163 deletions(-) rename src/PowerShellEditorServices/Language/{RunspaceSychronizer.cs => RunspaceSynchronizer.cs} (66%) diff --git a/src/PowerShellEditorServices/Language/AstOperations.cs b/src/PowerShellEditorServices/Language/AstOperations.cs index c0290fd7c..4223b4c49 100644 --- a/src/PowerShellEditorServices/Language/AstOperations.cs +++ b/src/PowerShellEditorServices/Language/AstOperations.cs @@ -97,65 +97,69 @@ static public async Task GetCompletionsAsync( var stopwatch = new Stopwatch(); - stopwatch.Start(); - - try - { - return CommandCompletion.CompleteInput( - scriptAst, - currentTokens, - cursorPosition, - options: null, - powershell: pwsh); - } - finally + if (powerShellContext.IsPSReadLineEnabled) { - stopwatch.Stop(); - logger.Write(LogLevel.Verbose, $"IntelliSense completed in {stopwatch.ElapsedMilliseconds}ms."); + stopwatch.Start(); + + try + { + return CommandCompletion.CompleteInput( + scriptAst, + currentTokens, + cursorPosition, + options: null, + powershell: pwsh); + } + finally + { + stopwatch.Stop(); + logger.Write(LogLevel.Verbose, $"IntelliSense completed in {stopwatch.ElapsedMilliseconds}ms."); + } } + // If the current runspace is out of process we can use // CommandCompletion.CompleteInput because PSReadLine won't be taking up the // main runspace. - // if (powerShellContext.IsCurrentRunspaceOutOfProcess()) - // { - // using (RunspaceHandle runspaceHandle = await powerShellContext.GetRunspaceHandleAsync(cancellationToken)) - // using (PowerShell powerShell = PowerShell.Create()) - // { - // powerShell.Runspace = runspaceHandle.Runspace; - // stopwatch.Start(); - // try - // { - // return CommandCompletion.CompleteInput( - // scriptAst, - // currentTokens, - // cursorPosition, - // options: null, - // powershell: powerShell); - // } - // finally - // { - // stopwatch.Stop(); - // logger.Write(LogLevel.Verbose, $"IntelliSense completed in {stopwatch.ElapsedMilliseconds}ms."); - // } - // } - // } - - // CommandCompletion commandCompletion = null; - // await powerShellContext.InvokeOnPipelineThreadAsync( - // pwsh => - // { - // stopwatch.Start(); - // commandCompletion = CommandCompletion.CompleteInput( - // scriptAst, - // currentTokens, - // cursorPosition, - // options: null, - // powershell: pwsh); - // }); - // stopwatch.Stop(); - // logger.Write(LogLevel.Verbose, $"IntelliSense completed in {stopwatch.ElapsedMilliseconds}ms."); - - // return commandCompletion; + if (powerShellContext.IsCurrentRunspaceOutOfProcess()) + { + using (RunspaceHandle runspaceHandle = await powerShellContext.GetRunspaceHandleAsync(cancellationToken)) + using (PowerShell powerShell = PowerShell.Create()) + { + powerShell.Runspace = runspaceHandle.Runspace; + stopwatch.Start(); + try + { + return CommandCompletion.CompleteInput( + scriptAst, + currentTokens, + cursorPosition, + options: null, + powershell: powerShell); + } + finally + { + stopwatch.Stop(); + logger.Write(LogLevel.Verbose, $"IntelliSense completed in {stopwatch.ElapsedMilliseconds}ms."); + } + } + } + + CommandCompletion commandCompletion = null; + await powerShellContext.InvokeOnPipelineThreadAsync( + pwsh => + { + stopwatch.Start(); + commandCompletion = CommandCompletion.CompleteInput( + scriptAst, + currentTokens, + cursorPosition, + options: null, + powershell: pwsh); + }); + stopwatch.Stop(); + logger.Write(LogLevel.Verbose, $"IntelliSense completed in {stopwatch.ElapsedMilliseconds}ms."); + + return commandCompletion; } finally { diff --git a/src/PowerShellEditorServices/Language/RunspaceSychronizer.cs b/src/PowerShellEditorServices/Language/RunspaceSynchronizer.cs similarity index 66% rename from src/PowerShellEditorServices/Language/RunspaceSychronizer.cs rename to src/PowerShellEditorServices/Language/RunspaceSynchronizer.cs index fdd64d265..e4df2e3de 100644 --- a/src/PowerShellEditorServices/Language/RunspaceSychronizer.cs +++ b/src/PowerShellEditorServices/Language/RunspaceSynchronizer.cs @@ -12,14 +12,18 @@ namespace Microsoft.PowerShell.EditorServices { using System.Management.Automation; + /// - /// Does a thing + /// This class is used to sync the state of one runspace to another. + /// It's done by copying over variables and reimporting modules into the target runspace. + /// It doesn't rely on the pipeline of the source runspace at all, instead leverages Reflection + /// to access internal properties and methods on the Runspace type. + /// Lastly, in order to trigger the synchronizing, you must call the Activate method. This will go + /// in the PSReadLine key handler for ENTER. /// public class RunspaceSynchronizer { - /// - /// Does a thing - /// + // Determines whether the HandleRunspaceStateChange event should attempt to sync the runspaces. private static bool SourceActionEnabled = false; // 'moduleCache' keeps track of all modules imported in the source Runspace. @@ -33,21 +37,71 @@ public class RunspaceSynchronizer // in the target Runspace, because all tab completion needs is the type information. private static Dictionary variableCache = new Dictionary(StringComparer.OrdinalIgnoreCase); - private static List moduleToImport = new List(); - private static List variablesToSet = new List(); - private static Runspace sourceRunspace; private static Runspace targetRunspace; private static EngineIntrinsics sourceEngineIntrinsics; private static EngineIntrinsics targetEngineIntrinsics; - private static object syncObj = new object(); + private readonly static HashSet POWERSHELL_MAGIC_VARIABLES = new HashSet(StringComparer.OrdinalIgnoreCase) + { + "PID", + "PSVersionTable", + "PSEdition", + "PSHOME", + "HOST", + "true", + "false", + "null", + "Error", + "IsMacOS", + "IsLinux", + "IsWindows" + }; /// - /// Does a thing + /// Determines if the RunspaceSynchronizer has been initialized. /// public static bool IsReadyForEvents { get; private set; } + #region Public methods + + /// + /// Does a thing + /// + public static void InitializeRunspaces(Runspace runspaceSource, Runspace runspaceTarget) + { + sourceRunspace = runspaceSource; + sourceEngineIntrinsics = ReflectionUtils.GetEngineIntrinsics(sourceRunspace); + targetRunspace = runspaceTarget; + targetEngineIntrinsics = ReflectionUtils.GetEngineIntrinsics(runspaceTarget); + IsReadyForEvents = true; + + sourceEngineIntrinsics.Events.SubscribeEvent( + source: null, + eventName: null, + sourceIdentifier: PSEngineEvent.OnIdle.ToString(), + data: null, + handlerDelegate: HandleRunspaceStateChange, + supportEvent: true, + forwardEvent: false); + + Activate(); + // Trigger events + HandleRunspaceStateChange(sender: null, args: null); + } + + /// + /// Does a thing + /// + public static void Activate() + { + SourceActionEnabled = true; + } + + #endregion + + #region Private Methods + private static void HandleRunspaceStateChange(object sender, PSEventArgs args) { if (!SourceActionEnabled) @@ -57,72 +111,37 @@ private static void HandleRunspaceStateChange(object sender, PSEventArgs args) SourceActionEnabled = false; - try + var newOrChangedModules = new List(); + List modules = ReflectionUtils.GetModules(sourceRunspace); + foreach (PSModuleInfo module in modules) { - // Maybe also track the latest history item id ($h = Get-History -Count 1; $h.Id) - // to make sure we do the collection only if there was actually any input. - - var newOrChangedModules = new List(); - List modules = ReflectionUtils.GetModules(sourceRunspace); - foreach (PSModuleInfo module in modules) + if (moduleCache.Add(module)) { - if (moduleCache.Add(module)) - { - newOrChangedModules.Add(module); - } + newOrChangedModules.Add(module); } + } - var newOrChangedVars = new List(); - - var variables = sourceEngineIntrinsics.GetVariables(); - foreach (var variable in variables) - { - // TODO: first filter out the built-in variables. - if(!variableCache.TryGetValue(variable.Name, out Type value) || value != variable.Value?.GetType()) - { - variableCache[variable.Name] = variable.Value?.GetType(); - - newOrChangedVars.Add(variable); - } - } - - if (newOrChangedModules.Count == 0 && newOrChangedVars.Count == 0) - { - return; - } + var newOrChangedVars = new List(); - lock (syncObj) + var variables = sourceEngineIntrinsics.GetVariables(); + foreach (var variable in variables) + { + // If the variable is a magic variable or it's type has not changed, then skip it. + if(POWERSHELL_MAGIC_VARIABLES.Contains(variable.Name) || + (variableCache.TryGetValue(variable.Name, out Type value) && value == variable.Value?.GetType())) { - moduleToImport.AddRange(newOrChangedModules); - variablesToSet.AddRange(newOrChangedVars); + continue; } - // Enable the action in target Runspace - UpdateTargetRunspaceState(); - } catch (Exception ex) { - System.Console.WriteLine(ex.Message); - System.Console.WriteLine(ex.StackTrace); - } - } - - private static void UpdateTargetRunspaceState() - { - List newOrChangedModules; - List newOrChangedVars; - - lock (syncObj) - { - newOrChangedModules = new List(moduleToImport); - newOrChangedVars = new List(variablesToSet); - - moduleToImport.Clear(); - variablesToSet.Clear(); + // Add the variable to the cache and mark it as a newOrChanged variable. + variableCache[variable.Name] = variable.Value?.GetType(); + newOrChangedVars.Add(variable); } if (newOrChangedModules.Count > 0) { - // Import the modules with -Force + // Import the modules in the targetRunspace with -Force using (PowerShell pwsh = PowerShell.Create()) { pwsh.Runspace = targetRunspace; @@ -152,47 +171,15 @@ private static void UpdateTargetRunspaceState() } } - /// - /// Does a thing - /// - public static void InitializeRunspaces(Runspace runspaceSource, Runspace runspaceTarget) - { - sourceRunspace = runspaceSource; - sourceEngineIntrinsics = ReflectionUtils.GetEngineIntrinsics(sourceRunspace); - IsReadyForEvents = true; - - targetRunspace = runspaceTarget; - targetEngineIntrinsics = ReflectionUtils.GetEngineIntrinsics(runspaceTarget); - - if(sourceEngineIntrinsics != null) - { - sourceEngineIntrinsics.Events.SubscribeEvent( - source: null, - eventName: null, - sourceIdentifier: PSEngineEvent.OnIdle.ToString(), - data: null, - handlerDelegate: HandleRunspaceStateChange, - supportEvent: true, - forwardEvent: false); - } - - Activate(); - // Trigger events - HandleRunspaceStateChange(sender: null, args: null); - } - - /// - /// Does a thing - /// - public static void Activate() - { - SourceActionEnabled = true; - } + #endregion - internal class ReflectionUtils + // A collection of helper methods that use Reflection in some form. + private class ReflectionUtils { private static BindingFlags bindingFlags = BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.Default; + // Gets the modules loaded in a runspace. + // This exists in runspace.ExecutionContext.Modules.GetModule(string[] patterns, bool all) internal static List GetModules(Runspace runspace) { var executionContext = typeof(Runspace) @@ -207,6 +194,8 @@ internal static List GetModules(Runspace runspace) return modules; } + // Gets the engine intrinsics object on a Runspace. + // This exists in runspace.ExecutionContext.EngineIntrinsics. internal static EngineIntrinsics GetEngineIntrinsics(Runspace runspace) { var executionContext = typeof(Runspace) @@ -220,12 +209,14 @@ internal static EngineIntrinsics GetEngineIntrinsics(Runspace runspace) } } + // Extension methods on EngineIntrinsics to streamline some setters and setters. internal static class EngineIntrinsicsExtensions { + private const int RETRY_ATTEMPTS = 3; internal static List GetVariables(this EngineIntrinsics engineIntrinsics) { List variables = new List(); - foreach (PSObject psobject in engineIntrinsics.GetItems(ItemType.Variable)) + foreach (PSObject psobject in engineIntrinsics.GetItems(ItemProviderType.Variable)) { var variable = (PSVariable) psobject.BaseObject; variables.Add(variable); @@ -235,12 +226,12 @@ internal static List GetVariables(this EngineIntrinsics engineIntrin internal static void SetVariable(this EngineIntrinsics engineIntrinsics, PSVariable variable) { - engineIntrinsics.SetItem(ItemType.Variable, variable.Name, variable.Value); + engineIntrinsics.SetItem(ItemProviderType.Variable, variable.Name, variable.Value); } - private static Collection GetItems(this EngineIntrinsics engineIntrinsics, ItemType itemType) + private static Collection GetItems(this EngineIntrinsics engineIntrinsics, ItemProviderType itemType) { - for (int i = 0; i < 3; i++) + for (int i = 0; i < RETRY_ATTEMPTS; i++) { try { @@ -255,9 +246,9 @@ private static Collection GetItems(this EngineIntrinsics engineIntrins return new Collection(); } - private static void SetItem(this EngineIntrinsics engineIntrinsics, ItemType itemType, string name, object value) + private static void SetItem(this EngineIntrinsics engineIntrinsics, ItemProviderType itemType, string name, object value) { - for (int i = 0; i < 3; i++) + for (int i = 0; i < RETRY_ATTEMPTS; i++) { try { @@ -271,7 +262,7 @@ private static void SetItem(this EngineIntrinsics engineIntrinsics, ItemType ite } } - private enum ItemType + private enum ItemProviderType { Variable, Function, diff --git a/src/PowerShellEditorServices/Session/PowerShellContext.cs b/src/PowerShellEditorServices/Session/PowerShellContext.cs index fbfe46cb2..0ddf2a19e 100644 --- a/src/PowerShellEditorServices/Session/PowerShellContext.cs +++ b/src/PowerShellEditorServices/Session/PowerShellContext.cs @@ -51,7 +51,6 @@ static PowerShellContext() private readonly SemaphoreSlim resumeRequestHandle = AsyncUtils.CreateSimpleLockingSemaphore(); - private bool isPSReadLineEnabled; private ILogger logger; private PowerShell powerShell; private bool ownsInitialRunspace; @@ -66,6 +65,11 @@ static PowerShellContext() private int isCommandLoopRestarterSet; + private readonly ScriptBlock psReadLineEnterKeyHandlerScriptBlock = ScriptBlock.Create(@" +[Microsoft.PowerShell.EditorServices.RunspaceSynchronizer]::Activate() +[Microsoft.PowerShell.PSConsoleReadLine]::AcceptLine() +"); + #endregion #region Properties @@ -98,6 +102,12 @@ public PowerShellContextState SessionState private set; } + internal bool IsPSReadLineEnabled + { + get; + private set; + } + /// /// Gets the PowerShell version details for the initial local runspace. /// @@ -150,7 +160,7 @@ public RunspaceDetails CurrentRunspace public PowerShellContext(ILogger logger, bool isPSReadLineEnabled) { this.logger = logger; - this.isPSReadLineEnabled = isPSReadLineEnabled; + this.IsPSReadLineEnabled = isPSReadLineEnabled; } /// @@ -328,7 +338,7 @@ public void Initialize( this.InvocationEventQueue = InvocationEventQueue.Create(this, this.PromptNest); if (powerShellVersion.Major >= 5 && - this.isPSReadLineEnabled && + this.IsPSReadLineEnabled && PSReadLinePromptContext.TryGetPSReadLineProxy(logger, initialRunspace, out PSReadLineProxy proxy)) { this.PromptContext = new PSReadLinePromptContext( @@ -336,6 +346,16 @@ public void Initialize( this.PromptNest, this.InvocationEventQueue, proxy); + + // Set up the PSReadLine key handler for the Runspace synchronizer used in completions. + using (PowerShell pwsh = PowerShell.Create()) + { + pwsh.Runspace = initialRunspace; + pwsh.AddCommand("Set-PSReadLineKeyHandler") + .AddParameter("Chord", "ENTER") + .AddParameter("ScriptBlock", psReadLineEnterKeyHandlerScriptBlock) + .Invoke(); + } } else { From 849015eac6eb18f3963c7c07081ab47a06a3553f Mon Sep 17 00:00:00 2001 From: Tyler Leonhardt Date: Thu, 20 Jun 2019 17:08:53 -0700 Subject: [PATCH 3/6] add tests --- .../RunspaceSynchronizer/testModule.psm1 | 13 ++++ .../Language/RunspaceSynchronizerTests.cs | 65 +++++++++++++++++++ 2 files changed, 78 insertions(+) create mode 100644 test/PowerShellEditorServices.Test.Shared/RunspaceSynchronizer/testModule.psm1 create mode 100644 test/PowerShellEditorServices.Test/Language/RunspaceSynchronizerTests.cs diff --git a/test/PowerShellEditorServices.Test.Shared/RunspaceSynchronizer/testModule.psm1 b/test/PowerShellEditorServices.Test.Shared/RunspaceSynchronizer/testModule.psm1 new file mode 100644 index 000000000..612819ab7 --- /dev/null +++ b/test/PowerShellEditorServices.Test.Shared/RunspaceSynchronizer/testModule.psm1 @@ -0,0 +1,13 @@ +# +# Copyright (c) Microsoft. All rights reserved. +# Licensed under the MIT license. See LICENSE file in the project root for full license information. +# + +function Search-Foo { + param () + "success" +} + +Set-Alias sfoo Search-Foo + +Export-ModuleMember -Function Search-Foo -Alias sfoo diff --git a/test/PowerShellEditorServices.Test/Language/RunspaceSynchronizerTests.cs b/test/PowerShellEditorServices.Test/Language/RunspaceSynchronizerTests.cs new file mode 100644 index 000000000..9951e383c --- /dev/null +++ b/test/PowerShellEditorServices.Test/Language/RunspaceSynchronizerTests.cs @@ -0,0 +1,65 @@ +// +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +// + +using System.Collections.ObjectModel; +using Xunit; + +namespace Microsoft.PowerShell.EditorServices.Test.Language +{ + using System.Management.Automation; + + public class RunspaceSynchronizerTests + { + [Trait("Category", "RunspaceSynchronizer")] + [Theory] + // variable test + [InlineData("$foo = 'foo'", "$foo", "foo")] + // module functions test + [InlineData("Import-Module ../../../../PowerShellEditorServices.Test.Shared/RunspaceSynchronizer/testModule.psm1", "Search-Foo", "success")] + // module aliases test + [InlineData("Import-Module ../../../../PowerShellEditorServices.Test.Shared/RunspaceSynchronizer/testModule.psm1", "(Get-Alias sfoo).Definition", "Search-Foo")] + public void TestRunspaceSynchronizerSyncsData(string sourceScript, string targetScript, object expected) + { + using (PowerShell pwshSource = PowerShell.Create()) + using (PowerShell pwshTarget = PowerShell.Create()) + { + RunspaceSynchronizer.InitializeRunspaces(pwshSource.Runspace, pwshTarget.Runspace); + AssertExpectedIsSynced(pwshSource, pwshTarget, sourceScript, targetScript, expected); + } + } + + [Fact] + public void TestRunspaceSynchronizerOverwritesTypes() + { + using (PowerShell pwshSource = PowerShell.Create()) + using (PowerShell pwshTarget = PowerShell.Create()) + { + RunspaceSynchronizer.InitializeRunspaces(pwshSource.Runspace, pwshTarget.Runspace); + AssertExpectedIsSynced(pwshSource, pwshTarget, "$foo = 444", "$foo.GetType().Name", "Int32"); + AssertExpectedIsSynced(pwshSource, pwshTarget, "$foo = 'change to string'", "$foo.GetType().Name", "String"); + } + } + + private static void AssertExpectedIsSynced( + PowerShell pwshSource, + PowerShell pwshTarget, + string sourceScript, + string targetScript, + object expected) + { + pwshSource.AddScript(sourceScript).Invoke(); + RunspaceSynchronizer.Activate(); + + // We need to allow the event some time to fire. + System.Threading.Thread.Sleep(1000); + + var results = pwshTarget.AddScript(targetScript).Invoke(); + + Assert.Single(results); + Assert.NotNull(results[0].BaseObject); + Assert.Equal(expected, results[0].BaseObject); + } + } +} From f4fbcf1fd86229e694dba5fa62e9d9eb1685f6dd Mon Sep 17 00:00:00 2001 From: Tyler Leonhardt Date: Thu, 20 Jun 2019 17:21:56 -0700 Subject: [PATCH 4/6] handle F8 --- src/PowerShellEditorServices.Protocol/Server/LanguageServer.cs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/PowerShellEditorServices.Protocol/Server/LanguageServer.cs b/src/PowerShellEditorServices.Protocol/Server/LanguageServer.cs index 9c7134def..6b6733e5c 100644 --- a/src/PowerShellEditorServices.Protocol/Server/LanguageServer.cs +++ b/src/PowerShellEditorServices.Protocol/Server/LanguageServer.cs @@ -1453,6 +1453,9 @@ protected Task HandleEvaluateRequestAsync( executeTask.ContinueWith( (task) => { + // This is the equivalent of hitting ENTER in the Integrated Console + // so we need to activate the RunspaceSynchronizer for completions. + RunspaceSynchronizer.Activate(); // Return an empty result since the result value is irrelevant // for this request in the LanguageServer return From 1cbeb607f72b6c2442656cbf292c5e3553844372 Mon Sep 17 00:00:00 2001 From: Tyler Leonhardt Date: Wed, 26 Jun 2019 15:19:44 -0700 Subject: [PATCH 5/6] ipmo modules correctly and don't sync while debugging --- .../Language/AstOperations.cs | 8 +- .../Language/RunspaceSynchronizer.cs | 82 +++++++++++-------- 2 files changed, 51 insertions(+), 39 deletions(-) diff --git a/src/PowerShellEditorServices/Language/AstOperations.cs b/src/PowerShellEditorServices/Language/AstOperations.cs index 4223b4c49..fa0f15c61 100644 --- a/src/PowerShellEditorServices/Language/AstOperations.cs +++ b/src/PowerShellEditorServices/Language/AstOperations.cs @@ -12,12 +12,10 @@ using System.Threading; using System.Threading.Tasks; using System.Management.Automation.Language; -using System.Management.Automation.Runspaces; namespace Microsoft.PowerShell.EditorServices { using System.Management.Automation; - using System.Management.Automation.Language; /// /// Provides common operations for the syntax tree of a parsed script. @@ -73,6 +71,7 @@ static public async Task GetCompletionsAsync( if (!RunspaceSynchronizer.IsReadyForEvents) { + pwsh.Runspace.Name = "RunspaceSynchronizerTargetRunspace"; RunspaceSynchronizer.InitializeRunspaces(powerShellContext.CurrentRunspace.Runspace, pwsh.Runspace); } @@ -97,7 +96,10 @@ static public async Task GetCompletionsAsync( var stopwatch = new Stopwatch(); - if (powerShellContext.IsPSReadLineEnabled) + // Static class members in Windows PowerShell had a thread synchronization issue. + // This issue was fixed in PowerShell 6+ so we only use the new completions if PSReadLine is enabled + // and we're running in .NET Core. + if (powerShellContext.IsPSReadLineEnabled && Utils.IsNetCore) { stopwatch.Start(); diff --git a/src/PowerShellEditorServices/Language/RunspaceSynchronizer.cs b/src/PowerShellEditorServices/Language/RunspaceSynchronizer.cs index e4df2e3de..d034635e4 100644 --- a/src/PowerShellEditorServices/Language/RunspaceSynchronizer.cs +++ b/src/PowerShellEditorServices/Language/RunspaceSynchronizer.cs @@ -4,10 +4,12 @@ // using System; +using System.Collections; using System.Collections.Generic; using System.Collections.ObjectModel; using System.Management.Automation.Runspaces; using System.Reflection; +using Microsoft.PowerShell.Commands; namespace Microsoft.PowerShell.EditorServices { @@ -23,6 +25,7 @@ namespace Microsoft.PowerShell.EditorServices /// public class RunspaceSynchronizer { + private static readonly Version versionZero = new Version(0, 0); // Determines whether the HandleRunspaceStateChange event should attempt to sync the runspaces. private static bool SourceActionEnabled = false; @@ -71,9 +74,9 @@ public class RunspaceSynchronizer public static void InitializeRunspaces(Runspace runspaceSource, Runspace runspaceTarget) { sourceRunspace = runspaceSource; - sourceEngineIntrinsics = ReflectionUtils.GetEngineIntrinsics(sourceRunspace); + sourceEngineIntrinsics = sourceRunspace.GetEngineIntrinsics(); targetRunspace = runspaceTarget; - targetEngineIntrinsics = ReflectionUtils.GetEngineIntrinsics(runspaceTarget); + targetEngineIntrinsics = runspaceTarget.GetEngineIntrinsics(); IsReadyForEvents = true; sourceEngineIntrinsics.Events.SubscribeEvent( @@ -104,7 +107,7 @@ public static void Activate() private static void HandleRunspaceStateChange(object sender, PSEventArgs args) { - if (!SourceActionEnabled) + if (!SourceActionEnabled || sourceRunspace.Debugger.IsActive) { return; } @@ -112,7 +115,7 @@ private static void HandleRunspaceStateChange(object sender, PSEventArgs args) SourceActionEnabled = false; var newOrChangedModules = new List(); - List modules = ReflectionUtils.GetModules(sourceRunspace); + List modules = sourceRunspace.GetModules(); foreach (PSModuleInfo module in modules) { if (moduleCache.Add(module)) @@ -150,8 +153,16 @@ private static void HandleRunspaceStateChange(object sender, PSEventArgs args) { if(moduleInfo.Path != null) { + string nameParameterValue = moduleInfo.Path; + // If the version is greater than zero, the module info was probably imported by the psd1 or module base. + // If so, we can just import from the module base which is the root of the module folder. + if (moduleInfo.Version > versionZero) + { + nameParameterValue = moduleInfo.ModuleBase; + } + pwsh.AddCommand("Import-Module") - .AddParameter("Name", moduleInfo.Path) + .AddParameter("Name", nameParameterValue) .AddParameter("Force") .AddStatement(); } @@ -172,40 +183,39 @@ private static void HandleRunspaceStateChange(object sender, PSEventArgs args) } #endregion + } - // A collection of helper methods that use Reflection in some form. - private class ReflectionUtils - { - private static BindingFlags bindingFlags = BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.Default; + internal static class RunspaceExtensions + { + private static BindingFlags bindingFlags = BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.Default; - // Gets the modules loaded in a runspace. - // This exists in runspace.ExecutionContext.Modules.GetModule(string[] patterns, bool all) - internal static List GetModules(Runspace runspace) - { - var executionContext = typeof(Runspace) - .GetProperty("ExecutionContext", bindingFlags) - .GetValue(runspace); - var ModuleIntrinsics = executionContext.GetType() - .GetProperty("Modules", bindingFlags) - .GetValue(executionContext); - var modules = ModuleIntrinsics.GetType() - .GetMethod("GetModules", bindingFlags, null, new Type[] { typeof(string[]), typeof(bool) }, null) - .Invoke(ModuleIntrinsics, new object[] { new string[] { "*" }, false }) as List; - return modules; - } + // Gets the modules loaded in a runspace. + // This exists in runspace.ExecutionContext.Modules.GetModule(string[] patterns, bool all) + internal static List GetModules(this Runspace runspace) + { + var executionContext = typeof(Runspace) + .GetProperty("ExecutionContext", bindingFlags) + .GetValue(runspace); + var ModuleIntrinsics = executionContext.GetType() + .GetProperty("Modules", bindingFlags) + .GetValue(executionContext); + var modules = ModuleIntrinsics.GetType() + .GetMethod("GetModules", bindingFlags, null, new Type[] { typeof(string[]), typeof(bool) }, null) + .Invoke(ModuleIntrinsics, new object[] { new string[] { "*" }, false }) as List; + return modules; + } - // Gets the engine intrinsics object on a Runspace. - // This exists in runspace.ExecutionContext.EngineIntrinsics. - internal static EngineIntrinsics GetEngineIntrinsics(Runspace runspace) - { - var executionContext = typeof(Runspace) - .GetProperty("ExecutionContext", bindingFlags) - .GetValue(runspace); - var engineIntrinsics = executionContext.GetType() - .GetProperty("EngineIntrinsics", bindingFlags) - .GetValue(executionContext) as EngineIntrinsics; - return engineIntrinsics; - } + // Gets the engine intrinsics object on a Runspace. + // This exists in runspace.ExecutionContext.EngineIntrinsics. + internal static EngineIntrinsics GetEngineIntrinsics(this Runspace runspace) + { + var executionContext = typeof(Runspace) + .GetProperty("ExecutionContext", bindingFlags) + .GetValue(runspace); + var engineIntrinsics = executionContext.GetType() + .GetProperty("EngineIntrinsics", bindingFlags) + .GetValue(executionContext) as EngineIntrinsics; + return engineIntrinsics; } } From 2fdff5e6dff9c8b75dc060ccd20f79d0a20f6eb0 Mon Sep 17 00:00:00 2001 From: Tyler Leonhardt Date: Wed, 26 Jun 2019 18:19:47 -0700 Subject: [PATCH 6/6] change to variable value --- .../Language/RunspaceSynchronizer.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/PowerShellEditorServices/Language/RunspaceSynchronizer.cs b/src/PowerShellEditorServices/Language/RunspaceSynchronizer.cs index d034635e4..a652658e4 100644 --- a/src/PowerShellEditorServices/Language/RunspaceSynchronizer.cs +++ b/src/PowerShellEditorServices/Language/RunspaceSynchronizer.cs @@ -38,7 +38,7 @@ public class RunspaceSynchronizer // 'variableCache' keeps all global scope variable names and their value type. // As long as the value type doesn't change, we don't need to update the variable // in the target Runspace, because all tab completion needs is the type information. - private static Dictionary variableCache = new Dictionary(StringComparer.OrdinalIgnoreCase); + private static Dictionary variableCache = new Dictionary(StringComparer.OrdinalIgnoreCase); private static Runspace sourceRunspace; private static Runspace targetRunspace; @@ -132,13 +132,13 @@ private static void HandleRunspaceStateChange(object sender, PSEventArgs args) { // If the variable is a magic variable or it's type has not changed, then skip it. if(POWERSHELL_MAGIC_VARIABLES.Contains(variable.Name) || - (variableCache.TryGetValue(variable.Name, out Type value) && value == variable.Value?.GetType())) + (variableCache.TryGetValue(variable.Name, out object value) && value == variable.Value)) { continue; } // Add the variable to the cache and mark it as a newOrChanged variable. - variableCache[variable.Name] = variable.Value?.GetType(); + variableCache[variable.Name] = variable.Value; newOrChangedVars.Add(variable); }