diff --git a/ghostscope/src/script/attach.rs b/ghostscope/src/script/attach.rs new file mode 100644 index 00000000..97df8ded --- /dev/null +++ b/ghostscope/src/script/attach.rs @@ -0,0 +1,316 @@ +use crate::core::GhostSession; +use anyhow::{Context, Result}; +use ghostscope_loader::GhostScopeLoader; +use tracing::{error, info, warn}; + +use super::runtime_maps::{apply_cached_offsets_for_session_pid, apply_pid_alias_for_session}; + +pub(super) fn target_display(config: &ghostscope_compiler::UProbeConfig) -> String { + let addr_disp = config.function_address.unwrap_or(0); + config + .function_name + .clone() + .unwrap_or_else(|| format!("{addr_disp:#x}")) +} + +pub(super) fn log_uprobe_configs( + uprobe_configs: &[ghostscope_compiler::UProbeConfig], + include_trace_id: bool, +) { + info!("Attaching {} uprobe configurations", uprobe_configs.len()); + for (i, config) in uprobe_configs.iter().enumerate() { + let fallback_name = format!("{:#x}", config.function_address.unwrap_or(0)); + if include_trace_id { + info!( + " Config {}: {:?} -> 0x{:x} (trace_id: {})", + i, + config.function_name.as_ref().unwrap_or(&fallback_name), + config.uprobe_offset.unwrap_or(0), + config.assigned_trace_id + ); + } else { + info!( + " Config {}: {:?} -> 0x{:x}", + i, + config.function_name.as_ref().unwrap_or(&fallback_name), + config.uprobe_offset.unwrap_or(0) + ); + } + } +} + +pub(super) fn log_attachment_hints() { + tracing::info!( + "Attachment hints: check privileges, target binary availability, PID validity, and function addresses if needed." + ); +} + +/// Create and attach a loader for a single uprobe configuration. +pub(super) async fn create_and_attach_loader( + config: &ghostscope_compiler::UProbeConfig, + attach_pid: Option, + session: &mut GhostSession, + compile_options: &ghostscope_compiler::CompileOptions, +) -> Result { + info!( + "Creating new eBPF loader with {} bytes of bytecode for trace_id {}", + config.ebpf_bytecode.len(), + config.assigned_trace_id + ); + + let max_entries = session + .config + .as_ref() + .map(|c| c.ebpf_config.proc_module_offsets_max_entries as u32) + .unwrap_or(4096); + let pin_path = ghostscope_process::pinned_bpf_maps::proc_offsets_pin_path() + .context("Failed to resolve pinned proc_module_offsets map path")?; + if let Err(e) = + ghostscope_process::pinned_bpf_maps::ensure_pinned_proc_offsets_exists(max_entries) + { + error!( + "Failed to ensure pinned proc_module_offsets map exists at {} ({} entries): {:#}", + pin_path.display(), + max_entries, + e + ); + return Err(e.context(format!( + "Unable to prepare pinned proc_module_offsets map at {}", + pin_path.display() + ))); + } + + let alias_pin_path = ghostscope_process::pinned_bpf_maps::pid_aliases_pin_path() + .context("Failed to resolve pinned pid_aliases map path")?; + if let Err(e) = + ghostscope_process::pinned_bpf_maps::ensure_pinned_pid_aliases_exists(max_entries) + { + error!( + "Failed to ensure pinned pid_aliases map exists at {} ({} entries): {:#}", + alias_pin_path.display(), + max_entries, + e + ); + return Err(e.context(format!( + "Unable to prepare pinned pid_aliases map at {}", + alias_pin_path.display() + ))); + } + + let range_meta_pin_path = + ghostscope_process::pinned_bpf_maps::proc_module_range_meta_pin_path() + .context("Failed to resolve pinned proc_module_range_meta map path")?; + if let Err(e) = + ghostscope_process::pinned_bpf_maps::ensure_pinned_proc_module_ranges_exist(max_entries) + { + error!( + "Failed to ensure pinned module range maps at {} ({} entries): {:#}", + range_meta_pin_path.display(), + max_entries, + e + ); + return Err(e.context(format!( + "Unable to prepare pinned module range maps at {}", + range_meta_pin_path.display() + ))); + } + + let share_backtrace_cfi_maps = !config.backtrace_module_row_ranges.is_empty(); + if share_backtrace_cfi_maps { + let unwind_row_entries = compile_options + .backtrace_unwind_rows_max_entries + .max(config.backtrace_unwind_rows.len() as u32) + .max(1); + let module_entries = u32::try_from(compile_options.proc_module_offsets_max_entries) + .unwrap_or(u32::MAX) + .max(1); + let rows_pin_path = ghostscope_process::pinned_bpf_maps::bt_unwind_rows_pin_path() + .context("Failed to resolve pinned bt_unwind_rows map path")?; + if let Err(e) = ghostscope_process::pinned_bpf_maps::ensure_pinned_backtrace_cfi_maps_exist( + unwind_row_entries, + module_entries, + ) { + error!( + "Failed to ensure pinned backtrace CFI maps at {} (rows={}, modules={}): {:#}", + rows_pin_path.display(), + unwind_row_entries, + module_entries, + e + ); + return Err(e.context(format!( + "Unable to prepare pinned backtrace CFI maps at {}", + rows_pin_path.display() + ))); + } + } + + let mut loader = GhostScopeLoader::new_with_shared_backtrace_maps( + &config.ebpf_bytecode, + share_backtrace_cfi_maps, + ) + .context("Failed to create eBPF loader for uprobe config")?; + + if let Some(cfg) = &session.config { + loader.set_perf_page_count(cfg.ebpf_config.perf_page_count); + tracing::info!( + "Configured PerfEventArray page count: {} pages per CPU", + cfg.ebpf_config.perf_page_count + ); + } + + info!( + "Setting TraceContext for loader: {} strings, {} variables", + config.trace_context.string_count(), + config.trace_context.variable_name_count() + ); + loader.set_trace_context(config.trace_context.clone()); + loader + .populate_backtrace_unwind_rows_and_module_row_ranges( + &config.backtrace_unwind_rows, + &config.backtrace_module_row_ranges, + ) + .context("Failed to populate DWARF backtrace unwind rows")?; + loader + .register_backtrace_tail_call_program( + config + .backtrace_tail_call_program + .as_ref() + .map(|program| program.step_program_name.as_str()), + ) + .context("Failed to register bt tail-call program")?; + + if attach_pid.is_none() { + let (prefilled, entries) = { + let mut coordinator = session + .coordinator + .lock() + .expect("coordinator mutex poisoned"); + let prefilled = coordinator + .ensure_prefill_module(&config.binary_path) + .unwrap_or(0); + let entries = coordinator.cached_offsets_for_module(&config.binary_path); + (prefilled, entries) + }; + tracing::info!( + "Coordinator cached offsets for {} pid(s) for module {}", + prefilled, + config.binary_path + ); + if !entries.is_empty() { + use ghostscope_process::pinned_bpf_maps::ProcModuleOffsetsValue; + use std::collections::HashMap; + + let mut by_pid: HashMap> = HashMap::new(); + for (pid, cookie, off, base, size) in entries { + by_pid.entry(pid).or_default().push(( + cookie, + ProcModuleOffsetsValue::new( + off.text, off.rodata, off.data, off.bss, base, size, + ), + )); + } + + let mut total = 0usize; + for (pid, items) in by_pid { + if let Err(e) = + ghostscope_process::pinned_bpf_maps::insert_offsets_for_pid(pid, &items) + { + tracing::warn!( + "Failed to write offsets to pinned map for PID {}: {}", + pid, + e + ); + } else { + total += items.len(); + } + } + tracing::info!( + "Applied {} cached offset entries to pinned map for module {}", + total, + config.binary_path + ); + } + } + + apply_pid_alias_for_session(session, compile_options); + apply_cached_offsets_for_session_pid(session); + + let Some(uprobe_offset) = config.uprobe_offset else { + return Err(anyhow::anyhow!("No uprobe offset available in config")); + }; + + if let Some(ref function_name) = config.function_name { + info!( + "Attaching to function '{}' at offset 0x{:x} in {} using eBPF function '{}'", + function_name, uprobe_offset, config.binary_path, config.ebpf_function_name + ); + loader.attach_uprobe_with_program_name( + &config.binary_path, + function_name, + Some(uprobe_offset), + attach_pid.map(|p| p as i32), + Some(&config.ebpf_function_name), + )?; + } else { + info!( + "Attaching to address 0x{:x} in {} using eBPF function '{}'", + uprobe_offset, config.binary_path, config.ebpf_function_name + ); + loader.attach_uprobe_with_program_name( + &config.binary_path, + &format!("0x{uprobe_offset:x}"), + Some(uprobe_offset), + attach_pid.map(|p| p as i32), + Some(&config.ebpf_function_name), + )?; + } + + Ok(loader) +} + +pub(super) fn register_attached_trace( + session: &mut GhostSession, + script: &str, + config: &ghostscope_compiler::UProbeConfig, + loader: GhostScopeLoader, +) -> bool { + let target_display = target_display(config); + let _registered_trace_id = + session + .trace_manager + .add_trace_with_id(crate::trace::manager::AddTraceParams { + trace_id: config.assigned_trace_id, + target: target_display.clone(), + script_content: script.to_string(), + pc: config.function_address.unwrap_or(0), + binary_path: config.binary_path.clone(), + target_display: target_display.clone(), + pid_context: crate::trace::instance::TracePidContext { + attach_pid: session.attach_pid(), + host_pid: session.host_pid(), + proc_pid: session.proc_pid(), + }, + loader: Some(loader), + ebpf_function_name: format!( + "gs_{}_{}_{}", + session.host_pid().unwrap_or(0), + target_display, + config.assigned_trace_id + ), + address_global_index: config.resolved_address_index, + }); + + if let Err(e) = session.trace_manager.enable_trace(config.assigned_trace_id) { + warn!( + "Failed to enable trace_id {}: {}", + config.assigned_trace_id, e + ); + false + } else { + info!( + "✓ Registered and enabled trace_id {} with trace manager", + config.assigned_trace_id + ); + true + } +} diff --git a/ghostscope/src/script/cli.rs b/ghostscope/src/script/cli.rs new file mode 100644 index 00000000..00015bae --- /dev/null +++ b/ghostscope/src/script/cli.rs @@ -0,0 +1,176 @@ +use crate::core::GhostSession; +use anyhow::Result; +use ghostscope_compiler::script::FailedTarget; +use tracing::{error, info, warn}; + +use super::attach::{ + create_and_attach_loader, log_attachment_hints, log_uprobe_configs, register_attached_trace, +}; +use super::compile::{compile_script_with_session, SessionCompileError}; +use super::runtime_maps::ensure_prefill_for_session_pid; +use super::runtime_prep::refresh_runtime_modules_before_compile; + +/// Compile a script for command line mode without attaching uprobes. +pub fn compile_script_for_cli( + script: &str, + session: &mut GhostSession, + compile_options: &ghostscope_compiler::CompileOptions, +) -> Result { + info!("Starting unified script compilation with DWARF integration..."); + + let compilation_result = match compile_script_with_session(script, session, compile_options) { + Ok(result) => result, + Err(SessionCompileError::Compile(e)) => { + let friendly = e.user_message().into_owned(); + error!("Script compilation failed: {}", friendly); + return Err(anyhow::anyhow!( + "Script compilation failed: {friendly}. Please check your script syntax and try again." + )); + } + Err(SessionCompileError::Setup(e)) => return Err(e), + }; + + info!( + "✓ Script compilation successful: {} trace points found, {} uprobe configs generated", + compilation_result.trace_count, + compilation_result.uprobe_configs.len() + ); + info!("Target info: {}", compilation_result.target_info); + + if !compilation_result.failed_targets.is_empty() { + warn!("Some targets failed to compile:"); + for failed in &compilation_result.failed_targets { + warn!( + " {} at 0x{:x}: {}", + failed.target_name, failed.pc_address, failed.error_message + ); + } + } + + Ok(compilation_result) +} + +/// Compile and load script for command line mode using session.command_loaders. +pub async fn compile_and_load_script_for_cli( + script: &str, + session: &mut GhostSession, + compile_options: &ghostscope_compiler::CompileOptions, +) -> Result<()> { + let mut compile_options = compile_options.clone(); + refresh_runtime_modules_before_compile(script, session, &mut compile_options).await?; + + let compilation_result = compile_script_for_cli(script, session, &compile_options)?; + ensure_prefill_for_session_pid(session); + + let ghostscope_compiler::CompilationResult { + uprobe_configs, + failed_targets, + .. + } = compilation_result; + + if uprobe_configs.is_empty() { + return handle_empty_cli_configs(session, &failed_targets); + } + + log_uprobe_configs(&uprobe_configs, false); + + let mut attached_count = 0; + for config in &uprobe_configs { + match create_and_attach_loader(config, session.attach_pid(), session, &compile_options) + .await + { + Ok(loader) => { + info!( + "✓ Successfully attached uprobe for trace_id {}", + config.assigned_trace_id + ); + if register_attached_trace(session, script, config, loader) { + attached_count += 1; + } + } + Err(e) => { + error!( + "Failed to attach uprobe for trace_id {}: {:#}", + config.assigned_trace_id, e + ); + log_attachment_hints(); + return Err(e.context(format!( + "Failed to attach uprobe for trace_id {}", + config.assigned_trace_id + ))); + } + } + } + + if attached_count > 0 { + info!( + "✓ Successfully attached {} of {} uprobes", + attached_count, + uprobe_configs.len() + ); + Ok(()) + } else { + Err(anyhow::anyhow!("No uprobes were successfully attached")) + } +} + +fn handle_empty_cli_configs(session: &GhostSession, failed_targets: &[FailedTarget]) -> Result<()> { + if !failed_targets.is_empty() { + let mut details = String::new(); + for failed in failed_targets { + let _ = std::fmt::Write::write_fmt( + &mut details, + format_args!( + " - {} at 0x{:x}: {}\n", + failed.target_name, failed.pc_address, failed.error_message + ), + ); + } + let full = format!( + "No uprobe configurations created because all {} target(s) failed to compile.\n\nFailed targets:\n{}\n\nTip: fix the reported compile-time errors above (e.g., avoid struct/union/array arithmetic; select a scalar field or use '&expr + ' in an alias/address context).", + failed_targets.len(), + details + ); + error!("{}", full); + return Err(anyhow::anyhow!(full)); + } + + let available_functions = session.list_functions(); + if available_functions.is_empty() { + return Err(anyhow::anyhow!( + "No debug information found in any module!\n\ + \n\ + The target binary and its libraries are stripped or compiled without debug symbols.\n\ + GhostScope requires debug information (DWARF) to:\n\ + - Locate functions by name\n\ + - Analyze variable types and locations\n\ + - Map source lines to addresses\n\ + \n\ + Solutions:\n\ + 1. Recompile your target with -g flag: gcc -g your_program.c -o your_program\n\ + 2. Install debug symbol packages (e.g., libc6-dbg on Debian/Ubuntu)\n\ + 3. For stripped binaries, use objcopy to create separate debug files:\n\ + objcopy --only-keep-debug binary binary.debug\n\ + objcopy --add-gnu-debuglink=binary.debug binary" + )); + } + + Err(anyhow::anyhow!( + "No uprobe configurations created - the functions referenced in your script were not found.\n\ + \n\ + Possible reasons:\n\ + - Function names are misspelled (check available functions below)\n\ + - Functions don't exist in the target binary\n\ + - Functions are from libraries that aren't loaded yet\n\ + \n\ + Available functions (first 10):\n{}\n\ + \n\ + Tip: Run GhostScope in TUI mode to browse all available functions", + available_functions + .iter() + .take(10) + .map(|f| format!(" - {f}")) + .collect::>() + .join("\n") + )) +} diff --git a/ghostscope/src/script/compile.rs b/ghostscope/src/script/compile.rs new file mode 100644 index 00000000..3da3c460 --- /dev/null +++ b/ghostscope/src/script/compile.rs @@ -0,0 +1,42 @@ +use crate::core::GhostSession; +use anyhow::Result; + +pub(super) enum SessionCompileError { + Setup(anyhow::Error), + Compile(ghostscope_compiler::CompileError), +} + +pub(super) fn main_executable_path(session: &mut GhostSession) -> Result { + let process_analyzer = session + .process_analyzer + .as_mut() + .ok_or_else(|| anyhow::anyhow!("Process analyzer is required for script compilation"))?; + + process_analyzer + .get_main_executable() + .map(|module| module.path.clone()) + .ok_or_else(|| anyhow::anyhow!("No main executable found in process")) +} + +pub(super) fn compile_script_with_session( + script: &str, + session: &mut GhostSession, + compile_options: &ghostscope_compiler::CompileOptions, +) -> std::result::Result { + let fallback_host_pid = session.host_pid(); + let starting_trace_id = session.trace_manager.get_next_trace_id(); + let process_analyzer = session.process_analyzer.as_mut().ok_or_else(|| { + SessionCompileError::Setup(anyhow::anyhow!( + "Process analyzer is required for script compilation" + )) + })?; + + ghostscope_compiler::compile_script( + script, + process_analyzer, + fallback_host_pid, + Some(starting_trace_id), + compile_options, + ) + .map_err(SessionCompileError::Compile) +} diff --git a/ghostscope/src/script/compiler.rs b/ghostscope/src/script/compiler.rs deleted file mode 100644 index fdde14b1..00000000 --- a/ghostscope/src/script/compiler.rs +++ /dev/null @@ -1,1040 +0,0 @@ -use crate::core::GhostSession; -use anyhow::{Context, Result}; -use ghostscope_compiler::script::Statement; -use ghostscope_loader::GhostScopeLoader; -use ghostscope_ui::events::{ExecutionStatus, ScriptCompilationDetails, ScriptExecutionResult}; -use tracing::{error, info, warn}; - -fn map_compile_error_message(e: &ghostscope_compiler::CompileError) -> String { - e.user_message().into_owned() -} - -fn statement_contains_backtrace(statement: &Statement) -> bool { - match statement { - Statement::Backtrace(_) => true, - Statement::TracePoint { body, .. } | Statement::Block(body) => { - statements_contain_backtrace(body) - } - Statement::If { - then_body, - else_body, - .. - } => { - statements_contain_backtrace(then_body) - || else_body - .as_deref() - .is_some_and(statement_contains_backtrace) - } - _ => false, - } -} - -fn statements_contain_backtrace(statements: &[Statement]) -> bool { - statements.iter().any(statement_contains_backtrace) -} - -fn script_contains_backtrace(script: &str) -> bool { - ghostscope_compiler::script::parser::parse(script) - .map(|program| statements_contain_backtrace(&program.statements)) - .unwrap_or(false) -} - -async fn refresh_runtime_modules_before_compile( - script: &str, - session: &mut GhostSession, - compile_options: &mut ghostscope_compiler::CompileOptions, -) -> Result<()> { - if let Err(e) = session.refresh_pid_runtime_modules_if_needed().await { - warn!( - "Failed to refresh PID runtime modules after sysmon map-change event: {:#}", - e - ); - } - - if session.is_target_mode() && script_contains_backtrace(script) { - session.enable_target_backtrace_runtime_modules(); - if let Err(e) = session.refresh_target_runtime_modules().await { - warn!( - "Failed to refresh target-mode runtime modules before backtrace compilation: {:#}", - e - ); - } - configure_target_mode_backtrace_pid_namespace(session, compile_options); - } - - Ok(()) -} - -fn configure_target_mode_backtrace_pid_namespace( - session: &GhostSession, - compile_options: &mut ghostscope_compiler::CompileOptions, -) { - if !session.is_target_mode() { - return; - } - let Some(target_binary) = session.binary_path() else { - return; - }; - - let target_pids = { - let coordinator = session - .coordinator - .lock() - .expect("coordinator mutex poisoned"); - let mut pids = coordinator - .cached_offsets_for_module(&target_binary) - .into_iter() - .map(|(pid, _, _, _, _)| pid) - .collect::>(); - pids.sort_unstable(); - pids.dedup(); - pids - }; - - let [proc_pid] = target_pids.as_slice() else { - if !target_pids.is_empty() { - tracing::debug!( - "target-mode backtrace PID namespace remains unchanged: matched {} target PIDs for {}", - target_pids.len(), - target_binary - ); - } - return; - }; - - let pid_views = match ghostscope_process::resolve_proc_pid(*proc_pid) { - Ok(pid_views) => pid_views, - Err(error) => { - warn!( - "Failed to resolve target-mode PID views for proc pid {}: {}", - proc_pid, error - ); - return; - } - }; - - let Some(pid_ns) = pid_views - .pid_ns - .filter(|pid_ns| pid_ns.helper_dev_inode().is_some()) - else { - tracing::debug!( - "target-mode backtrace PID namespace remains unchanged: no helper-usable namespace for proc pid {}", - proc_pid - ); - return; - }; - - compile_options.proc_offsets_pid_ns = Some(pid_ns); - record_target_mode_pid_aliases(session, *proc_pid, &pid_views); - - let (pid_ns_dev, pid_ns_inode) = pid_ns - .helper_dev_inode() - .expect("filtered to helper-usable pid namespace"); - info!( - "target-mode proc_module_offsets PID namespace configured from target PID {}: ns_dev={} ns_inode={}", - proc_pid, pid_ns_dev, pid_ns_inode - ); -} - -fn record_target_mode_pid_aliases( - session: &GhostSession, - proc_pid: u32, - pid_views: &ghostscope_process::PidViews, -) { - let mut runtime_pids = Vec::new(); - runtime_pids.push(pid_views.host_pid); - if let Some(container_pid) = pid_views.container_pid { - runtime_pids.push(container_pid); - } - if let Some(chain) = pid_views.nspid_chain.as_ref() { - runtime_pids.extend(chain.iter().copied()); - } - runtime_pids.sort_unstable(); - runtime_pids.dedup(); - - let mut coordinator = session - .coordinator - .lock() - .expect("coordinator mutex poisoned"); - for runtime_pid in runtime_pids { - if runtime_pid == proc_pid { - continue; - } - coordinator.record_runtime_pid_alias(runtime_pid, proc_pid); - match ghostscope_process::pinned_bpf_maps::insert_pid_alias(runtime_pid, proc_pid) { - Ok(()) => info!( - "target-mode PID alias applied runtime_pid={} -> proc_pid={}", - runtime_pid, proc_pid - ), - Err(error) => warn!( - "Failed to write target-mode PID alias runtime_pid={} -> proc_pid={}: {}", - runtime_pid, proc_pid, error - ), - } - } -} - -fn pid_alias_runtime_pid( - session: &GhostSession, - compile_options: &ghostscope_compiler::CompileOptions, -) -> Option { - let resolved_target_ns = session - .pid_views() - .and_then(|pid_views| Some((pid_views.pid_ns_dev()?, pid_views.pid_ns_inode()?))); - let proc_offsets_ns = compile_options - .proc_offsets_pid_ns - .and_then(|pid_ns| pid_ns.helper_dev_inode()); - - // When proc offsets are configured against the resolved target namespace, the - // eBPF-side runtime key is the namespace-local TGID rather than the host-visible - // PID used for `/proc` prefill. - if let (Some(pid_views), Some(target_ns)) = (session.pid_views(), resolved_target_ns) { - if proc_offsets_ns == Some(target_ns) && pid_views.container_pid.is_some() { - return pid_views.container_pid; - } - } - - match compile_options.pid_filter_spec { - Some(ghostscope_compiler::PidFilterSpec::NamespaceTgid { filter_pid, .. }) => { - Some(filter_pid) - } - Some(ghostscope_compiler::PidFilterSpec::HostTgid { filter_pid }) => Some(filter_pid), - None => session.host_pid(), - } -} - -fn pid_alias_pair( - session: &GhostSession, - compile_options: &ghostscope_compiler::CompileOptions, -) -> Option<(u32, u32)> { - let proc_pid = session.proc_pid()?; - let runtime_pid = pid_alias_runtime_pid(session, compile_options)?; - (runtime_pid != proc_pid).then_some((runtime_pid, proc_pid)) -} - -fn apply_pid_alias_for_session( - session: &GhostSession, - compile_options: &ghostscope_compiler::CompileOptions, -) { - if let Some((runtime_pid, proc_pid)) = pid_alias_pair(session, compile_options) { - match ghostscope_process::pinned_bpf_maps::insert_pid_alias(runtime_pid, proc_pid) { - Ok(()) => info!( - "✓ Applied PID alias runtime_pid={} -> proc_pid={}", - runtime_pid, proc_pid - ), - Err(e) => warn!( - "Failed to write PID alias runtime_pid={} -> proc_pid={}: {}", - runtime_pid, proc_pid, e - ), - } - } -} - -fn apply_cached_offsets_for_session_pid( - session: &GhostSession, - _compile_options: &ghostscope_compiler::CompileOptions, -) { - if let Some(proc_pid) = session.proc_pid() { - let items = { - let coordinator = session - .coordinator - .lock() - .expect("coordinator mutex poisoned"); - coordinator - .cached_offsets_with_paths_for_pid(proc_pid) - .map(|entries| entries.to_vec()) - }; - if let Some(items) = items { - use ghostscope_process::pinned_bpf_maps::ProcModuleOffsetsValue; - let adapted: Vec<(u64, ProcModuleOffsetsValue)> = items - .iter() - .map(|entry| { - ( - entry.cookie, - ProcModuleOffsetsValue::new( - entry.offsets.text, - entry.offsets.rodata, - entry.offsets.data, - entry.offsets.bss, - entry.base, - entry.size, - ), - ) - }) - .collect(); - - if let Err(e) = - ghostscope_process::pinned_bpf_maps::insert_offsets_for_pid(proc_pid, &adapted) - { - warn!( - "Failed to write cached offsets to pinned map for PID {}: {}", - proc_pid, e - ); - } else { - info!( - "✓ Applied {} cached offsets to pinned map for PID {}", - adapted.len(), - proc_pid - ); - } - if let Err(e) = - ghostscope_process::pinned_bpf_maps::replace_ranges_for_pid(proc_pid, &adapted) - { - warn!( - "Failed to write cached module ranges to pinned map for PID {}: {}", - proc_pid, e - ); - } - } - } -} - -/// Create and attach a loader for a single uprobe configuration -async fn create_and_attach_loader( - config: &ghostscope_compiler::UProbeConfig, - attach_pid: Option, - session: &mut crate::core::GhostSession, - compile_options: &ghostscope_compiler::CompileOptions, -) -> Result { - // Create a new loader for this uprobe configuration - info!( - "Creating new eBPF loader with {} bytes of bytecode for trace_id {}", - config.ebpf_bytecode.len(), - config.assigned_trace_id - ); - - // Ensure the per-process pinned proc_module_offsets map exists before creating the loader - let max_entries = session - .config - .as_ref() - .map(|c| c.ebpf_config.proc_module_offsets_max_entries as u32) - .unwrap_or(4096); - let pin_path = ghostscope_process::pinned_bpf_maps::proc_offsets_pin_path() - .context("Failed to resolve pinned proc_module_offsets map path")?; - if let Err(e) = - ghostscope_process::pinned_bpf_maps::ensure_pinned_proc_offsets_exists(max_entries) - { - error!( - "Failed to ensure pinned proc_module_offsets map exists at {} ({} entries): {:#}", - pin_path.display(), - max_entries, - e - ); - return Err(e.context(format!( - "Unable to prepare pinned proc_module_offsets map at {}", - pin_path.display() - ))); - } - let alias_pin_path = ghostscope_process::pinned_bpf_maps::pid_aliases_pin_path() - .context("Failed to resolve pinned pid_aliases map path")?; - if let Err(e) = - ghostscope_process::pinned_bpf_maps::ensure_pinned_pid_aliases_exists(max_entries) - { - error!( - "Failed to ensure pinned pid_aliases map exists at {} ({} entries): {:#}", - alias_pin_path.display(), - max_entries, - e - ); - return Err(e.context(format!( - "Unable to prepare pinned pid_aliases map at {}", - alias_pin_path.display() - ))); - } - let range_meta_pin_path = - ghostscope_process::pinned_bpf_maps::proc_module_range_meta_pin_path() - .context("Failed to resolve pinned proc_module_range_meta map path")?; - if let Err(e) = - ghostscope_process::pinned_bpf_maps::ensure_pinned_proc_module_ranges_exist(max_entries) - { - error!( - "Failed to ensure pinned module range maps at {} ({} entries): {:#}", - range_meta_pin_path.display(), - max_entries, - e - ); - return Err(e.context(format!( - "Unable to prepare pinned module range maps at {}", - range_meta_pin_path.display() - ))); - } - let share_backtrace_cfi_maps = !config.backtrace_module_row_ranges.is_empty(); - if share_backtrace_cfi_maps { - let unwind_row_entries = compile_options - .backtrace_unwind_rows_max_entries - .max(config.backtrace_unwind_rows.len() as u32) - .max(1); - let module_entries = u32::try_from(compile_options.proc_module_offsets_max_entries) - .unwrap_or(u32::MAX) - .max(1); - let rows_pin_path = ghostscope_process::pinned_bpf_maps::bt_unwind_rows_pin_path() - .context("Failed to resolve pinned bt_unwind_rows map path")?; - if let Err(e) = ghostscope_process::pinned_bpf_maps::ensure_pinned_backtrace_cfi_maps_exist( - unwind_row_entries, - module_entries, - ) { - error!( - "Failed to ensure pinned backtrace CFI maps at {} (rows={}, modules={}): {:#}", - rows_pin_path.display(), - unwind_row_entries, - module_entries, - e - ); - return Err(e.context(format!( - "Unable to prepare pinned backtrace CFI maps at {}", - rows_pin_path.display() - ))); - } - } - - let mut loader = GhostScopeLoader::new_with_shared_backtrace_maps( - &config.ebpf_bytecode, - share_backtrace_cfi_maps, - ) - .context("Failed to create eBPF loader for uprobe config")?; - - // Apply PerfEventArray page count from config (for kernels without RingBuf or forced Perf mode) - if let Some(cfg) = &session.config { - loader.set_perf_page_count(cfg.ebpf_config.perf_page_count); - tracing::info!( - "Configured PerfEventArray page count: {} pages per CPU", - cfg.ebpf_config.perf_page_count - ); - } - - // Set the TraceContext for trace event parsing - info!( - "Setting TraceContext for loader: {} strings, {} variables", - config.trace_context.string_count(), - config.trace_context.variable_name_count() - ); - loader.set_trace_context(config.trace_context.clone()); - loader - .populate_backtrace_unwind_rows_and_module_row_ranges( - &config.backtrace_unwind_rows, - &config.backtrace_module_row_ranges, - ) - .context("Failed to populate DWARF backtrace unwind rows")?; - loader - .register_backtrace_tail_call_program( - config - .backtrace_tail_call_program - .as_ref() - .map(|program| program.step_program_name.as_str()), - ) - .context("Failed to register bt tail-call program")?; - - // In -t mode (no attach PID), perform module prefill once per session and apply to this loader - if attach_pid.is_none() { - let (prefilled, entries) = { - let mut coordinator = session - .coordinator - .lock() - .expect("coordinator mutex poisoned"); - let prefilled = coordinator - .ensure_prefill_module(&config.binary_path) - .unwrap_or(0); - let entries = coordinator.cached_offsets_for_module(&config.binary_path); - (prefilled, entries) - }; - tracing::info!( - "Coordinator cached offsets for {} pid(s) for module {}", - prefilled, - config.binary_path - ); - // Apply cached offsets for this module to the loader's map - if !entries.is_empty() { - use ghostscope_process::pinned_bpf_maps::ProcModuleOffsetsValue; - // Group by pid for efficient batch insert - use std::collections::HashMap; - let mut by_pid: HashMap> = HashMap::new(); - for (pid, cookie, off, base, size) in entries { - by_pid.entry(pid).or_default().push(( - cookie, - ProcModuleOffsetsValue::new( - off.text, off.rodata, off.data, off.bss, base, size, - ), - )); - } - let mut total = 0usize; - for (pid, items) in by_pid { - if let Err(e) = - ghostscope_process::pinned_bpf_maps::insert_offsets_for_pid(pid, &items) - { - tracing::warn!( - "Failed to write offsets to pinned map for PID {}: {}", - pid, - e - ); - } else { - total += items.len(); - } - // Loader no longer manages offsets map; only pinned map is authoritative - } - tracing::info!( - "Applied {} cached offset entries to pinned map for module {}", - total, - config.binary_path - ); - } - } - - apply_pid_alias_for_session(session, compile_options); - // Populate PID-mode module offsets before attaching. Once the uprobe is - // attached, a running target can hit it immediately. - apply_cached_offsets_for_session_pid(session, compile_options); - - if let Some(uprobe_offset) = config.uprobe_offset { - if let Some(ref function_name) = config.function_name { - // Function-based attachment with calculated offset - info!( - "Attaching to function '{}' at offset 0x{:x} in {} using eBPF function '{}'", - function_name, uprobe_offset, config.binary_path, config.ebpf_function_name - ); - - loader.attach_uprobe_with_program_name( - &config.binary_path, - function_name, - Some(uprobe_offset), - attach_pid.map(|p| p as i32), - Some(&config.ebpf_function_name), - )?; - } else { - // Direct address attachment - info!( - "Attaching to address 0x{:x} in {} using eBPF function '{}'", - uprobe_offset, config.binary_path, config.ebpf_function_name - ); - - loader.attach_uprobe_with_program_name( - &config.binary_path, - &format!("0x{uprobe_offset:x}"), // Use address as function name - Some(uprobe_offset), - attach_pid.map(|p| p as i32), - Some(&config.ebpf_function_name), - )?; - } - } else { - return Err(anyhow::anyhow!("No uprobe offset available in config")); - } - - Ok(loader) -} - -/// Compile and load script specifically for TUI mode with detailed execution results -/// This function collects detailed results for each PC/target and returns comprehensive status -pub async fn compile_and_load_script_for_tui( - script: &str, - session: &mut GhostSession, - compile_options: &ghostscope_compiler::CompileOptions, -) -> Result { - let mut compile_options = compile_options.clone(); - refresh_runtime_modules_before_compile(script, session, &mut compile_options).await?; - - let fallback_host_pid = session.host_pid(); - - // Step 1: Validate process analyzer availability - let process_analyzer = session - .process_analyzer - .as_mut() - .ok_or_else(|| anyhow::anyhow!("Process analyzer is required for script compilation"))?; - - // Step 2: Get binary path early for error handling - let binary_path = if let Some(main_module) = process_analyzer.get_main_executable() { - main_module.path.clone() - } else { - return Err(anyhow::anyhow!("No main executable found in process")); - }; - - // Step 3: Get starting trace ID from trace manager - let starting_trace_id = session.trace_manager.get_next_trace_id(); - - // Step 4: Use provided compile options directly - // Compile script using DWARF analyzer with proper starting trace ID - let compilation_result = match ghostscope_compiler::compile_script( - script, - process_analyzer, - fallback_host_pid, - Some(starting_trace_id), // Use trace_manager's next available ID - &compile_options, - ) { - Ok(result) => result, - Err(e) => { - let friendly = map_compile_error_message(&e); - error!("Script compilation failed: {}", friendly); - return Ok(ScriptCompilationDetails { - trace_ids: vec![], - results: vec![ScriptExecutionResult { - pc_address: 0, - target_name: "compilation_failed".to_string(), - binary_path, - status: ExecutionStatus::Failed(format!("Compilation error: {friendly}")), - source_file: None, - source_line: None, - is_inline: None, - }], - total_count: 1, - success_count: 0, - failed_count: 1, - }); - } - }; - - info!( - "✓ Script compilation successful: {} uprobe configs generated", - compilation_result.uprobe_configs.len() - ); - - // Step 4: Process compilation results and create execution details - let mut results = Vec::new(); - let mut trace_ids = Vec::new(); - let mut success_count = 0; - let mut failed_count = 0; - - // Process successful configurations - for config in compilation_result.uprobe_configs.iter() { - let trace_id = config.assigned_trace_id; // Use the trace_id assigned by compiler - trace_ids.push(trace_id); - - // Compute source location and inline classification for this target - let (source_file, source_line, is_inline) = { - let addr = config.function_address.unwrap_or(0); - let module_address = ghostscope_dwarf::ModuleAddress::new( - std::path::PathBuf::from(&config.binary_path), - addr, - ); - let src = process_analyzer.lookup_source_location(&module_address); - let inline = process_analyzer.is_inline_at(&module_address); - ( - src.as_ref().map(|s| s.file_path.clone()), - src.as_ref().map(|s| s.line_number), - inline, - ) - }; - - results.push(ScriptExecutionResult { - pc_address: config.function_address.unwrap_or(0), - target_name: config - .function_name - .clone() - .unwrap_or_else(|| format!("{:#x}", config.function_address.unwrap_or(0))), - binary_path: config.binary_path.clone(), - status: ExecutionStatus::Success, - source_file, - source_line, - is_inline, - }); - success_count += 1; - } - - // Process failed targets - for failed in &compilation_result.failed_targets { - results.push(ScriptExecutionResult { - pc_address: failed.pc_address, - target_name: failed.target_name.clone(), - binary_path: binary_path.clone(), - status: ExecutionStatus::Failed(failed.error_message.clone()), - source_file: None, - source_line: None, - is_inline: None, - }); - failed_count += 1; - } - - info!( - "Compilation summary: {} successful, {} failed", - success_count, failed_count - ); - - // Ensure -p offsets are cached once per session - if let Some(proc_pid) = session.proc_pid() { - let result = { - let mut coordinator = session - .coordinator - .lock() - .expect("coordinator mutex poisoned"); - coordinator.ensure_prefill_pid(proc_pid) - }; - match result { - Ok(count) => info!( - "Coordinator cached {} module offset entries for PID {}", - count, proc_pid - ), - Err(e) => warn!( - "Failed to compute section offsets via coordinator: {} (globals may show OffsetsUnavailable)", - e - ), - } - } - - // Step 5: If we have successful configurations, attach uprobes and register traces - if !compilation_result.uprobe_configs.is_empty() { - // Prepare uprobe configurations with binary path - let uprobe_configs = compilation_result.uprobe_configs; - - info!("Attaching {} uprobe configurations", uprobe_configs.len()); - for (i, config) in uprobe_configs.iter().enumerate() { - let fallback_name = format!("{:#x}", config.function_address.unwrap_or(0)); - info!( - " Config {}: {:?} -> 0x{:x} (trace_id: {})", - i, - config.function_name.as_ref().unwrap_or(&fallback_name), - config.uprobe_offset.unwrap_or(0), - config.assigned_trace_id - ); - } - - // Step 6: Attach uprobes individually and create traces with their own loaders - let mut attached_count = 0; - for config in &uprobe_configs { - let addr_disp = config.function_address.unwrap_or(0); - let target_display = config - .function_name - .clone() - .unwrap_or_else(|| format!("{addr_disp:#x}")); - - // Create individual loader for this config - match create_and_attach_loader(config, session.attach_pid(), session, &compile_options) - .await - { - Ok(loader) => { - info!( - "✓ Successfully attached uprobe for trace_id {}", - config.assigned_trace_id - ); - - // Register trace with its own loader - let _registered_trace_id = session.trace_manager.add_trace_with_id( - crate::trace::manager::AddTraceParams { - trace_id: config.assigned_trace_id, - target: target_display.clone(), - script_content: script.to_string(), - pc: config.function_address.unwrap_or(0), - binary_path: config.binary_path.clone(), - target_display: target_display.clone(), - pid_context: crate::trace::instance::TracePidContext { - attach_pid: session.attach_pid(), - host_pid: session.host_pid(), - proc_pid: session.proc_pid(), - }, - loader: Some(loader), - ebpf_function_name: format!( - "gs_{}_{}_{}", - session.host_pid().unwrap_or(0), - target_display, - config.assigned_trace_id - ), - address_global_index: config.resolved_address_index, - }, - ); - - // Enable the trace immediately since we just attached it - if let Err(e) = session.trace_manager.enable_trace(config.assigned_trace_id) { - warn!( - "Failed to enable trace_id {}: {}", - config.assigned_trace_id, e - ); - } else { - info!( - "✓ Registered and enabled trace_id {} with trace manager", - config.assigned_trace_id - ); - attached_count += 1; - } - } - Err(e) => { - error!( - "Failed to attach uprobe for trace_id {}: {:#}", - config.assigned_trace_id, e - ); - tracing::info!( - "Attachment hints: check privileges, target binary availability, PID validity, and function addresses if needed." - ); - // Update corresponding result to failed - for result in &mut results { - if result.pc_address == config.function_address.unwrap_or(0) { - result.status = - ExecutionStatus::Failed(format!("Failed to attach uprobe: {e:#}")); - success_count -= 1; - failed_count += 1; - break; - } - } - } - } - } - - if attached_count > 0 { - info!( - "✓ Successfully attached {} of {} uprobes", - attached_count, - uprobe_configs.len() - ); - } else { - warn!("No uprobes were successfully attached"); - } - } - - Ok(ScriptCompilationDetails { - trace_ids, - results, - total_count: success_count + failed_count, - success_count, - failed_count, - }) -} - -/// Compile a script for command line mode without attaching uprobes. -pub fn compile_script_for_cli( - script: &str, - session: &mut GhostSession, - compile_options: &ghostscope_compiler::CompileOptions, -) -> Result { - let fallback_host_pid = session.host_pid(); - - info!("Starting unified script compilation with DWARF integration..."); - - // Step 1: Validate process analyzer - let process_analyzer = session - .process_analyzer - .as_mut() - .ok_or_else(|| anyhow::anyhow!("Process analyzer is required for script compilation"))?; - - // Step 2: Get starting trace ID from trace manager and use unified compilation interface with DwarfAnalyzer - let starting_trace_id = session.trace_manager.get_next_trace_id(); - - // Use provided compile options directly - let compilation_result = match ghostscope_compiler::compile_script( - script, - process_analyzer, - fallback_host_pid, - Some(starting_trace_id), // Use trace_manager's next available ID - compile_options, - ) { - Ok(result) => result, - Err(e) => { - let friendly = map_compile_error_message(&e); - error!("Script compilation failed: {}", friendly); - return Err(anyhow::anyhow!( - "Script compilation failed: {friendly}. Please check your script syntax and try again." - )); - } - }; - - info!( - "✓ Script compilation successful: {} trace points found, {} uprobe configs generated", - compilation_result.trace_count, - compilation_result.uprobe_configs.len() - ); - info!("Target info: {}", compilation_result.target_info); - - // Report failed targets if any - if !compilation_result.failed_targets.is_empty() { - warn!("Some targets failed to compile:"); - for failed in &compilation_result.failed_targets { - warn!( - " {} at 0x{:x}: {}", - failed.target_name, failed.pc_address, failed.error_message - ); - } - } - - Ok(compilation_result) -} - -/// Compile and load script for command line mode using session.command_loaders -pub async fn compile_and_load_script_for_cli( - script: &str, - session: &mut GhostSession, - compile_options: &ghostscope_compiler::CompileOptions, -) -> Result<()> { - let mut compile_options = compile_options.clone(); - refresh_runtime_modules_before_compile(script, session, &mut compile_options).await?; - - let compilation_result = compile_script_for_cli(script, session, &compile_options)?; - - // Ensure -p offsets are cached once per session - if let Some(proc_pid) = session.proc_pid() { - let result = { - let mut coordinator = session - .coordinator - .lock() - .expect("coordinator mutex poisoned"); - coordinator.ensure_prefill_pid(proc_pid) - }; - match result { - Ok(count) => info!( - "Coordinator cached {} module offset entries for PID {}", - count, proc_pid - ), - Err(e) => warn!( - "Failed to compute section offsets via coordinator: {} (globals may show OffsetsUnavailable)", - e - ), - } - } - - // Step 3: Prepare and attach uprobe configurations - let ghostscope_compiler::CompilationResult { - uprobe_configs, - failed_targets, - .. - } = compilation_result; - - if uprobe_configs.is_empty() { - // If we had resolved targets but all failed to compile, surface the real compile errors - if !failed_targets.is_empty() { - let mut details = String::new(); - for failed in &failed_targets { - let _ = std::fmt::Write::write_fmt( - &mut details, - format_args!( - " - {} at 0x{:x}: {}\n", - failed.target_name, failed.pc_address, failed.error_message - ), - ); - } - let full = format!( - "No uprobe configurations created because all {} target(s) failed to compile.\n\nFailed targets:\n{}\n\nTip: fix the reported compile-time errors above (e.g., avoid struct/union/array arithmetic; select a scalar field or use '&expr + ' in an alias/address context).", - failed_targets.len(), - details - ); - // Log the full multi-line error to ensure it appears in stderr when logging is enabled - error!("{}", full); - return Err(anyhow::anyhow!(full)); - } - - // Check if we have debug info - this is checked during module loading - let available_functions = session.list_functions(); - - if available_functions.is_empty() { - return Err(anyhow::anyhow!( - "No debug information found in any module!\n\ - \n\ - The target binary and its libraries are stripped or compiled without debug symbols.\n\ - GhostScope requires debug information (DWARF) to:\n\ - - Locate functions by name\n\ - - Analyze variable types and locations\n\ - - Map source lines to addresses\n\ - \n\ - Solutions:\n\ - 1. Recompile your target with -g flag: gcc -g your_program.c -o your_program\n\ - 2. Install debug symbol packages (e.g., libc6-dbg on Debian/Ubuntu)\n\ - 3. For stripped binaries, use objcopy to create separate debug files:\n\ - objcopy --only-keep-debug binary binary.debug\n\ - objcopy --add-gnu-debuglink=binary.debug binary" - )); - } - - return Err(anyhow::anyhow!( - "No uprobe configurations created - the functions referenced in your script were not found.\n\ - \n\ - Possible reasons:\n\ - - Function names are misspelled (check available functions below)\n\ - - Functions don't exist in the target binary\n\ - - Functions are from libraries that aren't loaded yet\n\ - \n\ - Available functions (first 10):\n{}\n\ - \n\ - Tip: Run GhostScope in TUI mode to browse all available functions", - available_functions.iter().take(10).map(|f| format!(" - {f}")).collect::>().join("\n") - )); - } - - info!("Attaching {} uprobe configurations", uprobe_configs.len()); - for (i, config) in uprobe_configs.iter().enumerate() { - let fallback_name = format!("{:#x}", config.function_address.unwrap_or(0)); - info!( - " Config {}: {:?} -> 0x{:x}", - i, - config.function_name.as_ref().unwrap_or(&fallback_name), - config.uprobe_offset.unwrap_or(0) - ); - } - - // Step 4: Attach uprobes individually and create traces with their own loaders - let mut attached_count = 0; - for config in &uprobe_configs { - let addr_disp = config.function_address.unwrap_or(0); - let target_display = config - .function_name - .clone() - .unwrap_or_else(|| format!("{addr_disp:#x}")); - - // Create individual loader for this config - match create_and_attach_loader(config, session.attach_pid(), session, &compile_options) - .await - { - Ok(loader) => { - info!( - "✓ Successfully attached uprobe for trace_id {}", - config.assigned_trace_id - ); - - // Register trace with its own loader (CLI mode) - let _registered_trace_id = session.trace_manager.add_trace_with_id( - crate::trace::manager::AddTraceParams { - trace_id: config.assigned_trace_id, - target: target_display.clone(), - script_content: script.to_string(), - pc: config.function_address.unwrap_or(0), - binary_path: config.binary_path.clone(), - target_display: target_display.clone(), - pid_context: crate::trace::instance::TracePidContext { - attach_pid: session.attach_pid(), - host_pid: session.host_pid(), - proc_pid: session.proc_pid(), - }, - loader: Some(loader), - ebpf_function_name: format!( - "gs_{}_{}_{}", - session.host_pid().unwrap_or(0), - target_display, - config.assigned_trace_id - ), - address_global_index: config.resolved_address_index, - }, - ); - - // Enable the trace immediately since we just attached it - if let Err(e) = session.trace_manager.enable_trace(config.assigned_trace_id) { - warn!( - "Failed to enable trace_id {}: {}", - config.assigned_trace_id, e - ); - } else { - info!( - "✓ Registered and enabled trace_id {} with trace manager", - config.assigned_trace_id - ); - attached_count += 1; - } - } - Err(e) => { - error!( - "Failed to attach uprobe for trace_id {}: {:#}", - config.assigned_trace_id, e - ); - tracing::info!( - "Attachment hints: check privileges, target binary availability, PID validity, and function addresses if needed." - ); - return Err(e.context(format!( - "Failed to attach uprobe for trace_id {}", - config.assigned_trace_id - ))); - } - } - } - - if attached_count > 0 { - info!( - "✓ Successfully attached {} of {} uprobes", - attached_count, - uprobe_configs.len() - ); - } else { - return Err(anyhow::anyhow!("No uprobes were successfully attached")); - } - - Ok(()) -} diff --git a/ghostscope/src/script/mod.rs b/ghostscope/src/script/mod.rs index 6efebfa4..44319b49 100644 --- a/ghostscope/src/script/mod.rs +++ b/ghostscope/src/script/mod.rs @@ -1,8 +1,12 @@ //! Script module - handles script compilation and processing -pub mod compiler; +mod attach; +mod cli; +mod compile; +mod runtime_maps; +mod runtime_prep; +mod tui; // Re-export main functions for convenience -pub use compiler::{ - compile_and_load_script_for_cli, compile_and_load_script_for_tui, compile_script_for_cli, -}; +pub use cli::{compile_and_load_script_for_cli, compile_script_for_cli}; +pub use tui::compile_and_load_script_for_tui; diff --git a/ghostscope/src/script/runtime_maps.rs b/ghostscope/src/script/runtime_maps.rs new file mode 100644 index 00000000..f8c3acc2 --- /dev/null +++ b/ghostscope/src/script/runtime_maps.rs @@ -0,0 +1,136 @@ +use crate::core::GhostSession; +use tracing::{info, warn}; + +fn pid_alias_runtime_pid( + session: &GhostSession, + compile_options: &ghostscope_compiler::CompileOptions, +) -> Option { + let resolved_target_ns = session + .pid_views() + .and_then(|pid_views| Some((pid_views.pid_ns_dev()?, pid_views.pid_ns_inode()?))); + let proc_offsets_ns = compile_options + .proc_offsets_pid_ns + .and_then(|pid_ns| pid_ns.helper_dev_inode()); + + // When proc offsets are configured against the resolved target namespace, the + // eBPF-side runtime key is the namespace-local TGID rather than the host-visible + // PID used for `/proc` prefill. + if let (Some(pid_views), Some(target_ns)) = (session.pid_views(), resolved_target_ns) { + if proc_offsets_ns == Some(target_ns) && pid_views.container_pid.is_some() { + return pid_views.container_pid; + } + } + + match compile_options.pid_filter_spec { + Some(ghostscope_compiler::PidFilterSpec::NamespaceTgid { filter_pid, .. }) => { + Some(filter_pid) + } + Some(ghostscope_compiler::PidFilterSpec::HostTgid { filter_pid }) => Some(filter_pid), + None => session.host_pid(), + } +} + +fn pid_alias_pair( + session: &GhostSession, + compile_options: &ghostscope_compiler::CompileOptions, +) -> Option<(u32, u32)> { + let proc_pid = session.proc_pid()?; + let runtime_pid = pid_alias_runtime_pid(session, compile_options)?; + (runtime_pid != proc_pid).then_some((runtime_pid, proc_pid)) +} + +pub(super) fn apply_pid_alias_for_session( + session: &GhostSession, + compile_options: &ghostscope_compiler::CompileOptions, +) { + if let Some((runtime_pid, proc_pid)) = pid_alias_pair(session, compile_options) { + match ghostscope_process::pinned_bpf_maps::insert_pid_alias(runtime_pid, proc_pid) { + Ok(()) => info!( + "✓ Applied PID alias runtime_pid={} -> proc_pid={}", + runtime_pid, proc_pid + ), + Err(e) => warn!( + "Failed to write PID alias runtime_pid={} -> proc_pid={}: {}", + runtime_pid, proc_pid, e + ), + } + } +} + +pub(super) fn ensure_prefill_for_session_pid(session: &GhostSession) { + if let Some(proc_pid) = session.proc_pid() { + let result = { + let mut coordinator = session + .coordinator + .lock() + .expect("coordinator mutex poisoned"); + coordinator.ensure_prefill_pid(proc_pid) + }; + match result { + Ok(count) => info!( + "Coordinator cached {} module offset entries for PID {}", + count, proc_pid + ), + Err(e) => warn!( + "Failed to compute section offsets via coordinator: {} (globals may show OffsetsUnavailable)", + e + ), + } + } +} + +pub(super) fn apply_cached_offsets_for_session_pid(session: &GhostSession) { + if let Some(proc_pid) = session.proc_pid() { + let items = { + let coordinator = session + .coordinator + .lock() + .expect("coordinator mutex poisoned"); + coordinator + .cached_offsets_with_paths_for_pid(proc_pid) + .map(|entries| entries.to_vec()) + }; + if let Some(items) = items { + use ghostscope_process::pinned_bpf_maps::ProcModuleOffsetsValue; + let adapted: Vec<(u64, ProcModuleOffsetsValue)> = items + .iter() + .map(|entry| { + ( + entry.cookie, + ProcModuleOffsetsValue::new( + entry.offsets.text, + entry.offsets.rodata, + entry.offsets.data, + entry.offsets.bss, + entry.base, + entry.size, + ), + ) + }) + .collect(); + + if let Err(e) = + ghostscope_process::pinned_bpf_maps::insert_offsets_for_pid(proc_pid, &adapted) + { + warn!( + "Failed to write cached offsets to pinned map for PID {}: {}", + proc_pid, e + ); + } else { + info!( + "✓ Applied {} cached offsets to pinned map for PID {}", + adapted.len(), + proc_pid + ); + } + if let Err(e) = + ghostscope_process::pinned_bpf_maps::replace_ranges_for_pid(proc_pid, &adapted) + { + warn!( + "Failed to write cached module ranges to pinned map for PID {}: {}", + proc_pid, e + ); + } + } + } +} diff --git a/ghostscope/src/script/runtime_prep.rs b/ghostscope/src/script/runtime_prep.rs new file mode 100644 index 00000000..86994e12 --- /dev/null +++ b/ghostscope/src/script/runtime_prep.rs @@ -0,0 +1,169 @@ +use crate::core::GhostSession; +use anyhow::Result; +use ghostscope_compiler::script::Statement; +use tracing::{info, warn}; + +fn statement_contains_backtrace(statement: &Statement) -> bool { + match statement { + Statement::Backtrace(_) => true, + Statement::TracePoint { body, .. } | Statement::Block(body) => { + statements_contain_backtrace(body) + } + Statement::If { + then_body, + else_body, + .. + } => { + statements_contain_backtrace(then_body) + || else_body + .as_deref() + .is_some_and(statement_contains_backtrace) + } + _ => false, + } +} + +fn statements_contain_backtrace(statements: &[Statement]) -> bool { + statements.iter().any(statement_contains_backtrace) +} + +fn script_contains_backtrace(script: &str) -> bool { + ghostscope_compiler::script::parser::parse(script) + .map(|program| statements_contain_backtrace(&program.statements)) + .unwrap_or(false) +} + +pub(super) async fn refresh_runtime_modules_before_compile( + script: &str, + session: &mut GhostSession, + compile_options: &mut ghostscope_compiler::CompileOptions, +) -> Result<()> { + if let Err(e) = session.refresh_pid_runtime_modules_if_needed().await { + warn!( + "Failed to refresh PID runtime modules after sysmon map-change event: {:#}", + e + ); + } + + if session.is_target_mode() && script_contains_backtrace(script) { + session.enable_target_backtrace_runtime_modules(); + if let Err(e) = session.refresh_target_runtime_modules().await { + warn!( + "Failed to refresh target-mode runtime modules before backtrace compilation: {:#}", + e + ); + } + configure_target_mode_backtrace_pid_namespace(session, compile_options); + } + + Ok(()) +} + +fn configure_target_mode_backtrace_pid_namespace( + session: &GhostSession, + compile_options: &mut ghostscope_compiler::CompileOptions, +) { + if !session.is_target_mode() { + return; + } + let Some(target_binary) = session.binary_path() else { + return; + }; + + let target_pids = { + let coordinator = session + .coordinator + .lock() + .expect("coordinator mutex poisoned"); + let mut pids = coordinator + .cached_offsets_for_module(&target_binary) + .into_iter() + .map(|(pid, _, _, _, _)| pid) + .collect::>(); + pids.sort_unstable(); + pids.dedup(); + pids + }; + + let [proc_pid] = target_pids.as_slice() else { + if !target_pids.is_empty() { + tracing::debug!( + "target-mode backtrace PID namespace remains unchanged: matched {} target PIDs for {}", + target_pids.len(), + target_binary + ); + } + return; + }; + + let pid_views = match ghostscope_process::resolve_proc_pid(*proc_pid) { + Ok(pid_views) => pid_views, + Err(error) => { + warn!( + "Failed to resolve target-mode PID views for proc pid {}: {}", + proc_pid, error + ); + return; + } + }; + + let Some(pid_ns) = pid_views + .pid_ns + .filter(|pid_ns| pid_ns.helper_dev_inode().is_some()) + else { + tracing::debug!( + "target-mode backtrace PID namespace remains unchanged: no helper-usable namespace for proc pid {}", + proc_pid + ); + return; + }; + + compile_options.proc_offsets_pid_ns = Some(pid_ns); + record_target_mode_pid_aliases(session, *proc_pid, &pid_views); + + let (pid_ns_dev, pid_ns_inode) = pid_ns + .helper_dev_inode() + .expect("filtered to helper-usable pid namespace"); + info!( + "target-mode proc_module_offsets PID namespace configured from target PID {}: ns_dev={} ns_inode={}", + proc_pid, pid_ns_dev, pid_ns_inode + ); +} + +fn record_target_mode_pid_aliases( + session: &GhostSession, + proc_pid: u32, + pid_views: &ghostscope_process::PidViews, +) { + let mut runtime_pids = Vec::new(); + runtime_pids.push(pid_views.host_pid); + if let Some(container_pid) = pid_views.container_pid { + runtime_pids.push(container_pid); + } + if let Some(chain) = pid_views.nspid_chain.as_ref() { + runtime_pids.extend(chain.iter().copied()); + } + runtime_pids.sort_unstable(); + runtime_pids.dedup(); + + let mut coordinator = session + .coordinator + .lock() + .expect("coordinator mutex poisoned"); + for runtime_pid in runtime_pids { + if runtime_pid == proc_pid { + continue; + } + coordinator.record_runtime_pid_alias(runtime_pid, proc_pid); + match ghostscope_process::pinned_bpf_maps::insert_pid_alias(runtime_pid, proc_pid) { + Ok(()) => info!( + "target-mode PID alias applied runtime_pid={} -> proc_pid={}", + runtime_pid, proc_pid + ), + Err(error) => warn!( + "Failed to write target-mode PID alias runtime_pid={} -> proc_pid={}: {}", + runtime_pid, proc_pid, error + ), + } + } +} diff --git a/ghostscope/src/script/tui.rs b/ghostscope/src/script/tui.rs new file mode 100644 index 00000000..93a7715c --- /dev/null +++ b/ghostscope/src/script/tui.rs @@ -0,0 +1,185 @@ +use crate::core::GhostSession; +use anyhow::Result; +use ghostscope_ui::events::{ExecutionStatus, ScriptCompilationDetails, ScriptExecutionResult}; +use tracing::{error, info, warn}; + +use super::attach::{ + create_and_attach_loader, log_attachment_hints, log_uprobe_configs, register_attached_trace, +}; +use super::compile::{compile_script_with_session, main_executable_path, SessionCompileError}; +use super::runtime_maps::ensure_prefill_for_session_pid; +use super::runtime_prep::refresh_runtime_modules_before_compile; + +/// Compile and load script specifically for TUI mode with detailed execution results. +pub async fn compile_and_load_script_for_tui( + script: &str, + session: &mut GhostSession, + compile_options: &ghostscope_compiler::CompileOptions, +) -> Result { + let mut compile_options = compile_options.clone(); + refresh_runtime_modules_before_compile(script, session, &mut compile_options).await?; + + let binary_path = main_executable_path(session)?; + let compilation_result = match compile_script_with_session(script, session, &compile_options) { + Ok(result) => result, + Err(SessionCompileError::Compile(e)) => { + let friendly = e.user_message().into_owned(); + error!("Script compilation failed: {}", friendly); + return Ok(compilation_failed_details(binary_path, friendly)); + } + Err(SessionCompileError::Setup(e)) => return Err(e), + }; + + info!( + "✓ Script compilation successful: {} uprobe configs generated", + compilation_result.uprobe_configs.len() + ); + + let process_analyzer = session + .process_analyzer + .as_ref() + .ok_or_else(|| anyhow::anyhow!("Process analyzer is required for script compilation"))?; + let (mut results, trace_ids, mut success_count, mut failed_count) = + build_tui_results(&compilation_result, &binary_path, process_analyzer); + + info!( + "Compilation summary: {} successful, {} failed", + success_count, failed_count + ); + + ensure_prefill_for_session_pid(session); + + if !compilation_result.uprobe_configs.is_empty() { + let uprobe_configs = compilation_result.uprobe_configs; + log_uprobe_configs(&uprobe_configs, true); + + let mut attached_count = 0; + for config in &uprobe_configs { + match create_and_attach_loader(config, session.attach_pid(), session, &compile_options) + .await + { + Ok(loader) => { + info!( + "✓ Successfully attached uprobe for trace_id {}", + config.assigned_trace_id + ); + if register_attached_trace(session, script, config, loader) { + attached_count += 1; + } + } + Err(e) => { + error!( + "Failed to attach uprobe for trace_id {}: {:#}", + config.assigned_trace_id, e + ); + log_attachment_hints(); + for result in &mut results { + if result.pc_address == config.function_address.unwrap_or(0) { + result.status = + ExecutionStatus::Failed(format!("Failed to attach uprobe: {e:#}")); + success_count -= 1; + failed_count += 1; + break; + } + } + } + } + } + + if attached_count > 0 { + info!( + "✓ Successfully attached {} of {} uprobes", + attached_count, + uprobe_configs.len() + ); + } else { + warn!("No uprobes were successfully attached"); + } + } + + Ok(ScriptCompilationDetails { + trace_ids, + results, + total_count: success_count + failed_count, + success_count, + failed_count, + }) +} + +fn compilation_failed_details(binary_path: String, friendly: String) -> ScriptCompilationDetails { + ScriptCompilationDetails { + trace_ids: vec![], + results: vec![ScriptExecutionResult { + pc_address: 0, + target_name: "compilation_failed".to_string(), + binary_path, + status: ExecutionStatus::Failed(format!("Compilation error: {friendly}")), + source_file: None, + source_line: None, + is_inline: None, + }], + total_count: 1, + success_count: 0, + failed_count: 1, + } +} + +fn build_tui_results( + compilation_result: &ghostscope_compiler::CompilationResult, + binary_path: &str, + process_analyzer: &ghostscope_dwarf::DwarfAnalyzer, +) -> (Vec, Vec, usize, usize) { + let mut results = Vec::new(); + let mut trace_ids = Vec::new(); + let mut success_count = 0; + let mut failed_count = 0; + + for config in compilation_result.uprobe_configs.iter() { + let trace_id = config.assigned_trace_id; + trace_ids.push(trace_id); + + let (source_file, source_line, is_inline) = { + let addr = config.function_address.unwrap_or(0); + let module_address = ghostscope_dwarf::ModuleAddress::new( + std::path::PathBuf::from(&config.binary_path), + addr, + ); + let src = process_analyzer.lookup_source_location(&module_address); + let inline = process_analyzer.is_inline_at(&module_address); + ( + src.as_ref().map(|s| s.file_path.clone()), + src.as_ref().map(|s| s.line_number), + inline, + ) + }; + + results.push(ScriptExecutionResult { + pc_address: config.function_address.unwrap_or(0), + target_name: config + .function_name + .clone() + .unwrap_or_else(|| format!("{:#x}", config.function_address.unwrap_or(0))), + binary_path: config.binary_path.clone(), + status: ExecutionStatus::Success, + source_file, + source_line, + is_inline, + }); + success_count += 1; + } + + for failed in &compilation_result.failed_targets { + results.push(ScriptExecutionResult { + pc_address: failed.pc_address, + target_name: failed.target_name.clone(), + binary_path: binary_path.to_string(), + status: ExecutionStatus::Failed(failed.error_message.clone()), + source_file: None, + source_line: None, + is_inline: None, + }); + failed_count += 1; + } + + (results, trace_ids, success_count, failed_count) +}