Skip to content

Commit

Permalink
fix: Make sure to fully restore connection (and re-discover processor…
Browse files Browse the repository at this point in the history
…s) if connection is lost
  • Loading branch information
dr1rrb committed Jul 31, 2024
1 parent 09fa627 commit 07e978b
Show file tree
Hide file tree
Showing 4 changed files with 90 additions and 35 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ private void WorkspaceLoadResult(HotReloadWorkspaceLoadResult hotReloadWorkspace
// This mean that HR won't work with the debugger attached.
if (Debugger.IsAttached)
{
_status.ReportServerState(HotReloadState.Disabled);
_status.ReportInvalidRuntime();
}
_hotReloadWorkloadSpaceLoaded.SetResult(hotReloadWorkspaceLoadResult.WorkspaceInitialized);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -70,20 +70,32 @@ private class StatusSink(ClientHotReloadProcessor owner)
#endif

private HotReloadState? _serverState;
private bool _isFinalServerState;
private ImmutableDictionary<long, HotReloadServerOperationData> _serverOperations = ImmutableDictionary<long, HotReloadServerOperationData>.Empty;
private ImmutableList<HotReloadClientOperation> _localOperations = ImmutableList<HotReloadClientOperation>.Empty;
private HotReloadSource _source;

public void ReportInvalidRuntime()
{
_serverState = HotReloadState.Disabled;
_isFinalServerState = true;
}

public void ReportServerState(HotReloadState state)
{
if (_isFinalServerState)
{
return;
}

_serverState = state;
NotifyStatusChanged();
}

#if HAS_UNO_WINUI
public void ReportServerStatus(HotReloadStatusMessage status)
{
if (_serverState is not HotReloadState.Disabled)
if (!_isFinalServerState)
{
_serverState = status.State; // Do not override the state if it has already been set (debugger attached with dev-server)
}
Expand Down Expand Up @@ -131,7 +143,12 @@ private Status BuildStatus()
{
var serverState = _serverState ?? (_localOperations.Any() ? HotReloadState.Ready /* no info */ : HotReloadState.Initializing);
var localState = _localOperations.Any(op => op.Result is null) ? HotReloadState.Processing : HotReloadState.Ready;
var globalState = _serverState is HotReloadState.Disabled ? HotReloadState.Disabled : (HotReloadState)Math.Max((int)serverState, (int)localState);
var globalState = _serverState switch
{
HotReloadState.Disabled => HotReloadState.Disabled,
HotReloadState.Initializing => HotReloadState.Initializing,
_ => (HotReloadState)Math.Max((int)serverState, (int)localState)
};

return new(globalState, (serverState, _serverOperations.Values.ToImmutableArray()), (localState, _localOperations));
}
Expand Down
51 changes: 31 additions & 20 deletions src/Uno.UI.RemoteControl/HotReload/ClientHotReloadProcessor.cs
Original file line number Diff line number Diff line change
Expand Up @@ -87,40 +87,51 @@ _forcedHotReloadMode is null
private async Task ConfigureServer()
{
var assembly = _rcClient.AppType.Assembly;

if (assembly.GetCustomAttributes(typeof(ProjectConfigurationAttribute), false) is ProjectConfigurationAttribute[] configs)
{
var config = configs.First();

_projectPath = config.ProjectPath;
_xamlPaths = config.XamlPaths;
_status.ReportServerState(HotReloadState.Initializing);

if (this.Log().IsEnabled(LogLevel.Debug))
try
{
this.Log().LogDebug($"ProjectConfigurationAttribute={config.ProjectPath}, Paths={_xamlPaths.Length}");
}
var config = configs.First();

if (this.Log().IsEnabled(LogLevel.Trace))
{
foreach (var path in _xamlPaths)
_projectPath = config.ProjectPath;
_xamlPaths = config.XamlPaths;

if (this.Log().IsEnabled(LogLevel.Debug))
{
this.Log().Trace($"\t- {path}");
this.Log().LogDebug($"ProjectConfigurationAttribute={config.ProjectPath}, Paths={_xamlPaths.Length}");
}
}

_msbuildProperties = Messages.ConfigureServer.BuildMSBuildProperties(config.MSBuildProperties);
if (this.Log().IsEnabled(LogLevel.Trace))
{
foreach (var path in _xamlPaths)
{
this.Log().Trace($"\t- {path}");
}
}

ConfigureHotReloadMode();
InitializeMetadataUpdater();
InitializePartialReload();
InitializeXamlReader();
_msbuildProperties = Messages.ConfigureServer.BuildMSBuildProperties(config.MSBuildProperties);

ConfigureServer message = new(_projectPath, _xamlPaths, GetMetadataUpdateCapabilities(), _serverMetadataUpdatesEnabled, config.MSBuildProperties);
ConfigureHotReloadMode();
InitializeMetadataUpdater();
InitializePartialReload();
InitializeXamlReader();

await _rcClient.SendMessage(message);
ConfigureServer message = new(_projectPath, _xamlPaths, GetMetadataUpdateCapabilities(), _serverMetadataUpdatesEnabled, config.MSBuildProperties);

await _rcClient.SendMessage(message);
}
catch
{
_status.ReportServerState(HotReloadState.Disabled);
throw;
}
}
else
{
_status.ReportServerState(HotReloadState.Disabled);

if (this.Log().IsEnabled(LogLevel.Error))
{
this.Log().LogError("Unable to find ProjectConfigurationAttribute");
Expand Down
51 changes: 39 additions & 12 deletions src/Uno.UI.RemoteControl/RemoteControlClient.cs
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,34 @@ internal static RemoteControlClient Initialize(Type appType, ServerEndpointAttri
private Timer? _keepAliveTimer;
private KeepAliveMessage _ping = new();

private record Connection(Uri EndPoint, Stopwatch Since, WebSocket? Socket);
private record Connection(RemoteControlClient Owner, Uri EndPoint, Stopwatch Since, WebSocket? Socket) : IAsyncDisposable
{
private static class States
{
public const int Ready = 0;
public const int Active = 1;
public const int Disposed = 255;
}

private readonly CancellationTokenSource _ct = new();
private int _state = Socket is null ? States.Disposed : States.Ready;

public void EnsureActive()
{
if (Interlocked.CompareExchange(ref _state, States.Active, States.Ready) is States.Ready)
{
DevServerDiagnostics.Current = Owner._status;
_ = Owner.ProcessMessages(Socket!, _ct.Token);
}
}

/// <inheritdoc />
public async ValueTask DisposeAsync()
{
_state = States.Disposed;
await _ct.CancelAsync();
}
}

private RemoteControlClient(Type appType, ServerEndpointAttribute[]? endpoints = null)
{
Expand Down Expand Up @@ -148,7 +175,7 @@ public void RegisterPreProcessor(IRemoteControlPreProcessor preprocessor)
{
var connectionTask = _connection;
var connection = await connectionTask;
if (connection is { Socket.State: not WebSocketState.Open } and { Since.ElapsedMilliseconds: >= _connectionRetryInterval })
if (connection is ({ Socket: null } or { Socket.State: not WebSocketState.Open }) and { Since.ElapsedMilliseconds: >= _connectionRetryInterval })
{
// We have a socket (and uri) but we lost the connection, try to reconnect (only if more than 5 sec since last attempt)
lock (_connectionGate)
Expand All @@ -157,11 +184,12 @@ public void RegisterPreProcessor(IRemoteControlPreProcessor preprocessor)

if (connectionTask == _connection)
{
_connection = Connect(connection.EndPoint, CancellationToken.None);
_connection = Connect(connection.EndPoint, CancellationToken.None)!;
}
}

connection = await _connection;
connection?.EnsureActive();
}

_status.ReportActiveConnection(connection);
Expand Down Expand Up @@ -199,7 +227,7 @@ public void RegisterPreProcessor(IRemoteControlPreProcessor preprocessor)
var cts = new CancellationTokenSource();
var task = Connect(s.endpoint, s.port, isHttps, cts.Token);
return (task: task, cts: cts);
return (task, cts);
})
.ToDictionary(c => c.task as Task);
var timeout = Task.Delay(30000);
Expand Down Expand Up @@ -230,7 +258,7 @@ public void RegisterPreProcessor(IRemoteControlPreProcessor preprocessor)

// If the connection is successful, break the loop
if (completed.IsCompleted
&& ((Task<Connection>)completed).Result is { Socket: not null } successfulConnection)
&& ((Task<Connection?>)completed).Result is { Socket: not null } successfulConnection)
{
connected = successfulConnection;
break;
Expand Down Expand Up @@ -258,8 +286,7 @@ public void RegisterPreProcessor(IRemoteControlPreProcessor preprocessor)
this.Log().LogDebug($"Connected to {connected!.EndPoint}");
}

DevServerDiagnostics.Current = _status;
_ = ProcessMessages(connected!.Socket!);
connected.EnsureActive();

return connected;
}
Expand Down Expand Up @@ -345,7 +372,7 @@ void AbortPending()
}
}

