From 649b445458e1d9dfe6f2d591f9ab19fdb56854ea Mon Sep 17 00:00:00 2001 From: Oliver Layer Date: Thu, 12 Jan 2023 16:30:45 +0100 Subject: [PATCH] fix: Leaking file descriptors --- homcc/common/arguments.py | 74 +++++++++++++++++++++++---------------- 1 file changed, 43 insertions(+), 31 deletions(-) diff --git a/homcc/common/arguments.py b/homcc/common/arguments.py index d3c3bbf..fb617a1 100644 --- a/homcc/common/arguments.py +++ b/homcc/common/arguments.py @@ -564,6 +564,45 @@ def _execute_args_sync( return ArgumentsExecutionResult.from_process_result(result) + @staticmethod + def _wait_for_async_termination( + poller: select.poll, + process: subprocess.Popen[bytes], + process_fd: int, + event_socket_fd: int, + timeout: Optional[float], + ): + start_time = time.time() + + while True: + now_time = time.time() + + if timeout is not None and now_time - start_time >= timeout: + raise TimeoutError(f"Compiler timed out. (Timeout: {timeout}s).") + + events = poller.poll(1) + for fd, event in events: + if fd == event_socket_fd: + logger.info("Terminating compilation process as socket got closed by remote. (event: %i)", event) + + process.terminate() + # we need to wait for the process to terminate, so that the handle is correctly closed + process.wait() + + raise ClientDisconnectedError + elif fd == process_fd: + logger.debug("Process has finished (process_fd has event): %i", event) + + stdout_bytes, stderr_bytes = process.communicate() + stdout = stdout_bytes.decode(ENCODING) + stderr = stderr_bytes.decode(ENCODING) + + return ArgumentsExecutionResult(process.returncode, stdout, stderr) + else: + logger.warning( + "Got poll() event for fd '%i', which does neither match the socket nor the process.", fd + ) + @staticmethod def _execute_async( args: List[str], @@ -571,7 +610,6 @@ def _execute_async( cwd: Path, timeout: Optional[float], ) -> ArgumentsExecutionResult: - start_time = time.time() with subprocess.Popen(args, cwd=cwd, stdout=subprocess.PIPE, stderr=subprocess.PIPE) as process: poller = select.poll() # socket is readable when TCP FIN is sended, so also check if we can read (POLLIN). @@ -582,36 +620,10 @@ def _execute_async( process_fd = os.pidfd_open(process.pid) poller.register(process_fd, select.POLLRDHUP | select.POLLIN) - while True: - now_time = time.time() - - if timeout is not None and now_time - start_time >= timeout: - raise TimeoutError(f"Compiler timed out. (Timeout: {timeout}s).") - - events = poller.poll(1) - for fd, event in events: - if fd == event_socket_fd: - logger.info( - "Terminating compilation process as socket got closed by remote. (event: %i)", event - ) - - process.terminate() - # we need to wait for the process to terminate, so that the handle is correctly closed - process.wait() - - raise ClientDisconnectedError - elif fd == process_fd: - logger.debug("Process has finished (process_fd has event): %i", event) - - stdout_bytes, stderr_bytes = process.communicate() - stdout = stdout_bytes.decode(ENCODING) - stderr = stderr_bytes.decode(ENCODING) - - return ArgumentsExecutionResult(process.returncode, stdout, stderr) - else: - logger.warning( - "Got poll() event for fd '%i', which does neither match the socket nor the process.", fd - ) + try: + return Arguments._wait_for_async_termination(poller, process, process_fd, event_socket_fd, timeout) + finally: + os.close(process_fd) def execute(self, **kwargs) -> ArgumentsExecutionResult: """