App Engine Python SDK version 1.8.9
[gae.git] / python / google / appengine / tools / sdk_update_checker.py
blob975267f88b8425dbb85a6554c76fc11e06d5c97d
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 sys
24 import time
25 import urllib2
27 import google
28 import yaml
30 from google.appengine.api import validation
31 from google.appengine.api import yaml_object
36 VERSION_FILE = '../../VERSION'
39 UPDATE_CHECK_TIMEOUT = 3
42 NAG_FILE = '.appcfg_nag'
45 class NagFile(validation.Validated):
46 """A validated YAML class to represent the user's nag preferences.
48 Attributes:
49 timestamp: The timestamp of the last nag.
50 opt_in: True if the user wants to check for updates on dev_appserver
51 start. False if not. May be None if we have not asked the user yet.
52 """
54 ATTRIBUTES = {
55 'timestamp': validation.TYPE_FLOAT,
56 'opt_in': validation.Optional(validation.TYPE_BOOL),
59 @staticmethod
60 def Load(nag_file):
61 """Load a single NagFile object where one and only one is expected.
63 Args:
64 nag_file: A file-like object or string containing the yaml data to parse.
66 Returns:
67 A NagFile instance.
68 """
69 return yaml_object.BuildSingleObject(NagFile, nag_file)
72 def GetVersionObject(isfile=os.path.isfile, open_fn=open):
73 """Gets the version of the SDK by parsing the VERSION file.
75 Args:
76 isfile: used for testing.
77 open_fn: Used for testing.
79 Returns:
80 A Yaml object or None if the VERSION file does not exist.
81 """
82 version_filename = os.path.join(os.path.dirname(google.appengine.__file__),
83 VERSION_FILE)
84 if not isfile(version_filename):
85 logging.error('Could not find version file at %s', version_filename)
86 return None
88 version_fh = open_fn(version_filename, 'r')
89 try:
90 version = yaml.safe_load(version_fh)
91 finally:
92 version_fh.close()
94 return version
97 def _VersionList(release):
98 """Parse a version string into a list of ints.
100 Args:
101 release: The 'release' version, e.g. '1.2.4'.
102 (Due to YAML parsing this may also be an int or float.)
104 Returns:
105 A list of ints corresponding to the parts of the version string
106 between periods. Example:
107 '1.2.4' -> [1, 2, 4]
108 '1.2.3.4' -> [1, 2, 3, 4]
110 Raises:
111 ValueError if not all the parts are valid integers.
113 return [int(part) for part in str(release).split('.')]
116 class SDKUpdateChecker(object):
117 """Determines if the local SDK is the latest version.
119 Nags the user when there are updates to the SDK. As the SDK becomes
120 more out of date, the language in the nagging gets stronger. We
121 store a little yaml file in the user's home directory so that we nag
122 the user only once a week.
124 The yaml file has the following field:
125 'timestamp': Last time we nagged the user in seconds since the epoch.
127 Attributes:
128 rpcserver: An AbstractRpcServer instance used to check for the latest SDK.
129 config: The app's AppInfoExternal. Needed to determine which api_version
130 the app is using.
133 def __init__(self,
134 rpcserver,
135 configs,
136 isdir=os.path.isdir,
137 isfile=os.path.isfile,
138 open_fn=open):
139 """Create a new SDKUpdateChecker.
141 Args:
142 rpcserver: The AbstractRpcServer to use.
143 configs: A list of yaml objects or a single yaml object that specify the
144 configuration of this application.
145 isdir: Replacement for os.path.isdir (for testing).
146 isfile: Replacement for os.path.isfile (for testing).
147 open_fn: Replacement for the open builtin (for testing).
149 if not isinstance(configs, list):
150 configs = [configs]
151 self.rpcserver = rpcserver
152 self.isdir = isdir
153 self.isfile = isfile
154 self.open = open_fn
155 self.runtimes = set(config.runtime for config in configs)
156 self.runtime_to_api_version = {}
157 for config in configs:
158 self.runtime_to_api_version.setdefault(
159 config.runtime, set()).add(config.api_version)
161 @staticmethod
162 def MakeNagFilename():
163 """Returns the filename for the nag file for this user."""
169 user_homedir = os.path.expanduser('~/')
170 if not os.path.isdir(user_homedir):
171 drive, unused_tail = os.path.splitdrive(os.__file__)
172 if drive:
173 os.environ['HOMEDRIVE'] = drive
175 return os.path.expanduser('~/' + NAG_FILE)
177 def _ParseVersionFile(self):
178 """Parse the local VERSION file.
180 Returns:
181 A Yaml object or None if the file does not exist.
183 return GetVersionObject(isfile=self.isfile, open_fn=self.open)
185 def CheckSupportedVersion(self):
186 """Determines if the app's api_version is supported by the SDK.
188 Uses the api_version field from the AppInfoExternal to determine if
189 the SDK supports that api_version.
191 Raises:
192 sys.exit if the api_version is not supported.
194 version = self._ParseVersionFile()
195 if version is None:
196 logging.error('Could not determine if the SDK supports the api_version '
197 'requested in app.yaml.')
198 return
199 unsupported_api_versions_found = False
200 for runtime, api_versions in self.runtime_to_api_version.items():
201 supported_api_versions = _GetSupportedApiVersions(version, runtime)
202 unsupported_api_versions = sorted(api_versions -
203 set(supported_api_versions))
204 if unsupported_api_versions:
205 unsupported_api_versions_found = True
206 if len(unsupported_api_versions) == 1:
207 logging.critical('The requested api_version (%s) is not supported by '
208 'the %s runtime in this release of the SDK. The '
209 'supported api_versions are %s.',
210 unsupported_api_versions[0], runtime,
211 supported_api_versions)
212 else:
213 logging.critical('The requested api_versions (%s) are not supported '
214 'by the %s runtime in this release of the SDK. The '
215 'supported api_versions are %s.',
216 unsupported_api_versions, runtime,
217 supported_api_versions)
218 if unsupported_api_versions_found:
219 sys.exit(1)
221 def CheckForUpdates(self):
222 """Queries the server for updates and nags the user if appropriate.
224 Queries the server for the latest SDK version at the same time reporting
225 the local SDK version. The server will respond with a yaml document
226 containing the fields:
227 'release': The name of the release (e.g. 1.2).
228 'timestamp': The time the release was created (YYYY-MM-DD HH:MM AM/PM TZ).
229 'api_versions': A list of api_version strings (e.g. ['1', 'beta']).
231 We will nag the user with increasing severity if:
232 - There is a new release.
233 - There is a new release with a new api_version.
234 - There is a new release that does not support an api_version named in
235 a configuration in self.configs.
237 version = self._ParseVersionFile()
238 if version is None:
239 logging.info('Skipping update check')
240 return
241 logging.info('Checking for updates to the SDK.')
243 responses = {}
247 try:
248 for runtime in self.runtimes:
249 responses[runtime] = yaml.safe_load(self.rpcserver.Send(
250 '/api/updatecheck',
251 timeout=UPDATE_CHECK_TIMEOUT,
252 release=version['release'],
253 timestamp=version['timestamp'],
254 api_versions=version['api_versions'],
255 runtime=runtime))
256 except (urllib2.URLError, socket.error), e:
257 logging.info('Update check failed: %s', e)
258 return
262 try:
263 latest = sorted(responses.values(), reverse=True,
264 key=lambda release: _VersionList(release['release']))[0]
265 except ValueError:
266 logging.warn('Could not parse this release version')
268 if version['release'] == latest['release']:
269 logging.info('The SDK is up to date.')
270 return
272 try:
273 this_release = _VersionList(version['release'])
274 except ValueError:
275 logging.warn('Could not parse this release version (%r)',
276 version['release'])
277 else:
278 try:
279 advertised_release = _VersionList(latest['release'])
280 except ValueError:
281 logging.warn('Could not parse advertised release version (%r)',
282 latest['release'])
283 else:
284 if this_release > advertised_release:
285 logging.info('This SDK release is newer than the advertised release.')
286 return
288 for runtime, response in responses.items():
289 api_versions = _GetSupportedApiVersions(response, runtime)
290 obsolete_versions = sorted(
291 self.runtime_to_api_version[runtime] - set(api_versions))
292 if len(obsolete_versions) == 1:
293 self._Nag(
294 'The api version you are using (%s) is obsolete! You should\n'
295 'upgrade your SDK and test that your code works with the new\n'
296 'api version.' % obsolete_versions[0],
297 response, version, force=True)
298 elif obsolete_versions:
299 self._Nag(
300 'The api versions you are using (%s) are obsolete! You should\n'
301 'upgrade your SDK and test that your code works with the new\n'
302 'api version.' % obsolete_versions,
303 response, version, force=True)
305 deprecated_versions = sorted(
306 self.runtime_to_api_version[runtime].intersection(api_versions[:-1]))
307 if len(deprecated_versions) == 1:
308 self._Nag(
309 'The api version you are using (%s) is deprecated. You should\n'
310 'upgrade your SDK to try the new functionality.' %
311 deprecated_versions[0], response, version)
312 elif deprecated_versions:
313 self._Nag(
314 'The api versions you are using (%s) are deprecated. You should\n'
315 'upgrade your SDK to try the new functionality.' %
316 deprecated_versions, response, version)
318 self._Nag('There is a new release of the SDK available.',
319 latest, version)
321 def _ParseNagFile(self):
322 """Parses the nag file.
324 Returns:
325 A NagFile if the file was present else None.
327 nag_filename = SDKUpdateChecker.MakeNagFilename()
328 if self.isfile(nag_filename):
329 fh = self.open(nag_filename, 'r')
330 try:
331 nag = NagFile.Load(fh)
332 finally:
333 fh.close()
334 return nag
335 return None
337 def _WriteNagFile(self, nag):
338 """Writes the NagFile to the user's nag file.
340 If the destination path does not exist, this method will log an error
341 and fail silently.
343 Args:
344 nag: The NagFile to write.
346 nagfilename = SDKUpdateChecker.MakeNagFilename()
347 try:
348 fh = self.open(nagfilename, 'w')
349 try:
350 fh.write(nag.ToYAML())
351 finally:
352 fh.close()
353 except (OSError, IOError), e:
354 logging.error('Could not write nag file to %s. Error: %s', nagfilename, e)
356 def _Nag(self, msg, latest, version, force=False):
357 """Prints a nag message and updates the nag file's timestamp.
359 Because we don't want to nag the user everytime, we store a simple
360 yaml document in the user's home directory. If the timestamp in this
361 doc is over a week old, we'll nag the user. And when we nag the user,
362 we update the timestamp in this doc.
364 Args:
365 msg: The formatted message to print to the user.
366 latest: The yaml document received from the server.
367 version: The local yaml version document.
368 force: If True, always nag the user, ignoring the nag file.
370 nag = self._ParseNagFile()
371 if nag and not force:
372 last_nag = datetime.datetime.fromtimestamp(nag.timestamp)
373 if datetime.datetime.now() - last_nag < datetime.timedelta(weeks=1):
374 logging.debug('Skipping nag message')
375 return
377 if nag is None:
378 nag = NagFile()
379 nag.timestamp = time.time()
380 self._WriteNagFile(nag)
382 print '****************************************************************'
383 print msg
384 print '-----------'
385 print 'Latest SDK:'
386 print yaml.dump(latest)
387 print '-----------'
388 print 'Your SDK:'
389 print yaml.dump(version)
390 print '-----------'
391 print 'Please visit https://developers.google.com/appengine/downloads'
392 print 'for the latest SDK'
393 print '****************************************************************'
395 def AllowedToCheckForUpdates(self, input_fn=raw_input):
396 """Determines if the user wants to check for updates.
398 On startup, the dev_appserver wants to check for updates to the SDK.
399 Because this action reports usage to Google when the user is not
400 otherwise communicating with Google (e.g. pushing a new app version),
401 the user must opt in.
403 If the user does not have a nag file, we will query the user and
404 save the response in the nag file. Subsequent calls to this function
405 will re-use that response.
407 Args:
408 input_fn: used to collect user input. This is for testing only.
410 Returns:
411 True if the user wants to check for updates. False otherwise.
413 nag = self._ParseNagFile()
414 if nag is None:
415 nag = NagFile()
416 nag.timestamp = 0.0
418 if nag.opt_in is None:
419 answer = input_fn('Allow dev_appserver to check for updates on startup? '
420 '(Y/n): ')
421 answer = answer.strip().lower()
422 if answer == 'n' or answer == 'no':
423 print ('dev_appserver will not check for updates on startup. To '
424 'change this setting, edit %s' %
425 SDKUpdateChecker.MakeNagFilename())
426 nag.opt_in = False
427 else:
429 print ('dev_appserver will check for updates on startup. To change '
430 'this setting, edit %s' % SDKUpdateChecker.MakeNagFilename())
431 nag.opt_in = True
432 self._WriteNagFile(nag)
433 return nag.opt_in
436 def _GetSupportedApiVersions(versions, runtime):
437 """Returns the runtime-specific or general list of supported runtimes.
439 The provided 'versions' dict contains a field called 'api_versions'
440 which is the list of default versions supported. This dict may also
441 contain a 'supported_api_versions' dict which lists api_versions by
442 runtime. This function will prefer to return the runtime-specific
443 api_versions list, but will default to the general list.
445 Args:
446 versions: dict of versions from app.yaml or /api/updatecheck server.
447 runtime: string of current runtime (e.g. 'go').
449 Returns:
450 List of supported api_versions (e.g. ['go1']).
452 if 'supported_api_versions' in versions:
453 return versions['supported_api_versions'].get(
454 runtime, versions)['api_versions']
455 return versions['api_versions']