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; using Wino.Messaging.Server; using Wino.Messaging.UI; namespace Wino.Core.UWP.Services { public class WinoServerConnectionManager : IWinoServerConnectionManager, IRecipient { private const int ServerConnectionTimeoutMs = 10000; public event EventHandler StatusChanged; public TaskCompletionSource ConnectingHandle { get; private set; } private ILogger Logger => Logger.ForContext(); private WinoServerConnectionStatus status; public WinoServerConnectionStatus Status { get { return status; } private set { Log.Information("Server connection status changed to {Status}.", value); 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 ConnectAsync() { 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; } } if (ApiInformation.IsApiContractPresent("Windows.ApplicationModel.FullTrustAppContract", 1, 0)) { try { ConnectingHandle = new TaskCompletionSource(); Status = WinoServerConnectionStatus.Connecting; var connectionCancellationToken = new CancellationTokenSource(TimeSpan.FromMilliseconds(ServerConnectionTimeoutMs)); await FullTrustProcessLauncher.LaunchFullTrustProcessForCurrentAppAsync("WinoServer"); // 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. await ConnectingHandle.Task.WaitAsync(connectionCancellationToken.Token); Log.Information("Server connection established successfully."); } catch (OperationCanceledException canceledException) { Log.Error(canceledException, $"Server process did not start in {ServerConnectionTimeoutMs} ms. Operation is canceled."); ConnectingHandle?.TrySetException(canceledException); Status = WinoServerConnectionStatus.Failed; return false; } catch (Exception ex) { Log.Error(ex, "Failed to connect to the server."); ConnectingHandle?.TrySetException(ex); Status = WinoServerConnectionStatus.Failed; return false; } return true; } else { Log.Information("FullTrustAppContract is not present in the system. Server connection is not possible."); } return false; } public async Task InitializeAsync() { var isConnectionSuccessfull = await ConnectAsync(); if (isConnectionSuccessfull) { Log.Information("ServerConnectionManager initialized successfully."); } else { Log.Error("ServerConnectionManager initialization failed."); } } 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; } } } } /// /// Unpacks IServerMessage objects and delegate it to Messenger for UI to process. /// /// Message data in json format. private void HandleUIMessage(string messageJson, string typeName) { switch (typeName) { 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)); break; default: throw new Exception("Invalid data type name passed to client."); } } private void ServerDisconnected(AppServiceConnection sender, AppServiceClosedEventArgs args) { Log.Information("Server disconnected."); } public async Task QueueRequestAsync(IRequestBase request, Guid accountId) { var queuePackage = new ServerRequestPackage(accountId, request); var queueResponse = await GetResponseInternalAsync(queuePackage, new Dictionary() { { MessageConstants.MessageDataRequestAccountIdKey, accountId } }); queueResponse.ThrowIfFailed(); } public Task> GetResponseAsync(TRequestType message, CancellationToken cancellationToken = default) where TRequestType : IClientMessage => GetResponseInternalAsync(message, cancellationToken: cancellationToken); private async Task> GetResponseInternalAsync(TRequestType message, Dictionary parameters = null, CancellationToken cancellationToken = default) { if (Status != WinoServerConnectionStatus.Connected) await ConnectAsync(); if (Connection == null) return WinoServerResponse.CreateErrorResponse("Server connection is not established."); 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.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); } } response = await Connection.SendMessageAsync(valueSet).AsTask(cancellationToken); } catch (OperationCanceledException) { return WinoServerResponse.CreateErrorResponse($"Request is canceled by client."); } catch (Exception serverSendException) { Logger.Error(serverSendException, $"Failed to send message to server."); return WinoServerResponse.CreateErrorResponse($"Failed to send message to server.\n{serverSendException.Message}"); } // It should be always Success. if (response.Status != AppServiceResponseStatus.Success) return WinoServerResponse.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.CreateErrorResponse("Server response did not contain message data."); // Try deserialize the message data. try { return JsonSerializer.Deserialize>(messageJson); } catch (Exception jsonDeserializationError) { Logger.Error(jsonDeserializationError, $"Failed to deserialize server response message data."); return WinoServerResponse.CreateErrorResponse($"Failed to deserialize Wino server response message data.\n{jsonDeserializationError.Message}"); } } public void Receive(WinoServerConnectionEstablished message) => ConnectingHandle?.TrySetResult(true); } }