Revised child process handling.
It is now possible to specify multiple commands.
This commit is contained in:
parent
f7c523eb2f
commit
cad5669e87
5 changed files with 188 additions and 153 deletions
4
Cargo.lock
generated
4
Cargo.lock
generated
|
@ -30,9 +30,9 @@ checksum = "b8f8bdf33df195859076e54ab11ee78a1b208382d3a26ec40d142ffc1ecc49ef"
|
|||
|
||||
[[package]]
|
||||
name = "redox_syscall"
|
||||
version = "0.2.5"
|
||||
version = "0.2.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "94341e4e44e24f6b591b59e47a8a027df12e008d73fd5672dbea9cc22f4507d9"
|
||||
checksum = "8270314b5ccceb518e7e578952f0b72b88222d02e8f77f5ecf7abbb673539041"
|
||||
dependencies = [
|
||||
"bitflags",
|
||||
]
|
||||
|
|
39
src/alarm.rs
39
src/alarm.rs
|
@ -22,7 +22,6 @@ use crate::utils::*;
|
|||
use crate::Config;
|
||||
use std::io::BufRead;
|
||||
use std::io::Write;
|
||||
use std::process::{Child, Command, Stdio};
|
||||
use termion::raw::RawTerminal;
|
||||
use termion::{color, cursor, style};
|
||||
use unicode_width::UnicodeWidthStr;
|
||||
|
@ -439,41 +438,3 @@ impl AlarmRoster {
|
|||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
// Execute the command given on the command line.
|
||||
pub fn exec_command(
|
||||
command: &Vec<String>,
|
||||
elapsed: u32,
|
||||
label: &String
|
||||
) -> Option<Child> {
|
||||
let time = if elapsed < 3600 {
|
||||
format!("{:02}:{:02}", elapsed / 60, elapsed % 60)
|
||||
} else {
|
||||
format!(
|
||||
"{:02}:{:02}:{:02}",
|
||||
elapsed / 3600,
|
||||
(elapsed / 60) % 60,
|
||||
elapsed % 60
|
||||
)
|
||||
};
|
||||
|
||||
let mut args: Vec<String> = Vec::new();
|
||||
// Build vector of command line arguments. Replace every occurrence of
|
||||
// "{t}" and "{l}".
|
||||
for s in command.iter().skip(1) {
|
||||
args.push(s.replace("{t}", &time).replace("{l}", &label));
|
||||
}
|
||||
|
||||
match Command::new(&command[0])
|
||||
.args(args)
|
||||
.stdout(Stdio::null())
|
||||
.stdin(Stdio::null())
|
||||
.spawn()
|
||||
{
|
||||
Ok(child) => Some(child),
|
||||
Err(error) => {
|
||||
eprintln!("Error: Could not execute command. ({})", error);
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
162
src/cradle.rs
Normal file
162
src/cradle.rs
Normal file
|
@ -0,0 +1,162 @@
|
|||
// Copyright 2021, Shy.
|
||||
//
|
||||
// This file is part of Kitchentimer.
|
||||
//
|
||||
// Kitchentimer is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// Kitchentimer is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with Kitchentimer. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
use std::process::{self, Command, Stdio};
|
||||
|
||||
// Manages spawned child processes.
|
||||
pub struct Cradle {
|
||||
commands: Vec<Vec<String>>,
|
||||
children: Vec<process::Child>,
|
||||
}
|
||||
|
||||
impl Drop for Cradle {
|
||||
fn drop(&mut self) {
|
||||
for child in self.children.iter_mut() {
|
||||
eprint!(
|
||||
"Waiting for spawned process (PID {}) to finish ...",
|
||||
child.id()
|
||||
);
|
||||
|
||||
match child.wait() {
|
||||
Ok(status) if status.success() => eprintln!(" ok"),
|
||||
Ok(status) if status.code().is_none() => eprintln!(" interrupted ({})", status),
|
||||
Ok(status) => eprintln!(" ok ({})", status),
|
||||
Err(error) => eprintln!(" failed ({})", error),
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Cradle {
|
||||
pub fn new() -> Cradle {
|
||||
Cradle {
|
||||
commands: Vec::new(),
|
||||
children: Vec::new(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn add(&mut self, mut command: Vec<String>) {
|
||||
// Vector will never change from here on.
|
||||
command.shrink_to_fit();
|
||||
self.commands.push(command);
|
||||
self.children.reserve(self.commands.len());
|
||||
}
|
||||
|
||||
pub fn run_all(&mut self, time: u32, label: &String) {
|
||||
// Do nothing if there are still running child processes.
|
||||
if !self.children.is_empty() { return; }
|
||||
|
||||
let time = if time < 3600 {
|
||||
format!("{:02}:{:02}", time / 60, time % 60)
|
||||
} else {
|
||||
format!(
|
||||
"{:02}:{:02}:{:02}",
|
||||
time / 3600,
|
||||
(time / 60) % 60,
|
||||
time % 60
|
||||
)
|
||||
};
|
||||
|
||||
for command in self.commands.iter() {
|
||||
let mut args: Vec<String> = Vec::new();
|
||||
// Build vector of command line arguments. Replace every occurrence of
|
||||
// "{t}" and "{l}".
|
||||
for s in command.iter().skip(1) {
|
||||
args.push(s.replace("{t}", &time).replace("{l}", &label));
|
||||
}
|
||||
|
||||
match Command::new(&command[0])
|
||||
.args(args)
|
||||
.stdout(Stdio::null())
|
||||
.stdin(Stdio::null())
|
||||
.spawn()
|
||||
{
|
||||
Ok(child) => self.children.push(child),
|
||||
Err(error) => eprintln!("Error: Could not execute command. ({})", error),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn tend(&mut self) {
|
||||
while let Some(mut child) = self.children.pop() {
|
||||
match child.try_wait() {
|
||||
// Process exited successfully.
|
||||
Ok(Some(status)) if status.success() => (),
|
||||
// Abnormal exit.
|
||||
Ok(Some(status)) => eprintln!("Spawned process terminated with non-zero exit status. ({})", status),
|
||||
// Process is still running. Put back child and return.
|
||||
// Leaving any other children unattended, which shouldn't be
|
||||
// a problem, as we will not spawn any further commands, as
|
||||
// long as self.children isn't empty.
|
||||
Ok(None) => {
|
||||
self.children.push(child);
|
||||
break;
|
||||
}
|
||||
// Other error.
|
||||
Err(error) => eprintln!("Error executing command. ({})", error),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Parse command line argument to --command into a vector of strings suitable
|
||||
// for process::Command::new().
|
||||
pub fn parse(input: &str) -> Vec<String> {
|
||||
let mut command: Vec<String> = Vec::new();
|
||||
let mut segment: String = String::new();
|
||||
let mut quoted = false;
|
||||
let mut escaped = false;
|
||||
|
||||
for c in input.chars() {
|
||||
match c {
|
||||
'\\' if !escaped => {
|
||||
// Next char is escaped. (If not escaped itself.)
|
||||
escaped = true;
|
||||
continue;
|
||||
}
|
||||
// Keep spaces when escaped or quoted.
|
||||
' ' if escaped || quoted => {
|
||||
&segment.push(' ');
|
||||
}
|
||||
// Otherwise end the current segment.
|
||||
' ' => {
|
||||
if !&segment.is_empty() {
|
||||
command.push(segment.clone());
|
||||
&segment.clear();
|
||||
}
|
||||
}
|
||||
// Quotation marks toggle quote.
|
||||
'"' | '\'' if !escaped => quoted = !quoted,
|
||||
// Carry everything else. Escape if found escaped.
|
||||
_ => {
|
||||
if escaped {
|
||||
&segment.push('\\');
|
||||
}
|
||||
&segment.push(c);
|
||||
}
|
||||
}
|
||||
escaped = false;
|
||||
}
|
||||
command.push(segment);
|
||||
// Vector will not change from here on.
|
||||
for segment in command.iter_mut() {
|
||||
segment.shrink_to_fit();
|
||||
}
|
||||
command.shrink_to_fit();
|
||||
command
|
||||
}
|
||||
}
|
||||
|
111
src/lib.rs
111
src/lib.rs
|
@ -17,20 +17,22 @@
|
|||
|
||||
extern crate signal_hook;
|
||||
extern crate termion;
|
||||
pub mod alarm;
|
||||
mod alarm;
|
||||
mod buffer;
|
||||
pub mod clock;
|
||||
pub mod consts;
|
||||
pub mod layout;
|
||||
mod clock;
|
||||
mod consts;
|
||||
mod cradle;
|
||||
mod layout;
|
||||
#[cfg(test)]
|
||||
mod tests;
|
||||
pub mod utils;
|
||||
mod utils;
|
||||
|
||||
pub use alarm::AlarmRoster;
|
||||
use alarm::{exec_command, Countdown};
|
||||
use alarm::Countdown;
|
||||
use buffer::Buffer;
|
||||
use clock::{font, Clock};
|
||||
pub use consts::ui::*;
|
||||
use cradle::Cradle;
|
||||
use layout::Layout;
|
||||
use signal_hook::consts::signal::*;
|
||||
use signal_hook::iterator::Signals;
|
||||
|
@ -43,9 +45,9 @@ use termion::raw::{IntoRawMode, RawTerminal};
|
|||
use termion::{clear, cursor, style};
|
||||
|
||||
pub fn run(
|
||||
config: Config,
|
||||
mut config: Config,
|
||||
mut alarm_roster: AlarmRoster,
|
||||
) -> Result<Option<process::Child>, std::io::Error> {
|
||||
) -> Result<(), std::io::Error> {
|
||||
let mut layout = Layout::new();
|
||||
// Initialise roster_width.
|
||||
layout.set_roster_width(alarm_roster.width());
|
||||
|
@ -57,7 +59,6 @@ pub fn run(
|
|||
let mut input_keys = async_stdin.keys();
|
||||
let stdout = std::io::stdout();
|
||||
let mut stdout = stdout.lock().into_raw_mode()?;
|
||||
let mut child: Option<process::Child> = None;
|
||||
let mut force_redraw = true;
|
||||
|
||||
// Register signals.
|
||||
|
@ -129,28 +130,8 @@ pub fn run(
|
|||
}
|
||||
|
||||
// Check on last spawned child process prior to processing the
|
||||
// alarm roster and possibly starting a new one.
|
||||
if let Some(ref mut spawn) = child {
|
||||
match spawn.try_wait() {
|
||||
// Process exited successfully.
|
||||
Ok(Some(status)) if status.success() => child = None,
|
||||
// Abnormal exit.
|
||||
Ok(Some(status)) => {
|
||||
eprintln!(
|
||||
"Spawned process terminated with non-zero exit status. ({})",
|
||||
status
|
||||
);
|
||||
child = None;
|
||||
}
|
||||
// Process is still running.
|
||||
Ok(None) => (),
|
||||
// Other error.
|
||||
Err(error) => {
|
||||
eprintln!("Error executing command. ({})", error);
|
||||
child = None;
|
||||
}
|
||||
}
|
||||
}
|
||||
// alarm roster and possibly spawning a new set.
|
||||
config.commands.tend();
|
||||
|
||||
// Check for exceeded alarms.
|
||||
if let Some(alarm) =
|
||||
|
@ -163,17 +144,9 @@ pub fn run(
|
|||
// Write ASCII bell code.
|
||||
write!(stdout, "{}", 0x07 as char)?;
|
||||
|
||||
match config.command {
|
||||
// Run command if configured and no command is running.
|
||||
Some(ref command) if child.is_none() => {
|
||||
child = exec_command(command, alarm.time, &alarm.label);
|
||||
}
|
||||
// Last command is still running.
|
||||
Some(_) => {
|
||||
eprintln!("Not executing command, as its predecessor is still running")
|
||||
}
|
||||
None => (),
|
||||
}
|
||||
// Run commands.
|
||||
config.commands.run_all(alarm.time, &alarm.label);
|
||||
|
||||
// Quit if configured.
|
||||
if config.quit && alarm_roster.idle() {
|
||||
break;
|
||||
|
@ -365,14 +338,14 @@ pub fn run(
|
|||
write!(stdout, "{}{}{}", clear::All, cursor::Restore, cursor::Show)?;
|
||||
stdout.flush()?;
|
||||
|
||||
Ok(child)
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub struct Config {
|
||||
quit: bool,
|
||||
fancy: bool,
|
||||
font: &'static font::Font,
|
||||
command: Option<Vec<String>>,
|
||||
commands: Cradle,
|
||||
}
|
||||
|
||||
impl Config {
|
||||
|
@ -385,7 +358,7 @@ impl Config {
|
|||
quit: false,
|
||||
fancy: false,
|
||||
font: &font::NORMAL,
|
||||
command: None,
|
||||
commands: Cradle::new(),
|
||||
};
|
||||
let mut iter = args.skip(1);
|
||||
|
||||
|
@ -408,8 +381,9 @@ impl Config {
|
|||
}
|
||||
"-q" | "--quit" => config.quit = true,
|
||||
"-e" | "--exec" => {
|
||||
if let Some(e) = iter.next() {
|
||||
config.command = Some(Config::parse_to_command(&e));
|
||||
if let Some(cmd) = iter.next() {
|
||||
//config.command = Some(Config::parse_to_command(&e));
|
||||
config.commands.add(Cradle::parse(&cmd));
|
||||
} else {
|
||||
return Err(format!("Missing parameter to \"{}\".", arg));
|
||||
}
|
||||
|
@ -431,49 +405,6 @@ impl Config {
|
|||
}
|
||||
Ok(config)
|
||||
}
|
||||
|
||||
// Parse command line argument to --command into a vector of strings suitable
|
||||
// for process::Command::new().
|
||||
fn parse_to_command(input: &str) -> Vec<String> {
|
||||
let mut command: Vec<String> = Vec::new();
|
||||
let mut segment: String = String::new();
|
||||
let mut quoted = false;
|
||||
let mut escaped = false;
|
||||
|
||||
for c in input.chars() {
|
||||
match c {
|
||||
'\\' if !escaped => {
|
||||
// Next char is escaped. (If not escaped itself.)
|
||||
escaped = true;
|
||||
continue;
|
||||
}
|
||||
// Keep spaces when escaped or quoted.
|
||||
' ' if escaped || quoted => {
|
||||
&segment.push(' ');
|
||||
}
|
||||
// Otherwise end the current segment.
|
||||
' ' => {
|
||||
if !&segment.is_empty() {
|
||||
command.push(segment.clone());
|
||||
&segment.clear();
|
||||
}
|
||||
}
|
||||
// Quotation marks toggle quote.
|
||||
'"' | '\'' if !escaped => quoted = !quoted,
|
||||
// Carry everything else. Escape if found escaped.
|
||||
_ => {
|
||||
if escaped {
|
||||
&segment.push('\\');
|
||||
}
|
||||
&segment.push(c);
|
||||
}
|
||||
}
|
||||
escaped = false;
|
||||
}
|
||||
command.push(segment);
|
||||
command.shrink_to_fit();
|
||||
command
|
||||
}
|
||||
}
|
||||
|
||||
// Prepare to suspend execution. Called on SIGTSTP.
|
||||
|
|
25
src/main.rs
25
src/main.rs
|
@ -37,27 +37,8 @@ fn main() {
|
|||
}
|
||||
|
||||
// Run main loop. Returns spawned child process if any.
|
||||
let child = match run(config, alarm_roster) {
|
||||
Ok(child) => child,
|
||||
Err(error) => {
|
||||
eprintln!("Main loop exited with error: {}", error);
|
||||
process::exit(1);
|
||||
}
|
||||
};
|
||||
|
||||
// Wait for remaining spawned process to exit.
|
||||
if let Some(mut child) = child {
|
||||
eprint!(
|
||||
"Waiting for spawned process (PID {}) to finish ...",
|
||||
child.id()
|
||||
);
|
||||
|
||||
match child.wait() {
|
||||
Ok(status) if status.success() => eprintln!(" ok"),
|
||||
// Unix only.
|
||||
Ok(status) if status.code().is_none() => eprintln!(" interrupted ({})", status),
|
||||
Ok(status) => eprintln!(" ok ({})", status),
|
||||
Err(error) => eprintln!(" failed ({})", error),
|
||||
}
|
||||
if let Err(error) = run(config, alarm_roster) {
|
||||
eprintln!("Main loop exited with error: {}", error);
|
||||
process::exit(1);
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue