xidlehook_core/
lib.rs

1#![warn(
2    // Harden built-in lints
3    missing_copy_implementations,
4    missing_debug_implementations,
5    missing_docs,
6    unreachable_pub,
7
8    // Harden clippy lints
9    clippy::cargo_common_metadata,
10    clippy::clone_on_ref_ptr,
11    clippy::dbg_macro,
12    clippy::decimal_literal_representation,
13    clippy::float_cmp_const,
14    clippy::get_unwrap,
15    clippy::integer_arithmetic,
16    clippy::integer_division,
17    clippy::print_stdout,
18)]
19#![allow(
20    // I don't agree with this lint
21    clippy::must_use_candidate,
22
23    // The integer arithmetic here is mostly regarding indexes into Vecs, indexes where memory
24    // allocation will fail far, far earlier than the arithmetic will fail.
25    clippy::integer_arithmetic,
26)]
27
28//! Instead of implementing your extension as something that
29//! communicates with xidlehook, what about implementing your
30//! extension as something that *is* xidlehook?
31//!
32//! This library lets you create your own xidlehook front-end using a
33//! powerful timer and module system.
34
35use std::{
36    cmp,
37    convert::TryInto,
38    fmt, ptr,
39    time::{Duration, Instant},
40};
41
42use log::{info, trace, warn};
43use nix::libc;
44
45/// The default error type for xidlehook. Unfortunately, it's a
46/// dynamic type for now.
47pub type Error = Box<dyn std::error::Error>;
48/// An alias to Result which overrides the default Error type.
49pub type Result<T, E = Error> = std::result::Result<T, E>;
50
51pub mod modules;
52pub mod timers;
53
54pub use self::{
55    modules::{Module, Progress},
56    timers::Timer,
57};
58
59/// An identifier for a timer, based on the index in the timer list
60/// and its length.
61#[derive(Clone, Copy, Debug)]
62pub struct TimerInfo {
63    /// The index of this timer in the timer list
64    pub index: usize,
65    /// The length of the timer list
66    pub length: usize,
67}
68
69/// Return value of `poll`, which specifies what one should do next: sleep,
70/// wait forever (until client modifies the xidlehook instance),
71#[derive(Clone, Copy, Debug, PartialEq, Eq)]
72pub enum Action {
73    /// Sleep for (at most) a specified duration
74    Sleep(Duration),
75    /// Xidlehook has nothing to do, so you should effectively wait forever until the client modifies the xidlehook instance
76    Forever,
77    /// A module wants xidlehook to quit
78    Quit,
79}
80
81/// The main xidlehook instance that allows you to schedule things
82pub struct Xidlehook<T: Timer, M: Module>
83where
84    T: Timer,
85    M: Module,
86{
87    module: M,
88
89    /// Whether to reset on sleep
90    detect_sleep: bool,
91
92    timers: Vec<T>,
93    next_index: usize,
94    /// The base idle time: the absolute idle time when the last timer
95    /// was called, used to retrieve the relative idle time since it.
96    base_idle_time: Duration,
97    /// The previous idle time, used for comparing whether or not the
98    /// user has moved.
99    previous_idle_time: Duration,
100    /// If a chain is aborted during the process, store this here as
101    /// to not make any more attempts to continue it.
102    aborted: bool,
103}
104impl<T: Timer> Xidlehook<T, ()> {
105    /// An empty instance without any modules
106    pub fn new(timers: Vec<T>) -> Self {
107        Self {
108            module: (),
109
110            detect_sleep: false,
111
112            timers,
113            next_index: 0,
114            base_idle_time: Duration::default(),
115            previous_idle_time: Duration::default(),
116            aborted: false,
117        }
118    }
119}
120
121macro_rules! with_module {
122    ($self:expr, $module:expr) => {
123        Xidlehook {
124            module: $module,
125
126            detect_sleep: $self.detect_sleep,
127
128            timers: $self.timers,
129            next_index: $self.next_index,
130            base_idle_time: $self.base_idle_time,
131            previous_idle_time: $self.previous_idle_time,
132            aborted: $self.aborted,
133        }
134    };
135}
136
137// There are some false positive with Self and generics.
138#[allow(clippy::use_self)]
139impl<T, M> Xidlehook<T, M>
140where
141    T: Timer,
142    M: Module,
143{
144    /// Return this xidlehook instance but with this module replaced.
145    pub fn with_module<N: Module>(self, other: N) -> Xidlehook<T, N> {
146        with_module!(self, other)
147    }
148
149    /// Return this xidlehook instance but with an additional module activated. This works using the
150    /// timer impl for `(A, B)` to get a fixed-size list of modules at compile time.
151    pub fn register<N: Module>(self, other: N) -> Xidlehook<T, (M, N)> {
152        // Sadly cannot use `self.with_module` safely due to use of `self.module` - Rust isn't
153        // intelligent enough to realize the function isn't using that field. This is one of the few
154        // shortcomings of Rust IMO.
155        with_module!(self, (self.module, other))
156    }
157
158    /// Set whether or not we reset the idle timer once a suspend was detected. This only affects
159    /// main/main_async.
160    pub fn set_detect_sleep(&mut self, value: bool) {
161        self.detect_sleep = value;
162    }
163    /// Get whether or not we reset the idle timer once a suspend was detected
164    pub fn detect_sleep(&self) -> bool {
165        self.detect_sleep
166    }
167    /// Set whether or not we reset the idle timer once a suspend was detected. This only affects
168    /// main/main_async. This is the chainable version of `set_detect_sleep`.
169    pub fn with_detect_sleep(mut self, value: bool) -> Self {
170        self.detect_sleep = value;
171        self
172    }
173
174    /// Returns an immutable list of all timers
175    pub fn timers(&self) -> &Vec<T> {
176        &self.timers
177    }
178
179    /// Returns a mutable list of all timers. Use this to add or remove timers as you wish. This
180    /// will abort the idle chain as that may otherwise panic.
181    pub fn timers_mut(&mut self) -> Result<&mut Vec<T>> {
182        self.abort()?;
183        Ok(&mut self.timers)
184    }
185
186    /// Returns the previous timer that was activated (but not deactivated)
187    fn previous(&mut self) -> Option<&mut T> {
188        self.next_index
189            .checked_sub(1)
190            .map(move |i| &mut self.timers[i])
191    }
192
193    /// Calls the abortion function on the current timer and stops pursuing the chain
194    pub fn abort(&mut self) -> Result<()> {
195        if self.aborted {
196            return Ok(());
197        }
198
199        self.aborted = true;
200        if let Some(prev) = self.previous() {
201            prev.abort()?;
202        }
203        Ok(())
204    }
205
206    /// Calls the abortion functions on the current timer and restarts from index zero. Just like
207    /// with the `poll` function, continued usage after an error discouraged.
208    pub fn reset(&mut self, absolute_time: Duration) -> Result<()> {
209        self.abort()?;
210
211        trace!("Resetting");
212
213        if self.next_index > 0 {
214            if let Err(err) = self.module.reset() {
215                self.module.warning(&err)?;
216            }
217            self.next_index = 0;
218        }
219
220        self.base_idle_time = absolute_time;
221        self.previous_idle_time = absolute_time;
222        self.aborted = false;
223
224        Ok(())
225    }
226
227    /// Skip ahead to the selected timer. Timers leading up to this point will not be ran. If you
228    /// pass `force`, modules will not even be able to prevent this from happening (all requests
229    /// pre-timer would be ignored). Post-timer requests are fully complied with.
230    ///
231    /// Whatever the return value is, it's already been handled. If the return value is `Err(...)`,
232    /// that means this function invoked the module's `warning` function and that still wanted to
233    /// propagate the error. If the return value is `Ok(Progress::Abort)`, never mind it. The
234    /// `self.abort()` function has already been invoked - it's all cool.
235    ///
236    /// # Panics
237    ///
238    /// - If the index is out of bounds
239    pub fn trigger(
240        &mut self,
241        index: usize,
242        absolute_time: Duration,
243        force: bool,
244    ) -> Result<Progress> {
245        macro_rules! handle {
246            ($progress:expr) => {
247                match $progress {
248                    Progress::Continue => (),
249                    Progress::Abort => {
250                        trace!("Module requested abort of chain.");
251                        self.abort()?;
252                        return Ok(Progress::Abort);
253                    },
254                    Progress::Reset => {
255                        trace!("Module requested reset of chain.");
256                        self.reset(absolute_time)?;
257                        return Ok(Progress::Reset);
258                    },
259                    Progress::Stop => return Ok(Progress::Stop),
260                }
261            };
262        }
263        trace!("Activating timer {}", index);
264
265        let timer_info = TimerInfo {
266            index,
267            length: self.timers.len(),
268        };
269
270        let next = &mut self.timers[index];
271
272        // Trigger module pre-timer
273        match self.module.pre_timer(timer_info) {
274            Ok(_) if force => (),
275            Ok(progress) => handle!(progress),
276            Err(err) => {
277                self.module.warning(&err)?;
278            },
279        }
280
281        // Send activation signal to current timer
282        next.activate()?;
283
284        // Send deactivation signal to previous timer
285        if let Some(previous) = self.previous() {
286            previous.deactivate()?;
287        }
288
289        // Reset the idle time to zero
290        self.base_idle_time = absolute_time;
291
292        // Send module post-timer
293        match self.module.post_timer(timer_info) {
294            Ok(progress) => handle!(progress),
295            Err(err) => {
296                self.module.warning(&err)?;
297            },
298        }
299
300        // Next time, continue from next index
301        self.next_index = index + 1;
302
303        Ok(Progress::Continue)
304    }
305
306    /// Polls the scheduler for any activated timers. On success, returns the max amount of time a
307    /// program can sleep for. Only fatal errors cause this function to return, and at that point,
308    /// the state of xidlehook is undefined so it should not be used.
309    pub fn poll(&mut self, absolute_time: Duration) -> Result<Action> {
310        if absolute_time < self.previous_idle_time {
311            // If the idle time has decreased, the only reasonable explanation is that the user
312            // briefly wasn't idle. We reset the base idle time to zero so the entire idle duration
313            // is counted.
314            self.reset(Duration::from_millis(0))?;
315        }
316
317        self.previous_idle_time = absolute_time;
318
319        // We can only ever sleep as long as it takes for the first timer to activate, since the
320        // user may become active (and then idle again) at any point.
321        let mut max_sleep = Duration::from_nanos(u64::MAX);
322
323        let mut first_timer = 0;
324
325        while let Some(timer) = self.timers.get_mut(first_timer) {
326            if !timer.disabled() {
327                break;
328            }
329
330            // This timer may re-activate in the future and take presedence over the timer we
331            // thought was the next enabled timer.
332            if let Some(remaining) = timer.time_left(Duration::from_nanos(0))? {
333                trace!(
334                    "Taking disabled first timer into account. Remaining: {:?}",
335                    remaining
336                );
337                max_sleep = cmp::min(max_sleep, remaining);
338            }
339
340            first_timer += 1;
341        }
342
343        if let Some(timer) = self.timers.get_mut(first_timer) {
344            if let Some(remaining) = timer.time_left(Duration::from_nanos(0))? {
345                trace!(
346                    "Taking first timer into account. Remaining: {:?}",
347                    remaining
348                );
349                max_sleep = cmp::min(max_sleep, remaining)
350            }
351        } else {
352            // No timer was enabled!
353            return Ok(Action::Forever);
354        }
355
356        if self.aborted {
357            trace!("This chain was aborted, I won't pursue it");
358            return Ok(Action::Sleep(max_sleep));
359        }
360
361        let relative_time = absolute_time - self.base_idle_time;
362        trace!("Relative time: {:?}", relative_time);
363
364        let mut next_index = self.next_index;
365
366        while let Some(timer) = self.timers.get_mut(next_index) {
367            if !timer.disabled() {
368                break;
369            }
370
371            // This timer may re-activate in the future and take presedence over the timer we
372            // thought was the next enabled timer.
373            if let Some(remaining) = timer.time_left(relative_time)? {
374                trace!(
375                    "Taking disabled timer into account. Remaining: {:?}",
376                    remaining
377                );
378                max_sleep = cmp::min(max_sleep, remaining);
379            }
380
381            next_index += 1;
382        }
383
384        // When there's a next timer available, get the time until that activates
385        if let Some(next) = self.timers.get_mut(next_index) {
386            if let Some(remaining) = next.time_left(relative_time)? {
387                trace!(
388                    "Taking next enabled timer into account. Remaining: {:?}",
389                    remaining
390                );
391                max_sleep = cmp::min(max_sleep, remaining);
392            } else {
393                trace!("Triggering timer #{}", next_index);
394                // Oh! It has already been passed - let's trigger it.
395                match self.trigger(next_index, absolute_time, false)? {
396                    Progress::Stop => return Ok(Action::Quit),
397                    _ => (),
398                }
399
400                // Recurse to find return value
401                return self.poll(absolute_time);
402            }
403        }
404
405        // When there's a previous timer, respect that timer's abort urgency (see
406        // `Timer::abort_urgency()`)
407        if let Some(abort) = self.previous() {
408            if let Some(urgency) = abort.abort_urgency() {
409                trace!(
410                    "Taking abort urgency into account. Remaining: {:?}",
411                    urgency
412                );
413                max_sleep = cmp::min(max_sleep, urgency);
414            }
415        }
416
417        Ok(Action::Sleep(max_sleep))
418    }
419
420    /// Runs a standard poll-sleep-repeat loop.
421    /// ```rust
422    /// # if std::env::var("DISPLAY").is_err() {
423    /// #     // Don't fail on CI.
424    /// #     return Ok::<(), xidlehook_core::Error>(());
425    /// # }
426    /// # use std::{
427    /// #     sync::atomic::{AtomicBool, Ordering},
428    /// #     time::Duration,
429    /// # };
430    /// #
431    /// # use nix::{
432    /// #     libc,
433    /// #     sys::{signal, wait},
434    /// # };
435    /// # use xidlehook_core::{
436    /// #     modules::{StopAt, Xcb},
437    /// #     timers::CallbackTimer,
438    /// #     Xidlehook,
439    /// # };
440    /// #
441    /// # let timers = vec![
442    /// #     CallbackTimer::new(Duration::from_millis(50), || println!("50ms passed!")),
443    /// # ];
444    /// # let mut xidlehook = Xidlehook::new(timers)
445    /// #     .register(StopAt::completion());
446    /// # let xcb = Xcb::new()?;
447    /// static EXITED: AtomicBool = AtomicBool::new(false);
448    ///
449    /// extern "C" fn exit_handler(_signo: libc::c_int) {
450    ///     EXITED.store(true, Ordering::SeqCst);
451    /// }
452    ///
453    /// unsafe {
454    ///     signal::sigaction(
455    ///         signal::Signal::SIGINT,
456    ///         &signal::SigAction::new(
457    ///             signal::SigHandler::Handler(exit_handler),
458    ///             signal::SaFlags::empty(),
459    ///             signal::SigSet::empty(),
460    ///         ),
461    ///     )?;
462    /// }
463    /// xidlehook.main_sync(&xcb, || EXITED.load(Ordering::SeqCst));
464    /// # Ok::<(), xidlehook_core::Error>(())
465    /// ```
466    pub fn main_sync<F>(mut self, xcb: &self::modules::Xcb, mut callback: F) -> Result<()>
467    where
468        F: FnMut() -> bool,
469    {
470        loop {
471            let idle = xcb.get_idle()?;
472            match self.poll(idle)? {
473                Action::Sleep(delay) => {
474                    trace!("Sleeping for {:?}", delay);
475
476                    let sleep_start = Instant::now();
477
478                    // This sleep, unlike `thread::sleep`, will stop for signals.
479                    unsafe {
480                        libc::nanosleep(
481                            &libc::timespec {
482                                tv_sec: delay
483                                    .as_secs()
484                                    .try_into()
485                                    .expect("woah that's one large number"),
486                                tv_nsec: delay
487                                    .subsec_nanos()
488                                    .try_into()
489                                    .expect("woah that's one large number"),
490                            },
491                            ptr::null_mut(),
492                        );
493                    }
494
495                    if let Some(time_difference) = sleep_start.elapsed().checked_sub(delay) {
496                        if time_difference >= Duration::from_secs(3) && self.detect_sleep {
497                            info!(
498                                "We slept {:?} longer than expected - has the computer been suspended?",
499                                time_difference,
500                            );
501                            self.reset(xcb.get_idle()?)?;
502                        }
503                    }
504                },
505                Action::Forever => {
506                    warn!("xidlehook has not, and will never get, anything to do");
507                    break;
508                },
509                Action::Quit => break,
510            }
511
512            if callback() {
513                // Oh look, the callback wants us to exit
514                break;
515            }
516        }
517        Ok(())
518    }
519
520    /// Runs a standard poll-sleep-repeat loop... asynchronously.
521    #[cfg(any(feature = "async-std", feature = "tokio"))]
522    pub async fn main_async(&mut self, xcb: &self::modules::Xcb) -> Result<()> {
523        loop {
524            let idle = xcb.get_idle()?;
525            match self.poll(idle)? {
526                Action::Sleep(delay) => {
527                    trace!("Sleeping for {:?}", delay);
528
529                    let sleep_start = Instant::now();
530
531                    #[cfg(feature = "async-std")]
532                    async_std::task::sleep(delay).await;
533                    #[cfg(feature = "tokio")]
534                    if cfg!(not(feature = "async-std")) {
535                        tokio::time::delay_for(delay).await;
536                    }
537
538                    if let Some(time_difference) = sleep_start.elapsed().checked_sub(delay) {
539                        if time_difference >= Duration::from_secs(3) && self.detect_sleep {
540                            info!(
541                                "We slept {:?} longer than expected - has the computer been suspended?",
542                                time_difference,
543                            );
544                            self.reset(xcb.get_idle()?)?;
545                        }
546                    }
547                },
548                Action::Forever => {
549                    trace!("Nothing to do");
550
551                    #[cfg(feature = "async-std")]
552                    async_std::future::pending::<()>().await;
553                    #[cfg(feature = "tokio")]
554                    if cfg!(not(feature = "async-std")) {
555                        use tokio::stream::StreamExt;
556                        tokio::stream::pending::<()>().next().await;
557                    }
558                },
559                Action::Quit => break,
560            }
561        }
562        Ok(())
563    }
564}
565
566impl<T, M> fmt::Debug for Xidlehook<T, M>
567where
568    T: Timer,
569    M: Module + fmt::Debug,
570{
571    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
572        write!(f, "Modules: {:?}", self.module)
573    }
574}