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