auto import from //branches/cupcake/...@130745
This commit is contained in:
202
tools/scripts/app_engine_server/LICENSE
Normal file
202
tools/scripts/app_engine_server/LICENSE
Normal file
@@ -0,0 +1,202 @@
|
||||
|
||||
Apache License
|
||||
Version 2.0, January 2004
|
||||
http://www.apache.org/licenses/
|
||||
|
||||
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||
|
||||
1. Definitions.
|
||||
|
||||
"License" shall mean the terms and conditions for use, reproduction,
|
||||
and distribution as defined by Sections 1 through 9 of this document.
|
||||
|
||||
"Licensor" shall mean the copyright owner or entity authorized by
|
||||
the copyright owner that is granting the License.
|
||||
|
||||
"Legal Entity" shall mean the union of the acting entity and all
|
||||
other entities that control, are controlled by, or are under common
|
||||
control with that entity. For the purposes of this definition,
|
||||
"control" means (i) the power, direct or indirect, to cause the
|
||||
direction or management of such entity, whether by contract or
|
||||
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
||||
outstanding shares, or (iii) beneficial ownership of such entity.
|
||||
|
||||
"You" (or "Your") shall mean an individual or Legal Entity
|
||||
exercising permissions granted by this License.
|
||||
|
||||
"Source" form shall mean the preferred form for making modifications,
|
||||
including but not limited to software source code, documentation
|
||||
source, and configuration files.
|
||||
|
||||
"Object" form shall mean any form resulting from mechanical
|
||||
transformation or translation of a Source form, including but
|
||||
not limited to compiled object code, generated documentation,
|
||||
and conversions to other media types.
|
||||
|
||||
"Work" shall mean the work of authorship, whether in Source or
|
||||
Object form, made available under the License, as indicated by a
|
||||
copyright notice that is included in or attached to the work
|
||||
(an example is provided in the Appendix below).
|
||||
|
||||
"Derivative Works" shall mean any work, whether in Source or Object
|
||||
form, that is based on (or derived from) the Work and for which the
|
||||
editorial revisions, annotations, elaborations, or other modifications
|
||||
represent, as a whole, an original work of authorship. For the purposes
|
||||
of this License, Derivative Works shall not include works that remain
|
||||
separable from, or merely link (or bind by name) to the interfaces of,
|
||||
the Work and Derivative Works thereof.
|
||||
|
||||
"Contribution" shall mean any work of authorship, including
|
||||
the original version of the Work and any modifications or additions
|
||||
to that Work or Derivative Works thereof, that is intentionally
|
||||
submitted to Licensor for inclusion in the Work by the copyright owner
|
||||
or by an individual or Legal Entity authorized to submit on behalf of
|
||||
the copyright owner. For the purposes of this definition, "submitted"
|
||||
means any form of electronic, verbal, or written communication sent
|
||||
to the Licensor or its representatives, including but not limited to
|
||||
communication on electronic mailing lists, source code control systems,
|
||||
and issue tracking systems that are managed by, or on behalf of, the
|
||||
Licensor for the purpose of discussing and improving the Work, but
|
||||
excluding communication that is conspicuously marked or otherwise
|
||||
designated in writing by the copyright owner as "Not a Contribution."
|
||||
|
||||
"Contributor" shall mean Licensor and any individual or Legal Entity
|
||||
on behalf of whom a Contribution has been received by Licensor and
|
||||
subsequently incorporated within the Work.
|
||||
|
||||
2. Grant of Copyright License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
copyright license to reproduce, prepare Derivative Works of,
|
||||
publicly display, publicly perform, sublicense, and distribute the
|
||||
Work and such Derivative Works in Source or Object form.
|
||||
|
||||
3. Grant of Patent License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
(except as stated in this section) patent license to make, have made,
|
||||
use, offer to sell, sell, import, and otherwise transfer the Work,
|
||||
where such license applies only to those patent claims licensable
|
||||
by such Contributor that are necessarily infringed by their
|
||||
Contribution(s) alone or by combination of their Contribution(s)
|
||||
with the Work to which such Contribution(s) was submitted. If You
|
||||
institute patent litigation against any entity (including a
|
||||
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
||||
or a Contribution incorporated within the Work constitutes direct
|
||||
or contributory patent infringement, then any patent licenses
|
||||
granted to You under this License for that Work shall terminate
|
||||
as of the date such litigation is filed.
|
||||
|
||||
4. Redistribution. You may reproduce and distribute copies of the
|
||||
Work or Derivative Works thereof in any medium, with or without
|
||||
modifications, and in Source or Object form, provided that You
|
||||
meet the following conditions:
|
||||
|
||||
(a) You must give any other recipients of the Work or
|
||||
Derivative Works a copy of this License; and
|
||||
|
||||
(b) You must cause any modified files to carry prominent notices
|
||||
stating that You changed the files; and
|
||||
|
||||
(c) You must retain, in the Source form of any Derivative Works
|
||||
that You distribute, all copyright, patent, trademark, and
|
||||
attribution notices from the Source form of the Work,
|
||||
excluding those notices that do not pertain to any part of
|
||||
the Derivative Works; and
|
||||
|
||||
(d) If the Work includes a "NOTICE" text file as part of its
|
||||
distribution, then any Derivative Works that You distribute must
|
||||
include a readable copy of the attribution notices contained
|
||||
within such NOTICE file, excluding those notices that do not
|
||||
pertain to any part of the Derivative Works, in at least one
|
||||
of the following places: within a NOTICE text file distributed
|
||||
as part of the Derivative Works; within the Source form or
|
||||
documentation, if provided along with the Derivative Works; or,
|
||||
within a display generated by the Derivative Works, if and
|
||||
wherever such third-party notices normally appear. The contents
|
||||
of the NOTICE file are for informational purposes only and
|
||||
do not modify the License. You may add Your own attribution
|
||||
notices within Derivative Works that You distribute, alongside
|
||||
or as an addendum to the NOTICE text from the Work, provided
|
||||
that such additional attribution notices cannot be construed
|
||||
as modifying the License.
|
||||
|
||||
You may add Your own copyright statement to Your modifications and
|
||||
may provide additional or different license terms and conditions
|
||||
for use, reproduction, or distribution of Your modifications, or
|
||||
for any such Derivative Works as a whole, provided Your use,
|
||||
reproduction, and distribution of the Work otherwise complies with
|
||||
the conditions stated in this License.
|
||||
|
||||
5. Submission of Contributions. Unless You explicitly state otherwise,
|
||||
any Contribution intentionally submitted for inclusion in the Work
|
||||
by You to the Licensor shall be under the terms and conditions of
|
||||
this License, without any additional terms or conditions.
|
||||
Notwithstanding the above, nothing herein shall supersede or modify
|
||||
the terms of any separate license agreement you may have executed
|
||||
with Licensor regarding such Contributions.
|
||||
|
||||
6. Trademarks. This License does not grant permission to use the trade
|
||||
names, trademarks, service marks, or product names of the Licensor,
|
||||
except as required for reasonable and customary use in describing the
|
||||
origin of the Work and reproducing the content of the NOTICE file.
|
||||
|
||||
7. Disclaimer of Warranty. Unless required by applicable law or
|
||||
agreed to in writing, Licensor provides the Work (and each
|
||||
Contributor provides its Contributions) on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||
implied, including, without limitation, any warranties or conditions
|
||||
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
||||
PARTICULAR PURPOSE. You are solely responsible for determining the
|
||||
appropriateness of using or redistributing the Work and assume any
|
||||
risks associated with Your exercise of permissions under this License.
|
||||
|
||||
8. Limitation of Liability. In no event and under no legal theory,
|
||||
whether in tort (including negligence), contract, or otherwise,
|
||||
unless required by applicable law (such as deliberate and grossly
|
||||
negligent acts) or agreed to in writing, shall any Contributor be
|
||||
liable to You for damages, including any direct, indirect, special,
|
||||
incidental, or consequential damages of any character arising as a
|
||||
result of this License or out of the use or inability to use the
|
||||
Work (including but not limited to damages for loss of goodwill,
|
||||
work stoppage, computer failure or malfunction, or any and all
|
||||
other commercial damages or losses), even if such Contributor
|
||||
has been advised of the possibility of such damages.
|
||||
|
||||
9. Accepting Warranty or Additional Liability. While redistributing
|
||||
the Work or Derivative Works thereof, You may choose to offer,
|
||||
and charge a fee for, acceptance of support, warranty, indemnity,
|
||||
or other liability obligations and/or rights consistent with this
|
||||
License. However, in accepting such obligations, You may act only
|
||||
on Your own behalf and on Your sole responsibility, not on behalf
|
||||
of any other Contributor, and only if You agree to indemnify,
|
||||
defend, and hold each Contributor harmless for any liability
|
||||
incurred by, or claims asserted against, such Contributor by reason
|
||||
of your accepting any such warranty or additional liability.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
|
||||
APPENDIX: How to apply the Apache License to your work.
|
||||
|
||||
To apply the Apache License to your work, attach the following
|
||||
boilerplate notice, with the fields enclosed by brackets "[]"
|
||||
replaced with your own identifying information. (Don't include
|
||||
the brackets!) The text should be enclosed in the appropriate
|
||||
comment syntax for the file format. We also recommend that a
|
||||
file or class name and description of purpose be included on the
|
||||
same "printed page" as the copyright notice for easier
|
||||
identification within third-party archives.
|
||||
|
||||
Copyright [yyyy] [name of copyright owner]
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
16
tools/scripts/app_engine_server/app.yaml
Executable file
16
tools/scripts/app_engine_server/app.yaml
Executable file
@@ -0,0 +1,16 @@
|
||||
application: androidappdocs-staging
|
||||
version: 1
|
||||
runtime: python
|
||||
api_version: 1
|
||||
|
||||
handlers:
|
||||
- url: /gae_shell/static
|
||||
static_dir: gae_shell/static
|
||||
expiration: 1d
|
||||
|
||||
- url: /gae_shell/.*
|
||||
script: /gae_shell/shell.py
|
||||
login: admin
|
||||
|
||||
- url: .*
|
||||
script: main.py
|
||||
17
tools/scripts/app_engine_server/gae_shell/README
Normal file
17
tools/scripts/app_engine_server/gae_shell/README
Normal file
@@ -0,0 +1,17 @@
|
||||
An interactive, stateful AJAX shell that runs Python code on the server.
|
||||
|
||||
Part of http://code.google.com/p/google-app-engine-samples/.
|
||||
|
||||
May be run as a standalone app or in an existing app as an admin-only handler.
|
||||
Can be used for system administration tasks, as an interactive way to try out
|
||||
APIs, or as a debugging aid during development.
|
||||
|
||||
The logging, os, sys, db, and users modules are imported automatically.
|
||||
|
||||
Interpreter state is stored in the datastore so that variables, function
|
||||
definitions, and other values in the global and local namespaces can be used
|
||||
across commands.
|
||||
|
||||
To use the shell in your app, copy shell.py, static/*, and templates/* into
|
||||
your app's source directory. Then, copy the URL handlers from app.yaml into
|
||||
your app.yaml.
|
||||
BIN
tools/scripts/app_engine_server/gae_shell/__init__.pyc
Normal file
BIN
tools/scripts/app_engine_server/gae_shell/__init__.pyc
Normal file
Binary file not shown.
308
tools/scripts/app_engine_server/gae_shell/shell.py
Executable file
308
tools/scripts/app_engine_server/gae_shell/shell.py
Executable file
@@ -0,0 +1,308 @@
|
||||
#!/usr/bin/python
|
||||
#
|
||||
# Copyright 2007 Google Inc.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
"""
|
||||
An interactive, stateful AJAX shell that runs Python code on the server.
|
||||
|
||||
Part of http://code.google.com/p/google-app-engine-samples/.
|
||||
|
||||
May be run as a standalone app or in an existing app as an admin-only handler.
|
||||
Can be used for system administration tasks, as an interactive way to try out
|
||||
APIs, or as a debugging aid during development.
|
||||
|
||||
The logging, os, sys, db, and users modules are imported automatically.
|
||||
|
||||
Interpreter state is stored in the datastore so that variables, function
|
||||
definitions, and other values in the global and local namespaces can be used
|
||||
across commands.
|
||||
|
||||
To use the shell in your app, copy shell.py, static/*, and templates/* into
|
||||
your app's source directory. Then, copy the URL handlers from app.yaml into
|
||||
your app.yaml.
|
||||
|
||||
TODO: unit tests!
|
||||
"""
|
||||
|
||||
import logging
|
||||
import new
|
||||
import os
|
||||
import pickle
|
||||
import sys
|
||||
import traceback
|
||||
import types
|
||||
import wsgiref.handlers
|
||||
|
||||
from google.appengine.api import users
|
||||
from google.appengine.ext import db
|
||||
from google.appengine.ext import webapp
|
||||
from google.appengine.ext.webapp import template
|
||||
|
||||
|
||||
# Set to True if stack traces should be shown in the browser, etc.
|
||||
_DEBUG = True
|
||||
|
||||
# The entity kind for shell sessions. Feel free to rename to suit your app.
|
||||
_SESSION_KIND = '_Shell_Session'
|
||||
|
||||
# Types that can't be pickled.
|
||||
UNPICKLABLE_TYPES = (
|
||||
types.ModuleType,
|
||||
types.TypeType,
|
||||
types.ClassType,
|
||||
types.FunctionType,
|
||||
)
|
||||
|
||||
# Unpicklable statements to seed new sessions with.
|
||||
INITIAL_UNPICKLABLES = [
|
||||
'import logging',
|
||||
'import os',
|
||||
'import sys',
|
||||
'from google.appengine.ext import db',
|
||||
'from google.appengine.api import users',
|
||||
]
|
||||
|
||||
|
||||
class Session(db.Model):
|
||||
"""A shell session. Stores the session's globals.
|
||||
|
||||
Each session globals is stored in one of two places:
|
||||
|
||||
If the global is picklable, it's stored in the parallel globals and
|
||||
global_names list properties. (They're parallel lists to work around the
|
||||
unfortunate fact that the datastore can't store dictionaries natively.)
|
||||
|
||||
If the global is not picklable (e.g. modules, classes, and functions), or if
|
||||
it was created by the same statement that created an unpicklable global,
|
||||
it's not stored directly. Instead, the statement is stored in the
|
||||
unpicklables list property. On each request, before executing the current
|
||||
statement, the unpicklable statements are evaluated to recreate the
|
||||
unpicklable globals.
|
||||
|
||||
The unpicklable_names property stores all of the names of globals that were
|
||||
added by unpicklable statements. When we pickle and store the globals after
|
||||
executing a statement, we skip the ones in unpicklable_names.
|
||||
|
||||
Using Text instead of string is an optimization. We don't query on any of
|
||||
these properties, so they don't need to be indexed.
|
||||
"""
|
||||
global_names = db.ListProperty(db.Text)
|
||||
globals = db.ListProperty(db.Blob)
|
||||
unpicklable_names = db.ListProperty(db.Text)
|
||||
unpicklables = db.ListProperty(db.Text)
|
||||
|
||||
def set_global(self, name, value):
|
||||
"""Adds a global, or updates it if it already exists.
|
||||
|
||||
Also removes the global from the list of unpicklable names.
|
||||
|
||||
Args:
|
||||
name: the name of the global to remove
|
||||
value: any picklable value
|
||||
"""
|
||||
blob = db.Blob(pickle.dumps(value))
|
||||
|
||||
if name in self.global_names:
|
||||
index = self.global_names.index(name)
|
||||
self.globals[index] = blob
|
||||
else:
|
||||
self.global_names.append(db.Text(name))
|
||||
self.globals.append(blob)
|
||||
|
||||
self.remove_unpicklable_name(name)
|
||||
|
||||
def remove_global(self, name):
|
||||
"""Removes a global, if it exists.
|
||||
|
||||
Args:
|
||||
name: string, the name of the global to remove
|
||||
"""
|
||||
if name in self.global_names:
|
||||
index = self.global_names.index(name)
|
||||
del self.global_names[index]
|
||||
del self.globals[index]
|
||||
|
||||
def globals_dict(self):
|
||||
"""Returns a dictionary view of the globals.
|
||||
"""
|
||||
return dict((name, pickle.loads(val))
|
||||
for name, val in zip(self.global_names, self.globals))
|
||||
|
||||
def add_unpicklable(self, statement, names):
|
||||
"""Adds a statement and list of names to the unpicklables.
|
||||
|
||||
Also removes the names from the globals.
|
||||
|
||||
Args:
|
||||
statement: string, the statement that created new unpicklable global(s).
|
||||
names: list of strings; the names of the globals created by the statement.
|
||||
"""
|
||||
self.unpicklables.append(db.Text(statement))
|
||||
|
||||
for name in names:
|
||||
self.remove_global(name)
|
||||
if name not in self.unpicklable_names:
|
||||
self.unpicklable_names.append(db.Text(name))
|
||||
|
||||
def remove_unpicklable_name(self, name):
|
||||
"""Removes a name from the list of unpicklable names, if it exists.
|
||||
|
||||
Args:
|
||||
name: string, the name of the unpicklable global to remove
|
||||
"""
|
||||
if name in self.unpicklable_names:
|
||||
self.unpicklable_names.remove(name)
|
||||
|
||||
|
||||
class FrontPageHandler(webapp.RequestHandler):
|
||||
"""Creates a new session and renders the shell.html template.
|
||||
"""
|
||||
|
||||
def get(self):
|
||||
# set up the session. TODO: garbage collect old shell sessions
|
||||
session_key = self.request.get('session')
|
||||
if session_key:
|
||||
session = Session.get(session_key)
|
||||
else:
|
||||
# create a new session
|
||||
session = Session()
|
||||
session.unpicklables = [db.Text(line) for line in INITIAL_UNPICKLABLES]
|
||||
session_key = session.put()
|
||||
|
||||
template_file = os.path.join(os.path.dirname(__file__), 'templates',
|
||||
'shell.html')
|
||||
session_url = '/?session=%s' % session_key
|
||||
vars = { 'server_software': os.environ['SERVER_SOFTWARE'],
|
||||
'python_version': sys.version,
|
||||
'session': str(session_key),
|
||||
'user': users.get_current_user(),
|
||||
'login_url': users.create_login_url(session_url),
|
||||
'logout_url': users.create_logout_url(session_url),
|
||||
}
|
||||
rendered = webapp.template.render(template_file, vars, debug=_DEBUG)
|
||||
self.response.out.write(rendered)
|
||||
|
||||
|
||||
class StatementHandler(webapp.RequestHandler):
|
||||
"""Evaluates a python statement in a given session and returns the result.
|
||||
"""
|
||||
|
||||
def get(self):
|
||||
self.response.headers['Content-Type'] = 'text/plain'
|
||||
|
||||
# extract the statement to be run
|
||||
statement = self.request.get('statement')
|
||||
if not statement:
|
||||
return
|
||||
|
||||
# the python compiler doesn't like network line endings
|
||||
statement = statement.replace('\r\n', '\n')
|
||||
|
||||
# add a couple newlines at the end of the statement. this makes
|
||||
# single-line expressions such as 'class Foo: pass' evaluate happily.
|
||||
statement += '\n\n'
|
||||
|
||||
# log and compile the statement up front
|
||||
try:
|
||||
logging.info('Compiling and evaluating:\n%s' % statement)
|
||||
compiled = compile(statement, '<string>', 'single')
|
||||
except:
|
||||
self.response.out.write(traceback.format_exc())
|
||||
return
|
||||
|
||||
# create a dedicated module to be used as this statement's __main__
|
||||
statement_module = new.module('__main__')
|
||||
|
||||
# use this request's __builtin__, since it changes on each request.
|
||||
# this is needed for import statements, among other things.
|
||||
import __builtin__
|
||||
statement_module.__builtins__ = __builtin__
|
||||
|
||||
# load the session from the datastore
|
||||
session = Session.get(self.request.get('session'))
|
||||
|
||||
# swap in our custom module for __main__. then unpickle the session
|
||||
# globals, run the statement, and re-pickle the session globals, all
|
||||
# inside it.
|
||||
old_main = sys.modules.get('__main__')
|
||||
try:
|
||||
sys.modules['__main__'] = statement_module
|
||||
statement_module.__name__ = '__main__'
|
||||
|
||||
# re-evaluate the unpicklables
|
||||
for code in session.unpicklables:
|
||||
exec code in statement_module.__dict__
|
||||
|
||||
# re-initialize the globals
|
||||
for name, val in session.globals_dict().items():
|
||||
try:
|
||||
statement_module.__dict__[name] = val
|
||||
except:
|
||||
msg = 'Dropping %s since it could not be unpickled.\n' % name
|
||||
self.response.out.write(msg)
|
||||
logging.warning(msg + traceback.format_exc())
|
||||
session.remove_global(name)
|
||||
|
||||
# run!
|
||||
old_globals = dict(statement_module.__dict__)
|
||||
try:
|
||||
old_stdout = sys.stdout
|
||||
old_stderr = sys.stderr
|
||||
try:
|
||||
sys.stdout = self.response.out
|
||||
sys.stderr = self.response.out
|
||||
exec compiled in statement_module.__dict__
|
||||
finally:
|
||||
sys.stdout = old_stdout
|
||||
sys.stderr = old_stderr
|
||||
except:
|
||||
self.response.out.write(traceback.format_exc())
|
||||
return
|
||||
|
||||
# extract the new globals that this statement added
|
||||
new_globals = {}
|
||||
for name, val in statement_module.__dict__.items():
|
||||
if name not in old_globals or val != old_globals[name]:
|
||||
new_globals[name] = val
|
||||
|
||||
if True in [isinstance(val, UNPICKLABLE_TYPES)
|
||||
for val in new_globals.values()]:
|
||||
# this statement added an unpicklable global. store the statement and
|
||||
# the names of all of the globals it added in the unpicklables.
|
||||
session.add_unpicklable(statement, new_globals.keys())
|
||||
logging.debug('Storing this statement as an unpicklable.')
|
||||
|
||||
else:
|
||||
# this statement didn't add any unpicklables. pickle and store the
|
||||
# new globals back into the datastore.
|
||||
for name, val in new_globals.items():
|
||||
if not name.startswith('__'):
|
||||
session.set_global(name, val)
|
||||
|
||||
finally:
|
||||
sys.modules['__main__'] = old_main
|
||||
|
||||
session.put()
|
||||
|
||||
|
||||
def main():
|
||||
application = webapp.WSGIApplication(
|
||||
[('/gae_shell/', FrontPageHandler),
|
||||
('/gae_shell/shell.do', StatementHandler)], debug=_DEBUG)
|
||||
wsgiref.handlers.CGIHandler().run(application)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
308
tools/scripts/app_engine_server/gae_shell/shell.py~
Executable file
308
tools/scripts/app_engine_server/gae_shell/shell.py~
Executable file
@@ -0,0 +1,308 @@
|
||||
#!/usr/bin/python
|
||||
#
|
||||
# Copyright 2007 Google Inc.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
"""
|
||||
An interactive, stateful AJAX shell that runs Python code on the server.
|
||||
|
||||
Part of http://code.google.com/p/google-app-engine-samples/.
|
||||
|
||||
May be run as a standalone app or in an existing app as an admin-only handler.
|
||||
Can be used for system administration tasks, as an interactive way to try out
|
||||
APIs, or as a debugging aid during development.
|
||||
|
||||
The logging, os, sys, db, and users modules are imported automatically.
|
||||
|
||||
Interpreter state is stored in the datastore so that variables, function
|
||||
definitions, and other values in the global and local namespaces can be used
|
||||
across commands.
|
||||
|
||||
To use the shell in your app, copy shell.py, static/*, and templates/* into
|
||||
your app's source directory. Then, copy the URL handlers from app.yaml into
|
||||
your app.yaml.
|
||||
|
||||
TODO: unit tests!
|
||||
"""
|
||||
|
||||
import logging
|
||||
import new
|
||||
import os
|
||||
import pickle
|
||||
import sys
|
||||
import traceback
|
||||
import types
|
||||
import wsgiref.handlers
|
||||
|
||||
from google.appengine.api import users
|
||||
from google.appengine.ext import db
|
||||
from google.appengine.ext import webapp
|
||||
from google.appengine.ext.webapp import template
|
||||
|
||||
|
||||
# Set to True if stack traces should be shown in the browser, etc.
|
||||
_DEBUG = True
|
||||
|
||||
# The entity kind for shell sessions. Feel free to rename to suit your app.
|
||||
_SESSION_KIND = '_Shell_Session'
|
||||
|
||||
# Types that can't be pickled.
|
||||
UNPICKLABLE_TYPES = (
|
||||
types.ModuleType,
|
||||
types.TypeType,
|
||||
types.ClassType,
|
||||
types.FunctionType,
|
||||
)
|
||||
|
||||
# Unpicklable statements to seed new sessions with.
|
||||
INITIAL_UNPICKLABLES = [
|
||||
'import logging',
|
||||
'import os',
|
||||
'import sys',
|
||||
'from google.appengine.ext import db',
|
||||
'from google.appengine.api import users',
|
||||
]
|
||||
|
||||
|
||||
class Session(db.Model):
|
||||
"""A shell session. Stores the session's globals.
|
||||
|
||||
Each session globals is stored in one of two places:
|
||||
|
||||
If the global is picklable, it's stored in the parallel globals and
|
||||
global_names list properties. (They're parallel lists to work around the
|
||||
unfortunate fact that the datastore can't store dictionaries natively.)
|
||||
|
||||
If the global is not picklable (e.g. modules, classes, and functions), or if
|
||||
it was created by the same statement that created an unpicklable global,
|
||||
it's not stored directly. Instead, the statement is stored in the
|
||||
unpicklables list property. On each request, before executing the current
|
||||
statement, the unpicklable statements are evaluated to recreate the
|
||||
unpicklable globals.
|
||||
|
||||
The unpicklable_names property stores all of the names of globals that were
|
||||
added by unpicklable statements. When we pickle and store the globals after
|
||||
executing a statement, we skip the ones in unpicklable_names.
|
||||
|
||||
Using Text instead of string is an optimization. We don't query on any of
|
||||
these properties, so they don't need to be indexed.
|
||||
"""
|
||||
global_names = db.ListProperty(db.Text)
|
||||
globals = db.ListProperty(db.Blob)
|
||||
unpicklable_names = db.ListProperty(db.Text)
|
||||
unpicklables = db.ListProperty(db.Text)
|
||||
|
||||
def set_global(self, name, value):
|
||||
"""Adds a global, or updates it if it already exists.
|
||||
|
||||
Also removes the global from the list of unpicklable names.
|
||||
|
||||
Args:
|
||||
name: the name of the global to remove
|
||||
value: any picklable value
|
||||
"""
|
||||
blob = db.Blob(pickle.dumps(value))
|
||||
|
||||
if name in self.global_names:
|
||||
index = self.global_names.index(name)
|
||||
self.globals[index] = blob
|
||||
else:
|
||||
self.global_names.append(db.Text(name))
|
||||
self.globals.append(blob)
|
||||
|
||||
self.remove_unpicklable_name(name)
|
||||
|
||||
def remove_global(self, name):
|
||||
"""Removes a global, if it exists.
|
||||
|
||||
Args:
|
||||
name: string, the name of the global to remove
|
||||
"""
|
||||
if name in self.global_names:
|
||||
index = self.global_names.index(name)
|
||||
del self.global_names[index]
|
||||
del self.globals[index]
|
||||
|
||||
def globals_dict(self):
|
||||
"""Returns a dictionary view of the globals.
|
||||
"""
|
||||
return dict((name, pickle.loads(val))
|
||||
for name, val in zip(self.global_names, self.globals))
|
||||
|
||||
def add_unpicklable(self, statement, names):
|
||||
"""Adds a statement and list of names to the unpicklables.
|
||||
|
||||
Also removes the names from the globals.
|
||||
|
||||
Args:
|
||||
statement: string, the statement that created new unpicklable global(s).
|
||||
names: list of strings; the names of the globals created by the statement.
|
||||
"""
|
||||
self.unpicklables.append(db.Text(statement))
|
||||
|
||||
for name in names:
|
||||
self.remove_global(name)
|
||||
if name not in self.unpicklable_names:
|
||||
self.unpicklable_names.append(db.Text(name))
|
||||
|
||||
def remove_unpicklable_name(self, name):
|
||||
"""Removes a name from the list of unpicklable names, if it exists.
|
||||
|
||||
Args:
|
||||
name: string, the name of the unpicklable global to remove
|
||||
"""
|
||||
if name in self.unpicklable_names:
|
||||
self.unpicklable_names.remove(name)
|
||||
|
||||
|
||||
class FrontPageHandler(webapp.RequestHandler):
|
||||
"""Creates a new session and renders the shell.html template.
|
||||
"""
|
||||
|
||||
def get(self):
|
||||
# set up the session. TODO: garbage collect old shell sessions
|
||||
session_key = self.request.get('session')
|
||||
if session_key:
|
||||
session = Session.get(session_key)
|
||||
else:
|
||||
# create a new session
|
||||
session = Session()
|
||||
session.unpicklables = [db.Text(line) for line in INITIAL_UNPICKLABLES]
|
||||
session_key = session.put()
|
||||
|
||||
template_file = os.path.join(os.path.dirname(__file__), 'templates',
|
||||
'shell.html')
|
||||
session_url = '/?session=%s' % session_key
|
||||
vars = { 'server_software': os.environ['SERVER_SOFTWARE'],
|
||||
'python_version': sys.version,
|
||||
'session': str(session_key),
|
||||
'user': users.get_current_user(),
|
||||
'login_url': users.create_login_url(session_url),
|
||||
'logout_url': users.create_logout_url(session_url),
|
||||
}
|
||||
rendered = webapp.template.render(template_file, vars, debug=_DEBUG)
|
||||
self.response.out.write(rendered)
|
||||
|
||||
|
||||
class StatementHandler(webapp.RequestHandler):
|
||||
"""Evaluates a python statement in a given session and returns the result.
|
||||
"""
|
||||
|
||||
def get(self):
|
||||
self.response.headers['Content-Type'] = 'text/plain'
|
||||
|
||||
# extract the statement to be run
|
||||
statement = self.request.get('statement')
|
||||
if not statement:
|
||||
return
|
||||
|
||||
# the python compiler doesn't like network line endings
|
||||
statement = statement.replace('\r\n', '\n')
|
||||
|
||||
# add a couple newlines at the end of the statement. this makes
|
||||
# single-line expressions such as 'class Foo: pass' evaluate happily.
|
||||
statement += '\n\n'
|
||||
|
||||
# log and compile the statement up front
|
||||
try:
|
||||
logging.info('Compiling and evaluating:\n%s' % statement)
|
||||
compiled = compile(statement, '<string>', 'single')
|
||||
except:
|
||||
self.response.out.write(traceback.format_exc())
|
||||
return
|
||||
|
||||
# create a dedicated module to be used as this statement's __main__
|
||||
statement_module = new.module('__main__')
|
||||
|
||||
# use this request's __builtin__, since it changes on each request.
|
||||
# this is needed for import statements, among other things.
|
||||
import __builtin__
|
||||
statement_module.__builtins__ = __builtin__
|
||||
|
||||
# load the session from the datastore
|
||||
session = Session.get(self.request.get('session'))
|
||||
|
||||
# swap in our custom module for __main__. then unpickle the session
|
||||
# globals, run the statement, and re-pickle the session globals, all
|
||||
# inside it.
|
||||
old_main = sys.modules.get('__main__')
|
||||
try:
|
||||
sys.modules['__main__'] = statement_module
|
||||
statement_module.__name__ = '__main__'
|
||||
|
||||
# re-evaluate the unpicklables
|
||||
for code in session.unpicklables:
|
||||
exec code in statement_module.__dict__
|
||||
|
||||
# re-initialize the globals
|
||||
for name, val in session.globals_dict().items():
|
||||
try:
|
||||
statement_module.__dict__[name] = val
|
||||
except:
|
||||
msg = 'Dropping %s since it could not be unpickled.\n' % name
|
||||
self.response.out.write(msg)
|
||||
logging.warning(msg + traceback.format_exc())
|
||||
session.remove_global(name)
|
||||
|
||||
# run!
|
||||
old_globals = dict(statement_module.__dict__)
|
||||
try:
|
||||
old_stdout = sys.stdout
|
||||
old_stderr = sys.stderr
|
||||
try:
|
||||
sys.stdout = self.response.out
|
||||
sys.stderr = self.response.out
|
||||
exec compiled in statement_module.__dict__
|
||||
finally:
|
||||
sys.stdout = old_stdout
|
||||
sys.stderr = old_stderr
|
||||
except:
|
||||
self.response.out.write(traceback.format_exc())
|
||||
return
|
||||
|
||||
# extract the new globals that this statement added
|
||||
new_globals = {}
|
||||
for name, val in statement_module.__dict__.items():
|
||||
if name not in old_globals or val != old_globals[name]:
|
||||
new_globals[name] = val
|
||||
|
||||
if True in [isinstance(val, UNPICKLABLE_TYPES)
|
||||
for val in new_globals.values()]:
|
||||
# this statement added an unpicklable global. store the statement and
|
||||
# the names of all of the globals it added in the unpicklables.
|
||||
session.add_unpicklable(statement, new_globals.keys())
|
||||
logging.debug('Storing this statement as an unpicklable.')
|
||||
|
||||
else:
|
||||
# this statement didn't add any unpicklables. pickle and store the
|
||||
# new globals back into the datastore.
|
||||
for name, val in new_globals.items():
|
||||
if not name.startswith('__'):
|
||||
session.set_global(name, val)
|
||||
|
||||
finally:
|
||||
sys.modules['__main__'] = old_main
|
||||
|
||||
session.put()
|
||||
|
||||
|
||||
def main():
|
||||
application = webapp.WSGIApplication(
|
||||
[('/', FrontPageHandler),
|
||||
('/shell.do', StatementHandler)], debug=_DEBUG)
|
||||
wsgiref.handlers.CGIHandler().run(application)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
195
tools/scripts/app_engine_server/gae_shell/static/shell.js
Normal file
195
tools/scripts/app_engine_server/gae_shell/static/shell.js
Normal file
@@ -0,0 +1,195 @@
|
||||
// Copyright 2007 Google Inc.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
/**
|
||||
* @fileoverview
|
||||
* Javascript code for the interactive AJAX shell.
|
||||
*
|
||||
* Part of http://code.google.com/p/google-app-engine-samples/.
|
||||
*
|
||||
* Includes a function (shell.runStatement) that sends the current python
|
||||
* statement in the shell prompt text box to the server, and a callback
|
||||
* (shell.done) that displays the results when the XmlHttpRequest returns.
|
||||
*
|
||||
* Also includes cross-browser code (shell.getXmlHttpRequest) to get an
|
||||
* XmlHttpRequest.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Shell namespace.
|
||||
* @type {Object}
|
||||
*/
|
||||
var shell = {}
|
||||
|
||||
/**
|
||||
* The shell history. history is an array of strings, ordered oldest to
|
||||
* newest. historyCursor is the current history element that the user is on.
|
||||
*
|
||||
* The last history element is the statement that the user is currently
|
||||
* typing. When a statement is run, it's frozen in the history, a new history
|
||||
* element is added to the end of the array for the new statement, and
|
||||
* historyCursor is updated to point to the new element.
|
||||
*
|
||||
* @type {Array}
|
||||
*/
|
||||
shell.history = [''];
|
||||
|
||||
/**
|
||||
* See {shell.history}
|
||||
* @type {number}
|
||||
*/
|
||||
shell.historyCursor = 0;
|
||||
|
||||
/**
|
||||
* A constant for the XmlHttpRequest 'done' state.
|
||||
* @type Number
|
||||
*/
|
||||
shell.DONE_STATE = 4;
|
||||
|
||||
/**
|
||||
* A cross-browser function to get an XmlHttpRequest object.
|
||||
*
|
||||
* @return {XmlHttpRequest?} a new XmlHttpRequest
|
||||
*/
|
||||
shell.getXmlHttpRequest = function() {
|
||||
if (window.XMLHttpRequest) {
|
||||
return new XMLHttpRequest();
|
||||
} else if (window.ActiveXObject) {
|
||||
try {
|
||||
return new ActiveXObject('Msxml2.XMLHTTP');
|
||||
} catch(e) {
|
||||
return new ActiveXObject('Microsoft.XMLHTTP');
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
/**
|
||||
* This is the prompt textarea's onkeypress handler. Depending on the key that
|
||||
* was pressed, it will run the statement, navigate the history, or update the
|
||||
* current statement in the history.
|
||||
*
|
||||
* @param {Event} event the keypress event
|
||||
* @return {Boolean} false to tell the browser not to submit the form.
|
||||
*/
|
||||
shell.onPromptKeyPress = function(event) {
|
||||
var statement = document.getElementById('statement');
|
||||
|
||||
if (this.historyCursor == this.history.length - 1) {
|
||||
// we're on the current statement. update it in the history before doing
|
||||
// anything.
|
||||
this.history[this.historyCursor] = statement.value;
|
||||
}
|
||||
|
||||
// should we pull something from the history?
|
||||
if (event.ctrlKey && event.keyCode == 38 /* up arrow */) {
|
||||
if (this.historyCursor > 0) {
|
||||
statement.value = this.history[--this.historyCursor];
|
||||
}
|
||||
return false;
|
||||
} else if (event.ctrlKey && event.keyCode == 40 /* down arrow */) {
|
||||
if (this.historyCursor < this.history.length - 1) {
|
||||
statement.value = this.history[++this.historyCursor];
|
||||
}
|
||||
return false;
|
||||
} else if (!event.altKey) {
|
||||
// probably changing the statement. update it in the history.
|
||||
this.historyCursor = this.history.length - 1;
|
||||
this.history[this.historyCursor] = statement.value;
|
||||
}
|
||||
|
||||
// should we submit?
|
||||
var ctrlEnter = (document.getElementById('submit_key').value == 'ctrl-enter');
|
||||
if (event.keyCode == 13 /* enter */ && !event.altKey && !event.shiftKey &&
|
||||
event.ctrlKey == ctrlEnter) {
|
||||
return this.runStatement();
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* The XmlHttpRequest callback. If the request succeeds, it adds the command
|
||||
* and its resulting output to the shell history div.
|
||||
*
|
||||
* @param {XmlHttpRequest} req the XmlHttpRequest we used to send the current
|
||||
* statement to the server
|
||||
*/
|
||||
shell.done = function(req) {
|
||||
if (req.readyState == this.DONE_STATE) {
|
||||
var statement = document.getElementById('statement')
|
||||
statement.className = 'prompt';
|
||||
|
||||
// add the command to the shell output
|
||||
var output = document.getElementById('output');
|
||||
|
||||
output.value += '\n>>> ' + statement.value;
|
||||
statement.value = '';
|
||||
|
||||
// add a new history element
|
||||
this.history.push('');
|
||||
this.historyCursor = this.history.length - 1;
|
||||
|
||||
// add the command's result
|
||||
var result = req.responseText.replace(/^\s*|\s*$/g, ''); // trim whitespace
|
||||
if (result != '')
|
||||
output.value += '\n' + result;
|
||||
|
||||
// scroll to the bottom
|
||||
output.scrollTop = output.scrollHeight;
|
||||
if (output.createTextRange) {
|
||||
var range = output.createTextRange();
|
||||
range.collapse(false);
|
||||
range.select();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* This is the form's onsubmit handler. It sends the python statement to the
|
||||
* server, and registers shell.done() as the callback to run when it returns.
|
||||
*
|
||||
* @return {Boolean} false to tell the browser not to submit the form.
|
||||
*/
|
||||
shell.runStatement = function() {
|
||||
var form = document.getElementById('form');
|
||||
|
||||
// build a XmlHttpRequest
|
||||
var req = this.getXmlHttpRequest();
|
||||
if (!req) {
|
||||
document.getElementById('ajax-status').innerHTML =
|
||||
"<span class='error'>Your browser doesn't support AJAX. :(</span>";
|
||||
return false;
|
||||
}
|
||||
|
||||
req.onreadystatechange = function() { shell.done(req); };
|
||||
|
||||
// build the query parameter string
|
||||
var params = '';
|
||||
for (i = 0; i < form.elements.length; i++) {
|
||||
var elem = form.elements[i];
|
||||
if (elem.type != 'submit' && elem.type != 'button' && elem.id != 'caret') {
|
||||
var value = escape(elem.value).replace(/\+/g, '%2B'); // escape ignores +
|
||||
params += '&' + elem.name + '=' + value;
|
||||
}
|
||||
}
|
||||
|
||||
// send the request and tell the user.
|
||||
document.getElementById('statement').className = 'prompt processing';
|
||||
req.open(form.method, form.action + '?' + params, true);
|
||||
req.setRequestHeader('Content-type',
|
||||
'application/x-www-form-urlencoded;charset=UTF-8');
|
||||
req.send(null);
|
||||
|
||||
return false;
|
||||
};
|
||||
BIN
tools/scripts/app_engine_server/gae_shell/static/spinner.gif
Normal file
BIN
tools/scripts/app_engine_server/gae_shell/static/spinner.gif
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.5 KiB |
122
tools/scripts/app_engine_server/gae_shell/templates/shell.html
Normal file
122
tools/scripts/app_engine_server/gae_shell/templates/shell.html
Normal file
@@ -0,0 +1,122 @@
|
||||
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN"
|
||||
"http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
|
||||
<html>
|
||||
<head>
|
||||
<meta http-equiv="content-type" content="text/html; charset=utf-8" />
|
||||
<title> Interactive Shell </title>
|
||||
<script type="text/javascript" src="/gae_shell/static/shell.js"></script>
|
||||
<style type="text/css">
|
||||
body {
|
||||
font-family: monospace;
|
||||
font-size: 10pt;
|
||||
}
|
||||
|
||||
p {
|
||||
margin: 0.5em;
|
||||
}
|
||||
|
||||
.prompt, #output {
|
||||
width: 45em;
|
||||
border: 1px solid silver;
|
||||
background-color: #f5f5f5;
|
||||
font-size: 10pt;
|
||||
margin: 0.5em;
|
||||
padding: 0.5em;
|
||||
padding-right: 0em;
|
||||
overflow-x: hidden;
|
||||
}
|
||||
|
||||
#toolbar {
|
||||
margin-left: 0.5em;
|
||||
padding-left: 0.5em;
|
||||
}
|
||||
|
||||
#caret {
|
||||
width: 2.5em;
|
||||
margin-right: 0px;
|
||||
padding-right: 0px;
|
||||
border-right: 0px;
|
||||
}
|
||||
|
||||
#statement {
|
||||
width: 43em;
|
||||
margin-left: -1em;
|
||||
padding-left: 0px;
|
||||
border-left: 0px;
|
||||
background-position: top right;
|
||||
background-repeat: no-repeat;
|
||||
}
|
||||
|
||||
.processing {
|
||||
background-image: url("/gae_shell/static/spinner.gif");
|
||||
}
|
||||
|
||||
#ajax-status {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.message {
|
||||
color: #8AD;
|
||||
font-weight: bold;
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
.error {
|
||||
color: #F44;
|
||||
}
|
||||
|
||||
.username {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
|
||||
<p> Interactive server-side Python shell for
|
||||
<a href="http://code.google.com/appengine/">Google App Engine</a>.
|
||||
(<a href="http://code.google.com/p/google-app-engine-samples/">source</a>)
|
||||
</p>
|
||||
|
||||
<textarea id="output" rows="22" readonly="readonly">
|
||||
{{ server_software }}
|
||||
Python {{ python_version }}
|
||||
</textarea>
|
||||
|
||||
<form id="form" action="shell.do" method="get">
|
||||
<nobr>
|
||||
<textarea class="prompt" id="caret" readonly="readonly" rows="4"
|
||||
onfocus="document.getElementById('statement').focus()"
|
||||
>>>></textarea>
|
||||
<textarea class="prompt" name="statement" id="statement" rows="4"
|
||||
onkeypress="return shell.onPromptKeyPress(event);"></textarea>
|
||||
</nobr>
|
||||
<input type="hidden" name="session" value="{{ session }}" />
|
||||
<input type="submit" style="display: none" />
|
||||
</form>
|
||||
|
||||
<p id="ajax-status"></p>
|
||||
|
||||
<p id="toolbar">
|
||||
{% if user %}
|
||||
<span class="username">{{ user.nickname }}</span>
|
||||
(<a href="{{ logout_url }}">log out</a>)
|
||||
{% else %}
|
||||
<a href="{{ login_url }}">log in</a>
|
||||
{% endif %}
|
||||
| Ctrl-Up/Down for history |
|
||||
<select id="submit_key">
|
||||
<option value="enter">Enter</option>
|
||||
<option value="ctrl-enter" selected="selected">Ctrl-Enter</option>
|
||||
</select>
|
||||
<label for="submit_key">submits</label>
|
||||
</p>
|
||||
|
||||
<script type="text/javascript">
|
||||
document.getElementById('statement').focus();
|
||||
</script>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
|
||||
12
tools/scripts/app_engine_server/index.yaml
Normal file
12
tools/scripts/app_engine_server/index.yaml
Normal file
@@ -0,0 +1,12 @@
|
||||
indexes:
|
||||
|
||||
# AUTOGENERATED
|
||||
|
||||
# This index.yaml is automatically updated whenever the dev_appserver
|
||||
# detects that a new type of query is run. If you want to manage the
|
||||
# index.yaml file manually, remove the above marker line (the line
|
||||
# saying "# AUTOGENERATED"). If you want to manage some indexes
|
||||
# manually, move them above the marker line. The index.yaml file is
|
||||
# automatically uploaded to the admin console when you next deploy
|
||||
# your application using appcfg.py.
|
||||
|
||||
412
tools/scripts/app_engine_server/memcache_zipserve.py
Normal file
412
tools/scripts/app_engine_server/memcache_zipserve.py
Normal file
@@ -0,0 +1,412 @@
|
||||
#!/usr/bin/env python
|
||||
#
|
||||
# Copyright 2009 Google Inc.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
#
|
||||
|
||||
"""A class to serve pages from zip files and use memcache for performance.
|
||||
|
||||
This contains a class and a function to create an anonymous instance of the
|
||||
class to serve HTTP GET requests. Memcache is used to increase response speed
|
||||
and lower processing cycles used in serving. Credit to Guido van Rossum and
|
||||
his implementation of zipserve which served as a reference as I wrote this.
|
||||
|
||||
MemcachedZipHandler: Class that serves request
|
||||
create_handler: method to create instance of MemcachedZipHandler
|
||||
"""
|
||||
|
||||
__author__ = 'jmatt@google.com (Justin Mattson)'
|
||||
|
||||
import email.Utils
|
||||
import logging
|
||||
import mimetypes
|
||||
import time
|
||||
import zipfile
|
||||
|
||||
from google.appengine.api import memcache
|
||||
from google.appengine.ext import webapp
|
||||
from google.appengine.ext.webapp import util
|
||||
|
||||
|
||||
def create_handler(zip_files, max_age=None, public=None):
|
||||
"""Factory method to create a MemcachedZipHandler instance.
|
||||
|
||||
Args:
|
||||
zip_files: A list of file names, or a list of lists of file name, first
|
||||
member of file mappings. See MemcachedZipHandler documentation for
|
||||
more information about using the list of lists format
|
||||
max_age: The maximum client-side cache lifetime
|
||||
public: Whether this should be declared public in the client-side cache
|
||||
Returns:
|
||||
A MemcachedZipHandler wrapped in a pretty, anonymous bow for use with App
|
||||
Engine
|
||||
|
||||
Raises:
|
||||
ValueError: if the zip_files argument is not a list
|
||||
"""
|
||||
# verify argument integrity. If the argument is passed in list format,
|
||||
# convert it to list of lists format
|
||||
|
||||
if zip_files and type(zip_files).__name__ == 'list':
|
||||
num_items = len(zip_files)
|
||||
while num_items > 0:
|
||||
if type(zip_files[num_items - 1]).__name__ != 'list':
|
||||
zip_files[num_items - 1] = [zip_files[num_items-1]]
|
||||
num_items -= 1
|
||||
else:
|
||||
raise ValueError('File name arguments must be a list')
|
||||
|
||||
class HandlerWrapper(MemcachedZipHandler):
|
||||
"""Simple wrapper for an instance of MemcachedZipHandler.
|
||||
|
||||
I'm still not sure why this is needed
|
||||
"""
|
||||
|
||||
def get(self, name):
|
||||
self.zipfilenames = zip_files
|
||||
self.TrueGet(name)
|
||||
if max_age is not None:
|
||||
MAX_AGE = max_age
|
||||
if public is not None:
|
||||
PUBLIC = public
|
||||
|
||||
return HandlerWrapper
|
||||
|
||||
|
||||
class MemcachedZipHandler(webapp.RequestHandler):
|
||||
"""Handles get requests for a given URL.
|
||||
|
||||
Serves a GET request from a series of zip files. As files are served they are
|
||||
put into memcache, which is much faster than retreiving them from the zip
|
||||
source file again. It also uses considerably fewer CPU cycles.
|
||||
"""
|
||||
zipfile_cache = {} # class cache of source zip files
|
||||
MAX_AGE = 600 # max client-side cache lifetime
|
||||
PUBLIC = True # public cache setting
|
||||
CACHE_PREFIX = 'cache://' # memcache key prefix for actual URLs
|
||||
NEG_CACHE_PREFIX = 'noncache://' # memcache key prefix for non-existant URL
|
||||
|
||||
def TrueGet(self, name):
|
||||
"""The top-level entry point to serving requests.
|
||||
|
||||
Called 'True' get because it does the work when called from the wrapper
|
||||
class' get method
|
||||
|
||||
Args:
|
||||
name: URL requested
|
||||
|
||||
Returns:
|
||||
None
|
||||
"""
|
||||
name = self.PreprocessUrl(name)
|
||||
|
||||
# see if we have the page in the memcache
|
||||
resp_data = self.GetFromCache(name)
|
||||
if resp_data is None:
|
||||
logging.info('Cache miss for %s', name)
|
||||
resp_data = self.GetFromNegativeCache(name)
|
||||
if resp_data is None:
|
||||
resp_data = self.GetFromStore(name)
|
||||
|
||||
# IF we have the file, put it in the memcache
|
||||
# ELSE put it in the negative cache
|
||||
if resp_data is not None:
|
||||
self.StoreOrUpdateInCache(name, resp_data)
|
||||
else:
|
||||
logging.info('Adding %s to negative cache, serving 404', name)
|
||||
self.StoreInNegativeCache(name)
|
||||
self.Write404Error()
|
||||
return
|
||||
else:
|
||||
self.Write404Error()
|
||||
return
|
||||
|
||||
content_type, encoding = mimetypes.guess_type(name)
|
||||
if content_type:
|
||||
self.response.headers['Content-Type'] = content_type
|
||||
self.SetCachingHeaders()
|
||||
self.response.out.write(resp_data)
|
||||
|
||||
def PreprocessUrl(self, name):
|
||||
"""Any preprocessing work on the URL when it comes it.
|
||||
|
||||
Put any work related to interpretting the incoming URL here. For example,
|
||||
this is used to redirect requests for a directory to the index.html file
|
||||
in that directory. Subclasses should override this method to do different
|
||||
preprocessing.
|
||||
|
||||
Args:
|
||||
name: The incoming URL
|
||||
|
||||
Returns:
|
||||
The processed URL
|
||||
"""
|
||||
# handle special case of requesting the domain itself
|
||||
if not name:
|
||||
name = 'index.html'
|
||||
|
||||
# determine if this is a request for a directory
|
||||
final_path_segment = name
|
||||
final_slash_offset = name.rfind('/')
|
||||
if final_slash_offset != len(name) - 1:
|
||||
final_path_segment = name[final_slash_offset + 1:]
|
||||
if final_path_segment.find('.') == -1:
|
||||
name = ''.join([name, '/'])
|
||||
|
||||
# if this is a directory, redirect to index.html
|
||||
if name[len(name) - 1:] == '/':
|
||||
return '%s%s' % (name, 'index.html')
|
||||
else:
|
||||
return name
|
||||
|
||||
def GetFromStore(self, file_path):
|
||||
"""Retrieve file from zip files.
|
||||
|
||||
Get the file from the source, it must not have been in the memcache. If
|
||||
possible, we'll use the zip file index to quickly locate where the file
|
||||
should be found. (See MapToFileArchive documentation for assumptions about
|
||||
file ordering.) If we don't have an index or don't find the file where the
|
||||
index says we should, look through all the zip files to find it.
|
||||
|
||||
Args:
|
||||
file_path: the file that we're looking for
|
||||
|
||||
Returns:
|
||||
The contents of the requested file
|
||||
"""
|
||||
resp_data = None
|
||||
file_itr = iter(self.zipfilenames)
|
||||
|
||||
# check the index, if we have one, to see what archive the file is in
|
||||
archive_name = self.MapFileToArchive(file_path)
|
||||
if not archive_name:
|
||||
archive_name = file_itr.next()[0]
|
||||
|
||||
while resp_data is None and archive_name:
|
||||
zip_archive = self.LoadZipFile(archive_name)
|
||||
if zip_archive:
|
||||
|
||||
# we expect some lookups will fail, and that's okay, 404s will deal
|
||||
# with that
|
||||
try:
|
||||
resp_data = zip_archive.read(file_path)
|
||||
except (KeyError, RuntimeError), err:
|
||||
# no op
|
||||
x = False
|
||||
if resp_data is not None:
|
||||
logging.info('%s read from %s', file_path, archive_name)
|
||||
|
||||
try:
|
||||
archive_name = file_itr.next()[0]
|
||||
except (StopIteration), err:
|
||||
archive_name = False
|
||||
|
||||
return resp_data
|
||||
|
||||
def LoadZipFile(self, zipfilename):
|
||||
"""Convenience method to load zip file.
|
||||
|
||||
Just a convenience method to load the zip file from the data store. This is
|
||||
useful if we ever want to change data stores and also as a means of
|
||||
dependency injection for testing. This method will look at our file cache
|
||||
first, and then load and cache the file if there's a cache miss
|
||||
|
||||
Args:
|
||||
zipfilename: the name of the zip file to load
|
||||
|
||||
Returns:
|
||||
The zip file requested, or None if there is an I/O error
|
||||
"""
|
||||
zip_archive = None
|
||||
zip_archive = self.zipfile_cache.get(zipfilename)
|
||||
if zip_archive is None:
|
||||
try:
|
||||
zip_archive = zipfile.ZipFile(zipfilename)
|
||||
self.zipfile_cache[zipfilename] = zip_archive
|
||||
except (IOError, RuntimeError), err:
|
||||
logging.error('Can\'t open zipfile %s, cause: %s' % (zipfilename,
|
||||
err))
|
||||
return zip_archive
|
||||
|
||||
def MapFileToArchive(self, file_path):
|
||||
"""Given a file name, determine what archive it should be in.
|
||||
|
||||
This method makes two critical assumptions.
|
||||
(1) The zip files passed as an argument to the handler, if concatenated
|
||||
in that same order, would result in a total ordering
|
||||
of all the files. See (2) for ordering type.
|
||||
(2) Upper case letters before lower case letters. The traversal of a
|
||||
directory tree is depth first. A parent directory's files are added
|
||||
before the files of any child directories
|
||||
|
||||
Args:
|
||||
file_path: the file to be mapped to an archive
|
||||
|
||||
Returns:
|
||||
The name of the archive where we expect the file to be
|
||||
"""
|
||||
num_archives = len(self.zipfilenames)
|
||||
while num_archives > 0:
|
||||
target = self.zipfilenames[num_archives - 1]
|
||||
if len(target) > 1:
|
||||
if self.CompareFilenames(target[1], file_path) >= 0:
|
||||
return target[0]
|
||||
num_archives -= 1
|
||||
|
||||
return None
|
||||
|
||||
def CompareFilenames(self, file1, file2):
|
||||
"""Determines whether file1 is lexigraphically 'before' file2.
|
||||
|
||||
WARNING: This method assumes that paths are output in a depth-first,
|
||||
with parent directories' files stored before childs'
|
||||
|
||||
We say that file1 is lexigraphically before file2 if the last non-matching
|
||||
path segment of file1 is alphabetically before file2.
|
||||
|
||||
Args:
|
||||
file1: the first file path
|
||||
file2: the second file path
|
||||
|
||||
Returns:
|
||||
A positive number if file1 is before file2
|
||||
A negative number if file2 is before file1
|
||||
0 if filenames are the same
|
||||
"""
|
||||
f1_segments = file1.split('/')
|
||||
f2_segments = file2.split('/')
|
||||
|
||||
segment_ptr = 0
|
||||
while (segment_ptr < len(f1_segments) and
|
||||
segment_ptr < len(f2_segments) and
|
||||
f1_segments[segment_ptr] == f2_segments[segment_ptr]):
|
||||
segment_ptr += 1
|
||||
|
||||
if len(f1_segments) == len(f2_segments):
|
||||
|
||||
# we fell off the end, the paths much be the same
|
||||
if segment_ptr == len(f1_segments):
|
||||
return 0
|
||||
|
||||
# we didn't fall of the end, compare the segments where they differ
|
||||
if f1_segments[segment_ptr] < f2_segments[segment_ptr]:
|
||||
return 1
|
||||
elif f1_segments[segment_ptr] > f2_segments[segment_ptr]:
|
||||
return -1
|
||||
else:
|
||||
return 0
|
||||
|
||||
# the number of segments differs, we either mismatched comparing
|
||||
# directories, or comparing a file to a directory
|
||||
else:
|
||||
|
||||
# IF we were looking at the last segment of one of the paths,
|
||||
# the one with fewer segments is first because files come before
|
||||
# directories
|
||||
# ELSE we just need to compare directory names
|
||||
if (segment_ptr + 1 == len(f1_segments) or
|
||||
segment_ptr + 1 == len(f2_segments)):
|
||||
return len(f2_segments) - len(f1_segments)
|
||||
else:
|
||||
if f1_segments[segment_ptr] < f2_segments[segment_ptr]:
|
||||
return 1
|
||||
elif f1_segments[segment_ptr] > f2_segments[segment_ptr]:
|
||||
return -1
|
||||
else:
|
||||
return 0
|
||||
|
||||
def SetCachingHeaders(self):
|
||||
"""Set caching headers for the request."""
|
||||
max_age = self.MAX_AGE
|
||||
self.response.headers['Expires'] = email.Utils.formatdate(
|
||||
time.time() + max_age, usegmt=True)
|
||||
cache_control = []
|
||||
if self.PUBLIC:
|
||||
cache_control.append('public')
|
||||
cache_control.append('max-age=%d' % max_age)
|
||||
self.response.headers['Cache-Control'] = ', '.join(cache_control)
|
||||
|
||||
def GetFromCache(self, filename):
|
||||
"""Get file from memcache, if available.
|
||||
|
||||
Args:
|
||||
filename: The URL of the file to return
|
||||
|
||||
Returns:
|
||||
The content of the file
|
||||
"""
|
||||
return memcache.get('%s%s' % (self.CACHE_PREFIX, filename))
|
||||
|
||||
def StoreOrUpdateInCache(self, filename, data):
|
||||
"""Store data in the cache.
|
||||
|
||||
Store a piece of data in the memcache. Memcache has a maximum item size of
|
||||
1*10^6 bytes. If the data is too large, fail, but log the failure. Future
|
||||
work will consider compressing the data before storing or chunking it
|
||||
|
||||
Args:
|
||||
filename: the name of the file to store
|
||||
data: the data of the file
|
||||
|
||||
Returns:
|
||||
None
|
||||
"""
|
||||
try:
|
||||
if not memcache.add('%s%s' % (self.CACHE_PREFIX, filename), data):
|
||||
memcache.replace('%s%s' % (self.CACHE_PREFIX, filename), data)
|
||||
except (ValueError), err:
|
||||
logging.warning('Data size too large to cache\n%s' % err)
|
||||
|
||||
def Write404Error(self):
|
||||
"""Ouptut a simple 404 response."""
|
||||
self.error(404)
|
||||
self.response.out.write(
|
||||
''.join(['<html><head><title>404: Not Found</title></head>',
|
||||
'<body><b><h2>Error 404</h2><br/>',
|
||||
'File not found</b></body></html>']))
|
||||
|
||||
def StoreInNegativeCache(self, filename):
|
||||
"""If a non-existant URL is accessed, cache this result as well.
|
||||
|
||||
Future work should consider setting a maximum negative cache size to
|
||||
prevent it from from negatively impacting the real cache.
|
||||
|
||||
Args:
|
||||
filename: URL to add ot negative cache
|
||||
|
||||
Returns:
|
||||
None
|
||||
"""
|
||||
memcache.add('%s%s' % (self.NEG_CACHE_PREFIX, filename), -1)
|
||||
|
||||
def GetFromNegativeCache(self, filename):
|
||||
"""Retrieve from negative cache.
|
||||
|
||||
Args:
|
||||
filename: URL to retreive
|
||||
|
||||
Returns:
|
||||
The file contents if present in the negative cache.
|
||||
"""
|
||||
return memcache.get('%s%s' % (self.NEG_CACHE_PREFIX, filename))
|
||||
|
||||
|
||||
def main():
|
||||
application = webapp.WSGIApplication([('/([^/]+)/(.*)',
|
||||
MemcachedZipHandler)])
|
||||
util.run_wsgi_app(application)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
Reference in New Issue
Block a user