2024-08-05 00:36:26 +02:00
using System ;
using System.Collections.Generic ;
2025-05-18 14:06:25 +02:00
using System.Diagnostics.CodeAnalysis ;
2024-08-05 00:36:26 +02:00
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 ;
2025-02-16 11:54:23 +01:00
namespace Wino.Core.UWP.Services ;
public class WinoServerConnectionManager :
IWinoServerConnectionManager < AppServiceConnection > ,
IRecipient < WinoServerConnectionEstablished >
2024-08-05 00:36:26 +02:00
{
2025-02-16 11:54:23 +01:00
private const int ServerConnectionTimeoutMs = 10000 ;
2024-08-05 00:36:26 +02:00
2025-02-16 11:54:23 +01:00
public event EventHandler < WinoServerConnectionStatus > StatusChanged ;
2024-08-11 15:25:40 +02:00
2025-02-16 11:54:23 +01:00
public TaskCompletionSource < bool > ConnectingHandle { get ; private set ; }
2024-08-05 00:36:26 +02:00
2025-02-16 11:54:23 +01:00
private ILogger Logger = > Logger . ForContext < WinoServerConnectionManager > ( ) ;
2024-08-05 00:36:26 +02:00
2025-02-16 11:54:23 +01:00
private WinoServerConnectionStatus status ;
2024-08-05 00:36:26 +02:00
2025-02-16 11:54:23 +01:00
public WinoServerConnectionStatus Status
{
get { return status ; }
private set
2024-08-05 00:36:26 +02:00
{
2025-02-16 11:54:23 +01:00
Log . Information ( "Server connection status changed to {Status}." , value ) ;
status = value ;
StatusChanged ? . Invoke ( this , value ) ;
2024-08-05 00:36:26 +02:00
}
2025-02-16 11:54:23 +01:00
}
2024-08-05 00:36:26 +02:00
2025-02-16 11:54:23 +01:00
private AppServiceConnection _connection ;
public AppServiceConnection Connection
{
get { return _connection ; }
set
2024-08-05 00:36:26 +02:00
{
2025-02-16 11:54:23 +01:00
if ( _connection ! = null )
2024-08-05 00:36:26 +02:00
{
2025-02-16 11:54:23 +01:00
_connection . RequestReceived - = ServerMessageReceived ;
_connection . ServiceClosed - = ServerDisconnected ;
}
2024-08-05 00:36:26 +02:00
2025-02-16 11:54:23 +01:00
_connection = value ;
2024-08-05 00:36:26 +02:00
2025-02-16 11:54:23 +01:00
if ( value = = null )
{
Status = WinoServerConnectionStatus . Disconnected ;
}
else
{
value . RequestReceived + = ServerMessageReceived ;
value . ServiceClosed + = ServerDisconnected ;
2024-08-05 00:36:26 +02:00
2025-02-16 11:54:23 +01:00
Status = WinoServerConnectionStatus . Connected ;
2024-08-05 00:36:26 +02:00
}
}
2025-02-16 11:54:23 +01:00
}
2024-08-05 00:36:26 +02:00
2025-02-16 11:54:23 +01:00
private readonly JsonSerializerOptions _jsonSerializerOptions = new ( )
{
TypeInfoResolver = new ServerRequestTypeInfoResolver ( )
} ;
public WinoServerConnectionManager ( )
{
WeakReferenceMessenger . Default . Register ( this ) ;
}
2024-08-05 00:36:26 +02:00
2025-02-16 11:54:23 +01:00
public async Task < bool > ConnectAsync ( )
{
if ( Status = = WinoServerConnectionStatus . Connected )
2024-08-05 00:36:26 +02:00
{
2025-02-16 11:54:23 +01:00
Log . Information ( "Server is already connected." ) ;
return true ;
2024-08-05 00:36:26 +02:00
}
2025-02-16 11:54:23 +01:00
if ( Status = = WinoServerConnectionStatus . Connecting )
2024-08-05 00:36:26 +02:00
{
2025-02-16 11:54:23 +01:00
// A connection is already being established at the moment.
// No need to run another connection establishment process.
// Await the connecting handler if possible.
2024-08-05 00:36:26 +02:00
2025-02-16 11:54:23 +01:00
if ( ConnectingHandle ! = null )
2024-08-05 00:36:26 +02:00
{
2025-02-16 11:54:23 +01:00
return await ConnectingHandle . Task ;
2025-02-16 11:43:30 +01:00
}
2025-02-16 11:54:23 +01:00
}
2024-08-13 16:12:34 +02:00
2025-02-16 11:54:23 +01:00
if ( ApiInformation . IsApiContractPresent ( "Windows.ApplicationModel.FullTrustAppContract" , 1 , 0 ) )
{
try
2025-02-16 11:43:30 +01:00
{
2025-02-16 11:54:23 +01:00
ConnectingHandle = new TaskCompletionSource < bool > ( ) ;
2024-08-05 00:36:26 +02:00
2025-02-16 11:54:23 +01:00
Status = WinoServerConnectionStatus . Connecting ;
2024-08-11 15:25:40 +02:00
2025-02-16 11:54:23 +01:00
var connectionCancellationToken = new CancellationTokenSource ( TimeSpan . FromMilliseconds ( ServerConnectionTimeoutMs ) ) ;
2024-08-13 16:12:34 +02:00
2025-02-16 11:54:23 +01:00
await FullTrustProcessLauncher . LaunchFullTrustProcessForCurrentAppAsync ( "WinoServer" ) ;
2024-08-13 16:14:25 +02:00
2025-02-16 11:54:23 +01:00
// Connection establishment handler is in App.xaml.cs OnBackgroundActivated.
// Once the connection is established, the handler will set the Connection property
// and WinoServerConnectionEstablished will be fired by the messenger.
2024-08-05 00:36:26 +02:00
2025-02-16 11:54:23 +01:00
await ConnectingHandle . Task . WaitAsync ( connectionCancellationToken . Token ) ;
2025-02-16 11:35:43 +01:00
2025-02-16 11:54:23 +01:00
Log . Information ( "Server connection established successfully." ) ;
}
catch ( OperationCanceledException canceledException )
{
Log . Error ( canceledException , $"Server process did not start in {ServerConnectionTimeoutMs} ms. Operation is canceled." ) ;
2024-08-05 00:36:26 +02:00
2025-02-16 11:54:23 +01:00
ConnectingHandle ? . TrySetException ( canceledException ) ;
2025-02-16 11:35:43 +01:00
2025-02-16 11:54:23 +01:00
Status = WinoServerConnectionStatus . Failed ;
return false ;
2025-02-16 11:43:30 +01:00
}
2025-02-16 11:54:23 +01:00
catch ( Exception ex )
2025-02-16 11:43:30 +01:00
{
2025-02-16 11:54:23 +01:00
Log . Error ( ex , "Failed to connect to the server." ) ;
ConnectingHandle ? . TrySetException ( ex ) ;
Status = WinoServerConnectionStatus . Failed ;
return false ;
2025-02-16 11:43:30 +01:00
}
2025-02-16 11:35:43 +01:00
2025-02-16 11:54:23 +01:00
return true ;
2025-02-16 11:35:43 +01:00
}
2025-02-16 11:54:23 +01:00
else
2025-02-16 11:35:43 +01:00
{
2025-02-16 11:54:23 +01:00
Log . Information ( "FullTrustAppContract is not present in the system. Server connection is not possible." ) ;
}
2025-02-16 11:43:30 +01:00
2025-02-16 11:54:23 +01:00
return false ;
}
public async Task InitializeAsync ( )
{
var isConnectionSuccessfull = await ConnectAsync ( ) ;
if ( isConnectionSuccessfull )
{
Log . Information ( "ServerConnectionManager initialized successfully." ) ;
}
else
{
Log . Error ( "ServerConnectionManager initialization failed." ) ;
2024-08-05 00:36:26 +02:00
}
2025-02-16 11:54:23 +01:00
}
2024-08-05 00:36:26 +02:00
2025-02-16 11:54:23 +01:00
private void ServerMessageReceived ( AppServiceConnection sender , AppServiceRequestReceivedEventArgs args )
{
if ( args . Request . Message . TryGetValue ( MessageConstants . MessageTypeKey , out object messageTypeObject ) & & messageTypeObject is int messageTypeInt )
2024-08-05 00:36:26 +02:00
{
2025-02-16 11:54:23 +01:00
var messageType = ( MessageType ) messageTypeInt ;
2025-02-16 11:43:30 +01:00
2025-02-16 11:54:23 +01:00
if ( args . Request . Message . TryGetValue ( MessageConstants . MessageDataKey , out object messageDataObject ) & & messageDataObject is string messageJson )
{
switch ( messageType )
2024-08-05 00:36:26 +02:00
{
2025-02-16 11:54:23 +01:00
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 ;
2024-08-05 00:36:26 +02:00
}
}
}
2025-02-16 11:54:23 +01:00
}
2024-08-05 00:36:26 +02:00
2025-02-16 11:54:23 +01:00
/// <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 )
2024-08-05 00:36:26 +02:00
{
2025-02-16 11:54:23 +01:00
case nameof ( MailAddedMessage ) :
WeakReferenceMessenger . Default . Send ( JsonSerializer . Deserialize ( messageJson , CommunicationMessagesContext . Default . MailAddedMessage ) ) ;
break ;
case nameof ( MailDownloadedMessage ) :
WeakReferenceMessenger . Default . Send ( JsonSerializer . Deserialize ( messageJson , CommunicationMessagesContext . Default . MailDownloadedMessage ) ) ;
break ;
case nameof ( MailRemovedMessage ) :
WeakReferenceMessenger . Default . Send ( JsonSerializer . Deserialize ( messageJson , CommunicationMessagesContext . Default . MailRemovedMessage ) ) ;
break ;
case nameof ( MailUpdatedMessage ) :
WeakReferenceMessenger . Default . Send ( JsonSerializer . Deserialize ( messageJson , CommunicationMessagesContext . Default . MailUpdatedMessage ) ) ;
break ;
case nameof ( AccountCreatedMessage ) :
WeakReferenceMessenger . Default . Send ( JsonSerializer . Deserialize ( messageJson , CommunicationMessagesContext . Default . AccountCreatedMessage ) ) ;
break ;
case nameof ( AccountRemovedMessage ) :
WeakReferenceMessenger . Default . Send ( JsonSerializer . Deserialize ( messageJson , CommunicationMessagesContext . Default . AccountRemovedMessage ) ) ;
break ;
case nameof ( AccountUpdatedMessage ) :
WeakReferenceMessenger . Default . Send ( JsonSerializer . Deserialize ( messageJson , CommunicationMessagesContext . Default . AccountUpdatedMessage ) ) ;
break ;
case nameof ( DraftCreated ) :
WeakReferenceMessenger . Default . Send ( JsonSerializer . Deserialize ( messageJson , CommunicationMessagesContext . Default . DraftCreated ) ) ;
break ;
case nameof ( DraftFailed ) :
WeakReferenceMessenger . Default . Send ( JsonSerializer . Deserialize ( messageJson , CommunicationMessagesContext . Default . DraftFailed ) ) ;
break ;
case nameof ( DraftMapped ) :
WeakReferenceMessenger . Default . Send ( JsonSerializer . Deserialize ( messageJson , CommunicationMessagesContext . Default . DraftMapped ) ) ;
break ;
case nameof ( FolderRenamed ) :
WeakReferenceMessenger . Default . Send ( JsonSerializer . Deserialize ( messageJson , CommunicationMessagesContext . Default . FolderRenamed ) ) ;
break ;
case nameof ( FolderSynchronizationEnabled ) :
WeakReferenceMessenger . Default . Send ( JsonSerializer . Deserialize ( messageJson , CommunicationMessagesContext . Default . FolderSynchronizationEnabled ) ) ;
break ;
case nameof ( MergedInboxRenamed ) :
WeakReferenceMessenger . Default . Send ( JsonSerializer . Deserialize ( messageJson , CommunicationMessagesContext . Default . MergedInboxRenamed ) ) ;
break ;
case nameof ( AccountSynchronizationCompleted ) :
WeakReferenceMessenger . Default . Send ( JsonSerializer . Deserialize ( messageJson , CommunicationMessagesContext . Default . AccountSynchronizationCompleted ) ) ;
break ;
case nameof ( RefreshUnreadCountsMessage ) :
WeakReferenceMessenger . Default . Send ( JsonSerializer . Deserialize ( messageJson , CommunicationMessagesContext . Default . RefreshUnreadCountsMessage ) ) ;
break ;
case nameof ( AccountSynchronizerStateChanged ) :
WeakReferenceMessenger . Default . Send ( JsonSerializer . Deserialize ( messageJson , CommunicationMessagesContext . Default . AccountSynchronizerStateChanged ) ) ;
break ;
case nameof ( AccountSynchronizationProgressUpdatedMessage ) :
WeakReferenceMessenger . Default . Send ( JsonSerializer . Deserialize ( messageJson , CommunicationMessagesContext . Default . AccountSynchronizationProgressUpdatedMessage ) ) ;
break ;
case nameof ( AccountFolderConfigurationUpdated ) :
WeakReferenceMessenger . Default . Send ( JsonSerializer . Deserialize ( messageJson , CommunicationMessagesContext . Default . AccountFolderConfigurationUpdated ) ) ;
break ;
case nameof ( CopyAuthURLRequested ) :
WeakReferenceMessenger . Default . Send ( JsonSerializer . Deserialize ( messageJson , CommunicationMessagesContext . Default . CopyAuthURLRequested ) ) ;
break ;
case nameof ( NewMailSynchronizationRequested ) :
2025-02-22 23:09:53 +01:00
WeakReferenceMessenger . Default . Send ( JsonSerializer . Deserialize ( messageJson , CommunicationMessagesContext . Default . NewMailSynchronizationRequested ) ) ;
break ;
case nameof ( AccountCacheResetMessage ) :
WeakReferenceMessenger . Default . Send ( JsonSerializer . Deserialize ( messageJson , CommunicationMessagesContext . Default . AccountCacheResetMessage ) ) ;
2025-02-16 11:54:23 +01:00
break ;
default :
throw new Exception ( "Invalid data type name passed to client." ) ;
2024-08-05 00:36:26 +02:00
}
2025-02-16 11:54:23 +01:00
}
2024-08-05 00:36:26 +02:00
2025-02-16 11:54:23 +01:00
private void ServerDisconnected ( AppServiceConnection sender , AppServiceClosedEventArgs args )
{
Log . Information ( "Server disconnected." ) ;
}
2024-08-05 00:36:26 +02:00
2025-02-16 11:54:23 +01:00
public async Task QueueRequestAsync ( IRequestBase request , Guid accountId )
{
var queuePackage = new ServerRequestPackage ( accountId , request ) ;
2024-08-05 00:36:26 +02:00
2025-02-16 11:54:23 +01:00
var queueResponse = await GetResponseInternalAsync < bool , ServerRequestPackage > ( queuePackage , new Dictionary < string , object > ( )
{
{ MessageConstants . MessageDataRequestAccountIdKey , accountId }
} ) ;
2024-08-05 00:36:26 +02:00
2025-02-16 11:54:23 +01:00
queueResponse . ThrowIfFailed ( ) ;
}
2024-11-11 01:09:05 +01:00
2025-02-16 11:54:23 +01: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
2025-05-18 14:06:25 +02:00
[RequiresDynamicCode("Calls System.Text.Json.JsonSerializer.Serialize<TValue>(TValue, JsonSerializerOptions)")]
[RequiresUnreferencedCode("Calls System.Text.Json.JsonSerializer.Serialize<TValue>(TValue, JsonSerializerOptions)")]
2025-02-16 11:54:23 +01:00
private async Task < WinoServerResponse < TResponse > > GetResponseInternalAsync < TResponse , TRequestType > ( TRequestType message ,
Dictionary < string , object > parameters = null ,
CancellationToken cancellationToken = default )
{
if ( Status ! = WinoServerConnectionStatus . Connected )
await ConnectAsync ( ) ;
2024-08-05 00:36:26 +02:00
2025-02-16 11:54:23 +01:00
if ( Connection = = null ) return WinoServerResponse < TResponse > . CreateErrorResponse ( "Server connection is not established." ) ;
2024-08-05 00:36:26 +02:00
2025-02-16 11:54:23 +01:00
string serializedMessage = string . Empty ;
2025-02-16 11:43:30 +01:00
2025-02-16 11:54:23 +01:00
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}" ) ;
}
2024-08-05 00:36:26 +02:00
2025-02-16 11:54:23 +01:00
AppServiceResponse response = null ;
2025-02-16 11:43:30 +01:00
2025-02-16 11:54:23 +01:00
try
{
var valueSet = new ValueSet
2025-02-16 11:35:43 +01:00
{
2025-02-16 11:54:23 +01:00
{ MessageConstants . MessageTypeKey , ( int ) MessageType . ServerMessage } ,
{ MessageConstants . MessageDataKey , serializedMessage } ,
{ MessageConstants . MessageDataTypeKey , message . GetType ( ) . Name }
} ;
2025-02-16 11:43:30 +01:00
2025-02-16 11:54:23 +01:00
// Add additional parameters into ValueSet
if ( parameters ! = null )
{
foreach ( var item in parameters )
2024-08-05 00:36:26 +02:00
{
2025-02-16 11:54:23 +01:00
valueSet . Add ( item . Key , item . Value ) ;
2024-08-05 00:36:26 +02:00
}
2025-02-16 11:43:30 +01:00
}
2024-08-05 00:36:26 +02:00
2025-02-16 11:54:23 +01:00
response = await Connection . SendMessageAsync ( valueSet ) . AsTask ( cancellationToken ) ;
}
catch ( OperationCanceledException )
{
return WinoServerResponse < TResponse > . CreateErrorResponse ( $"Request is canceled by client." ) ;
}
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}" ) ;
}
2024-08-05 00:36:26 +02:00
2025-02-16 11:54:23 +01:00
// It should be always Success.
if ( response . Status ! = AppServiceResponseStatus . Success )
return WinoServerResponse < TResponse > . CreateErrorResponse ( $"Wino Server responded with '{response.Status}' status to message delivery." ) ;
2024-08-05 00:36:26 +02:00
2025-02-16 11:54:23 +01:00
// 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." ) ;
2025-02-16 11:35:43 +01:00
2025-02-16 11:54:23 +01:00
// 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}" ) ;
}
2025-02-16 11:43:30 +01:00
}
2025-02-16 11:54:23 +01:00
public void Receive ( WinoServerConnectionEstablished message )
= > ConnectingHandle ? . TrySetResult ( true ) ;
2024-08-05 00:36:26 +02:00
}