Enhancement: Add support for stdin input passing to child process in exec, watchdog and spawn commands #247

This commit is contained in:
sagie gur ari 2022-05-25 14:22:25 +00:00
parent b75aac72c7
commit 0d80569bf6
15 changed files with 230 additions and 45 deletions

View file

@ -1,5 +1,9 @@
## CHANGELOG
### v0.8.12
* Enhancement: Add support for stdin input passing to child process in exec, watchdog and spawn commands #247
### v0.8.11 (2022-04-20)
* Fix: Runtime - fix control characters '\' parsing and expansion #237

View file

@ -5308,7 +5308,7 @@ ftp_put_in_memory
<a name="std__process__Execute"></a>
## `std::process::Execute`
```sh
exec [--fail-on-error] command [args]*
exec [--fail-on-error|--get-exit-code] [--input value] command [args]*
output = exec command [args]*
stdout = set ${output.stdout}
@ -5334,6 +5334,7 @@ If an output variable is set and the --get-exit-code flag is provided, the outpu
* --fail-on-error - If no output variable is provided, it will cause an error in case the executed process exits with an error exit code.
* --get-exit-code - If an output variable is provided, it will contain the exit code.
* --input - Optional content to be sent to the child process input stream.
* The command to execute and its arguments.
### Return Value
@ -5421,7 +5422,7 @@ pid, process_id
<a name="std__process__Spawn"></a>
## `std::process::Spawn`
```sh
pid = spawn [--silent] command [args]*
pid = spawn [--silent] [--input value] command [args]*
```
Executes the provided native command and arguments.<br>
@ -5429,7 +5430,8 @@ It will not wait for the process to finish and will return the process pid.
### Parameters
* Option --silent flag to suppress any output.
* Optional --silent flag to suppress any output.
* --input - Optional content to be sent to the child process input stream.
* The command to execute and its arguments.
### Return Value
@ -5451,7 +5453,7 @@ spawn
<a name="std__process__Watchdog"></a>
## `std::process::Watchdog`
```sh
count = watchdog [--max-retries value] [--interval value] -- command [arguments]*
count = watchdog [--max-retries value] [--interval value] [--input value] -- command [arguments]*
```
Executes the provided native command and arguments.<br>
@ -5463,6 +5465,7 @@ In case of an invalid command, the watchdog will not reattempt the invocation an
* --max-retries - Positive value of max retries (excluding the first invocation). value <= 0 for unlimited retries. Default is unlimited.
* --interval - The amount in milliseconds between retries. 0 for no waiting between invocations. Default is no wait.
* --input - Optional content to be sent to the child process input stream.
* The command to execute (preceded by a **--** separator).
* The command arguments.

View file

@ -1,5 +1,5 @@
```sh
exec [--fail-on-error] command [args]*
exec [--fail-on-error|--get-exit-code] [--input value] command [args]*
output = exec command [args]*
stdout = set ${output.stdout}
@ -25,6 +25,7 @@ If an output variable is set and the --get-exit-code flag is provided, the outpu
* --fail-on-error - If no output variable is provided, it will cause an error in case the executed process exits with an error exit code.
* --get-exit-code - If an output variable is provided, it will contain the exit code.
* --input - Optional content to be sent to the child process input stream.
* The command to execute and its arguments.
### Return Value

View file

