Files
Wino-Mail/Wino.Core.WinUI/Services/WinoServerConnectionManager.cs
T

370 lines
16 KiB
C#
Raw Normal View History

using System;
using System.Collections.Generic;
2025-05-18 14:06:25 +02:00
using System.Diagnostics.CodeAnalysis;
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;
using Wino.Messaging.UI;
2025-09-29 11:23:44 +02:00
namespace Wino.Core.WinUI.Services;
2025-02-16 11:54:23 +01:00
public class WinoServerConnectionManager :
IWinoServerConnectionManager<AppServiceConnection>,
IRecipient<WinoServerConnectionEstablished>
{
2025-02-16 11:54:23 +01:00
private const int ServerConnectionTimeoutMs = 10000;
2025-02-16 11:54:23 +01:00
public event EventHandler<WinoServerConnectionStatus> StatusChanged;
2025-02-16 11:54:23 +01:00
public TaskCompletionSource<bool> ConnectingHandle { get; private set; }
2025-02-16 11:54:23 +01:00
private ILogger Logger => Logger.ForContext<WinoServerConnectionManager>();
2025-02-16 11:54:23 +01:00
private WinoServerConnectionStatus status;
2025-02-16 11:54:23 +01:00
public WinoServerConnectionStatus Status
{
get { return status; }
private set
{
2025-02-16 11:54:23 +01:00
Log.Information("Server connection status changed to {Status}.", value);
status = value;
StatusChanged?.Invoke(this, value);
}
2025-02-16 11:54:23 +01:00
}
2025-02-16 11:54:23 +01:00
private AppServiceConnection _connection;
public AppServiceConnection Connection
{
get { return _connection; }
set
{
2025-02-16 11:54:23 +01:00
if (_connection != null)
{
2025-02-16 11:54:23 +01:00
_connection.RequestReceived -= ServerMessageReceived;
_connection.ServiceClosed -= ServerDisconnected;
}
2025-02-16 11:54:23 +01:00
_connection = value;
2025-02-16 11:54:23 +01:00
if (value == null)
{
Status = WinoServerConnectionStatus.Disconnected;
}
else
{
value.RequestReceived += ServerMessageReceived;
value.ServiceClosed += ServerDisconnected;
2025-02-16 11:54:23 +01:00
Status = WinoServerConnectionStatus.Connected;
}
}
2025-02-16 11:54:23 +01:00
}
2025-02-16 11:54:23 +01:00
private readonly JsonSerializerOptions _jsonSerializerOptions = new()
{
TypeInfoResolver = new ServerRequestTypeInfoResolver()
};
public WinoServerConnectionManager()
{
WeakReferenceMessenger.Default.Register(this);
}
2025-02-16 11:54:23 +01:00
public async Task<bool> ConnectAsync()
{
if (Status == WinoServerConnectionStatus.Connected)
{
2025-02-16 11:54:23 +01:00
Log.Information("Server is already connected.");
return true;
}
2025-02-16 11:54:23 +01:00
if (Status == WinoServerConnectionStatus.Connecting)
{
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.
2025-02-16 11:54:23 +01:00
if (ConnectingHandle != null)
{
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
}
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>();
2025-02-16 11:54:23 +01:00
Status = WinoServerConnectionStatus.Connecting;
2025-02-16 11:54:23 +01:00
var connectionCancellationToken = new CancellationTokenSource(TimeSpan.FromMilliseconds(ServerConnectionTimeoutMs));
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.
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.");
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.");
}
2025-02-16 11:54:23 +01: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)
{
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)
{
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;
}
}
}
2025-02-16 11:54:23 +01: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)
{
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):
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.");
}
2025-02-16 11:54:23 +01:00
}
2025-02-16 11:54:23 +01:00
private void ServerDisconnected(AppServiceConnection sender, AppServiceClosedEventArgs args)
{
Log.Information("Server disconnected.");
}
2025-02-16 11:54:23 +01:00
public async Task QueueRequestAsync(IRequestBase request, Guid accountId)
{
var queuePackage = new ServerRequestPackage(accountId, request);
2025-02-16 11:54:23 +01:00
var queueResponse = await GetResponseInternalAsync<bool, ServerRequestPackage>(queuePackage, new Dictionary<string, object>()
{
{ MessageConstants.MessageDataRequestAccountIdKey, accountId }
});
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);
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();
2025-02-16 11:54:23 +01:00
if (Connection == null) return WinoServerResponse<TResponse>.CreateErrorResponse("Server connection is not established.");
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}");
}
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)
{
2025-02-16 11:54:23 +01:00
valueSet.Add(item.Key, item.Value);
}
2025-02-16 11:43:30 +01: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}");
}
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.");
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);
}