Skip to content

Commit a587fd6

Browse files
Fix trailing whitespace
1 parent 4bc842e commit a587fd6

File tree

4 files changed

+220
-0
lines changed

4 files changed

+220
-0
lines changed
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
from dis import dis, Bytecode
2+
import inspect
3+
4+
source = """
5+
def func():
6+
x, y = 0, 1
7+
z = (x or 1) if y else 1
8+
print(z)
9+
"""
10+
11+
# source = """
12+
# def func():
13+
# z = 0.1
14+
# if z:
15+
# x, y = 0, 1
16+
# else:
17+
# x, y = 1, 0
18+
# print(x, y)
19+
# """
20+
21+
func = compile(source, "inline_bug_report.py", "exec", optimize=2)
22+
23+
print(dis(func))
24+
25+
# for name, value in inspect.getmembers(func.__code__):
26+
# print(name, value)
27+
28+
# print("-- lines are --")
29+
# lines = [line for line in func.__code__.co_lines()]
30+
# print(lines)
31+
32+
# for code in Bytecode(func):
33+
# print(f"{code.line_number} {code.opcode:06x} {code.opname}")
Lines changed: 138 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,138 @@
1+
# N.B.: We apply the monkeypatch before subprocess is imported because subprocess will
2+
# hold strong references to os.waitpid.
3+
from __future__ import annotations
4+
5+
import os
6+
import sys
7+
import textwrap
8+
import traceback
9+
from functools import wraps
10+
11+
orig_waitpid = os.waitpid
12+
orig_kill = os.kill
13+
freed_pids = set[int]()
14+
15+
16+
@wraps(orig_waitpid)
17+
def waitpid(pid: int, options: int, /) -> tuple[int, int]:
18+
print(f"--DBG: start waitpid({pid!r}, {options!r}) @")
19+
print(
20+
textwrap.indent(
21+
"".join(traceback.extract_stack(sys._getframe(1), limit=2).format()),
22+
prefix=" " * (-2 + len("--DBG: ")),
23+
),
24+
end="",
25+
)
26+
try:
27+
res = orig_waitpid(pid, options)
28+
except BaseException as exc:
29+
print(f"--DBG: finish waitpid({pid!r}, {options!r}) -> {exc!r}")
30+
raise
31+
else:
32+
res_pid, status = res
33+
if res_pid != 0:
34+
freed_pids.add(res_pid)
35+
print(f"--DBG: finish waitpid({pid!r}, {options!r}) = {res!r}")
36+
return res
37+
38+
39+
@wraps(orig_kill)
40+
def kill(pid: int, sig: int, /) -> None:
41+
print(f"--DBG: kill({pid}, {sig})")
42+
if pid in freed_pids:
43+
raise ValueError(
44+
"caller is trying to signal an already-freed PID! did a site call waitpid without telling the sites with references to that PID about it?"
45+
)
46+
return orig_kill(pid, sig)
47+
48+
49+
os.waitpid = waitpid
50+
os.kill = kill
51+
52+
assert "subprocess" not in sys.modules
53+
54+
import asyncio
55+
import subprocess
56+
from signal import Signals as Signal
57+
from typing import Literal
58+
from typing import assert_never
59+
60+
61+
async def main() -> None:
62+
_watcher_case: Literal["_PidfdChildWatcher", "_ThreadedChildWatcher"]
63+
if sys.version_info >= (3, 14):
64+
_watcher = asyncio.get_running_loop()._watcher # type: ignore[attr-defined]
65+
if isinstance(_watcher, asyncio.unix_events._PidfdChildWatcher): # type: ignore[attr-defined]
66+
_watcher_case = "_PidfdChildWatcher"
67+
elif isinstance(_watcher, asyncio.unix_events._ThreadedChildWatcher): # type: ignore[attr-defined]
68+
_watcher_case = "_ThreadedChildWatcher"
69+
else:
70+
raise NotImplementedError()
71+
else:
72+
_watcher = asyncio.get_child_watcher()
73+
if isinstance(_watcher, asyncio.PidfdChildWatcher):
74+
_watcher_case = "_PidfdChildWatcher"
75+
elif isinstance(_watcher, asyncio.ThreadedChildWatcher):
76+
_watcher_case = "_ThreadedChildWatcher"
77+
else:
78+
raise NotImplementedError()
79+
print(f"{_watcher_case = !r}")
80+
81+
process = await asyncio.create_subprocess_exec(
82+
"python",
83+
"-c",
84+
"import time; time.sleep(1)",
85+
stdin=subprocess.DEVNULL,
86+
stdout=subprocess.DEVNULL,
87+
stderr=subprocess.DEVNULL,
88+
)
89+
print(f"{process.pid = !r}")
90+
91+
process.send_signal(Signal.SIGKILL)
92+
93+
# This snippet is contrived, in order to make this snippet hit the race condition
94+
# consistently for reproduction & testing purposes.
95+
if _watcher_case == "_PidfdChildWatcher":
96+
os.waitid(os.P_PID, process.pid, os.WEXITED | os.WNOWAIT)
97+
# Or alternatively, time.sleep(0.1).
98+
99+
# On the next loop cycle asyncio will select on the pidfd and append the reader
100+
# callback:
101+
await asyncio.sleep(0)
102+
# On the next loop cycle the reader callback will run, calling (a) waitpid
103+
# (freeing the PID) and (b) call_soon_threadsafe(transport._process_exited):
104+
await asyncio.sleep(0)
105+
106+
# The _PidfdChildWatcher has now freed the PID but hasn't yet told the
107+
# asyncio.subprocess.Process or the subprocess.Popen about this
108+
# (call_soon_threadsafe).
109+
elif _watcher_case == "_ThreadedChildWatcher":
110+
if (thread := _watcher._threads.get(process.pid)) is not None: # type: ignore[attr-defined]
111+
thread.join()
112+
# Or alternatively, time.sleep(0.1).
113+
114+
# The _ThreadedChildWatcher has now freed the PID but hasn't yet told the
115+
# asyncio.subprocess.Process or the subprocess.Popen about this
116+
# (call_soon_threadsafe).
117+
else:
118+
assert_never(_watcher_case)
119+
120+
# The watcher has now freed the PID but hasn't yet told the
121+
# asyncio.subprocess.Process or the subprocess.Popen that the PID they hold a
122+
# reference to has been freed externally!
123+
#
124+
# I think these two things need to happen atomically.
125+
126+
try:
127+
process.send_signal(Signal.SIGKILL)
128+
except ProcessLookupError:
129+
pass
130+
131+
132+
# Pretend we don't have pidfd support
133+
# if sys.version_info >= (3, 14):
134+
# asyncio.unix_events.can_use_pidfd = lambda: False # type: ignore[attr-defined]
135+
# else:
136+
# asyncio.set_child_watcher(asyncio.ThreadedChildWatcher())
137+
138+
asyncio.run(main())

