Revised child process handling.

It is now possible to specify multiple commands.
This commit is contained in:
shy 2021-04-19 11:56:49 +02:00
parent f7c523eb2f
commit cad5669e87
5 changed files with 188 additions and 153 deletions

4
Cargo.lock generated
View file

@ -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",
]

View file

@ -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
View 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
}
}

View file

@ -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.

View file

@ -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) => {
if let Err(error) = run(config, alarm_roster) {
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),
}
}
}