diff --git a/PowerShellEditorServices.sln b/PowerShellEditorServices.sln index e71a5d46f..4c4487ac4 100644 --- a/PowerShellEditorServices.sln +++ b/PowerShellEditorServices.sln @@ -1,7 +1,7 @@  Microsoft Visual Studio Solution File, Format Version 12.00 -# Visual Studio 2013 -VisualStudioVersion = 12.0.40629.0 +# Visual Studio 14 +VisualStudioVersion = 14.0.24720.0 MinimumVisualStudioVersion = 10.0.40219.1 Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{F594E7FD-1E72-4E51-A496-B019C2BA3180}" EndProject diff --git a/appveyor.yml b/appveyor.yml index 732f2c921..fa2b73f3a 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -7,6 +7,14 @@ branches: only: - master +# NOTE: If you need to debug a problem with the AppVeyor build, uncomment the +# following two lines and push them to your PR branch. Once the next +# build starts you will see RDP connection details written out to the +# build console. **DON'T FORGET TO REMOVE THIS COMMIT BEFORE MERGING!** + +#init: +#- ps: iex ((new-object net.webclient).DownloadString('https://raw.githubusercontent.com/appveyor/ci/master/scripts/enable-rdp.ps1')) + assembly_info: patch: true file: '**\AssemblyInfo.*' diff --git a/scripts/AddCopyrightHeaders.ps1 b/scripts/AddCopyrightHeaders.ps1 index a11756eb2..bd0c161c1 100644 --- a/scripts/AddCopyrightHeaders.ps1 +++ b/scripts/AddCopyrightHeaders.ps1 @@ -11,24 +11,30 @@ $copyrightHeaderString = // '@ -$srcPath = Resolve-Path $PSScriptRoot\..\src -Push-Location $srcPath +$global:updateCount = 0; -$updateCount = 0; -$allSourceFiles = Get-ChildItem $srcPath -Recurse -Filter *.cs | ?{ $_.FullName -notmatch "\\obj\\?" } - -foreach ($sourceFile in $allSourceFiles) +function Add-CopyrightHeaders($basePath) { - $fileContent = (Get-Content $sourceFile.FullName -Raw).TrimStart() + Push-Location $basePath + $allSourceFiles = Get-ChildItem $basePath -Recurse -Filter *.cs | ?{ $_.FullName -notmatch "\\obj\\?" } - if ($fileContent.StartsWith($copyrightHeaderString) -eq $false) + foreach ($sourceFile in $allSourceFiles) { - # Add the copyright header to the file - Set-Content $sourceFile.FullName ($copyrightHeaderString + "`r`n`r`n" + $fileContent) - Write-Output ("Updated {0}" -f (Resolve-Path $sourceFile.FullName -Relative)) + $fileContent = (Get-Content $sourceFile.FullName -Raw).TrimStart() + + if ($fileContent.StartsWith($copyrightHeaderString) -eq $false) + { + # Add the copyright header to the file + Set-Content $sourceFile.FullName ($copyrightHeaderString + "`r`n`r`n" + $fileContent) + Write-Output ("Updated {0}" -f (Resolve-Path $sourceFile.FullName -Relative)) + $global:updateCount++ + } } + + Pop-Location } -Write-Output "`r`nDone, $updateCount files updated." +Add-CopyrightHeaders(Resolve-Path $PSScriptRoot\..\src) +Add-CopyrightHeaders(Resolve-Path $PSScriptRoot\..\test) -Pop-Location \ No newline at end of file +Write-Output "`r`nDone, $global:updateCount file(s) updated." diff --git a/src/PowerShellEditorServices.Host/IMessageProcessor.cs b/src/PowerShellEditorServices.Host/IMessageProcessor.cs deleted file mode 100644 index e5e3dc7bf..000000000 --- a/src/PowerShellEditorServices.Host/IMessageProcessor.cs +++ /dev/null @@ -1,27 +0,0 @@ -// -// Copyright (c) Microsoft. All rights reserved. -// Licensed under the MIT license. See LICENSE file in the project root for full license information. -// - -using Microsoft.PowerShell.EditorServices.Protocol.MessageProtocol; -using System.Threading.Tasks; - -namespace Microsoft.PowerShell.EditorServices.Host -{ - /// - /// Provides an interface for classes that can process an incoming - /// message of some type. - /// - public interface IMessageProcessor - { - /// - /// Performs some action - /// - /// - /// - Task ProcessMessage( - Message messageToProcess, - EditorSession editorSession, - MessageWriter messageWriter); - } -} diff --git a/src/PowerShellEditorServices.Host/MessageLoop.cs b/src/PowerShellEditorServices.Host/MessageLoop.cs deleted file mode 100644 index ced331b14..000000000 --- a/src/PowerShellEditorServices.Host/MessageLoop.cs +++ /dev/null @@ -1,207 +0,0 @@ -// -// Copyright (c) Microsoft. All rights reserved. -// Licensed under the MIT license. See LICENSE file in the project root for full license information. -// - -using Microsoft.PowerShell.EditorServices.Protocol.DebugAdapter; -using Microsoft.PowerShell.EditorServices.Protocol.LanguageServer; -using Microsoft.PowerShell.EditorServices.Protocol.MessageProtocol; -using Microsoft.PowerShell.EditorServices.Utility; -using Nito.AsyncEx; -using System; -using System.IO; -using System.Management.Automation; -using System.Text; -using System.Threading; -using System.Threading.Tasks; - -namespace Microsoft.PowerShell.EditorServices.Host -{ - public class MessageLoop : IHost - { - #region Private Fields - - private Stream inputStream; - private Stream outputStream; - private bool runDebugAdapter; - private IConsoleHost consoleHost; - private EditorSession editorSession; - private MessageReader messageReader; - private MessageWriter messageWriter; - private SynchronizationContext applicationSyncContext; - private SynchronizationContext messageLoopSyncContext; - private AsyncContextThread messageLoopThread; - - #endregion - - #region IHost Implementation - - string IHost.Name - { - get { throw new NotImplementedException(); } - } - - Version IHost.Version - { - get { throw new NotImplementedException(); } - } - - public void Start() - { - // Start the main message loop - AsyncContext.Run((Func)this.StartMessageLoop); - } - - #endregion - - #region Constructors - - public MessageLoop(bool runDebugAdapter) - { - this.runDebugAdapter = runDebugAdapter; - } - - #endregion - - #region Private Methods - - private async Task StartMessageLoop() - { - // Hold on to the current synchronization context - this.applicationSyncContext = SynchronizationContext.Current; - - // Start the message listener on another thread - this.messageLoopThread = new AsyncContextThread(true); - await this.messageLoopThread.Factory.Run(() => this.ListenForMessages()); - } - - async Task ListenForMessages() - { - this.messageLoopSyncContext = SynchronizationContext.Current; - - // Ensure that the console is using UTF-8 encoding - System.Console.InputEncoding = Encoding.UTF8; - System.Console.OutputEncoding = Encoding.UTF8; - - // Open the standard input/output streams - this.inputStream = System.Console.OpenStandardInput(); - this.outputStream = System.Console.OpenStandardOutput(); - - IMessageSerializer messageSerializer = null; - IMessageProcessor messageProcessor = null; - - // Use a different serializer and message processor based - // on whether this instance should host a language server - // debug adapter. - if (this.runDebugAdapter) - { - DebugAdapter debugAdapter = new DebugAdapter(); - debugAdapter.Initialize(); - - messageProcessor = debugAdapter; - messageSerializer = new V8MessageSerializer(); - } - else - { - // Set up the LanguageServer - LanguageServer languageServer = new LanguageServer(); - languageServer.Initialize(); - - messageProcessor = languageServer; - messageSerializer = new JsonRpcMessageSerializer(); - } - - // Set up the reader and writer - this.messageReader = - new MessageReader( - this.inputStream, - messageSerializer); - - this.messageWriter = - new MessageWriter( - this.outputStream, - messageSerializer); - - // Set up the console host which will send events - // through the MessageWriter - this.consoleHost = new StdioConsoleHost(messageWriter); - - // Set up the PowerShell session - this.editorSession = new EditorSession(); - this.editorSession.StartSession(this.consoleHost); - this.editorSession.PowerShellContext.OutputWritten += powerShellContext_OutputWritten; - - if (this.runDebugAdapter) - { - // Attach to debugger events from the PowerShell session - this.editorSession.DebugService.DebuggerStopped += DebugService_DebuggerStopped; - } - - // Run the message loop - bool isRunning = true; - while (isRunning) - { - Message newMessage = null; - - try - { - // Read a message from stdin - newMessage = await this.messageReader.ReadMessage(); - } - catch (MessageParseException e) - { - // TODO: Write an error response - - Logger.Write( - LogLevel.Error, - "Could not parse a message that was received:\r\n\r\n" + - e.ToString()); - - // Continue the loop - continue; - } - - // Process the message - await messageProcessor.ProcessMessage( - newMessage, - this.editorSession, - this.messageWriter); - } - } - - void DebugService_DebuggerStopped(object sender, DebuggerStopEventArgs e) - { - // Push the write operation to the correct thread - this.messageLoopSyncContext.Post( - async (obj) => - { - await this.messageWriter.WriteEvent( - StoppedEvent.Type, - new StoppedEventBody - { - Source = new Source - { - Path = e.InvocationInfo.ScriptName, - }, - Line = e.InvocationInfo.ScriptLineNumber, - Column = e.InvocationInfo.OffsetInLine, - ThreadId = 1, // TODO: Change this based on context - Reason = "breakpoint" // TODO: Change this based on context - }); - }, null); - } - - async void powerShellContext_OutputWritten(object sender, OutputWrittenEventArgs e) - { - await this.messageWriter.WriteEvent( - OutputEvent.Type, - new OutputEventBody - { - Output = e.OutputText + (e.IncludeNewLine ? "\r\n" : string.Empty), - Category = (e.OutputType == OutputType.Error) ? "stderr" : "stdout" - }); - } - - #endregion - } -} diff --git a/src/PowerShellEditorServices.Host/PowerShellEditorServices.Host.csproj b/src/PowerShellEditorServices.Host/PowerShellEditorServices.Host.csproj index fb6a09099..aa2dd9825 100644 --- a/src/PowerShellEditorServices.Host/PowerShellEditorServices.Host.csproj +++ b/src/PowerShellEditorServices.Host/PowerShellEditorServices.Host.csproj @@ -59,14 +59,8 @@ - - - - - - diff --git a/src/PowerShellEditorServices.Host/Program.cs b/src/PowerShellEditorServices.Host/Program.cs index fd754a21c..b8be3ff07 100644 --- a/src/PowerShellEditorServices.Host/Program.cs +++ b/src/PowerShellEditorServices.Host/Program.cs @@ -3,6 +3,7 @@ // Licensed under the MIT license. See LICENSE file in the project root for full license information. // +using Microsoft.PowerShell.EditorServices.Protocol.Server; using Microsoft.PowerShell.EditorServices.Utility; using System; using System.Diagnostics; @@ -39,6 +40,19 @@ static void Main(string[] args) } #endif + string logPath = null; + string logPathArgument = + args.FirstOrDefault( + arg => + arg.StartsWith( + "/logPath:", + StringComparison.InvariantCultureIgnoreCase)); + + if (!string.IsNullOrEmpty(logPathArgument)) + { + logPath = logPathArgument.Substring(9).Trim('"'); + } + bool runDebugAdapter = args.Any( arg => @@ -50,25 +64,32 @@ static void Main(string[] args) // Catch unhandled exceptions for logging purposes AppDomain.CurrentDomain.UnhandledException += CurrentDomain_UnhandledException; + ProtocolServer server = null; if (runDebugAdapter) { - // TODO: Remove this behavior in the near future -- - // Create the debug service log in a separate file - // so that there isn't a conflict with the default - // log file. - Logger.Initialize("DebugAdapter.log", LogLevel.Verbose); + logPath = logPath ?? "DebugAdapter.log"; + server = new DebugAdapter(); } else { - // Initialize the logger - // TODO: Set the level based on command line parameter - Logger.Initialize(minimumLogLevel: LogLevel.Verbose); + logPath = logPath ?? "EditorServices.log"; + server = new LanguageServer(); } + // Start the logger with the specified log path + // TODO: Set the level based on command line parameter + Logger.Initialize(logPath, LogLevel.Verbose); + + Logger.Write(LogLevel.Normal, "PowerShell Editor Services Host starting..."); + + // Start the server + server.Start(); Logger.Write(LogLevel.Normal, "PowerShell Editor Services Host started!"); - MessageLoop messageLoop = new MessageLoop(runDebugAdapter); - messageLoop.Start(); + // Wait for the server to finish + server.WaitForExit(); + + Logger.Write(LogLevel.Normal, "PowerShell Editor Services Host exited normally."); } static void CurrentDomain_UnhandledException( diff --git a/src/PowerShellEditorServices.Host/StdioConsoleHost.cs b/src/PowerShellEditorServices.Host/StdioConsoleHost.cs deleted file mode 100644 index be48a9065..000000000 --- a/src/PowerShellEditorServices.Host/StdioConsoleHost.cs +++ /dev/null @@ -1,110 +0,0 @@ -// -// Copyright (c) Microsoft. All rights reserved. -// Licensed under the MIT license. See LICENSE file in the project root for full license information. -// - -using Microsoft.PowerShell.EditorServices.Protocol.MessageProtocol; -using Microsoft.PowerShell.EditorServices.Utility; -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; - -namespace Microsoft.PowerShell.EditorServices.Host -{ - public class StdioConsoleHost : IConsoleHost - { - #region Private Fields - - private MessageWriter messageWriter; - private int currentReplEventSequence = 0; - private TaskCompletionSource currentPromptChoiceTask; - - #endregion - - #region Constructors - - public StdioConsoleHost(MessageWriter messageWriter) - { - Validate.IsNotNull("messageWriter", messageWriter); - - this.messageWriter = messageWriter; - } - - #endregion - - #region IConsoleHost Implementation - - Task IConsoleHost.PromptForChoice( - string caption, - string message, - IEnumerable choices, - int defaultChoice) - { - // NOTE: This code is held temporarily until a new model is - // found for dealing with interactive prompts. - - //// Create and store a TaskCompletionSource that will be - //// used to send the user's response back to the caller - //this.currentPromptChoiceTask = new TaskCompletionSource(); - //this.currentReplEventSequence++; - - //this.messageWriter.WriteMessage( - // new ReplPromptChoiceEvent - // { - // Body = new ReplPromptChoiceEventBody - // { - // Seq = this.currentReplEventSequence, - // Caption = caption, - // Message = message, - // DefaultChoice = defaultChoice, - // Choices = - // choices - // .Select(ReplPromptChoiceDetails.FromChoiceDescription) - // .ToArray() - // } - // }); - - //return this.currentPromptChoiceTask.Task; - - throw new NotImplementedException("This method is currently being refactored and is not available."); - } - - void IConsoleHost.PromptForChoiceResult( - int promptId, - int choiceResult) - { - // TODO: Validate that prompt ID exists - Validate.IsNotNull("currentPromptChoiceTask", this.currentPromptChoiceTask); - - this.currentPromptChoiceTask.SetResult(choiceResult); - this.currentPromptChoiceTask = null; - } - - void IConsoleHost.UpdateProgress( - long sourceId, - ProgressDetails progressDetails) - { - // TODO: Implement message for this - } - - void IConsoleHost.ExitSession(int exitCode) - { - // TODO: Implement message for this - } - - public void WriteOutput( - string outputString, - bool includeNewLine = true, - OutputType outputType = OutputType.Normal, - ConsoleColor foregroundColor = ConsoleColor.White, - ConsoleColor backgroundColor = ConsoleColor.Black) - { - // This is taken care of elsewhere now. This interface will - // be refactored out in the near future. - } - - #endregion - } -} diff --git a/src/PowerShellEditorServices.Protocol/Client/DebugAdapterClientBase.cs b/src/PowerShellEditorServices.Protocol/Client/DebugAdapterClientBase.cs new file mode 100644 index 000000000..8b149e569 --- /dev/null +++ b/src/PowerShellEditorServices.Protocol/Client/DebugAdapterClientBase.cs @@ -0,0 +1,42 @@ +// +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +// + +using Microsoft.PowerShell.EditorServices.Protocol.DebugAdapter; +using Microsoft.PowerShell.EditorServices.Protocol.MessageProtocol; +using Microsoft.PowerShell.EditorServices.Protocol.MessageProtocol.Channel; +using System.Threading.Tasks; + +namespace Microsoft.PowerShell.EditorServices.Protocol.Client +{ + public class DebugAdapterClient : ProtocolClient + { + public DebugAdapterClient(ChannelBase clientChannel) + : base(clientChannel, MessageProtocolType.DebugAdapter) + { + } + + public Task LaunchScript(string scriptFilePath) + { + return this.SendRequest( + LaunchRequest.Type, + new LaunchRequestArguments + { + Program = scriptFilePath + }); + } + + protected override async Task OnStart() + { + // Initialize the debug adapter + await this.SendRequest( + InitializeRequest.Type, + new InitializeRequestArguments + { + LinesStartAt1 = true + }); + } + } +} + diff --git a/src/PowerShellEditorServices.Protocol/Client/LanguageClientBase.cs b/src/PowerShellEditorServices.Protocol/Client/LanguageClientBase.cs new file mode 100644 index 000000000..c106fb59d --- /dev/null +++ b/src/PowerShellEditorServices.Protocol/Client/LanguageClientBase.cs @@ -0,0 +1,44 @@ +// +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +// + +using Microsoft.PowerShell.EditorServices.Protocol.LanguageServer; +using Microsoft.PowerShell.EditorServices.Protocol.MessageProtocol; +using Microsoft.PowerShell.EditorServices.Protocol.MessageProtocol.Channel; +using System.Threading.Tasks; + +namespace Microsoft.PowerShell.EditorServices.Protocol.Client +{ + /// + /// Provides a base implementation for language server clients. + /// + public abstract class LanguageClientBase : ProtocolClient + { + /// + /// Initializes an instance of the language client using the + /// specified channel for communication. + /// + /// The channel to use for communication with the server. + public LanguageClientBase(ChannelBase clientChannel) + : base(clientChannel, MessageProtocolType.LanguageServer) + { + } + + protected override Task OnStart() + { + // Initialize the implementation class + return this.Initialize(); + } + + protected override async Task OnStop() + { + // First, notify the language server that we're stopping + var response = await this.SendRequest(ShutdownRequest.Type, new object()); + await this.SendEvent(ExitNotification.Type, new object()); + } + + protected abstract Task Initialize(); + } +} + diff --git a/src/PowerShellEditorServices.Protocol/Client/LanguageServiceClient.cs b/src/PowerShellEditorServices.Protocol/Client/LanguageServiceClient.cs new file mode 100644 index 000000000..3cb65b07a --- /dev/null +++ b/src/PowerShellEditorServices.Protocol/Client/LanguageServiceClient.cs @@ -0,0 +1,116 @@ +// +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +// + +using Microsoft.PowerShell.EditorServices.Protocol.LanguageServer; +using Microsoft.PowerShell.EditorServices.Protocol.MessageProtocol; +using Microsoft.PowerShell.EditorServices.Protocol.MessageProtocol.Channel; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; + +namespace Microsoft.PowerShell.EditorServices.Protocol.Client +{ + public class LanguageServiceClient : LanguageClientBase + { + private Dictionary cachedDiagnostics = + new Dictionary(); + + public LanguageServiceClient(ChannelBase clientChannel) + : base(clientChannel) + { + } + + protected override async Task Initialize() + { + // Add handlers for common events + this.SetEventHandler(PublishDiagnosticsNotification.Type, HandlePublishDiagnosticsEvent); + + // Send the 'initialize' request and wait for the response + var initializeRequest = new InitializeRequest + { + RootPath = "", + Capabilities = new ClientCapabilities() + }; + + await this.SendRequest( + InitializeRequest.Type, + initializeRequest); + } + + #region Events + + public event EventHandler DiagnosticsReceived; + + protected void OnDiagnosticsReceived(string filePath) + { + if (this.DiagnosticsReceived != null) + { + this.DiagnosticsReceived(this, filePath); + } + } + + #endregion + + #region Private Methods + + private Task HandlePublishDiagnosticsEvent( + PublishDiagnosticsNotification diagnostics, + EventContext eventContext) + { + string normalizedPath = diagnostics.Uri.ToLower(); + + this.cachedDiagnostics[normalizedPath] = + diagnostics.Diagnostics + .Select(GetMarkerFromDiagnostic) + .ToArray(); + + this.OnDiagnosticsReceived(normalizedPath); + + return Task.FromResult(true); + } + + private static ScriptFileMarker GetMarkerFromDiagnostic(Diagnostic diagnostic) + { + DiagnosticSeverity severity = + diagnostic.Severity.GetValueOrDefault( + DiagnosticSeverity.Error); + + return new ScriptFileMarker + { + Level = MapDiagnosticSeverityToLevel(severity), + Message = diagnostic.Message, + ScriptRegion = new ScriptRegion + { + StartLineNumber = diagnostic.Range.Start.Line + 1, + StartColumnNumber = diagnostic.Range.Start.Character + 1, + EndLineNumber = diagnostic.Range.End.Line + 1, + EndColumnNumber = diagnostic.Range.End.Character + 1 + } + }; + } + + private static ScriptFileMarkerLevel MapDiagnosticSeverityToLevel(DiagnosticSeverity severity) + { + switch (severity) + { + case DiagnosticSeverity.Hint: + case DiagnosticSeverity.Information: + return ScriptFileMarkerLevel.Information; + + case DiagnosticSeverity.Warning: + return ScriptFileMarkerLevel.Warning; + + case DiagnosticSeverity.Error: + return ScriptFileMarkerLevel.Error; + + default: + return ScriptFileMarkerLevel.Error; + } + } + + #endregion + } +} diff --git a/src/PowerShellEditorServices.Protocol/Client/ProtocolClient.cs b/src/PowerShellEditorServices.Protocol/Client/ProtocolClient.cs new file mode 100644 index 000000000..01c66781d --- /dev/null +++ b/src/PowerShellEditorServices.Protocol/Client/ProtocolClient.cs @@ -0,0 +1,190 @@ +// +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +// + +using Microsoft.PowerShell.EditorServices.Protocol.MessageProtocol; +using Microsoft.PowerShell.EditorServices.Protocol.MessageProtocol.Channel; +using Newtonsoft.Json.Linq; +using System; +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; + +namespace Microsoft.PowerShell.EditorServices.Protocol.Client +{ + public class ProtocolClient + { + private bool isStarted; + private int currentMessageId; + private ChannelBase clientChannel; + private MessageProtocolType messageProtocolType; + private SynchronizationContext originalSynchronizationContext; + + private Dictionary> pendingRequests = + new Dictionary>(); + + /// + /// Initializes an instance of the protocol client using the + /// specified channel for communication. + /// + /// The channel to use for communication with the server. + /// The type of message protocol used by the server. + public ProtocolClient( + ChannelBase clientChannel, + MessageProtocolType messageProtocolType) + { + this.clientChannel = clientChannel; + this.messageProtocolType = messageProtocolType; + } + + /// + /// Starts the language server client and sends the Initialize method. + /// + /// A Task that can be awaited for initialization to complete. + public async Task Start() + { + if (!this.isStarted) + { + // Start the provided client channel + this.clientChannel.Start(this.messageProtocolType); + + // Set the handler for any message responses that come back + this.clientChannel.MessageDispatcher.SetResponseHandler(this.HandleResponse); + + // Listen for unhandled exceptions from the dispatcher + this.clientChannel.MessageDispatcher.UnhandledException += MessageDispatcher_UnhandledException; + + // Notify implementation about client start + await this.OnStart(); + + // Client is now started + this.isStarted = true; + } + } + + public async Task Stop() + { + if (this.isStarted) + { + // Stop the implementation first + await this.OnStop(); + + this.clientChannel.Stop(); + this.isStarted = false; + } + } + + /// + /// Sends a request to the server + /// + /// + /// + /// + /// + /// + public Task SendRequest( + RequestType requestType, + TParams requestParams) + { + return this.SendRequest(requestType, requestParams, true); + } + + public async Task SendRequest( + RequestType requestType, + TParams requestParams, + bool waitForResponse) + { + this.currentMessageId++; + + TaskCompletionSource responseTask = null; + + if (waitForResponse) + { + responseTask = new TaskCompletionSource(); + this.pendingRequests.Add( + this.currentMessageId.ToString(), + responseTask); + } + + await this.clientChannel.MessageWriter.WriteRequest( + requestType, + requestParams, + this.currentMessageId); + + if (responseTask != null) + { + var responseMessage = await responseTask.Task; + + return + responseMessage.Contents != null ? + responseMessage.Contents.ToObject() : + default(TResult); + } + else + { + // TODO: Better default value here? + return default(TResult); + } + } + + public async Task SendEvent(EventType eventType, TParams eventParams) + { + await this.clientChannel.MessageWriter.WriteMessage( + Message.Event( + eventType.MethodName, + JToken.FromObject(eventParams))); + } + + public void SetEventHandler( + EventType eventType, + Func eventHandler) + { + this.clientChannel.MessageDispatcher.SetEventHandler( + eventType, + eventHandler, + false); + } + + public void SetEventHandler( + EventType eventType, + Func eventHandler, + bool overrideExisting) + { + this.clientChannel.MessageDispatcher.SetEventHandler( + eventType, + eventHandler, + overrideExisting); + } + + private void MessageDispatcher_UnhandledException(object sender, Exception e) + { + if (this.originalSynchronizationContext != null) + { + this.originalSynchronizationContext.Post(o => { throw e; }, null); + } + } + + private void HandleResponse(Message responseMessage) + { + TaskCompletionSource pendingRequestTask = null; + + if (this.pendingRequests.TryGetValue(responseMessage.Id, out pendingRequestTask)) + { + pendingRequestTask.SetResult(responseMessage); + this.pendingRequests.Remove(responseMessage.Id); + } + } + + protected virtual Task OnStart() + { + return Task.FromResult(true); + } + + protected virtual Task OnStop() + { + return Task.FromResult(true); + } + } +} + diff --git a/src/PowerShellEditorServices.Protocol/DebugAdapter/AttachRequest.cs b/src/PowerShellEditorServices.Protocol/DebugAdapter/AttachRequest.cs index dde25ac05..4b8faa9c3 100644 --- a/src/PowerShellEditorServices.Protocol/DebugAdapter/AttachRequest.cs +++ b/src/PowerShellEditorServices.Protocol/DebugAdapter/AttachRequest.cs @@ -10,8 +10,8 @@ namespace Microsoft.PowerShell.EditorServices.Protocol.DebugAdapter public class AttachRequest { public static readonly - RequestType Type = - RequestType.Create("attach"); + RequestType Type = + RequestType.Create("attach"); } public class AttachRequestArguments diff --git a/src/PowerShellEditorServices.Protocol/DebugAdapter/ContinueRequest.cs b/src/PowerShellEditorServices.Protocol/DebugAdapter/ContinueRequest.cs index 43b7191ef..5ee5b2ec9 100644 --- a/src/PowerShellEditorServices.Protocol/DebugAdapter/ContinueRequest.cs +++ b/src/PowerShellEditorServices.Protocol/DebugAdapter/ContinueRequest.cs @@ -10,8 +10,8 @@ namespace Microsoft.PowerShell.EditorServices.Protocol.DebugAdapter public class ContinueRequest { public static readonly - RequestType Type = - RequestType.Create("continue"); + RequestType Type = + RequestType.Create("continue"); } } diff --git a/src/PowerShellEditorServices.Protocol/DebugAdapter/DisconnectRequest.cs b/src/PowerShellEditorServices.Protocol/DebugAdapter/DisconnectRequest.cs index 8caf23a54..7f7036b0c 100644 --- a/src/PowerShellEditorServices.Protocol/DebugAdapter/DisconnectRequest.cs +++ b/src/PowerShellEditorServices.Protocol/DebugAdapter/DisconnectRequest.cs @@ -10,8 +10,8 @@ namespace Microsoft.PowerShell.EditorServices.Protocol.DebugAdapter public class DisconnectRequest { public static readonly - RequestType Type = - RequestType.Create("disconnect"); + RequestType Type = + RequestType.Create("disconnect"); } } diff --git a/src/PowerShellEditorServices.Protocol/DebugAdapter/EvaluateRequest.cs b/src/PowerShellEditorServices.Protocol/DebugAdapter/EvaluateRequest.cs index 47ada3ae7..8f5b73391 100644 --- a/src/PowerShellEditorServices.Protocol/DebugAdapter/EvaluateRequest.cs +++ b/src/PowerShellEditorServices.Protocol/DebugAdapter/EvaluateRequest.cs @@ -10,8 +10,8 @@ namespace Microsoft.PowerShell.EditorServices.Protocol.DebugAdapter public class EvaluateRequest { public static readonly - RequestType Type = - RequestType.Create("evaluate"); + RequestType Type = + RequestType.Create("evaluate"); } public class EvaluateRequestArguments diff --git a/src/PowerShellEditorServices.Protocol/DebugAdapter/InitializeRequest.cs b/src/PowerShellEditorServices.Protocol/DebugAdapter/InitializeRequest.cs index 0715acec0..81dc7426d 100644 --- a/src/PowerShellEditorServices.Protocol/DebugAdapter/InitializeRequest.cs +++ b/src/PowerShellEditorServices.Protocol/DebugAdapter/InitializeRequest.cs @@ -10,8 +10,8 @@ namespace Microsoft.PowerShell.EditorServices.Protocol.DebugAdapter public class InitializeRequest { public static readonly - RequestType Type = - RequestType.Create("initialize"); + RequestType Type = + RequestType.Create("initialize"); } public class InitializeRequestArguments diff --git a/src/PowerShellEditorServices.Protocol/DebugAdapter/LaunchRequest.cs b/src/PowerShellEditorServices.Protocol/DebugAdapter/LaunchRequest.cs index d07ca94e2..ec93489e4 100644 --- a/src/PowerShellEditorServices.Protocol/DebugAdapter/LaunchRequest.cs +++ b/src/PowerShellEditorServices.Protocol/DebugAdapter/LaunchRequest.cs @@ -11,8 +11,8 @@ namespace Microsoft.PowerShell.EditorServices.Protocol.DebugAdapter public class LaunchRequest { public static readonly - RequestType Type = - RequestType.Create("launch"); + RequestType Type = + RequestType.Create("launch"); } public class LaunchRequestArguments diff --git a/src/PowerShellEditorServices.Protocol/DebugAdapter/NextRequest.cs b/src/PowerShellEditorServices.Protocol/DebugAdapter/NextRequest.cs index 3173bdde4..42dd7ba60 100644 --- a/src/PowerShellEditorServices.Protocol/DebugAdapter/NextRequest.cs +++ b/src/PowerShellEditorServices.Protocol/DebugAdapter/NextRequest.cs @@ -15,8 +15,8 @@ namespace Microsoft.PowerShell.EditorServices.Protocol.DebugAdapter public class NextRequest { public static readonly - RequestType Type = - RequestType.Create("next"); + RequestType Type = + RequestType.Create("next"); } } diff --git a/src/PowerShellEditorServices.Protocol/DebugAdapter/PauseRequest.cs b/src/PowerShellEditorServices.Protocol/DebugAdapter/PauseRequest.cs index 538f06658..86ceee4f4 100644 --- a/src/PowerShellEditorServices.Protocol/DebugAdapter/PauseRequest.cs +++ b/src/PowerShellEditorServices.Protocol/DebugAdapter/PauseRequest.cs @@ -10,8 +10,8 @@ namespace Microsoft.PowerShell.EditorServices.Protocol.DebugAdapter public class PauseRequest { public static readonly - RequestType Type = - RequestType.Create("pause"); + RequestType Type = + RequestType.Create("pause"); } } diff --git a/src/PowerShellEditorServices.Protocol/DebugAdapter/ScopesRequest.cs b/src/PowerShellEditorServices.Protocol/DebugAdapter/ScopesRequest.cs index b9cdc85ed..044f5bf73 100644 --- a/src/PowerShellEditorServices.Protocol/DebugAdapter/ScopesRequest.cs +++ b/src/PowerShellEditorServices.Protocol/DebugAdapter/ScopesRequest.cs @@ -11,8 +11,8 @@ namespace Microsoft.PowerShell.EditorServices.Protocol.DebugAdapter public class ScopesRequest { public static readonly - RequestType Type = - RequestType.Create("scopes"); + RequestType Type = + RequestType.Create("scopes"); } [DebuggerDisplay("FrameId = {FrameId}")] diff --git a/src/PowerShellEditorServices.Protocol/DebugAdapter/SetBreakpointsRequest.cs b/src/PowerShellEditorServices.Protocol/DebugAdapter/SetBreakpointsRequest.cs index 53172b32e..ff505f7e0 100644 --- a/src/PowerShellEditorServices.Protocol/DebugAdapter/SetBreakpointsRequest.cs +++ b/src/PowerShellEditorServices.Protocol/DebugAdapter/SetBreakpointsRequest.cs @@ -15,8 +15,8 @@ namespace Microsoft.PowerShell.EditorServices.Protocol.DebugAdapter public class SetBreakpointsRequest { public static readonly - RequestType Type = - RequestType.Create("setBreakpoints"); + RequestType Type = + RequestType.Create("setBreakpoints"); } public class SetBreakpointsRequestArguments diff --git a/src/PowerShellEditorServices.Protocol/DebugAdapter/SetExceptionBreakpointsRequest.cs b/src/PowerShellEditorServices.Protocol/DebugAdapter/SetExceptionBreakpointsRequest.cs index 349defe53..c86d33b09 100644 --- a/src/PowerShellEditorServices.Protocol/DebugAdapter/SetExceptionBreakpointsRequest.cs +++ b/src/PowerShellEditorServices.Protocol/DebugAdapter/SetExceptionBreakpointsRequest.cs @@ -15,8 +15,8 @@ namespace Microsoft.PowerShell.EditorServices.Protocol.DebugAdapter public class SetExceptionBreakpointsRequest { public static readonly - RequestType Type = - RequestType.Create("setExceptionBreakpoints"); + RequestType Type = + RequestType.Create("setExceptionBreakpoints"); } public class SetExceptionBreakpointsRequestArguments diff --git a/src/PowerShellEditorServices.Protocol/DebugAdapter/SourceRequest.cs b/src/PowerShellEditorServices.Protocol/DebugAdapter/SourceRequest.cs index 20c976326..fbb7285a8 100644 --- a/src/PowerShellEditorServices.Protocol/DebugAdapter/SourceRequest.cs +++ b/src/PowerShellEditorServices.Protocol/DebugAdapter/SourceRequest.cs @@ -10,8 +10,8 @@ namespace Microsoft.PowerShell.EditorServices.Protocol.DebugAdapter public class SourceRequest { public static readonly - RequestType Type = - RequestType.Create("source"); + RequestType Type = + RequestType.Create("source"); } public class SourceRequestArguments diff --git a/src/PowerShellEditorServices.Protocol/DebugAdapter/StackTraceRequest.cs b/src/PowerShellEditorServices.Protocol/DebugAdapter/StackTraceRequest.cs index b16fb6a70..9a11086bd 100644 --- a/src/PowerShellEditorServices.Protocol/DebugAdapter/StackTraceRequest.cs +++ b/src/PowerShellEditorServices.Protocol/DebugAdapter/StackTraceRequest.cs @@ -11,8 +11,8 @@ namespace Microsoft.PowerShell.EditorServices.Protocol.DebugAdapter public class StackTraceRequest { public static readonly - RequestType Type = - RequestType.Create("stackTrace"); + RequestType Type = + RequestType.Create("stackTrace"); } [DebuggerDisplay("ThreadId = {ThreadId}, Levels = {Levels}")] diff --git a/src/PowerShellEditorServices.Protocol/DebugAdapter/StepInRequest.cs b/src/PowerShellEditorServices.Protocol/DebugAdapter/StepInRequest.cs index 823f39dda..cfd8f7648 100644 --- a/src/PowerShellEditorServices.Protocol/DebugAdapter/StepInRequest.cs +++ b/src/PowerShellEditorServices.Protocol/DebugAdapter/StepInRequest.cs @@ -10,8 +10,8 @@ namespace Microsoft.PowerShell.EditorServices.Protocol.DebugAdapter public class StepInRequest { public static readonly - RequestType Type = - RequestType.Create("stepIn"); + RequestType Type = + RequestType.Create("stepIn"); } } diff --git a/src/PowerShellEditorServices.Protocol/DebugAdapter/StepOutRequest.cs b/src/PowerShellEditorServices.Protocol/DebugAdapter/StepOutRequest.cs index 3e381e1ea..5b86f3f7c 100644 --- a/src/PowerShellEditorServices.Protocol/DebugAdapter/StepOutRequest.cs +++ b/src/PowerShellEditorServices.Protocol/DebugAdapter/StepOutRequest.cs @@ -10,7 +10,7 @@ namespace Microsoft.PowerShell.EditorServices.Protocol.DebugAdapter public class StepOutRequest { public static readonly - RequestType Type = - RequestType.Create("stepOut"); + RequestType Type = + RequestType.Create("stepOut"); } } diff --git a/src/PowerShellEditorServices.Protocol/DebugAdapter/ThreadsRequest.cs b/src/PowerShellEditorServices.Protocol/DebugAdapter/ThreadsRequest.cs index 92c71464a..e72fcc969 100644 --- a/src/PowerShellEditorServices.Protocol/DebugAdapter/ThreadsRequest.cs +++ b/src/PowerShellEditorServices.Protocol/DebugAdapter/ThreadsRequest.cs @@ -10,8 +10,8 @@ namespace Microsoft.PowerShell.EditorServices.Protocol.DebugAdapter public class ThreadsRequest { public static readonly - RequestType Type = - RequestType.Create("threads"); + RequestType Type = + RequestType.Create("threads"); } public class ThreadsResponseBody diff --git a/src/PowerShellEditorServices.Protocol/DebugAdapter/VariablesRequest.cs b/src/PowerShellEditorServices.Protocol/DebugAdapter/VariablesRequest.cs index 945cd3400..40dbaabb9 100644 --- a/src/PowerShellEditorServices.Protocol/DebugAdapter/VariablesRequest.cs +++ b/src/PowerShellEditorServices.Protocol/DebugAdapter/VariablesRequest.cs @@ -11,8 +11,8 @@ namespace Microsoft.PowerShell.EditorServices.Protocol.DebugAdapter public class VariablesRequest { public static readonly - RequestType Type = - RequestType.Create("variables"); + RequestType Type = + RequestType.Create("variables"); } [DebuggerDisplay("VariablesReference = {VariablesReference}")] diff --git a/src/PowerShellEditorServices.Protocol/LanguageServer/CompletionMessages.cs b/src/PowerShellEditorServices.Protocol/LanguageServer/Completion.cs similarity index 85% rename from src/PowerShellEditorServices.Protocol/LanguageServer/CompletionMessages.cs rename to src/PowerShellEditorServices.Protocol/LanguageServer/Completion.cs index d4b6651c2..b302de838 100644 --- a/src/PowerShellEditorServices.Protocol/LanguageServer/CompletionMessages.cs +++ b/src/PowerShellEditorServices.Protocol/LanguageServer/Completion.cs @@ -15,15 +15,15 @@ namespace Microsoft.PowerShell.EditorServices.Protocol.LanguageServer public class CompletionRequest { public static readonly - RequestType Type = - RequestType.Create("textDocument/completion"); + RequestType Type = + RequestType.Create("textDocument/completion"); } public class CompletionResolveRequest { public static readonly - RequestType Type = - RequestType.Create("completionItem/resolve"); + RequestType Type = + RequestType.Create("completionItem/resolve"); } public enum CompletionItemKind diff --git a/src/PowerShellEditorServices.Protocol/LanguageServer/Definition.cs b/src/PowerShellEditorServices.Protocol/LanguageServer/Definition.cs index 0e636a492..37241dd63 100644 --- a/src/PowerShellEditorServices.Protocol/LanguageServer/Definition.cs +++ b/src/PowerShellEditorServices.Protocol/LanguageServer/Definition.cs @@ -10,8 +10,8 @@ namespace Microsoft.PowerShell.EditorServices.Protocol.LanguageServer public class DefinitionRequest { public static readonly - RequestType Type = - RequestType.Create("textDocument/definition"); + RequestType Type = + RequestType.Create("textDocument/definition"); } } diff --git a/src/PowerShellEditorServices.Protocol/LanguageServer/DiagnosticMessages.cs b/src/PowerShellEditorServices.Protocol/LanguageServer/Diagnostics.cs similarity index 100% rename from src/PowerShellEditorServices.Protocol/LanguageServer/DiagnosticMessages.cs rename to src/PowerShellEditorServices.Protocol/LanguageServer/Diagnostics.cs diff --git a/src/PowerShellEditorServices.Protocol/LanguageServer/DocumentHighlight.cs b/src/PowerShellEditorServices.Protocol/LanguageServer/DocumentHighlight.cs index 52c2148cf..c169acc44 100644 --- a/src/PowerShellEditorServices.Protocol/LanguageServer/DocumentHighlight.cs +++ b/src/PowerShellEditorServices.Protocol/LanguageServer/DocumentHighlight.cs @@ -24,8 +24,8 @@ public class DocumentHighlight public class DocumentHighlightRequest { public static readonly - RequestType Type = - RequestType.Create("textDocument/documentHighlight"); + RequestType Type = + RequestType.Create("textDocument/documentHighlight"); } } diff --git a/src/PowerShellEditorServices.Protocol/LanguageServer/ExpandAliasRequest.cs b/src/PowerShellEditorServices.Protocol/LanguageServer/ExpandAliasRequest.cs index c1a924e82..cd79c92f2 100644 --- a/src/PowerShellEditorServices.Protocol/LanguageServer/ExpandAliasRequest.cs +++ b/src/PowerShellEditorServices.Protocol/LanguageServer/ExpandAliasRequest.cs @@ -10,7 +10,7 @@ namespace Microsoft.PowerShell.EditorServices.Protocol.LanguageServer public class ExpandAliasRequest { public static readonly - RequestType Type = - RequestType.Create("powerShell/expandAlias"); + RequestType Type = + RequestType.Create("powerShell/expandAlias"); } } diff --git a/src/PowerShellEditorServices.Protocol/LanguageServer/Hover.cs b/src/PowerShellEditorServices.Protocol/LanguageServer/Hover.cs index 8d3e457e9..2af60e515 100644 --- a/src/PowerShellEditorServices.Protocol/LanguageServer/Hover.cs +++ b/src/PowerShellEditorServices.Protocol/LanguageServer/Hover.cs @@ -29,8 +29,8 @@ public class Hover public class HoverRequest { public static readonly - RequestType Type = - RequestType.Create("textDocument/hover"); + RequestType Type = + RequestType.Create("textDocument/hover"); } } diff --git a/src/PowerShellEditorServices.Protocol/LanguageServer/InitializeMessages.cs b/src/PowerShellEditorServices.Protocol/LanguageServer/Initialize.cs similarity index 87% rename from src/PowerShellEditorServices.Protocol/LanguageServer/InitializeMessages.cs rename to src/PowerShellEditorServices.Protocol/LanguageServer/Initialize.cs index c98fe246c..40a9d1a23 100644 --- a/src/PowerShellEditorServices.Protocol/LanguageServer/InitializeMessages.cs +++ b/src/PowerShellEditorServices.Protocol/LanguageServer/Initialize.cs @@ -10,8 +10,8 @@ namespace Microsoft.PowerShell.EditorServices.Protocol.LanguageServer public class InitializeRequest { public static readonly - RequestType Type = - RequestType.Create("initialize"); + RequestType Type = + RequestType.Create("initialize"); /// /// Gets or sets the root path of the editor's open workspace. diff --git a/src/PowerShellEditorServices.Protocol/LanguageServer/References.cs b/src/PowerShellEditorServices.Protocol/LanguageServer/References.cs index cec0eab0a..a55ebebf9 100644 --- a/src/PowerShellEditorServices.Protocol/LanguageServer/References.cs +++ b/src/PowerShellEditorServices.Protocol/LanguageServer/References.cs @@ -10,8 +10,8 @@ namespace Microsoft.PowerShell.EditorServices.Protocol.LanguageServer public class ReferencesRequest { public static readonly - RequestType Type = - RequestType.Create("textDocument/references"); + RequestType Type = + RequestType.Create("textDocument/references"); } public class ReferencesParams : TextDocumentPosition diff --git a/src/PowerShellEditorServices.Protocol/LanguageServer/ServerCapabilities.cs b/src/PowerShellEditorServices.Protocol/LanguageServer/ServerCapabilities.cs index cca55b924..34ba312f8 100644 --- a/src/PowerShellEditorServices.Protocol/LanguageServer/ServerCapabilities.cs +++ b/src/PowerShellEditorServices.Protocol/LanguageServer/ServerCapabilities.cs @@ -3,13 +3,6 @@ // Licensed under the MIT license. See LICENSE file in the project root for full license information. // -using Microsoft.PowerShell.EditorServices.Protocol.MessageProtocol; -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; - namespace Microsoft.PowerShell.EditorServices.Protocol.LanguageServer { public class ServerCapabilities diff --git a/src/PowerShellEditorServices.Protocol/LanguageServer/ShowOnlineHelpRequest.cs b/src/PowerShellEditorServices.Protocol/LanguageServer/ShowOnlineHelpRequest.cs index 40235b5ae..49c571faf 100644 --- a/src/PowerShellEditorServices.Protocol/LanguageServer/ShowOnlineHelpRequest.cs +++ b/src/PowerShellEditorServices.Protocol/LanguageServer/ShowOnlineHelpRequest.cs @@ -10,7 +10,7 @@ namespace Microsoft.PowerShell.EditorServices.Protocol.LanguageServer public class ShowOnlineHelpRequest { public static readonly - RequestType Type = - RequestType.Create("powerShell/showOnlineHelp"); + RequestType Type = + RequestType.Create("powerShell/showOnlineHelp"); } } diff --git a/src/PowerShellEditorServices.Protocol/LanguageServer/ShutdownMessages.cs b/src/PowerShellEditorServices.Protocol/LanguageServer/Shutdown.cs similarity index 87% rename from src/PowerShellEditorServices.Protocol/LanguageServer/ShutdownMessages.cs rename to src/PowerShellEditorServices.Protocol/LanguageServer/Shutdown.cs index 8a7604e30..519719015 100644 --- a/src/PowerShellEditorServices.Protocol/LanguageServer/ShutdownMessages.cs +++ b/src/PowerShellEditorServices.Protocol/LanguageServer/Shutdown.cs @@ -14,8 +14,8 @@ namespace Microsoft.PowerShell.EditorServices.Protocol.LanguageServer public class ShutdownRequest { public static readonly - RequestType Type = - RequestType.Create("shutdown"); + RequestType Type = + RequestType.Create("shutdown"); } /// diff --git a/src/PowerShellEditorServices.Protocol/LanguageServer/SignatureHelp.cs b/src/PowerShellEditorServices.Protocol/LanguageServer/SignatureHelp.cs index b463a45be..6387461dc 100644 --- a/src/PowerShellEditorServices.Protocol/LanguageServer/SignatureHelp.cs +++ b/src/PowerShellEditorServices.Protocol/LanguageServer/SignatureHelp.cs @@ -10,8 +10,8 @@ namespace Microsoft.PowerShell.EditorServices.Protocol.LanguageServer public class SignatureHelpRequest { public static readonly - RequestType Type = - RequestType.Create("textDocument/signatureHelp"); + RequestType Type = + RequestType.Create("textDocument/signatureHelp"); } public class ParameterInformation diff --git a/src/PowerShellEditorServices.Protocol/LanguageServer/TextDocumentMessages.cs b/src/PowerShellEditorServices.Protocol/LanguageServer/TextDocument.cs similarity index 100% rename from src/PowerShellEditorServices.Protocol/LanguageServer/TextDocumentMessages.cs rename to src/PowerShellEditorServices.Protocol/LanguageServer/TextDocument.cs diff --git a/src/PowerShellEditorServices.Protocol/LanguageServer/WorkspaceSymbols.cs b/src/PowerShellEditorServices.Protocol/LanguageServer/WorkspaceSymbols.cs index dca2c1bad..405d6e98e 100644 --- a/src/PowerShellEditorServices.Protocol/LanguageServer/WorkspaceSymbols.cs +++ b/src/PowerShellEditorServices.Protocol/LanguageServer/WorkspaceSymbols.cs @@ -43,15 +43,15 @@ public class SymbolInformation public class DocumentSymbolRequest { public static readonly - RequestType Type = - RequestType.Create("textDocument/documentSymbol"); + RequestType Type = + RequestType.Create("textDocument/documentSymbol"); } public class WorkspaceSymbolRequest { public static readonly - RequestType Type = - RequestType.Create("workspace/symbol"); + RequestType Type = + RequestType.Create("workspace/symbol"); } public class WorkspaceSymbolParams diff --git a/src/PowerShellEditorServices.Protocol/MessageProtocol/Channel/ChannelBase.cs b/src/PowerShellEditorServices.Protocol/MessageProtocol/Channel/ChannelBase.cs new file mode 100644 index 000000000..34d501648 --- /dev/null +++ b/src/PowerShellEditorServices.Protocol/MessageProtocol/Channel/ChannelBase.cs @@ -0,0 +1,84 @@ +// +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +// + +using Microsoft.PowerShell.EditorServices.Protocol.MessageProtocol.Serializers; +using Newtonsoft.Json.Linq; +using System.Threading.Tasks; + +namespace Microsoft.PowerShell.EditorServices.Protocol.MessageProtocol.Channel +{ + /// + /// Defines a base implementation for servers and their clients over a + /// single kind of communication channel. + /// + public abstract class ChannelBase + { + /// + /// Gets the MessageReader for reading messages from the channel. + /// + public MessageReader MessageReader { get; protected set; } + + /// + /// Gets the MessageWriter for writing messages to the channel. + /// + public MessageWriter MessageWriter { get; protected set; } + + /// + /// Gets the MessageDispatcher which allows registration of + /// handlers for requests, responses, and events that are + /// transmitted through the channel. + /// + public MessageDispatcher MessageDispatcher { get; private set; } + + /// + /// Starts the channel and initializes the MessageDispatcher. + /// + /// The type of message protocol used by the channel. + public void Start(MessageProtocolType messageProtocolType) + { + IMessageSerializer messageSerializer = null; + if (messageProtocolType == MessageProtocolType.LanguageServer) + { + messageSerializer = new JsonRpcMessageSerializer(); + } + else + { + messageSerializer = new V8MessageSerializer(); + } + + this.Initialize(messageSerializer); + + this.MessageDispatcher = + new MessageDispatcher( + this.MessageReader, + this.MessageWriter); + + this.MessageDispatcher.Start(); + } + + /// + /// Stops the channel. + /// + public void Stop() + { + this.MessageDispatcher.Stop(); + this.Shutdown(); + } + + /// + /// A method to be implemented by subclasses to handle the + /// actual initialization of the channel and the creation and + /// assignment of the MessageReader and MessageWriter properties. + /// + /// The IMessageSerializer to use for message serialization. + protected abstract void Initialize(IMessageSerializer messageSerializer); + + /// + /// A method to be implemented by subclasses to handle shutdown + /// of the channel once Stop is called. + /// + protected abstract void Shutdown(); + } +} diff --git a/src/PowerShellEditorServices.Protocol/MessageProtocol/Channel/StdioClientChannel.cs b/src/PowerShellEditorServices.Protocol/MessageProtocol/Channel/StdioClientChannel.cs new file mode 100644 index 000000000..41a2ba10f --- /dev/null +++ b/src/PowerShellEditorServices.Protocol/MessageProtocol/Channel/StdioClientChannel.cs @@ -0,0 +1,117 @@ +// +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +// + +using Microsoft.PowerShell.EditorServices.Protocol.MessageProtocol.Serializers; +using System.Diagnostics; +using System.IO; +using System.Text; + +namespace Microsoft.PowerShell.EditorServices.Protocol.MessageProtocol.Channel +{ + /// + /// Provides a client implementation for the standard I/O channel. + /// Launches the server process and then attaches to its console + /// streams. + /// + public class StdioClientChannel : ChannelBase + { + private string serviceProcessPath; + private string serviceProcessArguments; + + private Stream inputStream; + private Stream outputStream; + private Process serviceProcess; + + /// + /// Gets the process ID of the server process. + /// + public int ProcessId { get; private set; } + + /// + /// Initializes an instance of the StdioClient. + /// + /// The full path to the server process executable. + /// Optional arguments to pass to the service process executable. + public StdioClientChannel( + string serverProcessPath, + params string[] serverProcessArguments) + { + this.serviceProcessPath = serverProcessPath; + + if (serverProcessArguments != null) + { + this.serviceProcessArguments = + string.Join( + " ", + serverProcessArguments); + } + } + + protected override void Initialize(IMessageSerializer messageSerializer) + { + this.serviceProcess = new Process + { + StartInfo = new ProcessStartInfo + { + FileName = this.serviceProcessPath, + Arguments = this.serviceProcessArguments, + CreateNoWindow = true, + UseShellExecute = false, + RedirectStandardInput = true, + RedirectStandardOutput = true, + RedirectStandardError = true, + StandardOutputEncoding = Encoding.UTF8, + }, + EnableRaisingEvents = true, + }; + + // Start the process + this.serviceProcess.Start(); + this.ProcessId = this.serviceProcess.Id; + + // Open the standard input/output streams + this.inputStream = this.serviceProcess.StandardOutput.BaseStream; + this.outputStream = this.serviceProcess.StandardInput.BaseStream; + + // Set up the message reader and writer + this.MessageReader = + new MessageReader( + this.inputStream, + messageSerializer); + + this.MessageWriter = + new MessageWriter( + this.outputStream, + messageSerializer); + } + + protected override void Shutdown() + { + if (this.inputStream != null) + { + this.inputStream.Dispose(); + this.inputStream = null; + } + + if (this.outputStream != null) + { + this.outputStream.Dispose(); + this.outputStream = null; + } + + if (this.MessageReader != null) + { + this.MessageReader = null; + } + + if (this.MessageWriter != null) + { + this.MessageWriter = null; + } + + this.serviceProcess.Kill(); + } + } +} diff --git a/src/PowerShellEditorServices.Protocol/MessageProtocol/Channel/StdioServerChannel.cs b/src/PowerShellEditorServices.Protocol/MessageProtocol/Channel/StdioServerChannel.cs new file mode 100644 index 000000000..0be7dbbce --- /dev/null +++ b/src/PowerShellEditorServices.Protocol/MessageProtocol/Channel/StdioServerChannel.cs @@ -0,0 +1,51 @@ +// +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +// + +using Microsoft.PowerShell.EditorServices.Protocol.MessageProtocol.Serializers; +using System.IO; +using System.Text; +using System; + +namespace Microsoft.PowerShell.EditorServices.Protocol.MessageProtocol.Channel +{ + /// + /// Provides a server implementation for the standard I/O channel. + /// When started in a process, attaches to the console I/O streams + /// to communicate with the client that launched the process. + /// + public class StdioServerChannel : ChannelBase + { + private Stream inputStream; + private Stream outputStream; + + protected override void Initialize(IMessageSerializer messageSerializer) + { + // Ensure that the console is using UTF-8 encoding + System.Console.InputEncoding = Encoding.UTF8; + System.Console.OutputEncoding = Encoding.UTF8; + + // Open the standard input/output streams + this.inputStream = System.Console.OpenStandardInput(); + this.outputStream = System.Console.OpenStandardOutput(); + + // Set up the reader and writer + this.MessageReader = + new MessageReader( + this.inputStream, + messageSerializer); + + this.MessageWriter = + new MessageWriter( + this.outputStream, + messageSerializer); + } + + protected override void Shutdown() + { + // No default implementation needed, streams will be + // disposed on process shutdown. + } + } +} diff --git a/src/PowerShellEditorServices.Protocol/MessageProtocol/EventContext.cs b/src/PowerShellEditorServices.Protocol/MessageProtocol/EventContext.cs index be6c39bb6..e4f4051df 100644 --- a/src/PowerShellEditorServices.Protocol/MessageProtocol/EventContext.cs +++ b/src/PowerShellEditorServices.Protocol/MessageProtocol/EventContext.cs @@ -8,6 +8,10 @@ namespace Microsoft.PowerShell.EditorServices.Protocol.MessageProtocol { + /// + /// Provides context for a received event so that handlers + /// can write events back to the channel. + /// public class EventContext { private MessageWriter messageWriter; @@ -17,7 +21,9 @@ public EventContext(MessageWriter messageWriter) this.messageWriter = messageWriter; } - public async Task SendEvent(EventType eventType, TParams eventParams) + public async Task SendEvent( + EventType eventType, + TParams eventParams) { await this.messageWriter.WriteEvent( eventType, diff --git a/src/PowerShellEditorServices.Protocol/MessageProtocol/EventType.cs b/src/PowerShellEditorServices.Protocol/MessageProtocol/EventType.cs index f1766d295..1b2fe1893 100644 --- a/src/PowerShellEditorServices.Protocol/MessageProtocol/EventType.cs +++ b/src/PowerShellEditorServices.Protocol/MessageProtocol/EventType.cs @@ -3,18 +3,24 @@ // Licensed under the MIT license. See LICENSE file in the project root for full license information. // -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; - namespace Microsoft.PowerShell.EditorServices.Protocol.MessageProtocol { + /// + /// Defines an event type with a particular method name. + /// + /// The parameter type for this event. public class EventType { + /// + /// Gets the method name for the event type. + /// public string MethodName { get; private set; } + /// + /// Creates an EventType instance with the given parameter type and method name. + /// + /// The method name of the event. + /// A new EventType instance for the defined type. public static EventType Create(string methodName) { return new EventType() diff --git a/src/PowerShellEditorServices.Protocol/MessageProtocol/IMessageSerializer.cs b/src/PowerShellEditorServices.Protocol/MessageProtocol/IMessageSerializer.cs index f168cac1d..eafd8f209 100644 --- a/src/PowerShellEditorServices.Protocol/MessageProtocol/IMessageSerializer.cs +++ b/src/PowerShellEditorServices.Protocol/MessageProtocol/IMessageSerializer.cs @@ -3,16 +3,27 @@ // Licensed under the MIT license. See LICENSE file in the project root for full license information. // -using Microsoft.PowerShell.EditorServices.Protocol.LanguageServer; using Newtonsoft.Json.Linq; -using System; namespace Microsoft.PowerShell.EditorServices.Protocol.MessageProtocol { + /// + /// Defines a common interface for message serializers. + /// public interface IMessageSerializer { + /// + /// Serializes a Message to a JObject. + /// + /// The message to be serialized. + /// A JObject which contains the JSON representation of the message. JObject SerializeMessage(Message message); + /// + /// Deserializes a JObject to a Messsage. + /// + /// The JObject containing the JSON representation of the message. + /// The Message that was represented by the JObject. Message DeserializeMessage(JObject messageJson); } } diff --git a/src/PowerShellEditorServices.Protocol/MessageProtocol/Message.cs b/src/PowerShellEditorServices.Protocol/MessageProtocol/Message.cs index 10b060062..eb1fd3db9 100644 --- a/src/PowerShellEditorServices.Protocol/MessageProtocol/Message.cs +++ b/src/PowerShellEditorServices.Protocol/MessageProtocol/Message.cs @@ -4,14 +4,12 @@ // using Newtonsoft.Json.Linq; -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; namespace Microsoft.PowerShell.EditorServices.Protocol.MessageProtocol { + /// + /// Defines all possible message types. + /// public enum MessageType { Unknown, @@ -20,18 +18,40 @@ public enum MessageType Event } + /// + /// Provides common details for protocol messages of any format. + /// public class Message { + /// + /// Gets or sets the message type. + /// public MessageType MessageType { get; set; } + /// + /// Gets or sets the message's sequence ID. + /// public string Id { get; set; } + /// + /// Gets or sets the message's method/command name. + /// public string Method { get; set; } + /// + /// Gets or sets a JToken containing the contents of the message. + /// public JToken Contents { get; set; } + /// + /// Gets or sets a JToken containing error details. + /// public JToken Error { get; set; } - + + /// + /// Creates a message with an Unknown type. + /// + /// A message with Unknown type. public static Message Unknown() { return new Message @@ -40,6 +60,13 @@ public static Message Unknown() }; } + /// + /// Creates a message with a Request type. + /// + /// The sequence ID of the request. + /// The method name of the request. + /// The contents of the request. + /// A message with a Request type. public static Message Request(string id, string method, JToken contents) { return new Message @@ -51,6 +78,13 @@ public static Message Request(string id, string method, JToken contents) }; } + /// + /// Creates a message with a Response type. + /// + /// The sequence ID of the original request. + /// The method name of the original request. + /// The contents of the response. + /// A message with a Response type. public static Message Response(string id, string method, JToken contents) { return new Message @@ -62,6 +96,13 @@ public static Message Response(string id, string method, JToken contents) }; } + /// + /// Creates a message with a Response type and error details. + /// + /// The sequence ID of the original request. + /// The method name of the original request. + /// The error details of the response. + /// A message with a Response type and error details. public static Message ResponseError(string id, string method, JToken error) { return new Message @@ -73,6 +114,12 @@ public static Message ResponseError(string id, string method, JToken error) }; } + /// + /// Creates a message with an Event type. + /// + /// The method name of the event. + /// The contents of the event. + /// A message with an Event type. public static Message Event(string method, JToken contents) { return new Message @@ -83,6 +130,5 @@ public static Message Event(string method, JToken contents) }; } } - } diff --git a/src/PowerShellEditorServices.Protocol/MessageProtocol/MessageDispatcher.cs b/src/PowerShellEditorServices.Protocol/MessageProtocol/MessageDispatcher.cs index fa7de56b2..07dc0a722 100644 --- a/src/PowerShellEditorServices.Protocol/MessageProtocol/MessageDispatcher.cs +++ b/src/PowerShellEditorServices.Protocol/MessageProtocol/MessageDispatcher.cs @@ -3,32 +3,110 @@ // Licensed under the MIT license. See LICENSE file in the project root for full license information. // +using Microsoft.PowerShell.EditorServices.Utility; +using Nito.AsyncEx; using System; using System.Collections.Generic; +using System.Threading; using System.Threading.Tasks; namespace Microsoft.PowerShell.EditorServices.Protocol.MessageProtocol { - public class MessageDispatcher + public class MessageDispatcher { - private Dictionary> requestHandlers = - new Dictionary>(); + #region Fields - private Dictionary> eventHandlers = - new Dictionary>(); + private MessageReader messageReader; + private MessageWriter messageWriter; + private AsyncContextThread messageLoopThread; - public void AddRequestHandler( - RequestType requestType, - Func, Task> requestHandler) + private Dictionary> requestHandlers = + new Dictionary>(); + + private Dictionary> eventHandlers = + new Dictionary>(); + + private Action responseHandler; + + #endregion + + #region Properties + + public SynchronizationContext SynchronizationContext { get; private set; } + + public bool InMessageLoopThread + { + get + { + // We're in the same thread as the message loop if the + // current synchronization context equals the one we + // know. + return SynchronizationContext.Current == this.SynchronizationContext; + } + } + + #endregion + + #region Constructors + + public MessageDispatcher( + MessageReader messageReader, + MessageWriter messageWriter) + { + this.messageReader = messageReader; + this.messageWriter = messageWriter; + } + + #endregion + + #region Public Methods + + public void Start() { - // TODO: Error or replace existing handler? + // Start the main message loop thread. The Task is + // not explicitly awaited because it is running on + // an independent background thread. + this.messageLoopThread = new AsyncContextThread(true); + this.messageLoopThread + .Factory + .Run(this.ListenForMessages) + .ContinueWith(this.OnListenTaskCompleted); + } + + public void Stop() + { + // By disposing the thread we cancel all existing work + // and cause the thread to be destroyed. + this.messageLoopThread.Dispose(); + } + + public void SetRequestHandler( + RequestType requestType, + Func, Task> requestHandler) + { + this.SetRequestHandler( + requestType, + requestHandler, + false); + } + + public void SetRequestHandler( + RequestType requestType, + Func, Task> requestHandler, + bool overrideExisting) + { + if (overrideExisting) + { + // Remove the existing handler so a new one can be set + this.requestHandlers.Remove(requestType.MethodName); + } this.requestHandlers.Add( - requestType.TypeName, - (requestMessage, session, messageWriter) => + requestType.MethodName, + (requestMessage, messageWriter) => { var requestContext = - new RequestContext( + new RequestContext( requestMessage, messageWriter); @@ -39,17 +117,34 @@ public void AddRequestHandler( typedParams = requestMessage.Contents.ToObject(); } - return requestHandler(typedParams, session, requestContext); + return requestHandler(typedParams, requestContext); }); } - public void AddEventHandler( + public void SetEventHandler( EventType eventType, - Func eventHandler) + Func eventHandler) { + this.SetEventHandler( + eventType, + eventHandler, + false); + } + + public void SetEventHandler( + EventType eventType, + Func eventHandler, + bool overrideExisting) + { + if (overrideExisting) + { + // Remove the existing handler so a new one can be set + this.eventHandlers.Remove(eventType.MethodName); + } + this.eventHandlers.Add( eventType.MethodName, - (eventMessage, session, messageWriter) => + (eventMessage, messageWriter) => { var eventContext = new EventContext(messageWriter); @@ -60,33 +155,103 @@ public void AddEventHandler( typedParams = eventMessage.Contents.ToObject(); } - return eventHandler(typedParams, session, eventContext); + return eventHandler(typedParams, eventContext); }); } - public async Task DispatchMessage( + public void SetResponseHandler(Action responseHandler) + { + this.responseHandler = responseHandler; + } + + public event EventHandler UnhandledException; + + protected void OnUnhandledException(Exception unhandledException) + { + if (this.UnhandledException != null) + { + this.UnhandledException(this, unhandledException); + } + } + + #endregion + + #region Private Methods + + private async Task ListenForMessages() + { + this.SynchronizationContext = SynchronizationContext.Current; + + // Run the message loop + bool isRunning = true; + while (isRunning) + { + Message newMessage = null; + + try + { + // Read a message from stdin + newMessage = await this.messageReader.ReadMessage(); + } + catch (MessageParseException e) + { + // TODO: Write an error response + + Logger.Write( + LogLevel.Error, + "Could not parse a message that was received:\r\n\r\n" + + e.ToString()); + + // Continue the loop + continue; + } + catch (Exception e) + { + var b = e.Message; + newMessage = null; + } + + // The message could be null if there was an error parsing the + // previous message. In this case, do not try to dispatch it. + if (newMessage != null) + { + // Process the message + await this.DispatchMessage( + newMessage, + this.messageWriter); + } + } + } + + private async Task DispatchMessage( Message messageToDispatch, - TSession sessionContext, MessageWriter messageWriter) { if (messageToDispatch.MessageType == MessageType.Request) { - Func requestHandler = null; + Func requestHandler = null; if (this.requestHandlers.TryGetValue(messageToDispatch.Method, out requestHandler)) { - await requestHandler(messageToDispatch, sessionContext, messageWriter); + await requestHandler(messageToDispatch, messageWriter); } else { // TODO: Message not supported error } } + else if (messageToDispatch.MessageType == MessageType.Response) + { + if (this.responseHandler != null) + { + this.responseHandler(messageToDispatch); + } + } else if (messageToDispatch.MessageType == MessageType.Event) { - Func eventHandler = null; + Func eventHandler = null; if (this.eventHandlers.TryGetValue(messageToDispatch.Method, out eventHandler)) { - await eventHandler(messageToDispatch, sessionContext, messageWriter); + await eventHandler(messageToDispatch, messageWriter); } else { @@ -98,6 +263,20 @@ public async Task DispatchMessage( // TODO: Return message not supported } } + + private void OnListenTaskCompleted(Task listenTask) + { + if (listenTask.IsFaulted) + { + this.OnUnhandledException(listenTask.Exception); + } + else if (listenTask.IsCompleted || listenTask.IsCanceled) + { + // TODO: Dispose of anything? + } + } + + #endregion } } diff --git a/src/PowerShellEditorServices.Protocol/MessageProtocol/MessageProtocolType.cs b/src/PowerShellEditorServices.Protocol/MessageProtocol/MessageProtocolType.cs new file mode 100644 index 000000000..7aa3d6c81 --- /dev/null +++ b/src/PowerShellEditorServices.Protocol/MessageProtocol/MessageProtocolType.cs @@ -0,0 +1,23 @@ +// +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +// + +namespace Microsoft.PowerShell.EditorServices.Protocol.MessageProtocol +{ + /// + /// Defines the possible message protocol types. + /// + public enum MessageProtocolType + { + /// + /// Identifies the language server message protocol. + /// + LanguageServer, + + /// + /// Identifies the debug adapter message protocol. + /// + DebugAdapter + } +} diff --git a/src/PowerShellEditorServices.Protocol/MessageProtocol/MessageWriter.cs b/src/PowerShellEditorServices.Protocol/MessageProtocol/MessageWriter.cs index 22d0fe361..3e6c0c784 100644 --- a/src/PowerShellEditorServices.Protocol/MessageProtocol/MessageWriter.cs +++ b/src/PowerShellEditorServices.Protocol/MessageProtocol/MessageWriter.cs @@ -89,8 +89,8 @@ public async Task WriteMessage(Message messageToWrite) } } - public async Task WriteRequest( - RequestType requestType, + public async Task WriteRequest( + RequestType requestType, TParams requestParams, int requestId) { @@ -103,7 +103,7 @@ public async Task WriteRequest( await this.WriteMessage( Message.Request( requestId.ToString(), - requestType.TypeName, + requestType.MethodName, contentObject)); } diff --git a/src/PowerShellEditorServices.Protocol/MessageProtocol/RequestContext.cs b/src/PowerShellEditorServices.Protocol/MessageProtocol/RequestContext.cs index 7624de04f..a578f4668 100644 --- a/src/PowerShellEditorServices.Protocol/MessageProtocol/RequestContext.cs +++ b/src/PowerShellEditorServices.Protocol/MessageProtocol/RequestContext.cs @@ -8,7 +8,7 @@ namespace Microsoft.PowerShell.EditorServices.Protocol.MessageProtocol { - public class RequestContext + public class RequestContext { private Message requestMessage; private MessageWriter messageWriter; @@ -34,7 +34,7 @@ await this.messageWriter.WriteEvent( eventParams); } - public async Task SendError(TError errorDetails) + public async Task SendError(object errorDetails) { await this.messageWriter.WriteMessage( Message.ResponseError( diff --git a/src/PowerShellEditorServices.Protocol/MessageProtocol/RequestType.cs b/src/PowerShellEditorServices.Protocol/MessageProtocol/RequestType.cs index 141fea530..8391e647f 100644 --- a/src/PowerShellEditorServices.Protocol/MessageProtocol/RequestType.cs +++ b/src/PowerShellEditorServices.Protocol/MessageProtocol/RequestType.cs @@ -11,15 +11,15 @@ namespace Microsoft.PowerShell.EditorServices.Protocol.MessageProtocol { - public class RequestType + public class RequestType { - public string TypeName { get; private set; } + public string MethodName { get; private set; } - public static RequestType Create(string typeName) + public static RequestType Create(string typeName) { - return new RequestType() + return new RequestType() { - TypeName = typeName + MethodName = typeName }; } } diff --git a/src/PowerShellEditorServices.Protocol/LanguageServer/JsonRpcMessageSerializer.cs b/src/PowerShellEditorServices.Protocol/MessageProtocol/Serializers/JsonRpcMessageSerializer.cs similarity index 94% rename from src/PowerShellEditorServices.Protocol/LanguageServer/JsonRpcMessageSerializer.cs rename to src/PowerShellEditorServices.Protocol/MessageProtocol/Serializers/JsonRpcMessageSerializer.cs index 7f6bb3a21..f53a908f4 100644 --- a/src/PowerShellEditorServices.Protocol/LanguageServer/JsonRpcMessageSerializer.cs +++ b/src/PowerShellEditorServices.Protocol/MessageProtocol/Serializers/JsonRpcMessageSerializer.cs @@ -8,8 +8,12 @@ using Newtonsoft.Json.Linq; using System; -namespace Microsoft.PowerShell.EditorServices.Protocol.LanguageServer +namespace Microsoft.PowerShell.EditorServices.Protocol.MessageProtocol.Serializers { + /// + /// Serializes messages in the JSON RPC format. Used primarily + /// for language servers. + /// public class JsonRpcMessageSerializer : IMessageSerializer { public JObject SerializeMessage(Message message) diff --git a/src/PowerShellEditorServices.Protocol/DebugAdapter/V8MessageSerializer.cs b/src/PowerShellEditorServices.Protocol/MessageProtocol/Serializers/V8MessageSerializer.cs similarity index 95% rename from src/PowerShellEditorServices.Protocol/DebugAdapter/V8MessageSerializer.cs rename to src/PowerShellEditorServices.Protocol/MessageProtocol/Serializers/V8MessageSerializer.cs index 19276d108..d9e4d94d7 100644 --- a/src/PowerShellEditorServices.Protocol/DebugAdapter/V8MessageSerializer.cs +++ b/src/PowerShellEditorServices.Protocol/MessageProtocol/Serializers/V8MessageSerializer.cs @@ -8,8 +8,11 @@ using Newtonsoft.Json.Linq; using System; -namespace Microsoft.PowerShell.EditorServices.Protocol.DebugAdapter +namespace Microsoft.PowerShell.EditorServices.Protocol.MessageProtocol.Serializers { + /// + /// Serializes messages in the V8 format. Used primarily for debug adapters. + /// public class V8MessageSerializer : IMessageSerializer { public JObject SerializeMessage(Message message) diff --git a/src/PowerShellEditorServices.Protocol/PowerShellEditorServices.Protocol.csproj b/src/PowerShellEditorServices.Protocol/PowerShellEditorServices.Protocol.csproj index be0cc896b..97bf60a12 100644 --- a/src/PowerShellEditorServices.Protocol/PowerShellEditorServices.Protocol.csproj +++ b/src/PowerShellEditorServices.Protocol/PowerShellEditorServices.Protocol.csproj @@ -57,11 +57,16 @@ + + + + + @@ -86,7 +91,8 @@ - + + @@ -94,30 +100,39 @@ + + + + - - - - + + + + - - + + + + + + + diff --git a/src/PowerShellEditorServices.Host/DebugAdapter.cs b/src/PowerShellEditorServices.Protocol/Server/DebugAdapter.cs similarity index 61% rename from src/PowerShellEditorServices.Host/DebugAdapter.cs rename to src/PowerShellEditorServices.Protocol/Server/DebugAdapter.cs index 87a252f97..a3fdaf683 100644 --- a/src/PowerShellEditorServices.Host/DebugAdapter.cs +++ b/src/PowerShellEditorServices.Protocol/Server/DebugAdapter.cs @@ -5,124 +5,88 @@ using Microsoft.PowerShell.EditorServices.Protocol.DebugAdapter; using Microsoft.PowerShell.EditorServices.Protocol.MessageProtocol; +using Microsoft.PowerShell.EditorServices.Protocol.MessageProtocol.Channel; +using Microsoft.PowerShell.EditorServices.Protocol.Server; using Microsoft.PowerShell.EditorServices.Utility; using System; using System.Collections.Generic; using System.Linq; +using System.Management.Automation; using System.Threading.Tasks; -namespace Microsoft.PowerShell.EditorServices.Host +namespace Microsoft.PowerShell.EditorServices.Protocol.Server { - internal class DebugAdapter : IMessageProcessor + public class DebugAdapter : DebugAdapterBase { - private MessageDispatcher messageDispatcher; + private EditorSession editorSession; - public DebugAdapter() + public DebugAdapter() : this(new StdioServerChannel()) { - this.messageDispatcher = new MessageDispatcher(); } - public void Initialize() + public DebugAdapter(ChannelBase serverChannel) : base(serverChannel) { - // Register all supported message types - - this.AddRequestHandler(InitializeRequest.Type, this.HandleInitializeRequest); - this.AddRequestHandler(LaunchRequest.Type, this.HandleLaunchRequest); - this.AddRequestHandler(AttachRequest.Type, this.HandleAttachRequest); - this.AddRequestHandler(DisconnectRequest.Type, this.HandleDisconnectRequest); - - this.AddRequestHandler(SetBreakpointsRequest.Type, this.HandleSetBreakpointsRequest); - this.AddRequestHandler(SetExceptionBreakpointsRequest.Type, this.HandleSetExceptionBreakpointsRequest); - - this.AddRequestHandler(ContinueRequest.Type, this.HandleContinueRequest); - this.AddRequestHandler(NextRequest.Type, this.HandleNextRequest); - this.AddRequestHandler(StepInRequest.Type, this.HandleStepInRequest); - this.AddRequestHandler(StepOutRequest.Type, this.HandleStepOutRequest); - this.AddRequestHandler(PauseRequest.Type, this.HandlePauseRequest); - - this.AddRequestHandler(ThreadsRequest.Type, this.HandleThreadsRequest); - this.AddRequestHandler(StackTraceRequest.Type, this.HandleStackTraceRequest); - this.AddRequestHandler(ScopesRequest.Type, this.HandleScopesRequest); - this.AddRequestHandler(VariablesRequest.Type, this.HandleVariablesRequest); - this.AddRequestHandler(SourceRequest.Type, this.HandleSourceRequest); - this.AddRequestHandler(EvaluateRequest.Type, this.HandleEvaluateRequest); + this.editorSession = new EditorSession(); + this.editorSession.StartSession(); + this.editorSession.DebugService.DebuggerStopped += this.DebugService_DebuggerStopped; } - public void AddRequestHandler( - RequestType requestType, - Func, Task> requestHandler) + protected override void Initialize() { - this.messageDispatcher.AddRequestHandler( - requestType, - requestHandler); - } + // Register all supported message types - public void AddEventHandler( - EventType eventType, - Func eventHandler) - { - this.messageDispatcher.AddEventHandler( - eventType, - eventHandler); - } - - public async Task ProcessMessage( - Message messageToProcess, - EditorSession editorSession, - MessageWriter messageWriter) - { - await this.messageDispatcher.DispatchMessage( - messageToProcess, - editorSession, - messageWriter); + this.SetRequestHandler(LaunchRequest.Type, this.HandleLaunchRequest); + this.SetRequestHandler(AttachRequest.Type, this.HandleAttachRequest); + this.SetRequestHandler(DisconnectRequest.Type, this.HandleDisconnectRequest); + + this.SetRequestHandler(SetBreakpointsRequest.Type, this.HandleSetBreakpointsRequest); + this.SetRequestHandler(SetExceptionBreakpointsRequest.Type, this.HandleSetExceptionBreakpointsRequest); + + this.SetRequestHandler(ContinueRequest.Type, this.HandleContinueRequest); + this.SetRequestHandler(NextRequest.Type, this.HandleNextRequest); + this.SetRequestHandler(StepInRequest.Type, this.HandleStepInRequest); + this.SetRequestHandler(StepOutRequest.Type, this.HandleStepOutRequest); + this.SetRequestHandler(PauseRequest.Type, this.HandlePauseRequest); + + this.SetRequestHandler(ThreadsRequest.Type, this.HandleThreadsRequest); + this.SetRequestHandler(StackTraceRequest.Type, this.HandleStackTraceRequest); + this.SetRequestHandler(ScopesRequest.Type, this.HandleScopesRequest); + this.SetRequestHandler(VariablesRequest.Type, this.HandleVariablesRequest); + this.SetRequestHandler(SourceRequest.Type, this.HandleSourceRequest); + this.SetRequestHandler(EvaluateRequest.Type, this.HandleEvaluateRequest); } #region Built-in Message Handlers - protected async Task HandleInitializeRequest( - InitializeRequestArguments initializeParams, - EditorSession editorSession, - RequestContext requestContext) - { - // Send the Initialized event first so that we get breakpoints - await requestContext.SendEvent( - InitializedEvent.Type, - null); - - // Now send the Initialize response to continue setup - await requestContext.SendResult(new object()); - } - protected async Task HandleLaunchRequest( LaunchRequestArguments launchParams, - EditorSession editorSession, - RequestContext requestContext) + RequestContext requestContext) { // Execute the given PowerShell script and send the response. // Note that we aren't waiting for execution to complete here // because the debugger could stop while the script executes. - editorSession.PowerShellContext - .ExecuteScriptAtPath(launchParams.Program) - .ContinueWith( - async (t) => - { - Logger.Write(LogLevel.Verbose, "Execution completed, terminating..."); + Task executeTask = + editorSession.PowerShellContext + .ExecuteScriptAtPath(launchParams.Program) + .ContinueWith( + async (t) => + { + Logger.Write(LogLevel.Verbose, "Execution completed, terminating..."); - await requestContext.SendEvent( - TerminatedEvent.Type, - null); + await requestContext.SendEvent( + TerminatedEvent.Type, + null); - // TODO: Find a way to exit more gracefully! - Environment.Exit(0); - }); + // Stop the server + this.Stop(); + }); await requestContext.SendResult(null); } protected Task HandleAttachRequest( AttachRequestArguments attachParams, - EditorSession editorSession, - RequestContext requestContext) + RequestContext requestContext) { // TODO: Implement this once we support attaching to processes throw new NotImplementedException(); @@ -130,8 +94,7 @@ protected Task HandleAttachRequest( protected Task HandleDisconnectRequest( object disconnectParams, - EditorSession editorSession, - RequestContext requestContext) + RequestContext requestContext) { EventHandler handler = null; @@ -143,8 +106,8 @@ protected Task HandleDisconnectRequest( await requestContext.SendResult(null); editorSession.PowerShellContext.SessionStateChanged -= handler; - // TODO: Find a way to exit more gracefully! - Environment.Exit(0); + // Stop the server + this.Stop(); } }; @@ -156,8 +119,7 @@ protected Task HandleDisconnectRequest( protected async Task HandleSetBreakpointsRequest( SetBreakpointsRequestArguments setBreakpointsParams, - EditorSession editorSession, - RequestContext requestContext) + RequestContext requestContext) { ScriptFile scriptFile = editorSession.Workspace.GetFile( @@ -173,15 +135,14 @@ await requestContext.SendResult( { Breakpoints = breakpoints - .Select(Breakpoint.Create) + .Select(Protocol.DebugAdapter.Breakpoint.Create) .ToArray() }); } protected async Task HandleSetExceptionBreakpointsRequest( SetExceptionBreakpointsRequestArguments setExceptionBreakpointsParams, - EditorSession editorSession, - RequestContext requestContext) + RequestContext requestContext) { // TODO: Handle this appropriately @@ -190,8 +151,7 @@ protected async Task HandleSetExceptionBreakpointsRequest( protected async Task HandleContinueRequest( object continueParams, - EditorSession editorSession, - RequestContext requestContext) + RequestContext requestContext) { editorSession.DebugService.Continue(); @@ -200,8 +160,7 @@ protected async Task HandleContinueRequest( protected async Task HandleNextRequest( object nextParams, - EditorSession editorSession, - RequestContext requestContext) + RequestContext requestContext) { editorSession.DebugService.StepOver(); @@ -210,8 +169,7 @@ protected async Task HandleNextRequest( protected Task HandlePauseRequest( object pauseParams, - EditorSession editorSession, - RequestContext requestContext) + RequestContext requestContext) { editorSession.DebugService.Break(); @@ -221,8 +179,7 @@ protected Task HandlePauseRequest( protected async Task HandleStepInRequest( object stepInParams, - EditorSession editorSession, - RequestContext requestContext) + RequestContext requestContext) { editorSession.DebugService.StepIn(); @@ -231,8 +188,7 @@ protected async Task HandleStepInRequest( protected async Task HandleStepOutRequest( object stepOutParams, - EditorSession editorSession, - RequestContext requestContext) + RequestContext requestContext) { editorSession.DebugService.StepOut(); @@ -241,8 +197,7 @@ protected async Task HandleStepOutRequest( protected async Task HandleThreadsRequest( object threadsParams, - EditorSession editorSession, - RequestContext requestContext) + RequestContext requestContext) { await requestContext.SendResult( new ThreadsResponseBody @@ -261,8 +216,7 @@ await requestContext.SendResult( protected async Task HandleStackTraceRequest( StackTraceRequestArguments stackTraceParams, - EditorSession editorSession, - RequestContext requestContext) + RequestContext requestContext) { StackFrameDetails[] stackFrames = editorSession.DebugService.GetStackFrames(); @@ -288,8 +242,7 @@ await requestContext.SendResult( protected async Task HandleScopesRequest( ScopesRequestArguments scopesParams, - EditorSession editorSession, - RequestContext requestContext) + RequestContext requestContext) { VariableScope[] variableScopes = editorSession.DebugService.GetVariableScopes( @@ -307,8 +260,7 @@ await requestContext.SendResult( protected async Task HandleVariablesRequest( VariablesRequestArguments variablesParams, - EditorSession editorSession, - RequestContext requestContext) + RequestContext requestContext) { VariableDetailsBase[] variables = editorSession.DebugService.GetVariables( @@ -336,8 +288,7 @@ protected async Task HandleVariablesRequest( protected Task HandleSourceRequest( SourceRequestArguments sourceParams, - EditorSession editorSession, - RequestContext requestContext) + RequestContext requestContext) { // TODO: Implement this message. For now, doesn't seem to // be a problem that it's missing. @@ -347,8 +298,7 @@ protected Task HandleSourceRequest( protected async Task HandleEvaluateRequest( EvaluateRequestArguments evaluateParams, - EditorSession editorSession, - RequestContext requestContext) + RequestContext requestContext) { VariableDetails result = await editorSession.DebugService.EvaluateExpression( @@ -375,6 +325,27 @@ await requestContext.SendResult( } #endregion + + #region Event Handlers + + async void DebugService_DebuggerStopped(object sender, DebuggerStopEventArgs e) + { + await this.SendEvent( + StoppedEvent.Type, + new StoppedEventBody + { + Source = new Source + { + Path = e.InvocationInfo.ScriptName, + }, + Line = e.InvocationInfo.ScriptLineNumber, + Column = e.InvocationInfo.OffsetInLine, + ThreadId = 1, // TODO: Change this based on context + Reason = "breakpoint" // TODO: Change this based on context + }); + } + + #endregion } } diff --git a/src/PowerShellEditorServices.Protocol/Server/DebugAdapterBase.cs b/src/PowerShellEditorServices.Protocol/Server/DebugAdapterBase.cs new file mode 100644 index 000000000..c8492c2f4 --- /dev/null +++ b/src/PowerShellEditorServices.Protocol/Server/DebugAdapterBase.cs @@ -0,0 +1,63 @@ +// +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +// + +using Microsoft.PowerShell.EditorServices.Protocol.DebugAdapter; +using Microsoft.PowerShell.EditorServices.Protocol.MessageProtocol; +using Microsoft.PowerShell.EditorServices.Protocol.MessageProtocol.Channel; +using System.Threading.Tasks; + +namespace Microsoft.PowerShell.EditorServices.Protocol.Server +{ + public abstract class DebugAdapterBase : ProtocolServer + { + public DebugAdapterBase(ChannelBase serverChannel) + : base (serverChannel, MessageProtocolType.DebugAdapter) + { + } + + /// + /// Overridden by the subclass to provide initialization + /// logic after the server channel is started. + /// + protected abstract void Initialize(); + + /// + /// Can be overridden by the subclass to provide shutdown + /// logic before the server exits. + /// + protected virtual void Shutdown() + { + // No default implementation yet. + } + + protected override void OnStart() + { + // Register handlers for server lifetime messages + this.SetRequestHandler(InitializeRequest.Type, this.HandleInitializeRequest); + + // Initialize the implementation class + this.Initialize(); + } + + protected override void OnStop() + { + this.Shutdown(); + } + + private async Task HandleInitializeRequest( + object shutdownParams, + RequestContext requestContext) + { + // Send the Initialized event first so that we get breakpoints + await requestContext.SendEvent( + InitializedEvent.Type, + null); + + // Now send the Initialize response to continue setup + await requestContext.SendResult(new object()); + } + } +} + diff --git a/src/PowerShellEditorServices.Host/LanguageServer.cs b/src/PowerShellEditorServices.Protocol/Server/LanguageServer.cs similarity index 88% rename from src/PowerShellEditorServices.Host/LanguageServer.cs rename to src/PowerShellEditorServices.Protocol/Server/LanguageServer.cs index 5751a767d..704380a64 100644 --- a/src/PowerShellEditorServices.Host/LanguageServer.cs +++ b/src/PowerShellEditorServices.Protocol/Server/LanguageServer.cs @@ -3,99 +3,81 @@ // Licensed under the MIT license. See LICENSE file in the project root for full license information. // -using DebugAdapterMessages = Microsoft.PowerShell.EditorServices.Protocol.DebugAdapter; using Microsoft.PowerShell.EditorServices.Protocol.LanguageServer; using Microsoft.PowerShell.EditorServices.Protocol.MessageProtocol; +using Microsoft.PowerShell.EditorServices.Protocol.MessageProtocol.Channel; using Microsoft.PowerShell.EditorServices.Utility; using Nito.AsyncEx; using System; using System.Collections.Generic; +using System.IO; using System.Linq; using System.Management.Automation; using System.Management.Automation.Language; using System.Text.RegularExpressions; using System.Threading; using System.Threading.Tasks; -using System.IO; +using DebugAdapterMessages = Microsoft.PowerShell.EditorServices.Protocol.DebugAdapter; -namespace Microsoft.PowerShell.EditorServices.Host +namespace Microsoft.PowerShell.EditorServices.Protocol.Server { - internal class LanguageServer : IMessageProcessor + public class LanguageServer : LanguageServerBase { private static CancellationTokenSource existingRequestCancellation; - private MessageDispatcher messageDispatcher; + private EditorSession editorSession; private LanguageServerSettings currentSettings = new LanguageServerSettings(); - public LanguageServer() + public LanguageServer() : this(new StdioServerChannel()) { - this.messageDispatcher = new MessageDispatcher(); } - public void Initialize() + public LanguageServer(ChannelBase serverChannel) : base(serverChannel) { - // Register all supported message types - - this.AddRequestHandler(InitializeRequest.Type, this.HandleInitializeRequest); - this.AddRequestHandler(ShutdownRequest.Type, this.HandleShutdownRequest); - this.AddEventHandler(ExitNotification.Type, this.HandleExitNotification); - - this.AddEventHandler(DidOpenTextDocumentNotification.Type, this.HandleDidOpenTextDocumentNotification); - this.AddEventHandler(DidCloseTextDocumentNotification.Type, this.HandleDidCloseTextDocumentNotification); - this.AddEventHandler(DidChangeTextDocumentNotification.Type, this.HandleDidChangeTextDocumentNotification); - this.AddEventHandler(DidChangeConfigurationNotification.Type, this.HandleDidChangeConfigurationNotification); - - this.AddRequestHandler(DefinitionRequest.Type, this.HandleDefinitionRequest); - this.AddRequestHandler(ReferencesRequest.Type, this.HandleReferencesRequest); - this.AddRequestHandler(CompletionRequest.Type, this.HandleCompletionRequest); - this.AddRequestHandler(CompletionResolveRequest.Type, this.HandleCompletionResolveRequest); - this.AddRequestHandler(SignatureHelpRequest.Type, this.HandleSignatureHelpRequest); - this.AddRequestHandler(DocumentHighlightRequest.Type, this.HandleDocumentHighlightRequest); - this.AddRequestHandler(HoverRequest.Type, this.HandleHoverRequest); - this.AddRequestHandler(DocumentSymbolRequest.Type, this.HandleDocumentSymbolRequest); - this.AddRequestHandler(WorkspaceSymbolRequest.Type, this.HandleWorkspaceSymbolRequest); - - this.AddRequestHandler(ShowOnlineHelpRequest.Type, this.HandleShowOnlineHelpRequest); - this.AddRequestHandler(ExpandAliasRequest.Type, this.HandleExpandAliasRequest); - - this.AddRequestHandler(DebugAdapterMessages.EvaluateRequest.Type, this.HandleEvaluateRequest); + this.editorSession = new EditorSession(); + this.editorSession.StartSession(); + this.editorSession.PowerShellContext.OutputWritten += this.powerShellContext_OutputWritten; } - public void AddRequestHandler( - RequestType requestType, - Func, Task> requestHandler) + protected override void Initialize() { - this.messageDispatcher.AddRequestHandler( - requestType, - requestHandler); - } + // Register all supported message types - public void AddEventHandler( - EventType eventType, - Func eventHandler) - { - this.messageDispatcher.AddEventHandler( - eventType, - eventHandler); + this.SetRequestHandler(InitializeRequest.Type, this.HandleInitializeRequest); + + this.SetEventHandler(DidOpenTextDocumentNotification.Type, this.HandleDidOpenTextDocumentNotification); + this.SetEventHandler(DidCloseTextDocumentNotification.Type, this.HandleDidCloseTextDocumentNotification); + this.SetEventHandler(DidChangeTextDocumentNotification.Type, this.HandleDidChangeTextDocumentNotification); + this.SetEventHandler(DidChangeConfigurationNotification.Type, this.HandleDidChangeConfigurationNotification); + + this.SetRequestHandler(DefinitionRequest.Type, this.HandleDefinitionRequest); + this.SetRequestHandler(ReferencesRequest.Type, this.HandleReferencesRequest); + this.SetRequestHandler(CompletionRequest.Type, this.HandleCompletionRequest); + this.SetRequestHandler(CompletionResolveRequest.Type, this.HandleCompletionResolveRequest); + this.SetRequestHandler(SignatureHelpRequest.Type, this.HandleSignatureHelpRequest); + this.SetRequestHandler(DocumentHighlightRequest.Type, this.HandleDocumentHighlightRequest); + this.SetRequestHandler(HoverRequest.Type, this.HandleHoverRequest); + this.SetRequestHandler(DocumentSymbolRequest.Type, this.HandleDocumentSymbolRequest); + this.SetRequestHandler(WorkspaceSymbolRequest.Type, this.HandleWorkspaceSymbolRequest); + + this.SetRequestHandler(ShowOnlineHelpRequest.Type, this.HandleShowOnlineHelpRequest); + this.SetRequestHandler(ExpandAliasRequest.Type, this.HandleExpandAliasRequest); + + this.SetRequestHandler(DebugAdapterMessages.EvaluateRequest.Type, this.HandleEvaluateRequest); } - public async Task ProcessMessage( - Message messageToProcess, - EditorSession editorSession, - MessageWriter messageWriter) + protected override void Shutdown() { - await this.messageDispatcher.DispatchMessage( - messageToProcess, - editorSession, - messageWriter); + Logger.Write(LogLevel.Normal, "Language service is shutting down..."); + + this.editorSession.Dispose(); } #region Built-in Message Handlers protected async Task HandleInitializeRequest( InitializeRequest initializeParams, - EditorSession editorSession, - RequestContext requestContext) + RequestContext requestContext) { // Grab the workspace path from the parameters editorSession.Workspace.WorkspacePath = initializeParams.RootPath; @@ -125,20 +107,9 @@ await requestContext.SendResult( }); } - protected Task HandleShutdownRequest( - object shutdownParams, - EditorSession editorSession, - RequestContext requestContext) - { - // TODO: Shut down! - - return Task.FromResult(true); - } - protected async Task HandleShowOnlineHelpRequest( string helpParams, - EditorSession editorSession, - RequestContext requestContext) + RequestContext requestContext) { if (helpParams == null) { helpParams = "get-help"; } @@ -155,8 +126,7 @@ await editorSession.PowerShellContext.ExecuteCommand( private async Task HandleExpandAliasRequest( string content, - EditorSession editorSession, - RequestContext requestContext) + RequestContext requestContext) { var script = @" function __Expand-Alias { @@ -183,28 +153,17 @@ Sort Start -Descending }"; var psCommand = new PSCommand(); psCommand.AddScript(script); - await editorSession.PowerShellContext.ExecuteCommand(psCommand); + await this.editorSession.PowerShellContext.ExecuteCommand(psCommand); psCommand = new PSCommand(); psCommand.AddCommand("__Expand-Alias").AddArgument(content); - var result = await editorSession.PowerShellContext.ExecuteCommand(psCommand); + var result = await this.editorSession.PowerShellContext.ExecuteCommand(psCommand); await requestContext.SendResult(result.First().ToString()); } - protected Task HandleExitNotification( - object exitParams, - EditorSession editorSession, - EventContext eventContext) - { - // TODO: Shut down! - - return Task.FromResult(true); - } - protected Task HandleDidOpenTextDocumentNotification( DidOpenTextDocumentNotification openParams, - EditorSession editorSession, EventContext eventContext) { ScriptFile openedFile = @@ -225,7 +184,6 @@ protected Task HandleDidOpenTextDocumentNotification( protected Task HandleDidCloseTextDocumentNotification( TextDocumentIdentifier closeParams, - EditorSession editorSession, EventContext eventContext) { // Find and close the file in the current session @@ -243,7 +201,6 @@ protected Task HandleDidCloseTextDocumentNotification( protected Task HandleDidChangeTextDocumentNotification( DidChangeTextDocumentParams textChangeParams, - EditorSession editorSession, EventContext eventContext) { List changedFiles = new List(); @@ -272,7 +229,6 @@ protected Task HandleDidChangeTextDocumentNotification( protected async Task HandleDidChangeConfigurationNotification( DidChangeConfigurationParams configChangeParams, - EditorSession editorSession, EventContext eventContext) { bool oldScriptAnalysisEnabled = @@ -302,8 +258,7 @@ await PublishScriptDiagnostics( protected async Task HandleDefinitionRequest( TextDocumentPosition textDocumentPosition, - EditorSession editorSession, - RequestContext requestContext) + RequestContext requestContext) { ScriptFile scriptFile = editorSession.Workspace.GetFile( @@ -342,8 +297,7 @@ await editorSession.LanguageService.GetDefinitionOfSymbol( protected async Task HandleReferencesRequest( ReferencesParams referencesParams, - EditorSession editorSession, - RequestContext requestContext) + RequestContext requestContext) { ScriptFile scriptFile = editorSession.Workspace.GetFile( @@ -387,8 +341,7 @@ await editorSession.LanguageService.FindReferencesOfSymbol( protected async Task HandleCompletionRequest( TextDocumentPosition textDocumentPosition, - EditorSession editorSession, - RequestContext requestContext) + RequestContext requestContext) { int cursorLine = textDocumentPosition.Position.Line + 1; int cursorColumn = textDocumentPosition.Position.Character + 1; @@ -455,8 +408,7 @@ await editorSession.LanguageService.GetCompletionsInFile( protected async Task HandleCompletionResolveRequest( CompletionItem completionItem, - EditorSession editorSession, - RequestContext requestContext) + RequestContext requestContext) { if (completionItem.Kind == CompletionItemKind.Function) { @@ -483,8 +435,7 @@ protected async Task HandleCompletionResolveRequest( protected async Task HandleSignatureHelpRequest( TextDocumentPosition textDocumentPosition, - EditorSession editorSession, - RequestContext requestContext) + RequestContext requestContext) { ScriptFile scriptFile = editorSession.Workspace.GetFile( @@ -535,8 +486,7 @@ await requestContext.SendResult( protected async Task HandleDocumentHighlightRequest( TextDocumentPosition textDocumentPosition, - EditorSession editorSession, - RequestContext requestContext) + RequestContext requestContext) { ScriptFile scriptFile = editorSession.Workspace.GetFile( @@ -575,8 +525,7 @@ protected async Task HandleDocumentHighlightRequest( protected async Task HandleHoverRequest( TextDocumentPosition textDocumentPosition, - EditorSession editorSession, - RequestContext requestContext) + RequestContext requestContext) { ScriptFile scriptFile = editorSession.Workspace.GetFile( @@ -625,8 +574,7 @@ await requestContext.SendResult( protected async Task HandleDocumentSymbolRequest( TextDocumentIdentifier textDocumentIdentifier, - EditorSession editorSession, - RequestContext requestContext) + RequestContext requestContext) { ScriptFile scriptFile = editorSession.Workspace.GetFile( @@ -699,8 +647,7 @@ private string GetDecoratedSymbolName(SymbolReference symbolReference) protected async Task HandleWorkspaceSymbolRequest( WorkspaceSymbolParams workspaceSymbolParams, - EditorSession editorSession, - RequestContext requestContext) + RequestContext requestContext) { var symbols = new List(); @@ -748,8 +695,7 @@ private bool IsQueryMatch(string query, string symbolName) protected async Task HandleEvaluateRequest( DebugAdapterMessages.EvaluateRequestArguments evaluateParams, - EditorSession editorSession, - RequestContext requestContext) + RequestContext requestContext) { VariableDetails result = await editorSession.DebugService.EvaluateExpression( @@ -777,6 +723,21 @@ await requestContext.SendResult( #endregion + #region Event Handlers + + async void powerShellContext_OutputWritten(object sender, OutputWrittenEventArgs e) + { + await this.SendEvent( + DebugAdapterMessages.OutputEvent.Type, + new DebugAdapterMessages.OutputEventBody + { + Output = e.OutputText + (e.IncludeNewLine ? "\r\n" : string.Empty), + Category = (e.OutputType == OutputType.Error) ? "stderr" : "stdout" + }); + } + + #endregion + #region Helper Methods private static Range GetRangeFromScriptRegion(ScriptRegion scriptRegion) diff --git a/src/PowerShellEditorServices.Protocol/Server/LanguageServerBase.cs b/src/PowerShellEditorServices.Protocol/Server/LanguageServerBase.cs new file mode 100644 index 000000000..d869f1f07 --- /dev/null +++ b/src/PowerShellEditorServices.Protocol/Server/LanguageServerBase.cs @@ -0,0 +1,82 @@ +// +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +// + +using Microsoft.PowerShell.EditorServices.Protocol.LanguageServer; +using Microsoft.PowerShell.EditorServices.Protocol.MessageProtocol; +using Microsoft.PowerShell.EditorServices.Protocol.MessageProtocol.Channel; +using System.Threading.Tasks; + +namespace Microsoft.PowerShell.EditorServices.Protocol.Server +{ + public abstract class LanguageServerBase : ProtocolServer + { + private bool isStarted; + private ChannelBase serverChannel; + private TaskCompletionSource serverExitedTask; + + public LanguageServerBase(ChannelBase serverChannel) : + base(serverChannel, MessageProtocolType.LanguageServer) + { + this.serverChannel = serverChannel; + } + + protected override void OnStart() + { + // Register handlers for server lifetime messages + this.SetRequestHandler(ShutdownRequest.Type, this.HandleShutdownRequest); + this.SetEventHandler(ExitNotification.Type, this.HandleExitNotification); + + // Initialize the implementation class + this.Initialize(); + } + + protected override void OnStop() + { + this.Shutdown(); + } + + /// + /// Overridden by the subclass to provide initialization + /// logic after the server channel is started. + /// + protected abstract void Initialize(); + + /// + /// Can be overridden by the subclass to provide shutdown + /// logic before the server exits. + /// + protected virtual void Shutdown() + { + // No default implementation yet. + } + + private Task HandleShutdownRequest( + object shutdownParams, + RequestContext requestContext) + { + // Allow the implementor to shut down gracefully + this.Shutdown(); + + return requestContext.SendResult(new object()); + } + + private Task HandleExitNotification( + object exitParams, + EventContext eventContext) + { + // Stop the server channel + this.Stop(); + + // Notify any waiter that the server has exited + if (this.serverExitedTask != null) + { + this.serverExitedTask.SetResult(true); + } + + return Task.FromResult(true); + } + } +} + diff --git a/src/PowerShellEditorServices.Host/LanguageServerSettings.cs b/src/PowerShellEditorServices.Protocol/Server/LanguageServerSettings.cs similarity index 87% rename from src/PowerShellEditorServices.Host/LanguageServerSettings.cs rename to src/PowerShellEditorServices.Protocol/Server/LanguageServerSettings.cs index 5d46c9f46..256a81c99 100644 --- a/src/PowerShellEditorServices.Host/LanguageServerSettings.cs +++ b/src/PowerShellEditorServices.Protocol/Server/LanguageServerSettings.cs @@ -3,9 +3,9 @@ // Licensed under the MIT license. See LICENSE file in the project root for full license information. // -namespace Microsoft.PowerShell.EditorServices.Host +namespace Microsoft.PowerShell.EditorServices.Protocol.Server { - internal class LanguageServerSettings + public class LanguageServerSettings { public ScriptAnalysisSettings ScriptAnalysis { get; set; } @@ -23,7 +23,7 @@ public void Update(LanguageServerSettings settings) } } - internal class ScriptAnalysisSettings + public class ScriptAnalysisSettings { public bool? Enable { get; set; } @@ -41,7 +41,7 @@ public void Update(ScriptAnalysisSettings settings) } } - internal class SettingsWrapper + public class SettingsWrapper { // NOTE: This property is capitalized as 'Powershell' because the // mode name sent from the client is written as 'powershell' and diff --git a/src/PowerShellEditorServices.Protocol/Server/ProtocolServer.cs b/src/PowerShellEditorServices.Protocol/Server/ProtocolServer.cs new file mode 100644 index 000000000..e3292c9f1 --- /dev/null +++ b/src/PowerShellEditorServices.Protocol/Server/ProtocolServer.cs @@ -0,0 +1,151 @@ +// +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +// + +using Microsoft.PowerShell.EditorServices.Protocol.MessageProtocol; +using Microsoft.PowerShell.EditorServices.Protocol.MessageProtocol.Channel; +using Newtonsoft.Json.Linq; +using System; +using System.Threading; +using System.Threading.Tasks; + +namespace Microsoft.PowerShell.EditorServices.Protocol.Server +{ + public abstract class ProtocolServer + { + private bool isStarted; + private ChannelBase serverChannel; + private MessageProtocolType messageProtocolType; + private TaskCompletionSource serverExitedTask; + private SynchronizationContext originalSynchronizationContext; + + /// + /// Initializes an instance of the protocol server using the + /// specified channel for communication. + /// + /// The channel to use for communication with the client. + /// The type of message protocol used by the server. + public ProtocolServer( + ChannelBase serverChannel, + MessageProtocolType messageProtocolType) + { + this.serverChannel = serverChannel; + this.messageProtocolType = messageProtocolType; + this.originalSynchronizationContext = SynchronizationContext.Current; + } + + public void Start() + { + if (!this.isStarted) + { + // Start the provided server channel + this.serverChannel.Start(this.messageProtocolType); + + // Listen for unhandled exceptions from the dispatcher + this.serverChannel.MessageDispatcher.UnhandledException += MessageDispatcher_UnhandledException; + + // Notify implementation about server start + this.OnStart(); + + // Server is now started + this.isStarted = true; + } + } + + public void WaitForExit() + { + this.serverExitedTask = new TaskCompletionSource(); + this.serverExitedTask.Task.Wait(); + } + + public void Stop() + { + if (this.isStarted) + { + // Stop the implementation first + this.OnStop(); + + this.serverChannel.Stop(); + this.serverExitedTask.SetResult(true); + this.isStarted = false; + } + } + + public void SetRequestHandler( + RequestType requestType, + Func, Task> requestHandler) + { + this.serverChannel.MessageDispatcher.SetRequestHandler( + requestType, + requestHandler); + } + + public void SetEventHandler( + EventType eventType, + Func eventHandler) + { + this.serverChannel.MessageDispatcher.SetEventHandler( + eventType, + eventHandler); + } + + /// + /// Sends an event to the channel's endpoint. + /// + /// The event parameter type. + /// The type of event being sent. + /// The event parameters being sent. + /// A Task that tracks completion of the send operation. + public Task SendEvent( + EventType eventType, + TParams eventParams) + { + // In a server, some events could be raised from a different + // thread. To ensure that messages are written serially, + // dispatch the SendEvent call to the message loop thread. + + if (!this.serverChannel.MessageDispatcher.InMessageLoopThread) + { + this.serverChannel.MessageDispatcher.SynchronizationContext.Post( + async (obj) => + { + await this.serverChannel.MessageWriter.WriteMessage( + Message.Event( + eventType.MethodName, + JToken.FromObject(eventParams))); + }, null); + + return Task.FromResult(true); + } + else + { + return this.serverChannel.MessageWriter.WriteMessage( + Message.Event( + eventType.MethodName, + JToken.FromObject(eventParams))); + } + } + + protected virtual void OnStart() + { + } + + protected virtual void OnStop() + { + } + + private void MessageDispatcher_UnhandledException(object sender, Exception e) + { + if (this.serverExitedTask != null) + { + this.serverExitedTask.SetException(e); + } + else if (this.originalSynchronizationContext != null) + { + this.originalSynchronizationContext.Post(o => { throw e; }, null); + } + } + } +} + diff --git a/src/PowerShellEditorServices/Debugging/StackFrameDetails.cs b/src/PowerShellEditorServices/Debugging/StackFrameDetails.cs index 27bd4caa0..3bacc0ac5 100644 --- a/src/PowerShellEditorServices/Debugging/StackFrameDetails.cs +++ b/src/PowerShellEditorServices/Debugging/StackFrameDetails.cs @@ -47,6 +47,7 @@ public class StackFrameDetails /// /// /// A variable container with all the local variables for this stack frame. + /// /// A new instance of the StackFrameDetails class. static internal StackFrameDetails Create( CallStackFrame callStackFrame, diff --git a/src/PowerShellEditorServices/IHost.cs b/src/PowerShellEditorServices/IHost.cs deleted file mode 100644 index ef68a3437..000000000 --- a/src/PowerShellEditorServices/IHost.cs +++ /dev/null @@ -1,30 +0,0 @@ -// -// Copyright (c) Microsoft. All rights reserved. -// Licensed under the MIT license. See LICENSE file in the project root for full license information. -// - -using System; - -namespace Microsoft.PowerShell.EditorServices -{ - /// - /// Provides an interface for starting and identifying a host. - /// - public interface IHost - { - /// - /// Gets the host application's identifying name. - /// - string Name { get; } - - /// - /// Gets the host application's version number. - /// - Version Version { get; } - - /// - /// Starts the host's message pump. - /// - void Start(); - } -} diff --git a/src/PowerShellEditorServices/PowerShellEditorServices.csproj b/src/PowerShellEditorServices/PowerShellEditorServices.csproj index a7707d85d..a8d718113 100644 --- a/src/PowerShellEditorServices/PowerShellEditorServices.csproj +++ b/src/PowerShellEditorServices/PowerShellEditorServices.csproj @@ -66,7 +66,6 @@ - diff --git a/src/PowerShellEditorServices/Session/EditorSession.cs b/src/PowerShellEditorServices/Session/EditorSession.cs index e3c452a54..b99a1cb25 100644 --- a/src/PowerShellEditorServices/Session/EditorSession.cs +++ b/src/PowerShellEditorServices/Session/EditorSession.cs @@ -50,11 +50,7 @@ public class EditorSession /// Starts the session using the provided IConsoleHost implementation /// for the ConsoleService. /// - /// - /// An IConsoleHost implementation which is used to interact with the - /// host's user interface. - /// - public void StartSession(IConsoleHost consoleHost) + public void StartSession() { // Create a workspace to contain open files this.Workspace = new Workspace(); diff --git a/src/PowerShellEditorServices/Session/PowerShellContext.cs b/src/PowerShellEditorServices/Session/PowerShellContext.cs index 4ef1f4e56..3fbde817f 100644 --- a/src/PowerShellEditorServices/Session/PowerShellContext.cs +++ b/src/PowerShellEditorServices/Session/PowerShellContext.cs @@ -341,10 +341,24 @@ public async Task ExecuteScriptAtPath(string scriptPath) /// public void AbortExecution() { - Logger.Write(LogLevel.Verbose, "Execution abort requested..."); + if (this.SessionState != PowerShellContextState.Aborting) + { + Logger.Write(LogLevel.Verbose, "Execution abort requested..."); + + this.powerShell.BeginStop(null, null); + this.SessionState = PowerShellContextState.Aborting; - this.powerShell.BeginStop(null, null); - this.ResumeDebugger(DebuggerResumeAction.Stop); + if (this.IsDebuggerStopped) + { + this.ResumeDebugger(DebuggerResumeAction.Stop); + } + } + else + { + Logger.Write( + LogLevel.Verbose, + "Execution abort requested while already aborting"); + } } /// @@ -379,6 +393,13 @@ internal void ResumeDebugger(DebuggerResumeAction resumeAction) /// public void Dispose() { + // Do we need to abort a running execution? + if (this.SessionState == PowerShellContextState.Running || + this.IsDebuggerStopped) + { + this.AbortExecution(); + } + this.SessionState = PowerShellContextState.Disposed; if (this.powerShell != null) @@ -616,7 +637,12 @@ private void OnDebuggerStop(object sender, DebuggerStopEventArgs e) this.pipelineExecutionTask = new TaskCompletionSource(); // Update the session state - this.OnSessionStateChanged(this, new SessionStateChangedEventArgs(PowerShellContextState.Ready, PowerShellExecutionResult.Stopped, null)); + this.OnSessionStateChanged( + this, + new SessionStateChangedEventArgs( + PowerShellContextState.Ready, + PowerShellExecutionResult.Stopped, + null)); // Raise the event for the debugger service if (this.DebuggerStop != null) diff --git a/src/PowerShellEditorServices/Utility/Logger.cs b/src/PowerShellEditorServices/Utility/Logger.cs index b484cc452..512fca562 100644 --- a/src/PowerShellEditorServices/Utility/Logger.cs +++ b/src/PowerShellEditorServices/Utility/Logger.cs @@ -185,6 +185,11 @@ private bool TryOpenLogFile( { try { + // Make sure the log directory exists + Directory.CreateDirectory( + Path.GetDirectoryName( + logFilePath)); + // Open the log file for writing with UTF8 encoding this.textWriter = new StreamWriter( diff --git a/test/PowerShellEditorServices.Test.Host/AttachHelper.cs b/test/PowerShellEditorServices.Test.Host/AttachHelper.cs new file mode 100644 index 000000000..214aa2dba --- /dev/null +++ b/test/PowerShellEditorServices.Test.Host/AttachHelper.cs @@ -0,0 +1,111 @@ +// +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +// + +using EnvDTE; +using System; +using System.Linq; +using System.Runtime.InteropServices; + +namespace Microsoft.PowerShell.EditorServices.Test.Host +{ + internal class AttachHelper + { + public static void AttachToProcessIfDebugging(int processId) + { + if (System.Diagnostics.Debugger.IsAttached) + { + int tryCount = 5; + + while (tryCount-- > 0) + { + try + { + var dte = (DTE)Marshal.GetActiveObject("VisualStudio.DTE.12.0"); + var processes = dte.Debugger.LocalProcesses.OfType(); + var foundProcess = processes.SingleOrDefault(x => x.ProcessID == processId); + + //EnvDTE.Process foundProcess = null; + //for (int i = 0; i < dte.Debugger.LocalProcesses.Count; i++) + //{ + // foundProcess = dte.Debugger.LocalProcesses.Item(i) as EnvDTE.Process; + + // if (foundProcess != null && foundProcess.ProcessID == processId) + // { + // break; + // } + //} + + if (foundProcess != null) + { + foundProcess.Attach(); + break; + } + else + { + throw new InvalidOperationException("Could not find language service process!"); + } + } + catch (COMException) + { + // Wait a bit and try again + System.Threading.Thread.Sleep(1000); + } + } + } + } + } + + [ComImport, Guid("00000016-0000-0000-C000-000000000046"), InterfaceType(ComInterfaceType.InterfaceIsIUnknown)] + public interface IOleMessageFilter + { + [PreserveSig] + int HandleInComingCall(int dwCallType, IntPtr hTaskCaller, int dwTickCount, IntPtr lpInterfaceInfo); + + [PreserveSig] + int RetryRejectedCall(IntPtr hTaskCallee, int dwTickCount, int dwRejectType); + + [PreserveSig] + int MessagePending(IntPtr hTaskCallee, int dwTickCount, int dwPendingType); + } + + public class MessageFilter : IOleMessageFilter + { + private const int Handled = 0, RetryAllowed = 2, Retry = 99, Cancel = -1, WaitAndDispatch = 2; + + int IOleMessageFilter.HandleInComingCall(int dwCallType, IntPtr hTaskCaller, int dwTickCount, IntPtr lpInterfaceInfo) + { + return Handled; + } + + int IOleMessageFilter.RetryRejectedCall(IntPtr hTaskCallee, int dwTickCount, int dwRejectType) + { + return dwRejectType == RetryAllowed ? Retry : Cancel; + } + + int IOleMessageFilter.MessagePending(IntPtr hTaskCallee, int dwTickCount, int dwPendingType) + { + return WaitAndDispatch; + } + + public static void Register() + { + CoRegisterMessageFilter(new MessageFilter()); + } + + public static void Revoke() + { + CoRegisterMessageFilter(null); + } + + private static void CoRegisterMessageFilter(IOleMessageFilter newFilter) + { + IOleMessageFilter oldFilter; + CoRegisterMessageFilter(newFilter, out oldFilter); + } + + [DllImport("Ole32.dll")] + private static extern int CoRegisterMessageFilter(IOleMessageFilter newFilter, out IOleMessageFilter oldFilter); + } +} diff --git a/test/PowerShellEditorServices.Test.Host/DebugAdapterTests.cs b/test/PowerShellEditorServices.Test.Host/DebugAdapterTests.cs new file mode 100644 index 000000000..f42159f9d --- /dev/null +++ b/test/PowerShellEditorServices.Test.Host/DebugAdapterTests.cs @@ -0,0 +1,125 @@ +// +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +// + +using Microsoft.PowerShell.EditorServices.Protocol.Client; +using Microsoft.PowerShell.EditorServices.Protocol.DebugAdapter; +using Microsoft.PowerShell.EditorServices.Protocol.MessageProtocol; +using Microsoft.PowerShell.EditorServices.Protocol.MessageProtocol.Channel; +using System; +using System.IO; +using System.Threading.Tasks; +using Xunit; +using Xunit.Abstractions; +using Xunit.Sdk; + +namespace Microsoft.PowerShell.EditorServices.Test.Host +{ + public class DebugAdapterTests : IAsyncLifetime + { + private DebugAdapterClient debugAdapterClient; + private string DebugScriptPath = + Path.GetFullPath(@"..\..\..\PowerShellEditorServices.Test.Shared\Debugging\DebugTest.ps1"); + + public Task InitializeAsync() + { + string testLogPath = + Path.Combine( + AppDomain.CurrentDomain.BaseDirectory, + "logs", + this.GetType().Name, + Guid.NewGuid().ToString().Substring(0, 8) + ".log"); + + Console.WriteLine(" Output log at path: {0}", testLogPath); + + this.debugAdapterClient = + new DebugAdapterClient( + new StdioClientChannel( + "Microsoft.PowerShell.EditorServices.Host.exe", + "/debugAdapter", + "/logPath:\"" + testLogPath + "\"")); + + return this.debugAdapterClient.Start(); + } + + public Task DisposeAsync() + { + return this.debugAdapterClient.Stop(); + } + + [Fact] + public async Task DebugAdapterStopsOnBreakpoints() + { + await this.SendRequest( + SetBreakpointsRequest.Type, + new SetBreakpointsRequestArguments + { + Source = new Source + { + Path = DebugScriptPath + }, + Lines = new int[] { 5, 9 } + }); + + Task breakEventTask = this.WaitForEvent(StoppedEvent.Type); + await this.LaunchScript(DebugScriptPath); + + // Wait for a couple breakpoints + StoppedEventBody stoppedDetails = await breakEventTask; + Assert.Equal(DebugScriptPath, stoppedDetails.Source.Path); + Assert.Equal(5, stoppedDetails.Line); + + breakEventTask = this.WaitForEvent(StoppedEvent.Type); + await this.SendRequest(ContinueRequest.Type, new object()); + stoppedDetails = await breakEventTask; + Assert.Equal(DebugScriptPath, stoppedDetails.Source.Path); + Assert.Equal(9, stoppedDetails.Line); + + // Abort script execution + Task terminatedEvent = this.WaitForEvent(TerminatedEvent.Type); + await this.SendRequest(DisconnectRequest.Type, new object()); + await terminatedEvent; + } + + private Task LaunchScript(string scriptPath) + { + return this.debugAdapterClient.LaunchScript(scriptPath); + } + + private Task SendRequest( + RequestType requestType, + TParams requestParams) + { + return + this.debugAdapterClient.SendRequest( + requestType, + requestParams); + } + + private Task SendEvent(EventType eventType, TParams eventParams) + { + return + this.debugAdapterClient.SendEvent( + eventType, + eventParams); + } + + private Task WaitForEvent(EventType eventType) + { + TaskCompletionSource eventTask = new TaskCompletionSource(); + + this.debugAdapterClient.SetEventHandler( + eventType, + (p, ctx) => + { + eventTask.SetResult(p); + return Task.FromResult(true); + }, + true); // Override any existing handler + + return eventTask.Task; + } + } +} + diff --git a/test/PowerShellEditorServices.Test.Host/ScenarioTests.cs b/test/PowerShellEditorServices.Test.Host/LanguageServerTests.cs similarity index 83% rename from test/PowerShellEditorServices.Test.Host/ScenarioTests.cs rename to test/PowerShellEditorServices.Test.Host/LanguageServerTests.cs index c37720443..12a92d38e 100644 --- a/test/PowerShellEditorServices.Test.Host/ScenarioTests.cs +++ b/test/PowerShellEditorServices.Test.Host/LanguageServerTests.cs @@ -3,44 +3,47 @@ // Licensed under the MIT license. See LICENSE file in the project root for full license information. // +using Microsoft.PowerShell.EditorServices.Protocol.Client; using Microsoft.PowerShell.EditorServices.Protocol.DebugAdapter; using Microsoft.PowerShell.EditorServices.Protocol.LanguageServer; using Microsoft.PowerShell.EditorServices.Protocol.MessageProtocol; -using Newtonsoft.Json.Linq; +using Microsoft.PowerShell.EditorServices.Protocol.MessageProtocol.Channel; using System; -using System.Collections.Generic; using System.IO; using System.Linq; using System.Threading.Tasks; using Xunit; +using Xunit.Abstractions; namespace Microsoft.PowerShell.EditorServices.Test.Host { - public class ScenarioTests : IDisposable + public class LanguageServerTests : IAsyncLifetime { - private int messageId = 0; + private LanguageServiceClient languageServiceClient; - private LanguageServiceManager languageServiceManager = - new LanguageServiceManager(); - - private MessageReader MessageReader - { - get { return this.languageServiceManager.MessageReader; } - } - - private MessageWriter MessageWriter + public Task InitializeAsync() { - get { return this.languageServiceManager.MessageWriter; } + string testLogPath = + Path.Combine( + AppDomain.CurrentDomain.BaseDirectory, + "logs", + this.GetType().Name, + Guid.NewGuid().ToString().Substring(0, 8) + ".log"); + + Console.WriteLine(" Output log at path: {0}", testLogPath); + + this.languageServiceClient = + new LanguageServiceClient( + new StdioClientChannel( + "Microsoft.PowerShell.EditorServices.Host.exe", + "/logPath:\"" + testLogPath + "\"")); + + return this.languageServiceClient.Start(); } - public ScenarioTests() + public Task DisposeAsync() { - this.languageServiceManager.Start(); - } - - public void Dispose() - { - this.languageServiceManager.Stop(); + return this.languageServiceClient.Stop(); } [Fact] @@ -50,7 +53,9 @@ public async Task ServiceReturnsSyntaxErrors() await this.SendOpenFileEvent("TestFiles\\SimpleSyntaxError.ps1", false); // Wait for the diagnostic event - PublishDiagnosticsNotification diagnostics = this.WaitForEvent(PublishDiagnosticsNotification.Type); + PublishDiagnosticsNotification diagnostics = + await this.WaitForEvent( + PublishDiagnosticsNotification.Type); // Was there a syntax error? Assert.NotEqual(0, diagnostics.Diagnostics.Length); @@ -415,16 +420,22 @@ await this.SendRequest( [Fact] public async Task ServiceExecutesReplCommandAndReceivesOutput() { - await this.SendRequestWithoutWait( - EvaluateRequest.Type, - new EvaluateRequestArguments - { - Expression = "1 + 2" - }); + Task outputEventTask = + this.WaitForEvent( + OutputEvent.Type); + + Task evaluateTask = + this.SendRequest( + EvaluateRequest.Type, + new EvaluateRequestArguments + { + Expression = "1 + 2" + }); - OutputEventBody outputEvent = this.WaitForEvent(OutputEvent.Type); - this.WaitForResponse(EvaluateRequest.Type, this.messageId); + // Wait for both the evaluate response and the output event + await Task.WhenAll(evaluateTask, outputEventTask); + OutputEventBody outputEvent = outputEventTask.Result; Assert.Equal("3\r\n", outputEvent.Output); Assert.Equal("stdout", outputEvent.Category); } @@ -483,39 +494,39 @@ public async Task ServiceExecutesReplCommandAndReceivesChoicePrompt() // Assert.Equal("0", replWriteLineEvent.Body.LineContents); } - private async Task SendRequest( - RequestType requestType, + private Task SendRequest( + RequestType requestType, TParams requestParams) { - await this.SendRequestWithoutWait(requestType, requestParams); - return this.WaitForResponse(requestType, this.messageId); + return + this.languageServiceClient.SendRequest( + requestType, + requestParams); } - private async Task SendRequestWithoutWait( - RequestType requestType, - TParams requestParams) + private Task SendEvent(EventType eventType, TParams eventParams) { - this.messageId++; - - await this.MessageWriter.WriteMessage( - Message.Request( - this.messageId.ToString(), - requestType.TypeName, - JToken.FromObject(requestParams))); - } - - private async Task SendEvent(EventType eventType, TParams eventParams) - { - await this.MessageWriter.WriteMessage( - Message.Event( - eventType.MethodName, - JToken.FromObject(eventParams))); + return + this.languageServiceClient.SendEvent( + eventType, + eventParams); } private async Task SendOpenFileEvent(string filePath, bool waitForDiagnostics = true) { string fileContents = string.Join(Environment.NewLine, File.ReadAllLines(filePath)); + // Start the event waiter for diagnostics before sending the + // open event to make sure that we catch it + Task diagnosticWaitTask = null; + if (waitForDiagnostics) + { + // Wait for the diagnostic event + diagnosticWaitTask = + this.WaitForEvent( + PublishDiagnosticsNotification.Type); + } + await this.SendEvent( DidOpenTextDocumentNotification.Type, new DidOpenTextDocumentNotification() @@ -524,41 +535,26 @@ await this.SendEvent( Text = fileContents }); - if (waitForDiagnostics) + if (diagnosticWaitTask != null) { - // Wait for the diagnostic event - this.WaitForEvent(PublishDiagnosticsNotification.Type); + await diagnosticWaitTask; } } - private TParams WaitForEvent(EventType eventType) + private Task WaitForEvent(EventType eventType) { - // TODO: Integrate timeout! - Message receivedMessage = this.MessageReader.ReadMessage().Result; - - Assert.Equal(MessageType.Event, receivedMessage.MessageType); - Assert.Equal(eventType.MethodName, receivedMessage.Method); - - return - receivedMessage.Contents != null ? - receivedMessage.Contents.ToObject() : - default(TParams); - } + TaskCompletionSource eventTask = new TaskCompletionSource(); - private TResult WaitForResponse( - RequestType requestType, - int expectedId) - { - // TODO: Integrate timeout! - Message receivedMessage = this.MessageReader.ReadMessage().Result; - - Assert.Equal(MessageType.Response, receivedMessage.MessageType); - Assert.Equal(expectedId.ToString(), receivedMessage.Id); + this.languageServiceClient.SetEventHandler( + eventType, + (p, ctx) => + { + eventTask.SetResult(p); + return Task.FromResult(true); + }, + true); // Override any existing handler - return - receivedMessage.Contents != null ? - receivedMessage.Contents.ToObject() : - default(TResult); + return eventTask.Task; } } } diff --git a/test/PowerShellEditorServices.Test.Host/LanguageServiceManager.cs b/test/PowerShellEditorServices.Test.Host/LanguageServiceManager.cs deleted file mode 100644 index cebba8102..000000000 --- a/test/PowerShellEditorServices.Test.Host/LanguageServiceManager.cs +++ /dev/null @@ -1,220 +0,0 @@ -// -// Copyright (c) Microsoft. All rights reserved. -// Licensed under the MIT license. See LICENSE file in the project root for full license information. -// - -using EnvDTE; -using Microsoft.PowerShell.EditorServices.Protocol.LanguageServer; -using Microsoft.PowerShell.EditorServices.Protocol.MessageProtocol; -using System; -using System.Diagnostics; -using System.IO; -using System.Linq; -using System.Runtime.InteropServices; -using System.Text; -using Xunit; - -namespace Microsoft.PowerShell.EditorServices.Test.Host -{ - public class LanguageServiceManager - { - Stream inputStream; - Stream outputStream; - System.Diagnostics.Process languageServiceProcess; - - public MessageReader MessageReader { get; private set; } - - public MessageWriter MessageWriter { get; private set; } - - public void Start() - { - // If the test is running in the debugger, tell the language - // service to also wait for the debugger - string languageServiceArguments = string.Empty; - if (System.Diagnostics.Debugger.IsAttached) - { - languageServiceArguments = "/waitForDebugger"; - } - - this.languageServiceProcess = new System.Diagnostics.Process - { - StartInfo = new ProcessStartInfo - { - FileName = "Microsoft.PowerShell.EditorServices.Host.exe", - Arguments = languageServiceArguments, - CreateNoWindow = true, - UseShellExecute = false, - RedirectStandardInput = true, - RedirectStandardOutput = true, - RedirectStandardError = true, - StandardOutputEncoding = Encoding.UTF8, - }, - EnableRaisingEvents = true, - }; - - // Start the process - this.languageServiceProcess.Start(); - - // Attach to the language service process if debugging - if (System.Diagnostics.Debugger.IsAttached) - { - AttachToProcessIfDebugging(this.languageServiceProcess.Id); - } - - IMessageSerializer messageSerializer = new JsonRpcMessageSerializer(); - - // Open the standard input/output streams - this.inputStream = this.languageServiceProcess.StandardOutput.BaseStream; - this.outputStream = this.languageServiceProcess.StandardInput.BaseStream; - - // Set up the message reader and writer - this.MessageReader = - new MessageReader( - this.inputStream, - messageSerializer); - this.MessageWriter = - new MessageWriter( - this.outputStream, - messageSerializer); - - - // Send the 'initialize' request and wait for the response - var initializeRequest = new InitializeRequest - { - RootPath = "", - Capabilities = new ClientCapabilities() - }; - - // TODO: Assert some capability data? - this.MessageWriter.WriteRequest(InitializeRequest.Type, initializeRequest, 1).Wait(); - Message initializeResponse = this.MessageReader.ReadMessage().Result; - } - - public void Stop() - { - if (this.inputStream != null) - { - this.inputStream.Dispose(); - this.inputStream = null; - } - - if (this.outputStream != null) - { - this.outputStream.Dispose(); - this.outputStream = null; - } - - if (this.MessageReader != null) - { - this.MessageReader = null; - } - - if (this.MessageWriter != null) - { - this.MessageWriter = null; - } - - if (this.languageServiceProcess != null) - { - this.languageServiceProcess.Kill(); - this.languageServiceProcess = null; - } - } - - private static void AttachToProcessIfDebugging(int processId) - { - if (System.Diagnostics.Debugger.IsAttached) - { - int tryCount = 5; - - while (tryCount-- > 0) - { - try - { - var dte = (DTE)Marshal.GetActiveObject("VisualStudio.DTE.12.0"); - var processes = dte.Debugger.LocalProcesses.OfType(); - var foundProcess = processes.SingleOrDefault(x => x.ProcessID == processId); - - //EnvDTE.Process foundProcess = null; - //for (int i = 0; i < dte.Debugger.LocalProcesses.Count; i++) - //{ - // foundProcess = dte.Debugger.LocalProcesses.Item(i) as EnvDTE.Process; - - // if (foundProcess != null && foundProcess.ProcessID == processId) - // { - // break; - // } - //} - - if (foundProcess != null) - { - foundProcess.Attach(); - break; - } - else - { - throw new InvalidOperationException("Could not find language service process!"); - } - } - catch (COMException) - { - // Wait a bit and try again - System.Threading.Thread.Sleep(1000); - } - } - } - } - } - - [ComImport, Guid("00000016-0000-0000-C000-000000000046"), InterfaceType(ComInterfaceType.InterfaceIsIUnknown)] - public interface IOleMessageFilter - { - [PreserveSig] - int HandleInComingCall(int dwCallType, IntPtr hTaskCaller, int dwTickCount, IntPtr lpInterfaceInfo); - - [PreserveSig] - int RetryRejectedCall(IntPtr hTaskCallee, int dwTickCount, int dwRejectType); - - [PreserveSig] - int MessagePending(IntPtr hTaskCallee, int dwTickCount, int dwPendingType); - } - - public class MessageFilter : IOleMessageFilter - { - private const int Handled = 0, RetryAllowed = 2, Retry = 99, Cancel = -1, WaitAndDispatch = 2; - - int IOleMessageFilter.HandleInComingCall(int dwCallType, IntPtr hTaskCaller, int dwTickCount, IntPtr lpInterfaceInfo) - { - return Handled; - } - - int IOleMessageFilter.RetryRejectedCall(IntPtr hTaskCallee, int dwTickCount, int dwRejectType) - { - return dwRejectType == RetryAllowed ? Retry : Cancel; - } - - int IOleMessageFilter.MessagePending(IntPtr hTaskCallee, int dwTickCount, int dwPendingType) - { - return WaitAndDispatch; - } - - public static void Register() - { - CoRegisterMessageFilter(new MessageFilter()); - } - - public static void Revoke() - { - CoRegisterMessageFilter(null); - } - - private static void CoRegisterMessageFilter(IOleMessageFilter newFilter) - { - IOleMessageFilter oldFilter; - CoRegisterMessageFilter(newFilter, out oldFilter); - } - - [DllImport("Ole32.dll")] - private static extern int CoRegisterMessageFilter(IOleMessageFilter newFilter, out IOleMessageFilter oldFilter); - } -} diff --git a/test/PowerShellEditorServices.Test.Host/PowerShellEditorServices.Test.Host.csproj b/test/PowerShellEditorServices.Test.Host/PowerShellEditorServices.Test.Host.csproj index bf6f98206..b6e74bba5 100644 --- a/test/PowerShellEditorServices.Test.Host/PowerShellEditorServices.Test.Host.csproj +++ b/test/PowerShellEditorServices.Test.Host/PowerShellEditorServices.Test.Host.csproj @@ -64,9 +64,10 @@ - + + - + diff --git a/test/PowerShellEditorServices.Test.Host/Properties/AssemblyInfo.cs b/test/PowerShellEditorServices.Test.Host/Properties/AssemblyInfo.cs index b8c4f3f55..2cd2d0726 100644 --- a/test/PowerShellEditorServices.Test.Host/Properties/AssemblyInfo.cs +++ b/test/PowerShellEditorServices.Test.Host/Properties/AssemblyInfo.cs @@ -1,4 +1,9 @@ -using System.Reflection; +// +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +// + +using System.Reflection; using System.Runtime.CompilerServices; using System.Runtime.InteropServices; @@ -10,7 +15,7 @@ [assembly: AssemblyConfiguration("")] [assembly: AssemblyCompany("")] [assembly: AssemblyProduct("PowerShellEditorServices.Test.Host")] -[assembly: AssemblyCopyright("Copyright © 2015")] +[assembly: AssemblyCopyright("Copyright � 2015")] [assembly: AssemblyTrademark("")] [assembly: AssemblyCulture("")] @@ -34,3 +39,4 @@ // [assembly: AssemblyVersion("1.0.*")] [assembly: AssemblyVersion("1.0.0.0")] [assembly: AssemblyFileVersion("1.0.0.0")] + diff --git a/test/PowerShellEditorServices.Test.Protocol/DebugAdapter/V8MessageSerializerTests.cs b/test/PowerShellEditorServices.Test.Protocol/DebugAdapter/V8MessageSerializerTests.cs index 52a7007dd..7dfd47c20 100644 --- a/test/PowerShellEditorServices.Test.Protocol/DebugAdapter/V8MessageSerializerTests.cs +++ b/test/PowerShellEditorServices.Test.Protocol/DebugAdapter/V8MessageSerializerTests.cs @@ -1,11 +1,11 @@ -using Microsoft.PowerShell.EditorServices.Protocol.DebugAdapter; +// +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +// + using Microsoft.PowerShell.EditorServices.Protocol.MessageProtocol; +using Microsoft.PowerShell.EditorServices.Protocol.MessageProtocol.Serializers; using Newtonsoft.Json.Linq; -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; using Xunit; namespace Microsoft.PowerShell.EditorServices.Test.Protocol.DebugAdapter @@ -151,3 +151,4 @@ private static void AssertMessageFields( } } } + diff --git a/test/PowerShellEditorServices.Test.Protocol/LanguageServer/JsonRpcMessageSerializerTests.cs b/test/PowerShellEditorServices.Test.Protocol/LanguageServer/JsonRpcMessageSerializerTests.cs index 59bd137a1..876ca87a3 100644 --- a/test/PowerShellEditorServices.Test.Protocol/LanguageServer/JsonRpcMessageSerializerTests.cs +++ b/test/PowerShellEditorServices.Test.Protocol/LanguageServer/JsonRpcMessageSerializerTests.cs @@ -1,11 +1,11 @@ -using Microsoft.PowerShell.EditorServices.Protocol.LanguageServer; +// +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +// + using Microsoft.PowerShell.EditorServices.Protocol.MessageProtocol; +using Microsoft.PowerShell.EditorServices.Protocol.MessageProtocol.Serializers; using Newtonsoft.Json.Linq; -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; using Xunit; namespace Microsoft.PowerShell.EditorServices.Test.Protocol.LanguageServer @@ -141,3 +141,4 @@ private static void AssertMessageFields( } } } + diff --git a/test/PowerShellEditorServices.Test.Protocol/Message/MessageReaderWriterTests.cs b/test/PowerShellEditorServices.Test.Protocol/Message/MessageReaderWriterTests.cs index 42d17f56d..a82df2991 100644 --- a/test/PowerShellEditorServices.Test.Protocol/Message/MessageReaderWriterTests.cs +++ b/test/PowerShellEditorServices.Test.Protocol/Message/MessageReaderWriterTests.cs @@ -1,9 +1,10 @@ -// Copyright (c) Microsoft. All rights reserved. +// +// Copyright (c) Microsoft. All rights reserved. // Licensed under the MIT license. See LICENSE file in the project root for full license information. // -using Microsoft.PowerShell.EditorServices.Protocol.DebugAdapter; using Microsoft.PowerShell.EditorServices.Protocol.MessageProtocol; +using Microsoft.PowerShell.EditorServices.Protocol.MessageProtocol.Serializers; using System; using System.IO; using System.Text; @@ -22,7 +23,6 @@ public class MessageReaderWriterTests public MessageReaderWriterTests() { - // TODO: Set this! this.messageSerializer = new V8MessageSerializer(); } @@ -174,3 +174,4 @@ private byte[] GetMessageBytes(string messageString, Encoding encoding = null) } } } + diff --git a/test/PowerShellEditorServices.Test.Protocol/Properties/AssemblyInfo.cs b/test/PowerShellEditorServices.Test.Protocol/Properties/AssemblyInfo.cs index 20ebcee88..1a17daf66 100644 --- a/test/PowerShellEditorServices.Test.Protocol/Properties/AssemblyInfo.cs +++ b/test/PowerShellEditorServices.Test.Protocol/Properties/AssemblyInfo.cs @@ -1,4 +1,9 @@ -using System.Reflection; +// +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +// + +using System.Reflection; using System.Runtime.CompilerServices; using System.Runtime.InteropServices; @@ -10,7 +15,7 @@ [assembly: AssemblyConfiguration("")] [assembly: AssemblyCompany("")] [assembly: AssemblyProduct("PowerShellEditorServices.Test.Transport.Stdio")] -[assembly: AssemblyCopyright("Copyright © 2015")] +[assembly: AssemblyCopyright("Copyright � 2015")] [assembly: AssemblyTrademark("")] [assembly: AssemblyCulture("")] @@ -34,3 +39,4 @@ // [assembly: AssemblyVersion("1.0.*")] [assembly: AssemblyVersion("1.0.0.0")] [assembly: AssemblyFileVersion("1.0.0.0")] + diff --git a/test/PowerShellEditorServices.Test.Shared/Completion/CompleteCommandFromModule.cs b/test/PowerShellEditorServices.Test.Shared/Completion/CompleteCommandFromModule.cs index 2bc5d0461..3177dcbd5 100644 --- a/test/PowerShellEditorServices.Test.Shared/Completion/CompleteCommandFromModule.cs +++ b/test/PowerShellEditorServices.Test.Shared/Completion/CompleteCommandFromModule.cs @@ -1,4 +1,5 @@ -// Copyright (c) Microsoft. All rights reserved. +// +// Copyright (c) Microsoft. All rights reserved. // Licensed under the MIT license. See LICENSE file in the project root for full license information. // diff --git a/test/PowerShellEditorServices.Test.Shared/Completion/CompleteCommandInFile.cs b/test/PowerShellEditorServices.Test.Shared/Completion/CompleteCommandInFile.cs index ff5391cd1..3c201cd9d 100644 --- a/test/PowerShellEditorServices.Test.Shared/Completion/CompleteCommandInFile.cs +++ b/test/PowerShellEditorServices.Test.Shared/Completion/CompleteCommandInFile.cs @@ -1,4 +1,5 @@ -// Copyright (c) Microsoft. All rights reserved. +// +// Copyright (c) Microsoft. All rights reserved. // Licensed under the MIT license. See LICENSE file in the project root for full license information. // @@ -23,3 +24,4 @@ public class CompleteCommandInFile "Get-Something"); } } + diff --git a/test/PowerShellEditorServices.Test.Shared/Completion/CompleteVariableInFile.cs b/test/PowerShellEditorServices.Test.Shared/Completion/CompleteVariableInFile.cs index fdae6d885..9a728fa69 100644 --- a/test/PowerShellEditorServices.Test.Shared/Completion/CompleteVariableInFile.cs +++ b/test/PowerShellEditorServices.Test.Shared/Completion/CompleteVariableInFile.cs @@ -1,4 +1,5 @@ -// Copyright (c) Microsoft. All rights reserved. +// +// Copyright (c) Microsoft. All rights reserved. // Licensed under the MIT license. See LICENSE file in the project root for full license information. // @@ -23,3 +24,4 @@ public class CompleteVariableInFile "testVar1"); } } + diff --git a/test/PowerShellEditorServices.Test.Shared/Debugging/DebugTest.ps1 b/test/PowerShellEditorServices.Test.Shared/Debugging/DebugTest.ps1 index 47d7a8b38..f4c58cc2f 100644 --- a/test/PowerShellEditorServices.Test.Shared/Debugging/DebugTest.ps1 +++ b/test/PowerShellEditorServices.Test.Shared/Debugging/DebugTest.ps1 @@ -1,5 +1,4 @@ -Boo-Bah -Doo -Get-Process -bla +Get-Process -bla $i = 1 diff --git a/test/PowerShellEditorServices.Test.Shared/Definition/FindsVariableDefinition.cs b/test/PowerShellEditorServices.Test.Shared/Definition/FindsVariableDefinition.cs index 246f7b1f0..6dab3ddad 100644 --- a/test/PowerShellEditorServices.Test.Shared/Definition/FindsVariableDefinition.cs +++ b/test/PowerShellEditorServices.Test.Shared/Definition/FindsVariableDefinition.cs @@ -1,4 +1,9 @@ -// Copyright (c) Microsoft. All rights reserved. +// +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +// + +// Copyright (c) Microsoft. All rights reserved. // Licensed under the MIT license. See LICENSE file in the project root for full license information. // @@ -17,3 +22,4 @@ public class FindsVariableDefinition }; } } + diff --git a/test/PowerShellEditorServices.Test.Shared/Occurrences/FindOccurrencesOnParameter.cs b/test/PowerShellEditorServices.Test.Shared/Occurrences/FindOccurrencesOnParameter.cs index 60cc6d6e6..62fc82716 100644 --- a/test/PowerShellEditorServices.Test.Shared/Occurrences/FindOccurrencesOnParameter.cs +++ b/test/PowerShellEditorServices.Test.Shared/Occurrences/FindOccurrencesOnParameter.cs @@ -1,4 +1,5 @@ -// Copyright (c) Microsoft. All rights reserved. +// +// Copyright (c) Microsoft. All rights reserved. // Licensed under the MIT license. See LICENSE file in the project root for full license information. // @@ -15,3 +16,4 @@ public class FindOccurrencesOnParameter }; } } + diff --git a/test/PowerShellEditorServices.Test.Shared/Occurrences/FindsOccurrencesOnFunction.cs b/test/PowerShellEditorServices.Test.Shared/Occurrences/FindsOccurrencesOnFunction.cs index e5fb59e56..2b8258410 100644 --- a/test/PowerShellEditorServices.Test.Shared/Occurrences/FindsOccurrencesOnFunction.cs +++ b/test/PowerShellEditorServices.Test.Shared/Occurrences/FindsOccurrencesOnFunction.cs @@ -1,4 +1,5 @@ -// Copyright (c) Microsoft. All rights reserved. +// +// Copyright (c) Microsoft. All rights reserved. // Licensed under the MIT license. See LICENSE file in the project root for full license information. // @@ -17,3 +18,4 @@ public class FindsOccurrencesOnFunction }; } } + diff --git a/test/PowerShellEditorServices.Test.Shared/ParameterHints/FindsParameterSetsOnCommand.cs b/test/PowerShellEditorServices.Test.Shared/ParameterHints/FindsParameterSetsOnCommand.cs index ecff25227..fcb4e88f0 100644 --- a/test/PowerShellEditorServices.Test.Shared/ParameterHints/FindsParameterSetsOnCommand.cs +++ b/test/PowerShellEditorServices.Test.Shared/ParameterHints/FindsParameterSetsOnCommand.cs @@ -1,4 +1,5 @@ -// Copyright (c) Microsoft. All rights reserved. +// +// Copyright (c) Microsoft. All rights reserved. // Licensed under the MIT license. See LICENSE file in the project root for full license information. // @@ -16,4 +17,4 @@ public class FindsParameterSetsOnCommand StartColumnNumber = 14 }; } -} \ No newline at end of file +} diff --git a/test/PowerShellEditorServices.Test.Shared/ParameterHints/FindsParameterSetsOnCommandWithSpaces.cs b/test/PowerShellEditorServices.Test.Shared/ParameterHints/FindsParameterSetsOnCommandWithSpaces.cs index 909e7669e..920530ec3 100644 --- a/test/PowerShellEditorServices.Test.Shared/ParameterHints/FindsParameterSetsOnCommandWithSpaces.cs +++ b/test/PowerShellEditorServices.Test.Shared/ParameterHints/FindsParameterSetsOnCommandWithSpaces.cs @@ -1,4 +1,5 @@ -// Copyright (c) Microsoft. All rights reserved. +// +// Copyright (c) Microsoft. All rights reserved. // Licensed under the MIT license. See LICENSE file in the project root for full license information. // @@ -15,3 +16,4 @@ public class FindsParameterSetsOnCommandWithSpaces }; } } + diff --git a/test/PowerShellEditorServices.Test.Shared/Properties/AssemblyInfo.cs b/test/PowerShellEditorServices.Test.Shared/Properties/AssemblyInfo.cs index e70561cf8..164db4ac3 100644 --- a/test/PowerShellEditorServices.Test.Shared/Properties/AssemblyInfo.cs +++ b/test/PowerShellEditorServices.Test.Shared/Properties/AssemblyInfo.cs @@ -1,4 +1,9 @@ -using System.Reflection; +// +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +// + +using System.Reflection; using System.Runtime.CompilerServices; using System.Runtime.InteropServices; @@ -10,7 +15,7 @@ [assembly: AssemblyConfiguration("")] [assembly: AssemblyCompany("")] [assembly: AssemblyProduct("PowerShellEditorServices.Test.Shared")] -[assembly: AssemblyCopyright("Copyright © 2015")] +[assembly: AssemblyCopyright("Copyright � 2015")] [assembly: AssemblyTrademark("")] [assembly: AssemblyCulture("")] @@ -34,3 +39,4 @@ // [assembly: AssemblyVersion("1.0.*")] [assembly: AssemblyVersion("1.0.0.0")] [assembly: AssemblyFileVersion("1.0.0.0")] + diff --git a/test/PowerShellEditorServices.Test.Shared/References/FindsReferencesOnBuiltInCommandWithAlias.cs b/test/PowerShellEditorServices.Test.Shared/References/FindsReferencesOnBuiltInCommandWithAlias.cs index 2c1789b00..2a22b13f2 100644 --- a/test/PowerShellEditorServices.Test.Shared/References/FindsReferencesOnBuiltInCommandWithAlias.cs +++ b/test/PowerShellEditorServices.Test.Shared/References/FindsReferencesOnBuiltInCommandWithAlias.cs @@ -1,4 +1,5 @@ -// Copyright (c) Microsoft. All rights reserved. +// +// Copyright (c) Microsoft. All rights reserved. // Licensed under the MIT license. See LICENSE file in the project root for full license information. // @@ -25,3 +26,4 @@ public class FindsReferencesOnBuiltInAlias }; } } + diff --git a/test/PowerShellEditorServices.Test.Shared/References/FindsReferencesOnFunction.cs b/test/PowerShellEditorServices.Test.Shared/References/FindsReferencesOnFunction.cs index 633423b60..6a53d69f2 100644 --- a/test/PowerShellEditorServices.Test.Shared/References/FindsReferencesOnFunction.cs +++ b/test/PowerShellEditorServices.Test.Shared/References/FindsReferencesOnFunction.cs @@ -1,4 +1,5 @@ -// Copyright (c) Microsoft. All rights reserved. +// +// Copyright (c) Microsoft. All rights reserved. // Licensed under the MIT license. See LICENSE file in the project root for full license information. // @@ -15,3 +16,4 @@ public class FindsReferencesOnFunction }; } } + diff --git a/test/PowerShellEditorServices.Test.Shared/References/FindsReferencesOnFunctionMultiFileDotSource.cs b/test/PowerShellEditorServices.Test.Shared/References/FindsReferencesOnFunctionMultiFileDotSource.cs index 4eb4adc5d..5624edf02 100644 --- a/test/PowerShellEditorServices.Test.Shared/References/FindsReferencesOnFunctionMultiFileDotSource.cs +++ b/test/PowerShellEditorServices.Test.Shared/References/FindsReferencesOnFunctionMultiFileDotSource.cs @@ -1,4 +1,5 @@ -// Copyright (c) Microsoft. All rights reserved. +// +// Copyright (c) Microsoft. All rights reserved. // Licensed under the MIT license. See LICENSE file in the project root for full license information. // @@ -27,3 +28,4 @@ public class FindsReferencesOnFunctionMultiFileDotSourceFileC }; } } + diff --git a/test/PowerShellEditorServices.Test.Shared/References/FindsReferencesonVariable.cs b/test/PowerShellEditorServices.Test.Shared/References/FindsReferencesonVariable.cs index 6b9b8ce0a..b5ae61af4 100644 --- a/test/PowerShellEditorServices.Test.Shared/References/FindsReferencesonVariable.cs +++ b/test/PowerShellEditorServices.Test.Shared/References/FindsReferencesonVariable.cs @@ -1,4 +1,5 @@ -// Copyright (c) Microsoft. All rights reserved. +// +// Copyright (c) Microsoft. All rights reserved. // Licensed under the MIT license. See LICENSE file in the project root for full license information. // @@ -17,3 +18,4 @@ public class FindsReferencesOnVariable }; } } + diff --git a/test/PowerShellEditorServices.Test.Shared/Symbols/FindSymbolsInMultiSymbolFile.cs b/test/PowerShellEditorServices.Test.Shared/Symbols/FindSymbolsInMultiSymbolFile.cs index d3904f1fe..7f3dc68e8 100644 --- a/test/PowerShellEditorServices.Test.Shared/Symbols/FindSymbolsInMultiSymbolFile.cs +++ b/test/PowerShellEditorServices.Test.Shared/Symbols/FindSymbolsInMultiSymbolFile.cs @@ -1,4 +1,5 @@ -// Copyright (c) Microsoft. All rights reserved. +// +// Copyright (c) Microsoft. All rights reserved. // Licensed under the MIT license. See LICENSE file in the project root for full license information. // @@ -12,3 +13,4 @@ public class FindSymbolsInMultiSymbolFile }; } } + diff --git a/test/PowerShellEditorServices.Test.Shared/Symbols/FindSymbolsInNoSymbolsFile.cs b/test/PowerShellEditorServices.Test.Shared/Symbols/FindSymbolsInNoSymbolsFile.cs index 638894999..cf1692030 100644 --- a/test/PowerShellEditorServices.Test.Shared/Symbols/FindSymbolsInNoSymbolsFile.cs +++ b/test/PowerShellEditorServices.Test.Shared/Symbols/FindSymbolsInNoSymbolsFile.cs @@ -1,4 +1,5 @@ -// Copyright (c) Microsoft. All rights reserved. +// +// Copyright (c) Microsoft. All rights reserved. // Licensed under the MIT license. See LICENSE file in the project root for full license information. // @@ -12,3 +13,4 @@ public class FindSymbolsInNoSymbolsFile }; } } + diff --git a/test/PowerShellEditorServices.Test/Console/PowerShellContextTests.cs b/test/PowerShellEditorServices.Test/Console/PowerShellContextTests.cs index b193131f2..3b508332e 100644 --- a/test/PowerShellEditorServices.Test/Console/PowerShellContextTests.cs +++ b/test/PowerShellEditorServices.Test/Console/PowerShellContextTests.cs @@ -1,4 +1,9 @@ -using Nito.AsyncEx; +// +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +// + +using Nito.AsyncEx; using System; using System.Collections.Generic; using System.Linq; @@ -223,3 +228,4 @@ void OnOutputWritten(object sender, OutputWrittenEventArgs e) #endregion } } + diff --git a/test/PowerShellEditorServices.Test/Debugging/DebugServiceTests.cs b/test/PowerShellEditorServices.Test/Debugging/DebugServiceTests.cs index 73a1bfb76..11cc006b2 100644 --- a/test/PowerShellEditorServices.Test/Debugging/DebugServiceTests.cs +++ b/test/PowerShellEditorServices.Test/Debugging/DebugServiceTests.cs @@ -1,9 +1,16 @@ -using Nito.AsyncEx; +// +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +// + +using Nito.AsyncEx; using System; using System.Linq; using System.Management.Automation; +using System.Threading; using System.Threading.Tasks; using Xunit; +using Xunit.Sdk; namespace Microsoft.PowerShell.EditorServices.Test.Debugging { @@ -13,6 +20,7 @@ public class DebugServiceTests : IDisposable private DebugService debugService; private ScriptFile debugScriptFile; private PowerShellContext powerShellContext; + private SynchronizationContext runnerContext; private AsyncProducerConsumerQueue debuggerStoppedQueue = new AsyncProducerConsumerQueue(); @@ -34,6 +42,7 @@ public DebugServiceTests() this.debugService = new DebugService(this.powerShellContext); this.debugService.DebuggerStopped += debugService_DebuggerStopped; this.debugService.BreakpointUpdated += debugService_BreakpointUpdated; + this.runnerContext = SynchronizationContext.Current; } void powerShellContext_SessionStateChanged(object sender, SessionStateChangedEventArgs e) @@ -52,7 +61,11 @@ void debugService_BreakpointUpdated(object sender, BreakpointUpdatedEventArgs e) void debugService_DebuggerStopped(object sender, DebuggerStopEventArgs e) { - this.debuggerStoppedQueue.Enqueue(e); + this.runnerContext.Post( + (o) => + { + this.debuggerStoppedQueue.Enqueue(e); + }, null); } public void Dispose() @@ -97,32 +110,34 @@ await this.debugService.SetBreakpoints( new int[] { 5, 9 }); await this.AssertStateChange(PowerShellContextState.Ready); - this.powerShellContext.ExecuteScriptAtPath( - this.debugScriptFile.FilePath); + Task executeTask = + this.powerShellContext.ExecuteScriptAtPath( + this.debugScriptFile.FilePath); // Wait for a couple breakpoints await this.AssertDebuggerStopped(this.debugScriptFile.FilePath, 5); this.debugService.Continue(); + await this.AssertDebuggerStopped(this.debugScriptFile.FilePath, 9); this.debugService.Continue(); // Abort script execution early and wait for completion this.debugService.Abort(); - await this.AssertStateChange( - PowerShellContextState.Ready, - PowerShellExecutionResult.Aborted); + await executeTask; } [Fact] public async Task DebuggerBreaksWhenRequested() { - this.powerShellContext.ExecuteScriptString( - this.debugScriptFile.FilePath); + Task executeTask = + this.powerShellContext.ExecuteScriptString( + this.debugScriptFile.FilePath); // Break execution and wait for the debugger to stop this.debugService.Break(); - await this.AssertDebuggerStopped( - this.debugScriptFile.FilePath); + + // File path is an empty string when paused while running + await this.AssertDebuggerStopped(string.Empty); await this.AssertStateChange( PowerShellContextState.Ready, PowerShellExecutionResult.Stopped); @@ -137,8 +152,9 @@ await this.AssertStateChange( [Fact] public async Task DebuggerRunsCommandsWhileStopped() { - this.powerShellContext.ExecuteScriptString( - this.debugScriptFile.FilePath); + Task executeTask = + this.powerShellContext.ExecuteScriptString( + this.debugScriptFile.FilePath); // Break execution and wait for the debugger to stop this.debugService.Break(); @@ -168,7 +184,10 @@ await this.debugService.SetBreakpoints( new int[] { 14 }); // Execute the script and wait for the breakpoint to be hit - this.powerShellContext.ExecuteScriptString(variablesFile.FilePath); + Task executeTask = + this.powerShellContext.ExecuteScriptString( + variablesFile.FilePath); + await this.AssertDebuggerStopped(variablesFile.FilePath); StackFrameDetails[] stackFrames = debugService.GetStackFrames(); @@ -211,15 +230,15 @@ public async Task AssertDebuggerStopped( string scriptPath, int lineNumber = -1) { + SynchronizationContext syncContext = SynchronizationContext.Current; + DebuggerStopEventArgs eventArgs = await this.debuggerStoppedQueue.DequeueAsync(); - // TODO #22 - Need to re-enable these Asserts once we figure - // out how to make them work correctly - //Assert.Equal(scriptPath, eventArgs.InvocationInfo.ScriptName); + Assert.Equal(scriptPath, eventArgs.InvocationInfo.ScriptName); if (lineNumber > -1) { - //Assert.Equal(lineNumber, eventArgs.InvocationInfo.ScriptLineNumber); + Assert.Equal(lineNumber, eventArgs.InvocationInfo.ScriptLineNumber); } } @@ -230,9 +249,10 @@ private async Task AssertStateChange( SessionStateChangedEventArgs newState = await this.sessionStateQueue.DequeueAsync(); - // TODO #22 - //Assert.Equal(expectedState, newState.NewSessionState); - //Assert.Equal(expectedResult, newState.ExecutionResult); + Assert.Equal(expectedState, newState.NewSessionState); + Assert.Equal(expectedResult, newState.ExecutionResult); } } } + + diff --git a/test/PowerShellEditorServices.Test/Properties/AssemblyInfo.cs b/test/PowerShellEditorServices.Test/Properties/AssemblyInfo.cs index a7deee989..f14268215 100644 --- a/test/PowerShellEditorServices.Test/Properties/AssemblyInfo.cs +++ b/test/PowerShellEditorServices.Test/Properties/AssemblyInfo.cs @@ -1,4 +1,9 @@ -using System.Reflection; +// +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +// + +using System.Reflection; using System.Runtime.CompilerServices; using System.Runtime.InteropServices; @@ -10,7 +15,7 @@ [assembly: AssemblyConfiguration("")] [assembly: AssemblyCompany("")] [assembly: AssemblyProduct("PowerShellEditorServices.Test.Core")] -[assembly: AssemblyCopyright("Copyright © 2015")] +[assembly: AssemblyCopyright("Copyright � 2015")] [assembly: AssemblyTrademark("")] [assembly: AssemblyCulture("")] @@ -34,3 +39,4 @@ // [assembly: AssemblyVersion("1.0.*")] [assembly: AssemblyVersion("1.0.0.0")] [assembly: AssemblyFileVersion("1.0.0.0")] +