diff --git a/HISTORY.md b/HISTORY.md index e1d5d64b..07e1dfd1 100644 --- a/HISTORY.md +++ b/HISTORY.md @@ -6,6 +6,8 @@ Utility to optimize media files for Direct Play in Plex, Emby, Jellyfin, etc. - Version 3.20: - Added per-file stateful log level filtering to enable information level output after a warning or error event is detected, overriding the `--logwarning` warning only filter. Prior to this change the log output would only contain the trigger warning or error event, now it will also log all subsequent information level events for that file during its processing cycle. + - Always log the end-of-run summary at information level, even with `--logwarning`, so the modified, error, and verify-failed counts are recorded for every processing run. + - Handle the `SIGINT`, `SIGTERM`, and `SIGQUIT` termination signals (`docker stop`, `Ctrl+C`) so processing is interrupted gracefully and the summary and exit code are logged before exit. The custom `Ctrl+Q`/`Ctrl+Z` exit keys are removed in favor of the standard signals. - Version 3.19: - Reworked the CI/CD pipeline to a branch-scoped self-publishing model: a weekly scheduled run (and manual dispatch) publishes both `main` (stable, Docker `latest`) and `develop` (prerelease, Docker `develop`) - native executables, the multi-arch Docker image, and the GitHub release - while merges accumulate until the next run. No application changes. - Added `WORKFLOW.md` (the canonical CI/CD specification) and `repo-config/` (rulesets and repository settings as code). diff --git a/PlexCleaner/Monitor.cs b/PlexCleaner/Monitor.cs index 49d12609..6a4efb69 100644 --- a/PlexCleaner/Monitor.cs +++ b/PlexCleaner/Monitor.cs @@ -12,11 +12,7 @@ public class Monitor private readonly Lock _watchLock = new(); - private static void LogMonitorMessage() - { - Log.Information("Monitoring folders ..."); - Program.LogInterruptMessage(); - } + private static void LogMonitorMessage() => Log.Information("Monitoring folders ..."); public bool MonitorFolders(List folders) { diff --git a/PlexCleaner/Process.cs b/PlexCleaner/Process.cs index ba854e60..e1b7da45 100644 --- a/PlexCleaner/Process.cs +++ b/PlexCleaner/Process.cs @@ -366,7 +366,7 @@ public static bool DeleteEmptyFolders(IEnumerable folderList) fatalError = true; } - Log.Information("Deleted folders : {Deleted}", totalDeleted); + Log.Logger.LogOverrideContext().Information("Deleted folders : {Deleted}", totalDeleted); return !fatalError; } diff --git a/PlexCleaner/ProcessDriver.cs b/PlexCleaner/ProcessDriver.cs index a38253b7..4268278a 100644 --- a/PlexCleaner/ProcessDriver.cs +++ b/PlexCleaner/ProcessDriver.cs @@ -220,11 +220,11 @@ Func taskFunc // Stop the timer timer.Stop(); - // Done - Log.Information("Completed {TaskName}", taskName); - Log.Information("Processing time : {Elapsed}", timer.Elapsed); - Log.Information("Total files : {Count}", totalCount); - Log.Information("Error files : {Count}", errorCount); + // Done, force logging so the summary survives the warning floor and an interrupted run + Log.Logger.LogOverrideContext().Information("Completed {TaskName}", taskName); + Log.Logger.LogOverrideContext().Information("Processing time : {Elapsed}", timer.Elapsed); + Log.Logger.LogOverrideContext().Information("Total files : {Count}", totalCount); + Log.Logger.LogOverrideContext().Information("Error files : {Count}", errorCount); return !error; } diff --git a/PlexCleaner/Program.cs b/PlexCleaner/Program.cs index 1f88c469..daf6615a 100644 --- a/PlexCleaner/Program.cs +++ b/PlexCleaner/Program.cs @@ -12,6 +12,7 @@ namespace PlexCleaner; public static class Program { + // Never disposed, so signal handlers can safely call Cancel() for the process lifetime private static readonly CancellationTokenSource s_cancelSource = new(); private static readonly Lazy s_httpClient = new(CreateHttpClient); @@ -41,15 +42,6 @@ private static HttpClient CreateHttpClient() private static int MakeExitCode(bool success) => success ? (int)ExitCode.Success : (int)ExitCode.Error; - public static void LogInterruptMessage() - { - // Keyboard handler is only active if input is not redirected - if (!Console.IsInputRedirected) - { - Console.WriteLine("Press Ctrl+C or Ctrl+Z or Ctrl+Q to exit."); - } - } - private static int Main(string[] args) { // Wait for debugger to attach @@ -71,12 +63,20 @@ private static int Main(string[] args) // Setup CreateLogger(); - Console.CancelKeyPress += CancelEventHandler; - Task? consoleKeyTask = null; - if (!Console.IsInputRedirected) - { - consoleKeyTask = Task.Run(KeyPressHandler); - } + + // Handle termination signals to cancel gracefully and still log the summary before exit + PosixSignalRegistration sigIntRegistration = PosixSignalRegistration.Create( + PosixSignal.SIGINT, + PosixSignalHandler + ); + PosixSignalRegistration sigTermRegistration = PosixSignalRegistration.Create( + PosixSignal.SIGTERM, + PosixSignalHandler + ); + PosixSignalRegistration sigQuitRegistration = PosixSignalRegistration.Create( + PosixSignal.SIGQUIT, + PosixSignalHandler + ); // Keep the system from going to sleep KeepAwake.PreventSleep(); @@ -90,8 +90,10 @@ private static int Main(string[] args) // Cleanup Cancel(); - consoleKeyTask?.Wait(); - Console.CancelKeyPress -= CancelEventHandler; + // Unhook signals before flushing so a second signal reverts to default OS termination + sigIntRegistration.Dispose(); + sigTermRegistration.Dispose(); + sigQuitRegistration.Dispose(); keepAwakeTimer.Stop(); KeepAwake.AllowSleep(); @@ -135,47 +137,13 @@ public static void VerifyLatestVersion() } } - private static void KeyPressHandler() - { - for (; ; ) - { - // Wait on key available or cancelled - while (!Console.KeyAvailable) - { - if (WaitForCancel(100)) - { - // Done - return; - } - } - - // Read key and hide from console display - ConsoleKeyInfo keyInfo = Console.ReadKey(true); - - // Break on Ctrl+Q or Ctrl+Z, Ctrl+C and Ctrl+Break is handled in cancel handler - if ( - keyInfo.Key is ConsoleKey.Q or ConsoleKey.Z - && keyInfo.Modifiers == ConsoleModifiers.Control - ) - { - // Signal the cancel event - Cancel(ConsoleModifiers.Control, keyInfo.Key); - - // Done - return; - } - } - } - - private static void CancelEventHandler(object? sender, ConsoleCancelEventArgs eventArgs) + private static void PosixSignalHandler(PosixSignalContext context) { - Log.Warning("Cancel event triggered : {EventType}", eventArgs.SpecialKey); - - // Keep running and do graceful exit - eventArgs.Cancel = true; + Log.Warning("Operation interrupted : {Signal}", context.Signal); - // Signal the cancel event, use Ctrl+Break as signal - Cancel(ConsoleModifiers.Control, ConsoleKey.Pause); + // Keep running and do a graceful exit so the summary and exit code are logged + context.Cancel = true; + Cancel(); } private static void WaitForDebugger() @@ -563,12 +531,6 @@ public static void Cancel() => // Signal cancel s_cancelSource.Cancel(); - public static void Cancel(ConsoleModifiers modifiers, ConsoleKey key) - { - Log.Warning("Operation interrupted : {Modifiers}+{Key}", modifiers, key); - Cancel(); - } - public static CancellationToken CancelToken() => s_cancelSource.Token; private enum ExitCode diff --git a/README.md b/README.md index 8c94535f..4935a397 100644 --- a/README.md +++ b/README.md @@ -28,6 +28,7 @@ Utility to optimize media files for Direct Play in Plex, Emby, Jellyfin, etc. **Summary:** - Added per-file stateful log level filtering to enable information level output after a warning or error event is detected, overriding the `--logwarning` warning only filter. +- Always log the end-of-run summary, and handle stop signals (`docker stop`, `Ctrl+C`) so processing stops gracefully and the summary and exit code are logged before exit. See [Release History](./HISTORY.md) for complete release notes and older versions. @@ -222,6 +223,7 @@ services: image: docker.io/ptr727/plexcleaner:latest # Use :develop for pre-release builds container_name: PlexCleaner restart: unless-stopped + stop_grace_period: 30s # Allow time to finish the current file and log the summary on stop user: 1000:100 # Change to match your nonroot:users command: - /PlexCleaner/PlexCleaner @@ -237,6 +239,8 @@ services: - /data/media:/media # Map host path /data/media to container /media (read/write) ``` +On stop (`docker stop`, or Compose shutdown), PlexCleaner handles the termination signal, stops processing gracefully, and logs the run summary and exit code before exiting. Because it aborts the in-flight file rather than killing it mid-write, allow enough grace for a long re-encode to unwind: set `stop_grace_period` (Compose) or `--stop-timeout` / `docker stop --time 30` (Docker run). Without enough grace the container is force-killed (`SIGKILL`) and the summary is lost. + #### Docker Run Examples For a simple one-time process operation, see the [Getting Started](#getting-started) example.