From 6342c81a9c12885cd6ec26d84132179054b22acc Mon Sep 17 00:00:00 2001 From: swananan Date: Thu, 28 May 2026 19:18:30 +0800 Subject: [PATCH] feat: add dry-run trace plan details Add --dry-run and --dry-run-details for script-mode trace plan validation without attaching uprobes. Dry-run now validates the same eBPF startup privileges and kernel capabilities as a real run. The report includes resolved PCs, module paths, uprobe offsets, source locations, inline status, and DWARF variable visibility diagnostics, and separates resolved targets with unresolved uprobe offsets from attachable targets. --- docs/configuration.md | 2 + docs/scripting.md | 15 ++ docs/zh/configuration.md | 2 + docs/zh/scripting.md | 9 + ghostscope/src/cli/dry_run.rs | 316 +++++++++++++++++++++++++++ ghostscope/src/cli/mod.rs | 1 + ghostscope/src/cli/script_runtime.rs | 48 +++- ghostscope/src/config/args.rs | 58 +++++ ghostscope/src/config/user.rs | 10 + ghostscope/src/core/session.rs | 6 +- ghostscope/src/main.rs | 8 +- ghostscope/src/script/compiler.rs | 17 +- ghostscope/src/script/mod.rs | 4 +- 13 files changed, 484 insertions(+), 12 deletions(-) create mode 100644 ghostscope/src/cli/dry_run.rs diff --git a/docs/configuration.md b/docs/configuration.md index e7935f5f..b76b18c5 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -226,6 +226,8 @@ Behavior: | `--script-file ` | | Script file to execute | None | | `--script-help` | | Print the embedded script language reference and exit | Off | | `--script-output ` | | Script event stdout mode: pretty, plain | pretty | +| `--dry-run` | | Compile the script, resolve trace targets, and exit without attaching uprobes. Requires the same eBPF privileges and kernel capabilities as a real run. | Off | +| `--dry-run-details` | | Include source, inline, and variable diagnostics in dry-run output; requires `--dry-run` | Off | | `--status` | | Enable interactive DWARF/script/attach stderr status prompts | On | | `--no-status` | | Disable interactive DWARF/script/attach stderr status prompts | Off override | | `--script-timestamp ` | | Pretty output timestamp: local, boot, none | local | diff --git a/docs/scripting.md b/docs/scripting.md index 19c542bb..6ffa36aa 100644 --- a/docs/scripting.md +++ b/docs/scripting.md @@ -43,6 +43,21 @@ GhostScope supports the following statements: The `trace` statement is the top‑level construct used only at the script file level (not nested inside other trace blocks). +Before attaching uprobes, you can validate a script and inspect the resolved +targets: + +```bash +ghostscope -p 1234 --script-file trace.gs --dry-run +ghostscope -p 1234 --script-file trace.gs --dry-run --dry-run-details +``` + +`--dry-run` parses DWARF, compiles the script, resolves PCs and uprobe file +offsets, then exits without attaching. It still performs the same startup +privilege and kernel capability checks as a real run, so use `sudo` or +equivalent eBPF privileges when your system requires them. `--dry-run-details` +adds source locations, inline classification, script-used variables, visible +variables, and optimized-out/unavailable variable diagnostics. + ### Syntax ```ghostscope diff --git a/docs/zh/configuration.md b/docs/zh/configuration.md index 35f87400..ce56cef9 100644 --- a/docs/zh/configuration.md +++ b/docs/zh/configuration.md @@ -227,6 +227,8 @@ ghostscope bpffs prune --dry-run --json | `--script-file ` | | 要执行的脚本文件 | 无 | | `--script-help` | | 输出内嵌的脚本语言参考并退出 | 关 | | `--script-output ` | | 脚本事件 stdout 模式:pretty, plain | pretty | +| `--dry-run` | | 编译脚本、解析 trace 目标,然后退出,不 attach uprobe。需要与真实运行相同的 eBPF 权限和内核能力。 | 关 | +| `--dry-run-details` | | 在 dry-run 输出中包含源码、inline 和变量诊断;需要同时使用 `--dry-run` | 关 | | `--status` | | 启用交互式 DWARF/脚本/attach stderr 状态提示 | 开 | | `--no-status` | | 禁用交互式 DWARF/脚本/attach stderr 状态提示 | 关闭覆盖 | | `--script-timestamp ` | | pretty 输出时间戳:local, boot, none | local | diff --git a/docs/zh/scripting.md b/docs/zh/scripting.md index 610fafce..5407113d 100644 --- a/docs/zh/scripting.md +++ b/docs/zh/scripting.md @@ -43,6 +43,15 @@ GhostScope 支持以下语句类型: `trace` 语句是定义追踪点的顶层结构。它只在脚本文件中使用,不在追踪块内部使用。 +在真正 attach uprobe 之前,可以先验证脚本并查看解析结果: + +```bash +ghostscope -p 1234 --script-file trace.gs --dry-run +ghostscope -p 1234 --script-file trace.gs --dry-run --dry-run-details +``` + +`--dry-run` 会解析 DWARF、编译脚本、解析 PC 和 uprobe 文件偏移,然后直接退出,不 attach。它仍会执行与真实运行相同的启动权限和内核能力检查;如果当前系统要求,请使用 `sudo` 或等价 eBPF 权限。`--dry-run-details` 会追加源码位置、inline 分类、脚本用到的变量、当前 PC 可见变量,以及 optimized-out/不可用变量诊断。 + ### 语法 ```ghostscope diff --git a/ghostscope/src/cli/dry_run.rs b/ghostscope/src/cli/dry_run.rs new file mode 100644 index 00000000..f1a8369a --- /dev/null +++ b/ghostscope/src/cli/dry_run.rs @@ -0,0 +1,316 @@ +use ghostscope_compiler::{CompilationResult, UProbeConfig}; +use ghostscope_dwarf::{ + Availability, DwarfAnalyzer, ModuleAddress, Provenance, RuntimeCapabilities, + VariableLoweringKind, VariableQueryDiagnostic, VariableReadPlan, VisibleVariable, +}; + +const VARIABLE_DISPLAY_LIMIT: usize = 40; + +#[derive(Debug, Clone, Copy)] +pub struct DryRunReportOptions { + pub details: bool, +} + +pub fn print_dry_run_report( + result: &CompilationResult, + analyzer: Option<&DwarfAnalyzer>, + runtime_capabilities: &RuntimeCapabilities, + options: DryRunReportOptions, +) { + let attachable_configs: Vec<_> = result + .uprobe_configs + .iter() + .filter(|config| is_attachable_config(config)) + .collect(); + let unattachable_configs: Vec<_> = result + .uprobe_configs + .iter() + .filter(|config| !is_attachable_config(config)) + .collect(); + + println!("GhostScope dry run: no uprobes were attached."); + println!( + "Summary: {} attachable target(s), {} unattachable resolved target(s), {} failed target(s), next trace id {}", + attachable_configs.len(), + unattachable_configs.len(), + result.failed_targets.len(), + result.next_available_trace_id + ); + if !result.target_info.is_empty() { + println!("Primary target: {}", result.target_info); + } + + if attachable_configs.is_empty() { + println!("\nAttachable targets: none"); + } else { + println!("\nAttachable targets:"); + for (index, config) in attachable_configs.iter().enumerate() { + print_target( + index + 1, + config, + analyzer, + runtime_capabilities, + options.details, + ); + } + } + + if !unattachable_configs.is_empty() { + println!("\nResolved but not attachable targets:"); + for (index, config) in unattachable_configs.iter().enumerate() { + print_target( + index + 1, + config, + analyzer, + runtime_capabilities, + options.details, + ); + } + } + + if !result.failed_targets.is_empty() { + println!("\nFailed targets:"); + for failed in &result.failed_targets { + println!( + " - {} at 0x{:x}: {}", + failed.target_name, failed.pc_address, failed.error_message + ); + } + } +} + +fn is_attachable_config(config: &UProbeConfig) -> bool { + config.uprobe_offset.is_some() +} + +fn print_target( + index: usize, + config: &UProbeConfig, + analyzer: Option<&DwarfAnalyzer>, + runtime_capabilities: &RuntimeCapabilities, + details: bool, +) { + let pc = config.function_address.unwrap_or(0); + let target = target_label(config); + let offset = config + .uprobe_offset + .map(|offset| format!("0x{offset:x}")) + .unwrap_or_else(|| "".to_string()); + + println!( + " [{index}] {} -> pc=0x{pc:x}, uprobe_offset={}, module={}", + target, offset, config.binary_path + ); + + if !details { + return; + } + + println!(" trace_id: {}", config.assigned_trace_id); + println!(" eBPF program: {}", config.ebpf_function_name); + println!(" pattern: {}", trace_pattern_label(config)); + println!(" attachable: {}", attachable_label(config)); + if let Some(address_index) = config.resolved_address_index { + println!(" resolved address index: {address_index}"); + } + if let Some(source) = source_label(config, analyzer) { + println!(" source: {source}"); + } + if let Some(inline) = inline_label(config, analyzer) { + println!(" inline context: {inline}"); + } + print_used_variables(config); + print_visible_variables(config, analyzer, runtime_capabilities); +} + +fn target_label(config: &UProbeConfig) -> String { + config + .function_name + .clone() + .unwrap_or_else(|| format!("0x{:x}", config.function_address.unwrap_or(0))) +} + +fn trace_pattern_label(config: &UProbeConfig) -> String { + use ghostscope_compiler::script::TracePattern; + + match &config.trace_pattern { + TracePattern::FunctionName(name) => format!("function {name}"), + TracePattern::Wildcard(pattern) => format!("wildcard {pattern}"), + TracePattern::Address(address) => format!("address 0x{address:x}"), + TracePattern::AddressInModule { module, address } => { + format!("address {module}:0x{address:x}") + } + TracePattern::SourceLine { + file_path, + line_number, + } => format!("source {file_path}:{line_number}"), + } +} + +fn attachable_label(config: &UProbeConfig) -> &'static str { + if config.uprobe_offset.is_some() && !config.ebpf_bytecode.is_empty() { + "yes (file offset resolved; eBPF bytecode generated)" + } else if config.uprobe_offset.is_some() { + "partial (file offset resolved; empty eBPF bytecode)" + } else { + "no (file offset unresolved)" + } +} + +fn module_address(config: &UProbeConfig) -> Option { + config.function_address.map(|address| { + ModuleAddress::new( + std::path::PathBuf::from(config.binary_path.clone()), + address, + ) + }) +} + +fn source_label(config: &UProbeConfig, analyzer: Option<&DwarfAnalyzer>) -> Option { + let analyzer = analyzer?; + let module_address = module_address(config)?; + let source = analyzer.lookup_source_location(&module_address)?; + let column = source + .column + .map(|column| format!(":{column}")) + .unwrap_or_default(); + Some(format!( + "{}:{}{}", + source.file_path, source.line_number, column + )) +} + +fn inline_label(config: &UProbeConfig, analyzer: Option<&DwarfAnalyzer>) -> Option<&'static str> { + let analyzer = analyzer?; + let module_address = module_address(config)?; + analyzer + .is_inline_at(&module_address) + .map(|is_inline| if is_inline { "yes" } else { "no" }) +} + +fn print_used_variables(config: &UProbeConfig) { + if config.trace_context.variable_names.is_empty() { + println!(" variables used by script: none"); + return; + } + + println!( + " variables used by script: {}", + config.trace_context.variable_names.join(", ") + ); +} + +fn print_visible_variables( + config: &UProbeConfig, + analyzer: Option<&DwarfAnalyzer>, + runtime_capabilities: &RuntimeCapabilities, +) { + let Some(analyzer) = analyzer else { + println!(" visible variables: unavailable (no DWARF analyzer)"); + return; + }; + let Some(module_address) = module_address(config) else { + println!(" visible variables: unavailable (no PC)"); + return; + }; + + let visible = analyzer + .resolve_pc(&module_address) + .and_then(|ctx| analyzer.visible_variables_with_diagnostics(&ctx)); + + match visible { + Ok(result) => { + print_visible_variable_list(&result.variables, runtime_capabilities); + print_variable_diagnostics(&result.diagnostics); + } + Err(error) => { + println!(" visible variables: unavailable ({error})"); + } + } +} + +fn print_visible_variable_list( + variables: &[VisibleVariable], + runtime_capabilities: &RuntimeCapabilities, +) { + if variables.is_empty() { + println!(" visible variables: none"); + return; + } + + println!( + " visible variables: {} shown{}", + variables.len().min(VARIABLE_DISPLAY_LIMIT), + if variables.len() > VARIABLE_DISPLAY_LIMIT { + format!(" of {}", variables.len()) + } else { + String::new() + } + ); + + for variable in variables.iter().take(VARIABLE_DISPLAY_LIMIT) { + let plan = VariableReadPlan::from_visible_variable(variable.clone(), Provenance::DirectDie); + let materialization = plan.materialization_plan(runtime_capabilities); + println!( + " - {}{}: {} [{}; {}; {}]", + if variable.is_parameter { "param " } else { "" }, + variable.name, + variable.type_name, + availability_label(&materialization.availability), + lowering_label(&materialization.lowering.kind), + variable.location + ); + } + + if variables.len() > VARIABLE_DISPLAY_LIMIT { + println!( + " ... {} more variable(s) omitted", + variables.len() - VARIABLE_DISPLAY_LIMIT + ); + } +} + +fn print_variable_diagnostics(diagnostics: &[VariableQueryDiagnostic]) { + if diagnostics.is_empty() { + return; + } + + println!(" variable diagnostics:"); + for diagnostic in diagnostics.iter().take(VARIABLE_DISPLAY_LIMIT) { + let name = diagnostic.name.as_deref().unwrap_or(""); + println!( + " - {}: {} [{}]", + name, + diagnostic.detail, + availability_label(&diagnostic.availability) + ); + } + + if diagnostics.len() > VARIABLE_DISPLAY_LIMIT { + println!( + " ... {} more diagnostic(s) omitted", + diagnostics.len() - VARIABLE_DISPLAY_LIMIT + ); + } +} + +fn availability_label(availability: &Availability) -> String { + match availability { + Availability::Available => "available".to_string(), + Availability::PartiallyAvailable => "partially available".to_string(), + Availability::OptimizedOut => "optimized out".to_string(), + Availability::NotInScope => "not in scope".to_string(), + Availability::Unsupported(reason) => format!("unsupported: {reason:?}"), + Availability::Requires(requirement) => format!("requires: {requirement:?}"), + Availability::Ambiguous(reason) => format!("ambiguous: {reason:?}"), + } +} + +fn lowering_label(kind: &VariableLoweringKind) -> &'static str { + match kind { + VariableLoweringKind::DirectValue => "direct value", + VariableLoweringKind::UserMemoryRead => "user memory read", + VariableLoweringKind::Composite => "composite", + VariableLoweringKind::Unavailable => "unavailable", + } +} diff --git a/ghostscope/src/cli/mod.rs b/ghostscope/src/cli/mod.rs index 1f672a10..eb549989 100644 --- a/ghostscope/src/cli/mod.rs +++ b/ghostscope/src/cli/mod.rs @@ -2,6 +2,7 @@ mod color; mod docs; +mod dry_run; mod loading_reporter; pub mod script_output; pub mod script_runtime; diff --git a/ghostscope/src/cli/script_runtime.rs b/ghostscope/src/cli/script_runtime.rs index 945a5426..ebc5d5dc 100644 --- a/ghostscope/src/cli/script_runtime.rs +++ b/ghostscope/src/cli/script_runtime.rs @@ -295,20 +295,60 @@ async fn run_cli_with_session( // Step 6: Build compile options from merged config let binary_path_hint = crate::util::derive_binary_path_hint(&session); - let compile_options = config.get_compile_options( + let mut compile_options = config.get_compile_options( config.should_save_llvm_ir, config.should_save_ebpf, config.should_save_ast, binary_path_hint, ); + if config.dry_run { + compile_options.save_llvm_ir = false; + compile_options.save_ebpf = false; + compile_options.save_ast = false; + } - // Step 7: Compile and load script with graceful error handling + // Step 7: Compile the script and either report the plan or attach uprobes if show_cli_status { eprintln!( "{}", - stderr_colors.yellow(stderr_colors.bold("Compiling script...")) + stderr_colors.yellow(stderr_colors.bold(if config.dry_run { + "Compiling script for dry run..." + } else { + "Compiling script..." + })) + ); + } + + if config.dry_run { + let compilation_result = match crate::script::compile_script_for_cli( + &script_content, + &mut session, + &compile_options, + ) { + Ok(result) => result, + Err(e) => { + error!("Failed to compile dry-run script: {:#}", e); + return Err(e); + } + }; + + if show_cli_status { + eprintln!( + "{}", + stderr_colors.green("Dry run complete; no uprobes attached.") + ); + } + crate::cli::dry_run::print_dry_run_report( + &compilation_result, + session.process_analyzer.as_ref(), + &compile_options.runtime_capabilities, + crate::cli::dry_run::DryRunReportOptions { + details: config.dry_run_details, + }, ); + return Ok(()); } + if let Err(e) = crate::script::compile_and_load_script_for_cli( &script_content, &mut session, @@ -574,6 +614,8 @@ mod tests { script_color_mode: CliColorMode::Auto, script_output_events_per_sec: 10_000, tui_mode: false, + dry_run: false, + dry_run_details: false, should_save_llvm_ir: false, should_save_ebpf: false, should_save_ast: false, diff --git a/ghostscope/src/config/args.rs b/ghostscope/src/config/args.rs index ec81cdb6..ccb8d25c 100644 --- a/ghostscope/src/config/args.rs +++ b/ghostscope/src/config/args.rs @@ -234,6 +234,14 @@ pub struct Args { #[arg(long, value_name = "MODE", value_enum)] pub script_output: Option, + /// Compile and resolve trace targets without attaching uprobes + #[arg(long, action = clap::ArgAction::SetTrue)] + pub dry_run: bool, + + /// Include detailed target and variable diagnostics in dry-run output + #[arg(long = "dry-run-details", requires = "dry_run", action = clap::ArgAction::SetTrue)] + pub dry_run_details: bool, + /// Enable interactive DWARF/script/attach status prompts on stderr #[arg(long, action = clap::ArgAction::SetTrue)] pub status: bool, @@ -325,6 +333,8 @@ pub struct ParsedArgs { pub script_file: Option, pub pid: Option, pub tui_mode: bool, + pub dry_run: bool, + pub dry_run_details: bool, pub script_output: Option, pub status_enabled: bool, pub has_explicit_status_flag: bool, @@ -475,6 +485,8 @@ impl Args { let should_save_ebpf = Self::should_save_ebpf(&parsed); let should_save_ast = Self::should_save_ast(&parsed); let tui_mode = Self::determine_tui_mode(&parsed); + let dry_run = parsed.dry_run; + let dry_run_details = parsed.dry_run_details; let target_path = Self::resolve_target_path(&parsed); let (status_enabled, has_explicit_status_flag) = Self::determine_status_config(&parsed); let ( @@ -502,6 +514,8 @@ impl Args { script_file: parsed.script_file, pid: parsed.pid, tui_mode, + dry_run, + dry_run_details, script_output: parsed.script_output, status_enabled, has_explicit_status_flag, @@ -562,6 +576,10 @@ impl Args { /// Determine whether to start in TUI mode fn determine_tui_mode(parsed: &Args) -> bool { + if parsed.dry_run { + return false; + } + // Explicit --tui flag takes precedence if parsed.tui { return true; @@ -689,6 +707,8 @@ impl Args { mod tests { use std::path::PathBuf; + use clap::Parser; + use super::{ Args, BpffsCommand, BpffsPruneArgs, ParsedCommand, ScriptOutputMode, ScriptTimestampFormat, }; @@ -861,6 +881,44 @@ mod tests { } } + #[test] + fn parses_dry_run_details_flag() { + let parsed = Args::parse_args_from(vec![ + "ghostscope".to_string(), + "--pid".to_string(), + "1234".to_string(), + "--script".to_string(), + "trace main { print 1; }".to_string(), + "--dry-run".to_string(), + "--dry-run-details".to_string(), + ]); + + match parsed { + ParsedCommand::Trace(args) => { + assert!(args.dry_run); + assert!(args.dry_run_details); + assert!(!args.tui_mode); + } + other => panic!("unexpected parse result: {other:?}"), + } + } + + #[test] + fn dry_run_details_requires_dry_run() { + let err = Args::try_parse_from(vec![ + "ghostscope", + "--pid", + "1234", + "--script", + "trace main { print 1; }", + "--dry-run-details", + ]) + .unwrap_err() + .to_string(); + + assert!(err.contains("--dry-run")); + } + #[test] fn parses_debuginfod_flags() { let parsed = Args::parse_args_from(vec![ diff --git a/ghostscope/src/config/user.rs b/ghostscope/src/config/user.rs index 2d640445..9785e635 100644 --- a/ghostscope/src/config/user.rs +++ b/ghostscope/src/config/user.rs @@ -31,6 +31,8 @@ pub struct UserConfig { pub script_color_mode: CliColorMode, pub script_output_events_per_sec: u64, pub tui_mode: bool, + pub dry_run: bool, + pub dry_run_details: bool, // File saving options pub should_save_llvm_ir: bool, @@ -170,6 +172,8 @@ impl UserConfig { .script_output_events_per_sec .unwrap_or(config.script.output_events_per_sec), tui_mode, + dry_run: args.dry_run, + dry_run_details: args.dry_run_details, should_save_llvm_ir, should_save_ebpf, should_save_ast, @@ -278,6 +282,12 @@ impl UserConfig { } } + if self.dry_run && self.script.is_none() && self.script_file.is_none() { + return Err(anyhow::anyhow!( + "--dry-run requires --script or --script-file because there is no trace plan to resolve" + )); + } + if let Some(debug_file) = &self.debug_file { if !debug_file.exists() { return Err(anyhow::anyhow!( diff --git a/ghostscope/src/core/session.rs b/ghostscope/src/core/session.rs index ddcc23f0..4833db6a 100644 --- a/ghostscope/src/core/session.rs +++ b/ghostscope/src/core/session.rs @@ -114,7 +114,9 @@ impl GhostSession { // Start sysmon for standalone -t only. Combined -t -p uses the PID for // concrete process mappings and does not need system-wide lifecycle tracking. - if s.proc_pid().is_none() && s.target_binary.is_some() { + if config.dry_run { + info!("Sysmon not started (dry-run mode)"); + } else if s.proc_pid().is_none() && s.target_binary.is_some() { let tpath = PathBuf::from(s.target_binary.as_ref().unwrap()); if config.ebpf_config.enable_sysmon_for_target { let cfg = SysmonConfig { @@ -449,6 +451,8 @@ mod tests { script: None, script_file: None, tui_mode: false, + dry_run: false, + dry_run_details: false, script_output: None, status_enabled: true, has_explicit_status_flag: false, diff --git a/ghostscope/src/main.rs b/ghostscope/src/main.rs index f820f4c7..e8a637fd 100644 --- a/ghostscope/src/main.rs +++ b/ghostscope/src/main.rs @@ -49,13 +49,13 @@ async fn main() -> Result<()> { info!("{}", user_config.config_source_message()); - // Ensure we have the privileges needed for eBPF interaction + // Dry-run does not attach uprobes, but it still validates the same eBPF + // privileges and kernel capabilities as a real run. crate::util::ensure_privileges(); - - let kernel_caps = ghostscope_loader::KernelCapabilities::detect_for_startup( + let kernel_caps = *ghostscope_loader::KernelCapabilities::detect_for_startup( user_config.ebpf_config.force_perf_event_array, )?; - let resolved_config = config::ResolvedConfig::resolve(user_config, kernel_caps)?; + let resolved_config = config::ResolvedConfig::resolve(user_config, &kernel_caps)?; // Best-effort cleanup for this process's bpffs pins on graceful shutdown and panic unwind. let _pinned_maps_cleanup = crate::util::PinnedMapsCleanupGuard::new(); diff --git a/ghostscope/src/script/compiler.rs b/ghostscope/src/script/compiler.rs index e0b319fe..2f7d74b0 100644 --- a/ghostscope/src/script/compiler.rs +++ b/ghostscope/src/script/compiler.rs @@ -534,12 +534,12 @@ pub async fn compile_and_load_script_for_tui( }) } -/// Compile and load script for command line mode using session.command_loaders -pub async fn compile_and_load_script_for_cli( +/// 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<()> { +) -> Result { let fallback_host_pid = session.host_pid(); info!("Starting unified script compilation with DWARF integration..."); @@ -590,6 +590,17 @@ pub async fn compile_and_load_script_for_cli( } } + 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 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 = { diff --git a/ghostscope/src/script/mod.rs b/ghostscope/src/script/mod.rs index 80623ddc..6efebfa4 100644 --- a/ghostscope/src/script/mod.rs +++ b/ghostscope/src/script/mod.rs @@ -3,4 +3,6 @@ pub mod compiler; // Re-export main functions for convenience -pub use compiler::{compile_and_load_script_for_cli, compile_and_load_script_for_tui}; +pub use compiler::{ + compile_and_load_script_for_cli, compile_and_load_script_for_tui, compile_script_for_cli, +};