app/shell/shell.py
changeset 2335 366e64ecba91
child 2344 621252e2cc18
equal deleted inserted replaced
2334:6f5f6a9965c6 2335:366e64ecba91
       
     1 #!/usr/bin/python
       
     2 #
       
     3 # Copyright 2007 Google Inc.
       
     4 #
       
     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
       
     8 #
       
     9 #      http://www.apache.org/licenses/LICENSE-2.0
       
    10 #
       
    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.
       
    16 
       
    17 """
       
    18 An interactive, stateful AJAX shell that runs Python code on the server.
       
    19 
       
    20 Part of http://code.google.com/p/google-app-engine-samples/.
       
    21 
       
    22 May be run as a standalone app or in an existing app as an admin-only handler.
       
    23 Can be used for system administration tasks, as an interactive way to try out
       
    24 APIs, or as a debugging aid during development.
       
    25 
       
    26 The logging, os, sys, db, and users modules are imported automatically.
       
    27 
       
    28 Interpreter state is stored in the datastore so that variables, function
       
    29 definitions, and other values in the global and local namespaces can be used
       
    30 across commands.
       
    31 
       
    32 To use the shell in your app, copy shell.py, static/*, and templates/* into
       
    33 your app's source directory. Then, copy the URL handlers from app.yaml into
       
    34 your app.yaml.
       
    35 
       
    36 TODO: unit tests!
       
    37 """
       
    38 
       
    39 import logging
       
    40 import new
       
    41 import os
       
    42 import pickle
       
    43 import sys
       
    44 import traceback
       
    45 import types
       
    46 import wsgiref.handlers
       
    47 
       
    48 from django.template import loader
       
    49 from google.appengine.api import users
       
    50 from google.appengine.ext import db
       
    51 from google.appengine.ext import webapp
       
    52 from google.appengine.ext.webapp import template
       
    53 
       
    54 import gae_django
       
    55 
       
    56 
       
    57 # Set to True if stack traces should be shown in the browser, etc.
       
    58 _DEBUG = True
       
    59 
       
    60 # The entity kind for shell sessions. Feel free to rename to suit your app.
       
    61 _SESSION_KIND = '_Shell_Session'
       
    62 
       
    63 # Types that can't be pickled.
       
    64 UNPICKLABLE_TYPES = (
       
    65   types.ModuleType,
       
    66   types.TypeType,
       
    67   types.ClassType,
       
    68   types.FunctionType,
       
    69   )
       
    70 
       
    71 # Unpicklable statements to seed new sessions with.
       
    72 INITIAL_UNPICKLABLES = [
       
    73   'import logging',
       
    74   'import os',
       
    75   'import sys',
       
    76   'from google.appengine.ext import db',
       
    77   'from google.appengine.api import users',
       
    78   ]
       
    79 
       
    80 
       
    81 class ShellSession(db.Model):
       
    82   """A shell session. Stores the session's globals.
       
    83 
       
    84   Each session globals is stored in one of two places:
       
    85 
       
    86   If the global is picklable, it's stored in the parallel globals and
       
    87   global_names list properties. (They're parallel lists to work around the
       
    88   unfortunate fact that the datastore can't store dictionaries natively.)
       
    89 
       
    90   If the global is not picklable (e.g. modules, classes, and functions), or if
       
    91   it was created by the same statement that created an unpicklable global,
       
    92   it's not stored directly. Instead, the statement is stored in the
       
    93   unpicklables list property. On each request, before executing the current
       
    94   statement, the unpicklable statements are evaluated to recreate the
       
    95   unpicklable globals.
       
    96 
       
    97   The unpicklable_names property stores all of the names of globals that were
       
    98   added by unpicklable statements. When we pickle and store the globals after
       
    99   executing a statement, we skip the ones in unpicklable_names.
       
   100 
       
   101   Using Text instead of string is an optimization. We don't query on any of
       
   102   these properties, so they don't need to be indexed.
       
   103   """
       
   104   global_names = db.ListProperty(db.Text)
       
   105   globals = db.ListProperty(db.Blob)
       
   106   unpicklable_names = db.ListProperty(db.Text)
       
   107   unpicklables = db.ListProperty(db.Text)
       
   108 
       
   109   def set_global(self, name, value):
       
   110     """Adds a global, or updates it if it already exists.
       
   111 
       
   112     Also removes the global from the list of unpicklable names.
       
   113 
       
   114     Args:
       
   115       name: the name of the global to remove
       
   116       value: any picklable value
       
   117     """
       
   118     blob = db.Blob(pickle.dumps(value))
       
   119 
       
   120     if name in self.global_names:
       
   121       index = self.global_names.index(name)
       
   122       self.globals[index] = blob
       
   123     else:
       
   124       self.global_names.append(db.Text(name))
       
   125       self.globals.append(blob)
       
   126 
       
   127     self.remove_unpicklable_name(name)
       
   128 
       
   129   def remove_global(self, name):
       
   130     """Removes a global, if it exists.
       
   131 
       
   132     Args:
       
   133       name: string, the name of the global to remove
       
   134     """
       
   135     if name in self.global_names:
       
   136       index = self.global_names.index(name)
       
   137       del self.global_names[index]
       
   138       del self.globals[index]
       
   139 
       
   140   def globals_dict(self):
       
   141     """Returns a dictionary view of the globals.
       
   142     """
       
   143     return dict((name, pickle.loads(val))
       
   144                 for name, val in zip(self.global_names, self.globals))
       
   145 
       
   146   def add_unpicklable(self, statement, names):
       
   147     """Adds a statement and list of names to the unpicklables.
       
   148 
       
   149     Also removes the names from the globals.
       
   150 
       
   151     Args:
       
   152       statement: string, the statement that created new unpicklable global(s).
       
   153       names: list of strings; the names of the globals created by the statement.
       
   154     """
       
   155     self.unpicklables.append(db.Text(statement))
       
   156 
       
   157     for name in names:
       
   158       self.remove_global(name)
       
   159       if name not in self.unpicklable_names:
       
   160         self.unpicklable_names.append(db.Text(name))
       
   161 
       
   162   def remove_unpicklable_name(self, name):
       
   163     """Removes a name from the list of unpicklable names, if it exists.
       
   164 
       
   165     Args:
       
   166       name: string, the name of the unpicklable global to remove
       
   167     """
       
   168     if name in self.unpicklable_names:
       
   169       self.unpicklable_names.remove(name)
       
   170 
       
   171 
       
   172 class FrontPageHandler(webapp.RequestHandler):
       
   173   """Creates a new session and renders the shell.html template.
       
   174   """
       
   175 
       
   176   def get(self):
       
   177     # set up the session. TODO: garbage collect old shell sessions
       
   178     session_key = self.request.get('session')
       
   179     if session_key:
       
   180       session = ShellSession.get(session_key)
       
   181     else:
       
   182       # create a new session
       
   183       session = ShellSession()
       
   184       session.unpicklables = [db.Text(line) for line in INITIAL_UNPICKLABLES]
       
   185       session_key = session.put()
       
   186 
       
   187     template_file = os.path.join(os.path.dirname(__file__), 'templates',
       
   188                                  'shell.html')
       
   189     session_url = '/?session=%s' % session_key
       
   190     vars = { 'server_software': os.environ['SERVER_SOFTWARE'],
       
   191              'python_version': sys.version,
       
   192              'session': str(session_key),
       
   193              'user': users.get_current_user(),
       
   194              'login_url': users.create_login_url(session_url),
       
   195              'logout_url': users.create_logout_url(session_url),
       
   196              }
       
   197     
       
   198     rendered = loader.render_to_string('shell.html', dictionary=vars)
       
   199     # rendered = webapp.template.render(template_file, vars, debug=_DEBUG)
       
   200     self.response.out.write(rendered)
       
   201 
       
   202 
       
   203 class StatementHandler(webapp.RequestHandler):
       
   204   """Evaluates a python statement in a given session and returns the result.
       
   205   """
       
   206 
       
   207   def get(self):
       
   208     self.response.headers['Content-Type'] = 'text/plain'
       
   209 
       
   210     # extract the statement to be run
       
   211     statement = self.request.get('statement')
       
   212     if not statement:
       
   213       return
       
   214 
       
   215     # the python compiler doesn't like network line endings
       
   216     statement = statement.replace('\r\n', '\n')
       
   217 
       
   218     # add a couple newlines at the end of the statement. this makes
       
   219     # single-line expressions such as 'class Foo: pass' evaluate happily.
       
   220     statement += '\n\n'
       
   221 
       
   222     # log and compile the statement up front
       
   223     try:
       
   224       logging.info('Compiling and evaluating:\n%s' % statement)
       
   225       compiled = compile(statement, '<string>', 'single')
       
   226     except:
       
   227       self.response.out.write(traceback.format_exc())
       
   228       return
       
   229 
       
   230     # create a dedicated module to be used as this statement's __main__
       
   231     statement_module = new.module('__main__')
       
   232 
       
   233     # use this request's __builtin__, since it changes on each request.
       
   234     # this is needed for import statements, among other things.
       
   235     import __builtin__
       
   236     statement_module.__builtins__ = __builtin__
       
   237 
       
   238     # load the session from the datastore
       
   239     session = ShellSession.get(self.request.get('session'))
       
   240 
       
   241     # swap in our custom module for __main__. then unpickle the session
       
   242     # globals, run the statement, and re-pickle the session globals, all
       
   243     # inside it.
       
   244     old_main = sys.modules.get('__main__')
       
   245     try:
       
   246       sys.modules['__main__'] = statement_module
       
   247       statement_module.__name__ = '__main__'
       
   248 
       
   249       # re-evaluate the unpicklables
       
   250       for code in session.unpicklables:
       
   251         exec code in statement_module.__dict__
       
   252 
       
   253       # re-initialize the globals
       
   254       for name, val in session.globals_dict().items():
       
   255         try:
       
   256           statement_module.__dict__[name] = val
       
   257         except:
       
   258           msg = 'Dropping %s since it could not be unpickled.\n' % name
       
   259           self.response.out.write(msg)
       
   260           logging.warning(msg + traceback.format_exc())
       
   261           session.remove_global(name)
       
   262 
       
   263       # run!
       
   264       old_globals = dict(statement_module.__dict__)
       
   265       try:
       
   266         old_stdout = sys.stdout
       
   267         old_stderr = sys.stderr
       
   268         try:
       
   269           sys.stdout = self.response.out
       
   270           sys.stderr = self.response.out
       
   271           exec compiled in statement_module.__dict__
       
   272         finally:
       
   273           sys.stdout = old_stdout
       
   274           sys.stderr = old_stderr
       
   275       except:
       
   276         self.response.out.write(traceback.format_exc())
       
   277         return
       
   278 
       
   279       # extract the new globals that this statement added
       
   280       new_globals = {}
       
   281       for name, val in statement_module.__dict__.items():
       
   282         if name not in old_globals or val != old_globals[name]:
       
   283           new_globals[name] = val
       
   284 
       
   285       if True in [isinstance(val, UNPICKLABLE_TYPES)
       
   286                   for val in new_globals.values()]:
       
   287         # this statement added an unpicklable global. store the statement and
       
   288         # the names of all of the globals it added in the unpicklables.
       
   289         session.add_unpicklable(statement, new_globals.keys())
       
   290         logging.debug('Storing this statement as an unpicklable.')
       
   291 
       
   292       else:
       
   293         # this statement didn't add any unpicklables. pickle and store the
       
   294         # new globals back into the datastore.
       
   295         for name, val in new_globals.items():
       
   296           if not name.startswith('__'):
       
   297             session.set_global(name, val)
       
   298 
       
   299     finally:
       
   300       sys.modules['__main__'] = old_main
       
   301 
       
   302     session.put()
       
   303 
       
   304 
       
   305 def main():
       
   306   """Main program.
       
   307   """
       
   308   
       
   309   application = webapp.WSGIApplication(
       
   310     [('/admin/shell', FrontPageHandler),
       
   311      ('/admin/shell/shell.do', StatementHandler)], debug=_DEBUG)
       
   312   wsgiref.handlers.CGIHandler().run(application)
       
   313 
       
   314 
       
   315 if __name__ == '__main__':
       
   316   main()