feat: add background_process tool for launching long-running processes
Adds a new tool that allows launching processes (like game servers) in the background while g3 continues to operate. The process runs independently with stdout/stderr captured to a log file. Features: - Named process tracking for easy reference - Automatic log capture to logs/background_processes/ - Returns PID and log file path for use with shell tool - Automatic cleanup on agent shutdown via Drop trait Usage: Use shell tool to interact with the process: - Read logs: tail -100 <logfile> - Check status: ps -p <pid> - Stop process: kill <pid> Files: - New: crates/g3-core/src/background_process.rs - New: crates/g3-core/tests/background_process_demo_test.rs - Modified: crates/g3-core/src/lib.rs (tool definition + handler) - Modified: crates/g3-core/src/prompts.rs (documentation)
This commit is contained in:
279
crates/g3-core/src/background_process.rs
Normal file
279
crates/g3-core/src/background_process.rs
Normal file
@@ -0,0 +1,279 @@
|
|||||||
|
//! Background process management for long-running tasks like game servers.
|
||||||
|
//!
|
||||||
|
//! This module provides a way to launch processes in the background with:
|
||||||
|
//! - Automatic log capture to files (stdout/stderr combined)
|
||||||
|
//! - Named process tracking for easy reference
|
||||||
|
//! - Process lifecycle management (start, stop via shell)
|
||||||
|
//!
|
||||||
|
//! The design is intentionally minimal - only one tool (`background_process`) is exposed.
|
||||||
|
//! Users can use the regular `shell` tool to:
|
||||||
|
//! - Read logs: `cat /path/to/logs.txt` or `tail -100 /path/to/logs.txt`
|
||||||
|
//! - Stop processes: `kill <pid>` or `pkill -f <name>`
|
||||||
|
//! - Check status: `ps aux | grep <name>`
|
||||||
|
|
||||||
|
use std::collections::HashMap;
|
||||||
|
use std::fs::{self, OpenOptions};
|
||||||
|
use std::io::Write;
|
||||||
|
use std::path::PathBuf;
|
||||||
|
use std::process::{Child, Command, Stdio};
|
||||||
|
use std::sync::{Arc, Mutex};
|
||||||
|
use std::time::{SystemTime, UNIX_EPOCH};
|
||||||
|
use tracing::debug;
|
||||||
|
|
||||||
|
/// Information about a running background process
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct ProcessInfo {
|
||||||
|
/// User-provided name for the process
|
||||||
|
pub name: String,
|
||||||
|
/// The command that was executed
|
||||||
|
pub command: String,
|
||||||
|
/// Process ID
|
||||||
|
pub pid: u32,
|
||||||
|
/// Path to the log file (combined stdout/stderr)
|
||||||
|
pub log_file: PathBuf,
|
||||||
|
/// Timestamp when the process was started
|
||||||
|
pub started_at: u64,
|
||||||
|
/// Working directory where the process was started
|
||||||
|
pub working_dir: PathBuf,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Manages background processes launched by the agent
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub struct BackgroundProcessManager {
|
||||||
|
/// Map of process name -> process info
|
||||||
|
processes: Arc<Mutex<HashMap<String, ProcessInfo>>>,
|
||||||
|
/// Map of process name -> child handle (for cleanup)
|
||||||
|
children: Arc<Mutex<HashMap<String, Child>>>,
|
||||||
|
/// Directory where log files are stored
|
||||||
|
log_dir: PathBuf,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl BackgroundProcessManager {
|
||||||
|
/// Create a new background process manager
|
||||||
|
pub fn new(log_dir: PathBuf) -> Self {
|
||||||
|
// Ensure log directory exists
|
||||||
|
if let Err(e) = fs::create_dir_all(&log_dir) {
|
||||||
|
debug!("Failed to create log directory {:?}: {}", log_dir, e);
|
||||||
|
}
|
||||||
|
|
||||||
|
Self {
|
||||||
|
processes: Arc::new(Mutex::new(HashMap::new())),
|
||||||
|
children: Arc::new(Mutex::new(HashMap::new())),
|
||||||
|
log_dir,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Start a new background process
|
||||||
|
///
|
||||||
|
/// # Arguments
|
||||||
|
/// * `name` - A unique name for this process (used to reference it later)
|
||||||
|
/// * `command` - The shell command to execute
|
||||||
|
/// * `working_dir` - The directory to run the command in
|
||||||
|
///
|
||||||
|
/// # Returns
|
||||||
|
/// ProcessInfo on success, or an error message
|
||||||
|
pub fn start(
|
||||||
|
&self,
|
||||||
|
name: &str,
|
||||||
|
command: &str,
|
||||||
|
working_dir: &PathBuf,
|
||||||
|
) -> Result<ProcessInfo, String> {
|
||||||
|
// Check if a process with this name already exists
|
||||||
|
{
|
||||||
|
let processes = self.processes.lock().unwrap();
|
||||||
|
if processes.contains_key(name) {
|
||||||
|
return Err(format!(
|
||||||
|
"A process named '{}' is already running. Stop it first or use a different name.",
|
||||||
|
name
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create log file with timestamp
|
||||||
|
let timestamp = SystemTime::now()
|
||||||
|
.duration_since(UNIX_EPOCH)
|
||||||
|
.unwrap()
|
||||||
|
.as_secs();
|
||||||
|
let log_filename = format!("{}_{}.log", name, timestamp);
|
||||||
|
let log_file = self.log_dir.join(&log_filename);
|
||||||
|
|
||||||
|
// Open log file for writing
|
||||||
|
let log_handle = OpenOptions::new()
|
||||||
|
.create(true)
|
||||||
|
.write(true)
|
||||||
|
.truncate(true)
|
||||||
|
.open(&log_file)
|
||||||
|
.map_err(|e| format!("Failed to create log file: {}", e))?;
|
||||||
|
|
||||||
|
// Write header to log file
|
||||||
|
{
|
||||||
|
let mut file = &log_handle;
|
||||||
|
writeln!(file, "=== Background Process Log ===").ok();
|
||||||
|
writeln!(file, "Name: {}", name).ok();
|
||||||
|
writeln!(file, "Command: {}", command).ok();
|
||||||
|
writeln!(file, "Working Directory: {:?}", working_dir).ok();
|
||||||
|
writeln!(file, "Started: {}", timestamp).ok();
|
||||||
|
writeln!(file, "================================\n").ok();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clone the file handle for stderr
|
||||||
|
let log_handle_stderr = log_handle
|
||||||
|
.try_clone()
|
||||||
|
.map_err(|e| format!("Failed to clone log file handle: {}", e))?;
|
||||||
|
|
||||||
|
// Spawn the process
|
||||||
|
let child = Command::new("bash")
|
||||||
|
.arg("-c")
|
||||||
|
.arg(command)
|
||||||
|
.current_dir(working_dir)
|
||||||
|
.stdout(Stdio::from(log_handle))
|
||||||
|
.stderr(Stdio::from(log_handle_stderr))
|
||||||
|
.spawn()
|
||||||
|
.map_err(|e| format!("Failed to spawn process: {}", e))?;
|
||||||
|
|
||||||
|
let pid = child.id();
|
||||||
|
|
||||||
|
let info = ProcessInfo {
|
||||||
|
name: name.to_string(),
|
||||||
|
command: command.to_string(),
|
||||||
|
pid,
|
||||||
|
log_file: log_file.clone(),
|
||||||
|
started_at: timestamp,
|
||||||
|
working_dir: working_dir.clone(),
|
||||||
|
};
|
||||||
|
|
||||||
|
// Store process info and child handle
|
||||||
|
{
|
||||||
|
let mut processes = self.processes.lock().unwrap();
|
||||||
|
processes.insert(name.to_string(), info.clone());
|
||||||
|
}
|
||||||
|
{
|
||||||
|
let mut children = self.children.lock().unwrap();
|
||||||
|
children.insert(name.to_string(), child);
|
||||||
|
}
|
||||||
|
|
||||||
|
debug!(
|
||||||
|
"Started background process '{}' (PID: {}) with logs at {:?}",
|
||||||
|
name, pid, log_file
|
||||||
|
);
|
||||||
|
|
||||||
|
Ok(info)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// List all tracked background processes
|
||||||
|
pub fn list(&self) -> Vec<ProcessInfo> {
|
||||||
|
let processes = self.processes.lock().unwrap();
|
||||||
|
processes.values().cloned().collect()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get info about a specific process by name
|
||||||
|
pub fn get(&self, name: &str) -> Option<ProcessInfo> {
|
||||||
|
let processes = self.processes.lock().unwrap();
|
||||||
|
processes.get(name).cloned()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Check if a process is still running
|
||||||
|
pub fn is_running(&self, name: &str) -> bool {
|
||||||
|
let mut children = self.children.lock().unwrap();
|
||||||
|
if let Some(child) = children.get_mut(name) {
|
||||||
|
match child.try_wait() {
|
||||||
|
Ok(Some(_)) => false, // Process has exited
|
||||||
|
Ok(None) => true, // Still running
|
||||||
|
Err(_) => false, // Error checking, assume not running
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Remove a process from tracking (call after it has been killed)
|
||||||
|
pub fn remove(&self, name: &str) -> Option<ProcessInfo> {
|
||||||
|
let info = {
|
||||||
|
let mut processes = self.processes.lock().unwrap();
|
||||||
|
processes.remove(name)
|
||||||
|
};
|
||||||
|
{
|
||||||
|
let mut children = self.children.lock().unwrap();
|
||||||
|
children.remove(name);
|
||||||
|
}
|
||||||
|
info
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Clean up all processes on shutdown
|
||||||
|
pub fn cleanup(&self) {
|
||||||
|
let mut children = self.children.lock().unwrap();
|
||||||
|
for (name, mut child) in children.drain() {
|
||||||
|
debug!("Cleaning up background process '{}'", name);
|
||||||
|
let _ = child.kill();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Drop for BackgroundProcessManager {
|
||||||
|
fn drop(&mut self) {
|
||||||
|
self.cleanup();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
use std::thread;
|
||||||
|
use std::time::Duration;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_start_and_list_process() {
|
||||||
|
let temp_dir = std::env::temp_dir().join("g3_bg_test");
|
||||||
|
let _ = fs::remove_dir_all(&temp_dir);
|
||||||
|
fs::create_dir_all(&temp_dir).unwrap();
|
||||||
|
|
||||||
|
let manager = BackgroundProcessManager::new(temp_dir.clone());
|
||||||
|
|
||||||
|
// Start a simple process that sleeps
|
||||||
|
let result = manager.start("test_sleep", "sleep 10", &temp_dir);
|
||||||
|
assert!(result.is_ok());
|
||||||
|
|
||||||
|
let info = result.unwrap();
|
||||||
|
assert_eq!(info.name, "test_sleep");
|
||||||
|
assert!(info.pid > 0);
|
||||||
|
assert!(info.log_file.exists());
|
||||||
|
|
||||||
|
// List should contain our process
|
||||||
|
let list = manager.list();
|
||||||
|
assert_eq!(list.len(), 1);
|
||||||
|
assert_eq!(list[0].name, "test_sleep");
|
||||||
|
|
||||||
|
// Should be running
|
||||||
|
assert!(manager.is_running("test_sleep"));
|
||||||
|
|
||||||
|
// Cleanup
|
||||||
|
manager.cleanup();
|
||||||
|
|
||||||
|
// Give it a moment to clean up
|
||||||
|
thread::sleep(Duration::from_millis(100));
|
||||||
|
|
||||||
|
let _ = fs::remove_dir_all(&temp_dir);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_duplicate_name_rejected() {
|
||||||
|
let temp_dir = std::env::temp_dir().join("g3_bg_test_dup");
|
||||||
|
let _ = fs::remove_dir_all(&temp_dir);
|
||||||
|
fs::create_dir_all(&temp_dir).unwrap();
|
||||||
|
|
||||||
|
let manager = BackgroundProcessManager::new(temp_dir.clone());
|
||||||
|
|
||||||
|
// Start first process
|
||||||
|
let result1 = manager.start("my_game", "sleep 10", &temp_dir);
|
||||||
|
assert!(result1.is_ok());
|
||||||
|
|
||||||
|
// Try to start another with same name
|
||||||
|
let result2 = manager.start("my_game", "sleep 5", &temp_dir);
|
||||||
|
assert!(result2.is_err());
|
||||||
|
assert!(result2.unwrap_err().contains("already running"));
|
||||||
|
|
||||||
|
// Cleanup
|
||||||
|
manager.cleanup();
|
||||||
|
let _ = fs::remove_dir_all(&temp_dir);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,3 +1,4 @@
|
|||||||
|
pub mod background_process;
|
||||||
pub mod code_search;
|
pub mod code_search;
|
||||||
pub mod error_handling;
|
pub mod error_handling;
|
||||||
pub mod feedback_extraction;
|
pub mod feedback_extraction;
|
||||||
@@ -865,6 +866,7 @@ pub struct Agent<W: UiWriter> {
|
|||||||
requirements_sha: Option<String>,
|
requirements_sha: Option<String>,
|
||||||
/// Working directory for tool execution (set by --codebase-fast-start)
|
/// Working directory for tool execution (set by --codebase-fast-start)
|
||||||
working_dir: Option<String>,
|
working_dir: Option<String>,
|
||||||
|
background_process_manager: std::sync::Arc<background_process::BackgroundProcessManager>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<W: UiWriter> Agent<W> {
|
impl<W: UiWriter> Agent<W> {
|
||||||
@@ -1178,6 +1180,10 @@ impl<W: UiWriter> Agent<W> {
|
|||||||
tool_call_count: 0,
|
tool_call_count: 0,
|
||||||
requirements_sha: None,
|
requirements_sha: None,
|
||||||
working_dir: None,
|
working_dir: None,
|
||||||
|
background_process_manager: std::sync::Arc::new(
|
||||||
|
background_process::BackgroundProcessManager::new(
|
||||||
|
paths::get_logs_dir().join("background_processes")
|
||||||
|
)),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2796,6 +2802,28 @@ impl<W: UiWriter> Agent<W> {
|
|||||||
"required": ["command"]
|
"required": ["command"]
|
||||||
}),
|
}),
|
||||||
},
|
},
|
||||||
|
Tool {
|
||||||
|
name: "background_process".to_string(),
|
||||||
|
description: "Launch a long-running process in the background (e.g., game servers, dev servers). The process runs independently and logs are captured to a file. Use the regular 'shell' tool to read logs (cat/tail), check status (ps), or stop the process (kill). Returns the PID and log file path.".to_string(),
|
||||||
|
input_schema: json!({
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"name": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "A unique name for this process (e.g., 'game_server', 'my_app'). Used to identify the process and its log file."
|
||||||
|
},
|
||||||
|
"command": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "The shell command to execute in the background"
|
||||||
|
},
|
||||||
|
"working_dir": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "Optional working directory. Defaults to current directory if not specified."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": ["name", "command"]
|
||||||
|
}),
|
||||||
|
},
|
||||||
Tool {
|
Tool {
|
||||||
name: "read_file".to_string(),
|
name: "read_file".to_string(),
|
||||||
description: "Read the contents of a file. For image files (png, jpg, jpeg, gif, bmp, tiff, webp), automatically extracts text using OCR. For text files, optionally read a specific character range.".to_string(),
|
description: "Read the contents of a file. For image files (png, jpg, jpeg, gif, bmp, tiff, webp), automatically extracts text using OCR. For text files, optionally read a specific character range.".to_string(),
|
||||||
@@ -4793,6 +4821,50 @@ impl<W: UiWriter> Agent<W> {
|
|||||||
Ok("❌ Missing command argument".to_string())
|
Ok("❌ Missing command argument".to_string())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
"background_process" => {
|
||||||
|
debug!("Processing background_process tool call");
|
||||||
|
let name = tool_call.args.get("name")
|
||||||
|
.and_then(|v| v.as_str());
|
||||||
|
let name = match name {
|
||||||
|
Some(n) => n,
|
||||||
|
None => return Ok("❌ Missing 'name' argument".to_string()),
|
||||||
|
};
|
||||||
|
|
||||||
|
let command = tool_call.args.get("command")
|
||||||
|
.and_then(|v| v.as_str());
|
||||||
|
let command = match command {
|
||||||
|
Some(c) => c,
|
||||||
|
None => return Ok("❌ Missing 'command' argument".to_string()),
|
||||||
|
};
|
||||||
|
|
||||||
|
// Use provided working_dir, or fall back to agent's working_dir, or current dir
|
||||||
|
let work_dir = tool_call.args.get("working_dir")
|
||||||
|
.and_then(|v| v.as_str())
|
||||||
|
.map(|s| std::path::PathBuf::from(shellexpand::tilde(s).as_ref()))
|
||||||
|
.or_else(|| working_dir.map(std::path::PathBuf::from))
|
||||||
|
.unwrap_or_else(|| std::env::current_dir().unwrap_or_default());
|
||||||
|
|
||||||
|
match self.background_process_manager.start(name, command, &work_dir) {
|
||||||
|
Ok(info) => {
|
||||||
|
Ok(format!(
|
||||||
|
"✅ Background process '{}' started\n\n\
|
||||||
|
**PID:** {}\n\
|
||||||
|
**Log file:** {}\n\
|
||||||
|
**Working dir:** {}\n\n\
|
||||||
|
To interact with this process, use the shell tool:\n\
|
||||||
|
- View logs: `tail -100 {}`\n\
|
||||||
|
- Follow logs: `tail -f {}` (blocks until Ctrl+C)\n\
|
||||||
|
- Check status: `ps -p {}`\n\
|
||||||
|
- Stop process: `kill {}`",
|
||||||
|
info.name, info.pid,
|
||||||
|
info.log_file.display(), info.working_dir.display(),
|
||||||
|
info.log_file.display(), info.log_file.display(),
|
||||||
|
info.pid, info.pid
|
||||||
|
))
|
||||||
|
}
|
||||||
|
Err(e) => Ok(format!("❌ Failed to start background process: {}", e)),
|
||||||
|
}
|
||||||
|
}
|
||||||
"read_file" => {
|
"read_file" => {
|
||||||
debug!("Processing read_file tool call");
|
debug!("Processing read_file tool call");
|
||||||
if let Some(file_path) = tool_call.args.get("file_path") {
|
if let Some(file_path) = tool_call.args.get("file_path") {
|
||||||
|
|||||||
@@ -248,6 +248,12 @@ Short description for providers without native calling specs:
|
|||||||
- Format: {\"tool\": \"shell\", \"args\": {\"command\": \"your_command_here\"}
|
- Format: {\"tool\": \"shell\", \"args\": {\"command\": \"your_command_here\"}
|
||||||
- Example: {\"tool\": \"shell\", \"args\": {\"command\": \"ls ~/Downloads\"}
|
- Example: {\"tool\": \"shell\", \"args\": {\"command\": \"ls ~/Downloads\"}
|
||||||
|
|
||||||
|
- **background_process**: Launch a long-running process in the background (e.g., game servers, dev servers)
|
||||||
|
- Format: {\"tool\": \"background_process\", \"args\": {\"name\": \"unique_name\", \"command\": \"your_command\"}}
|
||||||
|
- Example: {\"tool\": \"background_process\", \"args\": {\"name\": \"game_server\", \"command\": \"./run.sh\"}}
|
||||||
|
- Returns PID and log file path. Use shell tool to read logs (`tail -100 <logfile>`), check status (`ps -p <pid>`), or stop (`kill <pid>`)
|
||||||
|
- Note: Process runs independently; logs are captured to a file for later inspection
|
||||||
|
|
||||||
- **read_file**: Read the contents of a file (supports partial reads via start/end)
|
- **read_file**: Read the contents of a file (supports partial reads via start/end)
|
||||||
- Format: {\"tool\": \"read_file\", \"args\": {\"file_path\": \"path/to/file\", \"start\": 0, \"end\": 100}
|
- Format: {\"tool\": \"read_file\", \"args\": {\"file_path\": \"path/to/file\", \"start\": 0, \"end\": 100}
|
||||||
- Example: {\"tool\": \"read_file\", \"args\": {\"file_path\": \"src/main.rs\"}
|
- Example: {\"tool\": \"read_file\", \"args\": {\"file_path\": \"src/main.rs\"}
|
||||||
|
|||||||
68
crates/g3-core/tests/background_process_demo_test.rs
Normal file
68
crates/g3-core/tests/background_process_demo_test.rs
Normal file
@@ -0,0 +1,68 @@
|
|||||||
|
use g3_core::background_process::BackgroundProcessManager;
|
||||||
|
use std::path::PathBuf;
|
||||||
|
use std::thread;
|
||||||
|
use std::time::Duration;
|
||||||
|
use std::fs;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn demo_background_process_with_script() {
|
||||||
|
// Create temp directories
|
||||||
|
let test_dir = std::env::temp_dir().join("g3_bg_demo");
|
||||||
|
let _ = fs::remove_dir_all(&test_dir);
|
||||||
|
fs::create_dir_all(&test_dir).unwrap();
|
||||||
|
|
||||||
|
// Create a test script
|
||||||
|
let script_path = test_dir.join("test.sh");
|
||||||
|
fs::write(&script_path, r#"#!/bin/bash
|
||||||
|
echo "Starting..."
|
||||||
|
for i in 1 2 3; do
|
||||||
|
echo "Tick $i"
|
||||||
|
sleep 0.5
|
||||||
|
done
|
||||||
|
echo "Done!"
|
||||||
|
"#).unwrap();
|
||||||
|
|
||||||
|
// Make executable
|
||||||
|
#[cfg(unix)]
|
||||||
|
{
|
||||||
|
use std::os::unix::fs::PermissionsExt;
|
||||||
|
fs::set_permissions(&script_path, fs::Permissions::from_mode(0o755)).unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
|
let log_dir = test_dir.join("logs");
|
||||||
|
let manager = BackgroundProcessManager::new(log_dir);
|
||||||
|
|
||||||
|
println!("\n=== Background Process Demo ===");
|
||||||
|
|
||||||
|
match manager.start("demo", "./test.sh", &test_dir) {
|
||||||
|
Ok(info) => {
|
||||||
|
println!("✅ Started '{}' with PID {}", info.name, info.pid);
|
||||||
|
println!(" Log file: {:?}", info.log_file);
|
||||||
|
|
||||||
|
// Wait for script to produce some output
|
||||||
|
thread::sleep(Duration::from_millis(800));
|
||||||
|
|
||||||
|
// Read logs
|
||||||
|
let logs = fs::read_to_string(&info.log_file).unwrap_or_default();
|
||||||
|
println!("\n📜 Logs so far:\n{}", logs);
|
||||||
|
|
||||||
|
// Should still be running
|
||||||
|
assert!(manager.is_running("demo"), "Process should still be running");
|
||||||
|
println!("🔍 Process is running: true");
|
||||||
|
|
||||||
|
// Wait for completion
|
||||||
|
thread::sleep(Duration::from_secs(2));
|
||||||
|
|
||||||
|
// Read final logs
|
||||||
|
let final_logs = fs::read_to_string(&info.log_file).unwrap_or_default();
|
||||||
|
println!("\n📜 Final logs:\n{}", final_logs);
|
||||||
|
|
||||||
|
assert!(final_logs.contains("Done!"), "Should have completed");
|
||||||
|
}
|
||||||
|
Err(e) => panic!("Failed to start: {}", e),
|
||||||
|
}
|
||||||
|
|
||||||
|
// Cleanup
|
||||||
|
let _ = fs::remove_dir_all(&test_dir);
|
||||||
|
println!("\n✅ Demo complete!");
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user