diff --git a/.gitignore b/.gitignore index ea8c4bf..5f9ff5d 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,2 @@ /target +.vscode/ \ No newline at end of file diff --git a/Cargo.lock b/Cargo.lock index 9f2e1df..363c425 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -110,6 +110,12 @@ dependencies = [ "syn 2.0.15", ] +[[package]] +name = "either" +version = "1.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7fcaabb2fef8c910e7f4c7ce9f67a1283a1715879a7c230ca9d6d1ae31f16d91" + [[package]] name = "hashbrown" version = "0.12.3" @@ -150,6 +156,15 @@ dependencies = [ "hashbrown", ] +[[package]] +name = "itertools" +version = "0.10.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b0fd2260e829bddf4cb6ea802289de2f86d6a7a690192fbe91b3f46e0f2c8473" +dependencies = [ + "either", +] + [[package]] name = "js-sys" version = "0.3.61" @@ -164,6 +179,7 @@ name = "laika" version = "0.1.0" dependencies = [ "chrono", + "itertools", "serde", "sysinfo", "toml", diff --git a/Cargo.toml b/Cargo.toml index b7f29d8..e87d8f1 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -7,6 +7,7 @@ edition = "2021" [dependencies] chrono = "0.4.24" +itertools = "0.10.5" serde = { version = "1.0.160", features = ["derive"] } sysinfo = { version = "0.28.4", default-features = false } toml = "0.7.3" diff --git a/laika.toml b/laika.toml index 8a1e39d..ba014bd 100644 --- a/laika.toml +++ b/laika.toml @@ -2,6 +2,9 @@ command = "calc.exe" target = "CalculatorApp.exe" workdir = "." delay = 2 -start = { hour = 11, minute = 50 } -end = { hour = 12, minute = 55 } +#start = { hour = 11, minute = 50 } +start = "11:50" +#end = { hour = 12, minute = 55 } +end = "12:55" + diff --git a/src/main.rs b/src/main.rs index 0a72f13..8a74d5c 100644 --- a/src/main.rs +++ b/src/main.rs @@ -2,16 +2,37 @@ use chrono::prelude::*; use chrono::{DateTime, Local, TimeZone}; +use itertools::Itertools; use serde::Deserialize; use std::path::Path; +use std::str::FromStr; use std::{thread, time::Duration}; use sysinfo::{System, SystemExt}; use std::{env, fs, path::PathBuf, process::Command}; +trait ErrorToString { + type Output; + fn str_err(self) -> std::result::Result; +} + +impl ErrorToString for std::result::Result +where + E: std::error::Error, +{ + type Output = T; + fn str_err(self) -> std::result::Result { + self.map_err(|e| e.to_string()) + } +} + +/// Time with hours (24) and minutes (60) #[derive(Deserialize, Default, PartialEq, Eq, PartialOrd, Ord, Copy, Clone, Debug)] +#[serde(try_from = "String")] struct TimeHM { + /// hour num (0..23) hour: u8, + /// minute num (0..59) minute: u8, } @@ -24,17 +45,60 @@ impl From> for TimeHM { } } -#[derive(Deserialize, Default)] +impl FromStr for TimeHM { + type Err = String; + + fn from_str(s: &str) -> Result { + if let Some((str_hour, str_minute)) = s.split(':').collect_tuple() { + let hour: u8 = str_hour.parse().str_err()?; + let minute: u8 = str_minute.parse().str_err()?; + Ok(TimeHM { hour, minute }) + } else { + Err("invalid time, must be hh:mm".into()) + } + } +} + +impl TryFrom for TimeHM { + type Error = String; + + fn try_from(value: String) -> Result { + value.parse() + } +} + +/// Application config +#[derive(Deserialize, Default, Debug)] struct Config { + /// is application active + #[serde(default = "default_true")] + active: bool, + /// need to exit + #[serde(default)] + exit_now: bool, + /// target command command: String, + /// target process name + #[serde(default)] target: String, + /// workdir for launch target command + #[serde(default)] workdir: String, + /// args for launch target command + #[serde(default)] args: Option>, + /// start time start: TimeHM, + /// end time end: TimeHM, + /// check delay (seconds) delay: u32, } +fn default_true() -> bool { + true +} + impl Config { fn read>(path: P) -> Result { let data = fs::read_to_string(path).map_err(|e| format!("can't read config: {:?}", e))?; @@ -75,39 +139,58 @@ impl Config { .unwrap() } } + fn is_valid(&self) -> bool { + (!self.command.is_empty()) && self.delay > 0 + } +} + +struct Laika { + config: Config, +} + +impl From for Laika { + fn from(config: Config) -> Self { + Laika { config } + } +} + +impl Laika { fn is_in_time(&self, value: TimeHM) -> bool { - self.start <= value && value <= self.end + self.config.start <= value && value <= self.config.end } fn is_active(&self) -> bool { let now: DateTime = Local::now(); - self.is_in_time(now.into()) + self.config.active && self.config.is_valid() && self.is_in_time(now.into()) } fn sleep(&self) { - thread::sleep(Duration::from_secs(self.delay.into())); + thread::sleep(Duration::from_secs(self.config.delay.into())); } fn is_target_alive(&self) -> bool { - let name = self.target_name(); + let name = self.config.target_name(); + if name.is_empty() { + return false; + } let sp = System::new_all(); let procs = sp.processes_by_exact_name(&name); procs.count() > 0 } fn relaunch_target(&self) { - let mut cmd = Command::new(&self.command); - cmd.current_dir(&self.workdir); - if let Some(args) = &self.args { + let mut cmd = Command::new(&self.config.command); + cmd.current_dir(&self.config.workdir); + if let Some(args) = &self.config.args { cmd.args(args); } let _res = cmd.spawn(); } - fn is_valid(&self) -> bool { - (!self.command.is_empty()) && self.delay > 0 - } + fn main_loop(self) { - if !self.is_valid() { + if !self.config.is_valid() { return; } loop { - if self.is_active() && !self.is_target_alive() { + if self.config.exit_now { + return; + } else if self.is_active() && !self.is_target_alive() { self.relaunch_target(); } self.sleep(); @@ -116,5 +199,7 @@ impl Config { } fn main() { - Config::get().main_loop() + let config = Config::get(); + let laika: Laika = config.into(); + laika.main_loop(); }