From 76375ef872b23c7b0307eab2780ca0d30bf78d27 Mon Sep 17 00:00:00 2001
From: Helmut Grohne <helmut@subdivi.de>
Date: Thu, 12 Jun 2025 10:58:24 +0200
Subject: run_in_fork: allow starting the function immediately

Doing so skips creating the EventFD.
---
 linuxnamespaces/__init__.py | 62 +++++++++++++++++++++++++++++++++------------
 1 file changed, 46 insertions(+), 16 deletions(-)

(limited to 'linuxnamespaces/__init__.py')

diff --git a/linuxnamespaces/__init__.py b/linuxnamespaces/__init__.py
index 8adf45b..5b163df 100644
--- a/linuxnamespaces/__init__.py
+++ b/linuxnamespaces/__init__.py
@@ -26,27 +26,39 @@ class run_in_fork:
     """Decorator for running the decorated function once in a separate process.
     """
 
-    def __init__(self, function: typing.Callable[[], None]):
-        """Fork a new process that will eventually run the given function and
-        then exit.
+    def __init__(
+        self, function: typing.Callable[[], None], start: bool = False
+    ):
+        """Fork a new process that will run the given function and then exit.
+        If start is true, run it immediately, otherwise the start or __call__
+        method should be used.
         """
-        self.efd = EventFD()
+        self.efd = None if start else EventFD()
         self.pid = os.fork()
         if self.pid == 0:
             try:
-                self.efd.read()
-                self.efd.close()
+                if self.efd is not None:
+                    self.efd.read()
+                    self.efd.close()
+                    self.efd = None
                 function()
             except:
                 os._exit(1)
             os._exit(0)
 
+    @classmethod
+    def now(cls, function: typing.Callable[[], None]) -> typing.Self:
+        """Fork a new process that will immediately run the given function and
+        then exit."""
+        return cls(function, start=True)
+
     def start(self) -> None:
         """Start the decorated function. It can only be started once."""
         if not self.efd:
             raise ValueError("this function can only be called once")
         self.efd.write(1)
         self.efd.close()
+        self.efd = None
 
     def wait(self) -> None:
         """Wait for the process running the decorated function to finish."""
@@ -57,8 +69,11 @@ class run_in_fork:
             raise ValueError("something failed")
 
     def __call__(self) -> None:
-        """Start the decorated function and wait for its process to finish."""
-        self.start()
+        """Start the decorated function if needed and wait for its process to
+        finish.
+        """
+        if self.efd:
+            self.start()
         self.wait()
 
 
@@ -69,9 +84,12 @@ class async_run_in_fork:
     synchronous and it must not access the event loop of the main process.
     """
 
-    def __init__(self, function: typing.Callable[[], None]):
-        """Fork a new process that will eventually run the given function and
-        then exit.
+    def __init__(
+        self, function: typing.Callable[[], None], start: bool = False
+    ):
+        """Fork a new process that will run the given function and then exit.
+        If start is true, run it immediately, otherwise the start or __call__
+        method should be used.
         """
         loop = asyncio.get_running_loop()
         with asyncio.get_child_watcher() as watcher:
@@ -80,18 +98,26 @@ class async_run_in_fork:
                     "active child watcher required for creating a process"
                 )
             self.future = loop.create_future()
-            self.efd = EventFD()
+            self.efd = None if start else EventFD()
             self.pid = os.fork()
             if self.pid == 0:
                 try:
-                    self.efd.read()
-                    self.efd.close()
+                    if self.efd:
+                        self.efd.read()
+                        self.efd.close()
+                        self.efd = None
                     function()
                 except:
                     os._exit(1)
                 os._exit(0)
             watcher.add_child_handler(self.pid, self._child_callback)
 
+    @classmethod
+    def now(cls, function: typing.Callable[[], None]) -> typing.Self:
+        """Fork a new process that will immediately run the given function and
+        then exit."""
+        return cls(function, start=True)
+
     def _child_callback(self, pid: int, returncode: int) -> None:
         if self.pid != pid:
             return
@@ -103,6 +129,7 @@ class async_run_in_fork:
             raise ValueError("this function can only be called once")
         self.efd.write(1)
         self.efd.close()
+        self.efd = None
 
     async def wait(self) -> None:
         """Wait for the process running the decorated function to finish."""
@@ -113,8 +140,11 @@ class async_run_in_fork:
             raise ValueError("something failed")
 
     async def __call__(self) -> None:
-        """Start the decorated function and wait for its process to finish."""
-        self.start()
+        """Start the decorated function if needed and wait for its process to
+        finish.
+        """
+        if self.efd:
+            self.start()
         await self.wait()
 
 
-- 
cgit v1.2.3