use std::io::Write as _;
use bun_core::{env_var, fmt as bun_fmt, output};
use bun_sys::{Fd, File, O};
use bun_threading::Guarded;
use crate::watcher_impl::{ChangedFilePath, WatchEvent, WatchList};
static TRACE_FILE: Guarded<Option<File>> = Guarded::new(None);
pub fn init() {
let mut slot = TRACE_FILE.lock();
if slot.is_some() {
return;
}
if let Some(trace_path) = env_var::BUN_WATCHER_TRACE.get() {
if !trace_path.is_empty() {
let flags = O::WRONLY | O::CREAT | O::APPEND;
let mode = 0o644;
*slot = File::openat(Fd::cwd(), trace_path, flags, mode).ok();
}
}
}
pub fn write_events(
watchlist: &WatchList,
events: &[WatchEvent],
changed_files: &[ChangedFilePath],
) {
use crate::watcher_impl::WatchItemColumns;
let guard = TRACE_FILE.lock();
let Some(file) = guard.as_ref() else {
return;
};
let buffered = file.buffered_writer();
let mut writer = scopeguard::guard(buffered, |mut w| {
if let Err(err) = w.flush() {
let mut name_buf = [0u8; 64];
let name = {
use std::io::Write as _;
let mut c = std::io::Cursor::new(&mut name_buf[..]);
let _ = write!(c, "{}", err.kind());
let n = c.position() as usize;
unsafe { core::str::from_utf8_unchecked(&name_buf[..n]) }
};
output::err(name, "Failed to flush watcher trace file", ());
}
});
let timestamp: i64 = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.map(|d| i64::try_from(d.as_millis()).expect("int cast"))
.unwrap_or(0);
if writer.write_all(b"{\"timestamp\":").is_err() {
return;
}
if write!(writer, "{}", timestamp).is_err() {
return;
}
if writer.write_all(b",\"files\":{").is_err() {
return;
}
let watchlist_slice = watchlist.slice();
let file_paths = watchlist_slice.items_file_path();
let mut first_file = true;
for event in events {
let file_path: &[u8] = if (event.index as usize) < file_paths.len() {
&file_paths[event.index as usize]
} else {
b"(unknown)"
};
let names = event.names(changed_files);
if !first_file {
if writer.write_all(b",").is_err() {
return;
}
}
first_file = false;
if write!(
writer,
"{}",
bun_fmt::format_json_string_utf8(file_path, Default::default())
)
.is_err()
{
return;
}
if writer.write_all(b":{\"events\":[").is_err() {
return;
}
let mut first = true;
for &(flag, name) in crate::watcher_impl::OP_NAMES {
if !event.op.contains(flag) {
continue;
}
if !first {
if writer.write_all(b",").is_err() {
return;
}
}
if write!(writer, "\"{}\"", name).is_err() {
return;
}
first = false;
}
if writer.write_all(b"]").is_err() {
return;
}
let mut has_changed = false;
for name_opt in names {
if name_opt.is_some() {
has_changed = true;
break;
}
}
if has_changed {
if writer.write_all(b",\"changed\":[").is_err() {
return;
}
first = true;
for name in names.iter().flatten() {
if !first {
if writer.write_all(b",").is_err() {
return;
}
}
first = false;
if write!(
writer,
"{}",
bun_fmt::format_json_string_utf8(name.as_bytes(), Default::default())
)
.is_err()
{
return;
}
}
if writer.write_all(b"]").is_err() {
return;
}
}
if writer.write_all(b"}").is_err() {
return;
}
}
if writer.write_all(b"}}\n").is_err() {
return;
}
}
pub(crate) fn deinit() {
let _ = TRACE_FILE.lock().take();
}