test_bugs_sprint_ep2025/simple_test.py

Whitespace-only changes.
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
import subprocess
2+
from subprocess import Popen
3+
import shlex
4+
5+
print("""\ncalling 'shlex.quote("for")'""")
6+
subprocess.call(shlex.quote("for"), shell=True)
7+
8+
print("""\ncalling 'shlex.quote("'for'")'""")
9+
subprocess.call(shlex.quote("'for'"), shell=True)
10+
11+
print("""\ncalling "'for'" """)
12+
subprocess.call("'for'", shell=True, env={'PATH': '.'})
13+
14+
print("""\ncalling "for" """)
15+
subprocess.call("for", shell=True, env={'PATH': '.'})
16+
17+
# import os, shlex, shutil, subprocess
18+
# open("do", "w").write("#!/bin/sh\necho Something is being done...")
19+
20+
# os.chmod("do", 0o700)
21+
22+
# subprocess.call(shlex.quote("'./my_command'"), shell=True)
23+
# subprocess.call("'my_command'", shell=True, env={'PATH': '.'})
24+
# subprocess.run(shlex.quote("do"), shell=True, env={'PATH': '.'})
25+
26+
# print(shlex.quote("my_command"))
27+
28+
2
29+
# p = Popen(shlex.split("mycommand"), shell=False, executable="/bin/bash")
30+
# print(p)
31+
32+
# test = shlex.quote("done")
33+
# print(test)
34+
35+
# class MyError(Exception):
36+
# def __init__(self):
37+
# print("Hello")
38+
39+
40+
# class SomeProcessError(MyError):
41+
# def __init__(self, returncode):
42+
# self.returncode = returncode
43+
44+
# def __str__(self):
45+
# return f"Died with returncode: {self.returncode}"
46+
47+
# raise SomeProcessError(3)
48+
49+

0 commit comments

Comments
 (0)