diff --git a/MareSynchronos/Configuration.cs b/MareSynchronos/Configuration.cs index e5a9d69..cb3ae11 100644 --- a/MareSynchronos/Configuration.cs +++ b/MareSynchronos/Configuration.cs @@ -3,11 +3,40 @@ using Dalamud.Plugin; using System; using System.Collections.Generic; using System.IO; +using System.Linq; +using MareSynchronos.Utils; using MareSynchronos.WebAPI; -using Newtonsoft.Json; namespace MareSynchronos { + public static class ConfigurationExtensions + { + public static bool HasValidSetup(this Configuration configuration) + { + return configuration.AcceptedAgreement && configuration.InitialScanComplete + && !string.IsNullOrEmpty(configuration.CacheFolder) + && Directory.Exists(configuration.CacheFolder) + && configuration.ClientSecret.ContainsKey(configuration.ApiUri); + } + + public static Dictionary GetCurrentServerUidComments(this Configuration configuration) + { + return configuration.UidServerComments.ContainsKey(configuration.ApiUri) + ? configuration.UidServerComments[configuration.ApiUri] + : new Dictionary(); + } + + public static void SetCurrentServerUidComment(this Configuration configuration, string uid, string comment) + { + if (!configuration.UidServerComments.ContainsKey(configuration.ApiUri)) + { + configuration.UidServerComments[configuration.ApiUri] = new Dictionary(); + } + + configuration.UidServerComments[configuration.ApiUri][uid] = comment; + } + } + [Serializable] public class Configuration : IPluginConfiguration { @@ -26,9 +55,6 @@ namespace MareSynchronos public string CacheFolder { get; set; } = string.Empty; public Dictionary ClientSecret { get; set; } = new(); public Dictionary CustomServerList { get; set; } = new(); - [JsonIgnore] - public bool HasValidSetup => AcceptedAgreement && InitialScanComplete && !string.IsNullOrEmpty(CacheFolder) && - Directory.Exists(CacheFolder) && ClientSecret.ContainsKey(ApiUri); public bool InitialScanComplete { get; set; } = false; public int MaxParallelScan @@ -46,8 +72,10 @@ namespace MareSynchronos } public bool FullPause { get; set; } = false; + public Dictionary> UidServerComments { get; set; } = new(); + public Dictionary UidComments { get; set; } = new(); - public int Version { get; set; } = 0; + public int Version { get; set; } = 1; public bool ShowTransferWindow { get; set; } = true; @@ -61,5 +89,31 @@ namespace MareSynchronos { _pluginInterface!.SavePluginConfig(this); } + + public void Migrate() + { + if (Version == 0) + { + Logger.Debug("Migrating Configuration from V0 to V1"); + Version = 1; + ApiUri = ApiUri.Replace("https", "wss"); + foreach (var kvp in ClientSecret.ToList()) + { + var newKey = kvp.Key.Replace("https", "wss"); + ClientSecret.Remove(kvp.Key); + if (ClientSecret.ContainsKey(newKey)) + { + ClientSecret[newKey] = kvp.Value; + } + else + { + ClientSecret.Add(newKey, kvp.Value); + } + } + UidServerComments.Add(ApiUri, UidComments.ToDictionary(k => k.Key, k => k.Value)); + UidComments.Clear(); + Save(); + } + } } } diff --git a/MareSynchronos/Managers/FileCacheManager.cs b/MareSynchronos/Managers/FileCacheManager.cs index 5a8e8a7..2485f84 100644 --- a/MareSynchronos/Managers/FileCacheManager.cs +++ b/MareSynchronos/Managers/FileCacheManager.cs @@ -14,9 +14,13 @@ namespace MareSynchronos.Managers public class FileCacheManager : IDisposable { private readonly IpcManager _ipcManager; + private readonly ConcurrentBag _modifiedFiles = new(); private readonly Configuration _pluginConfiguration; private FileSystemWatcher? _cacheDirWatcher; private FileSystemWatcher? _penumbraDirWatcher; + private Task? _rescanTask; + private CancellationTokenSource? _rescanTaskCancellationTokenSource; + private CancellationTokenSource? _rescanTaskRunCancellationTokenSource; private CancellationTokenSource? _scanCancellationTokenSource; private Task? _scanTask; public FileCacheManager(IpcManager ipcManager, Configuration pluginConfiguration) @@ -32,26 +36,10 @@ namespace MareSynchronos.Managers _ipcManager.PenumbraDisposed += IpcManagerOnPenumbraDisposed; } - private void StartWatchersAndScan() - { - if (!_ipcManager.Initialized || !_pluginConfiguration.HasValidSetup) return; - Logger.Debug("Penumbra is active, configuration is valid, starting watchers and scan"); - StartWatchers(); - StartInitialScan(); - } - - private void IpcManagerOnPenumbraInitialized(object? sender, EventArgs e) - { - StartWatchersAndScan(); - } - - private void IpcManagerOnPenumbraDisposed(object? sender, EventArgs e) - { - StopWatchersAndScan(); - } - public long CurrentFileProgress { get; private set; } + public long FileCacheSize { get; set; } + public bool IsScanRunning => !_scanTask?.IsCompleted ?? false; public long TotalFiles { get; private set; } @@ -83,17 +71,12 @@ namespace MareSynchronos.Managers _ipcManager.PenumbraInitialized -= IpcManagerOnPenumbraInitialized; _ipcManager.PenumbraDisposed -= IpcManagerOnPenumbraDisposed; + _rescanTaskCancellationTokenSource?.Cancel(); + _rescanTaskRunCancellationTokenSource?.Cancel(); StopWatchersAndScan(); } - private void StopWatchersAndScan() - { - _cacheDirWatcher?.Dispose(); - _penumbraDirWatcher?.Dispose(); - _scanCancellationTokenSource?.Cancel(); - } - public void StartInitialScan() { _scanCancellationTokenSource = new CancellationTokenSource(); @@ -102,8 +85,8 @@ namespace MareSynchronos.Managers public void StartWatchers() { - if (!_ipcManager.Initialized || !_pluginConfiguration.HasValidSetup) return; - Logger.Debug("Starting File System Watchers"); + if (!_ipcManager.Initialized || !_pluginConfiguration.HasValidSetup()) return; + Logger.Verbose("Starting File System Watchers"); _penumbraDirWatcher?.Dispose(); _cacheDirWatcher?.Dispose(); @@ -113,8 +96,9 @@ namespace MareSynchronos.Managers InternalBufferSize = 1048576 }; _penumbraDirWatcher.NotifyFilter = NotifyFilters.FileName | NotifyFilters.Size; - _penumbraDirWatcher.Deleted += OnDeleted; + _penumbraDirWatcher.Deleted += OnModified; _penumbraDirWatcher.Changed += OnModified; + _penumbraDirWatcher.Renamed += OnModified; _penumbraDirWatcher.Filters.Add("*.mtrl"); _penumbraDirWatcher.Filters.Add("*.mdl"); _penumbraDirWatcher.Filters.Add("*.tex"); @@ -127,8 +111,9 @@ namespace MareSynchronos.Managers InternalBufferSize = 1048576 }; _cacheDirWatcher.NotifyFilter = NotifyFilters.FileName | NotifyFilters.Size; - _cacheDirWatcher.Deleted += OnDeleted; + _cacheDirWatcher.Deleted += OnModified; _cacheDirWatcher.Changed += OnModified; + _cacheDirWatcher.Renamed += OnModified; _cacheDirWatcher.Filters.Add("*.mtrl"); _cacheDirWatcher.Filters.Add("*.mdl"); _cacheDirWatcher.Filters.Add("*.tex"); @@ -139,6 +124,16 @@ namespace MareSynchronos.Managers Task.Run(RecalculateFileCacheSize); } + private void IpcManagerOnPenumbraDisposed(object? sender, EventArgs e) + { + StopWatchersAndScan(); + } + + private void IpcManagerOnPenumbraInitialized(object? sender, EventArgs e) + { + StartWatchersAndScan(); + } + private bool IsFileLocked(FileInfo file) { try @@ -153,62 +148,10 @@ namespace MareSynchronos.Managers return false; } - private void OnDeleted(object sender, FileSystemEventArgs e) - { - var fi = new FileInfo(e.FullPath); - using var db = new FileCacheContext(); - var ext = fi.Extension.ToLower(); - if (ext is ".mdl" or ".tex" or ".mtrl") - { - Logger.Debug("File deleted: " + e.FullPath); - var fileInDb = db.FileCaches.SingleOrDefault(f => f.Filepath == fi.FullName.ToLower()); - if (fileInDb == null) return; - db.Remove(fileInDb); - - } - else - { - if (fi.Extension == string.Empty) - { - // this is most likely a folder - var filesToRemove = db.FileCaches.Where(f => f.Filepath.StartsWith(e.FullPath.ToLower())).ToList(); - Logger.Debug($"Folder deleted: {e.FullPath}, removing {filesToRemove.Count} files"); - db.RemoveRange(filesToRemove); - } - } - - db.SaveChanges(); - - if (e.FullPath.Contains(_pluginConfiguration.CacheFolder, StringComparison.OrdinalIgnoreCase)) - { - Task.Run(RecalculateFileCacheSize); - } - } - private void OnModified(object sender, FileSystemEventArgs e) { - var fi = new FileInfo(e.FullPath); - Logger.Debug("File changed: " + e.FullPath); - using var db = new FileCacheContext(); - var modifiedFile = Create(fi.FullName); - var fileInDb = db.FileCaches.SingleOrDefault(f => f.Filepath == fi.FullName.ToLower()); - if (fileInDb != null) - db.Remove(fileInDb); - else - { - var files = db.FileCaches.Where(f => f.Hash == modifiedFile.Hash); - foreach (var file in files) - { - if (!File.Exists(file.Filepath)) db.Remove(file.Filepath); - } - } - db.Add(modifiedFile); - db.SaveChanges(); - - if (e.FullPath.Contains(_pluginConfiguration.CacheFolder, StringComparison.OrdinalIgnoreCase)) - { - Task.Run(RecalculateFileCacheSize); - } + _modifiedFiles.Add(e.FullPath); + Task.Run(() => _ = RescanTask()); } private void RecalculateFileCacheSize() @@ -227,6 +170,55 @@ namespace MareSynchronos.Managers } } + public async Task RescanTask(bool force = false) + { + _rescanTaskRunCancellationTokenSource?.Cancel(); + _rescanTaskRunCancellationTokenSource = new CancellationTokenSource(); + var token = _rescanTaskRunCancellationTokenSource.Token; + if(!force) + await Task.Delay(TimeSpan.FromSeconds(1), token); + while ((!_rescanTask?.IsCompleted ?? false) && !token.IsCancellationRequested) + { + await Task.Delay(TimeSpan.FromSeconds(1), token); + } + + if (token.IsCancellationRequested) return; + + PluginLog.Debug("File changes detected, scanning the changes"); + + if (!_modifiedFiles.Any()) return; + + _rescanTaskCancellationTokenSource = new CancellationTokenSource(); + _rescanTask = Task.Run(async () => + { + var listCopy = _modifiedFiles.ToList(); + _modifiedFiles.Clear(); + await using var db = new FileCacheContext(); + foreach (var item in listCopy.Distinct()) + { + + var fi = new FileInfo(item); + if (!fi.Exists) + { + PluginLog.Verbose("Removed: " + item); + + db.RemoveRange(db.FileCaches.Where(f => f.Filepath.ToLower() == item.ToLower())); + } + else + { + PluginLog.Verbose("Changed :" + item); + var fileCache = Create(item); + db.RemoveRange(db.FileCaches.Where(f => f.Hash == fileCache.Hash)); + await db.AddAsync(fileCache, _rescanTaskCancellationTokenSource.Token); + } + } + + await db.SaveChangesAsync(_rescanTaskCancellationTokenSource.Token); + + RecalculateFileCacheSize(); + }, _rescanTaskCancellationTokenSource.Token); + } + private async Task StartFileScan(CancellationToken ct) { _scanCancellationTokenSource = new CancellationTokenSource(); @@ -341,5 +333,17 @@ namespace MareSynchronos.Managers _pluginConfiguration.Save(); } } + + private void StartWatchersAndScan() + { + if (!_ipcManager.Initialized || !_pluginConfiguration.HasValidSetup()) return; + Logger.Verbose("Penumbra is active, configuration is valid, starting watchers and scan"); + StartWatchers(); + StartInitialScan(); + } + private void StopWatchersAndScan() + { + _cacheDirWatcher?.Dispose(); + } } } diff --git a/MareSynchronos/MareSynchronos.csproj b/MareSynchronos/MareSynchronos.csproj index 53143fa..dffcd3f 100644 --- a/MareSynchronos/MareSynchronos.csproj +++ b/MareSynchronos/MareSynchronos.csproj @@ -29,7 +29,6 @@ - diff --git a/MareSynchronos/Plugin.cs b/MareSynchronos/Plugin.cs index 6c6c7c1..4e5a74f 100644 --- a/MareSynchronos/Plugin.cs +++ b/MareSynchronos/Plugin.cs @@ -47,6 +47,7 @@ namespace MareSynchronos _clientState = clientState; _configuration = PluginInterface.GetPluginConfig() as Configuration ?? new Configuration(); _configuration.Initialize(PluginInterface); + _configuration.Migrate(); _windowSystem = new WindowSystem("MareSynchronos"); @@ -123,7 +124,7 @@ namespace MareSynchronos HelpMessage = "Opens the Mare Synchronos UI" }); - if (!_configuration.HasValidSetup) + if (!_configuration.HasValidSetup()) { _introUi.IsOpen = true; return; @@ -188,7 +189,7 @@ namespace MareSynchronos private void OpenConfigUi() { - if (_configuration.HasValidSetup) + if (_configuration.HasValidSetup()) _pluginUi.Toggle(); else _introUi.Toggle(); diff --git a/MareSynchronos/UI/PluginUI.cs b/MareSynchronos/UI/PluginUI.cs index 994d146..28f0f26 100644 --- a/MareSynchronos/UI/PluginUI.cs +++ b/MareSynchronos/UI/PluginUI.cs @@ -4,6 +4,7 @@ using Dalamud.Interface.Windowing; using ImGuiNET; using MareSynchronos.WebAPI; using System; +using System.Collections.Generic; using System.IO; using System.Linq; using System.Numerics; @@ -179,7 +180,7 @@ namespace MareSynchronos.UI var marePaused = _configuration.FullPause; - if (_configuration.HasValidSetup) + if (_configuration.HasValidSetup()) { if (ImGui.Checkbox("Pause Mare Synchronos", ref marePaused)) { @@ -299,6 +300,8 @@ namespace MareSynchronos.UI { File.Delete(file); } + + _uiShared.ForceRescan(); }); } ImGui.TreePop(); @@ -340,11 +343,16 @@ namespace MareSynchronos.UI : ((item.IsPaused || item.IsPausedFromOthers) ? "Unpaired" : "Paired"); ImGui.TextColored(UiShared.GetBoolColor(item.IsSynced && !item.IsPaused && !item.IsPausedFromOthers), pairString); ImGui.TableNextColumn(); - string charComment = _configuration.UidComments.ContainsKey(item.OtherUID) ? _configuration.UidComments[item.OtherUID] : string.Empty; + string charComment = _configuration.GetCurrentServerUidComments().ContainsKey(item.OtherUID) ? _configuration.GetCurrentServerUidComments()[item.OtherUID] : string.Empty; ImGui.SetNextItemWidth(400); if (ImGui.InputTextWithHint("##comment" + item.OtherUID, "Add your comment here (comments will not be synced)", ref charComment, 255)) { - _configuration.UidComments[item.OtherUID] = charComment; + if (_configuration.GetCurrentServerUidComments().Count == 0) + { + _configuration.UidServerComments[_configuration.ApiUri] = + new Dictionary(); + } + _configuration.SetCurrentServerUidComment(item.OtherUID, charComment); _configuration.Save(); } ImGui.TableNextColumn(); @@ -362,21 +370,21 @@ namespace MareSynchronos.UI ImGui.EndTable(); } - var pairedClientEntry = tempNameUID; + var pairedClientEntry = _tempNameUID; ImGui.SetNextItemWidth(200); if (ImGui.InputText("UID", ref pairedClientEntry, 20)) { - tempNameUID = pairedClientEntry; + _tempNameUID = pairedClientEntry; } ImGui.SameLine(); ImGui.PushFont(UiBuilder.IconFont); if (ImGui.Button(FontAwesomeIcon.Plus.ToIconString() + "##addToPairedClients")) { - if (_apiController.PairedClients.All(w => w.OtherUID != tempNameUID)) + if (_apiController.PairedClients.All(w => w.OtherUID != _tempNameUID)) { - var nameToSend = tempNameUID; - tempNameUID = string.Empty; + var nameToSend = _tempNameUID; + _tempNameUID = string.Empty; _ = _apiController.SendPairedClientAddition(nameToSend); } } @@ -386,6 +394,6 @@ namespace MareSynchronos.UI } } - private string tempNameUID = string.Empty; + private string _tempNameUID = string.Empty; } } diff --git a/MareSynchronos/UI/UIShared.cs b/MareSynchronos/UI/UIShared.cs index f67b7a6..da5b28a 100644 --- a/MareSynchronos/UI/UIShared.cs +++ b/MareSynchronos/UI/UIShared.cs @@ -54,6 +54,11 @@ namespace MareSynchronos.UI return true; } + public void ForceRescan() + { + Task.Run(() => _ = _fileCacheManager.RescanTask(true)); + } + public void DrawFileScanState() { ImGui.Text("File Scanner Status"); @@ -78,10 +83,21 @@ namespace MareSynchronos.UI var serverName = _apiController.ServerDictionary.ContainsKey(_pluginConfiguration.ApiUri) ? _apiController.ServerDictionary[_pluginConfiguration.ApiUri] : _pluginConfiguration.ApiUri; - ImGui.Text("Service status of " + serverName); + ImGui.Text("Service status of \"" + serverName + "\":"); ImGui.SameLine(); var color = _apiController.ServerAlive ? ImGuiColors.ParsedGreen : ImGuiColors.DalamudRed; ImGui.TextColored(color, _apiController.ServerAlive ? "Available" : "Unavailable"); + if (_apiController.ServerAlive) + { + ImGui.SameLine(); + ImGui.TextUnformatted("("); + ImGui.SameLine(); + ImGui.TextColored(ImGuiColors.ParsedGreen, _apiController.OnlineUsers.ToString()); + ImGui.SameLine(); + ImGui.Text("Users Online (server-wide)"); + ImGui.SameLine(); + ImGui.Text(")"); + } } public static void TextWrapped(string text) @@ -209,7 +225,7 @@ namespace MareSynchronos.UI ImGui.InputText("Custom Service Address", ref _customServerUri, 255); if (ImGui.Button("Add Custom Service")) { - if (!string.IsNullOrEmpty(_customServerUri) + if (!string.IsNullOrEmpty(_customServerUri) && !string.IsNullOrEmpty(_customServerName) && !_pluginConfiguration.CustomServerList.ContainsValue(_customServerName)) { diff --git a/MareSynchronos/WebAPI/ApiController.cs b/MareSynchronos/WebAPI/ApiController.cs index b18a563..58f0bca 100644 --- a/MareSynchronos/WebAPI/ApiController.cs +++ b/MareSynchronos/WebAPI/ApiController.cs @@ -1,5 +1,4 @@ -using Dalamud.Logging; -using System; +using System; using System.Collections.Concurrent; using System.Collections.Generic; using System.Diagnostics; @@ -12,8 +11,8 @@ using LZ4; using MareSynchronos.API; using MareSynchronos.FileCacheDB; using MareSynchronos.Utils; +using Microsoft.AspNetCore.Http.Connections; using Microsoft.AspNetCore.SignalR.Client; -using Microsoft.EntityFrameworkCore.Metadata.Conventions; namespace MareSynchronos.WebAPI { @@ -21,7 +20,7 @@ namespace MareSynchronos.WebAPI { #if DEBUG public const string MainServer = "darkarchons Debug Server (Dev Server (CH))"; - public const string MainServiceUri = "https://darkarchon.internet-box.ch:5001"; + public const string MainServiceUri = "wss://darkarchon.internet-box.ch:5001"; #else public const string MainServer = "Lunae Crescere Incipientis (Central Server EU)"; public const string MainServiceUri = "to be defined"; @@ -88,12 +87,14 @@ namespace MareSynchronos.WebAPI public string UID { get; private set; } = string.Empty; private string ApiUri => _pluginConfiguration.ApiUri; + public int OnlineUsers { get; private set; } public async Task CreateConnections() { + await StopAllConnections(_cts.Token); + _cts = new CancellationTokenSource(); var token = _cts.Token; - await StopAllConnections(token); while (!ServerAlive && !token.IsCancellationRequested) { @@ -110,11 +111,14 @@ namespace MareSynchronos.WebAPI await _userHub.StartAsync(token); await _fileHub.StartAsync(token); + OnlineUsers = await _userHub.InvokeAsync("GetOnlineUsers", token); + if (_pluginConfiguration.FullPause) { UID = string.Empty; return; } + UID = await _heartbeatHub.InvokeAsync("Heartbeat", token); if (!string.IsNullOrEmpty(UID) && !token.IsCancellationRequested) // user is authorized { @@ -125,6 +129,7 @@ namespace MareSynchronos.WebAPI (s) => PairedClientOffline?.Invoke(s, EventArgs.Empty)); _userHub.On("AddOnlinePairedPlayer", (s) => PairedClientOnline?.Invoke(s, EventArgs.Empty)); + _userHub.On("UsersOnline", (count) => OnlineUsers = count); PairedClients = await _userHub!.InvokeAsync>("GetPairedClients", token); @@ -161,13 +166,12 @@ namespace MareSynchronos.WebAPI { options.Headers.Add("Authorization", SecretKey); } + + options.Transports = HttpTransportType.WebSockets; #if DEBUG - options.HttpMessageHandlerFactory = (message) => + options.HttpMessageHandlerFactory = (message) => new HttpClientHandler() { - if (message is HttpClientHandler clientHandler) - clientHandler.ServerCertificateCustomValidationCallback += - (sender, certificate, chain, sslPolicyErrors) => true; - return message; + ServerCertificateCustomValidationCallback = HttpClientHandler.DangerousAcceptAnyServerCertificateValidator }; #endif }) @@ -185,6 +189,8 @@ namespace MareSynchronos.WebAPI private Task HeartbeatHubOnReconnected(string? arg) { Logger.Debug("Connection restored"); + OnlineUsers = _userHub!.InvokeAsync("GetOnlineUsers").Result; + UID = _heartbeatHub!.InvokeAsync("Heartbeat").Result; Connected?.Invoke(this, EventArgs.Empty); return Task.CompletedTask; } @@ -198,7 +204,7 @@ namespace MareSynchronos.WebAPI private async Task StopAllConnections(CancellationToken token) { - if (_heartbeatHub is { State: HubConnectionState.Connected }) + if (_heartbeatHub is { State: HubConnectionState.Connected or HubConnectionState.Connecting or HubConnectionState.Reconnecting }) { await _heartbeatHub.StopAsync(token); _heartbeatHub.Closed -= HeartbeatHubOnClosed; @@ -207,13 +213,13 @@ namespace MareSynchronos.WebAPI await _heartbeatHub.DisposeAsync(); } - if (_fileHub is { State: HubConnectionState.Connected }) + if (_fileHub is { State: HubConnectionState.Connected or HubConnectionState.Connecting or HubConnectionState.Reconnecting }) { await _fileHub.StopAsync(token); await _fileHub.DisposeAsync(); } - if (_userHub is { State: HubConnectionState.Connected }) + if (_userHub is { State: HubConnectionState.Connected or HubConnectionState.Connecting or HubConnectionState.Reconnecting }) { await _userHub.StopAsync(token); await _userHub.DisposeAsync();