Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

HR: Changed the way code (XAML) is updated through Hot Reload (backport #19584) #19609

Merged
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
#nullable enable

using System.Collections.Generic;

namespace Uno.UI.RemoteControl.Messaging.IdeChannel;

public record ForceHotReloadIdeMessage(long CorrelationId) : IdeMessage(WellKnownScopes.HotReload);
public record ForceHotReloadIdeMessage(long CorrelationId) : IdeMessageWithCorrelationId(CorrelationId, WellKnownScopes.HotReload);
Original file line number Diff line number Diff line change
@@ -1,10 +1,3 @@
#nullable enable
namespace Uno.UI.RemoteControl.Messaging.IdeChannel;

namespace Uno.UI.RemoteControl.Messaging.IdeChannel;

/// <summary>
/// A message sent by the IDE to the dev-server to confirm a <see cref="ForceHotReloadIdeMessage"/> request has been processed.
/// </summary>
/// <param name="RequestId"><see cref="ForceHotReloadIdeMessage.CorrelationId"/> of the request.</param>
/// <param name="Result">Result of the request.</param>
public record HotReloadRequestedIdeMessage(long RequestId, Result Result) : IdeMessage(WellKnownScopes.HotReload);
public record HotReloadRequestedIdeMessage(long IdeCorrelationId, Result Result) : IdeMessage(WellKnownScopes.HotReload);
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
#nullable enable
namespace Uno.UI.RemoteControl.Messaging.IdeChannel;

public record IdeMessageWithCorrelationId(long CorrelationId, string Scope) : IdeMessage(Scope);
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
#nullable enable

namespace Uno.UI.RemoteControl.Messaging.IdeChannel;

public record IdeResultMessage(long IdeCorrelationId, Result Result) : IdeMessage(WellKnownScopes.HotReload);
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
namespace Uno.UI.RemoteControl.Messaging.IdeChannel;

public record UpdateFileIdeMessage(
long CorrelationId,
string FileFullName,
string FileContent,
bool ForceSaveOnDisk) : IdeMessageWithCorrelationId(CorrelationId, WellKnownScopes.HotReload);
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
#nullable enable

using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
Expand Down Expand Up @@ -36,7 +37,7 @@ partial class ServerHotReloadProcessor : IServerProcessor, IDisposable

private bool _useRoslynHotReload;

private bool InitializeMetadataUpdater(ConfigureServer configureServer)
private bool InitializeMetadataUpdater(ConfigureServer configureServer, Dictionary<string, string> properties)
{
_ = bool.TryParse(_remoteControlServer.GetServerConfiguration("metadata-updates"), out _useRoslynHotReload);

Expand All @@ -49,7 +50,7 @@ private bool InitializeMetadataUpdater(ConfigureServer configureServer)
// of a missing path on assemblies loaded from a memory stream.
CompilationWorkspaceProvider.RegisterAssemblyLoader();

InitializeInner(configureServer);
InitializeInner(configureServer, properties);

return true;
}
Expand All @@ -59,7 +60,7 @@ private bool InitializeMetadataUpdater(ConfigureServer configureServer)
}
}

