diff --git a/samply/resources/entitlement.xml b/samply/resources/entitlement.xml
new file mode 100644
index 000000000..782ae7eff
--- /dev/null
+++ b/samply/resources/entitlement.xml
@@ -0,0 +1,10 @@
+
+
+
+
+ com.apple.security.cs.debugger
+
+ com.apple.security.cs.disable-library-validation
+
+
+
diff --git a/samply/src/mac/process_launcher.rs b/samply/src/mac/process_launcher.rs
index cbdb6299f..5206407bd 100644
--- a/samply/src/mac/process_launcher.rs
+++ b/samply/src/mac/process_launcher.rs
@@ -2,27 +2,97 @@ use std::collections::BTreeMap;
use std::ffi::{OsStr, OsString};
use std::fs::File;
use std::io::Write;
-use std::mem;
use std::os::unix::prelude::OsStrExt;
use std::path::PathBuf;
-use std::process::{Child, Command};
+use std::process::{Child, Command, ExitStatus};
use std::sync::Arc;
use std::time::Duration;
use flate2::write::GzDecoder;
+use mach::task::{task_resume, task_suspend};
+use mach::traps::task_for_pid;
use tempfile::tempdir;
+use crate::shared::ctrl_c::CtrlC;
+
pub use super::mach_ipc::{mach_port_t, MachError, OsIpcSender};
-use super::mach_ipc::{BlockingMode, OsIpcMultiShotServer, MACH_PORT_NULL};
+use super::mach_ipc::{mach_task_self, BlockingMode, OsIpcMultiShotServer, MACH_PORT_NULL};
+
+pub trait RootTaskRunner {
+ fn run_root_task(&self) -> Result;
+}
pub struct TaskLauncher {
program: OsString,
args: Vec,
child_env: Vec<(OsString, OsString)>,
- _temp_dir: Arc,
+ iteration_count: u32,
+}
+
+impl RootTaskRunner for TaskLauncher {
+ fn run_root_task(&self) -> Result {
+ // Ignore Ctrl+C while the subcommand is running. The signal still reaches the process
+ // under observation while we continue to record it. (ctrl+c will send the SIGINT signal
+ // to all processes in the foreground process group).
+ let mut ctrl_c_receiver = CtrlC::observe_oneshot();
+
+ let mut root_child = self.launch_child();
+ let mut exit_status = root_child.wait().expect("couldn't wait for child");
+
+ for i in 2..=self.iteration_count {
+ if !exit_status.success() {
+ eprintln!(
+ "Skipping remaining iterations due to non-success exit status: \"{}\"",
+ exit_status
+ );
+ break;
+ }
+ eprintln!("Running iteration {i} of {}...", self.iteration_count);
+ let mut root_child = self.launch_child();
+ exit_status = root_child.wait().expect("couldn't wait for child");
+ }
+
+ // From now on, we want to terminate if the user presses Ctrl+C.
+ ctrl_c_receiver.close();
+
+ Ok(exit_status)
+ }
}
impl TaskLauncher {
+ pub fn new(
+ program: S,
+ args: I,
+ iteration_count: u32,
+ env_vars: &[(OsString, OsString)],
+ extra_env_vars: &[(OsString, OsString)],
+ ) -> Result
+ where
+ I: IntoIterator- ,
+ S: Into,
+ {
+ // Take this process's environment variables and add DYLD_INSERT_LIBRARIES
+ // and SAMPLY_BOOTSTRAP_SERVER_NAME.
+ let mut child_env: BTreeMap = std::env::vars_os().collect();
+ for (name, val) in env_vars {
+ child_env.insert(name.to_owned(), val.to_owned());
+ }
+ for (name, val) in extra_env_vars {
+ child_env.insert(name.to_owned(), val.to_owned());
+ }
+ let child_env: Vec<(OsString, OsString)> = child_env.into_iter().collect();
+
+ let args: Vec = args.into_iter().map(|a| a.into()).collect();
+ let program: OsString = program.into();
+
+ Ok(TaskLauncher {
+ program,
+ args,
+ child_env,
+ iteration_count,
+ })
+ }
+
pub fn launch_child(&self) -> Child {
match Command::new(&self.program)
.args(&self.args)
@@ -49,6 +119,8 @@ impl TaskLauncher {
pub struct TaskAccepter {
server: OsIpcMultiShotServer,
+ added_env: Vec<(OsString, OsString)>,
+ queue: Vec,
_temp_dir: Arc,
}
@@ -56,15 +128,7 @@ static PRELOAD_LIB_CONTENTS: &[u8] =
include_bytes!("../../resources/libsamply_mac_preload.dylib.gz");
impl TaskAccepter {
- pub fn new(
- program: S,
- args: I,
- env_vars: &[(OsString, OsString)],
- ) -> Result<(Self, TaskLauncher), MachError>
- where
- I: IntoIterator
- ,
- S: Into,
- {
+ pub fn new() -> Result {
let (server, server_name) = OsIpcMultiShotServer::new()?;
// Launch the child with DYLD_INSERT_LIBRARIES set to libsamply_mac_preload.dylib.
@@ -84,42 +148,38 @@ impl TaskAccepter {
.finish()
.expect("Couldn't write libsamply_mac_preload.dylib (error during finish)");
- // Take this process's environment variables and add DYLD_INSERT_LIBRARIES
- // and SAMPLY_BOOTSTRAP_SERVER_NAME.
- let mut child_env: BTreeMap = std::env::vars_os().collect();
- for (name, val) in env_vars {
- child_env.insert(name.to_owned(), val.to_owned());
- }
+ let mut added_env: Vec<(OsString, OsString)> = vec![];
let mut add_env = |name: &str, val: &OsStr| {
- child_env.insert(name.into(), val.to_owned());
+ added_env.push((name.into(), val.to_owned()));
// Also set the same variable with an `__XPC_` prefix, so that it gets applied
// to services launched via XPC. XPC strips the prefix when setting these environment
// variables on the launched process.
- child_env.insert(format!("__XPC_{name}").into(), val.to_owned());
+ added_env.push((format!("__XPC_{name}").into(), val.to_owned()));
};
add_env("DYLD_INSERT_LIBRARIES", preload_lib_path.as_os_str());
add_env("SAMPLY_BOOTSTRAP_SERVER_NAME", OsStr::new(&server_name));
- let child_env: Vec<(OsString, OsString)> = child_env.into_iter().collect();
- let args: Vec = args.into_iter().map(|a| a.into()).collect();
- let program: OsString = program.into();
- let dir = Arc::new(dir);
-
- Ok((
- TaskAccepter {
- server,
- _temp_dir: dir.clone(),
- },
- TaskLauncher {
- program,
- args,
- child_env,
- _temp_dir: dir,
- },
- ))
+ Ok(TaskAccepter {
+ server,
+ added_env,
+ queue: vec![],
+ _temp_dir: Arc::new(dir),
+ })
+ }
+
+ pub fn extra_env_vars(&self) -> &[(OsString, OsString)] {
+ &self.added_env
+ }
+
+ pub fn queue_received_stuff(&mut self, rs: ReceivedStuff) {
+ self.queue.push(rs);
}
pub fn next_message(&mut self, timeout: Duration) -> Result {
+ if let Some(rs) = self.queue.pop() {
+ return Ok(rs);
+ }
+
// Wait until the child is ready
let (res, mut channels, _) = self
.server
@@ -138,7 +198,7 @@ impl TaskAccepter {
ReceivedStuff::AcceptedTask(AcceptedTask {
task,
pid,
- sender_channel,
+ sender_channel: Some(sender_channel),
})
}
(b"Jitdump", jitdump_info) => {
@@ -174,12 +234,12 @@ pub enum ReceivedStuff {
pub struct AcceptedTask {
task: mach_port_t,
pid: u32,
- sender_channel: OsIpcSender,
+ sender_channel: Option,
}
impl AcceptedTask {
- pub fn take_task(&mut self) -> mach_port_t {
- mem::replace(&mut self.task, MACH_PORT_NULL)
+ pub fn task(&self) -> mach_port_t {
+ self.task
}
pub fn get_id(&self) -> u32 {
@@ -187,6 +247,51 @@ impl AcceptedTask {
}
pub fn start_execution(&self) {
- self.sender_channel.send(b"Proceed", vec![]).unwrap();
+ if let Some(sender_channel) = &self.sender_channel {
+ sender_channel.send(b"Proceed", vec![]).unwrap();
+ } else {
+ unsafe { task_resume(self.task) };
+ }
+ }
+}
+
+pub struct ExistingProcessRunner {
+ pid: u32,
+}
+
+impl RootTaskRunner for ExistingProcessRunner {
+ fn run_root_task(&self) -> Result {
+ let ctrl_c_receiver = CtrlC::observe_oneshot();
+
+ eprintln!("Profiling {}, press Ctrl-C to stop...", self.pid);
+
+ ctrl_c_receiver
+ .blocking_recv()
+ .expect("Ctrl+C receiver failed");
+
+ eprintln!("Done.");
+
+ Ok(ExitStatus::default())
+ }
+}
+
+impl ExistingProcessRunner {
+ pub fn new(pid: u32, task_accepter: &mut TaskAccepter) -> ExistingProcessRunner {
+ let task = unsafe {
+ let mut task = MACH_PORT_NULL;
+ let kr = task_for_pid(mach_task_self(), pid as i32, &mut task);
+ if kr != 0 {
+ eprintln!("Error: task_for_pid failed with error code {kr}. Does the profiler have entitlements?");
+ std::process::exit(1);
+ }
+ task_suspend(task);
+ task
+ };
+ task_accepter.queue_received_stuff(ReceivedStuff::AcceptedTask(AcceptedTask {
+ task,
+ pid,
+ sender_channel: None,
+ }));
+ ExistingProcessRunner { pid }
}
}
diff --git a/samply/src/mac/profiler.rs b/samply/src/mac/profiler.rs
index 1c963742d..7d3a4f94b 100644
--- a/samply/src/mac/profiler.rs
+++ b/samply/src/mac/profiler.rs
@@ -10,11 +10,12 @@ use crossbeam_channel::unbounded;
use serde_json::to_writer;
use super::error::SamplingError;
-use super::process_launcher::{MachError, ReceivedStuff, TaskAccepter};
-use super::sampler::{JitdumpOrMarkerPath, Sampler, TaskInit};
+use super::process_launcher::{
+ ExistingProcessRunner, MachError, ReceivedStuff, RootTaskRunner, TaskAccepter, TaskLauncher,
+};
+use super::sampler::{JitdumpOrMarkerPath, Sampler, TaskInit, TaskInitOrShutdown};
use super::time::get_monotonic_timestamp;
use crate::server::{start_server_main, ServerProps};
-use crate::shared::ctrl_c::CtrlC;
use crate::shared::recording_props::{
ProcessLaunchProps, ProfileCreationProps, RecordingMode, RecordingProps,
};
@@ -25,30 +26,70 @@ pub fn start_recording(
profile_creation_props: ProfileCreationProps,
server_props: Option,
) -> Result {
- let process_launch_props = match recording_mode {
- RecordingMode::All | RecordingMode::Pid(_) => {
+ let mut unlink_aux_files = profile_creation_props.unlink_aux_files;
+ let output_file = recording_props.output_file.clone();
+ let profile_name;
+
+ let mut task_accepter = TaskAccepter::new()?;
+
+ let root_task_runner: Box = match recording_mode {
+ RecordingMode::All => {
// TODO: Implement, by sudo launching a helper process which uses task_for_pid
eprintln!("Error: Profiling existing processes is currently not supported on macOS.");
eprintln!("You can only profile processes which you launch via samply.");
std::process::exit(1)
}
- RecordingMode::Launch(process_launch_props) => process_launch_props,
+ RecordingMode::Pid(pid) => {
+ profile_name = format!("pid {pid}");
+
+ Box::new(ExistingProcessRunner::new(pid, &mut task_accepter))
+ }
+ RecordingMode::Launch(process_launch_props) => {
+ profile_name = process_launch_props
+ .command_name
+ .to_string_lossy()
+ .to_string();
+
+ let ProcessLaunchProps {
+ mut env_vars,
+ command_name,
+ args,
+ iteration_count,
+ } = process_launch_props;
+
+ if recording_props.coreclr {
+ // We need to set DOTNET_PerfMapEnabled=2 in the environment if it's not already set.
+ // If we set it, we'll also set unlink_aux_files=true to avoid leaving files
+ // behind in the temp directory. But if it's set manually, assume the user
+ // knows what they're doing and will specify the arg as needed.
+ if !env_vars.iter().any(|p| p.0 == "DOTNET_PerfMapEnabled") {
+ env_vars.push(("DOTNET_PerfMapEnabled".into(), "2".into()));
+ unlink_aux_files = true;
+ }
+ }
+
+ let task_launcher = TaskLauncher::new(
+ &command_name,
+ &args,
+ iteration_count,
+ &env_vars,
+ task_accepter.extra_env_vars(),
+ )?;
+
+ Box::new(task_launcher)
+ }
};
- let ProcessLaunchProps {
- env_vars,
- command_name,
- args,
- iteration_count,
- } = process_launch_props;
- let command_name_copy = command_name.to_string_lossy().to_string();
- let output_file = recording_props.output_file.clone();
+ let profile_creation_props = ProfileCreationProps {
+ unlink_aux_files,
+ ..profile_creation_props
+ };
let (task_sender, task_receiver) = unbounded();
let sampler_thread = thread::spawn(move || {
let sampler = Sampler::new(
- command_name_copy,
+ profile_name,
task_receiver,
recording_props,
profile_creation_props,
@@ -56,13 +97,6 @@ pub fn start_recording(
sampler.run()
});
- // Ignore Ctrl+C while the subcommand is running. The signal still reaches the process
- // under observation while we continue to record it. (ctrl+c will send the SIGINT signal
- // to all processes in the foreground process group).
- let mut ctrl_c_receiver = CtrlC::observe_oneshot();
-
- let (mut task_accepter, task_launcher) = TaskAccepter::new(&command_name, &args, &env_vars)?;
-
let (accepter_sender, accepter_receiver) = unbounded();
let accepter_thread = thread::spawn(move || {
// Loop while accepting messages from the spawned process tree.
@@ -74,19 +108,20 @@ pub fn start_recording(
loop {
if let Ok(()) = accepter_receiver.try_recv() {
+ task_sender.send(TaskInitOrShutdown::Shutdown).ok();
break;
}
let timeout = Duration::from_secs_f64(1.0);
match task_accepter.next_message(timeout) {
- Ok(ReceivedStuff::AcceptedTask(mut accepted_task)) => {
+ Ok(ReceivedStuff::AcceptedTask(accepted_task)) => {
let pid = accepted_task.get_id();
let (path_sender, path_receiver) = unbounded();
- let send_result = task_sender.send(TaskInit {
+ let send_result = task_sender.send(TaskInitOrShutdown::TaskInit(TaskInit {
start_time_mono: get_monotonic_timestamp(),
- task: accepted_task.take_task(),
+ task: accepted_task.task(),
pid,
path_receiver,
- });
+ }));
path_senders_per_pid.insert(pid, path_sender);
if send_result.is_err() {
// The sampler has already shut down. This task arrived too late.
@@ -138,24 +173,8 @@ pub fn start_recording(
}
});
- let mut root_child = task_launcher.launch_child();
- let mut exit_status = root_child.wait().expect("couldn't wait for child");
-
- for i in 2..=iteration_count {
- if !exit_status.success() {
- eprintln!(
- "Skipping remaining iterations due to non-success exit status: \"{}\"",
- exit_status
- );
- break;
- }
- eprintln!("Running iteration {i} of {iteration_count}...");
- let mut root_child = task_launcher.launch_child();
- exit_status = root_child.wait().expect("couldn't wait for child");
- }
-
- // The launched subprocess is done. From now on, we want to terminate if the user presses Ctrl+C.
- ctrl_c_receiver.close();
+ // Run the root task: either launch or attach to existing pid
+ let exit_status = root_task_runner.run_root_task()?;
accepter_sender
.send(())
diff --git a/samply/src/mac/sampler.rs b/samply/src/mac/sampler.rs
index 62717ffac..8318075e2 100644
--- a/samply/src/mac/sampler.rs
+++ b/samply/src/mac/sampler.rs
@@ -20,6 +20,12 @@ pub enum JitdumpOrMarkerPath {
MarkerFilePath(PathBuf),
}
+#[derive(Debug, Clone)]
+pub enum TaskInitOrShutdown {
+ TaskInit(TaskInit),
+ Shutdown,
+}
+
#[derive(Debug, Clone)]
pub struct TaskInit {
pub start_time_mono: u64,
@@ -30,7 +36,7 @@ pub struct TaskInit {
pub struct Sampler {
command_name: String,
- task_receiver: Receiver,
+ task_receiver: Receiver,
recording_props: Arc,
profile_creation_props: Arc,
}
@@ -38,7 +44,7 @@ pub struct Sampler {
impl Sampler {
pub fn new(
command: String,
- task_receiver: Receiver,
+ task_receiver: Receiver,
recording_props: RecordingProps,
profile_creation_props: ProfileCreationProps,
) -> Self {
@@ -80,7 +86,11 @@ impl Sampler {
CategoryPairHandle::from(profile.add_category("User", CategoryColor::Yellow));
let root_task_init = match self.task_receiver.recv() {
- Ok(task_init) => task_init,
+ Ok(TaskInitOrShutdown::TaskInit(task_init)) => task_init,
+ Ok(TaskInitOrShutdown::Shutdown) => {
+ eprintln!("Unexpected Shutdown message for root task?");
+ return Err(SamplingError::CouldNotObtainRootTask);
+ }
Err(_) => {
// The sender went away. No profiling today.
return Err(SamplingError::CouldNotObtainRootTask);
@@ -109,10 +119,11 @@ impl Sampler {
let mut unwinder_cache = Default::default();
let mut unresolved_stacks = UnresolvedStacks::default();
let mut last_sleep_overshoot = 0;
+ let mut stop_profiling = false;
loop {
loop {
- let task_init = if !live_tasks.is_empty() {
+ let task_init_or_shutdown = if !live_tasks.is_empty() {
// Poll to see if there are any new tasks we should add. If no new tasks are available,
// this completes immediately.
self.task_receiver.try_recv().ok()
@@ -122,7 +133,17 @@ impl Sampler {
let all_dead_timeout = Duration::from_secs_f32(0.5);
self.task_receiver.recv_timeout(all_dead_timeout).ok()
};
- let Some(task_init) = task_init else { break };
+ let Some(task_or_shutdown) = task_init_or_shutdown else {
+ break;
+ };
+ let task_init = match task_or_shutdown {
+ TaskInitOrShutdown::TaskInit(task_init) => task_init,
+ TaskInitOrShutdown::Shutdown => {
+ // We got a shutdown message, so we should just stop profiling.
+ stop_profiling = true;
+ break;
+ }
+ };
if let Ok(new_task) = TaskProfiler::new(
task_init,
timestamp_converter,
@@ -139,6 +160,11 @@ impl Sampler {
}
}
+ if stop_profiling {
+ eprintln!("Stopping profile.");
+ break;
+ }
+
if live_tasks.is_empty() {
eprintln!("All tasks terminated.");
break;