| OLD | NEW |
| 1 # Copyright (c) 2012 The Chromium Authors. All rights reserved. | 1 # Copyright (c) 2012 The Chromium Authors. All rights reserved. |
| 2 # Use of this source code is governed by a BSD-style license that can be | 2 # Use of this source code is governed by a BSD-style license that can be |
| 3 # found in the LICENSE file. | 3 # found in the LICENSE file. |
| 4 | 4 |
| 5 import logging | 5 from appengine_wrappers import webapp |
| 6 import os | 6 from cron_servlet import CronServlet |
| 7 from StringIO import StringIO | 7 from render_servlet import RenderServlet |
| 8 import traceback | |
| 9 | 8 |
| 10 from appengine_wrappers import ( | 9 _SERVLETS = { |
| 11 DeadlineExceededError, IsDevServer, logservice, memcache, urlfetch, webapp) | 10 'cron': CronServlet, |
| 12 from branch_utility import BranchUtility | 11 } |
| 13 from server_instance import ServerInstance | |
| 14 import svn_constants | |
| 15 import time | |
| 16 | |
| 17 # The default channel to serve docs for if no channel is specified. | |
| 18 _DEFAULT_CHANNEL = 'stable' | |
| 19 | 12 |
| 20 class Handler(webapp.RequestHandler): | 13 class Handler(webapp.RequestHandler): |
| 21 # AppEngine instances should never need to call out to SVN. That should only | |
| 22 # ever be done by the cronjobs, which then write the result into DataStore, | |
| 23 # which is as far as instances look. | |
| 24 # | |
| 25 # Why? SVN is slow and a bit flaky. Cronjobs failing is annoying but | |
| 26 # temporary. Instances failing affects users, and is really bad. | |
| 27 # | |
| 28 # Anyway - to enforce this, we actually don't give instances access to SVN. | |
| 29 # If anything is missing from datastore, it'll be a 404. If the cronjobs | |
| 30 # don't manage to catch everything - uhoh. On the other hand, we'll figure it | |
| 31 # out pretty soon, and it also means that legitimate 404s are caught before a | |
| 32 # round trip to SVN. | |
| 33 # | |
| 34 # However, we can't expect users of preview.py nor the dev server to run a | |
| 35 # cronjob first, so, this is a hack allow that to be online all of the time. | |
| 36 # TODO(kalman): achieve this via proper dependency injection. | |
| 37 ALWAYS_ONLINE = IsDevServer() | |
| 38 | |
| 39 def __init__(self, request, response): | 14 def __init__(self, request, response): |
| 40 super(Handler, self).__init__(request, response) | 15 super(Handler, self).__init__(request, response) |
| 41 | 16 |
| 42 def _HandleGet(self, path): | |
| 43 channel_name, real_path = BranchUtility.SplitChannelNameFromPath(path) | |
| 44 | |
| 45 if channel_name == _DEFAULT_CHANNEL: | |
| 46 self.redirect('/%s' % real_path) | |
| 47 return | |
| 48 | |
| 49 if channel_name is None: | |
| 50 channel_name = _DEFAULT_CHANNEL | |
| 51 | |
| 52 # TODO(kalman): Check if |path| is a directory and serve path/index.html | |
| 53 # rather than special-casing apps/extensions. | |
| 54 if real_path.strip('/') == 'apps': | |
| 55 real_path = 'apps/index.html' | |
| 56 if real_path.strip('/') == 'extensions': | |
| 57 real_path = 'extensions/index.html' | |
| 58 | |
| 59 constructor = ( | |
| 60 ServerInstance.CreateOnline if Handler.ALWAYS_ONLINE else | |
| 61 ServerInstance.GetOrCreateOffline) | |
| 62 server_instance = constructor(channel_name) | |
| 63 | |
| 64 canonical_path = server_instance.path_canonicalizer.Canonicalize(real_path) | |
| 65 if real_path != canonical_path: | |
| 66 self.redirect(canonical_path) | |
| 67 return | |
| 68 | |
| 69 server_instance.Get(real_path, self.request, self.response) | |
| 70 | |
| 71 def _HandleCron(self, path): | |
| 72 # Cron strategy: | |
| 73 # | |
| 74 # Find all public template files and static files, and render them. Most of | |
| 75 # the time these won't have changed since the last cron run, so it's a | |
| 76 # little wasteful, but hopefully rendering is really fast (if it isn't we | |
| 77 # have a problem). | |
| 78 class MockResponse(object): | |
| 79 def __init__(self): | |
| 80 self.status = 200 | |
| 81 self.out = StringIO() | |
| 82 self.headers = {} | |
| 83 def set_status(self, status): | |
| 84 self.status = status | |
| 85 def clear(self, *args): | |
| 86 pass | |
| 87 | |
| 88 class MockRequest(object): | |
| 89 def __init__(self, path): | |
| 90 self.headers = {} | |
| 91 self.path = path | |
| 92 self.url = '//localhost/%s' % path | |
| 93 | |
| 94 channel = path.split('/')[-1] | |
| 95 logging.info('cron/%s: starting' % channel) | |
| 96 | |
| 97 server_instance = ServerInstance.CreateOnline(channel) | |
| 98 | |
| 99 def run_cron_for_dir(d, path_prefix=''): | |
| 100 success = True | |
| 101 start_time = time.time() | |
| 102 files = [f for f in server_instance.content_cache.GetFromFileListing(d) | |
| 103 if not f.endswith('/')] | |
| 104 logging.info('cron/%s: rendering %s files from %s...' % ( | |
| 105 channel, len(files), d)) | |
| 106 for i, f in enumerate(files): | |
| 107 error = None | |
| 108 path = '%s%s' % (path_prefix, f) | |
| 109 try: | |
| 110 response = MockResponse() | |
| 111 server_instance.Get(path, MockRequest(path), response) | |
| 112 if response.status != 200: | |
| 113 error = 'Got %s response' % response.status | |
| 114 except DeadlineExceededError: | |
| 115 logging.error( | |
| 116 'cron/%s: deadline exceeded rendering %s (%s of %s): %s' % ( | |
| 117 channel, path, i + 1, len(files), traceback.format_exc())) | |
| 118 raise | |
| 119 except error: | |
| 120 pass | |
| 121 if error: | |
| 122 logging.error('cron/%s: error rendering %s: %s' % ( | |
| 123 channel, path, error)) | |
| 124 success = False | |
| 125 logging.info('cron/%s: rendering %s files from %s took %s seconds' % ( | |
| 126 channel, len(files), d, time.time() - start_time)) | |
| 127 return success | |
| 128 | |
| 129 success = True | |
| 130 for path, path_prefix in ( | |
| 131 # Note: rendering the public templates will pull in all of the private | |
| 132 # templates. | |
| 133 (svn_constants.PUBLIC_TEMPLATE_PATH, ''), | |
| 134 # Note: rendering the public templates will have pulled in the .js and | |
| 135 # manifest.json files (for listing examples on the API reference pages), | |
| 136 # but there are still images, CSS, etc. | |
| 137 (svn_constants.STATIC_PATH, 'static/'), | |
| 138 (svn_constants.EXAMPLES_PATH, 'extensions/examples/')): | |
| 139 try: | |
| 140 # Note: don't try to short circuit any of this stuff. We want to run | |
| 141 # the cron for all the directories regardless of intermediate failures. | |
| 142 success = run_cron_for_dir(path, path_prefix=path_prefix) and success | |
| 143 except DeadlineExceededError: | |
| 144 success = False | |
| 145 break | |
| 146 | |
| 147 if success: | |
| 148 self.response.status = 200 | |
| 149 self.response.out.write('Success') | |
| 150 else: | |
| 151 self.response.status = 500 | |
| 152 self.response.out.write('Failure') | |
| 153 | |
| 154 logging.info('cron/%s: finished' % channel) | |
| 155 | |
| 156 def _RedirectSpecialCases(self, path): | 17 def _RedirectSpecialCases(self, path): |
| 157 google_dev_url = 'http://developer.google.com/chrome' | 18 if not path or path == 'index.html': |
| 158 if path == '/' or path == '/index.html': | 19 self.redirect('http://developer.google.com/chrome') |
| 159 self.redirect(google_dev_url) | |
| 160 return True | 20 return True |
| 161 | 21 |
| 162 if path == '/apps.html': | 22 if path == 'apps.html': |
| 163 self.redirect('/apps/about_apps.html') | 23 self.redirect('/apps/about_apps.html') |
| 164 return True | 24 return True |
| 165 | 25 |
| 166 return False | 26 return False |
| 167 | 27 |
| 168 def _RedirectFromCodeDotGoogleDotCom(self, path): | 28 def _RedirectFromCodeDotGoogleDotCom(self, path): |
| 169 if (not self.request.url.startswith(('http://code.google.com', | 29 if (not self.request.url.startswith(('http://code.google.com', |
| 170 'https://code.google.com'))): | 30 'https://code.google.com'))): |
| 171 return False | 31 return False |
| 172 | 32 |
| 173 new_url = 'http://developer.chrome.com/' | 33 new_url = 'http://developer.chrome.com/' |
| 174 | 34 |
| 175 # switch to https if necessary | 35 # switch to https if necessary |
| 176 if (self.request.url.startswith('https')): | 36 if (self.request.url.startswith('https')): |
| 177 new_url = new_url.replace('http', 'https', 1) | 37 new_url = new_url.replace('http', 'https', 1) |
| 178 | 38 |
| 179 path = path.split('/') | 39 path = path.split('/') |
| 180 if len(path) > 0 and path[0] == 'chrome': | 40 if len(path) > 0 and path[0] == 'chrome': |
| 181 path.pop(0) | 41 path.pop(0) |
| 182 for channel in BranchUtility.GetAllBranchNames(): | 42 for channel in BranchUtility.GetAllBranchNames(): |
| 183 if channel in path: | 43 if channel in path: |
| 184 position = path.index(channel) | 44 position = path.index(channel) |
| 185 path.pop(position) | 45 path.pop(position) |
| 186 path.insert(0, channel) | 46 path.insert(0, channel) |
| 187 new_url += '/'.join(path) | 47 new_url += '/'.join(path) |
| 188 self.redirect(new_url) | 48 self.redirect(new_url) |
| 189 return True | 49 return True |
| 190 | 50 |
| 191 def get(self): | 51 def get(self): |
| 192 path = self.request.path | 52 path, request, response = (self.request.path.lstrip('/'), |
| 53 self.request, |
| 54 self.response) |
| 193 | 55 |
| 194 if path in ['favicon.ico', 'robots.txt']: | 56 if path in ['favicon.ico', 'robots.txt']: |
| 195 response.set_status(404) | 57 response.set_status(404) |
| 196 return | 58 return |
| 197 | 59 |
| 198 if self._RedirectSpecialCases(path): | 60 if self._RedirectSpecialCases(path): |
| 199 return | 61 return |
| 200 | |
| 201 if path.startswith('/cron'): | |
| 202 # Crons often time out, and when they do *and* then eventually try to | |
| 203 # flush logs they die. Turn off autoflush and manually do so at the end. | |
| 204 logservice.AUTOFLUSH_ENABLED = False | |
| 205 try: | |
| 206 self._HandleCron(path) | |
| 207 finally: | |
| 208 logservice.flush() | |
| 209 return | |
| 210 | |
| 211 # Redirect paths like "directory" to "directory/". This is so relative | |
| 212 # file paths will know to treat this as a directory. | |
| 213 if os.path.splitext(path)[1] == '' and path[-1] != '/': | |
| 214 self.redirect(path + '/') | |
| 215 return | |
| 216 | |
| 217 path = path.strip('/') | |
| 218 if self._RedirectFromCodeDotGoogleDotCom(path): | 62 if self._RedirectFromCodeDotGoogleDotCom(path): |
| 219 return | 63 return |
| 220 | 64 |
| 221 self._HandleGet(path) | 65 if path.startswith('_'): |
| 66 servlet_path = path[1:] |
| 67 if servlet_path.find('/') == -1: |
| 68 servlet_path += '/' |
| 69 servlet_name, servlet_path = servlet_path.split('/', 1) |
| 70 servlet_class = _SERVLETS.get(servlet_name) |
| 71 if servlet_class is None: |
| 72 response.out.write('"%s" servlet not found' % servlet_path) |
| 73 response.set_status(404) |
| 74 return |
| 75 else: |
| 76 servlet_path = path |
| 77 servlet_class = RenderServlet |
| 78 |
| 79 servlet_class(servlet_path, request, response).Get() |
| OLD | NEW |