Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
813 changes: 813 additions & 0 deletions Cargo.lock

Large diffs are not rendered by default.

4 changes: 4 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,9 @@ members = [
"ghostscope-platform",
"ghostscope",
"ghostscope-dwarf",
"ghostscope-debuginfod",
"ghostscope-process",
"bins/debuginfod-client",
"bins/dwarf-tool",
"e2e-tests",
]
Expand All @@ -20,7 +22,9 @@ default-members = [
"ghostscope-platform",
"ghostscope",
"ghostscope-dwarf",
"ghostscope-debuginfod",
"ghostscope-process",
"bins/debuginfod-client",
"bins/dwarf-tool",
]

Expand Down
25 changes: 25 additions & 0 deletions bins/debuginfod-client/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
[package]
name = "debuginfod-client"
version.workspace = true
edition.workspace = true
authors.workspace = true
license.workspace = true
rust-version.workspace = true
repository.workspace = true
homepage.workspace = true
documentation.workspace = true
keywords.workspace = true
categories.workspace = true
description = "Standalone debuginfod client for exercising GhostScope's async debuginfod library."
publish = false

[[bin]]
name = "debuginfod-client"
path = "src/main.rs"

[dependencies]
anyhow.workspace = true
clap.workspace = true
ghostscope-debuginfod = { version = "0.1.4", path = "../../ghostscope-debuginfod" }
object.workspace = true
tokio.workspace = true
154 changes: 154 additions & 0 deletions bins/debuginfod-client/src/main.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,154 @@
use anyhow::{anyhow, bail, Context, Result};
use clap::{Args, Parser, Subcommand};
use ghostscope_debuginfod::{DebuginfodClient, DebuginfodConfig, FetchedFile};
use object::Object;
use std::{
fs,
path::{Path, PathBuf},
time::Duration,
};

const UBUNTU_DEBUGINFOD_URL: &str = "https://debuginfod.ubuntu.com";

#[derive(Debug, Parser)]
#[command(about = "Fetch debuginfod artifacts with GhostScope's async client")]
struct Cli {
/// Debuginfod server URL. May be passed more than once.
#[arg(long = "url", value_name = "URL", default_value = UBUNTU_DEBUGINFOD_URL)]
urls: Vec<String>,

/// Local cache directory for downloaded artifacts.
#[arg(
long,
value_name = "DIR",
default_value = "target/debuginfod-client-cache"
)]
cache_dir: PathBuf,

/// Request timeout in seconds. Use 0 to disable reqwest's global request timeout.
#[arg(long, default_value_t = ghostscope_debuginfod::DEFAULT_TIMEOUT_SECS)]
timeout_secs: u64,

/// Maximum response size in bytes. Omit for no explicit client-side cap.
#[arg(long, value_name = "BYTES")]
max_size: Option<u64>,

#[command(subcommand)]
command: Command,
}

#[derive(Debug, Subcommand)]
enum Command {
/// Fetch /buildid/<build-id>/debuginfo.
Debuginfo(BuildIdInput),
/// Fetch /buildid/<build-id>/executable.
Executable(BuildIdInput),
/// Fetch /buildid/<build-id>/source/<absolute-source-path>.
Source(SourceInput),
}

#[derive(Debug, Args)]
struct BuildIdInput {
/// Hex build-id to query.
#[arg(long, conflicts_with = "file")]
build_id: Option<String>,

/// ELF file to read the GNU build-id from.
#[arg(long, value_name = "ELF", conflicts_with = "build_id")]
file: Option<PathBuf>,
}

#[derive(Debug, Args)]
struct SourceInput {
#[command(flatten)]
build_id: BuildIdInput,

/// Absolute source path as recorded or resolved from DWARF.
#[arg(long, value_name = "PATH")]
path: String,
}

