Skip to content

Commit

Permalink
add: make shell command configurable
Browse files Browse the repository at this point in the history
  • Loading branch information
Nukesor committed Aug 18, 2023
1 parent 4cb610d commit ecf7bf0
Show file tree
Hide file tree
Showing 15 changed files with 123 additions and 36 deletions.
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,12 @@ All notable changes to this project will be documented in this file.

The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [3.3.0] - unreleased

### 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
Expand Down
1 change: 1 addition & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ snap = "1.1"
strum = "0.25"
strum_macros = "0.25"
tokio = { version = "1.29", features = ["rt-multi-thread", "time", "io-std"] }
handlebars = "4.3"

# Dev dependencies
anyhow = "1"
Expand Down
2 changes: 1 addition & 1 deletion pueue/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand All @@ -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 }
Expand Down
11 changes: 10 additions & 1 deletion pueue/src/client/client.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
Expand Down Expand Up @@ -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(),
Expand Down
14 changes: 9 additions & 5 deletions pueue/src/client/commands/edit.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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::*;
Expand All @@ -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,
Expand All @@ -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,
Expand Down Expand Up @@ -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>,
Expand All @@ -111,21 +115,21 @@ 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.
if edit_path {
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
Expand All @@ -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.")?;
Expand All @@ -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.")?;

Expand Down
3 changes: 3 additions & 0 deletions pueue/src/client/commands/restart.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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::task::{Task, TaskResult, TaskStatus};

use crate::client::commands::edit::edit_task_properties;
Expand All @@ -15,6 +16,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>,
Expand Down Expand Up @@ -80,6 +82,7 @@ pub async fn restart(

// Edit any properties, if requested.
let edited_props = edit_task_properties(
settings,
&task.command,
&task.path,
&task.label,
Expand Down
2 changes: 1 addition & 1 deletion pueue/src/daemon/task_handler/callback.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down
2 changes: 1 addition & 1 deletion pueue/src/daemon/task_handler/spawn_task.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
1 change: 1 addition & 0 deletions pueue/tests/helper/fixtures.rs
Original file line number Diff line number Diff line change
Expand Up @@ -168,6 +168,7 @@ pub fn daemon_base_setup() -> Result<(Settings, TempDir)> {
pause_all_on_failure: false,
callback: None,
callback_log_lines: 15,
shell_command: None,
groups: None,
};

Expand Down
1 change: 1 addition & 0 deletions pueue_lib/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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 }
Expand Down
40 changes: 39 additions & 1 deletion pueue_lib/src/process_helper/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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 {

Check failure on line 84 in pueue_lib/src/process_helper/mod.rs

View workflow job for this annotation

GitHub Actions / Lint on windows-latest for x86_64-pc-windows-msvc

`std::process::Command` is not an iterator

Check failure on line 84 in pueue_lib/src/process_helper/mod.rs

View workflow job for this annotation

GitHub Actions / Lint on windows-latest for x86_64-pc-windows-msvc

the size for values of type `str` cannot be known at compilation time

Check failure on line 84 in pueue_lib/src/process_helper/mod.rs

View workflow job for this annotation

GitHub Actions / Lint on windows-latest for x86_64-pc-windows-msvc

the size for values of type `str` cannot be known at compilation time

Check failure on line 84 in pueue_lib/src/process_helper/mod.rs

View workflow job for this annotation

GitHub Actions / Lint on windows-latest for x86_64-pc-windows-msvc

the size for values of type `str` cannot be known at compilation time

Check failure on line 84 in pueue_lib/src/process_helper/mod.rs

View workflow job for this annotation

GitHub Actions / Test on windows-latest for x86_64-pc-windows-msvc

`Command` is not an iterator

Check failure on line 84 in pueue_lib/src/process_helper/mod.rs

View workflow job for this annotation

GitHub Actions / Test on windows-latest for x86_64-pc-windows-msvc

the size for values of type `str` cannot be known at compilation time

Check failure on line 84 in pueue_lib/src/process_helper/mod.rs

View workflow job for this annotation

GitHub Actions / Test on windows-latest for x86_64-pc-windows-msvc

the size for values of type `str` cannot be known at compilation time

Check failure on line 84 in pueue_lib/src/process_helper/mod.rs

View workflow job for this annotation

GitHub Actions / Test on windows-latest for x86_64-pc-windows-msvc

the size for values of type `str` cannot be known at compilation time
let compiled_part = handlebars
.render_template(&part, &parameters)
.expect(&format!(

Check failure on line 87 in pueue_lib/src/process_helper/mod.rs

View workflow job for this annotation

GitHub Actions / Lint on ubuntu-latest for arm-unknown-linux-musleabihf

use of `expect` followed by a function call

Check failure on line 87 in pueue_lib/src/process_helper/mod.rs

View workflow job for this annotation

GitHub Actions / Lint on ubuntu-latest for armv7-unknown-linux-musleabihf

use of `expect` followed by a function call

Check failure on line 87 in pueue_lib/src/process_helper/mod.rs

View workflow job for this annotation

GitHub Actions / Lint on ubuntu-latest for aarch64-unknown-linux-musl

use of `expect` followed by a function call

Check failure on line 87 in pueue_lib/src/process_helper/mod.rs

View workflow job for this annotation

GitHub Actions / Lint on ubuntu-latest for x86_64-unknown-linux-gnu

use of `expect` followed by a function call

Check failure on line 87 in pueue_lib/src/process_helper/mod.rs

View workflow job for this annotation

GitHub Actions / Lint on macos-latest for x86_64-apple-darwin

use of `expect` followed by a function call

Check failure on line 87 in pueue_lib/src/process_helper/mod.rs

View workflow job for this annotation

GitHub Actions / Lint on macos-latest for aarch64-apple-darwin

use of `expect` followed by a function call
"Failed to render shell command for template: {part} and parameters: {parameters:?}"

Check failure on line 88 in pueue_lib/src/process_helper/mod.rs

View workflow job for this annotation

GitHub Actions / Lint on windows-latest for x86_64-pc-windows-msvc

the size for values of type `str` cannot be known at compilation time

Check failure on line 88 in pueue_lib/src/process_helper/mod.rs

View workflow job for this annotation

GitHub Actions / Test on windows-latest for x86_64-pc-windows-msvc

the size for values of type `str` cannot be known at compilation time
));

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
}
45 changes: 27 additions & 18 deletions pueue_lib/src/process_helper/unix.rs
Original file line number Diff line number Diff line change
@@ -1,17 +1,18 @@
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.
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;

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()];
};

