diff --git a/src/macos/event.rs b/src/macos/event.rs index 8c609c3..3f0e900 100644 --- a/src/macos/event.rs +++ b/src/macos/event.rs @@ -37,6 +37,10 @@ pub enum Event { flags: u64, fd: i32, }, + Rename { + from: String, + to: String, + }, } /// Converts Darwin `open(2)` flags to the Linux numbers the trace format diff --git a/src/macos/recon.rs b/src/macos/recon.rs index 1e5f775..408d0fc 100644 --- a/src/macos/recon.rs +++ b/src/macos/recon.rs @@ -38,6 +38,9 @@ const GUARDED_OPEN_DPROTECTED: u32 = 0x040c_0790; const OPENAT: u32 = 0x040c_073c; const OPENAT_NOCANCEL: u32 = 0x040c_0740; const OPENAT_DPROTECTED: u32 = 0x040c_0368; +const RENAME: u32 = 0x040c_0200; +const RENAMEAT: u32 = 0x040c_0744; +const RENAMEATX: u32 = 0x040c_07a0; const CHDIR: u32 = 0x040c_0030; const FCHDIR: u32 = 0x040c_0034; const CLOSE: u32 = 0x040c_0018; @@ -719,6 +722,28 @@ impl Collector { return Ok(()); } match code { + RENAME | RENAMEAT | RENAMEATX => { + if !self.file_events || call.lookups.len() < 2 { + return Ok(()); + } + // A rename looks up its source and then its destination. + let at = code != RENAME; + let last = call.lookups.len() - 1; + let (from_raw, to_raw) = + (call.lookups[last - 1].clone(), call.lookups[last].clone()); + let from_dir = at.then(|| call.args[0]).filter(|fd| *fd as i32 != AT_FDCWD); + let to_dir = at.then(|| call.args[2]).filter(|fd| *fd as i32 != AT_FDCWD); + let from = self.resolve(pid, &from_raw, from_dir); + let to = self.resolve(pid, &to_raw, to_dir); + if let (Some(from), Some(to)) = (from, to) { + out(Message { + mach_time, + pid, + ppid: 0, + event: Event::Rename { from, to }, + })?; + } + } CHDIR => { if let Some(raw) = call.lookups.first().cloned() { let resolved = self.resolve(pid, &raw, None); @@ -805,7 +830,10 @@ fn is_followed(code: u32) -> bool { is_open(code) || matches!( code, - CHDIR + RENAME + | RENAMEAT + | RENAMEATX + | CHDIR | FCHDIR | CLOSE | CLOSE_NOCANCEL @@ -1028,6 +1056,29 @@ mod tests { assert_eq!(paths, ["/src/dir", "/src/dir/b.o", "/src/sub/c.o"]); } + #[test] + fn reports_a_rename_with_both_paths_resolved() { + let mut stream = Stream::new(); + stream + .push(10, RENAME | 1, [0, 0, 0, 0]) + .lookup(10, 0x1234, "a-1234.o.tmp") + .lookup(10, 0x5678, "a.o") + .push(10, RENAME | 2, [0, 0, 0, 0]); + let messages = stream.collect(&mut collector()); + assert_eq!( + messages, + [Message { + mach_time: 1_040, + pid: 100, + ppid: 0, + event: Event::Rename { + from: "/src/a-1234.o.tmp".into(), + to: "/src/a.o".into(), + }, + }] + ); + } + #[test] fn a_new_process_inherits_and_is_reported() { let mut stream = Stream::new(); @@ -1297,6 +1348,7 @@ mod tests { Event::Exec { argv, .. } => format!("{} exec {}", message.pid, argv.join(" ")), Event::Open { path, .. } => format!("{} open {path}", message.pid), Event::Exit { .. } => format!("{} exit", message.pid), + Event::Rename { .. } => format!("{} rename", message.pid), }) .collect(); assert_eq!( diff --git a/src/macos/tracker.rs b/src/macos/tracker.rs index f17dbf9..c04d7c1 100644 --- a/src/macos/tracker.rs +++ b/src/macos/tracker.rs @@ -1,12 +1,12 @@ // Copyright 2026 The Buildprof Authors. // SPDX-License-Identifier: Apache-2.0 -//! Turns the collector's events into the same process segments and file opens -//! the Linux tracer records. +//! Turns the collector's events into the same process segments, file opens and +//! renames the Linux tracer records. use super::event::{Event, Message}; use crate::blind_spots::BlindSpots; -use crate::model::{FileOpen, Process, Segment, exit_code}; +use crate::model::{FileOpen, Process, Rename, Segment, exit_code}; use crate::perfetto::Writer; use std::collections::{HashMap, HashSet}; use std::io; @@ -22,6 +22,7 @@ pub trait Sink { fn process_started(&mut self, pid: i32) -> io::Result<()>; fn segment(&mut self, process: Process, segment: &Segment) -> io::Result<()>; fn file_open(&mut self, pid: i32, open: &FileOpen) -> io::Result<()>; + fn rename(&mut self, pid: i32, rename: &Rename) -> io::Result<()>; } /// The trace being recorded. @@ -39,6 +40,9 @@ impl Sink for TraceSink<'_> { fn file_open(&mut self, pid: i32, open: &FileOpen) -> io::Result<()> { self.writer.file_open(pid, open) } + fn rename(&mut self, pid: i32, rename: &Rename) -> io::Result<()> { + self.writer.rename(pid, rename) + } } /// Converts `mach_absolute_time` ticks to nanoseconds since recording began. @@ -179,6 +183,17 @@ impl<'a, S: Sink> Tracker<'a, S> { }, )? } + Event::Rename { from, to } => { + announce(self.sink, &mut self.announced, pid)?; + self.sink.rename( + pid, + &Rename { + timestamp_ns, + from, + to, + }, + )? + } } Ok(()) } @@ -335,6 +350,11 @@ mod tests { )); Ok(()) } + fn rename(&mut self, pid: i32, rename: &Rename) -> io::Result<()> { + self.0 + .push(format!("rename {pid} {} -> {}", rename.from, rename.to)); + Ok(()) + } } fn clock() -> Clock {