Fix duplicate log shows when run mic with 'yum'
[tools/mic.git] / mic / msger.py
1 #
2 # Copyright (c) 2013 Intel, Inc.
3 #
4 # This program is free software; you can redistribute it and/or modify it
5 # under the terms of the GNU General Public License as published by the Free
6 # Software Foundation; version 2 of the License
7 #
8 # This program is distributed in the hope that it will be useful, but
9 # WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
10 # or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
11 # for more details.
12 #
13 # You should have received a copy of the GNU General Public License along
14 # with this program; if not, write to the Free Software Foundation, Inc., 59
15 # Temple Place - Suite 330, Boston, MA 02111-1307, USA.
16
17 # Following messages should be disabled in pylint:
18 #  * Too many instance attributes (R0902)
19 #  * Too few public methods (R0903)
20 #  * Too many public methods (R0904)
21 #  * Anomalous backslash in string (W1401)
22 #  * __init__ method from base class %r is not called (W0231)
23 #  * __init__ method from a non direct base class %r is called (W0233)
24 #  * Invalid name for type (C0103)
25 #  * RootLogger has no '%s' member (E1103)
26 # pylint: disable=R0902,R0903,R0904,W1401,W0231,W0233,C0103,E1103
27
28 """ This logging module is fully compatible with the old msger module, and
29     it supports interactive mode, logs the messages with specified levels
30     to specified stream, can also catch all error messages including the
31     involved 3rd party modules to the logger
32 """
33 import os
34 import sys
35 import logging
36 import tempfile
37
38 __ALL__ = [
39     'get_loglevel',
40     'set_loglevel',
41     'set_logfile',
42     'enable_interactive',
43     'disable_interactive',
44     'enable_logstderr',
45     'disable_logstderr',
46     'raw',
47     'debug',
48     'verbose',
49     'info',
50     'warning',
51     'error',
52     'select',
53     'choice',
54     'ask',
55     'pause',
56 ]
57
58
59 # define the color constants
60 BLACK, RED, GREEN, YELLOW, BLUE, MAGENTA, CYAN, WHITE = range(30, 38)
61
62 # color sequence for tty terminal
63 COLOR_SEQ = "\033[%dm" 
64 # reset sequence for tty terminal
65 RESET_SEQ = "\033[0m"
66
67 # new log level
68 RAWTEXT = 25
69 VERBOSE = 15
70
71 # define colors for log levels
72 COLORS = {
73     'DEBUG':    COLOR_SEQ % BLUE,
74     'VERBOSE':  COLOR_SEQ % MAGENTA,
75     'INFO':     COLOR_SEQ % GREEN,
76     'WARNING':  COLOR_SEQ % YELLOW,
77     'ERROR':    COLOR_SEQ % RED,
78 }
79
80
81 class LevelFilter(logging.Filter):
82     """ A filter that selects logging message with specified level """
83     def __init__(self, levels):
84         self._levels = levels
85
86     def filter(self, record):
87         if self._levels:
88             return record.levelname in self._levels
89         return False
90
91
92 class MicStreamHandler(logging.StreamHandler):
93     """ A stream handler that print colorized levelname in tty terminal """
94     def __init__(self, stream=None):
95         logging.StreamHandler.__init__(self, stream)
96         msg_fmt = "%(color)s%(levelname)s:%(reset)s %(message)s"
97         self.setFormatter(logging.Formatter(fmt=msg_fmt))
98
99     def _use_color(self):
100         """ Check if to print in color or not """
101         in_emacs = (os.getenv("EMACS") and
102                     os.getenv("INSIDE_EMACS", "").endswith(",comint"))
103         return self.stream.isatty() and not in_emacs
104
105     def format(self, record):
106         """ Format the logging record if need color """
107         record.color = record.reset = ""
108         if self._use_color():
109             record.color = COLORS[record.levelname]
110             record.reset = RESET_SEQ
111         return logging.StreamHandler.format(self, record)
112
113
114 class RedirectedStderr(object):
115     """ A faked error stream that redirect stderr to a temp file """
116     def __init__(self):
117         self.tmpfile = tempfile.NamedTemporaryFile()
118         self.fderr = None
119         self.value = None
120
121     def __del__(self):
122         self.close()
123
124     def close(self):
125         """ Close the temp file and clear the buffer """
126         try:
127             self.value = None
128             self.tmpfile.close()
129         except OSError:
130             pass
131
132     def truncate(self):
133         """ Truncate the tempfile to size zero """
134         if self.tmpfile:
135             os.ftruncate(self.tmpfile.fileno(), 0)
136
137     def redirect(self):
138         """ Redirect stderr to the temp file """
139         self.fderr = os.dup(2)
140         os.dup2(self.tmpfile.fileno(), 2)
141
142     def restore(self):
143         """ Restore the stderr and read the bufferred data """
144         os.dup2(self.fderr, 2)
145         self.fderr = None
146
147         if self.tmpfile:
148             self.tmpfile.seek(0, 0)
149             self.value = self.tmpfile.read()
150
151     def getvalue(self):
152         """ Read the bufferred data """
153         if self.tmpfile:
154             self.tmpfile.seek(0, 0)
155             self.value = self.tmpfile.read()
156             os.ftruncate(self.tmpfile.fileno(), 0)
157             return self.value
158         return None
159
160 class MicFileHandler(logging.FileHandler):
161     """ This file handler is supposed to catch the stderr output from
162         all modules even 3rd party modules involed, as it redirects
163         the stderr stream to a temp file stream, if logfile assigned,
164         it will flush the record to file stream, else it's a buffer
165         handler; once logfile assigned, the buffer will be flushed
166     """
167     def __init__(self, filename=None, mode='w', encoding=None, capacity=10):
168         # we don't use FileHandler to initialize,
169         # because filename might be expected to None
170         logging.Handler.__init__(self)
171         self.stream = None
172         if filename:
173             self.baseFilename = os.path.abspath(filename)
174         else:
175             self.baseFilename = None
176         self.mode = mode
177         self.encoding = None
178         self.capacity = capacity
179         # buffering the records
180         self.buffer = []
181
182         # set formater locally
183         msg_fmt = "[%(asctime)s] %(message)s"
184         date_fmt = "%m/%d %H:%M:%S %Z"
185         self.setFormatter(logging.Formatter(fmt=msg_fmt, datefmt=date_fmt))
186         self.olderr = sys.stderr
187         self.stderr = RedirectedStderr()
188         self.errmsg = None
189
190     def set_logfile(self, filename, mode='w'):
191         """ Set logfile path to make it possible flush records not-on-fly """
192         self.baseFilename = os.path.abspath(filename)
193         self.mode = mode
194
195     def redirect_stderr(self):
196         """ Start to redirect stderr for catching all error output """
197         self.stderr.redirect()
198
199     def restore_stderr(self):
200         """ Restore stderr stream and log the error messages to both stderr
201             and log file if error messages are not empty
202         """
203         self.stderr.restore()
204         self.errmsg = self.stderr.value
205         if self.errmsg:
206             self.logstderr()
207
208     def logstderr(self):
209         """ Log catched error message from stderr redirector """
210         if not self.errmsg:
211             return
212
213         sys.stdout.write(self.errmsg)
214         sys.stdout.flush()
215
216         record = logging.makeLogRecord({'msg': self.errmsg})
217         self.buffer.append(record)
218
219         # truncate the redirector for the errors is logged
220         self.stderr.truncate()
221         self.errmsg = None
222
223     def emit(self, record):
224         """ Emit the log record to Handler """
225         # if there error message catched, log it first
226         self.errmsg = self.stderr.getvalue()
227         if self.errmsg:
228             self.logstderr()
229
230         # if no logfile assigned, it's a buffer handler
231         if not self.baseFilename:
232             self.buffer.append(record)
233             if len(self.buffer) >= self.capacity:
234                 self.buffer = []
235         else:
236             self.flushing(record)
237
238     def flushing(self, record=None):
239         """ Flush buffer and record to logfile """
240         # NOTE: 'flushing' can't be named 'flush' because of 'emit' calling it
241         # set file stream position to SEEK_END(=2)
242         if self.stream:
243             self.stream.seek(0, 2)
244         # if bufferred, flush it
245         if self.buffer:
246             for arecord in self.buffer:
247                 logging.FileHandler.emit(self, arecord)
248             self.buffer = []
249         # if recorded, flush it
250         if record:
251             logging.FileHandler.emit(self, record)
252
253     def close(self):
254         """ Close handler after flushing the buffer """
255         # if any left in buffer, flush it
256         if self.stream:
257             self.flushing()
258         logging.FileHandler.close(self)
259
260
261 class MicLogger(logging.Logger):
262     """ The MIC logger class, it supports interactive mode, and logs the
263         messages with specified levels tospecified stream, also can catch
264         all error messages including the involved 3rd party modules
265     """
266     def __init__(self, name, level=logging.INFO):
267         logging.Logger.__init__(self, name, level)
268         self.propagate = False
269         self.interactive = True
270         self.logfile = None
271         self._allhandlers = {
272             'default': logging.StreamHandler(sys.stdout),
273             'stdout': MicStreamHandler(sys.stdout),
274             'stderr': MicStreamHandler(sys.stderr),
275             'logfile': MicFileHandler(),
276         }
277
278         self._allhandlers['default'].addFilter(LevelFilter(['RAWTEXT']))
279         self._allhandlers['default'].setFormatter(
280             logging.Formatter(fmt="%(message)s"))
281         self.addHandler(self._allhandlers['default'])
282
283         self._allhandlers['stdout'].addFilter(LevelFilter(['DEBUG', 'VERBOSE',
284                                                           'INFO']))
285         self.addHandler(self._allhandlers['stdout'])
286
287         self._allhandlers['stderr'].addFilter(LevelFilter(['WARNING',
288                                                            'ERROR']))
289         self.addHandler(self._allhandlers['stderr'])
290
291         self.addHandler(self._allhandlers['logfile'])
292
293     def set_logfile(self, filename, mode='w'):
294         """ Set logfile path """
295         self.logfile = filename
296         self._allhandlers['logfile'].set_logfile(self.logfile, mode)
297
298     def enable_logstderr(self):
299         """ Start to log all error messages """
300         if self.logfile:
301             self._allhandlers['logfile'].redirect_stderr()
302
303     def disable_logstderr(self):
304         """ Stop to log all error messages """
305         if self.logfile:
306             self._allhandlers['logfile'].restore_stderr()
307
308     def verbose(self, msg, *args, **kwargs):
309         """ Log a message with level VERBOSE """
310         if self.isEnabledFor(VERBOSE):
311             self._log(VERBOSE, msg, args, **kwargs)
312
313     def raw(self, msg, *args, **kwargs):
314         """ Log a message in raw text format """
315         if self.isEnabledFor(RAWTEXT):
316             self._log(RAWTEXT, msg, args, **kwargs)
317
318     def select(self, msg, optdict, default=None):
319         """ Log a message in interactive mode """
320         if not optdict.keys():
321             return default
322         if default is None:
323             default = optdict.keys()[0]
324         msg += " [%s](%s): " % ('/'.join(optdict.keys()), default)
325         if not self.interactive or self.logfile:
326             reply = default
327             self.raw(msg + reply)
328         else:
329             while True:
330                 reply = raw_input(msg).strip()
331                 if not reply or reply in optdict:
332                     break
333             if not reply:
334                 reply = default
335         return optdict[reply]
336
337
338 def error(msg):
339     """ Log a message with level ERROR on the MIC logger """
340     LOGGER.error(msg)
341     sys.exit(2)
342
343 def warning(msg):
344     """ Log a message with level WARNING on the MIC logger """
345     LOGGER.warning(msg)
346
347 def info(msg):
348     """ Log a message with level INFO on the MIC logger """
349     LOGGER.info(msg)
350
351 def verbose(msg):
352     """ Log a message with level VERBOSE on the MIC logger """
353     LOGGER.verbose(msg)
354
355 def debug(msg):
356     """ Log a message with level DEBUG on the MIC logger """
357     LOGGER.debug(msg)
358
359 def raw(msg):
360     """ Log a message on the MIC logger in raw text format"""
361     LOGGER.raw(msg)
362
363 def select(msg, optdict, default=None):
364     """ Show an interactive scene in tty terminal and
365         logs them on MIC logger
366     """
367     return LOGGER.select(msg, optdict, default)
368
369 def choice(msg, optlist, default=0):
370     """ Give some alternatives to users for answering the question """
371     return LOGGER.select(msg, dict(zip(optlist, optlist)), optlist[default])
372
373 def ask(msg, ret=True):
374     """ Ask users to answer 'yes' or 'no' to the question """
375     answers = {'y': True, 'n': False}
376     default = {True: 'y', False: 'n'}[ret]
377     return LOGGER.select(msg, answers, default)
378
379 def pause(msg=None):
380     """ Pause for any key """
381     if msg is None:
382         msg = "press ANY KEY to continue ..."
383     raw_input(msg)
384
385 def set_logfile(logfile, mode='w'):
386     """ Set logfile path to the MIC logger """
387     LOGGER.set_logfile(logfile, mode)
388
389 def set_loglevel(level):
390     """ Set loglevel to the MIC logger """
391     if isinstance(level, basestring):
392         level = logging.getLevelName(level)
393     LOGGER.setLevel(level)
394
395 def get_loglevel():
396     """ Get the loglevel of the MIC logger """
397     return logging.getLevelName(LOGGER.level)
398
399 def disable_interactive():
400     """ Disable the interactive mode """
401     LOGGER.interactive = False
402
403 def enable_interactive():
404     """ Enable the interactive mode """
405     LOGGER.interactive = True
406
407 def set_interactive(value):
408     """ Set the interactive mode (for compatibility) """
409     if value:
410         enable_interactive()
411     else:
412         disable_interactive()
413
414 def enable_logstderr(fpath=None):
415     """ Start to log all error message on the MIC logger """
416     LOGGER.enable_logstderr()
417
418 def disable_logstderr():
419     """ Stop to log all error message on the MIC logger """
420     LOGGER.disable_logstderr()
421
422
423 # add two level to the MIC logger: 'VERBOSE', 'RAWTEXT'
424 logging.addLevelName(VERBOSE, 'VERBOSE')
425 logging.addLevelName(RAWTEXT, 'RAWTEXT')
426 # initial the MIC logger
427 logging.setLoggerClass(MicLogger)
428 LOGGER = logging.getLogger("MIC")