Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions src/Clients/js/src/std/bcl/errors/CoreIpcError.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,4 +3,6 @@ export class CoreIpcError extends Error {
super(message);
this.name = 'CoreIpcError';
}

data?: { [key: string]: any }
}
1 change: 1 addition & 0 deletions src/UiPath.CoreIpc.Tests/Services/ISystemService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ public interface ISystemService
Task<string> DanishNameOfDay(DayOfWeek day, CancellationToken ct);

Task<byte[]> ReverseBytes(byte[] bytes, CancellationToken ct = default);
Task<bool> ThrowWithData(string serializedkey, object? serializedValue, string notSerializedKey);
}

public interface IUnregisteredCallback
Expand Down
9 changes: 9 additions & 0 deletions src/UiPath.CoreIpc.Tests/Services/SystemService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -102,4 +102,13 @@ public Task<byte[]> ReverseBytes(byte[] bytes, CancellationToken ct = default)
}
return Task.FromResult(bytes);
}

public async Task<bool> ThrowWithData(string serializedkey, object? serializedValue, string notSerializedKey)
{
var ex = new NotImplementedException();
ex.Data[serializedkey] = serializedValue;
ex.Data[notSerializedKey] = serializedValue;
await Task.Yield();
throw ex;
}
}
45 changes: 45 additions & 0 deletions src/UiPath.CoreIpc.Tests/SystemTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -142,6 +142,51 @@ public async Task ServerCallingInexistentCallback_ShouldThrow()
marshalledExceptionType.ShouldBe(typeof(EndpointNotFoundException).FullName);
}

#if !NET461 //netframework only works with old style serializable types, so this won't work

[Fact]
public async Task ExceptionDataIsMarshalledForObject()
=> await ExceptionDataIsMarshalled(new ComplexNumber { I = 1, J = 2});

[Fact]
public async Task ExceptionDataIsMarshalledForArray()
=> await ExceptionDataIsMarshalled(new string[] { "bla", "bla" });

#endif

[Theory]
[InlineData("someString")]
[InlineData(2L)]
[InlineData(true)]
[InlineData(null)]
[InlineData(12.34d)]
public async Task ExceptionDataIsMarshalled(object? value)
{
const string notSerialized = "notSerializedKey";
const string notSerialized2 = "notSerializedKey2";
const string InlineDataKey = "somekey";
const string OnErrorDataKey = "extraData";
Error.SerializableDataKeys.Add(InlineDataKey);
Error.SerializableDataKeys.Add(OnErrorDataKey);
Error.SerializableDataKeys.Remove(notSerialized);

_onError = (callInfo, ex) =>
{
ex.Data.Add(OnErrorDataKey, value);
ex.Data.Add(notSerialized2, value);
var readValue = ex.Data[OnErrorDataKey];
readValue.ShouldBe(value);
return ex;
};

var ex = await Proxy.ThrowWithData(InlineDataKey, value, notSerialized).ShouldThrowAsync<RemoteException>();
AsJtokenOrPrimitive(ex.Data[InlineDataKey]).ShouldBeEquivalentTo(AsJtokenOrPrimitive(value));
ex.Data.Contains(notSerialized).ShouldBeFalse();
AsJtokenOrPrimitive(ex.Data[OnErrorDataKey]).ShouldBeEquivalentTo(AsJtokenOrPrimitive(value));

object? AsJtokenOrPrimitive(object? value) => value is null || value.GetType().IsPrimitive ? value : Newtonsoft.Json.Linq.JToken.FromObject(value);
}

[Fact]
public async Task ServerCallingInexistentCallback_ShouldThrow2()
=> await Proxy.AddIncrement(1, 2).ShouldThrowAsync<RemoteException>()
Expand Down
4 changes: 3 additions & 1 deletion src/UiPath.CoreIpc.Tests/TestBase.cs
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ public abstract class TestBase : IAsyncLifetime

protected readonly ConcurrentBag<CallInfo> _serverBeforeCalls = new();
protected BeforeCallHandler? _tailBeforeCall = null;
protected Func<CallInfo?, Exception, Exception>? _onError;

public TestBase(ITestOutputHelper outputHelper)
{
Expand Down Expand Up @@ -91,7 +92,8 @@ async Task<IpcServer> Core()
{
_serverBeforeCalls.Add(callInfo);
return _tailBeforeCall?.Invoke(callInfo, ct) ?? Task.CompletedTask;
}
},
OnError = (callInfo, exception) => _onError?.Invoke(callInfo, exception) ?? exception
};

