Skip to main content

sioul/spam/
by_itself.rs

1// SPDX-License-Identifier: GPL-3.0-or-later
2// Copyright © 2026 Aurélien Pierre
3
4//! The spam filter's training by itself, from the command line's side
5//! (`sioul_learn::auto` decides; docs/spam-filter.md, "Training by itself"):
6//! its job started apart, at once (`start`: for the window's minute, through
7//! `sioul spam train --by-itself`, and for `sioul watch`'s own minute,
8//! `every_minute`), then run (`run`: the lowest priority, its power watched,
9//! its end kept).
10
11use super::{jobs, long, report};
12use crate::Session;
13use sioul_learn::Dirs;
14use sioul_learn::auto::{self, Decision, Outcome, Run};
15use std::sync::Arc;
16use std::sync::atomic::{AtomicBool, Ordering};
17use std::time::Duration;
18
19/// A run by itself, started apart (`jobs`): downloading first or not, on
20/// `threads` (half the cores unless said), its start kept (`auto::Run`)
21/// before its process starts.
22pub(crate) fn start(s: &Session, fetch: bool, threads: Option<u32>) -> Result<jobs::Job, String> {
23    let threads = threads.unwrap_or_else(auto::threads);
24    let mut args = vec!["--by-itself".to_string(), "--threads".to_string(), threads.to_string()];
25    if !fetch {
26        args.push("--no-fetch".to_string());
27    }
28    let job = jobs::create(s, "train", args)?;
29    Run { started: job.created, job: job.id.clone(), fetched: fetch, ended: None, outcome: None }.save(&Dirs::standard())?;
30    jobs::spawn(s, job)
31}
32
33/// The run itself, as its job: the lowest priority for every thread it
34/// starts, its power asked every half minute (unplugged or saving power: its
35/// own stop file, and it stops at its next step), its end kept for the
36/// settings and the next decision.
37pub(crate) fn run(s: &Session, dirs: &Dirs, id: &str, ask: &report::TrainAsk, json: bool) -> Result<(), String> {
38    lowest_priority();
39    let done = Arc::new(AtomicBool::new(false));
40    let watcher = {
41        let (done, id) = (Arc::clone(&done), id.to_string());
42        std::thread::Builder::new().name("spam-supply".into()).spawn(move || {
43            auto::watch_supply(
44                sioul_sync::power::supply,
45                || {
46                    let _ = jobs::stop(&id);
47                },
48                || done.load(Ordering::Relaxed),
49                auto::SUPPLY_EVERY,
50            );
51        })
52    };
53    let mut replaced = None;
54    let result = long(s, json, Some(id), |progress, cancel| {
55        let report = report::train(s, dirs, ask, progress, cancel)?;
56        replaced = report.data["summary"]["replaced"].as_bool();
57        Ok(report)
58    });
59    done.store(true, Ordering::Relaxed);
60    if let Ok(watcher) = watcher {
61        let _ = watcher.join();
62    }
63    let stopped = jobs::look(id).is_ok_and(|job| job.state == jobs::State::Stopped);
64    let _ = Run::finish(dirs, id, outcome(&result, replaced, stopped), jiff::Timestamp::now().as_second());
65    result
66}
67
68/// How a run by itself ended, from its result, whether its table replaced
69/// the one in use, and whether its job says it stopped.
70pub(crate) fn outcome(result: &Result<(), String>, replaced: Option<bool>, stopped: bool) -> Outcome {
71    match (result, replaced) {
72        (Ok(()), Some(true)) => Outcome::Replaced,
73        (Ok(()), _) => Outcome::Kept,
74        (Err(_), _) if stopped => Outcome::Stopped,
75        (Err(_), _) => Outcome::Failed,
76    }
77}
78
79/// `sioul watch`'s own minute, as the window's: a run by itself started
80/// when everything allows it (`auto::decide`), the system asked only when
81/// nothing else holds it back. Its settings read again each minute; silent.
82pub(crate) fn every_minute(s: &Session) {
83    let (path, language) = (s.config_path.clone(), s.tr.language().to_string());
84    let _ = std::thread::Builder::new().name("spam-by-itself".into()).spawn(move || {
85        loop {
86            std::thread::sleep(Duration::from_secs(60));
87            let config = sioul_core::config::Config::load(&path).unwrap_or_default();
88            sioul_core::words::set_current(sioul_core::words::Words::of(&config));
89            let session = Session { config, config_path: path.clone(), tr: sioul_core::i18n::Translator::new(&language) };
90            let (dirs, now) = (Dirs::standard(), jiff::Timestamp::now().as_second());
91            let focus = sioul_core::timelog::running().is_some();
92            let facts = auto::facts(&dirs, session.config.spam.trains_by_itself(), &sioul_sync::lease::host_name(), false, focus, now, sioul_sync::power::read);
93            if let Decision::Train { fetch } = auto::decide(&facts, now) {
94                let _ = start(&session, fetch, None);
95            }
96        }
97    });
98}
99
100/// The lowest priority this system has, for this process and every thread
101/// it starts from now (fastText's): nice 19, and the idle I/O class on
102/// Linux; background mode on Windows (its processor, its disk, its memory);
103/// a throttled disk on a Mac.
104fn lowest_priority() {
105    #[cfg(unix)]
106    // SAFETY: setpriority only reads its arguments; 0 is this process (on
107    // Linux this thread, whose new threads take it).
108    unsafe {
109        libc::setpriority(libc::PRIO_PROCESS, 0, 19);
110    }
111    #[cfg(target_os = "linux")]
112    // SAFETY: ioprio_set only reads its arguments: this thread
113    // (IOPRIO_WHO_PROCESS, 0), the idle class (3 << 13), which its new threads take.
114    unsafe {
115        libc::syscall(libc::SYS_ioprio_set, 1, 0, 3 << 13);
116    }
117    #[cfg(windows)]
118    {
119        #[link(name = "kernel32")]
120        unsafe extern "system" {
121            fn GetCurrentProcess() -> isize;
122            fn SetPriorityClass(process: isize, class: u32) -> i32;
123        }
124        // SAFETY: this process's pseudo-handle, and PROCESS_MODE_BACKGROUND_BEGIN.
125        unsafe {
126            SetPriorityClass(GetCurrentProcess(), 0x0010_0000);
127        }
128    }
129    #[cfg(target_os = "macos")]
130    {
131        unsafe extern "C" {
132            fn setiopolicy_np(iotype: i32, scope: i32, policy: i32) -> i32;
133        }
134        // SAFETY: it only reads its arguments: the disk (IOPOL_TYPE_DISK), this
135        // process (IOPOL_SCOPE_PROCESS), throttled (IOPOL_THROTTLE).
136        unsafe {
137            setiopolicy_np(0, 0, 3);
138        }
139    }
140}
141
142#[cfg(test)]
143mod tests {
144    use super::*;
145
146    /// Trained (its table in use, or kept), stopped, or failed.
147    #[test]
148    fn how_a_run_by_itself_ended() {
149        let (ok, err) = (Ok(()), Err("x".to_string()));
150        assert_eq!(outcome(&ok, Some(true), false), Outcome::Replaced);
151        assert_eq!(outcome(&ok, Some(false), false), Outcome::Kept);
152        assert_eq!(outcome(&ok, None, false), Outcome::Kept);
153        assert_eq!(outcome(&err, None, true), Outcome::Stopped);
154        assert_eq!(outcome(&err, None, false), Outcome::Failed);
155    }
156}