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