Skip to content

Commit 566f815

Browse files
vstinnerstratakis
andauthored
[3.14] gh-156701: Backport test_dtrace from main (#159596)
* gh-98894: Fix DTrace test_check_probes for shared builds (#151122) When building with --enable-shared, the SystemTap/DTrace notes live in libpython. Add detection logic to be used by readelf. Force the C locale on readelf output. (cherry picked from commit 3a08e93) * gh-151496: Use process groups in test_dtrace (#151512) Create a new process group to run bpftrace commands, so it's possible to kill also child processes on timeout. (cherry picked from commit a064b32) * gh-151496: Use process groups in TraceBackend in test_dtrace (#152039) Run the generic DTrace/SystemTap commands in a new process group and terminate the whole group on timeout. This prevents a forked tracer child from keeping stdout/stderr pipes open after the direct tracer process is killed. (cherry picked from commit 1785f4b) * gh-98894: Check readelf failures in test_dtrace (#152345) Report readelf command failures directly instead of later failing with missing-probe assertions. (cherry picked from commit d60a677) * gh-98894: Check tracer exit status in test_dtrace (#152893) Fail functional dtrace and SystemTap cases directly when the tracer exits non-zero. (cherry picked from commit 311e7e7) * gh-98894: Quote test_dtrace tracer subcommands (#152901) Use shlex.join() when building tracer -c command strings for avoiding issues with spaces. (cherry picked from commit a159c68) * gh-98894: Fix dtrace tests in shared builds (#153372) Generate SystemTap probe definitions targeting libpython for shared builds and use centralized USDT probe object discovery for readelf and BPFTrace. (cherry picked from commit 298bef3) * gh-156703: Set LD_LIBRARY_PATH in SystemTap trace commands (GH-156424) Prefix commands passed to stap -c with LD_LIBRARY_PATH via env, when probes reside in libpython. This allows the interpreter to find libpython when staprun does not preserve the loader path. (cherry picked from commit faf4e52) * gh-156722, test_dtrace: Terminate tracing processes gracefully on timeout (#156753) Send SIGTERM to the tracing process group before falling back to SIGKILL. (cherry picked from commit 55ecc0a) * gh-156701: Increase the SystemTap usability timeout (#156720) SystemTap may compile a kernel module during its usability check (if no cached module is available). We need to allow a larger timeout for that case. (cherry picked from commit e1be613) --------- Co-authored-by: stratakis <cstratak@redhat.com> Co-authored-by: stratakis <stratakishar@gmail.com>
1 parent d96c93e commit 566f815

3 files changed

Lines changed: 180 additions & 45 deletions

File tree

‎Lib/test/dtracedata/call_stack.stp‎

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ function basename:string(path:string)
1010
return last_token;
1111
}
1212

13-
probe process.mark("function__entry")
13+
probe @PYTHON_SYSTEMTAP_PROBE@("function__entry")
1414
{
1515
funcname = user_string($arg2);
1616

@@ -19,7 +19,8 @@ probe process.mark("function__entry")
1919
}
2020
}
2121

22-
probe process.mark("function__entry"), process.mark("function__return")
22+
probe @PYTHON_SYSTEMTAP_PROBE@("function__entry"),
23+
@PYTHON_SYSTEMTAP_PROBE@("function__return")
2324
{
2425
filename = user_string($arg1);
2526
funcname = user_string($arg2);
@@ -31,7 +32,7 @@ probe process.mark("function__entry"), process.mark("function__return")
3132
}
3233
}
3334

34-
probe process.mark("function__return")
35+
probe @PYTHON_SYSTEMTAP_PROBE@("function__return")
3536
{
3637
funcname = user_string($arg2);
3738

‎Lib/test/dtracedata/gc.stp‎

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
global tracing
22

3-
probe process.mark("function__entry")
3+
probe @PYTHON_SYSTEMTAP_PROBE@("function__entry")
44
{
55
funcname = user_string($arg2);
66

@@ -9,14 +9,15 @@ probe process.mark("function__entry")
99
}
1010
}
1111

