Skip to content
Merged
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
68 changes: 0 additions & 68 deletions src/fdc3/dotnet/DesktopAgent.Client/README.md

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -32,8 +32,8 @@ public class DesktopAgentClient : IDesktopAgent, IAsyncDisposable
private readonly IMessaging _messaging;
private readonly ILoggerFactory _loggerFactory;
private readonly ILogger<DesktopAgentClient> _logger;
private readonly string _appId;
private readonly string _instanceId;
private string _appId;
private string _instanceId;
private IChannelHandler _channelHandler;
private IMetadataClient _metadataClient;
private IIntentsClient _intentsClient;
Expand All @@ -50,33 +50,100 @@ public class DesktopAgentClient : IDesktopAgent, IAsyncDisposable

private readonly TaskCompletionSource<string> _initializationTaskCompletionSource = new(TaskCreationOptions.RunContinuationsAsynchronously);

/// <summary>
/// Constructor for creating an instance of DesktopAgentClient. It retrieves the app and instance identifiers from the environment variables, which are expected to be set for the application.
/// If the environment variables are not set, it will throw an exception indicating that the required identifiers are missing.
/// </summary>
/// <param name="messaging"></param>
/// <param name="onReady">Callback which signals that the desktop agent client is ready for the module. This can be handy to ensure that the client is fully initialized before the module starts using it for Fdc3 operations, preventing potential issues related to uninitialized state.</param>
/// <param name="loggerFactory"></param>
public DesktopAgentClient(
IMessaging messaging,
Action<IDesktopAgent>? onReady = null,
ILoggerFactory? loggerFactory = null)
{
_messaging = messaging;
_loggerFactory = loggerFactory ?? NullLoggerFactory.Instance;
_logger = _loggerFactory.CreateLogger<DesktopAgentClient>();

_appId = Environment.GetEnvironmentVariable(nameof(AppIdentifier.AppId)) ?? throw ThrowHelper.MissingAppId(string.Empty);
_appId = Environment.GetEnvironmentVariable(nameof(AppIdentifier.AppId)) ?? throw ThrowHelper.MissingAppId(string.Empty);

_instanceId = Environment.GetEnvironmentVariable(nameof(AppIdentifier.InstanceId)) ?? throw ThrowHelper.MissingInstanceId(_appId, string.Empty);

var channelId = Environment.GetEnvironmentVariable(Fdc3StartupParameters.Fdc3ChannelId);

var openedAppContextId = Environment.GetEnvironmentVariable(Fdc3StartupParameters.OpenedAppContextId);


if (_logger.IsEnabled(LogLevel.Debug))
{
_logger.LogDebug("AppID: {AppId}; InstanceId: {InstanceId} is registered for the FDC3 client app.", _appId, _instanceId);
}

if (_logger.IsEnabled(LogLevel.Debug))
{
_logger.LogDebug("Retrieved startup parameters for channelId: {ChannelId}; openedAppContextId: {OpenedAppContext}.", channelId ?? "null", openedAppContextId ?? "null");
}

_metadataClient = new MetadataClient(_appId, _instanceId, _messaging, _loggerFactory.CreateLogger<MetadataClient>());
_openClient = new OpenClient(_instanceId, _messaging, this, _loggerFactory.CreateLogger<OpenClient>());

_ = Task.Run(() => InitializeAsync().ConfigureAwait(false));
_ = Task.Run(() => InitializeAsync(channelId, openedAppContextId, onReady).ConfigureAwait(false));
}

/// <summary>
/// Constructor for creating an instance of DesktopAgentClient with the app and instance identifiers provided as arguments.
/// This can be useful in scenarios where the app and instance identifiers are not available as environment variables, or when you want to explicitly set them for testing or other purposes.
/// </summary>
/// <param name="messaging"></param>
/// <param name="appId">Fdc3App app id passed through the constructor.</param>
/// <param name="instanceId">Fdc3App insatnce id passed through the constructor.</param>
/// <param name="initialOpenAppContextId">The initial opened app context id which can be provided for the app opened through fdc3.open call. This is needed to properly handle the context for the app opened through fdc3.open.</param>
/// <param name="initialUserChannelId">The initial user channel id which can be provided for the app to automatically join to a user channel on initialization.</param>
/// <param name="onReady">Callback which signals that the desktop agent client is ready for the module. This can be handy to ensure that the client is fully initialized before the module starts using it for Fdc3 operations, preventing potential issues related to uninitialized state.</param>
/// <param name="loggerFactory"></param>
Comment thread
ZKRobi marked this conversation as resolved.
public DesktopAgentClient(
Comment thread
ZKRobi marked this conversation as resolved.
IMessaging messaging,
string appId,
string instanceId,
string? initialUserChannelId = null,
string? initialOpenAppContextId = null,
Action<IDesktopAgent>? onReady = null,
ILoggerFactory? loggerFactory = null)
{
_messaging = messaging;
_loggerFactory = loggerFactory ?? NullLoggerFactory.Instance;
_logger = _loggerFactory.CreateLogger<DesktopAgentClient>();

_appId = appId ?? throw ThrowHelper.MissingAppIdentifier(appId, instanceId);

_instanceId = instanceId ?? throw ThrowHelper.MissingAppIdentifier(appId, instanceId);

var channelId = initialUserChannelId;

var openedAppContextId = initialOpenAppContextId;

if (_logger.IsEnabled(LogLevel.Debug))
{
_logger.LogDebug("AppID: {AppId}; InstanceId: {InstanceId} is registered for the FDC3 client app.", _appId, _instanceId);
}

if (_logger.IsEnabled(LogLevel.Debug))
{
_logger.LogDebug("Retrieved startup parameters for channelId: {ChannelId}; openedAppContextId: {OpenedAppContext}.", channelId ?? "null", openedAppContextId ?? "null");
}

_metadataClient = new MetadataClient(_appId, _instanceId, _messaging, _loggerFactory.CreateLogger<MetadataClient>());
_openClient = new OpenClient(_instanceId, _messaging, this, _loggerFactory.CreateLogger<OpenClient>());

_ = Task.Run(() => InitializeAsync(channelId, openedAppContextId, onReady).ConfigureAwait(false));
}