return new()
Expand Down
3 changes: 1 addition & 2 deletions src/UiPath.CoreIpc/Helpers/Helpers.cs
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,7 @@ namespace UiPath.Ipc;
internal static class Helpers
{
internal const BindingFlags InstanceFlags = BindingFlags.Instance | BindingFlags.Public | BindingFlags.DeclaredOnly;
internal static Error ToError(this Exception ex) => new(ex.Message, ex.StackTrace ?? ex.GetBaseException().StackTrace!, GetExceptionType(ex), ex.InnerException?.ToError());
private static string GetExceptionType(Exception exception) => (exception as RemoteException)?.Type ?? exception.GetType().FullName!;
internal static Error ToError(this Exception ex) => Error.FromException(ex);
internal static bool Enabled(this ILogger? logger, LogLevel logLevel = LogLevel.Information) => logger is not null && logger.IsEnabled(logLevel);
[Conditional("DEBUG")]
internal static void AssertDisposed(this SemaphoreSlim semaphore) => semaphore.AssertFieldNull("m_waitHandle");
Expand Down
2 changes: 2 additions & 0 deletions src/UiPath.CoreIpc/Helpers/Router.cs
Original file line number Diff line number Diff line change
Expand Up @@ -134,11 +134,13 @@ public static Route From(IServiceProvider? serviceProvider, ContractSettings end
BeforeCall = endpointSettings.BeforeIncomingCall,
Scheduler = endpointSettings.Scheduler.OrDefault(),
LoggerFactory = serviceProvider.MaybeCreateServiceFactory<ILoggerFactory>(),
OnError = endpointSettings.OnError,
};

public required ServiceFactory Service { get; init; }

public TaskScheduler Scheduler { get; init; }
public BeforeCallHandler? BeforeCall { get; init; }
public Func<ILoggerFactory>? LoggerFactory { get; init; }
public Func<CallInfo?, Exception, Exception>? OnError { get; init; }
}
3 changes: 3 additions & 0 deletions src/UiPath.CoreIpc/Server/ContractSettings.cs
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,13 @@ public sealed class ContractSettings
public TaskScheduler? Scheduler { get; set; }
public BeforeCallHandler? BeforeIncomingCall { get; set; }
internal ServiceFactory Service { get; }
public Func<CallInfo?, Exception, Exception>? OnError { get; set; }

internal Type ContractType => Service.Type;
internal object? ServiceInstance => Service.MaybeGetInstance();
internal IServiceProvider? ServiceProvider => Service.MaybeGetServiceProvider();


public ContractSettings(Type contractType, object? serviceInstance = null) : this(
serviceInstance is not null
? new ServiceFactory.Instance()
Expand Down Expand Up @@ -40,5 +42,6 @@ internal ContractSettings(ContractSettings other)
Scheduler = other.Scheduler;
BeforeIncomingCall = other.BeforeIncomingCall;
Service = other.Service;
OnError = other.OnError;
}
}
10 changes: 10 additions & 0 deletions src/UiPath.CoreIpc/Server/Server.cs
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,16 @@ private async ValueTask OnRequestReceived(Request request)
}
catch (Exception ex) when (response is null)
{
try
{
ex = route.OnError?.Invoke(null, ex) ?? ex;
}
catch (Exception handlerEx)
{
ex = new AggregateException(
$"Error while handling error for {request}.",
ex, handlerEx);
}
await OnError(request, timeoutHelper.CheckTimeout(ex, request.MethodName));
}
finally
Expand Down
46 changes: 39 additions & 7 deletions src/UiPath.CoreIpc/Wire/Dtos.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
using System.Diagnostics.CodeAnalysis;
using System.Text;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;

namespace UiPath.Ipc;

Expand Down Expand Up @@ -48,17 +49,40 @@ public TResult Deserialize<TResult>()
}
}

public record Error(string Message, string StackTrace, string Type, Error? InnerError)
public record Error(string Message, string StackTrace, string Type, Error? InnerError, IReadOnlyDictionary<string, object?>? Data)
{
public static readonly HashSet<string> SerializableDataKeys = ["UiPath.ErrorInfo.Error"];

[return: NotNullIfNotNull("exception")]
public static Error? FromException(Exception? exception)
=> exception is null
? null
=> exception is null
? null
: new(
Message: exception.Message,
StackTrace: exception.StackTrace ?? exception.GetBaseException().StackTrace!,
Type: GetExceptionType(exception),
InnerError: FromException(exception.InnerException));
Message: exception.Message,
StackTrace: exception.StackTrace ?? exception.GetBaseException().StackTrace!,
Type: GetExceptionType(exception),
InnerError: FromException(exception.InnerException),
Data: GetExceptionData(exception));

private static IReadOnlyDictionary<string, object?>? GetExceptionData(Exception exception)
{
Dictionary<string, object?>? data = null;
foreach (var key in SerializableDataKeys)
{
if (exception.Data.Contains(key))
{
data ??= [];
var value = exception.Data[key];
data[key] = value switch
{
null or string or int or bool or Int64 or double or decimal or float => value,
_ => JToken.FromObject(value, IpcJsonSerializer.StringArgsSerializer)
};
}
}
return data;
}

public override string ToString() => new RemoteException(this).ToString();

private static string GetExceptionType(Exception exception) => (exception as RemoteException)?.Type ?? exception.GetType().FullName!;
Expand All @@ -70,6 +94,14 @@ public class RemoteException : Exception
{
Type = error.Type;
StackTrace = error.StackTrace;
if (error.Data != null)
{
foreach (var key in error.Data)
{
var value = key.Value;
Data[key.Key] = value;
}
}
}
public string Type { get; }
public override string StackTrace { get; }
Expand Down
2 changes: 1 addition & 1 deletion src/UiPath.CoreIpc/Wire/IpcJsonSerializer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ internal class IpcJsonSerializer : IArrayPool<char>
{
public static readonly IpcJsonSerializer Instance = new();

static readonly JsonSerializer StringArgsSerializer = new() { CheckAdditionalContent = true };
internal static readonly JsonSerializer StringArgsSerializer = new() { CheckAdditionalContent = true };

#if !NET461
[AsyncMethodBuilder(typeof(PoolingAsyncValueTaskMethodBuilder<>))]
Expand Down