Upstream version 9.38.198.0
[platform/framework/web/crosswalk.git] / src / third_party / chromite / lib / dev_server_wrapper.py
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.
4
5 """Module containing methods and classes to interact with a devserver instance.
6 """
7
8 import logging
9 import multiprocessing
10 import os
11 import socket
12 import tempfile
13 import httplib
14 import urllib2
15
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
21
22
23 DEFAULT_PORT = 8080
24
25
26 def GenerateUpdateId(target, src, key, for_vm):
27   """Returns a simple representation id of |target| and |src| paths.
28
29   Args:
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 .
34   """
35   update_id = target
36   if src:
37     update_id = '->'.join([src, update_id])
38
39   if key:
40     update_id = '+'.join([update_id, key])
41
42   if not for_vm:
43     update_id = '+'.join([update_id, 'patched_kernel'])
44
45   return update_id
46
47
48 class DevServerException(Exception):
49   """Base exception class of devserver errors."""
50
51
52 class DevServerStartupError(DevServerException):
53   """Thrown when the devserver fails to start up."""
54
55
56 class DevServerStopError(DevServerException):
57   """Thrown when the devserver fails to stop."""
58
59
60 class DevServerResponseError(DevServerException):
61   """Thrown when the devserver responds with an error."""
62
63
64 class DevServerConnectionError(DevServerException):
65   """Thrown when unable to connect to devserver."""
66
67
68 class DevServerWrapper(multiprocessing.Process):
69   """A Simple wrapper around a dev server instance."""
70
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
74   KILL_TIMEOUT = 10
75
76   def __init__(self, static_dir=None, port=None, log_dir=None, src_image=None,
77                board=None):
78     """Initialize a DevServerWrapper instance.
79
80     Args:
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.
87     """
88     super(DevServerWrapper, self).__init__()
89     self.devserver_bin = 'start_devserver'
90     # Set port if it is given. Otherwise, devserver will start at any
91     # available port.
92     self.port = None if not port else port
93     self.src_image = src_image
94     self.board = board
95     self.tempdir = None
96     self.log_dir = log_dir
97     if not self.log_dir:
98       self.tempdir = osutils.TempDir(
99           base_dir=cros_build_lib.FromChrootPath('/tmp'),
100           prefix='devserver_wrapper',
101           sudo_rm=True)
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()
107     self._pid = None
108
109   @classmethod
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))
114
115     logging.info('Downloading %s to %s', url, dest)
116     osutils.WriteFile(dest, DevServerWrapper.OpenURL(url), mode='wb')
117
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)
121
122   @classmethod
123   def GetDevServerURL(cls, ip=None, port=None, sub_dir=None):
124     """Returns the dev server url.
125
126     Args:
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.
131     """
132     ip = cros_build_lib.GetIPv4Address() if not ip else ip
133     # If port number is not given, assume 8080 for backward
134     # compatibility.
135     port = DEFAULT_PORT if not port else port
136     url = 'http://%(ip)s:%(port)s' % {'ip': ip, 'port': str(port)}
137     if sub_dir:
138       url += '/' + sub_dir
139
140     return url
141
142   @classmethod
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)
146     try:
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)
155     else:
156       return res.read()
157
158   @classmethod
159   def WipeStaticDirectory(cls, static_dir):
160     """Cleans up |static_dir|.
161
162     Args:
163       static_dir: path to the static directory of the devserver instance.
164     """
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)
169
170   @classmethod
171   def WipePayloadCache(cls, devserver_bin='start_devserver', static_dir=None):
172     """Cleans up devserver cache of payloads.
173
174     Args:
175       devserver_bin: path to the devserver binary.
176       static_dir: path to use as the static directory of the devserver instance.
177     """
178     cros_build_lib.Info('Cleaning up previously generated payloads.')
179     cmd = [devserver_bin, '--clear_cache', '--exit']
180     if static_dir:
181       cmd.append('--static_dir=%s' % cros_build_lib.ToChrootPath(static_dir))
182
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)
186
187   def _ReadPortNumber(self):
188     """Read port number from file."""
189     if not self.is_alive():
190       raise DevServerStartupError('Devserver terminated unexpectedly!')
191
192     try:
193       timeout_util.WaitForReturnTrue(os.path.exists,
194                                      func_args=[self.port_file],
195                                      timeout=self.DEV_SERVER_TIMEOUT,
196                                      period=5)
197     except timeout_util.TimeoutError:
198       self.terminate()
199       raise DevServerStartupError('Devserver portfile does not exist!')
200
201     self.port = int(osutils.ReadFile(self.port_file).strip())
202
203   def IsReady(self):
204     """Check if devserver is up and running."""
205     if not self.is_alive():
206       raise DevServerStartupError('Devserver terminated unexpectedly!')
207
208     url = os.path.join('http://%s:%d' % (remote_access.LOCALHOST_IP, self.port),
209                        'check_health')
210     if self.OpenURL(url, ignore_url_error=True, timeout=2):
211       return True
212
213     return False
214
215   def _GetPIDFilePath(self):
216     """Returns pid file path."""
217     return tempfile.NamedTemporaryFile(prefix='devserver_wrapper',
218                                        dir=self.log_dir,
219                                        delete=False).name
220
221   def _GetPID(self):
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()
225
226   def _WaitUntilStarted(self):
227     """Wait until the devserver has started."""
228     if not self.port:
229       self._ReadPortNumber()
230
231     try:
232       timeout_util.WaitForReturnTrue(self.IsReady,
233                                      timeout=self.DEV_SERVER_TIMEOUT,
234                                      period=5)
235     except timeout_util.TimeoutError:
236       self.terminate()
237       raise DevServerStartupError('Devserver did not start')
238
239   def run(self):
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)
244
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),
249            '--port=%d' % port]
250
251     if not self.port:
252       cmd.append('--portfile=%s' % cros_build_lib.ToChrootPath(self.port_file))
253
254     if self.static_dir:
255       cmd.append(
256           '--static_dir=%s' % cros_build_lib.ToChrootPath(self.static_dir))
257
258     if self.src_image:
259       cmd.append('--src_image=%s' % cros_build_lib.ToChrootPath(self.src_image))
260
261     if self.board:
262       cmd.append('--board=%s' % self.board)
263
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'
271              '%s'
272              '--- End output from the devserver startup command ---'
273              ) % result.output
274       logging.error(msg)
275
276   def Start(self):
277     """Starts a background devserver and waits for it to start.
278
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.
281     """
282     self.start()
283     self._WaitUntilStarted()
284     self._pid = self._GetPID()
285
286   def Stop(self):
287     """Kills the devserver instance with SIGTERM and SIGKILL if SIGTERM fails"""
288     if not self._pid:
289       logging.debug('No devserver running.')
290       return
291
292     logging.debug('Stopping devserver instance with pid %s', self._pid)
293     if self.is_alive():
294       self._RunCommand(['kill', self._pid], error_code_ok=True)
295     else:
296       logging.debug('Devserver not running!')
297       return
298
299     self.join(self.KILL_TIMEOUT)
300     if self.is_alive():
301       logging.warning('Devserver is unstoppable. Killing with SIGKILL')
302       try:
303         self._RunCommand(['kill', '-9', self._pid])
304       except cros_build_lib.RunCommandError as e:
305         raise DevServerStopError('Unable to stop devserver: %s' % e)
306
307   def PrintLog(self):
308     """Print devserver output to stdout."""
309     print self.TailLog(num_lines='+1')
310
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.
316     if self._RunCommand(
317         ['test', '-f', fname], error_code_ok=True).returncode == 0:
318       result = self._RunCommand(['tail', '-n', str(num_lines), fname],
319                                 capture_output=True)
320       output = '--- Start output from %s ---' % fname
321       output += result.output
322       output += '--- End output from %s ---' % fname
323       return output
324
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)
329
330
331 class RemoteDevServerWrapper(DevServerWrapper):
332   """A wrapper of a devserver on a remote device.
333
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.
337
338   We assume there is no chroot on the device, thus we do not launch
339   devserver inside chroot.
340   """
341
342   # Shorter timeout because the remote devserver instance does not
343   # need to generate payloads.
344   DEV_SERVER_TIMEOUT = 30
345   KILL_TIMEOUT = 10
346   PID_FILE_PATH = '/tmp/devserver_wrapper.pid'
347
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.
353
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).
360   """
361
362   def __init__(self, remote_device, devserver_bin, **kwargs):
363     """Initializes a RemoteDevserverPortal object with the remote device.
364
365     Args:
366       remote_device: A RemoteDevice object.
367       devserver_bin: The path to the devserver script on the device.
368       **kwargs: See DevServerWrapper documentation.
369     """
370     super(RemoteDevServerWrapper, self).__init__(**kwargs)
371     self.device = remote_device
372     self.devserver_bin = devserver_bin
373     self.hostname = remote_device.hostname
374
375   def _GetPID(self):
376     """Returns the pid read from pid file."""
377     result = self._RunCommand(['cat', self._pid_file])
378     return result.output
379
380   def _GetPIDFilePath(self):
381     """Returns the pid filename"""
382     return self.PID_FILE_PATH
383
384   def _RunCommand(self, *args, **kwargs):
385     """Runs a remote shell command.
386
387     Args:
388       *args: See RemoteAccess.RemoteDevice documentation.
389       **kwargs: See RemoteAccess.RemoteDevice documentation.
390     """
391     kwargs.setdefault('debug_level', logging.DEBUG)
392     return self.device.RunCommand(*args, **kwargs)
393
394   def _ReadPortNumber(self):
395     """Read port number from file."""
396     if not self.is_alive():
397       raise DevServerStartupError('Devserver terminated unexpectedly!')
398
399     def PortFileExists():
400       result = self._RunCommand(['test', '-f', self.port_file],
401                                 error_code_ok=True)
402       return result.returncode == 0
403
404     try:
405       timeout_util.WaitForReturnTrue(PortFileExists,
406                                      timeout=self.DEV_SERVER_TIMEOUT,
407                                      period=5)
408     except timeout_util.TimeoutError:
409       self.terminate()
410       raise DevServerStartupError('Devserver portfile does not exist!')
411
412     self.port = int(self._RunCommand(
413         ['cat', self.port_file], capture_output=True).output.strip())
414
415   def IsReady(self):
416     """Returns True if devserver is ready to accept requests."""
417     if not self.is_alive():
418       raise DevServerStartupError('Devserver terminated unexpectedly!')
419
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
426
427   def run(self):
428     """Launches a devserver process on the device."""
429     self._RunCommand(['cat', '/dev/null', '>|', self.log_file])
430
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,
435            '--port=%d' % port,]
436
437     if not self.port:
438       cmd.append('--portfile=%s' % self.port_file)
439
440     if self.static_dir:
441       cmd.append('--static_dir=%s' % self.static_dir)
442
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'
449              '%s'
450              '--- End output from the devserver startup command ---'
451              ) % result.output
452       logging.error(msg)
453       if 'ImportError: No module named cherrypy' in result.output:
454         logging.error(self.CHERRYPY_ERROR_MSG)
455
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,
459                                 sub_dir=sub_dir)
460
461   @classmethod
462   def WipePayloadCache(cls, devserver_bin='start_devserver', static_dir=None):
463     """Cleans up devserver cache of payloads."""
464     raise NotImplementedError()
465
466   @classmethod
467   def WipeStaticDirectory(cls, static_dir):
468     """Cleans up |static_dir|."""
469     raise NotImplementedError()