Salome HOME
sat #20679 : maj doc pdf
[tools/sat.git] / src / logger.py
1 #!/usr/bin/env python
2 #-*- coding:utf-8 -*-
3 #  Copyright (C) 2010-2012  CEA/DEN
4 #
5 #  This library is free software; you can redistribute it and/or
6 #  modify it under the terms of the GNU Lesser General Public
7 #  License as published by the Free Software Foundation; either
8 #  version 2.1 of the License.
9 #
10 #  This library is distributed in the hope that it will be useful,
11 #  but WITHOUT ANY WARRANTY; without even the implied warranty of
12 #  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
13 #  Lesser General Public License for more details.
14 #
15 #  You should have received a copy of the GNU Lesser General Public
16 #  License along with this library; if not, write to the Free Software
17 #  Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307 USA
18
19 """\
20 Implements the classes and method relative to the logging
21 """
22
23 import sys
24 import os
25 import stat
26 import datetime
27 import re
28 import tempfile
29 import shutil
30
31 import src
32 import printcolors
33 import xmlManager
34
35 import src.debug as DBG
36
37 log_macro_command_file_expression = "^[0-9]{8}_+[0-9]{6}_+.*\.xml$"
38 log_all_command_file_expression = "^.*[0-9]{8}_+[0-9]{6}_+.*\.xml$"
39
40 verbose = True # cvw TODO
41
42 class Logger(object):
43     """\
44     Class to handle log mechanism.
45     """
46     def __init__(self,
47                  config= None,
48                  silent_sysstd=False,
49                  all_in_terminal=False,
50                  micro_command = False):
51         """Initialization
52         
53         :param config pyconf.Config: The global configuration.
54         :param silent_sysstd boolean: if True, do not write anything
55                                       in terminal.
56         """
57         DBG.write("src.logger.Logger", id(self))
58         self.config = config
59         self.default_level = 3
60         self.silentSysStd = silent_sysstd
61         
62         # Construct xml log file location for sat prints.
63         prefix = ""
64         if micro_command:
65             prefix = "micro_"
66         hour_command_host = (config.VARS.datehour + "_" + 
67                              config.VARS.command + "_" + 
68                              config.VARS.hostname)
69         logFileName = prefix + hour_command_host + ".xml"
70         log_dir = src.get_log_path(config)
71         logFilePath = os.path.join(log_dir, logFileName)
72         # Construct txt file location in order to log 
73         # the external commands calls (cmake, make, git clone, etc...)
74         txtFileName = prefix + hour_command_host + ".txt"
75         txtFilePath = os.path.join(log_dir, "OUT", txtFileName)
76
77         aDirLog = os.path.dirname(logFilePath)
78         if not os.path.exists(aDirLog):
79           print("create log dir %s" % aDirLog)
80           src.ensure_path_exists(aDirLog)
81           # sometimes other users make 'sat log' and create hat.xml file...
82           os.chmod(aDirLog,
83                    stat.S_IRUSR |
84                    stat.S_IRGRP |
85                    stat.S_IROTH |
86                    stat.S_IWUSR |
87                    stat.S_IWGRP |
88                    stat.S_IWOTH |
89                    stat.S_IXUSR |
90                    stat.S_IXGRP |
91                    stat.S_IXOTH)
92         src.ensure_path_exists(os.path.dirname(txtFilePath))
93         
94         # The path of the log files (one for sat traces, and the other for 
95         # the system commands traces)
96         self.logFileName = logFileName
97         self.logFilePath = logFilePath
98         self.txtFileName = txtFileName
99         self.txtFilePath = txtFilePath
100         
101         # The list of all log files corresponding to the current command and
102         # the commands called by the current command
103         self.l_logFiles = [logFilePath, txtFilePath]
104         
105         # Initialize xml instance and put first fields 
106         # like beginTime, user, command, etc... 
107         self.xmlFile = xmlManager.XmlLogFile(logFilePath, "SATcommand", 
108                             attrib = {"application" : config.VARS.application})
109         self.put_initial_xml_fields()
110         # Initialize the txt file for reading
111         try:
112             self.logTxtFile = open(str(self.txtFilePath), 'w')
113         except IOError:
114             #msg1 = _("WARNING! Trying to write to a file that"
115             #         " is not accessible:")
116             #msg2 = _("The logs won't be written.")
117             #print("%s\n%s\n%s\n" % (src.printcolors.printcWarning(msg1),
118             #                        src.printcolors.printcLabel(str(self.txtFilePath)),
119             #                        src.printcolors.printcWarning(msg2) ))
120             self.logTxtFile = tempfile.TemporaryFile()
121             
122         # If the option all_in_terminal was called, all the system commands
123         # are redirected to the terminal
124         if all_in_terminal:
125             self.logTxtFile = sys.__stdout__
126         
127     def put_initial_xml_fields(self):
128         """\
129         Called at class initialization: Put all fields 
130         corresponding to the command context (user, time, ...)
131         """
132         # command name
133         self.xmlFile.add_simple_node("Site", attrib={"command" : 
134                                                      self.config.VARS.command})
135         # version of salomeTools
136         self.xmlFile.append_node_attrib("Site", attrib={"satversion" : 
137                                             src.get_salometool_version(self.config)})
138         # machine name on which the command has been launched
139         self.xmlFile.append_node_attrib("Site", attrib={"hostname" : 
140                                                     self.config.VARS.hostname})
141         # Distribution of the machine
142         self.xmlFile.append_node_attrib("Site", attrib={"OS" : 
143                                                         self.config.VARS.dist})
144         # The user that have launched the command
145         self.xmlFile.append_node_attrib("Site", attrib={"user" : 
146                                                         self.config.VARS.user})
147         # The time when command was launched
148         Y, m, dd, H, M, S = date_to_datetime(self.config.VARS.datehour)
149         date_hour = "%4s/%2s/%2s %2sh%2sm%2ss" % (Y, m, dd, H, M, S)
150         self.xmlFile.append_node_attrib("Site", attrib={"beginTime" : 
151                                                         date_hour})
152         # The application if any
153         if "APPLICATION" in self.config:
154             self.xmlFile.append_node_attrib("Site", 
155                         attrib={"application" : self.config.VARS.application})
156         # The initialization of the trace node
157         self.xmlFile.add_simple_node("Log",text="")
158         # The system commands logs
159         self.xmlFile.add_simple_node("OutLog",
160                                     text=os.path.join("OUT", self.txtFileName))
161         # The initialization of the node where 
162         # to put the links to the other sat commands that can be called by any
163         # command 
164         self.xmlFile.add_simple_node("Links")
165
166     def add_link(self,
167                  log_file_name,
168                  command_name,
169                  command_res,
170                  full_launched_command):
171         """Add a link to another log file.
172         
173         :param log_file_name str: The file name of the link.
174         :param command_name str: The name of the command linked.
175         :param command_res str: The result of the command linked. "0" or "1"
176         :parma full_launched_command str: The full lanch command 
177                                           ("sat command ...")
178         """
179         xmlLinks = self.xmlFile.xmlroot.find("Links")
180         flc = src.xmlManager.escapeSequence(full_launched_command)
181         att = {"command" : command_name, "passed" : str(command_res), "launchedCommand" : flc}
182         src.xmlManager.add_simple_node(xmlLinks, "link", text = log_file_name, attrib = att)
183
184     def write(self, message, level=None, screenOnly=False):
185         """\
186         function used in the commands 
187         to print in the terminal and the log file.
188         
189         :param message str: The message to print.
190         :param level int: The output level corresponding 
191                           to the message 0 < level < 6.
192         :param screenOnly boolean: if True, do not write in log file.
193         """
194         # avoid traces if unittest
195         if isCurrentLoggerUnittest():
196             # print("doing unittest")
197             sendMessageToCurrentLogger(message, level)
198             return
199
200         # do not write message starting with \r to log file
201         if not message.startswith("\r") and not screenOnly:
202             self.xmlFile.append_node_text("Log", 
203                                           printcolors.cleancolor(message))
204
205         # get user or option output level
206         current_output_verbose_level = self.config.USER.output_verbose_level
207         if not ('isatty' in dir(sys.stdout) and sys.stdout.isatty()):
208             # clean the message color if the terminal is redirected by user
209             # ex: sat compile appli > log.txt
210             message = printcolors.cleancolor(message)
211         
212         # Print message regarding the output level value
213         if level:
214             if level <= current_output_verbose_level and not self.silentSysStd:
215                 sys.stdout.write(message)
216         else:
217             if self.default_level <= current_output_verbose_level and not self.silentSysStd:
218                 sys.stdout.write(message)
219         self.flush()
220
221     def error(self, message, prefix="ERROR: "):
222       """Print an error.
223
224       :param message str: The message to print.
225       """
226       # Print in the log file
227       self.xmlFile.append_node_text("traces", prefix + message)
228
229       # Print in the terminal and clean colors if the terminal
230       # is redirected by user
231       if not ('isatty' in dir(sys.stderr) and sys.stderr.isatty()):
232         sys.stderr.write(printcolors.printcError(prefix + message + "\n"))
233       else:
234         sys.stderr.write(prefix + message + "\n")
235
236     def step(self, message):
237       """Print an step message.
238
239       :param message str: The message to print.
240       """
241       self.write('STEP: ' + message, level=4)
242
243     def trace(self, message):
244       """Print an trace message.
245
246       :param message str: The message to print.
247       """
248       self.write('TRACE: ' + message, level=5)
249
250     def debug(self, message):
251       """Print an debug message.
252
253       :param message str: The message to print.
254       """
255       self.write('DEBUG: ' + message, level=6)
256
257     def warning(self, message):
258       """Print an warning message.
259
260       :param message str: The message to print.
261       """
262       self.error(message, prefix="WARNING: ")
263
264     def critical(self, message):
265       """Print an critical message.
266
267       :param message str: The message to print.
268       """
269       self.error(message, prefix="CRITICAL: ")
270
271
272
273     def flush(self):
274         """Flush terminal"""
275         sys.stdout.flush()
276         self.logTxtFile.flush()
277         
278     def end_write(self, attribute):
279         """\
280         Called just after command end: Put all fields 
281         corresponding to the command end context (time).
282         Write the log xml file on the hard drive.
283         And display the command to launch to get the log
284         
285         :param attribute dict: the attribute to add to the node "Site".
286         """       
287         # Get current time (end of command) and format it
288         dt = datetime.datetime.now()
289         Y, m, dd, H, M, S = date_to_datetime(self.config.VARS.datehour)
290         t0 = datetime.datetime(int(Y), int(m), int(dd), int(H), int(M), int(S))
291         tf = dt
292         delta = tf - t0
293         total_time = timedelta_total_seconds(delta)
294         hours = int(total_time / 3600)
295         minutes = int((total_time - hours*3600) / 60)
296         seconds = total_time - hours*3600 - minutes*60
297         # Add the fields corresponding to the end time
298         # and the total time of command
299         endtime = dt.strftime('%Y/%m/%d %Hh%Mm%Ss')
300         self.xmlFile.append_node_attrib("Site", attrib={"endTime" : endtime})
301         self.xmlFile.append_node_attrib("Site", 
302                 attrib={"TotalTime" : "%ih%im%is" % (hours, minutes, seconds)})
303         
304         # Add the attribute passed to the method
305         self.xmlFile.append_node_attrib("Site", attrib=attribute)
306         
307         # Call the method to write the xml file on the hard drive
308         self.xmlFile.write_tree(stylesheet = "command.xsl")
309
310         # so unconditionnaly copy stylesheet file(s)
311         xslDir = os.path.join(self.config.VARS.srcDir, 'xsl')
312         xslCommand = "command.xsl"
313         # xslHat = "hat.xsl" # have to be completed (one time at end)
314         xsltest = "test.xsl"
315         imgLogo = "LOGO-SAT.png"
316         files_to_copy = [xslCommand, xsltest, imgLogo]
317
318         logDir = src.get_log_path(self.config)
319         # copy the stylesheets in the log directory as soon as possible here
320         # because referenced in self.xmlFile.write_tree above
321         # OP We use copy instead of copy2 to update the creation date
322         #    So we can clean the LOGS directories easily
323         for f in files_to_copy:
324           f_init = os.path.join(xslDir, f)
325           f_target = os.path.join(logDir, f)
326           if not os.path.isfile(f_target): # do not overrride
327             shutil.copy(f_init, logDir)
328         
329         # Dump the config in a pyconf file in the log directory
330         dumpedPyconfFileName = (self.config.VARS.datehour
331                                 + "_" 
332                                 + self.config.VARS.command 
333                                 + ".pyconf")
334         dumpedPyconfFilePath = os.path.join(logDir, 'OUT', dumpedPyconfFileName)
335         try:
336             f = open(dumpedPyconfFilePath, 'w')
337             self.config.__save__(f)
338             f.close()
339         except IOError:
340             pass
341
342 def date_to_datetime(date):
343     """\
344     From a string date in format YYYYMMDD_HHMMSS
345     returns list year, mon, day, hour, minutes, seconds 
346     
347     :param date str: The date in format YYYYMMDD_HHMMSS
348     :return: the same date and time in separate variables.
349     :rtype: (str,str,str,str,str,str)
350     """
351     Y = date[:4]
352     m = date[4:6]
353     dd = date[6:8]
354     H = date[9:11]
355     M = date[11:13]
356     S = date[13:15]
357     return Y, m, dd, H, M, S
358
359 def timedelta_total_seconds(timedelta):
360     """\
361     Replace total_seconds from datetime module 
362     in order to be compatible with old python versions
363     
364     :param timedelta datetime.timedelta: The delta between two dates
365     :return: The number of seconds corresponding to timedelta.
366     :rtype: float
367     """
368     return (
369         timedelta.microseconds + 0.0 +
370         (timedelta.seconds + timedelta.days * 24 * 3600) * 10 ** 6) / 10 ** 6
371         
372 def show_command_log(logFilePath, cmd, application, notShownCommands):
373     """\
374     Used in updateHatXml. 
375     Determine if the log xml file logFilePath 
376     has to be shown or not in the hat log.
377     
378     :param logFilePath str: the path to the command xml log file
379     :param cmd str: the command of the log file
380     :param application str: the application passed as parameter 
381                             to the salomeTools command
382     :param notShownCommands list: the list of commands 
383                                   that are not shown by default
384     
385     :return: True if cmd is not in notShownCommands and the application 
386              in the log file corresponds to application
387     :rtype: boolean
388     """
389     # When the command is not in notShownCommands, no need to go further :
390     # Do not show
391     if cmd in notShownCommands:
392         return False, None, None
393  
394     # Get the application of the log file
395     try:
396         logFileXml = src.xmlManager.ReadXmlFile(logFilePath)
397     except Exception as e:
398         msg = _("WARNING: the log file %s cannot be read:" % logFilePath)
399         sys.stdout.write(printcolors.printcWarning("%s\n%s\n" % (msg, e)))
400         return False, None, None
401
402     try:
403         if 'application' in logFileXml.xmlroot.keys():
404           appliLog = logFileXml.xmlroot.get('application')
405           launched_cmd = logFileXml.xmlroot.find('Site').attrib['launchedCommand']
406           # if it corresponds, then the log has to be shown
407           if appliLog == application:
408               return True, appliLog, launched_cmd
409           elif application != 'None':
410               return False, appliLog, launched_cmd
411
412           return True, appliLog, launched_cmd
413     except Exception as e:
414         msg = _("WARNING: the log file %s cannot be parsed:" % logFilePath)
415         sys.stdout.write(printcolors.printcWarning("%s\n%s\n" % (msg, e)))
416         return False, None, None
417
418     if application == 'None':
419             return True, None, None
420         
421     return False, None, None
422
423 def list_log_file(dirPath, expression):
424     """Find all files corresponding to expression in dirPath
425     
426     :param dirPath str: the directory where to search the files
427     :param expression str: the regular expression of files to find
428     :return: the list of files path and informations about it
429     :rtype: list
430     """
431     lRes = []
432     for fileName in os.listdir(dirPath):
433         # YYYYMMDD_HHMMSS_namecmd.xml
434         sExpr = expression
435         oExpr = re.compile(sExpr)
436         if oExpr.search(fileName):
437             file_name = fileName
438             if fileName.startswith("micro_"):
439                 file_name = fileName[len("micro_"):]
440             # get date and hour and format it
441             date_hour_cmd_host = file_name.split('_')
442             date_not_formated = date_hour_cmd_host[0]
443             date = "%s/%s/%s" % (date_not_formated[6:8], 
444                                  date_not_formated[4:6], 
445                                  date_not_formated[0:4])
446             hour_not_formated = date_hour_cmd_host[1]
447             hour = "%s:%s:%s" % (hour_not_formated[0:2], 
448                                  hour_not_formated[2:4], 
449                                  hour_not_formated[4:6])
450             if len(date_hour_cmd_host) < 4:
451                 cmd = date_hour_cmd_host[2][:-len('.xml')]
452                 host = ""
453             else:
454                 cmd = date_hour_cmd_host[2]
455                 host = date_hour_cmd_host[3][:-len('.xml')]
456             lRes.append((os.path.join(dirPath, fileName), 
457                          date_not_formated,
458                          date,
459                          hour_not_formated,
460                          hour,
461                          cmd,
462                          host))
463     return lRes
464
465 def update_hat_xml(logDir, application=None, notShownCommands = []):
466     """\
467     Create the xml file in logDir that contain all the xml file 
468     and have a name like YYYYMMDD_HHMMSS_namecmd.xml
469     
470     :param logDir str: the directory to parse
471     :param application str: the name of the application if there is any
472     """
473     # Create an instance of XmlLogFile class to create hat.xml file
474     xmlHatFilePath = os.path.join(logDir, 'hat.xml')
475     xmlHat = src.xmlManager.XmlLogFile(xmlHatFilePath, "LOGlist", {"application" : application})
476     # parse the log directory to find all the command logs, 
477     # then add it to the xml file
478     lLogFile = list_log_file(logDir, log_macro_command_file_expression)
479     for filePath, __, date, __, hour, cmd, __ in lLogFile:
480         showLog, cmdAppli, full_cmd = show_command_log(filePath, cmd,
481                                               application, notShownCommands)
482         #if cmd not in notShownCommands:
483         if showLog:
484             # add a node to the hat.xml file
485             xmlHat.add_simple_node("LogCommand", 
486                                    text=os.path.basename(filePath), 
487                                    attrib = {"date" : date, 
488                                              "hour" : hour, 
489                                              "cmd" : cmd, 
490                                              "application" : cmdAppli,
491                                              "full_command" : full_cmd})
492     
493     # Write the file on the hard drive
494     xmlHat.write_tree('hat.xsl')
495     # Sometimes other users will make 'sat log' and update this file
496     os.chmod(xmlHatFilePath,
497              stat.S_IRUSR |
498              stat.S_IRGRP |
499              stat.S_IROTH |
500              stat.S_IWUSR |
501              stat.S_IWGRP |
502              stat.S_IWOTH )
503
504
505
506 # TODO for future
507 # prepare skip to logging logger sat5.1
508 # suppose only one logger in sat5.1
509 _currentLogger = []
510
511 def getCurrentLogger():
512   """get current logging logger, set as DefaultLogger if not set yet"""
513   if len(_currentLogger) == 0:
514     import src.loggingSimple as LOGSI
515     logger = LOGSI.getDefaultLogger()
516     _currentLogger.append(logger)
517     logger.warning("set by default current logger as %s" % logger.name)
518   return _currentLogger[0]
519
520 def getDefaultLogger():
521   """get simple logging logger DefaultLogger, set it as current"""
522   import src.loggingSimple as LOGSI
523   logger = LOGSI.getDefaultLogger()
524   setCurrentLogger(logger) # set it as current
525   return logger
526
527 def getUnittestLogger():
528   """get simple logging logger UnittestLogger, set it as current"""
529   import src.loggingSimple as LOGSI
530   logger = LOGSI.getUnittestLogger()
531   setCurrentLogger(logger) # set it as current
532   return logger
533
534 def setCurrentLogger(logger):
535   """temporary send all in stdout as simple logging logger"""
536   if len(_currentLogger) == 0:
537     _currentLogger.append(logger)
538     logger.debug("set current logger as %s" % logger.name)
539   else:
540     if _currentLogger[0].name != logger.name:
541       # logger.debug("quit current logger as default %s" % _currentLogger[0].name)
542       _currentLogger[0] = logger
543       logger.warning("change current logger as %s" % logger.name)
544   return _currentLogger[0]
545
546 def isCurrentLoggerUnittest():
547     logger = getCurrentLogger()
548     if "Unittest" in logger.name:
549       res = True
550     else:
551       res = False
552     #DBG.write("isCurrentLoggerUnittest %s" % logger.name, res)
553     return res
554
555 def sendMessageToCurrentLogger(message, level):
556     """
557     assume relay from obsolescent
558     logger.write(msg, 1/2/3...) to future
559     logging.critical/warning/info...(msg) (as logging package tips)
560     """
561     logger = getCurrentLogger()
562     if level is None:
563       lev = 2
564     else:
565       lev = level
566     if lev <= 1:
567       logger.critical(message)
568       return
569     if lev == 2:
570       logger.warning(message)
571       return
572     if lev == 3:
573       logger.info(message)
574       return
575     if lev == 4:
576       logger.step(message)
577       return
578     if lev == 5:
579       logger.trace(message)
580       return
581     if lev >= 6:
582       logger.debug(message)
583       return
584     msg = "What is this level: '%s' for message:\n%s" % (level, message)
585     logger.warning(msg)
586     return