Salome HOME
changing SITE.log.log_dir by USER.log_dir
[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 '''In this file are implemented the classes and method relative to the logging
19 '''
20
21 import sys
22 import os
23 import datetime
24 import re
25
26 import src
27 from . import printcolors
28 from . import xmlManager
29
30 logCommandFileExpression = "^[0-9]{8}_+[0-9]{6}_+.*\.xml$"
31
32 class Logger(object):
33     '''Class to handle log mechanism.
34     '''
35     def __init__(self, config, silent_sysstd=False, all_in_terminal=False):
36         '''Initialization
37         
38         :param config pyconf.Config: The global configuration.
39         :param silent_sysstd boolean: if True, do not write anything
40                                       in terminal.
41         '''
42         self.config = config
43         self.default_level = 3
44         self.silentSysStd = silent_sysstd
45         
46         # Construct xml log file location for sat prints.
47         logFileName = config.VARS.datehour + "_" + config.VARS.command + ".xml"
48         logFilePath = os.path.join(config.USER.log_dir, logFileName)
49         # Construct txt file location in order to log 
50         # the external commands calls (cmake, make, git clone, etc...)
51         txtFileName = config.VARS.datehour + "_" + config.VARS.command + ".txt"
52         txtFilePath = os.path.join(config.USER.log_dir, "OUT", txtFileName)
53         
54         src.ensure_path_exists(os.path.dirname(logFilePath))
55         src.ensure_path_exists(os.path.dirname(txtFilePath))
56         
57         # The path of the log files (one for sat traces, and the other for 
58         # the system commands traces)
59         self.logFileName = logFileName
60         self.logFilePath = logFilePath
61         self.txtFileName = txtFileName
62         self.txtFilePath = txtFilePath
63         
64         # The list of all log files corresponding to the current command and
65         # the commands called by the current command
66         self.l_logFiles = [logFilePath, txtFilePath]
67         
68         # Initialize xml instance and put first fields 
69         # like beginTime, user, command, etc... 
70         self.xmlFile = xmlManager.XmlLogFile(logFilePath, "SATcommand", 
71                             attrib = {"application" : config.VARS.application})
72         self.put_initial_xml_fields()
73         # Initialize the txt file for reading
74         self.logTxtFile = open(str(self.txtFilePath), 'w')
75         # If the option all_in_terminal was called, all the system commands
76         # are redirected to the terminal
77         if all_in_terminal:
78             self.logTxtFile = sys.__stdout__
79         
80     def put_initial_xml_fields(self):
81         '''Method called at class initialization : Put all fields 
82            corresponding to the command context (user, time, ...)
83         '''
84         # command name
85         self.xmlFile.add_simple_node("Site", attrib={"command" : 
86                                                      self.config.VARS.command})
87         # version of salomeTools
88         self.xmlFile.append_node_attrib("Site", attrib={"satversion" : 
89                                             self.config.INTERNAL.sat_version})
90         # machine name on which the command has been launched
91         self.xmlFile.append_node_attrib("Site", attrib={"hostname" : 
92                                                     self.config.VARS.hostname})
93         # Distribution of the machine
94         self.xmlFile.append_node_attrib("Site", attrib={"OS" : 
95                                                         self.config.VARS.dist})
96         # The user that have launched the command
97         self.xmlFile.append_node_attrib("Site", attrib={"user" : 
98                                                         self.config.VARS.user})
99         # The time when command was launched
100         Y, m, dd, H, M, S = date_to_datetime(self.config.VARS.datehour)
101         date_hour = "%2s/%2s/%4s %2sh%2sm%2ss" % (dd, m, Y, H, M, S)
102         self.xmlFile.append_node_attrib("Site", attrib={"beginTime" : 
103                                                         date_hour})
104         # The application if any
105         if "APPLICATION" in self.config:
106             self.xmlFile.append_node_attrib("Site", 
107                         attrib={"application" : self.config.VARS.application})
108         # The initialization of the trace node
109         self.xmlFile.add_simple_node("Log",text="")
110         # The system commands logs
111         self.xmlFile.add_simple_node("OutLog",
112                                     text=os.path.join("OUT", self.txtFileName))
113         # The initialization of the node where 
114         # to put the links to the other sat commands that can be called by any
115         # command 
116         self.xmlFile.add_simple_node("Links")
117
118     def add_link(self,
119                  log_file_name,
120                  command_name,
121                  command_res,
122                  full_launched_command):
123         '''Add a link to another log file.
124         
125         :param log_file_name str: The file name of the link.
126         :param command_name str: The name of the command linked.
127         :param command_res str: The result of the command linked. "0" or "1"
128         :parma full_launched_command str: The full lanch command 
129                                           ("sat command ...")
130         '''
131         xmlLinks = self.xmlFile.xmlroot.find("Links")
132         src.xmlManager.add_simple_node(xmlLinks,
133                                        "link", 
134                                        text = log_file_name,
135                                        attrib = {"command" : command_name,
136                                                  "passed" : command_res,
137                                            "launchedCommand" : full_launched_command})
138
139     def write(self, message, level=None, screenOnly=False):
140         '''the function used in the commands 
141         that will print in the terminal and the log file.
142         
143         :param message str: The message to print.
144         :param level int: The output level corresponding 
145                           to the message 0 < level < 6.
146         :param screenOnly boolean: if True, do not write in log file.
147         '''
148         # do not write message starting with \r to log file
149         if not message.startswith("\r") and not screenOnly:
150             self.xmlFile.append_node_text("Log", 
151                                           printcolors.cleancolor(message))
152
153         # get user or option output level
154         current_output_verbose_level = self.config.USER.output_verbose_level
155         if not ('isatty' in dir(sys.stdout) and sys.stdout.isatty()):
156             # clean the message color if the terminal is redirected by user
157             # ex: sat compile appli > log.txt
158             message = printcolors.cleancolor(message)
159         
160         # Print message regarding the output level value
161         if level:
162             if level <= current_output_verbose_level and not self.silentSysStd:
163                 sys.stdout.write(message)
164         else:
165             if self.default_level <= current_output_verbose_level and not self.silentSysStd:
166                 sys.stdout.write(message)
167         self.flush()
168
169     def error(self, message):
170         '''Print an error.
171         
172         :param message str: The message to print.
173         '''
174         # Print in the log file
175         self.xmlFile.append_node_text("traces", _('ERROR:') + message)
176
177         # Print in the terminal and clean colors if the terminal 
178         # is redirected by user
179         if not ('isatty' in dir(sys.stderr) and sys.stderr.isatty()):
180             sys.stderr.write(printcolors.printcError(_('ERROR:') + message))
181         else:
182             sys.stderr.write(_('ERROR:') + message)
183
184     def flush(self):
185         '''Flush terminal
186         '''
187         sys.stdout.flush()
188         self.logTxtFile.flush()
189         
190     def end_write(self, attribute):
191         '''Method called just after command end : Put all fields 
192            corresponding to the command end context (time).
193            Write the log xml file on the hard drive.
194            And display the command to launch to get the log
195         
196         :param attribute dict: the attribute to add to the node "Site".
197         '''       
198         # Get current time (end of command) and format it
199         dt = datetime.datetime.now()
200         Y, m, dd, H, M, S = date_to_datetime(self.config.VARS.datehour)
201         t0 = datetime.datetime(int(Y), int(m), int(dd), int(H), int(M), int(S))
202         tf = dt
203         delta = tf - t0
204         total_time = timedelta_total_seconds(delta)
205         hours = int(total_time / 3600)
206         minutes = int((total_time - hours*3600) / 60)
207         seconds = total_time - hours*3600 - minutes*60
208         # Add the fields corresponding to the end time
209         # and the total time of command
210         endtime = dt.strftime('%Y/%m/%d %Hh%Mm%Ss')
211         self.xmlFile.append_node_attrib("Site", attrib={"endTime" : endtime})
212         self.xmlFile.append_node_attrib("Site", 
213                 attrib={"TotalTime" : "%ih%im%is" % (hours, minutes, seconds)})
214         
215         # Add the attribute passed to the method
216         self.xmlFile.append_node_attrib("Site", attrib=attribute)
217         
218         # Call the method to write the xml file on the hard drive
219         self.xmlFile.write_tree(stylesheet = "command.xsl")
220         
221         # Dump the config in a pyconf file in the log directory
222         logDir = self.config.USER.log_dir
223         dumpedPyconfFileName = (self.config.VARS.datehour 
224                                 + "_" 
225                                 + self.config.VARS.command 
226                                 + ".pyconf")
227         dumpedPyconfFilePath = os.path.join(logDir, 'OUT', dumpedPyconfFileName)
228         f = open(dumpedPyconfFilePath, 'w')
229         self.config.__save__(f)
230         f.close()
231         
232
233 def date_to_datetime(date):
234     '''Little method that gets year, mon, day, hour , 
235        minutes and seconds from a str in format YYYYMMDD_HHMMSS
236     
237     :param date str: The date in format YYYYMMDD_HHMMSS
238     :return: the same date and time in separate variables.
239     :rtype: (str,str,str,str,str,str)
240     '''
241     Y = date[:4]
242     m = date[4:6]
243     dd = date[6:8]
244     H = date[9:11]
245     M = date[11:13]
246     S = date[13:15]
247     return Y, m, dd, H, M, S
248
249 def timedelta_total_seconds(timedelta):
250     '''Little method to replace total_seconds from 
251        datetime module in order to be compatible with old python versions
252     
253     :param timedelta datetime.timedelta: The delta between two dates
254     :return: The number of seconds corresponding to timedelta.
255     :rtype: float
256     '''
257     return (
258         timedelta.microseconds + 0.0 +
259         (timedelta.seconds + timedelta.days * 24 * 3600) * 10 ** 6) / 10 ** 6
260         
261 def show_command_log(logFilePath, cmd, application, notShownCommands):
262     '''Used in updateHatXml. Determine if the log xml file logFilePath 
263        has to be shown or not in the hat log.
264     
265     :param logFilePath str: the path to the command xml log file
266     :param cmd str: the command of the log file
267     :param application str: the application passed as parameter 
268                             to the salomeTools command
269     :param notShownCommands list: the list of commands 
270                                   that are not shown by default
271     
272     :return: True if cmd is not in notShownCommands and the application 
273              in the log file corresponds to application
274     :rtype: boolean
275     '''
276     # When the command is not in notShownCommands, no need to go further :
277     # Do not show
278     if cmd in notShownCommands:
279         return False, None
280  
281     # Get the application of the log file
282     try:
283         logFileXml = src.xmlManager.ReadXmlFile(logFilePath)
284     except Exception as e:
285         msg = _("WARNING: the log file %s cannot be read:" % logFilePath)
286         sys.stdout.write(printcolors.printcWarning("%s\n%s\n" % (msg, e)))
287         return False, None
288
289     if 'application' in logFileXml.xmlroot.keys():
290         appliLog = logFileXml.xmlroot.get('application')
291         # if it corresponds, then the log has to be shown
292         if appliLog == application:
293             return True, appliLog
294         elif application != 'None':
295             return False, appliLog
296         
297         return True, appliLog
298     
299     if application == 'None':
300             return True, None    
301         
302     return False, None
303
304 def list_log_file(dirPath, expression):
305     '''Find all files corresponding to expression in dirPath
306     
307     :param dirPath str: the directory where to search the files
308     :param expression str: the regular expression of files to find
309     :return: the list of files path and informations about it
310     :rtype: list
311     '''
312     lRes = []
313     for fileName in os.listdir(dirPath):
314         # YYYYMMDD_HHMMSS_namecmd.xml
315         sExpr = expression
316         oExpr = re.compile(sExpr)
317         if oExpr.search(fileName):
318             # get date and hour and format it
319             date_hour_cmd = fileName.split('_')
320             date_not_formated = date_hour_cmd[0]
321             date = "%s/%s/%s" % (date_not_formated[6:8], 
322                                  date_not_formated[4:6], 
323                                  date_not_formated[0:4])
324             hour_not_formated = date_hour_cmd[1]
325             hour = "%s:%s:%s" % (hour_not_formated[0:2], 
326                                  hour_not_formated[2:4], 
327                                  hour_not_formated[4:6])
328             cmd = date_hour_cmd[2][:-len('.xml')]
329             lRes.append((os.path.join(dirPath, fileName), 
330                          date_not_formated, date, hour_not_formated, hour, cmd))
331     return lRes
332
333 def update_hat_xml(logDir, application=None, notShownCommands = []):
334     '''Create the xml file in logDir that contain all the xml file 
335        and have a name like YYYYMMDD_HHMMSS_namecmd.xml
336     
337     :param logDir str: the directory to parse
338     :param application str: the name of the application if there is any
339     '''
340     # Create an instance of XmlLogFile class to create hat.xml file
341     xmlHatFilePath = os.path.join(logDir, 'hat.xml')
342     xmlHat = src.xmlManager.XmlLogFile(xmlHatFilePath,
343                                     "LOGlist", {"application" : application})
344     # parse the log directory to find all the command logs, 
345     # then add it to the xml file
346     lLogFile = list_log_file(logDir, logCommandFileExpression)
347     for filePath, _, date, _, hour, cmd in lLogFile:
348         showLog, cmdAppli = show_command_log(filePath, cmd,
349                                               application, notShownCommands)
350         #if cmd not in notShownCommands:
351         if showLog:
352             # add a node to the hat.xml file
353             xmlHat.add_simple_node("LogCommand", 
354                                    text=os.path.basename(filePath), 
355                                    attrib = {"date" : date, 
356                                              "hour" : hour, 
357                                              "cmd" : cmd, 
358                                              "application" : cmdAppli})
359     
360     # Write the file on the hard drive
361     xmlHat.write_tree('hat.xsl')