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 _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 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(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 { "--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; } } }