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."""
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.
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.
55 'timestamp': validation
.TYPE_FLOAT
,
56 'opt_in': validation
.Optional(validation
.TYPE_BOOL
),
61 """Load a single NagFile object where one and only one is expected.
64 nag_file: A file-like object or string containing the yaml data to parse.
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.
76 isfile: used for testing.
77 open_fn: Used for testing.
80 A Yaml object or None if the VERSION file does not exist.
82 version_filename
= os
.path
.join(os
.path
.dirname(google
.appengine
.__file
__),
84 if not isfile(version_filename
):
85 logging
.error('Could not find version file at %s', version_filename
)
88 version_fh
= open_fn(version_filename
, 'r')
90 version
= yaml
.safe_load(version_fh
)
97 def _VersionList(release
):
98 """Parse a version string into a list of ints.
101 release: The 'release' version, e.g. '1.2.4'.
102 (Due to YAML parsing this may also be an int or float.)
105 A list of ints corresponding to the parts of the version string
106 between periods. Example:
108 '1.2.3.4' -> [1, 2, 3, 4]
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.
128 rpcserver: An AbstractRpcServer instance used to check for the latest SDK.
129 config: The app's AppInfoExternal. Needed to determine which api_version
137 isfile
=os
.path
.isfile
,
139 """Create a new SDKUpdateChecker.
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):
151 self
.rpcserver
= rpcserver
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
)
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
__)
173 os
.environ
['HOMEDRIVE'] = drive
175 return os
.path
.expanduser('~/' + NAG_FILE
)
177 def _ParseVersionFile(self
):
178 """Parse the local VERSION file.
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.
192 sys.exit if the api_version is not supported.
194 version
= self
._ParseVersionFile
()
196 logging
.error('Could not determine if the SDK supports the api_version '
197 'requested in app.yaml.')
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
)
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
:
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
()
239 logging
.info('Skipping update check')
241 logging
.info('Checking for updates to the SDK.')
248 for runtime
in self
.runtimes
:
249 responses
[runtime
] = yaml
.safe_load(self
.rpcserver
.Send(
251 timeout
=UPDATE_CHECK_TIMEOUT
,
252 release
=version
['release'],
253 timestamp
=version
['timestamp'],
254 api_versions
=version
['api_versions'],
256 except (urllib2
.URLError
, socket
.error
), e
:
257 logging
.info('Update check failed: %s', e
)
263 latest
= sorted(responses
.values(), reverse
=True,
264 key
=lambda release
: _VersionList(release
['release']))[0]
266 logging
.warn('Could not parse this release version')
268 if version
['release'] == latest
['release']:
269 logging
.info('The SDK is up to date.')
273 this_release
= _VersionList(version
['release'])
275 logging
.warn('Could not parse this release version (%r)',
279 advertised_release
= _VersionList(latest
['release'])
281 logging
.warn('Could not parse advertised release version (%r)',
284 if this_release
> advertised_release
:
285 logging
.info('This SDK release is newer than the advertised release.')
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:
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
:
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:
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
:
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.',
321 def _ParseNagFile(self
):
322 """Parses the nag file.
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')
331 nag
= NagFile
.Load(fh
)
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
344 nag: The NagFile to write.
346 nagfilename
= SDKUpdateChecker
.MakeNagFilename()
348 fh
= self
.open(nagfilename
, 'w')
350 fh
.write(nag
.ToYAML())
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.
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')
379 nag
.timestamp
= time
.time()
380 self
._WriteNagFile
(nag
)
382 print '****************************************************************'
386 print yaml
.dump(latest
)
389 print yaml
.dump(version
)
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.
408 input_fn: used to collect user input. This is for testing only.
411 True if the user wants to check for updates. False otherwise.
413 nag
= self
._ParseNagFile
()
418 if nag
.opt_in
is None:
419 answer
= input_fn('Allow dev_appserver to check for updates on startup? '
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())
429 print ('dev_appserver will check for updates on startup. To change '
430 'this setting, edit %s' % SDKUpdateChecker
.MakeNagFilename())
432 self
._WriteNagFile
(nag
)
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.
446 versions: dict of versions from app.yaml or /api/updatecheck server.
447 runtime: string of current runtime (e.g. 'go').
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']