1.9.30 sync.
[gae.git] / python / google / appengine / tools / sdk_update_checker.py
blob8f9b11195d1203a6b0c7c7e41b970e958bb154c2
1 #!/usr/bin/env python
3 # Copyright 2007 Google Inc.
5 # Licensed under the Apache License, Version 2.0 (the "License");
6 # you may not use this file except in compliance with the License.
7 # You may obtain a copy of the License at
9 # http://www.apache.org/licenses/LICENSE-2.0
11 # Unless required by applicable law or agreed to in writing, software
12 # distributed under the License is distributed on an "AS IS" BASIS,
13 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14 # See the License for the specific language governing permissions and
15 # limitations under the License.
17 """Checks for SDK updates."""
19 import datetime
20 import logging
21 import os
22 import socket
23 import ssl
24 import sys
25 import time
26 import urllib2
28 import google
29 import yaml
31 from google.appengine.api import validation
32 from google.appengine.api import yaml_object
37 VERSION_FILE = '../../VERSION'
40 UPDATE_CHECK_TIMEOUT = 3
43 NAG_FILE = '.appcfg_nag'
46 class NagFile(validation.Validated):
47 """A validated YAML class to represent the user's nag preferences.
49 Attributes:
50 timestamp: The timestamp of the last nag.
51 opt_in: True if the user wants to check for updates on dev_appserver
52 start. False if not. May be None if we have not asked the user yet.
53 """
55 ATTRIBUTES = {
56 'timestamp': validation.TYPE_FLOAT,
57 'opt_in': validation.Optional(validation.TYPE_BOOL),
60 @staticmethod
61 def Load(nag_file):
62 """Load a single NagFile object where one and only one is expected.
64 Args:
65 nag_file: A file-like object or string containing the yaml data to parse.
67 Returns:
68 A NagFile instance.
69 """
70 return yaml_object.BuildSingleObject(NagFile, nag_file)
73 def GetVersionObject():
74 """Gets the version of the SDK by parsing the VERSION file.
76 Returns:
77 A Yaml object or None if the VERSION file does not exist.
78 """
79 version_filename = os.path.join(os.path.dirname(google.appengine.__file__),
80 VERSION_FILE)
81 try:
82 version_fh = open(version_filename)
83 except IOError:
84 logging.error('Could not find version file at %s', version_filename)
85 return None
86 try:
87 version = yaml.safe_load(version_fh)
88 finally:
89 version_fh.close()
91 return version
94 def _VersionList(release):
95 """Parse a version string into a list of ints.
97 Args:
98 release: The 'release' version, e.g. '1.2.4'.
99 (Due to YAML parsing this may also be an int or float.)
101 Returns:
102 A list of ints corresponding to the parts of the version string
103 between periods. Example:
104 '1.2.4' -> [1, 2, 4]
105 '1.2.3.4' -> [1, 2, 3, 4]
107 Raises:
108 ValueError if not all the parts are valid integers.
110 return [int(part) for part in str(release).split('.')]
113 class SDKUpdateChecker(object):
114 """Determines if the local SDK is the latest version.
116 Nags the user when there are updates to the SDK. As the SDK becomes
117 more out of date, the language in the nagging gets stronger. We
118 store a little yaml file in the user's home directory so that we nag
119 the user only once a week.
121 The yaml file has the following field:
122 'timestamp': Last time we nagged the user in seconds since the epoch.
124 Attributes:
125 rpcserver: An AbstractRpcServer instance used to check for the latest SDK.
126 config: The app's AppInfoExternal. Needed to determine which api_version
127 the app is using.
130 def __init__(self,
131 rpcserver,
132 configs):
133 """Create a new SDKUpdateChecker.
135 Args:
136 rpcserver: The AbstractRpcServer to use.
137 configs: A list of yaml objects or a single yaml object that specify the
138 configuration of this application.
140 if not isinstance(configs, list):
141 configs = [configs]
142 self.rpcserver = rpcserver
143 self.runtimes = set(config.runtime for config in configs)
144 self.runtime_to_api_version = {}
145 for config in configs:
146 self.runtime_to_api_version.setdefault(
147 config.runtime, set()).add(config.api_version)
149 @staticmethod
150 def MakeNagFilename():
151 """Returns the filename for the nag file for this user."""
157 user_homedir = os.path.expanduser('~/')
158 if not os.path.isdir(user_homedir):
159 drive, unused_tail = os.path.splitdrive(os.__file__)
160 if drive:
161 os.environ['HOMEDRIVE'] = drive
163 return os.path.expanduser('~/' + NAG_FILE)
165 def _ParseVersionFile(self):
166 """Parse the local VERSION file.
168 Returns:
169 A Yaml object or None if the file does not exist.
171 return GetVersionObject()
173 def CheckSupportedVersion(self):
174 """Determines if the app's api_version is supported by the SDK.
176 Uses the api_version field from the AppInfoExternal to determine if
177 the SDK supports that api_version.
179 Raises:
180 sys.exit if the api_version is not supported.
182 version = self._ParseVersionFile()
183 if version is None:
184 logging.error('Could not determine if the SDK supports the api_version '
185 'requested in app.yaml.')
186 return
187 unsupported_api_versions_found = False
188 for runtime, api_versions in self.runtime_to_api_version.items():
189 supported_api_versions = _GetSupportedApiVersions(version, runtime)
190 unsupported_api_versions = sorted(api_versions -
191 set(supported_api_versions))
192 if unsupported_api_versions:
193 unsupported_api_versions_found = True
194 if len(unsupported_api_versions) == 1:
195 logging.critical('The requested api_version (%s) is not supported by '
196 'the %s runtime in this release of the SDK. The '
197 'supported api_versions are %s.',
198 unsupported_api_versions[0], runtime,
199 supported_api_versions)
200 else:
201 logging.critical('The requested api_versions (%s) are not supported '
202 'by the %s runtime in this release of the SDK. The '
203 'supported api_versions are %s.',
204 unsupported_api_versions, runtime,
205 supported_api_versions)
206 if unsupported_api_versions_found:
207 sys.exit(1)
209 def CheckForUpdates(self):
210 """Queries the server for updates and nags the user if appropriate.
212 Queries the server for the latest SDK version at the same time reporting
213 the local SDK version. The server will respond with a yaml document
214 containing the fields:
215 'release': The name of the release (e.g. 1.2).
216 'timestamp': The time the release was created (YYYY-MM-DD HH:MM AM/PM TZ).
217 'api_versions': A list of api_version strings (e.g. ['1', 'beta']).
219 We will nag the user with increasing severity if:
220 - There is a new release.
221 - There is a new release with a new api_version.
222 - There is a new release that does not support an api_version named in
223 a configuration in self.configs.
225 version = self._ParseVersionFile()
226 if version is None:
227 logging.info('Skipping update check')
228 return
229 logging.info('Checking for updates to the SDK.')
231 responses = {}
235 try:
236 for runtime in self.runtimes:
237 responses[runtime] = yaml.safe_load(self.rpcserver.Send(
238 '/api/updatecheck',
239 timeout=UPDATE_CHECK_TIMEOUT,
240 release=version['release'],
241 timestamp=version['timestamp'],
242 api_versions=version['api_versions'],
243 runtime=runtime))
244 except (urllib2.URLError, socket.error, ssl.SSLError), e:
245 logging.info('Update check failed: %s', e)
246 return
250 try:
251 latest = sorted(responses.values(), reverse=True,
252 key=lambda release: _VersionList(release['release']))[0]
253 except ValueError:
254 logging.warn('Could not parse this release version')
256 if version['release'] == latest['release']:
257 logging.info('The SDK is up to date.')
258 return
260 try:
261 this_release = _VersionList(version['release'])
262 except ValueError:
263 logging.warn('Could not parse this release version (%r)',
264 version['release'])
265 else:
266 try:
267 advertised_release = _VersionList(latest['release'])
268 except ValueError:
269 logging.warn('Could not parse advertised release version (%r)',
270 latest['release'])
271 else:
272 if this_release > advertised_release:
273 logging.info('This SDK release is newer than the advertised release.')
274 return
276 for runtime, response in responses.items():
277 api_versions = _GetSupportedApiVersions(response, runtime)
278 obsolete_versions = sorted(
279 self.runtime_to_api_version[runtime] - set(api_versions))
280 if len(obsolete_versions) == 1:
281 self._Nag(
282 'The api version you are using (%s) is obsolete! You should\n'
283 'upgrade your SDK and test that your code works with the new\n'
284 'api version.' % obsolete_versions[0],
285 response, version, force=True)
286 elif obsolete_versions:
287 self._Nag(
288 'The api versions you are using (%s) are obsolete! You should\n'
289 'upgrade your SDK and test that your code works with the new\n'
290 'api version.' % obsolete_versions,
291 response, version, force=True)
293 deprecated_versions = sorted(
294 self.runtime_to_api_version[runtime].intersection(api_versions[:-1]))
295 if len(deprecated_versions) == 1:
296 self._Nag(
297 'The api version you are using (%s) is deprecated. You should\n'
298 'upgrade your SDK to try the new functionality.' %
299 deprecated_versions[0], response, version)
300 elif deprecated_versions:
301 self._Nag(
302 'The api versions you are using (%s) are deprecated. You should\n'
303 'upgrade your SDK to try the new functionality.' %
304 deprecated_versions, response, version)
306 self._Nag('There is a new release of the SDK available.',
307 latest, version)
309 def _ParseNagFile(self):
310 """Parses the nag file.
312 Returns:
313 A NagFile if the file was present else None.
315 nag_filename = SDKUpdateChecker.MakeNagFilename()
316 try:
317 fh = open(nag_filename)
318 except IOError:
319 return None
320 try:
321 nag = NagFile.Load(fh)
322 finally:
323 fh.close()
324 return nag
326 def _WriteNagFile(self, nag):
327 """Writes the NagFile to the user's nag file.
329 If the destination path does not exist, this method will log an error
330 and fail silently.
332 Args:
333 nag: The NagFile to write.
335 nagfilename = SDKUpdateChecker.MakeNagFilename()
336 try:
337 fh = open(nagfilename, 'w')
338 try:
339 fh.write(nag.ToYAML())
340 finally:
341 fh.close()
342 except (OSError, IOError), e:
343 logging.error('Could not write nag file to %s. Error: %s', nagfilename, e)
345 def _Nag(self, msg, latest, version, force=False):
346 """Prints a nag message and updates the nag file's timestamp.
348 Because we don't want to nag the user everytime, we store a simple
349 yaml document in the user's home directory. If the timestamp in this
350 doc is over a week old, we'll nag the user. And when we nag the user,
351 we update the timestamp in this doc.
353 Args:
354 msg: The formatted message to print to the user.
355 latest: The yaml document received from the server.
356 version: The local yaml version document.
357 force: If True, always nag the user, ignoring the nag file.
359 nag = self._ParseNagFile()
360 if nag and not force:
361 last_nag = datetime.datetime.fromtimestamp(nag.timestamp)
362 if datetime.datetime.now() - last_nag < datetime.timedelta(weeks=1):
363 logging.debug('Skipping nag message')
364 return
366 if nag is None:
367 nag = NagFile()
368 nag.timestamp = time.time()
369 self._WriteNagFile(nag)
371 print '****************************************************************'
372 print msg
373 print '-----------'
374 print 'Latest SDK:'
375 print yaml.dump(latest)
376 print '-----------'
377 print 'Your SDK:'
378 print yaml.dump(version)
379 print '-----------'
380 print 'Please visit https://developers.google.com/appengine/downloads'
381 print 'for the latest SDK'
382 print '****************************************************************'
384 def AllowedToCheckForUpdates(self, input_fn=raw_input):
385 """Determines if the user wants to check for updates.
387 On startup, the dev_appserver wants to check for updates to the SDK.
388 Because this action reports usage to Google when the user is not
389 otherwise communicating with Google (e.g. pushing a new app version),
390 the user must opt in.
392 If the user does not have a nag file, we will query the user and
393 save the response in the nag file. Subsequent calls to this function
394 will re-use that response.
396 Args:
397 input_fn: used to collect user input. This is for testing only.
399 Returns:
400 True if the user wants to check for updates. False otherwise.
402 nag = self._ParseNagFile()
403 if nag is None:
404 nag = NagFile()
405 nag.timestamp = 0.0
407 if nag.opt_in is None:
408 answer = input_fn('Allow dev_appserver to check for updates on startup? '
409 '(Y/n): ')
410 answer = answer.strip().lower()
411 if answer == 'n' or answer == 'no':
412 print ('dev_appserver will not check for updates on startup. To '
413 'change this setting, edit %s' %
414 SDKUpdateChecker.MakeNagFilename())
415 nag.opt_in = False
416 else:
418 print ('dev_appserver will check for updates on startup. To change '
419 'this setting, edit %s' % SDKUpdateChecker.MakeNagFilename())
420 nag.opt_in = True
421 self._WriteNagFile(nag)
422 return nag.opt_in
425 def _GetSupportedApiVersions(versions, runtime):
426 """Returns the runtime-specific or general list of supported runtimes.
428 The provided 'versions' dict contains a field called 'api_versions'
429 which is the list of default versions supported. This dict may also
430 contain a 'supported_api_versions' dict which lists api_versions by
431 runtime. This function will prefer to return the runtime-specific
432 api_versions list, but will default to the general list.
434 Args:
435 versions: dict of versions from app.yaml or /api/updatecheck server.
436 runtime: string of current runtime (e.g. 'go').
438 Returns:
439 List of supported api_versions (e.g. ['go1']).
441 if 'supported_api_versions' in versions:
442 return versions['supported_api_versions'].get(
443 runtime, versions)['api_versions']
444 return versions['api_versions']