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