diff options
Diffstat (limited to 'scripts/app_engine_server/gae_shell')
-rw-r--r-- | scripts/app_engine_server/gae_shell/README | 17 | ||||
-rw-r--r-- | scripts/app_engine_server/gae_shell/__init__.py | 0 | ||||
-rw-r--r-- | scripts/app_engine_server/gae_shell/__init__.pyc | bin | 0 -> 216 bytes | |||
-rwxr-xr-x | scripts/app_engine_server/gae_shell/shell.py | 308 | ||||
-rwxr-xr-x | scripts/app_engine_server/gae_shell/shell.py~ | 308 | ||||
-rw-r--r-- | scripts/app_engine_server/gae_shell/static/shell.js | 195 | ||||
-rw-r--r-- | scripts/app_engine_server/gae_shell/static/spinner.gif | bin | 0 -> 1514 bytes | |||
-rw-r--r-- | scripts/app_engine_server/gae_shell/templates/shell.html | 122 |
8 files changed, 950 insertions, 0 deletions
diff --git a/scripts/app_engine_server/gae_shell/README b/scripts/app_engine_server/gae_shell/README new file mode 100644 index 0000000..5b0089f --- /dev/null +++ b/scripts/app_engine_server/gae_shell/README @@ -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. diff --git a/scripts/app_engine_server/gae_shell/__init__.py b/scripts/app_engine_server/gae_shell/__init__.py new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/scripts/app_engine_server/gae_shell/__init__.py diff --git a/scripts/app_engine_server/gae_shell/__init__.pyc b/scripts/app_engine_server/gae_shell/__init__.pyc Binary files differnew file mode 100644 index 0000000..84951e9 --- /dev/null +++ b/scripts/app_engine_server/gae_shell/__init__.pyc diff --git a/scripts/app_engine_server/gae_shell/shell.py b/scripts/app_engine_server/gae_shell/shell.py new file mode 100755 index 0000000..df2fb17 --- /dev/null +++ b/scripts/app_engine_server/gae_shell/shell.py @@ -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() diff --git a/scripts/app_engine_server/gae_shell/shell.py~ b/scripts/app_engine_server/gae_shell/shell.py~ new file mode 100755 index 0000000..dee9fdb --- /dev/null +++ b/scripts/app_engine_server/gae_shell/shell.py~ @@ -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() diff --git a/scripts/app_engine_server/gae_shell/static/shell.js b/scripts/app_engine_server/gae_shell/static/shell.js new file mode 100644 index 0000000..4aa1583 --- /dev/null +++ b/scripts/app_engine_server/gae_shell/static/shell.js @@ -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; +}; diff --git a/scripts/app_engine_server/gae_shell/static/spinner.gif b/scripts/app_engine_server/gae_shell/static/spinner.gif Binary files differnew file mode 100644 index 0000000..3e58d6e --- /dev/null +++ b/scripts/app_engine_server/gae_shell/static/spinner.gif diff --git a/scripts/app_engine_server/gae_shell/templates/shell.html b/scripts/app_engine_server/gae_shell/templates/shell.html new file mode 100644 index 0000000..123b200 --- /dev/null +++ b/scripts/app_engine_server/gae_shell/templates/shell.html @@ -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> + |