/// <summary>
/// Joins to a user channel if the channel id is initially defined for the app and requests to backend to return the context if the app was opened through fdc3.open call.
/// </summary>
/// <returns></returns>
private async Task InitializeAsync()
private async Task InitializeAsync(string? channelId, string? openedAppContextId, Action<IDesktopAgent>? onReady = null)
{
try
{
Expand All @@ -85,19 +152,6 @@ private async Task InitializeAsync()
_logger.LogDebug("Initializing DesktopAgentClient...");
}

var channelId = Environment.GetEnvironmentVariable(Fdc3StartupParameters.Fdc3ChannelId);
var openedAppContextId = Environment.GetEnvironmentVariable(Fdc3StartupParameters.OpenedAppContextId);

if (_logger.IsEnabled(LogLevel.Debug))
{
_logger.LogDebug("Retrieved startup parameters for channelId: {ChannelId}; openedAppContextId: {OpenedAppContext}.", channelId ?? "null", openedAppContextId ?? "null");
}

if (!string.IsNullOrEmpty(channelId))
{
await JoinUserChannel(channelId!).ConfigureAwait(false);
}

if (!string.IsNullOrEmpty(openedAppContextId))
{
await GetOpenedAppContextAsync(openedAppContextId!).ConfigureAwait(false);
Expand All @@ -106,14 +160,25 @@ private async Task InitializeAsync()
_channelHandler = new ChannelHandler(_messaging, _instanceId, this, _openedAppContext, _loggerFactory);
_intentsClient = new IntentsClient(_messaging, _channelHandler, _instanceId, _loggerFactory);

await _channelHandler.ConfigureChannelSelectorAsync(CancellationToken.None).ConfigureAwait(false);
await _channelHandler.ConfigureChannelSelectorAsync().ConfigureAwait(false);

_initializationTaskCompletionSource.SetResult(_instanceId);
}
catch (Exception exception)
{
_initializationTaskCompletionSource.TrySetException(exception);
}
finally
{
//It can cause deadlock to trigger the channel selector within the lock in the JoinUserChannelAsync, we trigger it here after the initialization is completed and the channel is joined.
if (!string.IsNullOrEmpty(channelId))
{
await JoinUserChannelAsync(channelId!).ConfigureAwait(false);
_ = _channelHandler.NotifyUserChannelChangedAsync(_currentChannel!).ConfigureAwait(false);
}

onReady?.Invoke(this);
}
}

/// <summary>
Expand Down Expand Up @@ -334,15 +399,22 @@ public async Task<IEnumerable<IChannel>> GetUserChannels()
public async Task JoinUserChannel(string channelId)
{
await _initializationTaskCompletionSource.Task.ConfigureAwait(false);
await JoinUserChannelAsync(channelId).ConfigureAwait(false);
await _channelHandler.NotifyUserChannelChangedAsync(_currentChannel!).ConfigureAwait(false);
}