12-
probe process.mark("gc__start"), process.mark("gc__done")
12+
probe @PYTHON_SYSTEMTAP_PROBE@("gc__start"),
13+
@PYTHON_SYSTEMTAP_PROBE@("gc__done")
1314
{
1415
if (tracing) {
1516
printf("%d\t%s:%ld\n", gettimeofday_us(), $$name, $arg1);
1617
}
1718
}
1819

19-
probe process.mark("function__return")
20+
probe @PYTHON_SYSTEMTAP_PROBE@("function__return")
2021
{
2122
funcname = user_string($arg2);
2223

‎Lib/test/test_dtrace.py‎

Lines changed: 172 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,18 @@
11
import dis
22
import os.path
33
import re
4+
import shlex
5+
import signal
46
import subprocess
57
import sys
68
import sysconfig
9+
import tempfile
710
import types
811
import unittest
912

1013
from test import support
1114
from test.support import findfile, MS_WINDOWS
15+
from test.support import os_helper
1216

1317

1418
if not support.has_subprocess_support:
@@ -23,6 +27,31 @@ def abspath(filename):
2327
return os.path.abspath(findfile(filename, subdir="dtracedata"))
2428

2529

30+
def get_probe_binary():
31+
binary = sys.executable
32+
if sysconfig.get_config_var("Py_ENABLE_SHARED"):
33+
lib_dir = sysconfig.get_config_var("LIBDIR")
34+
if not lib_dir or sysconfig.is_python_build():
35+
lib_dir = os.path.abspath(os.path.dirname(sys.executable))
36+
37+
lib_names = []
38+
for name in (
39+
sysconfig.get_config_var("INSTSONAME"),
40+
sysconfig.get_config_var("LDLIBRARY"),
41+
):
42+
if name and name not in lib_names:
43+
lib_names.append(name)
44+
45+
if lib_dir:
46+
for name in lib_names:
47+
libpython_path = os.path.join(lib_dir, name)
48+
if os.path.exists(libpython_path):
49+
binary = libpython_path
50+
break
51+
52+
return binary
53+
54+
2655
def normalize_trace_output(output):
2756
"""Normalize DTrace output for comparison.
2857
@@ -48,16 +77,77 @@ def normalize_trace_output(output):
4877
)
4978

5079

80+
USE_PROCESS_GROUP = (hasattr(os, "setsid") and hasattr(os, "killpg"))
81+
TERMINATE_TIMEOUT = 10
82+
83+
def create_process_group(*args, **kwargs):
84+
if USE_PROCESS_GROUP:
85+
kwargs['start_new_session'] = True
86+
return subprocess.Popen(*args, **kwargs)
87+
88+
def terminate_process_group(proc):
89+
if USE_PROCESS_GROUP:
90+
try:
91+
os.killpg(proc.pid, signal.SIGTERM)
92+
except ProcessLookupError:
93+
pass
94+
else:
95+
proc.terminate()
96+
97+
try:
98+
proc.communicate(timeout=TERMINATE_TIMEOUT)
99+
except subprocess.TimeoutExpired:
100+
if USE_PROCESS_GROUP:
101+
try:
102+
os.killpg(proc.pid, signal.SIGKILL)
103+
except ProcessLookupError:
104+
pass
105+
else:
106+
proc.kill()
107+
proc.communicate(timeout=TERMINATE_TIMEOUT) # Clean up
108+
109+
110+
def run_readelf(cmd):
111+
# Force the C locale to disable localization.
112+
env = dict(os.environ, LC_ALL="C")
113+
try:
114+
proc = subprocess.Popen(
115+
cmd,
116+
stdout=subprocess.PIPE,
117+
stderr=subprocess.PIPE,
118+
text=True,
119+
env=env,
120+
)
121+
except OSError:
122+
raise unittest.SkipTest("Couldn't find readelf on the path")
123+
124+
with proc:
125+
stdout, stderr = proc.communicate()
126+
127+
if proc.returncode:
128+
raise AssertionError(
129+
f"Command {shlex.join(cmd)!r} failed "
130+
f"with exit code {proc.returncode}: "
131+
f"stdout={stdout!r} stderr={stderr!r}"
132+
)
133+
134+
return stdout
135+
136+
51137
class TraceBackend:
52138
EXTENSION = None
53139
COMMAND = None
54140
COMMAND_ARGS = []
141+
USABILITY_TIMEOUT = 10
55142

56143
def run_case(self, name, optimize_python=None):
57-
actual_output = normalize_trace_output(self.trace_python(
58-
script_file=abspath(name + self.EXTENSION),
59-
python_file=abspath(name + ".py"),
60-
optimize_python=optimize_python))
144+
try:
145+
actual_output = normalize_trace_output(self.trace_python(
146+
script_file=abspath(name + self.EXTENSION),
147+
python_file=abspath(name + ".py"),
148+
optimize_python=optimize_python))
149+
except subprocess.TimeoutExpired:
150+
raise AssertionError(f"{self.COMMAND[0]} timed out")
61151

62152
with open(abspath(name + self.EXTENSION + ".expected")) as f:
63153
expected_output = f.read().rstrip()
@@ -70,25 +160,42 @@ def generate_trace_command(self, script_file, subcommand=None):
70160
command += ["-c", subcommand]
71161
return command
72162

73-
def trace(self, script_file, subcommand=None):
163+
def trace(self, script_file, subcommand=None, *, timeout=None,
164+
check_returncode=False):
74165
command = self.generate_trace_command(script_file, subcommand)
75-
stdout, _ = subprocess.Popen(command,
76-
stdout=subprocess.PIPE,
77-
stderr=subprocess.STDOUT,
78-
universal_newlines=True).communicate()
166+
proc = create_process_group(command,
167+
stdout=subprocess.PIPE,
168+
stderr=subprocess.STDOUT,
169+
universal_newlines=True)
170+
try:
171+
stdout, _ = proc.communicate(timeout=timeout)
172+
except subprocess.TimeoutExpired:
173+
terminate_process_group(proc)
174+
raise
175+
if check_returncode and proc.returncode:
176+
raise AssertionError(
177+
f"Command {shlex.join(command)!r} failed "
178+
f"with exit code {proc.returncode}: output={stdout!r}"
179+
)
79180
return stdout
80181

81182
def trace_python(self, script_file, python_file, optimize_python=None):
82183
python_flags = []
83184
if optimize_python:
84185
python_flags.extend(["-O"] * optimize_python)
85-
subcommand = " ".join([sys.executable] + python_flags + [python_file])
86-
return self.trace(script_file, subcommand)
186+
subcommand = shlex.join([sys.executable] + python_flags + [python_file])
187+
return self.trace(script_file, subcommand, timeout=60,
188+
check_returncode=True)
87189

88190
def assert_usable(self):
89191
try:
90-
output = self.trace(abspath("assert_usable" + self.EXTENSION))
192+
output = self.trace(abspath("assert_usable" + self.EXTENSION),
193+
timeout=self.USABILITY_TIMEOUT)
91194
output = output.strip()
195+
except subprocess.TimeoutExpired:
196+
raise unittest.SkipTest(
197+
f"{self.COMMAND[0]} timed out during usability check"
198+
)
92199
except (FileNotFoundError, NotADirectoryError, PermissionError) as fnfe:
93200
output = str(fnfe)
94201
if output != "probe: success":
@@ -105,6 +212,57 @@ class DTraceBackend(TraceBackend):
105212
class SystemTapBackend(TraceBackend):
106213
EXTENSION = ".stp"
107214
COMMAND = ["stap", "-g"]
215+
PROBE_PLACEHOLDER = "@PYTHON_SYSTEMTAP_PROBE@"
216+
USABILITY_TIMEOUT = 60
217+
218+
@staticmethod
219+
def quote_systemtap_string(value):
220+
return value.replace("\\", "\\\\").replace('"', '\\"')
221+
222+
def python_probe(self):
223+
executable = self.quote_systemtap_string(sys.executable)
224+
probe_binary = get_probe_binary()
225+
if probe_binary == sys.executable:
226+
return f'process("{executable}").mark'
227+
228+
# Python built with --enable-shared
229+
probe_binary = self.quote_systemtap_string(probe_binary)
230+
return f'process("{executable}").library("{probe_binary}").mark'
231+
232+
def render_script(self, filename):
233+
with open(filename) as fp:
234+
script = fp.read()
235+
236+
return script.replace(self.PROBE_PLACEHOLDER, self.python_probe())
237+
238+
def generate_trace_command(self, script_file, subcommand=None):
239+
probe_binary = get_probe_binary()
240+
if subcommand and probe_binary != sys.executable:
241+
library_path = os.path.dirname(probe_binary)
242+
if existing_path := os.environ.get("LD_LIBRARY_PATH"):
243+
library_path = os.pathsep.join((library_path, existing_path))
244+
env = shlex.join(["env", f"LD_LIBRARY_PATH={library_path}"])
245+
subcommand = f"{env} {subcommand}"
246+
247+
return super().generate_trace_command(script_file, subcommand)
248+
249+
def trace(self, script_file, subcommand=None, *, timeout=None,
250+
check_returncode=False):
251+
with tempfile.NamedTemporaryFile(
252+
mode="w", encoding="utf-8", suffix=self.EXTENSION, delete=False
253+
) as script:
254+
script.write(self.render_script(script_file))
255+
generated_script_file = script.name
256+
257+
try:
258+
return super().trace(
259+
generated_script_file,
260+
subcommand,
261+
timeout=timeout,
262+
check_returncode=check_returncode,
263+
)
264+
finally:
265+
os_helper.unlink(generated_script_file)
108266

109267

110268
@unittest.skipIf(MS_WINDOWS, "Tests not compliant with trace on Windows.")
@@ -189,25 +347,7 @@ def setUpClass(cls):
189347

190348
@staticmethod
191349
def get_readelf_version():
192-
try:
193-
cmd = ["readelf", "--version"]
194-
proc = subprocess.Popen(
195-
cmd,
196-
stdout=subprocess.PIPE,
197-
stderr=subprocess.PIPE,
198-
universal_newlines=True,
199-
)
200-
with proc:
201-
version, stderr = proc.communicate()
202-
203-
if proc.returncode:
204-
raise Exception(
205-
f"Command {' '.join(cmd)!r} failed "
206-
f"with exit code {proc.returncode}: "
207-
f"stdout={version!r} stderr={stderr!r}"
208-
)
209-
except OSError:
210-
raise unittest.SkipTest("Couldn't find readelf on the path")
350+
version = run_readelf(["readelf", "--version"])
211351

212352
# Regex to parse:
213353
# 'GNU readelf (GNU Binutils) 2.40.0\n' -> 2.40
@@ -218,14 +358,7 @@ def get_readelf_version():
218358
return int(match.group(1)), int(match.group(2))
219359

220360
def get_readelf_output(self):
221-
command = ["readelf", "-n", sys.executable]
222-
stdout, _ = subprocess.Popen(
223-
command,
224-
stdout=subprocess.PIPE,
225-
stderr=subprocess.STDOUT,
226-
universal_newlines=True,
227-
).communicate()
228-
return stdout
361+
return run_readelf(["readelf", "-n", get_probe_binary()])
229362

230363
def test_check_probes(self):
231364
readelf_output = self.get_readelf_output()

0 commit comments

Comments
 (0)