From e36c2f5bfa49178b0244b5ee46bd264e9afa09d7 Mon Sep 17 00:00:00 2001 From: Jason Ross Date: Mon, 6 Oct 2025 21:17:52 -0500 Subject: [PATCH] Signal handling configured to revert to default color temp on exit or crash --- rewrite/Cargo.toml | 1 + rewrite/src/gamma_guard.rs | 54 ++++++++++++++ rewrite/src/main.rs | 146 ++++++++++++++++++++++++++----------- rewrite/src/signals.rs | 50 +++++++++++++ 4 files changed, 209 insertions(+), 42 deletions(-) create mode 100644 rewrite/src/gamma_guard.rs create mode 100644 rewrite/src/signals.rs diff --git a/rewrite/Cargo.toml b/rewrite/Cargo.toml index 35a86a9..c59a59c 100644 --- a/rewrite/Cargo.toml +++ b/rewrite/Cargo.toml @@ -17,3 +17,4 @@ toml = "0.8" dirs = "5.0" lazy_static = "1.5" dialoguer = "0.11" +signal-hook = "0.3" diff --git a/rewrite/src/gamma_guard.rs b/rewrite/src/gamma_guard.rs new file mode 100644 index 0000000..7510671 --- /dev/null +++ b/rewrite/src/gamma_guard.rs @@ -0,0 +1,54 @@ +/* gamma_guard.rs -- Gamma restoration guard for cleanup + * This module provides a RAII guard that ensures gamma is restored + * even if the program crashes or panics. + */ + +use crate::gamma::GammaMethod; +use crate::types::ColorSetting; + +/* Guard that restores gamma to neutral (6500K) on drop. + * This ensures cleanup happens on normal exit, panic, or signal. */ +pub struct GammaRestoreGuard<'a> { + gamma_method: &'a mut dyn GammaMethod, + restore_on_drop: bool, +} + +impl<'a> GammaRestoreGuard<'a> { + /* Create a new gamma restore guard. + * The gamma will be restored when this guard is dropped. */ + pub fn new(gamma_method: &'a mut dyn GammaMethod) -> Self { + GammaRestoreGuard { + gamma_method, + restore_on_drop: true, + } + } + + /* Disable automatic restoration. + * Call this if you want to keep the current gamma on exit. */ + #[allow(dead_code)] + pub fn disable_restore(&mut self) { + self.restore_on_drop = false; + } + + /* Get mutable reference to the gamma method. + * This allows using the gamma method while the guard is active. */ + pub fn get_mut(&mut self) -> &mut dyn GammaMethod { + self.gamma_method + } +} + +impl<'a> Drop for GammaRestoreGuard<'a> { + fn drop(&mut self) { + if self.restore_on_drop { + /* Restore to neutral temperature (6500K) */ + let neutral = ColorSetting { + temperature: 6500, + brightness: 1.0, + gamma: [1.0, 1.0, 1.0], + }; + + /* Ignore errors during cleanup - we're likely shutting down anyway */ + let _ = self.gamma_method.set_temperature(&neutral, false); + } + } +} diff --git a/rewrite/src/main.rs b/rewrite/src/main.rs index 3ae26a3..4826748 100644 --- a/rewrite/src/main.rs +++ b/rewrite/src/main.rs @@ -2,15 +2,18 @@ mod cities; mod colorramp; mod config; mod gamma; +mod gamma_guard; mod gamma_randr; mod interactive; mod location; +mod signals; mod solar; mod types; use clap::{Parser, ValueEnum}; use config::{Config, LocationSource}; use gamma::{DummyGammaMethod, GammaMethod}; +use gamma_guard::GammaRestoreGuard; use gamma_randr::RandrGammaMethod; use location::{GeoClue2LocationProvider, LocationProvider, ManualLocationProvider}; use std::time::{Duration, SystemTime, UNIX_EPOCH}; @@ -329,6 +332,9 @@ fn try_geoclue2(verbose: bool) -> Result { fn main() -> Result<(), Box> { let args = Args::parse(); + /* Install signal handlers for graceful shutdown and mode toggling */ + signals::install_handlers()?; + /* Validate temperature bounds */ if args.temp_day < MIN_TEMP || args.temp_day > MAX_TEMP { eprintln!( @@ -392,19 +398,24 @@ fn main() -> Result<(), Box> { return Ok(()); } + /* Create gamma restore guard to ensure cleanup on exit or panic */ + let mut gamma_guard = GammaRestoreGuard::new(gamma_method.as_mut()); + /* Apply color temperature */ if args.verbose { println!("Period: {}", period.name()); } - gamma_method.set_temperature(&color_setting, false)?; + gamma_guard.get_mut().set_temperature(&color_setting, false)?; if args.one_shot { + /* For one-shot mode, don't restore gamma on exit */ + gamma_guard.disable_restore(); return Ok(()); } /* Continual mode - continuously adjust color temperature */ - run_continual_mode(&location, &scheme, gamma_method.as_mut(), args.verbose)?; + run_continual_mode(&location, &scheme, &mut gamma_guard, args.verbose)?; Ok(()) } @@ -412,11 +423,11 @@ fn main() -> Result<(), Box> { /* Run continual mode loop. This is the main loop of the continual mode which keeps track of the current time and continuously updates the screen to the appropriate - color temperature. */ + color temperature. Also handles signals for toggling and clean exit. */ fn run_continual_mode( location: &Location, scheme: &TransitionScheme, - gamma_method: &mut dyn GammaMethod, + gamma_guard: &mut GammaRestoreGuard, verbose: bool, ) -> Result<(), Box> { /* Fade parameters */ @@ -430,6 +441,11 @@ fn run_continual_mode( let mut prev_target_interp = ColorSetting::default(); let mut interp = ColorSetting::default(); + /* State for signal handling */ + let mut disabled = false; + let mut prev_disabled = true; /* Start as true to trigger initial status print */ + let mut done = false; /* Set to true when starting shutdown fade */ + if verbose { println!("Color temperature: {}K", interp.temperature); println!("Brightness: {:.2}", interp.brightness); @@ -437,44 +453,84 @@ fn run_continual_mode( /* Continuously adjust color temperature */ loop { - /* Get current time */ - let now = SystemTime::now() - .duration_since(UNIX_EPOCH) - .unwrap() - .as_secs_f64(); - - /* Current angular elevation of the sun */ - let elevation = solar::solar_elevation(now, location.lat as f64, location.lon as f64); - - /* Determine period and transition progress */ - let period = if elevation >= scheme.high { - Period::Daytime - } else if elevation <= scheme.low { - Period::Night - } else { - Period::Transition - }; - - let transition_prog = get_transition_progress_from_elevation(scheme, elevation); - - /* Use transition progress to get target color temperature */ - let mut target_interp = ColorSetting::default(); - interpolate_transition_scheme(scheme, transition_prog, &mut target_interp); - - /* Print period if it changed during this update, - or if we are in the transition period. In transition we - print the progress, so we always print it in that case. */ - if verbose && (period != prev_period || period == Period::Transition) { - match period { - Period::Transition => { - println!("Period: Transition ({:.1}%)", transition_prog * 100.0); - } - _ => { - println!("Period: {}", period.name()); - } + /* Check for toggle signal (SIGUSR1) */ + if signals::check_toggle() && !done { + disabled = !disabled; + if verbose { + println!("Status: {}", if disabled { "Disabled" } else { "Enabled" }); } } + /* Check for exit signal (SIGINT/SIGTERM) */ + if signals::is_exiting() { + if done { + /* Second signal during fade - stop immediately */ + break; + } else { + /* First signal - start shutdown fade */ + done = true; + disabled = true; + signals::clear_exiting(); + } + } + + /* Print status change */ + if verbose && disabled != prev_disabled { + println!("Status: {}", if disabled { "Disabled" } else { "Enabled" }); + } + prev_disabled = disabled; + + /* When disabled, use neutral temperature; otherwise calculate from solar position */ + let mut target_interp = if disabled { + /* Neutral temperature (6500K) when disabled */ + ColorSetting { + temperature: 6500, + brightness: 1.0, + gamma: [1.0, 1.0, 1.0], + } + } else { + /* Get current time */ + let now = SystemTime::now() + .duration_since(UNIX_EPOCH) + .unwrap() + .as_secs_f64(); + + /* Current angular elevation of the sun */ + let elevation = solar::solar_elevation(now, location.lat as f64, location.lon as f64); + + /* Determine period and transition progress */ + let period = if elevation >= scheme.high { + Period::Daytime + } else if elevation <= scheme.low { + Period::Night + } else { + Period::Transition + }; + + let transition_prog = get_transition_progress_from_elevation(scheme, elevation); + + /* Use transition progress to get target color temperature */ + let mut temp_interp = ColorSetting::default(); + interpolate_transition_scheme(scheme, transition_prog, &mut temp_interp); + + /* Print period if it changed during this update, + or if we are in the transition period. In transition we + print the progress, so we always print it in that case. */ + if verbose && (period != prev_period || period == Period::Transition) { + match period { + Period::Transition => { + println!("Period: Transition ({:.1}%)", transition_prog * 100.0); + } + _ => { + println!("Period: {}", period.name()); + } + } + } + prev_period = period; + + temp_interp + }; + /* Start fade if the parameter differences are too big to apply instantly. */ if (fade_length == 0 && color_setting_diff_is_major(&interp, &target_interp)) || (fade_length != 0 && color_setting_diff_is_major(&target_interp, &prev_target_interp)) @@ -510,12 +566,16 @@ fn run_continual_mode( } /* Adjust temperature */ - gamma_method.set_temperature(&interp, false)?; + gamma_guard.get_mut().set_temperature(&interp, false)?; - /* Save period and target color setting as previous */ - prev_period = period; + /* Save target color setting as previous */ prev_target_interp = target_interp; + /* If shutdown was requested and fade is complete, exit */ + if done && fade_length == 0 { + break; + } + /* Sleep length depends on whether a fade is ongoing. */ let delay = if fade_length != 0 { SLEEP_DURATION_SHORT @@ -525,4 +585,6 @@ fn run_continual_mode( std::thread::sleep(Duration::from_millis(delay)); } + + Ok(()) } diff --git a/rewrite/src/signals.rs b/rewrite/src/signals.rs new file mode 100644 index 0000000..768c109 --- /dev/null +++ b/rewrite/src/signals.rs @@ -0,0 +1,50 @@ +/* signals.rs -- Signal handling for redshift + * This file provides signal handlers for graceful shutdown and toggling modes. + * + * Signals handled: + * - SIGUSR1: Toggle between enabled/disabled state (restores gamma when disabled) + * - SIGINT/SIGTERM: Clean shutdown with gamma restoration + */ + +use std::sync::atomic::{AtomicBool, Ordering}; +use std::sync::Arc; + +/* Global atomic flags for signal state. + * These are safe to access from signal handlers and main thread. */ +lazy_static::lazy_static! { + static ref EXITING: Arc = Arc::new(AtomicBool::new(false)); + static ref TOGGLE_REQUESTED: Arc = Arc::new(AtomicBool::new(false)); +} + +/* Install signal handlers. + * Returns an error if signal handler registration fails. */ +pub fn install_handlers() -> Result<(), Box> { + use signal_hook::consts::signal::*; + use signal_hook::flag; + + /* SIGINT and SIGTERM set the exiting flag */ + flag::register(SIGINT, Arc::clone(&EXITING))?; + flag::register(SIGTERM, Arc::clone(&EXITING))?; + + /* SIGUSR1 sets the toggle flag */ + flag::register(SIGUSR1, Arc::clone(&TOGGLE_REQUESTED))?; + + Ok(()) +} + +/* Check if an exit signal (SIGINT or SIGTERM) was received. + * This should be called from the main loop. */ +pub fn is_exiting() -> bool { + EXITING.load(Ordering::Relaxed) +} + +/* Check if a toggle signal (SIGUSR1) was received. + * This returns true only once per signal, then clears the flag. */ +pub fn check_toggle() -> bool { + TOGGLE_REQUESTED.swap(false, Ordering::Relaxed) +} + +/* Clear the exiting flag. Used after starting shutdown fade. */ +pub fn clear_exiting() { + EXITING.store(false, Ordering::Relaxed); +}