diff --git a/src/Cody.AgentTester/Cody.AgentTester.csproj b/src/Cody.AgentTester/Cody.AgentTester.csproj index 55c0cc0d..2eab2ae8 100644 --- a/src/Cody.AgentTester/Cody.AgentTester.csproj +++ b/src/Cody.AgentTester/Cody.AgentTester.csproj @@ -57,11 +57,10 @@ {9ff2cc40-78e9-46c8-b2ef-30a1f1be82f2} Cody.Core - - - - 2.9.85 - + + {3bb34f98-f069-4a38-bc4d-cf407d59b863} + Cody.VisualStudio + \ No newline at end of file diff --git a/src/Cody.AgentTester/Program.cs b/src/Cody.AgentTester/Program.cs index 8c5a3a21..a5678348 100644 --- a/src/Cody.AgentTester/Program.cs +++ b/src/Cody.AgentTester/Program.cs @@ -1,7 +1,8 @@ using Cody.Core.Agent; -using Cody.Core.Agent.Connector; using Cody.Core.Agent.Protocol; +using Cody.VisualStudio.Client; using System; +using System.Collections.Generic; using System.IO; using System.Threading.Tasks; @@ -9,29 +10,31 @@ namespace Cody.AgentTester { internal class Program { - private static AgentConnector connector; + private static AgentClient client; private static ConsoleLogger logger = new ConsoleLogger(); - private static IAgentClient agentClient; + private static IAgentService agentService; static async Task Main(string[] args) { // Set the env var to 3113 when running with local agent. - var portNumber = int.TryParse(Environment.GetEnvironmentVariable("CODY_VS_DEV_PORT"), out int port) ? port : (int?)null; + var devPort = Environment.GetEnvironmentVariable("CODY_VS_DEV_PORT"); + var portNumber = int.TryParse(devPort, out int port) ? port : 3113; - var options = new AgentConnectorOptions + var options = new AgentClientOptions { - NotificationsTarget = new NotificationHandlers(), + NotificationHandlers = new List { new NotificationHandlers() }, AgentDirectory = "../../../Cody.VisualStudio/Agent", RestartAgentOnFailure = true, Debug = true, - Port = portNumber, + ConnectToRemoteAgent = devPort != null, + RemoteAgentPort = portNumber, }; - connector = new AgentConnector(options, logger); + client = new AgentClient(options, logger); - await connector.Connect(); + client.Start(); - agentClient = connector.CreateClient(); + agentService = client.CreateAgentService(); await Initialize(); @@ -76,9 +79,9 @@ private static async Task Initialize() } }; - await agentClient.Initialize(clientInfo); + await agentService.Initialize(clientInfo); - agentClient.Initialized(); + agentService.Initialized(); } diff --git a/src/Cody.Core/Agent/AgentClientFactory.cs b/src/Cody.Core/Agent/AgentClientFactory.cs deleted file mode 100644 index dfd46f4d..00000000 --- a/src/Cody.Core/Agent/AgentClientFactory.cs +++ /dev/null @@ -1,24 +0,0 @@ -using Cody.Core.Agent.Connector; -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; - -namespace Cody.Core.Agent -{ - public class AgentClientFactory : IAgentClientFactory - { - private AgentConnector connector; - - public AgentClientFactory(AgentConnector connector) - { - this.connector = connector; - } - - public IAgentClient CreateAgentClient() - { - return connector.CreateClient(); - } - } -} diff --git a/src/Cody.Core/Agent/AgentMethodAttribute.cs b/src/Cody.Core/Agent/AgentMethodAttribute.cs new file mode 100644 index 00000000..e03df2fc --- /dev/null +++ b/src/Cody.Core/Agent/AgentMethodAttribute.cs @@ -0,0 +1,18 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Cody.Core.Agent +{ + public class AgentMethodAttribute : Attribute + { + public AgentMethodAttribute(string name) + { + Name = name; + } + + public string Name { get; private set; } + } +} diff --git a/src/Cody.Core/Agent/AgentNotificationAttribute.cs b/src/Cody.Core/Agent/AgentNotificationAttribute.cs new file mode 100644 index 00000000..1ec114a0 --- /dev/null +++ b/src/Cody.Core/Agent/AgentNotificationAttribute.cs @@ -0,0 +1,21 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Cody.Core.Agent +{ + public class AgentNotificationAttribute : Attribute + { + public AgentNotificationAttribute(string name, bool deserializeToSingleObject = false) + { + Name = name; + DeserializeToSingleObject = deserializeToSingleObject; + } + + public string Name { get; private set; } + + public bool DeserializeToSingleObject { get; private set; } + } +} diff --git a/src/Cody.Core/Agent/Connector/AgentConnector.cs b/src/Cody.Core/Agent/Connector/AgentConnector.cs deleted file mode 100644 index 7aea0230..00000000 --- a/src/Cody.Core/Agent/Connector/AgentConnector.cs +++ /dev/null @@ -1,108 +0,0 @@ -using Cody.Core.Logging; -using Newtonsoft.Json.Converters; -using Newtonsoft.Json.Serialization; -using StreamJsonRpc; -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; - -namespace Cody.Core.Agent.Connector -{ - public class AgentConnector - { - private IAgentProcess agentProcess; - private JsonRpc jsonRpc; - private IAgentClient agentClient; - private AgentConnectorOptions options; - private ILog log; - - public AgentConnector(AgentConnectorOptions connectorOptions, ILog log) - { - if (connectorOptions == null) throw new ArgumentNullException(nameof(connectorOptions)); - - options = connectorOptions; - this.log = log; - } - - public bool IsConnected { get; private set; } - - public async Task Connect() - { - if (IsConnected) return; - - if (options.Port != null) - { - agentProcess = await RemoteAgent.Connect(options, OnAgentExit); - } - else - { - agentProcess = AgentProcess.Start(options.AgentDirectory, options.Debug, log, OnAgentExit); - } - log.Info("The agent process has started."); - - var jsonMessageFormatter = new JsonMessageFormatter(); - jsonMessageFormatter.JsonSerializer.ContractResolver = new DefaultContractResolver() - { - NamingStrategy = new CamelCaseNamingStrategy() - }; - jsonMessageFormatter.JsonSerializer.Converters.Add(new StringEnumConverter(new CamelCaseNamingStrategy())); - - var handler = new HeaderDelimitedMessageHandler(agentProcess.SendingStream, agentProcess.ReceivingStream, jsonMessageFormatter); - jsonRpc = new JsonRpc(handler); - - if (options.NotificationsTarget != null) jsonRpc.AddLocalRpcTarget(options.NotificationsTarget); - agentClient = jsonRpc.Attach(); - options.NotificationsTarget.SetAgentClient(agentClient); - - jsonRpc.StartListening(); - IsConnected = true; - log.Info("A connection with the agent has been established."); - - if (options.AfterConnection != null) options.AfterConnection(agentClient); - } - - private void OnAgentExit(int exitCode) - { - DisconnectInternal(); - if (exitCode == 0) log.Info("The agent's process has ended."); - else log.Error($"The agent process unexpectedly ended with code {exitCode}."); - - if (options.RestartAgentOnFailure && exitCode != 0) - { - log.Info("Restarting the agent."); - // TODO: Make RemoteAgent wait until there's an agent to reconnect to before failing. - Connect().Wait(); - } - } - - public void Disconnect() - { - if (!IsConnected) return; - - DisconnectInternal(); - } - - private void DisconnectInternal() - { - if (options.BeforeDisconnection != null) options.BeforeDisconnection(agentClient); - - jsonRpc.Dispose(); - agentProcess.Dispose(); - - jsonRpc = null; - agentProcess = null; - - IsConnected = false; - log.Info("The connection with the agent has been terminated."); - } - - public IAgentClient CreateClient() - { - if (!IsConnected) Connect(); - - return agentClient; - } - } -} diff --git a/src/Cody.Core/Agent/Connector/AgentConnectorOptions.cs b/src/Cody.Core/Agent/Connector/AgentConnectorOptions.cs deleted file mode 100644 index 544ccd9d..00000000 --- a/src/Cody.Core/Agent/Connector/AgentConnectorOptions.cs +++ /dev/null @@ -1,28 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; - -namespace Cody.Core.Agent.Connector -{ - public class AgentConnectorOptions - { - public bool Debug { get; set; } - - public bool RestartAgentOnFailure { get; set; } - - /// - /// If non-null, the TCP port to connect to an existing Agent instance on. - /// - public int? Port { get; set; } - - public string AgentDirectory { get; set; } - - public Action AfterConnection { get; set; } - - public Action BeforeDisconnection { get; set; } - - public NotificationHandlers NotificationsTarget { get; set; } - } -} diff --git a/src/Cody.Core/Agent/Connector/AgentProcess.cs b/src/Cody.Core/Agent/Connector/AgentProcess.cs deleted file mode 100644 index a5e754f2..00000000 --- a/src/Cody.Core/Agent/Connector/AgentProcess.cs +++ /dev/null @@ -1,124 +0,0 @@ -using Cody.Core.Logging; -using System; -using System.Collections.Generic; -using System.Diagnostics; -using System.IO; -using System.Linq; -using System.Runtime.InteropServices; -using System.Text; -using System.Threading.Tasks; - -namespace Cody.Core.Agent.Connector -{ - public class AgentProcess : IAgentProcess - { - private readonly Process process = new Process(); - private string agentDirectory; - private static readonly string workingDirectory = "../../../../../cody/agent/dist"; - private bool debugMode; - private ILog logger; - private Action onExit; - - private AgentProcess(string agentDirectory, bool debugMode, ILog logger, Action onExit) - { - this.agentDirectory = agentDirectory; - this.debugMode = debugMode; - this.logger = logger; - this.onExit = onExit; - } - - public Stream SendingStream => process.StandardInput.BaseStream; - - public Stream ReceivingStream => process.StandardOutput.BaseStream; - - public static AgentProcess Start(string agentDirectory, bool debugMode, ILog logger, Action onExit) - { - - if (!Directory.Exists(agentDirectory)) - throw new ArgumentException("Directory does not exist", nameof(agentDirectory)); - - var agentProcess = new AgentProcess(agentDirectory, debugMode, logger, onExit); - agentProcess.StartInternal(); - - return agentProcess; - } - - - private void StartInternal() - { - var path = Path.Combine(agentDirectory, GetAgentFileName()); - - if (!File.Exists(path)) - throw new FileNotFoundException("Agent file not found", path); - - var port = Environment.GetEnvironmentVariable("CODY_VS_DEV_PORT"); - - if (port != null && Directory.Exists(workingDirectory)) - agentDirectory = workingDirectory; - - process.StartInfo.FileName = path; - process.StartInfo.Arguments = GetAgentArguments(debugMode); - process.StartInfo.WorkingDirectory = agentDirectory; - process.StartInfo.UseShellExecute = false; - process.StartInfo.RedirectStandardOutput = true; - process.StartInfo.RedirectStandardError = true; - process.StartInfo.RedirectStandardInput = true; - process.StartInfo.CreateNoWindow = true; - process.EnableRaisingEvents = true; - process.Exited += OnProcessExited; - process.ErrorDataReceived += OnErrorDataReceived; - - process.Start(); - process.BeginErrorReadLine(); - } - - private void OnErrorDataReceived(object sender, DataReceivedEventArgs e) - { - logger.Error(e.Data, "Agent errors"); - } - - private void OnProcessExited(object sender, EventArgs e) - { - if (onExit != null) onExit(process.ExitCode); - } - - private string GetAgentFileName() - { - if (RuntimeInformation.ProcessArchitecture == Architecture.Arm64) - return "node-win-arm64.exe"; - - return "node-win-x64.exe"; - } - - private string GetAgentArguments(bool debugMode) - { - var argList = new List(); - - if (debugMode) - { - argList.Add("--inspect"); - argList.Add("--enable-source-maps"); - } - - argList.Add("index.js api jsonrpc-stdio"); - - var arguments = string.Join(" ", argList); - return arguments; - } - - public void Dispose() - { - if (!process.HasExited) process.Kill(); - - process.Dispose(); - } - - public bool IsConnected - { - get - { - return !process.HasExited; - } - } - } -} diff --git a/src/Cody.Core/Agent/Connector/IAgentProcess.cs b/src/Cody.Core/Agent/Connector/IAgentProcess.cs deleted file mode 100644 index e2e03403..00000000 --- a/src/Cody.Core/Agent/Connector/IAgentProcess.cs +++ /dev/null @@ -1,16 +0,0 @@ -using System; -using System.Collections.Generic; -using System.IO; -using System.Linq; -using System.Text; -using System.Threading.Tasks; - -namespace Cody.Core.Agent.Connector -{ - internal interface IAgentProcess : IDisposable - { - bool IsConnected { get; } - Stream SendingStream { get; } - Stream ReceivingStream { get; } - } -} diff --git a/src/Cody.Core/Agent/Connector/RemoteAgent.cs b/src/Cody.Core/Agent/Connector/RemoteAgent.cs deleted file mode 100644 index ee70428c..00000000 --- a/src/Cody.Core/Agent/Connector/RemoteAgent.cs +++ /dev/null @@ -1,42 +0,0 @@ -using System; -using System.IO; -using System.Net; -using System.Net.Sockets; -using System.Threading.Tasks; - -namespace Cody.Core.Agent.Connector -{ - class RemoteAgent : IAgentProcess - { - internal static async Task Connect(AgentConnectorOptions options, Action onExit) - { - TcpClient client = new TcpClient(); - await client.ConnectAsync(IPAddress.Loopback, options.Port ?? 3113); - return new RemoteAgent(client, onExit); - } - - private TcpClient client; - private Action onExit; - - private RemoteAgent(TcpClient client, Action onExit) { - this.client = client; - this.onExit = onExit; - - // TODO: Wrap the returned streams and if they fail to read or write, call the onExit callback. - } - - public void Dispose() - { - this.client.Dispose(); - } - - public bool IsConnected { - get { - return this.client.Connected; - } - } - - public Stream SendingStream => client.GetStream(); - public Stream ReceivingStream => client.GetStream(); - } -} diff --git a/src/Cody.Core/Agent/IAgentClient.cs b/src/Cody.Core/Agent/IAgentClient.cs deleted file mode 100644 index 9250c6a2..00000000 --- a/src/Cody.Core/Agent/IAgentClient.cs +++ /dev/null @@ -1,61 +0,0 @@ -using Cody.Core.Agent.Protocol; -using StreamJsonRpc; -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; - -namespace Cody.Core.Agent -{ - public interface IAgentClient - { - [JsonRpcMethod("initialize")] - Task Initialize(ClientInfo clientInfo); - - [JsonRpcMethod("graphql/getCurrentUserCodySubscription")] - Task GetCurrentUserCodySubscription(); - - [JsonRpcMethod("initialized")] - void Initialized(); - - [JsonRpcMethod("git/codebaseName")] - Task GetGitCodebaseName(string url); - - [JsonRpcMethod("webview/resolveWebviewView", UseSingleObjectParameterDeserialization = true)] - Task ResolveWebviewView(ResolveWebviewViewParams paramValue); - - [JsonRpcMethod("webview/receiveMessageStringEncoded")] - Task ReceiveMessageStringEncoded(ReceiveMessageStringEncodedParams paramValue); - - [JsonRpcMethod("env/openExternal")] - Task OpenExternal(string url); - - [JsonRpcMethod("textDocument/didOpen", UseSingleObjectParameterDeserialization = true)] - void DidOpen(ProtocolTextDocument docState); - - [JsonRpcMethod("textDocument/didChange", UseSingleObjectParameterDeserialization = true)] - void DidChange(ProtocolTextDocument docState); - - [JsonRpcMethod("textDocument/didFocus")] - void DidFocus(string uri); - - [JsonRpcMethod("textDocument/didSave")] - void DidSave(string uri); - - [JsonRpcMethod("textDocument/didClose", UseSingleObjectParameterDeserialization = true)] - void DidClose(ProtocolTextDocument docState); - } -} - -public class ResolveWebviewViewParams -{ - public string ViewId { get; set; } - public string WebviewHandle { get; set; } -} - -public class ReceiveMessageStringEncodedParams -{ - public string Id { get; set; } - public string MessageStringEncoded { get; set; } -} \ No newline at end of file diff --git a/src/Cody.Core/Agent/IAgentService.cs b/src/Cody.Core/Agent/IAgentService.cs new file mode 100644 index 00000000..9cca7356 --- /dev/null +++ b/src/Cody.Core/Agent/IAgentService.cs @@ -0,0 +1,49 @@ +using Cody.Core.Agent.Protocol; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Cody.Core.Agent +{ + public interface IAgentService + { + [AgentMethod("initialize")] + Task Initialize(ClientInfo clientInfo); + + [AgentMethod("graphql/getCurrentUserCodySubscription")] + Task GetCurrentUserCodySubscription(); + + [AgentMethod("initialized")] + void Initialized(); + + [AgentMethod("git/codebaseName")] + Task GetGitCodebaseName(string url); + + [AgentMethod("webview/resolveWebviewView")] + Task ResolveWebviewView(ResolveWebviewViewParams paramValue); + + [AgentMethod("webview/receiveMessageStringEncoded")] + Task ReceiveMessageStringEncoded(ReceiveMessageStringEncodedParams paramValue); + + [AgentMethod("env/openExternal")] + Task OpenExternal(string url); + + [AgentMethod("textDocument/didOpen")] + void DidOpen(ProtocolTextDocument docState); + + [AgentMethod("textDocument/didChange")] + void DidChange(ProtocolTextDocument docState); + + [AgentMethod("textDocument/didFocus")] + void DidFocus(string uri); + + [AgentMethod("textDocument/didSave")] + void DidSave(string uri); + + [AgentMethod("textDocument/didClose")] + void DidClose(ProtocolTextDocument docState); + } +} + diff --git a/src/Cody.Core/Agent/IAgentClientFactory.cs b/src/Cody.Core/Agent/INotificationHandler.cs similarity index 66% rename from src/Cody.Core/Agent/IAgentClientFactory.cs rename to src/Cody.Core/Agent/INotificationHandler.cs index bc8e8301..d59d5802 100644 --- a/src/Cody.Core/Agent/IAgentClientFactory.cs +++ b/src/Cody.Core/Agent/INotificationHandler.cs @@ -6,8 +6,7 @@ namespace Cody.Core.Agent { - public interface IAgentClientFactory + public interface INotificationHandler { - IAgentClient CreateAgentClient(); } } diff --git a/src/Cody.Core/Agent/Connector/InitializeCallback.cs b/src/Cody.Core/Agent/InitializeCallback.cs similarity index 97% rename from src/Cody.Core/Agent/Connector/InitializeCallback.cs rename to src/Cody.Core/Agent/InitializeCallback.cs index 25ee06d6..caad707b 100644 --- a/src/Cody.Core/Agent/Connector/InitializeCallback.cs +++ b/src/Cody.Core/Agent/InitializeCallback.cs @@ -11,7 +11,7 @@ using System.Text; using System.Threading.Tasks; -namespace Cody.Core.Agent.Connector +namespace Cody.Core.Agent { public class InitializeCallback { @@ -35,7 +35,7 @@ public InitializeCallback( this.log = log; } - public async Task Initialize(IAgentClient client) + public async Task Initialize(IAgentService client) { // TODO: Get the solution directory path that the user is working on. var solutionDirPath = Environment.GetFolderPath(Environment.SpecialFolder.UserProfile); diff --git a/src/Cody.Core/Agent/NotificationHandlers.cs b/src/Cody.Core/Agent/NotificationHandlers.cs index f76392c1..88f65c4a 100644 --- a/src/Cody.Core/Agent/NotificationHandlers.cs +++ b/src/Cody.Core/Agent/NotificationHandlers.cs @@ -1,15 +1,12 @@ -using Cody.Core.Agent.Connector; -using Cody.Core.Agent.Protocol; +using Cody.Core.Agent.Protocol; using Newtonsoft.Json.Linq; -using StreamJsonRpc; using System; -using EnvDTE; using EnvDTE80; using System.Threading.Tasks; namespace Cody.Core.Agent { - public class NotificationHandlers + public class NotificationHandlers : INotificationHandler { public NotificationHandlers() { @@ -20,9 +17,9 @@ public NotificationHandlers() public event EventHandler OnSetHtmlEvent; public event EventHandler OnPostMessageEvent; - public IAgentClient agentClient; + public IAgentService agentClient; - public void SetAgentClient(IAgentClient agentClient) => this.agentClient = agentClient; + public void SetAgentClient(IAgentService agentClient) => this.agentClient = agentClient; // Send a message to the host from webview. public async Task SendWebviewMessage(string handle, string message) @@ -70,19 +67,19 @@ await agentClient.ReceiveMessageStringEncoded(new ReceiveMessageStringEncodedPar }); } - [JsonRpcMethod("debug/message")] + [AgentNotification("debug/message")] public void Debug(string channel, string message) { System.Diagnostics.Debug.WriteLine(message, "Agent Debug"); } - [JsonRpcMethod("webview/registerWebview")] + [AgentNotification("webview/registerWebview")] public void RegisterWebview(string handle) { System.Diagnostics.Debug.WriteLine(handle, "Agent registerWebview"); } - [JsonRpcMethod("webview/registerWebviewViewProvider")] + [AgentNotification("webview/registerWebviewViewProvider")] public async Task RegisterWebviewViewProvider(string viewId, bool retainContextWhenHidden) { System.Diagnostics.Debug.WriteLine(viewId, retainContextWhenHidden, "Agent registerWebviewViewProvider"); @@ -95,13 +92,13 @@ await agentClient.ResolveWebviewView(new ResolveWebviewViewParams }); } - [JsonRpcMethod("webview/createWebviewPanel", UseSingleObjectParameterDeserialization = true)] + [AgentNotification("webview/createWebviewPanel", deserializeToSingleObject: true)] public void CreateWebviewPanel(CreateWebviewPanelParams panelParams) { System.Diagnostics.Debug.WriteLine(panelParams, "Agent createWebviewPanel"); } - [JsonRpcMethod("webview/setOptions")] + [AgentNotification("webview/setOptions")] public void SetOptions(string handle, DefiniteWebviewOptions options) { if (options.EnableCommandUris is bool enableCmd) @@ -114,67 +111,61 @@ public void SetOptions(string handle, DefiniteWebviewOptions options) } } - [JsonRpcMethod("webview/setHtml")] + [AgentNotification("webview/setHtml")] public void SetHtml(string handle, string html) { System.Diagnostics.Debug.WriteLine(html, "Agent setHtml"); OnSetHtmlEvent?.Invoke(this, new SetHtmlEvent() { Handle = handle, Html = html }); } - [JsonRpcMethod("webview/PostMessage")] + [AgentNotification("webview/PostMessage")] public void PostMessage(string handle, string message) { PostMessageStringEncoded(handle, message); } - [JsonRpcMethod("webview/postMessageStringEncoded")] + [AgentNotification("webview/postMessageStringEncoded")] public void PostMessageStringEncoded(string id, string stringEncodedMessage) { System.Diagnostics.Debug.WriteLine(stringEncodedMessage, "Agent postMessageStringEncoded"); PostWebMessageAsJson?.Invoke(stringEncodedMessage); } - [JsonRpcMethod("webview/didDisposeNative")] + [AgentNotification("webview/didDisposeNative")] public void DidDisposeNative(string handle) { - ; + } - [JsonRpcMethod("extensionConfiguration/didChange")] + [AgentNotification("extensionConfiguration/didChange", deserializeToSingleObject: true)] public void ExtensionConfigDidChange(ExtensionConfiguration config) { System.Diagnostics.Debug.WriteLine(config, "Agent didChange"); } - [JsonRpcMethod("webview/dispose")] + [AgentNotification("webview/dispose")] public void Dispose(string handle) { System.Diagnostics.Debug.WriteLine(handle, "Agent dispose"); } - [JsonRpcMethod("webview/reveal")] + [AgentNotification("webview/reveal")] public void Reveal(string handle, int viewColumn, bool preserveFocus) { System.Diagnostics.Debug.WriteLine(handle, "Agent reveal"); } - [JsonRpcMethod("webview/setTitle")] + [AgentNotification("webview/setTitle")] public void SetTitle(string handle, string title) { System.Diagnostics.Debug.WriteLine(title, "Agent setTitle"); } - [JsonRpcMethod("webview/setIconPath")] + [AgentNotification("webview/setIconPath")] public void SetIconPath(string handle, string iconPathUri) { System.Diagnostics.Debug.WriteLine(iconPathUri, "Agent setIconPath"); } - [JsonRpcMethod("webview/createWebviewPanel")] - public void CreateWebviewPanel(string handle, string viewType, string title, ShowOptions showOptions, bool enableScripts, bool enableForms, bool enableCommandUris, bool enableFindWidget, bool retainContextWhenHidden) - { - System.Diagnostics.Debug.WriteLine(title, "Agent createWebviewPanel"); - } - } } diff --git a/src/Cody.Core/Agent/Protocol/ReceiveMessageStringEncodedParams.cs b/src/Cody.Core/Agent/Protocol/ReceiveMessageStringEncodedParams.cs new file mode 100644 index 00000000..a9c167f9 --- /dev/null +++ b/src/Cody.Core/Agent/Protocol/ReceiveMessageStringEncodedParams.cs @@ -0,0 +1,14 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Cody.Core.Agent.Protocol +{ + public class ReceiveMessageStringEncodedParams + { + public string Id { get; set; } + public string MessageStringEncoded { get; set; } + } +} diff --git a/src/Cody.Core/Agent/Protocol/ResolveWebviewViewParams.cs b/src/Cody.Core/Agent/Protocol/ResolveWebviewViewParams.cs new file mode 100644 index 00000000..bf2eff02 --- /dev/null +++ b/src/Cody.Core/Agent/Protocol/ResolveWebviewViewParams.cs @@ -0,0 +1,10 @@ +using System; + +namespace Cody.Core.Agent.Protocol +{ + public class ResolveWebviewViewParams + { + public string ViewId { get; set; } + public string WebviewHandle { get; set; } + } +} diff --git a/src/Cody.Core/Cody.Core.csproj b/src/Cody.Core/Cody.Core.csproj index 6dc512cf..3548634a 100644 --- a/src/Cody.Core/Cody.Core.csproj +++ b/src/Cody.Core/Cody.Core.csproj @@ -40,122 +40,10 @@ True - - ..\packages\MessagePack.2.5.108\lib\netstandard2.0\MessagePack.dll - - - ..\packages\MessagePack.Annotations.2.5.108\lib\netstandard2.0\MessagePack.Annotations.dll - - - ..\packages\Microsoft.Bcl.AsyncInterfaces.8.0.0\lib\net462\Microsoft.Bcl.AsyncInterfaces.dll - - - ..\packages\Microsoft.NET.StringTools.17.4.0\lib\net472\Microsoft.NET.StringTools.dll - - - ..\packages\Microsoft.VisualStudio.Threading.17.9.28\lib\net472\Microsoft.VisualStudio.Threading.dll - - - ..\packages\Microsoft.VisualStudio.Validation.17.8.8\lib\netstandard2.0\Microsoft.VisualStudio.Validation.dll - - - ..\packages\Microsoft.Win32.Registry.5.0.0\lib\net461\Microsoft.Win32.Registry.dll - - - ..\packages\Nerdbank.Streams.2.10.69\lib\netstandard2.0\Nerdbank.Streams.dll - - - ..\packages\Newtonsoft.Json.13.0.1\lib\net45\Newtonsoft.Json.dll - - - ..\packages\StreamJsonRpc.2.9.85\lib\netstandard2.0\StreamJsonRpc.dll - - - ..\packages\System.Buffers.4.5.1\lib\net461\System.Buffers.dll - - - ..\packages\System.Collections.Immutable.7.0.0\lib\net462\System.Collections.Immutable.dll - - - ..\packages\System.Diagnostics.DiagnosticSource.8.0.0\lib\net462\System.Diagnostics.DiagnosticSource.dll - - - ..\packages\System.IO.4.3.0\lib\net462\System.IO.dll - True - True - - - ..\packages\System.IO.Pipelines.7.0.0\lib\net462\System.IO.Pipelines.dll - - - ..\packages\System.Memory.4.5.5\lib\net461\System.Memory.dll - - - ..\packages\System.Net.Http.4.3.4\lib\net46\System.Net.Http.dll - True - True - - - ..\packages\System.Net.WebSockets.4.3.0\lib\net46\System.Net.WebSockets.dll - True - True - - - ..\packages\System.Numerics.Vectors.4.5.0\lib\net46\System.Numerics.Vectors.dll - - - ..\packages\System.Runtime.4.3.0\lib\net462\System.Runtime.dll - True - True - - - ..\packages\System.Runtime.CompilerServices.Unsafe.6.0.0\lib\net461\System.Runtime.CompilerServices.Unsafe.dll - - - ..\packages\System.Security.AccessControl.5.0.0\lib\net461\System.Security.AccessControl.dll - - - ..\packages\System.Security.Cryptography.Algorithms.4.3.0\lib\net463\System.Security.Cryptography.Algorithms.dll - True - True - - - ..\packages\System.Security.Cryptography.Encoding.4.3.0\lib\net46\System.Security.Cryptography.Encoding.dll - True - True - - - ..\packages\System.Security.Cryptography.Primitives.4.3.0\lib\net46\System.Security.Cryptography.Primitives.dll - True - True - - - ..\packages\System.Security.Cryptography.X509Certificates.4.3.0\lib\net461\System.Security.Cryptography.X509Certificates.dll - True - True - - - ..\packages\System.Security.Principal.Windows.5.0.0\lib\net461\System.Security.Principal.Windows.dll - - - ..\packages\System.Text.Encodings.Web.7.0.0\lib\net462\System.Text.Encodings.Web.dll - - - ..\packages\System.Text.Json.7.0.3\lib\net462\System.Text.Json.dll - - - ..\packages\System.Threading.Tasks.Dataflow.7.0.0\lib\net462\System.Threading.Tasks.Dataflow.dll - - - ..\packages\System.Threading.Tasks.Extensions.4.5.4\lib\net461\System.Threading.Tasks.Extensions.dll - - - ..\packages\System.ValueTuple.4.5.0\lib\net47\System.ValueTuple.dll - @@ -167,15 +55,11 @@ Properties\AssemblyVersion.cs - - - - - - - - - + + + + + @@ -189,8 +73,10 @@ + + @@ -208,22 +94,9 @@ - - - - - - - - - + + 13.0.1 + - - - - This project references NuGet package(s) that are missing on this computer. Use NuGet Package Restore to download them. For more information, see http://go.microsoft.com/fwlink/?LinkID=322105. The missing file is {0}. - - - \ No newline at end of file diff --git a/src/Cody.Core/DocumentSync/DocumentSyncCallback.cs b/src/Cody.Core/DocumentSync/DocumentSyncCallback.cs index 99620b89..2b8cae94 100644 --- a/src/Cody.Core/DocumentSync/DocumentSyncCallback.cs +++ b/src/Cody.Core/DocumentSync/DocumentSyncCallback.cs @@ -7,18 +7,17 @@ using System.Text; using System.Text.RegularExpressions; using System.Threading.Tasks; -using static Nerdbank.Streams.MultiplexingStream; namespace Cody.Core.DocumentSync { public class DocumentSyncCallback : IDocumentSyncActions { private ILog logger; - private IAgentClientFactory agentClientFactory; + private IAgentService agentService; - public DocumentSyncCallback(IAgentClientFactory agentClientFactory, ILog logger) + public DocumentSyncCallback(IAgentService agentService, ILog logger) { - this.agentClientFactory = agentClientFactory; + this.agentService = agentService; this.logger = logger; } @@ -86,7 +85,7 @@ public void OnChanged(string fullPath, DocumentRange visibleRange, DocumentRange }).ToArray() }; - agentClientFactory.CreateAgentClient().DidChange(docState); + agentService.DidChange(docState); } public void OnClosed(string fullPath) @@ -99,13 +98,13 @@ public void OnClosed(string fullPath) }; // Only the 'uri' property is required, other properties are ignored. - agentClientFactory.CreateAgentClient().DidClose(docState); + agentService.DidClose(docState); } public void OnFocus(string fullPath) { logger.Debug($"Sending DidFocus() for '{fullPath}'"); - agentClientFactory.CreateAgentClient().DidFocus(ToUri(fullPath)); + agentService.DidFocus(ToUri(fullPath)); } @@ -151,14 +150,14 @@ public void OnOpened(string fullPath, string content, DocumentRange visibleRange } }; - agentClientFactory.CreateAgentClient().DidOpen(docState); + agentService.DidOpen(docState); } public void OnSaved(string fullPath) { logger.Debug($"Sending DidSave() for '{fullPath}'"); - agentClientFactory.CreateAgentClient().DidSave(ToUri(fullPath)); + agentService.DidSave(ToUri(fullPath)); } } } diff --git a/src/Cody.Core/packages.config b/src/Cody.Core/packages.config deleted file mode 100644 index f9cd0a66..00000000 --- a/src/Cody.Core/packages.config +++ /dev/null @@ -1,38 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/src/Cody.UI/ViewModels/MainViewModel.cs b/src/Cody.UI/ViewModels/MainViewModel.cs index bded2d78..a02eec51 100644 --- a/src/Cody.UI/ViewModels/MainViewModel.cs +++ b/src/Cody.UI/ViewModels/MainViewModel.cs @@ -1,5 +1,4 @@ using Cody.Core.Agent; -using Cody.Core.Agent.Connector; using Cody.Core.Logging; using Cody.UI.MVVM; using System.Windows.Input; diff --git a/src/Cody.VisualStudio/Client/AgentClient.cs b/src/Cody.VisualStudio/Client/AgentClient.cs new file mode 100644 index 00000000..e8d56e08 --- /dev/null +++ b/src/Cody.VisualStudio/Client/AgentClient.cs @@ -0,0 +1,124 @@ +using Cody.Core.Logging; +using Newtonsoft.Json.Converters; +using Newtonsoft.Json.Serialization; +using StreamJsonRpc; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Cody.VisualStudio.Client +{ + public class AgentClient + { + private AgentClientOptions options; + private ILog log; + + private IAgentConnector connector; + private JsonRpc jsonRpc; + + public AgentClient(AgentClientOptions options, ILog log) + { + this.options = options; + this.log = log; + } + + public bool IsConnected { get; private set; } + + public void Start() + { + if (IsConnected) return; + + connector = CreateConnector(); + connector.ErrorReceived += OnErrorReceived; + connector.Disconnected += OnAgentDisconnected; + + connector.Connect(options); + + var jsonMessageFormatter = new JsonMessageFormatter(); + jsonMessageFormatter.JsonSerializer.ContractResolver = new DefaultContractResolver() + { + NamingStrategy = new CamelCaseNamingStrategy() + }; + jsonMessageFormatter.JsonSerializer.Converters.Add(new StringEnumConverter(new CamelCaseNamingStrategy())); + var handler = new HeaderDelimitedMessageHandler(connector.SendingStream, connector.ReceivingStream, jsonMessageFormatter); + + jsonRpc = new JsonRpc(handler); + + foreach (var target in options.NotificationHandlers) + { + var methods = NameTransformer.GetNotificationMethods(target.GetType()); + foreach (var method in methods) jsonRpc.AddLocalRpcMethod(method.Key, target, method.Value); + } + + jsonRpc.StartListening(); + IsConnected = true; + log.Info("A connection with the agent has been established."); + } + + public T CreateAgentService() where T : class + { + var proxyOptions = new JsonRpcProxyOptions { MethodNameTransform = NameTransformer.CreateTransformer() }; + return jsonRpc.Attach(proxyOptions); + } + + private void OnErrorReceived(object sender, string error) + { + log.Error(error, "Agent errors"); + } + + private IAgentConnector CreateConnector() + { + IAgentConnector connector; + if (options.ConnectToRemoteAgent) + { + connector = new RemoteAgentConnector(); + log.Info("Remote agent connector created"); + } + else + { + connector = new AgentProcessConnector(); + log.Info("Process agent connector created"); + } + + return connector; + } + + private void OnAgentDisconnected(object sender, int exitCode) + { + DisconnectInternal(); + if (exitCode == 0) log.Info("The agent's connection has ended."); + else log.Error($"The agent connection unexpectedly ended with code {exitCode}."); + + if (options.RestartAgentOnFailure && exitCode != 0) + { + log.Info("Restarting the agent."); + + Start(); + } + } + + public void Stop() + { + if (!IsConnected) return; + + DisconnectInternal(); + } + + private void DisconnectInternal() + { + jsonRpc.Dispose(); + connector.ErrorReceived -= OnErrorReceived; + connector.Disconnected -= OnAgentDisconnected; + connector.Disconnect(); + + jsonRpc = null; + connector = null; + + IsConnected = false; + log.Info("The connection with the agent has been terminated."); + } + } + +} diff --git a/src/Cody.VisualStudio/Client/AgentClientOptions.cs b/src/Cody.VisualStudio/Client/AgentClientOptions.cs new file mode 100644 index 00000000..ee846696 --- /dev/null +++ b/src/Cody.VisualStudio/Client/AgentClientOptions.cs @@ -0,0 +1,27 @@ +using Cody.Core.Agent; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Cody.VisualStudio.Client +{ + public class AgentClientOptions + { + public bool Debug { get; set; } + + public bool RestartAgentOnFailure { get; set; } = true; + + public bool ConnectToRemoteAgent { get; set; } = false; + + /// + /// If non-null, the TCP port to connect to an existing Agent instance on. + /// + public int RemoteAgentPort { get; set; } = 3113; + + public string AgentDirectory { get; set; } + + public List NotificationHandlers { get; set; } = new List(); + } +} diff --git a/src/Cody.VisualStudio/Client/AgentProcessConnector.cs b/src/Cody.VisualStudio/Client/AgentProcessConnector.cs new file mode 100644 index 00000000..9f962666 --- /dev/null +++ b/src/Cody.VisualStudio/Client/AgentProcessConnector.cs @@ -0,0 +1,80 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.IO; +using System.Linq; +using System.Runtime.InteropServices; +using System.Text; +using System.Threading.Tasks; + +namespace Cody.VisualStudio.Client +{ + public class AgentProcessConnector : IAgentConnector + { + private Process process; + + public event EventHandler Disconnected; + public event EventHandler ErrorReceived; + + public void Connect(AgentClientOptions options) + { + var path = Path.Combine(options.AgentDirectory, GetAgentFileName()); + + if (!File.Exists(path)) + throw new FileNotFoundException("Agent file not found", path); + + process = new Process(); + process.StartInfo.FileName = path; + process.StartInfo.Arguments = GetAgentArguments(options.Debug); + process.StartInfo.WorkingDirectory = options.AgentDirectory; + process.StartInfo.UseShellExecute = false; + process.StartInfo.RedirectStandardOutput = true; + process.StartInfo.RedirectStandardError = true; + process.StartInfo.RedirectStandardInput = true; + process.StartInfo.CreateNoWindow = true; + process.EnableRaisingEvents = true; + process.Exited += OnProcessExited; + process.ErrorDataReceived += OnErrorDataReceived; + + process.Start(); + process.BeginErrorReadLine(); + } + + private void OnErrorDataReceived(object sender, DataReceivedEventArgs e) => ErrorReceived?.Invoke(this, e.Data); + + private void OnProcessExited(object sender, EventArgs e) => Disconnected?.Invoke(this, process.ExitCode); + + public void Disconnect() + { + if (process != null && !process.HasExited) process.Kill(); + } + + public Stream SendingStream => process?.StandardInput?.BaseStream; + + public Stream ReceivingStream => process?.StandardOutput?.BaseStream; + + private string GetAgentFileName() + { + if (RuntimeInformation.ProcessArchitecture == Architecture.Arm64) + return "node-win-arm64.exe"; + + return "node-win-x64.exe"; + } + + private string GetAgentArguments(bool debugMode) + { + var argList = new List(); + + if (debugMode) + { + argList.Add("--inspect"); + argList.Add("--enable-source-maps"); + } + + argList.Add("index.js api jsonrpc-stdio"); + + var arguments = string.Join(" ", argList); + return arguments; + } + } +} diff --git a/src/Cody.VisualStudio/Client/IAgentConnector.cs b/src/Cody.VisualStudio/Client/IAgentConnector.cs new file mode 100644 index 00000000..086c6922 --- /dev/null +++ b/src/Cody.VisualStudio/Client/IAgentConnector.cs @@ -0,0 +1,23 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Cody.VisualStudio.Client +{ + public interface IAgentConnector + { + void Connect(AgentClientOptions options); + + void Disconnect(); + + event EventHandler Disconnected; + + event EventHandler ErrorReceived; + + Stream SendingStream { get; } + Stream ReceivingStream { get; } + } +} diff --git a/src/Cody.VisualStudio/Client/NameTransformer.cs b/src/Cody.VisualStudio/Client/NameTransformer.cs new file mode 100644 index 00000000..d2c8e096 --- /dev/null +++ b/src/Cody.VisualStudio/Client/NameTransformer.cs @@ -0,0 +1,44 @@ +using Cody.Core.Agent; +using StreamJsonRpc; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Reflection; +using System.Text; +using System.Threading.Tasks; + +namespace Cody.VisualStudio.Client +{ + public class NameTransformer + { + public static Func CreateTransformer(Type type) + { + var dic = type + .GetMethods() + .ToDictionary(k => k.Name, v => v.GetCustomAttribute()?.Name ?? v.Name); + + Func func = (x) => dic[x]; + + return func; + } + + public static Func CreateTransformer() where T : class => CreateTransformer(typeof(T)); + + public static IReadOnlyDictionary GetNotificationMethods(Type type) + { + var dic = type + .GetMethods() + .Where(x => x.GetCustomAttribute() != null) + .ToDictionary(k => k, v => + { + var att = v.GetCustomAttribute(); + return new JsonRpcMethodAttribute(att.Name) + { + UseSingleObjectParameterDeserialization = att.DeserializeToSingleObject + }; + }); + + return dic; + } + } +} diff --git a/src/Cody.VisualStudio/Client/RemoteAgentConnector.cs b/src/Cody.VisualStudio/Client/RemoteAgentConnector.cs new file mode 100644 index 00000000..6ab579a0 --- /dev/null +++ b/src/Cody.VisualStudio/Client/RemoteAgentConnector.cs @@ -0,0 +1,38 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Net.Sockets; +using System.Net; +using System.Text; +using System.Threading.Tasks; + +namespace Cody.VisualStudio.Client +{ + public class RemoteAgentConnector : IAgentConnector + { + private TcpClient client; + + public event EventHandler Disconnected; + public event EventHandler ErrorReceived; + + public void Connect(AgentClientOptions options) + { + client = new TcpClient(); + client.Connect(IPAddress.Loopback, options.RemoteAgentPort); + } + + public void Disconnect() + { + if (client != null && client.Connected) + { + client.Close(); + client = null; + Disconnected?.Invoke(this, 0); + } + } + + public Stream SendingStream => client?.GetStream(); + public Stream ReceivingStream => client?.GetStream(); + } +} diff --git a/src/Cody.VisualStudio/Cody.VisualStudio.csproj b/src/Cody.VisualStudio/Cody.VisualStudio.csproj index 5dbe4bb3..25f71551 100644 --- a/src/Cody.VisualStudio/Cody.VisualStudio.csproj +++ b/src/Cody.VisualStudio/Cody.VisualStudio.csproj @@ -46,6 +46,12 @@ 4 + + + + + + diff --git a/src/Cody.VisualStudio/CodyPackage.cs b/src/Cody.VisualStudio/CodyPackage.cs index b5f75d5c..47c19c2a 100644 --- a/src/Cody.VisualStudio/CodyPackage.cs +++ b/src/Cody.VisualStudio/CodyPackage.cs @@ -20,12 +20,13 @@ using System.IO; using Cody.Core.Settings; using Cody.Core.Infrastructure; -using Cody.Core.Agent.Connector; using Cody.Core.Agent; using Cody.Core.DocumentSync; using Microsoft.VisualStudio.Editor; using Microsoft.VisualStudio.ComponentModelHost; using Microsoft.VisualStudio.Shell.Interop; +using Cody.VisualStudio.Client; +using System.Collections.Generic; namespace Cody.VisualStudio { @@ -60,7 +61,8 @@ public sealed class CodyPackage : AsyncPackage public IVersionService VersionService; public IVsVersionService VsVersionService; public MainView MainView; - public AgentConnector AgentConnector; + public AgentClient AgentClient; + public IAgentService AgentService; public IUserSettingsService UserSettingsService; public InitializeCallback InitializeService; public IStatusbarService StatusbarService; @@ -68,7 +70,6 @@ public sealed class CodyPackage : AsyncPackage public NotificationHandlers NotificationHandlers; public IVsEditorAdaptersFactoryService VsEditorAdaptersFactoryService; public IVsUIShell VsUIShell; - public IAgentClientFactory AgentClientFactory; public DocumentsSyncService DocumentsSyncService; protected override async Task InitializeAsync(CancellationToken cancellationToken, IProgress progress) @@ -165,28 +166,34 @@ private async Task InitializeAgent() var agentDir = Path.Combine(Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location), "Agent"); NotificationHandlers = new NotificationHandlers(); - // Set the env var to 3113 when running with local agent. - var portNumber = int.TryParse(Environment.GetEnvironmentVariable("CODY_VS_DEV_PORT"), out int port) ? port : (int?)null; - var options = new AgentConnectorOptions + var devPort = Environment.GetEnvironmentVariable("CODY_VS_DEV_PORT"); + var portNumber = int.TryParse(devPort, out int port) ? port : 3113; + + var options = new AgentClientOptions { - NotificationsTarget = NotificationHandlers, + NotificationHandlers = new List { NotificationHandlers }, AgentDirectory = agentDir, RestartAgentOnFailure = true, - AfterConnection = (client) => InitializeService.Initialize(client), - Port = portNumber, + ConnectToRemoteAgent = devPort != null, + RemoteAgentPort = portNumber, }; - AgentConnector = new AgentConnector(options, Logger); - AgentClientFactory = new AgentClientFactory(AgentConnector); + AgentClient = new AgentClient(options, Logger); WebView2Dev.InitializeController(ThemeService.GetThemingScript()); NotificationHandlers.PostWebMessageAsJson = WebView2Dev.PostWebMessageAsJson; - _ = Task.Run(() => AgentConnector.Connect()) + _ = Task.Run(() => AgentClient.Start()) + .ContinueWith(async x => + { + AgentService = AgentClient.CreateAgentService(); + NotificationHandlers.SetAgentClient(AgentService); + await InitializeService.Initialize(AgentService); + }) .ContinueWith(x => { - var documentSyncCallback = new DocumentSyncCallback(AgentClientFactory, Logger); + var documentSyncCallback = new DocumentSyncCallback(AgentService, Logger); DocumentsSyncService = new DocumentsSyncService(VsUIShell, documentSyncCallback, VsEditorAdaptersFactoryService); DocumentsSyncService.Initialize(); })