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]]
|
[[package]]
|
||||||
name = "redox_syscall"
|
name = "redox_syscall"
|
||||||
version = "0.2.5"
|
version = "0.2.6"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "94341e4e44e24f6b591b59e47a8a027df12e008d73fd5672dbea9cc22f4507d9"
|
checksum = "8270314b5ccceb518e7e578952f0b72b88222d02e8f77f5ecf7abbb673539041"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"bitflags",
|
"bitflags",
|
||||||
]
|
]
|
||||||
|
|
39
src/alarm.rs
39
src/alarm.rs
|
@ -22,7 +22,6 @@ use crate::utils::*;
|
||||||
use crate::Config;
|
use crate::Config;
|
||||||
use std::io::BufRead;
|
use std::io::BufRead;
|
||||||
use std::io::Write;
|
use std::io::Write;
|
||||||
use std::process::{Child, Command, Stdio};
|
|
||||||
use termion::raw::RawTerminal;
|
use termion::raw::RawTerminal;
|
||||||
use termion::{color, cursor, style};
|
use termion::{color, cursor, style};
|
||||||
use unicode_width::UnicodeWidthStr;
|
use unicode_width::UnicodeWidthStr;
|
||||||
|
@ -439,41 +438,3 @@ impl AlarmRoster {
|
||||||
Ok(())
|
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 signal_hook;
|
||||||
extern crate termion;
|
extern crate termion;
|
||||||
pub mod alarm;
|
mod alarm;
|
||||||
mod buffer;
|
mod buffer;
|
||||||
pub mod clock;
|
mod clock;
|
||||||
pub mod consts;
|
mod consts;
|
||||||
pub mod layout;
|
mod cradle;
|
||||||
|
mod layout;
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests;
|
mod tests;
|
||||||
pub mod utils;
|
mod utils;
|
||||||
|
|
||||||
pub use alarm::AlarmRoster;
|
pub use alarm::AlarmRoster;
|
||||||
use alarm::{exec_command, Countdown};
|
use alarm::Countdown;
|
||||||
use buffer::Buffer;
|
use buffer::Buffer;
|
||||||
use clock::{font, Clock};
|
use clock::{font, Clock};
|
||||||
pub use consts::ui::*;
|
pub use consts::ui::*;
|
||||||
|
use cradle::Cradle;
|
||||||
use layout::Layout;
|
use layout::Layout;
|
||||||
use signal_hook::consts::signal::*;
|
use signal_hook::consts::signal::*;
|
||||||
use signal_hook::iterator::Signals;
|
use signal_hook::iterator::Signals;
|
||||||
|
@ -43,9 +45,9 @@ use termion::raw::{IntoRawMode, RawTerminal};
|
||||||
use termion::{clear, cursor, style};
|
use termion::{clear, cursor, style};
|
||||||
|
|
||||||
pub fn run(
|
pub fn run(
|
||||||
config: Config,
|
mut config: Config,
|
||||||
mut alarm_roster: AlarmRoster,
|
mut alarm_roster: AlarmRoster,
|
||||||
) -> Result<Option<process::Child>, std::io::Error> {
|
) -> Result<(), std::io::Error> {
|
||||||
let mut layout = Layout::new();
|
let mut layout = Layout::new();
|
||||||
// Initialise roster_width.
|
// Initialise roster_width.
|
||||||
layout.set_roster_width(alarm_roster.width());
|
layout.set_roster_width(alarm_roster.width());
|
||||||
|
@ -57,7 +59,6 @@ pub fn run(
|
||||||
let mut input_keys = async_stdin.keys();
|
let mut input_keys = async_stdin.keys();
|
||||||
let stdout = std::io::stdout();
|
let stdout = std::io::stdout();
|
||||||
let mut stdout = stdout.lock().into_raw_mode()?;
|
let mut stdout = stdout.lock().into_raw_mode()?;
|
||||||
let mut child: Option<process::Child> = None;
|
|
||||||
let mut force_redraw = true;
|
let mut force_redraw = true;
|
||||||
|
|
||||||
// Register signals.
|
// Register signals.
|
||||||
|
@ -129,28 +130,8 @@ pub fn run(
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check on last spawned child process prior to processing the
|
// Check on last spawned child process prior to processing the
|
||||||
// alarm roster and possibly starting a new one.
|
// alarm roster and possibly spawning a new set.
|
||||||
if let Some(ref mut spawn) = child {
|
config.commands.tend();
|
||||||
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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check for exceeded alarms.
|
// Check for exceeded alarms.
|
||||||
if let Some(alarm) =
|
if let Some(alarm) =
|
||||||
|
@ -163,17 +144,9 @@ pub fn run(
|
||||||
// Write ASCII bell code.
|
// Write ASCII bell code.
|
||||||
write!(stdout, "{}", 0x07 as char)?;
|
write!(stdout, "{}", 0x07 as char)?;
|
||||||
|
|
||||||
match config.command {
|
// Run commands.
|
||||||
// Run command if configured and no command is running.
|
config.commands.run_all(alarm.time, &alarm.label);
|
||||||
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 => (),
|
|
||||||
}
|
|
||||||
// Quit if configured.
|
// Quit if configured.
|
||||||
if config.quit && alarm_roster.idle() {
|
if config.quit && alarm_roster.idle() {
|
||||||
break;
|
break;
|
||||||
|
@ -365,14 +338,14 @@ pub fn run(
|
||||||
write!(stdout, "{}{}{}", clear::All, cursor::Restore, cursor::Show)?;
|
write!(stdout, "{}{}{}", clear::All, cursor::Restore, cursor::Show)?;
|
||||||
stdout.flush()?;
|
stdout.flush()?;
|
||||||
|
|
||||||
Ok(child)
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct Config {
|
pub struct Config {
|
||||||
quit: bool,
|
quit: bool,
|
||||||
fancy: bool,
|
fancy: bool,
|
||||||
font: &'static font::Font,
|
font: &'static font::Font,
|
||||||
command: Option<Vec<String>>,
|
commands: Cradle,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Config {
|
impl Config {
|
||||||
|
@ -385,7 +358,7 @@ impl Config {
|
||||||
quit: false,
|
quit: false,
|
||||||
fancy: false,
|
fancy: false,
|
||||||
font: &font::NORMAL,
|
font: &font::NORMAL,
|
||||||
command: None,
|
commands: Cradle::new(),
|
||||||
};
|
};
|
||||||
let mut iter = args.skip(1);
|
let mut iter = args.skip(1);
|
||||||
|
|
||||||
|
@ -408,8 +381,9 @@ impl Config {
|
||||||
}
|
}
|
||||||
"-q" | "--quit" => config.quit = true,
|
"-q" | "--quit" => config.quit = true,
|
||||||
"-e" | "--exec" => {
|
"-e" | "--exec" => {
|
||||||
if let Some(e) = iter.next() {
|
if let Some(cmd) = iter.next() {
|
||||||
config.command = Some(Config::parse_to_command(&e));
|
//config.command = Some(Config::parse_to_command(&e));
|
||||||
|
config.commands.add(Cradle::parse(&cmd));
|
||||||
} else {
|
} else {
|
||||||
return Err(format!("Missing parameter to \"{}\".", arg));
|
return Err(format!("Missing parameter to \"{}\".", arg));
|
||||||
}
|
}
|
||||||
|
@ -431,49 +405,6 @@ impl Config {
|
||||||
}
|
}
|
||||||
Ok(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.
|
// Prepare to suspend execution. Called on SIGTSTP.
|
||||||
|
|
21
src/main.rs
21
src/main.rs
|
@ -37,27 +37,8 @@ fn main() {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Run main loop. Returns spawned child process if any.
|
// Run main loop. Returns spawned child process if any.
|
||||||
let child = match run(config, alarm_roster) {
|
if let Err(error) = run(config, alarm_roster) {
|
||||||
Ok(child) => child,
|
|
||||||
Err(error) => {
|
|
||||||
eprintln!("Main loop exited with error: {}", error);
|
eprintln!("Main loop exited with error: {}", error);
|
||||||
process::exit(1);
|
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),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue