# Copyright 2008-2013 Gentoo Foundation # Distributed under the terms of the GNU General Public License v2 import fcntl import errno import gzip import portage from portage import os, _encodings, _unicode_encode from _emerge.AbstractPollTask import AbstractPollTask class PipeLogger(AbstractPollTask): """ This can be used for logging output of a child process, optionally outputing to log_file_path and/or stdout_fd. It can also monitor for EOF on input_fd, which may be used to detect termination of a child process. If log_file_path ends with '.gz' then the log file is written with compression. """ __slots__ = ("input_fd", "log_file_path", "stdout_fd") + \ ("_log_file", "_log_file_real", "_reg_id") def _start(self): log_file_path = self.log_file_path if log_file_path is not None: self._log_file = open(_unicode_encode(log_file_path, encoding=_encodings['fs'], errors='strict'), mode='ab') if log_file_path.endswith('.gz'): self._log_file_real = self._log_file self._log_file = gzip.GzipFile(filename='', mode='ab', fileobj=self._log_file) portage.util.apply_secpass_permissions(log_file_path, uid=portage.portage_uid, gid=portage.portage_gid, mode=0o660) fcntl_flags = os.O_NONBLOCK try: fcntl.FD_CLOEXEC except AttributeError: pass else: fcntl_flags |= fcntl.FD_CLOEXEC if isinstance(self.input_fd, int): fd = self.input_fd else: fd = self.input_fd.fileno() fcntl.fcntl(fd, fcntl.F_SETFL, fcntl.fcntl(fd, fcntl.F_GETFL) | fcntl_flags) self._reg_id = self.scheduler.io_add_watch(fd, self._registered_events, self._output_handler) self._registered = True def _cancel(self): self._unregister() if self.returncode is None: self.returncode = self._cancelled_returncode def _wait(self): if self.returncode is not None: return self.returncode self._wait_loop() self.returncode = os.EX_OK return self.returncode def _output_handler(self, fd, event): background = self.background stdout_fd = self.stdout_fd log_file = self._log_file while True: buf = self._read_buf(fd, event) if buf is None: # not a POLLIN event, EAGAIN, etc... break if not buf: # EOF self._unregister() self.wait() break else: if not background and stdout_fd is not None: failures = 0 stdout_buf = buf while stdout_buf: try: stdout_buf = \ stdout_buf[os.write(stdout_fd, stdout_buf):] except OSError as e: if e.errno != errno.EAGAIN: raise del e failures += 1 if failures > 50: # Avoid a potentially infinite loop. In # most cases, the failure count is zero # and it's unlikely to exceed 1. raise # This means that a subprocess has put an inherited # stdio file descriptor (typically stdin) into # O_NONBLOCK mode. This is not acceptable (see bug # #264435), so revert it. We need to use a loop # here since there's a race condition due to # parallel processes being able to change the # flags on the inherited file descriptor. # TODO: When possible, avoid having child processes # inherit stdio file descriptors from portage # (maybe it can't be avoided with # PROPERTIES=interactive). fcntl.fcntl(stdout_fd, fcntl.F_SETFL, fcntl.fcntl(stdout_fd, fcntl.F_GETFL) ^ os.O_NONBLOCK) if log_file is not None: log_file.write(buf) log_file.flush() self._unregister_if_appropriate(event) return True def _unregister(self): if self._reg_id is not None: self.scheduler.source_remove(self._reg_id) self._reg_id = None if self.input_fd is not None: if isinstance(self.input_fd, int): os.close(self.input_fd) else: self.input_fd.close() self.input_fd = None if self.stdout_fd is not None: os.close(self.stdout_fd) self.stdout_fd = None if self._log_file is not None: self._log_file.close() self._log_file = None if self._log_file_real is not None: # Avoid "ResourceWarning: unclosed file" since python 3.2. self._log_file_real.close() self._log_file_real = None self._registered = False