blob: 74e129048bb9e3e9b0d5fcba335e117da7b4bbe9 [file] [log] [blame]
Gabe Black3b567202015-09-23 21:07:591#!/usr/bin/python2
Chris Sosa7c931362010-10-12 02:49:012
Chris Sosa781ba6d2012-04-11 19:44:433# Copyright (c) 2009-2012 The Chromium OS Authors. All rights reserved.
[email protected]ded22402009-10-26 22:36:214# Use of this source code is governed by a BSD-style license that can be
5# found in the LICENSE file.
6
Chris Sosa3ae4dc12013-03-29 18:47:007"""Chromium OS development server that can be used for all forms of update.
8
9This devserver can be used to perform system-wide autoupdate and update
10of specific portage packages on devices running Chromium OS derived operating
11systems. It mainly operates in two modes:
12
131) archive mode: In this mode, the devserver is configured to stage and
14serve artifacts from Google Storage using the credentials provided to it before
15it is run. The easiest way to understand this is that the devserver is
16functioning as a local cache for artifacts produced and uploaded by build
17servers. Users of this form of devserver can either download the artifacts
18from the devservers static directory OR use the update RPC to perform a
19system-wide autoupdate. Archive mode is always active.
20
212) artifact-generation mode: in this mode, the devserver will attempt to
22generate update payloads and build artifacts when requested. This mode only
23works in the Chromium OS chroot as it uses build tools only present in the
24chroot (emerge, cros_generate_update_payload, etc.). By default, when a device
25requests an update from this form of devserver, the devserver will attempt to
26discover if a more recent build of the board has been built by the developer
27and generate a payload that the requested system can autoupdate to. In addition,
28it accepts gmerge requests from devices that will stage the newest version of
joychen84d13772013-08-06 16:17:2329a particular package from a developer's chroot onto a requesting device.
Chris Sosa3ae4dc12013-03-29 18:47:0030
31For example:
32gmerge gmerge -d <devserver_url>
33
34devserver will see if a newer package of gmerge is available. If gmerge is
35cros_work'd on, it will re-build gmerge. After this, gmerge will install that
36version of gmerge that the devserver just created/found.
37
38For autoupdates, there are many more advanced options that can help specify
39how to update and which payload to give to a requester.
40"""
41
Gabe Black3b567202015-09-23 21:07:5942from __future__ import print_function
Chris Sosa7c931362010-10-12 02:49:0143
Gilad Arnold55a2a372012-10-02 16:46:3244import json
Sean O'Connor14b6a0a2010-03-21 06:23:4845import optparse
[email protected]ded22402009-10-26 22:36:2146import os
Scott Zawalski4647ce62012-01-03 22:17:2847import re
Simran Basi4baad082013-02-14 21:39:1848import shutil
Mandeep Singh Baines38dcdda2012-12-08 01:55:3349import socket
Chris Masone816e38c2012-05-02 19:22:3650import subprocess
J. Richard Barnette3d977b82013-04-23 18:05:1951import sys
Chris Masone816e38c2012-05-02 19:22:3652import tempfile
Dan Shi59ae7092013-06-04 21:37:2753import threading
Dan Shiafd0e492015-05-27 21:23:5154import time
Gilad Arnoldd5ebaaa2012-10-02 18:52:3855import types
J. Richard Barnette3d977b82013-04-23 18:05:1956from logging import handlers
57
58import cherrypy
Chris Sosa855b8932013-08-21 20:24:5559from cherrypy import _cplogging as cplogging
60from cherrypy.process import plugins
[email protected]ded22402009-10-26 22:36:2161
Chris Sosa0356d3b2010-09-16 22:46:2262import autoupdate
Chris Sosa75490802013-10-01 00:21:4563import build_artifact
Gilad Arnold11fbef42014-02-10 19:04:1364import cherrypy_ext
Gilad Arnoldc65330c2012-09-20 22:17:4865import common_util
Simran Basief83d6a2014-08-28 21:32:0166import devserver_constants
Chris Sosa47a7d4e2012-03-28 18:26:5567import downloader
Chris Sosa7cd23202013-10-16 00:22:5768import gsutil_util
Gilad Arnoldc65330c2012-09-20 22:17:4869import log_util
joychen3cb228e2013-06-12 19:13:1370import xbuddy
Gilad Arnoldc65330c2012-09-20 22:17:4871
Gilad Arnoldc65330c2012-09-20 22:17:4872# Module-local log function.
Chris Sosa6a3697f2013-01-30 00:44:4373def _Log(message, *args):
74 return log_util.LogWithTag('DEVSERVER', message, *args)
Chris Sosa0356d3b2010-09-16 22:46:2275
Dan Shiafd0e492015-05-27 21:23:5176try:
77 import psutil
78except ImportError:
79 # Ignore psutil import failure. This is for backwards compatibility, so
80 # "cros flash" can still update duts with build without psutil installed.
81 # The reason is that, during cros flash, local devserver code is copied over
82 # to DUT, and devserver will be running inside DUT to stage the build.
83 _Log('Python module psutil is not installed, devserver load data will not be '
84 'collected')
85 psutil = None
Dan Shi94dcbe82015-06-09 03:51:1386except OSError as e:
87 # Ignore error like following. psutil may not work properly in builder. Ignore
88 # the error as load information of devserver is not used in builder.
89 # OSError: [Errno 2] No such file or directory: '/dev/pts/0'
90 _Log('psutil is failed to be imported, error: %s. devserver load data will '
91 'not be collected.', e)
92 psutil = None
93
Dan Shi72b16132015-10-08 19:10:3394try:
95 import android_build
96except ImportError as e:
97 # Ignore android_build import failure. This is to support devserver running
98 # inside a ChromeOS device triggered by cros flash. Most ChromeOS test images
99 # do not have google-api-python-client module and they don't need to support
100 # Android updating, therefore, ignore the import failure here.
101 _Log('Import module android_build failed with error: %s', e)
102 android_build = None
Frank Farzan40160872011-12-13 02:39:18103
Chris Sosa417e55d2011-01-26 00:40:48104CACHED_ENTRIES = 12
Don Garrettf90edf02010-11-17 01:36:14105
Simran Basi4baad082013-02-14 21:39:18106TELEMETRY_FOLDER = 'telemetry_src'
107TELEMETRY_DEPS = ['dep-telemetry_dep.tar.bz2',
108 'dep-page_cycler_dep.tar.bz2',
Simran Basi0d078682013-03-22 23:40:04109 'dep-chrome_test.tar.bz2',
110 'dep-perf_data_dep.tar.bz2']
Simran Basi4baad082013-02-14 21:39:18111
Chris Sosa0356d3b2010-09-16 22:46:22112# Sets up global to share between classes.
[email protected]21a5ca32009-11-04 18:23:23113updater = None
[email protected]ded22402009-10-26 22:36:21114
J. Richard Barnette3d977b82013-04-23 18:05:19115# Log rotation parameters. These settings correspond to once a week
J. Richard Barnette6dfa5342013-06-04 18:48:56116# at midnight between Friday and Saturday, with about three months
117# of old logs kept for backup.
J. Richard Barnette3d977b82013-04-23 18:05:19118#
119# For more, see the documentation for
120# logging.handlers.TimedRotatingFileHandler
J. Richard Barnette6dfa5342013-06-04 18:48:56121_LOG_ROTATION_TIME = 'W4'
J. Richard Barnette3d977b82013-04-23 18:05:19122_LOG_ROTATION_BACKUP = 13
123
Dan Shiafd0e492015-05-27 21:23:51124# Number of seconds between the collection of disk and network IO counters.
125STATS_INTERVAL = 10.0
Frank Farzan40160872011-12-13 02:39:18126
Chris Sosa9164ca32012-03-28 18:04:50127class DevServerError(Exception):
Chris Sosa47a7d4e2012-03-28 18:26:55128 """Exception class used by this module."""
Chris Sosa47a7d4e2012-03-28 18:26:55129
130
Dan Shiafd0e492015-05-27 21:23:51131def require_psutil():
Gabe Black3b567202015-09-23 21:07:59132 """Decorator for functions require psutil to run."""
Dan Shiafd0e492015-05-27 21:23:51133 def deco_require_psutil(func):
134 """Wrapper of the decorator function.
135
Gabe Black3b567202015-09-23 21:07:59136 Args:
137 func: function to be called.
Dan Shiafd0e492015-05-27 21:23:51138 """
139 def func_require_psutil(*args, **kwargs):
140 """Decorator for functions require psutil to run.
141
142 If psutil is not installed, skip calling the function.
143
Gabe Black3b567202015-09-23 21:07:59144 Args:
145 *args: arguments for function to be called.
146 **kwargs: keyword arguments for function to be called.
Dan Shiafd0e492015-05-27 21:23:51147 """
148 if psutil:
149 return func(*args, **kwargs)
150 else:
151 _Log('Python module psutil is not installed. Function call %s is '
152 'skipped.' % func)
153 return func_require_psutil
154 return deco_require_psutil
155
156
Gabe Black3b567202015-09-23 21:07:59157def _canonicalize_archive_url(archive_url):
158 """Canonicalizes archive_url strings.
159
160 Raises:
161 DevserverError: if archive_url is not set.
162 """
163 if archive_url:
164 if not archive_url.startswith('gs://'):
165 raise DevServerError("Archive URL isn't from Google Storage (%s) ." %
166 archive_url)
167
168 return archive_url.rstrip('/')
169 else:
170 raise DevServerError("Must specify an archive_url in the request")
171
172
173def _canonicalize_local_path(local_path):
174 """Canonicalizes |local_path| strings.
175
176 Raises:
177 DevserverError: if |local_path| is not set.
178 """
179 # Restrict staging of local content to only files within the static
180 # directory.
181 local_path = os.path.abspath(local_path)
182 if not local_path.startswith(updater.static_dir):
183 raise DevServerError('Local path %s must be a subdirectory of the static'
184 ' directory: %s' % (local_path, updater.static_dir))
185
186 return local_path.rstrip('/')
187
188
189def _get_artifacts(kwargs):
190 """Returns a tuple of named and file artifacts given the stage rpc kwargs.
191
192 Raises:
193 DevserverError if no artifacts would be returned.
194 """
195 artifacts = kwargs.get('artifacts')
196 files = kwargs.get('files')
197 if not artifacts and not files:
198 raise DevServerError('No artifacts specified.')
199
200 # Note we NEED to coerce files to a string as we get raw unicode from
201 # cherrypy and we treat files as strings elsewhere in the code.
202 return (str(artifacts).split(',') if artifacts else [],
203 str(files).split(',') if files else [])
204
205
206def _get_downloader(kwargs):
207 """Returns the downloader based on passed in arguments.
208
209 Args:
210 kwargs: Keyword arguments for the request.
211 """
212 local_path = kwargs.get('local_path')
213 if local_path:
214 local_path = _canonicalize_local_path(local_path)
215
216 dl = None
217 if local_path:
218 dl = downloader.LocalDownloader(updater.static_dir, local_path)
219
220 # Only Android build requires argument build_id. If it's not set, assume
221 # the download request is for ChromeOS.
222 build_id = kwargs.get('build_id', None)
223 if not build_id:
224 archive_url = kwargs.get('archive_url')
225 if not archive_url and not local_path:
226 raise DevServerError('Requires archive_url or local_path to be '
227 'specified.')
228 if archive_url and local_path:
229 raise DevServerError('archive_url and local_path can not both be '
230 'specified.')
231 if not dl:
232 archive_url = _canonicalize_archive_url(archive_url)
233 dl = downloader.GoogleStorageDownloader(updater.static_dir, archive_url)
234 elif not dl:
235 target = kwargs.get('target', None)
Dan Shi72b16132015-10-08 19:10:33236 branch = kwargs.get('branch', None)
237 if not target or not branch:
238 raise DevServerError(
239 'Both target and branch must be specified for Android build.')
240 dl = downloader.AndroidBuildDownloader(updater.static_dir, branch, build_id,
241 target)
Gabe Black3b567202015-09-23 21:07:59242
243 return dl
244
245
246def _get_downloader_and_factory(kwargs):
247 """Returns the downloader and artifact factory based on passed in arguments.
248
249 Args:
250 kwargs: Keyword arguments for the request.
251 """
252 artifacts, files = _get_artifacts(kwargs)
253 dl = _get_downloader(kwargs)
254
255 if (isinstance(dl, downloader.GoogleStorageDownloader) or
256 isinstance(dl, downloader.LocalDownloader)):
257 factory_class = build_artifact.ChromeOSArtifactFactory
Dan Shi72b16132015-10-08 19:10:33258 elif isinstance(dl, downloader.AndroidBuildDownloader):
Gabe Black3b567202015-09-23 21:07:59259 factory_class = build_artifact.AndroidArtifactFactory
260 else:
261 raise DevServerError('Unrecognized value for downloader type: %s' %
262 type(dl))
263
264 factory = factory_class(dl.GetBuildDir(), artifacts, files, dl.GetBuild())
265
266 return dl, factory
267
268
Scott Zawalski4647ce62012-01-03 22:17:28269def _LeadingWhiteSpaceCount(string):
270 """Count the amount of leading whitespace in a string.
271
272 Args:
273 string: The string to count leading whitespace in.
Don Garrettf84631a2014-01-08 02:21:26274
Scott Zawalski4647ce62012-01-03 22:17:28275 Returns:
276 number of white space chars before characters start.
277 """
Gabe Black3b567202015-09-23 21:07:59278 matched = re.match(r'^\s+', string)
Scott Zawalski4647ce62012-01-03 22:17:28279 if matched:
280 return len(matched.group())
281
282 return 0
283
284
285def _PrintDocStringAsHTML(func):
286 """Make a functions docstring somewhat HTML style.
287
288 Args:
289 func: The function to return the docstring from.
Don Garrettf84631a2014-01-08 02:21:26290
Scott Zawalski4647ce62012-01-03 22:17:28291 Returns:
292 A string that is somewhat formated for a web browser.
293 """
294 # TODO(scottz): Make this parse Args/Returns in a prettier way.
295 # Arguments could be bolded and indented etc.
296 html_doc = []
297 for line in func.__doc__.splitlines():
298 leading_space = _LeadingWhiteSpaceCount(line)
299 if leading_space > 0:
Chris Sosa47a7d4e2012-03-28 18:26:55300 line = '&nbsp;' * leading_space + line
Scott Zawalski4647ce62012-01-03 22:17:28301
302 html_doc.append('<BR>%s' % line)
303
304 return '\n'.join(html_doc)
305
306
Simran Basief83d6a2014-08-28 21:32:01307def _GetUpdateTimestampHandler(static_dir):
308 """Returns a handler to update directory staged.timestamp.
309
310 This handler resets the stage.timestamp whenever static content is accessed.
311
312 Args:
313 static_dir: Directory from which static content is being staged.
314
315 Returns:
316 A cherrypy handler to update the timestamp of accessed content.
317 """
318 def UpdateTimestampHandler():
319 if not '404' in cherrypy.response.status:
320 build_match = re.match(devserver_constants.STAGED_BUILD_REGEX,
321 cherrypy.request.path_info)
322 if build_match:
323 build_dir = os.path.join(static_dir, build_match.group('build'))
324 downloader.Downloader.TouchTimestampForStaged(build_dir)
325 return UpdateTimestampHandler
326
327
Chris Sosa7c931362010-10-12 02:49:01328def _GetConfig(options):
329 """Returns the configuration for the devserver."""
Mandeep Singh Baines38dcdda2012-12-08 01:55:33330
Mandeep Singh Baines38dcdda2012-12-08 01:55:33331 socket_host = '::'
Yu-Ju Hongc8d4af32013-11-12 23:14:26332 # Fall back to IPv4 when python is not configured with IPv6.
333 if not socket.has_ipv6:
Mandeep Singh Baines38dcdda2012-12-08 01:55:33334 socket_host = '0.0.0.0'
335
Simran Basief83d6a2014-08-28 21:32:01336 # Adds the UpdateTimestampHandler to cherrypy's tools. This tools executes
337 # on the on_end_resource hook. This hook is called once processing is
338 # complete and the response is ready to be returned.
339 cherrypy.tools.update_timestamp = cherrypy.Tool(
340 'on_end_resource', _GetUpdateTimestampHandler(options.static_dir))
341
Gabe Black3b567202015-09-23 21:07:59342 base_config = {'global':
343 {'server.log_request_headers': True,
344 'server.protocol_version': 'HTTP/1.1',
345 'server.socket_host': socket_host,
346 'server.socket_port': int(options.port),
347 'response.timeout': 6000,
348 'request.show_tracebacks': True,
349 'server.socket_timeout': 60,
350 'server.thread_pool': 2,
351 'engine.autoreload.on': False,
352 },
353 '/api':
354 {
355 # Gets rid of cherrypy parsing post file for args.
356 'request.process_request_body': False,
357 },
358 '/build':
359 {'response.timeout': 100000,
360 },
361 '/update':
362 {
363 # Gets rid of cherrypy parsing post file for args.
364 'request.process_request_body': False,
365 'response.timeout': 10000,
366 },
367 # Sets up the static dir for file hosting.
368 '/static':
369 {'tools.staticdir.dir': options.static_dir,
370 'tools.staticdir.on': True,
371 'response.timeout': 10000,
372 'tools.update_timestamp.on': True,
373 },
374 }
Chris Sosa5f118ef2012-07-12 18:37:50375 if options.production:
Alex Miller93beca52013-07-31 02:25:09376 base_config['global'].update({'server.thread_pool': 150})
Chris Sosa7cd23202013-10-16 00:22:57377 # TODO(sosa): Do this more cleanly.
378 gsutil_util.GSUTIL_ATTEMPTS = 5
Scott Zawalski1c5e7cd2012-02-27 18:12:52379
Chris Sosa7c931362010-10-12 02:49:01380 return base_config
[email protected]64244662009-11-12 00:52:08381
Darin Petkove17164a2010-08-11 20:24:41382
Gilad Arnoldd5ebaaa2012-10-02 18:52:38383def _GetRecursiveMemberObject(root, member_list):
384 """Returns an object corresponding to a nested member list.
385
386 Args:
387 root: the root object to search
388 member_list: list of nested members to search
Don Garrettf84631a2014-01-08 02:21:26389
Gilad Arnoldd5ebaaa2012-10-02 18:52:38390 Returns:
391 An object corresponding to the member name list; None otherwise.
392 """
393 for member in member_list:
394 next_root = root.__class__.__dict__.get(member)
395 if not next_root:
396 return None
397 root = next_root
398 return root
399
400
401def _IsExposed(name):
402 """Returns True iff |name| has an `exposed' attribute and it is set."""
403 return hasattr(name, 'exposed') and name.exposed
404
405
Gilad Arnold748c8322012-10-12 16:51:35406def _GetExposedMethod(root, nested_member, ignored=None):
Gilad Arnoldd5ebaaa2012-10-02 18:52:38407 """Returns a CherryPy-exposed method, if such exists.
408
409 Args:
410 root: the root object for searching
411 nested_member: a slash-joined path to the nested member
412 ignored: method paths to be ignored
Don Garrettf84631a2014-01-08 02:21:26413
Gilad Arnoldd5ebaaa2012-10-02 18:52:38414 Returns:
415 A function object corresponding to the path defined by |member_list| from
416 the |root| object, if the function is exposed and not ignored; None
417 otherwise.
418 """
Gilad Arnold748c8322012-10-12 16:51:35419 method = (not (ignored and nested_member in ignored) and
Gilad Arnoldd5ebaaa2012-10-02 18:52:38420 _GetRecursiveMemberObject(root, nested_member.split('/')))
Gabe Black3b567202015-09-23 21:07:59421 if method and type(method) == types.FunctionType and _IsExposed(method):
Gilad Arnoldd5ebaaa2012-10-02 18:52:38422 return method
423
424
Gilad Arnold748c8322012-10-12 16:51:35425def _FindExposedMethods(root, prefix, unlisted=None):
Gilad Arnoldd5ebaaa2012-10-02 18:52:38426 """Finds exposed CherryPy methods.
427
428 Args:
429 root: the root object for searching
430 prefix: slash-joined chain of members leading to current object
431 unlisted: URLs to be excluded regardless of their exposed status
Don Garrettf84631a2014-01-08 02:21:26432
Gilad Arnoldd5ebaaa2012-10-02 18:52:38433 Returns:
434 List of exposed URLs that are not unlisted.
435 """
436 method_list = []
437 for member in sorted(root.__class__.__dict__.keys()):
438 prefixed_member = prefix + '/' + member if prefix else member
Gilad Arnold748c8322012-10-12 16:51:35439 if unlisted and prefixed_member in unlisted:
Gilad Arnoldd5ebaaa2012-10-02 18:52:38440 continue
441 member_obj = root.__class__.__dict__[member]
442 if _IsExposed(member_obj):
443 if type(member_obj) == types.FunctionType:
444 method_list.append(prefixed_member)
445 else:
446 method_list += _FindExposedMethods(
447 member_obj, prefixed_member, unlisted)
448 return method_list
449
450
Dale Curtisc9aaf3a2011-08-09 22:47:40451class ApiRoot(object):
452 """RESTful API for Dev Server information."""
453 exposed = True
454
455 @cherrypy.expose
456 def hostinfo(self, ip):
457 """Returns a JSON dictionary containing information about the given ip.
458
Gilad Arnold1b908392012-10-05 18:36:27459 Args:
460 ip: address of host whose info is requested
Don Garrettf84631a2014-01-08 02:21:26461
Gilad Arnold1b908392012-10-05 18:36:27462 Returns:
463 A JSON dictionary containing all or some of the following fields:
464 last_event_type (int): last update event type received
465 last_event_status (int): last update event status received
466 last_known_version (string): last known version reported in update ping
467 forced_update_label (string): update label to force next update ping to
468 use, set by setnextupdate
469 See the OmahaEvent class in update_engine/omaha_request_action.h for
470 event type and status code definitions. If the ip does not exist an empty
471 string is returned.
Dale Curtisc9aaf3a2011-08-09 22:47:40472
Gilad Arnold1b908392012-10-05 18:36:27473 Example URL:
474 http://myhost/api/hostinfo?ip=192.168.1.5
475 """
Dale Curtisc9aaf3a2011-08-09 22:47:40476 return updater.HandleHostInfoPing(ip)
477
478 @cherrypy.expose
Gilad Arnold286a0062012-01-12 21:47:02479 def hostlog(self, ip):
Gilad Arnold1b908392012-10-05 18:36:27480 """Returns a JSON object containing a log of host event.
481
482 Args:
483 ip: address of host whose event log is requested, or `all'
Don Garrettf84631a2014-01-08 02:21:26484
Gilad Arnold1b908392012-10-05 18:36:27485 Returns:
486 A JSON encoded list (log) of dictionaries (events), each of which
487 containing a `timestamp' and other event fields, as described under
488 /api/hostinfo.
489
490 Example URL:
491 http://myhost/api/hostlog?ip=192.168.1.5
492 """
Gilad Arnold286a0062012-01-12 21:47:02493 return updater.HandleHostLogPing(ip)
494
495 @cherrypy.expose
Dale Curtisc9aaf3a2011-08-09 22:47:40496 def setnextupdate(self, ip):
497 """Allows the response to the next update ping from a host to be set.
498
499 Takes the IP of the host and an update label as normally provided to the
Gilad Arnold1b908392012-10-05 18:36:27500 /update command.
501 """
Dale Curtisc9aaf3a2011-08-09 22:47:40502 body_length = int(cherrypy.request.headers['Content-Length'])
503 label = cherrypy.request.rfile.read(body_length)
504
505 if label:
506 label = label.strip()
507 if label:
508 return updater.HandleSetUpdatePing(ip, label)
Chris Sosa4b951602014-04-10 03:26:07509 raise common_util.DevServerHTTPError(400, 'No label provided.')
Dale Curtisc9aaf3a2011-08-09 22:47:40510
511
Gilad Arnold55a2a372012-10-02 16:46:32512 @cherrypy.expose
Don Garrettf84631a2014-01-08 02:21:26513 def fileinfo(self, *args):
Gilad Arnold55a2a372012-10-02 16:46:32514 """Returns information about a given staged file.
515
516 Args:
Don Garrettf84631a2014-01-08 02:21:26517 args: path to the file inside the server's static staging directory
518
Gilad Arnold55a2a372012-10-02 16:46:32519 Returns:
520 A JSON encoded dictionary with information about the said file, which may
521 contain the following keys/values:
Gilad Arnold1b908392012-10-05 18:36:27522 size (int): the file size in bytes
523 sha1 (string): a base64 encoded SHA1 hash
524 sha256 (string): a base64 encoded SHA256 hash
525
526 Example URL:
527 http://myhost/api/fileinfo/some/path/to/file
Gilad Arnold55a2a372012-10-02 16:46:32528 """
Don Garrettf84631a2014-01-08 02:21:26529 file_path = os.path.join(updater.static_dir, *args)
Gilad Arnold55a2a372012-10-02 16:46:32530 if not os.path.exists(file_path):
531 raise DevServerError('file not found: %s' % file_path)
532 try:
533 file_size = os.path.getsize(file_path)
534 file_sha1 = common_util.GetFileSha1(file_path)
535 file_sha256 = common_util.GetFileSha256(file_path)
536 except os.error, e:
537 raise DevServerError('failed to get info for file %s: %s' %
Gilad Arnolde74b3812013-04-22 18:27:38538 (file_path, e))
539
540 is_delta = autoupdate.Autoupdate.IsDeltaFormatFile(file_path)
541
542 return json.dumps({
543 autoupdate.Autoupdate.SIZE_ATTR: file_size,
544 autoupdate.Autoupdate.SHA1_ATTR: file_sha1,
545 autoupdate.Autoupdate.SHA256_ATTR: file_sha256,
546 autoupdate.Autoupdate.ISDELTA_ATTR: is_delta
547 })
Gilad Arnold55a2a372012-10-02 16:46:32548
Chris Sosa76e44b92013-01-31 20:11:38549
David Rochberg7c79a812011-01-19 19:24:45550class DevServerRoot(object):
Chris Sosa7c931362010-10-12 02:49:01551 """The Root Class for the Dev Server.
552
553 CherryPy works as follows:
554 For each method in this class, cherrpy interprets root/path
555 as a call to an instance of DevServerRoot->method_name. For example,
556 a call to http://myhost/build will call build. CherryPy automatically
557 parses http args and places them as keyword arguments in each method.
558 For paths http://myhost/update/dir1/dir2, you can use *args so that
559 cherrypy uses the update method and puts the extra paths in args.
560 """
Gilad Arnoldf8f769f2012-09-24 15:43:01561 # Method names that should not be listed on the index page.
562 _UNLISTED_METHODS = ['index', 'doc']
563
Dale Curtisc9aaf3a2011-08-09 22:47:40564 api = ApiRoot()
Chris Sosa7c931362010-10-12 02:49:01565
Dan Shi59ae7092013-06-04 21:37:27566 # Number of threads that devserver is staging images.
567 _staging_thread_count = 0
568 # Lock used to lock increasing/decreasing count.
569 _staging_thread_count_lock = threading.Lock()
570
Dan Shiafd0e492015-05-27 21:23:51571 @require_psutil()
572 def _refresh_io_stats(self):
573 """A call running in a thread to update IO stats periodically."""
574 prev_disk_io_counters = psutil.disk_io_counters()
575 prev_network_io_counters = psutil.net_io_counters()
576 prev_read_time = time.time()
577 while True:
578 time.sleep(STATS_INTERVAL)
579 now = time.time()
580 interval = now - prev_read_time
581 prev_read_time = now
582 # Disk IO is for all disks.
583 disk_io_counters = psutil.disk_io_counters()
584 network_io_counters = psutil.net_io_counters()
585
586 self.disk_read_bytes_per_sec = (
587 disk_io_counters.read_bytes -
588 prev_disk_io_counters.read_bytes)/interval
589 self.disk_write_bytes_per_sec = (
590 disk_io_counters.write_bytes -
591 prev_disk_io_counters.write_bytes)/interval
592 prev_disk_io_counters = disk_io_counters
593
594 self.network_sent_bytes_per_sec = (
595 network_io_counters.bytes_sent -
596 prev_network_io_counters.bytes_sent)/interval
597 self.network_recv_bytes_per_sec = (
598 network_io_counters.bytes_recv -
599 prev_network_io_counters.bytes_recv)/interval
600 prev_network_io_counters = network_io_counters
601
602 @require_psutil()
603 def _start_io_stat_thread(self):
Gabe Black3b567202015-09-23 21:07:59604 """Start the thread to collect IO stats."""
Dan Shiafd0e492015-05-27 21:23:51605 thread = threading.Thread(target=self._refresh_io_stats)
606 thread.daemon = True
607 thread.start()
608
joychen3cb228e2013-06-12 19:13:13609 def __init__(self, _xbuddy):
Nick Sanders7dcaa2e2011-08-04 22:20:41610 self._builder = None
Simran Basi4baad082013-02-14 21:39:18611 self._telemetry_lock_dict = common_util.LockDict()
joychen3cb228e2013-06-12 19:13:13612 self._xbuddy = _xbuddy
David Rochberg7c79a812011-01-19 19:24:45613
Dan Shiafd0e492015-05-27 21:23:51614 # Cache of disk IO stats, a thread refresh the stats every 10 seconds.
615 # lock is not used for these variables as the only thread writes to these
616 # variables is _refresh_io_stats.
617 self.disk_read_bytes_per_sec = 0
618 self.disk_write_bytes_per_sec = 0
619 # Cache of network IO stats.
620 self.network_sent_bytes_per_sec = 0
621 self.network_recv_bytes_per_sec = 0
622 self._start_io_stat_thread()
623
Dale Curtisc9aaf3a2011-08-09 22:47:40624 @cherrypy.expose
David Rochberg7c79a812011-01-19 19:24:45625 def build(self, board, pkg, **kwargs):
Chris Sosa7c931362010-10-12 02:49:01626 """Builds the package specified."""
Nick Sanders7dcaa2e2011-08-04 22:20:41627 import builder
628 if self._builder is None:
629 self._builder = builder.Builder()
David Rochberg7c79a812011-01-19 19:24:45630 return self._builder.Build(board, pkg, kwargs)
Chris Sosa7c931362010-10-12 02:49:01631
Dale Curtisc9aaf3a2011-08-09 22:47:40632 @cherrypy.expose
Dan Shif8eb0d12013-08-02 00:52:06633 def is_staged(self, **kwargs):
634 """Check if artifacts have been downloaded.
635
Chris Sosa6b0c6172013-08-06 00:01:33636 async: True to return without waiting for download to complete.
637 artifacts: Comma separated list of named artifacts to download.
638 These are defined in artifact_info and have their implementation
639 in build_artifact.py.
640 files: Comma separated list of file artifacts to stage. These
641 will be available as is in the corresponding static directory with no
642 custom post-processing.
643
644 returns: True of all artifacts are staged.
Dan Shif8eb0d12013-08-02 00:52:06645
646 Example:
647 To check if autotest and test_suites are staged:
648 http://devserver_url:<port>/is_staged?archive_url=gs://your_url/path&
649 artifacts=autotest,test_suites
650 """
Gabe Black3b567202015-09-23 21:07:59651 dl, factory = _get_downloader_and_factory(kwargs)
652 return str(dl.IsStaged(factory))
Dan Shi59ae7092013-06-04 21:37:27653
Chris Sosa76e44b92013-01-31 20:11:38654 @cherrypy.expose
Prashanth Ba06d2d22014-03-07 23:35:19655 def list_image_dir(self, **kwargs):
656 """Take an archive url and list the contents in its staged directory.
657
658 Args:
659 kwargs:
660 archive_url: Google Storage URL for the build.
661
662 Example:
663 To list the contents of where this devserver should have staged
664 gs://image-archive/<board>-release/<build> call:
665 http://devserver_url:<port>/list_image_dir?archive_url=<gs://..>
666
667 Returns:
668 A string with information about the contents of the image directory.
669 """
Gabe Black3b567202015-09-23 21:07:59670 dl = _get_downloader(kwargs)
Prashanth Ba06d2d22014-03-07 23:35:19671 try:
Gabe Black3b567202015-09-23 21:07:59672 image_dir_contents = dl.ListBuildDir()
Prashanth Ba06d2d22014-03-07 23:35:19673 except build_artifact.ArtifactDownloadError as e:
674 return 'Cannot list the contents of staged artifacts. %s' % e
675 if not image_dir_contents:
Gabe Black3b567202015-09-23 21:07:59676 return '%s has not been staged on this devserver.' % dl.DescribeSource()
Prashanth Ba06d2d22014-03-07 23:35:19677 return image_dir_contents
678
679 @cherrypy.expose
Chris Sosa76e44b92013-01-31 20:11:38680 def stage(self, **kwargs):
Gabe Black3b567202015-09-23 21:07:59681 """Downloads and caches build artifacts.
Chris Sosa76e44b92013-01-31 20:11:38682
Gabe Black3b567202015-09-23 21:07:59683 Downloads and caches build artifacts, possibly from a Google Storage URL,
Dan Shi72b16132015-10-08 19:10:33684 or from Android's build server. Returns once these have been downloaded
Gabe Black3b567202015-09-23 21:07:59685 on the devserver. A call to this will attempt to cache non-specified
686 artifacts in the background for the given from the given URL following
687 the principle of spatial locality. Spatial locality of different
Chris Sosa76e44b92013-01-31 20:11:38688 artifacts is explicitly defined in the build_artifact module.
689
690 These artifacts will then be available from the static/ sub-directory of
691 the devserver.
692
693 Args:
694 archive_url: Google Storage URL for the build.
Simran Basi4243a862014-12-12 20:48:33695 local_path: Local path for the build.
Dan Shif8eb0d12013-08-02 00:52:06696 async: True to return without waiting for download to complete.
Chris Sosa6b0c6172013-08-06 00:01:33697 artifacts: Comma separated list of named artifacts to download.
698 These are defined in artifact_info and have their implementation
699 in build_artifact.py.
700 files: Comma separated list of files to stage. These
701 will be available as is in the corresponding static directory with no
702 custom post-processing.
Chris Sosa76e44b92013-01-31 20:11:38703
704 Example:
705 To download the autotest and test suites tarballs:
706 http://devserver_url:<port>/stage?archive_url=gs://your_url/path&
707 artifacts=autotest,test_suites
708 To download the full update payload:
709 http://devserver_url:<port>/stage?archive_url=gs://your_url/path&
710 artifacts=full_payload
Chris Sosa6b0c6172013-08-06 00:01:33711 To download just a file called blah.bin:
712 http://devserver_url:<port>/stage?archive_url=gs://your_url/path&
713 files=blah.bin
Chris Sosa76e44b92013-01-31 20:11:38714
715 For both these examples, one could find these artifacts at:
joychened64b222013-06-21 23:39:34716 http://devserver_url:<port>/static/<relative_path>*
Chris Sosa76e44b92013-01-31 20:11:38717
718 Note for this example, relative path is the archive_url stripped of its
719 basename i.e. path/ in the examples above. Specific example:
720
721 gs://chromeos-image-archive/x86-mario-release/R26-3920.0.0
722
723 Will get staged to:
724
joychened64b222013-06-21 23:39:34725 http://devserver_url:<port>/static/x86-mario-release/R26-3920.0.0
Chris Sosa76e44b92013-01-31 20:11:38726 """
Gabe Black3b567202015-09-23 21:07:59727 dl, factory = _get_downloader_and_factory(kwargs)
728
Dan Shi59ae7092013-06-04 21:37:27729 with DevServerRoot._staging_thread_count_lock:
730 DevServerRoot._staging_thread_count += 1
731 try:
Gabe Black3b567202015-09-23 21:07:59732 async = kwargs.get('async', False)
733 dl.Download(factory, async=async)
Dan Shi59ae7092013-06-04 21:37:27734 finally:
735 with DevServerRoot._staging_thread_count_lock:
736 DevServerRoot._staging_thread_count -= 1
Chris Sosa76e44b92013-01-31 20:11:38737 return 'Success'
Chris Sosacde6bf42012-06-01 01:36:39738
739 @cherrypy.expose
Simran Basi4baad082013-02-14 21:39:18740 def setup_telemetry(self, **kwargs):
741 """Extracts and sets up telemetry
742
743 This method goes through the telemetry deps packages, and stages them on
744 the devserver to be used by the drones and the telemetry tests.
745
746 Args:
747 archive_url: Google Storage URL for the build.
748
749 Returns:
750 Path to the source folder for the telemetry codebase once it is staged.
751 """
Gabe Black3b567202015-09-23 21:07:59752 dl = _get_downloader(kwargs)
Simran Basi4baad082013-02-14 21:39:18753
Gabe Black3b567202015-09-23 21:07:59754 build_path = dl.GetBuildDir()
Simran Basi4baad082013-02-14 21:39:18755 deps_path = os.path.join(build_path, 'autotest/packages')
756 telemetry_path = os.path.join(build_path, TELEMETRY_FOLDER)
757 src_folder = os.path.join(telemetry_path, 'src')
758
759 with self._telemetry_lock_dict.lock(telemetry_path):
760 if os.path.exists(src_folder):
761 # Telemetry is already fully stage return
762 return src_folder
763
764 common_util.MkDirP(telemetry_path)
765
766 # Copy over the required deps tar balls to the telemetry directory.
767 for dep in TELEMETRY_DEPS:
768 dep_path = os.path.join(deps_path, dep)
Simran Basi0d078682013-03-22 23:40:04769 if not os.path.exists(dep_path):
770 # This dep does not exist (could be new), do not extract it.
771 continue
Simran Basi4baad082013-02-14 21:39:18772 try:
773 common_util.ExtractTarball(dep_path, telemetry_path)
774 except common_util.CommonUtilError as e:
775 shutil.rmtree(telemetry_path)
776 raise DevServerError(str(e))
777
778 # By default all the tarballs extract to test_src but some parts of
779 # the telemetry code specifically hardcoded to exist inside of 'src'.
780 test_src = os.path.join(telemetry_path, 'test_src')
781 try:
782 shutil.move(test_src, src_folder)
783 except shutil.Error:
784 # This can occur if src_folder already exists. Remove and retry move.
785 shutil.rmtree(src_folder)
Gabe Black3b567202015-09-23 21:07:59786 raise DevServerError(
787 'Failure in telemetry setup for build %s. Appears that the '
788 'test_src to src move failed.' % dl.GetBuild())
Simran Basi4baad082013-02-14 21:39:18789
790 return src_folder
791
792 @cherrypy.expose
Chris Sosa76e44b92013-01-31 20:11:38793 def symbolicate_dump(self, minidump, **kwargs):
Chris Masone816e38c2012-05-02 19:22:36794 """Symbolicates a minidump using pre-downloaded symbols, returns it.
795
796 Callers will need to POST to this URL with a body of MIME-type
797 "multipart/form-data".
798 The body should include a single argument, 'minidump', containing the
799 binary-formatted minidump to symbolicate.
800
Chris Masone816e38c2012-05-02 19:22:36801 Args:
Chris Sosa76e44b92013-01-31 20:11:38802 archive_url: Google Storage URL for the build.
Chris Masone816e38c2012-05-02 19:22:36803 minidump: The binary minidump file to symbolicate.
804 """
Gabe Black3b567202015-09-23 21:07:59805 kwargs['artifacts'] = 'symbols'
806 dl = _get_downloader(kwargs)
807
Chris Sosa76e44b92013-01-31 20:11:38808 # Ensure the symbols have been staged.
Gabe Black3b567202015-09-23 21:07:59809 if self.stage(**kwargs) != 'Success':
810 raise DevServerError('Failed to stage symbols for %s' %
811 dl.DescribeSource())
Chris Sosa76e44b92013-01-31 20:11:38812
Chris Masone816e38c2012-05-02 19:22:36813 to_return = ''
814 with tempfile.NamedTemporaryFile() as local:
815 while True:
816 data = minidump.file.read(8192)
817 if not data:
818 break
819 local.write(data)
Chris Sosa76e44b92013-01-31 20:11:38820
Chris Masone816e38c2012-05-02 19:22:36821 local.flush()
Chris Sosa76e44b92013-01-31 20:11:38822
Gabe Black3b567202015-09-23 21:07:59823 symbols_directory = os.path.join(dl.GetBuildDir(), 'debug', 'breakpad')
Chris Sosa76e44b92013-01-31 20:11:38824
825 stackwalk = subprocess.Popen(
826 ['minidump_stackwalk', local.name, symbols_directory],
827 stdout=subprocess.PIPE, stderr=subprocess.PIPE)
828
Chris Masone816e38c2012-05-02 19:22:36829 to_return, error_text = stackwalk.communicate()
830 if stackwalk.returncode != 0:
831 raise DevServerError("Can't generate stack trace: %s (rc=%d)" % (
832 error_text, stackwalk.returncode))
833
834 return to_return
835
836 @cherrypy.expose
Don Garrettf84631a2014-01-08 02:21:26837 def latestbuild(self, **kwargs):
Scott Zawalski16954532012-03-20 19:31:36838 """Return a string representing the latest build for a given target.
839
840 Args:
841 target: The build target, typically a combination of the board and the
842 type of build e.g. x86-mario-release.
843 milestone: The milestone to filter builds on. E.g. R16. Optional, if not
844 provided the latest RXX build will be returned.
Don Garrettf84631a2014-01-08 02:21:26845
Scott Zawalski16954532012-03-20 19:31:36846 Returns:
847 A string representation of the latest build if one exists, i.e.
848 R19-1993.0.0-a1-b1480.
849 An empty string if no latest could be found.
850 """
Don Garrettf84631a2014-01-08 02:21:26851 if not kwargs:
Scott Zawalski16954532012-03-20 19:31:36852 return _PrintDocStringAsHTML(self.latestbuild)
853
Don Garrettf84631a2014-01-08 02:21:26854 if 'target' not in kwargs:
Chris Sosa4b951602014-04-10 03:26:07855 raise common_util.DevServerHTTPError(500, 'Error: target= is required!')
Scott Zawalski16954532012-03-20 19:31:36856 try:
Gilad Arnoldc65330c2012-09-20 22:17:48857 return common_util.GetLatestBuildVersion(
Don Garrettf84631a2014-01-08 02:21:26858 updater.static_dir, kwargs['target'],
859 milestone=kwargs.get('milestone'))
Gilad Arnold17fe03d2012-10-02 17:05:01860 except common_util.CommonUtilError as errmsg:
Chris Sosa4b951602014-04-10 03:26:07861 raise common_util.DevServerHTTPError(500, str(errmsg))
Scott Zawalski16954532012-03-20 19:31:36862
863 @cherrypy.expose
Don Garrettf84631a2014-01-08 02:21:26864 def controlfiles(self, **kwargs):
Scott Zawalski4647ce62012-01-03 22:17:28865 """Return a control file or a list of all known control files.
866
867 Example URL:
868 To List all control files:
beepsbd337242013-07-10 05:44:06869 http://dev-server/controlfiles?suite_name=&build=daisy_spring-release/R29-4279.0.0
870 To List all control files for, say, the bvt suite:
871 http://dev-server/controlfiles?suite_name=bvt&build=daisy_spring-release/R29-4279.0.0
Scott Zawalski4647ce62012-01-03 22:17:28872 To return the contents of a path:
Scott Zawalski84a39c92012-01-13 20:12:42873 http://dev-server/controlfiles?board=x86-alex-release&build=R18-1514.0.0&control_path=client/sleeptest/control
Scott Zawalski4647ce62012-01-03 22:17:28874
875 Args:
Scott Zawalski84a39c92012-01-13 20:12:42876 build: The build i.e. x86-alex-release/R18-1514.0.0-a1-b1450.
Scott Zawalski4647ce62012-01-03 22:17:28877 control_path: If you want the contents of a control file set this
878 to the path. E.g. client/site_tests/sleeptest/control
879 Optional, if not provided return a list of control files is returned.
beepsbd337242013-07-10 05:44:06880 suite_name: If control_path is not specified but a suite_name is
881 specified, list the control files belonging to that suite instead of
882 all control files. The empty string for suite_name will list all control
883 files for the build.
Don Garrettf84631a2014-01-08 02:21:26884
Scott Zawalski4647ce62012-01-03 22:17:28885 Returns:
886 Contents of a control file if control_path is provided.
887 A list of control files if no control_path is provided.
888 """
Don Garrettf84631a2014-01-08 02:21:26889 if not kwargs:
Scott Zawalski4647ce62012-01-03 22:17:28890 return _PrintDocStringAsHTML(self.controlfiles)
891
Don Garrettf84631a2014-01-08 02:21:26892 if 'build' not in kwargs:
Chris Sosa4b951602014-04-10 03:26:07893 raise common_util.DevServerHTTPError(500, 'Error: build= is required!')
Scott Zawalski4647ce62012-01-03 22:17:28894
Don Garrettf84631a2014-01-08 02:21:26895 if 'control_path' not in kwargs:
896 if 'suite_name' in kwargs and kwargs['suite_name']:
beepsbd337242013-07-10 05:44:06897 return common_util.GetControlFileListForSuite(
Don Garrettf84631a2014-01-08 02:21:26898 updater.static_dir, kwargs['build'], kwargs['suite_name'])
beepsbd337242013-07-10 05:44:06899 else:
900 return common_util.GetControlFileList(
Don Garrettf84631a2014-01-08 02:21:26901 updater.static_dir, kwargs['build'])
Scott Zawalski4647ce62012-01-03 22:17:28902 else:
Gilad Arnoldc65330c2012-09-20 22:17:48903 return common_util.GetControlFile(
Don Garrettf84631a2014-01-08 02:21:26904 updater.static_dir, kwargs['build'], kwargs['control_path'])
Frank Farzan40160872011-12-13 02:39:18905
906 @cherrypy.expose
Simran Basi99e63c02014-05-20 17:39:52907 def xbuddy_translate(self, *args, **kwargs):
Yu-Ju Hong1bdb7a92014-04-10 23:02:11908 """Translates an xBuddy path to a real path to artifact if it exists.
909
910 Args:
Simran Basi99e63c02014-05-20 17:39:52911 args: An xbuddy path in the form of {local|remote}/build_id/artifact.
912 Local searches the devserver's static directory. Remote searches a
913 Google Storage image archive.
914
915 Kwargs:
916 image_dir: Google Storage image archive to search in if requesting a
917 remote artifact. If none uses the default bucket.
Yu-Ju Hong1bdb7a92014-04-10 23:02:11918
919 Returns:
Simran Basi99e63c02014-05-20 17:39:52920 String in the format of build_id/artifact as stored on the local server
921 or in Google Storage.
Yu-Ju Hong1bdb7a92014-04-10 23:02:11922 """
Simran Basi99e63c02014-05-20 17:39:52923 build_id, filename = self._xbuddy.Translate(
Gabe Black3b567202015-09-23 21:07:59924 args, image_dir=kwargs.get('image_dir'))
Yu-Ju Hong1bdb7a92014-04-10 23:02:11925 response = os.path.join(build_id, filename)
926 _Log('Path translation requested, returning: %s', response)
927 return response
928
929 @cherrypy.expose
joycheneaf4cfc2013-07-02 15:38:57930 def xbuddy(self, *args, **kwargs):
931 """The full xBuddy call, returns resource specified by path_parts.
joychen3cb228e2013-06-12 19:13:13932
933 Args:
joycheneaf4cfc2013-07-02 15:38:57934 path_parts: the path following xbuddy/ in the call url is split into the
joychen121fc9b2013-08-02 21:30:30935 components of the path. The path can be understood as
936 "{local|remote}/build_id/artifact" where build_id is composed of
937 "board/version."
joycheneaf4cfc2013-07-02 15:38:57938
joychen121fc9b2013-08-02 21:30:30939 The first path element is optional, and can be "remote" or "local"
940 If local (the default), devserver will not attempt to access Google
941 Storage, and will only search the static directory for the files.
942 If remote, devserver will try to obtain the artifact off GS if it's
943 not found locally.
944 The board is the familiar board name, optionally suffixed.
945 The version can be the google storage version number, and may also be
946 any of a number of xBuddy defined version aliases that will be
947 translated into the latest built image that fits the description.
948 Defaults to latest.
949 The artifact is one of a number of image or artifact aliases used by
950 xbuddy, defined in xbuddy:ALIASES. Defaults to test.
joycheneaf4cfc2013-07-02 15:38:57951
952 Kwargs:
Yu-Ju Hong51495eb2013-12-13 01:08:43953 for_update: {true|false}
954 if true, pregenerates the update payloads for the image,
955 and returns the update uri to pass to the
956 update_engine_client.
joychen3cb228e2013-06-12 19:13:13957 return_dir: {true|false}
958 if set to true, returns the url to the update.gz
Yu-Ju Hong51495eb2013-12-13 01:08:43959 relative_path: {true|false}
960 if set to true, returns the relative path to the payload
961 directory from static_dir.
joychen3cb228e2013-06-12 19:13:13962 Example URL:
joycheneaf4cfc2013-07-02 15:38:57963 http://host:port/xbuddy/x86-generic/R26-4000.0.0/test
joychen3cb228e2013-06-12 19:13:13964 or
joycheneaf4cfc2013-07-02 15:38:57965 http://host:port/xbuddy/x86-generic/R26-4000.0.0/test?return_dir=true
joychen3cb228e2013-06-12 19:13:13966
967 Returns:
Yu-Ju Hong51495eb2013-12-13 01:08:43968 If |for_update|, returns a redirect to the image or update file
969 on the devserver. E.g.,
970 http://host:port/static/archive/x86-generic-release/R26-4000.0.0/
971 chromium-test-image.bin
972 If |return_dir|, return a uri to the folder where the artifact is. E.g.,
973 http://host:port/static/x86-generic-release/R26-4000.0.0/
974 If |relative_path| is true, return a relative path the folder where the
975 payloads are. E.g.,
976 archive/x86-generic-release/R26-4000.0.0
joychen3cb228e2013-06-12 19:13:13977 """
Chris Sosa75490802013-10-01 00:21:45978 boolean_string = kwargs.get('for_update')
979 for_update = xbuddy.XBuddy.ParseBoolean(boolean_string)
Yu-Ju Hong51495eb2013-12-13 01:08:43980 boolean_string = kwargs.get('return_dir')
981 return_dir = xbuddy.XBuddy.ParseBoolean(boolean_string)
982 boolean_string = kwargs.get('relative_path')
983 relative_path = xbuddy.XBuddy.ParseBoolean(boolean_string)
joychen121fc9b2013-08-02 21:30:30984
Yu-Ju Hong51495eb2013-12-13 01:08:43985 if return_dir and relative_path:
Chris Sosa4b951602014-04-10 03:26:07986 raise common_util.DevServerHTTPError(
987 500, 'Cannot specify both return_dir and relative_path')
Chris Sosa75490802013-10-01 00:21:45988
989 # For updates, we optimize downloading of test images.
990 file_name = None
991 build_id = None
992 if for_update:
993 try:
Yu-Ju Hong1bdb7a92014-04-10 23:02:11994 build_id = self._xbuddy.StageTestArtifactsForUpdate(args)
Chris Sosa75490802013-10-01 00:21:45995 except build_artifact.ArtifactDownloadError:
996 build_id = None
997
998 if not build_id:
999 build_id, file_name = self._xbuddy.Get(args)
1000
Yu-Ju Hong51495eb2013-12-13 01:08:431001 if for_update:
1002 _Log('Payload generation triggered by request')
1003 # Forces payload to be in cache and symlinked into build_id dir.
Chris Sosa75490802013-10-01 00:21:451004 updater.GetUpdateForLabel(autoupdate.FORCED_UPDATE, build_id,
1005 image_name=file_name)
Yu-Ju Hong51495eb2013-12-13 01:08:431006
1007 response = None
1008 if return_dir:
1009 response = os.path.join(cherrypy.request.base, 'static', build_id)
1010 _Log('Directory requested, returning: %s', response)
1011 elif relative_path:
1012 response = build_id
1013 _Log('Relative path requested, returning: %s', response)
1014 elif for_update:
1015 response = os.path.join(cherrypy.request.base, 'update', build_id)
1016 _Log('Update URI requested, returning: %s', response)
joychen3cb228e2013-06-12 19:13:131017 else:
Yu-Ju Hong51495eb2013-12-13 01:08:431018 # Redirect to download the payload if no kwargs are set.
joychen121fc9b2013-08-02 21:30:301019 build_id = '/' + os.path.join('static', build_id, file_name)
Yu-Ju Hong51495eb2013-12-13 01:08:431020 _Log('Payload requested, returning: %s', build_id)
joychen121fc9b2013-08-02 21:30:301021 raise cherrypy.HTTPRedirect(build_id, 302)
joychen3cb228e2013-06-12 19:13:131022
Yu-Ju Hong51495eb2013-12-13 01:08:431023 return response
1024
joychen3cb228e2013-06-12 19:13:131025 @cherrypy.expose
1026 def xbuddy_list(self):
1027 """Lists the currently available images & time since last access.
1028
Gilad Arnold452fd272014-02-04 19:09:281029 Returns:
1030 A string representation of a list of tuples [(build_id, time since last
1031 access),...]
joychen3cb228e2013-06-12 19:13:131032 """
1033 return self._xbuddy.List()
1034
1035 @cherrypy.expose
1036 def xbuddy_capacity(self):
Gilad Arnold452fd272014-02-04 19:09:281037 """Returns the number of images cached by xBuddy."""
joychen3cb228e2013-06-12 19:13:131038 return self._xbuddy.Capacity()
1039
1040 @cherrypy.expose
Chris Sosa7c931362010-10-12 02:49:011041 def index(self):
Gilad Arnoldf8f769f2012-09-24 15:43:011042 """Presents a welcome message and documentation links."""
Gilad Arnoldf8f769f2012-09-24 15:43:011043 return ('Welcome to the Dev Server!<br>\n'
1044 '<br>\n'
1045 'Here are the available methods, click for documentation:<br>\n'
1046 '<br>\n'
1047 '%s' %
1048 '<br>\n'.join(
1049 [('<a href=doc/%s>%s</a>' % (name, name))
Gilad Arnoldd5ebaaa2012-10-02 18:52:381050 for name in _FindExposedMethods(
1051 self, '', unlisted=self._UNLISTED_METHODS)]))
Gilad Arnoldf8f769f2012-09-24 15:43:011052
1053 @cherrypy.expose
1054 def doc(self, *args):
1055 """Shows the documentation for available methods / URLs.
1056
1057 Example:
1058 http://myhost/doc/update
1059 """
Gilad Arnoldd5ebaaa2012-10-02 18:52:381060 name = '/'.join(args)
1061 method = _GetExposedMethod(self, name)
Gilad Arnoldf8f769f2012-09-24 15:43:011062 if not method:
1063 raise DevServerError("No exposed method named `%s'" % name)
1064 if not method.__doc__:
1065 raise DevServerError("No documentation for exposed method `%s'" % name)
1066 return '<pre>\n%s</pre>' % method.__doc__
Chris Sosa7c931362010-10-12 02:49:011067
Dale Curtisc9aaf3a2011-08-09 22:47:401068 @cherrypy.expose
Chris Sosa7c931362010-10-12 02:49:011069 def update(self, *args):
Gilad Arnoldf8f769f2012-09-24 15:43:011070 """Handles an update check from a Chrome OS client.
1071
1072 The HTTP request should contain the standard Omaha-style XML blob. The URL
1073 line may contain an additional intermediate path to the update payload.
1074
joychen121fc9b2013-08-02 21:30:301075 This request can be handled in one of 4 ways, depending on the devsever
1076 settings and intermediate path.
joychenb0dfe552013-07-30 17:02:061077
joychen121fc9b2013-08-02 21:30:301078 1. No intermediate path
1079 If no intermediate path is given, the default behavior is to generate an
1080 update payload from the latest test image locally built for the board
1081 specified in the xml. Devserver serves the generated payload.
1082
1083 2. Path explicitly invokes XBuddy
1084 If there is a path given, it can explicitly invoke xbuddy by prefixing it
1085 with 'xbuddy'. This path is then used to acquire an image binary for the
1086 devserver to generate an update payload from. Devserver then serves this
1087 payload.
1088
1089 3. Path is left for the devserver to interpret.
1090 If the path given doesn't explicitly invoke xbuddy, devserver will attempt
1091 to generate a payload from the test image in that directory and serve it.
1092
1093 4. The devserver is in a 'forced' mode. TO BE DEPRECATED
1094 This comes from the usage of --forced_payload or --image when starting the
1095 devserver. No matter what path (or no path) gets passed in, devserver will
1096 serve the update payload (--forced_payload) or generate an update payload
1097 from the image (--image).
1098
1099 Examples:
1100 1. No intermediate path
1101 update_engine_client --omaha_url=http://myhost/update
1102 This generates an update payload from the latest test image locally built
1103 for the board specified in the xml.
1104
1105 2. Explicitly invoke xbuddy
1106 update_engine_client --omaha_url=
1107 http://myhost/update/xbuddy/remote/board/version/dev
1108 This would go to GS to download the dev image for the board, from which
1109 the devserver would generate a payload to serve.
1110
1111 3. Give a path for devserver to interpret
1112 update_engine_client --omaha_url=http://myhost/update/some/random/path
1113 This would attempt, in order to:
1114 a) Generate an update from a test image binary if found in
1115 static_dir/some/random/path.
1116 b) Serve an update payload found in static_dir/some/random/path.
1117 c) Hope that some/random/path takes the form "board/version" and
1118 and attempt to download an update payload for that board/version
1119 from GS.
Gilad Arnoldf8f769f2012-09-24 15:43:011120 """
joychen121fc9b2013-08-02 21:30:301121 label = '/'.join(args)
Gilad Arnold286a0062012-01-12 21:47:021122 body_length = int(cherrypy.request.headers.get('Content-Length', 0))
Chris Sosa7c931362010-10-12 02:49:011123 data = cherrypy.request.rfile.read(body_length)
Chris Sosa7c931362010-10-12 02:49:011124
joychen121fc9b2013-08-02 21:30:301125 return updater.HandleUpdatePing(data, label)
Chris Sosa0356d3b2010-09-16 22:46:221126
Dan Shiafd0e492015-05-27 21:23:511127 @require_psutil()
1128 def _get_io_stats(self):
1129 """Get the IO stats as a dictionary.
1130
Gabe Black3b567202015-09-23 21:07:591131 Returns:
1132 A dictionary of IO stats collected by psutil.
Dan Shiafd0e492015-05-27 21:23:511133
1134 """
1135 return {'disk_read_bytes_per_second': self.disk_read_bytes_per_sec,
1136 'disk_write_bytes_per_second': self.disk_write_bytes_per_sec,
1137 'disk_total_bytes_per_second': (self.disk_read_bytes_per_sec +
1138 self.disk_write_bytes_per_sec),
1139 'network_sent_bytes_per_second': self.network_sent_bytes_per_sec,
1140 'network_recv_bytes_per_second': self.network_recv_bytes_per_sec,
1141 'network_total_bytes_per_second': (self.network_sent_bytes_per_sec +
1142 self.network_recv_bytes_per_sec),
1143 'cpu_percent': psutil.cpu_percent(),}
1144
Dan Shif5ce2de2013-04-25 23:06:321145 @cherrypy.expose
1146 def check_health(self):
1147 """Collect the health status of devserver to see if it's ready for staging.
1148
Gilad Arnold452fd272014-02-04 19:09:281149 Returns:
1150 A JSON dictionary containing all or some of the following fields:
1151 free_disk (int): free disk space in GB
1152 staging_thread_count (int): number of devserver threads currently staging
1153 an image
Dan Shif5ce2de2013-04-25 23:06:321154 """
1155 # Get free disk space.
1156 stat = os.statvfs(updater.static_dir)
1157 free_disk = stat.f_bsize * stat.f_bavail / 1000000000
1158
Dan Shiafd0e492015-05-27 21:23:511159 health_data = {
Dan Shif5ce2de2013-04-25 23:06:321160 'free_disk': free_disk,
Dan Shiafd0e492015-05-27 21:23:511161 'staging_thread_count': DevServerRoot._staging_thread_count}
1162 health_data.update(self._get_io_stats() or {})
1163
1164 return json.dumps(health_data)
Dan Shif5ce2de2013-04-25 23:06:321165
1166
Chris Sosadbc20082012-12-10 21:39:111167def _CleanCache(cache_dir, wipe):
1168 """Wipes any excess cached items in the cache_dir.
1169
1170 Args:
1171 cache_dir: the directory we are wiping from.
1172 wipe: If True, wipe all the contents -- not just the excess.
1173 """
1174 if wipe:
1175 # Clear the cache and exit on error.
1176 cmd = 'rm -rf %s/*' % cache_dir
1177 if os.system(cmd) != 0:
1178 _Log('Failed to clear the cache with %s' % cmd)
1179 sys.exit(1)
1180 else:
1181 # Clear all but the last N cached updates
1182 cmd = ('cd %s; ls -tr | head --lines=-%d | xargs rm -rf' %
1183 (cache_dir, CACHED_ENTRIES))
1184 if os.system(cmd) != 0:
1185 _Log('Failed to clean up old delta cache files with %s' % cmd)
1186 sys.exit(1)
1187
1188
Chris Sosa3ae4dc12013-03-29 18:47:001189def _AddTestingOptions(parser):
1190 group = optparse.OptionGroup(
1191 parser, 'Advanced Testing Options', 'These are used by test scripts and '
1192 'developers writing integration tests utilizing the devserver. They are '
1193 'not intended to be really used outside the scope of someone '
1194 'knowledgable about the test.')
1195 group.add_option('--exit',
1196 action='store_true',
1197 help='do not start the server (yet pregenerate/clear cache)')
1198 group.add_option('--host_log',
1199 action='store_true', default=False,
1200 help='record history of host update events (/api/hostlog)')
1201 group.add_option('--max_updates',
Gabe Black3b567202015-09-23 21:07:591202 metavar='NUM', default=-1, type='int',
Chris Sosa3ae4dc12013-03-29 18:47:001203 help='maximum number of update checks handled positively '
1204 '(default: unlimited)')
1205 group.add_option('--private_key',
1206 metavar='PATH', default=None,
1207 help='path to the private key in pem format. If this is set '
1208 'the devserver will generate update payloads that are '
1209 'signed with this key.')
David Zeuthen52ccd012013-10-31 19:58:261210 group.add_option('--private_key_for_metadata_hash_signature',
1211 metavar='PATH', default=None,
1212 help='path to the private key in pem format. If this is set '
1213 'the devserver will sign the metadata hash with the given '
1214 'key and transmit in the Omaha-style XML response.')
1215 group.add_option('--public_key',
1216 metavar='PATH', default=None,
1217 help='path to the public key in pem format. If this is set '
1218 'the devserver will transmit a base64 encoded version of '
1219 'the content in the Omaha-style XML response.')
Chris Sosa3ae4dc12013-03-29 18:47:001220 group.add_option('--proxy_port',
1221 metavar='PORT', default=None, type='int',
1222 help='port to have the client connect to -- basically the '
1223 'devserver lies to the update to tell it to get the payload '
1224 'from a different port that will proxy the request back to '
1225 'the devserver. The proxy must be managed outside the '
1226 'devserver.')
1227 group.add_option('--remote_payload',
1228 action='store_true', default=False,
Chris Sosa4b951602014-04-10 03:26:071229 help='Payload is being served from a remote machine. With '
1230 'this setting enabled, this devserver instance serves as '
1231 'just an Omaha server instance. In this mode, the '
1232 'devserver enforces a few extra components of the Omaha '
Chris Sosafc715442014-04-10 03:45:231233 'protocol, such as hardware class, being sent.')
Chris Sosa3ae4dc12013-03-29 18:47:001234 group.add_option('-u', '--urlbase',
1235 metavar='URL',
Gabe Black3b567202015-09-23 21:07:591236 help='base URL for update images, other than the '
1237 'devserver. Use in conjunction with remote_payload.')
Chris Sosa3ae4dc12013-03-29 18:47:001238 parser.add_option_group(group)
1239
1240
1241def _AddUpdateOptions(parser):
1242 group = optparse.OptionGroup(
1243 parser, 'Autoupdate Options', 'These options can be used to change '
1244 'how the devserver either generates or serve update payloads. Please '
1245 'note that all of these option affect how a payload is generated and so '
1246 'do not work in archive-only mode.')
1247 group.add_option('--board',
1248 help='By default the devserver will create an update '
1249 'payload from the latest image built for the board '
1250 'a device that is requesting an update has. When we '
1251 'pre-generate an update (see below) and we do not specify '
1252 'another update_type option like image or payload, the '
1253 'devserver needs to know the board to generate the latest '
1254 'image for. This is that board.')
1255 group.add_option('--critical_update',
1256 action='store_true', default=False,
1257 help='Present update payload as critical')
Chris Sosa3ae4dc12013-03-29 18:47:001258 group.add_option('--image',
1259 metavar='FILE',
1260 help='Generate and serve an update using this image to any '
1261 'device that requests an update.')
Chris Sosa3ae4dc12013-03-29 18:47:001262 group.add_option('--payload',
1263 metavar='PATH',
1264 help='use the update payload from specified directory '
1265 '(update.gz).')
1266 group.add_option('-p', '--pregenerate_update',
1267 action='store_true', default=False,
1268 help='pre-generate the update payload before accepting '
1269 'update requests. Useful to help debug payload generation '
1270 'issues quickly. Also if an update payload will take a '
1271 'long time to generate, a client may timeout if you do not'
1272 'pregenerate the update.')
1273 group.add_option('--src_image',
1274 metavar='PATH', default='',
1275 help='If specified, delta updates will be generated using '
1276 'this image as the source image. Delta updates are when '
1277 'you are updating from a "source image" to a another '
1278 'image.')
1279 parser.add_option_group(group)
1280
1281
1282def _AddProductionOptions(parser):
1283 group = optparse.OptionGroup(
1284 parser, 'Advanced Server Options', 'These options can be used to changed '
1285 'for advanced server behavior.')
Chris Sosa3ae4dc12013-03-29 18:47:001286 group.add_option('--clear_cache',
1287 action='store_true', default=False,
1288 help='At startup, removes all cached entries from the'
1289 'devserver\'s cache.')
1290 group.add_option('--logfile',
1291 metavar='PATH',
1292 help='log output to this file instead of stdout')
Chris Sosa855b8932013-08-21 20:24:551293 group.add_option('--pidfile',
1294 metavar='PATH',
1295 help='path to output a pid file for the server.')
Gilad Arnold11fbef42014-02-10 19:04:131296 group.add_option('--portfile',
1297 metavar='PATH',
1298 help='path to output the port number being served on.')
Chris Sosa3ae4dc12013-03-29 18:47:001299 group.add_option('--production',
1300 action='store_true', default=False,
1301 help='have the devserver use production values when '
1302 'starting up. This includes using more threads and '
1303 'performing less logging.')
1304 parser.add_option_group(group)
1305
1306
J. Richard Barnette3d977b82013-04-23 18:05:191307def _MakeLogHandler(logfile):
1308 """Create a LogHandler instance used to log all messages."""
1309 hdlr_cls = handlers.TimedRotatingFileHandler
1310 hdlr = hdlr_cls(logfile, when=_LOG_ROTATION_TIME,
1311 backupCount=_LOG_ROTATION_BACKUP)
Chris Sosa855b8932013-08-21 20:24:551312 hdlr.setFormatter(cplogging.logfmt)
J. Richard Barnette3d977b82013-04-23 18:05:191313 return hdlr
1314
1315
Chris Sosacde6bf42012-06-01 01:36:391316def main():
Chris Sosa3ae4dc12013-03-29 18:47:001317 usage = '\n\n'.join(['usage: %prog [options]', __doc__])
Gilad Arnold286a0062012-01-12 21:47:021318 parser = optparse.OptionParser(usage=usage)
joychened64b222013-06-21 23:39:341319
1320 # get directory that the devserver is run from
1321 devserver_dir = os.path.dirname(os.path.abspath(sys.argv[0]))
joychen84d13772013-08-06 16:17:231322 default_static_dir = '%s/static' % devserver_dir
joychened64b222013-06-21 23:39:341323 parser.add_option('--static_dir',
Gilad Arnold9714d9b2012-10-04 17:09:421324 metavar='PATH',
joychen84d13772013-08-06 16:17:231325 default=default_static_dir,
joychened64b222013-06-21 23:39:341326 help='writable static directory')
Gilad Arnold9714d9b2012-10-04 17:09:421327 parser.add_option('--port',
1328 default=8080, type='int',
Gilad Arnoldaf696d12014-02-14 21:13:281329 help=('port for the dev server to use; if zero, binds to '
1330 'an arbitrary available port (default: 8080)'))
Gilad Arnold9714d9b2012-10-04 17:09:421331 parser.add_option('-t', '--test_image',
1332 action='store_true',
joychen121fc9b2013-08-02 21:30:301333 help='Deprecated.')
joychen5260b9a2013-07-16 21:48:011334 parser.add_option('-x', '--xbuddy_manage_builds',
1335 action='store_true',
1336 default=False,
1337 help='If set, allow xbuddy to manage images in'
1338 'build/images.')
Dan Shi72b16132015-10-08 19:10:331339 parser.add_option('-a', '--android_build_credential',
1340 default=None,
1341 help='Path to a json file which contains the credential '
1342 'needed to access Android builds.')
Chris Sosa3ae4dc12013-03-29 18:47:001343 _AddProductionOptions(parser)
1344 _AddUpdateOptions(parser)
1345 _AddTestingOptions(parser)
Chris Sosa7c931362010-10-12 02:49:011346 (options, _) = parser.parse_args()
[email protected]21a5ca32009-11-04 18:23:231347
J. Richard Barnette3d977b82013-04-23 18:05:191348 # Handle options that must be set globally in cherrypy. Do this
1349 # work up front, because calls to _Log() below depend on this
1350 # initialization.
1351 if options.production:
1352 cherrypy.config.update({'environment': 'production'})
1353 if not options.logfile:
1354 cherrypy.config.update({'log.screen': True})
1355 else:
1356 cherrypy.config.update({'log.error_file': '',
1357 'log.access_file': ''})
1358 hdlr = _MakeLogHandler(options.logfile)
1359 # Pylint can't seem to process these two calls properly
1360 # pylint: disable=E1101
1361 cherrypy.log.access_log.addHandler(hdlr)
1362 cherrypy.log.error_log.addHandler(hdlr)
1363 # pylint: enable=E1101
1364
joychened64b222013-06-21 23:39:341365 # set static_dir, from which everything will be served
joychen84d13772013-08-06 16:17:231366 options.static_dir = os.path.realpath(options.static_dir)
Chris Sosa0356d3b2010-09-16 22:46:221367
joychened64b222013-06-21 23:39:341368 cache_dir = os.path.join(options.static_dir, 'cache')
J. Richard Barnette3d977b82013-04-23 18:05:191369 # If our devserver is only supposed to serve payloads, we shouldn't be
1370 # mucking with the cache at all. If the devserver hadn't previously
1371 # generated a cache and is expected, the caller is using it wrong.
joychen7c2054a2013-07-25 18:14:071372 if os.path.exists(cache_dir):
Chris Sosadbc20082012-12-10 21:39:111373 _CleanCache(cache_dir, options.clear_cache)
Chris Sosa6b8c3742011-01-31 20:12:171374 else:
1375 os.makedirs(cache_dir)
Don Garrettf90edf02010-11-17 01:36:141376
Chris Sosadbc20082012-12-10 21:39:111377 _Log('Using cache directory %s' % cache_dir)
joychened64b222013-06-21 23:39:341378 _Log('Serving from %s' % options.static_dir)
[email protected]21a5ca32009-11-04 18:23:231379
joychen121fc9b2013-08-02 21:30:301380 _xbuddy = xbuddy.XBuddy(options.xbuddy_manage_builds,
1381 options.board,
joychen121fc9b2013-08-02 21:30:301382 static_dir=options.static_dir)
Chris Sosa75490802013-10-01 00:21:451383 if options.clear_cache and options.xbuddy_manage_builds:
1384 _xbuddy.CleanCache()
joychen121fc9b2013-08-02 21:30:301385
Chris Sosa6a3697f2013-01-30 00:44:431386 # We allow global use here to share with cherrypy classes.
1387 # pylint: disable=W0603
Chris Sosacde6bf42012-06-01 01:36:391388 global updater
Andrew de los Reyes52620802010-04-12 20:40:071389 updater = autoupdate.Autoupdate(
joychen121fc9b2013-08-02 21:30:301390 _xbuddy,
joychened64b222013-06-21 23:39:341391 static_dir=options.static_dir,
Andrew de los Reyes52620802010-04-12 20:40:071392 urlbase=options.urlbase,
Chris Sosa5d342a22010-09-28 23:54:411393 forced_image=options.image,
Gilad Arnold0c9c8602012-10-03 06:58:581394 payload_path=options.payload,
Don Garrett0ad09372010-12-07 00:20:301395 proxy_port=options.proxy_port,
Chris Sosa4136e692010-10-29 06:42:371396 src_image=options.src_image,
Chris Sosa08d55a22011-01-20 00:08:021397 board=options.board,
Chris Sosa0f1ec842011-02-15 00:33:221398 copy_to_static_root=not options.exit,
1399 private_key=options.private_key,
Gabe Black3b567202015-09-23 21:07:591400 private_key_for_metadata_hash_signature=(
1401 options.private_key_for_metadata_hash_signature),
David Zeuthen52ccd012013-10-31 19:58:261402 public_key=options.public_key,
Satoru Takabayashid733cbe2011-11-15 17:36:321403 critical_update=options.critical_update,
Gilad Arnold0c9c8602012-10-03 06:58:581404 remote_payload=options.remote_payload,
Gilad Arnolda564b4b2012-10-04 17:32:441405 max_updates=options.max_updates,
Gilad Arnold8318eac2012-10-04 19:52:231406 host_log=options.host_log,
Chris Sosa0f1ec842011-02-15 00:33:221407 )
Chris Sosa7c931362010-10-12 02:49:011408
Chris Sosa6a3697f2013-01-30 00:44:431409 if options.pregenerate_update:
1410 updater.PreGenerateUpdate()
Chris Sosa0356d3b2010-09-16 22:46:221411
J. Richard Barnette3d977b82013-04-23 18:05:191412 if options.exit:
1413 return
Chris Sosa2f1c41e2012-07-10 21:32:331414
joychen3cb228e2013-06-12 19:13:131415 dev_server = DevServerRoot(_xbuddy)
1416
Gilad Arnold11fbef42014-02-10 19:04:131417 # Patch CherryPy to support binding to any available port (--port=0).
1418 cherrypy_ext.ZeroPortPatcher.DoPatch(cherrypy)
1419
Chris Sosa855b8932013-08-21 20:24:551420 if options.pidfile:
1421 plugins.PIDFile(cherrypy.engine, options.pidfile).subscribe()
1422
Gilad Arnold11fbef42014-02-10 19:04:131423 if options.portfile:
1424 cherrypy_ext.PortFile(cherrypy.engine, options.portfile).subscribe()
1425
Dan Shi72b16132015-10-08 19:10:331426 if options.android_build_credential:
1427 with open(options.android_build_credential) as f:
1428 android_build.BuildAccessor.credential_info = json.load(f)
joychen3cb228e2013-06-12 19:13:131429 cherrypy.quickstart(dev_server, config=_GetConfig(options))
Chris Sosacde6bf42012-06-01 01:36:391430
1431
1432if __name__ == '__main__':
1433 main()