use anyhow::Result;
use serde::{Deserialize, Serialize};
use std::env;
use std::fs;
use std::path::{Path, PathBuf};
use tracing::{debug, info};
use crate::config::LayoutMode;
#[derive(Debug, Clone, Copy, PartialEq, Deserialize, Serialize, Default)]
pub enum PanelType {
Source,
EbpfInfo,
#[default]
InteractiveCommand,
}
#[derive(Debug, Clone, Copy, PartialEq, Deserialize, Serialize, Default)]
#[serde(rename_all = "lowercase")]
pub enum LogLevel {
Error,
#[default]
Warn,
Info,
Debug,
Trace,
}
impl std::fmt::Display for LogLevel {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
LogLevel::Error => write!(f, "error"),
LogLevel::Warn => write!(f, "warn"),
LogLevel::Info => write!(f, "info"),
LogLevel::Debug => write!(f, "debug"),
LogLevel::Trace => write!(f, "trace"),
}
}
}
impl LogLevel {
pub fn from_str(s: &str) -> Result<Self> {
match s.to_lowercase().as_str() {
"error" => Ok(LogLevel::Error),
"warn" => Ok(LogLevel::Warn),
"info" => Ok(LogLevel::Info),
"debug" => Ok(LogLevel::Debug),
"trace" => Ok(LogLevel::Trace),
_ => Err(anyhow::anyhow!(
"Invalid log level: {}. Valid options: error, warn, info, debug, trace",
s
)),
}
}
}
#[derive(Debug, Clone, Default, Deserialize, Serialize)]
pub struct Config {
#[serde(default)]
pub general: GeneralConfig,
#[serde(default)]
pub dwarf: DwarfConfig,
#[serde(default)]
pub files: FilesConfig,
#[serde(default)]
pub ui: UiConfigToml,
#[serde(default)]
pub ebpf: EbpfConfig,
#[serde(default)]
pub source: SourceConfig,
#[serde(skip)]
pub loaded_from: Option<PathBuf>,
}
#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct GeneralConfig {
#[serde(default = "default_log_file")]
pub log_file: String,
#[serde(default = "default_tui_mode")]
pub default_tui_mode: bool,
#[serde(default = "default_enable_logging")]
pub enable_logging: bool,
#[serde(default = "default_enable_console_logging")]
pub enable_console_logging: bool,
#[serde(default)]
pub log_level: LogLevel,
}
#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct DwarfConfig {
#[serde(default = "default_debug_search_paths")]
pub search_paths: Vec<String>,
#[serde(default)]
pub allow_loose_debug_match: bool,
}
#[derive(Debug, Clone, Deserialize, Serialize, Default)]
pub struct SourceConfig {
#[serde(default)]
pub substitutions: Vec<PathSubstitution>,
#[serde(default)]
pub search_dirs: Vec<String>,
}
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, Eq)]
pub struct PathSubstitution {
pub from: String,
pub to: String,
}
#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct EbpfConfig {
#[serde(default = "default_ringbuf_size")]
pub ringbuf_size: u64,
#[serde(default = "default_perf_page_count")]
pub perf_page_count: u32,
#[serde(default = "default_proc_module_offsets_max_entries")]
pub proc_module_offsets_max_entries: u64,
#[serde(default = "default_force_perf_event_array")]
pub force_perf_event_array: bool,
#[serde(default = "default_mem_dump_cap")]
pub mem_dump_cap: u32,
#[serde(default = "default_compare_cap")]
pub compare_cap: u32,
#[serde(default = "default_max_trace_event_size")]
pub max_trace_event_size: u32,
#[serde(default = "default_enable_sysmon_for_shared_lib")]
pub enable_sysmon_for_shared_lib: bool,
}
#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct FilesConfig {
#[serde(default = "default_save_option")]
pub save_llvm_ir: SaveOption,
#[serde(default = "default_save_option")]
pub save_ebpf: SaveOption,
#[serde(default = "default_save_option")]
pub save_ast: SaveOption,
}
#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct UiConfigToml {
#[serde(default = "default_layout")]
pub layout: LayoutMode,
#[serde(default)]
pub default_focus: PanelType,
#[serde(default = "default_panel_ratios")]
pub panel_ratios: [u16; 3],
#[serde(default = "default_show_source_panel")]
pub show_source_panel: bool,
#[serde(default)]
pub two_panel_ratios: Option<[u16; 2]>,
#[serde(default)]
pub history: HistoryConfigToml,
#[serde(default = "default_ebpf_max_messages")]
pub ebpf_max_messages: usize,
}
#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct HistoryConfigToml {
#[serde(default = "default_history_enabled")]
pub enabled: bool,
#[serde(default = "default_history_max_entries")]
pub max_entries: usize,
}
#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct SaveOption {
#[serde(default = "default_debug_save")]
pub debug: bool,
#[serde(default = "default_release_save")]
pub release: bool,
}
fn default_log_file() -> String {
"ghostscope.log".to_string()
}
fn default_tui_mode() -> bool {
true
}
fn default_enable_logging() -> bool {
false }
fn default_enable_console_logging() -> bool {
false }
fn default_debug_search_paths() -> Vec<String> {
vec![
"/usr/lib/debug".to_string(),
"/usr/local/lib/debug".to_string(),
]
}
fn default_ringbuf_size() -> u64 {
262144 }
fn default_perf_page_count() -> u32 {
64 }
fn default_proc_module_offsets_max_entries() -> u64 {
4096
}
fn default_force_perf_event_array() -> bool {
false
}
fn default_mem_dump_cap() -> u32 {
4096
}
fn default_max_trace_event_size() -> u32 {
32768
}
fn default_compare_cap() -> u32 {
64
}
fn default_enable_sysmon_for_shared_lib() -> bool {
false
}
fn default_save_option() -> SaveOption {
SaveOption {
debug: true,
release: false,
}
}
fn default_layout() -> LayoutMode {
LayoutMode::Horizontal
}
fn default_debug_save() -> bool {
true
}
fn default_release_save() -> bool {
false
}
fn default_panel_ratios() -> [u16; 3] {
[4, 3, 3] }
fn default_show_source_panel() -> bool {
true
}
fn default_history_enabled() -> bool {
true
}
fn default_history_max_entries() -> usize {
5000
}
fn default_ebpf_max_messages() -> usize {
2000
}
impl Default for GeneralConfig {
fn default() -> Self {
Self {
log_file: default_log_file(),
default_tui_mode: default_tui_mode(),
enable_logging: default_enable_logging(),
enable_console_logging: default_enable_console_logging(),
log_level: LogLevel::default(),
}
}
}
impl Default for DwarfConfig {
fn default() -> Self {
Self {
search_paths: default_debug_search_paths(),
allow_loose_debug_match: false,
}
}
}
impl Default for EbpfConfig {
fn default() -> Self {
Self {
ringbuf_size: default_ringbuf_size(),
perf_page_count: default_perf_page_count(),
proc_module_offsets_max_entries: default_proc_module_offsets_max_entries(),
force_perf_event_array: default_force_perf_event_array(),
mem_dump_cap: default_mem_dump_cap(),
compare_cap: default_compare_cap(),
max_trace_event_size: default_max_trace_event_size(),
enable_sysmon_for_shared_lib: default_enable_sysmon_for_shared_lib(),
}
}
}
impl EbpfConfig {
pub fn validate(&self, file_path: &str) -> Result<()> {
if !self.ringbuf_size.is_power_of_two() {
return Err(anyhow::anyhow!(
"❌ Invalid eBPF configuration in '{}':\n\n\
ringbuf_size must be a power of 2, got {}\n\n\
💡 Fix: Use a power of 2 value (e.g., 131072, 262144, 524288, 1048576)\n\
Recommended values:\n\
- Low-frequency tracing: 131072 (128KB)\n\
- Medium-frequency tracing: 262144 (256KB, default)\n\
- High-frequency tracing: 524288 (512KB) or 1048576 (1MB)",
file_path,
self.ringbuf_size
));
}
if self.ringbuf_size < 4096 || self.ringbuf_size > 16 * 1024 * 1024 {
return Err(anyhow::anyhow!(
"❌ Invalid eBPF configuration in '{}':\n\n\
ringbuf_size {} is out of reasonable range\n\n\
💡 Valid range: 4096 (4KB) to 16777216 (16MB)\n\
Recommended: 262144 (256KB)",
file_path,
self.ringbuf_size
));
}
if !self.perf_page_count.is_power_of_two() {
return Err(anyhow::anyhow!(
"❌ Invalid eBPF configuration in '{}':\n\n\
perf_page_count must be a power of 2, got {}\n\n\
💡 Fix: Use a power of 2 value (e.g., 32, 64, 128, 256)\n\
Recommended values:\n\
- Low-frequency tracing: 32 pages (~128KB per CPU)\n\
- Medium-frequency tracing: 64 pages (~256KB per CPU, default)\n\
- High-frequency tracing: 128-256 pages (512KB-1MB per CPU)",
file_path,
self.perf_page_count
));
}
if self.perf_page_count < 8 || self.perf_page_count > 1024 {
return Err(anyhow::anyhow!(
"❌ Invalid eBPF configuration in '{}':\n\n\
perf_page_count {} is out of reasonable range\n\n\
💡 Valid range: 8 to 1024 pages\n\
Recommended: 64 pages (~256KB per CPU)",
file_path,
self.perf_page_count
));
}
if self.proc_module_offsets_max_entries < 64 || self.proc_module_offsets_max_entries > 65536
{
return Err(anyhow::anyhow!(
"❌ Invalid eBPF configuration in '{}':\n\n\
proc_module_offsets_max_entries {} is out of reasonable range\n\n\
💡 Valid range: 64 to 65536\n\
Recommended values:\n\
- Single process: 1024\n\
- Multi-process: 4096 (default)\n\
- System-wide: 8192 or 16384",
file_path,
self.proc_module_offsets_max_entries
));
}
Ok(())
}
}
impl Default for FilesConfig {
fn default() -> Self {
Self {
save_llvm_ir: default_save_option(),
save_ebpf: default_save_option(),
save_ast: default_save_option(),
}
}
}
impl Default for UiConfigToml {
fn default() -> Self {
Self {
layout: default_layout(),
default_focus: PanelType::default(),
panel_ratios: default_panel_ratios(),
show_source_panel: default_show_source_panel(),
two_panel_ratios: None,
history: HistoryConfigToml::default(),
ebpf_max_messages: default_ebpf_max_messages(),
}
}
}
impl UiConfigToml {
pub fn validate(&self, file_path: &str) -> Result<()> {
if self.ebpf_max_messages < 100 {
return Err(anyhow::anyhow!(
"❌ Invalid UI configuration in '{}':\n\n\
ebpf_max_messages {} is too small\n\n\
💡 Minimum value: 100\n\
Recommended values:\n\
- Low-frequency tracing: 1000-2000\n\
- Medium-frequency tracing: 2000-5000 (default: 2000)\n\
- High-frequency tracing: 5000-10000\n\
Note: Larger values consume more memory",
file_path,
self.ebpf_max_messages
));
}
Ok(())
}
}
impl Default for HistoryConfigToml {
fn default() -> Self {
Self {
enabled: default_history_enabled(),
max_entries: default_history_max_entries(),
}
}
}
impl Default for SaveOption {
fn default() -> Self {
SaveOption {
debug: default_debug_save(),
release: default_release_save(),
}
}
}
impl Config {
pub fn load() -> Result<Self> {
let config_paths = Self::get_config_search_paths();
for path in &config_paths {
if path.exists() {
info!("Loading configuration from: {}", path.display());
let mut config = Self::load_from_file(path)?;
config.loaded_from = Some(path.clone());
return Ok(config);
} else {
debug!("Configuration file not found: {}", path.display());
}
}
info!("No configuration file found, using default settings");
Ok(Self::default())
}
pub fn load_from_file<P: AsRef<Path>>(path: P) -> Result<Self> {
let path = path.as_ref();
let content = fs::read_to_string(path).map_err(|e| {
anyhow::anyhow!(
"Failed to read configuration file '{}': {}",
path.display(),
e
)
})?;
Self::validate_toml_content(&content, &path.display().to_string())?;
let config: Config = toml::from_str(&content).map_err(|e| {
Self::create_friendly_toml_error(&path.display().to_string(), &content, e)
})?;
config.ebpf.validate(&path.display().to_string())?;
config.ui.validate(&path.display().to_string())?;
Ok(config)
}
fn validate_toml_content(content: &str, file_path: &str) -> Result<()> {
if let Some(panel_ratios_line) = content.lines().find(|line| {
let trimmed = line.trim();
trimmed.starts_with("panel_ratios") && trimmed.contains('=')
}) {
if let Some(array_part) = panel_ratios_line.split('=').nth(1) {
let array_str = array_part.trim();
if array_str.starts_with('[') && array_str.ends_with(']') {
let inner = &array_str[1..array_str.len() - 1];
let numbers: Vec<&str> = inner.split(',').map(|s| s.trim()).collect();
if numbers.len() == 3 {
for (i, num_str) in numbers.iter().enumerate() {
if let Ok(num) = num_str.parse::<u16>() {
if num == 0 {
let panel_names = ["Source", "EbpfInfo", "InteractiveCommand"];
return Err(anyhow::anyhow!(
"❌ Invalid panel configuration in '{}':\n\n\
Panel ratio for {} panel (index {}) is 0, which would hide the panel.\n\n\
💡 Fix: Change the 0 to a positive number (e.g., 1) in your config file:\n\
panel_ratios = [4, 3, 3] # Example with all positive values\n\n\
Valid values are positive integers representing relative sizes.",
file_path,
panel_names.get(i).unwrap_or(&"Unknown"),
i
));
}
}
}
}
}
}
}
if let Some(two_panel_ratios_line) = content.lines().find(|line| {
let trimmed = line.trim();
trimmed.starts_with("two_panel_ratios") && trimmed.contains('=')
}) {
if let Some(array_part) = two_panel_ratios_line.split('=').nth(1) {
let array_str = array_part.trim();
if array_str.starts_with('[') && array_str.ends_with(']') {
let inner = &array_str[1..array_str.len() - 1];
let numbers: Vec<&str> = inner.split(',').map(|s| s.trim()).collect();
if numbers.len() == 2 {
for (i, num_str) in numbers.iter().enumerate() {
if let Ok(num) = num_str.parse::<u16>() {
if num == 0 {
let panel_names = ["EbpfInfo", "InteractiveCommand"];
return Err(anyhow::anyhow!(
"❌ Invalid panel configuration in '{}':\n\n\
two_panel_ratios for {} (index {}) is 0, which would hide the panel.\n\n\
💡 Fix: Change the 0 to a positive number (e.g., 1) in your config file:\n\
two_panel_ratios = [3, 3] # Example with all positive values\n\n\
Valid values are positive integers representing relative sizes.",
file_path,
panel_names.get(i).unwrap_or(&"Unknown"),
i
));
}
}
}
}
}
}
}
Ok(())
}
fn create_friendly_toml_error(
file_path: &str,
content: &str,
error: toml::de::Error,
) -> anyhow::Error {
let error_msg = format!("Configuration file parsing error in '{file_path}'");
if let Some(span) = error.span() {
let lines: Vec<&str> = content.lines().collect();
let mut current_pos = 0;
let mut line_num: usize = 1;
let mut col_num: usize = 1;
for line in &lines {
let line_len = line.len() + 1; if current_pos + line_len > span.start {
col_num = span.start - current_pos + 1;
break;
}
current_pos += line_len;
line_num += 1;
}
let context_line = lines.get(line_num.saturating_sub(1)).unwrap_or(&"");
anyhow::anyhow!(
"{}\n\nError at line {}, column {}:\n{}\n\n{}\n{}^\n\nSuggestion: {}",
error_msg,
line_num,
col_num,
error,
context_line,
" ".repeat(col_num.saturating_sub(1)),
Self::get_error_suggestion(&error.to_string())
)
} else {
anyhow::anyhow!(
"{}\n\n{}\n\nSuggestion: {}",
error_msg,
error,
Self::get_error_suggestion(&error.to_string())
)
}
}
fn get_error_suggestion(error_msg: &str) -> &'static str {
if error_msg.contains("unknown variant") && error_msg.contains("horizontal") {
"Layout values should be capitalized: use 'Horizontal' instead of 'horizontal'"
} else if error_msg.contains("unknown variant") && error_msg.contains("vertical") {
"Layout values should be capitalized: use 'Vertical' instead of 'vertical'"
} else if error_msg.contains("unknown variant") && error_msg.contains("layout") {
"Valid layout options are: 'Horizontal', 'Vertical'"
} else if error_msg.contains("log_level") {
"Valid log levels are: 'error', 'warn', 'info', 'debug', 'trace'"
} else if error_msg.contains("unknown field") {
"Check the field name spelling and ensure it's in the correct section"
} else if error_msg.contains("invalid type") {
"Check the value type - strings should be in quotes, numbers should not"
} else if error_msg.contains("panel_ratios") {
"Panel ratios must be an array of 3 positive numbers, e.g., [4, 3, 3]"
} else if error_msg.contains("default_focus") {
"Valid panel focus options are: 'Source', 'EbpfInfo', 'InteractiveCommand'"
} else {
"Please check the configuration file syntax and refer to the example config.toml"
}
}
fn get_config_search_paths() -> Vec<PathBuf> {
let mut paths = Vec::new();
if let Some(home_dir) = env::var("HOME")
.ok()
.filter(|p| !p.is_empty())
.map(PathBuf::from)
.or_else(dirs::home_dir)
{
paths.push(home_dir.join(".ghostscope").join("config.toml"));
}
if let Ok(current_dir) = std::env::current_dir() {
paths.push(current_dir.join("ghostscope.toml"));
}
paths
}
pub fn load_with_explicit_path<P: AsRef<Path>>(config_path: P) -> Result<Self> {
let path = config_path.as_ref();
if !path.exists() {
return Err(anyhow::anyhow!(
"Specified configuration file does not exist: {}",
path.display()
));
}
info!("Loading configuration from: {}", path.display());
let mut config = Self::load_from_file(path)?;
config.loaded_from = Some(path.to_path_buf());
Ok(config)
}
}