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