diff options
| -rw-r--r-- | meta/lib/oeqa/utils/commands.py | 107 |
1 files changed, 85 insertions, 22 deletions
diff --git a/meta/lib/oeqa/utils/commands.py b/meta/lib/oeqa/utils/commands.py index 57286fcb10..5e5345434d 100644 --- a/meta/lib/oeqa/utils/commands.py +++ b/meta/lib/oeqa/utils/commands.py | |||
| @@ -13,6 +13,7 @@ import sys | |||
| 13 | import signal | 13 | import signal |
| 14 | import subprocess | 14 | import subprocess |
| 15 | import threading | 15 | import threading |
| 16 | import time | ||
| 16 | import logging | 17 | import logging |
| 17 | from oeqa.utils import CommandError | 18 | from oeqa.utils import CommandError |
| 18 | from oeqa.utils import ftools | 19 | from oeqa.utils import ftools |
| @@ -25,7 +26,7 @@ except ImportError: | |||
| 25 | pass | 26 | pass |
| 26 | 27 | ||
| 27 | class Command(object): | 28 | class Command(object): |
| 28 | def __init__(self, command, bg=False, timeout=None, data=None, **options): | 29 | def __init__(self, command, bg=False, timeout=None, data=None, output_log=None, **options): |
| 29 | 30 | ||
| 30 | self.defaultopts = { | 31 | self.defaultopts = { |
| 31 | "stdout": subprocess.PIPE, | 32 | "stdout": subprocess.PIPE, |
| @@ -48,41 +49,103 @@ class Command(object): | |||
| 48 | self.options.update(options) | 49 | self.options.update(options) |
| 49 | 50 | ||
| 50 | self.status = None | 51 | self.status = None |
| 52 | # We collect chunks of output before joining them at the end. | ||
| 53 | self._output_chunks = [] | ||
| 54 | self._error_chunks = [] | ||
| 51 | self.output = None | 55 | self.output = None |
| 52 | self.error = None | 56 | self.error = None |
| 53 | self.thread = None | 57 | self.threads = [] |
| 54 | 58 | ||
| 59 | self.output_log = output_log | ||
| 55 | self.log = logging.getLogger("utils.commands") | 60 | self.log = logging.getLogger("utils.commands") |
| 56 | 61 | ||
| 57 | def run(self): | 62 | def run(self): |
| 58 | self.process = subprocess.Popen(self.cmd, **self.options) | 63 | self.process = subprocess.Popen(self.cmd, **self.options) |
| 59 | 64 | ||
| 60 | def commThread(): | 65 | def readThread(output, stream, logfunc): |
| 61 | self.output, self.error = self.process.communicate(self.data) | 66 | if logfunc: |
| 62 | 67 | for line in stream: | |
| 63 | self.thread = threading.Thread(target=commThread) | 68 | output.append(line) |
| 64 | self.thread.start() | 69 | logfunc(line.decode("utf-8", errors='replace').rstrip()) |
| 70 | else: | ||
| 71 | output.append(stream.read()) | ||
| 72 | |||
| 73 | def readStderrThread(): | ||
| 74 | readThread(self._error_chunks, self.process.stderr, self.output_log.error if self.output_log else None) | ||
| 75 | |||
| 76 | def readStdoutThread(): | ||
| 77 | readThread(self._output_chunks, self.process.stdout, self.output_log.info if self.output_log else None) | ||
| 78 | |||
| 79 | def writeThread(): | ||
| 80 | try: | ||
| 81 | self.process.stdin.write(self.data) | ||
| 82 | self.process.stdin.close() | ||
| 83 | except OSError as ex: | ||
| 84 | # It's not an error when the command does not consume all | ||
| 85 | # of our data. subprocess.communicate() also ignores that. | ||
| 86 | if ex.errno != EPIPE: | ||
| 87 | raise | ||
| 88 | |||
| 89 | # We write in a separate thread because then we can read | ||
| 90 | # without worrying about deadlocks. The additional thread is | ||
| 91 | # expected to terminate by itself and we mark it as a daemon, | ||
| 92 | # so even it should happen to not terminate for whatever | ||
| 93 | # reason, the main process will still exit, which will then | ||
| 94 | # kill the write thread. | ||
| 95 | if self.data: | ||
| 96 | threading.Thread(target=writeThread, daemon=True).start() | ||
| 97 | if self.process.stderr: | ||
| 98 | thread = threading.Thread(target=readStderrThread) | ||
| 99 | thread.start() | ||
| 100 | self.threads.append(thread) | ||
| 101 | if self.output_log: | ||
| 102 | self.output_log.info('Running: %s' % self.cmd) | ||
| 103 | thread = threading.Thread(target=readStdoutThread) | ||
| 104 | thread.start() | ||
| 105 | self.threads.append(thread) | ||
| 65 | 106 | ||
| 66 | self.log.debug("Running command '%s'" % self.cmd) | 107 | self.log.debug("Running command '%s'" % self.cmd) |
| 67 | 108 | ||
| 68 | if not self.bg: | 109 | if not self.bg: |
| 69 | self.thread.join(self.timeout) | 110 | if self.timeout is None: |
| 111 | for thread in self.threads: | ||
| 112 | thread.join() | ||
| 113 | else: | ||
| 114 | deadline = time.time() + self.timeout | ||
| 115 | for thread in self.threads: | ||
| 116 | timeout = deadline - time.time() | ||
| 117 | if timeout < 0: | ||
| 118 | timeout = 0 | ||
| 119 | thread.join(timeout) | ||
| 70 | self.stop() | 120 | self.stop() |
| 71 | 121 | ||
| 72 | def stop(self): | 122 | def stop(self): |
| 73 | if self.thread.isAlive(): | 123 | for thread in self.threads: |
| 74 | self.process.terminate() | 124 | if thread.isAlive(): |
| 125 | self.process.terminate() | ||
| 75 | # let's give it more time to terminate gracefully before killing it | 126 | # let's give it more time to terminate gracefully before killing it |
| 76 | self.thread.join(5) | 127 | thread.join(5) |
| 77 | if self.thread.isAlive(): | 128 | if thread.isAlive(): |
| 78 | self.process.kill() | 129 | self.process.kill() |
| 79 | self.thread.join() | 130 | thread.join() |
| 80 | 131 | ||
| 81 | if not self.output: | 132 | def finalize_output(data): |
| 82 | self.output = "" | 133 | if not data: |
| 83 | else: | 134 | data = "" |
| 84 | self.output = self.output.decode("utf-8", errors='replace').rstrip() | 135 | else: |
| 85 | self.status = self.process.poll() | 136 | data = b"".join(data) |
| 137 | data = data.decode("utf-8", errors='replace').rstrip() | ||
| 138 | return data | ||
| 139 | |||
| 140 | self.output = finalize_output(self._output_chunks) | ||
| 141 | self._output_chunks = None | ||
| 142 | # self.error used to be a byte string earlier, probably unintentionally. | ||
| 143 | # Now it is a normal string, just like self.output. | ||
| 144 | self.error = finalize_output(self._error_chunks) | ||
| 145 | self._error_chunks = None | ||
| 146 | # At this point we know that the process has closed stdout/stderr, so | ||
| 147 | # it is safe and necessary to wait for the actual process completion. | ||
| 148 | self.status = self.process.wait() | ||
| 86 | 149 | ||
| 87 | self.log.debug("Command '%s' returned %d as exit code." % (self.cmd, self.status)) | 150 | self.log.debug("Command '%s' returned %d as exit code." % (self.cmd, self.status)) |
| 88 | # logging the complete output is insane | 151 | # logging the complete output is insane |
| @@ -98,7 +161,7 @@ class Result(object): | |||
| 98 | 161 | ||
| 99 | 162 | ||
| 100 | def runCmd(command, ignore_status=False, timeout=None, assert_error=True, | 163 | def runCmd(command, ignore_status=False, timeout=None, assert_error=True, |
| 101 | native_sysroot=None, limit_exc_output=0, **options): | 164 | native_sysroot=None, limit_exc_output=0, output_log=None, **options): |
| 102 | result = Result() | 165 | result = Result() |
| 103 | 166 | ||
| 104 | if native_sysroot: | 167 | if native_sysroot: |
| @@ -108,7 +171,7 @@ def runCmd(command, ignore_status=False, timeout=None, assert_error=True, | |||
| 108 | nenv['PATH'] = extra_paths + ':' + nenv.get('PATH', '') | 171 | nenv['PATH'] = extra_paths + ':' + nenv.get('PATH', '') |
| 109 | options['env'] = nenv | 172 | options['env'] = nenv |
| 110 | 173 | ||
| 111 | cmd = Command(command, timeout=timeout, **options) | 174 | cmd = Command(command, timeout=timeout, output_log=output_log, **options) |
| 112 | cmd.run() | 175 | cmd.run() |
| 113 | 176 | ||
| 114 | result.command = command | 177 | result.command = command |
| @@ -132,7 +195,7 @@ def runCmd(command, ignore_status=False, timeout=None, assert_error=True, | |||
| 132 | return result | 195 | return result |
| 133 | 196 | ||
| 134 | 197 | ||
| 135 | def bitbake(command, ignore_status=False, timeout=None, postconfig=None, **options): | 198 | def bitbake(command, ignore_status=False, timeout=None, postconfig=None, output_log=None, **options): |
| 136 | 199 | ||
| 137 | if postconfig: | 200 | if postconfig: |
| 138 | postconfig_file = os.path.join(os.environ.get('BUILDDIR'), 'oeqa-post.conf') | 201 | postconfig_file = os.path.join(os.environ.get('BUILDDIR'), 'oeqa-post.conf') |
| @@ -147,7 +210,7 @@ def bitbake(command, ignore_status=False, timeout=None, postconfig=None, **optio | |||
| 147 | cmd = [ "bitbake" ] + [a for a in (command + extra_args.split(" ")) if a not in [""]] | 210 | cmd = [ "bitbake" ] + [a for a in (command + extra_args.split(" ")) if a not in [""]] |
| 148 | 211 | ||
| 149 | try: | 212 | try: |
| 150 | return runCmd(cmd, ignore_status, timeout, **options) | 213 | return runCmd(cmd, ignore_status, timeout, output_log=output_log, **options) |
| 151 | finally: | 214 | finally: |
| 152 | if postconfig: | 215 | if postconfig: |
| 153 | os.remove(postconfig_file) | 216 | os.remove(postconfig_file) |
