diff --git a/src/Uno.UI.RemoteControl/HotReload/ClientHotReloadProcessor.ClientApi.cs b/src/Uno.UI.RemoteControl/HotReload/ClientHotReloadProcessor.ClientApi.cs
index 3d2f0b0f91cc..6d1116dc0ba2 100644
--- a/src/Uno.UI.RemoteControl/HotReload/ClientHotReloadProcessor.ClientApi.cs
+++ b/src/Uno.UI.RemoteControl/HotReload/ClientHotReloadProcessor.ClientApi.cs
@@ -1,6 +1,7 @@
#if HAS_UNO_WINUI && __SKIA__
using System;
+using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Runtime.ExceptionServices;
@@ -28,33 +29,92 @@ public record struct UpdateResult(
bool? ApplicationUpdated,
Exception? Error = null);
- public async Task UpdateFileAsync(string filePath, string oldText, string newText, bool waitForHotReload, CancellationToken ct)
+ ///
+ /// Request details of a file update
+ ///
+ /// Path of the file to update, relative to the solution root dir.
+ /// Current text to replace in the file.
+ /// Replacement text.
+ /// Indicates if we should also wait for the change to be applied in the application before completing the resulting task.
+ public record struct UpdateRequest(
+ string FilePath,
+ string OldText,
+ string NewText,
+ bool WaitForHotReload = true)
{
- if (await TryUpdateFileAsync(filePath, oldText, newText, waitForHotReload, ct) is { Error: { } error })
+ ///
+ /// The max delay to wait for the server to process a file update request.
+ ///
+ /// This includes the time to send the request to the server, the server to process it and send a reply.
+ public TimeSpan ServerUpdateTimeout { get; set; } = TimeSpan.FromSeconds(10);
+
+ ///
+ /// The max delay to wait for the server to process a hot-reload and send completion messages after a file has been updated.
+ ///
+ ///
+ /// Once a file has been updated on the server, this includes the time for the IDE/dev-server to detect the file update,
+ /// roslyn to generate delta (or error), send it to the app, and then the dev-server to send notification of HR completion.
+ ///
+ public TimeSpan ServerHotReloadTimeout { get; set; } = TimeSpan.FromSeconds(10);
+
+ ///
+ /// The max delay to wait for the local application to process a hot-reload delta.
+ ///
+ /// This includes the time to apply the delta locally and then to run all local handlers.
+ public TimeSpan LocalHotReloadTimeout { get; set; } = TimeSpan.FromSeconds(3);
+
+ public UpdateRequest WithExtendedTimeouts(float? factor = null)
+ {
+ factor ??= Debugger.IsAttached ? 30 : 10;
+
+ return this with
+ {
+ ServerUpdateTimeout = ServerUpdateTimeout * factor.Value,
+ ServerHotReloadTimeout = ServerHotReloadTimeout * factor.Value,
+ LocalHotReloadTimeout = LocalHotReloadTimeout * factor.Value
+ };
+ }
+
+ public UpdateRequest Undo()
+ => this with { OldText = NewText, NewText = OldText };
+
+ public UpdateRequest Undo(bool waitForHotReload)
+ => this with { OldText = NewText, NewText = OldText, WaitForHotReload = waitForHotReload };
+ }
+
+ public Task UpdateFileAsync(string filePath, string oldText, string newText, bool waitForHotReload, CancellationToken ct)
+ => UpdateFileAsync(new UpdateRequest(filePath, oldText, newText, waitForHotReload), ct);
+
+ public async Task UpdateFileAsync(UpdateRequest req, CancellationToken ct)
+ {
+ if (await TryUpdateFileAsync(req, ct) is { Error: { } error })
{
ExceptionDispatchInfo.Throw(error);
}
}
- public async Task TryUpdateFileAsync(string filePath, string oldText, string newText, bool waitForHotReload, CancellationToken ct)
+ public Task TryUpdateFileAsync(string filePath, string oldText, string newText, bool waitForHotReload, CancellationToken ct)
+ => TryUpdateFileAsync(new UpdateRequest(filePath, oldText, newText, waitForHotReload), ct);
+
+ public async Task TryUpdateFileAsync(UpdateRequest req, CancellationToken ct)
{
var result = default(UpdateResult);
try
{
- if (string.IsNullOrWhiteSpace(filePath))
+ if (string.IsNullOrWhiteSpace(req.FilePath))
{
- return result with { Error = new ArgumentOutOfRangeException(nameof(filePath), "File path is invalid (null or empty).") };
+ return result with { Error = new ArgumentOutOfRangeException(nameof(req.FilePath), "File path is invalid (null or empty).") };
}
var log = this.Log();
var trace = log.IsTraceEnabled(LogLevel.Trace) ? log : default;
var debug = log.IsDebugEnabled(LogLevel.Debug) ? log : default;
- var tag = $"[{Interlocked.Increment(ref _reqId):D2}-{Path.GetFileName(filePath)}]";
+ var tag = $"[{Interlocked.Increment(ref _reqId):D2}-{Path.GetFileName(req.FilePath)}]";
- debug?.Debug($"{tag} Updating file {filePath} (from: {oldText[..100]} | to: {newText[..100]}.");
+ debug?.Debug($"{tag} Updating file {req.FilePath} (from: {req.OldText[..100]} | to: {req.NewText[..100]}.");
- var request = new UpdateFile { FilePath = filePath, OldText = oldText, NewText = newText };
- var response = await UpdateFileCoreAsync(request, ct);
+ var request = new UpdateFile { FilePath = req.FilePath, OldText = req.OldText, NewText = req.NewText };
+ var response = await UpdateFileCoreAsync(request, req.ServerUpdateTimeout, ct);
if (response.Result is FileUpdateResult.NoChanges)
{
@@ -65,12 +125,12 @@ public async Task TryUpdateFileAsync(string filePath, string oldTe
if (response.Result is not FileUpdateResult.Success)
{
debug?.Debug($"{tag} Server failed to update file: {response.Result} (srv error: {response.Error}).");
- return result with { Error = new InvalidOperationException($"Failed to update file {filePath}: {response.Result} (see inner exception for more details)", new InvalidOperationException(response.Error)) };
+ return result with { Error = new InvalidOperationException($"Failed to update file {req.FilePath}: {response.Result} (see inner exception for more details)", new InvalidOperationException(response.Error)) };
}
result.FileUpdated = true;
- if (!waitForHotReload)
+ if (!req.WaitForHotReload)
{
trace?.Trace($"{tag} File updated successfully and do not wait for HR, completing.");
return result;
@@ -84,8 +144,8 @@ public async Task TryUpdateFileAsync(string filePath, string oldTe
trace?.Trace($"{tag} Successfully updated file on server ({response.Result}), waiting for server HR id {response.HotReloadCorrelationId}.");
- var localHrTask = WaitForNextLocalHotReload(ct);
- var serverHr = await WaitForServerHotReloadAsync(response.HotReloadCorrelationId.Value, ct);
+ var localHrTask = WaitForNextLocalHotReload(req.LocalHotReloadTimeout, ct);
+ var serverHr = await WaitForServerHotReloadAsync(response.HotReloadCorrelationId.Value, req.ServerHotReloadTimeout, ct);
if (serverHr.Result is HotReloadServerResult.NoChanges)
{
trace?.Trace($"{tag} Server didn't detected any changes in code, do not wait for local HR.");
@@ -97,7 +157,7 @@ public async Task TryUpdateFileAsync(string filePath, string oldTe
if (serverHr.Result is not HotReloadServerResult.Success)
{
debug?.Debug($"{tag} Server failed to applied changes in code: {serverHr.Result}.");
- return result with { Error = new InvalidOperationException($"Failed to update file {filePath}, hot-reload failed on server: {serverHr.Result}.") };
+ return result with { Error = new InvalidOperationException($"Failed to update file {req.FilePath}, hot-reload failed on server: {serverHr.Result}.") };
}
trace?.Trace($"{tag} Successfully got HR from server ({serverHr.Result}), waiting for local HR to complete.");
@@ -106,7 +166,7 @@ public async Task TryUpdateFileAsync(string filePath, string oldTe
if (localHr.Result is HotReloadClientResult.Failed)
{
debug?.Debug($"{tag} Failed to apply HR locally: {localHr.Result}.");
- return result with { Error = new InvalidOperationException($"Failed to update file {filePath}, hot-reload failed locally: {localHr.Result}.") };
+ return result with { Error = new InvalidOperationException($"Failed to update file {req.FilePath}, hot-reload failed locally: {localHr.Result}.") };
}
await Task.Delay(100, ct); // Wait a bit to make sure to let the dispatcher to resume, this is just for safety.
@@ -128,9 +188,9 @@ public async Task TryUpdateFileAsync(string filePath, string oldTe
#region File updates messaging
private EventHandler? _updateResponse;
- private async ValueTask UpdateFileCoreAsync(UpdateFile request, CancellationToken ct)
+ private async ValueTask UpdateFileCoreAsync(UpdateFile request, TimeSpan timeout, CancellationToken ct)
{
- var timeout = Task.Delay(10_000, ct);
+ var timeoutTask = Task.Delay(timeout, ct);
var responseAsync = new TaskCompletionSource();
try
@@ -139,7 +199,7 @@ private async ValueTask UpdateFileCoreAsync(UpdateFile reque
await _rcClient.SendMessage(request);
- if (await Task.WhenAny(responseAsync.Task, timeout) == timeout)
+ if (await Task.WhenAny(responseAsync.Task, timeoutTask) == timeoutTask)
{
throw new TimeoutException("Failed to get response from the server in the given delay.");
}
@@ -164,9 +224,9 @@ partial void ProcessUpdateFileResponse(UpdateFileResponse response)
=> _updateResponse?.Invoke(this, response);
#endregion
- private async ValueTask WaitForServerHotReloadAsync(long hotReloadId, CancellationToken ct)
+ private async ValueTask WaitForServerHotReloadAsync(long hotReloadId, TimeSpan timeout, CancellationToken ct)
{
- var timeout = Task.Delay(10_000, ct);
+ var timeoutTask = Task.Delay(timeout, ct);
var operationAsync = new TaskCompletionSource();
try
@@ -174,7 +234,7 @@ private async ValueTask WaitForServerHotReloadAsyn
StatusChanged += OnStatusChanged;
CheckIfCompleted(CurrentStatus);
- if (await Task.WhenAny(operationAsync.Task, timeout) == timeout)
+ if (await Task.WhenAny(operationAsync.Task, timeoutTask) == timeoutTask)
{
throw new TimeoutException($"Failed to get hot-reload (id: {hotReloadId}) from the server in the given delay.");
}
@@ -199,9 +259,9 @@ void CheckIfCompleted(Status status)
}
}
- private async ValueTask WaitForNextLocalHotReload(CancellationToken ct)
+ private async ValueTask WaitForNextLocalHotReload(TimeSpan timeout, CancellationToken ct)
{
- var timeout = Task.Delay(10_000, ct);
+ var timeoutTask = Task.Delay(timeout, ct);
var operationAsync = new TaskCompletionSource();
var previousId = CurrentStatus.Local.Operations is { Count: > 0 } ops ? ops.Max(op => op.Id) : -1;
@@ -209,7 +269,7 @@ private async ValueTask WaitForNextLocalHotReload(Canc
{
StatusChanged += OnStatusChanged;
- if (await Task.WhenAny(operationAsync.Task, timeout) == timeout)
+ if (await Task.WhenAny(operationAsync.Task, timeoutTask) == timeoutTask)
{
throw new TimeoutException($"Failed to get a local hot-reload (id: {previousId}+) in the given delay.");
}
diff --git a/src/Uno.UI.RuntimeTests/Tests/HotReload/Frame/HRApp/Tests/Given_TextBlock.cs b/src/Uno.UI.RuntimeTests/Tests/HotReload/Frame/HRApp/Tests/Given_TextBlock.cs
index 27b5192ede6a..fe276a8b3f5a 100644
--- a/src/Uno.UI.RuntimeTests/Tests/HotReload/Frame/HRApp/Tests/Given_TextBlock.cs
+++ b/src/Uno.UI.RuntimeTests/Tests/HotReload/Frame/HRApp/Tests/Given_TextBlock.cs
@@ -75,25 +75,21 @@ public async Task When_Changing_TextBlock_UsingHRClient()
var hr = Uno.UI.RemoteControl.RemoteControlClient.Instance?.Processors.OfType().Single();
var ctx = Uno.UI.RuntimeTests.Tests.HotReload.FrameworkElementExtensions.GetDebugParseContext(new HR_Frame_Pages_Page1());
+ var req = new Uno.UI.RemoteControl.HotReload.ClientHotReloadProcessor.UpdateRequest(
+ ctx.FileName,
+ FirstPageTextBlockOriginalText,
+ FirstPageTextBlockChangedText,
+ true)
+ .WithExtendedTimeouts(); // Required for CI
try
{
- await hr.UpdateFileAsync(
- ctx.FileName,
- FirstPageTextBlockOriginalText,
- FirstPageTextBlockChangedText,
- true,
- ct);
+ await hr.UpdateFileAsync(req, ct);
await UnitTestsUIContentHelper.Content.ValidateTextOnChildTextBlock(FirstPageTextBlockChangedText);
}
finally
{
- await hr.UpdateFileAsync(
- ctx.FileName,
- FirstPageTextBlockChangedText,
- FirstPageTextBlockOriginalText,
- false,
- CancellationToken.None);
+ await hr.UpdateFileAsync(req.Undo(waitForHotReload: false), CancellationToken.None);
}
}
}