1# Copyright (c) 2014-2015, Intel Corporation 2# All rights reserved. 3# 4# Redistribution and use in source and binary forms, with or without modification, 5# are permitted provided that the following conditions are met: 6# 7# 1. Redistributions of source code must retain the above copyright notice, this 8# list of conditions and the following disclaimer. 9# 10# 2. Redistributions in binary form must reproduce the above copyright notice, 11# this list of conditions and the following disclaimer in the documentation and/or 12# other materials provided with the distribution. 13# 14# 3. Neither the name of the copyright holder nor the names of its contributors 15# may be used to endorse or promote products derived from this software without 16# specific prior written permission. 17# 18# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 19# ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 20# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 21# DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR 22# ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 23# (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 24# LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON 25# ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 26# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 27# SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 28 29import threading 30import subprocess 31import logging 32import signal 33import os 34 35 36class StreamLoggerThread(threading.Thread): 37 38 """ File-like object used to log Popen stdout and stderr streams """ 39 40 def __init__(self, consoleLogger, level, name): 41 """ 42 StreamLoggerThread Object initializer 43 44 :param consoleLogger: console log handler 45 :type consoleLogger: Handler 46 :param level: desired logger level of the stream 47 :type level: logging.level 48 :param name: Thread name 49 :type name: string 50 """ 51 super().__init__() 52 53 # Pipe to interact with subprocess 54 self.__readFd, self.__writeFd = os.pipe() 55 56 self.__logger = logging.getLogger(__name__) 57 self.__logger.addHandler(consoleLogger) 58 59 self.__level = level 60 61 self.name = name 62 63 # Start stream logging 64 self.start() 65 66 def fileno(self): 67 """ Give Writing side of internal pipe to receive data """ 68 69 return self.__writeFd 70 71 def run(self): 72 """ Read the reading side of the pipe until EOF """ 73 74 with os.fdopen(self.__readFd) as stream: 75 for line in stream: 76 self.__logger.log(self.__level, line.strip('\n')) 77 78 def close(self): 79 """ Close writing pipe side """ 80 81 os.close(self.__writeFd) 82 83 84class SubprocessLoggerThread(threading.Thread): 85 86 """ This class is here to log long process stdout and stderr """ 87 88 # Event used to ask all SubprocessLoggerThread object to die 89 __closeEvent = threading.Event() 90 91 def __init__(self, cmd, consoleLogger): 92 """ 93 SubprocessLoggerThread Object initializer 94 95 :param cmd: command to launch 96 :type cmd: list 97 :param consoleLogger: console log handler 98 :type consoleLogger: Handler 99 """ 100 101 super().__init__() 102 103 self.__cmd = cmd 104 self.__subProc = None 105 106 self.name = "Thread : " + ' '.join(cmd) 107 108 self.__consoleLogger = consoleLogger 109 110 # Default logging level 111 self._stdOutLogLevel = logging.DEBUG 112 113 @classmethod 114 def closeAll(cls): 115 """ Set the closeEvent to ask the thread to die """ 116 cls.__closeEvent.set() 117 118 def __cleanup(self): 119 """ 120 Close properly the child with SIGINT. 121 122 The signal is sended to all the group to kill 123 subprocess launched by Popen 124 """ 125 os.killpg(self.__subProc.pid, signal.SIGINT) 126 127 def __subProcPreExec(self): 128 """ 129 Make Popen object a Group leader. 130 131 Avoid subprocess to receive signal destinated 132 to the MainThread. 133 """ 134 os.setpgrp() 135 136 def run(self): 137 """ Create Popen object and manage it """ 138 139 # Logging threaded file-object 140 stdOutLogger = StreamLoggerThread( 141 self.__consoleLogger, 142 self._stdOutLogLevel, 143 self.name + "STDOUT") 144 stdErrLogger = StreamLoggerThread( 145 self.__consoleLogger, 146 logging.ERROR, 147 self.name + "STDERR") 148 149 # Logging stdout and stderr through objects 150 self.__subProc = subprocess.Popen( 151 [os.getenv("SHELL"), "-c", ' '.join(self.__cmd)], 152 bufsize=1, 153 stdout=stdOutLogger, 154 stderr=stdErrLogger, 155 preexec_fn=self.__subProcPreExec, 156 shell=False) 157 158 # Waiting process close or closing order 159 while True: 160 try: 161 # We end the thread if we are requested to do so 162 if SubprocessLoggerThread.__closeEvent.wait(0.01): 163 self.__cleanup() 164 break 165 166 # or if the subprocess is dead 167 if self.__subProc.poll() is not None: 168 break 169 except KeyboardInterrupt: 170 continue 171 172 # Close pipes 173 if stdOutLogger.is_alive(): 174 stdOutLogger.close() 175 if stdErrLogger.is_alive(): 176 stdErrLogger.close() 177 178 179class ScriptLoggerThread(SubprocessLoggerThread): 180 181 """ This class is used to log script subprocess """ 182 183 def __init__(self, cmd, consoleLogger): 184 """ 185 ScriptLoggerThread Object initializer 186 187 :param cmd: command to launch 188 :type cmd: list 189 :param consoleLogger: console log handler 190 :type consoleLogger: Handler 191 """ 192 super().__init__(cmd, consoleLogger) 193 194 # Script logging level 195 self._stdOutLogLevel = logging.INFO 196 197 @classmethod 198 def getRunningInstances(cls): 199 """ 200 Running ScriptLoggerThread instances getter 201 202 :return: The list of running ScriptLoggerThread instances 203 :rtype: list 204 """ 205 return [t for t in threading.enumerate() if isinstance(t, cls)] 206