Salome HOME
application
[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.APPLICATION.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.logTxtFile,
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     warn = ['KERNEL', 'GUI']
216     if display:
217         for w in warn:
218             if w not in SALOME_modules:
219                 msg = _("WARNING: module %s is required to create application\n") % w
220                 logger.write(src.printcolors.printcWarning(msg), 2)
221
222     # generate the launch file
223     retcode = generate_launch_file(config,
224                                    appli_dir,
225                                    catalog,
226                                    logger,
227                                    SALOME_modules)
228     
229     if retcode == 0:
230         cmd = src.printcolors.printcLabel("%s/runAppli" % appli_dir)
231
232     if display:
233         logger.write("\n", 3, False)
234         logger.write(_("To launch the application, type:\n"), 3, False)
235         logger.write("  %s" % (cmd), 3, False)
236         logger.write("\n", 3, False)
237     return retcode
238
239 def get_SALOME_modules(config):
240     l_modules = []
241     for product in config.APPLICATION.products:
242         product_info = src.product.get_product_config(config, product)
243         if src.product.product_is_SALOME(product_info):
244             l_modules.append(product)
245     return l_modules
246
247 ##
248 # Obsolescent way of creating the application.
249 # This method will use appli_gen to create the application directory.
250 def generate_launch_file(config, appli_dir, catalog, logger, l_SALOME_modules):
251     retcode = -1
252
253     if len(catalog) > 0 and not os.path.exists(catalog):
254         raise IOError(_("Catalog not found: %s") % catalog)
255     
256     write_step(logger, _("Creating environment files"))
257     status = src.KO_STATUS
258     try:
259         import environ
260         # generate only shells the user wants (by default bash, csh, batch)
261         # the environ command will only generate file compatible 
262         # with the current system.
263         environ.write_all_source_files(config,
264                                        logger,
265                                        silent=True)
266         status = src.OK_STATUS
267     finally:
268         logger.write(src.printcolors.printc(status) + "\n", 2, False)
269
270     # build the application
271     env_file = os.path.join(config.APPLICATION.workdir, "env_launch.sh")
272     write_step(logger, _("Building application"), level=2)
273     cf = create_config_file(config, l_SALOME_modules, env_file, logger)
274
275     # create the application directory
276     os.makedirs(appli_dir)
277
278     # generate the application
279     status = src.KO_STATUS
280     try:
281         retcode = generate_application(config, appli_dir, cf, logger)
282         customize_app(config, appli_dir, logger)
283         status = src.OK_STATUS
284     finally:
285         logger.write(src.printcolors.printc(status) + "\n", 2, False)
286
287     # copy the catalog if one
288     if len(catalog) > 0:
289         shutil.copy(catalog, os.path.join(appli_dir, "CatalogResources.xml"))
290
291     return retcode
292
293
294 ##
295 # Generates a launcher that sources Salome's python and calls original launcher
296 def generate_sourcing_launcher(config, appli_dir, logger) :
297
298     # Rename original launcher
299     launcher_name = os.path.join( appli_dir,
300                                   "bin",
301                                   "salome",
302                                   config.APPLI.launch_alias_name )
303     original_launcher = launcher_name + "-original"
304     os.rename( launcher_name, original_launcher )
305     
306     # Open new launcher
307     f = open(launcher_name, "w")
308     
309     # Write the set up of the environment
310     env = src.environment.SalomeEnviron( config,
311                                          src.fileEnviron.get_file_environ(
312                                                                         f,
313                                                                         "bash",
314                                                                         {},
315                                                                         config))
316     env.set_a_product( "Python", logger)
317     
318     # Write the call to the original launcher
319     f.write( "\n\n")
320     f.write( "# This is the call to the original launcher\n")
321     f.write( original_launcher + " $*" )
322     f.write( "\n\n")
323     
324     # Write the cleaning of the environment
325     env.finish(True)
326     
327     # Close new launcher
328     f.close()
329     os.chmod(launcher_name, stat.S_IRWXU| stat.S_IRWXG| stat.S_IRWXO) # 0777
330
331    
332
333 ##
334 # Generates the catalog from a list of machines.
335 def generate_catalog(machines, config, logger):
336     # remove empty machines
337     machines = map(lambda l: l.strip(), machines)
338     machines = filter(lambda l: len(l) > 0, machines)
339
340     src.printcolors.print_value(logger,
341                                 _("Generate Resources Catalog"),
342                                 ", ".join(machines),
343                                 4)
344     cmd = '"cat /proc/cpuinfo | grep MHz ; cat /proc/meminfo | grep MemTotal"'
345     user = getpass.getuser()
346
347     catfile = src.get_tmp_filename(config, "CatalogResources.xml")
348     catalog = file(catfile, "w")
349     catalog.write("<!DOCTYPE ResourcesCatalog>\n<resources>\n")
350     for k in machines:
351         logger.write("    ssh %s " % (k + " ").ljust(20, '.'), 4)
352         logger.flush()
353
354         ssh_cmd = 'ssh -o "StrictHostKeyChecking no" %s %s' % (k, cmd)
355         p = subprocess.Popen(ssh_cmd, shell=True,
356                 stdin=subprocess.PIPE,
357                 stdout=subprocess.PIPE,
358                 stderr=subprocess.PIPE)
359         p.wait()
360
361         if p.returncode != 0:
362             logger.write(src.printcolors.printc(src.KO_STATUS) + "\n", 4)
363             logger.write("    " + src.printcolors.printcWarning(p.stderr.read()),
364                          2)
365         else:
366             logger.write(src.printcolors.printc(src.OK_STATUS) + "\n", 4)
367             lines = p.stdout.readlines()
368             freq = lines[0][:-1].split(':')[-1].split('.')[0].strip()
369             nb_proc = len(lines) -1
370             memory = lines[-1].split(':')[-1].split()[0].strip()
371             memory = int(memory) / 1000
372
373             catalog.write("    <machine\n")
374             catalog.write("        protocol=\"ssh\"\n")
375             catalog.write("        nbOfNodes=\"1\"\n")
376             catalog.write("        mode=\"interactif\"\n")
377             catalog.write("        OS=\"LINUX\"\n")
378             catalog.write("        CPUFreqMHz=\"%s\"\n" % freq)
379             catalog.write("        nbOfProcPerNode=\"%s\"\n" % nb_proc)
380             catalog.write("        memInMB=\"%s\"\n" % memory)
381             catalog.write("        userName=\"%s\"\n" % user)
382             catalog.write("        name=\"%s\"\n" % k)
383             catalog.write("        hostname=\"%s\"\n" % k)
384             catalog.write("    >\n")
385             catalog.write("    </machine>\n")
386
387     catalog.write("</resources>\n")
388     catalog.close()
389     return catfile
390
391 ##################################################
392
393 ##
394 # Describes the command
395 def description():
396     '''method that is called when salomeTools is called with --help option.
397     
398     :return: The text to display for the application command description.
399     :rtype: str
400     '''
401     return _("""The application command creates a SALOME application.\n"""
402              """WARNING: it works only for SALOME 6. Use the "launcher" """
403              """command for newer versions of SALOME""")
404
405 ##
406 # Runs the command.
407 def run(args, runner, logger):
408     '''method that is called when salomeTools is called with application
409        parameter.
410     '''
411     
412     (options, args) = parser.parse_args(args)
413
414     # check for product
415     src.check_config_has_application( runner.cfg )
416
417     application = src.printcolors.printcLabel(runner.cfg.VARS.application)
418     logger.write(_("Building application for %s\n") % application, 1)
419
420     # if section APPLI does not exists create one
421     if "APPLI" not in runner.cfg:
422         msg = _("The section APPLI is not defined in the product.")
423         logger.write(src.printcolors.printcError(msg), 1)
424         logger.write("\n", 1)
425         return 1
426
427     # get application dir
428     target_dir = runner.cfg.APPLICATION.workdir
429     if options.target:
430         target_dir = options.target
431
432     # set list of modules
433     if options.modules:
434         runner.cfg.APPLI['modules'] = options.modules
435
436     # set name and application_name
437     if options.name:
438         runner.cfg.APPLI['name'] = options.name
439         runner.cfg.APPLI['application_name'] = options.name + "_appdir"
440     
441     application_name = src.get_cfg_param(runner.cfg.APPLI,
442                                          "application_name",
443                                          runner.cfg.APPLI.name + "_appdir")
444     appli_dir = os.path.join(target_dir, application_name)
445
446     src.printcolors.print_value(logger,
447                                 _("Application directory"),
448                                 appli_dir,
449                                 3)
450
451     # get catalog
452     catalog, catalog_src = "", ""
453     if options.catalog:
454         # use catalog specified in the command line
455         catalog = options.catalog
456     elif options.gencat:
457         # generate catalog for given list of computers
458         catalog_src = options.gencat
459         catalog = generate_catalog(options.gencat.split(","),
460                                    runner.cfg,logger)
461     elif 'catalog' in runner.cfg.APPLI:
462         # use catalog specified in the product
463         if runner.cfg.APPLI.catalog.endswith(".xml"):
464             # catalog as a file
465             catalog = runner.cfg.APPLI.catalog
466         else:
467             # catalog as a list of computers
468             catalog_src = runner.cfg.APPLI.catalog
469             mlist = filter(lambda l: len(l.strip()) > 0,
470                            runner.cfg.APPLI.catalog.split(","))
471             if len(mlist) > 0:
472                 catalog = generate_catalog(runner.cfg.APPLI.catalog.split(","),
473                                            runner.cfg, logger)
474
475     # display which catalog is used
476     if len(catalog) > 0:
477         catalog = os.path.realpath(catalog)
478         if len(catalog_src) > 0:
479             src.printcolors.print_value(logger,
480                                         _("Resources Catalog"),
481                                         catalog_src,
482                                         3)
483         else:
484             src.printcolors.print_value(logger,
485                                         _("Resources Catalog"),
486                                         catalog,
487                                         3)
488
489     logger.write("\n", 3, False)
490
491     details = []
492
493     # remove previous application
494     if os.path.exists(appli_dir):
495         write_step(logger, _("Removing previous application directory"))
496         rres = src.KO_STATUS
497         try:
498             shutil.rmtree(appli_dir)
499             rres = src.OK_STATUS
500         finally:
501             logger.write(src.printcolors.printc(rres) + "\n", 3, False)
502
503     # generate the application
504     try:
505         try: # try/except/finally not supported in all version of python
506             retcode = create_application(runner.cfg, appli_dir, catalog, logger)
507         except Exception as exc:
508             details.append(str(exc))
509             raise
510     finally:
511         logger.write("\n", 3, False)
512
513     return retcode
514