gflow 0.4.15

A lightweight, single-node job scheduler written in Rust.
Documentation
use anyhow::Result;
use gflow::core::job::Job;
use gflow::utils::{parse_job_ids, substitute_parameters};
use gflow::{print_field, print_optional_field};
use std::path::PathBuf;
use std::time::SystemTime;

pub async fn handle_show(config_path: &Option<PathBuf>, job_ids_str: String) -> Result<()> {
    let client = gflow::create_client(config_path)?;

    let job_ids = parse_job_ids(&job_ids_str)?;

    for (index, &job_id) in job_ids.iter().enumerate() {
        if index > 0 {
            println!("\n{}", "=".repeat(80));
            println!();
        }

        let Some(job) = gflow::client::get_job_or_warn(&client, job_id).await? else {
            continue;
        };

        print_job_details(&job);
    }
    Ok(())
}

fn print_job_details(job: &Job) {
    println!("Job Details:");
    print_field!("ID", "{}", job.id);
    print_field!("State", "{} ({})", job.state, job.state.short_form());
    print_field!("Priority", "{}", job.priority);
    print_field!("SubmittedBy", "{}", job.submitted_by);
    if job.max_retries > 0 {
        print_field!("MaxRetries", "{}", job.max_retries);
    }
    print_optional_field!("GroupID", job.group_id);

    // Command or script
    print_optional_field!("Script", job.script, |s| s.display());
    if let Some(ref command) = job.command {
        // Check if command contains parameters
        let has_params = command.contains('{') && !job.parameters.is_empty();

        if has_params {
            print_field!("Command(template)", "{}", command);
            match substitute_parameters(command, &job.parameters) {
                Ok(substituted) => print_field!("Command(actual)", "{}", substituted),
                Err(e) => print_field!("Command(actual)", "Error: {}", e),
            }
        } else {
            print_field!("Command", "{}", command);
        }
    }

    // Parameters
    if !job.parameters.is_empty() {
        println!("\nParameters:");
        let mut params: Vec<_> = job.parameters.iter().collect();
        params.sort_by_key(|(k, _)| *k);
        for (key, value) in params {
            print_field!(key, "{}", value);
        }
    }

    // Resources
    println!("\nResources:");
    print_field!("GPUs", "{}", job.gpus);
    print_field!(
        "GPUSharing",
        "{}",
        format_gpu_sharing_mode(job.gpu_sharing_mode)
    );
    if let Some(gpu_memory_mb) = job.gpu_memory_limit_mb {
        print_field!(
            "GPUMemoryLimit",
            "{}",
            gflow::utils::format_memory(gpu_memory_mb)
        );
    }
    print_optional_field!("GPUIDs", job.gpu_ids, |ids| format_ids(ids));
    if let Some(memory_mb) = job.memory_limit_mb {
        print_field!("MemoryLimit", "{}", gflow::utils::format_memory(memory_mb));
    }
    print_optional_field!("CondaEnv", job.conda_env);

    // Working directory and run name
    println!("\nExecution:");
    print_field!("WorkingDir", "{}", job.run_dir.display());
    print_optional_field!("TmuxSession", job.run_name);
    if !job.notifications.is_empty() {
        print_field!(
            "NotifyEmail",
            "{}",
            job.notifications
                .emails
                .iter()
                .map(|email| email.as_str())
                .collect::<Vec<_>>()
                .join(",")
        );
        if !job.notifications.events.is_empty() {
            print_field!(
                "NotifyOn",
                "{}",
                job.notifications
                    .events
                    .iter()
                    .map(|event| event.as_str())
                    .collect::<Vec<_>>()
                    .join(",")
            );
        }
    }

    // Dependencies
    let all_deps = job.all_dependency_ids();
    if !all_deps.is_empty() || job.task_id.is_some() {
        println!("\nDependencies:");
        if !all_deps.is_empty() {
            print_field!("DependsOn", "{}", format_ids(&all_deps));
            if let Some(mode) = job.dependency_mode {
                print_field!("Mode", "{:?}", mode);
            }
            if job.auto_cancel_on_dependency_failure {
                print_field!("AutoCancel", "enabled");
            }
        }
        if let Some(task_id) = job.task_id {
            print_field!("TaskID", "{}", task_id);
        }
    }

    // Time information
    println!("\nTiming:");
    if let Some(time_limit) = job.time_limit {
        print_field!("TimeLimit", "{}", gflow::utils::format_duration(time_limit));
    }
    if let Some(submitted_at) = job.submitted_at {
        if let Some(wait_time) = job.wait_time() {
            print_field!(
                "Submitted",
                "{} (waited {})",
                format_time(submitted_at),
                gflow::utils::format_duration(wait_time)
            );
        } else {
            print_field!("Submitted", "{}", format_time(submitted_at));
        }
    }
    if let Some(started_at) = job.started_at {
        if let Some(finished_at) = job.finished_at {
            let runtime = finished_at.duration_since(started_at).ok();
            print_field!("Started", "{}", format_time(started_at));
            if let Some(rt) = runtime {
                print_field!(
                    "Finished",
                    "{} (runtime {})",
                    format_time(finished_at),
                    gflow::utils::format_duration(rt)
                );
            } else {
                print_field!("Finished", "{}", format_time(finished_at));
            }
        } else if let Ok(elapsed) = SystemTime::now().duration_since(started_at) {
            print_field!(
                "Started",
                "{} (running {} so far)",
                format_time(started_at),
                gflow::utils::format_duration(elapsed)
            );
        } else {
            print_field!("Started", "{}", format_time(started_at));
        }
    }
}

/// Format a slice of u32 IDs as a comma-separated string
fn format_ids(ids: &[u32]) -> String {
    ids.iter()
        .map(|id| id.to_string())
        .collect::<Vec<_>>()
        .join(",")
}

fn format_gpu_sharing_mode(mode: gflow::core::job::GpuSharingMode) -> &'static str {
    match mode {
        gflow::core::job::GpuSharingMode::Exclusive => "exclusive",
        gflow::core::job::GpuSharingMode::Shared => "shared",
    }
}

fn format_time(time: SystemTime) -> String {
    use chrono::{DateTime, Local};

    let datetime: DateTime<Local> = time.into();
    datetime.format("%m/%d-%H:%M:%S").to_string()
}