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."""
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.
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.
56 'timestamp': validation
.TYPE_FLOAT
,
57 'opt_in': validation
.Optional(validation
.TYPE_BOOL
),
62 """Load a single NagFile object where one and only one is expected.
65 nag_file: A file-like object or string containing the yaml data to parse.
70 return yaml_object
.BuildSingleObject(NagFile
, nag_file
)
73 def GetVersionObject():
74 """Gets the version of the SDK by parsing the VERSION file.
77 A Yaml object or None if the VERSION file does not exist.
79 version_filename
= os
.path
.join(os
.path
.dirname(google
.appengine
.__file
__),
82 version_fh
= open(version_filename
)
84 logging
.error('Could not find version file at %s', version_filename
)
87 version
= yaml
.safe_load(version_fh
)
94 def _VersionList(release
):
95 """Parse a version string into a list of ints.
98 release: The 'release' version, e.g. '1.2.4'.
99 (Due to YAML parsing this may also be an int or float.)
102 A list of ints corresponding to the parts of the version string
103 between periods. Example:
105 '1.2.3.4' -> [1, 2, 3, 4]
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.
125 rpcserver: An AbstractRpcServer instance used to check for the latest SDK.
126 config: The app's AppInfoExternal. Needed to determine which api_version
133 """Create a new SDKUpdateChecker.
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):
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
)
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
__)
161 os
.environ
['HOMEDRIVE'] = drive
163 return os
.path
.expanduser('~/' + NAG_FILE
)
165 def _ParseVersionFile(self
):
166 """Parse the local VERSION file.
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.
180 sys.exit if the api_version is not supported.
182 version
= self
._ParseVersionFile
()
184 logging
.error('Could not determine if the SDK supports the api_version '
185 'requested in app.yaml.')
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
)
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
:
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
()
227 logging
.info('Skipping update check')
229 logging
.info('Checking for updates to the SDK.')
236 for runtime
in self
.runtimes
:
237 responses
[runtime
] = yaml
.safe_load(self
.rpcserver
.Send(
239 timeout
=UPDATE_CHECK_TIMEOUT
,
240 release
=version
['release'],
241 timestamp
=version
['timestamp'],
242 api_versions
=version
['api_versions'],
244 except (urllib2
.URLError
, socket
.error
, ssl
.SSLError
), e
:
245 logging
.info('Update check failed: %s', e
)
251 latest
= sorted(responses
.values(), reverse
=True,
252 key
=lambda release
: _VersionList(release
['release']))[0]
254 logging
.warn('Could not parse this release version')
256 if version
['release'] == latest
['release']:
257 logging
.info('The SDK is up to date.')
261 this_release
= _VersionList(version
['release'])
263 logging
.warn('Could not parse this release version (%r)',
267 advertised_release
= _VersionList(latest
['release'])
269 logging
.warn('Could not parse advertised release version (%r)',
272 if this_release
> advertised_release
:
273 logging
.info('This SDK release is newer than the advertised release.')
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:
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
:
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:
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
:
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.',
309 def _ParseNagFile(self
):
310 """Parses the nag file.
313 A NagFile if the file was present else None.
315 nag_filename
= SDKUpdateChecker
.MakeNagFilename()
317 fh
= open(nag_filename
)
321 nag
= NagFile
.Load(fh
)
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
333 nag: The NagFile to write.
335 nagfilename
= SDKUpdateChecker
.MakeNagFilename()
337 fh
= open(nagfilename
, 'w')
339 fh
.write(nag
.ToYAML())
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.
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')
368 nag
.timestamp
= time
.time()
369 self
._WriteNagFile
(nag
)
371 print '****************************************************************'
375 print yaml
.dump(latest
)
378 print yaml
.dump(version
)
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.
397 input_fn: used to collect user input. This is for testing only.
400 True if the user wants to check for updates. False otherwise.
402 nag
= self
._ParseNagFile
()
407 if nag
.opt_in
is None:
408 answer
= input_fn('Allow dev_appserver to check for updates on startup? '
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())
418 print ('dev_appserver will check for updates on startup. To change '
419 'this setting, edit %s' % SDKUpdateChecker
.MakeNagFilename())
421 self
._WriteNagFile
(nag
)
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.
435 versions: dict of versions from app.yaml or /api/updatecheck server.
436 runtime: string of current runtime (e.g. 'go').
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']