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.
8 from __future__ import print_function
11 import multiprocessing
18 from chromite.cbuildbot import constants
19 from chromite.lib import cros_build_lib
20 from chromite.lib import osutils
21 from chromite.lib import timeout_util
22 from chromite.lib import remote_access
28 def GenerateUpdateId(target, src, key, for_vm):
29 """Returns a simple representation id of |target| and |src| paths.
32 target: Target image of the update payloads.
33 src: Base image to of the delta update payloads.
34 key: Private key used to sign the payloads.
35 for_vm: Whether the update payloads are to be used in a VM .
39 update_id = '->'.join([src, update_id])
42 update_id = '+'.join([update_id, key])
45 update_id = '+'.join([update_id, 'patched_kernel'])
50 class DevServerException(Exception):
51 """Base exception class of devserver errors."""
54 class DevServerStartupError(DevServerException):
55 """Thrown when the devserver fails to start up."""
58 class DevServerStopError(DevServerException):
59 """Thrown when the devserver fails to stop."""
62 class DevServerResponseError(DevServerException):
63 """Thrown when the devserver responds with an error."""
66 class DevServerConnectionError(DevServerException):
67 """Thrown when unable to connect to devserver."""
70 class DevServerWrapper(multiprocessing.Process):
71 """A Simple wrapper around a dev server instance."""
73 # Wait up to 15 minutes for the dev server to start. It can take a
74 # while to start when generating payloads in parallel.
75 DEV_SERVER_TIMEOUT = 900
78 def __init__(self, static_dir=None, port=None, log_dir=None, src_image=None,
80 """Initialize a DevServerWrapper instance.
83 static_dir: The static directory to be used by the devserver.
84 port: The port to used by the devserver.
85 log_dir: Directory to store the log files.
86 src_image: The path to the image to be used as the base to
87 generate delta payloads.
88 board: Override board to pass to the devserver for xbuddy pathing.
90 super(DevServerWrapper, self).__init__()
91 self.devserver_bin = 'start_devserver'
92 # Set port if it is given. Otherwise, devserver will start at any
94 self.port = None if not port else port
95 self.src_image = src_image
98 self.log_dir = log_dir
100 self.tempdir = osutils.TempDir(
101 base_dir=cros_build_lib.FromChrootPath('/tmp'),
102 prefix='devserver_wrapper',
104 self.log_dir = self.tempdir.tempdir
105 self.static_dir = static_dir
106 self.log_file = os.path.join(self.log_dir, 'dev_server.log')
107 self.port_file = os.path.join(self.log_dir, 'dev_server.port')
108 self._pid_file = self._GetPIDFilePath()
112 def DownloadFile(cls, url, dest):
113 """Download the file from the URL to a local path."""
114 if os.path.isdir(dest):
115 dest = os.path.join(dest, os.path.basename(url))
117 logging.info('Downloading %s to %s', url, dest)
118 osutils.WriteFile(dest, DevServerWrapper.OpenURL(url), mode='wb')
120 def GetURL(self, sub_dir=None):
121 """Returns the URL of this devserver instance."""
122 return self.GetDevServerURL(port=self.port, sub_dir=sub_dir)
125 def GetDevServerURL(cls, ip=None, port=None, sub_dir=None):
126 """Returns the dev server url.
129 ip: IP address of the devserver. If not set, use the IP
130 address of this machine.
131 port: Port number of devserver.
132 sub_dir: The subdirectory of the devserver url.
134 ip = cros_build_lib.GetIPv4Address() if not ip else ip
135 # If port number is not given, assume 8080 for backward
137 port = DEFAULT_PORT if not port else port
138 url = 'http://%(ip)s:%(port)s' % {'ip': ip, 'port': str(port)}
145 def OpenURL(cls, url, ignore_url_error=False, timeout=60):
146 """Returns the HTTP response of a URL."""
147 logging.debug('Retrieving %s', url)
149 res = urllib2.urlopen(url, timeout=timeout)
150 except (urllib2.HTTPError, httplib.HTTPException) as e:
151 logging.error('Devserver responsded with an error!')
152 raise DevServerResponseError(e)
153 except (urllib2.URLError, socket.timeout) as e:
154 if not ignore_url_error:
155 logging.error('Cannot connect to devserver!')
156 raise DevServerConnectionError(e)
161 def WipeStaticDirectory(cls, static_dir):
162 """Cleans up |static_dir|.
165 static_dir: path to the static directory of the devserver instance.
167 # Wipe the payload cache.
168 cls.WipePayloadCache(static_dir=static_dir)
169 cros_build_lib.Info('Cleaning up directory %s', static_dir)
170 osutils.RmDir(static_dir, ignore_missing=True, sudo=True)
173 def WipePayloadCache(cls, devserver_bin='start_devserver', static_dir=None):
174 """Cleans up devserver cache of payloads.
177 devserver_bin: path to the devserver binary.
178 static_dir: path to use as the static directory of the devserver instance.
180 cros_build_lib.Info('Cleaning up previously generated payloads.')
181 cmd = [devserver_bin, '--clear_cache', '--exit']
183 cmd.append('--static_dir=%s' % cros_build_lib.ToChrootPath(static_dir))
185 cros_build_lib.SudoRunCommand(
186 cmd, enter_chroot=True, print_cmd=False, combine_stdout_stderr=True,
187 redirect_stdout=True, redirect_stderr=True, cwd=constants.SOURCE_ROOT)
189 def _ReadPortNumber(self):
190 """Read port number from file."""
191 if not self.is_alive():
192 raise DevServerStartupError('Devserver terminated unexpectedly!')
195 timeout_util.WaitForReturnTrue(os.path.exists,
196 func_args=[self.port_file],
197 timeout=self.DEV_SERVER_TIMEOUT,
199 except timeout_util.TimeoutError:
201 raise DevServerStartupError('Devserver portfile does not exist!')
203 self.port = int(osutils.ReadFile(self.port_file).strip())
206 """Check if devserver is up and running."""
207 if not self.is_alive():
208 raise DevServerStartupError('Devserver terminated unexpectedly!')
210 url = os.path.join('http://%s:%d' % (remote_access.LOCALHOST_IP, self.port),
212 if self.OpenURL(url, ignore_url_error=True, timeout=2):
217 def _GetPIDFilePath(self):
218 """Returns pid file path."""
219 return tempfile.NamedTemporaryFile(prefix='devserver_wrapper',
224 """Returns the pid read from the pid file."""
225 # Pid file was passed into the chroot.
226 return osutils.ReadFile(self._pid_file).rstrip()
228 def _WaitUntilStarted(self):
229 """Wait until the devserver has started."""
231 self._ReadPortNumber()
234 timeout_util.WaitForReturnTrue(self.IsReady,
235 timeout=self.DEV_SERVER_TIMEOUT,
237 except timeout_util.TimeoutError:
239 raise DevServerStartupError('Devserver did not start')
242 """Kicks off devserver in a separate process and waits for it to finish."""
243 # Truncate the log file if it already exists.
244 if os.path.exists(self.log_file):
245 osutils.SafeUnlink(self.log_file, sudo=True)
247 port = self.port if self.port else 0
248 cmd = [self.devserver_bin,
249 '--pidfile', cros_build_lib.ToChrootPath(self._pid_file),
250 '--logfile', cros_build_lib.ToChrootPath(self.log_file),
254 cmd.append('--portfile=%s' % cros_build_lib.ToChrootPath(self.port_file))
258 '--static_dir=%s' % cros_build_lib.ToChrootPath(self.static_dir))
261 cmd.append('--src_image=%s' % cros_build_lib.ToChrootPath(self.src_image))
264 cmd.append('--board=%s' % self.board)
266 result = self._RunCommand(
267 cmd, enter_chroot=True, chroot_args=['--no-ns-pid'],
268 cwd=constants.SOURCE_ROOT, error_code_ok=True,
269 redirect_stdout=True, combine_stdout_stderr=True)
270 if result.returncode != 0:
271 msg = ('Devserver failed to start!\n'
272 '--- Start output from the devserver startup command ---\n'
274 '--- End output from the devserver startup command ---'
279 """Starts a background devserver and waits for it to start.
281 Starts a background devserver and waits for it to start. Will only return
282 once devserver has started and running pid has been read.
285 self._WaitUntilStarted()
286 self._pid = self._GetPID()
289 """Kills the devserver instance with SIGTERM and SIGKILL if SIGTERM fails"""
291 logging.debug('No devserver running.')
294 logging.debug('Stopping devserver instance with pid %s', self._pid)
296 self._RunCommand(['kill', self._pid], error_code_ok=True)
298 logging.debug('Devserver not running!')
301 self.join(self.KILL_TIMEOUT)
303 logging.warning('Devserver is unstoppable. Killing with SIGKILL')
305 self._RunCommand(['kill', '-9', self._pid])
306 except cros_build_lib.RunCommandError as e:
307 raise DevServerStopError('Unable to stop devserver: %s' % e)
310 """Print devserver output to stdout."""
311 print(self.TailLog(num_lines='+1'))
313 def TailLog(self, num_lines=50):
314 """Returns the most recent |num_lines| lines of the devserver log file."""
315 fname = self.log_file
316 # We use self._RunCommand here to check the existence of the log
317 # file, so it works for RemoteDevserverWrapper as well.
319 ['test', '-f', fname], error_code_ok=True).returncode == 0:
320 result = self._RunCommand(['tail', '-n', str(num_lines), fname],
322 output = '--- Start output from %s ---' % fname
323 output += result.output
324 output += '--- End output from %s ---' % fname
327 def _RunCommand(self, *args, **kwargs):
328 """Runs a shell commmand."""
329 kwargs.setdefault('debug_level', logging.DEBUG)
330 return cros_build_lib.SudoRunCommand(*args, **kwargs)
333 class RemoteDevServerWrapper(DevServerWrapper):
334 """A wrapper of a devserver on a remote device.
336 Devserver wrapper for RemoteDevice. This wrapper kills all existing
337 running devserver instances before startup, thus allowing one
338 devserver running at a time.
340 We assume there is no chroot on the device, thus we do not launch
341 devserver inside chroot.
344 # Shorter timeout because the remote devserver instance does not
345 # need to generate payloads.
346 DEV_SERVER_TIMEOUT = 30
348 PID_FILE_PATH = '/tmp/devserver_wrapper.pid'
350 CHERRYPY_ERROR_MSG = """
351 Your device does not have cherrypy package installed; cherrypy is
352 necessary for launching devserver on the device. Your device may be
353 running an older image (<R33-4986.0.0), where cherrypy is not
354 installed by default.
356 You can fix this with one of the following three options:
357 1. Update the device to a newer image with a USB stick.
358 2. Run 'cros deploy device cherrypy' to install cherrpy.
359 3. Run cros flash with --no-rootfs-update to update only the stateful
360 parition to a newer image (with the risk that the rootfs/stateful version
361 mismatch may cause some problems).
364 def __init__(self, remote_device, devserver_bin, **kwargs):
365 """Initializes a RemoteDevserverPortal object with the remote device.
368 remote_device: A RemoteDevice object.
369 devserver_bin: The path to the devserver script on the device.
370 **kwargs: See DevServerWrapper documentation.
372 super(RemoteDevServerWrapper, self).__init__(**kwargs)
373 self.device = remote_device
374 self.devserver_bin = devserver_bin
375 self.hostname = remote_device.hostname
378 """Returns the pid read from pid file."""
379 result = self._RunCommand(['cat', self._pid_file])
382 def _GetPIDFilePath(self):
383 """Returns the pid filename"""
384 return self.PID_FILE_PATH
386 def _RunCommand(self, *args, **kwargs):
387 """Runs a remote shell command.
390 *args: See RemoteAccess.RemoteDevice documentation.
391 **kwargs: See RemoteAccess.RemoteDevice documentation.
393 kwargs.setdefault('debug_level', logging.DEBUG)
394 return self.device.RunCommand(*args, **kwargs)
396 def _ReadPortNumber(self):
397 """Read port number from file."""
398 if not self.is_alive():
399 raise DevServerStartupError('Devserver terminated unexpectedly!')
401 def PortFileExists():
402 result = self._RunCommand(['test', '-f', self.port_file],
404 return result.returncode == 0
407 timeout_util.WaitForReturnTrue(PortFileExists,
408 timeout=self.DEV_SERVER_TIMEOUT,
410 except timeout_util.TimeoutError:
412 raise DevServerStartupError('Devserver portfile does not exist!')
414 self.port = int(self._RunCommand(
415 ['cat', self.port_file], capture_output=True).output.strip())
418 """Returns True if devserver is ready to accept requests."""
419 if not self.is_alive():
420 raise DevServerStartupError('Devserver terminated unexpectedly!')
422 url = os.path.join('http://127.0.0.1:%d' % self.port, 'check_health')
423 # Running wget through ssh because the port on the device is not
424 # accessible by default.
425 result = self.device.RunCommand(
426 ['wget', url, '-q', '-O', '/dev/null'], error_code_ok=True)
427 return result.returncode == 0
430 """Launches a devserver process on the device."""
431 self._RunCommand(['cat', '/dev/null', '>|', self.log_file])
433 port = self.port if self.port else 0
434 cmd = ['python', self.devserver_bin,
435 '--logfile=%s' % self.log_file,
436 '--pidfile', self._pid_file,
440 cmd.append('--portfile=%s' % self.port_file)
443 cmd.append('--static_dir=%s' % self.static_dir)
445 logging.info('Starting devserver on %s', self.hostname)
446 result = self._RunCommand(cmd, error_code_ok=True, redirect_stdout=True,
447 combine_stdout_stderr=True)
448 if result.returncode != 0:
449 msg = ('Remote devserver failed to start!\n'
450 '--- Start output from the devserver startup command ---\n'
452 '--- End output from the devserver startup command ---'
455 if 'ImportError: No module named cherrypy' in result.output:
456 logging.error(self.CHERRYPY_ERROR_MSG)
458 def GetURL(self, sub_dir=None):
459 """Returns the URL of this devserver instance."""
460 return self.GetDevServerURL(ip=self.hostname, port=self.port,
464 def WipePayloadCache(cls, devserver_bin='start_devserver', static_dir=None):
465 """Cleans up devserver cache of payloads."""
466 raise NotImplementedError()
469 def WipeStaticDirectory(cls, static_dir):
470 """Cleans up |static_dir|."""
471 raise NotImplementedError()