#[tokio::main]
async fn main() -> Result<()> {
let cli = Cli::parse();
let mut config = DebuginfodConfig::new(cli.urls, &cli.cache_dir)?;
if cli.timeout_secs == 0 {
config = config.without_timeout();
} else {
config = config.with_timeout(Duration::from_secs(cli.timeout_secs));
}
config = config.with_max_size(cli.max_size);

let client = DebuginfodClient::new(config)?;
let fetched = match cli.command {
Command::Debuginfo(input) => {
let build_id = input.resolve_build_id()?;
client.fetch_debuginfo(&build_id).await?
}
Command::Executable(input) => {
let build_id = input.resolve_build_id()?;
client.fetch_executable(&build_id).await?
}
Command::Source(input) => {
let build_id = input.build_id.resolve_build_id()?;
client.fetch_source(&build_id, &input.path).await?
}
};

match fetched {
Some(file) => print_fetched_file(&file),
None => bail!("artifact not found on any configured debuginfod server"),
}

Ok(())
}

impl BuildIdInput {
fn resolve_build_id(&self) -> Result<Vec<u8>> {
match (&self.build_id, &self.file) {
(Some(build_id), None) => parse_build_id_hex(build_id),
(None, Some(file)) => read_build_id_from_elf(file),
(None, None) => Err(anyhow!("pass either --build-id <hex> or --file <elf>")),
(Some(_), Some(_)) => unreachable!("clap enforces conflicts_with"),
}
}
}

fn print_fetched_file(file: &FetchedFile) {
println!("build-id: {}", file.build_id);
println!("path: {}", file.path.display());
println!("from-cache: {}", file.from_cache);
if let Some(url) = &file.url {
println!("url: {url}");
}
}

fn read_build_id_from_elf(path: &Path) -> Result<Vec<u8>> {
let bytes =
fs::read(path).with_context(|| format!("failed to read ELF file {}", path.display()))?;
let object = object::File::parse(&bytes[..])
.with_context(|| format!("failed to parse ELF file {}", path.display()))?;
object
.build_id()
.context("failed to read GNU build-id note")?
.map(|build_id| build_id.to_vec())
.ok_or_else(|| anyhow!("ELF file has no GNU build-id: {}", path.display()))
}

