using LightlessSync.FileCache; using Microsoft.Extensions.Logging; using System.Diagnostics; using System.IO.Pipes; using System.Text.Json; internal sealed class WorkerCompactorContext : ICompactorContext { public WorkerCompactorContext(string cacheFolder, bool isWine) { CacheFolder = cacheFolder; IsWine = isWine; } public bool UseCompactor => true; public string CacheFolder { get; } public bool IsWine { get; } } internal sealed class WorkerOptions { public string? FilePath { get; init; } public bool IsWine { get; init; } public string CacheFolder { get; init; } = string.Empty; public LogLevel LogLevel { get; init; } = LogLevel.Information; public string PipeName { get; init; } = "LightlessCompactor"; public int? ParentProcessId { get; init; } } internal static class Program { public static async Task Main(string[] args) { var options = ParseOptions(args, out var error); if (options is null) { Console.Error.WriteLine(error ?? "Invalid arguments."); Console.Error.WriteLine("Usage: LightlessCompactorWorker --file [--wine] [--cache-folder ] [--verbose]"); Console.Error.WriteLine(" or: LightlessCompactorWorker --pipe [--wine] [--parent ] [--verbose]"); return 2; } TrySetLowPriority(); using var loggerFactory = LoggerFactory.Create(builder => { builder.SetMinimumLevel(options.LogLevel); builder.AddSimpleConsole(o => { o.SingleLine = true; o.TimestampFormat = "HH:mm:ss.fff "; }); }); var logger = loggerFactory.CreateLogger(); var context = new WorkerCompactorContext(options.CacheFolder, options.IsWine); using var compactor = new FileCompactor(logger, context, new NoopCompactionExecutor()); if (!string.IsNullOrWhiteSpace(options.FilePath)) { var success = compactor.TryCompactFile(options.FilePath!); return success ? 0 : 1; } var serverLogger = loggerFactory.CreateLogger("CompactorWorker"); return await RunServerAsync(compactor, options, serverLogger).ConfigureAwait(false); } private static async Task RunServerAsync(FileCompactor compactor, WorkerOptions options, ILogger serverLogger) { using var cts = new CancellationTokenSource(); var token = cts.Token; if (options.ParentProcessId.HasValue) { _ = Task.Run(() => MonitorParent(options.ParentProcessId.Value, cts)); } serverLogger.LogInformation("Compactor worker listening on pipe {pipe}", options.PipeName); try { while (!token.IsCancellationRequested) { var server = new NamedPipeServerStream( options.PipeName, PipeDirection.InOut, NamedPipeServerStream.MaxAllowedServerInstances, PipeTransmissionMode.Byte, PipeOptions.Asynchronous); try { await server.WaitForConnectionAsync(token).ConfigureAwait(false); } catch { server.Dispose(); throw; } _ = Task.Run(() => HandleClientAsync(server, compactor, cts)); } } catch (OperationCanceledException) { // shutdown requested } catch (Exception ex) { serverLogger.LogWarning(ex, "Compactor worker terminated unexpectedly."); return 1; } return 0; } private static async Task HandleClientAsync(NamedPipeServerStream pipe, FileCompactor compactor, CancellationTokenSource shutdownCts) { await using var _ = pipe; using var reader = new StreamReader(pipe); using var writer = new StreamWriter(pipe) { AutoFlush = true }; var line = await reader.ReadLineAsync().ConfigureAwait(false); if (string.IsNullOrWhiteSpace(line)) return; CompactorRequest? request = null; try { request = JsonSerializer.Deserialize(line); } catch { // ignore } CompactorResponse response; if (request is null) { response = new CompactorResponse { Success = false, Error = "Invalid request." }; } else if (string.Equals(request.Type, "shutdown", StringComparison.OrdinalIgnoreCase)) { shutdownCts.Cancel(); response = new CompactorResponse { Success = true }; } else if (string.Equals(request.Type, "compact", StringComparison.OrdinalIgnoreCase)) { var success = compactor.TryCompactFile(request.Path ?? string.Empty); response = new CompactorResponse { Success = success }; } else { response = new CompactorResponse { Success = false, Error = "Unknown request type." }; } await writer.WriteLineAsync(JsonSerializer.Serialize(response)).ConfigureAwait(false); } private static void MonitorParent(int parentPid, CancellationTokenSource shutdownCts) { try { var parent = Process.GetProcessById(parentPid); parent.WaitForExit(); } catch { // parent missing } finally { shutdownCts.Cancel(); } } private static WorkerOptions? ParseOptions(string[] args, out string? error) { string? filePath = null; bool isWine = false; string cacheFolder = string.Empty; var logLevel = LogLevel.Information; string pipeName = "LightlessCompactor"; int? parentPid = null; for (int i = 0; i < args.Length; i++) { var arg = args[i]; switch (arg) { case "--file": if (i + 1 >= args.Length) { error = "Missing value for --file."; return null; } filePath = args[++i]; break; case "--cache-folder": if (i + 1 >= args.Length) { error = "Missing value for --cache-folder."; return null; } cacheFolder = args[++i]; break; case "--pipe": if (i + 1 >= args.Length) { error = "Missing value for --pipe."; return null; } pipeName = args[++i]; break; case "--parent": if (i + 1 >= args.Length || !int.TryParse(args[++i], out var pid)) { error = "Invalid value for --parent."; return null; } parentPid = pid; break; case "--wine": isWine = true; break; case "--verbose": logLevel = LogLevel.Trace; break; } } error = null; return new WorkerOptions { FilePath = filePath, IsWine = isWine, CacheFolder = cacheFolder, LogLevel = logLevel, PipeName = pipeName, ParentProcessId = parentPid }; } private static void TrySetLowPriority() { try { if (OperatingSystem.IsWindows()) Process.GetCurrentProcess().PriorityClass = ProcessPriorityClass.BelowNormal; } 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; } } }