* [gentoo-commits] proj/portage:master commit in: lib/portage/, lib/portage/tests/process/
@ 2020-08-09 3:02 Zac Medico
0 siblings, 0 replies; 2+ messages in thread
From: Zac Medico @ 2020-08-09 3:02 UTC (permalink / raw
To: gentoo-commits
commit: 6b08846736ebf8a224f8aad2a5b17baf66fec1e0
Author: Zac Medico <zmedico <AT> gentoo <DOT> org>
AuthorDate: Sat Aug 8 02:19:31 2020 +0000
Commit: Zac Medico <zmedico <AT> gentoo <DOT> org>
CommitDate: Sun Aug 9 00:48:12 2020 +0000
URL: https://gitweb.gentoo.org/proj/portage.git/commit/?id=6b088467
Add cached portage.getpid() function
Since getpid is a syscall, cache results, and update them
via an after fork hook.
Signed-off-by: Zac Medico <zmedico <AT> gentoo.org>
lib/portage/__init__.py | 16 ++++++++++++++++
lib/portage/tests/process/test_AsyncFunction.py | 24 ++++++++++++++++++++++++
2 files changed, 40 insertions(+)
diff --git a/lib/portage/__init__.py b/lib/portage/__init__.py
index 916c93510..4d4b590a8 100644
--- a/lib/portage/__init__.py
+++ b/lib/portage/__init__.py
@@ -14,6 +14,7 @@ try:
if not hasattr(errno, 'ESTALE'):
# ESTALE may not be defined on some systems, such as interix.
errno.ESTALE = -1
+ import multiprocessing.util
import re
import types
import platform
@@ -368,6 +369,21 @@ _internal_caller = False
_sync_mode = False
+class _ForkWatcher:
+ @staticmethod
+ def hook(_ForkWatcher):
+ _ForkWatcher.current_pid = _os.getpid()
+
+_ForkWatcher.hook(_ForkWatcher)
+
+multiprocessing.util.register_after_fork(_ForkWatcher, _ForkWatcher.hook)
+
+def getpid():
+ """
+ Cached version of os.getpid(). ForkProcess updates the cache.
+ """
+ return _ForkWatcher.current_pid
+
def _get_stdin():
"""
Buggy code in python's multiprocessing/process.py closes sys.stdin
diff --git a/lib/portage/tests/process/test_AsyncFunction.py b/lib/portage/tests/process/test_AsyncFunction.py
index 55857026d..3b360e02f 100644
--- a/lib/portage/tests/process/test_AsyncFunction.py
+++ b/lib/portage/tests/process/test_AsyncFunction.py
@@ -3,6 +3,7 @@
import sys
+import portage
from portage import os
from portage.tests import TestCase
from portage.util._async.AsyncFunction import AsyncFunction
@@ -36,3 +37,26 @@ class AsyncFunctionTestCase(TestCase):
def testAsyncFunctionStdin(self):
loop = asyncio._wrap_loop()
loop.run_until_complete(self._testAsyncFunctionStdin(loop))
+
+ def _test_getpid_fork(self):
+ """
+ Verify that portage.getpid() cache is updated in a forked child process.
+ """
+ loop = asyncio._wrap_loop()
+ proc = AsyncFunction(scheduler=loop, target=portage.getpid)
+ proc.start()
+ proc.wait()
+ self.assertEqual(proc.pid, proc.result)
+
+ def test_getpid_fork(self):
+ self._test_getpid_fork()
+
+ def test_getpid_double_fork(self):
+ """
+ Verify that portage.getpid() cache is updated correctly after
+ two forks.
+ """
+ loop = asyncio._wrap_loop()
+ proc = AsyncFunction(scheduler=loop, target=self._test_getpid_fork)
+ proc.start()
+ self.assertEqual(proc.wait(), 0)
^ permalink raw reply related [flat|nested] 2+ messages in thread
* [gentoo-commits] proj/portage:master commit in: lib/portage/, lib/portage/tests/process/
@ 2024-02-02 0:32 Zac Medico
0 siblings, 0 replies; 2+ messages in thread
From: Zac Medico @ 2024-02-02 0:32 UTC (permalink / raw
To: gentoo-commits
commit: c556fee09b6b103a9bb8c2afa1c7a7ef25022598
Author: Zac Medico <zmedico <AT> gentoo <DOT> org>
AuthorDate: Thu Feb 1 05:48:54 2024 +0000
Commit: Zac Medico <zmedico <AT> gentoo <DOT> org>
CommitDate: Thu Feb 1 16:10:28 2024 +0000
URL: https://gitweb.gentoo.org/proj/portage.git/commit/?id=c556fee0
process.spawn: Add returnproc parameter
In order to migrate away from unsafe os.fork() usage in threaded
processes (https://github.com/python/cpython/issues/84559), add a
returnproc parameter that is similar to returnpid, which causes
spawn to return a single Process object instead of a list of pids.
The Process API is a subset of asyncio.subprocess.Process. The
returnproc parameter conflicts with the logfile parameter, since
the caller is expected to use the fd_pipes parameter to implement
logging (this was also true for the returnpid parameter).
In the future, spawn will return objects of a different type but
with a compatible interface to Process, in order to encapsulate
implementation-dependent objects like multiprocessing.Process which
are designed to manage the process lifecycle and need to persist
until it exits.
Trigger a UserWarning when the returnpid parameter is used, in
order to encourage migration to returnproc (do not use
DeprecationWarning since it is hidden by default). This warning
will be temporarily suppressed for portage internals, until they
finish migrating to returnproc. There are probably very few if
any external consumers of spawn with the returnpid parameter,
so it seems safe to move quickly with this deprecation.
Bug: https://bugs.gentoo.org/916566
Signed-off-by: Zac Medico <zmedico <AT> gentoo.org>
lib/portage/process.py | 84 ++++++++++++++++++++++
lib/portage/tests/process/meson.build | 1 +
lib/portage/tests/process/test_spawn_returnproc.py | 39 ++++++++++
3 files changed, 124 insertions(+)
diff --git a/lib/portage/process.py b/lib/portage/process.py
index 71750a715f..6ec52efc4a 100644
--- a/lib/portage/process.py
+++ b/lib/portage/process.py
@@ -15,6 +15,7 @@ import subprocess
import sys
import traceback
import os as _os
+import warnings
from dataclasses import dataclass
from functools import lru_cache
@@ -27,6 +28,7 @@ import portage
portage.proxy.lazyimport.lazyimport(
globals(),
+ "portage.util._eventloop.global_event_loop:global_event_loop",
"portage.util:dump_traceback,writemsg,writemsg_level",
)
@@ -277,12 +279,78 @@ def calc_env_stats(env) -> EnvStats:
env_too_large_warnings = 0
+class Process:
+ """
+ An object that wraps OS processes created by spawn.
+ In the future, spawn will return objects of a different type
+ but with a compatible interface to this class, in order
+ to encapsulate implementation-dependent objects like
+ multiprocessing.Process which are designed to manage
+ the process lifecycle and need to persist until it exits.
+ """
+
+ def __init__(self, pid):
+ self.pid = pid
+ self.returncode = None
+ self._exit_waiters = []
+
+ def __repr__(self):
+ return f"<{self.__class__.__name__} {self.pid}>"
+
+ async def wait(self):
+ """
+ Wait for the child process to terminate.
+
+ Set and return the returncode attribute.
+ """
+ if self.returncode is not None:
+ return self.returncode
+
+ loop = global_event_loop()
+ if not self._exit_waiters:
+ loop._asyncio_child_watcher.add_child_handler(self.pid, self._child_handler)
+ waiter = loop.create_future()
+ self._exit_waiters.append(waiter)
+ return await waiter
+
+ def _child_handler(self, pid, returncode):
+ if pid != self.pid:
+ raise AssertionError(f"expected pid {self.pid}, got {pid}")
+ self.returncode = returncode
+
+ for waiter in self._exit_waiters:
+ if not waiter.cancelled():
+ waiter.set_result(returncode)
+ self._exit_waiters = None
+
+ def send_signal(self, sig):
+ """Send a signal to the process."""
+ if self.returncode is not None:
+ # Skip signalling a process that we know has already died.
+ return
+
+ try:
+ os.kill(self.pid, sig)
+ except ProcessLookupError:
+ # Suppress the race condition error; bpo-40550.
+ pass
+
+ def terminate(self):
+ """Terminate the process with SIGTERM"""
+ self.send_signal(signal.SIGTERM)
+
+ def kill(self):
+ """Kill the process with SIGKILL"""
+ self.send_signal(signal.SIGKILL)
+
+
def spawn(
mycommand,
env=None,
opt_name=None,
fd_pipes=None,
returnpid=False,
+ returnproc=False,
uid=None,
gid=None,
groups=None,
@@ -315,6 +383,9 @@ def spawn(
@param returnpid: Return the Process IDs for a successful spawn.
NOTE: This requires the caller clean up all the PIDs, otherwise spawn will clean them.
@type returnpid: Boolean
+ @param returnproc: Return a Process object for a successful spawn (conflicts with logfile parameter).
+ NOTE: This requires the caller to asynchronously wait for the Process.
+ @type returnproc: Boolean
@param uid: User ID to spawn as; useful for dropping privilages
@type uid: Integer
@param gid: Group ID to spawn as; useful for dropping privilages
@@ -350,6 +421,11 @@ def spawn(
"""
+ if logfile and returnproc:
+ raise ValueError(
+ "logfile parameter conflicts with returnproc (use fd_pipes instead)"
+ )
+
# mycommand is either a str or a list
if isinstance(mycommand, str):
mycommand = mycommand.split()
@@ -491,7 +567,15 @@ def spawn(
# If the caller wants to handle cleaning up the processes, we tell
# it about all processes that were created.
if returnpid:
+ if not portage._internal_caller:
+ warnings.warn(
+ "The portage.process.spawn returnpid paramenter is deprecated and replaced by returnproc",
+ UserWarning,
+ stacklevel=1,
+ )
return mypids
+ if returnproc:
+ return Process(mypids[0])
# Otherwise we clean them up.
while mypids:
diff --git a/lib/portage/tests/process/meson.build b/lib/portage/tests/process/meson.build
index b86fa10fb1..e2b3c11d3b 100644
--- a/lib/portage/tests/process/meson.build
+++ b/lib/portage/tests/process/meson.build
@@ -8,6 +8,7 @@ py.install_sources(
'test_pickle.py',
'test_poll.py',
'test_spawn_fail_e2big.py',
+ 'test_spawn_returnproc.py',
'test_spawn_warn_large_env.py',
'test_unshare_net.py',
'__init__.py',
diff --git a/lib/portage/tests/process/test_spawn_returnproc.py b/lib/portage/tests/process/test_spawn_returnproc.py
new file mode 100644
index 0000000000..6d823d9c3d
--- /dev/null
+++ b/lib/portage/tests/process/test_spawn_returnproc.py
@@ -0,0 +1,39 @@
+# Copyright 2024 Gentoo Authors
+# Distributed under the terms of the GNU General Public License v2
+
+import os
+import signal
+
+from portage.process import find_binary, spawn
+from portage.tests import TestCase
+from portage.util._eventloop.global_event_loop import global_event_loop
+
+
+class SpawnReturnProcTestCase(TestCase):
+ def testSpawnReturnProcWait(self):
+ true_binary = find_binary("true")
+ self.assertNotEqual(true_binary, None)
+
+ loop = global_event_loop()
+
+ async def watch_pid():
+ proc = spawn([true_binary], returnproc=True)
+ self.assertEqual(await proc.wait(), os.EX_OK)
+
+ # A second wait should also work.
+ self.assertEqual(await proc.wait(), os.EX_OK)
+
+ loop.run_until_complete(watch_pid())
+
+ def testSpawnReturnProcTerminate(self):
+ sleep_binary = find_binary("sleep")
+ self.assertNotEqual(sleep_binary, None)
+
+ loop = global_event_loop()
+
+ async def watch_pid():
+ proc = spawn([sleep_binary, 9999], returnproc=True)
+ proc.terminate()
+ self.assertEqual(await proc.wait(), -signal.SIGTERM)
+
+ loop.run_until_complete(watch_pid())
^ permalink raw reply related [flat|nested] 2+ messages in thread
end of thread, other threads:[~2024-02-02 0:32 UTC | newest]
Thread overview: 2+ messages (download: mbox.gz follow: Atom feed
-- links below jump to the message on this page --
2024-02-02 0:32 [gentoo-commits] proj/portage:master commit in: lib/portage/, lib/portage/tests/process/ Zac Medico
-- strict thread matches above, loose matches on Subject: below --
2020-08-09 3:02 Zac Medico
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox