242 lines
6.4 KiB
C#
242 lines
6.4 KiB
C#
using Microsoft.Extensions.Logging;
|
|
using System.Diagnostics;
|
|
using System.IO.Pipes;
|
|
using System.Text.Json;
|
|
|
|
namespace LightlessSync.FileCache;
|
|
|
|
internal sealed class ExternalCompactionExecutor : ICompactionExecutor, IDisposable
|
|
{
|
|
private readonly ILogger<ExternalCompactionExecutor> _logger;
|
|
private readonly ICompactorContext _context;
|
|
private readonly TimeSpan _timeout = TimeSpan.FromMinutes(5);
|
|
private readonly string _pipeName;
|
|
private Process? _workerProcess;
|
|
private bool _disposed;
|
|
private readonly object _sync = new();
|
|
|
|
public ExternalCompactionExecutor(ILogger<ExternalCompactionExecutor> logger, ICompactorContext context)
|
|
{
|
|
_logger = logger;
|
|
_context = context;
|
|
_pipeName = $"LightlessCompactor-{Environment.ProcessId}";
|
|
}
|
|
|
|
public bool TryCompact(string filePath)
|
|
{
|
|
if (string.IsNullOrWhiteSpace(filePath) || !File.Exists(filePath))
|
|
return false;
|
|
|
|
if (!EnsureWorkerRunning())
|
|
return false;
|
|
|
|
try
|
|
{
|
|
var request = new CompactorRequest
|
|
{
|
|
Type = "compact",
|
|
Path = filePath
|
|
};
|
|
|
|
return SendRequest(request, out var response) && response?.Success == true;
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
_logger.LogWarning(ex, "External compactor failed for {file}", filePath);
|
|
return false;
|
|
}
|
|
}
|
|
|
|
public void Dispose()
|
|
{
|
|
if (_disposed)
|
|
return;
|
|
|
|
_disposed = true;
|
|
|
|
try
|
|
{
|
|
SendRequest(new CompactorRequest { Type = "shutdown" }, out _);
|
|
}
|
|
catch
|
|
{
|
|
// ignore
|
|
}
|
|
|
|
lock (_sync)
|
|
{
|
|
if (_workerProcess is null)
|
|
return;
|
|
|
|
TryKill(_workerProcess);
|
|
_workerProcess.Dispose();
|
|
_workerProcess = null;
|
|
}
|
|
}
|
|
|
|
private bool EnsureWorkerRunning()
|
|
{
|
|
lock (_sync)
|
|
{
|
|
if (_workerProcess is { HasExited: false })
|
|
return true;
|
|
|
|
_workerProcess?.Dispose();
|
|
_workerProcess = null;
|
|
|
|
var workerPath = ResolveWorkerPath();
|
|
if (string.IsNullOrEmpty(workerPath))
|
|
return false;
|
|
|
|
var args = BuildArguments();
|
|
var startInfo = new ProcessStartInfo
|
|
{
|
|
FileName = workerPath,
|
|
Arguments = args,
|
|
CreateNoWindow = true,
|
|
UseShellExecute = false,
|
|
RedirectStandardOutput = true,
|
|
RedirectStandardError = true
|
|
};
|
|
|
|
var process = new Process { StartInfo = startInfo };
|
|
if (!process.Start())
|
|
return false;
|
|
|
|
TrySetLowPriority(process);
|
|
_ = DrainAsync(process.StandardOutput, "stdout");
|
|
_ = DrainAsync(process.StandardError, "stderr");
|
|
|
|
_workerProcess = process;
|
|
return true;
|
|
}
|
|
}
|
|
|
|
private bool SendRequest(CompactorRequest request, out CompactorResponse? response)
|
|
{
|
|
response = null;
|
|
using var pipe = new NamedPipeClientStream(".", _pipeName, PipeDirection.InOut, PipeOptions.Asynchronous);
|
|
|
|
try
|
|
{
|
|
pipe.Connect((int)_timeout.TotalMilliseconds);
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
_logger.LogDebug(ex, "Compactor pipe connection failed.");
|
|
return false;
|
|
}
|
|
|
|
using var writer = new StreamWriter(pipe) { AutoFlush = true };
|
|
using var reader = new StreamReader(pipe);
|
|
|
|
var payload = JsonSerializer.Serialize(request);
|
|
writer.WriteLine(payload);
|
|
|
|
var readTask = reader.ReadLineAsync();
|
|
if (!readTask.Wait(_timeout))
|
|
{
|
|
_logger.LogWarning("Compactor pipe timed out waiting for response.");
|
|
return false;
|
|
}
|
|
|
|
var line = readTask.Result;
|
|
if (string.IsNullOrWhiteSpace(line))
|
|
return false;
|
|
|
|
try
|
|
{
|
|
response = JsonSerializer.Deserialize<CompactorResponse>(line);
|
|
return response is not null;
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
_logger.LogDebug(ex, "Failed to parse compactor response.");
|
|
return false;
|
|
}
|
|
}
|
|
|
|
private string? ResolveWorkerPath()
|
|
{
|
|
var baseDir = AppContext.BaseDirectory;
|
|
var exeName = OperatingSystem.IsWindows() || _context.IsWine
|
|
? "LightlessCompactorWorker.exe"
|
|
: "LightlessCompactorWorker";
|
|
var path = Path.Combine(baseDir, exeName);
|
|
return File.Exists(path) ? path : null;
|
|
}
|
|
|
|
private string BuildArguments()
|
|
{
|
|
var args = new List<string> { "--pipe", Quote(_pipeName), "--parent", Environment.ProcessId.ToString() };
|
|
if (_context.IsWine)
|
|
args.Add("--wine");
|
|
return string.Join(' ', args);
|
|
}
|
|
|
|
private static string Quote(string value)
|
|
{
|
|
if (string.IsNullOrEmpty(value))
|
|
return "\"\"";
|
|
|
|
if (!value.Contains('"', StringComparison.Ordinal))
|
|
return "\"" + value + "\"";
|
|
|
|
return "\"" + value.Replace("\"", "\\\"", StringComparison.Ordinal) + "\"";
|
|
}
|
|
|
|
private static void TrySetLowPriority(Process process)
|
|
{
|
|
try
|
|
{
|
|
if (OperatingSystem.IsWindows())
|
|
process.PriorityClass = ProcessPriorityClass.BelowNormal;
|
|
}
|
|
catch
|
|
{
|
|
// ignore
|
|
}
|
|
}
|
|
|
|
private async Task DrainAsync(StreamReader reader, string label)
|
|
{
|
|
try
|
|
{
|
|
string? line;
|
|
while ((line = await reader.ReadLineAsync().ConfigureAwait(false)) != null)
|
|
{
|
|
if (_logger.IsEnabled(LogLevel.Trace))
|
|
_logger.LogTrace("Compactor {label}: {line}", label, line);
|
|
}
|
|
}
|
|
catch
|
|
{
|
|
// ignore
|
|
}
|
|
}
|
|
|
|
private static void TryKill(Process process)
|
|
{
|
|
try
|
|
{
|
|
process.Kill(entireProcessTree: true);
|
|
}
|
|
catch
|
|
{
|
|
// ignore
|
|
}
|
|
}
|
|
|
|
private sealed class CompactorRequest
|
|
{
|
|
public string Type { get; init; } = "compact";
|
|
public string? Path { get; init; }
|
|
}
|
|
|
|
private sealed class CompactorResponse
|
|
{
|
|
public bool Success { get; init; }
|
|
public string? Error { get; init; }
|
|
}
|
|
}
|