Skip to content

Commit

Permalink
Merge pull request #19609 from unoplatform/mergify/bp/release/stable/…
Browse files Browse the repository at this point in the history
…5.6/pr-19584

HR: Changed the way code (XAML) is updated through Hot Reload (backport #19584)
  • Loading branch information
jeromelaban authored Feb 27, 2025
2 parents e9b1cae + 9c9e477 commit fb989a1
Show file tree
Hide file tree
Showing 13 changed files with 791 additions and 59 deletions.
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

0 comments on commit fb989a1

Please sign in to comment.