command
shell_command.clone()
}

/// Send a signal to one of Pueue's child process group handle.
Expand Down Expand Up @@ -39,17 +40,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> {
Expand All @@ -75,7 +77,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");

Expand All @@ -87,9 +90,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));
Expand Down Expand Up @@ -120,9 +125,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));
Expand Down Expand Up @@ -153,9 +160,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));
Expand Down
21 changes: 13 additions & 8 deletions pueue_lib/src/process_helper/windows.rs
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,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,
Expand All @@ -26,14 +28,17 @@ 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}"
));

command
pub fn get_shell_command(settings: &Settings) -> Command {
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![

Check failure on line 34 in pueue_lib/src/process_helper/windows.rs

View workflow job for this annotation

GitHub Actions / Lint on windows-latest for x86_64-pc-windows-msvc

mismatched types

Check failure on line 34 in pueue_lib/src/process_helper/windows.rs

View workflow job for this annotation

GitHub Actions / Test on windows-latest for x86_64-pc-windows-msvc

mismatched types
"powershell".into(),
"-c".into(),
"[Console]::OutputEncoding = [Text.UTF8Encoding]::UTF8; {{ pueue_ command_string }}".into()
];
};

shell_command.clone()

Check failure on line 41 in pueue_lib/src/process_helper/windows.rs

View workflow job for this annotation

GitHub Actions / Lint on windows-latest for x86_64-pc-windows-msvc

mismatched types

Check failure on line 41 in pueue_lib/src/process_helper/windows.rs

View workflow job for this annotation

GitHub Actions / Test on windows-latest for x86_64-pc-windows-msvc

mismatched types
}

/// Send a signal to a windows process.
Expand Down
Loading

0 comments on commit ecf7bf0

Please sign in to comment.