Salome HOME
Fix command executed in a remote shell
[modules/kernel.git] / bin / runSession.py
1 #  -*- coding: iso-8859-1 -*-
2 # Copyright (C) 2007-2019  CEA/DEN, EDF R&D, OPEN CASCADE
3 #
4 # Copyright (C) 2003-2007  OPEN CASCADE, EADS/CCR, LIP6, CEA/DEN,
5 # CEDRAT, EDF R&D, LEG, PRINCIPIA R&D, BUREAU VERITAS
6 #
7 # This library is free software; you can redistribute it and/or
8 # modify it under the terms of the GNU Lesser General Public
9 # License as published by the Free Software Foundation; either
10 # version 2.1 of the License, or (at your option) any later version.
11 #
12 # This library is distributed in the hope that it will be useful,
13 # but WITHOUT ANY WARRANTY; without even the implied warranty of
14 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
15 # Lesser General Public License for more details.
16 #
17 # You should have received a copy of the GNU Lesser General Public
18 # License along with this library; if not, write to the Free Software
19 # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307 USA
20 #
21 # See http://www.salome-platform.org/ or email : webmaster.salome@opencascade.com
22 #
23
24 import os
25 import sys
26 from optparse import OptionParser
27 from NSparam import getNSparams
28 import socket
29 import subprocess
30 import re
31 from salomeContextUtils import getScriptsAndArgs, formatScriptsAndArgs, getShortAndExtraArgs
32 from salome_utils import getUserName, getShortHostName
33
34 # Use to display newlines (\n) in epilog
35 class MyParser(OptionParser):
36     def format_epilog(self, formatter):
37         return self.epilog
38 #
39
40 class SessionParameters:
41   def __init__(self, mode, port, machine, user, directory):
42     self.mode = mode
43     self.port = port
44     self.machine = machine
45     self.user = user
46     self.directory = directory
47   #
48   def __repr__(self):
49     msg = "\n# Session Parameters:\n"
50     msg += "     * mode: %s\n"%self.mode
51     msg += "     * port: %s\n"%self.port
52     msg += "     * machine: %s\n"%self.machine
53     msg += "     * user: %s\n"%self.user
54     msg += "     * directory: %s\n"%self.directory
55     return msg
56   #
57 #
58
59 def configureSession(args=None, exe=None):
60   if args is None:
61     args = []
62   if exe:
63       usage = "Usage: %s [options] [command] [-- <extra>]"%exe
64   else:
65       usage = "Usage: %prog [options] [command] [-- <extra>]"
66   epilog  = """\n
67 If command is not given a shell is opened; else execute the given command.\n
68 * Command may be an executable script or program, either identified by its
69   full path or located in a directory pointed by a system variable (e.g.
70   PATH).\n
71 * Command may also be a series of Python scripts with arguments:
72   [PYTHON_FILE [args] [PYTHON_FILE [args]...]]
73 Python file arguments, if any, must be comma-separated (without blank
74   characters) and prefixed by "args:" keyword (without quotes).
75 For example:
76        salome shell hello.py add.py args:1,2 hello.py args:you
77 will successively say hello, add 1+2, and say hello to you.\n
78 The double dash (--) syntax indicates an extra command to be run "as is". It
79   allows calling a extern program or system command with options and
80   arguments, using the syntax: -- <program> [options] [arguments].
81 For example:
82        salome shell -- ls -l *.py
83        salome shell -- python -tt hello.py
84 \n
85 If PORT and MACHINE are not given, try to connect to the last active session
86   on the local machine.
87 If PORT and MACHINE are given, try to connect to the remote session associated
88   with PORT on MACHINE.
89 If MACHINE is not given, try to connect to the session associated to PORT on
90   the local machine.
91 If PORT is not given, try to connect to the remote session associated to port
92   2810 on MACHINE.\n
93 If MACHINE is remote, the following options MUST be provided:
94      * DIRECTORY: The full path to the salome command on remote machine.
95      * USER: The user on the computer to connect to.\n
96 In case of remote call, syntax "out:res1,res2,..." can be used to get results
97   from remote machine.
98 For example:
99        salome shell -m remotemachine -p 2810 -u myself -d /full/path/to/salome
100   concatenate.py args:file1.txt,file2.txt out:result.txt
101 User "myself" connects to remotemachine to run the script concatenate.py in
102   a SALOME session on port 2810; the script takes two input parameters and
103   produces one result file.\n
104 """
105   parser = MyParser(usage=usage, epilog=epilog)
106   parser.add_option("-p", "--port", metavar="<port>", default=0,
107                     action="store", type="string", dest="port",
108                     help="The port to connect to."
109                     )
110   parser.add_option("-m", "--machine", metavar="<machine>", default=0,
111                     action="store", type="string", dest="host",
112                     help="The machine to connect to."
113                     )
114   parser.add_option('-d', '--directory', dest="directory", default=None,
115                     help="[Remote mode] The full path to the salome command on remote machine."
116                     )
117   parser.add_option('-u', '--user', dest="user", default=None,
118                     help="[Remote mode] The user on the computer to connect to."
119                     )
120
121   parser.add_option('-l', '--launcher', dest="launcher", default=None,
122                     help="[Remote mode] The machine and the port to connect to."
123                     )
124
125   short_args, extra_args = getShortAndExtraArgs(args)
126   try:
127     (options, args) = parser.parse_args(short_args)
128   except Exception as e:
129     print(e)
130     return None, []
131
132   port = options.port
133   host = options.host
134   launcher = options.launcher
135   if launcher is not None:
136     pos = launcher.find(":")
137     if pos != -1:
138       host = launcher[0:pos]
139       port = launcher[pos+1:]
140
141   # :GLITCH: this code defines specific environment variables (OMNIORB_CONFIG, NSPORT,
142   # NSHOST) which are later used by other modules. Working, but not really "safe"...
143   if not port:
144     if not host:
145       # neither MACHINE nor PORT are given
146       # --- set omniORB configuration to current session if any
147       omniorbUserPath = os.environ['OMNIORB_USER_PATH']
148       fileOmniConfig = omniorbUserPath + '/.omniORB_' + getUserName() + '_last.cfg'
149       if os.path.isfile(fileOmniConfig):
150         os.environ['OMNIORB_CONFIG'] = fileOmniConfig
151         # --- set environment variables for port and hostname of NamingService
152         host, port = getNSparams()
153       else:
154         # No running session
155         host = "no_host"
156         port = "no_port"
157     else:
158       # only MACHINE is given
159       port = '2810'
160       _writeConfigFile(port, host)
161     #
162   else:
163     if not host:
164       # only PORT is given
165       host = getShortHostName()
166     # both MACHINE and PORT are given
167     _writeConfigFile(port, host)
168   #
169   os.environ['NSPORT'] = port
170   os.environ['NSHOST'] = host
171
172   # determine running mode, that is either 'local' or 'remote'
173   here = getShortHostName()
174   mode = "local"
175   if host != here and host != "localhost" and host != "no_host" and launcher is None:
176     mode="remote"
177     pass
178   params = SessionParameters(mode, port, host, options.user, options.directory)
179   return params, args+extra_args
180 #
181
182 # --- set the OMNIORB_CONFIG file and environment relative to this run of SALOME
183 def _writeConfigFile(port, host):
184   path = os.environ['OMNIORB_USER_PATH']
185   kwargs = {'with_username' : getUserName()}
186
187   from ORBConfigFile import writeORBConfigFile
188   [ filename, msgSize ] = writeORBConfigFile(path, host, port, kwargs)
189
190   os.environ['OMNIORB_CONFIG'] = filename
191 #
192
193 # command looks like a Bash command-line:
194 # script1.py [args] ; script2.py [args] ; ...
195 def __runLocalSession(command):
196   if command:
197     sep = ";"
198     if sys.platform == "win32":
199       sep= "&"
200     command = command.split(sep)
201     outmsg = []
202     errmsg = []
203     for cmd in command:
204       single_cmd = cmd.strip().split(' ')
205       any_error = False
206       error_code = 1
207       try:
208         proc = subprocess.Popen(single_cmd)
209         (stdoutdata, stderrdata) = proc.communicate() # Wait for process to terminate
210         if stdoutdata:
211           outmsg.append(stdoutdata)
212         if stderrdata:
213           errmsg.append(stderrdata)
214
215         if proc.returncode != 0:
216           any_error = True
217           error_code = proc.returncode
218       except:
219           any_error = True
220           pass
221
222       if any_error:
223         errmsg.append("Error raised when executing command: %s\n"%cmd)
224         if outmsg:
225           sys.stdout.write("".join(outmsg))
226         if errmsg:
227           sys.stderr.write("".join(errmsg))
228         sys.exit(error_code)
229
230     return 0
231   else:
232     absoluteAppliPath = os.getenv('ABSOLUTE_APPLI_PATH','')
233     if sys.platform == "win32":
234       cmd = ["cmd", "/K", "set PROMPT=[SALOME] $P$G"]
235     else:
236       cmd = ["/bin/bash",  "--rcfile", absoluteAppliPath + "/.bashrc" ]
237     proc = subprocess.Popen(cmd, shell=False, close_fds=True)
238     proc.communicate()
239     return proc.returncode
240 #
241
242 def __copyFiles(user, machine, script, infiles, outfiles):
243   """Modify script, copy files to remote computer and return lists of copied files."""
244
245   namescript = os.path.basename(script)
246   import getpass
247   logname = getpass.getuser()
248   tmp_script = "/tmp/%s_%s_%s" % (logname, os.getpid(), namescript)
249   with open(script, 'r') as fscript:
250     script_text = fscript.read()
251
252   list_infiles = []
253   list_outfiles = []
254   n = 0
255   for infile in infiles:
256     # generate a temporary file name
257     namefile = os.path.basename(infile)
258     tmp_file = "/tmp/%s_%s_i%s_%s" % (logname, os.getpid(), n, namefile)
259
260     # modify the salome script
261     script_text = re.sub(infile, tmp_file, script_text)
262
263     # copy the infile to the remote server
264     cmd = "scp %s %s@%s:%s" % (infile, user, machine, tmp_file)
265     print("[  SCP  ]", cmd)
266     os.system(cmd)
267
268     list_infiles.append(tmp_file)
269     n = n + 1
270   #
271   n = 0
272   for outfile in outfiles:
273     # generate a temporary file name
274     namefile = os.path.basename(outfile)
275     tmp_file = "/tmp/%s_%s_o%s_%s" % (logname, os.getpid(), n, namefile)
276
277     # modify the salome script
278     script_text = re.sub(outfile, tmp_file, script_text)
279
280     list_outfiles.append(tmp_file)
281     n = n + 1
282   #
283
284   with open(tmp_script,'w') as fscript:
285     fscript.write(script_text)
286
287   # copy the salome script on the remote server
288   cmd = "scp %s %s@%s:%s" % (tmp_script, user, machine, tmp_script)
289   print("[  SCP  ]", cmd)
290   os.system(cmd)
291
292   return list_infiles, list_outfiles, tmp_script
293 #
294
295 # sa_obj is a ScriptAndArgs object (from salomeContextUtils)
296 def __runRemoteSession(sa_obj, params):
297   if not params.user:
298     print("ERROR: The user login on remote machine MUST be given.")
299     return 1
300   if not params.directory:
301     print("ERROR: The remote directory MUST be given.")
302     return 1
303
304   # sa_obj.script may be 'python script.py' --> only process .py file
305   header = " ".join(sa_obj.script.split()[:-1])
306   script = sa_obj.script.split()[-1]
307
308   tmp_in, tmp_out, tmp_script = __copyFiles(params.user, params.machine, script, sa_obj.args or [], sa_obj.out or [])
309
310   # execute command on the remote SALOME application
311   command = "%s/salome shell" % (params.directory)
312   if params.port:
313     command += " -p %s "%params.port
314   command += " %s %s args:%s"%(header, tmp_script, ",".join(tmp_in))
315   # salome shell command must run in a login shell because of module function
316   command = "ssh %s@%s -t 'bash -l -c \"%s\"'" % (params.user, params.machine, command)
317   print('[  SSH   ] ' + command)
318   os.system(command)
319
320   # Get remote files and clean
321   temp_files = tmp_in + tmp_out + [tmp_script]
322
323   # get the outfiles
324   for outfile in (sa_obj.out or []):
325     remote_outfile = tmp_out.pop(0)
326     command = "scp %s@%s:%s %s" %(params.user, params.machine, remote_outfile, outfile)
327     print("[  SCP  ] " + command)
328     os.system(command)
329
330   # clean temporary files
331   command = "ssh %s@%s \\rm -f %s" % (params.user, params.machine, " ".join(temp_files))
332   print('[  SSH   ] ' + command)
333   os.system(command)
334   os.remove(tmp_script)
335
336   return 0
337 #
338
339 def runSession(params, args):
340   scriptArgs = getScriptsAndArgs(args)
341
342   if params.mode == "local":
343     command = formatScriptsAndArgs(scriptArgs)
344     return __runLocalSession(command)
345
346   elif params.mode == "remote":
347     any_error = 0
348     for sa_obj in scriptArgs:
349       ok = __runRemoteSession(sa_obj, params)
350       if not ok:
351         any_error = 1
352     return any_error
353 #