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