@ -1,3 +1,4 @@
use crate::utils::exec::ExecInput;
use crate::utils::{exec, pckg};
use duckscript::types::command::{Command, CommandResult, Commands};
use duckscript::types::instruction::Instruction;
@ -8,6 +9,11 @@ use std::collections::HashMap;
#[path = "./mod_test.rs"]
mod mod_test;
enum LookingFor {
Flag,
Input,
}
#[derive(Clone)]
pub(crate) struct CommandImpl {
package: String,
@ -44,22 +50,48 @@ impl Command for CommandImpl {
_commands: &mut Commands,
_line: usize,
) -> CommandResult {
let allow_input = output_variable.is_some();
let (print_output, start_index, fail_on_error, exit_code_output) =
if !arguments.is_empty() && arguments[0] == "--fail-on-error" {
(
output_variable.is_none(),
1,
output_variable.is_none(),
false,
)
} else if !arguments.is_empty() && arguments[0] == "--get-exit-code" {
(true, 1, false, true)
} else {
(output_variable.is_none(), 0, false, false)
};
let mut input = if output_variable.is_some() {
ExecInput::External
} else {
ExecInput::None
};
let mut command_start_index = 0;
let mut print_output = output_variable.is_none();
let mut fail_on_error = false;
let mut exit_code_output = false;
match exec::exec(&arguments, print_output, allow_input, start_index) {
let mut index = 0;
let mut looking_for = LookingFor::Flag;
for argument in &arguments {
index = index + 1;
match looking_for {
LookingFor::Flag => match argument.as_str() {
"--fail-on-error" => {
fail_on_error = output_variable.is_none();
command_start_index = command_start_index + 1;
}
"--get-exit-code" => {
exit_code_output = true;
print_output = true;
command_start_index = command_start_index + 1;
}
"--input" => {
looking_for = LookingFor::Input;
command_start_index = command_start_index + 1;
}
_ => break,
},
LookingFor::Input => {
input = ExecInput::Text(argument.to_string());
command_start_index = command_start_index + 1;
looking_for = LookingFor::Flag;
}
}
}
match exec::exec(&arguments, print_output, input, command_start_index) {
Ok((stdout, stderr, exit_code)) => match output_variable {
Some(name) => {
if exit_code_output {

View file

@ -48,6 +48,24 @@ fn run_with_output() {
assert_eq!(exit_code, "0");
}
#[test]
#[cfg(target_os = "linux")]
fn run_with_input() {
let context = test::run_script_and_validate(
vec![create("")],
"out = exec --input test cat",
CommandValidation::Match("out.code".to_string(), "0".to_string()),
);
let stdout = context.variables.get("out.stdout").unwrap();
let stderr = context.variables.get("out.stderr").unwrap();
let exit_code = context.variables.get("out.code").unwrap();
assert!(stdout.contains("test"));
assert!(stderr.is_empty());
assert_eq!(exit_code, "0");
}
#[test]
fn run_error_code_with_output() {
test::run_script_and_error(vec![create("")], "out = exec badcommand", "out");

View file

@ -1,5 +1,5 @@
```sh
pid = spawn [--silent] command [args]*
pid = spawn [--silent] [--input value] command [args]*
```
Executes the provided native command and arguments.<br>
@ -7,7 +7,8 @@ It will not wait for the process to finish and will return the process pid.
### Parameters
* Option --silent flag to suppress any output.
* Optional --silent flag to suppress any output.
* --input - Optional content to be sent to the child process input stream.
* The command to execute and its arguments.
### Return Value

View file

@ -1,3 +1,4 @@
use crate::utils::exec::ExecInput;
use crate::utils::{exec, pckg};
use duckscript::types::command::{Command, CommandResult};
@ -5,6 +6,11 @@ use duckscript::types::command::{Command, CommandResult};
#[path = "./mod_test.rs"]
mod mod_test;
enum LookingFor {
Flag,
Input,
}
#[derive(Clone)]
pub(crate) struct CommandImpl {
package: String,
@ -28,13 +34,37 @@ impl Command for CommandImpl {
}
fn run(&self, arguments: Vec<String>) -> CommandResult {
let (print_output, start_index) = if !arguments.is_empty() && arguments[0] == "--silent" {
(false, 1)
} else {
(true, 0)
};
let mut print_output = true;
let mut input = ExecInput::None;
let mut command_start_index = 0;
match exec::spawn(&arguments, print_output, false, start_index) {
let mut index = 0;
let mut looking_for = LookingFor::Flag;
for argument in &arguments {
index = index + 1;
match looking_for {
LookingFor::Flag => match argument.as_str() {
"--silent" => {
print_output = false;
command_start_index = command_start_index + 1;
}
"--input" => {
looking_for = LookingFor::Input;
command_start_index = command_start_index + 1;
}
_ => break,
},
LookingFor::Input => {
input = ExecInput::Text(argument.to_string());
command_start_index = command_start_index + 1;
looking_for = LookingFor::Flag;
}
}
}
match exec::spawn(&arguments, print_output, true, input, command_start_index) {
Ok(child) => {
let pid = child.id();

View file

@ -29,3 +29,13 @@ fn run_valid_silent() {
CommandValidation::PositiveNumber("out".to_string()),
);
}
#[test]
#[cfg(target_os = "linux")]
fn run_valid_with_input() {
test::run_script_and_validate(
vec![create("")],
"out = spawn --input test cat",
CommandValidation::PositiveNumber("out".to_string()),
);
}

View file

@ -1,5 +1,5 @@
```sh
count = watchdog [--max-retries value] [--interval value] -- command [arguments]*
count = watchdog [--max-retries value] [--interval value] [--input value] -- command [arguments]*
```
Executes the provided native command and arguments.<br>
@ -11,6 +11,7 @@ In case of an invalid command, the watchdog will not reattempt the invocation an
* --max-retries - Positive value of max retries (excluding the first invocation). value <= 0 for unlimited retries. Default is unlimited.
* --interval - The amount in milliseconds between retries. 0 for no waiting between invocations. Default is no wait.
* --input - Optional content to be sent to the child process input stream.
* The command to execute (preceded by a **--** separator).
* The command arguments.

View file

@ -1,3 +1,4 @@
use crate::utils::exec::ExecInput;
use crate::utils::{exec, pckg};
use duckscript::types::command::{Command, CommandResult};
use std::thread;
@ -11,6 +12,7 @@ enum LookingFor {
Flag,
MaxRetries,
Interval,
Input,
}
#[derive(Clone)]
@ -41,6 +43,7 @@ impl Command for CommandImpl {
} else {
let mut max_retries: isize = -1;
let mut interval: u64 = 0;
let mut input = ExecInput::None;
let mut command_start_index = 0;
let mut index = 0;
@ -56,6 +59,7 @@ impl Command for CommandImpl {
}
"--max-retries" => looking_for = LookingFor::MaxRetries,
"--interval" => looking_for = LookingFor::Interval,
"--input" => looking_for = LookingFor::Input,
_ => {
return CommandResult::Error(
format!("Unexpected argument: {} found", argument).to_string(),
@ -94,6 +98,10 @@ impl Command for CommandImpl {
looking_for = LookingFor::Flag;
}
LookingFor::Input => {
input = ExecInput::Text(argument.to_string());
looking_for = LookingFor::Flag;
}
}
}
@ -106,7 +114,7 @@ impl Command for CommandImpl {
loop {
attempt = attempt + 1;
match exec::exec(&arguments, false, false, command_start_index) {
match exec::exec(&arguments, false, input.clone(), command_start_index) {
Ok(_) => (),
Err(error) => return CommandResult::Error(error),
}

View file

@ -39,6 +39,16 @@ fn run_with_retries() {
);
}
#[test]
#[cfg(target_os = "linux")]
fn run_with_input() {
test::run_script_and_validate(
vec![create("")],
"out = watchdog --max-retries 0 --interval 0 --input test -- cat",
CommandValidation::Match("out".to_string(), "1".to_string()),
);
}
#[test]
fn run_error_code_with_output() {
test::run_script_and_error(vec![create("")], "out = watchdog badcommand", "out");

View file

@ -1,18 +1,26 @@
use std::io::Write;
use std::process::{Child, Command, Stdio};
#[cfg(test)]
#[path = "./exec_test.rs"]
mod exec_test;
#[derive(Debug, Clone)]
pub(crate) enum ExecInput {
None,
External,
Text(String),
}
pub(crate) fn exec(
arguments: &Vec<String>,
print_output: bool,
allow_input: bool,
input: ExecInput,
start_index: usize,
) -> Result<(String, String, i32), String> {
let mut command = create_command(arguments, print_output, false, allow_input, start_index)?;
let child = spawn(arguments, print_output, false, input, start_index)?;
match command.output() {
match child.wait_with_output() {
Ok(ref output) => {
let stdout = String::from_utf8_lossy(&output.stdout).into_owned();
let stderr = String::from_utf8_lossy(&output.stderr).into_owned();
@ -36,13 +44,32 @@ pub(crate) fn exec(
pub(crate) fn spawn(
arguments: &Vec<String>,
print_output: bool,
allow_input: bool,
output_blocking: bool,
input: ExecInput,
start_index: usize,
) -> Result<Child, String> {
let mut command = create_command(arguments, print_output, true, allow_input, start_index)?;
let mut command = create_command(
arguments,
print_output,
output_blocking,
&input,
start_index,
)?;
match command.spawn() {
Ok(child) => Ok(child),
Ok(mut child) => match input {
ExecInput::Text(value) => match child.stdin.as_mut() {
Some(stdin) => match stdin.write_all(value.as_bytes()) {
Ok(_) => {
drop(stdin);
Ok(child)
}
Err(error) => Err(error.to_string()),
},
None => Err("Unable to write input to process".to_string()),
},
_ => Ok(child),
},
Err(error) => Err(error.to_string()),
}
}
@ -51,7 +78,7 @@ fn create_command(
arguments: &Vec<String>,
print_output: bool,
output_blocking: bool,
allow_input: bool,
input: &ExecInput,
start_index: usize,
) -> Result<Command, String> {
if arguments.len() <= start_index {
@ -64,16 +91,18 @@ fn create_command(
command.arg(argument);
}
if allow_input {
command.stdin(Stdio::inherit());
} else {
command.stdin(Stdio::null());
}
match input {
ExecInput::None => command.stdin(Stdio::null()),
ExecInput::External => command.stdin(Stdio::inherit()),
ExecInput::Text(_) => command.stdin(Stdio::piped()),
};
if print_output {
command.stdout(Stdio::inherit()).stderr(Stdio::inherit());
} else if output_blocking {
command.stdout(Stdio::null()).stderr(Stdio::null());
} else {
command.stdout(Stdio::piped()).stderr(Stdio::piped());
}
Ok(command)

View file

@ -10,7 +10,7 @@ fn exec_valid() {
"world".to_string(),
],
false,
false,
ExecInput::None,
1,
)
.unwrap();
@ -30,9 +30,25 @@ fn exec_error() {
"world".to_string(),
],
false,
false,
ExecInput::None,
0,
);
assert!(result.is_err());
}
#[test]
#[cfg(target_os = "linux")]
fn exec_with_input() {
let (stdout, stderr, code) = exec(
&vec!["cat".to_string()],
false,
ExecInput::Text("1 2 3".to_string()),
0,
)
.unwrap();
assert_eq!(code, 0);
assert_eq!(stdout.trim(), "1 2 3");
assert!(stderr.trim().is_empty());
}

View file

@ -11,6 +11,20 @@ fn test_echo_with_output
assert_eq ${exit_code} 0
end
fn test_echo_with_input
if not is_windows
output = exec --input "1 2 3" cat
stdout = trim ${output.stdout}
stderr = trim ${output.stderr}
exit_code = set ${output.code}
assert_eq ${stdout} "1 2 3"
assert_eq ${stderr} ""
assert_eq ${exit_code} 0
end
end
fn test_echo_without_output
exec echo hello world
end

View file

@ -17,13 +17,21 @@ fn test_with_retries_and_interval
assert_eq ${count} 4
end
fn test_with_input
if not is_windows
count = watchdog --max-retries 0 --input 1 -- cat
assert_eq ${count} 1
end
end
fn test_bad_command
count = watchdog --max-retries 3 --interval 10 -- badcommand
assert_eq ${count} false
end
fn test_negatived_max_retries
fn test_negative_max_retries
count = watchdog --max-retries -3 --interval 10 -- echo test
assert_eq ${count} 1