2024-08-05 00:36:26 +02:00
using System ;
using System.Collections.Generic ;
using System.Text.Json ;
using System.Threading ;
using System.Threading.Tasks ;
using CommunityToolkit.Mvvm.Messaging ;
using Nito.AsyncEx ;
using Serilog ;
using Windows.ApplicationModel ;
using Windows.ApplicationModel.AppService ;
using Windows.Foundation.Collections ;
using Windows.Foundation.Metadata ;
using Wino.Core.Domain.Enums ;
using Wino.Core.Domain.Interfaces ;
using Wino.Core.Domain.Models.Requests ;
using Wino.Core.Domain.Models.Server ;
using Wino.Core.Integration.Json ;
using Wino.Messaging ;
using Wino.Messaging.Client.Connection ;
using Wino.Messaging.Enums ;
2025-02-15 12:53:32 +01:00
using Wino.Messaging.Server ;
2024-08-05 00:36:26 +02:00
using Wino.Messaging.UI ;
namespace Wino.Core.UWP.Services
{
public class WinoServerConnectionManager :
IWinoServerConnectionManager < AppServiceConnection > ,
2024-08-09 14:23:51 +02:00
IRecipient < WinoServerConnectionEstablished >
2024-08-05 00:36:26 +02:00
{
2024-08-11 15:25:40 +02:00
private const int ServerConnectionTimeoutMs = 10000 ;
2024-08-05 00:36:26 +02:00
public event EventHandler < WinoServerConnectionStatus > StatusChanged ;
2024-08-11 15:25:40 +02:00
public TaskCompletionSource < bool > ConnectingHandle { get ; private set ; }
2024-08-05 00:36:26 +02:00
private ILogger Logger = > Logger . ForContext < WinoServerConnectionManager > ( ) ;
private WinoServerConnectionStatus status ;
public WinoServerConnectionStatus Status
{
get { return status ; }
private set
{
2024-08-11 15:25:40 +02:00
Log . Information ( "Server connection status changed to {Status}." , value ) ;
2024-08-05 00:36:26 +02:00
status = value ;
StatusChanged ? . Invoke ( this , value ) ;
}
}
private AppServiceConnection _connection ;
public AppServiceConnection Connection
{
get { return _connection ; }
set
{
if ( _connection ! = null )
{
_connection . RequestReceived - = ServerMessageReceived ;
_connection . ServiceClosed - = ServerDisconnected ;
}
_connection = value ;
if ( value = = null )
{
Status = WinoServerConnectionStatus . Disconnected ;
}
else
{
value . RequestReceived + = ServerMessageReceived ;
value . ServiceClosed + = ServerDisconnected ;
Status = WinoServerConnectionStatus . Connected ;
}
}
}
private readonly JsonSerializerOptions _jsonSerializerOptions = new ( )
{
TypeInfoResolver = new ServerRequestTypeInfoResolver ( )
} ;
public WinoServerConnectionManager ( )
{
WeakReferenceMessenger . Default . Register ( this ) ;
}
public async Task < bool > ConnectAsync ( )
{
2024-08-11 15:25:40 +02:00
if ( Status = = WinoServerConnectionStatus . Connected )
{
Log . Information ( "Server is already connected." ) ;
return true ;
}
if ( Status = = WinoServerConnectionStatus . Connecting )
{
// A connection is already being established at the moment.
// No need to run another connection establishment process.
// Await the connecting handler if possible.
if ( ConnectingHandle ! = null )
{
return await ConnectingHandle . Task ;
}
}
2024-08-05 00:36:26 +02:00
if ( ApiInformation . IsApiContractPresent ( "Windows.ApplicationModel.FullTrustAppContract" , 1 , 0 ) )
{
try
{
2024-08-13 16:12:34 +02:00
ConnectingHandle = new TaskCompletionSource < bool > ( ) ;
2024-08-05 00:36:26 +02:00
Status = WinoServerConnectionStatus . Connecting ;
2024-08-13 16:12:34 +02:00
var connectionCancellationToken = new CancellationTokenSource ( TimeSpan . FromMilliseconds ( ServerConnectionTimeoutMs ) ) ;
2024-11-11 13:56:56 +01:00
await FullTrustProcessLauncher . LaunchFullTrustProcessForCurrentAppAsync ( "WinoServer" ) ;
2024-08-05 00:36:26 +02:00
// Connection establishment handler is in App.xaml.cs OnBackgroundActivated.
// Once the connection is established, the handler will set the Connection property
2024-08-09 14:23:51 +02:00
// and WinoServerConnectionEstablished will be fired by the messenger.
2024-08-05 00:36:26 +02:00
2024-08-11 15:25:40 +02:00
await ConnectingHandle . Task . WaitAsync ( connectionCancellationToken . Token ) ;
Log . Information ( "Server connection established successfully." ) ;
2024-08-05 00:36:26 +02:00
}
2024-08-13 16:12:34 +02:00
catch ( OperationCanceledException canceledException )
{
Log . Error ( canceledException , $"Server process did not start in {ServerConnectionTimeoutMs} ms. Operation is canceled." ) ;
ConnectingHandle ? . TrySetException ( canceledException ) ;
2024-08-13 16:14:25 +02:00
Status = WinoServerConnectionStatus . Failed ;
2024-08-13 16:12:34 +02:00
return false ;
}
2024-08-11 15:25:40 +02:00
catch ( Exception ex )
2024-08-05 00:36:26 +02:00
{
2024-08-11 15:25:40 +02:00
Log . Error ( ex , "Failed to connect to the server." ) ;
2024-08-13 16:14:25 +02:00
ConnectingHandle ? . TrySetException ( ex ) ;
2024-08-05 00:36:26 +02:00
Status = WinoServerConnectionStatus . Failed ;
return false ;
}
return true ;
}
2024-08-11 15:25:40 +02:00
else
{
Log . Information ( "FullTrustAppContract is not present in the system. Server connection is not possible." ) ;
}
2024-08-05 00:36:26 +02:00
return false ;
}
public async Task InitializeAsync ( )
{
var isConnectionSuccessfull = await ConnectAsync ( ) ;
2024-08-11 15:25:40 +02:00
if ( isConnectionSuccessfull )
{
Log . Information ( "ServerConnectionManager initialized successfully." ) ;
}
else
{
Log . Error ( "ServerConnectionManager initialization failed." ) ;
}
2024-08-05 00:36:26 +02:00
}
private void ServerMessageReceived ( AppServiceConnection sender , AppServiceRequestReceivedEventArgs args )
{
if ( args . Request . Message . TryGetValue ( MessageConstants . MessageTypeKey , out object messageTypeObject ) & & messageTypeObject is int messageTypeInt )
{
var messageType = ( MessageType ) messageTypeInt ;
if ( args . Request . Message . TryGetValue ( MessageConstants . MessageDataKey , out object messageDataObject ) & & messageDataObject is string messageJson )
{
switch ( messageType )
{
case MessageType . UIMessage :
if ( ! args . Request . Message . TryGetValue ( MessageConstants . MessageDataTypeKey , out object dataTypeObject ) | | dataTypeObject is not string dataTypeName )
throw new ArgumentException ( "Message data type is missing." ) ;
HandleUIMessage ( messageJson , dataTypeName ) ;
break ;
default :
break ;
}
}
}
}
/// <summary>
/// Unpacks IServerMessage objects and delegate it to Messenger for UI to process.
/// </summary>
/// <param name="messageJson">Message data in json format.</param>
private void HandleUIMessage ( string messageJson , string typeName )
{
switch ( typeName )
{
case nameof ( MailAddedMessage ) :
2025-02-14 01:43:52 +01:00
WeakReferenceMessenger . Default . Send ( JsonSerializer . Deserialize ( messageJson , CommunicationMessagesContext . Default . MailAddedMessage ) ) ;
2024-08-05 00:36:26 +02:00
break ;
case nameof ( MailDownloadedMessage ) :
2025-02-14 01:43:52 +01:00
WeakReferenceMessenger . Default . Send ( JsonSerializer . Deserialize ( messageJson , CommunicationMessagesContext . Default . MailDownloadedMessage ) ) ;
2024-08-05 00:36:26 +02:00
break ;
case nameof ( MailRemovedMessage ) :
2025-02-14 01:43:52 +01:00
WeakReferenceMessenger . Default . Send ( JsonSerializer . Deserialize ( messageJson , CommunicationMessagesContext . Default . MailRemovedMessage ) ) ;
2024-08-05 00:36:26 +02:00
break ;
case nameof ( MailUpdatedMessage ) :
2025-02-14 01:43:52 +01:00
WeakReferenceMessenger . Default . Send ( JsonSerializer . Deserialize ( messageJson , CommunicationMessagesContext . Default . MailUpdatedMessage ) ) ;
2024-08-05 00:36:26 +02:00
break ;
case nameof ( AccountCreatedMessage ) :
2025-02-14 01:43:52 +01:00
WeakReferenceMessenger . Default . Send ( JsonSerializer . Deserialize ( messageJson , CommunicationMessagesContext . Default . AccountCreatedMessage ) ) ;
2024-08-05 00:36:26 +02:00
break ;
case nameof ( AccountRemovedMessage ) :
2025-02-14 01:43:52 +01:00
WeakReferenceMessenger . Default . Send ( JsonSerializer . Deserialize ( messageJson , CommunicationMessagesContext . Default . AccountRemovedMessage ) ) ;
2024-08-05 00:36:26 +02:00
break ;
case nameof ( AccountUpdatedMessage ) :
2025-02-14 01:43:52 +01:00
WeakReferenceMessenger . Default . Send ( JsonSerializer . Deserialize ( messageJson , CommunicationMessagesContext . Default . AccountUpdatedMessage ) ) ;
2024-08-05 00:36:26 +02:00
break ;
case nameof ( DraftCreated ) :
2025-02-14 01:43:52 +01:00
WeakReferenceMessenger . Default . Send ( JsonSerializer . Deserialize ( messageJson , CommunicationMessagesContext . Default . DraftCreated ) ) ;
2024-08-05 00:36:26 +02:00
break ;
case nameof ( DraftFailed ) :
2025-02-14 01:43:52 +01:00
WeakReferenceMessenger . Default . Send ( JsonSerializer . Deserialize ( messageJson , CommunicationMessagesContext . Default . DraftFailed ) ) ;
2024-08-05 00:36:26 +02:00
break ;
case nameof ( DraftMapped ) :
2025-02-14 01:43:52 +01:00
WeakReferenceMessenger . Default . Send ( JsonSerializer . Deserialize ( messageJson , CommunicationMessagesContext . Default . DraftMapped ) ) ;
2024-08-05 00:36:26 +02:00
break ;
case nameof ( FolderRenamed ) :
2025-02-14 01:43:52 +01:00
WeakReferenceMessenger . Default . Send ( JsonSerializer . Deserialize ( messageJson , CommunicationMessagesContext . Default . FolderRenamed ) ) ;
2024-08-05 00:36:26 +02:00
break ;
case nameof ( FolderSynchronizationEnabled ) :
2025-02-14 01:43:52 +01:00
WeakReferenceMessenger . Default . Send ( JsonSerializer . Deserialize ( messageJson , CommunicationMessagesContext . Default . FolderSynchronizationEnabled ) ) ;
2024-08-05 00:36:26 +02:00
break ;
case nameof ( MergedInboxRenamed ) :
2025-02-14 01:43:52 +01:00
WeakReferenceMessenger . Default . Send ( JsonSerializer . Deserialize ( messageJson , CommunicationMessagesContext . Default . MergedInboxRenamed ) ) ;
2024-08-05 00:36:26 +02:00
break ;
case nameof ( AccountSynchronizationCompleted ) :
2025-02-14 01:43:52 +01:00
WeakReferenceMessenger . Default . Send ( JsonSerializer . Deserialize ( messageJson , CommunicationMessagesContext . Default . AccountSynchronizationCompleted ) ) ;
2024-08-05 00:36:26 +02:00
break ;
case nameof ( RefreshUnreadCountsMessage ) :
2025-02-14 01:43:52 +01:00
WeakReferenceMessenger . Default . Send ( JsonSerializer . Deserialize ( messageJson , CommunicationMessagesContext . Default . RefreshUnreadCountsMessage ) ) ;
2024-08-05 00:36:26 +02:00
break ;
case nameof ( AccountSynchronizerStateChanged ) :
2025-02-14 01:43:52 +01:00
WeakReferenceMessenger . Default . Send ( JsonSerializer . Deserialize ( messageJson , CommunicationMessagesContext . Default . AccountSynchronizerStateChanged ) ) ;
2024-08-05 00:36:26 +02:00
break ;
case nameof ( AccountSynchronizationProgressUpdatedMessage ) :
2025-02-14 01:43:52 +01:00
WeakReferenceMessenger . Default . Send ( JsonSerializer . Deserialize ( messageJson , CommunicationMessagesContext . Default . AccountSynchronizationProgressUpdatedMessage ) ) ;
2024-08-05 00:36:26 +02:00
break ;
2024-08-24 17:22:47 +02:00
case nameof ( AccountFolderConfigurationUpdated ) :
2025-02-14 01:43:52 +01:00
WeakReferenceMessenger . Default . Send ( JsonSerializer . Deserialize ( messageJson , CommunicationMessagesContext . Default . AccountFolderConfigurationUpdated ) ) ;
2024-08-24 17:22:47 +02:00
break ;
2024-09-14 01:17:03 +02:00
case nameof ( CopyAuthURLRequested ) :
2025-02-14 01:43:52 +01:00
WeakReferenceMessenger . Default . Send ( JsonSerializer . Deserialize ( messageJson , CommunicationMessagesContext . Default . CopyAuthURLRequested ) ) ;
2024-09-14 01:17:03 +02:00
break ;
2025-02-15 12:53:32 +01:00
case nameof ( NewMailSynchronizationRequested ) :
WeakReferenceMessenger . Default . Send ( JsonSerializer . Deserialize < NewMailSynchronizationRequested > ( messageJson ) ) ;
break ;
2024-08-05 00:36:26 +02:00
default :
throw new Exception ( "Invalid data type name passed to client." ) ;
}
}
private void ServerDisconnected ( AppServiceConnection sender , AppServiceClosedEventArgs args )
{
2024-08-11 15:25:40 +02:00
Log . Information ( "Server disconnected." ) ;
2024-08-05 00:36:26 +02:00
}
public async Task QueueRequestAsync ( IRequestBase request , Guid accountId )
{
var queuePackage = new ServerRequestPackage ( accountId , request ) ;
var queueResponse = await GetResponseInternalAsync < bool , ServerRequestPackage > ( queuePackage , new Dictionary < string , object > ( )
{
{ MessageConstants . MessageDataRequestAccountIdKey , accountId }
} ) ;
queueResponse . ThrowIfFailed ( ) ;
}
2024-09-13 02:51:37 +02:00
public Task < WinoServerResponse < TResponse > > GetResponseAsync < TResponse , TRequestType > ( TRequestType message , CancellationToken cancellationToken = default ) where TRequestType : IClientMessage
= > GetResponseInternalAsync < TResponse , TRequestType > ( message , cancellationToken : cancellationToken ) ;
2024-08-05 00:36:26 +02:00
2024-09-13 02:51:37 +02:00
private async Task < WinoServerResponse < TResponse > > GetResponseInternalAsync < TResponse , TRequestType > ( TRequestType message ,
Dictionary < string , object > parameters = null ,
CancellationToken cancellationToken = default )
2024-08-05 00:36:26 +02:00
{
2024-08-11 15:25:40 +02:00
if ( Status ! = WinoServerConnectionStatus . Connected )
await ConnectAsync ( ) ;
2024-08-05 00:36:26 +02:00
2024-11-11 01:09:05 +01:00
if ( Connection = = null ) return WinoServerResponse < TResponse > . CreateErrorResponse ( "Server connection is not established." ) ;
2024-08-05 00:36:26 +02:00
string serializedMessage = string . Empty ;
try
{
serializedMessage = JsonSerializer . Serialize ( message , _jsonSerializerOptions ) ;
}
catch ( Exception serializationException )
{
Logger . Error ( serializationException , $"Failed to serialize client message for sending." ) ;
return WinoServerResponse < TResponse > . CreateErrorResponse ( $"Failed to serialize message.\n{serializationException.Message}" ) ;
}
AppServiceResponse response = null ;
try
{
var valueSet = new ValueSet
{
{ MessageConstants . MessageTypeKey , ( int ) MessageType . ServerMessage } ,
{ MessageConstants . MessageDataKey , serializedMessage } ,
{ MessageConstants . MessageDataTypeKey , message . GetType ( ) . Name }
} ;
// Add additional parameters into ValueSet
if ( parameters ! = null )
{
foreach ( var item in parameters )
{
valueSet . Add ( item . Key , item . Value ) ;
}
}
2024-09-13 02:51:37 +02:00
response = await Connection . SendMessageAsync ( valueSet ) . AsTask ( cancellationToken ) ;
}
catch ( OperationCanceledException )
{
return WinoServerResponse < TResponse > . CreateErrorResponse ( $"Request is canceled by client." ) ;
2024-08-05 00:36:26 +02:00
}
catch ( Exception serverSendException )
{
Logger . Error ( serverSendException , $"Failed to send message to server." ) ;
return WinoServerResponse < TResponse > . CreateErrorResponse ( $"Failed to send message to server.\n{serverSendException.Message}" ) ;
}
// It should be always Success.
if ( response . Status ! = AppServiceResponseStatus . Success )
return WinoServerResponse < TResponse > . CreateErrorResponse ( $"Wino Server responded with '{response.Status}' status to message delivery." ) ;
// All responses must contain a message data.
if ( ! ( response . Message . TryGetValue ( MessageConstants . MessageDataKey , out object messageDataObject ) & & messageDataObject is string messageJson ) )
return WinoServerResponse < TResponse > . CreateErrorResponse ( "Server response did not contain message data." ) ;
// Try deserialize the message data.
try
{
return JsonSerializer . Deserialize < WinoServerResponse < TResponse > > ( messageJson ) ;
}
catch ( Exception jsonDeserializationError )
{
Logger . Error ( jsonDeserializationError , $"Failed to deserialize server response message data." ) ;
return WinoServerResponse < TResponse > . CreateErrorResponse ( $"Failed to deserialize Wino server response message data.\n{jsonDeserializationError.Message}" ) ;
}
}
2024-08-09 14:23:51 +02:00
public void Receive ( WinoServerConnectionEstablished message )
2024-08-11 15:25:40 +02:00
= > ConnectingHandle ? . TrySetResult ( true ) ;
2024-08-05 00:36:26 +02:00
}
}