Gentoo Archives: gentoo-portage-dev

From: Zac Medico <zmedico@g.o>
To: gentoo-portage-dev@l.g.o
Cc: Zac Medico <zmedico@g.o>
Subject: [gentoo-portage-dev] [PATCH 2/2] Support PORTAGE_LOG_FILTER_FILE_CMD (bug 709746)
Date: Fri, 19 Jun 2020 20:39:53
Message-Id: 20200619203919.22519-3-zmedico@gentoo.org
In Reply to: [gentoo-portage-dev] [PATCH 0/2] Support PORTAGE_LOG_FILTER_FILE_CMD (bug 709746) by Zac Medico
1 This variable specifies a command that filters build log output to a
2 log file. The plan is to extend this to support a separate filter for
3 tty output in the future.
4
5 In order to enable the EbuildPhase class to write elog messages to
6 the build log with PORTAGE_LOG_FILTER_FILE_CMD support, convert its
7 _elog method to a coroutine, and add a SchedulerInterface async_output
8 method for it to use.
9
10 Use a new BuildLogger class to manage log output (with or without a
11 filter command), with compression support provided by PipeLogger.
12 BuildLogger has a stdin property which provides access to a writable
13 binary file stream (refers to a pipe) that log content is written to.
14
15 Bug: https://bugs.gentoo.org/709746
16 Signed-off-by: Zac Medico <zmedico@g.o>
17 ---
18 lib/_emerge/AbstractEbuildProcess.py | 3 +-
19 lib/_emerge/BinpkgFetcher.py | 3 +-
20 lib/_emerge/EbuildFetcher.py | 3 +-
21 lib/_emerge/EbuildPhase.py | 47 ++++++--
22 lib/_emerge/SpawnProcess.py | 58 +++++++---
23 lib/portage/dbapi/_MergeProcess.py | 3 +-
24 .../ebuild/_config/special_env_vars.py | 8 +-
25 lib/portage/util/_async/BuildLogger.py | 109 ++++++++++++++++++
26 lib/portage/util/_async/SchedulerInterface.py | 32 ++++-
27 man/make.conf.5 | 7 +-
28 10 files changed, 243 insertions(+), 30 deletions(-)
29 create mode 100644 lib/portage/util/_async/BuildLogger.py
30
31 diff --git a/lib/_emerge/AbstractEbuildProcess.py b/lib/_emerge/AbstractEbuildProcess.py
32 index 1c1955cfe..ae1aae55f 100644
33 --- a/lib/_emerge/AbstractEbuildProcess.py
34 +++ b/lib/_emerge/AbstractEbuildProcess.py
35 @@ -1,4 +1,4 @@
36 -# Copyright 1999-2019 Gentoo Foundation
37 +# Copyright 1999-2020 Gentoo Authors
38 # Distributed under the terms of the GNU General Public License v2
39
40 import errno
41 @@ -196,6 +196,7 @@ class AbstractEbuildProcess(SpawnProcess):
42 null_fd = os.open('/dev/null', os.O_RDONLY)
43 self.fd_pipes[0] = null_fd
44
45 + self.log_filter_file = self.settings.get('PORTAGE_LOG_FILTER_FILE_CMD')
46 try:
47 SpawnProcess._start(self)
48 finally:
49 diff --git a/lib/_emerge/BinpkgFetcher.py b/lib/_emerge/BinpkgFetcher.py
50 index 36d027de3..2e5861cc1 100644
51 --- a/lib/_emerge/BinpkgFetcher.py
52 +++ b/lib/_emerge/BinpkgFetcher.py
53 @@ -1,4 +1,4 @@
54 -# Copyright 1999-2018 Gentoo Foundation
55 +# Copyright 1999-2020 Gentoo Authors
56 # Distributed under the terms of the GNU General Public License v2
57
58 import functools
59 @@ -158,6 +158,7 @@ class _BinpkgFetcherProcess(SpawnProcess):
60 self.env = fetch_env
61 if settings.selinux_enabled():
62 self._selinux_type = settings["PORTAGE_FETCH_T"]
63 + self.log_filter_file = settings.get('PORTAGE_LOG_FILTER_FILE_CMD')
64 SpawnProcess._start(self)
65
66 def _pipe(self, fd_pipes):
67 diff --git a/lib/_emerge/EbuildFetcher.py b/lib/_emerge/EbuildFetcher.py
68 index 1e40994fb..55349c33c 100644
69 --- a/lib/_emerge/EbuildFetcher.py
70 +++ b/lib/_emerge/EbuildFetcher.py
71 @@ -1,4 +1,4 @@
72 -# Copyright 1999-2018 Gentoo Foundation
73 +# Copyright 1999-2020 Gentoo Authors
74 # Distributed under the terms of the GNU General Public License v2
75
76 import copy
77 @@ -225,6 +225,7 @@ class _EbuildFetcherProcess(ForkProcess):
78 settings["NOCOLOR"] = nocolor
79
80 self._settings = settings
81 + self.log_filter_file = settings.get('PORTAGE_LOG_FILTER_FILE_CMD')
82 ForkProcess._start(self)
83
84 # Free settings now since it's no longer needed in
85 diff --git a/lib/_emerge/EbuildPhase.py b/lib/_emerge/EbuildPhase.py
86 index 477e0ba97..ddb3dc719 100644
87 --- a/lib/_emerge/EbuildPhase.py
88 +++ b/lib/_emerge/EbuildPhase.py
89 @@ -26,6 +26,8 @@ from portage.package.ebuild.prepare_build_dirs import (_prepare_workdir,
90 from portage.util.futures.compat_coroutine import coroutine
91 from portage.util import writemsg
92 from portage.util._async.AsyncTaskFuture import AsyncTaskFuture
93 +from portage.util._async.BuildLogger import BuildLogger
94 +from portage.util.futures import asyncio
95 from portage.util.futures.executor.fork import ForkExecutor
96
97 try:
98 @@ -69,6 +71,11 @@ class EbuildPhase(CompositeTask):
99 _locked_phases = ("setup", "preinst", "postinst", "prerm", "postrm")
100
101 def _start(self):
102 + future = asyncio.ensure_future(self._async_start(), loop=self.scheduler)
103 + self._start_task(AsyncTaskFuture(future=future), self._async_start_exit)
104 +
105 + @coroutine
106 + def _async_start(self):
107
108 need_builddir = self.phase not in EbuildProcess._phases_without_builddir
109
110 @@ -126,7 +133,7 @@ class EbuildPhase(CompositeTask):
111 # Force background=True for this header since it's intended
112 # for the log and it doesn't necessarily need to be visible
113 # elsewhere.
114 - self._elog('einfo', msg, background=True)
115 + yield self._elog('einfo', msg, background=True)
116
117 if self.phase == 'package':
118 if 'PORTAGE_BINPKG_TMPFILE' not in self.settings:
119 @@ -134,6 +141,12 @@ class EbuildPhase(CompositeTask):
120 os.path.join(self.settings['PKGDIR'],
121 self.settings['CATEGORY'], self.settings['PF']) + '.tbz2'
122
123 + def _async_start_exit(self, task):
124 + task.future.cancelled() or task.future.result()
125 + if self._default_exit(task) != os.EX_OK:
126 + self.wait()
127 + return
128 +
129 if self.phase in ("pretend", "prerm"):
130 env_extractor = BinpkgEnvExtractor(background=self.background,
131 scheduler=self.scheduler, settings=self.settings)
132 @@ -391,6 +404,7 @@ class EbuildPhase(CompositeTask):
133 self.returncode = 1
134 self.wait()
135
136 + @coroutine
137 def _elog(self, elog_funcname, lines, background=None):
138 if background is None:
139 background = self.background
140 @@ -407,11 +421,30 @@ class EbuildPhase(CompositeTask):
141 portage.output.havecolor = global_havecolor
142 msg = out.getvalue()
143 if msg:
144 - log_path = None
145 - if self.settings.get("PORTAGE_BACKGROUND") != "subprocess":
146 - log_path = self.settings.get("PORTAGE_LOG_FILE")
147 - self.scheduler.output(msg, log_path=log_path,
148 - background=background)
149 + build_logger = None
150 + try:
151 + log_file = None
152 + log_path = None
153 + if self.settings.get("PORTAGE_BACKGROUND") != "subprocess":
154 + log_path = self.settings.get("PORTAGE_LOG_FILE")
155 + if log_path:
156 + build_logger = BuildLogger(env=self.settings.environ(),
157 + log_path=log_path,
158 + log_filter_file=self.settings.get('PORTAGE_LOG_FILTER_FILE_CMD'),
159 + scheduler=self.scheduler)
160 + build_logger.start()
161 + log_file = build_logger.stdin
162 +
163 + yield self.scheduler.async_output(msg, log_file=log_file,
164 + background=background)
165 +
166 + if build_logger is not None:
167 + build_logger.stdin.close()
168 + yield build_logger.async_wait()
169 + except asyncio.CancelledError:
170 + if build_logger is not None:
171 + build_logger.cancel()
172 + raise
173
174
175 class _PostPhaseCommands(CompositeTask):
176 @@ -480,4 +513,4 @@ class _PostPhaseCommands(CompositeTask):
177 qa_msg.extend("\t%s: %s" % (filename, " ".join(sorted(soname_deps)))
178 for filename, soname_deps in unresolved)
179 qa_msg.append("")
180 - self.elog("eqawarn", qa_msg)
181 + yield self.elog("eqawarn", qa_msg)
182 diff --git a/lib/_emerge/SpawnProcess.py b/lib/_emerge/SpawnProcess.py
183 index 395d66bb9..f96911571 100644
184 --- a/lib/_emerge/SpawnProcess.py
185 +++ b/lib/_emerge/SpawnProcess.py
186 @@ -1,4 +1,4 @@
187 -# Copyright 2008-2018 Gentoo Foundation
188 +# Copyright 2008-2020 Gentoo Authors
189 # Distributed under the terms of the GNU General Public License v2
190
191 try:
192 @@ -19,7 +19,10 @@ from portage.const import BASH_BINARY
193 from portage.localization import _
194 from portage.output import EOutput
195 from portage.util import writemsg_level
196 +from portage.util._async.BuildLogger import BuildLogger
197 from portage.util._async.PipeLogger import PipeLogger
198 +from portage.util.futures import asyncio
199 +from portage.util.futures.compat_coroutine import coroutine
200
201 class SpawnProcess(SubProcess):
202
203 @@ -34,8 +37,8 @@ class SpawnProcess(SubProcess):
204 "path_lookup", "pre_exec", "close_fds", "cgroup",
205 "unshare_ipc", "unshare_mount", "unshare_pid", "unshare_net")
206
207 - __slots__ = ("args",) + \
208 - _spawn_kwarg_names + ("_pipe_logger", "_selinux_type",)
209 + __slots__ = ("args", "log_filter_file") + \
210 + _spawn_kwarg_names + ("_main_task", "_selinux_type",)
211
212 # Max number of attempts to kill the processes listed in cgroup.procs,
213 # given that processes may fork before they can be killed.
214 @@ -137,13 +140,43 @@ class SpawnProcess(SubProcess):
215 fcntl.fcntl(stdout_fd,
216 fcntl.F_GETFD) | fcntl.FD_CLOEXEC)
217
218 - self._pipe_logger = PipeLogger(background=self.background,
219 + build_logger = BuildLogger(env=self.env,
220 + log_path=log_file_path,
221 + log_filter_file=self.log_filter_file,
222 + scheduler=self.scheduler)
223 + build_logger.start()
224 +
225 + pipe_logger = PipeLogger(background=self.background,
226 scheduler=self.scheduler, input_fd=master_fd,
227 - log_file_path=log_file_path,
228 + log_file_path=build_logger.stdin,
229 stdout_fd=stdout_fd)
230 - self._pipe_logger.addExitListener(self._pipe_logger_exit)
231 - self._pipe_logger.start()
232 +
233 + pipe_logger.start()
234 +
235 self._registered = True
236 + self._main_task = asyncio.ensure_future(self._main(build_logger, pipe_logger), loop=self.scheduler)
237 + self._main_task.add_done_callback(self._main_exit)
238 +
239 + @coroutine
240 + def _main(self, build_logger, pipe_logger):
241 + try:
242 + if pipe_logger.poll() is None:
243 + yield pipe_logger.async_wait()
244 + if build_logger.poll() is None:
245 + yield build_logger.async_wait()
246 + except asyncio.CancelledError:
247 + if pipe_logger.poll() is None:
248 + pipe_logger.cancel()
249 + if build_logger.poll() is None:
250 + build_logger.cancel()
251 + raise
252 +
253 + def _main_exit(self, main_task):
254 + try:
255 + main_task.result()
256 + except asyncio.CancelledError:
257 + self.cancel()
258 + self._async_waitpid()
259
260 def _can_log(self, slave_fd):
261 return True
262 @@ -167,20 +200,17 @@ class SpawnProcess(SubProcess):
263
264 return spawn_func(args, **kwargs)
265
266 - def _pipe_logger_exit(self, pipe_logger):
267 - self._pipe_logger = None
268 - self._async_waitpid()
269 -
270 def _unregister(self):
271 SubProcess._unregister(self)
272 if self.cgroup is not None:
273 self._cgroup_cleanup()
274 self.cgroup = None
275 - if self._pipe_logger is not None:
276 - self._pipe_logger.cancel()
277 - self._pipe_logger = None
278 + if self._main_task is not None:
279 + self._main_task.done() or self._main_task.cancel()
280
281 def _cancel(self):
282 + if self._main_task is not None:
283 + self._main_task.done() or self._main_task.cancel()
284 SubProcess._cancel(self)
285 self._cgroup_cleanup()
286
287 diff --git a/lib/portage/dbapi/_MergeProcess.py b/lib/portage/dbapi/_MergeProcess.py
288 index 371550079..236d1a255 100644
289 --- a/lib/portage/dbapi/_MergeProcess.py
290 +++ b/lib/portage/dbapi/_MergeProcess.py
291 @@ -1,4 +1,4 @@
292 -# Copyright 2010-2018 Gentoo Foundation
293 +# Copyright 2010-2020 Gentoo Authors
294 # Distributed under the terms of the GNU General Public License v2
295
296 import io
297 @@ -57,6 +57,7 @@ class MergeProcess(ForkProcess):
298 self.fd_pipes = self.fd_pipes.copy()
299 self.fd_pipes.setdefault(0, portage._get_stdin().fileno())
300
301 + self.log_filter_file = self.settings.get('PORTAGE_LOG_FILTER_FILE_CMD')
302 super(MergeProcess, self)._start()
303
304 def _lock_vdb(self):
305 diff --git a/lib/portage/package/ebuild/_config/special_env_vars.py b/lib/portage/package/ebuild/_config/special_env_vars.py
306 index 440dd00b2..f44cb9b1b 100644
307 --- a/lib/portage/package/ebuild/_config/special_env_vars.py
308 +++ b/lib/portage/package/ebuild/_config/special_env_vars.py
309 @@ -1,4 +1,4 @@
310 -# Copyright 2010-2019 Gentoo Authors
311 +# Copyright 2010-2020 Gentoo Authors
312 # Distributed under the terms of the GNU General Public License v2
313
314 from __future__ import unicode_literals
315 @@ -175,7 +175,7 @@ environ_filter += [
316 "PORTAGE_RO_DISTDIRS",
317 "PORTAGE_RSYNC_EXTRA_OPTS", "PORTAGE_RSYNC_OPTS",
318 "PORTAGE_RSYNC_RETRIES", "PORTAGE_SSH_OPTS", "PORTAGE_SYNC_STALE",
319 - "PORTAGE_USE",
320 + "PORTAGE_USE", "PORTAGE_LOG_FILTER_FILE_CMD",
321 "PORTAGE_LOGDIR", "PORTAGE_LOGDIR_CLEAN",
322 "QUICKPKG_DEFAULT_OPTS", "REPOMAN_DEFAULT_OPTS",
323 "RESUMECOMMAND", "RESUMECOMMAND_FTP",
324 @@ -204,7 +204,9 @@ default_globals = {
325 'PORTAGE_BZIP2_COMMAND': 'bzip2',
326 }
327
328 -validate_commands = ('PORTAGE_BZIP2_COMMAND', 'PORTAGE_BUNZIP2_COMMAND',)
329 +validate_commands = ('PORTAGE_BZIP2_COMMAND', 'PORTAGE_BUNZIP2_COMMAND',
330 + 'PORTAGE_LOG_FILTER_FILE_CMD',
331 +)
332
333 # To enhance usability, make some vars case insensitive
334 # by forcing them to lower case.
335 diff --git a/lib/portage/util/_async/BuildLogger.py b/lib/portage/util/_async/BuildLogger.py
336 new file mode 100644
337 index 000000000..f5fea77ea
338 --- /dev/null
339 +++ b/lib/portage/util/_async/BuildLogger.py
340 @@ -0,0 +1,109 @@
341 +# Copyright 2020 Gentoo Authors
342 +# Distributed under the terms of the GNU General Public License v2
343 +
344 +import subprocess
345 +
346 +from portage import os
347 +from portage.util import shlex_split
348 +from _emerge.AsynchronousTask import AsynchronousTask
349 +from portage.util._async.PipeLogger import PipeLogger
350 +from portage.util._async.PopenProcess import PopenProcess
351 +from portage.util.futures import asyncio
352 +from portage.util.futures.compat_coroutine import coroutine
353 +
354 +
355 +class BuildLogger(AsynchronousTask):
356 + """
357 + Write to a log file, with compression support provided by PipeLogger.
358 + If the log_filter_file parameter is specified, then it is interpreted
359 + as a command to execute which filters log output (see the
360 + PORTAGE_LOG_FILTER_FILE_CMD variable in make.conf(5)). The stdin property
361 + provides access to a writable binary file stream (refers to a pipe)
362 + that log content should be written to (usually redirected from
363 + subprocess stdout and stderr streams).
364 + """
365 +
366 + __slots__ = ('env', 'log_path', 'log_filter_file', '_main_task', '_stdin')
367 +
368 + @property
369 + def stdin(self):
370 + return self._stdin
371 +
372 + def _start(self):
373 + filter_proc = None
374 + log_input = None
375 + if self.log_path is not None:
376 + log_filter_file = self.log_filter_file
377 + if log_filter_file is not None:
378 + split_value = shlex_split(log_filter_file)
379 + log_filter_file = split_value if split_value else None
380 + if log_filter_file:
381 + filter_input, stdin = os.pipe()
382 + log_input, filter_output = os.pipe()
383 + try:
384 + filter_proc = PopenProcess(
385 + proc=subprocess.Popen(
386 + log_filter_file,
387 + env=self.env,
388 + stdin=filter_input,
389 + stdout=filter_output,
390 + stderr=filter_output,
391 + ),
392 + scheduler=self.scheduler,
393 + )
394 + filter_proc.start()
395 + except EnvironmentError:
396 + # Maybe the command is missing or broken somehow...
397 + os.close(filter_input)
398 + os.close(stdin)
399 + os.close(log_input)
400 + os.close(filter_output)
401 + else:
402 + self._stdin = os.fdopen(stdin, 'wb', 0)
403 + os.close(filter_input)
404 + os.close(filter_output)
405 +
406 + if self._stdin is None:
407 + # Since log_filter_file is unspecified or refers to a file
408 + # that is missing or broken somehow, create a pipe that
409 + # logs directly to pipe_logger.
410 + log_input, stdin = os.pipe()
411 + self._stdin = os.fdopen(stdin, 'wb', 0)
412 +
413 + # Set background=True so that pipe_logger does not log to stdout.
414 + pipe_logger = PipeLogger(background=True,
415 + scheduler=self.scheduler, input_fd=log_input,
416 + log_file_path=self.log_path)
417 + pipe_logger.start()
418 +
419 + self._main_task = asyncio.ensure_future(self._main(filter_proc, pipe_logger), loop=self.scheduler)
420 + self._main_task.add_done_callback(self._main_exit)
421 +
422 + @coroutine
423 + def _main(self, filter_proc, pipe_logger):
424 + try:
425 + if pipe_logger.poll() is None:
426 + yield pipe_logger.async_wait()
427 + if filter_proc is not None and filter_proc.poll() is None:
428 + yield filter_proc.async_wait()
429 + except asyncio.CancelledError:
430 + if pipe_logger.poll() is None:
431 + pipe_logger.cancel()
432 + if filter_proc is not None and filter_proc.poll() is None:
433 + filter_proc.cancel()
434 + raise
435 +
436 + def _cancel(self):
437 + if self._main_task is not None:
438 + self._main_task.done() or self._main_task.cancel()
439 + if self._stdin is not None and not self._stdin.closed:
440 + self._stdin.close()
441 +
442 + def _main_exit(self, main_task):
443 + try:
444 + main_task.result()
445 + except asyncio.CancelledError:
446 + self.cancel()
447 + self._was_cancelled()
448 + self.returncode = self.returncode or 0
449 + self._async_wait()
450 diff --git a/lib/portage/util/_async/SchedulerInterface.py b/lib/portage/util/_async/SchedulerInterface.py
451 index ec6417da1..3ff250d1d 100644
452 --- a/lib/portage/util/_async/SchedulerInterface.py
453 +++ b/lib/portage/util/_async/SchedulerInterface.py
454 @@ -1,4 +1,4 @@
455 -# Copyright 2012-2018 Gentoo Foundation
456 +# Copyright 2012-2020 Gentoo Authors
457 # Distributed under the terms of the GNU General Public License v2
458
459 import gzip
460 @@ -7,6 +7,8 @@ import errno
461 from portage import _encodings
462 from portage import _unicode_encode
463 from portage.util import writemsg_level
464 +from portage.util.futures._asyncio.streams import _writer
465 +from portage.util.futures.compat_coroutine import coroutine
466 from ..SlotObject import SlotObject
467
468 class SchedulerInterface(SlotObject):
469 @@ -53,6 +55,34 @@ class SchedulerInterface(SlotObject):
470 def _return_false():
471 return False
472
473 + @coroutine
474 + def async_output(self, msg, log_file=None, background=None,
475 + level=0, noiselevel=-1):
476 + """
477 + Output a msg to stdio (if not in background) and to a log file
478 + if provided.
479 +
480 + @param msg: a message string, including newline if appropriate
481 + @type msg: str
482 + @param log_file: log file in binary mode
483 + @type log_file: file
484 + @param background: send messages only to log (not to stdio)
485 + @type background: bool
486 + @param level: a numeric logging level (see the logging module)
487 + @type level: int
488 + @param noiselevel: passed directly to writemsg
489 + @type noiselevel: int
490 + """
491 + global_background = self._is_background()
492 + if background is None or global_background:
493 + background = global_background
494 +
495 + if not background:
496 + writemsg_level(msg, level=level, noiselevel=noiselevel)
497 +
498 + if log_file is not None:
499 + yield _writer(log_file, _unicode_encode(msg))
500 +
501 def output(self, msg, log_path=None, background=None,
502 level=0, noiselevel=-1):
503 """
504 diff --git a/man/make.conf.5 b/man/make.conf.5
505 index a3bd662ae..eb812150f 100644
506 --- a/man/make.conf.5
507 +++ b/man/make.conf.5
508 @@ -1,4 +1,4 @@
509 -.TH "MAKE.CONF" "5" "May 2020" "Portage VERSION" "Portage"
510 +.TH "MAKE.CONF" "5" "Jun 2020" "Portage VERSION" "Portage"
511 .SH "NAME"
512 make.conf \- custom settings for Portage
513 .SH "SYNOPSIS"
514 @@ -979,6 +979,11 @@ with an integer pid. For example, a value of "ionice \-c 3 \-p \\${PID}"
515 will set idle io priority. For more information about ionice, see
516 \fBionice\fR(1). This variable is unset by default.
517 .TP
518 +.B PORTAGE_LOG_FILTER_FILE_CMD
519 +This variable specifies a command that filters build log output to a
520 +log file. In order to filter ANSI escape codes from build logs,
521 +\fBansifilter\fR(1) is a convenient setting for this variable.
522 +.TP
523 .B PORTAGE_LOGDIR
524 This variable defines the directory in which per\-ebuild logs are kept.
525 Logs are created only when this is set. They are stored as
526 --
527 2.25.3

Replies