summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorChris McDonough <chrism@plope.com>2011-10-05 08:11:52 -0400
committerChris McDonough <chrism@plope.com>2011-10-05 08:11:52 -0400
commit0e0cb766f9b8c49ce53123fe9d13d0184196e9b1 (patch)
tree640425e0aec72ed92c2c3e606b9566be26bd7525
parenta30a39ae06b26f57c0c393bbfd3e0cc0b540df14 (diff)
downloadpyramid-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.py10
-rw-r--r--pyramid/scaffolds/__init__.py2
-rw-r--r--pyramid/scaffolds/copydir.py294
-rw-r--r--pyramid/scaffolds/template.py123
-rw-r--r--pyramid/scaffolds/tests.py9
-rw-r--r--pyramid/scripts/pserve.py724
-rw-r--r--setup.py3
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()
diff --git a/setup.py b/setup.py
index d029c1a61..1602f9aa7 100644
--- a/setup.py
+++ b/setup.py
@@ -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
"""
)