private async Task<Connection?> Connect(Uri serverUri, CancellationToken ct)
private async Task<Connection> Connect(Uri serverUri, CancellationToken ct)
{
// Note: This method **MUST NOT** throw any exception as it being used for re-connection

Expand All @@ -361,7 +388,7 @@ void AbortPending()

await client.ConnectAsync(serverUri, ct);

return new(serverUri, watch, client);
return new(this, serverUri, watch, client);
}
catch (Exception e)
{
Expand All @@ -370,7 +397,7 @@ void AbortPending()
this.Log().Trace($"Connecting to [{serverUri}] failed: {e.Message}");
}

return new(serverUri, watch, null);
return new(this, serverUri, watch, null);
}
}

Expand All @@ -393,7 +420,7 @@ private static void CleanupConnections(IEnumerable<Task> connections)
}
});

private async Task ProcessMessages(WebSocket socket)
private async Task ProcessMessages(WebSocket socket, CancellationToken ct)
{
_ = InitializeServerProcessors();

Expand All @@ -404,7 +431,7 @@ private async Task ProcessMessages(WebSocket socket)

StartKeepAliveTimer();

while (await WebSocketHelper.ReadFrame(socket, CancellationToken.None) is HotReload.Messages.Frame frame)
while (await WebSocketHelper.ReadFrame(socket, ct) is HotReload.Messages.Frame frame)
{
if (frame.Scope == WellKnownScopes.DevServerChannel)
{
Expand Down

0 comments on commit 07e978b

Please sign in to comment.