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
19 changes: 19 additions & 0 deletions Cargo.lock

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

1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
toml = "0.8"
thiserror = "1.0"
termcolor = "1.4"

[dev-dependencies]
tempfile = "3.8"
193 changes: 188 additions & 5 deletions src/commands/ext.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,11 @@ use crate::output::OutputManager;
use clap::{Arg, ArgMatches, Command};
use serde_json::Value;
use std::fs;
use std::io::Write;
use std::os::unix::fs as unix_fs;
use std::path::{Path, PathBuf};
use std::process::{Command as ProcessCommand, Stdio};
use termcolor::{Color, ColorChoice, ColorSpec, StandardStream, WriteColor};

/// Represents an extension and its type(s)
#[derive(Debug, Clone)]
Expand All @@ -17,6 +19,54 @@ struct Extension {
is_directory: bool, // true for directories, false for .raw files
}

/// Print a colored success message
fn print_colored_success(message: &str) {
// Use auto-detection but fallback gracefully
let color_choice =
if std::env::var("NO_COLOR").is_ok() || std::env::var("AVOCADO_TEST_MODE").is_ok() {
ColorChoice::Never
} else {
ColorChoice::Auto
};

let mut stdout = StandardStream::stdout(color_choice);
let mut color_spec = ColorSpec::new();
color_spec.set_fg(Some(Color::Green)).set_bold(true);

if stdout.set_color(&color_spec).is_ok() && color_choice != ColorChoice::Never {
let _ = write!(&mut stdout, "[SUCCESS]");
let _ = stdout.reset();
println!(" {message}");
} else {
// Fallback for environments without color support
println!("[SUCCESS] {message}");
}
}

/// Print a colored info message
fn print_colored_info(message: &str) {
// Use auto-detection but fallback gracefully
let color_choice =
if std::env::var("NO_COLOR").is_ok() || std::env::var("AVOCADO_TEST_MODE").is_ok() {
ColorChoice::Never
} else {
ColorChoice::Auto
};

let mut stdout = StandardStream::stdout(color_choice);
let mut color_spec = ColorSpec::new();
color_spec.set_fg(Some(Color::Blue)).set_bold(true);

if stdout.set_color(&color_spec).is_ok() && color_choice != ColorChoice::Never {
let _ = write!(&mut stdout, "[INFO]");
let _ = stdout.reset();
println!(" {message}");
} else {
// Fallback for environments without color support
println!("[INFO] {message}");
}
}

/// Create the ext subcommand definition
pub fn create_command() -> Command {
Command::new("ext")
Expand Down Expand Up @@ -577,7 +627,7 @@ fn display_status_summary(
println!(" - Configuration extensions: {}", mounted_confext.len());

if hitl_count > 0 {
println!(" 📡 HITL extensions are active - development mode");
print_colored_info("HITL extensions are active - development mode");
}
}

Expand Down Expand Up @@ -1332,6 +1382,7 @@ fn process_post_merge_tasks() -> Result<(), SystemdError> {
}

let mut depmod_needed = false;
let mut modprobe_modules = Vec::new();

// Read all files in the extension release directory
match fs::read_dir(&release_dir) {
Expand All @@ -1342,9 +1393,11 @@ fn process_post_merge_tasks() -> Result<(), SystemdError> {
if let Ok(content) = fs::read_to_string(&path) {
if check_avocado_on_merge_depmod(&content) {
depmod_needed = true;
// We can break early since we only need to call depmod once
break;
}

// Parse AVOCADO_MODPROBE modules
let mut modules = parse_avocado_modprobe(&content);
modprobe_modules.append(&mut modules);
}
}
}
Expand All @@ -1361,6 +1414,11 @@ fn process_post_merge_tasks() -> Result<(), SystemdError> {
run_depmod()?;
}

// Call modprobe for each module after depmod completes
if !modprobe_modules.is_empty() {
run_modprobe(&modprobe_modules)?;
}

Ok(())
}

Expand All @@ -1383,9 +1441,36 @@ fn check_avocado_on_merge_depmod(content: &str) -> bool {
false
}

