Skip to content

Commit

Permalink
feat(hr): Add ability to safely handle exceptions in UpdateFile
Browse files Browse the repository at this point in the history
  • Loading branch information
dr1rrb committed Aug 29, 2024
1 parent bcabbd2 commit e5974da
Showing 1 changed file with 92 additions and 53 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -3,93 +3,132 @@
using System;
using System.IO;
using System.Linq;
using System.Runtime.ExceptionServices;
using System.Threading;
using System.Threading.Tasks;
using Uno.Extensions;
using Uno.Foundation.Logging;
using Uno.UI.RemoteControl.HotReload.Messages;
using Windows.UI.Notifications;

namespace Uno.UI.RemoteControl.HotReload;

public partial class ClientHotReloadProcessor
{
private static int _reqId;

/// <summary>
/// Result details of a file update
/// </summary>
/// <param name="FileUpdated">Indicates if is known to have been updated on server-side.</param>
/// <param name="ApplicationUpdated">Indicates if the change had an impact on the compilation of the application (might be a success-full build or an error).</param>
/// <param name="Error">Gets the error if any happened during the update.</param>
public record struct UpdateResult(
bool FileUpdated,
bool? ApplicationUpdated,
Exception? Error = null);

public async Task UpdateFileAsync(string filePath, string oldText, string newText, bool waitForHotReload, CancellationToken ct)
{
if (string.IsNullOrWhiteSpace(filePath))
if (await TryUpdateFileAsync(filePath, oldText, newText, waitForHotReload, ct) is { Error: { } error })
{
throw new ArgumentOutOfRangeException(nameof(filePath), "File path is invalid (null or empty).");
ExceptionDispatchInfo.Throw(error);
}
}

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)}]";
public async Task<UpdateResult> TryUpdateFileAsync(string filePath, string oldText, string newText, bool waitForHotReload, CancellationToken ct)
{
var result = default(UpdateResult);
try
{
if (string.IsNullOrWhiteSpace(filePath))
{
return result with { Error = new ArgumentOutOfRangeException(nameof(filePath), "File path is invalid (null or empty).") };
}

debug?.Debug($"{tag} Updating file {filePath} (from: {oldText[..100]} | to: {newText[..100]}.");
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 request = new UpdateFile { FilePath = filePath, OldText = oldText, NewText = newText };
var response = await UpdateFileAsync(request, ct);
debug?.Debug($"{tag} Updating file {filePath} (from: {oldText[..100]} | to: {newText[..100]}.");

if (response.Result is FileUpdateResult.NoChanges)
{
debug?.Debug($"{tag} Changes requested has no effect on server, completing.");
return;
}
var request = new UpdateFile { FilePath = filePath, OldText = oldText, NewText = newText };
var response = await UpdateFileCoreAsync(request, ct);

if (response.Result is not FileUpdateResult.Success)
{
debug?.Debug($"{tag} Server failed to update file: {response.Result} (srv error: {response.Error}).");
throw new InvalidOperationException($"Failed to update file {filePath}: {response.Result}. Server replied: {response.Error}");
}
if (response.Result is FileUpdateResult.NoChanges)
{
debug?.Debug($"{tag} Changes requested has no effect on server, completing.");
return result;
}

if (!waitForHotReload)
{
trace?.Trace($"{tag} File updated successfully and do not wait for HR, completing.");
return;
}
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)) };
}

if (response.HotReloadCorrelationId is null)
{
debug?.Debug($"{tag} File updated successfully, but didn't get any HR id from server to wait for.");
throw new InvalidOperationException("Cannot wait for Hot reload for this file.");
}
result.FileUpdated = true;

trace?.Trace($"{tag} Successfully updated file on server ({response.Result}), waiting for server HR id {response.HotReloadCorrelationId}.");
if (!waitForHotReload)
{
trace?.Trace($"{tag} File updated successfully and do not wait for HR, completing.");
return result;
}

var localHrTask = WaitForNextLocalHotReload(ct);
var serverHr = await WaitForServerHotReloadAsync(response.HotReloadCorrelationId.Value, ct);
if (serverHr.Result is HotReloadServerResult.NoChanges)
{
trace?.Trace($"{tag} Server didn't detected any changes in code, do not wait for local HR.");
return;
}
if (response.HotReloadCorrelationId is null)
{
debug?.Debug($"{tag} File updated successfully, but didn't get any HR id from server to wait for.");
return result with { Error = new InvalidOperationException("Cannot wait for Hot reload for this file.") };
}

if (serverHr.Result is not HotReloadServerResult.Success)
{
debug?.Debug($"{tag} Server failed to applied changes in code: {serverHr.Result}.");
throw new InvalidOperationException($"Failed to update file {filePath}, hot-reload failed on server: {serverHr.Result}.");
}
trace?.Trace($"{tag} Successfully updated file on server ({response.Result}), waiting for server HR id {response.HotReloadCorrelationId}.");

trace?.Trace($"{tag} Successfully got HR from server ({serverHr.Result}), waiting for local HR to complete.");
var localHrTask = WaitForNextLocalHotReload(ct);
var serverHr = await WaitForServerHotReloadAsync(response.HotReloadCorrelationId.Value, ct);
if (serverHr.Result is HotReloadServerResult.NoChanges)
{
trace?.Trace($"{tag} Server didn't detected any changes in code, do not wait for local HR.");
return result with { ApplicationUpdated = false };
}

var localHr = await localHrTask;
if (localHr.Result is HotReloadClientResult.Failed)
{
debug?.Debug($"{tag} Failed to apply HR locally: {localHr.Result}.");
throw new InvalidOperationException($"Failed to update file {filePath}, hot-reload failed locally: {localHr.Result}.");
}
result.ApplicationUpdated = true;

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}.") };
}

trace?.Trace($"{tag} Successfully got HR from server ({serverHr.Result}), waiting for local HR to complete.");

var localHr = await localHrTask;
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}.") };
}

await Task.Delay(100, ct); // Wait a bit to make sure to let the dispatcher to resume, this is just for safety.

await Task.Delay(100, ct); // Wait a bit to make sure to let the dispatcher to resume, this is just for safety.
trace?.Trace($"{tag} Successfully updated file and completed HR.");

trace?.Trace($"{tag} Successfully updated file and completed HR.");
return result;
}
catch (OperationCanceledException) when (ct.IsCancellationRequested)
{
return result with { Error = new OperationCanceledException("Update file operation has been cancelled.") };
}
catch (Exception error)
{
return result with { Error = error };
}
}

#region File updates messaging
private EventHandler<UpdateFileResponse>? _updateResponse;

private async ValueTask<UpdateFileResponse> UpdateFileAsync(UpdateFile request, CancellationToken ct)
private async ValueTask<UpdateFileResponse> UpdateFileCoreAsync(UpdateFile request, CancellationToken ct)
{
var timeout = Task.Delay(10_000, ct);
var responseAsync = new TaskCompletionSource<UpdateFileResponse>();
Expand Down Expand Up @@ -122,7 +161,7 @@ void OnFileUpdated(object? _, UpdateFileResponse response)
}

private partial void ProcessUpdateFileResponse(UpdateFileResponse response)
=> _updateResponse?.Invoke(this, response);
=> _updateResponse?.Invoke(this, response);
#endregion

private async ValueTask<HotReloadServerOperationData> WaitForServerHotReloadAsync(long hotReloadId, CancellationToken ct)
Expand Down

0 comments on commit e5974da

Please sign in to comment.