Compactor multi-threaded, fixed many of IDE warnings

This commit is contained in:
cake
2025-11-14 23:56:39 +01:00
parent 3f1037dade
commit c3597b5789
14 changed files with 361 additions and 321 deletions

View File

@@ -134,13 +134,9 @@ public sealed class FileCacheManager : IHostedService
chosenLength = penumbraMatch; 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;
{
chosenPrefixed = cachePrefixed;
chosenLength = cacheMatch;
}
} }
return NormalizePrefixedPathKey(chosenPrefixed ?? normalized); return NormalizePrefixedPathKey(chosenPrefixed ?? normalized);
@@ -602,7 +598,6 @@ public sealed class FileCacheManager : IHostedService
private FileCacheEntity? GetValidatedFileCache(FileCacheEntity fileCache) private FileCacheEntity? GetValidatedFileCache(FileCacheEntity fileCache)
{ {
var resultingFileCache = ReplacePathPrefixes(fileCache); var resultingFileCache = ReplacePathPrefixes(fileCache);
//_logger.LogTrace("Validating {path}", fileCache.PrefixedFilePath);
resultingFileCache = Validate(resultingFileCache); resultingFileCache = Validate(resultingFileCache);
return resultingFileCache; return resultingFileCache;
} }
@@ -644,7 +639,7 @@ public sealed class FileCacheManager : IHostedService
return fileCache; return fileCache;
} }
public Task StartAsync(CancellationToken cancellationToken) public async Task StartAsync(CancellationToken cancellationToken)
{ {
_logger.LogInformation("Starting FileCacheManager"); _logger.LogInformation("Starting FileCacheManager");
@@ -695,14 +690,14 @@ public sealed class FileCacheManager : IHostedService
try try
{ {
_logger.LogInformation("Attempting to read {csvPath}", _csvPath); _logger.LogInformation("Attempting to read {csvPath}", _csvPath);
entries = File.ReadAllLines(_csvPath); entries = await File.ReadAllLinesAsync(_csvPath, cancellationToken).ConfigureAwait(false);
success = true; success = true;
} }
catch (Exception ex) catch (Exception ex)
{ {
attempts++; attempts++;
_logger.LogWarning(ex, "Could not open {file}, trying again", _csvPath); _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"); _logger.LogInformation("Started FileCacheManager");
return Task.CompletedTask; await Task.CompletedTask.ConfigureAwait(false);
} }
public Task StopAsync(CancellationToken cancellationToken) public async Task StopAsync(CancellationToken cancellationToken)
{ {
WriteOutFullCsv(); WriteOutFullCsv();
return Task.CompletedTask; await Task.CompletedTask.ConfigureAwait(false);
} }
} }

View File

