Salome HOME
For future compatibility with python 3.9.
[modules/kernel.git] / bin / killSalomeWithPort.py
1 #! /usr/bin/env python3
2 #  -*- coding: iso-8859-1 -*-
3 # Copyright (C) 2007-2021  CEA/DEN, EDF R&D, OPEN CASCADE
4 #
5 # Copyright (C) 2003-2007  OPEN CASCADE, EADS/CCR, LIP6, CEA/DEN,
6 # CEDRAT, EDF R&D, LEG, PRINCIPIA R&D, BUREAU VERITAS
7 #
8 # This library is free software; you can redistribute it and/or
9 # modify it under the terms of the GNU Lesser General Public
10 # License as published by the Free Software Foundation; either
11 # version 2.1 of the License, or (at your option) any later version.
12 #
13 # This library is distributed in the hope that it will be useful,
14 # but WITHOUT ANY WARRANTY; without even the implied warranty of
15 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
16 # Lesser General Public License for more details.
17 #
18 # You should have received a copy of the GNU Lesser General Public
19 # License along with this library; if not, write to the Free Software
20 # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307 USA
21 #
22 # See http://www.salome-platform.org/ or email : webmaster.salome@opencascade.com
23 #
24
25 ## @file killSalomeWithPort.py
26 #  @brief Forcibly stop %SALOME processes from given session(s).
27 #
28 #  The sessions are indicated by their ports on the command line as in below example:
29 #  @code
30 #  killSalomeWithPort.py 2811 2815
31 #  @endcode
32
33 """
34 Forcibly stop given SALOME session(s).
35
36 To stop one or more SALOME sessions, specify network ports they are bound to,
37 for example:
38
39 * in shell
40
41     $ killSalomeWithPort.py 2811 2815
42
43 * in Python script:
44
45     from killSalomeWithPort import killMyPort
46     killMyPort(2811, 2815)
47
48 """
49
50 # pragma pylint: disable=invalid-name
51
52 import itertools
53 import os
54 import os.path as osp
55 import pickle
56 import re
57 import shutil
58 import sys
59 from contextlib import suppress
60 from glob import glob
61 from threading import Thread
62 from time import sleep
63
64 import psutil
65
66 from salome_utils import (generateFileName, getHostName, getLogDir, getShortHostName,
67                           getUserName, killOmniNames, killPid, verbose)
68
69 def getPiDict(port, appname='salome', full=True, hidden=True, hostname=None):
70     """
71     Get path to the file that stores the list of SALOME processes.
72
73     This file is located in the user's home directory
74     and named .<user>_<host>_<port>_SALOME_pidict
75     where
76
77     - <user> is user name
78     - <host> is host name
79     - <port> is port number
80
81     :param port     : port number
82     :param appname  : application name (default: 'salome')
83     :param full     : if True, full path to the file is returned,
84                       otherwise only file name is returned
85     :param hidden   : if True, file name is prefixed with . (dot) symbol;
86                       this internal parameter is only used to support
87                       compatibility with older versions of SALOME
88     :param hostname : host name (if not given, it is auto-detected)
89     :return pidict file's name or path
90     """
91     # ensure port is an integer
92     # warning: this function is also called with port='#####'!!!
93     with suppress(ValueError):
94         port = int(port)
95
96     # hostname (if not specified via parameter)
97     with suppress(AttributeError):
98         hostname = hostname or os.getenv('NSHOST').split('.')[0]
99
100     # directory to store pidict file (if `full` is True)
101     # old style: pidict files aren't dot-prefixed, stored in the user's home directory
102     # new style: pidict files are dot-prefixed, stored in the system-dependant temporary directory
103     pidict_dir = getLogDir() if hidden else osp.expanduser('~')
104
105     return generateFileName(pidict_dir if full else '',
106                             suffix='pidict',
107                             hidden=hidden,
108                             with_username=True,
109                             with_hostname=(hostname or True),
110                             with_port=port,
111                             with_app=appname.upper())
112
113 def appliCleanOmniOrbConfig(port):
114     """
115     Remove omniorb config files related to given `port` in SALOME application:
116     - ${OMNIORB_USER_PATH}/.omniORB_${USER}_${HOSTNAME}_${NSPORT}.cfg
117     - ${OMNIORB_USER_PATH}/.omniORB_${USER}_last.cfg
118     the last is removed only if the link points to the first file.
119     :param port : port number
120     """
121     omniorb_user_path = os.getenv('OMNIORB_USER_PATH')
122     if not omniorb_user_path:
123         # outside application context
124         return
125
126     if verbose():
127         print("Cleaning OmniOrb config for port {}".format(port))
128
129     omniorb_config = generateFileName(omniorb_user_path,
130                                       prefix='omniORB',
131                                       extension='cfg',
132                                       hidden=True,
133                                       with_username=True,
134                                       with_hostname=True,
135                                       with_port=port)
136     last_running_config = generateFileName(omniorb_user_path,
137                                            prefix='omniORB',
138                                            suffix='last',
139                                            extension='cfg',
140                                            hidden=True,
141                                            with_username=True)
142
143     if os.access(last_running_config, os.F_OK):
144         if sys.platform == 'win32' or osp.samefile(last_running_config, omniorb_config):
145             os.remove(last_running_config)
146
147     if os.access(omniorb_config, os.F_OK):
148         os.remove(omniorb_config)
149
150     if osp.lexists(last_running_config):
151         return
152
153     # try to relink last.cfg to an existing config file if any
154     cfg_files = [(cfg_file, os.stat(cfg_file)) for cfg_file in \
155                      glob(osp.join(omniorb_user_path,
156                                    '.omniORB_{}_*.cfg'.format(getUserName())))]
157     next_config = next((i[0] for i in sorted(cfg_files, key=lambda i: i[1])), None)
158     if next_config:
159         if sys.platform == 'win32':
160             shutil.copyfile(osp.normpath(next_config), last_running_config)
161         else:
162             os.symlink(osp.normpath(next_config), last_running_config)
163
164 def shutdownMyPort(port, cleanup=True):
165     """
166     Shutdown SALOME session running on the specified port.
167     :param port    : port number
168     :param cleanup : perform additional cleanup actions (kill omniNames, etc.)
169     """
170     if not port:
171         return
172
173     # ensure port is an integer
174     with suppress(ValueError):
175         port = int(port)
176
177     # release port
178     with suppress(ImportError):
179         # DO NOT REMOVE NEXT LINE: it tests PortManager availability!
180         from PortManager import releasePort
181         releasePort(port)
182
183     # set OMNIORB_CONFIG variable to the proper file (if not set yet)
184     omniorb_user_path = os.getenv('OMNIORB_USER_PATH')
185     kwargs = {}
186     if omniorb_user_path is not None:
187         kwargs['with_username'] = True
188     else:
189         omniorb_user_path = osp.realpath(osp.expanduser('~'))
190     omniorb_config = generateFileName(omniorb_user_path,
191                                       prefix='omniORB',
192                                       extension='cfg',
193                                       hidden=True,
194                                       with_hostname=True,
195                                       with_port=port,
196                                       **kwargs)
197     os.environ['OMNIORB_CONFIG'] = omniorb_config
198     os.environ['NSPORT'] = str(port)
199
200     # give the chance to the servers to shutdown properly
201     with suppress(Exception):
202         from omniORB import CORBA
203         from LifeCycleCORBA import LifeCycleCORBA
204         orb = CORBA.ORB_init([''], CORBA.ORB_ID)
205         lcc = LifeCycleCORBA(orb) # see (1) below
206         # shutdown all
207         if verbose():
208             print("Terminating SALOME session on port {}...".format(port))
209         lcc.shutdownServers()
210         # give some time to shutdown to complete
211         sleep(1)
212         # shutdown omniNames
213         if cleanup:
214             killOmniNames(port)
215             __killMyPort(port, getPiDict(port))
216             # DO NOT REMOVE NEXT LINE: it tests PortManager availability!
217             from PortManager import releasePort
218             releasePort(port)
219             sleep(1)
220     sys.exit(0) # see (1) below
221
222 # (1) If --shutdown-servers option is set to 1, session close procedure is
223 # called twice: first explicitly by salome command, second by automatic
224 # atexit to handle Ctrl-C. During second call, LCC does not exist anymore and
225 # a RuntimeError is raised; we explicitly exit this function with code 0 to
226 # prevent parent thread from crashing.
227
228 def __killProcesses(processes):
229     '''
230     Terminate and kill all given processes (inernal).
231     :param processes : list of processes, each one is an instance of psutil.Process
232     '''
233     # terminate processes
234     for process in processes:
235         process.terminate()
236     # wait a little, then check for alive
237     _, alive = psutil.wait_procs(processes, timeout=5)
238     # finally kill alive
239     for process in alive:
240         process.kill()
241
242 def __killPids(pids):
243     """
244     Kill processes with given `pids`.
245     :param pids : processes IDs
246     """
247     processes = []
248     for pid in pids:
249         try:
250             processes.append(psutil.Process(pid))
251         except psutil.NoSuchProcess:
252             if verbose():
253                 print("  ------------------ Process {} not found".format(pid))
254     __killProcesses(processes)
255
256 def __killMyPort(port, filedict):
257     """
258     Kill processes for given port (internal).
259     :param port     : port number
260     :param filedict : pidict file
261     """
262     # ensure port is an integer
263     with suppress(ValueError):
264         port = int(port)
265
266     # read pids from pidict file
267     with suppress(Exception), open(filedict, 'rb') as fpid:
268         pids_lists = pickle.load(fpid)
269         # note: pids_list is a list of tuples!
270         for pids in pids_lists:
271             __killPids(pids)
272
273     # finally remove pidict file
274     os.remove(filedict)
275
276 def __guessPiDictFilename(port):
277     """
278     Guess and return pidict file for given `port` (internal).
279     :param port : port number
280     :return pidict file's path
281     """
282     # Check all possible versions of pidict file
283     # new-style - dot-prefixed pidict file: hidden is True, auto hostname
284     # old-style - not dot-prefixed pidict file: hidden is False, auto hostname
285     # old-style - dot-prefixed pidict file: hidden is True, short hostname
286     # old-style - not dot-prefixed pidict file: hidden is False, short hostname
287     # old-style - dot-prefixed pidict file: hidden is True, long hostname
288     # old-style - not dot-prefixed pidict file: hidden is False, long hostname
289     for hostname, hidden in itertools.product((None, getShortHostName(), getHostName()),
290                                               (True, False)):
291         filedict = getPiDict(port, hidden=hidden, hostname=hostname)
292         if not osp.exists(filedict):
293             if verbose():
294                 print('Trying {}... not found'.format(filedict))
295             continue
296         if verbose():
297             print('Trying {}... OK'.format(filedict))
298         return filedict
299
300     return None
301
302 def killMyPort(*ports):
303     """
304     Kill SALOME session running on the specified port.
305     :param ports : port numbers
306     """
307     for port in ports:
308         # ensure port is an integer
309         with suppress(ValueError):
310             port = int(port)
311
312         with suppress(Exception):
313             # DO NOT REMOVE NEXT LINE: it tests PortManager availability!
314             from PortManager import releasePort
315             # get pidict file
316             filedict = getPiDict(port)
317             if not osp.isfile(filedict): # removed by previous call, see (1) above
318                 if verbose():
319                     print("SALOME session on port {} is already stopped".format(port))
320                 # remove port from PortManager config file
321                 with suppress(ImportError):
322                     if verbose():
323                         print("Removing port from PortManager configuration file")
324                     releasePort(port)
325                 return
326
327         # try to shutdown session normally
328         Thread(target=shutdownMyPort, args=(port, True)).start()
329         # wait a little...
330         sleep(3)
331         # ... then kill processes (should be done if shutdown procedure hangs up)
332         try:
333             # DO NOT REMOVE NEXT LINE: it tests PortManager availability!
334             import PortManager # pragma pylint: disable=unused-import
335             for file_path in glob('{}*'.format(getPiDict(port))):
336                 __killMyPort(port, file_path)
337         except ImportError:
338             __killMyPort(port, __guessPiDictFilename(port))
339
340         # clear-up omniOrb config files
341         appliCleanOmniOrbConfig(port)
342
343 def cleanApplication(port):
344     """
345     Clean application running on the specified port.
346     :param port : port number
347     """
348     # ensure port is an integer
349     with suppress(ValueError):
350         port = int(port)
351
352     # remove pidict file
353     with suppress(Exception):
354         filedict = getPiDict(port)
355         os.remove(filedict)
356
357     # clear-up omniOrb config files
358     appliCleanOmniOrbConfig(port)
359
360 def killMyPortSpy(pid, port):
361     """
362     Start daemon process which watches for given process and kills session when process exits.
363     :param pid  : process ID
364     :param port : port number (to kill)
365     """
366     while True:
367         ret = killPid(int(pid), 0) # 0 is used to check process existence without actual killing it
368         if ret == 0:
369             break # process does not exist: exit loop
370         elif ret < 0:
371             return # something got wrong
372         sleep(1)
373
374     filedict = getPiDict(port)
375     if not osp.exists(filedict):
376         return
377
378     # check Session server
379     try:
380         import omniORB
381         orb = omniORB.CORBA.ORB_init(sys.argv, omniORB.CORBA.ORB_ID)
382         import SALOME_NamingServicePy
383         naming_service = SALOME_NamingServicePy.SALOME_NamingServicePy_i(orb, 3, True)
384         import SALOME #@UnresolvedImport @UnusedImport # pragma pylint: disable=unused-import
385         session = naming_service.Resolve('/Kernel/Session')
386         assert session
387     except: # pragma pylint: disable=bare-except
388         killMyPort(port)
389         return
390     try:
391         status = session.GetStatSession()
392     except: # pragma pylint: disable=bare-except
393         # -- session is in naming service but likely crashed
394         status = None
395     if status is not None and not status.activeGUI:
396         return
397     killMyPort(port)
398
399 def __checkUnkilledProcesses():
400     '''
401     Check all unkilled SALOME processes (internal).
402     :return: list of unkilled processes
403     '''
404     def _checkUserName(_process):
405         # The following is a workaround for Windows on which
406         # psutil.Process().username() returns 'usergroup' + 'username'
407         return getUserName() == _process.username()
408
409     def _getDictfromOutput(_processes, _wildcard=None):
410         for _process in psutil.process_iter(['name', 'username']):
411             with suppress(psutil.AccessDenied):
412                 if _checkUserName(_process) and re.match(_wildcard, _process.info['name']):
413                     _processes.append(_process)
414
415     processes = list()
416     _getDictfromOutput(processes, '(SALOME_*)')
417     _getDictfromOutput(processes, '(omniNames)')
418     _getDictfromOutput(processes, '(ghs3d)')
419     _getDictfromOutput(processes, '(ompi-server)')
420
421     return processes
422
423 def killUnkilledProcesses():
424     """
425     Kill processes which could remain even after shutdowning SALOME sessions.
426     """
427     __killProcesses(__checkUnkilledProcesses())
428
429 def main():
430     '''
431     Main function
432     '''
433     from argparse import ArgumentParser, ArgumentDefaultsHelpFormatter
434     formatter = lambda prog: ArgumentDefaultsHelpFormatter(prog, max_help_position=50, width=120)
435     parser = ArgumentParser(description='Forcibly stop given SALOME session(s)',
436                             formatter_class=formatter)
437     parser.add_argument('ports',
438                         help='ports to kill',
439                         nargs='*', type=int)
440     group = parser.add_mutually_exclusive_group()
441     group.add_argument('-s', '--spy',
442                        help='start daemon to watch PID and then kill given PORT',
443                        nargs=2, type=int, metavar=('PID', 'PORT'))
444     group.add_argument('-l', '--list',
445                        help='list unkilled SALOME processes',
446                        action='store_true')
447     args = parser.parse_args()
448
449     if args.ports and (args.spy or args.list):
450         print("{}: error: argument ports cannot be used with -s/--spy or -l/--list"
451               .format(parser.prog), file=sys.stderr)
452         sys.exit(1)
453
454     if args.spy:
455         killMyPortSpy(*args.spy)
456         sys.exit(0)
457
458     if args.list:
459         processes = __checkUnkilledProcesses()
460         if processes:
461             print("Unkilled processes: ")
462             print("---------------------")
463             print("   PID : Process name")
464             print("--------------------")
465             for process in processes:
466                 print("{0:6} : {1}".format(process.pid, process.name()))
467         else:
468             print("No SALOME related processed found")
469         sys.exit(0)
470
471     try:
472         from salomeContextUtils import setOmniOrbUserPath #@UnresolvedImport
473         setOmniOrbUserPath()
474     except Exception as exc: # pragma pylint: disable=broad-except
475         if verbose():
476             print(exc)
477         sys.exit(1)
478
479     killMyPort(*args.ports)
480
481 if __name__ == '__main__':
482     main()