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