On Fri, 19 Jun 2020 13:39:19 -0700
Zac Medico <zmed...@gentoo.org> wrote:

> This variable specifies a command that filters build log output to a
> log file. The plan is to extend this to support a separate filter for
> tty output in the future.
> 
> In order to enable the EbuildPhase class to write elog messages to
> the build log with PORTAGE_LOG_FILTER_FILE_CMD support, convert its
> _elog method to a coroutine, and add a SchedulerInterface async_output
> method for it to use.
> 
> Use a new BuildLogger class to manage log output (with or without a
> filter command), with compression support provided by PipeLogger.
> BuildLogger has a stdin property which provides access to a writable
> binary file stream (refers to a pipe) that log content is written to.
> 
> Bug: https://bugs.gentoo.org/709746
> Signed-off-by: Zac Medico <zmed...@gentoo.org>
> ---
>  lib/_emerge/AbstractEbuildProcess.py          |   3 +-
>  lib/_emerge/BinpkgFetcher.py                  |   3 +-
>  lib/_emerge/EbuildFetcher.py                  |   3 +-
>  lib/_emerge/EbuildPhase.py                    |  47 ++++++--
>  lib/_emerge/SpawnProcess.py                   |  58 +++++++---
>  lib/portage/dbapi/_MergeProcess.py            |   3 +-
>  .../ebuild/_config/special_env_vars.py        |   8 +-
>  lib/portage/util/_async/BuildLogger.py        | 109
> ++++++++++++++++++ lib/portage/util/_async/SchedulerInterface.py |
> 32 ++++- man/make.conf.5                               |   7 +-
>  10 files changed, 243 insertions(+), 30 deletions(-)
>  create mode 100644 lib/portage/util/_async/BuildLogger.py
> 
> diff --git a/lib/_emerge/AbstractEbuildProcess.py
> b/lib/_emerge/AbstractEbuildProcess.py index 1c1955cfe..ae1aae55f
> 100644 --- a/lib/_emerge/AbstractEbuildProcess.py
> +++ b/lib/_emerge/AbstractEbuildProcess.py
> @@ -1,4 +1,4 @@
> -# Copyright 1999-2019 Gentoo Foundation
> +# Copyright 1999-2020 Gentoo Authors
>  # Distributed under the terms of the GNU General Public License v2
>  
>  import errno
> @@ -196,6 +196,7 @@ class AbstractEbuildProcess(SpawnProcess):
>                       null_fd = os.open('/dev/null', os.O_RDONLY)
>                       self.fd_pipes[0] = null_fd
>  
> +             self.log_filter_file =
> self.settings.get('PORTAGE_LOG_FILTER_FILE_CMD') try:
>                       SpawnProcess._start(self)
>               finally:
> diff --git a/lib/_emerge/BinpkgFetcher.py
> b/lib/_emerge/BinpkgFetcher.py index 36d027de3..2e5861cc1 100644
> --- a/lib/_emerge/BinpkgFetcher.py
> +++ b/lib/_emerge/BinpkgFetcher.py
> @@ -1,4 +1,4 @@
> -# Copyright 1999-2018 Gentoo Foundation
> +# Copyright 1999-2020 Gentoo Authors
>  # Distributed under the terms of the GNU General Public License v2
>  
>  import functools
> @@ -158,6 +158,7 @@ class _BinpkgFetcherProcess(SpawnProcess):
>               self.env = fetch_env
>               if settings.selinux_enabled():
>                       self._selinux_type =
> settings["PORTAGE_FETCH_T"]
> +             self.log_filter_file =
> settings.get('PORTAGE_LOG_FILTER_FILE_CMD') SpawnProcess._start(self)
>  
>       def _pipe(self, fd_pipes):
> diff --git a/lib/_emerge/EbuildFetcher.py
> b/lib/_emerge/EbuildFetcher.py index 1e40994fb..55349c33c 100644
> --- a/lib/_emerge/EbuildFetcher.py
> +++ b/lib/_emerge/EbuildFetcher.py
> @@ -1,4 +1,4 @@
> -# Copyright 1999-2018 Gentoo Foundation
> +# Copyright 1999-2020 Gentoo Authors
>  # Distributed under the terms of the GNU General Public License v2
>  
>  import copy
> @@ -225,6 +225,7 @@ class _EbuildFetcherProcess(ForkProcess):
>                       settings["NOCOLOR"] = nocolor
>  
>               self._settings = settings
> +             self.log_filter_file =
> settings.get('PORTAGE_LOG_FILTER_FILE_CMD') ForkProcess._start(self)
>  
>               # Free settings now since it's no longer needed in
> diff --git a/lib/_emerge/EbuildPhase.py b/lib/_emerge/EbuildPhase.py
> index 477e0ba97..ddb3dc719 100644
> --- a/lib/_emerge/EbuildPhase.py
> +++ b/lib/_emerge/EbuildPhase.py
> @@ -26,6 +26,8 @@ from portage.package.ebuild.prepare_build_dirs
> import (_prepare_workdir, from portage.util.futures.compat_coroutine
> import coroutine from portage.util import writemsg
>  from portage.util._async.AsyncTaskFuture import AsyncTaskFuture
> +from portage.util._async.BuildLogger import BuildLogger
> +from portage.util.futures import asyncio
>  from portage.util.futures.executor.fork import ForkExecutor
>  
>  try:
> @@ -69,6 +71,11 @@ class EbuildPhase(CompositeTask):
>       _locked_phases = ("setup", "preinst", "postinst", "prerm",
> "postrm") 
>       def _start(self):
> +             future = asyncio.ensure_future(self._async_start(),
> loop=self.scheduler)
> +             self._start_task(AsyncTaskFuture(future=future),
> self._async_start_exit) +
> +     @coroutine
> +     def _async_start(self):
>  
>               need_builddir = self.phase not in
> EbuildProcess._phases_without_builddir 
> @@ -126,7 +133,7 @@ class EbuildPhase(CompositeTask):
>                       # Force background=True for this header
> since it's intended # for the log and it doesn't necessarily need to
> be visible # elsewhere.
> -                     self._elog('einfo', msg, background=True)
> +                     yield self._elog('einfo', msg,
> background=True) 
>               if self.phase == 'package':
>                       if 'PORTAGE_BINPKG_TMPFILE' not in
> self.settings: @@ -134,6 +141,12 @@ class EbuildPhase(CompositeTask):
>                                       os.path.join(self.settings['PKGDIR'],
>                                       self.settings['CATEGORY'],
> self.settings['PF']) + '.tbz2' 
> +     def _async_start_exit(self, task):
> +             task.future.cancelled() or task.future.result()
> +             if self._default_exit(task) != os.EX_OK:
> +                     self.wait()
> +                     return
> +
>               if self.phase in ("pretend", "prerm"):
>                       env_extractor =
> BinpkgEnvExtractor(background=self.background,
> scheduler=self.scheduler, settings=self.settings) @@ -391,6 +404,7 @@
> class EbuildPhase(CompositeTask): self.returncode = 1
>               self.wait()
>  
> +     @coroutine
>       def _elog(self, elog_funcname, lines, background=None):
>               if background is None:
>                       background = self.background
> @@ -407,11 +421,30 @@ class EbuildPhase(CompositeTask):
>                       portage.output.havecolor = global_havecolor
>               msg = out.getvalue()
>               if msg:
> -                     log_path = None
> -                     if self.settings.get("PORTAGE_BACKGROUND")
> != "subprocess":
> -                             log_path =
> self.settings.get("PORTAGE_LOG_FILE")
> -                     self.scheduler.output(msg, log_path=log_path,
> -                             background=background)
> +                     build_logger = None
> +                     try:
> +                             log_file = None
> +                             log_path = None
> +                             if
> self.settings.get("PORTAGE_BACKGROUND") != "subprocess":
> +                                     log_path =
> self.settings.get("PORTAGE_LOG_FILE")
> +                             if log_path:
> +                                     build_logger =
> BuildLogger(env=self.settings.environ(),
> +                                             log_path=log_path,
> +
> log_filter_file=self.settings.get('PORTAGE_LOG_FILTER_FILE_CMD'),
> +
> scheduler=self.scheduler)
> +                                     build_logger.start()
> +                                     log_file = build_logger.stdin
> +
> +                             yield
> self.scheduler.async_output(msg, log_file=log_file,
> +                                     background=background)
> +
> +                             if build_logger is not None:
> +                                     build_logger.stdin.close()
> +                                     yield
> build_logger.async_wait()
> +                     except asyncio.CancelledError:
> +                             if build_logger is not None:
> +                                     build_logger.cancel()
> +                             raise
>  
>  
>  class _PostPhaseCommands(CompositeTask):
> @@ -480,4 +513,4 @@ class _PostPhaseCommands(CompositeTask):
>                       qa_msg.extend("\t%s: %s" % (filename, "
> ".join(sorted(soname_deps))) for filename, soname_deps in unresolved)
>                       qa_msg.append("")
> -                     self.elog("eqawarn", qa_msg)
> +                     yield self.elog("eqawarn", qa_msg)
> diff --git a/lib/_emerge/SpawnProcess.py b/lib/_emerge/SpawnProcess.py
> index 395d66bb9..f96911571 100644
> --- a/lib/_emerge/SpawnProcess.py
> +++ b/lib/_emerge/SpawnProcess.py
> @@ -1,4 +1,4 @@
> -# Copyright 2008-2018 Gentoo Foundation
> +# Copyright 2008-2020 Gentoo Authors
>  # Distributed under the terms of the GNU General Public License v2
>  
>  try:
> @@ -19,7 +19,10 @@ from portage.const import BASH_BINARY
>  from portage.localization import _
>  from portage.output import EOutput
>  from portage.util import writemsg_level
> +from portage.util._async.BuildLogger import BuildLogger
>  from portage.util._async.PipeLogger import PipeLogger
> +from portage.util.futures import asyncio
> +from portage.util.futures.compat_coroutine import coroutine
>  
>  class SpawnProcess(SubProcess):
>  
> @@ -34,8 +37,8 @@ class SpawnProcess(SubProcess):
>               "path_lookup", "pre_exec", "close_fds", "cgroup",
>               "unshare_ipc", "unshare_mount", "unshare_pid",
> "unshare_net") 
> -     __slots__ = ("args",) + \
> -             _spawn_kwarg_names + ("_pipe_logger",
> "_selinux_type",)
> +     __slots__ = ("args", "log_filter_file") + \
> +             _spawn_kwarg_names + ("_main_task", "_selinux_type",)
>  
>       # Max number of attempts to kill the processes listed in
> cgroup.procs, # given that processes may fork before they can be
> killed. @@ -137,13 +140,43 @@ class SpawnProcess(SubProcess):
>                                               fcntl.fcntl(stdout_fd,
>                                               fcntl.F_GETFD) |
> fcntl.FD_CLOEXEC) 
> -             self._pipe_logger =
> PipeLogger(background=self.background,
> +             build_logger = BuildLogger(env=self.env,
> +                     log_path=log_file_path,
> +                     log_filter_file=self.log_filter_file,
> +                     scheduler=self.scheduler)
> +             build_logger.start()
> +
> +             pipe_logger = PipeLogger(background=self.background,
>                       scheduler=self.scheduler, input_fd=master_fd,
> -                     log_file_path=log_file_path,
> +                     log_file_path=build_logger.stdin,
>                       stdout_fd=stdout_fd)
> -
> self._pipe_logger.addExitListener(self._pipe_logger_exit)
> -             self._pipe_logger.start()
> +
> +             pipe_logger.start()
> +
>               self._registered = True
> +             self._main_task =
> asyncio.ensure_future(self._main(build_logger, pipe_logger),
> loop=self.scheduler)
> +             self._main_task.add_done_callback(self._main_exit)
> +
> +     @coroutine
> +     def _main(self, build_logger, pipe_logger):
> +             try:
> +                     if pipe_logger.poll() is None:
> +                             yield pipe_logger.async_wait()
> +                     if build_logger.poll() is None:
> +                             yield build_logger.async_wait()
> +             except asyncio.CancelledError:
> +                     if pipe_logger.poll() is None:
> +                             pipe_logger.cancel()
> +                     if build_logger.poll() is None:
> +                             build_logger.cancel()
> +                     raise
> +
> +     def _main_exit(self, main_task):
> +             try:
> +                     main_task.result()
> +             except asyncio.CancelledError:
> +                     self.cancel()
> +             self._async_waitpid()
>  
>       def _can_log(self, slave_fd):
>               return True
> @@ -167,20 +200,17 @@ class SpawnProcess(SubProcess):
>  
>               return spawn_func(args, **kwargs)
>  
> -     def _pipe_logger_exit(self, pipe_logger):
> -             self._pipe_logger = None
> -             self._async_waitpid()
> -
>       def _unregister(self):
>               SubProcess._unregister(self)
>               if self.cgroup is not None:
>                       self._cgroup_cleanup()
>                       self.cgroup = None
> -             if self._pipe_logger is not None:
> -                     self._pipe_logger.cancel()
> -                     self._pipe_logger = None
> +             if self._main_task is not None:
> +                     self._main_task.done() or
> self._main_task.cancel() 
>       def _cancel(self):
> +             if self._main_task is not None:
> +                     self._main_task.done() or
> self._main_task.cancel() SubProcess._cancel(self)
>               self._cgroup_cleanup()
>  
> diff --git a/lib/portage/dbapi/_MergeProcess.py
> b/lib/portage/dbapi/_MergeProcess.py index 371550079..236d1a255 100644
> --- a/lib/portage/dbapi/_MergeProcess.py
> +++ b/lib/portage/dbapi/_MergeProcess.py
> @@ -1,4 +1,4 @@
> -# Copyright 2010-2018 Gentoo Foundation
> +# Copyright 2010-2020 Gentoo Authors
>  # Distributed under the terms of the GNU General Public License v2
>  
>  import io
> @@ -57,6 +57,7 @@ class MergeProcess(ForkProcess):
>                       self.fd_pipes = self.fd_pipes.copy()
>               self.fd_pipes.setdefault(0,
> portage._get_stdin().fileno()) 
> +             self.log_filter_file =
> self.settings.get('PORTAGE_LOG_FILTER_FILE_CMD') super(MergeProcess,
> self)._start() 
>       def _lock_vdb(self):
> diff --git a/lib/portage/package/ebuild/_config/special_env_vars.py
> b/lib/portage/package/ebuild/_config/special_env_vars.py index
> 440dd00b2..f44cb9b1b 100644 ---
> a/lib/portage/package/ebuild/_config/special_env_vars.py +++
> b/lib/portage/package/ebuild/_config/special_env_vars.py @@ -1,4 +1,4
> @@ -# Copyright 2010-2019 Gentoo Authors
> +# Copyright 2010-2020 Gentoo Authors
>  # Distributed under the terms of the GNU General Public License v2
>  
>  from __future__ import unicode_literals
> @@ -175,7 +175,7 @@ environ_filter += [
>       "PORTAGE_RO_DISTDIRS",
>       "PORTAGE_RSYNC_EXTRA_OPTS", "PORTAGE_RSYNC_OPTS",
>       "PORTAGE_RSYNC_RETRIES", "PORTAGE_SSH_OPTS",
> "PORTAGE_SYNC_STALE",
> -     "PORTAGE_USE",
> +     "PORTAGE_USE", "PORTAGE_LOG_FILTER_FILE_CMD",
>       "PORTAGE_LOGDIR", "PORTAGE_LOGDIR_CLEAN",
>       "QUICKPKG_DEFAULT_OPTS", "REPOMAN_DEFAULT_OPTS",
>       "RESUMECOMMAND", "RESUMECOMMAND_FTP",
> @@ -204,7 +204,9 @@ default_globals = {
>       'PORTAGE_BZIP2_COMMAND':    'bzip2',
>  }
>  
> -validate_commands = ('PORTAGE_BZIP2_COMMAND',
> 'PORTAGE_BUNZIP2_COMMAND',) +validate_commands =
> ('PORTAGE_BZIP2_COMMAND', 'PORTAGE_BUNZIP2_COMMAND',
> +     'PORTAGE_LOG_FILTER_FILE_CMD',
> +)
>  
>  # To enhance usability, make some vars case insensitive
>  # by forcing them to lower case.
> diff --git a/lib/portage/util/_async/BuildLogger.py
> b/lib/portage/util/_async/BuildLogger.py new file mode 100644
> index 000000000..f5fea77ea
> --- /dev/null
> +++ b/lib/portage/util/_async/BuildLogger.py
> @@ -0,0 +1,109 @@
> +# Copyright 2020 Gentoo Authors
> +# Distributed under the terms of the GNU General Public License v2
> +
> +import subprocess
> +
> +from portage import os
> +from portage.util import shlex_split
> +from _emerge.AsynchronousTask import AsynchronousTask
> +from portage.util._async.PipeLogger import PipeLogger
> +from portage.util._async.PopenProcess import PopenProcess
> +from portage.util.futures import asyncio
> +from portage.util.futures.compat_coroutine import coroutine
> +
> +
> +class BuildLogger(AsynchronousTask):
> +     """
> +     Write to a log file, with compression support provided by
> PipeLogger.
> +     If the log_filter_file parameter is specified, then it is
> interpreted
> +     as a command to execute which filters log output (see the
> +     PORTAGE_LOG_FILTER_FILE_CMD variable in make.conf(5)). The
> stdin property
> +     provides access to a writable binary file stream (refers to
> a pipe)
> +     that log content should be written to (usually redirected
> from
> +     subprocess stdout and stderr streams).
> +     """
> +
> +     __slots__ = ('env', 'log_path', 'log_filter_file',
> '_main_task', '_stdin') +
> +     @property
> +     def stdin(self):
> +             return self._stdin
> +
> +     def _start(self):
> +             filter_proc = None
> +             log_input = None
> +             if self.log_path is not None:
> +                     log_filter_file = self.log_filter_file
> +                     if log_filter_file is not None:
> +                             split_value =
> shlex_split(log_filter_file)
> +                             log_filter_file = split_value if
> split_value else None
> +                     if log_filter_file:
> +                             filter_input, stdin = os.pipe()
> +                             log_input, filter_output = os.pipe()
> +                             try:
> +                                     filter_proc = PopenProcess(
> +
> proc=subprocess.Popen(
> +
> log_filter_file,
> +                                                     env=self.env,
> +
> stdin=filter_input,
> +
> stdout=filter_output,
> +
> stderr=filter_output,
> +                                             ),
> +
> scheduler=self.scheduler,
> +                                     )
> +                                     filter_proc.start()
> +                             except EnvironmentError:
> +                                     # Maybe the command is
> missing or broken somehow...
> +                                     os.close(filter_input)
> +                                     os.close(stdin)
> +                                     os.close(log_input)
> +                                     os.close(filter_output)
> +                             else:
> +                                     self._stdin =
> os.fdopen(stdin, 'wb', 0)
> +                                     os.close(filter_input)
> +                                     os.close(filter_output)
> +
> +             if self._stdin is None:
> +                     # Since log_filter_file is unspecified or
> refers to a file
> +                     # that is missing or broken somehow, create
> a pipe that
> +                     # logs directly to pipe_logger.
> +                     log_input, stdin = os.pipe()
> +                     self._stdin = os.fdopen(stdin, 'wb', 0)
> +
> +             # Set background=True so that pipe_logger does not
> log to stdout.
> +             pipe_logger = PipeLogger(background=True,
> +                     scheduler=self.scheduler, input_fd=log_input,
> +                     log_file_path=self.log_path)
> +             pipe_logger.start()
> +
> +             self._main_task =
> asyncio.ensure_future(self._main(filter_proc, pipe_logger),
> loop=self.scheduler)
> +             self._main_task.add_done_callback(self._main_exit)
> +
> +     @coroutine
> +     def _main(self, filter_proc, pipe_logger):
> +             try:
> +                     if pipe_logger.poll() is None:
> +                             yield pipe_logger.async_wait()
> +                     if filter_proc is not None and
> filter_proc.poll() is None:
> +                             yield filter_proc.async_wait()
> +             except asyncio.CancelledError:
> +                     if pipe_logger.poll() is None:
> +                             pipe_logger.cancel()
> +                     if filter_proc is not None and
> filter_proc.poll() is None:
> +                             filter_proc.cancel()
> +                     raise
> +
> +     def _cancel(self):
> +             if self._main_task is not None:
> +                     self._main_task.done() or
> self._main_task.cancel()
> +             if self._stdin is not None and not
> self._stdin.closed:
> +                     self._stdin.close()
> +
> +     def _main_exit(self, main_task):
> +             try:
> +                     main_task.result()
> +             except asyncio.CancelledError:
> +                     self.cancel()
> +                     self._was_cancelled()
> +             self.returncode = self.returncode or 0
> +             self._async_wait()
> diff --git a/lib/portage/util/_async/SchedulerInterface.py
> b/lib/portage/util/_async/SchedulerInterface.py index
> ec6417da1..3ff250d1d 100644 ---
> a/lib/portage/util/_async/SchedulerInterface.py +++
> b/lib/portage/util/_async/SchedulerInterface.py @@ -1,4 +1,4 @@
> -# Copyright 2012-2018 Gentoo Foundation
> +# Copyright 2012-2020 Gentoo Authors
>  # Distributed under the terms of the GNU General Public License v2
>  
>  import gzip
> @@ -7,6 +7,8 @@ import errno
>  from portage import _encodings
>  from portage import _unicode_encode
>  from portage.util import writemsg_level
> +from portage.util.futures._asyncio.streams import _writer
> +from portage.util.futures.compat_coroutine import coroutine
>  from ..SlotObject import SlotObject
>  
>  class SchedulerInterface(SlotObject):
> @@ -53,6 +55,34 @@ class SchedulerInterface(SlotObject):
>       def _return_false():
>               return False
>  
> +     @coroutine
> +     def async_output(self, msg, log_file=None, background=None,
> +             level=0, noiselevel=-1):
> +             """
> +             Output a msg to stdio (if not in background) and to
> a log file
> +             if provided.
> +
> +             @param msg: a message string, including newline if
> appropriate
> +             @type msg: str
> +             @param log_file: log file in binary mode
> +             @type log_file: file
> +             @param background: send messages only to log (not to
> stdio)
> +             @type background: bool
> +             @param level: a numeric logging level (see the
> logging module)
> +             @type level: int
> +             @param noiselevel: passed directly to writemsg
> +             @type noiselevel: int
> +             """
> +             global_background = self._is_background()
> +             if background is None or global_background:
> +                     background = global_background
> +
> +             if not background:
> +                     writemsg_level(msg, level=level,
> noiselevel=noiselevel) +
> +             if log_file is not None:
> +                     yield _writer(log_file, _unicode_encode(msg))
> +
>       def output(self, msg, log_path=None, background=None,
>               level=0, noiselevel=-1):
>               """
> diff --git a/man/make.conf.5 b/man/make.conf.5
> index a3bd662ae..eb812150f 100644
> --- a/man/make.conf.5
> +++ b/man/make.conf.5
> @@ -1,4 +1,4 @@
> -.TH "MAKE.CONF" "5" "May 2020" "Portage VERSION" "Portage"
> +.TH "MAKE.CONF" "5" "Jun 2020" "Portage VERSION" "Portage"
>  .SH "NAME"
>  make.conf \- custom settings for Portage
>  .SH "SYNOPSIS"
> @@ -979,6 +979,11 @@ with an integer pid. For example, a value of
> "ionice \-c 3 \-p \\${PID}" will set idle io priority. For more
> information about ionice, see \fBionice\fR(1). This variable is unset
> by default. .TP
> +.B PORTAGE_LOG_FILTER_FILE_CMD
> +This variable specifies a command that filters build log output to a
> +log file. In order to filter ANSI escape codes from build logs,
> +\fBansifilter\fR(1) is a convenient setting for this variable.
> +.TP
>  .B PORTAGE_LOGDIR
>  This variable defines the directory in which per\-ebuild logs are
> kept. Logs are created only when this is set. They are stored as

That's a lot of code...but I couldn't spot anything wrong, so looks good

Reply via email to