Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
83 changes: 57 additions & 26 deletions src/Uno.UI.RemoteControl.Host/IDEChannel/IdeChannelServer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,8 @@
using System.IO.Pipes;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using StreamJsonRpc;
using Uno.Extensions;
using Uno.UI.RemoteControl.Messaging.IdeChannel;
Expand All @@ -17,22 +17,21 @@ namespace Uno.UI.RemoteControl.Host.IdeChannel;
internal class IdeChannelServer : IIdeChannel, IDisposable
{
private readonly ILogger _logger;
private readonly IConfiguration _configuration;
private readonly IDisposable? _configSubscription;

private readonly Task<bool> _initializeTask;
private Task<bool> _initializeTask;
private NamedPipeServerStream? _pipeServer;
private JsonRpc? _rpcServer;
private Proxy? _proxy;

public IdeChannelServer(ILogger<IdeChannelServer> logger, IConfiguration configuration)
public IdeChannelServer(ILogger<IdeChannelServer> logger, IOptionsMonitor<IdeChannelServerOptions> config)
{
_logger = logger;
_configuration = configuration;

_initializeTask = Task.Run(InitializeServer);
_initializeTask = Task.Run(() => InitializeServer(config.CurrentValue.ChannelId));
_configSubscription = config.OnChange(opts => _initializeTask = InitializeServer(opts.ChannelId));
}


#region IIdeChannel

/// <inheritdoc />
Expand Down Expand Up @@ -65,37 +64,68 @@ public async ValueTask<bool> WaitForReady(CancellationToken ct = default)
/// <summary>
/// Initialize as dev-server (cf. IdeChannelClient for init as IDE)
/// </summary>
private async Task<bool> InitializeServer()
private async Task<bool> InitializeServer(Guid channelId)
{
if (!Guid.TryParse(_configuration["ideChannel"], out var ideChannel))
try
{
_logger.LogDebug("No IDE Channel ID specified, skipping.");
return false;
// First we remove the proxy to prevent messages being sent while we are re-initializing
_proxy = null;

// Dispose any existing server
_rpcServer?.Dispose();
if (_pipeServer is { } server)
{
server.Disconnect();
await server.DisposeAsync();
}
}
catch (Exception error)
{
_logger.LogWarning(error, "An error occurred while disposing the existing IDE channel server. Continuing initialization.");
}

_pipeServer = new NamedPipeServerStream(
pipeName: ideChannel.ToString(),
direction: PipeDirection.InOut,
maxNumberOfServerInstances: 1,
transmissionMode: PipeTransmissionMode.Byte,
options: PipeOptions.Asynchronous | PipeOptions.WriteThrough);
try
{
if (channelId == Guid.Empty)
{
return false;
}

await _pipeServer.WaitForConnectionAsync();
_pipeServer = new NamedPipeServerStream(
pipeName: channelId.ToString(),
direction: PipeDirection.InOut,
maxNumberOfServerInstances: 1,
transmissionMode: PipeTransmissionMode.Byte,
options: PipeOptions.Asynchronous | PipeOptions.WriteThrough);

if (_logger.IsEnabled(LogLevel.Debug))
{
_logger.LogDebug("IDE Connected");
}
await _pipeServer.WaitForConnectionAsync();

_proxy = new(this);
_rpcServer = JsonRpc.Attach(_pipeServer, _proxy);
_proxy = new(this);
_rpcServer = JsonRpc.Attach(_pipeServer, _proxy);

_ = StartKeepAliveAsync();
return true;
if (_logger.IsEnabled(LogLevel.Debug))
{
_logger.LogDebug("IDE channel successfully initialized.");
}

_ = StartKeepAliveAsync();

return true;
}
catch (Exception error)
{
if (_logger.IsEnabled(LogLevel.Error))
{
_logger.LogError(error, "Failed to init the IDE channel.");
}

return false;
}
}

private async Task StartKeepAliveAsync()
{
// Note: The dev-server is expected to send message regularly ... and AS SOON AS POSSIBLE (the Task.Delay is after the first SendToIde()!).
while (_pipeServer?.IsConnected ?? false)
{
_proxy?.SendToIde(new KeepAliveIdeMessage("dev-server"));
Expand All @@ -107,6 +137,7 @@ private async Task StartKeepAliveAsync()
/// <inheritdoc />
public void Dispose()
{
_configSubscription?.Dispose();
_rpcServer?.Dispose();
_pipeServer?.Dispose();
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
ο»Ώusing System;

namespace Uno.UI.RemoteControl.Host.IdeChannel;

public class IdeChannelServerOptions
{
public Guid ChannelId { get; set; }
}
20 changes: 17 additions & 3 deletions src/Uno.UI.RemoteControl.Host/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ static async Task Main(string[] args)
var httpPort = 0;
var parentPID = 0;
var solution = default(string);
var ideChannel = Guid.Empty;
var command = default(string);
var workingDir = default(string);
var timeoutMs = 30000;
Expand Down Expand Up @@ -75,6 +76,14 @@ static async Task Main(string[] args)
solution = s;
}
},
{
"ideChannel=", s => {
if(!Guid.TryParse(s, out ideChannel))
{
throw new ArgumentException($"The ide channel parameter is invalid {s}");
}
}
},
{
"c|command=", s => command = s
},
Expand Down Expand Up @@ -134,13 +143,20 @@ static async Task Main(string[] args)
.SetMinimumLevel(LogLevel.Debug));

globalServices.AddGlobalTelemetry(); // Global telemetry services (Singleton)
globalServices.AddOptions<IdeChannelServerOptions>().Configure(opts => opts.ChannelId = ideChannel);
globalServices.AddSingleton<IIdeChannel, IdeChannelServer>();

#pragma warning disable ASP0000 // Do not call ConfigureServices after calling UseKestrel.
var globalServiceProvider = globalServices.BuildServiceProvider();
#pragma warning restore ASP0000

telemetry = globalServiceProvider.GetRequiredService<ITelemetry>();

// Force resolution of the IDEChannel to enable connection (Note: We should use a BackgroundService instead)
// Note: The IDE channel is expected to inform IDE that we are up as soon as possible.
// This is required for UDEI to **not** log invalid timeout message.
globalServiceProvider.GetService<IIdeChannel>();

#pragma warning disable ASPDEPR004
// WebHostBuilder is deprecated in .NET 10 RC1.
// As we still build for .NET 9, ignore this warning until $(NetPrevious)=net10.
Expand All @@ -163,7 +179,7 @@ static async Task Main(string[] args)
})
.ConfigureServices(services =>
{
services.AddSingleton<IIdeChannel, IdeChannelServer>();
services.AddSingleton<IIdeChannel>(_ => globalServiceProvider.GetRequiredService<IIdeChannel>());
services.AddSingleton<UnoDevEnvironmentService>();

// Add the global service provider to the DI container
Expand Down Expand Up @@ -193,8 +209,6 @@ static async Task Main(string[] args)
// Once the app has started, we use the logger from the host
Uno.Extensions.LogExtensionPoint.AmbientLoggerFactory = host.Services.GetRequiredService<ILoggerFactory>();

// Force resolution of the IDEChannel to enable connection (Note: We should use a BackgroundService instead)
host.Services.GetService<IIdeChannel>();
_ = host.Services.GetRequiredService<UnoDevEnvironmentService>().StartAsync(ct.Token); // Background services are not supported by WebHostBuilder

// Display DevServer version banner
Expand Down
3 changes: 2 additions & 1 deletion src/Uno.UI.RemoteControl.VS/EntryPoint.cs
Original file line number Diff line number Diff line change
Expand Up @@ -527,7 +527,8 @@ private async Task EnsureServerAsync()

async Task TrackConnectionTimeoutAsync(IdeChannelClient ideChannel)
{
await Task.Delay(5000, devServerCt.Token);
// The dev-server is expected to connect back to the IDE as soon as possible, 10sec should be more than enough.
await Task.Delay(10_000, devServerCt.Token);
if (ideChannel.MessagesReceivedCount is 0 && _udei is not null && !devServerCt.IsCancellationRequested)
{
await _udei.NotifyDevServerTimeoutAsync(devServerCt.Token);
Expand Down
Loading