private async Task JoinUserChannelAsync(string channelId)
{
try
{
await _currentChannelLock.WaitAsync().ConfigureAwait(false);

if (_currentChannel != null)
{
_logger.LogInformation("Leaving current channel: {CurrentChannelId}...", _currentChannel.Id);
await LeaveCurrentChannel().ConfigureAwait(false);

//If we call directly the LeaveCurrentChannel then we might encounter a deadlock
LeaveCurrentChannelWithoutLock();
}

var channel = await _channelHandler.JoinUserChannelAsync(channelId).ConfigureAwait(false);
Expand Down Expand Up @@ -376,30 +448,32 @@ public async Task LeaveCurrentChannel()
try
{
await _currentChannelLock.WaitAsync().ConfigureAwait(false);
var contextListeners = _contextListeners.Reverse();

//The context listeners, that have been added through the `fdc3.addContextListener()` should unsubscribe, but the context listeners should remain registered to the DesktopAgentClient instance.
foreach (var contextListener in contextListeners)
{
if (_logger.IsEnabled(LogLevel.Debug))
{
_logger.LogDebug("Unsubscribing context listener from channel: {CurrentChannelId}...", _currentChannel?.Id);
}

contextListener.Key.Unsubscribe();
}

if (_currentChannel != null)
{
_currentChannel = null;
}
LeaveCurrentChannelWithoutLock();
}
finally
{
_currentChannelLock.Release();
}
}

private void LeaveCurrentChannelWithoutLock()
{
var contextListeners = _contextListeners.Reverse();

//The context listeners, that have been added through the `fdc3.addContextListener()` should unsubscribe, but the context listeners should remain registered to the DesktopAgentClient instance.
foreach (var contextListener in contextListeners)
{
if (_logger.IsEnabled(LogLevel.Debug))
{
_logger.LogDebug("Unsubscribing context listener from channel: {CurrentChannelId}...", _currentChannel?.Id);
}

contextListener.Key.Unsubscribe();
}

_currentChannel = null;
}

/// <summary>
/// Opens the specified app.
/// </summary>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,15 @@ public ValueTask<ContextListener<T>> CreateContextListenerAsync<T>(
/// <returns>A <see cref="ValueTask{IChannel}"/> representing the asynchronous operation.</returns>
public ValueTask<IChannel> JoinUserChannelAsync(string channelId);

/// <summary>
/// Sends a notification message to the module that the user channel has been changed to the specified client that is injected by the module.
/// The module is responsible to subscribe to the topic and handle based on its UI logic.
/// The module could also use IModuleChannelSelector to pass the logic to update the UI action based on the update without knowing what kind of topic they should subscribe to.
/// </summary>
/// <param name="channel"></param>
/// <returns></returns>
public ValueTask NotifyUserChannelChangedAsync(IChannel channel);

/// <summary>
/// Retrieves all available user channels.
/// </summary>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,6 @@
using MorganStanley.ComposeUI.Fdc3.DesktopAgent.Shared.Contracts;
using MorganStanley.ComposeUI.Fdc3.DesktopAgent.Shared.Exceptions;
using MorganStanley.ComposeUI.Messaging.Abstractions;
using MorganStanley.ComposeUI.Messaging.Abstractions.Exceptions;

namespace MorganStanley.ComposeUI.Fdc3.DesktopAgent.Client.Infrastructure.Internal;

Expand Down Expand Up @@ -213,18 +212,21 @@ public async ValueTask<IChannel> JoinUserChannelAsync(string channelId)
displayMetadata: response.DisplayMetadata,
loggerFactory: _loggerFactory);

return channel;
}

public async ValueTask NotifyUserChannelChangedAsync(IChannel channel)
{
try
{
var result = await _messaging.InvokeServiceAsync(Fdc3Topic.ChannelSelectorFromAPI(_instanceId), channelId).ConfigureAwait(false);
var result = await _messaging.InvokeServiceAsync(Fdc3Topic.NotifyUserChannelChanged(_instanceId), channel.Id).ConfigureAwait(false);

_logger.LogDebug("Triggered channel selector from API for module: {InstanceId}, with {ChannelId}, and backend returned result: {Result}", _instanceId, channel.Id, result);
}
catch (Exception exception)
{
_logger.LogWarning(exception, "Failed to trigger channel selector from API for module: {InstanceId}, with {ChannelId}.", channelId, _instanceId);
_logger.LogWarning(exception, "Failed to trigger channel selector from API for module: {InstanceId}, with {ChannelId}.", channel.Id, _instanceId);
}

return channel;
}

public async ValueTask<IChannel> FindChannelAsync(string channelId, ChannelType channelType)
Expand Down Expand Up @@ -309,12 +311,12 @@ public async ValueTask<IPrivateChannel> CreatePrivateChannelAsync()
public async ValueTask ConfigureChannelSelectorAsync(CancellationToken cancellationToken = default)
{
_channelSelector = await _messaging.RegisterServiceAsync(
Fdc3Topic.ChannelSelectorFromUI(_instanceId),
ChannelSelectorFromUI,
Fdc3Topic.NotifyUserChannelChangedViaUserInteraction(_instanceId),
NotifyUserChannelChangedViaUserInteraction,
cancellationToken).ConfigureAwait(false);
}

private async ValueTask<string?> ChannelSelectorFromUI(string? request)
private async ValueTask<string?> NotifyUserChannelChangedViaUserInteraction(string? request)
{
if (string.IsNullOrEmpty(request))
{
Expand Down
Loading
Loading