Skip to content

Commit d098f25

Browse files
committed
chore: move sudo child process code to Linux dir for conditional coverage
Signed-off-by: Josh Usiskin <56369778+jusiskin@users.noreply.github.com>
1 parent 621232c commit d098f25

File tree

2 files changed

+179
-174
lines changed

2 files changed

+179
-174
lines changed
Lines changed: 154 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,154 @@
1+
# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
2+
3+
import glob
4+
import os
5+
import sys
6+
import time
7+
from subprocess import Popen, DEVNULL, PIPE, STDOUT, run
8+
from typing import Optional
9+
10+
from .._logging import LoggerAdapter, LogContent, LogExtraInfo
11+
from .._os_checker import is_posix, is_linux
12+
13+
14+
class FindSignalTargetError(Exception):
15+
"""Exception when unable to detect the signal target"""
16+
17+
pass
18+
19+
20+
def find_sudo_child_process_group_id(
21+
*,
22+
logger: LoggerAdapter,
23+
sudo_process: Popen,
24+
timeout_seconds: float = 1,
25+
) -> Optional[int]:
26+
# Hint to mypy to not raise module attribute errors (e.g. missing os.getpgid)
27+
if sys.platform == "win32":
28+
raise NotImplementedError("This method is for POSIX hosts only")
29+
if not is_posix():
30+
raise NotImplementedError(f"Only POSIX supported, but running on {sys.platform}")
31+
if timeout_seconds <= 0:
32+
raise ValueError(f"Expected positive value for timeout_seconds but got {timeout_seconds}")
33+
34+
# For cross-user support, we use sudo which creates an intermediate process:
35+
#
36+
# openjd-process
37+
# |
38+
# +-- sudo
39+
# |
40+
# +-- subprocess
41+
#
42+
# Sudo forwards signals that it is able to handle, but in the case of SIGKILL sudo cannot
43+
# handle the signal and the kernel will kill it leaving the child orphaned. We need to
44+
# send SIGKILL signals to the subprocess of sudo
45+
start = time.monotonic()
46+
now = start
47+
sudo_pgid = os.getpgid(sudo_process.pid)
48+
49+
# Repeatedly scan for child processes
50+
#
51+
# This is put in a retry loop, because it takes a non-zero amount of time before sudo and
52+
# the kernel finish creating the subprocess. We cap this because the process may exit
53+
# quickly and we may never find the child process.
54+
sudo_child_pid: Optional[int] = None
55+
sudo_child_pgid: Optional[int] = None
56+
try:
57+
while now - start < timeout_seconds:
58+
if not sudo_child_pid:
59+
if is_linux():
60+
sudo_child_pid = find_sudo_child_process_id_procfs(
61+
sudo_pid=sudo_process.pid,
62+
logger=logger,
63+
)
64+
else:
65+
sudo_child_pid = find_child_process_id_pgrep(
66+
sudo_pid=sudo_process.pid,
67+
)
68+
69+
if sudo_child_pid:
70+
try:
71+
sudo_child_pgid = os.getpgid(sudo_child_pid)
72+
except ProcessLookupError:
73+
# If the process has exited, we short-circuit
74+
return None
75+
# sudo first forks, then creates a new process group. There is a race condition
76+
# where the process group ID we observe has not yet changed. If the PGID detected
77+
# matches the PGID of sudo, then we retry again in the loop
78+
if sudo_child_pgid == sudo_pgid:
79+
sudo_child_pgid = None
80+
else:
81+
break
82+
83+
# If we did not find any child processes yet, sleep for some time and retry
84+
time.sleep(min(0.05, now - start))
85+
now = time.monotonic()
86+
if not sudo_child_pid or not sudo_child_pgid:
87+
raise FindSignalTargetError("unable to detect subprocess before timeout")
88+
except FindSignalTargetError as e:
89+
logger.warning(
90+
f"Unable to determine signal target: {e}",
91+
extra=LogExtraInfo(openjd_log_content=LogContent.PROCESS_CONTROL),
92+
)
93+
94+
if sudo_child_pgid:
95+
logger.debug(
96+
f"Signal target PGID = {sudo_child_pgid}",
97+
extra=LogExtraInfo(openjd_log_content=LogContent.PROCESS_CONTROL),
98+
)
99+
100+
return sudo_child_pgid
101+
102+
103+
def find_sudo_child_process_id_procfs(
104+
*,
105+
logger: LoggerAdapter,
106+
sudo_pid: int,
107+
) -> Optional[int]:
108+
# Look for the child process of sudo using procfs. See
109+
# https://docs.kernel.org/filesystems/proc.html#proc-pid-task-tid-children-information-about-task-children
110+
111+
child_pids: set[int] = set()
112+
for task_children_path in glob.glob(f"/proc/{sudo_pid}/task/**/children"):
113+
with open(task_children_path, "r") as f:
114+
child_pids.update(int(pid_str) for pid_str in f.read().split())
115+
116+
# If we found exactly one child, we return it
117+
if len(child_pids) == 1:
118+
119+
child_pid = child_pids.pop()
120+
121+
logger.debug(
122+
f"Session action process (sudo child) PID is {child_pid}",
123+
extra=LogExtraInfo(openjd_log_content=LogContent.PROCESS_CONTROL),
124+
)
125+
return child_pid
126+
# If we found multiple child processes, this violates our assumptions about how sudo
127+
# works. We will fall-back to using pkill for signalling the process
128+
elif len(child_pids) > 1:
129+
raise FindSignalTargetError(
130+
f"Expected single child processes of sudo, but found {child_pids}"
131+
)
132+
return None
133+
134+
135+
def find_child_process_id_pgrep(
136+
*,
137+
sudo_pid: int,
138+
) -> Optional[int]:
139+
pgrep_result = run(
140+
["pgrep", "-P", str(sudo_pid)],
141+
stdout=PIPE,
142+
stderr=STDOUT,
143+
stdin=DEVNULL,
144+
text=True,
145+
)
146+
if pgrep_result.returncode != 0:
147+
raise FindSignalTargetError("Unable to query child processes of sudo process")
148+
results = pgrep_result.stdout.splitlines()
149+
if len(results) > 1:
150+
raise FindSignalTargetError(f"Expected a single child process of sudo, but found {results}")
151+
elif len(results) == 0:
152+
return None
153+
sudo_subproc_pid = int(results[0])
154+
return sudo_subproc_pid

0 commit comments

Comments
 (0)