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