| #!/usr/bin/env 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. |
| # |
| """Runs a development application server for an application. |
| |
| %(script)s [options] <application root> |
| |
| Application root must be the path to the application to run in this server. |
| Must contain a valid app.yaml or app.yml file. |
| |
| Options: |
| --help, -h View this helpful message. |
| --debug, -d Use debug logging. (Default false) |
| --clear_datastore, -c Clear the Datastore on startup. (Default false) |
| --address=ADDRESS, -a ADDRESS |
| Address to which this server should bind. (Default |
| %(address)s). |
| --port=PORT, -p PORT Port for the server to run on. (Default %(port)s) |
| --datastore_path=PATH Path to use for storing Datastore file stub data. |
| (Default %(datastore_path)s) |
| --history_path=PATH Path to use for storing Datastore history. |
| (Default %(history_path)s) |
| --require_indexes Disallows queries that require composite indexes |
| not defined in index.yaml. |
| --smtp_host=HOSTNAME SMTP host to send test mail to. Leaving this |
| unset will disable SMTP mail sending. |
| (Default '%(smtp_host)s') |
| --smtp_port=PORT SMTP port to send test mail to. |
| (Default %(smtp_port)s) |
| --smtp_user=USER SMTP user to connect as. Stub will only attempt |
| to login if this field is non-empty. |
| (Default '%(smtp_user)s'). |
| --smtp_password=PASSWORD Password for SMTP server. |
| (Default '%(smtp_password)s') |
| --enable_sendmail Enable sendmail when SMTP not configured. |
| (Default false) |
| --show_mail_body Log the body of emails in mail stub. |
| (Default false) |
| --auth_domain Authorization domain that this app runs in. |
| (Default gmail.com) |
| --debug_imports Enables debug logging for module imports, showing |
| search paths used for finding modules and any |
| errors encountered during the import process. |
| --allow_skipped_files Allow access to files matched by app.yaml's |
| skipped_files (default False) |
| --disable_static_caching Never allow the browser to cache static files. |
| (Default enable if expiration set in app.yaml) |
| """ |
| |
| |
| |
| from google.appengine.tools import os_compat |
| |
| import getopt |
| import logging |
| import os |
| import re |
| import sys |
| import traceback |
| import tempfile |
| |
| |
| def SetGlobals(): |
| """Set various global variables involving the 'google' package. |
| |
| This function should not be called until sys.path has been properly set. |
| """ |
| global yaml_errors, appcfg, appengine_rpc, dev_appserver, os_compat |
| from google.appengine.api import yaml_errors |
| from google.appengine.dist import py_zipimport |
| from google.appengine.tools import appcfg |
| from google.appengine.tools import appengine_rpc |
| from google.appengine.tools import dev_appserver |
| from google.appengine.tools import os_compat |
| |
| |
| |
| DEFAULT_ADMIN_CONSOLE_SERVER = 'appengine.google.com' |
| |
| ARG_ADDRESS = 'address' |
| ARG_ADMIN_CONSOLE_SERVER = 'admin_console_server' |
| ARG_ADMIN_CONSOLE_HOST = 'admin_console_host' |
| ARG_AUTH_DOMAIN = 'auth_domain' |
| ARG_CLEAR_DATASTORE = 'clear_datastore' |
| ARG_DATASTORE_PATH = 'datastore_path' |
| ARG_DEBUG_IMPORTS = 'debug_imports' |
| ARG_ENABLE_SENDMAIL = 'enable_sendmail' |
| ARG_SHOW_MAIL_BODY = 'show_mail_body' |
| ARG_HISTORY_PATH = 'history_path' |
| ARG_LOGIN_URL = 'login_url' |
| ARG_LOG_LEVEL = 'log_level' |
| ARG_PORT = 'port' |
| ARG_REQUIRE_INDEXES = 'require_indexes' |
| ARG_ALLOW_SKIPPED_FILES = 'allow_skipped_files' |
| ARG_SMTP_HOST = 'smtp_host' |
| ARG_SMTP_PASSWORD = 'smtp_password' |
| ARG_SMTP_PORT = 'smtp_port' |
| ARG_SMTP_USER = 'smtp_user' |
| ARG_STATIC_CACHING = 'static_caching' |
| ARG_TEMPLATE_DIR = 'template_dir' |
| ARG_TRUSTED = 'trusted' |
| |
| SDK_PATH = os.path.dirname( |
| os.path.dirname( |
| os.path.dirname( |
| os.path.dirname(os_compat.__file__) |
| ) |
| ) |
| ) |
| |
| DEFAULT_ARGS = { |
| ARG_PORT: 8080, |
| ARG_LOG_LEVEL: logging.INFO, |
| ARG_DATASTORE_PATH: os.path.join(tempfile.gettempdir(), |
| 'dev_appserver.datastore'), |
| ARG_HISTORY_PATH: os.path.join(tempfile.gettempdir(), |
| 'dev_appserver.datastore.history'), |
| ARG_LOGIN_URL: '/_ah/login', |
| ARG_CLEAR_DATASTORE: False, |
| ARG_REQUIRE_INDEXES: False, |
| ARG_TEMPLATE_DIR: os.path.join(SDK_PATH, 'templates'), |
| ARG_SMTP_HOST: '', |
| ARG_SMTP_PORT: 25, |
| ARG_SMTP_USER: '', |
| ARG_SMTP_PASSWORD: '', |
| ARG_ENABLE_SENDMAIL: False, |
| ARG_SHOW_MAIL_BODY: False, |
| ARG_AUTH_DOMAIN: 'gmail.com', |
| ARG_ADDRESS: 'localhost', |
| ARG_ADMIN_CONSOLE_SERVER: DEFAULT_ADMIN_CONSOLE_SERVER, |
| ARG_ADMIN_CONSOLE_HOST: None, |
| ARG_ALLOW_SKIPPED_FILES: False, |
| ARG_STATIC_CACHING: True, |
| ARG_TRUSTED: False, |
| } |
| |
| API_PATHS = {'1': |
| {'google': (), |
| 'antlr3': ('lib', 'antlr3'), |
| 'django': ('lib', 'django'), |
| 'webob': ('lib', 'webob'), |
| 'yaml': ('lib', 'yaml', 'lib'), |
| } |
| } |
| |
| DEFAULT_API_VERSION = '1' |
| |
| API_PATHS['test'] = API_PATHS[DEFAULT_API_VERSION].copy() |
| API_PATHS['test']['_test'] = ('nonexistent', 'test', 'path') |
| |
| |
| def SetPaths(app_config_path): |
| """Set the interpreter to use the specified API version. |
| |
| The app.yaml file is scanned for the api_version field and the value is |
| extracted. With that information, the paths in API_PATHS are added to the |
| front of sys.paths to make sure that they take precedent over any other paths |
| to older versions of a package. All modules for each package set are cleared |
| out of sys.modules to make sure only the newest version is used. |
| |
| Args: |
| - app_config_path: Path to the app.yaml file. |
| """ |
| api_version_re = re.compile(r'api_version:\s*(?P<api_version>[\w.]{1,32})') |
| api_version = None |
| app_config_file = open(app_config_path, 'r') |
| try: |
| for line in app_config_file: |
| re_match = api_version_re.match(line) |
| if re_match: |
| api_version = re_match.group('api_version') |
| break |
| finally: |
| app_config_file.close() |
| |
| if api_version is None: |
| logging.error("Application configuration file missing an 'api_version' " |
| "value:\n%s" % app_config_path) |
| sys.exit(1) |
| if api_version not in API_PATHS: |
| logging.error("Value of %r for 'api_version' from the application " |
| "configuration file is not valid:\n%s" % |
| (api_version, app_config_path)) |
| sys.exit(1) |
| |
| if api_version == DEFAULT_API_VERSION: |
| return DEFAULT_API_VERSION |
| |
| sdk_path = os.path.dirname( |
| os.path.dirname( |
| os.path.dirname( |
| os.path.dirname(os_compat.__file__) |
| ) |
| ) |
| ) |
| for pkg_name, path_parts in API_PATHS[api_version].iteritems(): |
| for name in sys.modules.keys(): |
| if name == pkg_name or name.startswith('%s.' % pkg_name): |
| del sys.modules[name] |
| pkg_path = os.path.join(sdk_path, *path_parts) |
| sys.path.insert(0, pkg_path) |
| |
| return api_version |
| |
| |
| def PrintUsageExit(code): |
| """Prints usage information and exits with a status code. |
| |
| Args: |
| code: Status code to pass to sys.exit() after displaying usage information. |
| """ |
| render_dict = DEFAULT_ARGS.copy() |
| render_dict['script'] = os.path.basename(sys.argv[0]) |
| print sys.modules['__main__'].__doc__ % render_dict |
| sys.stdout.flush() |
| sys.exit(code) |
| |
| |
| def ParseArguments(argv): |
| """Parses command-line arguments. |
| |
| Args: |
| argv: Command-line arguments, including the executable name, used to |
| execute this application. |
| |
| Returns: |
| Tuple (args, option_dict) where: |
| args: List of command-line arguments following the executable name. |
| option_dict: Dictionary of parsed flags that maps keys from DEFAULT_ARGS |
| to their values, which are either pulled from the defaults, or from |
| command-line flags. |
| """ |
| option_dict = DEFAULT_ARGS.copy() |
| |
| try: |
| opts, args = getopt.gnu_getopt( |
| argv[1:], |
| 'a:cdhp:', |
| [ 'address=', |
| 'admin_console_server=', |
| 'admin_console_host=', |
| 'allow_skipped_files', |
| 'auth_domain=', |
| 'clear_datastore', |
| 'datastore_path=', |
| 'debug', |
| 'debug_imports', |
| 'enable_sendmail', |
| 'disable_static_caching', |
| 'show_mail_body', |
| 'help', |
| 'history_path=', |
| 'port=', |
| 'require_indexes', |
| 'smtp_host=', |
| 'smtp_password=', |
| 'smtp_port=', |
| 'smtp_user=', |
| 'template_dir=', |
| 'trusted', |
| ]) |
| except getopt.GetoptError, e: |
| print >>sys.stderr, 'Error: %s' % e |
| PrintUsageExit(1) |
| |
| for option, value in opts: |
| if option in ('-h', '--help'): |
| PrintUsageExit(0) |
| |
| if option in ('-d', '--debug'): |
| option_dict[ARG_LOG_LEVEL] = logging.DEBUG |
| |
| if option in ('-p', '--port'): |
| try: |
| option_dict[ARG_PORT] = int(value) |
| if not (65535 > option_dict[ARG_PORT] > 0): |
| raise ValueError |
| except ValueError: |
| print >>sys.stderr, 'Invalid value supplied for port' |
| PrintUsageExit(1) |
| |
| if option in ('-a', '--address'): |
| option_dict[ARG_ADDRESS] = value |
| |
| if option == '--datastore_path': |
| option_dict[ARG_DATASTORE_PATH] = os.path.abspath(value) |
| |
| if option == '--history_path': |
| option_dict[ARG_HISTORY_PATH] = os.path.abspath(value) |
| |
| if option in ('-c', '--clear_datastore'): |
| option_dict[ARG_CLEAR_DATASTORE] = True |
| |
| if option == '--require_indexes': |
| option_dict[ARG_REQUIRE_INDEXES] = True |
| |
| if option == '--smtp_host': |
| option_dict[ARG_SMTP_HOST] = value |
| |
| if option == '--smtp_port': |
| try: |
| option_dict[ARG_SMTP_PORT] = int(value) |
| if not (65535 > option_dict[ARG_SMTP_PORT] > 0): |
| raise ValueError |
| except ValueError: |
| print >>sys.stderr, 'Invalid value supplied for SMTP port' |
| PrintUsageExit(1) |
| |
| if option == '--smtp_user': |
| option_dict[ARG_SMTP_USER] = value |
| |
| if option == '--smtp_password': |
| option_dict[ARG_SMTP_PASSWORD] = value |
| |
| if option == '--enable_sendmail': |
| option_dict[ARG_ENABLE_SENDMAIL] = True |
| |
| if option == '--show_mail_body': |
| option_dict[ARG_SHOW_MAIL_BODY] = True |
| |
| if option == '--auth_domain': |
| option_dict['_DEFAULT_ENV_AUTH_DOMAIN'] = value |
| |
| if option == '--debug_imports': |
| option_dict['_ENABLE_LOGGING'] = True |
| |
| if option == '--template_dir': |
| option_dict[ARG_TEMPLATE_DIR] = value |
| |
| if option == '--admin_console_server': |
| option_dict[ARG_ADMIN_CONSOLE_SERVER] = value.strip() |
| |
| if option == '--admin_console_host': |
| option_dict[ARG_ADMIN_CONSOLE_HOST] = value |
| |
| if option == '--allow_skipped_files': |
| option_dict[ARG_ALLOW_SKIPPED_FILES] = True |
| |
| if option == '--disable_static_caching': |
| option_dict[ARG_STATIC_CACHING] = False |
| |
| if option == '--trusted': |
| option_dict[ARG_TRUSTED] = True |
| |
| return args, option_dict |
| |
| |
| def MakeRpcServer(option_dict): |
| """Create a new HttpRpcServer. |
| |
| Creates a new HttpRpcServer to check for updates to the SDK. |
| |
| Args: |
| option_dict: The dict of command line options. |
| |
| Returns: |
| A HttpRpcServer. |
| """ |
| server = appengine_rpc.HttpRpcServer( |
| option_dict[ARG_ADMIN_CONSOLE_SERVER], |
| lambda: ('unused_email', 'unused_password'), |
| appcfg.GetUserAgent(), |
| appcfg.GetSourceName(), |
| host_override=option_dict[ARG_ADMIN_CONSOLE_HOST]) |
| server.authenticated = True |
| return server |
| |
| |
| def main(argv): |
| """Runs the development application server.""" |
| args, option_dict = ParseArguments(argv) |
| |
| if len(args) != 1: |
| print >>sys.stderr, 'Invalid arguments' |
| PrintUsageExit(1) |
| |
| root_path = args[0] |
| for suffix in ('yaml', 'yml'): |
| path = os.path.join(root_path, 'app.%s' % suffix) |
| if os.path.exists(path): |
| api_version = SetPaths(path) |
| break |
| else: |
| logging.error("Application configuration file not found in %s" % root_path) |
| return 1 |
| |
| SetGlobals() |
| dev_appserver.API_VERSION = api_version |
| |
| if '_DEFAULT_ENV_AUTH_DOMAIN' in option_dict: |
| auth_domain = option_dict['_DEFAULT_ENV_AUTH_DOMAIN'] |
| dev_appserver.DEFAULT_ENV['AUTH_DOMAIN'] = auth_domain |
| if '_ENABLE_LOGGING' in option_dict: |
| enable_logging = option_dict['_ENABLE_LOGGING'] |
| dev_appserver.HardenedModulesHook.ENABLE_LOGGING = enable_logging |
| |
| log_level = option_dict[ARG_LOG_LEVEL] |
| port = option_dict[ARG_PORT] |
| datastore_path = option_dict[ARG_DATASTORE_PATH] |
| login_url = option_dict[ARG_LOGIN_URL] |
| template_dir = option_dict[ARG_TEMPLATE_DIR] |
| serve_address = option_dict[ARG_ADDRESS] |
| require_indexes = option_dict[ARG_REQUIRE_INDEXES] |
| allow_skipped_files = option_dict[ARG_ALLOW_SKIPPED_FILES] |
| static_caching = option_dict[ARG_STATIC_CACHING] |
| |
| logging.basicConfig( |
| level=log_level, |
| format='%(levelname)-8s %(asctime)s %(filename)s:%(lineno)s] %(message)s') |
| |
| config = None |
| try: |
| config, matcher = dev_appserver.LoadAppConfig(root_path, {}) |
| except yaml_errors.EventListenerError, e: |
| logging.error('Fatal error when loading application configuration:\n' + |
| str(e)) |
| return 1 |
| except dev_appserver.InvalidAppConfigError, e: |
| logging.error('Application configuration file invalid:\n%s', e) |
| return 1 |
| |
| if option_dict[ARG_ADMIN_CONSOLE_SERVER] != '': |
| server = MakeRpcServer(option_dict) |
| update_check = appcfg.UpdateCheck(server, config) |
| update_check.CheckSupportedVersion() |
| if update_check.AllowedToCheckForUpdates(): |
| update_check.CheckForUpdates() |
| |
| try: |
| dev_appserver.SetupStubs(config.application, **option_dict) |
| except: |
| exc_type, exc_value, exc_traceback = sys.exc_info() |
| logging.error(str(exc_type) + ': ' + str(exc_value)) |
| logging.debug(''.join(traceback.format_exception( |
| exc_type, exc_value, exc_traceback))) |
| return 1 |
| |
| http_server = dev_appserver.CreateServer( |
| root_path, |
| login_url, |
| port, |
| template_dir, |
| sdk_dir=SDK_PATH, |
| serve_address=serve_address, |
| require_indexes=require_indexes, |
| allow_skipped_files=allow_skipped_files, |
| static_caching=static_caching) |
| |
| logging.info('Running application %s on port %d: http://%s:%d', |
| config.application, port, serve_address, port) |
| try: |
| try: |
| http_server.serve_forever() |
| except KeyboardInterrupt: |
| logging.info('Server interrupted by user, terminating') |
| except: |
| exc_info = sys.exc_info() |
| info_string = '\n'.join(traceback.format_exception(*exc_info)) |
| logging.error('Error encountered:\n%s\nNow terminating.', info_string) |
| return 1 |
| finally: |
| http_server.server_close() |
| |
| return 0 |
| |
| |
| if __name__ == '__main__': |
| sys.exit(main(sys.argv)) |
| else: |
| SetGlobals() |