service.py 5.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176
  1. # Licensed to the Software Freedom Conservancy (SFC) under one
  2. # or more contributor license agreements. See the NOTICE file
  3. # distributed with this work for additional information
  4. # regarding copyright ownership. The SFC licenses this file
  5. # to you under the Apache License, Version 2.0 (the
  6. # "License"); you may not use this file except in compliance
  7. # with the License. You may obtain a copy of the License at
  8. #
  9. # http://www.apache.org/licenses/LICENSE-2.0
  10. #
  11. # Unless required by applicable law or agreed to in writing,
  12. # software distributed under the License is distributed on an
  13. # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
  14. # KIND, either express or implied. See the License for the
  15. # specific language governing permissions and limitations
  16. # under the License.
  17. import errno
  18. import os
  19. import platform
  20. import subprocess
  21. from subprocess import PIPE
  22. import time
  23. from selenium.common.exceptions import WebDriverException
  24. from selenium.webdriver.common import utils
  25. try:
  26. from subprocess import DEVNULL
  27. _HAS_NATIVE_DEVNULL = True
  28. except ImportError:
  29. DEVNULL = -3
  30. _HAS_NATIVE_DEVNULL = False
  31. class Service(object):
  32. def __init__(self, executable, port=0, log_file=DEVNULL, env=None, start_error_message=""):
  33. self.path = executable
  34. self.port = port
  35. if self.port == 0:
  36. self.port = utils.free_port()
  37. if not _HAS_NATIVE_DEVNULL and log_file == DEVNULL:
  38. log_file = open(os.devnull, 'wb')
  39. self.start_error_message = start_error_message
  40. self.log_file = log_file
  41. self.env = env or os.environ
  42. @property
  43. def service_url(self):
  44. """
  45. Gets the url of the Service
  46. """
  47. return "http://%s" % utils.join_host_port('localhost', self.port)
  48. def command_line_args(self):
  49. raise NotImplemented("This method needs to be implemented in a sub class")
  50. def start(self):
  51. """
  52. Starts the Service.
  53. :Exceptions:
  54. - WebDriverException : Raised either when it can't start the service
  55. or when it can't connect to the service
  56. """
  57. try:
  58. cmd = [self.path]
  59. cmd.extend(self.command_line_args())
  60. self.process = subprocess.Popen(cmd, env=self.env,
  61. close_fds=platform.system() != 'Windows',
  62. stdout=self.log_file, stderr=self.log_file)
  63. except TypeError:
  64. raise
  65. except OSError as err:
  66. if err.errno == errno.ENOENT:
  67. raise WebDriverException(
  68. "'%s' executable needs to be in PATH. %s" % (
  69. os.path.basename(self.path), self.start_error_message)
  70. )
  71. elif err.errno == errno.EACCES:
  72. raise WebDriverException(
  73. "'%s' executable may have wrong permissions. %s" % (
  74. os.path.basename(self.path), self.start_error_message)
  75. )
  76. else:
  77. raise
  78. except Exception as e:
  79. raise WebDriverException(
  80. "The executable %s needs to be available in the path. %s\n%s" %
  81. (os.path.basename(self.path), self.start_error_message, str(e)))
  82. count = 0
  83. while True:
  84. self.assert_process_still_running()
  85. if self.is_connectable():
  86. break
  87. count += 1
  88. time.sleep(1)
  89. if count == 30:
  90. raise WebDriverException("Can not connect to the Service %s" % self.path)
  91. def assert_process_still_running(self):
  92. return_code = self.process.poll()
  93. if return_code is not None:
  94. raise WebDriverException(
  95. 'Service %s unexpectedly exited. Status code was: %s'
  96. % (self.path, return_code)
  97. )
  98. def is_connectable(self):
  99. return utils.is_connectable(self.port)
  100. def send_remote_shutdown_command(self):
  101. try:
  102. from urllib import request as url_request
  103. URLError = url_request.URLError
  104. except ImportError:
  105. import urllib2 as url_request
  106. import urllib2
  107. URLError = urllib2.URLError
  108. try:
  109. url_request.urlopen("%s/shutdown" % self.service_url)
  110. except URLError:
  111. return
  112. for x in range(30):
  113. if not self.is_connectable():
  114. break
  115. else:
  116. time.sleep(1)
  117. def stop(self):
  118. """
  119. Stops the service.
  120. """
  121. if self.log_file != PIPE and not (self.log_file == DEVNULL and _HAS_NATIVE_DEVNULL):
  122. try:
  123. self.log_file.close()
  124. except Exception:
  125. pass
  126. if self.process is None:
  127. return
  128. try:
  129. self.send_remote_shutdown_command()
  130. except TypeError:
  131. pass
  132. try:
  133. if self.process:
  134. for stream in [self.process.stdin,
  135. self.process.stdout,
  136. self.process.stderr]:
  137. try:
  138. stream.close()
  139. except AttributeError:
  140. pass
  141. self.process.terminate()
  142. self.process.wait()
  143. self.process.kill()
  144. self.process = None
  145. except OSError:
  146. pass
  147. def __del__(self):
  148. # `subprocess.Popen` doesn't send signal on `__del__`;
  149. # so we attempt to close the launched process when `__del__`
  150. # is triggered.
  151. try:
  152. self.stop()
  153. except Exception:
  154. pass