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