Salome HOME
Improve the application command
[tools/sat.git] / commands / application.py
1 #!/usr/bin/env python
2 #-*- coding:utf-8 -*-
3 #  Copyright (C) 2010-2013  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 import os
20 import stat
21 import sys
22 import shutil
23 import subprocess
24 import getpass
25
26 from src import ElementTree as etree
27 import src
28
29 parser = src.options.Options()
30 parser.add_option('n', 'name', 'string', 'name',
31     _('The name of the application (default is APPLI.name or runAppli)'))
32 parser.add_option('c', 'catalog', 'string', 'catalog',
33     _('The resources catalog to use'))
34 parser.add_option('t', 'target', 'string', 'target',
35     _('The directory where to create the application (default is PRODUCT.out_dir)'))
36 parser.add_option('', 'gencat', 'string', 'gencat',
37     _("""Create a resources catalog for the specified machines (separated with ',')
38 \tNOTICE: this command will ssh to retrieve information to each machine in the list"""))
39 parser.add_option('m', 'module', 'list2', 'modules',
40     _("module(s) to include in the application"))
41
42 ##
43 # Creates an alias for runAppli.
44 def make_alias(appli_path, alias_path, force=False):
45     assert len(alias_path) > 0, "Bad name for alias"
46     if os.path.exists(alias_path) and not force:
47         raise src.SatException(_("Cannot create the alias '%s'\n") % alias_path)
48     else: # find relative path
49         os.symlink(appli_path, alias_path)
50
51 ##
52 # add the definition of a module to out stream.
53 def add_module_to_appli(out, module, has_gui, module_path, logger, flagline):
54     if not os.path.exists(module_path):
55         if not flagline:
56             logger.write("\n", 3, False)
57             flagline = True
58         logger.write("  " + src.printcolors.printcWarning(_(
59                         "WARNING: module %s not installed") % module) + "\n", 3)
60
61     out.write('   <module name="%s" gui="%s" path="%s"/>\n' % (module,
62                                                                has_gui,
63                                                                module_path))
64     return flagline
65
66 ##
67 # Creates the config file to create an application with the list of modules.
68 def create_config_file(config, modules, env_file, logger):
69
70     samples = ""
71     if 'SAMPLES' in config.APPLICATION.products:
72         samples = src.product.get_product_config(config, 'SAMPLES').source_dir
73
74     config_file = src.get_tmp_filename(config, "appli_config.xml")
75     f = open(config_file, "w")
76
77     f.write('<application>\n')
78     f.write('<prerequisites path="%s"/>\n' % env_file)
79     f.write('<resources path="CatalogResources.xml"/>\n')
80     f.write('<modules>\n')
81
82     flagline = False
83     for m in modules:
84         mm = src.product.get_product_config(config, m)
85         if src.product.product_is_smesh_plugin(mm):
86             continue
87
88         if 'install_dir' in mm and bool(mm.install_dir) :
89             if src.product.product_is_cpp(mm):
90                 # cpp module
91                 for aa in src.product.get_product_components(mm):
92                     install_dir = os.path.join(config.APPLICATION.workdir,
93                                                "INSTALL")
94                     mp = os.path.join(install_dir, aa)
95                     flagline = add_module_to_appli(f,
96                                                    aa,
97                                                    "yes",
98                                                    mp,
99                                                    logger,
100                                                    flagline)
101             else:
102                 # regular module
103                 mp = mm.install_dir
104                 gui = src.get_cfg_param(mm, "has_gui", "yes")
105                 flagline = add_module_to_appli(f, m, gui, mp, logger, flagline)
106
107     f.write('</modules>\n')
108     f.write('<samples path="%s"/>\n' % samples)
109     f.write('</application>\n')
110     f.close()
111
112     return config_file
113
114 ##
115 # Customizes the application by editing SalomeApp.xml.
116 def customize_app(config, appli_dir, logger):
117     if 'configure' not in config.APPLI \
118         or len(config.APPLI.configure) == 0:
119         return
120
121     # shortcut to get an element (section or parameter) from parent.
122     def get_element(parent, name, strtype):
123         for c in parent.getchildren():
124             if c.attrib['name'] == name:
125                 return c
126
127         # element not found create it
128         elt = add_simple_node(parent, strtype)
129         elt.attrib['name'] = name
130         return elt
131
132     # shortcut method to create a node
133     def add_simple_node(parent, node_name, text=None):
134         n = etree.Element(node_name)
135         if text is not None:
136             try:
137                 n.text = text.strip("\n\t").decode("UTF-8")
138             except:
139                 sys.stderr.write("################ %s %s\n" % (node_name, text))
140                 n.text = "?"
141         parent.append(n)
142         return n
143
144     # read the app file
145     app_file = os.path.join(appli_dir, "SalomeApp.xml")
146     tree = etree.parse(app_file)
147     document = tree.getroot()
148     assert document is not None, "document tag not found"
149
150     logger.write("\n", 4)
151     for section_name in config.APPLI.configure:
152         for parameter_name in config.APPLI.configure[section_name]:
153             parameter_value = config.APPLI.configure[section_name][parameter_name]
154             logger.write("  configure: %s/%s = %s\n" % (section_name,
155                                                         parameter_name,
156                                                         parameter_value), 4)
157             section = get_element(document, section_name, "section")
158             parameter = get_element(section, parameter_name, "parameter")
159             parameter.attrib['value'] = parameter_value
160
161     # write the file
162     f = open(app_file, "w")
163     f.write("<?xml version='1.0' encoding='utf-8'?>\n")
164     f.write(etree.tostring(document, encoding='utf-8'))
165     f.close()
166
167 ##
168 # Generates the application with the config_file.
169 def generate_application(config, appli_dir, config_file, logger):
170     target_dir = os.path.dirname(appli_dir)
171
172     install_KERNEL_dir = src.product.get_product_config(config,
173                                                         'KERNEL').install_dir
174     script = os.path.join(install_KERNEL_dir, "bin", "salome", "appli_gen.py")
175     if not os.path.exists(script):
176         raise src.SatException(_("KERNEL is not installed"))
177     
178     # Add SALOME python in the environment in order to avoid python version 
179     # problems at appli_gen.py call
180     if 'Python' in config.APPLICATIONS.products:
181         envi = src.environment.SalomeEnviron(config,
182                                              src.environment.Environ(
183                                                               dict(os.environ)),
184                                              True)
185         envi.set_a_product('Python', logger)
186     
187     command = "python %s --prefix=%s --config=%s" % (script,
188                                                      appli_dir,
189                                                      config_file)
190     logger.write("\n>" + command + "\n", 5, False)
191     res = subprocess.call(command,
192                     shell=True,
193                     cwd=target_dir,
194                     env=envi.environ.environ,
195                     stdout=logger.log_file,
196                     stderr=subprocess.STDOUT)
197     
198     if res != 0:
199         raise src.SatException(_("Cannot create application, code = %d\n") % res)
200
201     return res
202
203 ##
204 #
205 def write_step(logger, message, level=3, pad=50):
206     logger.write("%s %s " % (message, '.' * (pad - len(message.decode("UTF-8")))), level)
207     logger.flush()
208
209 ##
210 # Creates a SALOME application.
211 def create_application(config, appli_dir, catalog, logger, display=True):
212       
213     SALOME_modules = get_SALOME_modules(config)
214     
215     prerequis = config.PRODUCT.prerequis
216
217     warn = ['KERNEL', 'GUI']
218     if display:
219         for w in warn:
220             if w not in SALOME_modules:
221                 msg = _("WARNING: module %s is required to create application\n") % w
222                 logger.write(src.printcolors.printcWarning(msg), 2)
223
224     # generate the launch file
225     retcode = generate_launch_file(config,
226                                    appli_dir,
227                                    catalog,
228                                    logger,
229                                    SALOME_modules)
230     
231     if retcode == 0:
232         cmd = src.printcolors.printcLabel("%s/runAppli" % appli_dir)
233
234     if display:
235         logger.write("\n", 3, False)
236         logger.write(_("To launch the application, type:\n"), 3, False)
237         logger.write("  %s" % (cmd), 3, False)
238         logger.write("\n", 3, False)
239     return retcode
240
241 def get_SALOME_modules(config):
242     l_modules = []
243     for product in config.APPLICATION.products:
244         product_info = src.product.get_product_config(config, product)
245         if src.product.product_is_SALOME(product_info):
246             l_modules.append(product)
247     return l_modules
248
249 ##
250 # Obsolescent way of creating the application.
251 # This method will use appli_gen to create the application directory.
252 def generate_launch_file(config, appli_dir, catalog, logger, l_SALOME_modules):
253     retcode = -1
254
255     if len(catalog) > 0 and not os.path.exists(catalog):
256         raise IOError(_("Catalog not found: %s") % catalog)
257     
258     write_step(logger, _("Creating environment files"))
259     status = src.KO_STATUS
260     try:
261         import environ
262         # generate only shells the user wants (by default bash, csh, batch)
263         # the environ command will only generate file compatible with the current system.
264         shells = src.get_cfg_param(config.TOOLS.environ,
265                                    "shells",
266                                    [ "bash", "batch"])
267         environ.write_all_source_files(config,
268                                        logger,
269                                        silent=True,
270                                        shells=shells)
271         status = src.OK_STATUS
272     finally:
273         logger.write(src.printcolors.printc(status) + "\n", 2, False)
274
275     # build the application
276     env_file = os.path.join(config.PRODUCT.out_dir, "env_launch.sh")
277     write_step(logger, _("Building application"), level=2)
278     cf = create_config_file(config, l_SALOME_modules, env_file, logger)
279
280     # create the application directory
281     os.makedirs(appli_dir)
282
283     # generate the application
284     status = src.KO_STATUS
285     try:
286         retcode = generate_application(config, appli_dir, cf, logger)
287         customize_app(config, appli_dir, logger)
288         status = src.OK_STATUS
289     finally:
290         logger.write(src.printcolors.printc(status) + "\n", 2, False)
291
292     # copy the catalog if one
293     if len(catalog) > 0:
294         shutil.copy(catalog, os.path.join(appli_dir, "CatalogResources.xml"))
295
296     return retcode
297
298
299 ##
300 # Generates a launcher that sources Salome's python and calls original launcher
301 def generate_sourcing_launcher(config, appli_dir, logger) :
302
303     # Rename original launcher
304     launcher_name = os.path.join( appli_dir,
305                                   "bin",
306                                   "salome",
307                                   config.APPLI.launch_alias_name )
308     original_launcher = launcher_name + "-original"
309     os.rename( launcher_name, original_launcher )
310     
311     # Open new launcher
312     f = open(launcher_name, "w")
313     
314     # Write the set up of the environment
315     env = src.environment.SalomeEnviron( config,
316                                          src.fileEnviron.get_file_environ(
317                                                                         f,
318                                                                         "bash",
319                                                                         {},
320                                                                         config))
321     env.set_a_product( "Python", logger)
322     
323     # Write the call to the original launcher
324     f.write( "\n\n")
325     f.write( "# This is the call to the original launcher\n")
326     f.write( original_launcher + " $*" )
327     f.write( "\n\n")
328     
329     # Write the cleaning of the environment
330     env.finish(True)
331     
332     # Close new launcher
333     f.close()
334     os.chmod(launcher_name, stat.S_IRWXU| stat.S_IRWXG| stat.S_IRWXO) # 0777
335
336    
337
338 ##
339 # Generates the catalog from a list of machines.
340 def generate_catalog(machines, config, logger):
341     # remove empty machines
342     machines = map(lambda l: l.strip(), machines)
343     machines = filter(lambda l: len(l) > 0, machines)
344
345     src.printcolors.print_value(logger,
346                                 _("Generate Resources Catalog"),
347                                 ", ".join(machines),
348                                 4)
349     cmd = '"cat /proc/cpuinfo | grep MHz ; cat /proc/meminfo | grep MemTotal"'
350     user = getpass.getuser()
351
352     catfile = src.get_tmp_filename(config, "CatalogResources.xml")
353     catalog = file(catfile, "w")
354     catalog.write("<!DOCTYPE ResourcesCatalog>\n<resources>\n")
355     for k in machines:
356         logger.write("    ssh %s " % (k + " ").ljust(20, '.'), 4)
357         logger.flush()
358
359         ssh_cmd = 'ssh -o "StrictHostKeyChecking no" %s %s' % (k, cmd)
360         p = subprocess.Popen(ssh_cmd, shell=True,
361                 stdin=subprocess.PIPE,
362                 stdout=subprocess.PIPE,
363                 stderr=subprocess.PIPE)
364         p.wait()
365
366         if p.returncode != 0:
367             logger.write(src.printcolors.printc(src.KO_STATUS) + "\n", 4)
368             logger.write("    " + src.printcolors.printcWarning(p.stderr.read()),
369                          2)
370         else:
371             logger.write(src.printcolors.printc(src.OK_STATUS) + "\n", 4)
372             lines = p.stdout.readlines()
373             freq = lines[0][:-1].split(':')[-1].split('.')[0].strip()
374             nb_proc = len(lines) -1
375             memory = lines[-1].split(':')[-1].split()[0].strip()
376             memory = int(memory) / 1000
377
378             catalog.write("    <machine\n")
379             catalog.write("        protocol=\"ssh\"\n")
380             catalog.write("        nbOfNodes=\"1\"\n")
381             catalog.write("        mode=\"interactif\"\n")
382             catalog.write("        OS=\"LINUX\"\n")
383             catalog.write("        CPUFreqMHz=\"%s\"\n" % freq)
384             catalog.write("        nbOfProcPerNode=\"%s\"\n" % nb_proc)
385             catalog.write("        memInMB=\"%s\"\n" % memory)
386             catalog.write("        userName=\"%s\"\n" % user)
387             catalog.write("        name=\"%s\"\n" % k)
388             catalog.write("        hostname=\"%s\"\n" % k)
389             catalog.write("    >\n")
390             catalog.write("    </machine>\n")
391
392     catalog.write("</resources>\n")
393     catalog.close()
394     return catfile
395
396 ##################################################
397
398 ##
399 # Describes the command
400 def description():
401     '''method that is called when salomeTools is called with --help option.
402     
403     :return: The text to display for the application command description.
404     :rtype: str
405     '''
406     return _("""The application command creates a SALOME application.\n"""
407              """WARNING: it works only for SALOME 6. Use the "launcher" """
408              """command for newer versions of SALOME""")
409
410 ##
411 # Runs the command.
412 def run(args, runner, logger):
413     '''method that is called when salomeTools is called with application
414        parameter.
415     '''
416     
417     (options, args) = parser.parse_args(args)
418
419     # check for product
420     src.check_config_has_application( runner.cfg )
421
422     application = src.printcolors.printcLabel(runner.cfg.VARS.application)
423     logger.write(_("Building application for %s\n") % application, 1)
424
425     # if section APPLI does not exists create one
426     if "APPLI" not in runner.cfg:
427         msg = _("The section APPLI is not defined in the product.")
428         logger.write(src.printcolors.printcError(msg), 1)
429         logger.write("\n", 1)
430         return 1
431
432     # get application dir
433     target_dir = runner.cfg.APPLICATION.workdir
434     if options.target:
435         target_dir = options.target
436
437     # set list of modules
438     if options.modules:
439         runner.cfg.APPLI['modules'] = options.modules
440
441     # set name and application_name
442     if options.name:
443         runner.cfg.APPLI['name'] = options.name
444         runner.cfg.APPLI['application_name'] = options.name + "_appdir"
445     
446     application_name = src.get_cfg_param(runner.cfg.APPLI,
447                                          "application_name",
448                                          runner.cfg.APPLI.name + "_appdir")
449     appli_dir = os.path.join(target_dir, application_name)
450
451     src.printcolors.print_value(logger,
452                                 _("Application directory"),
453                                 appli_dir,
454                                 3)
455
456     # get catalog
457     catalog, catalog_src = "", ""
458     if options.catalog:
459         # use catalog specified in the command line
460         catalog = options.catalog
461     elif options.gencat:
462         # generate catalog for given list of computers
463         catalog_src = options.gencat
464         catalog = generate_catalog(options.gencat.split(","),
465                                    runner.cfg,logger)
466     elif 'catalog' in runner.cfg.APPLI:
467         # use catalog specified in the product
468         if runner.cfg.APPLI.catalog.endswith(".xml"):
469             # catalog as a file
470             catalog = runner.cfg.APPLI.catalog
471         else:
472             # catalog as a list of computers
473             catalog_src = runner.cfg.APPLI.catalog
474             mlist = filter(lambda l: len(l.strip()) > 0,
475                            runner.cfg.APPLI.catalog.split(","))
476             if len(mlist) > 0:
477                 catalog = generate_catalog(runner.cfg.APPLI.catalog.split(","),
478                                            runner.cfg, logger)
479
480     # display which catalog is used
481     if len(catalog) > 0:
482         catalog = os.path.realpath(catalog)
483         if len(catalog_src) > 0:
484             src.printcolors.print_value(logger,
485                                         _("Resources Catalog"),
486                                         catalog_src,
487                                         3)
488         else:
489             src.printcolors.print_value(logger,
490                                         _("Resources Catalog"),
491                                         catalog,
492                                         3)
493
494     logger.write("\n", 3, False)
495
496     details = []
497
498     # remove previous application
499     if os.path.exists(appli_dir):
500         write_step(logger, _("Removing previous application directory"))
501         rres = src.KO_STATUS
502         try:
503             shutil.rmtree(appli_dir)
504             rres = src.OK_STATUS
505         finally:
506             logger.write(src.printcolors.printc(rres) + "\n", 3, False)
507
508     # generate the application
509     try:
510         try: # try/except/finally not supported in all version of python
511             retcode = create_application(runner.cfg, appli_dir, catalog, logger)
512         except Exception as exc:
513             details.append(str(exc))
514             raise
515     finally:
516         logger.write("\n", 3, False)
517
518     return retcode
519