blob: 4002db31b5b85995482fcf3ebffdbbcac7fa1eef [file] [log] [blame]
Keith Haddow58f36d12020-10-28 16:16:39 +00001#!/usr/bin/env python3
Luis Hector Chavezdca9dd72018-06-12 12:56:30 -07002# -*- coding: utf-8 -*-
Mike Frysinger8b0fc372022-09-08 03:24:24 -04003# Copyright 2009-2012 The ChromiumOS Authors
rtc@google.comded22402009-10-26 22:36:21 +00004# Use of this source code is governed by a BSD-style license that can be
5# found in the LICENSE file.
6
Amin Hassani2aa34282020-11-18 01:18:19 +00007"""Chromium OS development server that can be used for all forms of update.
Chris Sosa3ae4dc12013-03-29 11:47:00 -07008
Amin Hassani2aa34282020-11-18 01:18:19 +00009This devserver can be used to perform system-wide autoupdate and update
10of specific portage packages on devices running Chromium OS derived operating
11systems.
12
13The 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
21For autoupdates, there are many more advanced options that can help specify
22how to update and which payload to give to a requester.
Chris Sosa3ae4dc12013-03-29 11:47:00 -070023"""
24
Gabe Black3b567202015-09-23 14:07:59 -070025from __future__ import print_function
Chris Sosa7c931362010-10-11 19:49:01 -070026
Gilad Arnold55a2a372012-10-02 09:46:32 -070027import json
Chris McDonald771446e2021-08-05 14:23:08 -060028import logging
Jack Rosenthal8de609d2023-02-09 13:20:35 -070029from logging import handlers
David Riley2fcb0122017-11-02 11:25:39 -070030import optparse # pylint: disable=deprecated-module
rtc@google.comded22402009-10-26 22:36:21 +000031import os
Scott Zawalski4647ce62012-01-03 17:17:28 -050032import re
Simran Basi4baad082013-02-14 13:39:18 -080033import shutil
Mandeep Singh Baines38dcdda2012-12-07 17:55:33 -080034import socket
Chris Masone816e38c2012-05-02 12:22:36 -070035import subprocess
J. Richard Barnette3d977b82013-04-23 11:05:19 -070036import sys
Chris Masone816e38c2012-05-02 12:22:36 -070037import tempfile
Dan Shi59ae7092013-06-04 14:37:27 -070038import threading
Gilad Arnoldd5ebaaa2012-10-02 11:52:38 -070039import types
J. Richard Barnette3d977b82013-04-23 11:05:19 -070040
Jack Rosenthal8de609d2023-02-09 13:20:35 -070041import autoupdate
Amin Hassanid4e35392019-10-03 11:02:44 -070042
43# pylint: disable=no-name-in-module, import-error
J. Richard Barnette3d977b82013-04-23 11:05:19 -070044import cherrypy
Chris Sosa855b8932013-08-21 13:24:55 -070045from cherrypy import _cplogging as cplogging
Amin Hassanid4e35392019-10-03 11:02:44 -070046from cherrypy.process import plugins
Achuith Bhandarkar662fb722019-10-31 16:12:49 -070047import cherrypy_ext
Achuith Bhandarkar662fb722019-10-31 16:12:49 -070048import health_checker
49
Richard Barnettedf35c322017-08-18 17:02:13 -070050# This must happen before any local modules get a chance to import
51# anything from chromite. Otherwise, really bad things will happen, and
52# you will _not_ understand why.
Congbin Guo3afae6c2019-08-13 16:29:42 -070053import setup_chromite # pylint: disable=unused-import
Jack Rosenthal8de609d2023-02-09 13:20:35 -070054from six.moves import http_client
55
Eliot Courtneyf39420b2020-10-27 18:34:04 +090056from chromite.lib import cros_build_lib
Achuith Bhandarkar662fb722019-10-31 16:12:49 -070057from chromite.lib.xbuddy import android_build
58from chromite.lib.xbuddy import artifact_info
59from chromite.lib.xbuddy import build_artifact
Achuith Bhandarkar662fb722019-10-31 16:12:49 -070060from chromite.lib.xbuddy import common_util
61from chromite.lib.xbuddy import devserver_constants
62from chromite.lib.xbuddy import downloader
63from chromite.lib.xbuddy import xbuddy
Gilad Arnoldc65330c2012-09-20 15:17:48 -070064
Amin Hassani3587fb32021-04-28 10:10:01 -070065
Jack Rosenthal8de609d2023-02-09 13:20:35 -070066# pylint: enable=no-name-in-module, import-error
67
68
Gilad Arnoldc65330c2012-09-20 15:17:48 -070069# Module-local log function.
Chris Sosa6a3697f2013-01-29 16:44:43 -080070def _Log(message, *args):
Jack Rosenthal8de609d2023-02-09 13:20:35 -070071 return logging.info(message, *args)
72
Frank Farzan40160872011-12-12 18:39:18 -080073
Chris Sosa417e55d2011-01-25 16:40:48 -080074CACHED_ENTRIES = 12
Don Garrettf90edf02010-11-16 17:36:14 -080075
Jack Rosenthal8de609d2023-02-09 13:20:35 -070076TELEMETRY_FOLDER = "telemetry_src"
77TELEMETRY_DEPS = [
78 "dep-telemetry_dep.tar.bz2",
79 "dep-page_cycler_dep.tar.bz2",
80 "dep-chrome_test.tar.bz2",
81 "dep-perf_data_dep.tar.bz2",
82]
Simran Basi4baad082013-02-14 13:39:18 -080083
Amin Hassani2aa34282020-11-18 01:18:19 +000084# Sets up global to share between classes.
85updater = None
86
xixuan3d48bff2017-01-30 19:00:09 -080087# Log rotation parameters. These settings correspond to twice a day once
88# devserver is started, with about two weeks (28 backup files) of old logs
89# kept for backup.
J. Richard Barnette3d977b82013-04-23 11:05:19 -070090#
xixuan3d48bff2017-01-30 19:00:09 -080091# For more, see the documentation in standard python library for
J. Richard Barnette3d977b82013-04-23 11:05:19 -070092# logging.handlers.TimedRotatingFileHandler
Jack Rosenthal8de609d2023-02-09 13:20:35 -070093_LOG_ROTATION_TIME = "H"
Congbin Guo3afae6c2019-08-13 16:29:42 -070094_LOG_ROTATION_INTERVAL = 12 # hours
95_LOG_ROTATION_BACKUP = 28 # backup counts
Frank Farzan40160872011-12-12 18:39:18 -080096
Sanika Kulkarnid4496fd2020-02-04 17:26:25 -080097# Error msg for deprecated RPC usage.
Jack Rosenthal8de609d2023-02-09 13:20:35 -070098DEPRECATED_RPC_ERROR_MSG = (
99 "The %s RPC has been deprecated. Usage of this "
100 "RPC is discouraged. Please go to "
101 "go/devserver-deprecation for more information."
102)
Sanika Kulkarnid4496fd2020-02-04 17:26:25 -0800103
xixuan52c2fba2016-05-20 17:02:48 -0700104
Amin Hassanid4e35392019-10-03 11:02:44 -0700105class DevServerError(Exception):
Jack Rosenthal8de609d2023-02-09 13:20:35 -0700106 """Exception class used by DevServer."""
Amin Hassanid4e35392019-10-03 11:02:44 -0700107
108
Sanika Kulkarnid4496fd2020-02-04 17:26:25 -0800109class DeprecatedRPCError(DevServerError):
Jack Rosenthal8de609d2023-02-09 13:20:35 -0700110 """Exception class used when an RPC is deprecated but is still being used."""
Sanika Kulkarnid4496fd2020-02-04 17:26:25 -0800111
Jack Rosenthal8de609d2023-02-09 13:20:35 -0700112 def __init__(self, rpc_name):
113 """Constructor for DeprecatedRPCError class.
Sanika Kulkarnid4496fd2020-02-04 17:26:25 -0800114
Jack Rosenthal8de609d2023-02-09 13:20:35 -0700115 :param rpc_name: (str) name of the RPC that has been deprecated.
116 """
117 super(DeprecatedRPCError, self).__init__(
118 DEPRECATED_RPC_ERROR_MSG % rpc_name
119 )
120 self.rpc_name = rpc_name
Sanika Kulkarnid4496fd2020-02-04 17:26:25 -0800121
122
Amin Hassani722e0962019-11-15 15:45:31 -0800123class DevServerHTTPError(cherrypy.HTTPError):
Jack Rosenthal8de609d2023-02-09 13:20:35 -0700124 """Exception class to log the HTTPResponse before routing it to cherrypy."""
Amin Hassani722e0962019-11-15 15:45:31 -0800125
Jack Rosenthal8de609d2023-02-09 13:20:35 -0700126 def __init__(self, status, message):
127 """CherryPy error with logging.
128
129 Args:
130 status: HTTPResponse status.
131 message: Message associated with the response.
132 """
133 cherrypy.HTTPError.__init__(self, status, message)
134 _Log("HTTPError status: %s message: %s", status, message)
Amin Hassani722e0962019-11-15 15:45:31 -0800135
136
Gabe Black3b567202015-09-23 14:07:59 -0700137def _canonicalize_archive_url(archive_url):
Jack Rosenthal8de609d2023-02-09 13:20:35 -0700138 """Canonicalizes archive_url strings.
Gabe Black3b567202015-09-23 14:07:59 -0700139
Jack Rosenthal8de609d2023-02-09 13:20:35 -0700140 Raises:
141 DevserverError: if archive_url is not set.
142 """
143 if archive_url:
144 if not archive_url.startswith("gs://"):
145 raise DevServerError(
146 "Archive URL isn't from Google Storage (%s) ." % archive_url
147 )
Gabe Black3b567202015-09-23 14:07:59 -0700148
Jack Rosenthal8de609d2023-02-09 13:20:35 -0700149 return archive_url.rstrip("/")
150 else:
151 raise DevServerError("Must specify an archive_url in the request")
Gabe Black3b567202015-09-23 14:07:59 -0700152
153
Amin Hassani2aa34282020-11-18 01:18:19 +0000154def _canonicalize_local_path(local_path):
Jack Rosenthal8de609d2023-02-09 13:20:35 -0700155 """Canonicalizes |local_path| strings.
Gabe Black3b567202015-09-23 14:07:59 -0700156
Jack Rosenthal8de609d2023-02-09 13:20:35 -0700157 Raises:
158 DevserverError: if |local_path| is not set.
159 """
160 # Restrict staging of local content to only files within the static
161 # directory.
162 local_path = os.path.abspath(local_path)
163 if not local_path.startswith(updater.static_dir):
164 raise DevServerError(
165 "Local path %s must be a subdirectory of the static"
166 " directory: %s" % (local_path, updater.static_dir)
167 )
Gabe Black3b567202015-09-23 14:07:59 -0700168
Jack Rosenthal8de609d2023-02-09 13:20:35 -0700169 return local_path.rstrip("/")
Gabe Black3b567202015-09-23 14:07:59 -0700170
171
172def _get_artifacts(kwargs):
Jack Rosenthal8de609d2023-02-09 13:20:35 -0700173 """Returns a tuple of named and file artifacts given the stage rpc kwargs.
Gabe Black3b567202015-09-23 14:07:59 -0700174
Jack Rosenthal8de609d2023-02-09 13:20:35 -0700175 Raises:
176 DevserverError if no artifacts would be returned.
177 """
178 artifacts = kwargs.get("artifacts")
179 files = kwargs.get("files")
180 if not artifacts and not files:
181 raise DevServerError("No artifacts specified.")
Gabe Black3b567202015-09-23 14:07:59 -0700182
Jack Rosenthal8de609d2023-02-09 13:20:35 -0700183 # Note we NEED to coerce files to a string as we get raw unicode from
184 # cherrypy and we treat files as strings elsewhere in the code.
185 return (
186 str(artifacts).split(",") if artifacts else [],
187 str(files).split(",") if files else [],
188 )
Gabe Black3b567202015-09-23 14:07:59 -0700189
190
Dan Shi61305df2015-10-26 16:52:35 -0700191def _is_android_build_request(kwargs):
Jack Rosenthal8de609d2023-02-09 13:20:35 -0700192 """Check if a devserver call is for Android build, based on the arguments.
Dan Shi61305df2015-10-26 16:52:35 -0700193
Jack Rosenthal8de609d2023-02-09 13:20:35 -0700194 This method exams the request's arguments (os_type) to determine if the
195 request is for Android build. If os_type is set to `android`, returns True.
196 If os_type is not set or has other values, returns False.
Dan Shi61305df2015-10-26 16:52:35 -0700197
Jack Rosenthal8de609d2023-02-09 13:20:35 -0700198 Args:
199 kwargs: Keyword arguments for the request.
Dan Shi61305df2015-10-26 16:52:35 -0700200
Jack Rosenthal8de609d2023-02-09 13:20:35 -0700201 Returns:
202 True if the request is for Android build. False otherwise.
203 """
204 os_type = kwargs.get("os_type", None)
205 return os_type == "android"
Dan Shi61305df2015-10-26 16:52:35 -0700206
207
Amin Hassani2aa34282020-11-18 01:18:19 +0000208def _get_downloader(kwargs):
Jack Rosenthal8de609d2023-02-09 13:20:35 -0700209 """Returns the downloader based on passed in arguments.
Gabe Black3b567202015-09-23 14:07:59 -0700210
Jack Rosenthal8de609d2023-02-09 13:20:35 -0700211 Args:
212 kwargs: Keyword arguments for the request.
213 """
214 local_path = kwargs.get("local_path")
215 if local_path:
216 local_path = _canonicalize_local_path(local_path)
Gabe Black3b567202015-09-23 14:07:59 -0700217
Jack Rosenthal8de609d2023-02-09 13:20:35 -0700218 dl = None
219 if local_path:
220 delete_source = _parse_boolean_arg(kwargs, "delete_source")
221 dl = downloader.LocalDownloader(
222 updater.static_dir, local_path, delete_source=delete_source
223 )
Gabe Black3b567202015-09-23 14:07:59 -0700224
Jack Rosenthal8de609d2023-02-09 13:20:35 -0700225 if not _is_android_build_request(kwargs):
226 archive_url = kwargs.get("archive_url")
227 if not archive_url and not local_path:
228 raise DevServerError(
229 "Requires archive_url or local_path to be specified."
230 )
231 if archive_url and local_path:
232 raise DevServerError(
233 "archive_url and local_path can not both be specified."
234 )
235 if not dl:
236 archive_url = _canonicalize_archive_url(archive_url)
237 dl = downloader.GoogleStorageDownloader(
238 updater.static_dir,
239 archive_url,
240 downloader.GoogleStorageDownloader.GetBuildIdFromArchiveURL(
241 archive_url
242 ),
243 )
244 elif not dl:
245 target = kwargs.get("target", None)
246 branch = kwargs.get("branch", None)
247 build_id = kwargs.get("build_id", None)
248 if not target or not branch or not build_id:
249 raise DevServerError(
250 "target, branch, build ID must all be specified for "
251 "downloading Android build."
252 )
253 dl = downloader.AndroidBuildDownloader(
254 updater.static_dir, branch, build_id, target
255 )
Gabe Black3b567202015-09-23 14:07:59 -0700256
Jack Rosenthal8de609d2023-02-09 13:20:35 -0700257 return dl
Gabe Black3b567202015-09-23 14:07:59 -0700258
259
Amin Hassani2aa34282020-11-18 01:18:19 +0000260def _get_downloader_and_factory(kwargs):
Jack Rosenthal8de609d2023-02-09 13:20:35 -0700261 """Returns the downloader and artifact factory based on passed in arguments.
Gabe Black3b567202015-09-23 14:07:59 -0700262
Jack Rosenthal8de609d2023-02-09 13:20:35 -0700263 Args:
264 kwargs: Keyword arguments for the request.
265 """
266 artifacts, files = _get_artifacts(kwargs)
267 dl = _get_downloader(kwargs)
Gabe Black3b567202015-09-23 14:07:59 -0700268
Jack Rosenthal8de609d2023-02-09 13:20:35 -0700269 if isinstance(
270 dl, (downloader.GoogleStorageDownloader, downloader.LocalDownloader)
271 ):
272 factory_class = build_artifact.ChromeOSArtifactFactory
273 elif isinstance(dl, downloader.AndroidBuildDownloader):
274 factory_class = build_artifact.AndroidArtifactFactory
275 else:
276 raise DevServerError(
277 "Unrecognized value for downloader type: %s" % type(dl)
278 )
Gabe Black3b567202015-09-23 14:07:59 -0700279
Jack Rosenthal8de609d2023-02-09 13:20:35 -0700280 factory = factory_class(dl.GetBuildDir(), artifacts, files, dl.GetBuild())
Gabe Black3b567202015-09-23 14:07:59 -0700281
Jack Rosenthal8de609d2023-02-09 13:20:35 -0700282 return dl, factory
Gabe Black3b567202015-09-23 14:07:59 -0700283
284
Scott Zawalski4647ce62012-01-03 17:17:28 -0500285def _LeadingWhiteSpaceCount(string):
Jack Rosenthal8de609d2023-02-09 13:20:35 -0700286 """Count the amount of leading whitespace in a string.
Scott Zawalski4647ce62012-01-03 17:17:28 -0500287
Jack Rosenthal8de609d2023-02-09 13:20:35 -0700288 Args:
289 string: The string to count leading whitespace in.
Don Garrettf84631a2014-01-07 18:21:26 -0800290
Jack Rosenthal8de609d2023-02-09 13:20:35 -0700291 Returns:
292 number of white space chars before characters start.
293 """
294 matched = re.match(r"^\s+", string)
295 if matched:
296 return len(matched.group())
Scott Zawalski4647ce62012-01-03 17:17:28 -0500297
Jack Rosenthal8de609d2023-02-09 13:20:35 -0700298 return 0
Scott Zawalski4647ce62012-01-03 17:17:28 -0500299
300
301def _PrintDocStringAsHTML(func):
Jack Rosenthal8de609d2023-02-09 13:20:35 -0700302 """Make a functions docstring somewhat HTML style.
Scott Zawalski4647ce62012-01-03 17:17:28 -0500303
Jack Rosenthal8de609d2023-02-09 13:20:35 -0700304 Args:
305 func: The function to return the docstring from.
Don Garrettf84631a2014-01-07 18:21:26 -0800306
Jack Rosenthal8de609d2023-02-09 13:20:35 -0700307 Returns:
308 A string that is somewhat formated for a web browser.
309 """
310 # TODO(scottz): Make this parse Args/Returns in a prettier way.
311 # Arguments could be bolded and indented etc.
312 html_doc = []
313 for line in func.__doc__.splitlines():
314 leading_space = _LeadingWhiteSpaceCount(line)
315 if leading_space > 0:
316 line = " " * leading_space + line
Scott Zawalski4647ce62012-01-03 17:17:28 -0500317
Jack Rosenthal8de609d2023-02-09 13:20:35 -0700318 html_doc.append("<BR>%s" % line)
Scott Zawalski4647ce62012-01-03 17:17:28 -0500319
Jack Rosenthal8de609d2023-02-09 13:20:35 -0700320 return "\n".join(html_doc)
Scott Zawalski4647ce62012-01-03 17:17:28 -0500321
322
Simran Basief83d6a2014-08-28 14:32:01 -0700323def _GetUpdateTimestampHandler(static_dir):
Jack Rosenthal8de609d2023-02-09 13:20:35 -0700324 """Returns a handler to update directory staged.timestamp.
Simran Basief83d6a2014-08-28 14:32:01 -0700325
Jack Rosenthal8de609d2023-02-09 13:20:35 -0700326 This handler resets the stage.timestamp whenever static content is accessed.
Simran Basief83d6a2014-08-28 14:32:01 -0700327
Jack Rosenthal8de609d2023-02-09 13:20:35 -0700328 Args:
329 static_dir: Directory from which static content is being staged.
Simran Basief83d6a2014-08-28 14:32:01 -0700330
Jack Rosenthal8de609d2023-02-09 13:20:35 -0700331 Returns:
332 A cherrypy handler to update the timestamp of accessed content.
333 """
334
335 def UpdateTimestampHandler():
336 if not "404" in cherrypy.response.status:
337 build_match = re.match(
338 devserver_constants.STAGED_BUILD_REGEX,
339 cherrypy.request.path_info,
340 )
341 if build_match:
342 build_dir = os.path.join(static_dir, build_match.group("build"))
343 downloader.Downloader.TouchTimestampForStaged(build_dir)
344
345 return UpdateTimestampHandler
Simran Basief83d6a2014-08-28 14:32:01 -0700346
347
Chris Sosa7c931362010-10-11 19:49:01 -0700348def _GetConfig(options):
Jack Rosenthal8de609d2023-02-09 13:20:35 -0700349 """Returns the configuration for the devserver."""
Mandeep Singh Baines38dcdda2012-12-07 17:55:33 -0800350
Jack Rosenthal8de609d2023-02-09 13:20:35 -0700351 socket_host = "::"
352 # Fall back to IPv4 when python is not configured with IPv6.
353 if not socket.has_ipv6:
354 socket_host = "0.0.0.0"
Mandeep Singh Baines38dcdda2012-12-07 17:55:33 -0800355
Jack Rosenthal8de609d2023-02-09 13:20:35 -0700356 # Adds the UpdateTimestampHandler to cherrypy's tools. This tools executes
357 # on the on_end_resource hook. This hook is called once processing is
358 # complete and the response is ready to be returned.
359 cherrypy.tools.update_timestamp = cherrypy.Tool(
360 "on_end_resource", _GetUpdateTimestampHandler(options.static_dir)
361 )
Simran Basief83d6a2014-08-28 14:32:01 -0700362
Jack Rosenthal8de609d2023-02-09 13:20:35 -0700363 base_config = {
364 "global": {
365 "server.log_request_headers": True,
366 "server.protocol_version": "HTTP/1.1",
367 "server.socket_host": socket_host,
368 "server.socket_port": int(options.port),
369 "response.timeout": 6000,
370 "request.show_tracebacks": True,
371 "server.socket_timeout": 60,
372 "server.thread_pool": 2,
373 "engine.autoreload.on": False,
374 },
375 "/build": {
376 "response.timeout": 100000,
377 },
378 "/update": {
379 # Gets rid of cherrypy parsing post file for args.
380 "request.process_request_body": False,
381 "response.timeout": 10000,
382 },
383 # Sets up the static dir for file hosting.
384 "/static": {
385 "tools.staticdir.dir": options.static_dir,
386 "tools.staticdir.on": True,
387 "response.timeout": 10000,
388 "tools.update_timestamp.on": True,
389 },
390 }
391 if options.production:
392 base_config["global"].update({"server.thread_pool": 150})
Scott Zawalski1c5e7cd2012-02-27 13:12:52 -0500393
Jack Rosenthal8de609d2023-02-09 13:20:35 -0700394 return base_config
rtc@google.com64244662009-11-12 00:52:08 +0000395
Darin Petkove17164a2010-08-11 13:24:41 -0700396
Gilad Arnoldd5ebaaa2012-10-02 11:52:38 -0700397def _GetRecursiveMemberObject(root, member_list):
Jack Rosenthal8de609d2023-02-09 13:20:35 -0700398 """Returns an object corresponding to a nested member list.
Gilad Arnoldd5ebaaa2012-10-02 11:52:38 -0700399
Jack Rosenthal8de609d2023-02-09 13:20:35 -0700400 Args:
401 root: the root object to search
402 member_list: list of nested members to search
Don Garrettf84631a2014-01-07 18:21:26 -0800403
Jack Rosenthal8de609d2023-02-09 13:20:35 -0700404 Returns:
405 An object corresponding to the member name list; None otherwise.
406 """
407 for member in member_list:
408 next_root = root.__class__.__dict__.get(member)
409 if not next_root:
410 return None
411 root = next_root
412 return root
Gilad Arnoldd5ebaaa2012-10-02 11:52:38 -0700413
414
415def _IsExposed(name):
Jack Rosenthal8de609d2023-02-09 13:20:35 -0700416 """Returns True iff |name| has an `exposed' attribute and it is set."""
417 return hasattr(name, "exposed") and name.exposed
Gilad Arnoldd5ebaaa2012-10-02 11:52:38 -0700418
419
Congbin Guo6bc32182019-08-20 17:54:30 -0700420def _GetExposedMethod(nested_member):
Jack Rosenthal8de609d2023-02-09 13:20:35 -0700421 """Returns a CherryPy-exposed method, if such exists.
Gilad Arnoldd5ebaaa2012-10-02 11:52:38 -0700422
Jack Rosenthal8de609d2023-02-09 13:20:35 -0700423 Args:
424 nested_member: a slash-joined path to the nested member
Don Garrettf84631a2014-01-07 18:21:26 -0800425
Jack Rosenthal8de609d2023-02-09 13:20:35 -0700426 Returns:
427 A function object corresponding to the path defined by |nested_member| from
428 the app root object registered, if the function is exposed; None otherwise.
429 """
430 for app in cherrypy.tree.apps.values():
431 # Use the 'index' function doc as the doc of the app.
432 if nested_member == app.script_name.lstrip("/"):
433 nested_member = "index"
Congbin Guo6bc32182019-08-20 17:54:30 -0700434
Jack Rosenthal8de609d2023-02-09 13:20:35 -0700435 method = _GetRecursiveMemberObject(app.root, nested_member.split("/"))
436 if (
437 method
438 and isinstance(method, types.FunctionType)
439 and _IsExposed(method)
440 ):
441 return method
Gilad Arnoldd5ebaaa2012-10-02 11:52:38 -0700442
443
Gilad Arnold748c8322012-10-12 09:51:35 -0700444def _FindExposedMethods(root, prefix, unlisted=None):
Jack Rosenthal8de609d2023-02-09 13:20:35 -0700445 """Finds exposed CherryPy methods.
Gilad Arnoldd5ebaaa2012-10-02 11:52:38 -0700446
Jack Rosenthal8de609d2023-02-09 13:20:35 -0700447 Args:
448 root: the root object for searching
449 prefix: slash-joined chain of members leading to current object
450 unlisted: URLs to be excluded regardless of their exposed status
Don Garrettf84631a2014-01-07 18:21:26 -0800451
Jack Rosenthal8de609d2023-02-09 13:20:35 -0700452 Returns:
453 List of exposed URLs that are not unlisted.
454 """
455 method_list = []
456 for member in root.__class__.__dict__.keys():
457 prefixed_member = prefix + "/" + member if prefix else member
458 if unlisted and prefixed_member in unlisted:
459 continue
460 member_obj = root.__class__.__dict__[member]
461 if _IsExposed(member_obj):
462 if isinstance(member_obj, types.FunctionType):
463 # Regard the app name as exposed "method" name if it exposed 'index'
464 # function.
465 if prefix and member == "index":
466 method_list.append(prefix)
467 else:
468 method_list.append(prefixed_member)
469 else:
470 method_list += _FindExposedMethods(
471 member_obj, prefixed_member, unlisted
472 )
473 return method_list
Gilad Arnoldd5ebaaa2012-10-02 11:52:38 -0700474
475
xixuan52c2fba2016-05-20 17:02:48 -0700476def _parse_boolean_arg(kwargs, key):
Jack Rosenthal8de609d2023-02-09 13:20:35 -0700477 """Parse boolean arg from kwargs.
xixuanac89ce82016-11-30 16:48:20 -0800478
Jack Rosenthal8de609d2023-02-09 13:20:35 -0700479 Args:
480 kwargs: the parameters to be checked.
481 key: the key to be parsed.
xixuanac89ce82016-11-30 16:48:20 -0800482
Jack Rosenthal8de609d2023-02-09 13:20:35 -0700483 Returns:
484 The boolean value of kwargs[key], or False if key doesn't exist in kwargs.
xixuanac89ce82016-11-30 16:48:20 -0800485
Jack Rosenthal8de609d2023-02-09 13:20:35 -0700486 Raises:
487 DevServerHTTPError if kwargs[key] is not a boolean variable.
488 """
489 if key in kwargs:
490 if kwargs[key] == "True":
491 return True
492 elif kwargs[key] == "False":
493 return False
494 else:
495 raise DevServerHTTPError(
496 http_client.INTERNAL_SERVER_ERROR,
497 "The value for key %s is not boolean." % key,
498 )
xixuan52c2fba2016-05-20 17:02:48 -0700499 else:
Jack Rosenthal8de609d2023-02-09 13:20:35 -0700500 return False
xixuan52c2fba2016-05-20 17:02:48 -0700501
xixuan447ad9d2017-02-28 14:46:20 -0800502
xixuanac89ce82016-11-30 16:48:20 -0800503def _parse_string_arg(kwargs, key):
Jack Rosenthal8de609d2023-02-09 13:20:35 -0700504 """Parse string arg from kwargs.
xixuanac89ce82016-11-30 16:48:20 -0800505
Jack Rosenthal8de609d2023-02-09 13:20:35 -0700506 Args:
507 kwargs: the parameters to be checked.
508 key: the key to be parsed.
xixuanac89ce82016-11-30 16:48:20 -0800509
Jack Rosenthal8de609d2023-02-09 13:20:35 -0700510 Returns:
511 The string value of kwargs[key], or None if key doesn't exist in kwargs.
512 """
513 if key in kwargs:
514 return kwargs[key]
515 else:
516 return None
xixuanac89ce82016-11-30 16:48:20 -0800517
xixuan447ad9d2017-02-28 14:46:20 -0800518
xixuanac89ce82016-11-30 16:48:20 -0800519def _build_uri_from_build_name(build_name):
Jack Rosenthal8de609d2023-02-09 13:20:35 -0700520 """Get build url from a given build name.
xixuanac89ce82016-11-30 16:48:20 -0800521
Jack Rosenthal8de609d2023-02-09 13:20:35 -0700522 Args:
523 build_name: the build name to be parsed, whose format is
524 'board/release_version'.
xixuanac89ce82016-11-30 16:48:20 -0800525
Jack Rosenthal8de609d2023-02-09 13:20:35 -0700526 Returns:
527 The release_archive_url on Google Storage for this build name.
528 """
529 # TODO(ahassani): This function doesn't seem to be used anywhere since its
530 # previous use of lib.paygen.gspath was broken and it doesn't seem to be
531 # causing any runtime issues. So deprecate this in the future.
532 tokens = build_name.split("/")
533 return "gs://chromeos-releases/stable-channel/%s/%s" % (
534 tokens[0],
535 tokens[1],
536 )
xixuan52c2fba2016-05-20 17:02:48 -0700537
xixuan447ad9d2017-02-28 14:46:20 -0800538
Sanika Kulkarnid4496fd2020-02-04 17:26:25 -0800539def is_deprecated_server():
Jack Rosenthal8de609d2023-02-09 13:20:35 -0700540 """Gets whether the devserver has deprecated RPCs."""
541 return cherrypy.config.get("infra_removal", False)
Sanika Kulkarnid4496fd2020-02-04 17:26:25 -0800542
543
David Rochberg7c79a812011-01-19 14:24:45 -0500544class DevServerRoot(object):
Jack Rosenthal8de609d2023-02-09 13:20:35 -0700545 """The Root Class for the Dev Server.
Chris Sosa7c931362010-10-11 19:49:01 -0700546
Jack Rosenthal8de609d2023-02-09 13:20:35 -0700547 CherryPy works as follows:
548 For each method in this class, cherrpy interprets root/path
549 as a call to an instance of DevServerRoot->method_name. For example,
550 a call to http://myhost/build will call build. CherryPy automatically
551 parses http args and places them as keyword arguments in each method.
552 For paths http://myhost/update/dir1/dir2, you can use *args so that
553 cherrypy uses the update method and puts the extra paths in args.
Dan Shif8eb0d12013-08-01 17:52:06 -0700554 """
Dan Shi59ae7092013-06-04 14:37:27 -0700555
Jack Rosenthal8de609d2023-02-09 13:20:35 -0700556 # Method names that should not be listed on the index page.
557 _UNLISTED_METHODS = ["index", "doc"]
Prashanth Ba06d2d22014-03-07 15:35:19 -0800558
Jack Rosenthal8de609d2023-02-09 13:20:35 -0700559 # Number of threads that devserver is staging images.
560 _staging_thread_count = 0
561 # Lock used to lock increasing/decreasing count.
562 _staging_thread_count_lock = threading.Lock()
Prashanth Ba06d2d22014-03-07 15:35:19 -0800563
Jack Rosenthal8de609d2023-02-09 13:20:35 -0700564 def __init__(self, _xbuddy):
565 self._builder = None
566 self._telemetry_lock_dict = common_util.LockDict()
567 self._xbuddy = _xbuddy
Congbin Guo3afae6c2019-08-13 16:29:42 -0700568
Jack Rosenthal8de609d2023-02-09 13:20:35 -0700569 @property
570 def staging_thread_count(self):
571 """Get the staging thread count."""
572 return self._staging_thread_count
Prashanth Ba06d2d22014-03-07 15:35:19 -0800573
Jack Rosenthal8de609d2023-02-09 13:20:35 -0700574 @cherrypy.expose
575 def build(self, board, pkg, **kwargs):
576 """Builds the package specified."""
577 if is_deprecated_server():
578 raise DeprecatedRPCError("build")
Chris Sosa76e44b92013-01-31 12:11:38 -0800579
Jack Rosenthal8de609d2023-02-09 13:20:35 -0700580 import builder
Chris Sosa76e44b92013-01-31 12:11:38 -0800581
Jack Rosenthal8de609d2023-02-09 13:20:35 -0700582 if self._builder is None:
583 self._builder = builder.Builder()
584 return self._builder.Build(board, pkg, kwargs)
Chris Sosa76e44b92013-01-31 12:11:38 -0800585
Jack Rosenthal8de609d2023-02-09 13:20:35 -0700586 @cherrypy.expose
587 def is_staged(self, **kwargs):
588 """Check if artifacts have been downloaded.
Chris Sosa76e44b92013-01-31 12:11:38 -0800589
Jack Rosenthal8de609d2023-02-09 13:20:35 -0700590 Examples:
591 To check if autotest and test_suites are staged:
592 http://devserver_url:<port>/is_staged?archive_url=gs://your_url/path&
593 artifacts=autotest,test_suites
Chris Sosa76e44b92013-01-31 12:11:38 -0800594
Jack Rosenthal8de609d2023-02-09 13:20:35 -0700595 Args:
596 async: True to return without waiting for download to complete.
597 artifacts: Comma separated list of named artifacts to download.
598 These are defined in artifact_info and have their implementation
599 in build_artifact.py.
600 files: Comma separated list of file artifacts to stage. These
601 will be available as is in the corresponding static directory with no
602 custom post-processing.
Chris Sosa76e44b92013-01-31 12:11:38 -0800603
Jack Rosenthal8de609d2023-02-09 13:20:35 -0700604 Returns:
605 True of all artifacts are staged.
606 """
607 dl, factory = _get_downloader_and_factory(kwargs)
608 response = str(dl.IsStaged(factory))
609 _Log("Responding to is_staged %s request with %r", kwargs, response)
610 return response
Chris Sosa76e44b92013-01-31 12:11:38 -0800611
Jack Rosenthal8de609d2023-02-09 13:20:35 -0700612 @cherrypy.expose
613 def list_image_dir(self, **kwargs):
614 """Take an archive url and list the contents in its staged directory.
Chris Sosa76e44b92013-01-31 12:11:38 -0800615
Jack Rosenthal8de609d2023-02-09 13:20:35 -0700616 Examples:
617 To list the contents of where this devserver should have staged
618 gs://image-archive/<board>-release/<build> call:
619 http://devserver_url:<port>/list_image_dir?archive_url=<gs://..>
Congbin Guo3afae6c2019-08-13 16:29:42 -0700620
Jack Rosenthal8de609d2023-02-09 13:20:35 -0700621 Args:
622 archive_url: Google Storage URL for the build.
Gabe Black3b567202015-09-23 14:07:59 -0700623
Jack Rosenthal8de609d2023-02-09 13:20:35 -0700624 Returns:
625 A string with information about the contents of the image directory.
626 """
627 dl = _get_downloader(kwargs)
Simran Basi4baad082013-02-14 13:39:18 -0800628 try:
Jack Rosenthal8de609d2023-02-09 13:20:35 -0700629 image_dir_contents = dl.ListBuildDir()
630 except build_artifact.ArtifactDownloadError as e:
631 return "Cannot list the contents of staged artifacts. %s" % e
632 if not image_dir_contents:
633 return (
634 "%s has not been staged on this devserver."
635 % dl.DescribeSource()
636 )
637 return image_dir_contents
Simran Basi4baad082013-02-14 13:39:18 -0800638
Jack Rosenthal8de609d2023-02-09 13:20:35 -0700639 @cherrypy.expose
640 def stage(self, **kwargs):
641 """Downloads and caches build artifacts.
642
643 Downloads and caches build artifacts, possibly from a Google Storage URL,
644 or from Android's build server. Returns once these have been downloaded
645 on the devserver. A call to this will attempt to cache non-specified
646 artifacts in the background for the given from the given URL following
647 the principle of spatial locality. Spatial locality of different
648 artifacts is explicitly defined in the build_artifact module.
649
650 These artifacts will then be available from the static/ sub-directory of
651 the devserver.
652
653 Examples:
654 To download the autotest and test suites tarballs:
655 http://devserver_url:<port>/stage?archive_url=gs://your_url/path&
656 artifacts=autotest,test_suites
657 To download the full update payload:
658 http://devserver_url:<port>/stage?archive_url=gs://your_url/path&
659 artifacts=full_payload
660 To download just a file called blah.bin:
661 http://devserver_url:<port>/stage?archive_url=gs://your_url/path&
662 files=blah.bin
663
664 For both these examples, one could find these artifacts at:
665 http://devserver_url:<port>/static/<relative_path>*
666
667 Note for this example, relative path is the archive_url stripped of its
668 basename i.e. path/ in the examples above. Specific example:
669
670 gs://chromeos-image-archive/x86-mario-release/R26-3920.0.0
671
672 Will get staged to:
673
674 http://devserver_url:<port>/static/x86-mario-release/R26-3920.0.0
675
676 Args:
677 archive_url: Google Storage URL for the build.
678 local_path: Local path for the build.
679 delete_source: Only meaningful with local_path. bool to indicate if the
680 source files should be deleted. This is especially useful when staging
681 a file locally in resource constrained environments as it allows us to
682 move the relevant files locally instead of copying them.
683 async: True to return without waiting for download to complete.
684 artifacts: Comma separated list of named artifacts to download.
685 These are defined in artifact_info and have their implementation
686 in build_artifact.py.
687 files: Comma separated list of files to stage. These
688 will be available as is in the corresponding static directory with no
689 custom post-processing.
690 clean: True to remove any previously staged artifacts first.
691 """
692 dl, factory = _get_downloader_and_factory(kwargs)
693
694 with DevServerRoot._staging_thread_count_lock:
695 DevServerRoot._staging_thread_count += 1
696 try:
697 boolean_string = kwargs.get("clean")
698 clean = xbuddy.XBuddy.ParseBoolean(boolean_string)
699 if clean and os.path.exists(dl.GetBuildDir()):
700 _Log("Removing %s" % dl.GetBuildDir())
701 shutil.rmtree(dl.GetBuildDir())
702 dl.Download(factory)
703 finally:
704 with DevServerRoot._staging_thread_count_lock:
705 DevServerRoot._staging_thread_count -= 1
706 return "Success"
707
708 @cherrypy.expose
709 def locate_file(self, **kwargs):
710 """Get the path to the given file name.
711
712 This method looks up the given file name inside specified build artifacts.
713 One use case is to help caller to locate an apk file inside a build
714 artifact. The location of the apk file could be different based on the
715 branch and target.
716
717 Args:
718 file_name: Name of the file to look for.
719 artifacts: A list of artifact names to search for the file.
720
721 Returns:
722 Path to the file with the given name. It's relative to the folder for the
723 build, e.g., DATA/priv-app/sl4a/sl4a.apk
724 """
725 if is_deprecated_server():
726 raise DeprecatedRPCError("locate_file")
727
728 dl, _ = _get_downloader_and_factory(kwargs)
729 try:
730 file_name = kwargs["file_name"]
731 artifacts = kwargs["artifacts"]
732 except KeyError:
733 raise DevServerError(
734 "`file_name` and `artifacts` are required to search "
735 "for a file in build artifacts."
736 )
737 build_path = dl.GetBuildDir()
738 for artifact in artifacts:
739 # Get the unzipped folder of the artifact. If it's not defined in
740 # ARTIFACT_UNZIP_FOLDER_MAP, assume the files are unzipped to the build
741 # directory directly.
742 folder = artifact_info.ARTIFACT_UNZIP_FOLDER_MAP.get(artifact, "")
743 artifact_path = os.path.join(build_path, folder)
744 for root, _, filenames in os.walk(artifact_path):
745 if file_name in set([f for f in filenames]):
746 return os.path.relpath(
747 os.path.join(root, file_name), build_path
748 )
Amin Hassanid4e35392019-10-03 11:02:44 -0700749 raise DevServerError(
Jack Rosenthal8de609d2023-02-09 13:20:35 -0700750 "File `%s` can not be found in artifacts: %s"
751 % (file_name, artifacts)
752 )
Simran Basi4baad082013-02-14 13:39:18 -0800753
Jack Rosenthal8de609d2023-02-09 13:20:35 -0700754 @cherrypy.expose
755 def setup_telemetry(self, **kwargs):
756 """Extracts and sets up telemetry
Simran Basi4baad082013-02-14 13:39:18 -0800757
Jack Rosenthal8de609d2023-02-09 13:20:35 -0700758 This method goes through the telemetry deps packages, and stages them on
759 the devserver to be used by the drones and the telemetry tests.
Chris Masone816e38c2012-05-02 12:22:36 -0700760
Jack Rosenthal8de609d2023-02-09 13:20:35 -0700761 Args:
762 archive_url: Google Storage URL for the build.
Chris Masone816e38c2012-05-02 12:22:36 -0700763
Jack Rosenthal8de609d2023-02-09 13:20:35 -0700764 Returns:
765 Path to the source folder for the telemetry codebase once it is staged.
766 """
767 dl = _get_downloader(kwargs)
Sanika Kulkarnid4496fd2020-02-04 17:26:25 -0800768
Jack Rosenthal8de609d2023-02-09 13:20:35 -0700769 build_path = dl.GetBuildDir()
770 deps_path = os.path.join(build_path, "autotest/packages")
771 telemetry_path = os.path.join(build_path, TELEMETRY_FOLDER)
772 src_folder = os.path.join(telemetry_path, "src")
Dan Shif08fe492016-10-04 14:39:25 -0700773
Jack Rosenthal8de609d2023-02-09 13:20:35 -0700774 with self._telemetry_lock_dict.lock(telemetry_path):
775 if os.path.exists(src_folder):
776 # Telemetry is already fully stage return
777 return src_folder
Chris Sosa76e44b92013-01-31 12:11:38 -0800778
Jack Rosenthal8de609d2023-02-09 13:20:35 -0700779 common_util.MkDirP(telemetry_path)
Chris Sosa76e44b92013-01-31 12:11:38 -0800780
Jack Rosenthal8de609d2023-02-09 13:20:35 -0700781 # Copy over the required deps tar balls to the telemetry directory.
782 for dep in TELEMETRY_DEPS:
783 dep_path = os.path.join(deps_path, dep)
784 if not os.path.exists(dep_path):
785 # This dep does not exist (could be new), do not extract it.
786 continue
787 try:
788 cros_build_lib.ExtractTarball(dep_path, telemetry_path)
789 except cros_build_lib.TarballError as e:
790 shutil.rmtree(telemetry_path)
791 raise DevServerError(str(e))
Chris Sosa76e44b92013-01-31 12:11:38 -0800792
Jack Rosenthal8de609d2023-02-09 13:20:35 -0700793 # By default all the tarballs extract to test_src but some parts of
794 # the telemetry code specifically hardcoded to exist inside of 'src'.
795 test_src = os.path.join(telemetry_path, "test_src")
796 try:
797 shutil.move(test_src, src_folder)
798 except shutil.Error:
799 # This can occur if src_folder already exists. Remove and retry move.
800 shutil.rmtree(src_folder)
801 raise DevServerError(
802 "Failure in telemetry setup for build %s. Appears that the "
803 "test_src to src move failed." % dl.GetBuild()
804 )
Chris Sosa76e44b92013-01-31 12:11:38 -0800805
Jack Rosenthal8de609d2023-02-09 13:20:35 -0700806 return src_folder
Chris Sosa76e44b92013-01-31 12:11:38 -0800807
Jack Rosenthal8de609d2023-02-09 13:20:35 -0700808 @cherrypy.expose
809 def symbolicate_dump(self, minidump, **kwargs):
810 """Symbolicates a minidump using pre-downloaded symbols, returns it.
Chris Masone816e38c2012-05-02 12:22:36 -0700811
Jack Rosenthal8de609d2023-02-09 13:20:35 -0700812 Callers will need to POST to this URL with a body of MIME-type
813 "multipart/form-data".
814 The body should include a single argument, 'minidump', containing the
815 binary-formatted minidump to symbolicate.
Chris Masone816e38c2012-05-02 12:22:36 -0700816
Jack Rosenthal8de609d2023-02-09 13:20:35 -0700817 Args:
818 archive_url: Google Storage URL for the build.
819 minidump: The binary minidump file to symbolicate.
820 """
821 if is_deprecated_server():
822 raise DeprecatedRPCError("symbolicate_dump")
Scott Zawalski16954532012-03-20 15:31:36 -0400823
Jack Rosenthal8de609d2023-02-09 13:20:35 -0700824 # Ensure the symbols have been staged.
825 # Try debug.tar.xz first, then debug.tgz
826 for artifact in (artifact_info.SYMBOLS_ONLY, artifact_info.SYMBOLS):
827 kwargs["artifacts"] = artifact
828 dl = _get_downloader(kwargs)
Don Garrettf84631a2014-01-07 18:21:26 -0800829
Jack Rosenthal8de609d2023-02-09 13:20:35 -0700830 try:
831 if self.stage(**kwargs) == "Success":
832 break
833 except build_artifact.ArtifactDownloadError:
834 continue
835 else:
836 raise DevServerError(
837 "Failed to stage symbols for %s" % dl.DescribeSource()
838 )
Sanika Kulkarni07d47ed2020-08-06 14:56:46 -0700839
Jack Rosenthal8de609d2023-02-09 13:20:35 -0700840 to_return = ""
841 with tempfile.NamedTemporaryFile() as local:
842 while True:
843 data = minidump.file.read(8192)
844 if not data:
845 break
846 local.write(data)
Scott Zawalski16954532012-03-20 15:31:36 -0400847
Jack Rosenthal8de609d2023-02-09 13:20:35 -0700848 local.flush()
Dan Shi61305df2015-10-26 16:52:35 -0700849
Jack Rosenthal8de609d2023-02-09 13:20:35 -0700850 symbols_directory = os.path.join(
851 dl.GetBuildDir(), "debug", "breakpad"
852 )
Dan Shi61305df2015-10-26 16:52:35 -0700853
Jack Rosenthal8de609d2023-02-09 13:20:35 -0700854 # The location of minidump_stackwalk is defined in chromeos-admin.
855 stackwalk = subprocess.Popen(
856 [
857 "/usr/local/bin/minidump_stackwalk",
858 local.name,
859 symbols_directory,
860 ],
861 stdout=subprocess.PIPE,
862 stderr=subprocess.PIPE,
863 )
Scott Zawalski16954532012-03-20 15:31:36 -0400864
Jack Rosenthal8de609d2023-02-09 13:20:35 -0700865 to_return, error_text = stackwalk.communicate()
866 if stackwalk.returncode != 0:
867 raise DevServerError(
868 "Can't generate stack trace: %s (rc=%d)"
869 % (error_text, stackwalk.returncode)
870 )
xixuan7efd0002016-04-14 15:34:01 -0700871
Jack Rosenthal8de609d2023-02-09 13:20:35 -0700872 return to_return
xixuan7efd0002016-04-14 15:34:01 -0700873
Jack Rosenthal8de609d2023-02-09 13:20:35 -0700874 @cherrypy.expose
875 def latestbuild(self, **kwargs):
876 """Return a string representing the latest build for a given target.
xixuan7efd0002016-04-14 15:34:01 -0700877
Jack Rosenthal8de609d2023-02-09 13:20:35 -0700878 Args:
879 target: The build target, typically a combination of the board and the
880 type of build e.g. x86-mario-release.
881 milestone: The milestone to filter builds on. E.g. R16. Optional, if not
882 provided the latest RXX build will be returned.
Sanika Kulkarnid4496fd2020-02-04 17:26:25 -0800883
Jack Rosenthal8de609d2023-02-09 13:20:35 -0700884 Returns:
885 A string representation of the latest build if one exists, i.e.
886 R19-1993.0.0-a1-b1480.
887 An empty string if no latest could be found.
888 """
889 if is_deprecated_server():
890 raise DeprecatedRPCError("latestbuild")
xixuan7efd0002016-04-14 15:34:01 -0700891
Jack Rosenthal8de609d2023-02-09 13:20:35 -0700892 if not kwargs:
893 return _PrintDocStringAsHTML(self.latestbuild)
xixuan7efd0002016-04-14 15:34:01 -0700894
Jack Rosenthal8de609d2023-02-09 13:20:35 -0700895 if "target" not in kwargs:
896 raise DevServerHTTPError(
897 http_client.INTERNAL_SERVER_ERROR, "Error: target= is required!"
898 )
xixuan7efd0002016-04-14 15:34:01 -0700899
Jack Rosenthal8de609d2023-02-09 13:20:35 -0700900 if _is_android_build_request(kwargs):
901 branch = kwargs.get("branch", None)
902 target = kwargs.get("target", None)
903 if not target or not branch:
904 raise DevServerError(
905 "Both target and branch must be specified to query"
906 " for the latest Android build."
907 )
908 return android_build.BuildAccessor.GetLatestBuildID(target, branch)
xixuan7efd0002016-04-14 15:34:01 -0700909
Jack Rosenthal8de609d2023-02-09 13:20:35 -0700910 try:
911 return common_util.GetLatestBuildVersion(
912 updater.static_dir,
913 kwargs["target"],
914 milestone=kwargs.get("milestone"),
915 )
916 except common_util.CommonUtilError as errmsg:
917 raise DevServerHTTPError(
918 http_client.INTERNAL_SERVER_ERROR, str(errmsg)
919 )
xixuan7efd0002016-04-14 15:34:01 -0700920
Jack Rosenthal8de609d2023-02-09 13:20:35 -0700921 @cherrypy.expose
922 def list_suite_controls(self, **kwargs):
923 """Return a list of contents of all known control files.
xixuan7efd0002016-04-14 15:34:01 -0700924
Jack Rosenthal8de609d2023-02-09 13:20:35 -0700925 Example URL:
926 To List all control files' content:
927 http://dev-server/list_suite_controls?suite_name=bvt&
928 build=daisy_spring-release/R29-4279.0.0
Scott Zawalski4647ce62012-01-03 17:17:28 -0500929
Jack Rosenthal8de609d2023-02-09 13:20:35 -0700930 Args:
931 build: The build i.e. x86-alex-release/R18-1514.0.0-a1-b1450.
932 suite_name: List the control files belonging to that suite.
Scott Zawalski4647ce62012-01-03 17:17:28 -0500933
Jack Rosenthal8de609d2023-02-09 13:20:35 -0700934 Returns:
935 A dictionary of all control files's path to its content for given suite.
936 """
937 if is_deprecated_server():
938 raise DeprecatedRPCError("list_suite_controls")
Don Garrettf84631a2014-01-07 18:21:26 -0800939
Jack Rosenthal8de609d2023-02-09 13:20:35 -0700940 if not kwargs:
941 return _PrintDocStringAsHTML(self.controlfiles)
Sanika Kulkarnid4496fd2020-02-04 17:26:25 -0800942
Jack Rosenthal8de609d2023-02-09 13:20:35 -0700943 if "build" not in kwargs:
944 raise DevServerHTTPError(
945 http_client.INTERNAL_SERVER_ERROR, "Error: build= is required!"
946 )
947
948 if "suite_name" not in kwargs:
949 raise DevServerHTTPError(
950 http_client.INTERNAL_SERVER_ERROR,
951 "Error: suite_name= is required!",
952 )
953
954 control_file_list = [
955 line.rstrip()
956 for line in common_util.GetControlFileListForSuite(
957 updater.static_dir, kwargs["build"], kwargs["suite_name"]
958 ).splitlines()
959 ]
960
961 control_file_content_dict = {}
962 for control_path in control_file_list:
963 control_file_content_dict[
964 control_path
965 ] = common_util.GetControlFile(
966 updater.static_dir, kwargs["build"], control_path
967 )
968
969 return json.dumps(control_file_content_dict)
970
971 @cherrypy.expose
972 def controlfiles(self, **kwargs):
973 """Return a control file or a list of all known control files.
974
975 Example URL:
976 To List all control files:
977 http://dev-server/controlfiles?suite_name=&build=daisy_spring-release/R29-4279.0.0
978 To List all control files for, say, the bvt suite:
979 http://dev-server/controlfiles?suite_name=bvt&build=daisy_spring-release/R29-4279.0.0
980 To return the contents of a path:
981 http://dev-server/controlfiles?board=x86-alex-release&build=R18-1514.0.0&control_path=client/sleeptest/control
982
983 Args:
984 build: The build i.e. x86-alex-release/R18-1514.0.0-a1-b1450.
985 control_path: If you want the contents of a control file set this
986 to the path. E.g. client/site_tests/sleeptest/control
987 Optional, if not provided return a list of control files is returned.
988 suite_name: If control_path is not specified but a suite_name is
989 specified, list the control files belonging to that suite instead of
990 all control files. The empty string for suite_name will list all control
991 files for the build.
992
993 Returns:
994 Contents of a control file if control_path is provided.
995 A list of control files if no control_path is provided.
996 """
997 if is_deprecated_server():
998 raise DeprecatedRPCError("controlfiles")
999
1000 if not kwargs:
1001 return _PrintDocStringAsHTML(self.controlfiles)
Scott Zawalski4647ce62012-01-03 17:17:28 -05001002
Jack Rosenthal8de609d2023-02-09 13:20:35 -07001003 if "build" not in kwargs:
1004 raise DevServerHTTPError(
1005 http_client.INTERNAL_SERVER_ERROR, "Error: build= is required!"
1006 )
Scott Zawalski4647ce62012-01-03 17:17:28 -05001007
Jack Rosenthal8de609d2023-02-09 13:20:35 -07001008 if "control_path" not in kwargs:
1009 if "suite_name" in kwargs and kwargs["suite_name"]:
1010 return common_util.GetControlFileListForSuite(
1011 updater.static_dir, kwargs["build"], kwargs["suite_name"]
1012 )
1013 else:
1014 return common_util.GetControlFileList(
1015 updater.static_dir, kwargs["build"]
1016 )
1017 else:
1018 return common_util.GetControlFile(
1019 updater.static_dir, kwargs["build"], kwargs["control_path"]
1020 )
Frank Farzan40160872011-12-12 18:39:18 -08001021
Jack Rosenthal8de609d2023-02-09 13:20:35 -07001022 @cherrypy.expose
1023 def xbuddy_translate(self, *args, **kwargs):
1024 """Translates an xBuddy path to a real path to artifact if it exists.
Yu-Ju Hong1bdb7a92014-04-10 16:02:11 -07001025
Jack Rosenthal8de609d2023-02-09 13:20:35 -07001026 Args:
1027 args: An xbuddy path in the form of {local|remote}/build_id/artifact.
1028 Local searches the devserver's static directory. Remote searches a
1029 Google Storage image archive.
Simran Basi99e63c02014-05-20 10:39:52 -07001030
Jack Rosenthal8de609d2023-02-09 13:20:35 -07001031 Kwargs:
1032 image_dir: Google Storage image archive to search in if requesting a
1033 remote artifact. If none uses the default bucket.
Yu-Ju Hong1bdb7a92014-04-10 16:02:11 -07001034
Jack Rosenthal8de609d2023-02-09 13:20:35 -07001035 Returns:
1036 String in the format of build_id/artifact as stored on the local server
1037 or in Google Storage.
1038 """
1039 if is_deprecated_server():
1040 raise DeprecatedRPCError("xbuddy_translate")
Sanika Kulkarnid4496fd2020-02-04 17:26:25 -08001041
Jack Rosenthal8de609d2023-02-09 13:20:35 -07001042 build_id, filename = self._xbuddy.Translate(
1043 args, image_dir=kwargs.get("image_dir")
1044 )
1045 response = os.path.join(build_id, filename)
1046 _Log("Path translation requested, returning: %s", response)
1047 return response
Yu-Ju Hong1bdb7a92014-04-10 16:02:11 -07001048
Jack Rosenthal8de609d2023-02-09 13:20:35 -07001049 @cherrypy.expose
1050 def xbuddy(self, *args, **kwargs):
1051 """The full xBuddy call, returns resource specified by path_parts.
joychen3cb228e2013-06-12 12:13:13 -07001052
Jack Rosenthal8de609d2023-02-09 13:20:35 -07001053 Args:
1054 path_parts: the path following xbuddy/ in the call url is split into the
1055 components of the path. The path can be understood as
1056 "{local|remote}/build_id/artifact" where build_id is composed of
1057 "board/version."
joycheneaf4cfc2013-07-02 08:38:57 -07001058
Jack Rosenthal8de609d2023-02-09 13:20:35 -07001059 The first path element is optional, and can be "remote" or "local"
1060 If local (the default), devserver will not attempt to access Google
1061 Storage, and will only search the static directory for the files.
1062 If remote, devserver will try to obtain the artifact off GS if it's
1063 not found locally.
1064 The board is the familiar board name, optionally suffixed.
1065 The version can be the google storage version number, and may also be
1066 any of a number of xBuddy defined version aliases that will be
1067 translated into the latest built image that fits the description.
1068 Defaults to latest.
1069 The artifact is one of a number of image or artifact aliases used by
1070 xbuddy, defined in xbuddy:ALIASES. Defaults to test.
joycheneaf4cfc2013-07-02 08:38:57 -07001071
Jack Rosenthal8de609d2023-02-09 13:20:35 -07001072 Kwargs:
1073 return_dir: {true|false}
1074 if set to true, returns the url to the update.gz
1075 relative_path: {true|false}
1076 if set to true, returns the relative path to the payload
1077 directory from static_dir.
1078 Example URL:
1079 http://host:port/xbuddy/x86-generic/R26-4000.0.0/test
1080 or
1081 http://host:port/xbuddy/x86-generic/R26-4000.0.0/test?return_dir=true
joychen3cb228e2013-06-12 12:13:13 -07001082
Jack Rosenthal8de609d2023-02-09 13:20:35 -07001083 Returns:
1084 If |return_dir|, return a uri to the folder where the artifact is. E.g.,
1085 http://host:port/static/x86-generic-release/R26-4000.0.0/
1086 If |relative_path| is true, return a relative path the folder where the
1087 payloads are. E.g.,
1088 archive/x86-generic-release/R26-4000.0.0
1089 """
1090 if is_deprecated_server():
1091 raise DeprecatedRPCError("xbuddy")
Sanika Kulkarnid4496fd2020-02-04 17:26:25 -08001092
Jack Rosenthal8de609d2023-02-09 13:20:35 -07001093 boolean_string = kwargs.get("return_dir")
1094 return_dir = xbuddy.XBuddy.ParseBoolean(boolean_string)
1095 boolean_string = kwargs.get("relative_path")
1096 relative_path = xbuddy.XBuddy.ParseBoolean(boolean_string)
joychen121fc9b2013-08-02 14:30:30 -07001097
Jack Rosenthal8de609d2023-02-09 13:20:35 -07001098 if return_dir and relative_path:
1099 raise DevServerHTTPError(
1100 http_client.INTERNAL_SERVER_ERROR,
1101 "Cannot specify both return_dir and relative_path",
1102 )
Chris Sosa75490802013-09-30 17:21:45 -07001103
Jack Rosenthal8de609d2023-02-09 13:20:35 -07001104 build_id, file_name = self._xbuddy.Get(args)
Yu-Ju Hong51495eb2013-12-12 17:08:43 -08001105
Jack Rosenthal8de609d2023-02-09 13:20:35 -07001106 response = None
1107 if return_dir:
1108 response = os.path.join(cherrypy.request.base, "static", build_id)
1109 _Log("Directory requested, returning: %s", response)
1110 elif relative_path:
1111 response = build_id
1112 _Log("Relative path requested, returning: %s", response)
1113 else:
1114 # Redirect to download the payload if no kwargs are set.
1115 build_id = "/" + os.path.join("static", build_id, file_name)
1116 _Log("Payload requested, returning: %s", build_id)
1117 raise cherrypy.HTTPRedirect(build_id, 302)
joychen3cb228e2013-06-12 12:13:13 -07001118
Jack Rosenthal8de609d2023-02-09 13:20:35 -07001119 return response
Yu-Ju Hong51495eb2013-12-12 17:08:43 -08001120
Jack Rosenthal8de609d2023-02-09 13:20:35 -07001121 @cherrypy.expose
1122 def xbuddy_capacity(self):
1123 """Returns the number of images cached by xBuddy."""
1124 if is_deprecated_server():
1125 raise DeprecatedRPCError("xbuddy_capacity")
Sanika Kulkarnid4496fd2020-02-04 17:26:25 -08001126
Jack Rosenthal8de609d2023-02-09 13:20:35 -07001127 return self._xbuddy.Capacity()
joychen3cb228e2013-06-12 12:13:13 -07001128
Jack Rosenthal8de609d2023-02-09 13:20:35 -07001129 @cherrypy.expose
1130 def index(self):
1131 """Presents a welcome message and documentation links."""
1132 if is_deprecated_server():
1133 raise DeprecatedRPCError("index")
Sanika Kulkarnid4496fd2020-02-04 17:26:25 -08001134
Jack Rosenthal8de609d2023-02-09 13:20:35 -07001135 html_template = (
1136 "Welcome to the Dev Server!<br>\n"
1137 "<br>\n"
1138 "Here are the available methods, click for documentation:<br>\n"
1139 "<br>\n"
1140 "%s"
1141 )
Congbin Guo6bc32182019-08-20 17:54:30 -07001142
Jack Rosenthal8de609d2023-02-09 13:20:35 -07001143 exposed_methods = []
1144 for app in cherrypy.tree.apps.values():
1145 exposed_methods += _FindExposedMethods(
1146 app.root,
1147 app.script_name.lstrip("/"),
1148 unlisted=self._UNLISTED_METHODS,
1149 )
Congbin Guo6bc32182019-08-20 17:54:30 -07001150
Jack Rosenthal8de609d2023-02-09 13:20:35 -07001151 return html_template % "<br>\n".join(
1152 [
1153 "<a href=doc/%s>%s</a>" % (name, name)
1154 for name in sorted(exposed_methods)
1155 ]
1156 )
Gilad Arnoldf8f769f2012-09-24 08:43:01 -07001157
Jack Rosenthal8de609d2023-02-09 13:20:35 -07001158 @cherrypy.expose
1159 def doc(self, *args):
1160 """Shows the documentation for available methods / URLs.
Gilad Arnoldf8f769f2012-09-24 08:43:01 -07001161
Jack Rosenthal8de609d2023-02-09 13:20:35 -07001162 Examples:
1163 http://myhost/doc/update
1164 """
1165 if is_deprecated_server():
1166 raise DeprecatedRPCError("doc")
Sanika Kulkarnid4496fd2020-02-04 17:26:25 -08001167
Jack Rosenthal8de609d2023-02-09 13:20:35 -07001168 name = "/".join(args)
1169 method = _GetExposedMethod(name)
1170 if not method:
1171 raise DevServerError("No exposed method named `%s'" % name)
1172 if not method.__doc__:
1173 raise DevServerError(
1174 "No documentation for exposed method `%s'" % name
1175 )
1176 return "<pre>\n%s</pre>" % method.__doc__
Chris Sosa7c931362010-10-11 19:49:01 -07001177
Jack Rosenthal8de609d2023-02-09 13:20:35 -07001178 @cherrypy.expose
1179 def update(self, *args, **kwargs):
1180 """Handles an update check from a Chrome OS client.
Amin Hassani2aa34282020-11-18 01:18:19 +00001181
Jack Rosenthal8de609d2023-02-09 13:20:35 -07001182 The HTTP request should contain the standard Omaha-style XML blob. The URL
1183 line may contain an additional intermediate path to the update payload.
Amin Hassani2aa34282020-11-18 01:18:19 +00001184
Jack Rosenthal8de609d2023-02-09 13:20:35 -07001185 This request can be handled in one of 4 ways, depending on the devsever
1186 settings and intermediate path.
Amin Hassani2aa34282020-11-18 01:18:19 +00001187
Jack Rosenthal8de609d2023-02-09 13:20:35 -07001188 1. No intermediate path. DEPRECATED
Amin Hassani2aa34282020-11-18 01:18:19 +00001189
Jack Rosenthal8de609d2023-02-09 13:20:35 -07001190 2. Path explicitly invokes XBuddy
1191 If there is a path given, it can explicitly invoke xbuddy by prefixing it
1192 with 'xbuddy'. This path is then used to acquire an image binary for the
1193 devserver to generate an update payload from. Devserver then serves this
1194 payload.
Amin Hassani2aa34282020-11-18 01:18:19 +00001195
Jack Rosenthal8de609d2023-02-09 13:20:35 -07001196 3. Path is left for the devserver to interpret.
1197 If the path given doesn't explicitly invoke xbuddy, devserver will attempt
1198 to generate a payload from the test image in that directory and serve it.
Amin Hassani2aa34282020-11-18 01:18:19 +00001199
Jack Rosenthal8de609d2023-02-09 13:20:35 -07001200 Examples:
1201 2. Explicitly invoke xbuddy
1202 update_engine_client --omaha_url=
1203 http://myhost/update/xbuddy/remote/board/version/dev
1204 This would go to GS to download the dev image for the board, from which
1205 the devserver would generate a payload to serve.
Amin Hassani2aa34282020-11-18 01:18:19 +00001206
Jack Rosenthal8de609d2023-02-09 13:20:35 -07001207 3. Give a path for devserver to interpret
1208 update_engine_client --omaha_url=http://myhost/update/some/random/path
1209 This would attempt, in order to:
1210 a) Generate an update from a test image binary if found in
1211 static_dir/some/random/path.
1212 b) Serve an update payload found in static_dir/some/random/path.
1213 c) Hope that some/random/path takes the form "board/version" and
1214 and attempt to download an update payload for that board/version
1215 from GS.
1216 """
1217 label = "/".join(args)
1218 body_length = int(cherrypy.request.headers.get("Content-Length", 0))
1219 data = cherrypy.request.rfile.read(body_length)
Amin Hassani2aa34282020-11-18 01:18:19 +00001220
Jack Rosenthal8de609d2023-02-09 13:20:35 -07001221 return updater.HandleUpdatePing(data, label, **kwargs)
Amin Hassani2aa34282020-11-18 01:18:19 +00001222
Dan Shif5ce2de2013-04-25 16:06:32 -07001223
Chris Sosadbc20082012-12-10 13:39:11 -08001224def _CleanCache(cache_dir, wipe):
Jack Rosenthal8de609d2023-02-09 13:20:35 -07001225 """Wipes any excess cached items in the cache_dir.
Chris Sosadbc20082012-12-10 13:39:11 -08001226
Jack Rosenthal8de609d2023-02-09 13:20:35 -07001227 Args:
1228 cache_dir: the directory we are wiping from.
1229 wipe: If True, wipe all the contents -- not just the excess.
1230 """
1231 if wipe:
1232 # Clear the cache and exit on error.
1233 cmd = "rm -rf %s/*" % cache_dir
1234 if os.system(cmd) != 0:
1235 _Log("Failed to clear the cache with %s" % cmd)
1236 sys.exit(1)
1237 else:
1238 # Clear all but the last N cached updates
1239 cmd = "cd %s; ls -tr | head --lines=-%d | xargs rm -rf" % (
1240 cache_dir,
1241 CACHED_ENTRIES,
1242 )
1243 if os.system(cmd) != 0:
1244 _Log("Failed to clean up old delta cache files with %s" % cmd)
1245 sys.exit(1)
Chris Sosadbc20082012-12-10 13:39:11 -08001246
1247
Chris Sosa3ae4dc12013-03-29 11:47:00 -07001248def _AddTestingOptions(parser):
Jack Rosenthal8de609d2023-02-09 13:20:35 -07001249 group = optparse.OptionGroup(
1250 parser,
1251 "Advanced Testing Options",
1252 "These are used by test scripts and "
1253 "developers writing integration tests utilizing the devserver. They are "
1254 "not intended to be really used outside the scope of someone "
1255 "knowledgable about the test.",
1256 )
1257 group.add_option(
1258 "--exit",
1259 action="store_true",
1260 help="do not start the server (yet clear cache)",
1261 )
1262 parser.add_option_group(group)
Chris Sosa3ae4dc12013-03-29 11:47:00 -07001263
1264
Chris Sosa3ae4dc12013-03-29 11:47:00 -07001265def _AddProductionOptions(parser):
Jack Rosenthal8de609d2023-02-09 13:20:35 -07001266 group = optparse.OptionGroup(
1267 parser,
1268 "Advanced Server Options",
1269 "These options can be used to changed " "for advanced server behavior.",
1270 )
1271 group.add_option(
1272 "--clear_cache",
1273 action="store_true",
1274 default=False,
1275 help="At startup, removes all cached entries from the"
1276 "devserver's cache.",
1277 )
1278 group.add_option(
1279 "--logfile",
1280 metavar="PATH",
1281 help="log output to this file instead of stdout",
1282 )
1283 group.add_option(
1284 "--pidfile",
1285 metavar="PATH",
1286 help="path to output a pid file for the server.",
1287 )
1288 group.add_option(
1289 "--portfile",
1290 metavar="PATH",
1291 help="path to output the port number being served on.",
1292 )
1293 group.add_option(
1294 "--production",
1295 action="store_true",
1296 default=False,
1297 help="have the devserver use production values when "
1298 "starting up. This includes using more threads and "
1299 "performing less logging.",
1300 )
1301 parser.add_option_group(group)
Chris Sosa3ae4dc12013-03-29 11:47:00 -07001302
1303
Paul Hobbsef4e0702016-06-27 17:01:42 -07001304def MakeLogHandler(logfile):
Jack Rosenthal8de609d2023-02-09 13:20:35 -07001305 """Create a LogHandler instance used to log all messages."""
1306 hdlr_cls = handlers.TimedRotatingFileHandler
1307 hdlr = hdlr_cls(
1308 logfile,
1309 when=_LOG_ROTATION_TIME,
1310 interval=_LOG_ROTATION_INTERVAL,
1311 backupCount=_LOG_ROTATION_BACKUP,
1312 )
1313 hdlr.setFormatter(cplogging.logfmt)
1314 return hdlr
J. Richard Barnette3d977b82013-04-23 11:05:19 -07001315
1316
Chris Sosacde6bf42012-05-31 18:36:39 -07001317def main():
Jack Rosenthal8de609d2023-02-09 13:20:35 -07001318 usage = "\n\n".join(["usage: %prog [options]", __doc__])
1319 parser = optparse.OptionParser(usage=usage)
joychened64b222013-06-21 16:39:34 -07001320
Jack Rosenthal8de609d2023-02-09 13:20:35 -07001321 # get directory that the devserver is run from
1322 devserver_dir = os.path.dirname(os.path.abspath(sys.argv[0]))
1323 default_static_dir = "%s/static" % devserver_dir
1324 parser.add_option(
1325 "--static_dir",
1326 metavar="PATH",
1327 default=default_static_dir,
1328 help="writable static directory",
1329 )
1330 parser.add_option(
1331 "--port",
1332 default=8080,
1333 type="int",
1334 help=(
1335 "port for the dev server to use; if zero, binds to "
1336 "an arbitrary available port (default: 8080)"
1337 ),
1338 )
1339 parser.add_option(
1340 "-t", "--test_image", action="store_true", help="Deprecated."
1341 )
1342 parser.add_option(
1343 "-x",
1344 "--xbuddy_manage_builds",
1345 action="store_true",
1346 default=False,
1347 help="If set, allow xbuddy to manage images in" "build/images.",
1348 )
1349 parser.add_option(
1350 "-a",
1351 "--android_build_credential",
1352 default=None,
1353 help="Path to a json file which contains the credential "
1354 "needed to access Android builds.",
1355 )
1356 parser.add_option(
1357 "--infra_removal",
1358 action="store_true",
1359 default=False,
1360 help="If option is present, some RPCs will be disabled to "
1361 "help with infra removal efforts. See "
1362 "go/devserver-deprecation",
1363 )
1364 _AddProductionOptions(parser)
1365 _AddTestingOptions(parser)
1366 (options, _) = parser.parse_args()
rtc@google.com21a5ca32009-11-04 18:23:23 +00001367
Jack Rosenthal8de609d2023-02-09 13:20:35 -07001368 # Handle options that must be set globally in cherrypy. Do this
1369 # work up front, because calls to _Log() below depend on this
1370 # initialization.
1371 if options.production:
1372 cherrypy.config.update({"environment": "production"})
1373 cherrypy.config.update({"infra_removal": options.infra_removal})
1374 if not options.logfile:
1375 cherrypy.config.update({"log.screen": True})
1376 else:
1377 cherrypy.config.update({"log.error_file": "", "log.access_file": ""})
1378 hdlr = MakeLogHandler(options.logfile)
1379 # Pylint can't seem to process these two calls properly
1380 # pylint: disable=E1101
1381 cherrypy.log.access_log.addHandler(hdlr)
1382 cherrypy.log.error_log.addHandler(hdlr)
1383 # pylint: enable=E1101
J. Richard Barnette3d977b82013-04-23 11:05:19 -07001384
Jack Rosenthal8de609d2023-02-09 13:20:35 -07001385 # set static_dir, from which everything will be served
1386 options.static_dir = os.path.realpath(options.static_dir)
Chris Sosa0356d3b2010-09-16 15:46:22 -07001387
Jack Rosenthal8de609d2023-02-09 13:20:35 -07001388 cache_dir = os.path.join(options.static_dir, "cache")
1389 # If our devserver is only supposed to serve payloads, we shouldn't be
1390 # mucking with the cache at all. If the devserver hadn't previously
1391 # generated a cache and is expected, the caller is using it wrong.
1392 if os.path.exists(cache_dir):
1393 _CleanCache(cache_dir, options.clear_cache)
1394 else:
1395 os.makedirs(cache_dir)
Don Garrettf90edf02010-11-16 17:36:14 -08001396
Jack Rosenthal8de609d2023-02-09 13:20:35 -07001397 pkgroot_dir = os.path.join(options.static_dir, "pkgroot")
1398 common_util.SymlinkFile("/build", pkgroot_dir)
Amin Hassanief523622020-07-06 12:09:23 -07001399
Jack Rosenthal8de609d2023-02-09 13:20:35 -07001400 _Log("Using cache directory %s" % cache_dir)
1401 _Log("Serving from %s" % options.static_dir)
rtc@google.com21a5ca32009-11-04 18:23:23 +00001402
Jack Rosenthal8de609d2023-02-09 13:20:35 -07001403 _xbuddy = xbuddy.XBuddy(
1404 manage_builds=options.xbuddy_manage_builds,
1405 static_dir=options.static_dir,
1406 )
1407 if options.clear_cache and options.xbuddy_manage_builds:
1408 _xbuddy.CleanCache()
joychen121fc9b2013-08-02 14:30:30 -07001409
Jack Rosenthal8de609d2023-02-09 13:20:35 -07001410 # We allow global use here to share with cherrypy classes.
1411 # pylint: disable=W0603
1412 global updater
1413 updater = autoupdate.Autoupdate(_xbuddy, static_dir=options.static_dir)
Amin Hassani2aa34282020-11-18 01:18:19 +00001414
Jack Rosenthal8de609d2023-02-09 13:20:35 -07001415 if options.exit:
1416 return
Chris Sosa2f1c41e2012-07-10 14:32:33 -07001417
Jack Rosenthal8de609d2023-02-09 13:20:35 -07001418 dev_server = DevServerRoot(_xbuddy)
1419 health_checker_app = health_checker.Root(dev_server, options.static_dir)
joychen3cb228e2013-06-12 12:13:13 -07001420
Jack Rosenthal8de609d2023-02-09 13:20:35 -07001421 if options.pidfile:
1422 plugins.PIDFile(cherrypy.engine, options.pidfile).subscribe()
Chris Sosa855b8932013-08-21 13:24:55 -07001423
Jack Rosenthal8de609d2023-02-09 13:20:35 -07001424 if options.portfile:
1425 cherrypy_ext.PortFile(cherrypy.engine, options.portfile).subscribe()
Gilad Arnold11fbef42014-02-10 11:04:13 -08001426
Jack Rosenthal8de609d2023-02-09 13:20:35 -07001427 if options.android_build_credential and os.path.exists(
1428 options.android_build_credential
1429 ):
1430 try:
1431 with open(options.android_build_credential) as f:
1432 android_build.BuildAccessor.credential_info = json.load(f)
1433 except ValueError as e:
1434 _Log(
1435 "Failed to load the android build credential: %s. Error: %s."
1436 % (options.android_build_credential, e)
1437 )
Congbin Guo3afae6c2019-08-13 16:29:42 -07001438
Jack Rosenthal8de609d2023-02-09 13:20:35 -07001439 cherrypy.tree.mount(
1440 health_checker_app, "/check_health", config=health_checker.get_config()
1441 )
1442 cherrypy.quickstart(dev_server, config=_GetConfig(options))
Chris Sosacde6bf42012-05-31 18:36:39 -07001443
1444
Jack Rosenthal8de609d2023-02-09 13:20:35 -07001445if __name__ == "__main__":
1446 main()