1 # Copyright (c) 2011 The Chromium OS Authors. All rights reserved.
2 # Use of this source code is governed by a BSD-style license that can be
3 # found in the LICENSE file.
5 """Module containing methods and classes to interact with a devserver instance.
16 from chromite.cbuildbot import constants
17 from chromite.lib import cros_build_lib
18 from chromite.lib import osutils
19 from chromite.lib import timeout_util
20 from chromite.lib import remote_access
26 def GenerateUpdateId(target, src, key, for_vm):
27 """Returns a simple representation id of |target| and |src| paths.
30 target: Target image of the update payloads.
31 src: Base image to of the delta update payloads.
32 key: Private key used to sign the payloads.
33 for_vm: Whether the update payloads are to be used in a VM .
37 update_id = '->'.join([src, update_id])
40 update_id = '+'.join([update_id, key])
43 update_id = '+'.join([update_id, 'patched_kernel'])
48 class DevServerException(Exception):
49 """Base exception class of devserver errors."""
52 class DevServerStartupError(DevServerException):
53 """Thrown when the devserver fails to start up."""
56 class DevServerStopError(DevServerException):
57 """Thrown when the devserver fails to stop."""
60 class DevServerResponseError(DevServerException):
61 """Thrown when the devserver responds with an error."""
64 class DevServerConnectionError(DevServerException):
65 """Thrown when unable to connect to devserver."""
68 class DevServerWrapper(multiprocessing.Process):
69 """A Simple wrapper around a dev server instance."""
71 # Wait up to 15 minutes for the dev server to start. It can take a
72 # while to start when generating payloads in parallel.
73 DEV_SERVER_TIMEOUT = 900
76 def __init__(self, static_dir=None, port=None, log_dir=None, src_image=None,
78 """Initialize a DevServerWrapper instance.
81 static_dir: The static directory to be used by the devserver.
82 port: The port to used by the devserver.
83 log_dir: Directory to store the log files.
84 src_image: The path to the image to be used as the base to
85 generate delta payloads.
86 board: Override board to pass to the devserver for xbuddy pathing.
88 super(DevServerWrapper, self).__init__()
89 self.devserver_bin = 'start_devserver'
90 # Set port if it is given. Otherwise, devserver will start at any
92 self.port = None if not port else port
93 self.src_image = src_image
96 self.log_dir = log_dir
98 self.tempdir = osutils.TempDir(
99 base_dir=cros_build_lib.FromChrootPath('/tmp'),
100 prefix='devserver_wrapper',
102 self.log_dir = self.tempdir.tempdir
103 self.static_dir = static_dir
104 self.log_file = os.path.join(self.log_dir, 'dev_server.log')
105 self.port_file = os.path.join(self.log_dir, 'dev_server.port')
106 self._pid_file = self._GetPIDFilePath()
110 def DownloadFile(cls, url, dest):
111 """Download the file from the URL to a local path."""
112 if os.path.isdir(dest):
113 dest = os.path.join(dest, os.path.basename(url))
115 logging.info('Downloading %s to %s', url, dest)
116 osutils.WriteFile(dest, DevServerWrapper.OpenURL(url), mode='wb')
118 def GetURL(self, sub_dir=None):
119 """Returns the URL of this devserver instance."""
120 return self.GetDevServerURL(port=self.port, sub_dir=sub_dir)
123 def GetDevServerURL(cls, ip=None, port=None, sub_dir=None):
124 """Returns the dev server url.
127 ip: IP address of the devserver. If not set, use the IP
128 address of this machine.
129 port: Port number of devserver.
130 sub_dir: The subdirectory of the devserver url.
132 ip = cros_build_lib.GetIPv4Address() if not ip else ip
133 # If port number is not given, assume 8080 for backward
135 port = DEFAULT_PORT if not port else port
136 url = 'http://%(ip)s:%(port)s' % {'ip': ip, 'port': str(port)}
143 def OpenURL(cls, url, ignore_url_error=False, timeout=60):
144 """Returns the HTTP response of a URL."""
145 logging.debug('Retrieving %s', url)
147 res = urllib2.urlopen(url, timeout=timeout)
148 except (urllib2.HTTPError, httplib.HTTPException) as e:
149 logging.error('Devserver responsded with an error!')
150 raise DevServerResponseError(e)
151 except (urllib2.URLError, socket.timeout) as e:
152 if not ignore_url_error:
153 logging.error('Cannot connect to devserver!')
154 raise DevServerConnectionError(e)
159 def WipeStaticDirectory(cls, static_dir):
160 """Cleans up |static_dir|.
163 static_dir: path to the static directory of the devserver instance.
165 # Wipe the payload cache.
166 cls.WipePayloadCache(static_dir=static_dir)
167 cros_build_lib.Info('Cleaning up directory %s', static_dir)
168 osutils.RmDir(static_dir, ignore_missing=True, sudo=True)
171 def WipePayloadCache(cls, devserver_bin='start_devserver', static_dir=None):
172 """Cleans up devserver cache of payloads.
175 devserver_bin: path to the devserver binary.
176 static_dir: path to use as the static directory of the devserver instance.
178 cros_build_lib.Info('Cleaning up previously generated payloads.')
179 cmd = [devserver_bin, '--clear_cache', '--exit']
181 cmd.append('--static_dir=%s' % cros_build_lib.ToChrootPath(static_dir))
183 cros_build_lib.SudoRunCommand(
184 cmd, enter_chroot=True, print_cmd=False, combine_stdout_stderr=True,
185 redirect_stdout=True, redirect_stderr=True, cwd=constants.SOURCE_ROOT)
187 def _ReadPortNumber(self):
188 """Read port number from file."""
189 if not self.is_alive():
190 raise DevServerStartupError('Devserver terminated unexpectedly!')
193 timeout_util.WaitForReturnTrue(os.path.exists,
194 func_args=[self.port_file],
195 timeout=self.DEV_SERVER_TIMEOUT,
197 except timeout_util.TimeoutError:
199 raise DevServerStartupError('Devserver portfile does not exist!')
201 self.port = int(osutils.ReadFile(self.port_file).strip())
204 """Check if devserver is up and running."""
205 if not self.is_alive():
206 raise DevServerStartupError('Devserver terminated unexpectedly!')
208 url = os.path.join('http://%s:%d' % (remote_access.LOCALHOST_IP, self.port),
210 if self.OpenURL(url, ignore_url_error=True, timeout=2):
215 def _GetPIDFilePath(self):
216 """Returns pid file path."""
217 return tempfile.NamedTemporaryFile(prefix='devserver_wrapper',
222 """Returns the pid read from the pid file."""
223 # Pid file was passed into the chroot.
224 return osutils.ReadFile(self._pid_file).rstrip()
226 def _WaitUntilStarted(self):
227 """Wait until the devserver has started."""
229 self._ReadPortNumber()
232 timeout_util.WaitForReturnTrue(self.IsReady,
233 timeout=self.DEV_SERVER_TIMEOUT,
235 except timeout_util.TimeoutError:
237 raise DevServerStartupError('Devserver did not start')
240 """Kicks off devserver in a separate process and waits for it to finish."""
241 # Truncate the log file if it already exists.
242 if os.path.exists(self.log_file):
243 osutils.SafeUnlink(self.log_file, sudo=True)
245 port = self.port if self.port else 0
246 cmd = [self.devserver_bin,
247 '--pidfile', cros_build_lib.ToChrootPath(self._pid_file),
248 '--logfile', cros_build_lib.ToChrootPath(self.log_file),
252 cmd.append('--portfile=%s' % cros_build_lib.ToChrootPath(self.port_file))
256 '--static_dir=%s' % cros_build_lib.ToChrootPath(self.static_dir))
259 cmd.append('--src_image=%s' % cros_build_lib.ToChrootPath(self.src_image))
262 cmd.append('--board=%s' % self.board)
264 result = self._RunCommand(
265 cmd, enter_chroot=True,
266 cwd=constants.SOURCE_ROOT, error_code_ok=True,
267 redirect_stdout=True, combine_stdout_stderr=True)
268 if result.returncode != 0:
269 msg = ('Devserver failed to start!\n'
270 '--- Start output from the devserver startup command ---\n'
272 '--- End output from the devserver startup command ---'
277 """Starts a background devserver and waits for it to start.
279 Starts a background devserver and waits for it to start. Will only return
280 once devserver has started and running pid has been read.
283 self._WaitUntilStarted()
284 self._pid = self._GetPID()
287 """Kills the devserver instance with SIGTERM and SIGKILL if SIGTERM fails"""
289 logging.debug('No devserver running.')
292 logging.debug('Stopping devserver instance with pid %s', self._pid)
294 self._RunCommand(['kill', self._pid], error_code_ok=True)
296 logging.debug('Devserver not running!')
299 self.join(self.KILL_TIMEOUT)
301 logging.warning('Devserver is unstoppable. Killing with SIGKILL')
303 self._RunCommand(['kill', '-9', self._pid])
304 except cros_build_lib.RunCommandError as e:
305 raise DevServerStopError('Unable to stop devserver: %s' % e)
308 """Print devserver output to stdout."""
309 print self.TailLog(num_lines='+1')
311 def TailLog(self, num_lines=50):
312 """Returns the most recent |num_lines| lines of the devserver log file."""
313 fname = self.log_file
314 # We use self._RunCommand here to check the existence of the log
315 # file, so it works for RemoteDevserverWrapper as well.
317 ['test', '-f', fname], error_code_ok=True).returncode == 0:
318 result = self._RunCommand(['tail', '-n', str(num_lines), fname],
320 output = '--- Start output from %s ---' % fname
321 output += result.output
322 output += '--- End output from %s ---' % fname
325 def _RunCommand(self, *args, **kwargs):
326 """Runs a shell commmand."""
327 kwargs.setdefault('debug_level', logging.DEBUG)
328 return cros_build_lib.SudoRunCommand(*args, **kwargs)
331 class RemoteDevServerWrapper(DevServerWrapper):
332 """A wrapper of a devserver on a remote device.
334 Devserver wrapper for RemoteDevice. This wrapper kills all existing
335 running devserver instances before startup, thus allowing one
336 devserver running at a time.
338 We assume there is no chroot on the device, thus we do not launch
339 devserver inside chroot.
342 # Shorter timeout because the remote devserver instance does not
343 # need to generate payloads.
344 DEV_SERVER_TIMEOUT = 30
346 PID_FILE_PATH = '/tmp/devserver_wrapper.pid'
348 CHERRYPY_ERROR_MSG = """
349 Your device does not have cherrypy package installed; cherrypy is
350 necessary for launching devserver on the device. Your device may be
351 running an older image (<R33-4986.0.0), where cherrypy is not
352 installed by default.
354 You can fix this with one of the following three options:
355 1. Update the device to a newer image with a USB stick.
356 2. Run 'cros deploy device cherrypy' to install cherrpy.
357 3. Run cros flash with --no-rootfs-update to update only the stateful
358 parition to a newer image (with the risk that the rootfs/stateful version
359 mismatch may cause some problems).
362 def __init__(self, remote_device, devserver_bin, **kwargs):
363 """Initializes a RemoteDevserverPortal object with the remote device.
366 remote_device: A RemoteDevice object.
367 devserver_bin: The path to the devserver script on the device.
368 **kwargs: See DevServerWrapper documentation.
370 super(RemoteDevServerWrapper, self).__init__(**kwargs)
371 self.device = remote_device
372 self.devserver_bin = devserver_bin
373 self.hostname = remote_device.hostname
376 """Returns the pid read from pid file."""
377 result = self._RunCommand(['cat', self._pid_file])
380 def _GetPIDFilePath(self):
381 """Returns the pid filename"""
382 return self.PID_FILE_PATH
384 def _RunCommand(self, *args, **kwargs):
385 """Runs a remote shell command.
388 *args: See RemoteAccess.RemoteDevice documentation.
389 **kwargs: See RemoteAccess.RemoteDevice documentation.
391 kwargs.setdefault('debug_level', logging.DEBUG)
392 return self.device.RunCommand(*args, **kwargs)
394 def _ReadPortNumber(self):
395 """Read port number from file."""
396 if not self.is_alive():
397 raise DevServerStartupError('Devserver terminated unexpectedly!')
399 def PortFileExists():
400 result = self._RunCommand(['test', '-f', self.port_file],
402 return result.returncode == 0
405 timeout_util.WaitForReturnTrue(PortFileExists,
406 timeout=self.DEV_SERVER_TIMEOUT,
408 except timeout_util.TimeoutError:
410 raise DevServerStartupError('Devserver portfile does not exist!')
412 self.port = int(self._RunCommand(
413 ['cat', self.port_file], capture_output=True).output.strip())
416 """Returns True if devserver is ready to accept requests."""
417 if not self.is_alive():
418 raise DevServerStartupError('Devserver terminated unexpectedly!')
420 url = os.path.join('http://127.0.0.1:%d' % self.port, 'check_health')
421 # Running wget through ssh because the port on the device is not
422 # accessible by default.
423 result = self.device.RunCommand(
424 ['wget', url, '-q', '-O', '/dev/null'], error_code_ok=True)
425 return result.returncode == 0
428 """Launches a devserver process on the device."""
429 self._RunCommand(['cat', '/dev/null', '>|', self.log_file])
431 port = self.port if self.port else 0
432 cmd = ['python', self.devserver_bin,
433 '--logfile=%s' % self.log_file,
434 '--pidfile', self._pid_file,
438 cmd.append('--portfile=%s' % self.port_file)
441 cmd.append('--static_dir=%s' % self.static_dir)
443 logging.info('Starting devserver on %s', self.hostname)
444 result = self._RunCommand(cmd, error_code_ok=True, redirect_stdout=True,
445 combine_stdout_stderr=True)
446 if result.returncode != 0:
447 msg = ('Remote devserver failed to start!\n'
448 '--- Start output from the devserver startup command ---\n'
450 '--- End output from the devserver startup command ---'
453 if 'ImportError: No module named cherrypy' in result.output:
454 logging.error(self.CHERRYPY_ERROR_MSG)
456 def GetURL(self, sub_dir=None):
457 """Returns the URL of this devserver instance."""
458 return self.GetDevServerURL(ip=self.hostname, port=self.port,
462 def WipePayloadCache(cls, devserver_bin='start_devserver', static_dir=None):
463 """Cleans up devserver cache of payloads."""
464 raise NotImplementedError()
467 def WipeStaticDirectory(cls, static_dir):
468 """Cleans up |static_dir|."""
469 raise NotImplementedError()