Захват "KeyboardInterrupt" без закрытия сеансов Selenium Webdriver в Python

Программа Python управляет Firefox через Selenium WebDriver. Код встроен в блок try/except следующим образом:

session = selenium.webdriver.Firefox(firefox_profile)
try:
    # do stuff
except (Exception, KeyboardInterrupt) as exception:
    logging.info("Caught exception.")
    traceback.print_exc(file=sys.stdout)

Если программа прерывается из-за ошибки, сеанс WebDriver не закрывается, и поэтому окно Firefox остается открытым. Но если программа прерывается с исключением KeyboardInterrupt, окно Firefox закрывается (я полагаю, потому что сеансы WebDriver также выпущены), и я хотел бы избежать этого.

Я знаю, что оба исключения проходят через один и тот же обработчик, потому что я вижу сообщение "Caught exception" в обоих случаях.

Как я мог избежать закрытия окна Firefox с помощью KeyboardInterrupt?

Ответ 1

У меня есть решение, но это довольно уродливо.

Когда Ctrl + C нажата, python получает сигнал прерывания (SIGINT), который распространяется по всему процессу. Python также генерирует KeyboardInterrupt, поэтому вы можете попытаться обработать что-то, что связано с логикой вашего процесса, но на логику, связанную с дочерними процессами, нельзя влиять.

Чтобы повлиять на то, какие сигналы передаются вашим дочерним процессам, вам нужно будет указать, как должны обрабатываться сигналы, прежде чем процесс будет порожден через subprocess.Popen.

Существуют различные варианты, которые взяты из другого ответа:

import subprocess
import signal

def 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
)

Проблема в том, что вы не тот, кто зовет Popen, который делегирован селену. Существуют различные дискуссии о SO. Из того, что я собрал, другие решения, которые пытаются повлиять на маскирование сигналов, подвержены сбою, когда маскирование не выполняется непосредственно перед вызовом Popen.

Также имейте в виду, существует большое толковое предупреждение относительно использования preexec_fn в документации на python, поэтому используйте это по своему усмотрению.

"К счастью" python позволяет переопределять функции во время выполнения, поэтому мы можем это сделать:

>>> 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'

с monkey.py следующим образом:

import errno
import os
import platform
import subprocess
from subprocess import PIPE
import signal
import time
from selenium.common.exceptions import WebDriverException
from selenium.webdriver.common import utils

def 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)

код для запуска - из селена, а добавленная строка выделена. Это грубый взлом, он может вас укусить. Удачи: D