271 lines
8.3 KiB
C#
271 lines
8.3 KiB
C#
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<int> 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 <path> [--wine] [--cache-folder <path>] [--verbose]");
|
|
Console.Error.WriteLine(" or: LightlessCompactorWorker --pipe <name> [--wine] [--parent <pid>] [--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<FileCompactor>();
|
|
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<int> 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<CompactorRequest>(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; }
|
|
}
|
|
}
|