private void InitializeInner(ConfigureServer configureServer)
private void InitializeInner(ConfigureServer configureServer, Dictionary<string, string> properties)
{
if (Assembly.Load("Microsoft.CodeAnalysis.Workspaces") is { } wsAsm)
{
Expand All @@ -81,10 +82,6 @@ private void InitializeInner(ConfigureServer configureServer)
{
await Notify(HotReloadEvent.Initializing);

var properties = configureServer
.MSBuildProperties
.ToDictionary();

// Flag the current build as created for hot reload, which allows for running targets or settings
// props/items in the context of the hot reload workspace.
properties["UnoIsHotReloadHost"] = "True";
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ namespace Uno.UI.RemoteControl.Host.HotReload
{
partial class ServerHotReloadProcessor : IServerProcessor, IDisposable
{
private static readonly TimeSpan _waitForIdeResultTimeout = TimeSpan.FromSeconds(25);
private readonly IRemoteControlServer _remoteControlServer;

public ServerHotReloadProcessor(IRemoteControlServer remoteControlServer)
Expand All @@ -35,8 +36,19 @@ public ServerHotReloadProcessor(IRemoteControlServer remoteControlServer)

public string Scope => WellKnownScopes.HotReload;

private bool _isRunningInsideVisualStudio;

private void InterpretMsbuildProperties(IDictionary<string, string> properties)
{
// This is called from ProcessConfigureServer() during initialization.

_isRunningInsideVisualStudio = properties
.TryGetValue("BuildingInsideVisualStudio", out var vs) && vs.Equals("true", StringComparison.OrdinalIgnoreCase);
}

public async Task ProcessFrame(Frame frame)
{
// Messages received from the CLIENT application
switch (frame.Name)
{
case ConfigureServer.Name:
Expand All @@ -54,14 +66,13 @@ public async Task ProcessFrame(Frame frame)
/// <inheritdoc />
public async Task ProcessIdeMessage(IdeMessage message, CancellationToken ct)
{
// Messages received from the IDE
switch (message)
{
case HotReloadRequestedIdeMessage hrRequested:
// Note: For now the IDE will notify the ProcessingFiles only in case of force hot reload request sent by client!
await Notify(HotReloadEvent.ProcessingFiles, HotReloadEventSource.IDE);
if (_pendingHotReloadRequestToIde.TryGetValue(hrRequested.RequestId, out var request))
case IdeResultMessage resultMessage:
if (_pendingRequestsToIde.TryGetValue(resultMessage.IdeCorrelationId, out var tcs))
{
request.TrySetResult(hrRequested.Result);
tcs.TrySetResult(resultMessage.Result);
}
break;

Expand Down Expand Up @@ -338,7 +349,7 @@ private async ValueTask Complete(HotReloadServerResult result, Exception? except
await Task.Delay(_noChangesRetryDelay);
}

if (await _owner.RequestHotReloadToIde(Id))
if (await _owner.RequestHotReloadToIde())
{
return;
}
Expand Down Expand Up @@ -397,7 +408,13 @@ private void ProcessConfigureServer(ConfigureServer configureServer)
this.Log().LogDebug($"Base project path: {configureServer.ProjectPath}");
}

if (InitializeMetadataUpdater(configureServer))
var properties = configureServer
.MSBuildProperties
.ToDictionary();

InterpretMsbuildProperties(properties);

if (InitializeMetadataUpdater(configureServer, properties))
{
this.Log().LogDebug($"Metadata updater initialized");
}
Expand All @@ -410,8 +427,39 @@ private void ProcessConfigureServer(ConfigureServer configureServer)
}
#endregion

#region SendAndWaitForResult
private readonly ConcurrentDictionary<long, TaskCompletionSource<Result>> _pendingRequestsToIde = new();

private long _lasIdeCorrelationId;

private long GetNextIdeCorrelationId() => Interlocked.Increment(ref _lasIdeCorrelationId);

private async Task<Result> SendAndWaitForResult<TMessage>(TMessage message)
where TMessage : IdeMessageWithCorrelationId
{
var tcs = new TaskCompletionSource<Result>();
try
{
using var cts = new CancellationTokenSource(_waitForIdeResultTimeout);
await using var ctReg = cts.Token.Register(() => tcs.TrySetCanceled());

_pendingRequestsToIde.TryAdd(message.CorrelationId, tcs);
await _remoteControlServer.SendMessageToIDEAsync(message);

return await tcs.Task;
}
catch (Exception ex)
{
return Result.Fail(ex);
}
finally
{
_pendingRequestsToIde.TryRemove(message.CorrelationId, out _);
}
}
#endregion

#region UpdateFile
private readonly ConcurrentDictionary<long, TaskCompletionSource<Result>> _pendingHotReloadRequestToIde = new();

private async Task ProcessUpdateFile(UpdateFile message)
{
Expand All @@ -422,15 +470,26 @@ private async Task ProcessUpdateFile(UpdateFile message)
var (result, error) = message switch
{
{ FilePath: null or { Length: 0 } } => (FileUpdateResult.BadRequest, "Invalid request (file path is empty)"),
{ OldText: not null, NewText: not null } => await DoUpdate(message.OldText, message.NewText),
{ OldText: null, NewText: not null } => await DoWrite(message.NewText),
{ NewText: null, IsCreateDeleteAllowed: true } => await DoDelete(),
{ OldText: not null, NewText: not null } => await (_isRunningInsideVisualStudio
? DoRemoteUpdateInIde(message.NewText)
: DoUpdateOnDisk(message.OldText, message.NewText)),
{ OldText: null, NewText: not null } => await (_isRunningInsideVisualStudio
? DoRemoteUpdateInIde(message.NewText)
: DoWriteToDisk(message.NewText)),
{ NewText: null, IsCreateDeleteAllowed: true } => await DoDeleteFromDisk(),
_ => (FileUpdateResult.BadRequest, "Invalid request")
};
if ((int)result < 300 && !message.IsForceHotReloadDisabled)

var isIdeSupportingHotReload = _isRunningInsideVisualStudio;

if (message.IsForceHotReloadDisabled is false && (int)result < 300)
{
hotReload.EnableAutoRetryIfNoChanges(message.ForceHotReloadAttempts, message.ForceHotReloadDelay);
await RequestHotReloadToIde(hotReload.Id);

if (isIdeSupportingHotReload)
{
await RequestHotReloadToIde();
}
}

await _remoteControlServer.SendFrame(new UpdateFileResponse(message.RequestId, message.FilePath ?? "", result, error, hotReload.Id));
Expand All @@ -441,7 +500,21 @@ private async Task ProcessUpdateFile(UpdateFile message)
await _remoteControlServer.SendFrame(new UpdateFileResponse(message.RequestId, message.FilePath ?? "", FileUpdateResult.Failed, ex.Message));
}

async ValueTask<(FileUpdateResult, string?)> DoUpdate(string oldText, string newText)
async Task<(FileUpdateResult, string?)> DoRemoteUpdateInIde(string newText)
{
var saveToDisk = message.ForceSaveOnDisk ?? true; // Temporary set to true until this issue is fixed: https://github.com/unoplatform/uno.hotdesign/issues/3454

// Right now, when running on VS, we're delegating the file update to the code that is running inside VS.
// we're not doing this for other file operations because they are not/less required for hot-reload. We may need to revisit this eventually.
var ideMsg = new UpdateFileIdeMessage(GetNextIdeCorrelationId(), message.FilePath, newText, saveToDisk);
var result = await SendAndWaitForResult(ideMsg);

return result.IsSuccess
? (FileUpdateResult.Success, null)
: (FileUpdateResult.Failed, result.Error);
}

async Task<(FileUpdateResult, string?)> DoUpdateOnDisk(string oldText, string newText)
{
if (!File.Exists(message.FilePath))
{
Expand Down Expand Up @@ -482,7 +555,7 @@ private async Task ProcessUpdateFile(UpdateFile message)
return (FileUpdateResult.Success, null);
}

async ValueTask<(FileUpdateResult, string?)> DoWrite(string newText)
async Task<(FileUpdateResult, string?)> DoWriteToDisk(string newText)
{
if (!message.IsCreateDeleteAllowed && !File.Exists(message.FilePath))
{
Expand All @@ -506,7 +579,7 @@ private async Task ProcessUpdateFile(UpdateFile message)
return (FileUpdateResult.Success, null);
}

async ValueTask<(FileUpdateResult, string?)> DoDelete()
async ValueTask<(FileUpdateResult, string?)> DoDeleteFromDisk()
{
if (!File.Exists(message.FilePath))
{
Expand Down Expand Up @@ -555,37 +628,22 @@ async Task WaitForFileUpdated()
};
watcher.EnableRaisingEvents = true;

if (await Task.WhenAny(tcs.Task, Task.Delay(TimeSpan.FromSeconds(2))) != tcs.Task
if (await Task.WhenAny(tcs.Task, Task.Delay(TimeSpan.FromSeconds(5))) != tcs.Task
&& this.Log().IsEnabled(LogLevel.Debug))
{
this.Log().LogDebug($"File update event not received for '{message.FilePath}', continuing anyway [{message.RequestId}].");
}
}
}

private async Task<bool> RequestHotReloadToIde(long sequenceId)
private async Task<bool> RequestHotReloadToIde()
{
var hrRequest = new ForceHotReloadIdeMessage(sequenceId);
var hrRequested = new TaskCompletionSource<Result>();

try
{
using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(2));
await using var ctReg = cts.Token.Register(() => hrRequested.TrySetCanceled());
var result = await SendAndWaitForResult(new ForceHotReloadIdeMessage(GetNextIdeCorrelationId()));

_pendingHotReloadRequestToIde.TryAdd(hrRequest.CorrelationId, hrRequested);
await _remoteControlServer.SendMessageToIDEAsync(hrRequest);
// Note: For now the IDE will notify the ProcessingFiles only in case of force hot reload request sent by client!
await Notify(HotReloadEvent.ProcessingFiles, HotReloadEventSource.IDE);

return await hrRequested.Task is { IsSuccess: true };
}
catch (Exception)
{
return false;
}
finally
{
_pendingHotReloadRequestToIde.TryRemove(hrRequest.CorrelationId, out _);
}
return result.IsSuccess;
}
#endregion

Expand Down
Loading
Loading