Files
LightlessClient/LightlessSync/FileCache/ExternalCompactionExecutor.cs
2026-01-16 11:00:58 +09:00

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; }
}
}