diff options
| author | Chris McDonough <chrism@plope.com> | 2011-10-05 08:11:52 -0400 |
|---|---|---|
| committer | Chris McDonough <chrism@plope.com> | 2011-10-05 08:11:52 -0400 |
| commit | 0e0cb766f9b8c49ce53123fe9d13d0184196e9b1 (patch) | |
| tree | 640425e0aec72ed92c2c3e606b9566be26bd7525 | |
| parent | a30a39ae06b26f57c0c393bbfd3e0cc0b540df14 (diff) | |
| download | pyramid-0e0cb766f9b8c49ce53123fe9d13d0184196e9b1.tar.gz pyramid-0e0cb766f9b8c49ce53123fe9d13d0184196e9b1.tar.bz2 pyramid-0e0cb766f9b8c49ce53123fe9d13d0184196e9b1.zip | |
add pserve command and move template code out of glue; add a wsgiref paste.server_runner entry point
| -rw-r--r-- | pyramid/compat.py | 10 | ||||
| -rw-r--r-- | pyramid/scaffolds/__init__.py | 2 | ||||
| -rw-r--r-- | pyramid/scaffolds/copydir.py | 294 | ||||
| -rw-r--r-- | pyramid/scaffolds/template.py | 123 | ||||
| -rw-r--r-- | pyramid/scaffolds/tests.py | 9 | ||||
| -rw-r--r-- | pyramid/scripts/pserve.py | 724 | ||||
| -rw-r--r-- | setup.py | 3 |
7 files changed, 1159 insertions, 6 deletions
diff --git a/pyramid/compat.py b/pyramid/compat.py index e686be27d..552a90b4b 100644 --- a/pyramid/compat.py +++ b/pyramid/compat.py @@ -224,3 +224,13 @@ if PY3: # pragma: no cover from html import escape else: from cgi import escape + +try: + input_ = raw_input +except NameError: + input_ = input + +try: # pragma: no cover + import configparser +except ImportError: + import ConfigParser as configparser diff --git a/pyramid/scaffolds/__init__.py b/pyramid/scaffolds/__init__.py index 8661038bf..93a5db12a 100644 --- a/pyramid/scaffolds/__init__.py +++ b/pyramid/scaffolds/__init__.py @@ -6,7 +6,7 @@ from pyramid.compat import ( native_ ) -from glue.template import Template +from pyramid.scaffolds.template import Template class PyramidTemplate(Template): def pre(self, command, output_dir, vars): diff --git a/pyramid/scaffolds/copydir.py b/pyramid/scaffolds/copydir.py new file mode 100644 index 000000000..5728fce5f --- /dev/null +++ b/pyramid/scaffolds/copydir.py @@ -0,0 +1,294 @@ +# (c) 2005 Ian Bicking and contributors; written for Paste +# (http://pythonpaste.org) Licensed under the MIT license: +# http://www.opensource.org/licenses/mit-license.php + +import os +import sys +import pkg_resources +import cgi +import urllib + +from pyramid.compat import ( + input_, + native_ + ) + +fsenc = sys.getfilesystemencoding() + +class SkipTemplate(Exception): + """ + Raised to indicate that the template should not be copied over. + Raise this exception during the substitution of your template + """ + +def copy_dir(source, dest, vars, verbosity, simulate, indent=0, + sub_vars=True, interactive=False, overwrite=True, + template_renderer=None): + """ + Copies the ``source`` directory to the ``dest`` directory. + + ``vars``: A dictionary of variables to use in any substitutions. + + ``verbosity``: Higher numbers will show more about what is happening. + + ``simulate``: If true, then don't actually *do* anything. + + ``indent``: Indent any messages by this amount. + + ``sub_vars``: If true, variables in ``_tmpl`` files and ``+var+`` + in filenames will be substituted. + + ``overwrite``: If false, then don't every overwrite anything. + + ``interactive``: If you are overwriting a file and interactive is + true, then ask before overwriting. + + ``template_renderer``: This is a function for rendering templates (if you + don't want to use string.Template). It should have the signature + ``template_renderer(content_as_string, vars_as_dict, + filename=filename)``. + """ + # This allows you to use a leading +dot+ in filenames which would + # otherwise be skipped because leading dots make the file hidden: + vars.setdefault('dot', '.') + vars.setdefault('plus', '+') + use_pkg_resources = isinstance(source, tuple) + if use_pkg_resources: + names = sorted(pkg_resources.resource_listdir(source[0], source[1])) + else: + names = sorted(os.listdir(source)) + pad = ' '*(indent*2) + if not os.path.exists(dest): + if verbosity >= 1: + print('%sCreating %s/' % (pad, dest)) + if not simulate: + makedirs(dest, verbosity=verbosity, pad=pad) + elif verbosity >= 2: + print('%sDirectory %s exists' % (pad, dest)) + for name in names: + if use_pkg_resources: + full = '/'.join([source[1], name]) + else: + full = os.path.join(source, name) + reason = should_skip_file(name) + if reason: + if verbosity >= 2: + reason = pad + reason % {'filename': full} + print(reason) + continue + if sub_vars: + dest_full = os.path.join(dest, substitute_filename(name, vars)) + sub_file = False + if dest_full.endswith('_tmpl'): + dest_full = dest_full[:-5] + sub_file = sub_vars + if use_pkg_resources and pkg_resources.resource_isdir(source[0], full): + if verbosity: + print('%sRecursing into %s' % (pad, os.path.basename(full))) + copy_dir((source[0], full), dest_full, vars, verbosity, simulate, + indent=indent+1, + sub_vars=sub_vars, interactive=interactive, + template_renderer=template_renderer) + continue + elif not use_pkg_resources and os.path.isdir(full): + if verbosity: + print('%sRecursing into %s' % (pad, os.path.basename(full))) + copy_dir(full, dest_full, vars, verbosity, simulate, + indent=indent+1, + sub_vars=sub_vars, interactive=interactive, + template_renderer=template_renderer) + continue + elif use_pkg_resources: + content = pkg_resources.resource_string(source[0], full) + else: + f = open(full, 'rb') + content = f.read() + f.close() + if sub_file: + try: + content = substitute_content( + content, vars, filename=full, + template_renderer=template_renderer + ) + except SkipTemplate: + continue + if content is None: + continue + already_exists = os.path.exists(dest_full) + if already_exists: + f = open(dest_full, 'rb') + old_content = f.read() + f.close() + if old_content == content: + if verbosity: + print('%s%s already exists (same content)' % + (pad, dest_full)) + continue + if interactive: + if not query_interactive( + native_(full, fsenc), native_(dest_full, fsenc), + native_(content, fsenc), native_(old_content, fsenc), + simulate=simulate): + continue + elif not overwrite: + continue + if verbosity and use_pkg_resources: + print('%sCopying %s to %s' % (pad, full, dest_full)) + elif verbosity: + print( + '%sCopying %s to %s' % (pad, os.path.basename(full), dest_full)) + if not simulate: + f = open(dest_full, 'wb') + f.write(content) + f.close() + +def should_skip_file(name): + """ + Checks if a file should be skipped based on its name. + + If it should be skipped, returns the reason, otherwise returns + None. + """ + if name.startswith('.'): + return 'Skipping hidden file %(filename)s' + if name.endswith('~') or name.endswith('.bak'): + return 'Skipping backup file %(filename)s' + if name.endswith('.pyc') or name.endswith('.pyo'): + return 'Skipping %s file %(filename)s' % os.path.splitext(name)[1] + if name.endswith('$py.class'): + return 'Skipping $py.class file %(filename)s' + if name in ('CVS', '_darcs'): + return 'Skipping version control directory %(filename)s' + return None + +# Overridden on user's request: +all_answer = None + +def query_interactive(src_fn, dest_fn, src_content, dest_content, + simulate): + global all_answer + from difflib import unified_diff, context_diff + u_diff = list(unified_diff( + dest_content.splitlines(), + src_content.splitlines(), + dest_fn, src_fn)) + c_diff = list(context_diff( + dest_content.splitlines(), + src_content.splitlines(), + dest_fn, src_fn)) + added = len([l for l in u_diff if l.startswith('+') + and not l.startswith('+++')]) + removed = len([l for l in u_diff if l.startswith('-') + and not l.startswith('---')]) + if added > removed: + msg = '; %i lines added' % (added-removed) + elif removed > added: + msg = '; %i lines removed' % (removed-added) + else: + msg = '' + print('Replace %i bytes with %i bytes (%i/%i lines changed%s)' % ( + len(dest_content), len(src_content), + removed, len(dest_content.splitlines()), msg)) + prompt = 'Overwrite %s [y/n/d/B/?] ' % dest_fn + while 1: + if all_answer is None: + response = input_(prompt).strip().lower() + else: + response = all_answer + if not response or response[0] == 'b': + import shutil + new_dest_fn = dest_fn + '.bak' + n = 0 + while os.path.exists(new_dest_fn): + n += 1 + new_dest_fn = dest_fn + '.bak' + str(n) + print('Backing up %s to %s' % (dest_fn, new_dest_fn)) + if not simulate: + shutil.copyfile(dest_fn, new_dest_fn) + return True + elif response.startswith('all '): + rest = response[4:].strip() + if not rest or rest[0] not in ('y', 'n', 'b'): + print(query_usage) + continue + response = all_answer = rest[0] + if response[0] == 'y': + return True + elif response[0] == 'n': + return False + elif response == 'dc': + print('\n'.join(c_diff)) + elif response[0] == 'd': + print('\n'.join(u_diff)) + else: + print(query_usage) + +query_usage = """\ +Responses: + Y(es): Overwrite the file with the new content. + N(o): Do not overwrite the file. + D(iff): Show a unified diff of the proposed changes (dc=context diff) + B(ackup): Save the current file contents to a .bak file + (and overwrite) + Type "all Y/N/B" to use Y/N/B for answer to all future questions +""" + +def makedirs(dir, verbosity, pad): + parent = os.path.dirname(os.path.abspath(dir)) + if not os.path.exists(parent): + makedirs(parent, verbosity, pad) + os.mkdir(dir) + +def substitute_filename(fn, vars): + for var, value in vars.items(): + fn = fn.replace('+%s+' % var, str(value)) + return fn + +def substitute_content(content, vars, filename='<string>', + template_renderer=None): + v = standard_vars.copy() + v.update(vars) + return template_renderer(content, v, filename=filename) + +def html_quote(s): + if s is None: + return '' + return cgi.escape(str(s), 1) + +def url_quote(s): + if s is None: + return '' + return urllib.quote(str(s)) + +def test(conf, true_cond, false_cond=None): + if conf: + return true_cond + else: + return false_cond + +def skip_template(condition=True, *args): + """ + Raise SkipTemplate, which causes copydir to skip the template + being processed. If you pass in a condition, only raise if that + condition is true (allows you to use this with string.Template) + + If you pass any additional arguments, they will be used to + instantiate SkipTemplate (generally use like + ``skip_template(license=='GPL', 'Skipping file; not using GPL')``) + """ + if condition: + raise SkipTemplate(*args) + +standard_vars = { + 'nothing': None, + 'html_quote': html_quote, + 'url_quote': url_quote, + 'empty': '""', + 'test': test, + 'repr': repr, + 'str': str, + 'bool': bool, + 'SkipTemplate': SkipTemplate, + 'skip_template': skip_template, + } + diff --git a/pyramid/scaffolds/template.py b/pyramid/scaffolds/template.py new file mode 100644 index 000000000..f9af8010a --- /dev/null +++ b/pyramid/scaffolds/template.py @@ -0,0 +1,123 @@ +# (c) 2005 Ian Bicking and contributors; written for Paste +# (http://pythonpaste.org) Licensed under the MIT license: +# http://www.opensource.org/licenses/mit-license.php + +import re +import sys +import os + +from pyramid.compat import ( + native_, + bytes_, + ) + +from pyramid.scaffolds import copydir + +fsenc = sys.getfilesystemencoding() + +class Template(object): + + def __init__(self, name): + self.name = name + + def template_renderer(self, content, vars, filename=None): + content = native_(content, fsenc) + try: + return bytes_( + substitute_double_braces(content, TypeMapper(vars)), fsenc) + except Exception as e: + _add_except(e, ' in file %s' % filename) + raise + + def module_dir(self): + """Returns the module directory of this template.""" + mod = sys.modules[self.__class__.__module__] + return os.path.dirname(mod.__file__) + + def template_dir(self): + assert self._template_dir is not None, ( + "Template %r didn't set _template_dir" % self) + if isinstance( self._template_dir, tuple): + return self._template_dir + else: + return os.path.join(self.module_dir(), self._template_dir) + + def run(self, command, output_dir, vars): + self.pre(command, output_dir, vars) + self.write_files(command, output_dir, vars) + self.post(command, output_dir, vars) + + def pre(self, command, output_dir, vars): + """ + Called before template is applied. + """ + pass + + def post(self, command, output_dir, vars): + """ + Called after template is applied. + """ + pass + + def write_files(self, command, output_dir, vars): + template_dir = self.template_dir() + if not os.path.exists(output_dir): + print("Creating directory %s" % output_dir) + if not command.simulate: + # Don't let copydir create this top-level directory, + # since copydir will svn add it sometimes: + os.makedirs(output_dir) + copydir.copy_dir(template_dir, output_dir, + vars, + verbosity=command.verbose, + simulate=command.options.simulate, + interactive=command.interactive, + overwrite=command.options.overwrite, + indent=1, + template_renderer=self.template_renderer) + + +class TypeMapper(dict): + + def __getitem__(self, item): + options = item.split('|') + for op in options[:-1]: + try: + value = eval_with_catch(op, dict(self.items())) + break + except (NameError, KeyError): + pass + else: + value = eval(options[-1], dict(self.items())) + if value is None: + return '' + else: + return str(value) + +def eval_with_catch(expr, vars): + try: + return eval(expr, vars) + except Exception as e: + _add_except(e, 'in expression %r' % expr) + raise + +double_brace_pattern = re.compile(r'{{(?P<braced>.*?)}}') + +def substitute_double_braces(content, values): + def double_bracerepl(match): + value = match.group('braced').strip() + return values[value] + return double_brace_pattern.sub(double_bracerepl, content) + +def _add_except(exc, info): + if not hasattr(exc, 'args') or exc.args is None: + return + args = list(exc.args) + if args: + args[0] += ' ' + info + else: + args = [info] + exc.args = tuple(args) + return + + diff --git a/pyramid/scaffolds/tests.py b/pyramid/scaffolds/tests.py index 7eb838e2f..9b8b975a3 100644 --- a/pyramid/scaffolds/tests.py +++ b/pyramid/scaffolds/tests.py @@ -34,8 +34,7 @@ class TemplateTest(object): [os.path.join(self.directory, 'bin', 'python'), 'setup.py', 'develop']) os.chdir(self.directory) - subprocess.check_call(['bin/paster', 'create', '-t', tmpl_name, - 'Dingle']) + subprocess.check_call(['bin/pcreate', '-s', tmpl_name, 'Dingle']) os.chdir('Dingle') py = os.path.join(self.directory, 'bin', 'python') subprocess.check_call([py, 'setup.py', 'install']) @@ -79,10 +78,10 @@ if __name__ == '__main__': # pragma: no cover raise ValueError(returncode) subprocess.check_call = check_call - templates = ['pyramid_starter', 'pyramid_alchemy', 'pyramid_routesalchemy',] + templates = ['starter', 'alchemy', 'routesalchemy',] - if sys.version_info >= (2, 5): - templates.append('pyramid_zodb') + if sys.version_info >= (2, 5) and sys.version_info < (3, 0): + templates.append('zodb') for name in templates: test = TemplateTest() diff --git a/pyramid/scripts/pserve.py b/pyramid/scripts/pserve.py new file mode 100644 index 000000000..f3414d2cf --- /dev/null +++ b/pyramid/scripts/pserve.py @@ -0,0 +1,724 @@ +# (c) 2005 Ian Bicking and contributors; written for Paste +# (http://pythonpaste.org) Licensed under the MIT license: +# http://www.opensource.org/licenses/mit-license.php @@: This should be moved +# to paste.deploy For discussion of daemonizing: +# http://aspn.activestate.com/ASPN/Cookbook/Python/Recipe/278731 Code taken +# also from QP: http://www.mems-exchange.org/software/qp/ From lib/site.py + +import re +import optparse +import os +import errno +import sys +import time +import subprocess +import threading +import atexit +import logging +from logging.config import fileConfig + +from pyramid.compat import configparser + +from paste.deploy import loadapp, loadserver + +MAXFD = 1024 + +jython = sys.platform.startswith('java') + +def main(argv=sys.argv): + command = ServeCommand(argv) + return command.run() + +class DaemonizeException(Exception): + pass + +class ServeCommand(object): + + min_args = 0 + usage = 'CONFIG_FILE [start|stop|restart|status] [var=value]' + takes_config_file = 1 + summary = "Serve the described application" + description = """\ + This command serves a web application that uses a paste.deploy + configuration file for the server and application. + + If start/stop/restart is given, then --daemon is implied, and it will + start (normal operation), stop (--stop-daemon), or do both. + + You can also include variable assignments like 'http_port=8080' + and then use %(http_port)s in your config files. + """ + verbose = 1 + + # used by subclasses that configure apps and servers differently + requires_config_file = True + + parser = optparse.OptionParser() + parser.add_option( + '-n', '--app-name', + dest='app_name', + metavar='NAME', + help="Load the named application (default main)") + parser.add_option( + '-s', '--server', + dest='server', + metavar='SERVER_TYPE', + help="Use the named server.") + parser.add_option( + '--server-name', + dest='server_name', + metavar='SECTION_NAME', + help="Use the named server as defined in the configuration file (default: main)") + if hasattr(os, 'fork'): + parser.add_option( + '--daemon', + dest="daemon", + action="store_true", + help="Run in daemon (background) mode") + parser.add_option( + '--pid-file', + dest='pid_file', + metavar='FILENAME', + help="Save PID to file (default to pyramid.pid if running in daemon mode)") + parser.add_option( + '--log-file', + dest='log_file', + metavar='LOG_FILE', + help="Save output to the given log file (redirects stdout)") + parser.add_option( + '--reload', + dest='reload', + action='store_true', + help="Use auto-restart file monitor") + parser.add_option( + '--reload-interval', + dest='reload_interval', + default=1, + help="Seconds between checking files (low number can cause significant CPU usage)") + parser.add_option( + '--monitor-restart', + dest='monitor_restart', + action='store_true', + help="Auto-restart server if it dies") + parser.add_option( + '--status', + action='store_true', + dest='show_status', + help="Show the status of the (presumably daemonized) server") + + if hasattr(os, 'setuid'): + # I don't think these are available on Windows + parser.add_option( + '--user', + dest='set_user', + metavar="USERNAME", + help="Set the user (usually only possible when run as root)") + parser.add_option( + '--group', + dest='set_group', + metavar="GROUP", + help="Set the group (usually only possible when run as root)") + + parser.add_option( + '--stop-daemon', + dest='stop_daemon', + action='store_true', + help='Stop a daemonized server (given a PID file, or default pyramid.pid file)') + + if jython: + parser.add_option( + '--disable-jython-reloader', + action='store_true', + dest='disable_jython_reloader', + help="Disable the Jython reloader") + + _scheme_re = re.compile(r'^[a-z][a-z]+:', re.I) + + default_verbosity = 1 + + _reloader_environ_key = 'PYTHON_RELOADER_SHOULD_RUN' + _monitor_environ_key = 'PASTE_MONITOR_SHOULD_RUN' + + possible_subcommands = ('start', 'stop', 'restart', 'status') + + def __init__(self, argv): + self.options, self.args = self.parser.parse_args(argv[1:]) + + def run(self): + if self.options.stop_daemon: + return self.stop_daemon() + + if not hasattr(self.options, 'set_user'): + # Windows case: + self.options.set_user = self.options.set_group = None + # @@: Is this the right stage to set the user at? + self.change_user_group( + self.options.set_user, self.options.set_group) + + if self.requires_config_file: + if not self.args: + raise ValueError('You must give a config file') + app_spec = self.args[0] + if (len(self.args) > 1 + and self.args[1] in self.possible_subcommands): + cmd = self.args[1] + restvars = self.args[2:] + else: + cmd = None + restvars = self.args[1:] + else: + app_spec = "" + if (self.args + and self.args[0] in self.possible_subcommands): + cmd = self.args[0] + restvars = self.args[1:] + else: + cmd = None + restvars = self.args[:] + + jython_monitor = False + if self.options.reload: + if jython and not self.options.disable_jython_reloader: + # JythonMonitor raises the special SystemRestart + # exception that'll cause the Jython interpreter to + # reload in the existing Java process (avoiding + # subprocess startup time) + try: + from paste.reloader import JythonMonitor + except ImportError: + pass + else: + jython_monitor = JythonMonitor(poll_interval=int( + self.options.reload_interval)) + if self.requires_config_file: + jython_monitor.watch_file(self.args[0]) + + if not jython_monitor: + if os.environ.get(self._reloader_environ_key): + from paste import reloader + if self.verbose > 1: + print('Running reloading file monitor') + reloader.install(int(self.options.reload_interval)) + if self.requires_config_file: + reloader.watch_file(self.args[0]) + else: + return self.restart_with_reloader() + + if cmd not in (None, 'start', 'stop', 'restart', 'status'): + raise ValueError( + 'Error: must give start|stop|restart (not %s)' % cmd) + + if cmd == 'status' or self.options.show_status: + return self.show_status() + + if cmd == 'restart' or cmd == 'stop': + result = self.stop_daemon() + if result: + if cmd == 'restart': + print("Could not stop daemon; aborting") + else: + print("Could not stop daemon") + return result + if cmd == 'stop': + return result + self.options.daemon = True + + if cmd == 'start': + self.options.daemon = True + + app_name = self.options.app_name + vars = self.parse_vars(restvars) + if not self._scheme_re.search(app_spec): + app_spec = 'config:' + app_spec + server_name = self.options.server_name + if self.options.server: + server_spec = 'egg:pyramid' + assert server_name is None + server_name = self.options.server + else: + server_spec = app_spec + base = os.getcwd() + + if getattr(self.options, 'daemon', False): + if not self.options.pid_file: + self.options.pid_file = 'pyramid.pid' + if not self.options.log_file: + self.options.log_file = 'pyramid.log' + + # Ensure the log file is writeable + if self.options.log_file: + try: + writeable_log_file = open(self.options.log_file, 'a') + except IOError as ioe: + msg = 'Error: Unable to write to log file: %s' % ioe + raise ValueError(msg) + writeable_log_file.close() + + # Ensure the pid file is writeable + if self.options.pid_file: + try: + writeable_pid_file = open(self.options.pid_file, 'a') + except IOError as ioe: + msg = 'Error: Unable to write to pid file: %s' % ioe + raise ValueError(msg) + writeable_pid_file.close() + + if getattr(self.options, 'daemon', False): + try: + self.daemonize() + except DaemonizeException as ex: + if self.verbose > 0: + print(str(ex)) + return + + if (self.options.monitor_restart + and not os.environ.get(self._monitor_environ_key)): + return self.restart_with_monitor() + + if self.options.pid_file: + self.record_pid(self.options.pid_file) + + if self.options.log_file: + stdout_log = LazyWriter(self.options.log_file, 'a') + sys.stdout = stdout_log + sys.stderr = stdout_log + logging.basicConfig(stream=stdout_log) + + log_fn = app_spec + if log_fn.startswith('config:'): + log_fn = app_spec[len('config:'):] + elif log_fn.startswith('egg:'): + log_fn = None + if log_fn: + log_fn = os.path.join(base, log_fn) + self.logging_file_config(log_fn) + + server = self.loadserver(server_spec, name=server_name, + relative_to=base, global_conf=vars) + app = self.loadapp(app_spec, name=app_name, + relative_to=base, global_conf=vars) + + if self.verbose > 0: + if hasattr(os, 'getpid'): + msg = 'Starting server in PID %i.' % os.getpid() + else: + msg = 'Starting server.' + print(msg) + + def serve(): + try: + server(app) + except (SystemExit, KeyboardInterrupt) as e: + if self.verbose > 1: + raise + if str(e): + msg = ' '+str(e) + else: + msg = '' + print('Exiting%s (-v to see traceback)' % msg) + + if jython_monitor: + # JythonMonitor has to be ran from the main thread + threading.Thread(target=serve).start() + print('Starting Jython file monitor') + jython_monitor.periodic_reload() + else: + serve() + + def loadserver(self, server_spec, name, relative_to, **kw): + return loadserver( + server_spec, name=name, + relative_to=relative_to, **kw) + + def loadapp(self, app_spec, name, relative_to, **kw): + return loadapp( + app_spec, name=name, relative_to=relative_to, + **kw) + + def parse_vars(self, args): + """ + Given variables like ``['a=b', 'c=d']`` turns it into ``{'a': + 'b', 'c': 'd'}`` + """ + result = {} + for arg in args: + if '=' not in arg: + raise ValueError( + 'Variable assignment %r invalid (no "=")' + % arg) + name, value = arg.split('=', 1) + result[name] = value + return result + + def logging_file_config(self, config_file): + """ + Setup logging via the logging module's fileConfig function with the + specified ``config_file``, if applicable. + + ConfigParser defaults are specified for the special ``__file__`` + and ``here`` variables, similar to PasteDeploy config loading. + """ + parser = configparser.ConfigParser() + parser.read([config_file]) + if parser.has_section('loggers'): + config_file = os.path.abspath(config_file) + fileConfig(config_file, dict(__file__=config_file, + here=os.path.dirname(config_file))) + + def quote_first_command_arg(self, arg): + """ + There's a bug in Windows when running an executable that's + located inside a path with a space in it. This method handles + that case, or on non-Windows systems or an executable with no + spaces, it just leaves well enough alone. + """ + if (sys.platform != 'win32' or ' ' not in arg): + # Problem does not apply: + return arg + try: + import win32api + except ImportError: + raise ValueError( + "The executable %r contains a space, and in order to " + "handle this issue you must have the win32api module " + "installed" % arg) + arg = win32api.GetShortPathName(arg) + return arg + + def daemonize(self): + pid = live_pidfile(self.options.pid_file) + if pid: + raise DaemonizeException( + "Daemon is already running (PID: %s from PID file %s)" + % (pid, self.options.pid_file)) + + if self.verbose > 0: + print('Entering daemon mode') + pid = os.fork() + if pid: + # The forked process also has a handle on resources, so we + # *don't* want proper termination of the process, we just + # want to exit quick (which os._exit() does) + os._exit(0) + # Make this the session leader + os.setsid() + # Fork again for good measure! + pid = os.fork() + if pid: + os._exit(0) + + # @@: Should we set the umask and cwd now? + + import resource # Resource usage information. + maxfd = resource.getrlimit(resource.RLIMIT_NOFILE)[1] + if (maxfd == resource.RLIM_INFINITY): + maxfd = MAXFD + # Iterate through and close all file descriptors. + for fd in range(0, maxfd): + try: + os.close(fd) + except OSError: # ERROR, fd wasn't open to begin with (ignored) + pass + + if (hasattr(os, "devnull")): + REDIRECT_TO = os.devnull + else: + REDIRECT_TO = "/dev/null" + os.open(REDIRECT_TO, os.O_RDWR) # standard input (0) + # Duplicate standard input to standard output and standard error. + os.dup2(0, 1) # standard output (1) + os.dup2(0, 2) # standard error (2) + + def record_pid(self, pid_file): + pid = os.getpid() + if self.verbose > 1: + print('Writing PID %s to %s' % (pid, pid_file)) + f = open(pid_file, 'w') + f.write(str(pid)) + f.close() + atexit.register(_remove_pid_file, pid, pid_file, self.verbose) + + def stop_daemon(self): + pid_file = self.options.pid_file or 'pyramid.pid' + if not os.path.exists(pid_file): + print('No PID file exists in %s' % pid_file) + return 1 + pid = read_pidfile(pid_file) + if not pid: + print("Not a valid PID file in %s" % pid_file) + return 1 + pid = live_pidfile(pid_file) + if not pid: + print("PID in %s is not valid (deleting)" % pid_file) + try: + os.unlink(pid_file) + except (OSError, IOError) as e: + print("Could not delete: %s" % e) + return 2 + return 1 + for j in range(10): + if not live_pidfile(pid_file): + break + import signal + os.kill(pid, signal.SIGTERM) + time.sleep(1) + else: + print("failed to kill web process %s" % pid) + return 3 + if os.path.exists(pid_file): + os.unlink(pid_file) + return 0 + + def show_status(self): + pid_file = self.options.pid_file or 'pyramid.pid' + if not os.path.exists(pid_file): + print('No PID file %s' % pid_file) + return 1 + pid = read_pidfile(pid_file) + if not pid: + print('No PID in file %s' % pid_file) + return 1 + pid = live_pidfile(pid_file) + if not pid: + print('PID %s in %s is not running' % (pid, pid_file)) + return 1 + print('Server running in PID %s' % pid) + return 0 + + def restart_with_reloader(self): + self.restart_with_monitor(reloader=True) + + def restart_with_monitor(self, reloader=False): + if self.verbose > 0: + if reloader: + print('Starting subprocess with file monitor') + else: + print('Starting subprocess with monitor parent') + while 1: + args = [self.quote_first_command_arg(sys.executable)] + sys.argv + new_environ = os.environ.copy() + if reloader: + new_environ[self._reloader_environ_key] = 'true' + else: + new_environ[self._monitor_environ_key] = 'true' + proc = None + try: + try: + _turn_sigterm_into_systemexit() + proc = subprocess.Popen(args, env=new_environ) + exit_code = proc.wait() + proc = None + except KeyboardInterrupt: + print('^C caught in monitor process') + if self.verbose > 1: + raise + return 1 + finally: + if (proc is not None + and hasattr(os, 'kill')): + import signal + try: + os.kill(proc.pid, signal.SIGTERM) + except (OSError, IOError): + pass + + if reloader: + # Reloader always exits with code 3; but if we are + # a monitor, any exit code will restart + if exit_code != 3: + return exit_code + if self.verbose > 0: + print('-'*20, 'Restarting', '-'*20) + + def change_user_group(self, user, group): + if not user and not group: + return + import pwd, grp + uid = gid = None + if group: + try: + gid = int(group) + group = grp.getgrgid(gid).gr_name + except ValueError: + import grp + try: + entry = grp.getgrnam(group) + except KeyError: + raise ValueError( + "Bad group: %r; no such group exists" % group) + gid = entry.gr_gid + try: + uid = int(user) + user = pwd.getpwuid(uid).pw_name + except ValueError: + try: + entry = pwd.getpwnam(user) + except KeyError: + raise ValueError( + "Bad username: %r; no such user exists" % user) + if not gid: + gid = entry.pw_gid + uid = entry.pw_uid + if self.verbose > 0: + print('Changing user to %s:%s (%s:%s)' % ( + user, group or '(unknown)', uid, gid)) + if gid: + os.setgid(gid) + if uid: + os.setuid(uid) + +class LazyWriter(object): + + """ + File-like object that opens a file lazily when it is first written + to. + """ + + def __init__(self, filename, mode='w'): + self.filename = filename + self.fileobj = None + self.lock = threading.Lock() + self.mode = mode + + def open(self): + if self.fileobj is None: + self.lock.acquire() + try: + if self.fileobj is None: + self.fileobj = open(self.filename, self.mode) + finally: + self.lock.release() + return self.fileobj + + def write(self, text): + fileobj = self.open() + fileobj.write(text) + fileobj.flush() + + def writelines(self, text): + fileobj = self.open() + fileobj.writelines(text) + fileobj.flush() + + def flush(self): + self.open().flush() + +def live_pidfile(pidfile): + """(pidfile:str) -> int | None + Returns an int found in the named file, if there is one, + and if there is a running process with that process id. + Return None if no such process exists. + """ + pid = read_pidfile(pidfile) + if pid: + try: + os.kill(int(pid), 0) + return pid + except OSError as e: + if e.errno == errno.EPERM: + return pid + return None + +def read_pidfile(filename): + if os.path.exists(filename): + try: + f = open(filename) + content = f.read() + f.close() + return int(content.strip()) + except (ValueError, IOError): + return None + else: + return None + +def _remove_pid_file(written_pid, filename, verbosity): + current_pid = os.getpid() + if written_pid != current_pid: + # A forked process must be exiting, not the process that + # wrote the PID file + return + if not os.path.exists(filename): + return + f = open(filename) + content = f.read().strip() + f.close() + try: + pid_in_file = int(content) + except ValueError: + pass + else: + if pid_in_file != current_pid: + print("PID file %s contains %s, not expected PID %s" % ( + filename, pid_in_file, current_pid)) + return + if verbosity > 0: + print("Removing PID file %s" % filename) + try: + os.unlink(filename) + return + except OSError as e: + # Record, but don't give traceback + print("Cannot remove PID file: %s" % e) + # well, at least lets not leave the invalid PID around... + try: + f = open(filename, 'w') + f.write('') + f.close() + except OSError as e: + print('Stale PID left in file: %s (%e)' % (filename, e)) + else: + print('Stale PID removed') + + +def ensure_port_cleanup(bound_addresses, maxtries=30, sleeptime=2): + """ + This makes sure any open ports are closed. + + Does this by connecting to them until they give connection + refused. Servers should call like:: + + import paste.script + ensure_port_cleanup([80, 443]) + """ + atexit.register(_cleanup_ports, bound_addresses, maxtries=maxtries, + sleeptime=sleeptime) + +def _cleanup_ports(bound_addresses, maxtries=30, sleeptime=2): + # Wait for the server to bind to the port. + import socket + import errno + for bound_address in bound_addresses: + for attempt in range(maxtries): + sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + try: + sock.connect(bound_address) + except socket.error as e: + if e.args[0] != errno.ECONNREFUSED: + raise + break + else: + time.sleep(sleeptime) + else: + raise SystemExit('Timeout waiting for port.') + sock.close() + +def _turn_sigterm_into_systemexit(): + """ + Attempts to turn a SIGTERM exception into a SystemExit exception. + """ + try: + import signal + except ImportError: + return + def handle_term(signo, frame): + raise SystemExit + signal.signal(signal.SIGTERM, handle_term) + +# For paste.deploy server instantiation (egg:pyramid#wsgiref) +def wsgiref_server_runner(wsgi_app, global_conf, **kw): + from wsgiref.simple_server import make_server + host = kw.get('host', '0.0.0.0') + port = int(kw.get('port', 8080)) + server = make_server(host, port, wsgi_app) + print('Starting HTTP server on http://%s:%s' % (host, port)) + server.serve_forever() @@ -97,6 +97,9 @@ setup(name='pyramid', [console_scripts] bfg2pyramid = pyramid.fixers.fix_bfg_imports:main pcreate = pyramid.scripts.pcreate:main + pserve = pyramid.scripts.pserve:main + [paste.server_runner] + wsgiref = pyramid.scripts.pserve:wsgiref_server_runner """ ) |
