using System.ComponentModel; using System.IO; using System.IO.Compression; using System.Net; using System.Net.Http; using System.Net.Sockets; using System.Windows; using CommunityToolkit.Mvvm.ComponentModel; using CommunityToolkit.Mvvm.Messaging; using Livia.Utility; using Microsoft.Extensions.Logging; namespace Livia.Models.Data; public interface IServerHandler : INotifyPropertyChanged, INotifyPropertyChanging { bool Processing { get; set; } bool IsLoggedIn { get; } string Quota { get; } Task<(bool success, string messageIndex)> ProcessData(DataBlock dataBlock, CancellationToken token); Task<(bool success, string messageIndex)> DownloadData(DataBlock dataBlock); Task<(bool success, string messageIndex)> DownloadReport(string key, int id, string savePath, CancellationToken token); Task<(bool success, string messageIndex)> EditPassword(string oldPassword, string newPassword); Task<(List list, int count)> CheckServerJobList(int limit, int offset); Task<(List list, int count)> SearchData(SearchQueryJson json); Task> GetReportList(string key); Task Login(string username, string password); Task Cancel(string key); Task DeleteReport(string key, int id); Task<(bool success, string messageIndex)> DownloadArchive(string? key, string savePath, CancellationToken token); Task<(bool success, string messageIndex)> DownloadSorted(string? key, string savePath, CancellationToken none); Task<(bool success, string messageIndex)> DeleteData(int? id, CancellationToken cancellationToken); Task<(bool success, string messageIndex)> DeleteHistory(string? key, CancellationToken cancellationToken); Task UpdateDongleInfo(); Task GetNotice(); Task> GetSeriesGroupList(string key); Task<(bool success, string messageIndex)> SelectSeries(string key, List series); Task<(bool success, string messageIndex)> SelectReportModule(string fileName, SelectReportModuleJson json, CancellationToken token); Task<(bool success, string messageIndex)> PushToPacs(string key, int id); Task GetDicomNodeInfo(); } public class ServerHandler : ObservableRecipient, IServerHandler, IRecipient, IRecipient { public bool Processing { get => _processing; set => SetProperty(ref _processing, value); } public bool IsLoggedIn { get => _isLoggedIn; private set => SetProperty(ref _isLoggedIn, value); } public string Quota { get => _quota; private set => SetProperty(ref _quota, value); } private bool _processing; private bool _isLoggedIn; private string _quota = string.Empty; private readonly ILogger _logger; private bool _updatingPatientList; private readonly LiviaHttpClientFactory _liviaHttpClientFactory; private const int CheckResultRetryCount = 10; private const int CheckResultRetryTime = 5000; public ServerHandler(ILogger logger, LiviaHttpClientFactory liviaHttpClientFactory) { _logger = logger; _liviaHttpClientFactory = liviaHttpClientFactory; WeakReferenceMessenger.Default.RegisterAll(this); } public async Task Cancel(string key) { if (string.IsNullOrEmpty(key)) { _logger.LogWarning("Key is invalid, cannot cancel job"); return; } try { ServerResponseJson serverResponseJson = await _liviaHttpClientFactory.Create().Cancel(key).ConfigureAwait(false); _logger.LogInformation($"Cancel request response is {serverResponseJson}"); } catch (Exception e) { _logger.LogError(e, "Error canceling data"); } } public async Task DeleteReport(string key, int id) { try { ServerResponseJson serverResponseJson = await _liviaHttpClientFactory.Create().DeleteReport(key, id).ConfigureAwait(false); _logger.LogInformation($"DeleteReport response is {serverResponseJson}"); } catch (Exception e) { _logger.LogError(e, "Error DeleteReport {id}", id); } } public async Task<(bool success, string messageIndex)> DownloadData(DataBlock dataBlock) { Processing = true; string messageIndex = string.Empty; try { if (!string.IsNullOrEmpty(dataBlock.ResultPath)) { // it is finished return (true, messageIndex); } string saveName = $"{dataBlock.Key}.zip"; string savePath = Path.Combine(ServiceConfigurations.TempFolder, saveName); await _liviaHttpClientFactory.Create().CheckResult(dataBlock.Key, savePath, CancellationToken.None).ConfigureAwait(false); string path = Path.Combine(ServiceConfigurations.TempFolder, dataBlock.Key); //remove if somehow it is there if (Directory.Exists(path)) { Directory.Delete(path, true); } ZipFile.ExtractToDirectory(savePath, path); dataBlock.ResultPath = path; return (true, messageIndex); } catch (HttpRequestException e) { if (e.StatusCode == HttpStatusCode.Unauthorized) { messageIndex = "ServerUnauthorizedError"; WeakReferenceMessenger.Default.Send(new LogoutMessage()); } else { messageIndex = "ProcessingError"; } _logger.LogError(e, "Error processing data"); return (false, messageIndex); } catch (Exception e) { _logger.LogError(e, "Error processing data"); messageIndex = "ProcessingError"; return (false, messageIndex); } finally { Processing = false; } } //TODO::create a return object instead maybe so that fileName can also be returned public virtual async Task<(bool success, string messageIndex)> ProcessData(DataBlock dataBlock, CancellationToken token) { Processing = true; string messageIndex = string.Empty; try { await dataBlock.Pack(token).ConfigureAwait(false); ProcessDataResponseJson serverResponseJson = await _liviaHttpClientFactory.Create().ProcessData(dataBlock.ZipPath, token).ConfigureAwait(false); if (serverResponseJson.Code != 0) { return (false, GetErrorMessage(serverResponseJson.ErrorNumber)); } //check for response dataBlock.Key = serverResponseJson.Key; int retry = 0; while (retry < CheckResultRetryCount) { try { string saveName = $"{dataBlock.Key}.zip"; string savePath = Path.Combine(ServiceConfigurations.TempFolder, saveName); await _liviaHttpClientFactory.Create().CheckResult(dataBlock.Key, savePath, token).ConfigureAwait(false); await UpdateDongleInfo(); dataBlock.ResultPath = Path.Combine(ServiceConfigurations.TempFolder, dataBlock.Key); ZipFile.ExtractToDirectory(savePath, dataBlock.ResultPath); return (true, messageIndex); } catch (ResultNotReadyException e) { if (e.ServerResponseJson.Code != 0) { return (false, GetErrorMessage(e.ServerResponseJson.ErrorNumber)); } await Task.Delay(CheckResultRetryTime, token); } catch (Exception e) { retry++; _logger.LogWarning(e, "Error checking result, try again in 5 seconds"); await Task.Delay(CheckResultRetryTime, token); } } throw new TimeoutException(); } catch (OperationCanceledException) { await Cancel(dataBlock.Key); return (false, "ProcessingError"); } catch (HttpRequestException e) { if (e.StatusCode == HttpStatusCode.Unauthorized) { messageIndex = "ServerUnauthorizedError"; WeakReferenceMessenger.Default.Send(new LogoutMessage()); } else { messageIndex = "ProcessingError"; } _logger.LogError(e, "Error processing data"); return (false, messageIndex); } catch (Exception e) { _logger.LogError(e, "Error processing data"); messageIndex = "ProcessingError"; return (false, messageIndex); } finally { Processing = false; } } public async Task<(bool success, string messageIndex)> DownloadReport(string key, int id, string savePath, CancellationToken token) { Processing = true; string messageIndex; try { await _liviaHttpClientFactory.Create().DownloadReport(key, id, savePath, token).ConfigureAwait(false); return (true, "DownloadReportSuccess"); } catch (OperationCanceledException) { return (false, "ExportFailed"); } catch (HttpRequestException e) { if (e.StatusCode == HttpStatusCode.Unauthorized) { messageIndex = "ServerUnauthorizedError"; WeakReferenceMessenger.Default.Send(new LogoutMessage()); } else { messageIndex = "ExportFailed"; } _logger.LogError(e, "DownloadReport"); return (false, messageIndex); } catch (Exception e) { _logger.LogError(e, "DownloadReport"); messageIndex = "ExportFailed"; return (false, messageIndex); } finally { Processing = false; } } public async Task<(bool success, string messageIndex)> DownloadArchive(string? key, string savePath, CancellationToken token) { if (string.IsNullOrEmpty(key)) return (false, "ProcessingError"); Processing = true; string messageIndex = string.Empty; try { await _liviaHttpClientFactory.Create().DownloadArchive(key, savePath, token).ConfigureAwait(false); return (true, messageIndex); } catch (OperationCanceledException) { await Cancel(key); try { File.Delete(savePath); } catch (Exception e) { _logger.LogError(e, "Cannot delete {path}", savePath); } return (false, "ProcessingError"); } catch (HttpRequestException e) { if (e.StatusCode == HttpStatusCode.Unauthorized) { messageIndex = "ServerUnauthorizedError"; WeakReferenceMessenger.Default.Send(new LogoutMessage()); } else { messageIndex = "ProcessingError"; } _logger.LogError(e, "Error DownloadArchive"); return (false, messageIndex); } catch (Exception e) { _logger.LogError(e, "Error DownloadArchive"); messageIndex = "ProcessingError"; return (false, messageIndex); } finally { Processing = false; } } public async Task<(bool success, string messageIndex)> DownloadSorted(string? key, string savePath, CancellationToken token) { if (string.IsNullOrEmpty(key)) return (false, "ProcessingError"); Processing = true; string messageIndex = string.Empty; try { await _liviaHttpClientFactory.Create().DownloadSorted(key, savePath, token).ConfigureAwait(false); return (true, messageIndex); } catch (OperationCanceledException) { await Cancel(key); try { File.Delete(savePath); } catch (Exception e) { _logger.LogError(e, "Cannot delete {path}", savePath); } return (false, "ProcessingError"); } catch (HttpRequestException e) { if (e.StatusCode == HttpStatusCode.Unauthorized) { messageIndex = "ServerUnauthorizedError"; WeakReferenceMessenger.Default.Send(new LogoutMessage()); } else { messageIndex = "ProcessingError"; } _logger.LogError(e, "Error DownloadSorted"); return (false, messageIndex); } catch (Exception e) { _logger.LogError(e, "Error DownloadSorted"); messageIndex = "ProcessingError"; return (false, messageIndex); } finally { Processing = false; } } public async Task<(bool success, string messageIndex)> DeleteData(int? id, CancellationToken token) { if (id == null) return (false, "ProcessingError"); Processing = true; string messageIndex; try { ServerResponseJson serverResponseJson = await _liviaHttpClientFactory.Create().DeleteData(id.ToString(), token).ConfigureAwait(false); return serverResponseJson.Code != 0 ? (false, GetErrorMessage(serverResponseJson.ErrorNumber)) : (true, "DeleteSuccess"); } catch (OperationCanceledException) { return (false, "ProcessingError"); } catch (HttpRequestException e) { if (e.StatusCode == HttpStatusCode.Unauthorized) { messageIndex = "ServerUnauthorizedError"; WeakReferenceMessenger.Default.Send(new LogoutMessage()); } else { messageIndex = "ProcessingError"; } _logger.LogError(e, "Error DeleteData"); return (false, messageIndex); } catch (Exception e) { _logger.LogError(e, "Error DeleteData"); messageIndex = "ProcessingError"; return (false, messageIndex); } finally { Processing = false; } } public async Task<(bool success, string messageIndex)> DeleteHistory(string? key, CancellationToken token) { if (string.IsNullOrEmpty(key)) return (false, "ProcessingError"); Processing = true; string messageIndex; try { ServerResponseJson serverResponseJson = await _liviaHttpClientFactory.Create().DeleteHistory(key, token).ConfigureAwait(false); return serverResponseJson.Code != 0 ? (false, GetErrorMessage(serverResponseJson.ErrorNumber)) : (true, "DeleteSuccess"); } catch (OperationCanceledException) { return (false, "ProcessingError"); } catch (HttpRequestException e) { if (e.StatusCode == HttpStatusCode.Unauthorized) { messageIndex = "ServerUnauthorizedError"; WeakReferenceMessenger.Default.Send(new LogoutMessage()); } else { messageIndex = "ProcessingError"; } _logger.LogError(e, "Error DeleteData"); return (false, messageIndex); } catch (Exception e) { _logger.LogError(e, "Error DeleteData"); messageIndex = "ProcessingError"; return (false, messageIndex); } finally { Processing = false; } } public async Task<(bool success, string messageIndex)> EditPassword(string oldPassword, string newPassword) { Processing = true; string messageIndex; try { ServerResponseJson serverResponseJson = await _liviaHttpClientFactory.Create().EditPassword(oldPassword, newPassword).ConfigureAwait(false); return serverResponseJson.Code != 0 ? (false, GetErrorMessage(serverResponseJson.ErrorNumber)) : (true, "EditPasswordSuccess"); } catch (HttpRequestException e) { if (e.StatusCode == HttpStatusCode.Unauthorized) { messageIndex = "ServerUnauthorizedError"; WeakReferenceMessenger.Default.Send(new LogoutMessage()); } else { messageIndex = "ProcessingError"; } _logger.LogError(e, "Error processing data"); return (false, messageIndex); } catch (Exception e) { _logger.LogError(e, "Error processing data"); messageIndex = "ProcessingError"; return (false, messageIndex); } finally { Processing = false; } } public async Task<(List list, int count)> CheckServerJobList(int limit, int offset) { //TODO::should return fail instead, so it is now used List resultList = []; int count = 0; if (_updatingPatientList) return (resultList, count); if (!IsLoggedIn) return (resultList, count); _updatingPatientList = true; try { ServerJobListJsonResponse serverResponseJson = await _liviaHttpClientFactory.Create().CheckServerJobList(limit, offset).ConfigureAwait(false); if (serverResponseJson.Code != 0) throw new Exception(GetErrorMessage(serverResponseJson.Code)); resultList.AddRange(serverResponseJson.DataList); count = serverResponseJson.Count; } //TODO::we may get 401 here catch (Exception e) { _logger.LogError(e, "Error updating patient list"); } finally { _updatingPatientList = false; } return (resultList, count); } public async Task<(List list, int count)> SearchData(SearchQueryJson json) { //TODO::should return fail instead, so it is now used List resultList = []; int count = 0; if (_updatingPatientList) return (resultList, count); if (!IsLoggedIn) return (resultList, count); _updatingPatientList = true; try { ServerJobListJsonResponse serverResponseJson = await _liviaHttpClientFactory.Create().SearchData(json).ConfigureAwait(false); if (serverResponseJson.Code != 0) throw new Exception(GetErrorMessage(serverResponseJson.Code)); resultList.AddRange(serverResponseJson.DataList); count = serverResponseJson.Count; } //TODO::we may get 401 here catch (Exception e) { _logger.LogError(e, "Error SearchData"); } finally { _updatingPatientList = false; } return (resultList, count); } public async Task> GetReportList(string key) { try { return await _liviaHttpClientFactory.Create().GetReportList(key).ConfigureAwait(false); } catch (Exception e) { _logger.LogError(e, "Error GetReportList"); } return []; } private string GetErrorMessage(int code) { _logger.LogError($"Server error, errno = {code}"); string index = $"ServerError{code}"; return Application.Current.TryFindResource(index) != null ? index : "ServerErrorUnknown"; } public async Task UpdateDongleInfo() { if (!IsLoggedIn) return; try { DongleInfoResponseJson serverResponseJson = await _liviaHttpClientFactory.Create().UpdateDongleInfo().ConfigureAwait(false); if (serverResponseJson.Code != 0) throw new Exception(serverResponseJson.Msg); if (serverResponseJson.Quota == uint.MaxValue) { if (Application.Current.TryFindResource("UnlimitedQuota") is string str) { Quota = str; } else { Quota = ""; } } else { Quota = serverResponseJson.Quota.ToString(); } } //TODO::we may get 401 here catch (Exception e) { _logger.LogError(e, "Error GetDongleInfo"); } } public async Task GetNotice() { try { ServerNoticeJsonResponse serverResponseJson = await _liviaHttpClientFactory.Create().GetNotice().ConfigureAwait(false); //no notice if (serverResponseJson.Code == -1) return null; if (serverResponseJson.Code != 0) throw new Exception(GetErrorMessage(serverResponseJson.Code)); return serverResponseJson.Notice; } //TODO::we may get 401 here catch (Exception e) { _logger.LogError(e, "Error updating ServerNotice"); } return null; } public async Task> GetSeriesGroupList(string key) { try { SeriesGroupListJson serverResponseJson = await _liviaHttpClientFactory.Create().GetSeriesGroupList(key).ConfigureAwait(false); //no notice if (serverResponseJson.Code == -1) return []; if (serverResponseJson.Code != 0) throw new Exception(GetErrorMessage(serverResponseJson.Code)); return serverResponseJson.SeriesGroupList; } //TODO::we may get 401 here catch (Exception e) { _logger.LogError(e, "Error GetSeriesGroupList"); } return []; } public async Task<(bool success, string messageIndex)> SelectSeries(string key, List series) { string messageIndex; try { ServerResponseJson serverResponseJson = await _liviaHttpClientFactory.Create().SelectSeries(key, series).ConfigureAwait(false); if (serverResponseJson.Code != 0) return (false, GetErrorMessage(serverResponseJson.ErrorNumber)); serverResponseJson = await _liviaHttpClientFactory.Create().Start(key).ConfigureAwait(false); return serverResponseJson.Code != 0 ? (false, GetErrorMessage(serverResponseJson.ErrorNumber)) : (true, "SelectSeriesSuccess"); } catch (HttpRequestException e) { if (e.StatusCode == HttpStatusCode.Unauthorized) { messageIndex = "ServerUnauthorizedError"; WeakReferenceMessenger.Default.Send(new LogoutMessage()); } else { messageIndex = "ProcessingError"; } _logger.LogError(e, "Error SelectSeries"); return (false, messageIndex); } catch (Exception e) { _logger.LogError(e, "Error SelectSeries"); messageIndex = "ProcessingError"; return (false, messageIndex); } } public virtual async Task<(bool success, string messageIndex)> SelectReportModule(string fileName, SelectReportModuleJson json, CancellationToken token) { Processing = true; string messageIndex; try { ReportIdJson reportIdJson = await _liviaHttpClientFactory.Create().SelectReportModule(json).ConfigureAwait(false); await _liviaHttpClientFactory.Create().DownloadReport(json.Key, reportIdJson.ReportId, fileName, token).ConfigureAwait(false); return (true, "DownloadReportSuccess"); } catch (SelectReportModuleFailedException e) { return (false, GetErrorMessage(e.ErrorNumber)); } catch (HttpRequestException e) { if (e.StatusCode == HttpStatusCode.Unauthorized) { messageIndex = "ServerUnauthorizedError"; WeakReferenceMessenger.Default.Send(new LogoutMessage()); } else { messageIndex = "ExportFailed"; } _logger.LogError(e, "Error ReportModule"); return (false, messageIndex); } catch (Exception e) { _logger.LogError(e, "Error ReportModule"); messageIndex = "ExportFailed"; return (false, messageIndex); } finally { Processing = false; } } public async Task<(bool success, string messageIndex)> PushToPacs(string key, int id) { string messageIndex; try { ServerResponseJson serverResponseJson = await _liviaHttpClientFactory.Create().PushToPacs(key, id).ConfigureAwait(false); return serverResponseJson.Code != 0 ? (false, GetErrorMessage(serverResponseJson.ErrorNumber)) : (true, "PushToPacsSuccess"); } catch (HttpRequestException e) { if (e.StatusCode == HttpStatusCode.Unauthorized) { messageIndex = "ServerUnauthorizedError"; WeakReferenceMessenger.Default.Send(new LogoutMessage()); } else { messageIndex = "ProcessingError"; } _logger.LogError(e, "Error PushToPacs"); return (false, messageIndex); } catch (Exception e) { _logger.LogError(e, "Error PushToPacs"); messageIndex = "ProcessingError"; return (false, messageIndex); } } public async Task GetDicomNodeInfo() { try { DicomNodeInfoJsonResponse serverResponseJson = await _liviaHttpClientFactory.Create().GetDicomNodeInfo().ConfigureAwait(false); //no notice if (serverResponseJson.Code == -1) return null; if (serverResponseJson.Code != 0) throw new Exception(GetErrorMessage(serverResponseJson.Code)); return serverResponseJson.Dicom; } //TODO::we may get 401 here catch (Exception e) { _logger.LogError(e, "Error updating ServerNotice"); } return null; } public async Task Login(string username, string password) { //TODO::maybe only return index instead if (Processing) { return (string)Application.Current.TryFindResource("LoginError"); } Processing = true; string message = string.Empty; try { _logger.LogInformation("Sending post request"); await _liviaHttpClientFactory.Create().Login(username, password).ConfigureAwait(false); IsLoggedIn = true; await UpdateDongleInfo(); } catch (LoginErrorException e) { message = (string)(Application.Current.TryFindResource($"ServerError{e.ServerResponseJson.ErrorNumber}") ?? Application.Current.TryFindResource("LoginError")); //Account is locked if (e.ServerResponseJson.ErrorNumber == 10021) { message = string.Format(message, e.ServerResponseJson.Count, e.ServerResponseJson.Seconds); } } catch (HttpRequestException e) { _logger.LogError(e, "HttpRequestException while logging in"); if (e.StatusCode == HttpStatusCode.Unauthorized) { _logger.LogError(e, "Incorrect password"); message = (string)Application.Current.TryFindResource("IncorrectPasswordError"); } else if (e.InnerException is SocketException) { _logger.LogError(e, "SocketException"); message = (string)Application.Current.TryFindResource("SocketExceptionError"); } else { message = (string)Application.Current.TryFindResource("LoginError"); } } catch (Exception e) { _logger.LogError(e, "Error logging in"); message = (string)Application.Current.TryFindResource("LoginError"); } finally { Processing = false; } return message; } public void Receive(LogoutMessage message) { IsLoggedIn = false; LiviaHttpClient.ClearToken(); } public void Receive(IdleTimerTickMessage message) { if (Processing) message.IdleTimer.ResetTimer(); } }