@@ -11,7 +11,7 @@ using static LightlessSync.Utils.FileSystemHelper;
namespace LightlessSync.FileCache; namespace LightlessSync.FileCache;
public sealed class FileCompactor : IDisposable public sealed partial class FileCompactor : IDisposable
{ {
public const uint FSCTL_DELETE_EXTERNAL_BACKING = 0x90314U; public const uint FSCTL_DELETE_EXTERNAL_BACKING = 0x90314U;
public const ulong WOF_PROVIDER_FILE = 2UL; public const ulong WOF_PROVIDER_FILE = 2UL;
@@ -29,23 +29,26 @@ public sealed class FileCompactor : IDisposable
private readonly SemaphoreSlim _globalGate; private readonly SemaphoreSlim _globalGate;
//Limit btrfs gate on half of threads given to compactor. //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 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, Algorithm = (int)CompressionAlgorithm.XPRESS8K,
Flags = 0 Flags = 0
}; };
[StructLayout(LayoutKind.Sequential, Pack = 1)] [StructLayout(LayoutKind.Sequential, Pack = 1)]
private struct WOF_FILE_COMPRESSION_INFO_V1 private struct WofFileCompressionInfoV1
{ {
public int Algorithm; public int Algorithm;
public ulong Flags; public ulong Flags;
} }
private enum CompressionAlgorithm private enum CompressionAlgorithm
{ {
NO_COMPRESSION = -2, NO_COMPRESSION = -2,
LZNT1 = -1, LZNT1 = -1,
@@ -61,6 +64,7 @@ public sealed class FileCompactor : IDisposable
_logger = logger; _logger = logger;
_lightlessConfigService = lightlessConfigService; _lightlessConfigService = lightlessConfigService;
_dalamudUtilService = dalamudUtilService; _dalamudUtilService = dalamudUtilService;
_isWindows = OperatingSystem.IsWindows();
_compactionQueue = Channel.CreateUnbounded<string>(new UnboundedChannelOptions _compactionQueue = Channel.CreateUnbounded<string>(new UnboundedChannelOptions
{ {
@@ -68,29 +72,36 @@ public sealed class FileCompactor : IDisposable
SingleWriter = false SingleWriter = false
}); });
//Amount of threads given for the compactor
int workers = Math.Clamp(Math.Min(Environment.ProcessorCount / 2, 4), 1, 8); 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); _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( _workers.Add(Task.Factory.StartNew(
() => ProcessQueueWorkerAsync(_compactionCts.Token), () => ProcessQueueWorkerAsync(_compactionCts.Token, workerId),
_compactionCts.Token, _compactionCts.Token,
TaskCreationOptions.LongRunning, TaskCreationOptions.LongRunning,
TaskScheduler.Default).Unwrap()); TaskScheduler.Default).Unwrap());
} }
//Uses an batching service for the filefrag command on Linux
_fragBatch = new BatchFilefragService( _fragBatch = new BatchFilefragService(
useShell: _dalamudUtilService.IsWine, useShell: _dalamudUtilService.IsWine,
log: _logger, log: _logger,
batchSize: 64, batchSize: 64,
flushMs: 25, flushMs: 25,
runDirect: RunProcessDirect, runDirect: RunProcessDirect,
runShell: RunProcessShell runShell: RunProcessShell
); );
_logger.LogInformation("FileCompactor started with {workers} workers", workerCount); _logger.LogInformation("FileCompactor started with {workers} workers", _workerCount);
} }
public bool MassCompactRunning { get; private set; } public bool MassCompactRunning { get; private set; }
@@ -100,37 +111,90 @@ public sealed class FileCompactor : IDisposable
/// Compact the storage of the Cache Folder /// Compact the storage of the Cache Folder
/// </summary> /// </summary>
/// <param name="compress">Used to check if files needs to be compressed</param> /// <param name="compress">Used to check if files needs to be compressed</param>
public void CompactStorage(bool compress) public void CompactStorage(bool compress, int? maxDegree = null)
{ {
MassCompactRunning = true; MassCompactRunning = true;
try try
{ {
var allFiles = Directory.EnumerateFiles(_lightlessConfigService.Current.CacheFolder).ToList(); var folder = _lightlessConfigService.Current.CacheFolder;
int total = allFiles.Count; if (string.IsNullOrWhiteSpace(folder) || !Directory.Exists(folder))
int current = 0;
foreach (var file in allFiles)
{ {
current++; _logger.LogWarning("Filecompacator couldnt find your Cache folder: {folder}", folder);
Progress = $"{current}/{total}"; 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 try
{ {
// Compress or decompress files try
if (compress) {
CompactFile(file); if (compress)
else {
DecompressFile(file); 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)
{ return workerId;
_logger.LogWarning(ex, "Error compacting/decompressing file {file}", file); },
} localFinally: _ =>
} {
//Ignore local finally for now
});
}
catch (OperationCanceledException ex)
{
_logger.LogDebug(ex, "Mass compaction call got cancelled, shutting off compactor.");
} }
finally finally
{ {
@@ -139,6 +203,7 @@ public sealed class FileCompactor : IDisposable
} }
} }
/// <summary> /// <summary>
/// Write all bytes into a directory async /// Write all bytes into a directory async
/// </summary> /// </summary>
@@ -197,24 +262,32 @@ public sealed class FileCompactor : IDisposable
{ {
try try
{ {
bool isWindowsProc = RuntimeInformation.IsOSPlatform(OSPlatform.Windows); bool isWine = _dalamudUtilService?.IsWine ?? false;
var (_, linuxPath) = ResolvePathsForBtrfs(fileInfo.FullName);
var (ok, output, err, code) = string linuxPath = isWine ? ToLinuxPathIfWine(fileInfo.FullName, isWine)
isWindowsProc : fileInfo.FullName;
? RunProcessShell($"stat -c='%b' {QuoteSingle(linuxPath)}", workingDir: null, 10000)
: RunProcessDirect("stat", ["-c='%b'", linuxPath], workingDir: null, 10000);
if (ok && long.TryParse(output.Trim(), out long blocks)) (bool ok, string so, string se, int code) res;
return (false, blocks * 512L); // st_blocks are always 512B units
_logger.LogDebug("Btrfs size probe failed for {linux} (stat {code}, err {err}). Falling back to Length.", linuxPath, code, err); res = isWine
return (false, fileInfo.Length); ? 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) catch (Exception ex)
{ {
_logger.LogDebug(ex, "Failed Btrfs size probe for {file}, using Length", fileInfo.FullName); _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
/// <summary> /// <summary>
/// Compressing the given path with BTRFS or NTFS file system. /// Compressing the given path with BTRFS or NTFS file system.
/// </summary> /// </summary>
/// <param name="path">Path of the decompressed/normal file</param> /// <param name="filePath">Path of the decompressed/normal file</param>
private void CompactFile(string filePath) /// <param name="workerId">Worker/Process Id</param>
private void CompactFile(string filePath, int workerId)
{ {
var fi = new FileInfo(filePath); var fi = new FileInfo(filePath);
if (!fi.Exists) if (!fi.Exists)
{ {
_logger.LogTrace("Skip compaction: missing {file}", filePath); _logger.LogTrace("[W{worker}] Skip compaction: missing {file}", workerId, filePath);
return; return;
} }
var fsType = GetFilesystemType(filePath, _dalamudUtilService.IsWine); var fsType = GetFilesystemType(filePath, _dalamudUtilService.IsWine);
var oldSize = fi.Length; 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. // 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 long minSizeBytes = fsType == FilesystemType.Btrfs
@@ -264,7 +338,7 @@ public sealed class FileCompactor : IDisposable
if (oldSize < minSizeBytes) 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; return;
} }
@@ -272,20 +346,19 @@ public sealed class FileCompactor : IDisposable
{ {
if (!IsWOFCompactedFile(filePath)) if (!IsWOFCompactedFile(filePath))
{ {
_logger.LogDebug("NTFS compaction XPRESS8K: {file}", filePath);
if (WOFCompressFile(filePath)) if (WOFCompressFile(filePath))
{ {
var newSize = GetFileSizeOnDisk(fi); 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 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 else
{ {
_logger.LogTrace("Already NTFS-compressed: {file}", filePath); _logger.LogTrace("[W{worker}] Already NTFS-compressed with XPRESS8K: {file}", workerId, filePath);
} }
return; return;
} }
@@ -294,41 +367,41 @@ public sealed class FileCompactor : IDisposable
{ {
if (!IsBtrfsCompressedFile(filePath)) if (!IsBtrfsCompressedFile(filePath))
{ {
_logger.LogDebug("Btrfs compression zstd: {file}", filePath);
if (BtrfsCompressFile(filePath)) if (BtrfsCompressFile(filePath))
{ {
var newSize = GetFileSizeOnDisk(fi); 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 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 else
{ {
_logger.LogTrace("Already Btrfs-compressed: {file}", filePath); _logger.LogTrace("[W{worker}] Already Btrfs-compressed with clzo: {file}", workerId, filePath);
} }
return; return;
} }
_logger.LogTrace("Skip compact: unsupported FS for {file}", filePath); _logger.LogTrace("[W{worker}] Skip compact: unsupported FS for {file}", workerId, filePath);
} }
/// <summary> /// <summary>
/// Decompressing the given path with BTRFS file system or NTFS file system. /// Decompressing the given path with BTRFS file system or NTFS file system.
/// </summary> /// </summary>
/// <param name="path">Path of the compressed file</param> /// <param name="filePath">Path of the decompressed/normal file</param>
private void DecompressFile(string path) /// <param name="workerId">Worker/Process Id</param>
private void DecompressFile(string filePath, int workerId)
{ {
_logger.LogDebug("Decompress request: {file}", path); _logger.LogDebug("[W{worker}] Decompress request: {file}", workerId, filePath);
var fsType = GetFilesystemType(path, _dalamudUtilService.IsWine); var fsType = GetFilesystemType(filePath, _dalamudUtilService.IsWine);
if (fsType == FilesystemType.NTFS && !_dalamudUtilService.IsWine) if (fsType == FilesystemType.NTFS && !_dalamudUtilService.IsWine)
{ {
try try
{ {
bool flowControl = DecompressWOFFile(path); bool flowControl = DecompressWOFFile(filePath, workerId);
if (!flowControl) if (!flowControl)
{ {
return; return;
@@ -336,7 +409,7 @@ public sealed class FileCompactor : IDisposable
} }
catch (Exception ex) 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 try
{ {
bool flowControl = DecompressBtrfsFile(path); bool flowControl = DecompressBtrfsFile(filePath);
if (!flowControl) if (!flowControl)
{ {
return; return;
@@ -352,7 +425,7 @@ public sealed class FileCompactor : IDisposable
} }
catch (Exception ex) 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; string linuxPath = isWine ? ToLinuxPathIfWine(path, isWine) : path;
var opts = GetMountOptionsForPath(linuxPath); var opts = GetMountOptionsForPath(linuxPath);
bool hasCompress = opts.Contains("compress", StringComparison.OrdinalIgnoreCase); if (!string.IsNullOrEmpty(opts))
bool hasCompressForce = opts.Contains("compress-force", StringComparison.OrdinalIgnoreCase); _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; return false;
} }
if (hasCompress) if (!string.IsNullOrWhiteSpace(defrag.stdout))
{ _logger.LogTrace("btrfs defragment output for {file}: {out}", linuxPath, defrag.stdout.Trim());
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());
_logger.LogInformation("Decompressed (rewritten uncompressed) Btrfs file: {file}", linuxPath); _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; return true;
} }
catch (Exception ex) catch (Exception ex)
@@ -432,18 +502,18 @@ public sealed class FileCompactor : IDisposable
/// </summary> /// </summary>
/// <param name="path">Path of the compressed file</param> /// <param name="path">Path of the compressed file</param>
/// <returns>Decompressing state</returns> /// <returns>Decompressing state</returns>
private bool DecompressWOFFile(string path) private bool DecompressWOFFile(string path, int workerID)
{ {
//Check if its already been compressed //Check if its already been compressed
if (TryIsWofExternal(path, out bool isExternal, out int algo)) if (TryIsWofExternal(path, out bool isExternal, out int algo))
{ {
if (!isExternal) if (!isExternal)
{ {
_logger.LogTrace("Already decompressed file: {file}", path); _logger.LogTrace("[W{worker}] Already decompressed file: {file}", workerID, path);
return true; return true;
} }
var compressString = ((CompressionAlgorithm)algo).ToString(); 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. //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. // 342 error code means its been decompressed after the control, we handle it as it succesfully been decompressed.
if (err == 342) if (err == 342)
{ {
_logger.LogTrace("Successfully decompressed NTFS file {file}", path); _logger.LogTrace("[W{worker}] Successfully decompressed NTFS file {file}", workerID, path);
return true; 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; return false;
} }
_logger.LogTrace("Successfully decompressed NTFS file {file}", path); _logger.LogTrace("[W{worker}] Successfully decompressed NTFS file {file}", workerID, path);
return true; return true;
}); });
} }
@@ -478,6 +548,7 @@ public sealed class FileCompactor : IDisposable
/// <returns>Converted path to be used in Linux</returns> /// <returns>Converted path to be used in Linux</returns>
private string ToLinuxPathIfWine(string path, bool isWine, bool preferShell = true) private string ToLinuxPathIfWine(string path, bool isWine, bool preferShell = true)
{ {
//Return if not wine
if (!isWine || !IsProbablyWine()) if (!isWine || !IsProbablyWine())
return path; return path;
@@ -539,7 +610,7 @@ public sealed class FileCompactor : IDisposable
/// <returns>Compessing state</returns> /// <returns>Compessing state</returns>
private bool WOFCompressFile(string path) private bool WOFCompressFile(string path)
{ {
int size = Marshal.SizeOf<WOF_FILE_COMPRESSION_INFO_V1>(); int size = Marshal.SizeOf<WofFileCompressionInfoV1>();
IntPtr efInfoPtr = Marshal.AllocHGlobal(size); IntPtr efInfoPtr = Marshal.AllocHGlobal(size);
try try
@@ -592,7 +663,7 @@ public sealed class FileCompactor : IDisposable
{ {
try try
{ {
uint buf = (uint)Marshal.SizeOf<WOF_FILE_COMPRESSION_INFO_V1>(); uint buf = (uint)Marshal.SizeOf<WofFileCompressionInfoV1>();
int result = WofIsExternalFile(filePath, out int isExternal, out _, out var info, ref buf); int result = WofIsExternalFile(filePath, out int isExternal, out _, out var info, ref buf);
if (result != 0 || isExternal == 0) if (result != 0 || isExternal == 0)
return false; return false;
@@ -621,7 +692,7 @@ public sealed class FileCompactor : IDisposable
algorithm = 0; algorithm = 0;
try try
{ {
uint buf = (uint)Marshal.SizeOf<WOF_FILE_COMPRESSION_INFO_V1>(); uint buf = (uint)Marshal.SizeOf<WofFileCompressionInfoV1>();
int hr = WofIsExternalFile(path, out int ext, out _, out var info, ref buf); int hr = WofIsExternalFile(path, out int ext, out _, out var info, ref buf);
if (hr == 0 && ext != 0) if (hr == 0 && ext != 0)
{ {
@@ -630,13 +701,13 @@ public sealed class FileCompactor : IDisposable
} }
return true; return true;
} }
catch (DllNotFoundException) catch (DllNotFoundException)
{ {
return false; return false;
} }
catch (EntryPointNotFoundException) catch (EntryPointNotFoundException)
{ {
return false; return false;
} }
} }
@@ -651,8 +722,7 @@ public sealed class FileCompactor : IDisposable
{ {
try try
{ {
bool windowsProc = RuntimeInformation.IsOSPlatform(OSPlatform.Windows); string linuxPath = _isWindows ? ResolveLinuxPathForWine(path) : path;
string linuxPath = windowsProc ? ResolveLinuxPathForWine(path) : path;
var task = _fragBatch.IsCompressedAsync(linuxPath, _compactionCts.Token); var task = _fragBatch.IsCompressedAsync(linuxPath, _compactionCts.Token);
@@ -685,7 +755,6 @@ public sealed class FileCompactor : IDisposable
try try
{ {
var (winPath, linuxPath) = ResolvePathsForBtrfs(path); var (winPath, linuxPath) = ResolvePathsForBtrfs(path);
bool isWindowsProc = RuntimeInformation.IsOSPlatform(OSPlatform.Windows);
if (IsBtrfsCompressedFile(linuxPath)) if (IsBtrfsCompressedFile(linuxPath))
{ {
@@ -699,8 +768,13 @@ public sealed class FileCompactor : IDisposable
return false; 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) = (bool ok, string stdout, string stderr, int code) =
isWindowsProc _isWindows
? RunProcessShell($"btrfs filesystem defragment -clzo -- {QuoteSingle(linuxPath)}") ? RunProcessShell($"btrfs filesystem defragment -clzo -- {QuoteSingle(linuxPath)}")
: RunProcessDirect("btrfs", ["filesystem", "defragment", "-clzo", "--", linuxPath]); : RunProcessDirect("btrfs", ["filesystem", "defragment", "-clzo", "--", linuxPath]);
@@ -783,9 +857,10 @@ public sealed class FileCompactor : IDisposable
RedirectStandardOutput = true, RedirectStandardOutput = true,
RedirectStandardError = true, RedirectStandardError = true,
UseShellExecute = false, UseShellExecute = false,
CreateNoWindow = true CreateNoWindow = true,
WorkingDirectory = workingDir ?? "/",
}; };
if (!string.IsNullOrEmpty(workingDir)) psi.WorkingDirectory = workingDir;
foreach (var a in args) psi.ArgumentList.Add(a); foreach (var a in args) psi.ArgumentList.Add(a);
EnsureUnixPathEnv(psi); EnsureUnixPathEnv(psi);
@@ -799,8 +874,18 @@ public sealed class FileCompactor : IDisposable
} }
int code; int code;
try { code = proc.ExitCode; } catch { code = -1; } try { code = proc.ExitCode; }
return (code == 0, so2, se2, code); catch { code = -1; }
bool ok = code == 0;
if (!ok && code == -1 &&
string.IsNullOrWhiteSpace(se2) && !string.IsNullOrWhiteSpace(so2))
{
ok = true;
}
return (ok, so2, se2, code);
} }
/// <summary> /// <summary>
@@ -811,15 +896,14 @@ public sealed class FileCompactor : IDisposable
/// <returns>State of the process, output of the process and error with exit code</returns> /// <returns>State of the process, output of the process and error with exit code</returns>
private (bool ok, string stdout, string stderr, int exitCode) RunProcessShell(string command, string? workingDir = null, int timeoutMs = 60000) private (bool ok, string stdout, string stderr, int exitCode) RunProcessShell(string command, string? workingDir = null, int timeoutMs = 60000)
{ {
var psi = new ProcessStartInfo("/bin/bash") var psi = new ProcessStartInfo("/bin/bash")
{ {
RedirectStandardOutput = true, RedirectStandardOutput = true,
RedirectStandardError = true, RedirectStandardError = true,
UseShellExecute = false, 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 // Use a Login shell so PATH includes /usr/sbin etc. AKA -lc for login shell
psi.ArgumentList.Add("-lc"); psi.ArgumentList.Add("-lc");
@@ -836,65 +920,72 @@ public sealed class FileCompactor : IDisposable
} }
int code; int code;
try { code = proc.ExitCode; } catch { code = -1; } try { code = proc.ExitCode; }
return (code == 0, so2, se2, code); catch { code = -1; }
bool ok = code == 0;
if (!ok && code == -1 && string.IsNullOrWhiteSpace(se2) && !string.IsNullOrWhiteSpace(so2))
{
ok = true;
}
return (ok, so2, se2, code);
} }
/// <summary> /// <summary>
/// Checking the process result for shell or direct processes /// Checking the process result for shell or direct processes
/// </summary> /// </summary>
/// <param name="proc">Process</param> /// <param name="proc">Process</param>
/// <param name="timeoutMs">How long when timeout is gotten</param> /// <param name="timeoutMs">How long when timeout goes over threshold</param>
/// <param name="token">Cancellation Token</param> /// <param name="token">Cancellation Token</param>
/// <returns>Multiple variables</returns> /// <returns>Multiple variables</returns>
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 outTask = proc.StandardOutput.ReadToEndAsync(token);
var errTask = proc.StandardError.ReadToEndAsync(token); var errTask = proc.StandardError.ReadToEndAsync(token);
var bothTasks = Task.WhenAll(outTask, errTask); var bothTasks = Task.WhenAll(outTask, errTask);
//On wine, we dont wanna use waitforexit as it will be always broken and giving an error. var finished = Task.WhenAny(bothTasks, Task.Delay(timeoutMs, token)).GetAwaiter().GetResult();
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 stderr = errTask.Result; if (token.IsCancellationRequested)
var ok = string.IsNullOrWhiteSpace(stderr); return KillProcess(proc, outTask, errTask, token);
return (ok, outTask.Result, stderr);
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 var stdout = outTask.Status == TaskStatus.RanToCompletion ? outTask.Result : "";
if (!proc.WaitForExit(timeoutMs)) var stderr = errTask.Status == TaskStatus.RanToCompletion ? errTask.Result : "";
{
try
{
proc.Kill(entireProcessTree: true);
Task.WaitAll([outTask, errTask], 1000, token);
}
catch
{
// ignore this
}
return (false, outTask.IsCompleted ? outTask.Result : "", "timeout");
}
Task.WaitAll(outTask, errTask); int code = -1;
return (true, outTask.Result, errTask.Result); 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<string> outTask, Task<string> 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);
}
} }
/// <summary> /// <summary>
@@ -957,7 +1048,7 @@ public sealed class FileCompactor : IDisposable
/// Process the queue with, meant for a worker/thread /// Process the queue with, meant for a worker/thread
/// </summary> /// </summary>
/// <param name="token">Cancellation token for the worker whenever it needs to be stopped</param> /// <param name="token">Cancellation token for the worker whenever it needs to be stopped</param>
private async Task ProcessQueueWorkerAsync(CancellationToken token) private async Task ProcessQueueWorkerAsync(CancellationToken token, int workerId)
{ {
try try
{ {
@@ -973,7 +1064,7 @@ public sealed class FileCompactor : IDisposable
try try
{ {
if (_lightlessConfigService.Current.UseCompactor && File.Exists(filePath)) if (_lightlessConfigService.Current.UseCompactor && File.Exists(filePath))
CompactFile(filePath); CompactFile(filePath, workerId);
} }
finally finally
{ {
@@ -992,8 +1083,8 @@ public sealed class FileCompactor : IDisposable
} }
} }
} }
catch (OperationCanceledException) catch (OperationCanceledException)
{ {
// Shutting down worker, this exception is expected // Shutting down worker, this exception is expected
} }
} }
@@ -1005,7 +1096,7 @@ public sealed class FileCompactor : IDisposable
/// <returns>Linux path to be used in Linux</returns> /// <returns>Linux path to be used in Linux</returns>
private string ResolveLinuxPathForWine(string windowsPath) 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(); if (ok && !string.IsNullOrWhiteSpace(outp)) return outp.Trim();
return ToLinuxPathIfWine(windowsPath, isWine: true); return ToLinuxPathIfWine(windowsPath, isWine: true);
} }
@@ -1029,9 +1120,7 @@ public sealed class FileCompactor : IDisposable
/// <returns></returns> /// <returns></returns>
private (string windowsPath, string linuxPath) ResolvePathsForBtrfs(string path) private (string windowsPath, string linuxPath) ResolvePathsForBtrfs(string path)
{ {
bool isWindowsProc = RuntimeInformation.IsOSPlatform(OSPlatform.Windows); if (!_isWindows)
if (!isWindowsProc)
return (path, path); return (path, path);
var (ok, outp, _, _) = RunProcessShell($"winepath -u {QuoteSingle(path)}", workingDir: null, 5000); var (ok, outp, _, _) = RunProcessShell($"winepath -u {QuoteSingle(path)}", workingDir: null, 5000);
@@ -1050,7 +1139,7 @@ public sealed class FileCompactor : IDisposable
{ {
try try
{ {
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) if (_isWindows)
{ {
using var _ = new FileStream(winePath, FileMode.Open, FileAccess.Read, FileShare.ReadWrite); 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)] [LibraryImport("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); private static partial uint GetCompressedFileSizeW([MarshalAs(UnmanagedType.LPWStr)] string lpFileName, out uint lpFileSizeHigh);
[DllImport("kernel32.dll")] [LibraryImport("kernel32.dll", SetLastError = true)]
private static extern uint GetCompressedFileSizeW([In, MarshalAs(UnmanagedType.LPWStr)] string lpFileName, [Out, MarshalAs(UnmanagedType.U4)] out uint lpFileSizeHigh); [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")] [LibraryImport("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); 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)] [LibraryImport("WofUtil.dll")]
private static extern int WofSetFileDataLocation(SafeFileHandle FileHandle, ulong Provider, IntPtr ExternalFileInfo, ulong Length); private static partial int WofSetFileDataLocation(SafeFileHandle FileHandle, ulong Provider, IntPtr ExternalFileInfo, ulong Length);
private static string QuoteSingle(string s) => "'" + s.Replace("'", "'\\''", StringComparison.Ordinal) + "'"; private static string QuoteSingle(string s) => "'" + s.Replace("'", "'\\''", StringComparison.Ordinal) + "'";
@@ -1103,7 +1193,11 @@ public sealed class FileCompactor : IDisposable
public void Dispose() public void Dispose()
{ {
//Cleanup of gates and frag service
_fragBatch?.Dispose(); _fragBatch?.Dispose();
_btrfsGate?.Dispose();
_globalGate?.Dispose();
_compactionQueue.Writer.TryComplete(); _compactionQueue.Writer.TryComplete();
_compactionCts.Cancel(); _compactionCts.Cancel();
@@ -1111,8 +1205,8 @@ public sealed class FileCompactor : IDisposable
{ {
Task.WaitAll([.. _workers.Where(t => t != null)], TimeSpan.FromSeconds(5)); Task.WaitAll([.. _workers.Where(t => t != null)], TimeSpan.FromSeconds(5));
} }
catch catch
{ {
// Ignore this catch on the dispose // Ignore this catch on the dispose
} }
finally finally

View File

@@ -20,7 +20,10 @@ internal sealed class DalamudLogger : ILogger
_hasModifiedGameFiles = hasModifiedGameFiles; _hasModifiedGameFiles = hasModifiedGameFiles;
} }
public IDisposable BeginScope<TState>(TState state) => default!; IDisposable? ILogger.BeginScope<TState>(TState state)
{
return default!;
}
public bool IsEnabled(LogLevel logLevel) public bool IsEnabled(LogLevel logLevel)
{ {

View File

@@ -92,13 +92,13 @@ namespace LightlessSync.Services.Compactor
} }
if ((flushAt - DateTime.UtcNow) <= TimeSpan.Zero) break; if ((flushAt - DateTime.UtcNow) <= TimeSpan.Zero) break;
try try
{ {
await Task.Delay(TimeSpan.FromMilliseconds(5), _cts.Token).ConfigureAwait(false); await Task.Delay(TimeSpan.FromMilliseconds(5), _cts.Token).ConfigureAwait(false);
} }
catch catch
{ {
break; break;
} }
} }
@@ -124,8 +124,8 @@ namespace LightlessSync.Services.Compactor
} }
} }
} }
catch (OperationCanceledException) catch (OperationCanceledException)
{ {
//Shutting down worker, exception called //Shutting down worker, exception called
} }
} }
@@ -145,17 +145,13 @@ namespace LightlessSync.Services.Compactor
if (_useShell) 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: "/"); res = _runShell(inner, timeoutMs: 15000, workingDir: "/");
} }
else else
{ {
var args = new List<string> { "-v" }; var args = new List<string> { "-v", "--" };
foreach (var path in list) args.AddRange(list);
{
args.Add(' ' + path);
}
res = _runDirect("filefrag", args, workingDir: "/", timeoutMs: 15000); 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 return on the Linux/Wine systems, giving back the amount
/// </summary> /// </summary>
/// <returns>Regex of the File Size</returns> /// <returns>Regex of the File Size</returns>
[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(); private static partial Regex SizeRegex();
/// <summary> /// <summary>

View File

@@ -22,7 +22,7 @@ namespace LightlessSync.UI
private readonly UiSharedService _uiSharedService; private readonly UiSharedService _uiSharedService;
private readonly BroadcastScannerService _broadcastScannerService; private readonly BroadcastScannerService _broadcastScannerService;
private IReadOnlyList<GroupFullInfoDto> _allSyncshells; private IReadOnlyList<GroupFullInfoDto> _allSyncshells = Array.Empty<GroupFullInfoDto>();
private string _userUid = string.Empty; private string _userUid = string.Empty;
private readonly List<(string Label, string? GID, bool IsAvailable)> _syncshellOptions = new(); private readonly List<(string Label, string? GID, bool IsAvailable)> _syncshellOptions = new();

View File

@@ -13,13 +13,15 @@ internal sealed partial class CharaDataHubUi
AccessTypeDto.AllPairs => "All Pairs", AccessTypeDto.AllPairs => "All Pairs",
AccessTypeDto.ClosePairs => "Direct Pairs", AccessTypeDto.ClosePairs => "Direct Pairs",
AccessTypeDto.Individuals => "Specified", AccessTypeDto.Individuals => "Specified",
AccessTypeDto.Public => "Everyone" AccessTypeDto.Public => "Everyone",
_ => throw new NotSupportedException()
}; };
private static string GetShareTypeString(ShareTypeDto dto) => dto switch private static string GetShareTypeString(ShareTypeDto dto) => dto switch
{ {
ShareTypeDto.Private => "Code Only", ShareTypeDto.Private => "Code Only",
ShareTypeDto.Shared => "Shared" ShareTypeDto.Shared => "Shared",
_ => throw new NotSupportedException()
}; };
private static string GetWorldDataTooltipText(PoseEntryExtended poseEntry) private static string GetWorldDataTooltipText(PoseEntryExtended poseEntry)
@@ -31,7 +33,7 @@ internal sealed partial class CharaDataHubUi
private void GposeMetaInfoAction(Action<CharaDataMetaInfoExtendedDto?> gposeActionDraw, string actionDescription, CharaDataMetaInfoExtendedDto? dto, bool hasValidGposeTarget, bool isSpawning) private void GposeMetaInfoAction(Action<CharaDataMetaInfoExtendedDto?> gposeActionDraw, string actionDescription, CharaDataMetaInfoExtendedDto? dto, bool hasValidGposeTarget, bool isSpawning)
{ {
StringBuilder sb = new StringBuilder(); StringBuilder sb = new();
sb.AppendLine(actionDescription); sb.AppendLine(actionDescription);
bool isDisabled = false; bool isDisabled = false;

View File

@@ -29,6 +29,7 @@ using System.Globalization;
using System.Linq; using System.Linq;
using System.Numerics; using System.Numerics;
using System.Reflection; using System.Reflection;
using System.Runtime.InteropServices;
namespace LightlessSync.UI; namespace LightlessSync.UI;
@@ -105,7 +106,7 @@ public class CompactUi : WindowMediatorSubscriberBase
_renamePairTagUi = renameTagUi; _renamePairTagUi = renameTagUi;
_ipcManager = ipcManager; _ipcManager = ipcManager;
_broadcastService = broadcastService; _broadcastService = broadcastService;
_tabMenu = new TopTabMenu(Mediator, _apiController, _pairManager, _uiSharedService, pairRequestService, dalamudUtilService, lightlessNotificationService); _tabMenu = new TopTabMenu(Mediator, _apiController, _pairManager, _uiSharedService, pairRequestService);
AllowPinning = true; AllowPinning = true;
AllowClickthrough = false; AllowClickthrough = false;
@@ -285,11 +286,10 @@ public class CompactUi : WindowMediatorSubscriberBase
private void DrawPairs() private void DrawPairs()
{ {
var ySize = _transferPartHeight == 0 float ySize = Math.Abs(_transferPartHeight) < 0.0001f
? 1 ? 1
: (ImGui.GetWindowContentRegionMax().Y - ImGui.GetWindowContentRegionMin().Y : ((ImGui.GetWindowContentRegionMax().Y - ImGui.GetWindowContentRegionMin().Y
+ ImGui.GetTextLineHeight() - ImGui.GetStyle().WindowPadding.Y - ImGui.GetStyle().WindowBorderSize) - _transferPartHeight - ImGui.GetCursorPosY(); + ImGui.GetTextLineHeight() - ImGui.GetStyle().WindowPadding.Y - ImGui.GetStyle().WindowBorderSize) - _transferPartHeight - ImGui.GetCursorPosY());
ImGui.BeginChild("list", new Vector2(_windowContentWidth, ySize), border: false); ImGui.BeginChild("list", new Vector2(_windowContentWidth, ySize), border: false);
foreach (var item in _drawFolders) foreach (var item in _drawFolders)
@@ -470,6 +470,7 @@ public class CompactUi : WindowMediatorSubscriberBase
return new DownloadSummary(totalFiles, transferredFiles, transferredBytes, totalBytes); return new DownloadSummary(totalFiles, transferredFiles, transferredBytes, totalBytes);
} }
[StructLayout(LayoutKind.Auto)]
private readonly record struct DownloadSummary(int TotalFiles, int TransferredFiles, long TransferredBytes, long TotalBytes) private readonly record struct DownloadSummary(int TotalFiles, int TransferredFiles, long TransferredBytes, long TotalBytes)
{ {
public bool HasDownloads => TotalFiles > 0 || TotalBytes > 0; public bool HasDownloads => TotalFiles > 0 || TotalBytes > 0;

View File

@@ -297,7 +297,6 @@ public class EditProfileUi : WindowMediatorSubscriberBase
ImGui.Dummy(new Vector2(5)); ImGui.Dummy(new Vector2(5));
var font = UiBuilder.MonoFont; var font = UiBuilder.MonoFont;
var playerUID = _apiController.UID;
var playerDisplay = _apiController.DisplayName; var playerDisplay = _apiController.DisplayName;
var previewTextColor = textEnabled ? textColor : Vector4.One; var previewTextColor = textEnabled ? textColor : Vector4.One;

View File

@@ -154,13 +154,11 @@ public class IdDisplayHandler
Vector2 itemMin; Vector2 itemMin;
Vector2 itemMax; Vector2 itemMax;
Vector2 textSize;
using (ImRaii.PushFont(font, textIsUid)) using (ImRaii.PushFont(font, textIsUid))
{ {
SeStringUtils.RenderSeStringWithHitbox(seString, rowStart, font, pair.UserData.UID); SeStringUtils.RenderSeStringWithHitbox(seString, rowStart, font, pair.UserData.UID);
itemMin = ImGui.GetItemRectMin(); itemMin = ImGui.GetItemRectMin();
itemMax = ImGui.GetItemRectMax(); itemMax = ImGui.GetItemRectMax();
//textSize = itemMax - itemMin;
} }
if (useHighlight) if (useHighlight)
@@ -202,7 +200,7 @@ public class IdDisplayHandler
if (ImGui.IsItemHovered()) if (ImGui.IsItemHovered())
{ {
if (!string.Equals(_lastMouseOverUid, id)) if (!string.Equals(_lastMouseOverUid, id, StringComparison.Ordinal))
{ {
_popupTime = DateTime.UtcNow.AddSeconds(_lightlessConfigService.Current.ProfileDelay); _popupTime = DateTime.UtcNow.AddSeconds(_lightlessConfigService.Current.ProfileDelay);
} }
@@ -223,7 +221,7 @@ public class IdDisplayHandler
} }
else else
{ {
if (string.Equals(_lastMouseOverUid, id)) if (string.Equals(_lastMouseOverUid, id, StringComparison.Ordinal))
{ {
_mediator.Publish(new ProfilePopoutToggle(Pair: null)); _mediator.Publish(new ProfilePopoutToggle(Pair: null));
_lastMouseOverUid = string.Empty; _lastMouseOverUid = string.Empty;

View File

@@ -139,7 +139,7 @@ public class StandaloneProfileUi : WindowMediatorSubscriberBase
} }
} }
if (Pair.UserPair.Groups.Any()) if (Pair.UserPair?.Groups?.Count > 0)
{ {
ImGui.TextUnformatted("Paired through Syncshells:"); ImGui.TextUnformatted("Paired through Syncshells:");
foreach (var group in Pair.UserPair.Groups) foreach (var group in Pair.UserPair.Groups)

View File

@@ -222,6 +222,13 @@ public class SyncshellAdminUI : WindowMediatorSubscriberBase
{ {
ImGui.Dummy(new Vector2(5)); 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)) if (!_profileImage.SequenceEqual(_profileData.ImageData.Value))
{ {
_profileImage = _profileData.ImageData.Value; _profileImage = _profileData.ImageData.Value;
@@ -379,7 +386,7 @@ public class SyncshellAdminUI : WindowMediatorSubscriberBase
UiSharedService.AttachToolTip("Clears your profile description text"); UiSharedService.AttachToolTip("Clears your profile description text");
ImGui.Separator(); ImGui.Separator();
ImGui.TextUnformatted($"Profile Options:"); ImGui.TextUnformatted($"Profile Options:");
var isNsfw = _profileData.IsNsfw; var isNsfw = _profileData?.IsNsfw ?? false;
if (ImGui.Checkbox("Profile is NSFW", ref isNsfw)) 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)); _ = _apiController.GroupSetProfile(new GroupProfileDto(new GroupData(GroupFullInfo.Group.AliasOrGID), Description: null, Tags: null, PictureBase64: null, BannerBase64: null, IsNsfw: isNsfw, IsDisabled: null));

View File

@@ -9,7 +9,6 @@ using LightlessSync.LightlessConfiguration.Models;
using LightlessSync.PlayerData.Pairs; using LightlessSync.PlayerData.Pairs;
using LightlessSync.Services; using LightlessSync.Services;
using LightlessSync.Services.Mediator; using LightlessSync.Services.Mediator;
using LightlessSync.Utils;
using LightlessSync.WebAPI; using LightlessSync.WebAPI;
using System.Numerics; using System.Numerics;
@@ -24,27 +23,21 @@ public class TopTabMenu
private readonly PairManager _pairManager; private readonly PairManager _pairManager;
private readonly PairRequestService _pairRequestService; private readonly PairRequestService _pairRequestService;
private readonly DalamudUtilService _dalamudUtilService;
private readonly HashSet<string> _pendingPairRequestActions = new(StringComparer.Ordinal); private readonly HashSet<string> _pendingPairRequestActions = new(StringComparer.Ordinal);
private bool _pairRequestsExpanded; // useless for now
private int _lastRequestCount;
private readonly UiSharedService _uiSharedService; private readonly UiSharedService _uiSharedService;
private readonly NotificationService _lightlessNotificationService;
private string _filter = string.Empty; private string _filter = string.Empty;
private int _globalControlCountdown = 0; private int _globalControlCountdown = 0;
private float _pairRequestsHeight = 150f; private float _pairRequestsHeight = 150f;
private string _pairToAdd = string.Empty; private string _pairToAdd = string.Empty;
private SelectedTab _selectedTab = SelectedTab.None; 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; _lightlessMediator = lightlessMediator;
_apiController = apiController; _apiController = apiController;
_pairManager = pairManager; _pairManager = pairManager;
_pairRequestService = pairRequestService; _pairRequestService = pairRequestService;
_dalamudUtilService = dalamudUtilService;
_uiSharedService = uiSharedService; _uiSharedService = uiSharedService;
_lightlessNotificationService = lightlessNotificationService;
} }
private enum SelectedTab private enum SelectedTab
@@ -70,13 +63,17 @@ public class TopTabMenu
_filter = value; _filter = value;
} }
} }
private SelectedTab TabSelection
private SelectedTab GetTabSelection()
{ {
get => _selectedTab; set return _selectedTab;
{
_selectedTab = value;
}
} }
private void SetTabSelection(SelectedTab value)
{
_selectedTab = value;
}
public void Draw() public void Draw()
{ {
var availableWidth = ImGui.GetWindowContentRegionMax().X - ImGui.GetWindowContentRegionMin().X; var availableWidth = ImGui.GetWindowContentRegionMax().X - ImGui.GetWindowContentRegionMin().X;
@@ -85,7 +82,7 @@ public class TopTabMenu
var buttonY = _uiSharedService.GetIconButtonSize(FontAwesomeIcon.Pause).Y; var buttonY = _uiSharedService.GetIconButtonSize(FontAwesomeIcon.Pause).Y;
var buttonSize = new Vector2(buttonX, buttonY); var buttonSize = new Vector2(buttonX, buttonY);
var drawList = ImGui.GetWindowDrawList(); 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))); var btncolor = ImRaii.PushColor(ImGuiCol.Button, ImGui.ColorConvertFloat4ToU32(new(0, 0, 0, 0)));
ImGuiHelpers.ScaledDummy(spacing.Y / 2f); ImGuiHelpers.ScaledDummy(spacing.Y / 2f);
@@ -95,11 +92,11 @@ public class TopTabMenu
var x = ImGui.GetCursorScreenPos(); var x = ImGui.GetCursorScreenPos();
if (ImGui.Button(FontAwesomeIcon.User.ToIconString(), buttonSize)) 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(); ImGui.SameLine();
var xAfter = ImGui.GetCursorScreenPos(); var xAfter = ImGui.GetCursorScreenPos();
if (TabSelection == SelectedTab.Individual) if (GetTabSelection() == SelectedTab.Individual)
drawList.AddLine(x with { Y = x.Y + buttonSize.Y + spacing.Y }, 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 }, xAfter with { Y = xAfter.Y + buttonSize.Y + spacing.Y, X = xAfter.X - spacing.X },
underlineColor, 2); underlineColor, 2);
@@ -111,11 +108,11 @@ public class TopTabMenu
var x = ImGui.GetCursorScreenPos(); var x = ImGui.GetCursorScreenPos();
if (ImGui.Button(FontAwesomeIcon.Users.ToIconString(), buttonSize)) 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(); ImGui.SameLine();
var xAfter = ImGui.GetCursorScreenPos(); var xAfter = ImGui.GetCursorScreenPos();
if (TabSelection == SelectedTab.Syncshell) if (GetTabSelection() == SelectedTab.Syncshell)
drawList.AddLine(x with { Y = x.Y + buttonSize.Y + spacing.Y }, 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 }, xAfter with { Y = xAfter.Y + buttonSize.Y + spacing.Y, X = xAfter.X - spacing.X },
underlineColor, 2); underlineColor, 2);
@@ -128,12 +125,12 @@ public class TopTabMenu
var x = ImGui.GetCursorScreenPos(); var x = ImGui.GetCursorScreenPos();
if (ImGui.Button(FontAwesomeIcon.Compass.ToIconString(), buttonSize)) 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(); ImGui.SameLine();
var xAfter = ImGui.GetCursorScreenPos(); var xAfter = ImGui.GetCursorScreenPos();
if (TabSelection == SelectedTab.Lightfinder) if (GetTabSelection() == SelectedTab.Lightfinder)
drawList.AddLine(x with { Y = x.Y + buttonSize.Y + spacing.Y }, 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 }, xAfter with { Y = xAfter.Y + buttonSize.Y + spacing.Y, X = xAfter.X - spacing.X },
underlineColor, 2); underlineColor, 2);
@@ -146,12 +143,12 @@ public class TopTabMenu
var x = ImGui.GetCursorScreenPos(); var x = ImGui.GetCursorScreenPos();
if (ImGui.Button(FontAwesomeIcon.UserCog.ToIconString(), buttonSize)) 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(); ImGui.SameLine();
var xAfter = ImGui.GetCursorScreenPos(); var xAfter = ImGui.GetCursorScreenPos();
if (TabSelection == SelectedTab.UserConfig) if (GetTabSelection() == SelectedTab.UserConfig)
drawList.AddLine(x with { Y = x.Y + buttonSize.Y + spacing.Y }, 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 }, xAfter with { Y = xAfter.Y + buttonSize.Y + spacing.Y, X = xAfter.X - spacing.X },
underlineColor, 2); underlineColor, 2);
@@ -161,7 +158,7 @@ public class TopTabMenu
ImGui.SameLine(); ImGui.SameLine();
using (ImRaii.PushFont(UiBuilder.IconFont)) using (ImRaii.PushFont(UiBuilder.IconFont))
{ {
var x = ImGui.GetCursorScreenPos(); ImGui.GetCursorScreenPos();
if (ImGui.Button(FontAwesomeIcon.Cog.ToIconString(), buttonSize)) if (ImGui.Button(FontAwesomeIcon.Cog.ToIconString(), buttonSize))
{ {
_lightlessMediator.Publish(new UiToggleMessage(typeof(SettingsUi))); _lightlessMediator.Publish(new UiToggleMessage(typeof(SettingsUi)));
@@ -175,26 +172,26 @@ public class TopTabMenu
ImGuiHelpers.ScaledDummy(spacing); ImGuiHelpers.ScaledDummy(spacing);
if (TabSelection == SelectedTab.Individual) if (GetTabSelection() == SelectedTab.Individual)
{ {
DrawAddPair(availableWidth, spacing.X); DrawAddPair(availableWidth, spacing.X);
DrawGlobalIndividualButtons(availableWidth, spacing.X); DrawGlobalIndividualButtons(availableWidth, spacing.X);
} }
else if (TabSelection == SelectedTab.Syncshell) else if (GetTabSelection() == SelectedTab.Syncshell)
{ {
DrawSyncshellMenu(availableWidth, spacing.X); DrawSyncshellMenu(availableWidth, spacing.X);
DrawGlobalSyncshellButtons(availableWidth, spacing.X); DrawGlobalSyncshellButtons(availableWidth, spacing.X);
} }
else if (TabSelection == SelectedTab.Lightfinder) else if (GetTabSelection() == SelectedTab.Lightfinder)
{ {
DrawLightfinderMenu(availableWidth, spacing.X); DrawLightfinderMenu(availableWidth, spacing.X);
} }
else if (TabSelection == SelectedTab.UserConfig) else if (GetTabSelection() == SelectedTab.UserConfig)
{ {
DrawUserConfig(availableWidth, spacing.X); DrawUserConfig(availableWidth, spacing.X);
} }
if (TabSelection != SelectedTab.None) ImGuiHelpers.ScaledDummy(3f); if (GetTabSelection() != SelectedTab.None) ImGuiHelpers.ScaledDummy(3f);
DrawIncomingPairRequests(availableWidth); DrawIncomingPairRequests(availableWidth);
@@ -227,17 +224,9 @@ public class TopTabMenu
var count = requests.Count; var count = requests.Count;
if (count == 0) if (count == 0)
{ {
_pairRequestsExpanded = false;
_lastRequestCount = 0;
return; return;
} }
if (count > _lastRequestCount)
{
_pairRequestsExpanded = true;
}
_lastRequestCount = count;
var label = $"Incoming Pair Requests - {count}##IncomingPairRequestsHeader"; var label = $"Incoming Pair Requests - {count}##IncomingPairRequestsHeader";
using (ImRaii.PushColor(ImGuiCol.Header, UIColors.Get("LightlessPurple"))) using (ImRaii.PushColor(ImGuiCol.Header, UIColors.Get("LightlessPurple")))
@@ -245,16 +234,12 @@ public class TopTabMenu
using (ImRaii.PushColor(ImGuiCol.HeaderActive, UIColors.Get("LightlessPurple"))) using (ImRaii.PushColor(ImGuiCol.HeaderActive, UIColors.Get("LightlessPurple")))
{ {
bool open = ImGui.CollapsingHeader(label, ImGuiTreeNodeFlags.DefaultOpen); bool open = ImGui.CollapsingHeader(label, ImGuiTreeNodeFlags.DefaultOpen);
_pairRequestsExpanded = open;
if (ImGui.IsItemHovered()) if (ImGui.IsItemHovered())
UiSharedService.AttachToolTip("Expand to view incoming pair requests."); UiSharedService.AttachToolTip("Expand to view incoming pair requests.");
if (open) 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); ImGui.SetCursorPosX(ImGui.GetCursorPosX() - 2f);
using (ImRaii.PushColor(ImGuiCol.ChildBg, UIColors.Get("LightlessPurple"))) using (ImRaii.PushColor(ImGuiCol.ChildBg, UIColors.Get("LightlessPurple")))
@@ -300,7 +285,6 @@ public class TopTabMenu
{ {
float playerColWidth = 207f * ImGuiHelpers.GlobalScale; float playerColWidth = 207f * ImGuiHelpers.GlobalScale;
float receivedColWidth = 73f * ImGuiHelpers.GlobalScale; float receivedColWidth = 73f * ImGuiHelpers.GlobalScale;
float actionsColWidth = 50f * ImGuiHelpers.GlobalScale;
ImGui.Separator(); ImGui.Separator();
ImGui.TextUnformatted("Player"); ImGui.TextUnformatted("Player");
@@ -385,7 +369,6 @@ public class TopTabMenu
try try
{ {
var myCidHash = (await _dalamudUtilService.GetCIDAsync().ConfigureAwait(false)).ToString().GetHash256();
await _apiController.TryPairWithContentId(request.HashedCid).ConfigureAwait(false); await _apiController.TryPairWithContentId(request.HashedCid).ConfigureAwait(false);
_pairRequestService.RemoveRequest(request.HashedCid); _pairRequestService.RemoveRequest(request.HashedCid);

View File

@@ -25,7 +25,6 @@ public class UpdateNotesUi : WindowMediatorSubscriberBase
private ChangelogFile _changelog = new(); private ChangelogFile _changelog = new();
private CreditsFile _credits = new(); private CreditsFile _credits = new();
private bool _scrollToTop; private bool _scrollToTop;
private int _selectedTab;
private bool _hasInitializedCollapsingHeaders; private bool _hasInitializedCollapsingHeaders;
private struct Particle private struct Particle
@@ -160,7 +159,7 @@ public class UpdateNotesUi : WindowMediatorSubscriberBase
DrawParticleEffects(headerStart, extendedParticleSize); DrawParticleEffects(headerStart, extendedParticleSize);
} }
private void DrawGradientBackground(Vector2 headerStart, Vector2 headerEnd) private static void DrawGradientBackground(Vector2 headerStart, Vector2 headerEnd)
{ {
var drawList = ImGui.GetWindowDrawList(); 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 drawList = ImGui.GetWindowDrawList();
var gradientHeight = 60f; var gradientHeight = 60f;
@@ -513,7 +512,6 @@ public class UpdateNotesUi : WindowMediatorSubscriberBase
{ {
if (changelogTab) if (changelogTab)
{ {
_selectedTab = 0;
DrawChangelog(); DrawChangelog();
} }
} }
@@ -524,7 +522,6 @@ public class UpdateNotesUi : WindowMediatorSubscriberBase
{ {
if (creditsTab) if (creditsTab)
{ {
_selectedTab = 1;
DrawCredits(); 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")); DrawFeatureSection(category.Category, UIColors.Get("LightlessBlue"));

View File

@@ -1,6 +1,5 @@
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
using System.Collections.Concurrent; using System.Collections.Concurrent;
using System.Diagnostics;
using System.Runtime.InteropServices; using System.Runtime.InteropServices;
namespace LightlessSync.Utils namespace LightlessSync.Utils
@@ -32,7 +31,7 @@ namespace LightlessSync.Utils
{ {
string rootPath; string rootPath;
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows) && (!IsProbablyWine() || !isWine)) if (OperatingSystem.IsWindows() && (!IsProbablyWine() || !isWine))
{ {
var info = new FileInfo(filePath); var info = new FileInfo(filePath);
var dir = info.Directory ?? new DirectoryInfo(filePath); var dir = info.Directory ?? new DirectoryInfo(filePath);
@@ -50,7 +49,7 @@ namespace LightlessSync.Utils
FilesystemType detected; FilesystemType detected;
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows) && (!IsProbablyWine() || !isWine)) if (OperatingSystem.IsWindows() && (!IsProbablyWine() || !isWine))
{ {
var root = new DriveInfo(rootPath); var root = new DriveInfo(rootPath);
var format = root.DriveFormat?.ToUpperInvariant() ?? string.Empty; var format = root.DriveFormat?.ToUpperInvariant() ?? string.Empty;
@@ -157,7 +156,7 @@ namespace LightlessSync.Utils
return mountOptions; return mountOptions;
} }
catch (Exception ex) catch (Exception)
{ {
return string.Empty; return string.Empty;
} }
@@ -214,7 +213,7 @@ namespace LightlessSync.Utils
if (_blockSizeCache.TryGetValue(root, out int cached)) if (_blockSizeCache.TryGetValue(root, out int cached))
return cached; return cached;
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows) && !isWine) if (OperatingSystem.IsWindows() && !isWine)
{ {
int result = GetDiskFreeSpaceW(root, int result = GetDiskFreeSpaceW(root,
out uint sectorsPerCluster, out uint sectorsPerCluster,
@@ -234,40 +233,6 @@ namespace LightlessSync.Utils
return clusterSize; 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; return _defaultBlockSize;
} }
catch (Exception ex) catch (Exception ex)