Ignore the pylint warning of raising-bad-type and unbalanced-tuple-unpacking in conne...
[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 = list(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             os.lseek(self.tmpfile.fileno(), 0, os.SEEK_SET)
137
138     def redirect(self):
139         """ Redirect stderr to the temp file """
140         self.fderr = os.dup(2)
141         os.dup2(self.tmpfile.fileno(), 2)
142
143     def restore(self):
144         """ Restore the stderr and read the bufferred data """
145         os.dup2(self.fderr, 2)
146         self.fderr = None
147
148         if self.tmpfile:
149             self.tmpfile.seek(0, 0)
150             self.value = self.tmpfile.read()
151             if isinstance(self.value, bytes):
152                 self.value = self.value.decode()
153
154     def getvalue(self):
155         """ Read the bufferred data """
156         if self.tmpfile:
157             self.tmpfile.seek(0, 0)
158             self.value = self.tmpfile.read()
159             os.ftruncate(self.tmpfile.fileno(), 0)
160             os.lseek(self.tmpfile.fileno(), 0, os.SEEK_SET)
161             if isinstance(self.value, bytes):
162                 self.value = self.value.decode()
163             return self.value
164         return None
165
166 class MicFileHandler(logging.FileHandler):
167     """ This file handler is supposed to catch the stderr output from
168         all modules even 3rd party modules involed, as it redirects
169         the stderr stream to a temp file stream, if logfile assigned,
170         it will flush the record to file stream, else it's a buffer
171         handler; once logfile assigned, the buffer will be flushed
172     """
173     def __init__(self, filename=None, mode='w', encoding=None, capacity=10):
174         # we don't use FileHandler to initialize,
175         # because filename might be expected to None
176         logging.Handler.__init__(self)
177         self._builtin_open = open
178         self.errors = None
179         self.stream = None
180         if filename:
181             self.baseFilename = os.path.abspath(filename)
182         else:
183             self.baseFilename = None
184         self.mode = mode
185         self.encoding = None
186         self.capacity = capacity
187         # buffering the records
188         self.buffer = []
189
190         # set formater locally
191         msg_fmt = "[%(asctime)s] %(message)s"
192         date_fmt = "%m/%d %H:%M:%S %Z"
193         self.setFormatter(logging.Formatter(fmt=msg_fmt, datefmt=date_fmt))
194         self.olderr = sys.stderr
195         self.stderr = RedirectedStderr()
196         self.errmsg = None
197
198     def set_logfile(self, filename, mode='w'):
199         """ Set logfile path to make it possible flush records not-on-fly """
200         self.baseFilename = os.path.abspath(filename)
201         self.mode = mode
202
203     def redirect_stderr(self):
204         """ Start to redirect stderr for catching all error output """
205         self.stderr.redirect()
206
207     def restore_stderr(self):
208         """ Restore stderr stream and log the error messages to both stderr
209             and log file if error messages are not empty
210         """
211         self.stderr.restore()
212         self.errmsg = self.stderr.value
213         if self.errmsg:
214             self.logstderr()
215
216     def logstderr(self):
217         """ Log catched error message from stderr redirector """
218         if not self.errmsg:
219             return
220
221         if isinstance(self.errmsg, bytes):
222             self.errmsg = self.errmsg.decode()
223         sys.stdout.write(self.errmsg)
224         sys.stdout.flush()
225
226         record = logging.makeLogRecord({'msg': self.errmsg})
227         self.buffer.append(record)
228
229         # truncate the redirector for the errors is logged
230         self.stderr.truncate()
231         self.errmsg = None
232
233     def emit(self, record):
234         """ Emit the log record to Handler """
235         # if there error message catched, log it first
236         self.errmsg = self.stderr.getvalue()
237         if self.errmsg:
238             self.logstderr()
239
240         # if no logfile assigned, it's a buffer handler
241         if not self.baseFilename:
242             self.buffer.append(record)
243             if len(self.buffer) >= self.capacity:
244                 self.buffer = []
245         else:
246             self.flushing(record)
247
248     def flushing(self, record=None):
249         """ Flush buffer and record to logfile """
250         # NOTE: 'flushing' can't be named 'flush' because of 'emit' calling it
251         # set file stream position to SEEK_END(=2)
252         if self.stream:
253             self.stream.seek(0, 2)
254         # if bufferred, flush it
255         if self.buffer:
256             for arecord in self.buffer:
257                 logging.FileHandler.emit(self, arecord)
258             self.buffer = []
259         # if recorded, flush it
260         if record:
261             logging.FileHandler.emit(self, record)
262
263     def close(self):
264         """ Close handler after flushing the buffer """
265         # if any left in buffer, flush it
266         if self.stream:
267             self.flushing()
268         logging.FileHandler.close(self)
269
270
271 class MicLogger(logging.Logger):
272     """ The MIC logger class, it supports interactive mode, and logs the
273         messages with specified levels tospecified stream, also can catch
274         all error messages including the involved 3rd party modules
275     """
276     def __init__(self, name, level=logging.INFO):
277         logging.Logger.__init__(self, name, level)
278         self.propagate = False
279         self.interactive = True
280         self.logfile = None
281         self._allhandlers = {
282             'default': logging.StreamHandler(sys.stdout),
283             'stdout': MicStreamHandler(sys.stdout),
284             'stderr': MicStreamHandler(sys.stderr),
285             'logfile': MicFileHandler(),
286         }
287
288         self._allhandlers['default'].addFilter(LevelFilter(['RAWTEXT']))
289         self._allhandlers['default'].setFormatter(
290             logging.Formatter(fmt="%(message)s"))
291         self.addHandler(self._allhandlers['default'])
292
293         self._allhandlers['stdout'].addFilter(LevelFilter(['DEBUG', 'VERBOSE',
294                                                           'INFO']))
295         self.addHandler(self._allhandlers['stdout'])
296
297         self._allhandlers['stderr'].addFilter(LevelFilter(['WARNING',
298                                                            'ERROR']))
299         self.addHandler(self._allhandlers['stderr'])
300
301         self.addHandler(self._allhandlers['logfile'])
302
303     def set_logfile(self, filename, mode='w'):
304         """ Set logfile path """
305         self.logfile = filename
306         self._allhandlers['logfile'].set_logfile(self.logfile, mode)
307
308     def enable_logstderr(self):
309         """ Start to log all error messages """
310         if self.logfile:
311             self._allhandlers['logfile'].redirect_stderr()
312
313     def disable_logstderr(self):
314         """ Stop to log all error messages """
315         if self.logfile:
316             self._allhandlers['logfile'].restore_stderr()
317
318     def verbose(self, msg, *args, **kwargs):
319         """ Log a message with level VERBOSE """
320         if self.isEnabledFor(VERBOSE):
321             self._log(VERBOSE, msg, args, **kwargs)
322
323     def raw(self, msg, *args, **kwargs):
324         """ Log a message in raw text format """
325         if self.isEnabledFor(RAWTEXT):
326             self._log(RAWTEXT, msg, args, **kwargs)
327
328     def select(self, msg, optdict, default=None):
329         """ Log a message in interactive mode """
330         if not list(optdict.keys()):
331             return default
332         if default is None:
333             default = list(optdict.keys())[0]
334         msg += " [%s](%s): " % ('/'.join(list(optdict.keys())), default)
335         if not self.interactive or self.logfile:
336             reply = default
337             self.raw(msg + reply)
338         else:
339             while True:
340                 reply = input(msg).strip()
341                 if not reply or reply in optdict:
342                     break
343             if not reply:
344                 reply = default
345         return optdict[reply]
346
347
348 def error(msg):
349     """ Log a message with level ERROR on the MIC logger """
350     LOGGER.error(msg)
351     sys.exit(2)
352
353 def warning(msg):
354     """ Log a message with level WARNING on the MIC logger """
355     LOGGER.warning(msg)
356
357 def info(msg):
358     """ Log a message with level INFO on the MIC logger """
359     LOGGER.info(msg)
360
361 def verbose(msg):
362     """ Log a message with level VERBOSE on the MIC logger """
363     LOGGER.verbose(msg)
364
365 def debug(msg):
366     """ Log a message with level DEBUG on the MIC logger """
367     LOGGER.debug(msg)
368
369 def raw(msg):
370     """ Log a message on the MIC logger in raw text format"""
371     LOGGER.raw(msg)
372
373 def select(msg, optdict, default=None):
374     """ Show an interactive scene in tty terminal and
375         logs them on MIC logger
376     """
377     return LOGGER.select(msg, optdict, default)
378
379 def choice(msg, optlist, default=0):
380     """ Give some alternatives to users for answering the question """
381     return LOGGER.select(msg, dict(list(zip(optlist, optlist))), optlist[default])
382
383 def ask(msg, ret=True):
384     """ Ask users to answer 'yes' or 'no' to the question """
385     answers = {'y': True, 'n': False}
386     default = {True: 'y', False: 'n'}[ret]
387     return LOGGER.select(msg, answers, default)
388
389 def pause(msg=None):
390     """ Pause for any key """
391     if msg is None:
392         msg = "press ANY KEY to continue ..."
393     eval(input(msg))
394
395 def set_logfile(logfile, mode='w'):
396     """ Set logfile path to the MIC logger """
397     LOGGER.set_logfile(logfile, mode)
398
399 def set_loglevel(level):
400     """ Set loglevel to the MIC logger """
401     if isinstance(level, str):
402         level = logging.getLevelName(level)
403     LOGGER.setLevel(level)
404
405 def get_loglevel():
406     """ Get the loglevel of the MIC logger """
407     return logging.getLevelName(LOGGER.level)
408
409 def disable_interactive():
410     """ Disable the interactive mode """
411     LOGGER.interactive = False
412
413 def enable_interactive():
414     """ Enable the interactive mode """
415     LOGGER.interactive = True
416
417 def set_interactive(value):
418     """ Set the interactive mode (for compatibility) """
419     if value:
420         enable_interactive()
421     else:
422         disable_interactive()
423
424 def enable_logstderr(fpath=None):
425     """ Start to log all error message on the MIC logger """
426     LOGGER.enable_logstderr()
427
428 def disable_logstderr():
429     """ Stop to log all error message on the MIC logger """
430     LOGGER.disable_logstderr()
431
432
433 # add two level to the MIC logger: 'VERBOSE', 'RAWTEXT'
434 logging.addLevelName(VERBOSE, 'VERBOSE')
435 logging.addLevelName(RAWTEXT, 'RAWTEXT')
436 # initial the MIC logger
437 logging.setLoggerClass(MicLogger)
438 LOGGER = logging.getLogger("MIC")