Salome HOME
[EDF] Taking SALOME launcher options into account through environment variable SALOME...
[modules/kernel.git] / bin / killSalomeWithPort.py
1 #! /usr/bin/env python3
2 #  -*- coding: iso-8859-1 -*-
3 # Copyright (C) 2007-2024  CEA, EDF, 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     logging.getLogger().debug("Cleaning OmniOrb config for port {}".format(port))
129
130     omniorb_config = generateFileName(omniorb_user_path,
131                                       prefix='omniORB',
132                                       extension='cfg',
133                                       hidden=True,
134                                       with_username=True,
135                                       with_hostname=True,
136                                       with_port=port)
137     last_running_config = generateFileName(omniorb_user_path,
138                                            prefix='omniORB',
139                                            suffix='last',
140                                            extension='cfg',
141                                            hidden=True,
142                                            with_username=True)
143     logging.getLogger().debug("Omniorb_config file deduced by port : {}".format(omniorb_config))
144     logging.getLogger().debug("Omniorb_config file of last : {}".format(last_running_config))
145     if osp.exists(last_running_config):
146         if sys.platform == 'win32' or ( osp.exists(omniorb_config) and 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             logger.debug("  ------------------ Process {} not found".format(pid))
256     __killProcesses(processes)
257
258 def __killMyPort(port, filedict):
259     """
260     Kill processes for given port (internal).
261     :param port     : port number
262     :param filedict : pidict file
263     """
264     # ensure port is an integer
265     with suppress(ValueError):
266         port = int(port)
267     logger.debug("Into __killMyPort with port {}. File containing PID to kill is {}".format(port,filedict))
268     # read pids from pidict file
269     with suppress(Exception), open(filedict, 'rb') as fpid:
270         pids_lists = pickle.load(fpid)
271         # note: pids_list is a list of tuples!
272         for pids in pids_lists:
273             __killPids(pids)
274
275     # finally remove pidict file
276     try:
277         os.remove(filedict)
278     except:
279         pass
280
281 def __guessPiDictFilename(port):
282     """
283     Guess and return pidict file for given `port` (internal).
284     :param port : port number
285     :return pidict file's path
286     """
287     # Check all possible versions of pidict file
288     # new-style - dot-prefixed pidict file: hidden is True, auto hostname
289     # old-style - not dot-prefixed pidict file: hidden is False, auto hostname
290     # old-style - dot-prefixed pidict file: hidden is True, short hostname
291     # old-style - not dot-prefixed pidict file: hidden is False, short hostname
292     # old-style - dot-prefixed pidict file: hidden is True, long hostname
293     # old-style - not dot-prefixed pidict file: hidden is False, long hostname
294     for hostname, hidden in itertools.product((None, getShortHostName(), getHostName()),
295                                               (True, False)):
296         filedict = getPiDict(port, hidden=hidden, hostname=hostname)
297         if not osp.exists(filedict):
298             if verbose():
299                 print('Trying {}... not found'.format(filedict))
300             continue
301         if verbose():
302             print('Trying {}... OK'.format(filedict))
303         return filedict
304
305     return None
306
307 def killProcessSSL(port, pids_list):
308     """ Called by runSalome.py after CTRL-C.
309     This method :
310     - Kill all PIDS in pids
311     - update file of pidict 
312     """
313     __killPids(pids_list)
314
315     with suppress(ValueError):
316         port = int(port)
317         
318     for filedict in glob('{}*'.format(getPiDict(port))):
319         with suppress(Exception), open(filedict, 'rb') as fpid:
320             logging.getLogger().debug("Removing following PIDS from file \"{}\" : {}"
321             .format(filedict,pids_list))
322             pids_lists_in_file = pickle.load(fpid)
323         for dico_of_pids in pids_lists_in_file:
324             for pid in pids_list:
325                 if pid in dico_of_pids:
326                     del dico_of_pids[pid]
327         pids_lists_in_file = [elt for elt in pids_lists_in_file if len(elt)>0]
328         if len(pids_lists_in_file) == 0:
329             try:
330                 logging.getLogger().debug("List of PIDS to Kill is now empty -> Remove file \"{}\"".format(filedict))
331                 os.remove(filedict)
332             except:
333                 pass
334             continue
335         with suppress(Exception), open(filedict, 'wb') as fpid:
336             logging.getLogger().debug("Writing back into file \"{}\"".format(filedict))
337             pickle.dump(pids_lists_in_file,fpid)
338     # clear-up omniOrb config files
339     appliCleanOmniOrbConfig(port)
340
341 def killMyPort(*ports):
342     """
343     Kill SALOME session running on the specified port.
344     :param ports : port numbers
345     """
346     for port in ports:
347         # ensure port is an integer
348         with suppress(ValueError):
349             port = int(port)
350
351         with suppress(Exception):
352             # DO NOT REMOVE NEXT LINE: it tests PortManager availability!
353             from PortManager import releasePort
354             # get pidict file
355             filedict = getPiDict(port)
356             if not osp.isfile(filedict): # removed by previous call, see (1) above
357                 if verbose():
358                     print("SALOME session on port {} is already stopped".format(port))
359                 # remove port from PortManager config file
360                 with suppress(ImportError):
361                     if verbose():
362                         print("Removing port from PortManager configuration file")
363                     releasePort(port)
364                 return
365
366         # try to shutdown session normally
367         Thread(target=shutdownMyPort, args=(port, True)).start()
368         # wait a little...
369         sleep(3)
370         # ... then kill processes (should be done if shutdown procedure hangs up)
371         try:
372             # DO NOT REMOVE NEXT LINE: it tests PortManager availability!
373             import PortManager # pragma pylint: disable=unused-import
374             for file_path in glob('{}*'.format(getPiDict(port))):
375                 __killMyPort(port, file_path)
376         except ImportError:
377             __killMyPort(port, __guessPiDictFilename(port))
378
379         # clear-up omniOrb config files
380         appliCleanOmniOrbConfig(port)
381
382 def cleanApplication(port):
383     """
384     Clean application running on the specified port.
385     :param port : port number
386     """
387     # ensure port is an integer
388     with suppress(ValueError):
389         port = int(port)
390
391     # remove pidict file
392     with suppress(Exception):
393         filedict = getPiDict(port)
394         os.remove(filedict)
395
396     # clear-up omniOrb config files
397     appliCleanOmniOrbConfig(port)
398
399 def killMyPortSpy(pid, port):
400     """
401     Start daemon process which watches for given process and kills session when process exits.
402     :param pid  : process ID
403     :param port : port number (to kill)
404     """
405     while True:
406         ret = killPid(int(pid), 0) # 0 is used to check process existence without actual killing it
407         if ret == 0:
408             break # process does not exist: exit loop
409         elif ret < 0:
410             return # something got wrong
411         sleep(1)
412
413     filedict = getPiDict(port)
414     if not osp.exists(filedict):
415         return
416
417     # check Session server
418     try:
419         import omniORB
420         orb = omniORB.CORBA.ORB_init(sys.argv, omniORB.CORBA.ORB_ID)
421         import SALOME_NamingServicePy
422         naming_service = SALOME_NamingServicePy.SALOME_NamingServicePy_i(orb, 3, True)
423         import SALOME #@UnresolvedImport @UnusedImport # pragma pylint: disable=unused-import
424         session = naming_service.Resolve('/Kernel/Session')
425         assert session
426     except: # pragma pylint: disable=bare-except
427         killMyPort(port)
428         return
429     try:
430         status = session.GetStatSession()
431     except: # pragma pylint: disable=bare-except
432         # -- session is in naming service but likely crashed
433         status = None
434     if status is not None and not status.activeGUI:
435         return
436     killMyPort(port)
437
438 def __checkUnkilledProcesses():
439     '''
440     Check all unkilled SALOME processes (internal).
441     :return: list of unkilled processes
442     '''
443     def _checkUserName(_process):
444         # The following is a workaround for Windows on which
445         # psutil.Process().username() returns 'usergroup' + 'username'
446         return getUserName() == _process.username().split('\\')[-1]
447
448     def _getDictfromOutput(_processes, _wildcard=None):
449         for _process in psutil.process_iter(['name', 'username']):
450             with suppress(psutil.AccessDenied):
451                 if _checkUserName(_process) and re.match(_wildcard, _process.info['name']):
452                     _processes.append(_process)
453
454     processes = list()
455     _getDictfromOutput(processes, '(SALOME_*)')
456     _getDictfromOutput(processes, '(omniNames)')
457     _getDictfromOutput(processes, '(ghs3d)')
458     _getDictfromOutput(processes, '(ompi-server)')
459
460     return processes
461
462 def killUnkilledProcesses():
463     """
464     Kill processes which could remain even after shutdowning SALOME sessions.
465     """
466     __killProcesses(__checkUnkilledProcesses())
467
468 def main():
469     '''
470     Main function
471     '''
472     from argparse import ArgumentParser, ArgumentDefaultsHelpFormatter
473     formatter = lambda prog: ArgumentDefaultsHelpFormatter(prog, max_help_position=50, width=120)
474     parser = ArgumentParser(description='Forcibly stop given SALOME session(s)',
475                             formatter_class=formatter)
476     parser.add_argument('ports',
477                         help='ports to kill',
478                         nargs='*', type=int)
479     group = parser.add_mutually_exclusive_group()
480     group.add_argument('-s', '--spy',
481                        help='start daemon to watch PID and then kill given PORT',
482                        nargs=2, type=int, metavar=('PID', 'PORT'))
483     group.add_argument('-l', '--list',
484                        help='list unkilled SALOME processes',
485                        action='store_true')
486     args = parser.parse_args()
487
488     if args.ports and (args.spy or args.list):
489         print("{}: error: argument ports cannot be used with -s/--spy or -l/--list"
490               .format(parser.prog), file=sys.stderr)
491         sys.exit(1)
492
493     if args.spy:
494         killMyPortSpy(*args.spy)
495         sys.exit(0)
496
497     if args.list:
498         processes = __checkUnkilledProcesses()
499         if processes:
500             print("Unkilled processes: ")
501             print("---------------------")
502             print("   PID : Process name")
503             print("--------------------")
504             for process in processes:
505                 print("{0:6} : {1}".format(process.pid, process.name()))
506         else:
507             print("No SALOME related processed found")
508         sys.exit(0)
509
510     try:
511         from salomeContextUtils import setOmniOrbUserPath #@UnresolvedImport
512         setOmniOrbUserPath()
513     except Exception as exc: # pragma pylint: disable=broad-except
514         if verbose():
515             print(exc)
516         sys.exit(1)
517
518     killMyPort(*args.ports)
519
520 if __name__ == '__main__':
521     main()