summaryrefslogtreecommitdiffstats
path: root/chrome/test/webdriver/chromedriver_launcher.py
blob: b078141b2d7ba3461cf5b4bcc4b14a2824247bd5 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
#!/usr/bin/python
# Copyright (c) 2011 The Chromium Authors. All rights reserved.
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.

"""Launches and kills ChromeDriver.

For ChromeDriver documentation, refer to:
  http://dev.chromium.org/developers/testing/webdriver-for-chrome
"""

import logging
import os
import platform
import signal
import subprocess
import sys
import threading
import urllib2


class ChromeDriverLauncher:
  """Launches and kills the ChromeDriver process."""

  def __init__(self, exe_path=None, root_path=None, port=None, url_base=None):
    """Initializes a new launcher.

    Args:
      exe_path:  path to the ChromeDriver executable
      root_path: base path from which ChromeDriver webserver will serve files
      port:      port that ChromeDriver will listen on
      url_base:  base URL which ChromeDriver webserver will listen from
    """
    self._exe_path = exe_path
    self._root_path = root_path
    self._port = port
    self._url_base = url_base
    if self._exe_path is None:
      self._exe_path = ChromeDriverLauncher.LocateExe()
      if self._exe_path is None:
        raise RuntimeError('ChromeDriver exe could not be found in its default '
                           'location. Searched in following directories: ' +
                           ', '.join(self.DefaultExeLocations()))
    if self._root_path is not None:
      self._root_path = os.path.abspath(self._root_path)
    self._process = None

    if not os.path.exists(self._exe_path):
      raise RuntimeError('ChromeDriver exe not found at: ' + self._exe_path)

    os.environ['PATH'] = os.path.dirname(self._exe_path) + os.environ['PATH']
    self.Start()

  @staticmethod
  def DefaultExeLocations():
    """Returns the paths that are used to find the ChromeDriver executable.

    Returns:
      a list of directories that would be searched for the executable
    """
    script_dir = os.path.dirname(__file__)
    chrome_src = os.path.abspath(os.path.join(
        script_dir, os.pardir, os.pardir, os.pardir))
    bin_dirs = {
      'linux2': [ os.path.join(chrome_src, 'out', 'Debug'),
                  os.path.join(chrome_src, 'sconsbuild', 'Debug'),
                  os.path.join(chrome_src, 'out', 'Release'),
                  os.path.join(chrome_src, 'sconsbuild', 'Release')],
      'linux3': [ os.path.join(chrome_src, 'out', 'Debug'),
                  os.path.join(chrome_src, 'sconsbuild', 'Debug'),
                  os.path.join(chrome_src, 'out', 'Release'),
                  os.path.join(chrome_src, 'sconsbuild', 'Release')],
      'darwin': [ os.path.join(chrome_src, 'xcodebuild', 'Debug'),
                  os.path.join(chrome_src, 'xcodebuild', 'Release')],
      'win32':  [ os.path.join(chrome_src, 'chrome', 'Debug'),
                  os.path.join(chrome_src, 'build', 'Debug'),
                  os.path.join(chrome_src, 'chrome', 'Release'),
                  os.path.join(chrome_src, 'build', 'Release')],
    }
    return [os.getcwd()] + bin_dirs.get(sys.platform, [])

  @staticmethod
  def LocateExe():
    """Attempts to locate the ChromeDriver executable.

    This searches the current directory, then checks the appropriate build
    locations according to platform.

    Returns:
      absolute path to the ChromeDriver executable, or None if not found
    """
    exe_name = 'chromedriver'
    if platform.system() == 'Windows':
      exe_name += '.exe'

    for dir in ChromeDriverLauncher.DefaultExeLocations():
      path = os.path.join(dir, exe_name)
      if os.path.exists(path):
        return os.path.abspath(path)
    return None

  def Start(self):
    """Starts a new ChromeDriver process.

    Kills a previous one if it is still running.

    Raises:
      RuntimeError if ChromeDriver does not start
    """
    def _WaitForLaunchResult(stdout, started_event, launch_result):
      """Reads from the stdout of ChromeDriver and parses the launch result.

      Args:
        stdout:        handle to ChromeDriver's standard output
        started_event: condition variable to notify when the launch result
                       has been parsed
        launch_result: dictionary to add the result of this launch to
      """
      status_line = stdout.readline()
      started_event.acquire()
      try:
        launch_result['success'] = status_line.startswith('Started')
        launch_result['status_line'] = status_line
        if launch_result['success']:
          port_line = stdout.readline()
          launch_result['port'] = int(port_line.split('=')[1])
        started_event.notify()
      finally:
        started_event.release()

    if self._process is not None:
      self.Kill()

    chromedriver_args = [self._exe_path]
    if self._root_path is not None:
      chromedriver_args += ['--root=%s' % self._root_path]
    if self._port is not None:
      chromedriver_args += ['--port=%d' % self._port]
    if self._url_base is not None:
      chromedriver_args += ['--url-base=%s' % self._url_base]
    proc = subprocess.Popen(chromedriver_args,
                            stdout=subprocess.PIPE)
    if proc is None:
      raise RuntimeError('ChromeDriver cannot be started')
    self._process = proc

    # Wait for ChromeDriver to be initialized before returning.
    launch_result = {}
    started_event = threading.Condition()
    started_event.acquire()
    spawn_thread = threading.Thread(
        target=_WaitForLaunchResult,
        args=(proc.stdout, started_event, launch_result))
    spawn_thread.start()
    started_event.wait(20)
    timed_out = 'success' not in launch_result
    started_event.release()
    if timed_out:
      raise RuntimeError('ChromeDriver did not respond')
    elif not launch_result['success']:
      raise RuntimeError('ChromeDriver failed to launch: ' +
                         launch_result['status_line'])
    self._port = launch_result['port']
    logging.info('ChromeDriver running on port %s' % self._port)

  def Kill(self):
    """Kills a currently running ChromeDriver process, if it is running."""
    def _WaitForShutdown(process, shutdown_event):
      """Waits for the process to quit and then notifies."""
      process.wait()
      shutdown_event.acquire()
      shutdown_event.notify()
      shutdown_event.release()

    if self._process is None:
      return
    try:
      urllib2.urlopen(self.GetURL() + '/shutdown').close()
    except urllib2.URLError:
      # Could not shutdown. Kill.
      pid = self._process.pid
      if platform.system() == 'Windows':
        subprocess.call(['taskkill.exe', '/T', '/F', '/PID', str(pid)])
      else:
        os.kill(pid, signal.SIGTERM)

    # Wait for ChromeDriver process to exit before returning.
    # Even if we had to kill the process above, we still should call wait
    # to cleanup the zombie.
    shutdown_event = threading.Condition()
    shutdown_event.acquire()
    wait_thread = threading.Thread(
        target=_WaitForShutdown,
        args=(self._process, shutdown_event))
    wait_thread.start()
    shutdown_event.wait(10)
    shutdown_event.release()
    self._process = None

  def GetURL(self):
    url = 'http://localhost:' + str(self._port)
    if self._url_base:
      url += self._url_base
    return url

  def GetPort(self):
    return self._port