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
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
@@ -0,0 +1,41 @@
/*
* Morgan Stanley makes this available to you under the Apache License,
* Version 2.0 (the "License"). You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0.
*
* See the NOTICE file distributed with this work for additional information
* regarding copyright ownership. Unless required by applicable law or agreed
* to in writing, software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
* or implied. See the License for the specific language governing permissions
* and limitations under the License.
*/

using Finos.Fdc3;
using MorganStanley.ComposeUI.Fdc3.DesktopAgent.Shared;

namespace MorganStanley.ComposeUI.Fdc3.DesktopAgent.Client;

/// <summary>
/// Shell specific factory which will be used by the Fdc3DesktopAgent to retrieve the IDesktopAgent implementation that will be used for Fdc3 integration. This is needed to abstract away how the DesktopAgentClient can be resolved.
/// </summary>
public interface IDesktopAgentClientFactory
{
/// <summary>
/// Returns the IDesktopAgent implementation that will be used by the Fdc3DesktopAgent for Fdc3 integration. The implementation of this factory should handle the resolution of the IDesktopAgent, whether it's through a service locator, dependency injection, or any other method. This allows the DesktopAgentClient to remain decoupled from the specifics of how the IDesktopAgent is provided, enabling greater flexibility and testability.
/// </summary>
/// <param name="identifier">Unique identifier of the module</param>
/// <param name="onReady">Callback which signals that the desktop agent client is ready for the module.</param>
/// <returns></returns>
public Task GetDesktopAgentAsync(string identifier, Action<IDesktopAgent> onReady);

/// <summary>
/// Registers the Fdc3StartupProperties for in-process apps, which can be used to provide necessary information about the app to the DesktopAgentClient for proper Fdc3 integration.
/// This helps to identify the different native modules and provides ability to create the DesktopAgent client which can be used for in process apps to collaborate in the FDC3 ecosystem.
/// The implementation of this method should handle the storage and management of the Fdc3StartupProperties for in-process apps, ensuring that the DesktopAgentClient can access this information when needed for Fdc3 operations.
/// This is particularly important for scenarios where multiple in-process apps are running and need to be distinguished from each other for proper Fdc3 functionality.
/// </summary>
/// <param name="fdc3Properties"></param>
public Task RegisterInProcessAppPropertiesAsync(Fdc3StartupProperties fdc3Properties);
}
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,65 @@ public class DesktopAgentClient : IDesktopAgent, IAsyncDisposable

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

/// <summary>
/// The app and instance identifiers can be provided through environment variables or by setting the relative arguments in the ctor.
/// </summary>
/// <param name="messaging"></param>
/// <param name="appId">Fdc3App app id</param>
/// <param name="instanceId">Fdc3App insatnce id</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>
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

string? initialUserChannelId = null,
string? initialOpenAppContextId = null,
Action? onReady = null
are missing from XML comment

public DesktopAgentClient(
IMessaging messaging,
string? appId = null,
string? instanceId = null,
string? initialUserChannelId = null,
string? initialOpenAppContextId = null,
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);
_instanceId = Environment.GetEnvironmentVariable(nameof(AppIdentifier.InstanceId)) ?? throw ThrowHelper.MissingInstanceId(_appId, string.Empty);
_appId = Environment.GetEnvironmentVariable(nameof(AppIdentifier.AppId))
?? appId
?? throw ThrowHelper.MissingAppIdentifier(appId, instanceId);

_instanceId = Environment.GetEnvironmentVariable(nameof(AppIdentifier.InstanceId))
?? instanceId
?? throw ThrowHelper.MissingAppIdentifier(appId, instanceId);

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

var openedAppContextId = Environment.GetEnvironmentVariable(Fdc3StartupParameters.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().ConfigureAwait(false));
_ = 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 +117,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 +125,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.TriggerChannelSelectorAsync(_currentChannel!).ConfigureAwait(false);
}

onReady?.Invoke(this);
}
}

/// <summary>
Expand Down Expand Up @@ -334,15 +364,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.TriggerChannelSelectorAsync(_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 +413,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,13 @@ public ValueTask<ContextListener<T>> CreateContextListenerAsync<T>(
/// <returns>A <see cref="ValueTask{IChannel}"/> representing the asynchronous operation.</returns>
public ValueTask<IChannel> JoinUserChannelAsync(string channelId);

/// <summary>
/// Triggers channel selector for the current instance.
/// </summary>
/// <param name="channel"></param>
/// <returns></returns>
public ValueTask TriggerChannelSelectorAsync(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 TriggerChannelSelectorAsync(IChannel channel)
{
try
{
var result = await _messaging.InvokeServiceAsync(Fdc3Topic.ChannelSelectorFromAPI(_instanceId), channelId).ConfigureAwait(false);
var result = await _messaging.InvokeServiceAsync(Fdc3Topic.ChannelSelectorFromAPI(_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
Loading
Loading