1 #! /usr/bin/env python3
2 # Copyright (C) 2013-2024 CEA, EDF, OPEN CASCADE
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.
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.
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
18 # See http://www.salome-platform.org/ or email : webmaster.salome@opencascade.com
26 from parseConfigFile import parseConfigFile
34 from salomeContextUtils import SalomeContextException
36 def usage(appended_cmd_doc = "", appended_opt_doc = ""):
37 add_in_help = {"appended_cmd_doc":appended_cmd_doc,"appended_opt_doc":appended_opt_doc}
39 Usage: salome [command] [options] [--config=<file,folder,...>] [--with-env-modules=<env_module1,env_module2,...>]
43 start Start a new SALOME instance. Start a single SALOME_Session_Server_No_Server
44 process with environment relevant to the application and hosting all servants in it.
45 context Initialize SALOME context. Current environment is extended.
46 shell Initialize SALOME context, attached to the last created SALOME
47 instance if any, and executes scripts passed as command arguments.
48 User works in a Shell terminal. SALOME environment is set but
49 application is not started.
50 test Run SALOME tests.
51 info Display some information about SALOME.
52 doc <module(s)> Show online module documentation (if available).
53 Module names must be separated by blank characters.
54 help Show this message.
55 remote run command in SALOME environment from remote call, ssh or rsh.
56 withsession Start a new SWS SALOME instance with multiple servers hosting all servants.
57 connect In SWS context, Connect a Python console to the active SALOME instance.
58 kill <port(s)> In SWS context, Terminate SALOME instances running on given ports for current user.
59 Port numbers must be separated by blank characters.
60 killall Terminate *all* SALOME running SWS instances for current user.
61 Do not start a new one.
63 If no command is given, default is start.
67 Use salome <command> --help to show help on command. Available for the
68 following commands: start, shell, connect, test, info.
70 --config=<file,folder,...>
71 ==========================
72 Initialize SALOME context from a list of context files and/or a list
73 of folders containing context files. The list is comma-separated, without
76 --with-env-modules=<env_module1,env_module2,...>
77 ================================================
78 Initialize SALOME context with the provided additional environment modules.
79 The list is comma-separated, without any blank characters.
83 print(msg%add_in_help)
87 The SalomeContext class in an API to configure SALOME context then
88 start SALOME using a single python command.
93 Initialize context from a list of configuration files
94 identified by their names.
95 These files should be in appropriate .cfg format.
97 def __init__(self, configFileNames=0):
98 self.getLogger().setLevel(logging.INFO)
99 #it could be None explicitly (if user use multiples setVariable...for standalone)
100 if configFileNames is None:
102 configFileNames = configFileNames or []
103 if len(configFileNames) == 0:
104 raise SalomeContextException("No configuration files given")
106 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']
107 for filename in configFileNames:
108 basename, extension = os.path.splitext(filename)
109 if extension == ".cfg":
110 self.__setContextFromConfigFile(filename, reserved)
112 self.getLogger().error("Unrecognized extension for configuration file: %s", filename)
115 def __loadEnvModules(self, env_modules):
116 modulecmd = os.getenv('LMOD_CMD')
118 raise SalomeContextException("Module environment not present")
121 out, err = subprocess.Popen([modulecmd, "python", "try-load"] + env_modules, stdout=subprocess.PIPE).communicate()
122 exec(out) # define specific environment variables
124 raise SalomeContextException("Failed to load env modules: %s ..." % ' '.join(env_modules))
128 def runSalome(self, args):
130 # Run this module as a script, in order to use appropriate Python interpreter
131 # according to current path (initialized from context files).
132 env_modules_option = "--with-env-modules="
133 env_modules_l = [x for x in args if x.startswith(env_modules_option)]
135 env_modules = env_modules_l[-1][len(env_modules_option):].split(',')
136 self.__loadEnvModules(env_modules)
137 args = [x for x in args if not x.startswith(env_modules_option)]
139 env_modules = os.getenv("SALOME_ENV_MODULES", None)
141 self.__loadEnvModules(env_modules.split(','))
143 absoluteAppliPath = os.getenv('ABSOLUTE_APPLI_PATH','')
144 env_copy = os.environ.copy()
145 selfBytes= pickle.dumps(self, protocol=0)
146 argsBytes= pickle.dumps(args, protocol=0)
147 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)
148 out, err = proc.communicate()
149 return out, err, proc.returncode
152 """Append value to PATH environment variable"""
153 def addToPath(self, value):
154 self.addToVariable('PATH', value)
157 """Append value to LD_LIBRARY_PATH environment variable"""
158 def addToLdLibraryPath(self, value):
159 if sys.platform == 'win32':
160 self.addToVariable('PATH', value)
161 elif sys.platform == 'darwin':
162 if "LAPACK" in value:
163 self.addToVariable('DYLD_FALLBACK_LIBRARY_PATH', value)
165 self.addToVariable('DYLD_LIBRARY_PATH', value)
167 self.addToVariable('LD_LIBRARY_PATH', value)
170 """Append value to DYLD_LIBRARY_PATH environment variable"""
171 def addToDyldLibraryPath(self, value):
172 self.addToVariable('DYLD_LIBRARY_PATH', value)
175 """Append value to PYTHONPATH environment variable"""
176 def addToPythonPath(self, value):
177 self.addToVariable('PYTHONPATH', value)
180 """Set environment variable to value"""
181 def setVariable(self, name, value, overwrite=False):
182 env = os.getenv(name, '')
183 if env and not overwrite:
184 self.getLogger().error("Environment variable already existing (and not overwritten): %s=%s", name, value)
188 self.getLogger().debug("Overwriting environment variable: %s=%s", name, value)
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
195 def setDefaultValue(self, name, value):
196 """ Set environment variable only if it is undefined."""
197 env = os.getenv(name, '')
199 value = os.path.expandvars(value) # expand environment variables
200 self.getLogger().debug("Set environment variable: %s=%s", name, value)
201 os.environ[name] = value
203 """Unset environment variable"""
204 def unsetVariable(self, name):
205 if name in os.environ:
209 """Prepend value to environment variable"""
210 def addToVariable(self, name, value, separator=os.pathsep):
214 value = os.path.expandvars(value) # expand environment variables
215 self.getLogger().debug("Add to %s: %s", name, value)
216 env = os.getenv(name, None)
218 os.environ[name] = value
220 os.environ[name] = value + separator + env
223 """Append a variable"""
224 def appendVariable(self, name, value, separator=os.pathsep):
228 value = os.path.expandvars(value) # expand environment variables
229 env = os.getenv(name, None)
231 os.environ[name] = value
233 os.environ[name] = env + separator + value
236 """Remove value from environment variable"""
237 def removeFromVariable(self, name, value, separator=os.pathsep):
241 value = os.path.expandvars(value) # expand environment variables
242 self.getLogger().debug("Remove from %s: %s", name, value)
243 env = os.getenv(name, None)
247 # env = env.removeprefix(value + separator) (Python >= 3.9)
248 str = value + separator
249 if env.startswith(str):
251 # env = env.removesuffix(separator + value) (Python >= 3.9)
252 str = separator + value
253 if env.endswith(str):
254 env = env[:-len(str)]
255 env = env.replace(separator + value + separator, ':')
257 os.environ[name] = env
260 ###################################
261 # This begins the private section #
262 ###################################
264 def __parseArguments(self, args):
265 if len(args) == 0 or args[0].startswith("-"):
271 availableCommands = {
272 'start' : '_sessionless',
273 'withsession' : '_runAppli',
274 'context' : '_setContext',
275 'shell' : '_runSession',
276 'remote' : '_runRemote',
277 'connect' : '_runConsole',
279 'killall' : '_killAll',
280 'test' : '_runTests',
281 'info' : '_showInfo',
284 'coffee' : '_makeCoffee',
288 if command not in availableCommands:
292 return availableCommands[command], options
297 Args consist in a mandatory command followed by optional parameters.
298 See usage for details on commands.
300 def _startSalome(self, args):
304 from setenv import add_path
305 absoluteAppliPath = os.getenv('ABSOLUTE_APPLI_PATH')
306 path = os.path.realpath(os.path.join(absoluteAppliPath, "bin", "salome"))
307 add_path(path, "PYTHONPATH")
308 path = os.path.realpath(os.path.join(absoluteAppliPath, "bin", "salome", "appliskel"))
309 add_path(path, "PYTHONPATH")
314 command, options = self.__parseArguments(args)
318 if args and args[0] in ["-h","--help","help"]:
321 # try to default to "start" command
322 command = "_sessionless"
325 res = getattr(self, command)(options) # run appropriate method
327 except SystemExit as ex:
329 self.getLogger().error("SystemExit %s in method %s.", ex.code, command)
331 except SalomeContextException as e:
332 self.getLogger().error(e)
335 self.getLogger().error("Unexpected error:")
337 traceback.print_exc()
341 def __setContextFromConfigFile(self, filename, reserved=None):
342 mesa_root_dir = "MESA_ROOT_DIR"
346 configInfo = parseConfigFile(filename, reserved)
347 unsetVars = configInfo.unsetVariables
348 configVars = configInfo.outputVariables
349 reservedDict = configInfo.reservedValues
350 defaultValues = configInfo.defaultValues
351 except SalomeContextException as e:
353 self.getLogger().error(msg)
357 for var in unsetVars:
358 self.unsetVariable(var)
361 if "MESA_GL_VERSION_OVERRIDE" in os.environ:
362 configVarsDict = {k:v for (k,v) in configVars}
363 if mesa_root_dir in configVarsDict:
364 path_to_mesa_lib = os.path.join(configVarsDict[mesa_root_dir],"lib")
365 if os.name == "posix":
366 self.addToVariable("LD_LIBRARY_PATH",path_to_mesa_lib)
368 self.addToVariable("PATH",path_to_mesa_lib)
371 for reserved in reservedDict:
372 a = [_f for _f in reservedDict[reserved] if _f] # remove empty elements
373 a = [ os.path.realpath(x) for x in a ]
374 reformattedVals = os.pathsep.join(a)
375 if reserved in ["INCLUDE", "LIBPATH"]:
376 self.addToVariable(reserved, reformattedVals, separator=' ')
378 self.addToVariable(reserved, reformattedVals)
382 for key,val in configVars:
383 self.setVariable(key, val, overwrite=True)
386 for key,val in defaultValues:
387 self.setDefaultValue(key, val)
390 pythonpath = os.getenv('PYTHONPATH','').split(os.pathsep)
391 pythonpath = [ os.path.realpath(x) for x in pythonpath ]
392 sys.path[:0] = pythonpath
395 def _runAppli(self, args=None):
398 # Initialize SALOME environment
399 sys.argv = ['runSalomeOld'] + args
401 setenv.main(True, exeName="salome withsession")
404 runSalomeOld.runSalome()
408 def _sessionless(self, args=None):
411 sys.argv = ['runSalome'] + args
413 setenv.main(True, exeName="salome withsession")
416 runSalome.runSalome()
420 def _setContext(self, args=None):
421 salome_context_set = os.getenv("SALOME_CONTEXT_SET")
422 if salome_context_set:
424 print("*** SALOME context has already been set.")
425 print("*** Enter 'exit' (only once!) to leave SALOME context.")
429 os.environ["SALOME_CONTEXT_SET"] = "yes"
431 print("*** SALOME context is now set.")
432 print("*** Enter 'exit' (only once!) to leave SALOME context.")
435 if sys.platform == 'win32':
439 proc = subprocess.Popen(cmd, shell=False, close_fds=True)
441 return proc.returncode
444 def _runSession(self, args=None):
447 sys.argv = ['runSession'] + args
449 params, args = runSession.configureSession(args, exe="salome shell")
451 sys.argv = ['runSession'] + args
455 return runSession.runSession(params, args)
458 def _runRemote(self, args=None):
461 # complete salome environment
462 sys.argv = ['runRemote']
467 return runRemote.runRemote(args)
470 def _runConsole(self, args=None):
473 # Initialize SALOME environment
474 sys.argv = ['runConsole']
479 return runConsole.connect(args)
482 def _kill(self, args=None):
487 print("Port number(s) not provided to command: salome kill <port(s)>")
494 if os.getenv("NSHOST") == "no_host":
495 os.unsetenv("NSHOST")
497 if sys.platform == "win32":
498 proc = subprocess.Popen([os.getenv("PYTHONBIN"), "-m", "killSalomeWithPort", str(port)])
500 proc = subprocess.Popen(["killSalomeWithPort.py", str(port)])
505 def _killAll(self, unused=None):
506 sys.argv = ['killAll']
509 if os.getenv("NSHOST") == "no_host":
510 os.unsetenv("NSHOST")
512 import PortManager # mandatory
514 ports = PortManager.getBusyPorts()['this']
518 if sys.platform == "win32":
519 proc = subprocess.Popen([os.getenv("PYTHONBIN"), "-m", "killSalomeWithPort", str(port)])
521 proc = subprocess.Popen(["killSalomeWithPort.py", str(port)])
524 # :TODO: should be declared obsolete
525 from killSalome import killAllPorts
528 from addToKillList import killList
533 def _runTests(self, args=None):
536 sys.argv = ['runTests']
541 return runTests.runTests(args, exe="salome test")
544 def _showSoftwareVersions(self, softwares=None):
545 config = configparser.SafeConfigParser()
546 absoluteAppliPath = os.getenv('ABSOLUTE_APPLI_PATH')
547 filename = os.path.join(absoluteAppliPath, "sha1_collections.txt")
550 with open(filename) as f:
553 software, version, sha1 = line.split()
554 versions[software.upper()] = version
555 if len(software) > max_len:
556 max_len = len(software)
562 for soft in softwares:
563 if soft.upper() in versions:
564 print(soft.upper().rjust(max_len), versions[soft.upper()])
567 od = collections.OrderedDict(sorted(versions.items()))
568 for name, version in od.items():
569 print(name.rjust(max_len), versions[name])
572 def _showInfo(self, args=None):
576 usage = "Usage: salome info [options]"
578 Display some information about SALOME.\n
579 Available options are:
580 -p,--ports Show the list of busy ports (running SALOME instances).
581 -s,--softwares [software(s)] Show the list and versions of SALOME softwares.
582 Software names must be separated by blank characters.
583 If no software is given, show version of all softwares.
584 -v,--version Show running SALOME version.
585 -h,--help Show this message.
590 if "-h" in args or "--help" in args:
591 print(usage + epilog)
594 if "-p" in args or "--ports" in args:
596 ports = PortManager.getBusyPorts()
597 this_ports = ports['this']
598 other_ports = ports['other']
599 if this_ports or other_ports:
600 print("SALOME instances are running on the following ports:")
602 print(" This application:", this_ports)
604 print(" No SALOME instances of this application")
606 print(" Other applications:", other_ports)
608 print(" No SALOME instances of other applications")
610 print("No SALOME instances are running")
612 if "-s" in args or "--softwares" in args:
614 index = args.index("-s")
616 index = args.index("--softwares")
618 while indexEnd < len(args) and args[indexEnd][0] != "-":
619 indexEnd = indexEnd + 1
620 self._showSoftwareVersions(softwares=args[index+1:indexEnd])
622 if "-v" in args or "--version" in args:
623 print("Running with python", platform.python_version())
624 return self._sessionless(["--version"])
629 def _showDoc(self, args=None):
635 print("Module(s) not provided to command: salome doc <module(s)>")
638 appliPath = os.getenv("ABSOLUTE_APPLI_PATH")
640 raise SalomeContextException("Unable to find application path. Please check that the variable ABSOLUTE_APPLI_PATH is set.")
641 baseDir = os.path.join(appliPath, "share", "doc", "salome")
642 for module in modules:
643 docfile = os.path.join(baseDir, "gui", module.upper(), "index.html")
644 if not os.path.isfile(docfile):
645 docfile = os.path.join(baseDir, "tui", module.upper(), "index.html")
646 if not os.path.isfile(docfile):
647 docfile = os.path.join(baseDir, "dev", module.upper(), "index.html")
648 if os.path.isfile(docfile):
649 out, err = subprocess.Popen(["xdg-open", docfile]).communicate()
651 print("Online documentation is not accessible for module:", module)
653 def _usage(self, unused=None):
657 def _makeCoffee(self, unused=None):
660 print(" ___...(-------)-....___")
661 print(" .-\"\" ) ( \"\"-.")
662 print(" .-\'``\'|-._ ) _.-|")
663 print(" / .--.| `\"\"---...........---\"\"` |")
667 print(" `\\ `\\ | |")
668 print(" `\\ `| SALOME |")
669 print(" _/ /\\ 4 EVER /")
670 print(" (__/ \\ <3 /")
671 print(" _..---\"\"` \\ /`\"\"---.._")
672 print(" .-\' \\ / \'-.")
673 print(" : `-.__ __.-\' :")
674 print(" : ) \"\"---...---\"\" ( :")
675 print(" \'._ `\"--...___...--\"` _.\'")
676 print(" \\\"\"--..__ __..--\"\"/")
677 print(" \'._ \"\"\"----.....______.....----\"\"\" _.\'")
678 print(" `\"\"--..,,_____ _____,,..--\"\"`")
679 print(" `\"\"\"----\"\"\"`")
681 print(" SALOME is working for you; what else?")
685 def _getCar(self, unused=None):
686 print(" _____________")
687 print(" ..---:::::::-----------. ::::;;.")
688 print(" .\'\"\"\"\"\"\" ;; \\ \":.")
689 print(" .\'\' ; \\ \"\\__.")
690 print(" .\' ;; ; \\\\\";")
691 print(" .\' ; _____; \\\\/")
692 print(" .\' :; ;\" \\ ___:\'.")
693 print(" .\'--........................... : = ____:\" \\ \\")
694 print(" ..-\"\" \"\"\"\' o\"\"\" ; ; :")
695 print(" .--\"\" .----- ..----... _.- --. ..-\" ; ; ; ;")
696 print(" .\"\"_- \"--\"\"-----\'\"\" _-\" .-\"\" ; ; .-.")
697 print(" .\' .\' SALOME .\" .\" ; ; /. |")
698 print(" /-./\' 4 EVER <3 .\" / _.. ; ; ;;;|")
699 print(" : ;-.______ / _________==. /_ \\ ; ; ;;;;")
700 print(" ; / | \"\"\"\"\"\"\"\"\"\"\".---.\"\"\"\"\"\"\" : /\" \". |; ; _; ;;;")
701 print(" /\"-/ | / / / / ;|; ;-\" | ;\';")
702 print(":- : \"\"\"----______ / / ____. . .\"\'. ;; .-\"..T\" .")
703 print("\'. \" ___ \"\": \'\"\"\"\"\"\"\"\"\"\"\"\"\"\" . ; ; ;; ;.\" .\" \'--\"")
704 print(" \", __ \"\"\" \"\"---... :- - - - - - - - - \' \' ; ; ; ;;\" .\"")
705 print(" /. ; \"\"\"---___ ; ; ; ;|.\"\"")
706 print(" : \": \"\"\"----. .-------. ; ; ; ;:")
707 print(" \\ \'--__ \\ \\ \\ / | ; ;;")
708 print(" \'-.. \"\"\"\"---___ : .______..\\ __/..-\"\"| ; ; ;")
709 print(" \"\"--.. \"\"\"--\" m l s . \". . ;")
710 print(" \"\"------... ..--\"\" \" :")
711 print(" \"\"\"\"\"\"\"\"\"\"\"\"\"\"\"\"\"\" \\ /")
714 print(" Drive your simulation properly with SALOME!")
718 # Add the following two methods since logger is not pickable
719 # Ref: http://stackoverflow.com/questions/2999638/how-to-stop-attributes-from-being-pickled-in-python
720 def __getstate__(self):
721 d = dict(self.__dict__)
722 if hasattr(self, '_logger'):
726 def __setstate__(self, d):
727 self.__dict__.update(d) # I *think* this is a safe way to do it
729 # Excluding self._logger from pickle operation imply using the following method to access logger
731 if not hasattr(self, '_logger'):
732 self._logger = logging.getLogger(__name__)
733 #self._logger.setLevel(logging.DEBUG)
734 #self._logger.setLevel(logging.WARNING)
735 self._logger.setLevel(logging.ERROR)
739 if __name__ == "__main__":
740 if len(sys.argv) == 3:
741 context = pickle.loads(sys.argv[1].encode('latin1'))
742 args = pickle.loads(sys.argv[2].encode('latin1'))
744 status = context._startSalome(args)