feat: bootstrap awg2 harness rust workspace and docs
This commit is contained in:
@@ -0,0 +1,286 @@
|
||||
//! reboot-scheduler — демон планового перезапуска сервера.
|
||||
//!
|
||||
//! Логика работы:
|
||||
//! 1. При запуске вычисляет случайное время в окне [01:00, 02:00) UTC
|
||||
//! текущих суток (если окно ещё не началось) либо следующих суток.
|
||||
//! 2. Ждёт до наступления выбранного момента.
|
||||
//! 3. Выполняет `systemctl --no-block reboot` и завершается:
|
||||
//! - код 0 при успешном запросе;
|
||||
//! - код 1 при ошибке.
|
||||
//!
|
||||
//! Логирование ведётся только в stderr/stdout и собирается journald.
|
||||
//! Нет сторонних зависимостей — только std.
|
||||
|
||||
use std::process::Command;
|
||||
use std::thread;
|
||||
use std::time::{Duration, SystemTime, UNIX_EPOCH};
|
||||
|
||||
// ─── Константы ────────────────────────────────────────────────────────────────
|
||||
|
||||
/// Начало окна перезагрузки — 01:00:00 UTC в секундах от полуночи.
|
||||
const WINDOW_START_SEC: u64 = 3600;
|
||||
/// Конец окна перезагрузки — 02:00:00 UTC в секундах от полуночи (не включая).
|
||||
const WINDOW_END_SEC: u64 = 2 * 3600;
|
||||
|
||||
/// Интервал опроса при ожидании (проверяем раз в 30 секунд).
|
||||
const POLL_INTERVAL: Duration = Duration::from_secs(30);
|
||||
|
||||
// ─── Логгер ───────────────────────────────────────────────────────────────────
|
||||
|
||||
/// Возвращает текущее время в виде строки «YYYY-MM-DD HH:MM:SS UTC».
|
||||
fn fmt_time(ts: u64) -> String {
|
||||
let secs = ts;
|
||||
let days_total = secs / 86400;
|
||||
let time_of_day = secs % 86400;
|
||||
|
||||
let hh = time_of_day / 3600;
|
||||
let mm = (time_of_day % 3600) / 60;
|
||||
let ss = time_of_day % 60;
|
||||
|
||||
// Алгоритм Фликера для перевода дней с эпохи в дату.
|
||||
let z = days_total as i64 + 719468;
|
||||
let era: i64 = if z >= 0 { z } else { z - 146096 } / 146097;
|
||||
let doe = z - era * 146097;
|
||||
let yoe = (doe - doe / 1460 + doe / 36524 - doe / 146096) / 365;
|
||||
let y = yoe + era * 400;
|
||||
let doy = doe - (365 * yoe + yoe / 4 - yoe / 100);
|
||||
let mp = (5 * doy + 2) / 153;
|
||||
let d = doy - (153 * mp + 2) / 5 + 1;
|
||||
let m = if mp < 10 { mp + 3 } else { mp - 9 };
|
||||
let y = if m <= 2 { y + 1 } else { y };
|
||||
|
||||
format!(
|
||||
"{:04}-{:02}-{:02} {:02}:{:02}:{:02} UTC",
|
||||
y, m, d, hh, mm, ss
|
||||
)
|
||||
}
|
||||
|
||||
/// Записывает строку в журнал с временной меткой.
|
||||
fn log(msg: &str) {
|
||||
let now = unix_now();
|
||||
let line = format!("[{}] {}", fmt_time(now), msg);
|
||||
eprintln!("{}", line);
|
||||
}
|
||||
|
||||
// ─── Время ────────────────────────────────────────────────────────────────────
|
||||
|
||||
/// Возвращает текущий unix-timestamp (секунды).
|
||||
fn unix_now() -> u64 {
|
||||
SystemTime::now()
|
||||
.duration_since(UNIX_EPOCH)
|
||||
.unwrap_or_default()
|
||||
.as_secs()
|
||||
}
|
||||
|
||||
/// Простой LCG-генератор псевдослучайных чисел, инициализированный от времени.
|
||||
struct Lcg(u64);
|
||||
|
||||
impl Lcg {
|
||||
fn new(seed: u64) -> Self {
|
||||
// Перемешиваем seed splitmix64, чтобы даже близкие значения давали
|
||||
// разные последовательности.
|
||||
let mut x = seed.wrapping_add(0x9e3779b97f4a7c15);
|
||||
x = (x ^ (x >> 30)).wrapping_mul(0xbf58476d1ce4e5b9);
|
||||
x = (x ^ (x >> 27)).wrapping_mul(0x94d049bb133111eb);
|
||||
x ^= x >> 31;
|
||||
Self(x)
|
||||
}
|
||||
|
||||
/// Возвращает следующее псевдослучайное число в диапазоне [0, range).
|
||||
fn next_u64(&mut self, range: u64) -> u64 {
|
||||
// LCG параметры (Knuth).
|
||||
self.0 = self
|
||||
.0
|
||||
.wrapping_mul(6364136223846793005)
|
||||
.wrapping_add(1442695040888963407);
|
||||
self.0 % range
|
||||
}
|
||||
}
|
||||
|
||||
/// Вычисляет unix-timestamp следующего момента перезагрузки.
|
||||
///
|
||||
/// Правила:
|
||||
/// - Берём сегодняшнюю полночь UTC.
|
||||
/// - Если сейчас до WINDOW_START, цель — сегодняшнее окно.
|
||||
/// - Иначе — окно следующего дня.
|
||||
/// - Внутри окна выбираем случайную секунду.
|
||||
fn next_reboot_ts_from(now: u64, random_offset: u64) -> u64 {
|
||||
let today_midnight = now - (now % 86400); // полночь текущих суток UTC
|
||||
let time_of_day = now % 86400;
|
||||
|
||||
let base_midnight = if time_of_day < WINDOW_START_SEC {
|
||||
today_midnight
|
||||
} else {
|
||||
today_midnight + 86400 // следующие сутки
|
||||
};
|
||||
|
||||
let window_len = WINDOW_END_SEC - WINDOW_START_SEC; // 3600 секунд
|
||||
let offset = random_offset % window_len;
|
||||
|
||||
base_midnight + WINDOW_START_SEC + offset
|
||||
}
|
||||
|
||||
/// Вычисляет unix-timestamp следующего момента перезагрузки.
|
||||
fn next_reboot_ts() -> u64 {
|
||||
let now = unix_now();
|
||||
let window_len = WINDOW_END_SEC - WINDOW_START_SEC;
|
||||
let mut rng = Lcg::new(now);
|
||||
let offset = rng.next_u64(window_len);
|
||||
next_reboot_ts_from(now, offset)
|
||||
}
|
||||
|
||||
// ─── Перезагрузка ─────────────────────────────────────────────────────────────
|
||||
|
||||
fn run_command(program: &str, args: &[&str]) -> Result<std::process::ExitStatus, String> {
|
||||
Command::new(program)
|
||||
.args(args)
|
||||
.status()
|
||||
.map_err(|e| format!("не удалось запустить `{}`: {}", program, e))
|
||||
}
|
||||
|
||||
/// Выполняет reboot-запрос через указанную команду.
|
||||
fn do_reboot_with(program: &str, args: &[&str]) -> Result<(), String> {
|
||||
let status = run_command(program, args)?;
|
||||
|
||||
if status.success() {
|
||||
Ok(())
|
||||
} else {
|
||||
Err(format!(
|
||||
"команда `{}` завершилась с кодом {:?}",
|
||||
program,
|
||||
status.code()
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
/// Выполняет мягкую перезагрузку через systemctl.
|
||||
fn do_reboot() -> Result<(), String> {
|
||||
do_reboot_with("systemctl", &["--no-block", "reboot"])
|
||||
}
|
||||
|
||||
// ─── Точка входа ──────────────────────────────────────────────────────────────
|
||||
|
||||
fn main() {
|
||||
log("Демон запущен.");
|
||||
|
||||
let target_ts = next_reboot_ts();
|
||||
log(&format!(
|
||||
"Запланированная перезагрузка: {}",
|
||||
fmt_time(target_ts)
|
||||
));
|
||||
|
||||
loop {
|
||||
let now = unix_now();
|
||||
|
||||
if now >= target_ts {
|
||||
log("Наступило запланированное время. Инициируем перезагрузку.");
|
||||
match do_reboot() {
|
||||
Ok(()) => {
|
||||
log("Запрос на перезагрузку принят (systemctl --no-block reboot).");
|
||||
std::process::exit(0);
|
||||
}
|
||||
Err(e) => {
|
||||
log(&format!(
|
||||
"ОШИБКА: запрос на перезагрузку не выполнен: {}",
|
||||
e
|
||||
));
|
||||
std::process::exit(1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let remaining = target_ts - now;
|
||||
// Логируем оставшееся время раз в час (попадаем в окно ±30 с).
|
||||
if remaining % 3600 < 30 {
|
||||
log(&format!("До перезагрузки осталось {} мин.", remaining / 60));
|
||||
}
|
||||
|
||||
thread::sleep(POLL_INTERVAL);
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
fn day(seconds: u64) -> u64 {
|
||||
seconds - (seconds % 86400)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn next_reboot_before_window_uses_today_window() {
|
||||
let now = 25 * 60;
|
||||
let ts = next_reboot_ts_from(now, 123);
|
||||
let base = day(now);
|
||||
assert!(ts >= base + WINDOW_START_SEC);
|
||||
assert!(ts < base + WINDOW_END_SEC);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn next_reboot_on_start_boundary_uses_next_day_window() {
|
||||
let now = WINDOW_START_SEC;
|
||||
let ts = next_reboot_ts_from(now, 0);
|
||||
let base = day(now) + 86400;
|
||||
assert!(ts >= base + WINDOW_START_SEC);
|
||||
assert!(ts < base + WINDOW_END_SEC);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn next_reboot_after_window_uses_next_day_window() {
|
||||
let now = WINDOW_END_SEC + 1;
|
||||
let ts = next_reboot_ts_from(now, 42);
|
||||
let base = day(now) + 86400;
|
||||
assert!(ts >= base + WINDOW_START_SEC);
|
||||
assert!(ts < base + WINDOW_END_SEC);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn next_reboot_always_inside_window() {
|
||||
let now = 1_700_000_000;
|
||||
let ts = next_reboot_ts_from(now, 3599);
|
||||
let tod = ts % 86400;
|
||||
assert!((WINDOW_START_SEC..WINDOW_END_SEC).contains(&tod));
|
||||
}
|
||||
|
||||
#[cfg(unix)]
|
||||
#[test]
|
||||
fn do_reboot_with_success_command_returns_ok() {
|
||||
let res = do_reboot_with("/bin/true", &[]);
|
||||
assert!(res.is_ok());
|
||||
}
|
||||
|
||||
#[cfg(unix)]
|
||||
#[test]
|
||||
fn do_reboot_with_failing_command_returns_err() {
|
||||
let res = do_reboot_with("/bin/false", &[]);
|
||||
assert!(res.is_err());
|
||||
}
|
||||
|
||||
#[cfg(unix)]
|
||||
#[test]
|
||||
fn do_reboot_with_missing_command_returns_err() {
|
||||
let res = do_reboot_with("/definitely/not/exist", &[]);
|
||||
assert!(res.is_err());
|
||||
}
|
||||
|
||||
#[cfg(windows)]
|
||||
#[test]
|
||||
fn do_reboot_with_success_command_returns_ok() {
|
||||
let res = do_reboot_with("cmd", &["/C", "exit", "0"]);
|
||||
assert!(res.is_ok());
|
||||
}
|
||||
|
||||
#[cfg(windows)]
|
||||
#[test]
|
||||
fn do_reboot_with_failing_command_returns_err() {
|
||||
let res = do_reboot_with("cmd", &["/C", "exit", "1"]);
|
||||
assert!(res.is_err());
|
||||
}
|
||||
|
||||
#[cfg(windows)]
|
||||
#[test]
|
||||
fn do_reboot_with_missing_command_returns_err() {
|
||||
let res = do_reboot_with("C:\\definitely\\not\\exist\\cmd.exe", &[]);
|
||||
assert!(res.is_err());
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user