d5f207424b
Python CI / Python bindings (ubuntu-latest) (push) Failing after 0s
Build & Publish / Build Neovim aarch64-linux-android (push) Failing after 0s
Build & Publish / Build Neovim aarch64-unknown-linux-gnu (push) Failing after 0s
Build & Publish / Build MCP x86_64-unknown-linux-musl (push) Failing after 1s
Build & Publish / Build Python wheels aarch64 (ubuntu-latest) (push) Failing after 0s
Build & Publish / Build Python wheels x86_64 (ubuntu-latest) (push) Failing after 1s
Build & Publish / Build Python sdist (push) Failing after 1s
Rust CI / Fuzz Tests (ubuntu-latest) (push) Failing after 1s
Rust CI / Build i686-unknown-linux-gnu (push) Failing after 0s
Rust CI / cargo clippy (push) Failing after 1s
e2e Tests / e2e (ubuntu-latest) (push) Failing after 1s
Lua CI / luacheck lint (push) Failing after 1s
Build & Publish / Build Neovim aarch64-unknown-linux-musl (push) Failing after 1s
Build & Publish / Build Neovim x86_64-unknown-linux-gnu (push) Failing after 0s
Build & Publish / Build Neovim x86_64-unknown-linux-musl (push) Failing after 1s
Build & Publish / Build C FFI aarch64-linux-android (push) Failing after 0s
Build & Publish / Build C FFI aarch64-unknown-linux-gnu (push) Failing after 0s
Build & Publish / Build C FFI aarch64-unknown-linux-musl (push) Failing after 1s
Build & Publish / Build C FFI x86_64-unknown-linux-gnu (push) Failing after 0s
Build & Publish / Build C FFI x86_64-unknown-linux-musl (push) Failing after 1s
Build & Publish / Build MCP aarch64-unknown-linux-gnu (push) Failing after 1s
Build & Publish / Build MCP aarch64-unknown-linux-musl (push) Failing after 0s
Build & Publish / Build MCP x86_64-unknown-linux-gnu (push) Failing after 1s
Spelling / Spell Check with Typos (push) Failing after 0s
Rust CI / Test (ubuntu-latest) (push) Failing after 0s
Rust CI / cargo fmt (push) Failing after 0s
Stylua / Check lua files using Stylua (push) Failing after 1s
Lua CI / lua-language-server type check (push) Failing after 12m29s
e2e Tests / e2e (alpine-musl) (push) Successful in 57m31s
e2e Tests / e2e (macos-latest) (push) Has been cancelled
Build & Publish / Build C FFI aarch64-apple-darwin (push) Has been cancelled
Rust CI / Test (windows-latest) (push) Has been cancelled
e2e Tests / e2e (windows-latest) (push) Has been cancelled
Nix CI / check (push) Has been cancelled
Python CI / Python bindings (macos-latest) (push) Has been cancelled
Python CI / Python bindings (windows-latest) (push) Has been cancelled
Build & Publish / Build Neovim aarch64-apple-darwin (push) Has been cancelled
Build & Publish / Build Neovim aarch64-pc-windows-msvc (push) Has been cancelled
Build & Publish / Build Neovim x86_64-apple-darwin (push) Has been cancelled
Build & Publish / Build Neovim x86_64-pc-windows-msvc (push) Has been cancelled
Build & Publish / Build C FFI aarch64-pc-windows-msvc (push) Has been cancelled
Build & Publish / Build C FFI x86_64-apple-darwin (push) Has been cancelled
Build & Publish / Build C FFI x86_64-pc-windows-msvc (push) Has been cancelled
Build & Publish / Build MCP aarch64-apple-darwin (push) Has been cancelled
Build & Publish / Build MCP aarch64-pc-windows-msvc (push) Has been cancelled
Build & Publish / Build MCP x86_64-apple-darwin (push) Has been cancelled
Build & Publish / Build MCP x86_64-pc-windows-msvc (push) Has been cancelled
Build & Publish / Build Python wheels aarch64 (macos-latest) (push) Has been cancelled
Build & Publish / Build Python wheels x86_64 (macos-latest) (push) Has been cancelled
Build & Publish / Build Python wheels x86_64 (windows-latest) (push) Has been cancelled
Build & Publish / Release (push) Has been cancelled
Build & Publish / Publish Python wheels to PyPI (push) Has been cancelled
Build & Publish / Publish Rust crates (push) Has been cancelled
Build & Publish / Publish npm packages (push) Has been cancelled
Rust CI / Fuzz Tests (windows-latest) (push) Has been cancelled
Rust CI / Test (macos-latest) (push) Has been cancelled
Rust CI / Fuzz Tests (macos-latest) (push) Has been cancelled
282 lines
9.3 KiB
Rust
282 lines
9.3 KiB
Rust
use std::io;
|
|
use std::path::{Path, PathBuf};
|
|
use std::sync::OnceLock;
|
|
use tracing_appender::non_blocking;
|
|
use tracing_subscriber::fmt::format::FmtSpan;
|
|
use tracing_subscriber::{EnvFilter, fmt, prelude::*};
|
|
|
|
// Set once on first init_tracing; doubles as the init-once gate.
|
|
static LOG_FILE_PATH: OnceLock<PathBuf> = OnceLock::new();
|
|
static CRASH_HOOKS: OnceLock<()> = OnceLock::new();
|
|
|
|
fn write_crash_report(header: &str, body: &str) {
|
|
let msg = format!(
|
|
"\n=== CRASH (this might NOT BE fff related) {} ===\n{}\n=== CRASH END {} ===\n",
|
|
header, body, header
|
|
);
|
|
let _ = std::io::Write::write_all(&mut std::io::stderr(), msg.as_bytes());
|
|
if let Some(path) = LOG_FILE_PATH.get() {
|
|
let _ = std::fs::OpenOptions::new()
|
|
.create(true)
|
|
.append(true)
|
|
.open(path)
|
|
.and_then(|mut f| std::io::Write::write_all(&mut f, msg.as_bytes()));
|
|
}
|
|
}
|
|
|
|
// SIGSEGV handler writes a banner to a pre-opened fd (open(2) inside a signal
|
|
// handler is unsafe due to path-resolution allocs). Unix only.
|
|
#[cfg(unix)]
|
|
mod sigsegv {
|
|
use std::os::fd::IntoRawFd;
|
|
use std::path::Path;
|
|
use std::sync::atomic::{AtomicI32, Ordering};
|
|
|
|
static LOG_FD: AtomicI32 = AtomicI32::new(-1);
|
|
|
|
// Must `create(true)` — this runs before init_tracing opens/creates the
|
|
// writer file, so an append-only open on a non-existent path silently
|
|
// fails, LOG_FD stays -1, and the SIGSEGV banner never reaches the log.
|
|
pub fn set_log_fd(path: &Path) {
|
|
if let Ok(file) = std::fs::OpenOptions::new()
|
|
.create(true)
|
|
.append(true)
|
|
.open(path)
|
|
{
|
|
let prev = LOG_FD.swap(file.into_raw_fd(), Ordering::Relaxed);
|
|
if prev >= 0 {
|
|
unsafe { libc::close(prev) };
|
|
}
|
|
}
|
|
}
|
|
|
|
// Body must be async-signal-safe: write(2), atomic load, signal(2). Nothing else.
|
|
fn handler(_info: &libc::siginfo_t) {
|
|
const BANNER: &[u8] = b"\n=== CRASH SIGSEGV (fff) ===\n\
|
|
fff.nvim's rust extension hit a segfault and is about to die.\n\
|
|
Please file the bug at https://github.com/dmtrKovalenko/fff/issues with this banner attached.\n\
|
|
=== CRASH END SIGSEGV ===\n";
|
|
unsafe {
|
|
libc::write(2, BANNER.as_ptr().cast(), BANNER.len());
|
|
let log_fd = LOG_FD.load(Ordering::Relaxed);
|
|
if log_fd >= 0 {
|
|
libc::write(log_fd, BANNER.as_ptr().cast(), BANNER.len());
|
|
}
|
|
// Reset to default so handler return → kernel kills us instead of
|
|
// re-running the faulting instruction in an infinite loop.
|
|
libc::signal(libc::SIGSEGV, libc::SIG_DFL);
|
|
}
|
|
}
|
|
|
|
pub fn install() {
|
|
// signal-hook-registry chains to LuaJIT's prior handler automatically.
|
|
unsafe {
|
|
let _ = signal_hook_registry::register_unchecked(libc::SIGSEGV, handler);
|
|
}
|
|
}
|
|
}
|
|
|
|
#[cfg(not(unix))]
|
|
mod sigsegv {
|
|
use std::path::Path;
|
|
pub fn set_log_fd(_path: &Path) {}
|
|
pub fn install() {}
|
|
}
|
|
|
|
pub fn install_panic_hook() {
|
|
CRASH_HOOKS.get_or_init(install_crash_hooks);
|
|
}
|
|
|
|
fn install_crash_hooks() {
|
|
let default_panic = std::panic::take_hook();
|
|
std::panic::set_hook(Box::new(move |panic_info| {
|
|
let message = if let Some(s) = panic_info.payload().downcast_ref::<&str>() {
|
|
s.to_string()
|
|
} else if let Some(s) = panic_info.payload().downcast_ref::<String>() {
|
|
s.clone()
|
|
} else {
|
|
"Unknown panic payload".to_string()
|
|
};
|
|
|
|
let location = panic_info
|
|
.location()
|
|
.map(|l| format!("{}:{}:{}", l.file(), l.line(), l.column()))
|
|
.unwrap_or_else(|| "unknown location".to_string());
|
|
|
|
tracing::error!(
|
|
panic.message = %message,
|
|
panic.location = %location,
|
|
"PANIC occurred in FFF"
|
|
);
|
|
|
|
write_crash_report(
|
|
"RUST PANIC",
|
|
&format!("Message: {}\nLocation: {}", message, location),
|
|
);
|
|
default_panic(panic_info);
|
|
}));
|
|
|
|
sigsegv::install();
|
|
}
|
|
|
|
/// Parse a log level string into a `tracing::Level`.
|
|
pub fn parse_log_level(level: Option<&str>) -> tracing::Level {
|
|
match level.as_ref().map(|s| s.trim().to_lowercase()).as_deref() {
|
|
Some("trace") => tracing::Level::TRACE,
|
|
Some("debug") => tracing::Level::DEBUG,
|
|
Some("info") => tracing::Level::INFO,
|
|
Some("warn") => tracing::Level::WARN,
|
|
Some("error") => tracing::Level::ERROR,
|
|
_ => tracing::Level::INFO,
|
|
}
|
|
}
|
|
|
|
/// Default retention: how many prior nvim sessions' log files to keep.
|
|
const DEFAULT_RETAIN_RUNS: usize = 20;
|
|
|
|
pub fn generate_trace_id() -> String {
|
|
use std::sync::atomic::{AtomicU64, Ordering};
|
|
static TRACE_COUNTER: AtomicU64 = AtomicU64::new(0);
|
|
|
|
let nanos = std::time::SystemTime::now()
|
|
.duration_since(std::time::UNIX_EPOCH)
|
|
.map(|d| d.as_nanos() as u64)
|
|
.unwrap_or(0);
|
|
|
|
let pid = std::process::id() as u64;
|
|
let counter = TRACE_COUNTER.fetch_add(1, Ordering::Relaxed);
|
|
|
|
// very simple hash functions helps to distinguish trace ids visually
|
|
let id = nanos ^ (pid.wrapping_mul(0x9E37_79B9_7F4A_7C15)) ^ (counter << 32);
|
|
format!("{:016x}", id)
|
|
}
|
|
|
|
pub fn trace_span(trace_id: &str, label: &'static str) -> tracing::Span {
|
|
tracing::info_span!("fff.trace", trace_id = trace_id, label = label)
|
|
}
|
|
|
|
fn unix_secs() -> u64 {
|
|
std::time::SystemTime::now()
|
|
.duration_since(std::time::UNIX_EPOCH)
|
|
.map(|d| d.as_secs())
|
|
.unwrap_or(0)
|
|
}
|
|
|
|
fn session_path_from_hint(hint: &Path) -> PathBuf {
|
|
let stem = hint.file_stem().and_then(|s| s.to_str()).unwrap_or("fff");
|
|
let ext = hint.extension().and_then(|e| e.to_str()).unwrap_or("log");
|
|
let parent = hint.parent().unwrap_or_else(|| Path::new("."));
|
|
parent.join(format!(
|
|
"{stem}+{ts}+{pid}.{ext}",
|
|
ts = unix_secs(),
|
|
pid = std::process::id(),
|
|
))
|
|
}
|
|
|
|
fn rotate_logs(dir: &Path, stem: &str, ext: &str, retain_runs: usize) {
|
|
let Ok(entries) = std::fs::read_dir(dir) else {
|
|
return;
|
|
};
|
|
let prefix = format!("{stem}+");
|
|
let suffix = format!(".{ext}");
|
|
|
|
let mut files: Vec<(std::time::SystemTime, PathBuf)> = entries
|
|
.filter_map(|res| {
|
|
let entry = res.ok()?;
|
|
let name = entry.file_name();
|
|
let name = name.to_str()?;
|
|
if !name.starts_with(&prefix) || !name.ends_with(&suffix) {
|
|
return None;
|
|
}
|
|
|
|
let mtime = entry.metadata().ok()?.modified().ok()?;
|
|
Some((mtime, entry.path()))
|
|
})
|
|
.collect();
|
|
|
|
if files.len() <= retain_runs {
|
|
return;
|
|
}
|
|
// Newest first, then drop everything past retain_runs.
|
|
files.sort_by_key(|(mtime, _)| std::cmp::Reverse(*mtime));
|
|
for (_, path) in files.into_iter().skip(retain_runs) {
|
|
let _ = std::fs::remove_file(path);
|
|
}
|
|
}
|
|
|
|
/// `log_file_path` is a path-shape hint. Each call writes a unique sibling
|
|
/// `<stem>+<unix-secs>+<pid>.<ext>` so concurrent processes never collide.
|
|
/// Returns the absolute path of the session file.
|
|
pub fn init_tracing(
|
|
log_file_path: &str,
|
|
log_level: Option<&str>,
|
|
retain_runs: Option<usize>,
|
|
) -> Result<String, io::Error> {
|
|
let hint = Path::new(log_file_path);
|
|
let session_dir = hint
|
|
.parent()
|
|
.unwrap_or_else(|| Path::new("."))
|
|
.to_path_buf();
|
|
std::fs::create_dir_all(&session_dir)?;
|
|
|
|
let session_path = session_path_from_hint(hint);
|
|
|
|
// First init wins; repeat callers no-op and return the original path.
|
|
if LOG_FILE_PATH.set(session_path.clone()).is_err() {
|
|
return Ok(LOG_FILE_PATH
|
|
.get()
|
|
.map(|p| p.to_string_lossy().into_owned())
|
|
.unwrap_or_default());
|
|
}
|
|
|
|
sigsegv::set_log_fd(&session_path);
|
|
install_panic_hook();
|
|
|
|
let stem = hint.file_stem().and_then(|s| s.to_str()).unwrap_or("fff");
|
|
let ext = hint.extension().and_then(|e| e.to_str()).unwrap_or("log");
|
|
rotate_logs(
|
|
&session_dir,
|
|
stem,
|
|
ext,
|
|
retain_runs.unwrap_or(DEFAULT_RETAIN_RUNS),
|
|
);
|
|
|
|
let writer_file = std::fs::OpenOptions::new()
|
|
.create(true)
|
|
.append(true)
|
|
.open(&session_path)?;
|
|
|
|
// we intinionally leark the guard we don't ever want to stop logging
|
|
let (non_blocking_appender, guard) = non_blocking(writer_file);
|
|
Box::leak(Box::new(guard));
|
|
|
|
let subscriber = tracing_subscriber::registry()
|
|
.with(
|
|
fmt::layer()
|
|
.with_writer(non_blocking_appender)
|
|
.with_target(true)
|
|
.with_thread_ids(false)
|
|
.with_thread_names(true)
|
|
.with_ansi(false)
|
|
.with_span_events(FmtSpan::NEW | FmtSpan::CLOSE),
|
|
)
|
|
.with(
|
|
EnvFilter::builder()
|
|
.with_default_directive(parse_log_level(log_level).into())
|
|
.from_env_lossy(),
|
|
);
|
|
|
|
if let Err(e) = tracing::subscriber::set_global_default(subscriber) {
|
|
eprintln!("Failed to set tracing subscriber: {}", e);
|
|
} else {
|
|
tracing::info!(
|
|
"FFF tracing initialized: {} (pid={}, retain_runs={})",
|
|
session_path.display(),
|
|
std::process::id(),
|
|
retain_runs.unwrap_or(DEFAULT_RETAIN_RUNS),
|
|
);
|
|
}
|
|
|
|
Ok(session_path.to_string_lossy().into_owned())
|
|
}
|