/// Parse AVOCADO_MODPROBE modules from release file content
fn parse_avocado_modprobe(content: &str) -> Vec<String> {
let mut modules = Vec::new();

for line in content.lines() {
let line = line.trim();
if line.starts_with("AVOCADO_MODPROBE=") {
let value = line
.split('=')
.nth(1)
.unwrap_or("")
.trim_matches('"')
.trim();

// Parse space-separated list of modules
for module in value.split_whitespace() {
if !module.is_empty() {
modules.push(module.to_string());
}
}
break; // Only process the first AVOCADO_MODPROBE line
}
}

modules
}

/// Run the depmod command
fn run_depmod() -> Result<(), SystemdError> {
println!("Running depmod to update kernel module dependencies...");
print_colored_info("Running depmod to update kernel module dependencies...");

// Check if we're in test mode and should use mock commands
let command_name = if std::env::var("AVOCADO_TEST_MODE").is_ok() {
Expand All @@ -1412,7 +1497,47 @@ fn run_depmod() -> Result<(), SystemdError> {
});
}

println!("depmod completed successfully.");
print_colored_success("depmod completed successfully.");
Ok(())
}

/// Run modprobe for a list of modules
fn run_modprobe(modules: &[String]) -> Result<(), SystemdError> {
if modules.is_empty() {
return Ok(());
}

print_colored_info(&format!("Loading kernel modules: {}", modules.join(", ")));

for module in modules {
// Check if we're in test mode and should use mock commands
let command_name = if std::env::var("AVOCADO_TEST_MODE").is_ok() {
"mock-modprobe"
} else {
"modprobe"
};

let output = ProcessCommand::new(command_name)
.arg(module)
.stdout(Stdio::piped())
.stderr(Stdio::piped())
.output()
.map_err(|e| SystemdError::CommandFailed {
command: format!("{command_name} {module}"),
source: e,
})?;

if !output.status.success() {
let stderr = String::from_utf8_lossy(&output.stderr);
eprintln!("Warning: Failed to load module {module}: {stderr}");
// Don't fail the entire operation for individual module failures
// Just log the warning and continue with other modules
} else {
print_colored_success(&format!("Module {module} loaded successfully."));
}
}

print_colored_success("Module loading completed.");
Ok(())
}

Expand Down Expand Up @@ -1695,4 +1820,62 @@ OTHER_KEY=value
"#;
assert!(!check_avocado_on_merge_depmod(content_with_empty_value));
}

#[test]
fn test_parse_avocado_modprobe() {
// Test case with multiple modules
let content_with_modules = r#"
VERSION_ID=2.0
AVOCADO_MODPROBE="nvidia i915 radeon"
OTHER_KEY=value
"#;
let modules = parse_avocado_modprobe(content_with_modules);
assert_eq!(modules, vec!["nvidia", "i915", "radeon"]);

// Test case with single module without quotes
let content_single_module = r#"
VERSION_ID=1.5
AVOCADO_MODPROBE=snd_hda_intel
OTHER_KEY=value
"#;
let modules = parse_avocado_modprobe(content_single_module);
assert_eq!(modules, vec!["snd_hda_intel"]);

// Test case with no AVOCADO_MODPROBE
let content_no_modprobe = r#"
VERSION_ID=1.0
AVOCADO_ON_MERGE=depmod
OTHER_KEY=value
"#;
let modules = parse_avocado_modprobe(content_no_modprobe);
assert!(modules.is_empty());

// Test case with empty AVOCADO_MODPROBE
let content_empty_modprobe = r#"
VERSION_ID=1.0
AVOCADO_MODPROBE=""
OTHER_KEY=value
"#;
let modules = parse_avocado_modprobe(content_empty_modprobe);
assert!(modules.is_empty());

// Test case with extra whitespace
let content_with_whitespace = r#"
VERSION_ID=1.0
AVOCADO_MODPROBE=" nvidia i915 radeon "
OTHER_KEY=value
"#;
let modules = parse_avocado_modprobe(content_with_whitespace);
assert_eq!(modules, vec!["nvidia", "i915", "radeon"]);

// Test case with mixed quotes and no quotes in different lines (only first should be processed)
let content_multiple_lines = r#"
VERSION_ID=1.0
AVOCADO_MODPROBE="nvidia i915"
AVOCADO_MODPROBE=should_be_ignored
OTHER_KEY=value
"#;
let modules = parse_avocado_modprobe(content_multiple_lines);
assert_eq!(modules, vec!["nvidia", "i915"]);
}
}
Loading