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( 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/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; + } + } } } 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