Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions e2e/tasks/test_task_info
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ assert_json "mise tasks info lint --json" "$(
"dir": null,
"hide": false,
"raw": false,
"interactive": false,
"sources": [],
"outputs": [],
"shell": null,
Expand Down
6 changes: 6 additions & 0 deletions e2e/tasks/test_task_ls
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,7 @@ assert_json "mise tasks ls --json" "$(
"hide": false,
"global": false,
"raw": false,
"interactive": false,
"sources": [],
"outputs": [],
"shell": null,
Expand Down Expand Up @@ -98,6 +99,7 @@ assert_json "mise tasks ls --json" "$(
"hide": false,
"global": false,
"raw": false,
"interactive": false,
"sources": [],
"outputs": [],
"shell": null,
Expand All @@ -123,6 +125,7 @@ assert_json "mise tasks ls --json" "$(
"hide": false,
"global": false,
"raw": false,
"interactive": false,
"sources": [],
"outputs": [],
"shell": null,
Expand All @@ -148,6 +151,7 @@ assert_json "mise tasks ls --json" "$(
"hide": false,
"global": false,
"raw": false,
"interactive": false,
"sources": [],
"outputs": [],
"shell": null,
Expand Down Expand Up @@ -175,6 +179,7 @@ assert_json "mise tasks ls --json" "$(
"hide": false,
"global": false,
"raw": false,
"interactive": false,
"sources": [],
"outputs": [],
"shell": null,
Expand Down Expand Up @@ -205,6 +210,7 @@ assert_json "mise tasks ls --json" "$(
"hide": false,
"global": false,
"raw": false,
"interactive": false,
"sources": [],
"outputs": [],
"shell": null,
Expand Down
10 changes: 10 additions & 0 deletions schema/mise-task.json
Original file line number Diff line number Diff line change
Expand Up @@ -237,6 +237,11 @@
"description": "directly connect task to stdin/stdout/stderr",
"type": "boolean"
},
"interactive": {
"default": false,
"description": "run task with direct TTY/stdin/stdout/stderr passthrough",
"type": "boolean"
},
"run": {
"oneOf": [
{
Expand Down Expand Up @@ -890,6 +895,11 @@
"description": "directly connect task to stdin/stdout/stderr",
"type": "boolean"
},
"interactive": {
"default": false,
"description": "run task with direct TTY/stdin/stdout/stderr passthrough",
"type": "boolean"
},
"run": {
"oneOf": [
{
Expand Down
15 changes: 15 additions & 0 deletions schema/mise.json
Original file line number Diff line number Diff line change
Expand Up @@ -1846,6 +1846,11 @@
"description": "directly connect task to stdin/stdout/stderr",
"type": "boolean"
},
"interactive": {
"default": false,
"description": "run task with direct TTY/stdin/stdout/stderr passthrough",
"type": "boolean"
},
"run": {
"oneOf": [
{
Expand Down Expand Up @@ -2439,6 +2444,11 @@
"description": "directly connect task to stdin/stdout/stderr",
"type": "boolean"
},
"interactive": {
"default": false,
"description": "run task with direct TTY/stdin/stdout/stderr passthrough",
"type": "boolean"
},
"run": {
"oneOf": [
{
Expand Down Expand Up @@ -2684,6 +2694,11 @@
"description": "directly connect task to stdin/stdout/stderr",
"type": "boolean"
},
"interactive": {
"default": false,
"description": "run task with direct TTY/stdin/stdout/stderr passthrough",
"type": "boolean"
},
"run": {
"oneOf": [
{
Expand Down
1 change: 1 addition & 0 deletions src/cli/mcp.rs
Original file line number Diff line number Diff line change
Expand Up @@ -326,6 +326,7 @@ impl ServerHandler for MiseServer {
"dir": task.dir.clone(),
"hide": task.hide,
"raw": task.raw,
"interactive": task.interactive,
"sources": task.sources.clone(),
"outputs": task.outputs.clone(),
"shell": task.shell.clone(),
Expand Down
10 changes: 9 additions & 1 deletion src/cli/run.rs
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ use crate::task::{Deps, Task};
use crate::toolset::{InstallOptions, ToolsetBuilder};
use crate::ui::{ctrlc, info, style};
use clap::{CommandFactory, ValueHint};
use eyre::{Result, bail, eyre};
use eyre::{Result, bail, ensure, eyre};
use itertools::Itertools;
use tokio::sync::Mutex;

Expand Down Expand Up @@ -658,6 +658,14 @@ impl Run {
fn validate_task(&self, task: &Task) -> Result<()> {
use crate::file;
use crate::ui;
if task.interactive {
use std::io::IsTerminal;
ensure!(
std::io::stdin().is_terminal(),
"task '{}' requires an interactive TTY on stdin to run",
task.name
Comment thread
mackwic marked this conversation as resolved.
);
Comment thread
mackwic marked this conversation as resolved.
}
if let Some(path) = &task.file
&& path.exists()
&& !file::is_executable(path)
Expand Down
1 change: 1 addition & 0 deletions src/cli/tasks/info.rs
Original file line number Diff line number Diff line change
Expand Up @@ -137,6 +137,7 @@ impl TasksInfo {
"dir": task.dir,
"hide": task.hide,
"raw": task.raw,
"interactive": task.interactive,
"sources": task.sources,
"outputs": task.outputs,
"shell": task.shell,
Expand Down
1 change: 1 addition & 0 deletions src/cli/tasks/ls.rs
Original file line number Diff line number Diff line change
Expand Up @@ -191,6 +191,7 @@ impl TasksLs {
"hide": task.hide,
"global": task.global,
"raw": task.raw,
"interactive": task.interactive,
"sources": task.sources,
"outputs": task.outputs,
"shell": task.shell,
Expand Down
54 changes: 52 additions & 2 deletions src/cmd.rs
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@ use eyre::{Context, bail};
use signal_hook::consts::{SIGHUP, SIGINT, SIGQUIT, SIGTERM, SIGUSR1, SIGUSR2};
#[cfg(not(any(test, target_os = "windows")))]
use signal_hook::iterator::Signals;
#[cfg(unix)]
use std::os::unix::process::CommandExt;
use std::sync::LazyLock as Lazy;

use crate::config::Settings;
Expand Down Expand Up @@ -106,6 +108,7 @@ pub struct CmdLineRunner<'a> {
stdin: Option<String>,
redactor: Redactor,
raw: bool,
interactive: bool,
pass_signals: bool,
on_stdout: Option<Box<dyn Fn(String) + Send + 'a>>,
on_stderr: Option<Box<dyn Fn(String) + Send + 'a>>,
Expand Down Expand Up @@ -243,6 +246,7 @@ impl<'a> CmdLineRunner<'a> {
stdin: None,
redactor: Default::default(),
raw: false,
interactive: false,
pass_signals: false,
on_stdout: None,
on_stderr: None,
Expand Down Expand Up @@ -395,6 +399,11 @@ impl<'a> CmdLineRunner<'a> {
self
}

pub fn interactive(mut self, interactive: bool) -> Self {
self.interactive = interactive;
self
}

pub fn with_pass_signals(&mut self) -> &mut Self {
self.pass_signals = true;
self
Expand Down Expand Up @@ -541,12 +550,53 @@ impl<'a> CmdLineRunner<'a> {
}

fn execute_raw(mut self) -> Result<()> {
#[cfg(unix)]
let tty_before = unsafe {
let stdin = std::os::fd::BorrowedFd::borrow_raw(0);
if std::io::IsTerminal::is_terminal(&stdin) {
nix::sys::termios::tcgetattr(stdin).ok()
} else {
None
}
};
#[cfg(unix)]
unsafe {
let interactive = self.interactive;
self.cmd.pre_exec(move || {
// Interactive tasks in non-tty contexts run in a separate process group.
let stdin = std::os::fd::BorrowedFd::borrow_raw(0);
if interactive && !std::io::IsTerminal::is_terminal(&stdin) {
let _ = nix::unistd::setpgid(
nix::unistd::Pid::from_raw(0),
nix::unistd::Pid::from_raw(0),
);
}
Ok(())
});
Comment thread
mackwic marked this conversation as resolved.
}
let mut cp = self.spawn_with_etxtbsy_retry()?;
let timeout_guard = self.timeout.map(|t| TimeoutGuard::new(t, cp.id()));
let status = cp.wait()?;
let id = cp.id();
// Edge case: interactive/raw commands must be tracked so global Ctrl-C can kill them.
RUNNING_PIDS.lock().unwrap().insert(id);
let timeout_guard = self.timeout.map(|t| TimeoutGuard::new(t, id));
let status = cp.wait();
#[cfg(unix)]
if let Some(termios) = tty_before {
// Edge case: interactive tasks may mutate tty state (`stty -echo`); always restore.
unsafe {
let stdin = std::os::fd::BorrowedFd::borrow_raw(0);
let _ = nix::sys::termios::tcsetattr(
stdin,
nix::sys::termios::SetArg::TCSANOW,
&termios,
);
}
}
RUNNING_PIDS.lock().unwrap().remove(&id);
if let Some(g) = &timeout_guard {
g.cancel();
}
let status = status?;
if !status.success() {
if let Some(duration) = timeout_guard.as_ref().and_then(|g| g.timed_out()) {
bail!("timed out after {duration:?}");
Expand Down
30 changes: 30 additions & 0 deletions src/config/config_file/mise_toml.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2329,6 +2329,36 @@ mod tests {
parse(toml).env_entries().unwrap().into_iter().join("\n")
}

#[test]
fn test_tasks_interactive_deserialize_true_and_default_false() {
let cf = parse(formatdoc! {r#"
[tasks.interactive-task]
run = "echo interactive"
interactive = true

[tasks.normal-task]
run = "echo normal"
"#});

assert!(cf.tasks.0["interactive-task"].interactive);
assert!(!cf.tasks.0["normal-task"].interactive);
}

#[test]
fn test_tasks_interactive_rejects_non_bool() {
let path = CWD.as_ref().unwrap().join(".test.mise.toml");
let toml = formatdoc! {r#"
[tasks.bad]
run = "echo bad"
interactive = "true"
"#};

file::write(&path, toml).unwrap();
let result = MiseToml::from_file(&path);
file::remove_file(&path).unwrap();
assert!(result.is_err());
}

#[tokio::test]
async fn test_table_syntax_preserves_registry_defaults() {
// Test for #8039: table syntax like `ansible = { version = "latest" }`
Expand Down
6 changes: 6 additions & 0 deletions src/task/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -246,6 +246,8 @@ pub struct Task {
#[serde(default)]
pub raw: bool,
#[serde(default)]
pub interactive: bool,
#[serde(default)]
pub sources: Vec<String>,
#[serde(default)]
pub outputs: TaskOutputs,
Expand Down Expand Up @@ -380,6 +382,7 @@ impl Task {
task.dir = p.parse_str("dir");
task.hide = !file::is_executable(path) || p.parse_bool("hide").unwrap_or_default();
task.raw = p.parse_bool("raw").unwrap_or_default();
task.interactive = p.parse_bool("interactive").unwrap_or_default();
task.sources = p.parse_array("sources").unwrap_or_default();
task.outputs = p.get_raw("outputs").map(|to| to.into()).unwrap_or_default();
task.file = Some(path.to_path_buf());
Expand Down Expand Up @@ -1245,6 +1248,7 @@ impl Default for Task {
hide: false,
global: false,
raw: false,
interactive: false,
sources: vec![],
outputs: Default::default(),
raw_outputs: Default::default(),
Expand Down Expand Up @@ -2268,6 +2272,7 @@ echo "hello world"
#MISE env={TEST_VAR="value"}
#MISE dir="/some/dir"
#MISE hide=true
#MISE interactive=true
#MISE raw=true
#MISE sources=["src1.txt", "src2.txt"]
#MISE outputs=["out1.txt"]
Expand All @@ -2293,6 +2298,7 @@ echo "test"
assert_eq!(task.wait_for.len(), 1);
assert_eq!(task.dir, Some("/some/dir".to_string()));
assert_eq!(task.hide, true);
assert_eq!(task.interactive, true);
assert_eq!(task.raw, true);
assert_eq!(task.sources, vec!["src1.txt", "src2.txt"]);
assert_eq!(task.shell, Some("bash -c".to_string()));
Expand Down
Loading
Loading