Initialize migration. (#88)
Co-authored-by: defnotken <itsdefnotken@gmail.com> Co-authored-by: cake <admin@cakeandbanana.nl> Reviewed-on: #88 Reviewed-by: cake <cake@noreply.git.lightless-sync.org> Co-authored-by: defnotken <defnotken@noreply.git.lightless-sync.org> Co-committed-by: defnotken <defnotken@noreply.git.lightless-sync.org>
This commit was merged in pull request #88.
This commit is contained in:
@@ -12,12 +12,11 @@ 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;
|
||||
public const int _maxRetries = 3;
|
||||
private readonly bool _isWindows;
|
||||
|
||||
private readonly ConcurrentDictionary<string, byte> _pendingCompactions;
|
||||
private readonly ILogger<FileCompactor> _logger;
|
||||
@@ -31,23 +30,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,
|
||||
@@ -71,29 +73,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(workerId, _compactionCts.Token),
|
||||
_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; }
|
||||
@@ -103,37 +112,91 @@ public sealed class FileCompactor : IDisposable
|
||||
/// Compact the storage of the Cache Folder
|
||||
/// </summary>
|
||||
/// <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;
|
||||
|
||||
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}";
|
||||
if (_logger.IsEnabled(LogLevel.Warning))
|
||||
_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
|
||||
{
|
||||
@@ -142,6 +205,7 @@ public sealed class FileCompactor : IDisposable
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Write all bytes into a directory async
|
||||
/// </summary>
|
||||
@@ -207,16 +271,13 @@ public sealed class FileCompactor : IDisposable
|
||||
? 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))
|
||||
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);
|
||||
return (false, fileInfo.Length);
|
||||
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);
|
||||
if (_logger.IsEnabled(LogLevel.Debug))
|
||||
_logger.LogDebug(ex, "Failed Btrfs size probe for {file}, using Length", fileInfo.FullName);
|
||||
return (flowControl: true, value: fileInfo.Length);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -257,19 +318,21 @@ public sealed class FileCompactor : IDisposable
|
||||
/// <summary>
|
||||
/// Compressing the given path with BTRFS or NTFS file system.
|
||||
/// </summary>
|
||||
/// <param name="path">Path of the decompressed/normal file</param>
|
||||
private void CompactFile(string filePath)
|
||||
/// <param name="filePath">Path of the decompressed/normal file</param>
|
||||
/// <param name="workerId">Worker/Process Id</param>
|
||||
private void CompactFile(string filePath, int workerId)
|
||||
{
|
||||
var fi = new FileInfo(filePath);
|
||||
if (!fi.Exists)
|
||||
{
|
||||
_logger.LogTrace("Skip compaction: missing {file}", filePath);
|
||||
if (_logger.IsEnabled(LogLevel.Trace))
|
||||
_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
|
||||
@@ -278,7 +341,8 @@ public sealed class FileCompactor : IDisposable
|
||||
|
||||
if (oldSize < minSizeBytes)
|
||||
{
|
||||
_logger.LogTrace("Skip compaction: {file} ({size} B) < threshold ({th} B)", filePath, oldSize, minSizeBytes);
|
||||
if (_logger.IsEnabled(LogLevel.Trace))
|
||||
_logger.LogTrace("[W{worker}] Skip compaction: {file} ({size} B) < threshold ({th} B)", workerId, filePath, oldSize, minSizeBytes);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -286,20 +350,20 @@ 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);
|
||||
if (_logger.IsEnabled(LogLevel.Trace))
|
||||
_logger.LogTrace("[W{worker}] Already NTFS-compressed with XPRESS8K: {file}", workerId, filePath);
|
||||
}
|
||||
return;
|
||||
}
|
||||
@@ -308,41 +372,43 @@ 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);
|
||||
if (_logger.IsEnabled(LogLevel.Trace))
|
||||
_logger.LogTrace("[W{worker}] Already Btrfs-compressed with clzo: {file}", workerId, filePath);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
_logger.LogTrace("Skip compact: unsupported FS for {file}", filePath);
|
||||
if (_logger.IsEnabled(LogLevel.Trace))
|
||||
_logger.LogTrace("[W{worker}] Skip compact: unsupported FS for {file}", workerId, filePath);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Decompressing the given path with BTRFS file system or NTFS file system.
|
||||
/// </summary>
|
||||
/// <param name="path">Path of the compressed file</param>
|
||||
private void DecompressFile(string path)
|
||||
/// <param name="filePath">Path of the decompressed/normal file</param>
|
||||
/// <param name="workerId">Worker/Process Id</param>
|
||||
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;
|
||||
@@ -350,7 +416,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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -358,7 +424,7 @@ public sealed class FileCompactor : IDisposable
|
||||
{
|
||||
try
|
||||
{
|
||||
bool flowControl = DecompressBtrfsFile(path);
|
||||
bool flowControl = DecompressBtrfsFile(filePath);
|
||||
if (!flowControl)
|
||||
{
|
||||
return;
|
||||
@@ -366,7 +432,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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -386,51 +452,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)
|
||||
@@ -446,18 +509,18 @@ public sealed class FileCompactor : IDisposable
|
||||
/// </summary>
|
||||
/// <param name="path">Path of the compressed file</param>
|
||||
/// <returns>Decompressing state</returns>
|
||||
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.
|
||||
@@ -471,15 +534,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;
|
||||
});
|
||||
}
|
||||
@@ -492,6 +555,7 @@ public sealed class FileCompactor : IDisposable
|
||||
/// <returns>Converted path to be used in Linux</returns>
|
||||
private string ToLinuxPathIfWine(string path, bool isWine, bool preferShell = true)
|
||||
{
|
||||
//Return if not wine
|
||||
if (!isWine || !IsProbablyWine())
|
||||
return path;
|
||||
|
||||
@@ -553,7 +617,7 @@ public sealed class FileCompactor : IDisposable
|
||||
/// <returns>Compessing state</returns>
|
||||
private bool WOFCompressFile(string path)
|
||||
{
|
||||
int size = Marshal.SizeOf<WOF_FILE_COMPRESSION_INFO_V1>();
|
||||
int size = Marshal.SizeOf<WofFileCompressionInfoV1>();
|
||||
IntPtr efInfoPtr = Marshal.AllocHGlobal(size);
|
||||
|
||||
try
|
||||
@@ -606,7 +670,7 @@ public sealed class FileCompactor : IDisposable
|
||||
{
|
||||
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);
|
||||
if (result != 0 || isExternal == 0)
|
||||
return false;
|
||||
@@ -635,7 +699,7 @@ public sealed class FileCompactor : IDisposable
|
||||
algorithm = 0;
|
||||
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);
|
||||
if (hr == 0 && ext != 0)
|
||||
{
|
||||
@@ -644,13 +708,13 @@ public sealed class FileCompactor : IDisposable
|
||||
}
|
||||
return true;
|
||||
}
|
||||
catch (DllNotFoundException)
|
||||
catch (DllNotFoundException)
|
||||
{
|
||||
return false;
|
||||
return false;
|
||||
}
|
||||
catch (EntryPointNotFoundException)
|
||||
{
|
||||
return false;
|
||||
catch (EntryPointNotFoundException)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -665,8 +729,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);
|
||||
|
||||
@@ -712,6 +775,11 @@ 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) =
|
||||
_isWindows
|
||||
? RunProcessShell($"btrfs filesystem defragment -clzo -- {QuoteSingle(linuxPath)}")
|
||||
@@ -796,9 +864,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);
|
||||
|
||||
@@ -812,8 +881,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);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -824,15 +903,14 @@ public sealed class FileCompactor : IDisposable
|
||||
/// <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)
|
||||
{
|
||||
|
||||
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");
|
||||
@@ -849,65 +927,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);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Checking the process result for shell or direct processes
|
||||
/// </summary>
|
||||
/// <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>
|
||||
/// <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 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<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>
|
||||
@@ -967,10 +1052,10 @@ public sealed class FileCompactor : IDisposable
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Process the queue with, meant for a worker/thread
|
||||
/// Process the queue, meant for a worker/thread
|
||||
/// </summary>
|
||||
/// <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(int workerId, CancellationToken token)
|
||||
{
|
||||
try
|
||||
{
|
||||
@@ -986,7 +1071,7 @@ public sealed class FileCompactor : IDisposable
|
||||
try
|
||||
{
|
||||
if (_lightlessConfigService.Current.UseCompactor && File.Exists(filePath))
|
||||
CompactFile(filePath);
|
||||
CompactFile(filePath, workerId);
|
||||
}
|
||||
finally
|
||||
{
|
||||
@@ -1005,8 +1090,8 @@ public sealed class FileCompactor : IDisposable
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
// Shutting down worker, this exception is expected
|
||||
}
|
||||
}
|
||||
@@ -1018,7 +1103,7 @@ public sealed class FileCompactor : IDisposable
|
||||
/// <returns>Linux path to be used in Linux</returns>
|
||||
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);
|
||||
}
|
||||
@@ -1071,7 +1156,11 @@ public sealed class FileCompactor : IDisposable
|
||||
}
|
||||
return true;
|
||||
}
|
||||
catch { return false; }
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogTrace(ex, "Probe open failed for {file} (linux={linux})", winePath, linuxPath);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -1096,17 +1185,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) + "'";
|
||||
|
||||
@@ -1114,7 +1204,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();
|
||||
|
||||
@@ -1122,8 +1216,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
|
||||
|
||||
Reference in New Issue
Block a user