From c3597b5789de1e638725093c9f3cdfa7c8a699a0 Mon Sep 17 00:00:00 2001 From: cake Date: Fri, 14 Nov 2025 23:56:39 +0100 Subject: [PATCH] Compactor multi-threaded, fixed many of IDE warnings --- LightlessSync/FileCache/FileCacheManager.cs | 21 +- LightlessSync/FileCache/FileCompactor.cs | 468 +++++++++++------- LightlessSync/Interop/DalamudLogger.cs | 5 +- .../Compactor/BatchFileFragService.cs | 28 +- LightlessSync/UI/BroadcastUI.cs | 2 +- LightlessSync/UI/CharaDataHubUi.Functions.cs | 8 +- LightlessSync/UI/CompactUI.cs | 11 +- LightlessSync/UI/EditProfileUi.cs | 1 - LightlessSync/UI/Handlers/IdDisplayHandler.cs | 6 +- LightlessSync/UI/StandaloneProfileUi.cs | 2 +- LightlessSync/UI/SyncshellAdminUI.cs | 9 +- LightlessSync/UI/TopTabMenu.cs | 69 +-- LightlessSync/UI/UpdateNotesUi.cs | 9 +- LightlessSync/Utils/FileSystemHelper.cs | 43 +- 14 files changed, 361 insertions(+), 321 deletions(-) diff --git a/LightlessSync/FileCache/FileCacheManager.cs b/LightlessSync/FileCache/FileCacheManager.cs index 7ee6c99..29d2aa7 100644 --- a/LightlessSync/FileCache/FileCacheManager.cs +++ b/LightlessSync/FileCache/FileCacheManager.cs @@ -134,13 +134,9 @@ public sealed class FileCacheManager : IHostedService chosenLength = penumbraMatch; } - if (TryBuildPrefixedPath(normalized, _configService.Current.CacheFolder, CachePrefix, out var cachePrefixed, out var cacheMatch)) + if (TryBuildPrefixedPath(normalized, _configService.Current.CacheFolder, CachePrefix, out var cachePrefixed, out var cacheMatch) && cacheMatch > chosenLength) { - if (cacheMatch > chosenLength) - { - chosenPrefixed = cachePrefixed; - chosenLength = cacheMatch; - } + chosenPrefixed = cachePrefixed; } return NormalizePrefixedPathKey(chosenPrefixed ?? normalized); @@ -602,7 +598,6 @@ public sealed class FileCacheManager : IHostedService private FileCacheEntity? GetValidatedFileCache(FileCacheEntity fileCache) { var resultingFileCache = ReplacePathPrefixes(fileCache); - //_logger.LogTrace("Validating {path}", fileCache.PrefixedFilePath); resultingFileCache = Validate(resultingFileCache); return resultingFileCache; } @@ -644,7 +639,7 @@ public sealed class FileCacheManager : IHostedService return fileCache; } - public Task StartAsync(CancellationToken cancellationToken) + public async Task StartAsync(CancellationToken cancellationToken) { _logger.LogInformation("Starting FileCacheManager"); @@ -695,14 +690,14 @@ public sealed class FileCacheManager : IHostedService try { _logger.LogInformation("Attempting to read {csvPath}", _csvPath); - entries = File.ReadAllLines(_csvPath); + entries = await File.ReadAllLinesAsync(_csvPath, cancellationToken).ConfigureAwait(false); success = true; } catch (Exception ex) { attempts++; _logger.LogWarning(ex, "Could not open {file}, trying again", _csvPath); - Task.Delay(100, cancellationToken); + await Task.Delay(100, cancellationToken).ConfigureAwait(false); } } @@ -823,12 +818,12 @@ public sealed class FileCacheManager : IHostedService _logger.LogInformation("Started FileCacheManager"); - return Task.CompletedTask; + await Task.CompletedTask.ConfigureAwait(false); } - public Task StopAsync(CancellationToken cancellationToken) + public async Task StopAsync(CancellationToken cancellationToken) { WriteOutFullCsv(); - return Task.CompletedTask; + await Task.CompletedTask.ConfigureAwait(false); } } \ No newline at end of file diff --git a/LightlessSync/FileCache/FileCompactor.cs b/LightlessSync/FileCache/FileCompactor.cs index 3edf96a..c321f94 100644 --- a/LightlessSync/FileCache/FileCompactor.cs +++ b/LightlessSync/FileCache/FileCompactor.cs @@ -11,7 +11,7 @@ using static LightlessSync.Utils.FileSystemHelper; namespace LightlessSync.FileCache; -public sealed class FileCompactor : IDisposable +public sealed partial class FileCompactor : IDisposable { public const uint FSCTL_DELETE_EXTERNAL_BACKING = 0x90314U; public const ulong WOF_PROVIDER_FILE = 2UL; @@ -29,23 +29,26 @@ public sealed class FileCompactor : IDisposable private readonly SemaphoreSlim _globalGate; //Limit btrfs gate on half of threads given to compactor. - private static readonly SemaphoreSlim _btrfsGate = new(4, 4); + private readonly SemaphoreSlim _btrfsGate; private readonly BatchFilefragService _fragBatch; - private readonly WOF_FILE_COMPRESSION_INFO_V1 _efInfo = new() + private readonly bool _isWindows; + private readonly int _workerCount; + + private readonly WofFileCompressionInfoV1 _efInfo = new() { Algorithm = (int)CompressionAlgorithm.XPRESS8K, Flags = 0 }; [StructLayout(LayoutKind.Sequential, Pack = 1)] - private struct WOF_FILE_COMPRESSION_INFO_V1 + private struct WofFileCompressionInfoV1 { public int Algorithm; public ulong Flags; } - private enum CompressionAlgorithm + private enum CompressionAlgorithm { NO_COMPRESSION = -2, LZNT1 = -1, @@ -61,6 +64,7 @@ public sealed class FileCompactor : IDisposable _logger = logger; _lightlessConfigService = lightlessConfigService; _dalamudUtilService = dalamudUtilService; + _isWindows = OperatingSystem.IsWindows(); _compactionQueue = Channel.CreateUnbounded(new UnboundedChannelOptions { @@ -68,29 +72,36 @@ public sealed class FileCompactor : IDisposable SingleWriter = false }); + //Amount of threads given for the compactor int workers = Math.Clamp(Math.Min(Environment.ProcessorCount / 2, 4), 1, 8); + //Setup gates for the threads and setup worker count _globalGate = new SemaphoreSlim(workers, workers); - int workerCount = Math.Max(workers * 2, workers); + _btrfsGate = new SemaphoreSlim(workers / 2, workers / 2); + _workerCount = Math.Max(workers * 2, workers); - for (int i = 0; i < workerCount; i++) + //Setup workers on the queue + for (int i = 0; i < _workerCount; i++) { + int workerId = i; + _workers.Add(Task.Factory.StartNew( - () => ProcessQueueWorkerAsync(_compactionCts.Token), + () => ProcessQueueWorkerAsync(_compactionCts.Token, workerId), _compactionCts.Token, TaskCreationOptions.LongRunning, TaskScheduler.Default).Unwrap()); } + //Uses an batching service for the filefrag command on Linux _fragBatch = new BatchFilefragService( useShell: _dalamudUtilService.IsWine, log: _logger, batchSize: 64, - flushMs: 25, + flushMs: 25, runDirect: RunProcessDirect, runShell: RunProcessShell ); - _logger.LogInformation("FileCompactor started with {workers} workers", workerCount); + _logger.LogInformation("FileCompactor started with {workers} workers", _workerCount); } public bool MassCompactRunning { get; private set; } @@ -100,37 +111,90 @@ public sealed class FileCompactor : IDisposable /// Compact the storage of the Cache Folder /// /// Used to check if files needs to be compressed - public void CompactStorage(bool compress) + public void CompactStorage(bool compress, int? maxDegree = null) { MassCompactRunning = true; + try { - var allFiles = Directory.EnumerateFiles(_lightlessConfigService.Current.CacheFolder).ToList(); - int total = allFiles.Count; - int current = 0; - - foreach (var file in allFiles) + var folder = _lightlessConfigService.Current.CacheFolder; + if (string.IsNullOrWhiteSpace(folder) || !Directory.Exists(folder)) { - current++; - Progress = $"{current}/{total}"; + _logger.LogWarning("Filecompacator couldnt find your Cache folder: {folder}", folder); + Progress = "0/0"; + return; + } + + var files = Directory.EnumerateFiles(folder).ToArray(); + var total = files.Length; + Progress = $"0/{total}"; + if (total == 0) return; + + var degree = maxDegree ?? Math.Clamp(Environment.ProcessorCount / 2, 1, 8); + + var done = 0; + int workerCounter = -1; + var po = new ParallelOptions + { + MaxDegreeOfParallelism = degree, + CancellationToken = _compactionCts.Token + }; + + Parallel.ForEach(files, po, localInit: () => Interlocked.Increment(ref workerCounter), body: (file, state, workerId) => + { + _globalGate.WaitAsync(po.CancellationToken).GetAwaiter().GetResult(); + + if (!_pendingCompactions.TryAdd(file, 0)) + return -1; try { - // Compress or decompress files - if (compress) - CompactFile(file); - else - DecompressFile(file); + try + { + if (compress) + { + if (_lightlessConfigService.Current.UseCompactor) + CompactFile(file, workerId); + } + else + { + DecompressFile(file, workerId); + } + } + catch (IOException ioEx) + { + _logger.LogDebug(ioEx, "[W{worker}] File being read/written, skipping file: {file}", workerId, file); + } + catch (OperationCanceledException) + { + throw; + } + catch (Exception ex) + { + _logger.LogWarning(ex, "[W{worker}] Error processing file: {file}", workerId, file); + } + finally + { + var n = Interlocked.Increment(ref done); + Progress = $"{n}/{total}"; + } } - catch (IOException ioEx) + finally { - _logger.LogDebug(ioEx, "File {file} locked or busy, skipping", file); + _pendingCompactions.TryRemove(file, out _); + _globalGate.Release(); } - catch (Exception ex) - { - _logger.LogWarning(ex, "Error compacting/decompressing file {file}", file); - } - } + + return workerId; + }, + localFinally: _ => + { + //Ignore local finally for now + }); + } + catch (OperationCanceledException ex) + { + _logger.LogDebug(ex, "Mass compaction call got cancelled, shutting off compactor."); } finally { @@ -139,6 +203,7 @@ public sealed class FileCompactor : IDisposable } } + /// /// Write all bytes into a directory async /// @@ -197,24 +262,32 @@ public sealed class FileCompactor : IDisposable { try { - bool isWindowsProc = RuntimeInformation.IsOSPlatform(OSPlatform.Windows); - var (_, linuxPath) = ResolvePathsForBtrfs(fileInfo.FullName); + bool isWine = _dalamudUtilService?.IsWine ?? false; - var (ok, output, err, code) = - isWindowsProc - ? RunProcessShell($"stat -c='%b' {QuoteSingle(linuxPath)}", workingDir: null, 10000) - : RunProcessDirect("stat", ["-c='%b'", linuxPath], workingDir: null, 10000); + string linuxPath = isWine ? ToLinuxPathIfWine(fileInfo.FullName, isWine) + : fileInfo.FullName; - if (ok && long.TryParse(output.Trim(), out long blocks)) - return (false, blocks * 512L); // st_blocks are always 512B units + (bool ok, string so, string se, int code) res; - _logger.LogDebug("Btrfs size probe failed for {linux} (stat {code}, err {err}). Falling back to Length.", linuxPath, code, err); - return (false, fileInfo.Length); + res = isWine + ? RunProcessShell($"stat -c %b -- {QuoteSingle(linuxPath)}", timeoutMs: 10000) + : RunProcessDirect("stat", ["-c", "%b", "--", linuxPath], "/", 10000); + + var outTrim = res.so?.Trim() ?? ""; + + if (res.ok && long.TryParse(outTrim, out long blocks) && blocks >= 0) + { + // st_blocks are 512-byte units + return (flowControl: false, value: blocks * 512L); + } + + _logger.LogDebug("Btrfs size probe failed for {linux} (exit {code}). stdout='{so}' stderr='{se}'. Falling back to Length.", linuxPath, res.code, outTrim, (res.se ?? "").Trim()); + return (flowControl: false, value: fileInfo.Length); } catch (Exception ex) { _logger.LogDebug(ex, "Failed Btrfs size probe for {file}, using Length", fileInfo.FullName); - return (false, fileInfo.Length); + return (flowControl: true, value: fileInfo.Length); } } @@ -243,19 +316,20 @@ public sealed class FileCompactor : IDisposable /// /// Compressing the given path with BTRFS or NTFS file system. /// - /// Path of the decompressed/normal file - private void CompactFile(string filePath) + /// Path of the decompressed/normal file + /// Worker/Process Id + private void CompactFile(string filePath, int workerId) { var fi = new FileInfo(filePath); if (!fi.Exists) { - _logger.LogTrace("Skip compaction: missing {file}", filePath); + _logger.LogTrace("[W{worker}] Skip compaction: missing {file}", workerId, filePath); return; } var fsType = GetFilesystemType(filePath, _dalamudUtilService.IsWine); var oldSize = fi.Length; - int blockSize = GetBlockSizeForPath(fi.FullName, _logger, _dalamudUtilService.IsWine); + int blockSize = (int)(GetFileSizeOnDisk(fi) / 512); // We skipping small files (128KiB) as they slow down the system a lot for BTRFS. as BTRFS has a different blocksize it requires an different calculation. long minSizeBytes = fsType == FilesystemType.Btrfs @@ -264,7 +338,7 @@ public sealed class FileCompactor : IDisposable if (oldSize < minSizeBytes) { - _logger.LogTrace("Skip compaction: {file} ({size} B) < threshold ({th} B)", filePath, oldSize, minSizeBytes); + _logger.LogTrace("[W{worker}] Skip compaction: {file} ({size} B) < threshold ({th} B)", workerId, filePath, oldSize, minSizeBytes); return; } @@ -272,20 +346,19 @@ public sealed class FileCompactor : IDisposable { if (!IsWOFCompactedFile(filePath)) { - _logger.LogDebug("NTFS compaction XPRESS8K: {file}", filePath); if (WOFCompressFile(filePath)) { var newSize = GetFileSizeOnDisk(fi); - _logger.LogDebug("NTFS compressed {file} {old} -> {new}", filePath, oldSize, newSize); + _logger.LogDebug("[W{worker}] NTFS compressed XPRESS8K {file} {old} -> {new}", workerId, filePath, oldSize, newSize); } else { - _logger.LogWarning("NTFS compression failed or unavailable for {file}", filePath); + _logger.LogWarning("[W{worker}] NTFS compression failed or unavailable for {file}", workerId, filePath); } } else { - _logger.LogTrace("Already NTFS-compressed: {file}", filePath); + _logger.LogTrace("[W{worker}] Already NTFS-compressed with XPRESS8K: {file}", workerId, filePath); } return; } @@ -294,41 +367,41 @@ public sealed class FileCompactor : IDisposable { if (!IsBtrfsCompressedFile(filePath)) { - _logger.LogDebug("Btrfs compression zstd: {file}", filePath); if (BtrfsCompressFile(filePath)) { var newSize = GetFileSizeOnDisk(fi); - _logger.LogDebug("Btrfs compressed {file} {old} -> {new}", filePath, oldSize, newSize); + _logger.LogDebug("[W{worker}] Btrfs compressed clzo {file} {old} -> {new}", workerId, filePath, oldSize, newSize); } else { - _logger.LogWarning("Btrfs compression failed or unavailable for {file}", filePath); + _logger.LogWarning("[W{worker}] Btrfs compression failed or unavailable for {file}", workerId, filePath); } } else { - _logger.LogTrace("Already Btrfs-compressed: {file}", filePath); + _logger.LogTrace("[W{worker}] Already Btrfs-compressed with clzo: {file}", workerId, filePath); } return; } - _logger.LogTrace("Skip compact: unsupported FS for {file}", filePath); + _logger.LogTrace("[W{worker}] Skip compact: unsupported FS for {file}", workerId, filePath); } /// /// Decompressing the given path with BTRFS file system or NTFS file system. /// - /// Path of the compressed file - private void DecompressFile(string path) + /// Path of the decompressed/normal file + /// Worker/Process Id + private void DecompressFile(string filePath, int workerId) { - _logger.LogDebug("Decompress request: {file}", path); - var fsType = GetFilesystemType(path, _dalamudUtilService.IsWine); + _logger.LogDebug("[W{worker}] Decompress request: {file}", workerId, filePath); + var fsType = GetFilesystemType(filePath, _dalamudUtilService.IsWine); if (fsType == FilesystemType.NTFS && !_dalamudUtilService.IsWine) { try { - bool flowControl = DecompressWOFFile(path); + bool flowControl = DecompressWOFFile(filePath, workerId); if (!flowControl) { return; @@ -336,7 +409,7 @@ public sealed class FileCompactor : IDisposable } catch (Exception ex) { - _logger.LogWarning(ex, "NTFS decompress error {file}", path); + _logger.LogWarning(ex, "[W{worker}] NTFS decompress error {file}", workerId, filePath); } } @@ -344,7 +417,7 @@ public sealed class FileCompactor : IDisposable { try { - bool flowControl = DecompressBtrfsFile(path); + bool flowControl = DecompressBtrfsFile(filePath); if (!flowControl) { return; @@ -352,7 +425,7 @@ public sealed class FileCompactor : IDisposable } catch (Exception ex) { - _logger.LogWarning(ex, "Btrfs decompress error {file}", path); + _logger.LogWarning(ex, "[W{worker}] Btrfs decompress error {file}", workerId, filePath); } } } @@ -372,51 +445,48 @@ public sealed class FileCompactor : IDisposable string linuxPath = isWine ? ToLinuxPathIfWine(path, isWine) : path; var opts = GetMountOptionsForPath(linuxPath); - bool hasCompress = opts.Contains("compress", StringComparison.OrdinalIgnoreCase); - bool hasCompressForce = opts.Contains("compress-force", StringComparison.OrdinalIgnoreCase); + if (!string.IsNullOrEmpty(opts)) + _logger.LogTrace("Mount opts for {file}: {opts}", linuxPath, opts); - if (hasCompressForce) + var probe = RunProcessShell("command -v btrfs || which btrfs", timeoutMs: 5000); + var _btrfsAvailable = probe.ok && !string.IsNullOrWhiteSpace(probe.stdout); + if (!_btrfsAvailable) + _logger.LogWarning("btrfs cli not found in path. Compression will be skipped."); + + var prop = isWine + ? RunProcessShell($"btrfs property set -- {QuoteSingle(linuxPath)} compression none", timeoutMs: 15000) + : RunProcessDirect("btrfs", ["property", "set", "--", linuxPath, "compression", "none"], "/", 15000); + + if (prop.ok) _logger.LogTrace("Set per-file 'compression none' on {file}", linuxPath); + else _logger.LogTrace("btrfs property set failed for {file} (exit {code}): {err}", linuxPath, prop.exitCode, prop.stderr); + + var defrag = isWine + ? RunProcessShell($"btrfs filesystem defragment -f -- {QuoteSingle(linuxPath)}", timeoutMs: 60000) + : RunProcessDirect("btrfs", ["filesystem", "defragment", "-f", "--", linuxPath], "/", 60000); + + if (!defrag.ok) { - _logger.LogWarning("Cannot safely decompress {file}: mount options contains compress-force ({opts}).", linuxPath, opts); + _logger.LogWarning("btrfs defragment (decompress) failed for {file} (exit {code}): {err}", + linuxPath, defrag.exitCode, defrag.stderr); return false; } - if (hasCompress) - { - var setCmd = $"btrfs property set -- {QuoteDouble(linuxPath)} compression none"; - var (okSet, _, errSet, codeSet) = isWine - ? RunProcessShell(setCmd) - : RunProcessDirect("btrfs", ["property", "set", "--", linuxPath, "compression", "none"]); - - if (!okSet) - { - _logger.LogWarning("Failed to set 'compression none' on {file}, please check drive options (exit code is: {code}): {err}", linuxPath, codeSet, errSet); - return false; - } - _logger.LogTrace("Set per-file 'compression none' on {file}", linuxPath); - } - - if (!IsBtrfsCompressedFile(linuxPath)) - { - _logger.LogTrace("{file} is not compressed, skipping decompression completely", linuxPath); - return true; - } - - var (ok, stdout, stderr, code) = isWine - ? RunProcessShell($"btrfs filesystem defragment -- {QuoteDouble(linuxPath)}") - : RunProcessDirect("btrfs", ["filesystem", "defragment", "--", linuxPath]); - - if (!ok) - { - _logger.LogWarning("btrfs defragment (decompress) failed for {file} (exit code is: {code}): {stderr}", - linuxPath, code, stderr); - return false; - } - - if (!string.IsNullOrWhiteSpace(stdout)) - _logger.LogTrace("btrfs defragment output for {file}: {out}", linuxPath, stdout.Trim()); + if (!string.IsNullOrWhiteSpace(defrag.stdout)) + _logger.LogTrace("btrfs defragment output for {file}: {out}", linuxPath, defrag.stdout.Trim()); _logger.LogInformation("Decompressed (rewritten uncompressed) Btrfs file: {file}", linuxPath); + + try + { + if (_fragBatch != null) + { + var compressed = _fragBatch.IsCompressedAsync(linuxPath, _compactionCts.Token).GetAwaiter().GetResult(); + if (compressed) + _logger.LogTrace("Post-check: {file} still shows 'compressed' flag (may be stale).", linuxPath); + } + } + catch { /* ignore verification noisy */ } + return true; } catch (Exception ex) @@ -432,18 +502,18 @@ public sealed class FileCompactor : IDisposable /// /// Path of the compressed file /// Decompressing state - private bool DecompressWOFFile(string path) + private bool DecompressWOFFile(string path, int workerID) { //Check if its already been compressed if (TryIsWofExternal(path, out bool isExternal, out int algo)) { if (!isExternal) { - _logger.LogTrace("Already decompressed file: {file}", path); + _logger.LogTrace("[W{worker}] Already decompressed file: {file}", workerID, path); return true; } var compressString = ((CompressionAlgorithm)algo).ToString(); - _logger.LogTrace("WOF compression (algo={algo}) detected for {file}", compressString, path); + _logger.LogTrace("[W{worker}] WOF compression (algo={algo}) detected for {file}", workerID, compressString, path); } //This will attempt to start WOF thread. @@ -457,15 +527,15 @@ public sealed class FileCompactor : IDisposable // 342 error code means its been decompressed after the control, we handle it as it succesfully been decompressed. if (err == 342) { - _logger.LogTrace("Successfully decompressed NTFS file {file}", path); + _logger.LogTrace("[W{worker}] Successfully decompressed NTFS file {file}", workerID, path); return true; } - _logger.LogWarning("DeviceIoControl failed for {file} with Win32 error {err}", path, err); + _logger.LogWarning("[W{worker}] DeviceIoControl failed for {file} with Win32 error {err}", workerID, path, err); return false; } - _logger.LogTrace("Successfully decompressed NTFS file {file}", path); + _logger.LogTrace("[W{worker}] Successfully decompressed NTFS file {file}", workerID, path); return true; }); } @@ -478,6 +548,7 @@ public sealed class FileCompactor : IDisposable /// Converted path to be used in Linux private string ToLinuxPathIfWine(string path, bool isWine, bool preferShell = true) { + //Return if not wine if (!isWine || !IsProbablyWine()) return path; @@ -539,7 +610,7 @@ public sealed class FileCompactor : IDisposable /// Compessing state private bool WOFCompressFile(string path) { - int size = Marshal.SizeOf(); + int size = Marshal.SizeOf(); IntPtr efInfoPtr = Marshal.AllocHGlobal(size); try @@ -592,7 +663,7 @@ public sealed class FileCompactor : IDisposable { try { - uint buf = (uint)Marshal.SizeOf(); + uint buf = (uint)Marshal.SizeOf(); int result = WofIsExternalFile(filePath, out int isExternal, out _, out var info, ref buf); if (result != 0 || isExternal == 0) return false; @@ -621,7 +692,7 @@ public sealed class FileCompactor : IDisposable algorithm = 0; try { - uint buf = (uint)Marshal.SizeOf(); + uint buf = (uint)Marshal.SizeOf(); int hr = WofIsExternalFile(path, out int ext, out _, out var info, ref buf); if (hr == 0 && ext != 0) { @@ -630,13 +701,13 @@ public sealed class FileCompactor : IDisposable } return true; } - catch (DllNotFoundException) + catch (DllNotFoundException) { - return false; + return false; } - catch (EntryPointNotFoundException) - { - return false; + catch (EntryPointNotFoundException) + { + return false; } } @@ -651,8 +722,7 @@ public sealed class FileCompactor : IDisposable { try { - bool windowsProc = RuntimeInformation.IsOSPlatform(OSPlatform.Windows); - string linuxPath = windowsProc ? ResolveLinuxPathForWine(path) : path; + string linuxPath = _isWindows ? ResolveLinuxPathForWine(path) : path; var task = _fragBatch.IsCompressedAsync(linuxPath, _compactionCts.Token); @@ -685,7 +755,6 @@ public sealed class FileCompactor : IDisposable try { var (winPath, linuxPath) = ResolvePathsForBtrfs(path); - bool isWindowsProc = RuntimeInformation.IsOSPlatform(OSPlatform.Windows); if (IsBtrfsCompressedFile(linuxPath)) { @@ -699,8 +768,13 @@ public sealed class FileCompactor : IDisposable return false; } + var probe = RunProcessShell("command -v btrfs || which btrfs", timeoutMs: 5000); + var _btrfsAvailable = probe.ok && !string.IsNullOrWhiteSpace(probe.stdout); + if (!_btrfsAvailable) + _logger.LogWarning("btrfs cli not found in path. Compression will be skipped."); + (bool ok, string stdout, string stderr, int code) = - isWindowsProc + _isWindows ? RunProcessShell($"btrfs filesystem defragment -clzo -- {QuoteSingle(linuxPath)}") : RunProcessDirect("btrfs", ["filesystem", "defragment", "-clzo", "--", linuxPath]); @@ -783,9 +857,10 @@ public sealed class FileCompactor : IDisposable RedirectStandardOutput = true, RedirectStandardError = true, UseShellExecute = false, - CreateNoWindow = true + CreateNoWindow = true, + WorkingDirectory = workingDir ?? "/", }; - if (!string.IsNullOrEmpty(workingDir)) psi.WorkingDirectory = workingDir; + foreach (var a in args) psi.ArgumentList.Add(a); EnsureUnixPathEnv(psi); @@ -799,8 +874,18 @@ public sealed class FileCompactor : IDisposable } int code; - try { code = proc.ExitCode; } catch { code = -1; } - return (code == 0, so2, se2, code); + try { code = proc.ExitCode; } + catch { code = -1; } + + bool ok = code == 0; + + if (!ok && code == -1 && + string.IsNullOrWhiteSpace(se2) && !string.IsNullOrWhiteSpace(so2)) + { + ok = true; + } + + return (ok, so2, se2, code); } /// @@ -811,15 +896,14 @@ public sealed class FileCompactor : IDisposable /// State of the process, output of the process and error with exit code private (bool ok, string stdout, string stderr, int exitCode) RunProcessShell(string command, string? workingDir = null, int timeoutMs = 60000) { - var psi = new ProcessStartInfo("/bin/bash") { RedirectStandardOutput = true, RedirectStandardError = true, UseShellExecute = false, - CreateNoWindow = true + CreateNoWindow = true, + WorkingDirectory = workingDir ?? "/", }; - if (!string.IsNullOrEmpty(workingDir)) psi.WorkingDirectory = workingDir; // Use a Login shell so PATH includes /usr/sbin etc. AKA -lc for login shell psi.ArgumentList.Add("-lc"); @@ -836,65 +920,72 @@ public sealed class FileCompactor : IDisposable } int code; - try { code = proc.ExitCode; } catch { code = -1; } - return (code == 0, so2, se2, code); + try { code = proc.ExitCode; } + catch { code = -1; } + + bool ok = code == 0; + + if (!ok && code == -1 && string.IsNullOrWhiteSpace(se2) && !string.IsNullOrWhiteSpace(so2)) + { + ok = true; + } + + return (ok, so2, se2, code); } /// /// Checking the process result for shell or direct processes /// /// Process - /// How long when timeout is gotten + /// How long when timeout goes over threshold /// Cancellation Token /// Multiple variables - private (bool success, string testy, string testi) CheckProcessResult(Process proc, int timeoutMs, CancellationToken token) + private (bool success, string output, string errorCode) CheckProcessResult(Process proc, int timeoutMs, CancellationToken token) { var outTask = proc.StandardOutput.ReadToEndAsync(token); var errTask = proc.StandardError.ReadToEndAsync(token); var bothTasks = Task.WhenAll(outTask, errTask); - //On wine, we dont wanna use waitforexit as it will be always broken and giving an error. - if (_dalamudUtilService.IsWine) - { - var finished = Task.WhenAny(bothTasks, Task.Delay(timeoutMs, token)).GetAwaiter().GetResult(); - if (finished != bothTasks) - { - try - { - proc.Kill(entireProcessTree: true); - Task.WaitAll([outTask, errTask], 1000, token); - } - catch - { - // ignore this - } - var so = outTask.IsCompleted ? outTask.Result : ""; - var se = errTask.IsCompleted ? errTask.Result : "timeout"; - return (false, so, se); - } + var finished = Task.WhenAny(bothTasks, Task.Delay(timeoutMs, token)).GetAwaiter().GetResult(); - var stderr = errTask.Result; - var ok = string.IsNullOrWhiteSpace(stderr); - return (ok, outTask.Result, stderr); + if (token.IsCancellationRequested) + return KillProcess(proc, outTask, errTask, token); + + if (finished != bothTasks) + return KillProcess(proc, outTask, errTask, token); + + bool isWine = _dalamudUtilService?.IsWine ?? false; + if (!isWine) + { + try { proc.WaitForExit(); } catch { /* ignore quirks */ } + } + else + { + var sw = Stopwatch.StartNew(); + while (!proc.HasExited && sw.ElapsedMilliseconds < 75) + Thread.Sleep(5); } - // On linux, we can use it as we please - if (!proc.WaitForExit(timeoutMs)) - { - try - { - proc.Kill(entireProcessTree: true); - Task.WaitAll([outTask, errTask], 1000, token); - } - catch - { - // ignore this - } - return (false, outTask.IsCompleted ? outTask.Result : "", "timeout"); - } + var stdout = outTask.Status == TaskStatus.RanToCompletion ? outTask.Result : ""; + var stderr = errTask.Status == TaskStatus.RanToCompletion ? errTask.Result : ""; - Task.WaitAll(outTask, errTask); - return (true, outTask.Result, errTask.Result); + int code = -1; + try { if (proc.HasExited) code = proc.ExitCode; } catch { /* Wine may still throw */ } + + bool ok = code == 0 || (isWine && string.IsNullOrWhiteSpace(stderr)); + + return (ok, stdout, stderr); + + static (bool success, string output, string errorCode) KillProcess( + Process proc, Task outTask, Task errTask, CancellationToken token) + { + try { proc.Kill(entireProcessTree: true); } catch { /* ignore */ } + try { Task.WaitAll([outTask, errTask], 1000, token); } catch { /* ignore */ } + + var so = outTask.IsCompleted ? outTask.Result : ""; + var se = errTask.IsCompleted ? errTask.Result : "canceled/timeout"; + return (false, so, se); + } } /// @@ -957,7 +1048,7 @@ public sealed class FileCompactor : IDisposable /// Process the queue with, meant for a worker/thread /// /// Cancellation token for the worker whenever it needs to be stopped - private async Task ProcessQueueWorkerAsync(CancellationToken token) + private async Task ProcessQueueWorkerAsync(CancellationToken token, int workerId) { try { @@ -973,7 +1064,7 @@ public sealed class FileCompactor : IDisposable try { if (_lightlessConfigService.Current.UseCompactor && File.Exists(filePath)) - CompactFile(filePath); + CompactFile(filePath, workerId); } finally { @@ -992,8 +1083,8 @@ public sealed class FileCompactor : IDisposable } } } - catch (OperationCanceledException) - { + catch (OperationCanceledException) + { // Shutting down worker, this exception is expected } } @@ -1005,7 +1096,7 @@ public sealed class FileCompactor : IDisposable /// Linux path to be used in Linux private string ResolveLinuxPathForWine(string windowsPath) { - var (ok, outp, _, _) = RunProcessShell($"winepath -u {QuoteSingle(windowsPath)}", null, 5000); + var (ok, outp, _, _) = RunProcessShell($"winepath -u {QuoteSingle(windowsPath)}", workingDir: null, 5000); if (ok && !string.IsNullOrWhiteSpace(outp)) return outp.Trim(); return ToLinuxPathIfWine(windowsPath, isWine: true); } @@ -1029,9 +1120,7 @@ public sealed class FileCompactor : IDisposable /// private (string windowsPath, string linuxPath) ResolvePathsForBtrfs(string path) { - bool isWindowsProc = RuntimeInformation.IsOSPlatform(OSPlatform.Windows); - - if (!isWindowsProc) + if (!_isWindows) return (path, path); var (ok, outp, _, _) = RunProcessShell($"winepath -u {QuoteSingle(path)}", workingDir: null, 5000); @@ -1050,7 +1139,7 @@ public sealed class FileCompactor : IDisposable { try { - if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) + if (_isWindows) { using var _ = new FileStream(winePath, FileMode.Open, FileAccess.Read, FileShare.ReadWrite); } @@ -1085,17 +1174,18 @@ public sealed class FileCompactor : IDisposable } - [DllImport("kernel32.dll", SetLastError = true)] - private static extern bool DeviceIoControl(SafeFileHandle hDevice, uint dwIoControlCode, IntPtr lpInBuffer, uint nInBufferSize, IntPtr lpOutBuffer, uint nOutBufferSize, out uint lpBytesReturned, IntPtr lpOverlapped); + [LibraryImport("kernel32.dll", SetLastError = true)] + private static partial uint GetCompressedFileSizeW([MarshalAs(UnmanagedType.LPWStr)] string lpFileName, out uint lpFileSizeHigh); - [DllImport("kernel32.dll")] - private static extern uint GetCompressedFileSizeW([In, MarshalAs(UnmanagedType.LPWStr)] string lpFileName, [Out, MarshalAs(UnmanagedType.U4)] out uint lpFileSizeHigh); + [LibraryImport("kernel32.dll", SetLastError = true)] + [return: MarshalAs(UnmanagedType.Bool)] + private static partial bool DeviceIoControl(SafeFileHandle hDevice, uint dwIoControlCode, IntPtr lpInBuffer, uint nInBufferSize, IntPtr lpOutBuffer, uint nOutBufferSize, out uint lpBytesReturned, IntPtr lpOverlapped); - [DllImport("WofUtil.dll")] - private static extern int WofIsExternalFile([MarshalAs(UnmanagedType.LPWStr)] string Filepath, out int IsExternalFile, out uint Provider, out WOF_FILE_COMPRESSION_INFO_V1 Info, ref uint BufferLength); + [LibraryImport("WofUtil.dll")] + private static partial int WofIsExternalFile([MarshalAs(UnmanagedType.LPWStr)] string Filepath, out int IsExternalFile, out uint Provider, out WofFileCompressionInfoV1 Info, ref uint BufferLength); - [DllImport("WofUtil.dll", SetLastError = true)] - private static extern int WofSetFileDataLocation(SafeFileHandle FileHandle, ulong Provider, IntPtr ExternalFileInfo, ulong Length); + [LibraryImport("WofUtil.dll")] + private static partial int WofSetFileDataLocation(SafeFileHandle FileHandle, ulong Provider, IntPtr ExternalFileInfo, ulong Length); private static string QuoteSingle(string s) => "'" + s.Replace("'", "'\\''", StringComparison.Ordinal) + "'"; @@ -1103,7 +1193,11 @@ public sealed class FileCompactor : IDisposable public void Dispose() { + //Cleanup of gates and frag service _fragBatch?.Dispose(); + _btrfsGate?.Dispose(); + _globalGate?.Dispose(); + _compactionQueue.Writer.TryComplete(); _compactionCts.Cancel(); @@ -1111,8 +1205,8 @@ public sealed class FileCompactor : IDisposable { Task.WaitAll([.. _workers.Where(t => t != null)], TimeSpan.FromSeconds(5)); } - catch - { + catch + { // Ignore this catch on the dispose } finally diff --git a/LightlessSync/Interop/DalamudLogger.cs b/LightlessSync/Interop/DalamudLogger.cs index 3a833b9..24fcac2 100644 --- a/LightlessSync/Interop/DalamudLogger.cs +++ b/LightlessSync/Interop/DalamudLogger.cs @@ -20,7 +20,10 @@ internal sealed class DalamudLogger : ILogger _hasModifiedGameFiles = hasModifiedGameFiles; } - public IDisposable BeginScope(TState state) => default!; + IDisposable? ILogger.BeginScope(TState state) + { + return default!; + } public bool IsEnabled(LogLevel logLevel) { diff --git a/LightlessSync/Services/Compactor/BatchFileFragService.cs b/LightlessSync/Services/Compactor/BatchFileFragService.cs index b31919e..b99934b 100644 --- a/LightlessSync/Services/Compactor/BatchFileFragService.cs +++ b/LightlessSync/Services/Compactor/BatchFileFragService.cs @@ -92,13 +92,13 @@ namespace LightlessSync.Services.Compactor } if ((flushAt - DateTime.UtcNow) <= TimeSpan.Zero) break; - try - { - await Task.Delay(TimeSpan.FromMilliseconds(5), _cts.Token).ConfigureAwait(false); + try + { + await Task.Delay(TimeSpan.FromMilliseconds(5), _cts.Token).ConfigureAwait(false); } - catch - { - break; + catch + { + break; } } @@ -124,8 +124,8 @@ namespace LightlessSync.Services.Compactor } } } - catch (OperationCanceledException) - { + catch (OperationCanceledException) + { //Shutting down worker, exception called } } @@ -145,17 +145,13 @@ namespace LightlessSync.Services.Compactor if (_useShell) { - var inner = "filefrag -v " + string.Join(' ', list.Select(QuoteSingle)); + var inner = "filefrag -v -- " + string.Join(' ', list.Select(QuoteSingle)); res = _runShell(inner, timeoutMs: 15000, workingDir: "/"); } else { - var args = new List { "-v" }; - foreach (var path in list) - { - args.Add(' ' + path); - } - + var args = new List { "-v", "--" }; + args.AddRange(list); res = _runDirect("filefrag", args, workingDir: "/", timeoutMs: 15000); } @@ -200,7 +196,7 @@ namespace LightlessSync.Services.Compactor /// Regex of the File Size return on the Linux/Wine systems, giving back the amount /// /// Regex of the File Size - [GeneratedRegex(@"^File size of (/.+?) is ", RegexOptions.ExplicitCapture | RegexOptions.CultureInvariant,matchTimeoutMilliseconds: 500)] + [GeneratedRegex(@"^File size of (/.+?) is ", RegexOptions.ExplicitCapture | RegexOptions.CultureInvariant, matchTimeoutMilliseconds: 500)] private static partial Regex SizeRegex(); /// diff --git a/LightlessSync/UI/BroadcastUI.cs b/LightlessSync/UI/BroadcastUI.cs index c760a45..60e064f 100644 --- a/LightlessSync/UI/BroadcastUI.cs +++ b/LightlessSync/UI/BroadcastUI.cs @@ -22,7 +22,7 @@ namespace LightlessSync.UI private readonly UiSharedService _uiSharedService; private readonly BroadcastScannerService _broadcastScannerService; - private IReadOnlyList _allSyncshells; + private IReadOnlyList _allSyncshells = Array.Empty(); private string _userUid = string.Empty; private readonly List<(string Label, string? GID, bool IsAvailable)> _syncshellOptions = new(); diff --git a/LightlessSync/UI/CharaDataHubUi.Functions.cs b/LightlessSync/UI/CharaDataHubUi.Functions.cs index 665e640..ccef174 100644 --- a/LightlessSync/UI/CharaDataHubUi.Functions.cs +++ b/LightlessSync/UI/CharaDataHubUi.Functions.cs @@ -13,13 +13,15 @@ internal sealed partial class CharaDataHubUi AccessTypeDto.AllPairs => "All Pairs", AccessTypeDto.ClosePairs => "Direct Pairs", AccessTypeDto.Individuals => "Specified", - AccessTypeDto.Public => "Everyone" + AccessTypeDto.Public => "Everyone", + _ => throw new NotSupportedException() }; private static string GetShareTypeString(ShareTypeDto dto) => dto switch { ShareTypeDto.Private => "Code Only", - ShareTypeDto.Shared => "Shared" + ShareTypeDto.Shared => "Shared", + _ => throw new NotSupportedException() }; private static string GetWorldDataTooltipText(PoseEntryExtended poseEntry) @@ -31,7 +33,7 @@ internal sealed partial class CharaDataHubUi private void GposeMetaInfoAction(Action gposeActionDraw, string actionDescription, CharaDataMetaInfoExtendedDto? dto, bool hasValidGposeTarget, bool isSpawning) { - StringBuilder sb = new StringBuilder(); + StringBuilder sb = new(); sb.AppendLine(actionDescription); bool isDisabled = false; diff --git a/LightlessSync/UI/CompactUI.cs b/LightlessSync/UI/CompactUI.cs index cc8d326..723d3ae 100644 --- a/LightlessSync/UI/CompactUI.cs +++ b/LightlessSync/UI/CompactUI.cs @@ -29,6 +29,7 @@ using System.Globalization; using System.Linq; using System.Numerics; using System.Reflection; +using System.Runtime.InteropServices; namespace LightlessSync.UI; @@ -105,7 +106,7 @@ public class CompactUi : WindowMediatorSubscriberBase _renamePairTagUi = renameTagUi; _ipcManager = ipcManager; _broadcastService = broadcastService; - _tabMenu = new TopTabMenu(Mediator, _apiController, _pairManager, _uiSharedService, pairRequestService, dalamudUtilService, lightlessNotificationService); + _tabMenu = new TopTabMenu(Mediator, _apiController, _pairManager, _uiSharedService, pairRequestService); AllowPinning = true; AllowClickthrough = false; @@ -285,11 +286,10 @@ public class CompactUi : WindowMediatorSubscriberBase private void DrawPairs() { - var ySize = _transferPartHeight == 0 + float ySize = Math.Abs(_transferPartHeight) < 0.0001f ? 1 - : (ImGui.GetWindowContentRegionMax().Y - ImGui.GetWindowContentRegionMin().Y - + ImGui.GetTextLineHeight() - ImGui.GetStyle().WindowPadding.Y - ImGui.GetStyle().WindowBorderSize) - _transferPartHeight - ImGui.GetCursorPosY(); - + : ((ImGui.GetWindowContentRegionMax().Y - ImGui.GetWindowContentRegionMin().Y + + ImGui.GetTextLineHeight() - ImGui.GetStyle().WindowPadding.Y - ImGui.GetStyle().WindowBorderSize) - _transferPartHeight - ImGui.GetCursorPosY()); ImGui.BeginChild("list", new Vector2(_windowContentWidth, ySize), border: false); foreach (var item in _drawFolders) @@ -470,6 +470,7 @@ public class CompactUi : WindowMediatorSubscriberBase return new DownloadSummary(totalFiles, transferredFiles, transferredBytes, totalBytes); } + [StructLayout(LayoutKind.Auto)] private readonly record struct DownloadSummary(int TotalFiles, int TransferredFiles, long TransferredBytes, long TotalBytes) { public bool HasDownloads => TotalFiles > 0 || TotalBytes > 0; diff --git a/LightlessSync/UI/EditProfileUi.cs b/LightlessSync/UI/EditProfileUi.cs index 5dedf81..ef64f7f 100644 --- a/LightlessSync/UI/EditProfileUi.cs +++ b/LightlessSync/UI/EditProfileUi.cs @@ -297,7 +297,6 @@ public class EditProfileUi : WindowMediatorSubscriberBase ImGui.Dummy(new Vector2(5)); var font = UiBuilder.MonoFont; - var playerUID = _apiController.UID; var playerDisplay = _apiController.DisplayName; var previewTextColor = textEnabled ? textColor : Vector4.One; diff --git a/LightlessSync/UI/Handlers/IdDisplayHandler.cs b/LightlessSync/UI/Handlers/IdDisplayHandler.cs index 4d362a9..4955b48 100644 --- a/LightlessSync/UI/Handlers/IdDisplayHandler.cs +++ b/LightlessSync/UI/Handlers/IdDisplayHandler.cs @@ -154,13 +154,11 @@ public class IdDisplayHandler Vector2 itemMin; Vector2 itemMax; - Vector2 textSize; using (ImRaii.PushFont(font, textIsUid)) { SeStringUtils.RenderSeStringWithHitbox(seString, rowStart, font, pair.UserData.UID); itemMin = ImGui.GetItemRectMin(); itemMax = ImGui.GetItemRectMax(); - //textSize = itemMax - itemMin; } if (useHighlight) @@ -202,7 +200,7 @@ public class IdDisplayHandler if (ImGui.IsItemHovered()) { - if (!string.Equals(_lastMouseOverUid, id)) + if (!string.Equals(_lastMouseOverUid, id, StringComparison.Ordinal)) { _popupTime = DateTime.UtcNow.AddSeconds(_lightlessConfigService.Current.ProfileDelay); } @@ -223,7 +221,7 @@ public class IdDisplayHandler } else { - if (string.Equals(_lastMouseOverUid, id)) + if (string.Equals(_lastMouseOverUid, id, StringComparison.Ordinal)) { _mediator.Publish(new ProfilePopoutToggle(Pair: null)); _lastMouseOverUid = string.Empty; diff --git a/LightlessSync/UI/StandaloneProfileUi.cs b/LightlessSync/UI/StandaloneProfileUi.cs index 6ef21d5..ebe5a0b 100644 --- a/LightlessSync/UI/StandaloneProfileUi.cs +++ b/LightlessSync/UI/StandaloneProfileUi.cs @@ -139,7 +139,7 @@ public class StandaloneProfileUi : WindowMediatorSubscriberBase } } - if (Pair.UserPair.Groups.Any()) + if (Pair.UserPair?.Groups?.Count > 0) { ImGui.TextUnformatted("Paired through Syncshells:"); foreach (var group in Pair.UserPair.Groups) diff --git a/LightlessSync/UI/SyncshellAdminUI.cs b/LightlessSync/UI/SyncshellAdminUI.cs index be8e1d4..0967290 100644 --- a/LightlessSync/UI/SyncshellAdminUI.cs +++ b/LightlessSync/UI/SyncshellAdminUI.cs @@ -222,6 +222,13 @@ public class SyncshellAdminUI : WindowMediatorSubscriberBase { ImGui.Dummy(new Vector2(5)); + if (_profileData == null) + { + UiSharedService.ColorTextWrapped("Failed to load profile data.", ImGuiColors.DalamudRed); + ImGui.TreePop(); + return; + } + if (!_profileImage.SequenceEqual(_profileData.ImageData.Value)) { _profileImage = _profileData.ImageData.Value; @@ -379,7 +386,7 @@ public class SyncshellAdminUI : WindowMediatorSubscriberBase UiSharedService.AttachToolTip("Clears your profile description text"); ImGui.Separator(); ImGui.TextUnformatted($"Profile Options:"); - var isNsfw = _profileData.IsNsfw; + var isNsfw = _profileData?.IsNsfw ?? false; if (ImGui.Checkbox("Profile is NSFW", ref isNsfw)) { _ = _apiController.GroupSetProfile(new GroupProfileDto(new GroupData(GroupFullInfo.Group.AliasOrGID), Description: null, Tags: null, PictureBase64: null, BannerBase64: null, IsNsfw: isNsfw, IsDisabled: null)); diff --git a/LightlessSync/UI/TopTabMenu.cs b/LightlessSync/UI/TopTabMenu.cs index b4327c0..f0036ba 100644 --- a/LightlessSync/UI/TopTabMenu.cs +++ b/LightlessSync/UI/TopTabMenu.cs @@ -9,7 +9,6 @@ using LightlessSync.LightlessConfiguration.Models; using LightlessSync.PlayerData.Pairs; using LightlessSync.Services; using LightlessSync.Services.Mediator; -using LightlessSync.Utils; using LightlessSync.WebAPI; using System.Numerics; @@ -24,27 +23,21 @@ public class TopTabMenu private readonly PairManager _pairManager; private readonly PairRequestService _pairRequestService; - private readonly DalamudUtilService _dalamudUtilService; private readonly HashSet _pendingPairRequestActions = new(StringComparer.Ordinal); - private bool _pairRequestsExpanded; // useless for now - private int _lastRequestCount; private readonly UiSharedService _uiSharedService; - private readonly NotificationService _lightlessNotificationService; private string _filter = string.Empty; private int _globalControlCountdown = 0; private float _pairRequestsHeight = 150f; private string _pairToAdd = string.Empty; - private SelectedTab _selectedTab = SelectedTab.None; - public TopTabMenu(LightlessMediator lightlessMediator, ApiController apiController, PairManager pairManager, UiSharedService uiSharedService, PairRequestService pairRequestService, DalamudUtilService dalamudUtilService, NotificationService lightlessNotificationService) + + public TopTabMenu(LightlessMediator lightlessMediator, ApiController apiController, PairManager pairManager, UiSharedService uiSharedService, PairRequestService pairRequestService) { _lightlessMediator = lightlessMediator; _apiController = apiController; _pairManager = pairManager; _pairRequestService = pairRequestService; - _dalamudUtilService = dalamudUtilService; _uiSharedService = uiSharedService; - _lightlessNotificationService = lightlessNotificationService; } private enum SelectedTab @@ -70,13 +63,17 @@ public class TopTabMenu _filter = value; } } - private SelectedTab TabSelection + + private SelectedTab GetTabSelection() { - get => _selectedTab; set - { - _selectedTab = value; - } + return _selectedTab; } + + private void SetTabSelection(SelectedTab value) + { + _selectedTab = value; + } + public void Draw() { var availableWidth = ImGui.GetWindowContentRegionMax().X - ImGui.GetWindowContentRegionMin().X; @@ -85,7 +82,7 @@ public class TopTabMenu var buttonY = _uiSharedService.GetIconButtonSize(FontAwesomeIcon.Pause).Y; var buttonSize = new Vector2(buttonX, buttonY); var drawList = ImGui.GetWindowDrawList(); - var underlineColor = ImGui.GetColorU32(UIColors.Get("LightlessPurpleActive")); // ImGui.GetColorU32(ImGuiCol.Separator); + var underlineColor = ImGui.GetColorU32(UIColors.Get("LightlessPurpleActive")); var btncolor = ImRaii.PushColor(ImGuiCol.Button, ImGui.ColorConvertFloat4ToU32(new(0, 0, 0, 0))); ImGuiHelpers.ScaledDummy(spacing.Y / 2f); @@ -95,11 +92,11 @@ public class TopTabMenu var x = ImGui.GetCursorScreenPos(); if (ImGui.Button(FontAwesomeIcon.User.ToIconString(), buttonSize)) { - TabSelection = TabSelection == SelectedTab.Individual ? SelectedTab.None : SelectedTab.Individual; + SetTabSelection(GetTabSelection() == SelectedTab.Individual ? SelectedTab.None : SelectedTab.Individual); } ImGui.SameLine(); var xAfter = ImGui.GetCursorScreenPos(); - if (TabSelection == SelectedTab.Individual) + if (GetTabSelection() == SelectedTab.Individual) drawList.AddLine(x with { Y = x.Y + buttonSize.Y + spacing.Y }, xAfter with { Y = xAfter.Y + buttonSize.Y + spacing.Y, X = xAfter.X - spacing.X }, underlineColor, 2); @@ -111,11 +108,11 @@ public class TopTabMenu var x = ImGui.GetCursorScreenPos(); if (ImGui.Button(FontAwesomeIcon.Users.ToIconString(), buttonSize)) { - TabSelection = TabSelection == SelectedTab.Syncshell ? SelectedTab.None : SelectedTab.Syncshell; + SetTabSelection(GetTabSelection() == SelectedTab.Syncshell ? SelectedTab.None : SelectedTab.Syncshell); } ImGui.SameLine(); var xAfter = ImGui.GetCursorScreenPos(); - if (TabSelection == SelectedTab.Syncshell) + if (GetTabSelection() == SelectedTab.Syncshell) drawList.AddLine(x with { Y = x.Y + buttonSize.Y + spacing.Y }, xAfter with { Y = xAfter.Y + buttonSize.Y + spacing.Y, X = xAfter.X - spacing.X }, underlineColor, 2); @@ -128,12 +125,12 @@ public class TopTabMenu var x = ImGui.GetCursorScreenPos(); if (ImGui.Button(FontAwesomeIcon.Compass.ToIconString(), buttonSize)) { - TabSelection = TabSelection == SelectedTab.Lightfinder ? SelectedTab.None : SelectedTab.Lightfinder; + SetTabSelection(GetTabSelection() == SelectedTab.Lightfinder ? SelectedTab.None : SelectedTab.Lightfinder); } ImGui.SameLine(); var xAfter = ImGui.GetCursorScreenPos(); - if (TabSelection == SelectedTab.Lightfinder) + if (GetTabSelection() == SelectedTab.Lightfinder) drawList.AddLine(x with { Y = x.Y + buttonSize.Y + spacing.Y }, xAfter with { Y = xAfter.Y + buttonSize.Y + spacing.Y, X = xAfter.X - spacing.X }, underlineColor, 2); @@ -146,12 +143,12 @@ public class TopTabMenu var x = ImGui.GetCursorScreenPos(); if (ImGui.Button(FontAwesomeIcon.UserCog.ToIconString(), buttonSize)) { - TabSelection = TabSelection == SelectedTab.UserConfig ? SelectedTab.None : SelectedTab.UserConfig; + SetTabSelection(GetTabSelection() == SelectedTab.UserConfig ? SelectedTab.None : SelectedTab.UserConfig); } ImGui.SameLine(); var xAfter = ImGui.GetCursorScreenPos(); - if (TabSelection == SelectedTab.UserConfig) + if (GetTabSelection() == SelectedTab.UserConfig) drawList.AddLine(x with { Y = x.Y + buttonSize.Y + spacing.Y }, xAfter with { Y = xAfter.Y + buttonSize.Y + spacing.Y, X = xAfter.X - spacing.X }, underlineColor, 2); @@ -161,7 +158,7 @@ public class TopTabMenu ImGui.SameLine(); using (ImRaii.PushFont(UiBuilder.IconFont)) { - var x = ImGui.GetCursorScreenPos(); + ImGui.GetCursorScreenPos(); if (ImGui.Button(FontAwesomeIcon.Cog.ToIconString(), buttonSize)) { _lightlessMediator.Publish(new UiToggleMessage(typeof(SettingsUi))); @@ -175,26 +172,26 @@ public class TopTabMenu ImGuiHelpers.ScaledDummy(spacing); - if (TabSelection == SelectedTab.Individual) + if (GetTabSelection() == SelectedTab.Individual) { DrawAddPair(availableWidth, spacing.X); DrawGlobalIndividualButtons(availableWidth, spacing.X); } - else if (TabSelection == SelectedTab.Syncshell) + else if (GetTabSelection() == SelectedTab.Syncshell) { DrawSyncshellMenu(availableWidth, spacing.X); DrawGlobalSyncshellButtons(availableWidth, spacing.X); } - else if (TabSelection == SelectedTab.Lightfinder) + else if (GetTabSelection() == SelectedTab.Lightfinder) { DrawLightfinderMenu(availableWidth, spacing.X); } - else if (TabSelection == SelectedTab.UserConfig) + else if (GetTabSelection() == SelectedTab.UserConfig) { DrawUserConfig(availableWidth, spacing.X); } - if (TabSelection != SelectedTab.None) ImGuiHelpers.ScaledDummy(3f); + if (GetTabSelection() != SelectedTab.None) ImGuiHelpers.ScaledDummy(3f); DrawIncomingPairRequests(availableWidth); @@ -227,17 +224,9 @@ public class TopTabMenu var count = requests.Count; if (count == 0) { - _pairRequestsExpanded = false; - _lastRequestCount = 0; return; } - if (count > _lastRequestCount) - { - _pairRequestsExpanded = true; - } - _lastRequestCount = count; - var label = $"Incoming Pair Requests - {count}##IncomingPairRequestsHeader"; using (ImRaii.PushColor(ImGuiCol.Header, UIColors.Get("LightlessPurple"))) @@ -245,16 +234,12 @@ public class TopTabMenu using (ImRaii.PushColor(ImGuiCol.HeaderActive, UIColors.Get("LightlessPurple"))) { bool open = ImGui.CollapsingHeader(label, ImGuiTreeNodeFlags.DefaultOpen); - _pairRequestsExpanded = open; if (ImGui.IsItemHovered()) UiSharedService.AttachToolTip("Expand to view incoming pair requests."); if (open) { - var lineHeight = ImGui.GetTextLineHeightWithSpacing(); - //var desiredHeight = Math.Clamp(count * lineHeight * 2f, 130f * ImGuiHelpers.GlobalScale, 185f * ImGuiHelpers.GlobalScale); we use resize bar instead - ImGui.SetCursorPosX(ImGui.GetCursorPosX() - 2f); using (ImRaii.PushColor(ImGuiCol.ChildBg, UIColors.Get("LightlessPurple"))) @@ -300,7 +285,6 @@ public class TopTabMenu { float playerColWidth = 207f * ImGuiHelpers.GlobalScale; float receivedColWidth = 73f * ImGuiHelpers.GlobalScale; - float actionsColWidth = 50f * ImGuiHelpers.GlobalScale; ImGui.Separator(); ImGui.TextUnformatted("Player"); @@ -385,7 +369,6 @@ public class TopTabMenu try { - var myCidHash = (await _dalamudUtilService.GetCIDAsync().ConfigureAwait(false)).ToString().GetHash256(); await _apiController.TryPairWithContentId(request.HashedCid).ConfigureAwait(false); _pairRequestService.RemoveRequest(request.HashedCid); diff --git a/LightlessSync/UI/UpdateNotesUi.cs b/LightlessSync/UI/UpdateNotesUi.cs index 02e0b4d..54a45f0 100644 --- a/LightlessSync/UI/UpdateNotesUi.cs +++ b/LightlessSync/UI/UpdateNotesUi.cs @@ -25,7 +25,6 @@ public class UpdateNotesUi : WindowMediatorSubscriberBase private ChangelogFile _changelog = new(); private CreditsFile _credits = new(); private bool _scrollToTop; - private int _selectedTab; private bool _hasInitializedCollapsingHeaders; private struct Particle @@ -160,7 +159,7 @@ public class UpdateNotesUi : WindowMediatorSubscriberBase DrawParticleEffects(headerStart, extendedParticleSize); } - private void DrawGradientBackground(Vector2 headerStart, Vector2 headerEnd) + private static void DrawGradientBackground(Vector2 headerStart, Vector2 headerEnd) { var drawList = ImGui.GetWindowDrawList(); @@ -188,7 +187,7 @@ public class UpdateNotesUi : WindowMediatorSubscriberBase } } - private void DrawBottomGradient(Vector2 headerStart, Vector2 headerEnd, float width) + private static void DrawBottomGradient(Vector2 headerStart, Vector2 headerEnd, float width) { var drawList = ImGui.GetWindowDrawList(); var gradientHeight = 60f; @@ -513,7 +512,6 @@ public class UpdateNotesUi : WindowMediatorSubscriberBase { if (changelogTab) { - _selectedTab = 0; DrawChangelog(); } } @@ -524,7 +522,6 @@ public class UpdateNotesUi : WindowMediatorSubscriberBase { if (creditsTab) { - _selectedTab = 1; DrawCredits(); } } @@ -558,7 +555,7 @@ public class UpdateNotesUi : WindowMediatorSubscriberBase } } - private void DrawCreditCategory(CreditCategory category) + private static void DrawCreditCategory(CreditCategory category) { DrawFeatureSection(category.Category, UIColors.Get("LightlessBlue")); diff --git a/LightlessSync/Utils/FileSystemHelper.cs b/LightlessSync/Utils/FileSystemHelper.cs index d63b3b9..b27fb1c 100644 --- a/LightlessSync/Utils/FileSystemHelper.cs +++ b/LightlessSync/Utils/FileSystemHelper.cs @@ -1,6 +1,5 @@ using Microsoft.Extensions.Logging; using System.Collections.Concurrent; -using System.Diagnostics; using System.Runtime.InteropServices; namespace LightlessSync.Utils @@ -32,7 +31,7 @@ namespace LightlessSync.Utils { string rootPath; - if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows) && (!IsProbablyWine() || !isWine)) + if (OperatingSystem.IsWindows() && (!IsProbablyWine() || !isWine)) { var info = new FileInfo(filePath); var dir = info.Directory ?? new DirectoryInfo(filePath); @@ -50,7 +49,7 @@ namespace LightlessSync.Utils FilesystemType detected; - if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows) && (!IsProbablyWine() || !isWine)) + if (OperatingSystem.IsWindows() && (!IsProbablyWine() || !isWine)) { var root = new DriveInfo(rootPath); var format = root.DriveFormat?.ToUpperInvariant() ?? string.Empty; @@ -157,7 +156,7 @@ namespace LightlessSync.Utils return mountOptions; } - catch (Exception ex) + catch (Exception) { return string.Empty; } @@ -214,7 +213,7 @@ namespace LightlessSync.Utils if (_blockSizeCache.TryGetValue(root, out int cached)) return cached; - if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows) && !isWine) + if (OperatingSystem.IsWindows() && !isWine) { int result = GetDiskFreeSpaceW(root, out uint sectorsPerCluster, @@ -234,40 +233,6 @@ namespace LightlessSync.Utils return clusterSize; } - string realPath = fi.FullName; - if (isWine && realPath.StartsWith("Z:\\", StringComparison.OrdinalIgnoreCase)) - { - realPath = "/" + realPath.Substring(3).Replace('\\', '/'); - } - - var psi = new ProcessStartInfo - { - FileName = "/bin/bash", - Arguments = $"-c \"stat -f -c %s '{realPath.Replace("'", "'\\''")}'\"", - RedirectStandardOutput = true, - RedirectStandardError = true, - UseShellExecute = false, - CreateNoWindow = true, - WorkingDirectory = "/" - }; - - using var proc = Process.Start(psi); - - string stdout = proc?.StandardOutput.ReadToEnd().Trim() ?? ""; - string _stderr = proc?.StandardError.ReadToEnd() ?? ""; - - try { proc?.WaitForExit(); } - catch (Exception ex) { logger?.LogTrace(ex, "stat WaitForExit failed under Wine; ignoring"); } - - if (!(!int.TryParse(stdout, out int block) || block <= 0)) - { - _blockSizeCache[root] = block; - logger?.LogTrace("Filesystem block size via stat for {root}: {block}", root, block); - return block; - } - - logger?.LogTrace("stat did not return valid block size for {file}, output: {out}", fi.FullName, stdout); - _blockSizeCache[root] = _defaultBlockSize; return _defaultBlockSize; } catch (Exception ex)