]> SALOME platform Git repositories - modules/kernel.git/blob - bin/killSalomeWithPort.py
Salome HOME
04e6939f6702a96ef1826388d28fcc9e82b62a4a
[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 import logging
69 logger = logging.getLogger()
70
71 def getPiDict(port, appname='salome', full=True, hidden=True, hostname=None):
72     """
73     Get path to the file that stores the list of SALOME processes.
74
75     This file is located in the user's home directory
76     and named .<user>_<host>_<port>_SALOME_pidict
77     where
78
79     - <user> is user name
80     - <host> is host name
81     - <port> is port number
82
83     :param port     : port number
84     :param appname  : application name (default: 'salome')
85     :param full     : if True, full path to the file is returned,
86                       otherwise only file name is returned
87     :param hidden   : if True, file name is prefixed with . (dot) symbol;
88                       this internal parameter is only used to support
89                       compatibility with older versions of SALOME
90     :param hostname : host name (if not given, it is auto-detected)
91     :return pidict file's name or path
92     """
93     # ensure port is an integer
94     # warning: this function is also called with port='#####'!!!
95     with suppress(ValueError):
96         port = int(port)
97
98     # hostname (if not specified via parameter)
99     with suppress(AttributeError):
100         hostname = hostname or os.getenv('NSHOST').split('.')[0]
101
102     # directory to store pidict file (if `full` is True)
103     # old style: pidict files aren't dot-prefixed, stored in the user's home directory
104     # new style: pidict files are dot-prefixed, stored in the system-dependant temporary directory
105     pidict_dir = getLogDir() if hidden else osp.expanduser('~')
106
107     return generateFileName(pidict_dir if full else '',
108                             suffix='pidict',
109                             hidden=hidden,
110                             with_username=True,
111                             with_hostname=(hostname or True),
112                             with_port=port,
113                             with_app=appname.upper())
114
115 def appliCleanOmniOrbConfig(port):
116     """
117     Remove omniorb config files related to given `port` in SALOME application:
118     - ${OMNIORB_USER_PATH}/.omniORB_${USER}_${HOSTNAME}_${NSPORT}.cfg
119     - ${OMNIORB_USER_PATH}/.omniORB_${USER}_last.cfg
120     the last is removed only if the link points to the first file.
121     :param port : port number
122     """
123     omniorb_user_path = os.getenv('OMNIORB_USER_PATH')
124     if not omniorb_user_path:
125         # outside application context
126         return
127
128     if verbose():
129         print("Cleaning OmniOrb config for port {}".format(port))
130
131     omniorb_config = generateFileName(omniorb_user_path,
132                                       prefix='omniORB',
133                                       extension='cfg',
134                                       hidden=True,
135                                       with_username=True,
136                                       with_hostname=True,
137                                       with_port=port)
138     last_running_config = generateFileName(omniorb_user_path,
139                                            prefix='omniORB',
140                                            suffix='last',
141                                            extension='cfg',
142                                            hidden=True,
143                                            with_username=True)
144
145     if os.access(last_running_config, os.F_OK):
146         if sys.platform == 'win32' or osp.samefile(last_running_config, omniorb_config):
147             os.remove(last_running_config)
148
149     if os.access(omniorb_config, os.F_OK):
150         os.remove(omniorb_config)
151
152     if osp.lexists(last_running_config):
153         return
154
155     # try to relink last.cfg to an existing config file if any
156     cfg_files = [(cfg_file, os.stat(cfg_file)) for cfg_file in \
157                      glob(osp.join(omniorb_user_path,
158                                    '.omniORB_{}_*.cfg'.format(getUserName())))]
159     next_config = next((i[0] for i in sorted(cfg_files, key=lambda i: i[1])), None)
160     if next_config:
161         if sys.platform == 'win32':
162             shutil.copyfile(osp.normpath(next_config), last_running_config)
163         else:
164             os.symlink(osp.normpath(next_config), last_running_config)
165
166 def shutdownMyPort(port, cleanup=True):
167     """
168     Shutdown SALOME session running on the specified port.
169     :param port    : port number
170     :param cleanup : perform additional cleanup actions (kill omniNames, etc.)
171     """
172     if not port:
173         return
174
175     # ensure port is an integer
176     with suppress(ValueError):
177         port = int(port)
178
179     # release port
180     with suppress(ImportError):
181         # DO NOT REMOVE NEXT LINE: it tests PortManager availability!
182         from PortManager import releasePort
183         releasePort(port)
184
185     # set OMNIORB_CONFIG variable to the proper file (if not set yet)
186     omniorb_user_path = os.getenv('OMNIORB_USER_PATH')
187     kwargs = {}
188     if omniorb_user_path is not None:
189         kwargs['with_username'] = True
190     else:
191         omniorb_user_path = osp.realpath(osp.expanduser('~'))
192     omniorb_config = generateFileName(omniorb_user_path,
193                                       prefix='omniORB',
194                                       extension='cfg',
195                                       hidden=True,
196                                       with_hostname=True,
197                                       with_port=port,
198                                       **kwargs)
199     os.environ['OMNIORB_CONFIG'] = omniorb_config
200     os.environ['NSPORT'] = str(port)
201
202     # give the chance to the servers to shutdown properly
203     with suppress(Exception):
204         from omniORB import CORBA
205         from LifeCycleCORBA import LifeCycleCORBA
206         orb = CORBA.ORB_init([''], CORBA.ORB_ID)
207         lcc = LifeCycleCORBA(orb) # see (1) below
208         # shutdown all
209         if verbose():
210             print("Terminating SALOME session on port {}...".format(port))
211         lcc.shutdownServers()
212         # give some time to shutdown to complete
213         sleep(1)
214         # shutdown omniNames
215         if cleanup:
216             killOmniNames(port)
217             __killMyPort(port, getPiDict(port))
218             # DO NOT REMOVE NEXT LINE: it tests PortManager availability!
219             from PortManager import releasePort
220             releasePort(port)
221             sleep(1)
222     sys.exit(0) # see (1) below
223
224 # (1) If --shutdown-servers option is set to 1, session close procedure is
225 # called twice: first explicitly by salome command, second by automatic
226 # atexit to handle Ctrl-C. During second call, LCC does not exist anymore and
227 # a RuntimeError is raised; we explicitly exit this function with code 0 to
228 # prevent parent thread from crashing.
229
230 def __killProcesses(processes):
231     '''
232     Terminate and kill all given processes (inernal).
233     :param processes : list of processes, each one is an instance of psutil.Process
234     '''
235     # terminate processes
236     for process in processes:
237         process.terminate()
238     # wait a little, then check for alive
239     _, alive = psutil.wait_procs(processes, timeout=5)
240     # finally kill alive
241     for process in alive:
242         process.kill()
243
244 def __killPids(pids):
245     """
246     Kill processes with given `pids`.
247     :param pids : processes IDs
248     """
249     processes = []
250     for pid in pids:
251         try:
252             logger.debug("Add process with PID = {} into PIDList to kill".format(pid))
253             processes.append(psutil.Process(pid))
254         except psutil.NoSuchProcess:
255             if verbose():
256                 print("  ------------------ Process {} not found".format(pid))
257     __killProcesses(processes)
258
259 def __killMyPort(port, filedict):
260     """
261     Kill processes for given port (internal).
262     :param port     : port number
263     :param filedict : pidict file
264     """
265     # ensure port is an integer
266     with suppress(ValueError):
267         port = int(port)
268     logger.debug("Into __killMyPort with port {}. File containing PID to kill is {}".format(port,filedict))
269     # read pids from pidict file
270     with suppress(Exception), open(filedict, 'rb') as fpid:
271         pids_lists = pickle.load(fpid)
272         # note: pids_list is a list of tuples!
273         for pids in pids_lists:
274             __killPids(pids)
275
276     # finally remove pidict file
277     try:
278         os.remove(filedict)
279     except:
280         pass
281
282 def __guessPiDictFilename(port):
283     """
284     Guess and return pidict file for given `port` (internal).
285     :param port : port number
286     :return pidict file's path
287     """
288     # Check all possible versions of pidict file
289     # new-style - dot-prefixed pidict file: hidden is True, auto hostname
290     # old-style - not dot-prefixed pidict file: hidden is False, auto hostname
291     # old-style - dot-prefixed pidict file: hidden is True, short hostname
292     # old-style - not dot-prefixed pidict file: hidden is False, short hostname
293     # old-style - dot-prefixed pidict file: hidden is True, long hostname
294     # old-style - not dot-prefixed pidict file: hidden is False, long hostname
295     for hostname, hidden in itertools.product((None, getShortHostName(), getHostName()),
296                                               (True, False)):
297         filedict = getPiDict(port, hidden=hidden, hostname=hostname)
298         if not osp.exists(filedict):
299             if verbose():
300                 print('Trying {}... not found'.format(filedict))
301             continue
302         if verbose():
303             print('Trying {}... OK'.format(filedict))
304         return filedict
305
306     return None
307
308 def killMyPortSSL(*ports):
309     """ Called by runSalome.py after CTRL-C"""
310     for port in ports:
311         # ensure port is an integer
312         with suppress(ValueError):
313             port = int(port)
314
315         with suppress(Exception):
316             # DO NOT REMOVE NEXT LINE: it tests PortManager availability!
317             from PortManager import releasePort
318             # get pidict file
319             filedict = getPiDict(port)
320             if not osp.isfile(filedict): # removed by previous call, see (1) above
321                 if verbose():
322                     print("SALOME session on port {} is already stopped".format(port))
323                 # remove port from PortManager config file
324                 with suppress(ImportError):
325                     if verbose():
326                         print("Removing port from PortManager configuration file")
327                     releasePort(port)
328                 return
329         try:
330             # DO NOT REMOVE NEXT LINE: it tests PortManager availability!
331             import PortManager # pragma pylint: disable=unused-import
332             for file_path in glob('{}*'.format(getPiDict(port))):
333                 __killMyPort(port, file_path)
334         except ImportError:
335             __killMyPort(port, __guessPiDictFilename(port))
336
337         # clear-up omniOrb config files
338         appliCleanOmniOrbConfig(port)
339
340 def killMyPort(*ports):
341     """
342     Kill SALOME session running on the specified port.
343     :param ports : port numbers
344     """
345     for port in ports:
346         # ensure port is an integer
347         with suppress(ValueError):
348             port = int(port)
349
350         with suppress(Exception):
351             # DO NOT REMOVE NEXT LINE: it tests PortManager availability!
352             from PortManager import releasePort
353             # get pidict file
354             filedict = getPiDict(port)
355             if not osp.isfile(filedict): # removed by previous call, see (1) above
356                 if verbose():
357                     print("SALOME session on port {} is already stopped".format(port))
358                 # remove port from PortManager config file
359                 with suppress(ImportError):
360                     if verbose():
361                         print("Removing port from PortManager configuration file")
362                     releasePort(port)
363                 return
364
365         # try to shutdown session normally
366         Thread(target=shutdownMyPort, args=(port, True)).start()
367         # wait a little...
368         sleep(3)
369         # ... then kill processes (should be done if shutdown procedure hangs up)
370         try:
371             # DO NOT REMOVE NEXT LINE: it tests PortManager availability!
372             import PortManager # pragma pylint: disable=unused-import
373             for file_path in glob('{}*'.format(getPiDict(port))):
374                 __killMyPort(port, file_path)
375         except ImportError:
376             __killMyPort(port, __guessPiDictFilename(port))
377
378         # clear-up omniOrb config files
379         appliCleanOmniOrbConfig(port)
380
381 def cleanApplication(port):
382     """
383     Clean application running on the specified port.
384     :param port : port number
385     """
386     # ensure port is an integer
387     with suppress(ValueError):
388         port = int(port)
389
390     # remove pidict file
391     with suppress(Exception):
392         filedict = getPiDict(port)
393         os.remove(filedict)
394
395     # clear-up omniOrb config files
396     appliCleanOmniOrbConfig(port)
397
398 def killMyPortSpy(pid, port):
399     """
400     Start daemon process which watches for given process and kills session when process exits.
401     :param pid  : process ID
402     :param port : port number (to kill)
403     """
404     while True:
405         ret = killPid(int(pid), 0) # 0 is used to check process existence without actual killing it
406         if ret == 0:
407             break # process does not exist: exit loop
408         elif ret < 0:
409             return # something got wrong
410         sleep(1)
411
412     filedict = getPiDict(port)
413     if not osp.exists(filedict):
414         return
415
416     # check Session server
417     try:
418         import omniORB
419         orb = omniORB.CORBA.ORB_init(sys.argv, omniORB.CORBA.ORB_ID)
420         import SALOME_NamingServicePy
421         naming_service = SALOME_NamingServicePy.SALOME_NamingServicePy_i(orb, 3, True)
422         import SALOME #@UnresolvedImport @UnusedImport # pragma pylint: disable=unused-import
423         session = naming_service.Resolve('/Kernel/Session')
424         assert session
425     except: # pragma pylint: disable=bare-except
426         killMyPort(port)
427         return
428     try:
429         status = session.GetStatSession()
430     except: # pragma pylint: disable=bare-except
431         # -- session is in naming service but likely crashed
432         status = None
433     if status is not None and not status.activeGUI:
434         return
435     killMyPort(port)
436
437 def __checkUnkilledProcesses():
438     '''
439     Check all unkilled SALOME processes (internal).
440     :return: list of unkilled processes
441     '''
442     def _checkUserName(_process):
443         # The following is a workaround for Windows on which
444         # psutil.Process().username() returns 'usergroup' + 'username'
445         return getUserName() == _process.username().split('\\')[-1]
446
447     def _getDictfromOutput(_processes, _wildcard=None):
448         for _process in psutil.process_iter(['name', 'username']):
449             with suppress(psutil.AccessDenied):
450                 if _checkUserName(_process) and re.match(_wildcard, _process.info['name']):
451                     _processes.append(_process)
452
453     processes = list()
454     _getDictfromOutput(processes, '(SALOME_*)')
455     _getDictfromOutput(processes, '(omniNames)')
456     _getDictfromOutput(processes, '(ghs3d)')
457     _getDictfromOutput(processes, '(ompi-server)')
458
459     return processes
460
461 def killUnkilledProcesses():
462     """
463     Kill processes which could remain even after shutdowning SALOME sessions.
464     """
465     __killProcesses(__checkUnkilledProcesses())
466
467 def main():
468     '''
469     Main function
470     '''
471     from argparse import ArgumentParser, ArgumentDefaultsHelpFormatter
472     formatter = lambda prog: ArgumentDefaultsHelpFormatter(prog, max_help_position=50, width=120)
473     parser = ArgumentParser(description='Forcibly stop given SALOME session(s)',
474                             formatter_class=formatter)
475     parser.add_argument('ports',
476                         help='ports to kill',
477                         nargs='*', type=int)
478     group = parser.add_mutually_exclusive_group()
479     group.add_argument('-s', '--spy',
480                        help='start daemon to watch PID and then kill given PORT',
481                        nargs=2, type=int, metavar=('PID', 'PORT'))
482     group.add_argument('-l', '--list',
483                        help='list unkilled SALOME processes',
484                        action='store_true')
485     args = parser.parse_args()
486
487     if args.ports and (args.spy or args.list):
488         print("{}: error: argument ports cannot be used with -s/--spy or -l/--list"
489               .format(parser.prog), file=sys.stderr)
490         sys.exit(1)
491
492     if args.spy:
493         killMyPortSpy(*args.spy)
494         sys.exit(0)
495
496     if args.list:
497         processes = __checkUnkilledProcesses()
498         if processes:
499             print("Unkilled processes: ")
500             print("---------------------")
501             print("   PID : Process name")
502             print("--------------------")
503             for process in processes:
504                 print("{0:6} : {1}".format(process.pid, process.name()))
505         else:
506             print("No SALOME related processed found")
507         sys.exit(0)
508
509     try:
510         from salomeContextUtils import setOmniOrbUserPath #@UnresolvedImport
511         setOmniOrbUserPath()
512     except Exception as exc: # pragma pylint: disable=broad-except
513         if verbose():
514             print(exc)
515         sys.exit(1)
516
517     killMyPort(*args.ports)
518
519 if __name__ == '__main__':
520     main()