From 8b05977ae0d56ea51bd5d2470ec3805f7d701fb7 Mon Sep 17 00:00:00 2001 From: Stephan Boyer Date: Fri, 5 Apr 2024 02:00:07 -0700 Subject: [PATCH] Handle signals and clean up subprocesses --- CHANGELOG.md | 5 +++ Cargo.lock | 119 ++++++++++++++++++++++++++++++++++++++++++++------- Cargo.toml | 9 ++-- src/main.rs | 41 ++++++++++++++++-- src/run.rs | 49 ++++++++++----------- 5 files changed, 176 insertions(+), 47 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3924132..ca2844e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,11 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [0.24.0] - 2024-04-05 + +### Fixed +- Docuum now cleans up child processes when exiting due to a signal (`SIGHUP`, `SIGINT`, or `SIGTERM`). + ## [0.23.1] - 2023-10-02 ### Added diff --git a/Cargo.lock b/Cargo.lock index 2072de1..f2bb73e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -134,7 +134,7 @@ checksum = "2674ec482fbc38012cf31e6c42ba0177b431a0cb6f15fe40efa5aab1bda516f6" dependencies = [ "is-terminal", "lazy_static", - "windows-sys", + "windows-sys 0.48.0", ] [[package]] @@ -186,6 +186,16 @@ dependencies = [ "cfg-if", ] +[[package]] +name = "ctrlc" +version = "3.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b467862cc8610ca6fc9a1532d7777cee0804e678ab45410897b9396495994a0b" +dependencies = [ + "nix", + "windows-sys 0.52.0", +] + [[package]] name = "dirs" version = "3.0.2" @@ -208,18 +218,18 @@ dependencies = [ [[package]] name = "docuum" -version = "0.23.1" +version = "0.24.0" dependencies = [ "atty", "byte-unit", "chrono", "clap", "colored", + "ctrlc", "dirs", "env_logger", "log", "regex", - "scopeguard", "serde", "serde_json", "serde_yaml", @@ -252,7 +262,7 @@ checksum = "6b30f669a7961ef1631673d2766cc92f52d64f7ef354d4fe0ddfd30ed52f0f4f" dependencies = [ "errno-dragonfly", "libc", - "windows-sys", + "windows-sys 0.48.0", ] [[package]] @@ -344,7 +354,7 @@ checksum = "cb0889898416213fab133e1d33a0e5858a48177452750691bde3666d0fdbaf8b" dependencies = [ "hermit-abi 0.3.2", "rustix", - "windows-sys", + "windows-sys 0.48.0", ] [[package]] @@ -407,6 +417,17 @@ dependencies = [ "autocfg", ] +[[package]] +name = "nix" +version = "0.27.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2eb04e9c688eff1c89d72b407f168cf79bb9e867a9d3323ed6c01519eb9cc053" +dependencies = [ + "bitflags 2.4.0", + "cfg-if", + "libc", +] + [[package]] name = "ntapi" version = "0.3.7" @@ -549,7 +570,7 @@ dependencies = [ "errno", "libc", "linux-raw-sys", - "windows-sys", + "windows-sys 0.48.0", ] [[package]] @@ -649,7 +670,7 @@ dependencies = [ "fastrand", "redox_syscall 0.3.5", "rustix", - "windows-sys", + "windows-sys 0.48.0", ] [[package]] @@ -839,7 +860,7 @@ version = "0.48.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e686886bc078bc1b0b600cac0147aadb815089b6e4da64016cbd754b6342700f" dependencies = [ - "windows-targets", + "windows-targets 0.48.5", ] [[package]] @@ -848,7 +869,16 @@ version = "0.48.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" dependencies = [ - "windows-targets", + "windows-targets 0.48.5", +] + +[[package]] +name = "windows-sys" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" +dependencies = [ + "windows-targets 0.52.4", ] [[package]] @@ -857,13 +887,28 @@ version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c" dependencies = [ - "windows_aarch64_gnullvm", - "windows_aarch64_msvc", - "windows_i686_gnu", - "windows_i686_msvc", - "windows_x86_64_gnu", - "windows_x86_64_gnullvm", - "windows_x86_64_msvc", + "windows_aarch64_gnullvm 0.48.5", + "windows_aarch64_msvc 0.48.5", + "windows_i686_gnu 0.48.5", + "windows_i686_msvc 0.48.5", + "windows_x86_64_gnu 0.48.5", + "windows_x86_64_gnullvm 0.48.5", + "windows_x86_64_msvc 0.48.5", +] + +[[package]] +name = "windows-targets" +version = "0.52.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7dd37b7e5ab9018759f893a1952c9420d060016fc19a472b4bb20d1bdd694d1b" +dependencies = [ + "windows_aarch64_gnullvm 0.52.4", + "windows_aarch64_msvc 0.52.4", + "windows_i686_gnu 0.52.4", + "windows_i686_msvc 0.52.4", + "windows_x86_64_gnu 0.52.4", + "windows_x86_64_gnullvm 0.52.4", + "windows_x86_64_msvc 0.52.4", ] [[package]] @@ -872,42 +917,84 @@ version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8" +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.52.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bcf46cf4c365c6f2d1cc93ce535f2c8b244591df96ceee75d8e83deb70a9cac9" + [[package]] name = "windows_aarch64_msvc" version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc" +[[package]] +name = "windows_aarch64_msvc" +version = "0.52.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da9f259dd3bcf6990b55bffd094c4f7235817ba4ceebde8e6d11cd0c5633b675" + [[package]] name = "windows_i686_gnu" version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e" +[[package]] +name = "windows_i686_gnu" +version = "0.52.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b474d8268f99e0995f25b9f095bc7434632601028cf86590aea5c8a5cb7801d3" + [[package]] name = "windows_i686_msvc" version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406" +[[package]] +name = "windows_i686_msvc" +version = "0.52.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1515e9a29e5bed743cb4415a9ecf5dfca648ce85ee42e15873c3cd8610ff8e02" + [[package]] name = "windows_x86_64_gnu" version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e" +[[package]] +name = "windows_x86_64_gnu" +version = "0.52.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5eee091590e89cc02ad514ffe3ead9eb6b660aedca2183455434b93546371a03" + [[package]] name = "windows_x86_64_gnullvm" version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc" +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.52.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77ca79f2451b49fa9e2af39f0747fe999fcda4f5e241b2898624dca97a1f2177" + [[package]] name = "windows_x86_64_msvc" version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" +[[package]] +name = "windows_x86_64_msvc" +version = "0.52.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32b752e52a2da0ddfbdbcc6fceadfeede4c939ed16d13e648833a61dfb611ed8" + [[package]] name = "yaml-rust" version = "0.4.5" diff --git a/Cargo.toml b/Cargo.toml index 906a47e..ab75564 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "docuum" -version = "0.23.1" +version = "0.24.0" authors = ["Stephan Boyer "] edition = "2021" description = "LRU eviction of Docker images." @@ -24,11 +24,10 @@ colored = "2" dirs = "3" env_logger = { version = "0.8", default-features = false, features = ["termcolor", "atty"] } log = "0.4" -scopeguard = "1" +regex = { version = "1.5.5", default-features = false, features = ["std", "unicode-perl"] } serde_json = "1.0" serde_yaml = "0.8" tempfile = "3" -regex = { version = "1.5.5", default-features = false, features = ["std", "unicode-perl"] } [target.'cfg(target_os = "linux")'.dependencies] sysinfo = "0.23.5" @@ -37,6 +36,10 @@ sysinfo = "0.23.5" version = "2" features = ["wrap_help"] +[dependencies.ctrlc] +version = "3" +features = ["termination"] # [tag:ctrlc_term] + [dependencies.serde] version = "1" features = ["derive"] diff --git a/src/main.rs b/src/main.rs index 195c6fe..255cc82 100644 --- a/src/main.rs +++ b/src/main.rs @@ -16,6 +16,7 @@ use { io::{self, Write}, process::exit, str::FromStr, + sync::{Arc, Mutex}, thread::sleep, time::Duration, }, @@ -231,8 +232,34 @@ fn settings() -> io::Result { }) } +// This function consumes and runs all the registered destructors. We use this mechanism instead of +// RAII for things that need to be cleaned up even when the process is killed due to a signal. +#[allow(clippy::type_complexity)] +fn run_destructors(destructors: &Arc>>>) { + let mut mutex_guard = destructors.lock().unwrap(); + let destructor_fns = std::mem::take(&mut *mutex_guard); + for destructor in destructor_fns { + destructor(); + } +} + // Let the fun begin! fn main() { + // If Docuum is in the foreground process group for some TTY, the process will receive a SIGINT + // when the user types CTRL+C at the terminal. The default behavior is to crash when this signal + // is received. However, we would rather clean up resources before terminating, so we trap the + // signal here. This code also traps SIGHUP and SIGTERM, since we compile the `ctrlc` crate with + // the `termination` feature [ref:ctrlc_term]. + let destructors = Arc::new(Mutex::new(Vec::>::new())); + let destructors_clone = destructors.clone(); + if let Err(error) = ctrlc::set_handler(move || { + run_destructors(&destructors_clone); + exit(1); + }) { + // Log the error and proceed anyway. + error!("{}", error); + } + // Determine whether to print colored output. colored::control::set_override(atty::is(Stream::Stderr)); @@ -265,10 +292,16 @@ fn main() { // Stream Docker events and vacuum when necessary. Restart if an error occurs. loop { - if let Err(e) = run(&settings, &mut state, &mut first_run) { - error!("{}", e); - info!("Retrying in 5 seconds\u{2026}"); - sleep(Duration::from_secs(5)); + // This will run until an error occurs (it never returns `Ok`). + if let Err(error) = run(&settings, &mut state, &mut first_run, &destructors) { + error!("{}", error); } + + // Clean up any resources left over from that run. + run_destructors(&destructors); + + // Wait a moment and then retry. + info!("Retrying in 5 seconds\u{2026}"); + sleep(Duration::from_secs(5)); } } diff --git a/src/run.rs b/src/run.rs index badbd87..cb3967c 100644 --- a/src/run.rs +++ b/src/run.rs @@ -7,15 +7,14 @@ use { byte_unit::Byte, chrono::DateTime, regex::RegexSet, - scopeguard::guard, serde::{Deserialize, Serialize}, std::{ cmp::max, collections::{hash_map::Entry, HashMap, HashSet}, io::{self, BufRead, BufReader}, - mem::drop, ops::Deref, process::{Command, Stdio}, + sync::{Arc, Mutex}, time::{Duration, SystemTime, UNIX_EPOCH}, }, }; @@ -731,7 +730,13 @@ fn vacuum( } // Stream Docker events and vacuum when necessary. -pub fn run(settings: &Settings, state: &mut State, first_run: &mut bool) -> io::Result<()> { +#[allow(clippy::type_complexity)] +pub fn run( + settings: &Settings, + state: &mut State, + first_run: &mut bool, + destructors: &Arc>>>, +) -> io::Result<()> { // Determine the threshold in bytes. let threshold = match settings.threshold { Threshold::Absolute(b) => b, @@ -764,27 +769,23 @@ pub fn run(settings: &Settings, state: &mut State, first_run: &mut bool) -> io:: *first_run = false; // Spawn `docker events --format '{{json .}}'`. - let mut child = guard( - Command::new("docker") - .args(["events", "--format", "{{json .}}"]) - .stdout(Stdio::piped()) - .spawn()?, - |mut child| { - drop(child.kill()); - drop(child.wait()); - }, - ); + let mut child = Command::new("docker") + .args(["events", "--format", "{{json .}}"]) + .stdout(Stdio::piped()) // [tag:stdout] + .spawn()?; - // Buffer the data as we read it line-by-line. - let reader = BufReader::new(child.stdout.as_mut().map_or_else( - || { - Err(io::Error::new( - io::ErrorKind::Other, - format!("Unable to read output from {}.", "docker events".code_str()), - )) - }, - Ok, - )?); + // Buffer the data as we read it line-by-line. The `unwrap` is safe due to [ref:stdout]. + let reader = BufReader::new(child.stdout.take().unwrap()); + + // When this run is done (e.g., due to an error) or when a termination signal is received, kill + // the child process. + destructors.lock().unwrap().push(Box::new(move || { + if let Err(error) = child.kill() { + error!("{}", error); + } else if let Err(error) = child.wait() { + error!("{}", error); + } + })); // Handle each incoming event. info!("Listening for Docker events\u{2026}"); @@ -854,7 +855,7 @@ pub fn run(settings: &Settings, state: &mut State, first_run: &mut bool) -> io:: // The `for` loop above will only terminate if something happened to `docker events`. Err(io::Error::new( io::ErrorKind::Other, - format!("{} unexpectedly terminated.", "docker events".code_str()), + format!("{} terminated.", "docker events".code_str()), )) }