Skip to content

Commit d5a92e8

Browse files
committed
[cron] full implementation
1 parent a1eceb4 commit d5a92e8

File tree

9 files changed

+912
-1
lines changed

9 files changed

+912
-1
lines changed

Cargo.toml

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ resolver = "2"
44
members = [
55
"awk",
66
"calc",
7+
"cron",
78
"datetime",
89
"dev",
910
"display",
@@ -25,7 +26,8 @@ members = [
2526
"tree",
2627
"users",
2728
"xform",
28-
"i18n"
29+
"i18n",
30+
"cron",
2931
]
3032

3133
[workspace.package]

cron/Cargo.toml

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
[package]
2+
name = "posixutils-cron"
3+
version = "0.1.0"
4+
edition = "2021"
5+
6+
[dependencies]
7+
gettext-rs = { workspace = true }
8+
plib = { path = "../plib" }
9+
clap = { workspace = true }
10+
libc = { workspace = true }
11+
chrono = { workspace = true }
12+
13+
[[bin]]
14+
name = "crontab"
15+
path = "src/bin/crontab.rs"
16+
17+
[[bin]]
18+
name = "crond"
19+
path = "src/bin/crond.rs"
20+
21+
[[bin]]
22+
name = "main"
23+
path = "src/main.rs"
24+
25+
[lib]
26+
path = "./src/lib.rs"

cron/src/bin/crond.rs

Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
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+
use chrono::Local;
11+
use gettextrs::{bind_textdomain_codeset, gettext, setlocale, textdomain, LocaleCategory};
12+
use posixutils_cron::job::Database;
13+
use std::env;
14+
use std::error::Error;
15+
use std::fs;
16+
use std::str::FromStr;
17+
18+
fn parse_cronfile(username: &str) -> Result<Database, Box<dyn Error>> {
19+
#[cfg(target_os = "linux")]
20+
let file = format!("/var/spool/cron/{username}");
21+
#[cfg(target_os = "macos")]
22+
let file = format!("/var/at/tabs/{username}");
23+
let s = fs::read_to_string(&file)?;
24+
Ok(s.lines()
25+
.filter_map(|x| Database::from_str(x).ok())
26+
.fold(Database(vec![]), |acc, next| acc.merge(next)))
27+
}
28+
29+
fn main() -> Result<(), Box<dyn std::error::Error>> {
30+
setlocale(LocaleCategory::LcAll, "");
31+
textdomain("posixutils-rs")?;
32+
bind_textdomain_codeset("posixutils-rs", "UTF-8")?;
33+
34+
let Ok(logname) = env::var("LOGNAME") else {
35+
panic!("Could not obtain the user's logname.")
36+
};
37+
38+
// Daemon setup
39+
unsafe {
40+
use libc::*;
41+
42+
let pid = fork();
43+
if pid > 0 {
44+
return Ok(());
45+
}
46+
47+
setsid();
48+
chdir(b"/\0" as *const _ as *const c_char);
49+
50+
close(STDIN_FILENO);
51+
close(STDOUT_FILENO);
52+
close(STDERR_FILENO);
53+
}
54+
55+
// Daemon code
56+
57+
loop {
58+
let db = parse_cronfile(&logname)?;
59+
let Some(x) = db.nearest_job() else {
60+
sleep(60);
61+
continue;
62+
};
63+
let Some(next_exec) = x.next_execution(&Local::now().naive_local()) else {
64+
sleep(60);
65+
continue;
66+
};
67+
let now = Local::now();
68+
let diff = now.naive_local() - next_exec;
69+
let sleep_time = diff.num_seconds();
70+
71+
if sleep_time < 60 {
72+
sleep(sleep_time as u32);
73+
x.run_job()?;
74+
} else {
75+
sleep(60);
76+
}
77+
}
78+
}
79+
80+
fn sleep(target: u32) {
81+
unsafe { libc::sleep(target) };
82+
}

cron/src/bin/crontab.rs

Lines changed: 172 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,172 @@
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+
use clap::Parser;
11+
12+
use gettextrs::{bind_textdomain_codeset, gettext, setlocale, textdomain, LocaleCategory};
13+
use std::env;
14+
use std::fs;
15+
use std::fs::File;
16+
use std::io::{ErrorKind, Read, Result, Write};
17+
use std::process::{exit, ExitStatus};
18+
19+
#[derive(Parser)]
20+
struct CronArgs {
21+
#[arg(short, long, help = gettext("edit user's crontab"))]
22+
edit: bool,
23+
#[arg(short, long, help = gettext("list user's crontab"))]
24+
list: bool,
25+
#[arg(short, long, help = gettext("delete user's crontab"))]
26+
remove: bool,
27+
#[arg(name = "FILE", help = gettext("file to replace user's current crontab with"))]
28+
file: Option<String>,
29+
}
30+
31+
fn print_usage(err: &str) {
32+
let name = env::args().next().unwrap();
33+
eprintln!("{name}: usage error: {err}");
34+
eprintln!("usage:\t{name} [ FILE ]");
35+
eprintln!("\t{name} [ -e | -l | -r ]");
36+
eprintln!("\t-e\t- edit user's crontab");
37+
eprintln!("\t-l\t- list user's crontab");
38+
eprintln!("\t-r\t- delete user's crontab");
39+
}
40+
41+
fn list_crontab(path: &str) -> Result<String> {
42+
fs::read_to_string(path)
43+
}
44+
45+
fn remove_crontab(path: &str) -> Result<()> {
46+
fs::remove_file(path)
47+
}
48+
49+
fn edit_crontab(path: &str) -> Result<ExitStatus> {
50+
File::create(path)?;
51+
let editor = env::var("EDITOR").unwrap_or("vi".to_string());
52+
let shell = env::var("SHELL").unwrap_or("sh".to_string());
53+
let args = ["-c".to_string(), format!("{editor} {path}")];
54+
std::process::Command::new(shell).args(args).status()
55+
}
56+
57+
fn replace_crontab(from: &str, to: &str) -> Result<()> {
58+
let mut source = File::open(from)?;
59+
let mut target = File::create(to)?;
60+
let mut buffer = Vec::new();
61+
62+
source.read_to_end(&mut buffer)?;
63+
target.write_all(&buffer)?;
64+
65+
Ok(())
66+
}
67+
68+
fn main() -> std::result::Result<(), Box<dyn std::error::Error>> {
69+
setlocale(LocaleCategory::LcAll, "");
70+
textdomain("posixutils-rs")?;
71+
bind_textdomain_codeset("posixutils-rs", "UTF-8")?;
72+
73+
let args = CronArgs::parse();
74+
let Ok(logname) = env::var("LOGNAME") else {
75+
println!("Could not obtain the user's logname.");
76+
exit(1);
77+
};
78+
#[cfg(target_os = "linux")]
79+
let path = format!("/var/spool/cron/{logname}");
80+
#[cfg(target_os = "macos")]
81+
let path = format!("/var/spool/cron/{logname}");
82+
83+
let opt_count = [args.edit, args.list, args.remove, args.file.is_some()]
84+
.into_iter()
85+
.map(|x| x as i32)
86+
.sum::<i32>();
87+
if opt_count > 1 {
88+
print_usage("Too many options specified.");
89+
exit(1);
90+
}
91+
92+
if opt_count < 1 {
93+
print_usage("Not enough options specified.");
94+
exit(1);
95+
}
96+
97+
if args.edit {
98+
match edit_crontab(&path) {
99+
Ok(status) => exit(status.code().unwrap_or(0)),
100+
Err(err) => {
101+
match err.kind() {
102+
ErrorKind::NotFound => println!("No crontab file has been found."),
103+
ErrorKind::PermissionDenied => {
104+
println!("Permission to access user's crontab file denied.")
105+
}
106+
ErrorKind::Interrupted => println!("crontab was interrupted."),
107+
ErrorKind::OutOfMemory => println!("crontab exceeded available memory."),
108+
_ => println!("Unknown error: {}", err),
109+
}
110+
exit(1);
111+
}
112+
}
113+
}
114+
115+
if args.list {
116+
match list_crontab(&path) {
117+
Ok(content) => println!("{}", content),
118+
Err(err) => {
119+
match err.kind() {
120+
ErrorKind::NotFound => println!("No crontab file has been found."),
121+
ErrorKind::PermissionDenied => {
122+
println!("Permission to access user's crontab file denied.")
123+
}
124+
ErrorKind::Interrupted => println!("crontab was interrupted."),
125+
ErrorKind::OutOfMemory => println!("crontab exceeded available memory."),
126+
_ => println!("Unknown error: {}", err),
127+
}
128+
exit(1);
129+
}
130+
}
131+
}
132+
133+
if args.remove {
134+
match remove_crontab(&path) {
135+
Ok(()) => println!("Removed crontab file"),
136+
Err(err) => {
137+
match err.kind() {
138+
ErrorKind::NotFound => println!("No crontab file has been found."),
139+
ErrorKind::PermissionDenied => {
140+
println!("Permission to access user's crontab file denied.")
141+
}
142+
ErrorKind::Interrupted => println!("crontab was interrupted."),
143+
ErrorKind::OutOfMemory => println!("crontab exceeded available memory."),
144+
_ => println!("Unknown error: {}", err),
145+
}
146+
exit(1);
147+
}
148+
}
149+
}
150+
151+
if let Some(file) = args.file {
152+
match replace_crontab(&file, &path) {
153+
Ok(()) => println!("Replaced crontab file with {file}"),
154+
Err(err) => {
155+
match err.kind() {
156+
ErrorKind::NotFound => {
157+
println!("Crontab file or user-specified file has not been found.")
158+
}
159+
ErrorKind::PermissionDenied => println!(
160+
"Permission to access user's crontab file or user-specified file denied."
161+
),
162+
ErrorKind::Interrupted => println!("crontab was interrupted."),
163+
ErrorKind::OutOfMemory => println!("crontab exceeded available memory."),
164+
_ => println!("Unknown error: {}", err),
165+
}
166+
exit(1);
167+
}
168+
}
169+
}
170+
171+
Ok(())
172+
}

0 commit comments

Comments
 (0)