Skip to content

Commit 1b81592

Browse files
authored
Merge branch 'master' into set_gha_workflow_permissions
2 parents 45998dc + 4580345 commit 1b81592

3 files changed

Lines changed: 184 additions & 35 deletions

File tree

src/dump.rs

Lines changed: 68 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
use std::io::Write;
2+
13
use anyhow::{Context, Error};
24
use console::{style, Term};
35

@@ -8,64 +10,91 @@ use crate::stack_trace::StackTrace;
810
use remoteprocess::Pid;
911

1012
pub fn print_traces(pid: Pid, config: &Config, parent: Option<Pid>) -> Result<(), Error> {
13+
let stdout = std::io::stdout();
14+
let mut out = stdout.lock();
15+
write_traces(&mut out, pid, config, parent)
16+
}
17+
18+
pub fn write_traces<W: Write>(
19+
out: &mut W,
20+
pid: Pid,
21+
config: &Config,
22+
parent: Option<Pid>,
23+
) -> Result<(), Error> {
1124
let mut process = PythonSpy::new(pid, config)?;
1225
if config.dump_json {
1326
let traces = process
1427
.get_stack_traces()
1528
.context("Failed to get stack traces")?;
16-
println!("{}", serde_json::to_string_pretty(&traces)?);
29+
writeln!(out, "{}", serde_json::to_string_pretty(&traces)?)?;
1730
return Ok(());
1831
}
1932

20-
println!(
33+
writeln!(
34+
out,
2135
"Process {}: {}",
2236
style(process.pid).bold().yellow(),
2337
process.process.cmdline()?.join(" ")
24-
);
38+
)?;
2539

26-
println!(
40+
writeln!(
41+
out,
2742
"Python v{} ({})",
2843
style(&process.version).bold(),
2944
style(process.process.exe()?).dim()
30-
);
45+
)?;
3146

3247
if let Some(parentpid) = parent {
3348
let parentprocess = remoteprocess::Process::new(parentpid)?;
34-
println!(
49+
writeln!(
50+
out,
3551
"Parent Process {}: {}",
3652
style(parentpid).bold().yellow(),
3753
parentprocess.cmdline()?.join(" ")
38-
);
54+
)?;
3955
}
40-
println!();
56+
writeln!(out)?;
4157
let traces = process
4258
.get_stack_traces()
4359
.context("Failed to get stack traces")?;
4460
for trace in traces.iter().rev() {
45-
print_trace(trace, true);
46-
if config.subprocesses {
47-
for (childpid, parentpid) in process
48-
.process
49-
.child_processes()
50-
.expect("failed to get subprocesses")
51-
{
52-
let term = Term::stdout();
53-
let (_, width) = term.size();
54-
55-
println!("\n{}", &style("-".repeat(width as usize)).dim());
56-
// child_processes() returns the whole process tree, since we're recursing here
57-
// though we could end up printing grandchild processes multiple times. Limit down
58-
// to just once
59-
if parentpid == pid {
60-
print_traces(childpid, config, Some(parentpid))?;
61-
}
61+
write_trace(out, trace, true)?;
62+
}
63+
64+
if config.subprocesses {
65+
for (childpid, parentpid) in process
66+
.process
67+
.child_processes()
68+
.expect("failed to get subprocesses")
69+
{
70+
let term = Term::stdout();
71+
let (_, width) = term.size();
72+
73+
writeln!(out, "\n{}", &style("-".repeat(width as usize)).dim())?;
74+
// child_processes() returns the whole process tree, since we're recursing here
75+
// though we could end up printing grandchild processes multiple times. Limit down
76+
// to just once
77+
if parentpid == pid {
78+
write_traces(out, childpid, config, Some(parentpid))?;
6279
}
6380
}
6481
}
6582
Ok(())
6683
}
6784

85+
#[cfg(target_os = "linux")]
6886
pub fn print_trace(trace: &StackTrace, include_activity: bool) {
87+
let stdout = std::io::stdout();
88+
let mut out = stdout.lock();
89+
// Swallow write errors here to preserve the old println! behaviour.
90+
let _ = write_trace(&mut out, trace, include_activity);
91+
}
92+
93+
pub fn write_trace<W: Write>(
94+
out: &mut W,
95+
trace: &StackTrace,
96+
include_activity: bool,
97+
) -> Result<(), Error> {
6998
let thread_id = trace.format_threadid();
7099

71100
let status = if include_activity {
@@ -78,15 +107,16 @@ pub fn print_trace(trace: &StackTrace, include_activity: bool) {
78107

79108
match trace.thread_name.as_ref() {
80109
Some(name) => {
81-
println!(
110+
writeln!(
111+
out,
82112
"Thread {}{}: \"{}\"",
83113
style(thread_id).bold().yellow(),
84114
status,
85115
name
86-
);
116+
)?;
87117
}
88118
None => {
89-
println!("Thread {}{}", style(thread_id).bold().yellow(), status);
119+
writeln!(out, "Thread {}{}", style(thread_id).bold().yellow(), status)?;
90120
}
91121
};
92122

@@ -96,35 +126,38 @@ pub fn print_trace(trace: &StackTrace, include_activity: bool) {
96126
None => &frame.filename,
97127
};
98128
if frame.line != 0 {
99-
println!(
129+
writeln!(
130+
out,
100131
" {} ({}:{})",
101132
style(&frame.name).green(),
102133
style(&filename).cyan(),
103134
style(frame.line).dim()
104-
);
135+
)?;
105136
} else {
106-
println!(
137+
writeln!(
138+
out,
107139
" {} ({})",
108140
style(&frame.name).green(),
109141
style(&filename).cyan()
110-
);
142+
)?;
111143
}
112144

113145
if let Some(locals) = &frame.locals {
114146
let mut shown_args = false;
115147
let mut shown_locals = false;
116148
for local in locals {
117149
if local.arg && !shown_args {
118-
println!(" {}", style("Arguments:").dim());
150+
writeln!(out, " {}", style("Arguments:").dim())?;
119151
shown_args = true;
120152
} else if !local.arg && !shown_locals {
121-
println!(" {}", style("Locals:").dim());
153+
writeln!(out, " {}", style("Locals:").dim())?;
122154
shown_locals = true;
123155
}
124156

125157
let repr = local.repr.as_deref().unwrap_or("?");
126-
println!(" {}: {}", local.name, repr);
158+
writeln!(out, " {}: {}", local.name, repr)?;
127159
}
128160
}
129161
}
162+
Ok(())
130163
}

tests/integration_test.rs

Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -554,3 +554,84 @@ fn test_delayed_subprocess() {
554554
break;
555555
}
556556
}
557+
558+
#[cfg(not(target_os = "freebsd"))]
559+
#[test]
560+
fn test_dump_subprocesses_no_duplication() {
561+
#[cfg(target_os = "macos")]
562+
{
563+
if unsafe { libc::geteuid() } != 0 {
564+
return;
565+
}
566+
}
567+
568+
use std::io::{BufRead, BufReader};
569+
570+
// Regression test for the dup-per-thread bug in dump.rs: spawning the
571+
// target as a child of this test process makes the test the ptrace
572+
// ancestor, so we don't need elevated privileges on Linux.
573+
console::set_colors_enabled(false);
574+
575+
let mut child = std::process::Command::new("python")
576+
.arg("./tests/scripts/dump_subprocesses_threads.py")
577+
.stdout(std::process::Stdio::piped())
578+
.stderr(std::process::Stdio::null())
579+
.spawn()
580+
.expect("failed to spawn parent script");
581+
582+
struct Killer(std::process::Child);
583+
impl Drop for Killer {
584+
fn drop(&mut self) {
585+
let _ = self.0.kill();
586+
}
587+
}
588+
let stdout = child.stdout.take().unwrap();
589+
let mut child = Killer(child);
590+
591+
let mut parent_pid: Option<Pid> = None;
592+
let mut child_pid: Option<Pid> = None;
593+
let reader = BufReader::new(stdout);
594+
for line in reader.lines() {
595+
let line = line.expect("failed to read from parent stdout");
596+
if let Some(rest) = line.strip_prefix("PID_PARENT=") {
597+
parent_pid = Some(rest.trim().parse().unwrap());
598+
} else if let Some(rest) = line.strip_prefix("PID_CHILD=") {
599+
child_pid = Some(rest.trim().parse().unwrap());
600+
} else if line.trim() == "READY" {
601+
break;
602+
}
603+
}
604+
let parent_pid = parent_pid.expect("parent script never reported PID_PARENT");
605+
let child_pid = child_pid.expect("parent script never reported PID_CHILD");
606+
607+
// Give the child subprocess a moment to finish Python initialization.
608+
std::thread::sleep(std::time::Duration::from_millis(500));
609+
610+
let config = Config {
611+
subprocesses: true,
612+
..Default::default()
613+
};
614+
let mut buf: Vec<u8> = Vec::new();
615+
py_spy::dump::write_traces(&mut buf, parent_pid, &config, None).expect("write_traces failed");
616+
let out = String::from_utf8(buf).expect("dump output was not utf-8");
617+
618+
// Match at line start only, so "Parent Process <pid>:" headers don't count.
619+
let parent_header = format!("Process {}:", parent_pid);
620+
let child_header = format!("Process {}:", child_pid);
621+
let count_line_start =
622+
|needle: &str| -> usize { out.lines().filter(|line| line.starts_with(needle)).count() };
623+
let parent_occurrences = count_line_start(&parent_header);
624+
let child_occurrences = count_line_start(&child_header);
625+
assert_eq!(
626+
parent_occurrences, 1,
627+
"expected parent process header once, got {}:\n{}",
628+
parent_occurrences, out
629+
);
630+
assert_eq!(
631+
child_occurrences, 1,
632+
"expected child process header once, got {}:\n{}",
633+
child_occurrences, out
634+
);
635+
636+
let _ = child.0.kill();
637+
}
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
import os
2+
import subprocess
3+
import sys
4+
import threading
5+
import time
6+
7+
8+
def _worker():
9+
while True:
10+
time.sleep(0.05)
11+
12+
13+
def main():
14+
for _ in range(4):
15+
threading.Thread(target=_worker, daemon=True).start()
16+
17+
child = subprocess.Popen(
18+
[sys.executable, "-c", "import time\nwhile True: time.sleep(1)"],
19+
stdout=subprocess.DEVNULL,
20+
stderr=subprocess.DEVNULL,
21+
)
22+
23+
print("PID_PARENT=%d" % os.getpid(), flush=True)
24+
print("PID_CHILD=%d" % child.pid, flush=True)
25+
print("READY", flush=True)
26+
27+
try:
28+
child.wait()
29+
finally:
30+
if child.poll() is None:
31+
child.kill()
32+
33+
34+
if __name__ == "__main__":
35+
main()

0 commit comments

Comments
 (0)