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