Skip to content

Commit 8ecc159

Browse files
authored
fix: Fix a bunch of bugs in subprocess integration (getsentry#449)
* fix: Fix a bunch of bugs in subprocess integration * fix: Lints * fix: Add comment * fix: Skip test on pypy
1 parent 5b3412d commit 8ecc159

File tree

2 files changed

+146
-29
lines changed

2 files changed

+146
-29
lines changed

sentry_sdk/integrations/stdlib.py

+35-11
Original file line numberDiff line numberDiff line change
@@ -7,12 +7,15 @@
77
from sentry_sdk.integrations import Integration
88
from sentry_sdk.scope import add_global_event_processor
99
from sentry_sdk.tracing import EnvironHeaders, record_http_request
10+
from sentry_sdk.utils import capture_internal_exceptions, safe_repr
11+
1012

1113
try:
1214
from httplib import HTTPConnection # type: ignore
1315
except ImportError:
1416
from http.client import HTTPConnection
1517

18+
1619
_RUNTIME_CONTEXT = {
1720
"name": platform.python_implementation(),
1821
"version": "%s.%s.%s" % (sys.version_info[:3]),
@@ -123,14 +126,18 @@ def _init_argument(args, kwargs, name, position, setdefault_callback=None):
123126

124127
if name in kwargs:
125128
rv = kwargs[name]
126-
if rv is None and setdefault_callback is not None:
127-
rv = kwargs[name] = setdefault_callback()
129+
if setdefault_callback is not None:
130+
rv = setdefault_callback(rv)
131+
if rv is not None:
132+
kwargs[name] = rv
128133
elif position < len(args):
129134
rv = args[position]
130-
if rv is None and setdefault_callback is not None:
131-
rv = args[position] = setdefault_callback()
135+
if setdefault_callback is not None:
136+
rv = setdefault_callback(rv)
137+
if rv is not None:
138+
args[position] = rv
132139
else:
133-
rv = setdefault_callback and setdefault_callback()
140+
rv = setdefault_callback and setdefault_callback(None)
134141
if rv is not None:
135142
kwargs[name] = rv
136143

@@ -145,20 +152,37 @@ def sentry_patched_popen_init(self, *a, **kw):
145152
if hub.get_integration(StdlibIntegration) is None:
146153
return old_popen_init(self, *a, **kw)
147154

148-
# do not setdefault! args is required by Popen, doing setdefault would
149-
# make invalid calls valid
155+
# Convert from tuple to list to be able to set values.
156+
a = list(a)
157+
150158
args = _init_argument(a, kw, "args", 0) or []
151-
cwd = _init_argument(a, kw, "cwd", 10)
159+
cwd = _init_argument(a, kw, "cwd", 9)
160+
161+
# if args is not a list or tuple (and e.g. some iterator instead),
162+
# let's not use it at all. There are too many things that can go wrong
163+
# when trying to collect an iterator into a list and setting that list
164+
# into `a` again.
165+
#
166+
# Also invocations where `args` is not a sequence are not actually
167+
# legal. They just happen to work under CPython.
168+
description = None
169+
170+
if isinstance(args, (list, tuple)) and len(args) < 100:
171+
with capture_internal_exceptions():
172+
description = " ".join(map(str, args))
173+
174+
if description is None:
175+
description = safe_repr(args)
152176

153177
env = None
154178

155179
for k, v in hub.iter_trace_propagation_headers():
156180
if env is None:
157-
env = _init_argument(a, kw, "env", 11, lambda: dict(os.environ))
181+
env = _init_argument(a, kw, "env", 10, lambda x: dict(x or os.environ))
158182
env["SUBPROCESS_" + k.upper().replace("-", "_")] = v
159183

160-
with hub.span(op="subprocess", description=" ".join(map(str, args))) as span:
161-
span.set_tag("subprocess.cwd", cwd)
184+
with hub.span(op="subprocess", description=description) as span:
185+
span.set_data("subprocess.cwd", cwd)
162186

163187
return old_popen_init(self, *a, **kw)
164188

tests/integrations/stdlib/test_subprocess.py

+111-18
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import os
2+
import platform
23
import subprocess
34
import sys
45

@@ -9,45 +10,137 @@
910
from sentry_sdk.integrations.stdlib import StdlibIntegration
1011

1112

12-
def test_subprocess_basic(sentry_init, capture_events, monkeypatch):
13+
if PY2:
14+
from collections import Mapping
15+
else:
16+
from collections.abc import Mapping
17+
18+
19+
class ImmutableDict(Mapping):
20+
def __init__(self, inner):
21+
self.inner = inner
22+
23+
def __getitem__(self, key):
24+
return self.inner[key]
25+
26+
def __iter__(self):
27+
return iter(self.inner)
28+
29+
def __len__(self):
30+
return len(self.inner)
31+
32+
33+
@pytest.mark.parametrize("positional_args", [True, False])
34+
@pytest.mark.parametrize(
35+
"iterator",
36+
[
37+
pytest.param(
38+
True,
39+
marks=pytest.mark.skipif(
40+
platform.python_implementation() == "PyPy",
41+
reason="https://github.com/getsentry/sentry-python/pull/449",
42+
),
43+
),
44+
False,
45+
],
46+
)
47+
@pytest.mark.parametrize("env_mapping", [None, os.environ, ImmutableDict(os.environ)])
48+
@pytest.mark.parametrize("with_cwd", [True, False])
49+
def test_subprocess_basic(
50+
sentry_init,
51+
capture_events,
52+
monkeypatch,
53+
positional_args,
54+
iterator,
55+
env_mapping,
56+
with_cwd,
57+
):
1358
monkeypatch.setenv("FOO", "bar")
1459

1560
old_environ = dict(os.environ)
1661

1762
sentry_init(integrations=[StdlibIntegration()], traces_sample_rate=1.0)
63+
events = capture_events()
1864

1965
with Hub.current.span(transaction="foo", op="foo") as span:
20-
output = subprocess.check_output(
21-
[
22-
sys.executable,
23-
"-c",
24-
"import os; "
25-
"import sentry_sdk; "
26-
"from sentry_sdk.integrations.stdlib import get_subprocess_traceparent_headers; "
27-
"sentry_sdk.init(); "
28-
"assert os.environ['FOO'] == 'bar'; "
29-
"print(dict(get_subprocess_traceparent_headers()))",
30-
]
31-
)
66+
args = [
67+
sys.executable,
68+
"-c",
69+
"import os; "
70+
"import sentry_sdk; "
71+
"from sentry_sdk.integrations.stdlib import get_subprocess_traceparent_headers; "
72+
"sentry_sdk.init(); "
73+
"assert os.environ['FOO'] == 'bar'; "
74+
"print(dict(get_subprocess_traceparent_headers()))",
75+
]
76+
77+
if iterator:
78+
args = iter(args)
79+
80+
if positional_args:
81+
a = (
82+
args,
83+
0, # bufsize
84+
None, # executable
85+
None, # stdin
86+
subprocess.PIPE, # stdout
87+
None, # stderr
88+
None, # preexec_fn
89+
False, # close_fds
90+
False, # shell
91+
os.getcwd() if with_cwd else None, # cwd
92+
)
93+
94+
if env_mapping is not None:
95+
a += (env_mapping,)
96+
97+
popen = subprocess.Popen(*a)
98+
99+
else:
100+
kw = {"args": args, "stdout": subprocess.PIPE}
101+
102+
if with_cwd:
103+
kw["cwd"] = os.getcwd()
104+
105+
if env_mapping is not None:
106+
kw["env"] = env_mapping
107+
108+
popen = subprocess.Popen(**kw)
109+
110+
output, unused_err = popen.communicate()
111+
retcode = popen.poll()
112+
assert not retcode
32113

33114
assert os.environ == old_environ
34115

35116
assert span.trace_id in str(output)
36117

37-
events = capture_events()
38-
39118
capture_message("hi")
40119

41-
event, = events
120+
transaction_event, message_event, = events
121+
122+
assert message_event["message"] == "hi"
42123

43-
crumb, = event["breadcrumbs"]
124+
data = {"subprocess.cwd": os.getcwd()} if with_cwd else {}
125+
crumb, = message_event["breadcrumbs"]
44126
assert crumb == {
45127
"category": "subprocess",
46-
"data": {},
128+
"data": data,
47129
"timestamp": crumb["timestamp"],
48130
"type": "subprocess",
49131
}
50132

133+
assert transaction_event["type"] == "transaction"
134+
135+
subprocess_span, = transaction_event["spans"]
136+
137+
assert subprocess_span["data"] == data
138+
if iterator:
139+
assert "iterator" in subprocess_span["description"]
140+
assert subprocess_span["description"].startswith("<")
141+
else:
142+
assert sys.executable + " -c" in subprocess_span["description"]
143+
51144

52145
def test_subprocess_invalid_args(sentry_init):
53146
sentry_init(integrations=[StdlibIntegration()])

0 commit comments

Comments
 (0)