xref: /petsc/config/BuildSystem/logger.py (revision a69119a591a03a9d906b29c0a4e9802e4d7c9795)
1from __future__ import absolute_import
2import args
3import sys
4import os
5import textwrap
6
7# Ugly stuff to have curses called ONLY once, instead of for each
8# new Configure object created (and flashing the screen)
9global LineWidth
10global RemoveDirectory
11global backupRemoveDirectory
12LineWidth = -1
13RemoveDirectory = os.path.join(os.getcwd(),'')
14backupRemoveDirectory = ''
15
16class Logger(args.ArgumentProcessor):
17  '''This class creates a shared log and provides methods for writing to it'''
18  defaultLog = None
19  defaultOut = sys.stdout
20
21  def __init__(self, clArgs = None, argDB = None, log = None, out = defaultOut, debugLevel = None, debugSections = None, debugIndent = None):
22    args.ArgumentProcessor.__init__(self, clArgs, argDB)
23    self.logName       = None
24    self.log           = log
25    self.out           = out
26    self.debugLevel    = debugLevel
27    self.debugSections = debugSections
28    self.debugIndent   = debugIndent
29    self.dividerLength = 93
30    self.getRoot()
31    return
32
33  def __getstate__(self):
34    '''We do not want to pickle the default log stream'''
35    d = args.ArgumentProcessor.__getstate__(self)
36    if 'logBkp' in d:
37        del d['logBkp']
38    if 'log' in d:
39      if d['log'] is Logger.defaultLog:
40        del d['log']
41      else:
42        d['log'] = None
43    if 'out' in d:
44      if d['out'] is Logger.defaultOut:
45        del d['out']
46      else:
47        d['out'] = None
48    return d
49
50  def __setstate__(self, d):
51    '''We must create the default log stream'''
52    args.ArgumentProcessor.__setstate__(self, d)
53    if not 'log' in d:
54      self.log = self.createLog(None)
55    if not 'out' in d:
56      self.out = Logger.defaultOut
57    self.__dict__.update(d)
58    return
59
60  def setupArguments(self, argDB):
61    '''Setup types in the argument database'''
62    import nargs
63
64    argDB = args.ArgumentProcessor.setupArguments(self, argDB)
65    argDB.setType('log',           nargs.Arg(None, 'buildsystem.log', 'The filename for the log'))
66    argDB.setType('logAppend',     nargs.ArgBool(None, 0, 'The flag determining whether we backup or append to the current log', isTemporary = 1))
67    argDB.setType('debugLevel',    nargs.ArgInt(None, 3, 'Integer 0 to 4, where a higher level means more detail', 0, 5))
68    argDB.setType('debugSections', nargs.Arg(None, [], 'Message types to print, e.g. [compile,link,hg,install]'))
69    argDB.setType('debugIndent',   nargs.Arg(None, '  ', 'The string used for log indentation'))
70    argDB.setType('scrollOutput',  nargs.ArgBool(None, 0, 'Flag to allow output to scroll rather than overwriting a single line'))
71    argDB.setType('noOutput',      nargs.ArgBool(None, 0, 'Flag to suppress output to the terminal'))
72    return argDB
73
74  def setup(self):
75    '''Setup the terminal output and filtering flags'''
76    self.log = self.createLog(self.logName, self.log)
77    args.ArgumentProcessor.setup(self)
78
79    if self.argDB['noOutput']:
80      self.out           = None
81    if self.debugLevel is None:
82      self.debugLevel    = self.argDB['debugLevel']
83    if self.debugSections is None:
84      self.debugSections = self.argDB['debugSections']
85    if self.debugIndent is None:
86      self.debugIndent   = self.argDB['debugIndent']
87    return
88
89  def checkLog(self, logName):
90    import nargs
91    import os
92
93    if logName is None:
94      logName = nargs.Arg.findArgument('log', self.clArgs)
95    if logName is None:
96      if not self.argDB is None and 'log' in self.argDB:
97        logName    = self.argDB['log']
98      else:
99        logName    = 'default.log'
100    self.logName   = logName
101    self.logExists = os.path.exists(self.logName)
102    return self.logExists
103
104  def createLog(self, logName, initLog = None):
105    '''Create a default log stream, unless initLog is given'''
106    import nargs
107
108    if not initLog is None:
109      log = initLog
110    else:
111      if Logger.defaultLog is None:
112        appendArg = nargs.Arg.findArgument('logAppend', self.clArgs)
113        if self.checkLog(logName):
114          if not self.argDB is None and ('logAppend' in self.argDB and self.argDB['logAppend']) or (not appendArg is None and bool(appendArg)):
115            Logger.defaultLog = open(self.logName, 'a')
116          else:
117            try:
118              import os
119
120              os.rename(self.logName, self.logName+'.bkp')
121              Logger.defaultLog = open(self.logName, 'w')
122            except OSError:
123              sys.stdout.write('WARNING: Cannot backup log file, appending instead.\n')
124              Logger.defaultLog = open(self.logName, 'a')
125        else:
126          Logger.defaultLog = open(self.logName, 'w')
127      log = Logger.defaultLog
128    return log
129
130  def closeLog(self):
131    '''Closes the log file'''
132    self.log.close()
133
134  def saveLog(self):
135    if self.debugLevel <= 3: return
136    import io
137    self.logBkp = self.log
138    self.log = io.StringIO()
139
140  def restoreLog(self):
141    if self.debugLevel <= 3: return
142    s = self.log.getvalue()
143    self.log.close()
144    self.log = self.logBkp
145    del(self.logBkp)
146    return s
147
148  def getLinewidth(self):
149    global LineWidth
150    if not hasattr(self, '_linewidth'):
151      if self.out is None or not self.out.isatty() or self.argDB['scrollOutput']:
152        self._linewidth = -1
153      else:
154        if LineWidth == -1:
155          try:
156            import curses
157
158            try:
159              curses.setupterm()
160              (y, self._linewidth) = curses.initscr().getmaxyx()
161              curses.endwin()
162            except curses.error:
163              self._linewidth = -1
164          except:
165            self._linewidth = -1
166          LineWidth = self._linewidth
167        else:
168          self._linewidth = LineWidth
169    return self._linewidth
170  def setLinewidth(self, linewidth):
171    self._linewidth = linewidth
172    return
173  linewidth = property(getLinewidth, setLinewidth, doc = 'The maximum number of characters per log line')
174
175  def checkWrite(self, f, debugLevel, debugSection, writeAll = 0):
176    '''Check whether the log line should be written
177       - If writeAll is true, return true
178       - If debugLevel >= current level, and debugSection in current section or sections is empty, return true'''
179    if not isinstance(debugLevel, int):
180      raise RuntimeError('Debug level must be an integer: '+str(debugLevel))
181    if f is None:
182      return False
183    if writeAll:
184      return True
185    if self.debugLevel >= debugLevel and (not len(self.debugSections) or debugSection in self.debugSections):
186      return True
187    return False
188
189  def checkANSIEscapeSequences(self, ostream):
190    """
191    Return True if the stream supports ANSI escape sequences, False otherwise
192    """
193    try:
194      # _io.TextIoWrapper use 'name' attribute to store the file name
195      key = ostream.name
196    except AttributeError:
197      return False
198
199    try:
200      return self._ansi_esc_seq_cache[key]
201    except KeyError:
202      pass # have not processed this stream before
203    except AttributeError:
204      # have never done this before
205      self._ansi_esc_seq_cache = {}
206
207    is_a_tty = hasattr(ostream,'isatty') and ostream.isatty()
208    return self._ansi_esc_seq_cache.setdefault(key,is_a_tty and (
209      sys.platform != 'win32' or os.environ.get('TERM','').startswith(('xterm','ANSI')) or
210      # Windows Terminal supports VT codes.
211      'WT_SESSION' in os.environ or
212      # Microsoft Visual Studio Code's built-in terminal supports colors.
213      os.environ.get('TERM_PROGRAM') == 'vscode'
214    ))
215
216  def logIndent(self, debugLevel = -1, debugSection = None, comm = None):
217    '''Write the proper indentation to the log streams'''
218    import traceback
219
220    indentLevel = len(traceback.extract_stack())-5
221    for writeAll, f in enumerate([self.out, self.log]):
222      if self.checkWrite(f, debugLevel, debugSection, writeAll):
223        if not comm is None:
224          f.write('[')
225          f.write(str(comm.rank()))
226          f.write(']')
227        for i in range(indentLevel):
228          f.write(self.debugIndent)
229    return
230
231  def logBack(self):
232    '''Backup the current line if we are not scrolling output'''
233    if self.out is not None and self.linewidth > 0:
234      self.out.write('\r')
235    return
236
237  def logClear(self):
238    '''Clear the current line if we are not scrolling output'''
239    out,lw = self.out,self.linewidth
240    if out is not None and lw > 0:
241      out.write('\r\033[K' if self.checkANSIEscapeSequences(out) else ' '*lw)
242      try:
243        out.flush()
244      except AttributeError:
245        pass
246    return
247
248  def logPrintDivider(self, single = False, length = None, **kwargs):
249    if length is None:
250      length = self.dividerLength
251    kwargs.setdefault('rmDir',False)
252    kwargs.setdefault('indent',False)
253    kwargs.setdefault('forceScroll',False)
254    kwargs.setdefault('forceNewLine',True)
255    divider = ('-' if single else '=')*length
256    return self.logPrint(divider, **kwargs)
257
258  def logPrintWarning(self, msg, title = None, **kwargs):
259    if title is None:
260      title = 'WARNING'
261    return self.logPrintBox(msg,title='***** {} *****'.format(title),**kwargs)
262
263  def logPrintBox(self, msg, debugLevel = -1, debugSection = 'screen', indent = 1, comm = None, rmDir = 1, prefix = None, title = None):
264    def center_wrap(banner,text,length = None,**kwargs):
265      def center_line(line):
266        return line.center(length).rstrip()
267
268      if length is None:
269        length = self.dividerLength
270      kwargs.setdefault('break_on_hyphens',False)
271      kwargs.setdefault('break_long_words',False)
272      kwargs.setdefault('width',length-2)
273      kwargs.setdefault('initial_indent',prefix)
274      kwargs.setdefault('subsequent_indent',prefix)
275      wrapped = [
276        line for para in text.splitlines() for line in textwrap.wrap(textwrap.dedent(para),**kwargs)
277      ]
278      if len(wrapped) == 1:
279        # center-justify single lines, and remove the bogus prefix
280        wrapped[0] = center_line(wrapped[0].lstrip())
281      if banner:
282        # add the banner
283        wrapped.insert(0,center_line(banner))
284      return '\n'.join(wrapped)
285
286
287    if prefix is None:
288      prefix = ' '*2
289
290    if rmDir:
291      rmDir = center_wrap(title,self.logStripDirectory(msg))
292    msg = center_wrap(title,msg)
293    self.logClear()
294    self.logPrintDivider(debugLevel = debugLevel, debugSection = debugSection)
295    self.logPrint(msg, debugLevel = debugLevel, debugSection = debugSection, rmDir = rmDir, forceNewLine = True, forceScroll = True, indent = 0)
296    self.logPrintDivider(debugLevel = debugLevel, debugSection = debugSection)
297    return
298
299  def logStripDirectory(self,msg):
300    return msg.replace(RemoveDirectory,'')
301
302  def logClearRemoveDirectory(self):
303    global RemoveDirectory
304    global backupRemoveDirectory
305    backupRemoveDirectory = RemoveDirectory
306    RemoveDirectory = ''
307
308  def logResetRemoveDirectory(self):
309    global RemoveDirectory
310    global backupRemoveDirectory
311    RemoveDirectory = backupRemoveDirectory
312
313
314  def logWrite(self, msg, debugLevel = -1, debugSection = None, forceScroll = 0, rmDir = 1):
315    '''Write the message to the log streams'''
316    '''Generally goes to the file but not the screen'''
317    if not msg: return
318    for writeAll, f in enumerate([self.out, self.log]):
319      if self.checkWrite(f, debugLevel, debugSection, writeAll):
320        if rmDir:
321          if isinstance(rmDir,str):
322            clean_msg = rmDir
323          else:
324            clean_msg = self.logStripDirectory(msg)
325        else:
326          clean_msg = msg
327        if not forceScroll and not writeAll and self.linewidth > 0:
328          self.logClear()
329          for ms in clean_msg.splitlines():
330            f.write(ms[:self.linewidth])
331        else:
332          if not debugSection is None and not debugSection == 'screen' and len(msg):
333            f.write(str(debugSection))
334            f.write(': ')
335          f.write(msg if writeAll else clean_msg)
336        if hasattr(f, 'flush'):
337          f.flush()
338    return
339
340  def logPrint(self, msg, debugLevel = -1, debugSection = None, indent = 1, comm = None, forceScroll = 0, rmDir = 1, forceNewLine = False):
341    '''Write the message to the log streams with proper indentation and a newline'''
342    '''Generally goes to the file and the screen'''
343    if indent:
344      self.logIndent(debugLevel, debugSection, comm)
345    self.logWrite(msg, debugLevel, debugSection, forceScroll = forceScroll, rmDir = rmDir)
346    for writeAll, f in enumerate([self.out, self.log]):
347      if self.checkWrite(f, debugLevel, debugSection, writeAll):
348        if forceNewLine or writeAll or self.linewidth < 0:
349          f.write('\n')
350    return
351
352
353  def getRoot(self):
354    '''Return the directory containing this module
355       - This has the problem that when we reload a module of the same name, this gets screwed up
356         Therefore, we call it in the initializer, and stash it'''
357    #print '      In getRoot'
358    #print hasattr(self, '__root')
359    #print '      done checking'
360    if not hasattr(self, '__root'):
361      import os
362      import sys
363
364      # Work around a bug with pdb in 2.3
365      if hasattr(sys.modules[self.__module__], '__file__') and not os.path.basename(sys.modules[self.__module__].__file__) == 'pdb.py':
366        self.__root = os.path.abspath(os.path.dirname(sys.modules[self.__module__].__file__))
367      else:
368        self.__root = os.getcwd()
369    #print '      Exiting getRoot'
370    return self.__root
371  def setRoot(self, root):
372    self.__root = root
373    return
374  root = property(getRoot, setRoot, doc = 'The directory containing this module')
375