Skip to content
Merged
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
2 changes: 1 addition & 1 deletion Cargo.lock

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

2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[package]
name = "stone"
version = "1.7.0"
version = "1.7.1"
edition = "2024"
description = "A CLI for managing Avocado stones."
homepage = "https://github.com/avocado-linux/stone"
Expand Down
31 changes: 2 additions & 29 deletions src/commands/stone/provision.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,9 @@ use clap::Args;

use std::collections::HashMap;
use std::fs;
use std::io::{BufRead, BufReader};

use std::path::{Path, PathBuf};
use std::process::{Command, Stdio};
use std::process::Command;

#[derive(Args, Debug)]
pub struct ProvisionArgs {
Expand Down Expand Up @@ -796,42 +796,15 @@ fn execute_provision_script(
}
}

command.stdout(Stdio::piped());
command.stderr(Stdio::piped());

let mut child = command
.spawn()
.map_err(|e| format!("Failed to execute provision script '{provision_file}': {e}"))?;

let stdout = child.stdout.take().unwrap();
let stderr = child.stderr.take().unwrap();

let stdout_reader = BufReader::new(stdout);
let stderr_reader = BufReader::new(stderr);

// Stream stdout in real-time
let stdout_handle = std::thread::spawn(move || {
for line in stdout_reader.lines().map_while(Result::ok) {
println!("{line}");
}
});

// Stream stderr in real-time
let stderr_handle = std::thread::spawn(move || {
for line in stderr_reader.lines().map_while(Result::ok) {
eprintln!("{line}");
}
});

// Wait for the process to complete
let status = child
.wait()
.map_err(|e| format!("Failed to wait for provision script '{provision_file}': {e}"))?;

// Wait for the output threads to complete
let _ = stdout_handle.join();
let _ = stderr_handle.join();

if !status.success() {
return Err(format!(
"Provision script '{}' failed with exit code {}",
Expand Down
88 changes: 88 additions & 0 deletions tests/commands/stone/provision/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -369,6 +369,94 @@ VENDOR_NAME="Avocado Linux""#;
assert!(output_content.contains("Provision script executed"));
}

#[test]
fn test_provision_preserves_all_tty_streams_for_interactive_scripts() {
let temp_dir = TempDir::new().unwrap();
let input_path = temp_dir.path();

// Create a minimal manifest with provision field
let manifest_content = r#"{
"runtime": {
"platform": "test-platform",
"architecture": "noarch",
"provision": "provision.sh"
},
"storage_devices": {
"test_device": {
"out": "test.img",
"devpath": "/dev/test",
"images": {
"simple_image": "simple.img"
},
"partitions": []
}
}
}"#;

fs::write(input_path.join("manifest.json"), manifest_content).unwrap();
fs::write(input_path.join("simple.img"), "test content").unwrap();

// Create provision script that tests read builtin functionality and output streams
let provision_script = r#"#!/bin/bash
# Test stdout output - should appear in stone's stdout
echo "STDOUT: Script is running"

# Test stderr output - should appear in stone's stderr
echo "STDERR: Script error message" >&2

# Test that read builtin works and prompt appears - this was broken by mixed TTY state
echo "Testing read functionality..." > tty_test_output.txt
echo -n "Enter test value: " >&2
read response
echo "read completed with response: $response" >> tty_test_output.txt

echo "provision script completed" >> tty_test_output.txt
exit 0
"#;
fs::write(input_path.join("provision.sh"), provision_script).unwrap();

// Make the script executable (on Unix systems)
#[cfg(unix)]
{
use std::os::unix::fs::PermissionsExt;
let mut perms = fs::metadata(input_path.join("provision.sh"))
.unwrap()
.permissions();
perms.set_mode(0o755);
fs::set_permissions(input_path.join("provision.sh"), perms).unwrap();
}

// Create os-release file for AVOCADO_OS_VERSION
let os_release_content = r#"NAME="Avocado Linux"
VERSION="1.0.0"
ID=avocado
VERSION_ID="1.0.0"
VERSION_CODENAME=test
PRETTY_NAME="Avocado Linux 1.0.0"
VENDOR_NAME="Avocado Linux""#;
fs::write(input_path.join("os-release"), os_release_content).unwrap();

Command::cargo_bin("stone")
.unwrap()
.args([
"provision",
"--input-dir",
&input_path.to_string_lossy(),
"--verbose",
])
.write_stdin("test_input\n")
.assert()
.success()
.stdout(predicates::str::contains("STDOUT: Script is running"))
.stderr(predicates::str::contains("STDERR: Script error message"))
.stderr(predicates::str::contains("Enter test value: "));

// Check that read builtin works and captures input properly
assert!(input_path.join("tty_test_output.txt").exists());
let output_content = fs::read_to_string(input_path.join("tty_test_output.txt")).unwrap();
assert!(output_content.contains("read completed with response: test_input"));
}

#[test]
fn test_provision_with_failing_provision_script() {
let temp_dir = TempDir::new().unwrap();
Expand Down
Loading