Catching `KeyboardInterrupt` without closing Selenium Webdriver sessions in Python Catching `KeyboardInterrupt` without closing Selenium Webdriver sessions in Python python python

Catching `KeyboardInterrupt` without closing Selenium Webdriver sessions in Python


I've got a solution, but it's pretty ugly.

When Ctrl+C is pressed, python receives a Interrupt Signal (SIGINT), which is propagated throughout your process tree.Python also generates a KeyboardInterrupt, so you can try to handle something that is bound to the logic of your process, but logic that is coupled to child processes cannot be influenced.

To influence which signals are passed on to your child processes, you'd have to specify how signals should be handled, before the process is spawned through subprocess.Popen.

There are various options, this one is taken from another answer:

import subprocessimport signaldef preexec_function():    # Ignore the SIGINT signal by setting the handler to the standard    # signal handler SIG_IGN.    signal.signal(signal.SIGINT, signal.SIG_IGN)my_process = subprocess.Popen(    ["my_executable"],    preexec_fn = preexec_function)

Problem is, you're not the one calling Popen, that is delegated to selenium. There are various discussions on SO. From what I've gathered other solutions that try to influence signal masking are prone to failure when the masking is not executed right before the call to Popen.

Also keep in mind, there is a big fat warning regarding the use of preexec_fn in the python documentation, so use that at your own discretion.

"Luckily" python allows to override functions at runtime, so we could do this:

>>> import monkey>>> import selenium.webdriver>>> selenium.webdriver.common.service.Service.start = monkey.start>>> ffx = selenium.webdriver.Firefox()>>> # pressed Ctrl+C, window stays open.KeyboardInterrupt>>> ffx.service.assert_process_still_running()>>> ffx.quit()>>> ffx.service.assert_process_still_running()Traceback (most recent call last):  File "<stdin>", line 1, in <module>  File "/usr/lib/python3.6/site-packages/selenium/webdriver/common/service.py", line 107, in assert_process_still_running    return_code = self.process.poll()AttributeError: 'NoneType' object has no attribute 'poll'

with monkey.py as follows:

import errnoimport osimport platformimport subprocessfrom subprocess import PIPEimport signalimport timefrom selenium.common.exceptions import WebDriverExceptionfrom selenium.webdriver.common import utilsdef preexec_function():    signal.signal(signal.SIGINT, signal.SIG_IGN)def start(self):  """        Starts the Service.        :Exceptions:         - WebDriverException : Raised either when it can't start the service           or when it can't connect to the service        """  try:    cmd = [self.path]    cmd.extend(self.command_line_args())    self.process = subprocess.Popen(cmd, env=self.env,                                    close_fds=platform.system() != 'Windows',                                    stdout=self.log_file,                                    stderr=self.log_file,                                    stdin=PIPE,                                    preexec_fn=preexec_function)#                                   ^^^^^^^^^^^^^^^^^^^^^^^^^^^  except TypeError:    raise  except OSError as err:    if err.errno == errno.ENOENT:      raise WebDriverException(        "'%s' executable needs to be in PATH. %s" % (          os.path.basename(self.path), self.start_error_message)      )    elif err.errno == errno.EACCES:      raise WebDriverException(        "'%s' executable may have wrong permissions. %s" % (          os.path.basename(self.path), self.start_error_message)      )    else:      raise  except Exception as e:    raise WebDriverException(      "The executable %s needs to be available in the path. %s\n%s" %      (os.path.basename(self.path), self.start_error_message, str(e)))  count = 0  while True:    self.assert_process_still_running()    if self.is_connectable():      break    count += 1    time.sleep(1)    if count == 30:      raise WebDriverException("Can not connect to the Service %s" % self.path)

the code for start is from selenium, with the added line as highlighted.It's a crude hack, it might as well bite you. Good luck :D


I was inpired by @einsweniger answer, thanks a lot! This code worked for me:

import subprocess, functools, osimport selenium.webdriverdef new_start(*args, **kwargs):    def preexec_function():        # signal.signal(signal.SIGINT, signal.SIG_IGN) # this one didn't worked for me        os.setpgrp()    default_Popen = subprocess.Popen    subprocess.Popen = functools.partial(subprocess.Popen, preexec_fn=preexec_function)    try:        new_start.default_start(*args, **kwargs)    finally:        subprocess.Popen = default_Popennew_start.default_start = selenium.webdriver.common.service.Service.startselenium.webdriver.common.service.Service.start = new_start

It's less intrusive than the previous answer as it does not rewrite the code of the full function. But on the other hand it modifies the subprocess.Popen function itself, which can be called a pretty ugly move by some.

It does the job anyway, and you don't have to update the code when the source code of Service.start changes.