#[macro_use]
extern crate clap;
extern crate console;
extern crate ctrlc;
extern crate env_logger;
#[macro_use]
extern crate failure;
extern crate goblin;
extern crate indicatif;
#[macro_use]
extern crate lazy_static;
extern crate libc;
#[cfg(target_os = "macos")]
extern crate libproc;
#[cfg(target_os = "macos")]
extern crate mach;
#[cfg(target_os = "linux")]
extern crate nix;
#[macro_use]
extern crate log;
extern crate memmap;
extern crate proc_maps;
extern crate benfred_read_process_memory as read_process_memory;
extern crate regex;
extern crate tempdir;
extern crate tempfile;
#[cfg(unix)]
extern crate termios;
#[cfg(windows)]
extern crate winapi;
mod config;
mod binary_parser;
mod python_bindings;
mod python_interpreters;
mod python_spy;
mod stack_trace;
mod console_viewer;
mod flamegraph;
mod process;
mod utils;
use std::io::Read;
use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::Arc;
use std::time::Duration;
use failure::Error;
use python_spy::PythonSpy;
use stack_trace::StackTrace;
use console_viewer::ConsoleViewer;
fn print_traces(traces: &[StackTrace], show_idle: bool) {
for trace in traces {
if !show_idle && !trace.active {
continue;
}
println!("Thread {:#X} ({})", trace.thread_id, trace.status_str());
for frame in &trace.frames {
let filename = match &frame.short_filename { Some(f) => &f, None => &frame.filename };
println!("\t {} ({}:{})", frame.name, filename, frame.line);
}
}
}
fn process_exitted(err: &Error) -> bool {
err.iter_chain().any(|cause| {
if let Some(ioerror) = cause.downcast_ref::<std::io::Error>() {
if let Some(err_code) = ioerror.raw_os_error() {
if err_code == 3 || err_code == 60 || err_code == 299 {
return true;
}
}
}
false
})
}
#[cfg(unix)]
fn permission_denied(err: &Error) -> bool {
err.iter_chain().any(|cause| {
if let Some(ioerror) = cause.downcast_ref::<std::io::Error>() {
ioerror.kind() == std::io::ErrorKind::PermissionDenied
} else {
false
}
})
}
fn sample_console(process: &PythonSpy,
display: &str,
config: &config::Config) -> Result<(), Error> {
let rate = config.sampling_rate;
let mut console = ConsoleViewer::new(config.show_line_numbers, display,
&format!("{}", process.version),
1.0 / rate as f64)?;
let mut exitted_count = 0;
for sleep in utils::Timer::new(Duration::from_nanos(1_000_000_000 / rate)) {
if let Err(elapsed) = sleep {
console.increment_late_sample(elapsed);
}
match process.get_stack_traces() {
Ok(traces) => {
console.increment(&traces)?;
},
Err(err) => {
if process_exitted(&err) {
exitted_count += 1;
if exitted_count > 5 {
println!("\nprocess {} ended", process.pid);
break;
}
} else {
console.increment_error(&err);
}
}
}
}
Ok(())
}
fn sample_flame(process: &PythonSpy, filename: &str, config: &config::Config) -> Result<(), Error> {
let max_samples = config.duration * config.sampling_rate;
let mut flame = flamegraph::Flamegraph::new(config.show_line_numbers);
use indicatif::ProgressBar;
let progress = ProgressBar::new(max_samples);
println!("Sampling process {} times a second for {} seconds. Press Control-C to exit.",
config.sampling_rate, config.duration);
let mut errors = 0;
let mut samples = 0;
let mut exitted_count = 0;
println!();
let running = Arc::new(AtomicBool::new(true));
let r = running.clone();
ctrlc::set_handler(move || {
r.store(false, Ordering::SeqCst);
})?;
let mut exit_message = "";
for sleep in utils::Timer::new(Duration::from_nanos(1_000_000_000 / config.sampling_rate)) {
if let Err(delay) = sleep {
if delay > Duration::from_secs(1) {
let term = console::Term::stdout();
term.move_cursor_up(2)?;
println!("{:.2?} behind in sampling, results may be inaccurate. Try reducing the sampling rate.", delay);
term.move_cursor_down(1)?;
}
}
if !running.load(Ordering::SeqCst) {
exit_message = "Stopped sampling because Control-C pressed";
break;
}
match process.get_stack_traces() {
Ok(traces) => {
flame.increment(&traces)?;
samples += 1;
if samples >= max_samples {
break;
}
},
Err(err) => {
if process_exitted(&err) {
exitted_count += 1;
if exitted_count > 3 {
exit_message = "Stopped sampling because the process ended";
break;
}
}
errors += 1;
}
}
progress.inc(1);
}
progress.finish();
if exit_message.len() > 0 {
println!("{}", exit_message);
}
let out_file = std::fs::File::create(filename)?;
flame.write(out_file)?;
println!("Wrote flame graph '{}'. Samples: {} Errors: {}", filename, samples, errors);
#[cfg(target_os = "macos")]
std::process::Command::new("open").arg(filename).spawn()?;
Ok(())
}
fn pyspy_main() -> Result<(), Error> {
let config = config::Config::from_commandline()?;
#[cfg(target_os="macos")]
{
if unsafe { libc::geteuid() } != 0 {
eprintln!("This program requires root on OSX.");
eprintln!("Try running again with elevated permissions by going 'sudo !!'");
std::process::exit(1)
}
}
if let Some(pid) = config.pid {
let process = PythonSpy::retry_new(pid, &config, 3)?;
if config.dump {
print_traces(&process.get_stack_traces()?, true);
} else if let Some(ref flame_file) = config.flame_file_name {
sample_flame(&process, &flame_file, &config)?;
} else {
sample_console(&process, &format!("pid: {}", pid), &config)?;
}
}
else if let Some(ref subprocess) = config.python_program {
let mut process_output = tempfile::NamedTempFile::new()?;
let mut command = std::process::Command::new(&subprocess[0])
.args(&subprocess[1..])
.stdin(std::process::Stdio::null())
.stdout(process_output.reopen()?)
.stderr(process_output.reopen()?)
.spawn().map_err(|e| format_err!("Failed to create process '{}': {}", subprocess[0], e))?;
#[cfg(target_os="macos")]
{
std::thread::sleep(Duration::from_millis(50));
}
let result = match PythonSpy::retry_new(command.id() as read_process_memory::Pid, &config, 8) {
Ok(process) => {
if let Some(ref flame_file) = config.flame_file_name {
sample_flame(&process, &flame_file, &config)
} else {
sample_console(&process, &subprocess.join(" "), &config)
}
},
Err(e) => Err(e)
};
std::thread::sleep(Duration::from_millis(1));
let success = match command.try_wait()? {
Some(exit) => exit.success(),
None => true
};
if !success || result.is_err() {
let mut buffer = String::new();
if process_output.read_to_string(&mut buffer).is_ok() {
eprintln!("{}", buffer);
}
}
if command.kill().is_err() {
}
return result;
}
Ok(())
}
fn main() {
env_logger::init();
if let Err(err) = pyspy_main() {
#[cfg(unix)]
{
if permission_denied(&err) {
eprintln!("Permission Denied: Try running again with elevated permissions by going 'sudo env \"PATH=$PATH\" !!'");
std::process::exit(1);
}
}
eprintln!("Error: {}", err);
for (i, suberror) in err.iter_chain().enumerate() {
if i > 0 {
eprintln!("Reason: {}", suberror);
}
}
eprintln!("{}", err.backtrace());
std::process::exit(1);
}
}