fn parse_build_id_hex(raw: &str) -> Result<Vec<u8>> {
let raw = raw.trim();
if raw.is_empty() {
bail!("build-id must not be empty");
}
if raw.len() % 2 != 0 {
bail!("build-id hex must contain an even number of digits");
}

let mut bytes = Vec::with_capacity(raw.len() / 2);
for idx in (0..raw.len()).step_by(2) {
let byte = u8::from_str_radix(&raw[idx..idx + 2], 16)
.with_context(|| format!("invalid build-id hex at byte {}", idx / 2))?;
bytes.push(byte);
}
Ok(bytes)
}
35 changes: 35 additions & 0 deletions config-zh.toml
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,41 @@ search_paths = [
# 也会继续使用该独立调试文件(会记录警告日志)。仅建议在排障或环境不规范时短期启用。
allow_loose_debug_match = false

[dwarf.debuginfod]
# debuginfod 调试信息回退。
# 模式参考 GDB:
# - "off":完全不使用 debuginfod,也不读取 debuginfod 相关环境变量
# - "on":嵌入式 DWARF 和本地独立调试文件都失败后,允许使用 debuginfod
# - "ask":预留给未来 TUI 交互确认(当前不会使用)
#
# 默认:"off"(除非显式开启,否则不会联网)
enabled = "off"

# 服务器 URL。开启后如果此列表为空,GhostScope 会回退读取 DEBUGINFOD_URLS。
# 环境变量中是空白分隔;配置文件中使用 TOML 数组。
#
# 示例:
# - "https://debuginfod.ubuntu.com"
# - "https://debuginfod.archlinux.org"
urls = [
# "https://debuginfod.ubuntu.com",
]

# 下载的 debuginfo/source 本地缓存目录。
# 未设置或留空时使用:
# 1. DEBUGINFOD_CACHE_PATH
# 2. $XDG_CACHE_HOME/debuginfod_client
# 3. ~/.cache/debuginfod_client
# cache_dir = "~/.cache/debuginfod_client"

# 请求超时时间(秒)。优先级:命令行 > 配置文件 > DEBUGINFOD_TIMEOUT > 内置默认 5 秒。
# 设置为 0 表示不设置请求超时。
timeout_secs = 5

# 最大响应大小(字节)。优先级:命令行 > 配置文件 > DEBUGINFOD_MAXSIZE。
# 设置为 0 表示不设置客户端大小上限。
max_size_bytes = 0

[files]
# 文件保存选项(可通过 --save-*/--no-save-* 参数覆盖)
# 格式:{ debug = bool, release = bool }
Expand Down
37 changes: 37 additions & 0 deletions config.toml
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,43 @@ search_paths = [
# ad-hoc environments; may cause inaccurate symbol/line info.
allow_loose_debug_match = false

[dwarf.debuginfod]
# debuginfod fallback for separate debug information.
# This follows GDB-style modes:
# - "off": never use debuginfod and do not read debuginfod environment variables
# - "on": allow debuginfod after embedded DWARF and local debug files fail
# - "ask": reserved for future TUI confirmation support (currently not used)
#
# Default: "off" (no network access unless explicitly enabled)
enabled = "off"

# Server URLs. When enabled and this list is empty, GhostScope falls back to
# DEBUGINFOD_URLS. Values are whitespace-separated in the environment but are
# configured as a TOML array here.
#
# Examples:
# - "https://debuginfod.ubuntu.com"
# - "https://debuginfod.archlinux.org"
urls = [
# "https://debuginfod.ubuntu.com",
]

# Cache directory for downloaded debuginfo/source artifacts.
# Leave unset or empty to use:
# 1. DEBUGINFOD_CACHE_PATH
# 2. $XDG_CACHE_HOME/debuginfod_client
# 3. ~/.cache/debuginfod_client
# cache_dir = "~/.cache/debuginfod_client"

# Request timeout in seconds. CLI overrides this, then config, then
# DEBUGINFOD_TIMEOUT, then the built-in default of 5 seconds. Use 0 for no
# request timeout.
timeout_secs = 5

# Maximum response size in bytes. CLI overrides this, then config, then
# DEBUGINFOD_MAXSIZE. Use 0 for no explicit client-side cap.
max_size_bytes = 0

[files]
# File saving options (overridden by --save-*/--no-save-* flags)
# Format: { debug = bool, release = bool }
Expand Down
39 changes: 39 additions & 0 deletions docs/configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -226,6 +226,11 @@ Behavior:
| `--no-status` | | Disable interactive DWARF/script/attach stderr status prompts | Off override |
| `--script-timestamp <FORMAT>` | | Pretty output timestamp: local, boot, none | local |
| `--debug-file <PATH>` | `-d` | Debug info file path | Auto-detect |
| `--debuginfod <MODE>` | | debuginfod mode: off, on, ask | off |
| `--debuginfod-url <URL>` | | debuginfod server URL; may be repeated | None |
| `--debuginfod-cache-dir <DIR>` | | debuginfod cache directory | debuginfod-compatible default |
| `--debuginfod-timeout-secs <SECONDS>` | | debuginfod request timeout; 0 disables timeout | 5 |
| `--debuginfod-max-size <BYTES>` | | debuginfod maximum response size; 0 disables cap | 0 |
| `--tui` | | Start in TUI mode | Auto |
| `--log` | | Enable file logging | Script: off, TUI: on |
| `--no-log` | | Disable all logging | - |
Expand Down Expand Up @@ -340,6 +345,40 @@ search_paths = [
# symbol/line information. Prefer leaving this off unless you know what you are doing.
allow_loose_debug_match = false

[dwarf.debuginfod]
# Optional debuginfod fallback for separate debug information.
# Modes follow GDB's model:
# - "off": never use debuginfod and do not read debuginfod environment variables
# - "on": allow debuginfod after embedded DWARF and local debug files fail
# - "ask": reserved for future TUI confirmation support (currently not used)
#
# Default: "off" (no network access unless explicitly enabled)
enabled = "off"

# Server URLs. When enabled and this list is empty, GhostScope falls back to
# DEBUGINFOD_URLS. Values are whitespace-separated in the environment but are
# configured as a TOML array here.
urls = [
# "https://debuginfod.ubuntu.com",
# "https://debuginfod.archlinux.org",
]

# Cache directory for downloaded debuginfo/source artifacts.
# Leave unset to use:
# 1. DEBUGINFOD_CACHE_PATH
# 2. $XDG_CACHE_HOME/debuginfod_client
# 3. ~/.cache/debuginfod_client
# cache_dir = "~/.cache/debuginfod_client"

# Request timeout in seconds. Priority:
# CLI > config file > DEBUGINFOD_TIMEOUT > built-in default (5).
# Use 0 for no request timeout.
timeout_secs = 5

# Maximum response size in bytes. Priority:
# CLI > config file > DEBUGINFOD_MAXSIZE. Use 0 for no explicit cap.
max_size_bytes = 0

[files]
# Save LLVM IR files
[files.save_llvm_ir]
Expand Down
38 changes: 38 additions & 0 deletions docs/zh/configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -227,6 +227,11 @@ ghostscope bpffs prune --dry-run --json
| `--no-status` | | 禁用交互式 DWARF/脚本/attach stderr 状态提示 | 关闭覆盖 |
| `--script-timestamp <FORMAT>` | | pretty 输出时间戳:local, boot, none | local |
| `--debug-file <PATH>` | `-d` | 调试信息文件路径 | 自动检测 |
| `--debuginfod <MODE>` | | debuginfod 模式:off, on, ask | off |
| `--debuginfod-url <URL>` | | debuginfod 服务 URL,可重复传递 | 无 |
| `--debuginfod-cache-dir <DIR>` | | debuginfod 缓存目录 | debuginfod 兼容默认值 |
| `--debuginfod-timeout-secs <SECONDS>` | | debuginfod 请求超时;0 表示不设置超时 | 5 |
| `--debuginfod-max-size <BYTES>` | | debuginfod 最大响应大小;0 表示不设置上限 | 0 |
| `--tui` | | 以 TUI 模式启动 | 自动 |
| `--log` | | 启用文件日志 | Script: 关, TUI: 开 |
| `--no-log` | | 禁用所有日志 | - |
Expand Down Expand Up @@ -338,6 +343,39 @@ search_paths = [
# 也会继续使用该独立调试文件(会记录警告日志)。仅建议在排障或环境不规范时短期启用。
allow_loose_debug_match = false

[dwarf.debuginfod]
# 可选的 debuginfod 调试信息回退。
# 模式参考 GDB:
# - "off":完全不使用 debuginfod,也不读取 debuginfod 相关环境变量
# - "on":嵌入式 DWARF 和本地独立调试文件都失败后,允许使用 debuginfod
# - "ask":预留给未来 TUI 交互确认(当前不会使用)
#
# 默认:"off"(除非显式开启,否则不会联网)
enabled = "off"

# 服务器 URL。开启后如果此列表为空,GhostScope 会回退读取 DEBUGINFOD_URLS。
# 环境变量中是空白分隔;配置文件中使用 TOML 数组。
urls = [
# "https://debuginfod.ubuntu.com",
# "https://debuginfod.archlinux.org",
]

# 下载的 debuginfo/source 本地缓存目录。
# 未设置时使用:
# 1. DEBUGINFOD_CACHE_PATH
# 2. $XDG_CACHE_HOME/debuginfod_client
# 3. ~/.cache/debuginfod_client
# cache_dir = "~/.cache/debuginfod_client"

# 请求超时时间(秒)。优先级:
# 命令行 > 配置文件 > DEBUGINFOD_TIMEOUT > 内置默认 5 秒。
# 设置为 0 表示不设置请求超时。
timeout_secs = 5

# 最大响应大小(字节)。优先级:
# 命令行 > 配置文件 > DEBUGINFOD_MAXSIZE。设置为 0 表示不设置客户端大小上限。
max_size_bytes = 0

[files]
# 保存 LLVM IR 文件
[files.save_llvm_ir]
Expand Down
1 change: 1 addition & 0 deletions e2e-tests/tests/common/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -229,6 +229,7 @@ pub fn init() {
// Exercise runner builder methods so they are referenced in all bins.
let _ = runner::GhostscopeRunner::new()
.with_target("/")
.with_cli_args([std::ffi::OsString::from("--help")])
.force_perf_event_array(false)
.enable_sysmon_shared_lib(false);

Expand Down
Loading
Loading