From 812fe3e7cf0e2f69499ea3e5116410de7de83b12 Mon Sep 17 00:00:00 2001 From: David Wilson Date: Tue, 15 Sep 2015 20:23:34 -0700 Subject: [PATCH 1/3] Enable Content-Length header in stdio reader This change turns on the "Content-Length" header expectation for the MessageReader that is used in the language service host. The VS Code plugin now writes out this header for all of its requests. --- src/PowerShellEditorServices.Transport.Stdio/StdioHost.cs | 2 +- .../LanguageServiceManager.cs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/PowerShellEditorServices.Transport.Stdio/StdioHost.cs b/src/PowerShellEditorServices.Transport.Stdio/StdioHost.cs index bc92f92ac..dcaf81863 100644 --- a/src/PowerShellEditorServices.Transport.Stdio/StdioHost.cs +++ b/src/PowerShellEditorServices.Transport.Stdio/StdioHost.cs @@ -142,7 +142,7 @@ async Task ListenForMessages() MessageReader messageReader = new MessageReader( System.Console.In, - MessageFormat.WithoutContentLength, + MessageFormat.WithContentLength, messageTypeResolver); MessageWriter messageWriter = diff --git a/test/PowerShellEditorServices.Test.Host/LanguageServiceManager.cs b/test/PowerShellEditorServices.Test.Host/LanguageServiceManager.cs index c728dc7db..b2c43ce7f 100644 --- a/test/PowerShellEditorServices.Test.Host/LanguageServiceManager.cs +++ b/test/PowerShellEditorServices.Test.Host/LanguageServiceManager.cs @@ -73,7 +73,7 @@ public void Start() this.MessageWriter = new MessageWriter( this.languageServiceProcess.StandardInput, - MessageFormat.WithoutContentLength, + MessageFormat.WithContentLength, messageTypeResolver); // Wait for the 'started' event From 01801792370ba9ff08a05cc681ba54701fbe23e4 Mon Sep 17 00:00:00 2001 From: David Wilson Date: Tue, 15 Sep 2015 20:24:19 -0700 Subject: [PATCH 2/3] Add logic to pick alternative log path on error This change causes the Logger class to pick an alternative path for the log file if the desired location can't be used. One case where this occurs is when the host process is installed under the Program Files folder. --- .../Utility/Logger.cs | 56 +++++++++++++++---- 1 file changed, 46 insertions(+), 10 deletions(-) diff --git a/src/PowerShellEditorServices/Utility/Logger.cs b/src/PowerShellEditorServices/Utility/Logger.cs index 0d8e89909..b484cc452 100644 --- a/src/PowerShellEditorServices/Utility/Logger.cs +++ b/src/PowerShellEditorServices/Utility/Logger.cs @@ -126,15 +126,16 @@ public LogWriter(LogLevel minimumLogLevel, string logFilePath, bool deleteExisti logFilePath); } - // Open the log file for writing with UTF8 encoding - this.textWriter = - new StreamWriter( - new FileStream( - logFilePath, - deleteExisting ? - FileMode.Create : - FileMode.Append), - Encoding.UTF8); + if (!this.TryOpenLogFile(logFilePath, deleteExisting)) + { + // If the log file couldn't be opened at this location, + // try opening it in a more reliable path + this.TryOpenLogFile( + Path.Combine( + Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), + Path.GetFileName(logFilePath)), + deleteExisting); + } } public void Write( @@ -144,7 +145,8 @@ public void Write( string callerSourceFile = null, int callerLineNumber = 0) { - if (logLevel >= this.minimumLogLevel) + if (this.textWriter != null && + logLevel >= this.minimumLogLevel) { // Print the timestamp and log level this.textWriter.WriteLine( @@ -176,5 +178,39 @@ public void Dispose() this.textWriter = null; } } + + private bool TryOpenLogFile( + string logFilePath, + bool deleteExisting) + { + try + { + // Open the log file for writing with UTF8 encoding + this.textWriter = + new StreamWriter( + new FileStream( + logFilePath, + deleteExisting ? + FileMode.Create : + FileMode.Append), + Encoding.UTF8); + + return true; + } + catch (Exception e) + { + if (e is UnauthorizedAccessException || + e is IOException) + { + // This exception is thrown when we can't open the file + // at the path in logFilePath. Return false to indicate + // that the log file couldn't be created. + return false; + } + + // Unexpected exception, rethrow it + throw; + } + } } } From e59cbbf5b49ec6428078e96e863fb1666508c16d Mon Sep 17 00:00:00 2001 From: David Wilson Date: Tue, 15 Sep 2015 20:26:21 -0700 Subject: [PATCH 3/3] Throttle "geterr" requests for better performance This change causes the "geterr" request to be throttled to the specified millisecond delay argument. This prevents the language service from getting overloaded by many successive calls into Script Analyzer. --- .../Request/ErrorRequest.cs | 78 ++++++++++++++++++- 1 file changed, 77 insertions(+), 1 deletion(-) diff --git a/src/PowerShellEditorServices.Transport.Stdio/Request/ErrorRequest.cs b/src/PowerShellEditorServices.Transport.Stdio/Request/ErrorRequest.cs index 098670afa..88ac1bfa5 100644 --- a/src/PowerShellEditorServices.Transport.Stdio/Request/ErrorRequest.cs +++ b/src/PowerShellEditorServices.Transport.Stdio/Request/ErrorRequest.cs @@ -6,13 +6,19 @@ using Microsoft.PowerShell.EditorServices.Session; using Microsoft.PowerShell.EditorServices.Transport.Stdio.Event; using Microsoft.PowerShell.EditorServices.Transport.Stdio.Message; +using Microsoft.PowerShell.EditorServices.Utility; +using System; using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; namespace Microsoft.PowerShell.EditorServices.Transport.Stdio.Request { [MessageTypeName("geterr")] public class ErrorRequest : RequestBase { + private static CancellationTokenSource existingRequestCancellation; + public static ErrorRequest Create(params string[] filePaths) { return new ErrorRequest @@ -30,8 +36,78 @@ public override void ProcessMessage( { List fileList = new List(); + // If there's an existing task, attempt to cancel it + try + { + if (existingRequestCancellation != null) + { + // Try to cancel the request + existingRequestCancellation.Cancel(); + + // If cancellation didn't throw an exception, + // clean up the existing token + existingRequestCancellation.Dispose(); + existingRequestCancellation = null; + } + } + catch (Exception e) + { + // TODO: Catch a more specific exception! + Logger.Write( + LogLevel.Error, + string.Format( + "Exception while cancelling analysis task:\n\n{0}", + e.ToString())); + + return; + } + + // Create a fresh cancellation token and then start the task. + // We create this on a different TaskScheduler so that we + // don't block the main message loop thread. + // TODO: Is there a better way to do this? + existingRequestCancellation = new CancellationTokenSource(); + Task.Factory.StartNew( + () => + DelayThenInvokeDiagnostics( + this.Arguments.Delay, + this.Arguments.Files, + editorSession, + messageWriter, + existingRequestCancellation.Token), + CancellationToken.None, + TaskCreationOptions.None, + TaskScheduler.Default); + } + + private static async Task DelayThenInvokeDiagnostics( + int delayMilliseconds, + string[] filesToAnalyze, + EditorSession editorSession, + MessageWriter messageWriter, + CancellationToken cancellationToken) + { + // First of all, wait for the desired delay period before + // analyzing the provided list of files + try + { + await Task.Delay(delayMilliseconds, cancellationToken); + } + catch (TaskCanceledException) + { + // If the task is cancelled, exit directly + return; + } + + // If we've made it past the delay period then we don't care + // about the cancellation token anymore. This could happen + // when the user stops typing for long enough that the delay + // period ends but then starts typing while analysis is going + // on. It makes sense to send back the results from the first + // delay period while the second one is ticking away. + // Get the requested files - foreach (string filePath in this.Arguments.Files) + foreach (string filePath in filesToAnalyze) { ScriptFile scriptFile = editorSession.Workspace.GetFile(