|
|
|
|
@@ -82,7 +82,7 @@ public sealed class FileCompactor : IDisposable
|
|
|
|
|
_fragBatch = new BatchFilefragService(
|
|
|
|
|
useShell: _dalamudUtilService.IsWine,
|
|
|
|
|
log: _logger,
|
|
|
|
|
batchSize: 128,
|
|
|
|
|
batchSize: 256,
|
|
|
|
|
flushMs: 25);
|
|
|
|
|
|
|
|
|
|
_logger.LogInformation("FileCompactor started with {workers} workers", workerCount);
|
|
|
|
|
@@ -192,23 +192,32 @@ public sealed class FileCompactor : IDisposable
|
|
|
|
|
{
|
|
|
|
|
try
|
|
|
|
|
{
|
|
|
|
|
bool isWine = _dalamudUtilService?.IsWine ?? false;
|
|
|
|
|
string realPath = isWine ? ToLinuxPathIfWine(fileInfo.FullName, isWine) : fileInfo.FullName;
|
|
|
|
|
bool isWindowsProc = RuntimeInformation.IsOSPlatform(OSPlatform.Windows);
|
|
|
|
|
var (_, linuxPath) = ResolvePathsForBtrfs(fileInfo.FullName);
|
|
|
|
|
|
|
|
|
|
(bool ok, string stdout, string stderr, int code) =
|
|
|
|
|
RunProcessDirect("stat", ["-c", "%b", realPath]);
|
|
|
|
|
var (ok1, out1, err1, code1) =
|
|
|
|
|
isWindowsProc
|
|
|
|
|
? RunProcessShell($"stat -c %b -- {QuoteSingle(linuxPath)}", null, 10000)
|
|
|
|
|
: RunProcessDirect("stat", new[] { "-c", "%b", "--", linuxPath }, null, 10000);
|
|
|
|
|
|
|
|
|
|
if (!ok || !long.TryParse(stdout.Trim(), out var blocks))
|
|
|
|
|
throw new InvalidOperationException($"stat failed (exit {code}): {stderr}");
|
|
|
|
|
if (ok1 && long.TryParse(out1.Trim(), out long blocks))
|
|
|
|
|
return (false, blocks * 512L); // st_blocks are 512B units
|
|
|
|
|
|
|
|
|
|
return (flowControl: false, value: blocks * 512L);
|
|
|
|
|
// Fallback: du -B1 (true on-disk bytes)
|
|
|
|
|
var (ok2, out2, err2, code2) = RunProcessShell($"du -B1 -- {QuoteSingle(linuxPath)} | cut -f1", null, 10000); // use shell for the pipe
|
|
|
|
|
|
|
|
|
|
if (ok2 && long.TryParse(out2.Trim(), out long bytes))
|
|
|
|
|
return (false, bytes);
|
|
|
|
|
|
|
|
|
|
_logger.LogDebug("Btrfs size probe failed for {linux} (stat {code1}, du {code2}). Falling back to Length.",
|
|
|
|
|
linuxPath, code1, code2);
|
|
|
|
|
return (false, fileInfo.Length);
|
|
|
|
|
}
|
|
|
|
|
catch (Exception ex)
|
|
|
|
|
{
|
|
|
|
|
_logger.LogDebug(ex, "Failed stat size for {file}, fallback to Length", fileInfo.FullName);
|
|
|
|
|
_logger.LogDebug(ex, "Failed Btrfs size probe for {file}, using Length", fileInfo.FullName);
|
|
|
|
|
return (false, fileInfo.Length);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return (flowControl: true, value: default);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
@@ -356,48 +365,49 @@ public sealed class FileCompactor : IDisposable
|
|
|
|
|
/// <param name="path">Path of the compressed file</param>
|
|
|
|
|
/// <returns>Decompressing state</returns>
|
|
|
|
|
private bool DecompressBtrfsFile(string path)
|
|
|
|
|
{
|
|
|
|
|
return RunWithBtrfsGate(() =>
|
|
|
|
|
{
|
|
|
|
|
try
|
|
|
|
|
{
|
|
|
|
|
_btrfsGate.Wait(_compactionCts.Token);
|
|
|
|
|
bool isWine = _dalamudUtilService?.IsWine ?? false;
|
|
|
|
|
string realPath = isWine ? ToLinuxPathIfWine(path, isWine) : path;
|
|
|
|
|
var (winPath, linuxPath) = ResolvePathsForBtrfs(path);
|
|
|
|
|
bool isWindowsProc = RuntimeInformation.IsOSPlatform(OSPlatform.Windows);
|
|
|
|
|
|
|
|
|
|
var mountOptions = GetMountOptionsForPath(realPath);
|
|
|
|
|
if (mountOptions.Contains("compress", StringComparison.OrdinalIgnoreCase))
|
|
|
|
|
var opts = GetMountOptionsForPath(linuxPath);
|
|
|
|
|
if (opts.Contains("compress", StringComparison.OrdinalIgnoreCase))
|
|
|
|
|
{
|
|
|
|
|
_logger.LogWarning(
|
|
|
|
|
"Cannot safely decompress {file}: filesystem mounted with compression ({opts}). " +
|
|
|
|
|
"Remount with 'compress=no' before running decompression.",
|
|
|
|
|
realPath, mountOptions);
|
|
|
|
|
"Cannot safely decompress {file}: filesystem mounted with compression ({opts}). Remount with 'compress=no'.",
|
|
|
|
|
linuxPath, opts);
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (!IsBtrfsCompressedFile(realPath))
|
|
|
|
|
if (!IsBtrfsCompressedFile(linuxPath))
|
|
|
|
|
{
|
|
|
|
|
_logger.LogTrace("File {file} is not compressed, skipping decompression.", realPath);
|
|
|
|
|
_logger.LogTrace("Btrfs: not compressed, skip {file}", linuxPath);
|
|
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (!ProbeFileReadable(realPath))
|
|
|
|
|
if (!ProbeFileReadableForBtrfs(winPath, linuxPath))
|
|
|
|
|
return false;
|
|
|
|
|
|
|
|
|
|
// Rewrite file uncompressed
|
|
|
|
|
(bool ok, string stdout, string stderr, int code) =
|
|
|
|
|
isWine
|
|
|
|
|
? RunProcessShell($"btrfs filesystem defragment -- {QuoteSingle(realPath)}")
|
|
|
|
|
: RunProcessDirect("btrfs", ["filesystem", "defragment", "--", realPath]);
|
|
|
|
|
isWindowsProc
|
|
|
|
|
? RunProcessShell($"btrfs filesystem defragment -- {QuoteSingle(linuxPath)}")
|
|
|
|
|
: RunProcessDirect("btrfs", ["filesystem", "defragment", "--", linuxPath]);
|
|
|
|
|
|
|
|
|
|
if (!ok)
|
|
|
|
|
{
|
|
|
|
|
_logger.LogWarning("btrfs defragment (decompress) failed for {file} (exit {code}): {stderr}",
|
|
|
|
|
realPath, code, stderr);
|
|
|
|
|
linuxPath, code, stderr);
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (!string.IsNullOrWhiteSpace(stdout))
|
|
|
|
|
_logger.LogTrace("btrfs defragment output for {file}: {stdout}", realPath, stdout.Trim());
|
|
|
|
|
_logger.LogTrace("btrfs (decompress) output {file}: {out}", linuxPath, stdout.Trim());
|
|
|
|
|
|
|
|
|
|
_logger.LogInformation("Decompressed (rewritten) Btrfs file: {file}", realPath);
|
|
|
|
|
_logger.LogInformation("Decompressed (rewritten) Btrfs file: {file}", linuxPath);
|
|
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
catch (Exception ex)
|
|
|
|
|
@@ -405,11 +415,7 @@ public sealed class FileCompactor : IDisposable
|
|
|
|
|
_logger.LogWarning(ex, "Error rewriting {file} for Btrfs decompression", path);
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
finally
|
|
|
|
|
{
|
|
|
|
|
if (_btrfsGate.CurrentCount < 4)
|
|
|
|
|
_btrfsGate.Release();
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
@@ -459,19 +465,70 @@ public sealed class FileCompactor : IDisposable
|
|
|
|
|
/// <param name="path">Path that has to be converted</param>
|
|
|
|
|
/// <param name="isWine">Extra check if using the wine enviroment</param>
|
|
|
|
|
/// <returns>Converted path to be used in Linux</returns>
|
|
|
|
|
private string ToLinuxPathIfWine(string path, bool isWine)
|
|
|
|
|
private static string ToLinuxPathIfWine(string path, bool isWine)
|
|
|
|
|
{
|
|
|
|
|
if (!IsProbablyWine() && !isWine)
|
|
|
|
|
if (!isWine || !IsProbablyWine())
|
|
|
|
|
return path;
|
|
|
|
|
|
|
|
|
|
string linuxPath = path;
|
|
|
|
|
if (path.StartsWith("Z:\\", StringComparison.OrdinalIgnoreCase))
|
|
|
|
|
linuxPath = "/" + path[3..].Replace('\\', '/');
|
|
|
|
|
else if (path.StartsWith("C:\\", StringComparison.OrdinalIgnoreCase))
|
|
|
|
|
linuxPath = Path.Combine(Environment.GetEnvironmentVariable("HOME") ?? "/home", path[3..].Replace('\\', '/')).Replace('\\', '/');
|
|
|
|
|
return "/" + path[3..].Replace('\\', '/');
|
|
|
|
|
|
|
|
|
|
_logger.LogTrace("Detected Wine environment. Converted path for compression: {realPath}", linuxPath);
|
|
|
|
|
return linuxPath;
|
|
|
|
|
if (path.StartsWith("C:\\", StringComparison.OrdinalIgnoreCase))
|
|
|
|
|
{
|
|
|
|
|
var p = path.Replace('/', '\\');
|
|
|
|
|
|
|
|
|
|
const string usersPrefix = "C:\\Users\\";
|
|
|
|
|
if (p.StartsWith(usersPrefix, StringComparison.OrdinalIgnoreCase))
|
|
|
|
|
{
|
|
|
|
|
int afterUsers = usersPrefix.Length;
|
|
|
|
|
int slash = p.IndexOf('\\', afterUsers);
|
|
|
|
|
if (slash > 0 && slash + 1 < p.Length)
|
|
|
|
|
{
|
|
|
|
|
var rel = p[(slash + 1)..].Replace('\\', '/');
|
|
|
|
|
var home = Environment.GetEnvironmentVariable("HOME");
|
|
|
|
|
if (string.IsNullOrEmpty(home))
|
|
|
|
|
{
|
|
|
|
|
var linuxUser = Environment.GetEnvironmentVariable("USER") ?? Environment.UserName;
|
|
|
|
|
home = "/home/" + linuxUser;
|
|
|
|
|
}
|
|
|
|
|
// Join as Unix path
|
|
|
|
|
return (home.TrimEnd('/') + "/" + rel).Replace("//", "/", StringComparison.Ordinal);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
try
|
|
|
|
|
{
|
|
|
|
|
var inner = "winepath -u " + "'" + path.Replace("'", "'\\''", StringComparison.Ordinal) + "'";
|
|
|
|
|
var psi = new ProcessStartInfo
|
|
|
|
|
{
|
|
|
|
|
FileName = "/bin/bash",
|
|
|
|
|
Arguments = "-c " + "\"" + inner.Replace("\\", "\\\\", StringComparison.Ordinal).Replace("\"", "\\\"", StringComparison.Ordinal) + "\"",
|
|
|
|
|
RedirectStandardOutput = true,
|
|
|
|
|
RedirectStandardError = true,
|
|
|
|
|
UseShellExecute = false,
|
|
|
|
|
CreateNoWindow = true,
|
|
|
|
|
WorkingDirectory = "/"
|
|
|
|
|
};
|
|
|
|
|
using var proc = Process.Start(psi);
|
|
|
|
|
var outp = proc?.StandardOutput.ReadToEnd().Trim();
|
|
|
|
|
try
|
|
|
|
|
{
|
|
|
|
|
proc?.WaitForExit();
|
|
|
|
|
}
|
|
|
|
|
catch
|
|
|
|
|
{
|
|
|
|
|
/* Wine can throw here; ignore */
|
|
|
|
|
}
|
|
|
|
|
if (!string.IsNullOrEmpty(outp) && outp.StartsWith("/", StringComparison.Ordinal))
|
|
|
|
|
return outp;
|
|
|
|
|
}
|
|
|
|
|
catch
|
|
|
|
|
{
|
|
|
|
|
/* ignore and fall through */
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return path.Replace('\\', '/');
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
@@ -588,26 +645,32 @@ public sealed class FileCompactor : IDisposable
|
|
|
|
|
/// <param name="path">Path of the file</param>
|
|
|
|
|
/// <returns>State of the file</returns>
|
|
|
|
|
private bool IsBtrfsCompressedFile(string path)
|
|
|
|
|
{
|
|
|
|
|
return RunWithBtrfsGate(() =>
|
|
|
|
|
{
|
|
|
|
|
try
|
|
|
|
|
{
|
|
|
|
|
_btrfsGate.Wait(_compactionCts.Token);
|
|
|
|
|
bool windowsProc = RuntimeInformation.IsOSPlatform(OSPlatform.Windows);
|
|
|
|
|
string linuxPath = windowsProc ? ResolveLinuxPathForWine(path) : path;
|
|
|
|
|
|
|
|
|
|
bool isWine = _dalamudUtilService?.IsWine ?? false;
|
|
|
|
|
string realPath = isWine ? ToLinuxPathIfWine(path, isWine) : path;
|
|
|
|
|
var task = _fragBatch.IsCompressedAsync(linuxPath, _compactionCts.Token);
|
|
|
|
|
|
|
|
|
|
return _fragBatch.IsCompressedAsync(realPath, _compactionCts.Token).GetAwaiter().GetResult();
|
|
|
|
|
if (task.Wait(TimeSpan.FromSeconds(5), _compactionCts.Token) && task.IsCompletedSuccessfully)
|
|
|
|
|
return task.Result;
|
|
|
|
|
|
|
|
|
|
_logger.LogTrace("filefrag batch timed out for {file}", linuxPath);
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
catch (OperationCanceledException)
|
|
|
|
|
{
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
catch (Exception ex)
|
|
|
|
|
{
|
|
|
|
|
_logger.LogDebug(ex, "Failed to detect Btrfs compression for {file}", path);
|
|
|
|
|
_logger.LogDebug(ex, "filefrag batch check failed for {file}", path);
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
finally
|
|
|
|
|
{
|
|
|
|
|
if (_btrfsGate.CurrentCount < 4)
|
|
|
|
|
_btrfsGate.Release();
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
@@ -617,47 +680,40 @@ public sealed class FileCompactor : IDisposable
|
|
|
|
|
/// <returns>Compessing state</returns>
|
|
|
|
|
private bool BtrfsCompressFile(string path)
|
|
|
|
|
{
|
|
|
|
|
return RunWithBtrfsGate(() => {
|
|
|
|
|
try
|
|
|
|
|
{
|
|
|
|
|
bool isWine = _dalamudUtilService?.IsWine ?? false;
|
|
|
|
|
string realPath = isWine ? ToLinuxPathIfWine(path, isWine) : path;
|
|
|
|
|
var (winPath, linuxPath) = ResolvePathsForBtrfs(path);
|
|
|
|
|
bool isWindowsProc = RuntimeInformation.IsOSPlatform(OSPlatform.Windows);
|
|
|
|
|
|
|
|
|
|
var fi = new FileInfo(realPath);
|
|
|
|
|
|
|
|
|
|
if (fi == null)
|
|
|
|
|
if (IsBtrfsCompressedFile(linuxPath))
|
|
|
|
|
{
|
|
|
|
|
_logger.LogWarning("Failed to open {file} for compression; skipping", realPath);
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (IsBtrfsCompressedFile(realPath))
|
|
|
|
|
{
|
|
|
|
|
_logger.LogTrace("File {file} already compressed (Btrfs), skipping file", realPath);
|
|
|
|
|
_logger.LogTrace("Already Btrfs compressed: {file} (linux={linux})", winPath, linuxPath);
|
|
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (!ProbeFileReadable(realPath))
|
|
|
|
|
if (!ProbeFileReadableForBtrfs(winPath, linuxPath))
|
|
|
|
|
{
|
|
|
|
|
_logger.LogTrace("Probe failed; cannot open file for compress: {file} (linux={linux})", winPath, linuxPath);
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
(bool ok, string stdout, string stderr, int code) =
|
|
|
|
|
isWine
|
|
|
|
|
? RunProcessShell($"btrfs filesystem defragment -clzo -- {QuoteSingle(realPath)}")
|
|
|
|
|
: RunProcessDirect("btrfs", ["filesystem", "defragment", "-clzo", "--", realPath]);
|
|
|
|
|
isWindowsProc
|
|
|
|
|
? RunProcessShell($"btrfs filesystem defragment -clzo -- {QuoteSingle(linuxPath)}")
|
|
|
|
|
: RunProcessDirect("btrfs", ["filesystem", "defragment", "-clzo", "--", linuxPath]);
|
|
|
|
|
|
|
|
|
|
if (!ok)
|
|
|
|
|
{
|
|
|
|
|
_logger.LogWarning("btrfs defragment failed for {file} (exit {code}): {stderr}", realPath, code, stderr);
|
|
|
|
|
_logger.LogWarning("btrfs defragment failed for {file} (linux={linux}) exit {code}: {stderr}",
|
|
|
|
|
winPath, linuxPath, code, stderr);
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (!string.IsNullOrWhiteSpace(stdout))
|
|
|
|
|
_logger.LogTrace("btrfs output for {file}: {stdout}", realPath, stdout.Trim());
|
|
|
|
|
|
|
|
|
|
if (!string.IsNullOrWhiteSpace(stdout))
|
|
|
|
|
_logger.LogTrace("btrfs defragment output for {file}: {stdout}", realPath, stdout.Trim());
|
|
|
|
|
|
|
|
|
|
_logger.LogInformation("Compressed btrfs file successfully: {file}", realPath);
|
|
|
|
|
_logger.LogTrace("btrfs output for {file}: {out}", winPath, stdout.Trim());
|
|
|
|
|
|
|
|
|
|
_logger.LogInformation("Compressed btrfs file successfully: {file} (linux={linux})", winPath, linuxPath);
|
|
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
catch (Exception ex)
|
|
|
|
|
@@ -665,37 +721,7 @@ public sealed class FileCompactor : IDisposable
|
|
|
|
|
_logger.LogWarning(ex, "Error running btrfs defragment for {file}", path);
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
/// Probe file if its readable for certain amount of tries.
|
|
|
|
|
/// </summary>
|
|
|
|
|
/// <param name="path">Path where the file is located</param>
|
|
|
|
|
/// <param name="fs">Filestream used for the function</param>
|
|
|
|
|
/// <returns>State of the filestream opening</returns>
|
|
|
|
|
private bool ProbeFileReadable(string path)
|
|
|
|
|
{
|
|
|
|
|
for (int attempt = 0; attempt < _maxRetries; attempt++)
|
|
|
|
|
{
|
|
|
|
|
try
|
|
|
|
|
{
|
|
|
|
|
using var _ = new FileStream(path, FileMode.Open, FileAccess.Read, FileShare.ReadWrite | FileShare.Delete);
|
|
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
catch (IOException ex)
|
|
|
|
|
{
|
|
|
|
|
if (attempt == _maxRetries - 1)
|
|
|
|
|
{
|
|
|
|
|
_logger.LogWarning(ex, "File still in use after {attempts} attempts, skipping {file}", _maxRetries, path);
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
int delay = 150 * (attempt + 1);
|
|
|
|
|
_logger.LogTrace(ex, "File busy, retrying in {delay}ms for {file}", delay, path);
|
|
|
|
|
Thread.Sleep(delay);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return false;
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
@@ -742,7 +768,7 @@ public sealed class FileCompactor : IDisposable
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
/// Runs an nonshell process meant for Linux/Wine enviroments
|
|
|
|
|
/// Runs an nonshell process meant for Linux enviroments
|
|
|
|
|
/// </summary>
|
|
|
|
|
/// <param name="fileName">File that has to be excuted</param>
|
|
|
|
|
/// <param name="args">Arguments meant for the file/command</param>
|
|
|
|
|
@@ -759,32 +785,47 @@ public sealed class FileCompactor : IDisposable
|
|
|
|
|
CreateNoWindow = true
|
|
|
|
|
};
|
|
|
|
|
if (!string.IsNullOrEmpty(workingDir)) psi.WorkingDirectory = workingDir;
|
|
|
|
|
|
|
|
|
|
foreach (var a in args) psi.ArgumentList.Add(a);
|
|
|
|
|
EnsureUnixPathEnv(psi);
|
|
|
|
|
|
|
|
|
|
using var proc = Process.Start(psi);
|
|
|
|
|
if (proc is null) return (false, "", "failed to start process", -1);
|
|
|
|
|
|
|
|
|
|
var outTask = proc.StandardOutput.ReadToEndAsync(_compactionCts.Token);
|
|
|
|
|
var errTask = proc.StandardError.ReadToEndAsync(_compactionCts.Token);
|
|
|
|
|
var both = Task.WhenAll(outTask, errTask);
|
|
|
|
|
|
|
|
|
|
if (_dalamudUtilService.IsWine)
|
|
|
|
|
{
|
|
|
|
|
var finished = Task.WhenAny(both, Task.Delay(timeoutMs, _compactionCts.Token)).GetAwaiter().GetResult();
|
|
|
|
|
if (finished != both)
|
|
|
|
|
{
|
|
|
|
|
try { proc.Kill(entireProcessTree: true); } catch { /* ignore this */ }
|
|
|
|
|
try { Task.WaitAll(new[] { outTask, errTask }, 1000, _compactionCts.Token); } catch { /* ignore this */ }
|
|
|
|
|
var so = outTask.IsCompleted ? outTask.Result : "";
|
|
|
|
|
var se = errTask.IsCompleted ? errTask.Result : "timeout";
|
|
|
|
|
return (false, so, se, -1);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
var stdout = outTask.Result;
|
|
|
|
|
var stderr = errTask.Result;
|
|
|
|
|
var ok = string.IsNullOrWhiteSpace(stderr);
|
|
|
|
|
return (ok, stdout, stderr, ok ? 0 : -1);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (!proc.WaitForExit(timeoutMs))
|
|
|
|
|
{
|
|
|
|
|
try
|
|
|
|
|
{
|
|
|
|
|
proc.Kill(entireProcessTree: true);
|
|
|
|
|
}
|
|
|
|
|
catch
|
|
|
|
|
{
|
|
|
|
|
// Ignore this catch on the dispose
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
Task.WaitAll([outTask, errTask], 1000, _compactionCts.Token);
|
|
|
|
|
return (false, outTask.Result, "timeout", -1);
|
|
|
|
|
try { proc.Kill(entireProcessTree: true); } catch { /* ignore this */ }
|
|
|
|
|
try { Task.WaitAll([outTask, errTask], 1000, _compactionCts.Token); } catch { /* ignore this */ }
|
|
|
|
|
return (false, outTask.IsCompleted ? outTask.Result : "", "timeout", -1);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
Task.WaitAll(outTask, errTask);
|
|
|
|
|
return (proc.ExitCode == 0, outTask.Result, errTask.Result, proc.ExitCode);
|
|
|
|
|
var so2 = outTask.Result;
|
|
|
|
|
var se2 = errTask.Result;
|
|
|
|
|
int code;
|
|
|
|
|
try { code = proc.ExitCode; } catch { code = -1; }
|
|
|
|
|
return (code == 0, so2, se2, code);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
@@ -793,8 +834,9 @@ public sealed class FileCompactor : IDisposable
|
|
|
|
|
/// <param name="command">Command that has to be excuted</param>
|
|
|
|
|
/// <param name="timeoutMs">Timeout timer for the process</param>
|
|
|
|
|
/// <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, int timeoutMs = 60000)
|
|
|
|
|
private (bool ok, string stdout, string stderr, int exitCode) RunProcessShell(string command, string? workingDir = null, int timeoutMs = 60000)
|
|
|
|
|
{
|
|
|
|
|
// Use a LOGIN shell so PATH includes /usr/sbin etc.
|
|
|
|
|
var psi = new ProcessStartInfo("/bin/bash")
|
|
|
|
|
{
|
|
|
|
|
RedirectStandardOutput = true,
|
|
|
|
|
@@ -802,32 +844,50 @@ public sealed class FileCompactor : IDisposable
|
|
|
|
|
UseShellExecute = false,
|
|
|
|
|
CreateNoWindow = true
|
|
|
|
|
};
|
|
|
|
|
psi.ArgumentList.Add("-c");
|
|
|
|
|
psi.ArgumentList.Add(command);
|
|
|
|
|
if (!string.IsNullOrEmpty(workingDir)) psi.WorkingDirectory = workingDir;
|
|
|
|
|
|
|
|
|
|
psi.ArgumentList.Add("-lc");
|
|
|
|
|
psi.ArgumentList.Add(QuoteDouble(command));
|
|
|
|
|
EnsureUnixPathEnv(psi);
|
|
|
|
|
|
|
|
|
|
using var proc = Process.Start(psi);
|
|
|
|
|
if (proc is null) return (false, "", "failed to start /bin/bash", -1);
|
|
|
|
|
|
|
|
|
|
var outTask = proc.StandardOutput.ReadToEndAsync(_compactionCts.Token);
|
|
|
|
|
var errTask = proc.StandardError.ReadToEndAsync(_compactionCts.Token);
|
|
|
|
|
var both = Task.WhenAll(outTask, errTask);
|
|
|
|
|
|
|
|
|
|
if (_dalamudUtilService.IsWine)
|
|
|
|
|
{
|
|
|
|
|
var finished = Task.WhenAny(both, Task.Delay(timeoutMs, _compactionCts.Token)).GetAwaiter().GetResult();
|
|
|
|
|
if (finished != both)
|
|
|
|
|
{
|
|
|
|
|
try { proc.Kill(entireProcessTree: true); } catch { /* ignore this */ }
|
|
|
|
|
try { Task.WaitAll([outTask, errTask], 1000, _compactionCts.Token); } catch { /* ignore this */ }
|
|
|
|
|
var so = outTask.IsCompleted ? outTask.Result : "";
|
|
|
|
|
var se = errTask.IsCompleted ? errTask.Result : "timeout";
|
|
|
|
|
return (false, so, se, -1);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
var stdout = outTask.Result;
|
|
|
|
|
var stderr = errTask.Result;
|
|
|
|
|
var ok = string.IsNullOrWhiteSpace(stderr);
|
|
|
|
|
return (ok, stdout, stderr, ok ? 0 : -1);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (!proc.WaitForExit(timeoutMs))
|
|
|
|
|
{
|
|
|
|
|
try
|
|
|
|
|
{
|
|
|
|
|
proc.Kill(entireProcessTree: true);
|
|
|
|
|
}
|
|
|
|
|
catch
|
|
|
|
|
{
|
|
|
|
|
// Ignore this catch on the dispose
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
Task.WaitAll([outTask, errTask], 1000, _compactionCts.Token);
|
|
|
|
|
return (false, outTask.Result, "timeout", -1);
|
|
|
|
|
try { proc.Kill(entireProcessTree: true); } catch { /* ignore this */ }
|
|
|
|
|
try { Task.WaitAll([outTask, errTask], 1000, _compactionCts.Token); } catch { /* ignore this */ }
|
|
|
|
|
return (false, outTask.IsCompleted ? outTask.Result : "", "timeout", -1);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
Task.WaitAll(outTask, errTask);
|
|
|
|
|
return (proc.ExitCode == 0, outTask.Result, errTask.Result, proc.ExitCode);
|
|
|
|
|
var so2 = outTask.Result;
|
|
|
|
|
var se2 = errTask.Result;
|
|
|
|
|
int code;
|
|
|
|
|
try { code = proc.ExitCode; } catch { code = -1; }
|
|
|
|
|
return (code == 0, so2, se2, code);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
@@ -931,6 +991,69 @@ public sealed class FileCompactor : IDisposable
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private string ResolveLinuxPathForWine(string windowsPath)
|
|
|
|
|
{
|
|
|
|
|
var (ok, outp, _, _) = RunProcessShell($"winepath -u {QuoteSingle(windowsPath)}", null, 5000);
|
|
|
|
|
if (ok && !string.IsNullOrWhiteSpace(outp)) return outp.Trim();
|
|
|
|
|
return ToLinuxPathIfWine(windowsPath, isWine: true);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private static void EnsureUnixPathEnv(ProcessStartInfo psi)
|
|
|
|
|
{
|
|
|
|
|
if (!psi.Environment.TryGetValue("PATH", out var p) || string.IsNullOrWhiteSpace(p))
|
|
|
|
|
psi.Environment["PATH"] = "/usr/sbin:/usr/bin:/bin";
|
|
|
|
|
else if (!p.Contains("/usr/sbin", StringComparison.Ordinal))
|
|
|
|
|
psi.Environment["PATH"] = "/usr/sbin:/usr/bin:/bin:" + p;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private (string windowsPath, string linuxPath) ResolvePathsForBtrfs(string path)
|
|
|
|
|
{
|
|
|
|
|
bool isWindowsProc = RuntimeInformation.IsOSPlatform(OSPlatform.Windows);
|
|
|
|
|
|
|
|
|
|
if (!isWindowsProc)
|
|
|
|
|
return (path, path);
|
|
|
|
|
|
|
|
|
|
// Prefer winepath -u; fall back to your existing mapper
|
|
|
|
|
var (ok, outp, _, _) = RunProcessShell($"winepath -u {QuoteSingle(path)}", null, 5000);
|
|
|
|
|
var linux = (ok && !string.IsNullOrWhiteSpace(outp)) ? outp.Trim()
|
|
|
|
|
: ToLinuxPathIfWine(path, isWine: true);
|
|
|
|
|
|
|
|
|
|
return (path, linux);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private bool ProbeFileReadableForBtrfs(string windowsPath, string linuxPath)
|
|
|
|
|
{
|
|
|
|
|
try
|
|
|
|
|
{
|
|
|
|
|
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
|
|
|
|
|
{
|
|
|
|
|
using var _ = new FileStream(windowsPath, FileMode.Open, FileAccess.Read, FileShare.ReadWrite);
|
|
|
|
|
}
|
|
|
|
|
else
|
|
|
|
|
{
|
|
|
|
|
using var _ = new FileStream(linuxPath, FileMode.Open, FileAccess.Read, FileShare.ReadWrite);
|
|
|
|
|
}
|
|
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
catch { return false; }
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private T RunWithBtrfsGate<T>(Func<T> body)
|
|
|
|
|
{
|
|
|
|
|
bool acquired = false;
|
|
|
|
|
try
|
|
|
|
|
{
|
|
|
|
|
_btrfsGate.Wait(_compactionCts.Token);
|
|
|
|
|
acquired = true;
|
|
|
|
|
return body();
|
|
|
|
|
}
|
|
|
|
|
finally
|
|
|
|
|
{
|
|
|
|
|
if (acquired) _btrfsGate.Release();
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
[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);
|
|
|
|
|
|
|
|
|
|
@@ -945,6 +1068,8 @@ public sealed class FileCompactor : IDisposable
|
|
|
|
|
|
|
|
|
|
private static string QuoteSingle(string s) => "'" + s.Replace("'", "'\\''", StringComparison.Ordinal) + "'";
|
|
|
|
|
|
|
|
|
|
private static string QuoteDouble(string s) => "\"" + s.Replace("\\", "\\\\", StringComparison.Ordinal).Replace("\"", "\\\"", StringComparison.Ordinal).Replace("$", "\\$", StringComparison.Ordinal).Replace("`", "\\`", StringComparison.Ordinal) + "\"";
|
|
|
|
|
|
|
|
|
|
public void Dispose()
|
|
|
|
|
{
|
|
|
|
|
_fragBatch?.Dispose();
|
|
|
|
|
|