Skip to content

Commit c2b9431

Browse files
authored
Merge pull request #75 from Moomboh/master
2 parents 6ccc3cb + 82b6685 commit c2b9431

File tree

3 files changed

+64
-44
lines changed

3 files changed

+64
-44
lines changed

src/shellingham/posix/__init__.py

+40-18
Original file line numberDiff line numberDiff line change
@@ -4,34 +4,51 @@
44
from .._core import SHELL_NAMES, ShellDetectionFailure
55
from . import proc, ps
66

7-
8-
def _get_process_mapping():
7+
# Based on QEMU docs: https://www.qemu.org/docs/master/user/main.html
8+
QEMU_BIN_REGEX = re.compile(
9+
r"""qemu-
10+
(alpha
11+
|armeb
12+
|arm
13+
|m68k
14+
|cris
15+
|i386
16+
|x86_64
17+
|microblaze
18+
|mips
19+
|mipsel
20+
|mips64
21+
|mips64el
22+
|mipsn32
23+
|mipsn32el
24+
|nios2
25+
|ppc64
26+
|ppc
27+
|sh4eb
28+
|sh4
29+
|sparc
30+
|sparc32plus
31+
|sparc64
32+
)""",
33+
re.VERBOSE,
34+
)
35+
36+
37+
def _iter_process_parents(pid, max_depth=10):
938
"""Select a way to obtain process information from the system.
1039
1140
* `/proc` is used if supported.
1241
* The system `ps` utility is used as a fallback option.
1342
"""
1443
for impl in (proc, ps):
1544
try:
16-
mapping = impl.get_process_mapping()
45+
iterator = impl.iter_process_parents(pid, max_depth)
1746
except EnvironmentError:
1847
continue
19-
return mapping
48+
return iterator
2049
raise ShellDetectionFailure("compatible proc fs or ps utility is required")
2150

2251

23-
def _iter_process_args(mapping, pid, max_depth):
24-
"""Traverse up the tree and yield each process's argument list."""
25-
for _ in range(max_depth):
26-
try:
27-
proc = mapping[pid]
28-
except KeyError: # We've reached the root process. Give up.
29-
break
30-
if proc.args: # Presumably the process should always have a name?
31-
yield proc.args
32-
pid = proc.ppid # Go up one level.
33-
34-
3552
def _get_login_shell(proc_cmd):
3653
"""Form shell information from SHELL environ if possible."""
3754
login_shell = os.environ.get("SHELL", "")
@@ -71,6 +88,12 @@ def _get_shell(cmd, *args):
7188
if cmd.startswith("-"): # Login shell! Let's use this.
7289
return _get_login_shell(cmd)
7390
name = os.path.basename(cmd).lower()
91+
if name == "rosetta" or QEMU_BIN_REGEX.fullmatch(name):
92+
# If the current process is Rosetta or QEMU, this likely is a
93+
# containerized process. Parse out the actual command instead.
94+
cmd = args[0]
95+
args = args[1:]
96+
name = os.path.basename(cmd).lower()
7497
if name in SHELL_NAMES: # Command looks like a shell.
7598
return (name, cmd)
7699
shell = _get_interpreter_shell(name, args)
@@ -82,8 +105,7 @@ def _get_shell(cmd, *args):
82105
def get_shell(pid=None, max_depth=10):
83106
"""Get the shell that the supplied pid or os.getpid() is running in."""
84107
pid = str(pid or os.getpid())
85-
mapping = _get_process_mapping()
86-
for proc_args in _iter_process_args(mapping, pid, max_depth):
108+
for proc_args, _, _ in _iter_process_parents(pid, max_depth):
87109
shell = _get_shell(*proc_args)
88110
if shell:
89111
return shell

src/shellingham/posix/proc.py

+11-21
Original file line numberDiff line numberDiff line change
@@ -9,11 +9,9 @@
99
# NetBSD: https://man.netbsd.org/NetBSD-9.3-STABLE/mount_procfs.8
1010
# DragonFlyBSD: https://www.dragonflybsd.org/cgi/web-man?command=procfs
1111
BSD_STAT_PPID = 2
12-
BSD_STAT_TTY = 5
1312

1413
# See https://docs.kernel.org/filesystems/proc.html
1514
LINUX_STAT_PPID = 3
16-
LINUX_STAT_TTY = 6
1715

1816
STAT_PATTERN = re.compile(r"\(.+\)|\S+")
1917

@@ -41,14 +39,14 @@ def _use_bsd_stat_format():
4139
return False
4240

4341

44-
def _get_stat(pid, name):
42+
def _get_ppid(pid, name):
4543
path = os.path.join("/proc", str(pid), name)
4644
with io.open(path, encoding="ascii", errors="replace") as f:
4745
parts = STAT_PATTERN.findall(f.read())
4846
# We only care about TTY and PPID -- both are numbers.
4947
if _use_bsd_stat_format():
50-
return parts[BSD_STAT_TTY], parts[BSD_STAT_PPID]
51-
return parts[LINUX_STAT_TTY], parts[LINUX_STAT_PPID]
48+
return parts[BSD_STAT_PPID]
49+
return parts[LINUX_STAT_PPID]
5250

5351

5452
def _get_cmdline(pid):
@@ -66,21 +64,13 @@ class ProcFormatError(EnvironmentError):
6664
pass
6765

6866

69-
def get_process_mapping():
67+
def iter_process_parents(pid, max_depth=10):
7068
"""Try to look up the process tree via the /proc interface."""
7169
stat_name = detect_proc()
72-
self_tty = _get_stat(os.getpid(), stat_name)[0]
73-
processes = {}
74-
for pid in os.listdir("/proc"):
75-
if not pid.isdigit():
76-
continue
77-
try:
78-
tty, ppid = _get_stat(pid, stat_name)
79-
if tty != self_tty:
80-
continue
81-
args = _get_cmdline(pid)
82-
processes[pid] = Process(args=args, pid=pid, ppid=ppid)
83-
except IOError:
84-
# Process has disappeared - just ignore it.
85-
continue
86-
return processes
70+
for _ in range(max_depth):
71+
ppid = _get_ppid(pid, stat_name)
72+
args = _get_cmdline(pid)
73+
yield Process(args=args, pid=pid, ppid=ppid)
74+
if ppid == "0":
75+
break
76+
pid = ppid

src/shellingham/posix/ps.py

+13-5
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ class PsNotAvailable(EnvironmentError):
99
pass
1010

1111

12-
def get_process_mapping():
12+
def iter_process_parents(pid, max_depth=10):
1313
"""Try to look up the process tree via the output of `ps`."""
1414
try:
1515
cmd = ["ps", "-ww", "-o", "pid=", "-o", "ppid=", "-o", "args="]
@@ -22,12 +22,13 @@ def get_process_mapping():
2222
# `ps` can return 1 if the process list is completely empty.
2323
# (sarugaku/shellingham#15)
2424
if not e.output.strip():
25-
return {}
25+
return
2626
raise
2727
if not isinstance(output, str):
2828
encoding = sys.getfilesystemencoding() or sys.getdefaultencoding()
2929
output = output.decode(encoding)
30-
processes = {}
30+
31+
processes_mapping = {}
3132
for line in output.split("\n"):
3233
try:
3334
pid, ppid, args = line.strip().split(None, 2)
@@ -39,5 +40,12 @@ def get_process_mapping():
3940
args = tuple(a.strip() for a in args.split(" "))
4041
except ValueError:
4142
continue
42-
processes[pid] = Process(args=args, pid=pid, ppid=ppid)
43-
return processes
43+
processes_mapping[pid] = Process(args=args, pid=pid, ppid=ppid)
44+
45+
for _ in range(max_depth):
46+
try:
47+
process = processes_mapping[pid]
48+
except KeyError:
49+
return
50+
yield process
51+
pid = process.ppid

0 commit comments

Comments
 (0)