From dcecc260f0f75f0bd92443982ec6b547335fd4da Mon Sep 17 00:00:00 2001 From: Jerome Laban Date: Mon, 8 Jan 2024 15:43:31 -0500 Subject: [PATCH] feat: Add support for devserver IDE channel --- .../IDEChannel/IIdeChannelServerProvider.cs | 9 + .../IDEChannel/IdeChannelServer.cs | 33 + .../IDEChannel/IdeChannelServerProvider.cs | 94 +++ src/Uno.UI.RemoteControl.Host/Program.cs | 22 +- .../RemoteControlExtensions.cs | 11 +- .../RemoteControlServer.cs | 475 ++++++------- .../Uno.UI.RemoteControl.Host.csproj | 4 + .../AssemblyInfo.cs | 4 + .../IDEChannel/ForceHotReloadIdeMessage.cs | 5 + .../IDEChannel/IIdeChannelServer.cs | 15 + .../IDEChannel/IdeMessage.cs | 6 + .../IDEChannel/KeepAliveIdeMessage.cs | 6 + .../IRemoteControlServer.cs | 3 + src/Uno.UI.RemoteControl.VS/EntryPoint.cs | 624 +++++++++--------- .../Helpers/ILogger.cs | 19 + .../IDEChannel/IDEChannelClient.cs | 100 +++ .../Uno.UI.RemoteControl.VS.csproj | 30 +- .../Helpers/WebSocketHelper.cs | 7 +- 18 files changed, 915 insertions(+), 552 deletions(-) create mode 100644 src/Uno.UI.RemoteControl.Host/IDEChannel/IIdeChannelServerProvider.cs create mode 100644 src/Uno.UI.RemoteControl.Host/IDEChannel/IdeChannelServer.cs create mode 100644 src/Uno.UI.RemoteControl.Host/IDEChannel/IdeChannelServerProvider.cs create mode 100644 src/Uno.UI.RemoteControl.Messaging/AssemblyInfo.cs create mode 100644 src/Uno.UI.RemoteControl.Messaging/IDEChannel/ForceHotReloadIdeMessage.cs create mode 100644 src/Uno.UI.RemoteControl.Messaging/IDEChannel/IIdeChannelServer.cs create mode 100644 src/Uno.UI.RemoteControl.Messaging/IDEChannel/IdeMessage.cs create mode 100644 src/Uno.UI.RemoteControl.Messaging/IDEChannel/KeepAliveIdeMessage.cs create mode 100644 src/Uno.UI.RemoteControl.VS/Helpers/ILogger.cs create mode 100644 src/Uno.UI.RemoteControl.VS/IDEChannel/IDEChannelClient.cs diff --git a/src/Uno.UI.RemoteControl.Host/IDEChannel/IIdeChannelServerProvider.cs b/src/Uno.UI.RemoteControl.Host/IDEChannel/IIdeChannelServerProvider.cs new file mode 100644 index 000000000000..1150928c45f0 --- /dev/null +++ b/src/Uno.UI.RemoteControl.Host/IDEChannel/IIdeChannelServerProvider.cs @@ -0,0 +1,9 @@ +using System.Threading.Tasks; +using Uno.UI.RemoteControl.Messaging.IdeChannel; + +namespace Uno.UI.RemoteControl.Host.IdeChannel; + +internal interface IIdeChannelServerProvider +{ + Task GetIdeChannelServerAsync(); +} diff --git a/src/Uno.UI.RemoteControl.Host/IDEChannel/IdeChannelServer.cs b/src/Uno.UI.RemoteControl.Host/IDEChannel/IdeChannelServer.cs new file mode 100644 index 000000000000..e2d9332da466 --- /dev/null +++ b/src/Uno.UI.RemoteControl.Host/IDEChannel/IdeChannelServer.cs @@ -0,0 +1,33 @@ +using System; +using System.Threading.Tasks; +using Uno.UI.RemoteControl.Messaging.IdeChannel; + +namespace Uno.UI.RemoteControl.Host.IdeChannel; + +internal class IdeChannelServer : IIdeChannelServer +{ + private IServiceProvider _serviceProvider; + + public IdeChannelServer(IServiceProvider serviceProvider) + { + _serviceProvider = serviceProvider; + } + + public event EventHandler? MessageFromIDE; + + public event EventHandler? MessageFromDevServer; + + public async Task SendToIdeAsync(IdeMessage message) + { + MessageFromDevServer?.Invoke(this, message); + + await Task.Yield(); + } + + public async Task SendToDevServerAsync(IdeMessage message) + { + MessageFromIDE?.Invoke(this, message); + + await Task.Yield(); + } +} diff --git a/src/Uno.UI.RemoteControl.Host/IDEChannel/IdeChannelServerProvider.cs b/src/Uno.UI.RemoteControl.Host/IDEChannel/IdeChannelServerProvider.cs new file mode 100644 index 000000000000..841f1560c051 --- /dev/null +++ b/src/Uno.UI.RemoteControl.Host/IDEChannel/IdeChannelServerProvider.cs @@ -0,0 +1,94 @@ +using System; +using System.IO.Pipes; +using System.Net.Http.Headers; +using System.Threading.Tasks; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.Logging; +using StreamJsonRpc; +using Uno.UI.RemoteControl.Messaging.IdeChannel; + +namespace Uno.UI.RemoteControl.Host.IdeChannel; + +internal class IdeChannelServerProvider : IIdeChannelServerProvider +{ + private readonly ILogger _logger; + private readonly IConfiguration _configuration; + private readonly IServiceProvider _serviceProvider; + private readonly Task _initializeTask; + private NamedPipeServerStream? _pipeServer; + private IdeChannelServer? _ideChannelServer; + private JsonRpc? _rpcServer; + + public IdeChannelServerProvider(ILogger logger, IConfiguration configuration, IServiceProvider serviceProvider) + { + _logger = logger; + _configuration = configuration; + _serviceProvider = serviceProvider; + + _initializeTask = Task.Run(Initialize); + } + + private async Task Initialize() + { + if (!Guid.TryParse(_configuration["ideChannel"], out var ideChannel)) + { + _logger.LogDebug("No IDE Channel ID specified, skipping"); + return null; + } + + _pipeServer = new NamedPipeServerStream( + pipeName: ideChannel.ToString(), + direction: PipeDirection.InOut, + maxNumberOfServerInstances: 1, + transmissionMode: PipeTransmissionMode.Byte, + options: PipeOptions.Asynchronous | PipeOptions.WriteThrough); + + await _pipeServer.WaitForConnectionAsync(); + + if (_logger.IsEnabled(LogLevel.Debug)) + { + _logger.LogDebug("IDE Connected"); + } + + _ideChannelServer = new IdeChannelServer(_serviceProvider); + _ideChannelServer.MessageFromIDE += OnMessageFromIDE; + _rpcServer = JsonRpc.Attach(_pipeServer, _ideChannelServer); + + _ = StartKeepaliveAsync(); + + return _ideChannelServer; + } + + private async Task StartKeepaliveAsync() + { + while (_pipeServer?.IsConnected ?? false) + { + _ideChannelServer?.SendToIdeAsync(new KeepAliveIdeMessage()); + + await Task.Delay(5000); + } + } + + private void OnMessageFromIDE(object? sender, IdeMessage ideMessage) + { + if (ideMessage is KeepAliveIdeMessage) + { +#if DEBUG + _logger.LogDebug("Keepalive from IDE"); +#endif + } + else + { + _logger.LogDebug($"Unknown message type {ideMessage?.GetType()} from IDE"); + } + } + + public async Task GetIdeChannelServerAsync() + { +#pragma warning disable IDE0022 // Use expression body for method +#pragma warning disable VSTHRD003 // Avoid awaiting foreign Tasks + return await _initializeTask; +#pragma warning restore VSTHRD003 // Avoid awaiting foreign Tasks +#pragma warning restore IDE0022 // Use expression body for method + } +} diff --git a/src/Uno.UI.RemoteControl.Host/Program.cs b/src/Uno.UI.RemoteControl.Host/Program.cs index 2486341b0d7c..eed62ebe6890 100644 --- a/src/Uno.UI.RemoteControl.Host/Program.cs +++ b/src/Uno.UI.RemoteControl.Host/Program.cs @@ -3,14 +3,19 @@ using System.IO; using Microsoft.AspNetCore.Hosting; using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; using Mono.Options; using Microsoft.Extensions.Logging; +using System.Diagnostics; +using System.ComponentModel; +using System.Threading.Tasks; +using Uno.UI.RemoteControl.Host.IdeChannel; namespace Uno.UI.RemoteControl.Host { class Program { - static void Main(string[] args) + static async Task Main(string[] args) { var httpPort = 0; var parentPID = 0; @@ -31,7 +36,7 @@ static void Main(string[] args) throw new ArgumentException($"The parent process id parameter is invalid {s}"); } } - }, + } }; p.Parse(args); @@ -41,7 +46,7 @@ static void Main(string[] args) throw new ArgumentException($"The httpPort parameter is required."); } - var host = new WebHostBuilder() + var builder = new WebHostBuilder() .UseSetting("UseIISIntegration", false.ToString()) .UseKestrel() .UseUrls($"http://*:{httpPort}/") @@ -56,11 +61,18 @@ static void Main(string[] args) { config.AddCommandLine(args); }) - .Build(); + .ConfigureServices(services => + { + services.AddSingleton(); + }); + + var host = builder.Build(); + + host.Services.GetService(); using var parentObserver = ParentProcessObserver.Observe(host, parentPID); - host.Run(); + await host.RunAsync(); } } } diff --git a/src/Uno.UI.RemoteControl.Host/RemoteControlExtensions.cs b/src/Uno.UI.RemoteControl.Host/RemoteControlExtensions.cs index a2304f2575a2..b229c96d0e89 100644 --- a/src/Uno.UI.RemoteControl.Host/RemoteControlExtensions.cs +++ b/src/Uno.UI.RemoteControl.Host/RemoteControlExtensions.cs @@ -1,10 +1,12 @@ -using System.Threading; +using System; +using System.Threading; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Routing; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; using Uno.Extensions; +using Uno.UI.RemoteControl.Host.IdeChannel; namespace Uno.UI.RemoteControl.Host { @@ -33,9 +35,12 @@ public static IApplicationBuilder UseRemoteControlServer( { if (context.RequestServices.GetService() is { } configuration) { - using (var server = new RemoteControlServer(configuration)) + using (var server = new RemoteControlServer( + configuration, + context.RequestServices.GetService() ?? throw new InvalidOperationException("IIDEChannelServerProvider is required"), + context.RequestServices)) { - await server.Run(await context.WebSockets.AcceptWebSocketAsync(), CancellationToken.None); + await server.RunAsync(await context.WebSockets.AcceptWebSocketAsync(), CancellationToken.None); } } else diff --git a/src/Uno.UI.RemoteControl.Host/RemoteControlServer.cs b/src/Uno.UI.RemoteControl.Host/RemoteControlServer.cs index 19080740f123..b67d8ea5ff7e 100644 --- a/src/Uno.UI.RemoteControl.Host/RemoteControlServer.cs +++ b/src/Uno.UI.RemoteControl.Host/RemoteControlServer.cs @@ -1,6 +1,8 @@ using System; using System.Collections.Generic; +using System.Diagnostics.Contracts; using System.IO; +using System.IO.Pipes; using System.Net.WebSockets; using System.Reflection; using System.Runtime.Loader; @@ -9,342 +11,367 @@ using Microsoft.Extensions.Configuration; using Microsoft.Extensions.Logging; using Newtonsoft.Json; +using StreamJsonRpc; using Uno.Extensions; using Uno.UI.RemoteControl.Helpers; +using Uno.UI.RemoteControl.Host.IdeChannel; using Uno.UI.RemoteControl.HotReload.Messages; using Uno.UI.RemoteControl.Messages; +using Uno.UI.RemoteControl.Messaging.IdeChannel; +using static Uno.UI.RemoteControl.Host.RemoteControlServer; -namespace Uno.UI.RemoteControl.Host +namespace Uno.UI.RemoteControl.Host; + +internal class RemoteControlServer : IRemoteControlServer, IDisposable { - internal class RemoteControlServer : IRemoteControlServer, IDisposable + private readonly object _loadContextGate = new(); + private static readonly Dictionary _loadContexts = new(); + private readonly Dictionary _processors = new(); + + private string? _resolveAssemblyLocation; + private WebSocket? _socket; + private IdeChannelServer? _ideChannelServer; + private readonly List _appInstanceIds = new(); + private readonly IConfiguration _configuration; + private readonly IIdeChannelServerProvider _ideChannelProvider; + private readonly IServiceProvider _serviceProvider; + + public RemoteControlServer(IConfiguration configuration, IIdeChannelServerProvider ideChannelProvider, IServiceProvider serviceProvider) { - private readonly object _loadContextGate = new(); - private static readonly Dictionary _loadContexts = new(); - private readonly Dictionary _processors = new(); - - private string? _resolveAssemblyLocation; - private WebSocket? _socket; - private readonly List _appInstanceIds = new(); - private readonly IConfiguration _configuration; + _configuration = configuration; + _ideChannelProvider = ideChannelProvider; + _serviceProvider = serviceProvider; - public RemoteControlServer(IConfiguration configuration) + if (this.Log().IsEnabled(LogLevel.Debug)) { - _configuration = configuration; - - if (this.Log().IsEnabled(LogLevel.Debug)) - { - this.Log().LogDebug("Starting RemoteControlServer"); - } + this.Log().LogDebug("Starting RemoteControlServer"); } + } - string IRemoteControlServer.GetServerConfiguration(string key) - => _configuration[key] ?? ""; + string IRemoteControlServer.GetServerConfiguration(string key) + => _configuration[key] ?? ""; - private AssemblyLoadContext GetAssemblyLoadContext(string applicationId) + private AssemblyLoadContext GetAssemblyLoadContext(string applicationId) + { + lock (_loadContextGate) { - lock (_loadContextGate) + if (_loadContexts.TryGetValue(applicationId, out var lc)) { - if (_loadContexts.TryGetValue(applicationId, out var lc)) - { - _loadContexts[applicationId] = (lc.Context, lc.Count + 1); + _loadContexts[applicationId] = (lc.Context, lc.Count + 1); - return lc.Context; - } + return lc.Context; + } - var loadContext = new AssemblyLoadContext(applicationId, isCollectible: true); - loadContext.Unloading += (e) => + var loadContext = new AssemblyLoadContext(applicationId, isCollectible: true); + loadContext.Unloading += (e) => + { + if (this.Log().IsEnabled(LogLevel.Debug)) { - if (this.Log().IsEnabled(LogLevel.Debug)) - { - this.Log().LogDebug("Unloading assembly context {name}", e.Name); - } - }; + this.Log().LogDebug("Unloading assembly context {name}", e.Name); + } + }; - // Add custom resolving so we can find dependencies even when the processor assembly - // is built for a different .net version than the host process. - loadContext.Resolving += (context, assemblyName) => + // Add custom resolving so we can find dependencies even when the processor assembly + // is built for a different .net version than the host process. + loadContext.Resolving += (context, assemblyName) => + { + if (!string.IsNullOrWhiteSpace(_resolveAssemblyLocation)) { - if (!string.IsNullOrWhiteSpace(_resolveAssemblyLocation)) + try { - try + var dir = Path.GetDirectoryName(_resolveAssemblyLocation); + if (!string.IsNullOrEmpty(dir)) { - var dir = Path.GetDirectoryName(_resolveAssemblyLocation); - if (!string.IsNullOrEmpty(dir)) + var relPath = Path.Combine(dir, assemblyName.Name + ".dll"); + if (File.Exists(relPath)) { - var relPath = Path.Combine(dir, assemblyName.Name + ".dll"); - if (File.Exists(relPath)) + if (this.Log().IsEnabled(LogLevel.Trace)) { - if (this.Log().IsEnabled(LogLevel.Trace)) - { - this.Log().LogTrace("Loading assembly from resolved path: {relPath}", relPath); - } - - using var fs = File.Open(relPath, FileMode.Open, FileAccess.Read, FileShare.Read); - return context.LoadFromStream(fs); + this.Log().LogTrace("Loading assembly from resolved path: {relPath}", relPath); } - } - } - catch (Exception exc) - { - if (this.Log().IsEnabled(LogLevel.Error)) - { - this.Log().LogError(exc, "Failed for load dependency: {assemblyName}", assemblyName); + + using var fs = File.Open(relPath, FileMode.Open, FileAccess.Read, FileShare.Read); + return context.LoadFromStream(fs); } } } - else + catch (Exception exc) { - if (this.Log().IsEnabled(LogLevel.Debug)) + if (this.Log().IsEnabled(LogLevel.Error)) { - this.Log().LogDebug("Failed for identify location of dependency: {assemblyName}", assemblyName); + this.Log().LogError(exc, "Failed for load dependency: {assemblyName}", assemblyName); } } - - // We haven't found the assembly in our context, let the runtime - // find it using standard resolution mechanisms. - return null; - }; - - if (!_loadContexts.TryAdd(applicationId, (loadContext, 1))) + } + else { - if (this.Log().IsEnabled(LogLevel.Trace)) + if (this.Log().IsEnabled(LogLevel.Debug)) { - this.Log().LogTrace("Failed to add a LoadContext for : {appId}", applicationId); + this.Log().LogDebug("Failed for identify location of dependency: {assemblyName}", assemblyName); } } - return loadContext; + // We haven't found the assembly in our context, let the runtime + // find it using standard resolution mechanisms. + return null; + }; + + if (!_loadContexts.TryAdd(applicationId, (loadContext, 1))) + { + if (this.Log().IsEnabled(LogLevel.Trace)) + { + this.Log().LogTrace("Failed to add a LoadContext for : {appId}", applicationId); + } } + + return loadContext; } + } - private void RegisterProcessor(IServerProcessor hotReloadProcessor) - => _processors[hotReloadProcessor.Scope] = hotReloadProcessor; + private void RegisterProcessor(IServerProcessor hotReloadProcessor) + => _processors[hotReloadProcessor.Scope] = hotReloadProcessor; - public async Task Run(WebSocket socket, CancellationToken ct) - { - _socket = socket; + public IdeChannelServer? IDEChannelServer => _ideChannelServer; - while (await WebSocketHelper.ReadFrame(socket, ct) is Frame frame) - { - if (frame.Scope == "RemoteControlServer") - { - if (frame.Name == ProcessorsDiscovery.Name) - { - ProcessDiscoveryFrame(frame); - continue; - } + public async Task RunAsync(WebSocket socket, CancellationToken ct) + { + _socket = socket; - if (frame.Name == KeepAliveMessage.Name) - { - if (this.Log().IsEnabled(LogLevel.Trace)) - { - this.Log().LogTrace($"Client Keepalive frame"); - } + await TryStartIDEChannelAsync(); - await SendFrame(new KeepAliveMessage()); - continue; - } + while (await WebSocketHelper.ReadFrame(socket, ct) is Frame frame) + { + if (frame.Scope == "RemoteControlServer") + { + if (frame.Name == ProcessorsDiscovery.Name) + { + ProcessDiscoveryFrame(frame); + continue; } - if (_processors.TryGetValue(frame.Scope, out var processor)) + if (frame.Name == KeepAliveMessage.Name) { - if (this.Log().IsEnabled(LogLevel.Debug)) + if (this.Log().IsEnabled(LogLevel.Trace)) { - this.Log().LogDebug("Received Frame [{Scope} / {Name}] to be processed by {processor}", frame.Scope, frame.Name, processor); + this.Log().LogTrace($"Client Keepalive frame"); } - await processor.ProcessFrame(frame); + await SendFrame(new KeepAliveMessage()); + continue; } - else + } + + if (_processors.TryGetValue(frame.Scope, out var processor)) + { + if (this.Log().IsEnabled(LogLevel.Debug)) { - if (this.Log().IsEnabled(LogLevel.Debug)) - { - this.Log().LogDebug("Unknown Frame [{Scope} / {Name}]", frame.Scope, frame.Name); - } + this.Log().LogDebug("Received Frame [{Scope} / {Name}] to be processed by {processor}", frame.Scope, frame.Name, processor); + } + + await processor.ProcessFrame(frame); + } + else + { + if (this.Log().IsEnabled(LogLevel.Debug)) + { + this.Log().LogDebug("Unknown Frame [{Scope} / {Name}]", frame.Scope, frame.Name); } } } + } - private void ProcessDiscoveryFrame(Frame frame) - { - var msg = JsonConvert.DeserializeObject(frame.Content)!; - var serverAssemblyName = typeof(IServerProcessor).Assembly.GetName().Name; + private async Task TryStartIDEChannelAsync() + { + _ideChannelServer = await _ideChannelProvider.GetIdeChannelServerAsync(); + } - var assemblies = new List(); + private void ProcessDiscoveryFrame(Frame frame) + { + var msg = JsonConvert.DeserializeObject(frame.Content)!; + var serverAssemblyName = typeof(IServerProcessor).Assembly.GetName().Name; - _resolveAssemblyLocation = string.Empty; + var assemblies = new List(); - if (!_appInstanceIds.Contains(msg.AppInstanceId)) - { - _appInstanceIds.Add(msg.AppInstanceId); - } + _resolveAssemblyLocation = string.Empty; - var assemblyLoadContext = GetAssemblyLoadContext(msg.AppInstanceId); + if (!_appInstanceIds.Contains(msg.AppInstanceId)) + { + _appInstanceIds.Add(msg.AppInstanceId); + } + + var assemblyLoadContext = GetAssemblyLoadContext(msg.AppInstanceId); - // If BasePath is a specific file, try and load that - if (File.Exists(msg.BasePath)) + // If BasePath is a specific file, try and load that + if (File.Exists(msg.BasePath)) + { + try { - try - { - using var fs = File.Open(msg.BasePath, FileMode.Open, FileAccess.Read, FileShare.Read); - assemblies.Add(assemblyLoadContext.LoadFromStream(fs)); + using var fs = File.Open(msg.BasePath, FileMode.Open, FileAccess.Read, FileShare.Read); + assemblies.Add(assemblyLoadContext.LoadFromStream(fs)); - _resolveAssemblyLocation = msg.BasePath; - } - catch (Exception exc) + _resolveAssemblyLocation = msg.BasePath; + } + catch (Exception exc) + { + if (this.Log().IsEnabled(LogLevel.Error)) { - if (this.Log().IsEnabled(LogLevel.Error)) - { - this.Log().LogError("Failed to load assembly {BasePath} : {Exc}", msg.BasePath, exc); - } + this.Log().LogError("Failed to load assembly {BasePath} : {Exc}", msg.BasePath, exc); } } - else - { - // As BasePath is a directory, try and load processors from assemblies within that dir - var basePath = msg.BasePath.Replace('/', Path.DirectorySeparatorChar); + } + else + { + // As BasePath is a directory, try and load processors from assemblies within that dir + var basePath = msg.BasePath.Replace('/', Path.DirectorySeparatorChar); #if NET8_0_OR_GREATER - basePath = Path.Combine(basePath, "net8.0"); + basePath = Path.Combine(basePath, "net8.0"); #elif NET7_0_OR_GREATER - basePath = Path.Combine(basePath, "net7.0"); + basePath = Path.Combine(basePath, "net7.0"); #endif - // Additional processors may not need the directory added immediately above. - if (!Directory.Exists(basePath)) + // Additional processors may not need the directory added immediately above. + if (!Directory.Exists(basePath)) + { + basePath = msg.BasePath; + } + + foreach (var file in Directory.GetFiles(basePath, "Uno.*.dll")) + { + if (Path.GetFileNameWithoutExtension(file).Equals(serverAssemblyName, StringComparison.OrdinalIgnoreCase)) { - basePath = msg.BasePath; + continue; } - foreach (var file in Directory.GetFiles(basePath, "Uno.*.dll")) + if (this.Log().IsEnabled(LogLevel.Debug)) { - if (Path.GetFileNameWithoutExtension(file).Equals(serverAssemblyName, StringComparison.OrdinalIgnoreCase)) - { - continue; - } + this.Log().LogDebug("Discovery: Loading {File}", file); + } + try + { + assemblies.Add(assemblyLoadContext.LoadFromAssemblyPath(file)); + } + catch (Exception exc) + { + // With additional processors there may be duplicates of assemblies already loaded if (this.Log().IsEnabled(LogLevel.Debug)) { - this.Log().LogDebug("Discovery: Loading {File}", file); - } - - try - { - assemblies.Add(assemblyLoadContext.LoadFromAssemblyPath(file)); - } - catch (Exception exc) - { - // With additional processors there may be duplicates of assemblies already loaded - if (this.Log().IsEnabled(LogLevel.Debug)) - { - this.Log().LogDebug("Failed to load assembly {File} : {Exc}", file, exc); - } + this.Log().LogDebug("Failed to load assembly {File} : {Exc}", file, exc); } } } + } - foreach (var asm in assemblies) + foreach (var asm in assemblies) + { + try { - try + if (assemblies.Count > 1 || string.IsNullOrEmpty(_resolveAssemblyLocation)) { - if (assemblies.Count > 1 || string.IsNullOrEmpty(_resolveAssemblyLocation)) - { - _resolveAssemblyLocation = asm.Location; - } + _resolveAssemblyLocation = asm.Location; + } - var attributes = asm.GetCustomAttributes(typeof(ServerProcessorAttribute), false); + var attributes = asm.GetCustomAttributes(typeof(ServerProcessorAttribute), false); - foreach (var processorAttribute in attributes) + foreach (var processorAttribute in attributes) + { + if (processorAttribute is ServerProcessorAttribute processor) { - if (processorAttribute is ServerProcessorAttribute processor) + if (this.Log().IsEnabled(LogLevel.Debug)) { - if (this.Log().IsEnabled(LogLevel.Debug)) - { - this.Log().LogDebug("Discovery: Registering {ProcessorType}", processor.ProcessorType); - } + this.Log().LogDebug("Discovery: Registering {ProcessorType}", processor.ProcessorType); + } - if (asm.CreateInstance(processor.ProcessorType.FullName!, ignoreCase: false, bindingAttr: BindingFlags.Instance | BindingFlags.Public, binder: null, args: new[] { this }, culture: null, activationAttributes: null) is IServerProcessor serverProcessor) - { - RegisterProcessor(serverProcessor); - } - else + if (asm.CreateInstance(processor.ProcessorType.FullName!, ignoreCase: false, bindingAttr: BindingFlags.Instance | BindingFlags.Public, binder: null, args: new[] { this }, culture: null, activationAttributes: null) is IServerProcessor serverProcessor) + { + RegisterProcessor(serverProcessor); + } + else + { + if (this.Log().IsEnabled(LogLevel.Debug)) { - if (this.Log().IsEnabled(LogLevel.Debug)) - { - this.Log().LogDebug("Failed to create server processor {ProcessorType}", processor.ProcessorType); - } + this.Log().LogDebug("Failed to create server processor {ProcessorType}", processor.ProcessorType); } } } } - catch (Exception exc) + } + catch (Exception exc) + { + if (this.Log().IsEnabled(LogLevel.Error)) { - if (this.Log().IsEnabled(LogLevel.Error)) - { - this.Log().LogError("Failed to create instance of server processor in {Asm} : {Exc}", asm, exc); - } + this.Log().LogError("Failed to create instance of server processor in {Asm} : {Exc}", asm, exc); } } - - // Being thorough about trying to ensure everything is unloaded - assemblies.Clear(); } - public async Task SendFrame(IMessage message) + // Being thorough about trying to ensure everything is unloaded + assemblies.Clear(); + } + + public async Task SendFrame(IMessage message) + { + if (_socket is not null) { - if (_socket is not null) + await WebSocketHelper.SendFrame( + _socket, + Frame.Create( + 1, + message.Scope, + message.Name, + message + ), + CancellationToken.None); + } + else + { + if (this.Log().IsEnabled(LogLevel.Debug)) { - await WebSocketHelper.SendFrame( - _socket, - Frame.Create( - 1, - message.Scope, - message.Name, - message - ), - CancellationToken.None); - } - else - { - if (this.Log().IsEnabled(LogLevel.Debug)) - { - this.Log().LogDebug($"Failed to send, no connection available"); - } + this.Log().LogDebug($"Failed to send, no connection available"); } } + } + + public async Task SendMessageToIDEAsync(IdeMessage message) + { + if (IDEChannelServer is not null) + { + await IDEChannelServer.SendToIdeAsync(message); + } + } - public void Dispose() + public void Dispose() + { + foreach (var processor in _processors) { - foreach (var processor in _processors) - { - processor.Value.Dispose(); - } + processor.Value.Dispose(); + } - // Unload any AssemblyLoadContexts not being used by any current connection - foreach (var appId in _appInstanceIds) + // Unload any AssemblyLoadContexts not being used by any current connection + foreach (var appId in _appInstanceIds) + { + lock (_loadContextGate) { - lock (_loadContextGate) + if (_loadContexts.TryGetValue(appId, out var lc)) { - if (_loadContexts.TryGetValue(appId, out var lc)) + if (lc.Count > 1) { - if (lc.Count > 1) + _loadContexts[appId] = (lc.Context, lc.Count - 1); + } + else + { + try { - _loadContexts[appId] = (lc.Context, lc.Count - 1); + _loadContexts[appId].Context.Unload(); + + _loadContexts.Remove(appId); } - else + catch (Exception exc) { - try - { - _loadContexts[appId].Context.Unload(); - - _loadContexts.Remove(appId); - } - catch (Exception exc) + if (this.Log().IsEnabled(LogLevel.Error)) { - if (this.Log().IsEnabled(LogLevel.Error)) - { - this.Log().LogError("Failed to unload AssemblyLoadContext for '{appId}' : {Exc}", appId, exc); - } + this.Log().LogError("Failed to unload AssemblyLoadContext for '{appId}' : {Exc}", appId, exc); } } } diff --git a/src/Uno.UI.RemoteControl.Host/Uno.UI.RemoteControl.Host.csproj b/src/Uno.UI.RemoteControl.Host/Uno.UI.RemoteControl.Host.csproj index 724f4861721f..e959e53988d8 100644 --- a/src/Uno.UI.RemoteControl.Host/Uno.UI.RemoteControl.Host.csproj +++ b/src/Uno.UI.RemoteControl.Host/Uno.UI.RemoteControl.Host.csproj @@ -6,6 +6,7 @@ true minor enable + $(NoWarn);VSTHRD200 @@ -17,6 +18,8 @@ + + @@ -24,6 +27,7 @@ + diff --git a/src/Uno.UI.RemoteControl.Messaging/AssemblyInfo.cs b/src/Uno.UI.RemoteControl.Messaging/AssemblyInfo.cs new file mode 100644 index 000000000000..a958846dafd4 --- /dev/null +++ b/src/Uno.UI.RemoteControl.Messaging/AssemblyInfo.cs @@ -0,0 +1,4 @@ +using System.Runtime.CompilerServices; + +[assembly: InternalsVisibleTo("Uno.UI.RemoteControl.VS")] +[assembly: InternalsVisibleTo("Uno.UI.RemoteControl.Host")] diff --git a/src/Uno.UI.RemoteControl.Messaging/IDEChannel/ForceHotReloadIdeMessage.cs b/src/Uno.UI.RemoteControl.Messaging/IDEChannel/ForceHotReloadIdeMessage.cs new file mode 100644 index 000000000000..2f560bfe2bf7 --- /dev/null +++ b/src/Uno.UI.RemoteControl.Messaging/IDEChannel/ForceHotReloadIdeMessage.cs @@ -0,0 +1,5 @@ +#nullable enable + +namespace Uno.UI.RemoteControl.Messaging.IdeChannel; + +public record ForceHotReloadIdeMessage : IdeMessage; diff --git a/src/Uno.UI.RemoteControl.Messaging/IDEChannel/IIdeChannelServer.cs b/src/Uno.UI.RemoteControl.Messaging/IDEChannel/IIdeChannelServer.cs new file mode 100644 index 000000000000..7fdc44b3fc7a --- /dev/null +++ b/src/Uno.UI.RemoteControl.Messaging/IDEChannel/IIdeChannelServer.cs @@ -0,0 +1,15 @@ +#nullable enable + +using System; +using System.Collections.Generic; +using System.Text; +using System.Threading.Tasks; + +namespace Uno.UI.RemoteControl.Messaging.IdeChannel; + +internal interface IIdeChannelServer +{ + Task SendToDevServerAsync(IdeMessage message); + + event EventHandler? MessageFromDevServer; +} diff --git a/src/Uno.UI.RemoteControl.Messaging/IDEChannel/IdeMessage.cs b/src/Uno.UI.RemoteControl.Messaging/IDEChannel/IdeMessage.cs new file mode 100644 index 000000000000..37c3b1e93d04 --- /dev/null +++ b/src/Uno.UI.RemoteControl.Messaging/IDEChannel/IdeMessage.cs @@ -0,0 +1,6 @@ +#nullable enable + +namespace Uno.UI.RemoteControl.Messaging.IdeChannel; + +public record IdeMessage; + diff --git a/src/Uno.UI.RemoteControl.Messaging/IDEChannel/KeepAliveIdeMessage.cs b/src/Uno.UI.RemoteControl.Messaging/IDEChannel/KeepAliveIdeMessage.cs new file mode 100644 index 000000000000..42235f040d69 --- /dev/null +++ b/src/Uno.UI.RemoteControl.Messaging/IDEChannel/KeepAliveIdeMessage.cs @@ -0,0 +1,6 @@ +#nullable enable + +namespace Uno.UI.RemoteControl.Messaging.IdeChannel; + +public record KeepAliveIdeMessage : IdeMessage; + diff --git a/src/Uno.UI.RemoteControl.Messaging/IRemoteControlServer.cs b/src/Uno.UI.RemoteControl.Messaging/IRemoteControlServer.cs index 82c9e9ea319f..87c0ff3ef7f1 100644 --- a/src/Uno.UI.RemoteControl.Messaging/IRemoteControlServer.cs +++ b/src/Uno.UI.RemoteControl.Messaging/IRemoteControlServer.cs @@ -1,6 +1,7 @@ using System.Threading.Tasks; using Uno.UI.RemoteControl; using Uno.UI.RemoteControl.HotReload.Messages; +using Uno.UI.RemoteControl.Messaging.IdeChannel; namespace Uno.UI.RemoteControl.Host { @@ -9,5 +10,7 @@ public interface IRemoteControlServer string GetServerConfiguration(string key); Task SendFrame(IMessage fileReload); + + Task SendMessageToIDEAsync(IdeMessage message); } } diff --git a/src/Uno.UI.RemoteControl.VS/EntryPoint.cs b/src/Uno.UI.RemoteControl.VS/EntryPoint.cs index ee61ce6485e2..5c9fbdf3d1ff 100644 --- a/src/Uno.UI.RemoteControl.VS/EntryPoint.cs +++ b/src/Uno.UI.RemoteControl.VS/EntryPoint.cs @@ -5,331 +5,372 @@ using System.Diagnostics; using System.Globalization; using System.IO; +using System.IO.Pipes; using System.Linq; using System.Net; using System.Net.Sockets; using System.Reflection; using System.Text; +using System.Threading; using System.Threading.Tasks; using EnvDTE; using EnvDTE80; using Microsoft.Build.Evaluation; +using Microsoft.Build.Framework; using Microsoft.VisualStudio.ProjectSystem; using Microsoft.VisualStudio.ProjectSystem.Build; using Microsoft.VisualStudio.Shell; using Microsoft.VisualStudio.Shell.Interop; +using StreamJsonRpc; +using Uno.UI.RemoteControl.Messaging.IdeChannel; using Uno.UI.RemoteControl.VS.Helpers; +using Uno.UI.RemoteControl.VS.IDEChannel; +using ILogger = Uno.UI.RemoteControl.VS.Helpers.ILogger; using Task = System.Threading.Tasks.Task; #pragma warning disable VSTHRD010 #pragma warning disable VSTHRD109 -namespace Uno.UI.RemoteControl.VS +namespace Uno.UI.RemoteControl.VS; + +public class EntryPoint : IDisposable { - public class EntryPoint : IDisposable + private const string UnoPlatformOutputPane = "Uno Platform"; + private const string FolderKind = "{66A26720-8FB5-11D2-AA7E-00C04F688DDE}"; + private const string RemoteControlServerPortProperty = "UnoRemoteControlPort"; + private readonly DTE _dte; + private readonly DTE2 _dte2; + private readonly string _toolsPath; + private readonly AsyncPackage _asyncPackage; + private Action? _debugAction; + private Action? _infoAction; + private Action? _verboseAction; + private Action? _warningAction; + private Action? _errorAction; + private System.Diagnostics.Process? _process; + + private int RemoteControlServerPort; + private bool _closing; + private bool _isDisposed; + private IDEChannelClient? _iDEChannel; + private readonly _dispSolutionEvents_BeforeClosingEventHandler _closeHandler; + private readonly _dispBuildEvents_OnBuildDoneEventHandler _onBuildDoneHandler; + private readonly _dispBuildEvents_OnBuildProjConfigBeginEventHandler _onBuildProjConfigBeginHandler; + + public EntryPoint(DTE2 dte2, string toolsPath, AsyncPackage asyncPackage, Action>>> globalPropertiesProvider) { - private const string UnoPlatformOutputPane = "Uno Platform"; - private const string FolderKind = "{66A26720-8FB5-11D2-AA7E-00C04F688DDE}"; - private const string RemoteControlServerPortProperty = "UnoRemoteControlPort"; - private readonly DTE _dte; - private readonly DTE2 _dte2; - private readonly string _toolsPath; - private readonly AsyncPackage _asyncPackage; - private Action _debugAction; - private Action _infoAction; - private Action _verboseAction; - private Action _warningAction; - private Action _errorAction; - private System.Diagnostics.Process _process; - - private int RemoteControlServerPort; - private bool _closing; - private bool _isDisposed; - - private readonly _dispSolutionEvents_BeforeClosingEventHandler _closeHandler; - private readonly _dispBuildEvents_OnBuildDoneEventHandler _onBuildDoneHandler; - private readonly _dispBuildEvents_OnBuildProjConfigBeginEventHandler _onBuildProjConfigBeginHandler; - - public EntryPoint(DTE2 dte2, string toolsPath, AsyncPackage asyncPackage, Action>>> globalPropertiesProvider) - { - _dte = dte2 as DTE; - _dte2 = dte2; - _toolsPath = toolsPath; - _asyncPackage = asyncPackage; - globalPropertiesProvider(OnProvideGlobalPropertiesAsync); + _dte = dte2 as DTE; + _dte2 = dte2; + _toolsPath = toolsPath; + _asyncPackage = asyncPackage; + globalPropertiesProvider(OnProvideGlobalPropertiesAsync); - SetupOutputWindow(); + SetupOutputWindow(); - _closeHandler = () => SolutionEvents_BeforeClosing(); - _dte.Events.SolutionEvents.BeforeClosing += _closeHandler; + _closeHandler = () => SolutionEvents_BeforeClosing(); + _dte.Events.SolutionEvents.BeforeClosing += _closeHandler; - _onBuildDoneHandler = (s, a) => BuildEvents_OnBuildDone(s, a); - _dte.Events.BuildEvents.OnBuildDone += _onBuildDoneHandler; + _onBuildDoneHandler = (s, a) => BuildEvents_OnBuildDone(s, a); + _dte.Events.BuildEvents.OnBuildDone += _onBuildDoneHandler; - _onBuildProjConfigBeginHandler = (string project, string projectConfig, string platform, string solutionConfig) => _ = BuildEvents_OnBuildProjConfigBeginAsync(project, projectConfig, platform, solutionConfig); - _dte.Events.BuildEvents.OnBuildProjConfigBegin += _onBuildProjConfigBeginHandler; + _onBuildProjConfigBeginHandler = (string project, string projectConfig, string platform, string solutionConfig) => _ = BuildEvents_OnBuildProjConfigBeginAsync(project, projectConfig, platform, solutionConfig); + _dte.Events.BuildEvents.OnBuildProjConfigBegin += _onBuildProjConfigBeginHandler; - // Start the RC server early, as iOS and Android projects capture the globals early - // and don't recreate it unless out-of-process msbuild.exe instances are terminated. - // - // This will can possibly be removed when all projects are migrated to the sdk project system. - _ = UpdateProjectsAsync(); - } + // Start the RC server early, as iOS and Android projects capture the globals early + // and don't recreate it unless out-of-process msbuild.exe instances are terminated. + // + // This will can possibly be removed when all projects are migrated to the sdk project system. + _ = UpdateProjectsAsync(); + } - private Task> OnProvideGlobalPropertiesAsync() + private Task> OnProvideGlobalPropertiesAsync() + { + if (RemoteControlServerPort == 0) { - if (RemoteControlServerPort == 0) - { - _warningAction( - $"The Remote Control server is not yet started, providing [0] as the server port. " + - $"Rebuilding the application may fix the issue."); - } - - return Task.FromResult(new Dictionary { - { RemoteControlServerPortProperty, RemoteControlServerPort.ToString(CultureInfo.InvariantCulture) } - }); + _warningAction?.Invoke( + $"The Remote Control server is not yet started, providing [0] as the server port. " + + $"Rebuilding the application may fix the issue."); } - private void SetupOutputWindow() - { - var ow = _dte2.ToolWindows.OutputWindow; - - // Add a new pane to the Output window. - var owPane = ow - .OutputWindowPanes - .OfType() - .FirstOrDefault(p => p.Name == UnoPlatformOutputPane); + return Task.FromResult(new Dictionary { + { RemoteControlServerPortProperty, RemoteControlServerPort.ToString(CultureInfo.InvariantCulture) } + }); + } - if (owPane == null) - { - owPane = ow - .OutputWindowPanes - .Add(UnoPlatformOutputPane); - } + private void SetupOutputWindow() + { + var ow = _dte2.ToolWindows.OutputWindow; - _debugAction = s => - { - if (!_closing) - { - owPane.OutputString("[DEBUG] " + s + "\r\n"); - } - }; - _infoAction = s => - { - if (!_closing) - { - owPane.OutputString("[INFO] " + s + "\r\n"); - } - }; - _verboseAction = s => - { - if (!_closing) - { - owPane.OutputString("[VERBOSE] " + s + "\r\n"); - } - }; - _warningAction = s => - { - if (!_closing) - { - owPane.OutputString("[WARNING] " + s + "\r\n"); - } - }; - _errorAction = e => - { - if (!_closing) - { - owPane.OutputString("[ERROR] " + e + "\r\n"); - } - }; + // Add a new pane to the Output window. + var owPane = ow + .OutputWindowPanes + .OfType() + .FirstOrDefault(p => p.Name == UnoPlatformOutputPane); - _infoAction($"Uno Remote Control initialized ({GetAssemblyVersion()})"); + if (owPane == null) + { + owPane = ow + .OutputWindowPanes + .Add(UnoPlatformOutputPane); } - private object GetAssemblyVersion() + _debugAction = s => { - var assembly = GetType().GetTypeInfo().Assembly; - - if (assembly.GetCustomAttribute() is AssemblyInformationalVersionAttribute aiva) + if (!_closing) { - return aiva.InformationalVersion; + owPane.OutputString("[DEBUG] " + s + "\r\n"); } - else if (assembly.GetCustomAttribute() is AssemblyVersionAttribute ava) + }; + _infoAction = s => + { + if (!_closing) { - return ava.Version; + owPane.OutputString("[INFO] " + s + "\r\n"); } - else + }; + _verboseAction = s => + { + if (!_closing) { - return "Unknown"; + owPane.OutputString("[VERBOSE] " + s + "\r\n"); } - } - - private async Task BuildEvents_OnBuildProjConfigBeginAsync(string project, string projectConfig, string platform, string solutionConfig) + }; + _warningAction = s => { - await UpdateProjectsAsync(); - } - - private async Task UpdateProjectsAsync() - { - try + if (!_closing) { - StartServer(); - var portString = RemoteControlServerPort.ToString(CultureInfo.InvariantCulture); - foreach (var p in await GetProjectsAsync()) - { - var filename = string.Empty; - try - { - filename = p.FileName; - } - catch (Exception ex) - { - _debugAction($"Exception on retrieving {p.UniqueName} details. Err: {ex}."); - _warningAction($"Cannot read {p.UniqueName} project details (It may be unloaded)."); - } - if (string.IsNullOrWhiteSpace(filename) == false - && GetMsbuildProject(filename) is Microsoft.Build.Evaluation.Project msbProject - && IsApplication(msbProject)) - { - SetGlobalProperty(filename, RemoteControlServerPortProperty, portString); - } - } + owPane.OutputString("[WARNING] " + s + "\r\n"); } - catch (Exception e) + }; + _errorAction = e => + { + if (!_closing) { - _debugAction($"UpdateProjectsAsync failed: {e}"); + owPane.OutputString("[ERROR] " + e + "\r\n"); } - } + }; + + _infoAction($"Uno Remote Control initialized ({GetAssemblyVersion()})"); + } - private void BuildEvents_OnBuildDone(vsBuildScope Scope, vsBuildAction Action) + private object GetAssemblyVersion() + { + var assembly = GetType().GetTypeInfo().Assembly; + + if (assembly.GetCustomAttribute() is AssemblyInformationalVersionAttribute aiva) { - StartServer(); + return aiva.InformationalVersion; } - - private void SolutionEvents_BeforeClosing() + else if (assembly.GetCustomAttribute() is AssemblyVersionAttribute ava) + { + return ava.Version; + } + else { - // Detach event handler to avoid this being called multiple times - _dte.Events.SolutionEvents.BeforeClosing -= _closeHandler; + return "Unknown"; + } + } + + private async Task BuildEvents_OnBuildProjConfigBeginAsync(string project, string projectConfig, string platform, string solutionConfig) + { + await UpdateProjectsAsync(); + } - if (_process != null) + private async Task UpdateProjectsAsync() + { + try + { + StartServer(); + var portString = RemoteControlServerPort.ToString(CultureInfo.InvariantCulture); + foreach (var p in await GetProjectsAsync()) { + var filename = string.Empty; try { - _debugAction($"Terminating Remote Control server (pid: {_process.Id})"); - _process.Kill(); - _debugAction($"Terminated Remote Control server (pid: {_process.Id})"); + filename = p.FileName; } - catch (Exception e) + catch (Exception ex) { - _debugAction($"Failed to terminate Remote Control server (pid: {_process.Id}): {e}"); + _debugAction?.Invoke($"Exception on retrieving {p.UniqueName} details. Err: {ex}."); + _warningAction?.Invoke($"Cannot read {p.UniqueName} project details (It may be unloaded)."); } - finally + if (string.IsNullOrWhiteSpace(filename) == false + && GetMsbuildProject(filename) is Microsoft.Build.Evaluation.Project msbProject + && IsApplication(msbProject)) { - _closing = true; - _process = null; - - // Invoke Dispose to make sure other event handlers are detached - Dispose(); + SetGlobalProperty(filename, RemoteControlServerPortProperty, portString); } } } + catch (Exception e) + { + _debugAction?.Invoke($"UpdateProjectsAsync failed: {e}"); + } + } + + private void BuildEvents_OnBuildDone(vsBuildScope Scope, vsBuildAction Action) + { + StartServer(); + } + + private void SolutionEvents_BeforeClosing() + { + // Detach event handler to avoid this being called multiple times + _dte.Events.SolutionEvents.BeforeClosing -= _closeHandler; - private int GetDotnetMajorVersion() + if (_process is not null) { - var result = ProcessHelpers.RunProcess("dotnet", "--version", Path.GetDirectoryName(_dte.Solution.FileName)); + try + { + _debugAction?.Invoke($"Terminating Remote Control server (pid: {_process.Id})"); + _process.Kill(); + _debugAction?.Invoke($"Terminated Remote Control server (pid: {_process.Id})"); - if (result.exitCode != 0) + _iDEChannel?.Dispose(); + _iDEChannel = null; + } + catch (Exception e) { - throw new InvalidOperationException($"Unable to detect current dotnet version (\"dotnet --version\" exited with code {result.exitCode})"); + _debugAction?.Invoke($"Failed to terminate Remote Control server (pid: {_process.Id}): {e}"); } - - if (result.output.Contains(".")) + finally { - if (int.TryParse(result.output.Substring(0, result.output.IndexOf('.')), out int majorVersion)) - { - return majorVersion; - } + _closing = true; + _process = null; + + // Invoke Dispose to make sure other event handlers are detached + Dispose(); } + } + } - throw new InvalidOperationException($"Unable to detect current dotnet version (\"dotnet --version\" returned \"{result.output}\")"); + private int GetDotnetMajorVersion() + { + var result = ProcessHelpers.RunProcess("dotnet", "--version", Path.GetDirectoryName(_dte.Solution.FileName)); + + if (result.exitCode != 0) + { + throw new InvalidOperationException($"Unable to detect current dotnet version (\"dotnet --version\" exited with code {result.exitCode})"); } - private void StartServer() + if (result.output.Contains(".")) { - if (_process?.HasExited ?? true) + if (int.TryParse(result.output.Substring(0, result.output.IndexOf('.')), out int majorVersion)) { - RemoteControlServerPort = GetTcpPort(); + return majorVersion; + } + } - var version = GetDotnetMajorVersion(); - if (version < 7) - { - throw new InvalidOperationException($"Unsupported dotnet version ({version}) detected"); - } - var runtimeVersionPath = $"net{version}.0"; + throw new InvalidOperationException($"Unable to detect current dotnet version (\"dotnet --version\" returned \"{result.output}\")"); + } - var sb = new StringBuilder(); + private void StartServer() + { + if (_process?.HasExited ?? true) + { + RemoteControlServerPort = GetTcpPort(); - var hostBinPath = Path.Combine(_toolsPath, "host", runtimeVersionPath, "Uno.UI.RemoteControl.Host.dll"); - string arguments = $"\"{hostBinPath}\" --httpPort {RemoteControlServerPort} --ppid {System.Diagnostics.Process.GetCurrentProcess().Id}"; - var pi = new ProcessStartInfo("dotnet", arguments) - { - UseShellExecute = false, - CreateNoWindow = true, - WindowStyle = ProcessWindowStyle.Hidden, - WorkingDirectory = Path.Combine(_toolsPath, "host"), - }; + var version = GetDotnetMajorVersion(); + if (version < 7) + { + throw new InvalidOperationException($"Unsupported dotnet version ({version}) detected"); + } + var runtimeVersionPath = $"net{version}.0"; - // redirect the output - pi.RedirectStandardOutput = true; - pi.RedirectStandardError = true; + var sb = new StringBuilder(); - _process = new System.Diagnostics.Process(); + var pipeGuid = Guid.NewGuid(); - // hookup the eventhandlers to capture the data that is received - _process.OutputDataReceived += (sender, args) => _debugAction(args.Data); - _process.ErrorDataReceived += (sender, args) => _errorAction(args.Data); + var hostBinPath = Path.Combine(_toolsPath, "host", runtimeVersionPath, "Uno.UI.RemoteControl.Host.dll"); + string arguments = $"\"{hostBinPath}\" --httpPort {RemoteControlServerPort} --ppid {System.Diagnostics.Process.GetCurrentProcess().Id} --ideChannel \"{pipeGuid}\""; + var pi = new ProcessStartInfo("dotnet", arguments) + { + UseShellExecute = false, + CreateNoWindow = true, + WindowStyle = ProcessWindowStyle.Hidden, + WorkingDirectory = Path.Combine(_toolsPath, "host"), + }; - _process.StartInfo = pi; - _process.Start(); + // redirect the output + pi.RedirectStandardOutput = true; + pi.RedirectStandardError = true; - // start our event pumps - _process.BeginOutputReadLine(); - _process.BeginErrorReadLine(); - } - } + _process = new System.Diagnostics.Process(); - private static int GetTcpPort() - { - var l = new TcpListener(IPAddress.Loopback, 0); - l.Start(); - var port = ((IPEndPoint)l.LocalEndpoint).Port; - l.Stop(); - return port; + // hookup the event handlers to capture the data that is received + _process.OutputDataReceived += (sender, args) => _debugAction?.Invoke(args.Data); + _process.ErrorDataReceived += (sender, args) => _errorAction?.Invoke(args.Data); + + _process.StartInfo = pi; + _process.Start(); + + // start our event pumps + _process.BeginOutputReadLine(); + _process.BeginErrorReadLine(); + + _iDEChannel = new IDEChannelClient(pipeGuid, new Logger(this)); + _iDEChannel.ConnectToHost(); } + } - private async System.Threading.Tasks.Task> GetProjectsAsync() - { - ThreadHelper.ThrowIfNotOnUIThread(); + private static int GetTcpPort() + { + var l = new TcpListener(IPAddress.Loopback, 0); + l.Start(); + var port = ((IPEndPoint)l.LocalEndpoint).Port; + l.Stop(); + return port; + } + + private async System.Threading.Tasks.Task> GetProjectsAsync() + { + ThreadHelper.ThrowIfNotOnUIThread(); + + var projectService = await _asyncPackage.GetServiceAsync(typeof(IProjectService)) as IProjectService; - var projectService = await _asyncPackage.GetServiceAsync(typeof(IProjectService)) as IProjectService; + var solutionProjectItems = _dte.Solution.Projects; - var solutionProjectItems = _dte.Solution.Projects; + if (solutionProjectItems != null) + { + return EnumerateProjects(solutionProjectItems); + } + else + { + return Array.Empty(); + } + } - if (solutionProjectItems != null) + private IEnumerable EnumerateProjects(EnvDTE.Projects vsSolution) + { + foreach (var project in vsSolution.OfType()) + { + if (project.Kind == FolderKind /* Folder */) { - return EnumerateProjects(solutionProjectItems); + foreach (var subProject in EnumSubProjects(project)) + { + yield return subProject; + } } else { - return Array.Empty(); + yield return project; } } + } - private IEnumerable EnumerateProjects(EnvDTE.Projects vsSolution) + private IEnumerable EnumSubProjects(EnvDTE.Project folder) + { + if (folder.ProjectItems != null) { - foreach (var project in vsSolution.OfType()) + var subProjects = folder.ProjectItems + .OfType() + .Select(p => p.Object) + .Where(p => p != null) + .Cast(); + + foreach (var project in subProjects) { - if (project.Kind == FolderKind /* Folder */) + if (project.Kind == FolderKind) { foreach (var subProject in EnumSubProjects(project)) { @@ -342,97 +383,80 @@ private static int GetTcpPort() } } } + } - private IEnumerable EnumSubProjects(EnvDTE.Project folder) + public void SetGlobalProperty(string projectFullName, string propertyName, string propertyValue) + { + var msbuildProject = GetMsbuildProject(projectFullName); + if (msbuildProject == null) { - if (folder.ProjectItems != null) - { - var subProjects = folder.ProjectItems - .OfType() - .Select(p => p.Object) - .Where(p => p != null) - .Cast(); - - foreach (var project in subProjects) - { - if (project.Kind == FolderKind) - { - foreach (var subProject in EnumSubProjects(project)) - { - yield return subProject; - } - } - else - { - yield return project; - } - } - } + _debugAction?.Invoke($"Failed to find project {projectFullName}, cannot provide listen port to the app."); } - - public void SetGlobalProperty(string projectFullName, string propertyName, string propertyValue) + else { - var msbuildProject = GetMsbuildProject(projectFullName); - if (msbuildProject == null) - { - _debugAction($"Failed to find project {projectFullName}, cannot provide listen port to the app."); - } - else - { - SetGlobalProperty(msbuildProject, propertyName, propertyValue); - } + SetGlobalProperty(msbuildProject, propertyName, propertyValue); } + } - private static Microsoft.Build.Evaluation.Project GetMsbuildProject(string projectFullName) - => ProjectCollection.GlobalProjectCollection.GetLoadedProjects(projectFullName).FirstOrDefault(); + private static Microsoft.Build.Evaluation.Project GetMsbuildProject(string projectFullName) + => ProjectCollection.GlobalProjectCollection.GetLoadedProjects(projectFullName).FirstOrDefault(); - public void SetGlobalProperties(string projectFullName, IDictionary properties) + public void SetGlobalProperties(string projectFullName, IDictionary properties) + { + var msbuildProject = ProjectCollection.GlobalProjectCollection.GetLoadedProjects(projectFullName).FirstOrDefault(); + if (msbuildProject == null) + { + _debugAction?.Invoke($"Failed to find project {projectFullName}, cannot provide listen port to the app."); + } + else { - var msbuildProject = ProjectCollection.GlobalProjectCollection.GetLoadedProjects(projectFullName).FirstOrDefault(); - if (msbuildProject == null) + foreach (var property in properties) { - _debugAction($"Failed to find project {projectFullName}, cannot provide listen port to the app."); - } - else - { - foreach (var property in properties) - { - SetGlobalProperty(msbuildProject, property.Key, property.Value); - } + SetGlobalProperty(msbuildProject, property.Key, property.Value); } } + } - private void SetGlobalProperty(Microsoft.Build.Evaluation.Project msbuildProject, string propertyName, string propertyValue) - { - msbuildProject.SetGlobalProperty(propertyName, propertyValue); + private void SetGlobalProperty(Microsoft.Build.Evaluation.Project msbuildProject, string propertyName, string propertyValue) + { + msbuildProject.SetGlobalProperty(propertyName, propertyValue); - } + } - private bool IsApplication(Microsoft.Build.Evaluation.Project project) - { - var outputType = project.GetPropertyValue("OutputType"); - return outputType is not null && + private bool IsApplication(Microsoft.Build.Evaluation.Project project) + { + var outputType = project.GetPropertyValue("OutputType"); + return outputType is not null && (outputType.Equals("Exe", StringComparison.OrdinalIgnoreCase) || outputType.Equals("WinExe", StringComparison.OrdinalIgnoreCase)); - } + } - public void Dispose() + public void Dispose() + { + if (_isDisposed) { - if (_isDisposed) - { - return; - } - _isDisposed = true; + return; + } + _isDisposed = true; - try - { - _dte.Events.BuildEvents.OnBuildDone -= _onBuildDoneHandler; - _dte.Events.BuildEvents.OnBuildProjConfigBegin -= _onBuildProjConfigBeginHandler; - } - catch (Exception e) - { - _debugAction($"Failed to dispose Remote Control server: {e}"); - } + try + { + _dte.Events.BuildEvents.OnBuildDone -= _onBuildDoneHandler; + _dte.Events.BuildEvents.OnBuildProjConfigBegin -= _onBuildProjConfigBeginHandler; } + catch (Exception e) + { + _debugAction?.Invoke($"Failed to dispose Remote Control server: {e}"); + } + } + + private class Logger(EntryPoint entryPoint) : ILogger + { + private readonly EntryPoint _entryPoint = entryPoint; + public void Debug(string message) => _entryPoint._debugAction?.Invoke(message); + public void Error(string message) => _entryPoint._errorAction?.Invoke(message); + public void Info(string message) => _entryPoint._infoAction?.Invoke(message); + public void Warn(string message) => _entryPoint._warningAction?.Invoke(message); + public void Verbose(string message) => _entryPoint._verboseAction?.Invoke(message); } } diff --git a/src/Uno.UI.RemoteControl.VS/Helpers/ILogger.cs b/src/Uno.UI.RemoteControl.VS/Helpers/ILogger.cs new file mode 100644 index 000000000000..931be4d3585e --- /dev/null +++ b/src/Uno.UI.RemoteControl.VS/Helpers/ILogger.cs @@ -0,0 +1,19 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Uno.UI.RemoteControl.VS.Helpers; + +internal interface ILogger +{ + void Info(string message); + + void Debug(string message); + + void Warn(string message); + + void Error(string message); + void Verbose(string message); +} diff --git a/src/Uno.UI.RemoteControl.VS/IDEChannel/IDEChannelClient.cs b/src/Uno.UI.RemoteControl.VS/IDEChannel/IDEChannelClient.cs new file mode 100644 index 000000000000..5031f999e59e --- /dev/null +++ b/src/Uno.UI.RemoteControl.VS/IDEChannel/IDEChannelClient.cs @@ -0,0 +1,100 @@ +using System; +using System.Collections.Generic; +using System.IO.Pipes; +using System.Linq; +using System.Runtime.Remoting.Messaging; +using System.Text; +using System.Threading; +using System.Threading.Tasks; +using StreamJsonRpc; +using Uno.UI.RemoteControl.Messaging.IdeChannel; +using Uno.UI.RemoteControl.VS.Helpers; + +namespace Uno.UI.RemoteControl.VS.IDEChannel; + +internal class IDEChannelClient +{ + private NamedPipeClientStream? _pipeServer; + private Guid _pipeGuid; + private CancellationTokenSource? _IDEChannelCancellation; + private Task? _connectTask; + private JsonRpc? _rpc; + private IIdeChannelServer? _roslynServer; + private readonly ILogger _logger; + + public IDEChannelClient(Guid pipeGuid, ILogger logger) + { + _logger = logger; + _pipeGuid = pipeGuid; + } + + public void ConnectToHost() + { + _IDEChannelCancellation = new CancellationTokenSource(); + + _connectTask = Task.Run(async () => + { + try + { + _pipeServer = new NamedPipeClientStream( + serverName: ".", + pipeName: _pipeGuid.ToString(), + direction: PipeDirection.InOut, + options: PipeOptions.Asynchronous | PipeOptions.WriteThrough); + + _logger.Debug($"Creating IDE Channel to Dev Server ({_pipeGuid})"); + + await _pipeServer.ConnectAsync(_IDEChannelCancellation.Token); + + _rpc = JsonRpc.Attach(_pipeServer); + _rpc.AllowModificationWhileListening = true; + _roslynServer = _rpc.Attach(); + _rpc.AllowModificationWhileListening = false; + + _roslynServer.MessageFromDevServer += ProcessDevServerMessage; + + _ = Task.Run(StartKeepaliveAsync); + } + catch (Exception e) + { + _logger.Error($"Error creating IDE channel: {e}"); + } + }, _IDEChannelCancellation.Token); + } + + private async Task StartKeepaliveAsync() + { + while (_IDEChannelCancellation is { IsCancellationRequested: false }) + { + _roslynServer?.SendToDevServerAsync(new KeepAliveIdeMessage()); + + await Task.Delay(5000); + } + } + + private void ProcessDevServerMessage(object sender, IdeMessage devServerMessage) + { + _logger.Info($"IDE: IDEChannel message received {devServerMessage}"); + + if (devServerMessage is ForceHotReloadIdeMessage) + { + _logger.Debug($"Hot reload requested"); + } + else if (devServerMessage is KeepAliveIdeMessage) + { +#if DEBUG + _logger.Verbose($"Keep alive from Dev Server"); +#endif + } + else + { + _logger.Debug($"Unknown message type {devServerMessage?.GetType()} from DevServer"); + } + } + + internal void Dispose() + { + _IDEChannelCancellation?.Cancel(); + _pipeServer?.Dispose(); + } +} diff --git a/src/Uno.UI.RemoteControl.VS/Uno.UI.RemoteControl.VS.csproj b/src/Uno.UI.RemoteControl.VS/Uno.UI.RemoteControl.VS.csproj index b1e998951059..0854ea40ba9d 100644 --- a/src/Uno.UI.RemoteControl.VS/Uno.UI.RemoteControl.VS.csproj +++ b/src/Uno.UI.RemoteControl.VS/Uno.UI.RemoteControl.VS.csproj @@ -1,43 +1,39 @@ - + - - - net461;net48 - + + net48 + $(NoWarn);NU1701 false true + enable - - - - - - - - - + + + - + + + + - - + diff --git a/src/Uno.UI.RemoteControl/Helpers/WebSocketHelper.cs b/src/Uno.UI.RemoteControl/Helpers/WebSocketHelper.cs index d0e90b1ba68e..658180e9ee10 100644 --- a/src/Uno.UI.RemoteControl/Helpers/WebSocketHelper.cs +++ b/src/Uno.UI.RemoteControl/Helpers/WebSocketHelper.cs @@ -18,7 +18,8 @@ public static class WebSocketHelper var pool = ArrayPool.Shared; var buff = pool.Rent(BufferSize); var segment = new ArraySegment(buff); - using var mem = manager.GetStream(); + using var mem = manager.GetStream() + ?? throw new InvalidOperationException($"Unable to get memory stream"); try { @@ -34,7 +35,7 @@ public static class WebSocketHelper { if (result.Count != 0) { - mem.Write(buff, 0, result.Count); + await mem.WriteAsync(buff, 0, result.Count); } mem.Position = 0; @@ -43,7 +44,7 @@ public static class WebSocketHelper } else { - mem.Write(buff, 0, result.Count); + await mem.WriteAsync(buff, 0, result.Count); } } }