Salome HOME
Merge remote-tracking branch 'origin/master' into V9_dev
[modules/kernel.git] / bin / salomeContext.py
1 # Copyright (C) 2013-2017  CEA/DEN, EDF R&D, OPEN CASCADE
2 #
3 # This library is free software; you can redistribute it and/or
4 # modify it under the terms of the GNU Lesser General Public
5 # License as published by the Free Software Foundation; either
6 # version 2.1 of the License, or (at your option) any later version.
7 #
8 # This library is distributed in the hope that it will be useful,
9 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
11 # Lesser General Public License for more details.
12 #
13 # You should have received a copy of the GNU Lesser General Public
14 # License along with this library; if not, write to the Free Software
15 # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307 USA
16 #
17 # See http://www.salome-platform.org/ or email : webmaster.salome@opencascade.com
18 #
19
20 import os
21 import sys
22 import logging
23 import configparser
24
25 from parseConfigFile import parseConfigFile
26
27 import tempfile
28 import pickle
29 import subprocess
30 import platform
31
32 from salomeContextUtils import SalomeContextException
33
34 def usage():
35   msg = '''\
36 Usage: salome [command] [options] [--config=<file,folder,...>]
37
38 Commands:
39 =========
40     start           Start a new SALOME instance.
41     context         Initialize SALOME context. Current environment is extended.
42     shell           Initialize SALOME context, attached to the last created SALOME
43                     instance if any, and executes scripts passed as command arguments.
44                     User works in a Shell terminal. SALOME environment is set but
45                     application is not started.
46     connect         Connect a Python console to the active SALOME instance.
47     kill <port(s)>  Terminate SALOME instances running on given ports for current user.
48                     Port numbers must be separated by blank characters.
49     killall         Terminate *all* SALOME running instances for current user.
50                     Do not start a new one.
51     test            Run SALOME tests.
52     info            Display some information about SALOME.
53     doc <module(s)> Show online module documentation (if available).
54                     Module names must be separated by blank characters.
55     help            Show this message.
56
57 If no command is given, default is start.
58
59 Command options:
60 ================
61     Use salome <command> --help to show help on command. Available for the
62     following commands: start, shell, connect, test, info.
63
64 --config=<file,folder,...>
65 ==========================
66     Initialize SALOME context from a list of context files and/or a list
67     of folders containing context files. The list is comma-separated, whithout
68     any blank characters.
69 '''
70
71   print(msg)
72 #
73
74 """
75 The SalomeContext class in an API to configure SALOME context then
76 start SALOME using a single python command.
77
78 """
79 class SalomeContext:
80   """
81   Initialize context from a list of configuration files
82   identified by their names.
83   These files should be in appropriate .cfg format.
84   """
85   def __init__(self, configFileNames=0):
86     self.getLogger().setLevel(logging.INFO)
87     #it could be None explicitely (if user use multiples setVariable...for standalone)
88     if configFileNames is None:
89        return
90     configFileNames = configFileNames or []
91     if len(configFileNames) == 0:
92       raise SalomeContextException("No configuration files given")
93
94     reserved=['PATH', 'DYLD_FALLBACK_LIBRARY_PATH', 'DYLD_LIBRARY_PATH', 'LD_LIBRARY_PATH', 'PYTHONPATH', 'MANPATH', 'PV_PLUGIN_PATH', 'INCLUDE', 'LIBPATH', 'SALOME_PLUGINS_PATH', 'LIBRARY_PATH', 'QT_PLUGIN_PATH']
95     for filename in configFileNames:
96       basename, extension = os.path.splitext(filename)
97       if extension == ".cfg":
98         self.__setContextFromConfigFile(filename, reserved)
99       else:
100         self.getLogger().error("Unrecognized extension for configuration file: %s", filename)
101   #
102
103   def __loadEnvModules(self, env_modules):
104     print("Trying to load env modules: %s..." % ' '.join(env_modules))
105     try:
106       out, err = subprocess.Popen(["modulecmd", "python", "load"] + env_modules, stdout=subprocess.PIPE).communicate()
107       exec(out) # define specific environment variables
108       print("OK")
109     except:
110       print("** Failed **")
111       pass
112   #
113
114   def runSalome(self, args):
115     import os
116     # Run this module as a script, in order to use appropriate Python interpreter
117     # according to current path (initialized from context files).
118     env_modules_option = "--with-env-modules="
119     env_modules_l = [x for x in args if x.startswith(env_modules_option)]
120     if env_modules_l:
121       env_modules = env_modules_l[-1][len(env_modules_option):].split(',')
122       self.__loadEnvModules(env_modules)
123       args = [x for x in args if not x.startswith(env_modules_option)]
124     else:
125       env_modules = os.getenv("SALOME_ENV_MODULES", None)
126       if env_modules:
127         self.__loadEnvModules(env_modules.split(','))
128
129     absoluteAppliPath = os.getenv('ABSOLUTE_APPLI_PATH','')
130     env_copy = os.environ.copy()
131     selfBytes= pickle.dumps(self, protocol=0)
132     argsBytes= pickle.dumps(args, protocol=0)
133     proc = subprocess.Popen(['python3', os.path.join(absoluteAppliPath,"bin","salome","salomeContext.py"), selfBytes.decode(), argsBytes.decode()], shell=False, close_fds=True, env=env_copy)
134     out, err = proc.communicate()
135     return out, err, proc.returncode
136   #
137
138   """Append value to PATH environment variable"""
139   def addToPath(self, value):
140     self.addToVariable('PATH', value)
141   #
142
143   """Append value to LD_LIBRARY_PATH environment variable"""
144   def addToLdLibraryPath(self, value):
145     if platform.system() == 'Windows':
146       self.addToVariable('PATH', value)
147     elif platform.system() == 'Darwin':
148       if "LAPACK" in value:
149         self.addToVariable('DYLD_FALLBACK_LIBRARY_PATH', value)
150       else:
151         self.addToVariable('DYLD_LIBRARY_PATH', value)
152     else:
153       self.addToVariable('LD_LIBRARY_PATH', value)
154   #
155
156   """Append value to DYLD_LIBRARY_PATH environment variable"""
157   def addToDyldLibraryPath(self, value):
158     self.addToVariable('DYLD_LIBRARY_PATH', value)
159   #
160
161   """Append value to PYTHONPATH environment variable"""
162   def addToPythonPath(self, value):
163     self.addToVariable('PYTHONPATH', value)
164   #
165
166   """Set environment variable to value"""
167   def setVariable(self, name, value, overwrite=False):
168     env = os.getenv(name, '')
169     if env and not overwrite:
170       self.getLogger().error("Environment variable already existing (and not overwritten): %s=%s", name, value)
171       return
172
173     if env:
174       self.getLogger().debug("Overwriting environment variable: %s=%s", name, value)
175
176     value = os.path.expandvars(value) # expand environment variables
177     self.getLogger().debug("Set environment variable: %s=%s", name, value)
178     os.environ[name] = value
179   #
180
181   """Unset environment variable"""
182   def unsetVariable(self, name):
183     if os.environ.has_key(name):
184       del os.environ[name]
185   #
186
187   """Append value to environment variable"""
188   def addToVariable(self, name, value, separator=os.pathsep):
189     if value == '':
190       return
191
192     value = os.path.expandvars(value) # expand environment variables
193     self.getLogger().debug("Add to %s: %s", name, value)
194     env = os.getenv(name, None)
195     if env is None:
196       os.environ[name] = value
197     else:
198       os.environ[name] = value + separator + env
199   #
200
201   ###################################
202   # This begins the private section #
203   ###################################
204
205   def __parseArguments(self, args):
206     if len(args) == 0 or args[0].startswith("-"):
207       return None, args
208
209     command = args[0]
210     options = args[1:]
211
212     availableCommands = {
213       'start'   : '_runAppli',
214       'context' : '_setContext',
215       'shell'   : '_runSession',
216       'connect' : '_runConsole',
217       'kill'    : '_kill',
218       'killall' : '_killAll',
219       'test'    : '_runTests',
220       'info'    : '_showInfo',
221       'doc'     : '_showDoc',
222       'help'    : '_usage',
223       'coffee'  : '_makeCoffee',
224       'car'     : '_getCar',
225       }
226
227     if command not in availableCommands:
228       command = "start"
229       options = args
230
231     return availableCommands[command], options
232   #
233
234   """
235   Run SALOME!
236   Args consist in a mandatory command followed by optionnal parameters.
237   See usage for details on commands.
238   """
239   def _startSalome(self, args):
240     import os
241     import sys
242     try:
243       from setenv import add_path
244       absoluteAppliPath = os.getenv('ABSOLUTE_APPLI_PATH')
245       path = os.path.realpath(os.path.join(absoluteAppliPath, "bin", "salome"))
246       add_path(path, "PYTHONPATH")
247       path = os.path.realpath(os.path.join(absoluteAppliPath, "bin", "salome", "appliskel"))
248       add_path(path, "PYTHONPATH")
249
250     except:
251       pass
252
253     command, options = self.__parseArguments(args)
254     sys.argv = options
255
256     if command is None:
257       if args and args[0] in ["-h","--help","help"]:
258         usage()
259         return 0
260       # try to default to "start" command
261       command = "_runAppli"
262
263     try:
264       res = getattr(self, command)(options) # run appropriate method
265       return res or 0
266     except SystemExit as ex:
267       if ex.code != 0:
268         self.getLogger().error("SystemExit %s in method %s.", ex.code, command)
269       return ex.code
270     except SalomeContextException as e:
271       self.getLogger().error(e)
272       return 1
273     except Exception:
274       self.getLogger().error("Unexpected error:")
275       import traceback
276       traceback.print_exc()
277       return 1
278   #
279
280   def __setContextFromConfigFile(self, filename, reserved=None):
281     if reserved is None:
282       reserved = []
283     try:
284       unsetVars, configVars, reservedDict = parseConfigFile(filename, reserved)
285     except SalomeContextException as e:
286       msg = "%s"%e
287       self.getLogger().error(msg)
288       return 1
289
290     # unset variables
291     for var in unsetVars:
292       self.unsetVariable(var)
293
294     # set context
295     for reserved in reservedDict:
296       a = [_f for _f in reservedDict[reserved] if _f] # remove empty elements
297       a = [ os.path.realpath(x) for x in a ]
298       reformattedVals = os.pathsep.join(a)
299       if reserved in ["INCLUDE", "LIBPATH"]:
300         self.addToVariable(reserved, reformattedVals, separator=' ')
301       else:
302         self.addToVariable(reserved, reformattedVals)
303       pass
304
305     for key,val in configVars:
306       self.setVariable(key, val, overwrite=True)
307       pass
308
309     pythonpath = os.getenv('PYTHONPATH','').split(os.pathsep)
310     pythonpath = [ os.path.realpath(x) for x in pythonpath ]
311     sys.path[:0] = pythonpath
312   #
313
314   def _runAppli(self, args=None):
315     if args is None:
316       args = []
317     # Initialize SALOME environment
318     sys.argv = ['runSalome'] + args
319     import setenv
320     setenv.main(True, exeName="salome start")
321
322     import runSalome
323     runSalome.runSalome()
324     return 0
325   #
326
327   def _setContext(self, args=None):
328     salome_context_set = os.getenv("SALOME_CONTEXT_SET")
329     if salome_context_set:
330       print("***")
331       print("*** SALOME context has already been set.")
332       print("*** Enter 'exit' (only once!) to leave SALOME context.")
333       print("***")
334       return 0
335
336     os.environ["SALOME_CONTEXT_SET"] = "yes"
337     print("***")
338     print("*** SALOME context is now set.")
339     print("*** Enter 'exit' (only once!) to leave SALOME context.")
340     print("***")
341
342     cmd = ["/bin/bash"]
343     proc = subprocess.Popen(cmd, shell=False, close_fds=True)
344     proc.communicate()
345     return proc.returncode()
346   #
347
348   def _runSession(self, args=None):
349     if args is None:
350       args = []
351     sys.argv = ['runSession'] + args
352     import runSession
353     params, args = runSession.configureSession(args, exe="salome shell")
354
355     sys.argv = ['runSession'] + args
356     import setenv
357     setenv.main(True)
358
359     return runSession.runSession(params, args)
360   #
361
362   def _runConsole(self, args=None):
363     if args is None:
364       args = []
365     # Initialize SALOME environment
366     sys.argv = ['runConsole']
367     import setenv
368     setenv.main(True)
369
370     import runConsole
371     return runConsole.connect(args)
372   #
373
374   def _kill(self, args=None):
375     if args is None:
376       args = []
377     ports = args
378     if not ports:
379       print("Port number(s) not provided to command: salome kill <port(s)>")
380       return 1
381
382     from multiprocessing import Process
383     from killSalomeWithPort import killMyPort
384     import tempfile
385     for port in ports:
386       with tempfile.NamedTemporaryFile():
387         p = Process(target = killMyPort, args=(port,))
388         p.start()
389         p.join()
390     return 0
391   #
392
393   def _killAll(self, unused=None):
394     try:
395       import PortManager # mandatory
396       from multiprocessing import Process
397       from killSalomeWithPort import killMyPort
398       ports = PortManager.getBusyPorts()['this']
399
400       if ports:
401         import tempfile
402         for port in ports:
403           with tempfile.NamedTemporaryFile():
404             p = Process(target = killMyPort, args=(port,))
405             p.start()
406             p.join()
407     except ImportError:
408       # :TODO: should be declared obsolete
409       from killSalome import killAllPorts
410       killAllPorts()
411       pass
412     return 0
413   #
414
415   def _runTests(self, args=None):
416     if args is None:
417       args = []
418     sys.argv = ['runTests']
419     import setenv
420     setenv.main(True)
421
422     import runTests
423     return runTests.runTests(args, exe="salome test")
424   #
425
426   def _showSoftwareVersions(self, softwares=None):
427     config = configparser.SafeConfigParser()
428     absoluteAppliPath = os.getenv('ABSOLUTE_APPLI_PATH')
429     filename = os.path.join(absoluteAppliPath, "sha1_collections.txt")
430     versions = {}
431     max_len = 0
432     with open(filename) as f:
433       for line in f:
434         try:
435           software, version, sha1 = line.split()
436           versions[software.upper()] = version
437           if len(software) > max_len:
438             max_len = len(software)
439         except:
440           pass
441         pass
442       pass
443     if softwares:
444       for soft in softwares:
445         if versions.has_key(soft.upper()):
446           print(soft.upper().rjust(max_len), versions[soft.upper()])
447     else:
448       import collections
449       od = collections.OrderedDict(sorted(versions.items()))
450       for name, version in od.iteritems():
451         print(name.rjust(max_len), versions[name])
452     pass
453
454   def _showInfo(self, args=None):
455     if args is None:
456       args = []
457
458     usage = "Usage: salome info [options]"
459     epilog  = """\n
460 Display some information about SALOME.\n
461 Available options are:
462     -p,--ports                     Show the list of busy ports (running SALOME instances).
463     -s,--softwares [software(s)]   Show the list and versions of SALOME softwares.
464                                    Software names must be separated by blank characters.
465                                    If no software is given, show version of all softwares.
466     -v,--version                   Show running SALOME version.
467     -h,--help                      Show this message.
468 """
469     if not args:
470       args = ["--version"]
471
472     if "-h" in args or "--help" in args:
473       print(usage + epilog)
474       return 0
475
476     if "-p" in args or "--ports" in args:
477       import PortManager
478       ports = PortManager.getBusyPorts()
479       this_ports = ports['this']
480       other_ports = ports['other']
481       if this_ports or other_ports:
482           print("SALOME instances are running on the following ports:")
483           if this_ports:
484               print("   This application:", this_ports)
485           else:
486               print("   No SALOME instances of this application")
487           if other_ports:
488               print("   Other applications:", other_ports)
489           else:
490               print("   No SALOME instances of other applications")
491       else:
492           print("No SALOME instances are running")
493
494     if "-s" in args or "--softwares" in args:
495       if "-s" in args:
496         index = args.index("-s")
497       else:
498         index = args.index("--softwares")
499       indexEnd=index+1
500       while indexEnd < len(args) and args[indexEnd][0] != "-":
501         indexEnd = indexEnd + 1
502       self._showSoftwareVersions(softwares=args[index+1:indexEnd])
503
504     if "-v" in args or "--version" in args:
505       print("Running with python", platform.python_version())
506       return self._runAppli(["--version"])
507
508     return 0
509   #
510
511   def _showDoc(self, args=None):
512     if args is None:
513       args = []
514
515     modules = args
516     if not modules:
517       print("Module(s) not provided to command: salome doc <module(s)>")
518       return 1
519
520     appliPath = os.getenv("ABSOLUTE_APPLI_PATH")
521     if not appliPath:
522       raise SalomeContextException("Unable to find application path. Please check that the variable ABSOLUTE_APPLI_PATH is set.")
523     baseDir = os.path.join(appliPath, "share", "doc", "salome")
524     for module in modules:
525       docfile = os.path.join(baseDir, "gui", module.upper(), "index.html")
526       if not os.path.isfile(docfile):
527         docfile = os.path.join(baseDir, "tui", module.upper(), "index.html")
528       if not os.path.isfile(docfile):
529         docfile = os.path.join(baseDir, "dev", module.upper(), "index.html")
530       if os.path.isfile(docfile):
531         out, err = subprocess.Popen(["xdg-open", docfile]).communicate()
532       else:
533         print("Online documentation is not accessible for module:", module)
534
535   def _usage(self, unused=None):
536     usage()
537   #
538
539   def _makeCoffee(self, unused=None):
540     print("                        (")
541     print("                          )     (")
542     print("                   ___...(-------)-....___")
543     print("               .-\"\"       )    (          \"\"-.")
544     print("         .-\'``\'|-._             )         _.-|")
545     print("        /  .--.|   `\"\"---...........---\"\"`   |")
546     print("       /  /    |                             |")
547     print("       |  |    |                             |")
548     print("        \\  \\   |                             |")
549     print("         `\\ `\\ |                             |")
550     print("           `\\ `|            SALOME           |")
551     print("           _/ /\\            4 EVER           /")
552     print("          (__/  \\             <3            /")
553     print("       _..---\"\"` \\                         /`\"\"---.._")
554     print("    .-\'           \\                       /          \'-.")
555     print("   :               `-.__             __.-\'              :")
556     print("   :                  ) \"\"---...---\"\" (                 :")
557     print("    \'._               `\"--...___...--\"`              _.\'")
558     print("      \\\"\"--..__                              __..--\"\"/")
559     print("       \'._     \"\"\"----.....______.....----\"\"\"     _.\'")
560     print("          `\"\"--..,,_____            _____,,..--\"\"`")
561     print("                        `\"\"\"----\"\"\"`")
562     print("")
563     print("                    SALOME is working for you; what else?")
564     print("")
565   #
566
567   def _getCar(self, unused=None):
568     print("                                              _____________")
569     print("                                  ..---:::::::-----------. ::::;;.")
570     print("                               .\'\"\"\"\"\"\"                  ;;   \\  \":.")
571     print("                            .\'\'                          ;     \\   \"\\__.")
572     print("                          .\'                            ;;      ;   \\\\\";")
573     print("                        .\'                              ;   _____;   \\\\/")
574     print("                      .\'                               :; ;\"     \\ ___:\'.")
575     print("                    .\'--...........................    : =   ____:\"    \\ \\")
576     print("               ..-\"\"                               \"\"\"\'  o\"\"\"     ;     ; :")
577     print("          .--\"\"  .----- ..----...    _.-    --.  ..-\"     ;       ;     ; ;")
578     print("       .\"\"_-     \"--\"\"-----\'\"\"    _-\"        .-\"\"         ;        ;    .-.")
579     print("    .\'  .\'   SALOME             .\"         .\"              ;       ;   /. |")
580     print("   /-./\'         4 EVER <3    .\"          /           _..  ;       ;   ;;;|")
581     print("  :  ;-.______               /       _________==.    /_  \\ ;       ;   ;;;;")
582     print("  ;  / |      \"\"\"\"\"\"\"\"\"\"\".---.\"\"\"\"\"\"\"          :    /\" \". |;       ; _; ;;;")
583     print(" /\"-/  |                /   /                  /   /     ;|;      ;-\" | ;\';")
584     print(":-  :   \"\"\"----______  /   /              ____.   .  .\"\'. ;;   .-\"..T\"   .")
585     print("\'. \"  ___            \"\":   \'\"\"\"\"\"\"\"\"\"\"\"\"\"\"    .   ; ;    ;; ;.\" .\"   \'--\"")
586     print(" \",   __ \"\"\"  \"\"---... :- - - - - - - - - \' \'  ; ;  ;    ;;\"  .\"")
587     print("  /. ;  \"\"\"---___                             ;  ; ;     ;|.\"\"")
588     print(" :  \":           \"\"\"----.    .-------.       ;   ; ;     ;:")
589     print("  \\  \'--__               \\   \\        \\     /    | ;     ;;")
590     print("   \'-..   \"\"\"\"---___      :   .______..\\ __/..-\"\"|  ;   ; ;")
591     print("       \"\"--..       \"\"\"--\"        m l s         .   \". . ;")
592     print("             \"\"------...                  ..--\"\"      \" :")
593     print("                        \"\"\"\"\"\"\"\"\"\"\"\"\"\"\"\"\"\"    \\        /")
594     print("                                               \"------\"")
595     print("")
596     print("                                Drive your simulation properly with SALOME!")
597     print("")
598   #
599
600   # Add the following two methods since logger is not pickable
601   # Ref: http://stackoverflow.com/questions/2999638/how-to-stop-attributes-from-being-pickled-in-python
602   def __getstate__(self):
603     d = dict(self.__dict__)
604     if hasattr(self, '_logger'):
605       del d['_logger']
606     return d
607   #
608   def __setstate__(self, d):
609     self.__dict__.update(d) # I *think* this is a safe way to do it
610   #
611   # Excluding self._logger from pickle operation imply using the following method to access logger
612   def getLogger(self):
613     if not hasattr(self, '_logger'):
614       self._logger = logging.getLogger(__name__)
615       #self._logger.setLevel(logging.DEBUG)
616       #self._logger.setLevel(logging.WARNING)
617       self._logger.setLevel(logging.ERROR)
618     return self._logger
619   #
620
621 if __name__ == "__main__":
622   if len(sys.argv) == 3:
623     context = pickle.loads(sys.argv[1].encode())
624     args = pickle.loads(sys.argv[2].encode())
625
626     status = context._startSalome(args)
627     sys.exit(status)
628   else:
629     usage()
630 #