From 7622da95fd97c07d7a51f14dfcfb07048db687c1 Mon Sep 17 00:00:00 2001 From: Zac Medico Date: Tue, 3 Oct 2023 07:59:17 -0700 Subject: _get_lock_fn: support multiprocessing spawn start method (bug 915119) Ensure that _get_lock_fn arguments to multiprocessing.Process will successfully pickle, as required by the spawn start method, which is the default for macOS since Python 3.8. Since file descriptors are not inherited unless the fork start method is used, the subprocess should only try to close an inherited file descriptor for the fork start method. Bug: https://bugs.gentoo.org/915119 Signed-off-by: Zac Medico --- lib/portage/locks.py | 50 ++++++++++++++++++++++++++++++++------------------ 1 file changed, 32 insertions(+), 18 deletions(-) diff --git a/lib/portage/locks.py b/lib/portage/locks.py index 1c3e13ce4..ee40451b1 100644 --- a/lib/portage/locks.py +++ b/lib/portage/locks.py @@ -79,6 +79,11 @@ class _lock_manager: del _open_inodes[self.inode_key] +def _lockf_test_lock_fn(path, fd, flags): + fcntl.lockf(fd, flags) + return functools.partial(unlockfile, (path, fd, flags, fcntl.lockf)) + + def _get_lock_fn(): """ Returns fcntl.lockf if proven to work, and otherwise returns fcntl.flock. @@ -88,10 +93,7 @@ def _get_lock_fn(): if _lock_fn is not None: return _lock_fn - if _test_lock_fn( - lambda path, fd, flags: fcntl.lockf(fd, flags) - and functools.partial(unlockfile, (path, fd, flags, fcntl.lockf)) - ): + if _test_lock_fn(_lockf_test_lock_fn): _lock_fn = fcntl.lockf return _lock_fn @@ -103,19 +105,6 @@ def _get_lock_fn(): def _test_lock_fn( lock_fn: typing.Callable[[str, int, int], typing.Callable[[], None]] ) -> bool: - def _test_lock(fd, lock_path): - os.close(fd) - try: - with open(lock_path, "a") as f: - lock_fn(lock_path, f.fileno(), fcntl.LOCK_EX | fcntl.LOCK_NB) - except (TryAgain, OSError) as e: - if isinstance(e, TryAgain) or e.errno == errno.EAGAIN: - # Parent process holds lock, as expected. - sys.exit(0) - - # Something went wrong. - sys.exit(1) - fd, lock_path = tempfile.mkstemp() unlock_fn = None try: @@ -125,7 +114,17 @@ def _test_lock_fn( pass else: _lock_manager(fd, os.fstat(fd), lock_path) - proc = multiprocessing.Process(target=_test_lock, args=(fd, lock_path)) + proc = multiprocessing.Process( + target=_subprocess_test_lock, + args=( + # Since file descriptors are not inherited unless the fork start + # method is used, the subprocess should only try to close an + # inherited file descriptor for the fork start method. + fd if multiprocessing.get_start_method() == "fork" else None, + lock_fn, + lock_path, + ), + ) proc.start() proc.join() if proc.exitcode == os.EX_OK: @@ -141,6 +140,21 @@ def _test_lock_fn( return False +def _subprocess_test_lock(fd, lock_fn, lock_path): + if fd is not None: + os.close(fd) + try: + with open(lock_path, "a") as f: + lock_fn(lock_path, f.fileno(), fcntl.LOCK_EX | fcntl.LOCK_NB) + except (TryAgain, OSError) as e: + if isinstance(e, TryAgain) or e.errno == errno.EAGAIN: + # Parent process holds lock, as expected. + sys.exit(0) + + # Something went wrong. + sys.exit(1) + + def _close_fds(): """ This is intended to be called after a fork, in order to close file -- cgit v1.2.3-65-gdbad