Skip to content

Commit b25997b

Browse files
committed
feat: added SIGHUP handling for crond
1 parent d8b211e commit b25997b

File tree

6 files changed

+258
-27
lines changed

6 files changed

+258
-27
lines changed

cron/Cargo.toml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -29,5 +29,5 @@ name = "crond"
2929
path = "./crond.rs"
3030

3131
[lib]
32-
name = "job"
33-
path = "./job.rs"
32+
name = "cron"
33+
path = "./lib.rs"

cron/crond.rs

Lines changed: 102 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -7,43 +7,85 @@
77
// SPDX-License-Identifier: MIT
88
//
99

10-
pub mod job;
11-
12-
use crate::job::Database;
10+
use cron::job::Database;
1311
use chrono::Local;
1412
use gettextrs::{bind_textdomain_codeset, setlocale, textdomain, LocaleCategory};
1513
use std::env;
1614
use std::error::Error;
17-
use std::fs;
1815
use std::str::FromStr;
16+
use std::fs;
17+
use std::time::UNIX_EPOCH;
18+
use std::fmt;
19+
use std::sync::Mutex;
1920

20-
fn parse_cronfile(username: &str) -> Result<Database, Box<dyn Error>> {
21-
#[cfg(target_os = "linux")]
22-
let file = format!("/var/spool/cron/{username}");
23-
#[cfg(target_os = "macos")]
24-
let file = format!("/var/at/tabs/{username}");
25-
let s = fs::read_to_string(&file)?;
26-
Ok(s.lines()
27-
.filter_map(|x| Database::from_str(x).ok())
28-
.fold(Database(vec![]), |acc, next| acc.merge(next)))
21+
static CRONTAB: Mutex<Option<Database>> = Mutex::new(None);
22+
static LAST_MODIFIED: Mutex<Option<u64>> = Mutex::new(None);
23+
24+
#[derive(Debug)]
25+
enum CronError{
26+
Fork,
27+
NoLogname,
28+
NoCrontab
2929
}
3030

31-
fn main() -> Result<(), Box<dyn std::error::Error>> {
32-
setlocale(LocaleCategory::LcAll, "");
33-
textdomain("posixutils-rs")?;
34-
bind_textdomain_codeset("posixutils-rs", "UTF-8")?;
31+
impl Error for CronError {}
32+
33+
impl fmt::Display for CronError {
34+
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
35+
match self{
36+
Self::NoLogname => write!(f, "Could not obtain the user's logname"),
37+
Self::NoCrontab => write!(f, "Could not format database"),
38+
Self::Fork => write!(f, "Could not create child process")
39+
}
40+
}
41+
}
42+
43+
/// Check if logname file is changed
44+
fn is_file_changed(filepath: &str) -> Result<bool, Box<dyn Error>> {
45+
let last_modified = fs::metadata(filepath)?
46+
.modified()?
47+
.duration_since(UNIX_EPOCH)?
48+
.as_secs();
49+
50+
let Some(last_checked) = *LAST_MODIFIED.lock().unwrap() else{
51+
*LAST_MODIFIED.lock().unwrap() = Some(last_modified);
52+
return Ok(true);
53+
};
54+
if last_checked <= last_modified{
55+
*LAST_MODIFIED.lock().unwrap() = Some(last_modified);
56+
Ok(true)
57+
}else{
58+
Ok(false)
59+
}
60+
}
3561

62+
/// Update [`CRONTAB`] if logname file is changed
63+
fn sync_cronfile() -> Result<(), Box<dyn Error>> {
3664
let Ok(logname) = env::var("LOGNAME") else {
37-
panic!("Could not obtain the user's logname.")
65+
return Err(Box::new(CronError::NoLogname));
3866
};
67+
#[cfg(target_os = "linux")]
68+
let file = format!("/var/spool/cron/{logname}");
69+
#[cfg(target_os = "macos")]
70+
let file = format!("/var/at/tabs/{logname}");
71+
if (*CRONTAB.lock().unwrap()).is_none() || is_file_changed(&file)?{
72+
let s = fs::read_to_string(&file)?;
73+
let crontab = s.lines()
74+
.filter_map(|x| Database::from_str(x).ok())
75+
.fold(Database(vec![]), |acc, next| acc.merge(next));
76+
*CRONTAB.lock().unwrap() = Some(crontab);
77+
}
78+
Ok(())
79+
}
3980

40-
// Daemon setup
81+
/// Create new daemon process of crond
82+
fn setup() -> i32{
4183
unsafe {
4284
use libc::*;
4385

4486
let pid = fork();
45-
if pid > 0 {
46-
return Ok(());
87+
if pid != 0 {
88+
return pid;
4789
}
4890

4991
setsid();
@@ -52,12 +94,28 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
5294
close(STDIN_FILENO);
5395
close(STDOUT_FILENO);
5496
close(STDERR_FILENO);
97+
98+
return pid;
5599
}
100+
}
56101

57-
// Daemon code
102+
/// Handles incoming signals
103+
fn handle_signals(signal_code: libc::c_int) {
104+
if signal_code == libc::SIGHUP {
105+
if let Err(err) = sync_cronfile(){
106+
eprintln!("{err}");
107+
std::process::exit(1);
108+
}
109+
}
110+
}
58111

112+
/// Daemon loop
113+
fn daemon_loop() -> Result<(), Box<dyn Error>> {
59114
loop {
60-
let db = parse_cronfile(&logname)?;
115+
sync_cronfile()?;
116+
let Some(db) = CRONTAB.lock().unwrap().clone() else{
117+
return Err(Box::new(CronError::NoCrontab));
118+
};
61119
let Some(x) = db.nearest_job() else {
62120
sleep(60);
63121
continue;
@@ -79,6 +137,26 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
79137
}
80138
}
81139

140+
fn main() -> Result<(), Box<dyn Error>> {
141+
setlocale(LocaleCategory::LcAll, "");
142+
textdomain("posixutils-rs")?;
143+
bind_textdomain_codeset("posixutils-rs", "UTF-8")?;
144+
145+
let pid = setup();
146+
147+
if pid < 0{
148+
return Err(Box::new(CronError::Fork));
149+
}else if pid > 0{
150+
return Ok(());
151+
}
152+
153+
unsafe {
154+
libc::signal(libc::SIGHUP, handle_signals as usize);
155+
}
156+
157+
daemon_loop()
158+
}
159+
82160
fn sleep(target: u32) {
83161
unsafe { libc::sleep(target) };
84-
}
162+
}

cron/job.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -118,6 +118,7 @@ pub struct CronJob {
118118
pub command: String,
119119
}
120120

121+
#[derive(Clone)]
121122
pub struct Database(pub Vec<CronJob>);
122123

123124
impl Database {

cron/lib.rs

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
//
2+
// Copyright (c) 2024 Hemi Labs, Inc.
3+
//
4+
// This file is part of the posixutils-rs project covered under
5+
// the MIT License. For the full license text, please see the LICENSE
6+
// file in the root directory of this project.
7+
// SPDX-License-Identifier: MIT
8+
//
9+
10+
pub mod job;

cron/tests/crond/mod.rs

Lines changed: 44 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,10 +7,13 @@
77
// SPDX-License-Identifier: MIT
88
//
99

10+
mod pid;
11+
1012
use chrono::{NaiveDate, NaiveDateTime, NaiveTime};
11-
use job::Database;
13+
use cron::job::Database;
1214
use std::io::Write;
1315
use std::process::{Command, Output, Stdio};
16+
use std::str::FromStr;
1417
use std::thread;
1518
use std::time::Duration;
1619

@@ -212,3 +215,43 @@ fn test_month() {
212215
.unwrap()
213216
);
214217
}
218+
219+
#[test]
220+
fn test_signal() {
221+
std::env::set_var("LOGNAME", "root");
222+
let logname = std::env::var("LOGNAME").unwrap_or("root".to_string());
223+
#[cfg(target_os = "linux")]
224+
let file = format!("/var/spool/cron/{logname}");
225+
#[cfg(target_os = "macos")]
226+
let file = format!("/var/at/tabs/{logname}");
227+
let mut tmp_file_created = false;
228+
let filepath = std::path::PathBuf::from_str(&file).unwrap();
229+
if !filepath.exists(){
230+
std::fs::File::create(&file).unwrap();
231+
tmp_file_created = true;
232+
}
233+
234+
let output = run_test_base("crond", &vec![], b"");
235+
assert_eq!(output.status.code(), Some(0));
236+
237+
let pids = pid::get_pids("target/debug/crond").unwrap();
238+
assert!(!pids.is_empty());
239+
for pid in &pids{
240+
unsafe{
241+
libc::kill(*pid, libc::SIGHUP);
242+
}
243+
}
244+
245+
let mut old_pids = pids;
246+
let mut pids = pid::get_pids("target/debug/crond").unwrap();
247+
248+
pids.sort();
249+
old_pids.sort();
250+
assert!(pids == old_pids || !pids.is_empty());
251+
252+
let _ = pid::kill("target/debug/crond").unwrap();
253+
254+
if tmp_file_created && filepath.starts_with("/var/at/tabs"){
255+
let _ = std::fs::remove_file(file);
256+
}
257+
}

cron/tests/crond/pid.rs

Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
1+
use std::process::{Command, Output, Stdio};
2+
use std::string::FromUtf8Error;
3+
4+
fn run_command(cmd: &mut Command) -> Result<Output, String> {
5+
cmd.stdout(Stdio::piped())
6+
.stderr(Stdio::piped())
7+
.output()
8+
.map_err(|e| format!("Failed to execute command: {}", e))
9+
}
10+
11+
pub fn get_pids(process_name: &str) -> Result<Vec<i32>, String> {
12+
// Use '[v]i' pattern to prevent grep from matching itself
13+
let grep_pattern =
14+
format!("[{}]", process_name.chars().next().unwrap_or('?')) + &process_name[1..];
15+
let list_cmd_str = format!("ps aux | grep '{}'", grep_pattern);
16+
17+
let mut list_cmd = Command::new("sh");
18+
list_cmd.arg("-c").arg(&list_cmd_str);
19+
20+
let output = run_command(&mut list_cmd)?;
21+
22+
if !output.status.success() {
23+
// `grep` returns non-zero if no lines match, which isn't necessarily a hard error here.
24+
// Check stderr for actual execution errors from `ps` or `sh`.
25+
if !output.stderr.is_empty() && output.stdout.is_empty() {
26+
return Err(format!(
27+
"Process listing command failed with status {}: {}",
28+
output.status,
29+
String::from_utf8_lossy(&output.stderr).trim()
30+
));
31+
}
32+
}
33+
34+
let stdout_str = String::from_utf8(output.stdout.clone())
35+
.map_err(|e: FromUtf8Error| format!("Failed to parse stdout as UTF-8: {}", e))?;
36+
37+
println!("{}", stdout_str);
38+
let mut pids = Vec::new();
39+
for line in stdout_str.lines() {
40+
let parts: Vec<&str> = line.split_whitespace().collect();
41+
// `ps aux` typically has PID in the second column (index 1)
42+
if parts.len() > 1 {
43+
if let Ok(pid) = parts[1].parse::<i32>() {
44+
// Double-check it's not the grep command itself if the simple pattern was used
45+
if !line.contains("grep") {
46+
pids.push(pid);
47+
}
48+
}
49+
}
50+
}
51+
52+
Ok(pids)
53+
}
54+
55+
pub fn kill(process_name: &str) -> Result<(), String> {
56+
let pids = get_pids(process_name)?;
57+
58+
if pids.is_empty() {
59+
return Ok(());
60+
}
61+
62+
let mut kill_errors = Vec::new();
63+
64+
for pid in pids {
65+
let mut kill_cmd = Command::new("kill");
66+
kill_cmd.arg("-9").arg(pid.to_string()); // Send SIGKILL
67+
68+
match run_command(&mut kill_cmd) {
69+
Ok(kill_output) => {
70+
if !kill_output.status.success() {
71+
// Kill can fail due to permissions or if the process died already
72+
let err_msg = format!(
73+
"Failed to kill PID {}: Exit status: {}, Stderr: {}",
74+
pid,
75+
kill_output.status,
76+
String::from_utf8_lossy(&kill_output.stderr).trim()
77+
);
78+
eprintln!("{}", err_msg); // Print immediately
79+
kill_errors.push(err_msg);
80+
}
81+
}
82+
Err(e) => {
83+
// Error executing the kill command itself
84+
let err_msg = format!("Failed to execute kill command for PID {}: {}", pid, e);
85+
eprintln!("{}", err_msg);
86+
kill_errors.push(err_msg);
87+
}
88+
}
89+
}
90+
91+
if kill_errors.is_empty() {
92+
Ok(())
93+
} else {
94+
Err(format!(
95+
"Encountered errors while killing processes:\n{}",
96+
kill_errors.join("\n")
97+
))
98+
}
99+
}

0 commit comments

Comments
 (0)