mirror of
https://github.com/nukesor/pueue
synced 2024-10-01 13:34:07 +00:00
add: make shell command configurable
This commit is contained in:
parent
887c409104
commit
22b7fc0c1d
|
@ -15,6 +15,10 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
|||
- QoL improvement: Don't pause groups if there're no queued tasks. [#452](https://github.com/Nukesor/pueue/issues/452)
|
||||
Auto-pausing of groups was only done to prevent the unwanted execution of other tasks, but this isn't necessary, if there're no queued tasks.
|
||||
|
||||
### Added
|
||||
|
||||
- Allow configuration of the shell command that executes task commands. [#454](https://github.com/Nukesor/pueue/issues/454)
|
||||
|
||||
## [3.2.0] - 2023-06-13
|
||||
|
||||
### Added
|
||||
|
|
1
Cargo.lock
generated
1
Cargo.lock
generated
|
@ -1213,6 +1213,7 @@ dependencies = [
|
|||
"chrono",
|
||||
"command-group",
|
||||
"dirs",
|
||||
"handlebars",
|
||||
"libproc",
|
||||
"log",
|
||||
"portpicker",
|
||||
|
|
|
@ -25,6 +25,7 @@ snap = "1.1"
|
|||
strum = "0.25"
|
||||
strum_macros = "0.25"
|
||||
tokio = { version = "1.32", features = ["rt-multi-thread", "time", "io-std"] }
|
||||
handlebars = "4.3"
|
||||
|
||||
# Dev dependencies
|
||||
anyhow = "1"
|
||||
|
|
|
@ -24,7 +24,6 @@ clap_complete = "4.3"
|
|||
comfy-table = "7"
|
||||
crossterm = { version = "0.26", default-features = false }
|
||||
ctrlc = { version = "3", features = ["termination"] }
|
||||
handlebars = "4.3"
|
||||
pest = "2.7"
|
||||
pest_derive = "2.7"
|
||||
shell-escape = "0.1"
|
||||
|
@ -33,6 +32,7 @@ tempfile = "3"
|
|||
|
||||
chrono = { workspace = true }
|
||||
command-group = { workspace = true }
|
||||
handlebars = { workspace = true }
|
||||
log = { workspace = true }
|
||||
serde = { workspace = true }
|
||||
serde_json = { workspace = true }
|
||||
|
|
|
@ -199,7 +199,15 @@ impl Client {
|
|||
path,
|
||||
label,
|
||||
} => {
|
||||
let message = edit(&mut self.stream, *task_id, *command, *path, *label).await?;
|
||||
let message = edit(
|
||||
&mut self.stream,
|
||||
&self.settings,
|
||||
*task_id,
|
||||
*command,
|
||||
*path,
|
||||
*label,
|
||||
)
|
||||
.await?;
|
||||
self.handle_response(message)?;
|
||||
Ok(true)
|
||||
}
|
||||
|
@ -231,6 +239,7 @@ impl Client {
|
|||
(self.settings.client.restart_in_place || *in_place) && !*not_in_place;
|
||||
restart(
|
||||
&mut self.stream,
|
||||
&self.settings,
|
||||
task_ids.clone(),
|
||||
*all_failed,
|
||||
failed_in_group.clone(),
|
||||
|
|
|
@ -3,6 +3,7 @@ use std::io::{Read, Seek, Write};
|
|||
use std::path::{Path, PathBuf};
|
||||
|
||||
use anyhow::{bail, Context, Result};
|
||||
use pueue_lib::settings::Settings;
|
||||
use tempfile::NamedTempFile;
|
||||
|
||||
use pueue_lib::network::message::*;
|
||||
|
@ -18,6 +19,7 @@ use pueue_lib::process_helper::compile_shell_command;
|
|||
/// Upon exiting the text editor, the line will then be read and sent to the server
|
||||
pub async fn edit(
|
||||
stream: &mut GenericStream,
|
||||
settings: &Settings,
|
||||
task_id: usize,
|
||||
edit_command: bool,
|
||||
edit_path: bool,
|
||||
|
@ -41,6 +43,7 @@ pub async fn edit(
|
|||
|
||||
// Edit all requested properties.
|
||||
let edit_result = edit_task_properties(
|
||||
settings,
|
||||
&init_response.command,
|
||||
&init_response.path,
|
||||
&init_response.label,
|
||||
|
@ -100,6 +103,7 @@ pub struct EditedProperties {
|
|||
///
|
||||
/// The returned values are: `(command, path, label)`
|
||||
pub fn edit_task_properties(
|
||||
settings: &Settings,
|
||||
original_command: &str,
|
||||
original_path: &Path,
|
||||
original_label: &Option<String>,
|
||||
|
@ -111,7 +115,7 @@ pub fn edit_task_properties(
|
|||
|
||||
// Update the command if requested.
|
||||
if edit_command {
|
||||
props.command = Some(edit_line(original_command)?);
|
||||
props.command = Some(edit_line(settings, original_command)?);
|
||||
};
|
||||
|
||||
// Update the path if requested.
|
||||
|
@ -119,13 +123,13 @@ pub fn edit_task_properties(
|
|||
let str_path = original_path
|
||||
.to_str()
|
||||
.context("Failed to convert task path to string")?;
|
||||
let changed_path = edit_line(str_path)?;
|
||||
let changed_path = edit_line(settings, str_path)?;
|
||||
props.path = Some(PathBuf::from(changed_path));
|
||||
}
|
||||
|
||||
// Update the label if requested.
|
||||
if edit_label {
|
||||
let edited_label = edit_line(&original_label.clone().unwrap_or_default())?;
|
||||
let edited_label = edit_line(settings, &original_label.clone().unwrap_or_default())?;
|
||||
|
||||
// If the user deletes the label in their editor, an empty string will be returned.
|
||||
// This is an indicator that the task should no longer have a label, in which case we
|
||||
|
@ -143,7 +147,7 @@ pub fn edit_task_properties(
|
|||
/// This function enables the user to edit a task's details.
|
||||
/// Save any string to a temporary file, which is opened in the specified `$EDITOR`.
|
||||
/// As soon as the editor is closed, read the file content and return the line.
|
||||
fn edit_line(line: &str) -> Result<String> {
|
||||
fn edit_line(settings: &Settings, line: &str) -> Result<String> {
|
||||
// Create a temporary file with the command so we can edit it with the editor.
|
||||
let mut file = NamedTempFile::new().expect("Failed to create a temporary file");
|
||||
writeln!(file, "{line}").context("Failed to write to temporary file.")?;
|
||||
|
@ -158,7 +162,7 @@ fn edit_line(line: &str) -> Result<String> {
|
|||
// We escape the file path for good measure, but it shouldn't be necessary.
|
||||
let path = shell_escape::escape(file.path().to_string_lossy());
|
||||
let editor_command = format!("{editor} {path}");
|
||||
let status = compile_shell_command(&editor_command)
|
||||
let status = compile_shell_command(settings, &editor_command)
|
||||
.status()
|
||||
.context("Editor command did somehow fail. Aborting.")?;
|
||||
|
||||
|
|
|
@ -2,6 +2,7 @@ use anyhow::{bail, Result};
|
|||
|
||||
use pueue_lib::network::message::*;
|
||||
use pueue_lib::network::protocol::*;
|
||||
use pueue_lib::settings::Settings;
|
||||
use pueue_lib::state::FilteredTasks;
|
||||
use pueue_lib::task::{Task, TaskResult, TaskStatus};
|
||||
|
||||
|
@ -16,6 +17,7 @@ use crate::client::commands::get_state;
|
|||
#[allow(clippy::too_many_arguments)]
|
||||
pub async fn restart(
|
||||
stream: &mut GenericStream,
|
||||
settings: &Settings,
|
||||
task_ids: Vec<usize>,
|
||||
all_failed: bool,
|
||||
failed_in_group: Option<String>,
|
||||
|
@ -85,6 +87,7 @@ pub async fn restart(
|
|||
|
||||
// Edit any properties, if requested.
|
||||
let edited_props = edit_task_properties(
|
||||
settings,
|
||||
&task.command,
|
||||
&task.path,
|
||||
&task.label,
|
||||
|
|
|
@ -20,7 +20,7 @@ impl TaskHandler {
|
|||
}
|
||||
};
|
||||
|
||||
let mut command = compile_shell_command(&callback_command);
|
||||
let mut command = compile_shell_command(&self.settings, &callback_command);
|
||||
|
||||
// Spawn the callback subprocess and log if it fails.
|
||||
let spawn_result = command.spawn();
|
||||
|
|
|
@ -135,7 +135,7 @@ impl TaskHandler {
|
|||
};
|
||||
|
||||
// Build the shell command that should be executed.
|
||||
let mut command = compile_shell_command(&command);
|
||||
let mut command = compile_shell_command(&self.settings, &command);
|
||||
|
||||
// Determine the worker's id depending on the current group.
|
||||
// Inject that info into the environment.
|
||||
|
|
|
@ -32,6 +32,7 @@ thiserror = "1.0"
|
|||
tokio-rustls = { version = "0.24", default-features = false }
|
||||
|
||||
command-group = { workspace = true }
|
||||
handlebars = { workspace = true }
|
||||
log = { workspace = true }
|
||||
serde = { workspace = true }
|
||||
serde_json = { workspace = true }
|
||||
|
|
|
@ -4,7 +4,9 @@
|
|||
//! each supported platform.
|
||||
//! Depending on the target, the respective platform is read and loaded into this scope.
|
||||
|
||||
use crate::network::message::Signal as InternalSignal;
|
||||
use std::{collections::HashMap, process::Command};
|
||||
|
||||
use crate::{network::message::Signal as InternalSignal, settings::Settings};
|
||||
|
||||
// Unix specific process handling
|
||||
// Shared between Linux and Apple
|
||||
|
@ -63,3 +65,39 @@ impl From<InternalSignal> for Signal {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Take a platform specific shell command and insert the actual task command via templating.
|
||||
pub fn compile_shell_command(settings: &Settings, command: &str) -> Command {
|
||||
let shell_command = get_shell_command(settings);
|
||||
|
||||
let mut handlebars = handlebars::Handlebars::new();
|
||||
handlebars.set_strict_mode(true);
|
||||
handlebars.register_escape_fn(handlebars::no_escape);
|
||||
|
||||
// Make the command available to the template engine.
|
||||
let mut parameters = HashMap::new();
|
||||
parameters.insert("pueue_command_string", command);
|
||||
|
||||
// We allow users to provide their own shell command.
|
||||
// They should use the `{{ pueue_command_string }}` placeholder.
|
||||
let mut compiled_command = Vec::new();
|
||||
for part in shell_command {
|
||||
let compiled_part = handlebars
|
||||
.render_template(&part, ¶meters)
|
||||
.unwrap_or_else(|_| {
|
||||
panic!("Failed to render shell command for template: {part} and parameters: {parameters:?}")
|
||||
});
|
||||
|
||||
compiled_command.push(compiled_part);
|
||||
}
|
||||
|
||||
let executable = compiled_command.remove(0);
|
||||
|
||||
// Chain two `powershell` commands, one that sets the output encoding to utf8 and then the user provided one.
|
||||
let mut command = Command::new(executable);
|
||||
for arg in compiled_command {
|
||||
command.arg(&arg);
|
||||
}
|
||||
|
||||
command
|
||||
}
|
||||
|
|
|
@ -1,5 +1,3 @@
|
|||
use std::process::Command;
|
||||
|
||||
// We allow anyhow in here, as this is a module that'll be strictly used internally.
|
||||
// As soon as it's obvious that this is code is intended to be exposed to library users, we have to
|
||||
// go ahead and replace any `anyhow` usage by proper error handling via our own Error type.
|
||||
|
@ -7,11 +5,18 @@ use anyhow::Result;
|
|||
use command_group::{GroupChild, Signal, UnixChildExt};
|
||||
use log::info;
|
||||
|
||||
pub fn compile_shell_command(command_string: &str) -> Command {
|
||||
let mut command = Command::new("sh");
|
||||
command.arg("-c").arg(command_string);
|
||||
use crate::settings::Settings;
|
||||
|
||||
command
|
||||
pub fn get_shell_command(settings: &Settings) -> Vec<String> {
|
||||
let Some(ref shell_command) = settings.daemon.shell_command else {
|
||||
return vec![
|
||||
"sh".into(),
|
||||
"-c".into(),
|
||||
"{{ pueue_command_string }}".into(),
|
||||
];
|
||||
};
|
||||
|
||||
shell_command.clone()
|
||||
}
|
||||
|
||||
/// Send a signal to one of Pueue's child process group handle.
|
||||
|
@ -39,17 +44,18 @@ pub fn kill_child(task_id: usize, child: &mut GroupChild) -> std::io::Result<()>
|
|||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use log::warn;
|
||||
use std::process::Command;
|
||||
use std::thread::sleep;
|
||||
use std::time::Duration;
|
||||
|
||||
use anyhow::Result;
|
||||
use command_group::CommandGroup;
|
||||
use libproc::processes::{pids_by_type, ProcFilter};
|
||||
use log::warn;
|
||||
use pretty_assertions::assert_eq;
|
||||
|
||||
use super::*;
|
||||
use crate::process_helper::process_exists;
|
||||
use crate::process_helper::{compile_shell_command, process_exists};
|
||||
|
||||
/// List all PIDs that are part of the process group
|
||||
pub fn get_process_group_pids(pgrp: u32) -> Vec<u32> {
|
||||
|
@ -75,7 +81,8 @@ mod tests {
|
|||
|
||||
#[test]
|
||||
fn test_spawn_command() {
|
||||
let mut child = compile_shell_command("sleep 0.1")
|
||||
let settings = Settings::default();
|
||||
let mut child = compile_shell_command(&settings, "sleep 0.1")
|
||||
.group_spawn()
|
||||
.expect("Failed to spawn echo");
|
||||
|
||||
|
@ -87,9 +94,11 @@ mod tests {
|
|||
#[test]
|
||||
/// Ensure a `sh -c` command will be properly killed without detached processes.
|
||||
fn test_shell_command_is_killed() -> Result<()> {
|
||||
let mut child = compile_shell_command("sleep 60 & sleep 60 && echo 'this is a test'")
|
||||
.group_spawn()
|
||||
.expect("Failed to spawn echo");
|
||||
let settings = Settings::default();
|
||||
let mut child =
|
||||
compile_shell_command(&settings, "sleep 60 & sleep 60 && echo 'this is a test'")
|
||||
.group_spawn()
|
||||
.expect("Failed to spawn echo");
|
||||
let pid = child.id();
|
||||
// Sleep a little to give everything a chance to spawn.
|
||||
sleep(Duration::from_millis(500));
|
||||
|
@ -120,9 +129,11 @@ mod tests {
|
|||
/// Ensure a `sh -c` command will be properly killed without detached processes when using unix
|
||||
/// signals directly.
|
||||
fn test_shell_command_is_killed_with_signal() -> Result<()> {
|
||||
let mut child = compile_shell_command("sleep 60 & sleep 60 && echo 'this is a test'")
|
||||
.group_spawn()
|
||||
.expect("Failed to spawn echo");
|
||||
let settings = Settings::default();
|
||||
let mut child =
|
||||
compile_shell_command(&settings, "sleep 60 & sleep 60 && echo 'this is a test'")
|
||||
.group_spawn()
|
||||
.expect("Failed to spawn echo");
|
||||
let pid = child.id();
|
||||
// Sleep a little to give everything a chance to spawn.
|
||||
sleep(Duration::from_millis(500));
|
||||
|
@ -153,9 +164,11 @@ mod tests {
|
|||
/// Ensure that a `sh -c` process with a child process that has children of its own
|
||||
/// will properly kill all processes and their children's children without detached processes.
|
||||
fn test_shell_command_children_are_killed() -> Result<()> {
|
||||
let mut child = compile_shell_command("bash -c 'sleep 60 && sleep 60' && sleep 60")
|
||||
.group_spawn()
|
||||
.expect("Failed to spawn echo");
|
||||
let settings = Settings::default();
|
||||
let mut child =
|
||||
compile_shell_command(&settings, "bash -c 'sleep 60 && sleep 60' && sleep 60")
|
||||
.group_spawn()
|
||||
.expect("Failed to spawn echo");
|
||||
let pid = child.id();
|
||||
// Sleep a little to give everything a chance to spawn.
|
||||
sleep(Duration::from_millis(500));
|
||||
|
|
|
@ -1,5 +1,3 @@
|
|||
use std::process::Command;
|
||||
|
||||
// We allow anyhow in here, as this is a module that'll be strictly used internally.
|
||||
// As soon as it's obvious that this is code is intended to be exposed to library users, we have to
|
||||
// go ahead and replace any `anyhow` usage by proper error handling via our own Error type.
|
||||
|
@ -17,6 +15,8 @@ use winapi::um::tlhelp32::{
|
|||
};
|
||||
use winapi::um::winnt::THREAD_SUSPEND_RESUME;
|
||||
|
||||
use crate::settings::Settings;
|
||||
|
||||
/// Shim signal enum for windows.
|
||||
pub enum Signal {
|
||||
SIGINT,
|
||||
|
@ -26,14 +26,18 @@ pub enum Signal {
|
|||
SIGSTOP,
|
||||
}
|
||||
|
||||
pub fn compile_shell_command(command_string: &str) -> Command {
|
||||
// Chain two `powershell` commands, one that sets the output encoding to utf8 and then the user provided one.
|
||||
let mut command = Command::new("powershell");
|
||||
command.arg("-c").arg(format!(
|
||||
"[Console]::OutputEncoding = [Text.UTF8Encoding]::UTF8; {command_string}"
|
||||
));
|
||||
pub fn get_shell_command(settings: &Settings) -> Vec<String> {
|
||||
let Some(ref shell_command) = settings.daemon.shell_command else {
|
||||
// Chain two `powershell` commands, one that sets the output encoding to utf8 and then the user provided one.
|
||||
return vec![
|
||||
"powershell".into(),
|
||||
"-c".into(),
|
||||
"[Console]::OutputEncoding = [Text.UTF8Encoding]::UTF8; {{ pueue_command_string }}"
|
||||
.into(),
|
||||
];
|
||||
};
|
||||
|
||||
command
|
||||
shell_command.clone()
|
||||
}
|
||||
|
||||
/// Send a signal to a windows process.
|
||||
|
@ -251,12 +255,14 @@ pub fn process_exists(pid: u32) -> bool {
|
|||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use std::process::Command;
|
||||
use std::thread::sleep;
|
||||
use std::time::Duration;
|
||||
|
||||
use command_group::CommandGroup;
|
||||
|
||||
use super::*;
|
||||
use crate::process_helper::compile_shell_command;
|
||||
|
||||
/// Assert that certain process id no longer exists
|
||||
fn process_is_gone(pid: u32) -> bool {
|
||||
|
@ -292,7 +298,8 @@ mod test {
|
|||
|
||||
#[test]
|
||||
fn test_spawn_command() {
|
||||
let mut child = compile_shell_command("sleep 0.1")
|
||||
let settings = Settings::default();
|
||||
let mut child = compile_shell_command(&settings, "sleep 0.1")
|
||||
.group_spawn()
|
||||
.expect("Failed to spawn echo");
|
||||
|
||||
|
@ -308,9 +315,11 @@ mod test {
|
|||
/// This test is ignored for now, as it is flaky from time to time.
|
||||
/// See https://github.com/Nukesor/pueue/issues/315
|
||||
fn test_shell_command_is_killed() -> Result<()> {
|
||||
let mut child = compile_shell_command("sleep 60; sleep 60; echo 'this is a test'")
|
||||
.group_spawn()
|
||||
.expect("Failed to spawn echo");
|
||||
let settings = Settings::default();
|
||||
let mut child =
|
||||
compile_shell_command(&settings, "sleep 60; sleep 60; echo 'this is a test'")
|
||||
.group_spawn()
|
||||
.expect("Failed to spawn echo");
|
||||
let pid = child.id();
|
||||
|
||||
// Get all processes, so we can make sure they no longer exist afterwards.
|
||||
|
@ -338,9 +347,11 @@ mod test {
|
|||
/// Ensure that a `powershell -c` process with a child process that has children of it's own
|
||||
/// will properly kill all processes and their children's children without detached processes.
|
||||
fn test_shell_command_children_are_killed() -> Result<()> {
|
||||
let mut child = compile_shell_command("powershell -c 'sleep 60; sleep 60'; sleep 60")
|
||||
.group_spawn()
|
||||
.expect("Failed to spawn echo");
|
||||
let settings = Settings::default();
|
||||
let mut child =
|
||||
compile_shell_command(&settings, "powershell -c 'sleep 60; sleep 60'; sleep 60")
|
||||
.group_spawn()
|
||||
.expect("Failed to spawn echo");
|
||||
let pid = child.id();
|
||||
// Get all processes, so we can make sure they no longer exist afterwards.
|
||||
let process_ids = assert_process_ids(pid, 2, 5000)?;
|
||||
|
|
|
@ -119,6 +119,15 @@ pub struct Daemon {
|
|||
/// The amount of log lines from stdout/stderr that are passed to the callback command.
|
||||
#[serde(default = "default_callback_log_lines")]
|
||||
pub callback_log_lines: usize,
|
||||
/// The command that should be used for task and callback execution.
|
||||
/// The following are the only officially supported modi for Pueue.
|
||||
///
|
||||
/// Unix default:
|
||||
/// `vec!["sh", "-c", "{{ pueue_command_string }}"]`.
|
||||
///
|
||||
/// Windows default:
|
||||
/// `vec!["powershell", "-c", "[Console]::OutputEncoding = [Text.UTF8Encoding]::UTF8; {{ pueue_command_string }}"]`
|
||||
pub shell_command: Option<Vec<String>>,
|
||||
}
|
||||
|
||||
impl Default for Shared {
|
||||
|
@ -165,6 +174,7 @@ impl Default for Daemon {
|
|||
pause_all_on_failure: false,
|
||||
callback: None,
|
||||
callback_log_lines: default_callback_log_lines(),
|
||||
shell_command: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue