Skip to content

Commit 1e1b794

Browse files
committed
Unify checks for subprocess permissions
1 parent f25a1c4 commit 1e1b794

File tree

7 files changed

+411
-545
lines changed

7 files changed

+411
-545
lines changed

Lib/test/support/__init__.py

Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@
3939
"has_fork_support", "requires_fork",
4040
"has_subprocess_support", "requires_subprocess",
4141
"has_socket_support", "requires_working_socket",
42+
"has_remote_subprocess_debugging", "requires_remote_subprocess_debugging",
4243
"anticipate_failure", "load_package_tests", "detect_api_mismatch",
4344
"check__all__", "skip_if_buggy_ucrt_strfptime",
4445
"check_disallow_instantiation", "check_sanitizer", "skip_if_sanitizer",
@@ -643,6 +644,93 @@ def requires_working_socket(*, module=False):
643644
else:
644645
return unittest.skipUnless(has_socket_support, msg)
645646

647+
648+
@functools.cache
649+
def has_remote_subprocess_debugging():
650+
"""Check if we have permissions to debug subprocesses remotely.
651+
652+
Returns True if we have permissions, False if we don't.
653+
Checks for:
654+
- Platform support (Linux, macOS, Windows only)
655+
- On Linux: process_vm_readv support
656+
- _remote_debugging module availability
657+
- Actual subprocess debugging permissions (e.g., macOS entitlements)
658+
Result is cached.
659+
"""
660+
# Check platform support
661+
if sys.platform not in ("linux", "darwin", "win32"):
662+
return False
663+
664+
try:
665+
import _remote_debugging
666+
except ImportError:
667+
return False
668+
669+
# On Linux, check for process_vm_readv support
670+
if sys.platform == "linux":
671+
if not getattr(_remote_debugging, "PROCESS_VM_READV_SUPPORTED", False):
672+
return False
673+
674+
# First check if we can read our own process
675+
if not _remote_debugging.is_python_process(os.getpid()):
676+
return False
677+
678+
# Check subprocess access - debugging child processes may require
679+
# additional permissions depending on platform security settings
680+
import socket
681+
import subprocess
682+
683+
# Create a socket for child to signal readiness
684+
server = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
685+
server.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
686+
server.bind(("127.0.0.1", 0))
687+
server.listen(1)
688+
port = server.getsockname()[1]
689+
690+
# Child connects to signal it's ready, then waits for parent to close
691+
child_code = f"""
692+
import socket
693+
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
694+
s.connect(("127.0.0.1", {port}))
695+
s.recv(1) # Wait for parent to signal done
696+
"""
697+
proc = subprocess.Popen(
698+
[sys.executable, "-c", child_code],
699+
stdout=subprocess.DEVNULL,
700+
stderr=subprocess.DEVNULL,
701+
)
702+
try:
703+
server.settimeout(5.0)
704+
conn, _ = server.accept()
705+
# Child is ready, test if we can probe it
706+
result = _remote_debugging.is_python_process(proc.pid)
707+
# Check if subprocess is still alive after probing
708+
if proc.poll() is not None:
709+
return False
710+
conn.close() # Signal child to exit
711+
return result
712+
except (socket.timeout, OSError):
713+
return False
714+
finally:
715+
server.close()
716+
proc.kill()
717+
proc.wait()
718+
719+
720+
def requires_remote_subprocess_debugging():
721+
"""Skip tests that require remote subprocess debugging permissions.
722+
723+
This also implies subprocess support, so no need to use both
724+
@requires_subprocess() and @requires_remote_subprocess_debugging().
725+
"""
726+
if not has_subprocess_support:
727+
return unittest.skip("requires subprocess support")
728+
return unittest.skipUnless(
729+
has_remote_subprocess_debugging(),
730+
"requires remote subprocess debugging permissions"
731+
)
732+
733+
646734
# Does strftime() support glibc extension like '%4Y'?
647735
has_strftime_extensions = False
648736
if sys.platform != "win32":

0 commit comments

Comments
 (0)