Skip to content
Open
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
981 changes: 638 additions & 343 deletions Cargo.lock

Large diffs are not rendered by default.

4 changes: 2 additions & 2 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -20,13 +20,13 @@ walkdir = "2.3"
notify = "8"
chrono = "0.4"
terminal_size = "0.4.1"
log = "0.4"
env_logger = "0.11.6"
async-trait = "0.1.86"
validator = { version = "0.16", features = ["derive"] }
schemars = { version = "0.8", features = ["derive"] }
tempfile = "3.8"
tracing = "0.1"
tracing-subscriber = { version = "0.3", features = ["env-filter", "json"] }
tracing-appender = "0.2"

[dev-dependencies]

Expand Down
8 changes: 8 additions & 0 deletions src/cli.rs
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,14 @@ pub struct Args {
#[arg(long)]
pub debug: bool,

/// Enable verbose logging
#[arg(short, long)]
pub verbose: bool,

/// Enable quiet mode (only errors)
#[arg(short, long)]
pub quiet: bool,

/// Dry-run mode - show what would be executed without running commands
#[arg(long)]
pub dry_run: bool,
Expand Down
38 changes: 19 additions & 19 deletions src/graph.rs
Original file line number Diff line number Diff line change
Expand Up @@ -94,55 +94,55 @@ impl Graph {
}

pub fn print_debug(&self) {
log::debug!("\nGraph Debug Info:");
log::debug!("Nodes: {}", self.nodes.len());
tracing::debug!("\nGraph Debug Info:");
tracing::debug!("Nodes: {}", self.nodes.len());
for node in &self.nodes {
match &node.kind {
NodeKind::Task(task) => {
log::debug!(" Task[{}]: {}", node.id, task.name);
tracing::debug!(" Task[{}]: {}", node.id, task.name);
if let Some(desc) = &task.description {
log::debug!(" Description: {}", desc);
tracing::debug!(" Description: {}", desc);
}
if let Some(cmd) = &task.command {
log::debug!(" Command: {}", cmd);
tracing::debug!(" Command: {}", cmd);
}
if let Some(dir) = &task.working_dir {
log::debug!(" Working Dir: {}", dir);
tracing::debug!(" Working Dir: {}", dir);
}
if !task.env.is_empty() {
log::debug!(" Environment:");
tracing::debug!(" Environment:");
for (k, v) in &task.env {
log::debug!(" {}={}", k, v);
tracing::debug!(" {}={}", k, v);
}
}
}
NodeKind::Command(cmd) => {
log::debug!(" Command[{}]: {}", node.id, cmd.raw_command);
tracing::debug!(" Command[{}]: {}", node.id, cmd.raw_command);
}
NodeKind::ConcurrentGroup(group) => {
log::debug!(" ConcurrentGroup[{}]:", node.id);
log::debug!(" Children: {:?}", group.child_nodes);
log::debug!(" Fail Fast: {}", group.fail_fast);
tracing::debug!(" ConcurrentGroup[{}]:", node.id);
tracing::debug!(" Children: {:?}", group.child_nodes);
tracing::debug!(" Fail Fast: {}", group.fail_fast);
if let Some(max) = group.max_concurrent {
log::debug!(" Max Concurrent: {}", max);
tracing::debug!(" Max Concurrent: {}", max);
}
if let Some(timeout) = group.timeout_secs {
log::debug!(" Timeout: {}s", timeout);
tracing::debug!(" Timeout: {}s", timeout);
}
}
}
if !node.metadata.is_empty() {
log::debug!(" Metadata:");
tracing::debug!(" Metadata:");
for (k, v) in &node.metadata {
log::debug!(" {}={}", k, v);
tracing::debug!(" {}={}", k, v);
}
}
}
log::debug!("\nEdges: {}", self.edges.len());
tracing::debug!("\nEdges: {}", self.edges.len());
for edge in &self.edges {
log::debug!(" {} -> {}", edge.from, edge.to);
tracing::debug!(" {} -> {}", edge.from, edge.to);
}
log::debug!("");
tracing::debug!("");
}

pub fn detect_cycle(&self) -> Option<Vec<NodeId>> {
Expand Down
45 changes: 31 additions & 14 deletions src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,35 +12,52 @@ use bodo::{
BodoError,
};
use clap::Parser;
use log::{error, LevelFilter};
use std::{collections::HashMap, process::exit};
use tracing_subscriber::{layer::SubscriberExt, util::SubscriberInitExt};

fn main() {
let args = Args::parse();

if args.debug {
// Set log level based on flags
if args.verbose || args.debug {
std::env::set_var("RUST_LOG", "bodo=debug");
} else if args.quiet {
std::env::set_var("RUST_LOG", "bodo=error");
} else if std::env::var("RUST_LOG").is_err() {
std::env::set_var("RUST_LOG", "bodo=info");
}
env_logger::Builder::from_default_env()
.filter_module(
"bodo",
if args.debug {
LevelFilter::Debug
} else {
LevelFilter::Info
},
)

// Set up log file rotation
let file_appender = tracing_appender::rolling::daily("logs", "bodo.log");
let (non_blocking, guard) = tracing_appender::non_blocking(file_appender);

// Create layers
let console_layer = tracing_subscriber::fmt::layer()
.with_target(false)
.with_thread_ids(false)
.with_thread_names(false);

let file_layer = tracing_subscriber::fmt::layer()
.with_writer(non_blocking)
.json()
.with_target(false)
.with_thread_ids(false)
.with_thread_names(false);

// Initialize tracing subscriber with both layers
tracing_subscriber::registry()
.with(tracing_subscriber::EnvFilter::from_default_env())
.with(console_layer)
.with(file_layer)
.init();

if let Err(e) = run(args) {
error!("Error: {}", e);
if let Err(e) = run(args, guard) {
tracing::error!("Error: {}", e);
exit(1);
}
}

fn run(args: Args) -> Result<(), BodoError> {
fn run(args: Args, _guard: tracing_appender::non_blocking::WorkerGuard) -> Result<(), BodoError> {
Comment on lines +54 to +60
Copy link

Copilot AI Sep 19, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

WorkerGuard is moved into run and dropped before the error log in main, so the file appender may not flush the final error message to the log file. Keep the guard alive in main for the entire program and do not pass it into run.

Copilot uses AI. Check for mistakes.

Comment on lines +54 to +60
Copy link

Copilot AI Sep 19, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

"Suggested concrete fix for WorkerGuard lifetime: keep the guard in main and restore the original run signature. For example:

Copilot uses AI. Check for mistakes.

let watch_mode = if std::env::var("BODO_NO_WATCH").is_ok() {
false
} else if args.auto_watch {
Expand Down
3 changes: 3 additions & 0 deletions src/manager.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ use crate::{
Result,
};
use std::collections::HashMap;
use tracing::instrument;

pub struct GraphManager {
pub config: BodoConfig,
Expand All @@ -33,6 +34,7 @@ impl GraphManager {
self.plugin_manager.register(plugin);
}

#[instrument(skip(self, config))]
pub fn build_graph(&mut self, config: BodoConfig) -> Result<&Graph> {
self.config = config.clone();
let mut loader = ScriptLoader::new();
Expand Down Expand Up @@ -95,6 +97,7 @@ impl GraphManager {
Ok(())
}

#[instrument(skip(self, config))]
pub fn run_plugins(&mut self, config: Option<PluginConfig>) -> Result<()> {
self.plugin_manager.sort_plugins();
self.plugin_manager.run_lifecycle(&mut self.graph, config)?;
Expand Down
3 changes: 3 additions & 0 deletions src/plugins/execution_plugin.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ use crate::{
process::ProcessManager,
sandbox::Sandbox,
};
use tracing::instrument;

pub struct ExecutionPlugin {
pub task_name: Option<String>,
Expand Down Expand Up @@ -317,6 +318,7 @@ impl Plugin for ExecutionPlugin {
Ok(())
}

#[instrument(skip(self, graph))]
fn on_after_run(&mut self, graph: &mut Graph) -> Result<()> {
let task_name = if let Some(name) = &self.task_name {
name.clone()
Expand Down Expand Up @@ -498,6 +500,7 @@ impl ExecutionPlugin {
let mut visited = std::collections::HashSet::new();
let mut pm = ProcessManager::new(true);

#[instrument(skip(graph, pm, visited, expand_env_vars_fn, get_prefix_settings_fn))]
#[allow(clippy::type_complexity)]
fn run_node(
node_id: usize,
Expand Down
11 changes: 5 additions & 6 deletions src/plugins/print_list_plugin.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
use colored::Colorize;
use log::info;
use std::{any::Any, cmp::Ordering, collections::HashMap};

use crate::{
Expand Down Expand Up @@ -115,24 +114,24 @@ impl Plugin for PrintListPlugin {
for line in lines {
if line.is_heading {
if printed_first_heading {
info!("");
tracing::info!("");
}
info!("{}", line.left_col);
tracing::info!("{}", line.left_col);
printed_first_heading = true;
continue;
}
if let Some(desc) = line.desc {
info!(
tracing::info!(
" {:<width$} {}",
line.left_col,
desc.dimmed(),
width = padded_width
);
} else {
info!(" {}", line.left_col);
tracing::info!(" {}", line.left_col);
}
}
info!("");
tracing::info!("");
Ok(())
}
}
6 changes: 3 additions & 3 deletions src/plugins/timeout_plugin.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
use humantime::parse_duration;
use log::debug;
use std::any::Any;

use crate::{
Expand Down Expand Up @@ -45,9 +44,10 @@ impl Plugin for TimeoutPlugin {
let seconds = Self::parse_timeout(timeout_str)?;
node.metadata
.insert("timeout_seconds".to_string(), seconds.to_string());
debug!(
tracing::debug!(
"TimeoutPlugin: node {} has timeout of {} seconds",
node.id, seconds
node.id,
seconds
);
}
}
Expand Down
36 changes: 21 additions & 15 deletions src/plugins/watch_plugin.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@ use crate::{
Result,
};
use globset::{Glob, GlobSet, GlobSetBuilder};
use log::{debug, error, warn};
use notify::{Config as NotifyConfig, Event, RecommendedWatcher, RecursiveMode, Watcher};
use std::{
any::Any,
Expand Down Expand Up @@ -57,7 +56,7 @@ impl WatchPlugin {
// Here we'll keep it simpler and just store a flag we can read in on_after_run.

fn create_watcher() -> Result<(RecommendedWatcher, Receiver<notify::Result<Event>>)> {
debug!("Creating file watcher with 1s poll interval");
tracing::debug!("Creating file watcher with 1s poll interval");
let (tx, rx) = mpsc::channel();
let watcher = RecommendedWatcher::new(
move |res| {
Expand All @@ -84,7 +83,7 @@ impl WatchPlugin {
let cwd = match std::env::current_dir() {
Ok(path) => path,
Err(e) => {
warn!("Failed to get current directory: {}", e);
tracing::warn!("Failed to get current directory: {}", e);
return vec![];
}
};
Expand All @@ -93,7 +92,7 @@ impl WatchPlugin {
let changed_abs = match changed_path.canonicalize() {
Ok(p) => p,
Err(e) => {
warn!(
tracing::warn!(
"Failed to canonicalize path {}: {}",
changed_path.display(),
e
Expand All @@ -106,7 +105,7 @@ impl WatchPlugin {
let watch_abs = match watch_dir.canonicalize() {
Ok(p) => p,
Err(e) => {
warn!(
tracing::warn!(
"Failed to canonicalize watch dir {}: {}",
watch_dir.display(),
e
Expand Down Expand Up @@ -218,10 +217,15 @@ impl Plugin for WatchPlugin {
auto_watch: true, ..
}) = &task_data.watch
{
// Found auto_watch == true, enable watch mode only if BODO_NO_WATCH is not set
if std::env::var("BODO_NO_WATCH").is_err() {
self.watch_mode = true;
break;
// Found auto_watch == true, enable watch mode only if BODO_NO_WATCH is not set to "1"
match std::env::var("BODO_NO_WATCH") {
Ok(ref v) if v == "1" => {
// BODO_NO_WATCH is set to "1", do not enable watch mode
}
_ => {
self.watch_mode = true;
break;
}
}
Comment on lines +220 to 229
Copy link

Copilot AI Sep 19, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This logic only disables watch when BODO_NO_WATCH == "1" but main currently disables when the var is set to any value, creating conflicting behavior. Update main to mirror this exact check so both places agree.

Copilot uses AI. Check for mistakes.

}
}
Expand Down Expand Up @@ -313,7 +317,7 @@ impl Plugin for WatchPlugin {
for d in &all_dirs {
if d.is_dir() {
if let Err(e) = watcher.watch(d, RecursiveMode::Recursive) {
warn!("WatchPlugin: Failed to watch '{}': {}", d.display(), e);
tracing::warn!("WatchPlugin: Failed to watch '{}': {}", d.display(), e);
}
}
}
Expand All @@ -327,22 +331,22 @@ impl Plugin for WatchPlugin {
let event = match rx.recv() {
Ok(e) => e,
Err(_) => {
debug!("WatchPlugin: Watcher channel closed. Exiting loop.");
tracing::debug!("WatchPlugin: Watcher channel closed. Exiting loop.");
break;
}
};
let event = match event {
Ok(ev) => ev,
Err(err) => {
warn!("WatchPlugin: Watch error: {}", err);
tracing::warn!("WatchPlugin: Watch error: {}", err);
continue;
}
};

let now = Instant::now();
let since_last = now.duration_since(last_run);
if since_last < Duration::from_millis(max_debounce) {
debug!("Debouncing event (too soon after last run)");
tracing::debug!("Debouncing event (too soon after last run)");
continue;
}
last_run = now;
Expand Down Expand Up @@ -402,9 +406,11 @@ impl Plugin for WatchPlugin {
options: Some(options),
};
if let Err(e) = new_manager.run_plugins(Some(plugin_config)) {
error!("WatchPlugin: re-run failed: {}", e);
tracing::error!("WatchPlugin: re-run failed: {}", e);
if self.stop_on_fail {
warn!("WatchPlugin: Stopping watch loop due to re-run failure");
tracing::warn!(
"WatchPlugin: Stopping watch loop due to re-run failure"
);
return Ok(());
}
}
Expand Down
Loading
Loading