refactor of commands to more robust server

This commit is contained in:
Mark McClain 2011-01-07 01:10:35 -05:00
parent a6ede2c096
commit a7a4ce77f1
10 changed files with 914 additions and 400 deletions

View File

@ -1,315 +0,0 @@
"""
PasteScript commands for Pecan.
"""
from configuration import _runtime_conf, set_config
from paste.script import command as paste_command
from paste.script.create_distro import CreateDistroCommand
from webtest import TestApp
from templates import DEFAULT_TEMPLATE
import copy
import optparse
import os
import pkg_resources
import sys
import warnings
class CommandRunner(object):
"""
Dispatches command execution requests.
This is a custom PasteScript command runner that is specific to Pecan
commands. For a command to show up, its name must begin with "pecan-".
It is also recommended that its group name be set to "Pecan" so that it
shows up under that group when using ``paster`` directly.
"""
def __init__(self):
# set up the parser
self.parser = optparse.OptionParser(add_help_option=False,
version='Pecan %s' % self.get_version(),
usage='%prog [options] COMMAND [command_options]')
self.parser.disable_interspersed_args()
self.parser.add_option('-h', '--help',
action='store_true',
dest='show_help',
help='show detailed help message')
# suppress BaseException.message warnings for BadCommand
if sys.version_info < (2, 7):
warnings.filterwarnings(
'ignore',
'BaseException\.message has been deprecated as of Python 2\.6',
DeprecationWarning,
paste_command.__name__.replace('.', '\\.'))
# register Pecan as a system plugin when using the custom runner
paste_command.system_plugins.append('Pecan')
def get_command_template(self, command_names):
if not command_names:
max_length = 10
else:
max_length = max([len(name) for name in command_names])
return ' %%-%ds %%s\n' % max_length
def get_commands(self):
commands = {}
for name, command in paste_command.get_commands().iteritems():
if name.startswith('pecan-'):
commands[name[6:]] = command.load()
return commands
def get_version(self):
try:
dist = pkg_resources.get_distribution('Pecan')
if os.path.dirname(os.path.dirname(__file__)) == dist.location:
return dist.version
else:
return '(development)'
except:
return '(development)'
def print_usage(self, file=sys.stdout):
self.parser.print_help(file=file)
file.write('\n')
command_groups = {}
commands = self.get_commands()
if not commands:
file.write('No commands registered.\n')
return
command_template = self.get_command_template(commands.keys())
for name, command in commands.iteritems():
group_name = command.group_name
if group_name.lower() == 'pecan':
group_name = ''
command_groups.setdefault(group_name, {})[name] = command
command_groups = sorted(command_groups.items())
for i, (group, commands) in enumerate(command_groups):
file.write('%s:\n' % (group or 'Commands'))
for name, command in sorted(commands.items()):
file.write(command_template % (name, command.summary))
if i + 1 < len(command_groups):
file.write('\n')
def print_known_commands(self, file=sys.stderr):
commands = self.get_commands()
command_names = sorted(commands.keys())
if not command_names:
file.write('No commands registered.\n')
return
file.write('Known commands:\n')
command_template = self.get_command_template(command_names)
for name in command_names:
file.write(command_template % (name, commands[name].summary))
def run(self, args):
options, args = self.parser.parse_args(args)
if not args:
self.print_usage()
return 0
command_name = args.pop(0)
commands = self.get_commands()
if command_name not in commands:
sys.stderr.write('Command %s not known\n\n' % command_name)
self.print_known_commands()
return 1
else:
command = commands[command_name](command_name)
if options.show_help:
return command.run(['-h'])
else:
return command.run(args)
@classmethod
def handle_command_line(cls):
try:
runner = CommandRunner()
exit_code = runner.run(sys.argv[1:])
except paste_command.BadCommand, ex:
sys.stderr.write('%s\n' % ex)
exit_code = ex.exit_code
sys.exit(exit_code)
class Command(paste_command.Command):
"""
Base class for Pecan commands.
This provides some standard functionality for interacting with Pecan
applications and handles some of the basic PasteScript command cruft.
See ``paste.script.command.Command`` for more information.
"""
# command information
group_name = 'Pecan'
summary = ''
# command parser
parser = paste_command.Command.standard_parser()
def run(self, args):
try:
return paste_command.Command.run(self, args)
except paste_command.BadCommand, ex:
ex.args[0] = self.parser.error(ex.args[0])
raise
def can_import(self, name):
try:
__import__(name)
return True
except ImportError:
return False
def get_package_names(self, config):
if not hasattr(config.app, 'modules'):
return []
return [module.__name__ for module in config.app.modules if hasattr(module, '__name__')]
def load_configuration(self, name):
set_config(name)
return _runtime_conf
def load_app(self, config):
for package_name in self.get_package_names(config):
module_name = '%s.app' % package_name
if self.can_import(module_name):
module = sys.modules[module_name]
if hasattr(module, 'setup_app'):
return module.setup_app(config)
raise paste_command.BadCommand('No app.setup_app found in any of the configured app.modules')
def load_model(self, config):
for package_name in self.get_package_names(config):
module_name = '%s.model' % package_name
if self.can_import(module_name):
return sys.modules[module_name]
return None
def command(self):
pass
class ServeCommand(Command):
"""
Serve the described application.
"""
# command information
usage = 'CONFIG_NAME'
summary = __doc__.strip().splitlines()[0].rstrip('.')
# command options/arguments
min_args = 1
max_args = 1
def command(self):
# load the application
config = self.load_configuration(self.args[0])
app = self.load_app(config)
from paste import httpserver
try:
httpserver.serve(app, config.server.host, config.server.port)
except KeyboardInterrupt:
print '^C'
class ShellCommand(Command):
"""
Open an interactive shell with the Pecan app loaded.
"""
# command information
usage = 'CONFIG_NAME'
summary = __doc__.strip().splitlines()[0].rstrip('.')
# command options/arguments
min_args = 1
max_args = 1
def command(self):
# load the application
config = self.load_configuration(self.args[0])
setattr(config.app, 'reload', False)
app = self.load_app(config)
# prepare the locals
locs = dict(__name__='pecan-admin')
locs['wsgiapp'] = app
locs['app'] = TestApp(app)
# find the model for the app
model = self.load_model(config)
if model:
locs['model'] = model
# insert the pecan locals
exec('from pecan import abort, conf, redirect, request, response') in locs
# prepare the banner
banner = ' The following objects are available:\n'
banner += ' %-10s - This project\'s WSGI App instance\n' % 'wsgiapp'
banner += ' %-10s - webtest.TestApp wrapped around wsgiapp\n' % 'app'
if model:
model_name = getattr(model, '__module__', getattr(model, '__name__', 'model'))
banner += ' %-10s - Models from %s\n' % ('model', model_name)
# launch the shell, using IPython if available
try:
from IPython.Shell import IPShellEmbed
shell = IPShellEmbed(argv=self.args)
shell.set_banner(shell.IP.BANNER + '\n\n' + banner)
shell(local_ns=locs, global_ns={})
except ImportError:
import code
py_prefix = sys.platform.startswith('java') and 'J' or 'P'
shell_banner = 'Pecan Interactive Shell\n%sython %s\n\n' % \
(py_prefix, sys.version)
shell = code.InteractiveConsole(locals=locs)
try:
import readline
except ImportError:
pass
shell.interact(shell_banner + banner)
class CreateCommand(CreateDistroCommand, Command):
"""
Creates the file layout for a new Pecan distribution.
For a template to show up when using this command, its name must begin
with "pecan-". Although not required, it should also include the "Pecan"
egg plugin for user convenience.
"""
# command information
summary = __doc__.strip().splitlines()[0].rstrip('.')
description = None
def command(self):
if not self.options.list_templates:
if not self.options.templates:
self.options.templates = [DEFAULT_TEMPLATE]
try:
return CreateDistroCommand.command(self)
except LookupError, ex:
sys.stderr.write('%s\n\n' % ex)
CreateDistroCommand.list_templates(self)
return 2
def all_entry_points(self):
entry_points = []
for entry in CreateDistroCommand.all_entry_points(self):
if entry.name.startswith('pecan-'):
entry = copy.copy(entry)
entry_points.append(entry)
entry.name = entry.name[6:]
return entry_points

View File

@ -0,0 +1,8 @@
"""
PasteScript commands for Pecan.
"""
from runner import CommandRunner
from create import CreateCommand
from shell import ShellCommand
from serve import ServeCommand

67
pecan/commands/base.py Normal file
View File

@ -0,0 +1,67 @@
"""
PasteScript base commands for Pecan.
"""
from pecan.configuration import _runtime_conf, set_config
from paste.script import command as paste_command
import sys
class Command(paste_command.Command):
"""
Base class for Pecan commands.
This provides some standard functionality for interacting with Pecan
applications and handles some of the basic PasteScript command cruft.
See ``paste.script.command.Command`` for more information.
"""
# command information
group_name = 'Pecan'
summary = ''
# command parser
parser = paste_command.Command.standard_parser()
def run(self, args):
try:
return paste_command.Command.run(self, args)
except paste_command.BadCommand, ex:
ex.args[0] = self.parser.error(ex.args[0])
raise
def can_import(self, name):
try:
__import__(name)
return True
except ImportError:
return False
def get_package_names(self, config):
if not hasattr(config.app, 'modules'):
return []
return [module.__name__ for module in config.app.modules if hasattr(module, '__name__')]
def load_configuration(self, name):
set_config(name)
return _runtime_conf
def load_app(self, config):
for package_name in self.get_package_names(config):
module_name = '%s.app' % package_name
if self.can_import(module_name):
module = sys.modules[module_name]
if hasattr(module, 'setup_app'):
return module.setup_app(config)
raise paste_command.BadCommand('No app.setup_app found in any of the configured app.modules')
def load_model(self, config):
for package_name in self.get_package_names(config):
module_name = '%s.model' % package_name
if self.can_import(module_name):
return sys.modules[module_name]
return None
def command(self):
pass

44
pecan/commands/create.py Normal file
View File

@ -0,0 +1,44 @@
"""
PasteScript commands for Pecan.
"""
from paste.script.create_distro import CreateDistroCommand
from pecan.templates import DEFAULT_TEMPLATE
from base import Command
import copy
import os
import sys
class CreateCommand(CreateDistroCommand, Command):
"""
Creates the file layout for a new Pecan distribution.
For a template to show up when using this command, its name must begin
with "pecan-". Although not required, it should also include the "Pecan"
egg plugin for user convenience.
"""
# command information
summary = __doc__.strip().splitlines()[0].rstrip('.')
description = None
def command(self):
if not self.options.list_templates:
if not self.options.templates:
self.options.templates = [DEFAULT_TEMPLATE]
try:
return CreateDistroCommand.command(self)
except LookupError, ex:
sys.stderr.write('%s\n\n' % ex)
CreateDistroCommand.list_templates(self)
return 2
def all_entry_points(self):
entry_points = []
for entry in CreateDistroCommand.all_entry_points(self):
if entry.name.startswith('pecan-'):
entry = copy.copy(entry)
entry_points.append(entry)
entry.name = entry.name[6:]
return entry_points

129
pecan/commands/runner.py Normal file
View File

@ -0,0 +1,129 @@
"""
PasteScript Command Runner
"""
from paste.script import command as paste_command
import optparse
import os
import pkg_resources
import sys
import warnings
class CommandRunner(object):
"""
Dispatches command execution requests.
This is a custom PasteScript command runner that is specific to Pecan
commands. For a command to show up, its name must begin with "pecan-".
It is also recommended that its group name be set to "Pecan" so that it
shows up under that group when using ``paster`` directly.
"""
def __init__(self):
# set up the parser
self.parser = optparse.OptionParser(add_help_option=False,
version='Pecan %s' % self.get_version(),
usage='%prog [options] COMMAND [command_options]')
self.parser.disable_interspersed_args()
self.parser.add_option('-h', '--help',
action='store_true',
dest='show_help',
help='show detailed help message')
# suppress BaseException.message warnings for BadCommand
if sys.version_info < (2, 7):
warnings.filterwarnings(
'ignore',
'BaseException\.message has been deprecated as of Python 2\.6',
DeprecationWarning,
paste_command.__name__.replace('.', '\\.'))
# register Pecan as a system plugin when using the custom runner
paste_command.system_plugins.append('Pecan')
def get_command_template(self, command_names):
if not command_names:
max_length = 10
else:
max_length = max([len(name) for name in command_names])
return ' %%-%ds %%s\n' % max_length
def get_commands(self):
commands = {}
for name, command in paste_command.get_commands().iteritems():
if name.startswith('pecan-'):
commands[name[6:]] = command.load()
return commands
def get_version(self):
try:
dist = pkg_resources.get_distribution('Pecan')
if os.path.dirname(os.path.dirname(__file__)) == dist.location:
return dist.version
else:
return '(development)'
except:
return '(development)'
def print_usage(self, file=sys.stdout):
self.parser.print_help(file=file)
file.write('\n')
command_groups = {}
commands = self.get_commands()
if not commands:
file.write('No commands registered.\n')
return
command_template = self.get_command_template(commands.keys())
for name, command in commands.iteritems():
group_name = command.group_name
if group_name.lower() == 'pecan':
group_name = ''
command_groups.setdefault(group_name, {})[name] = command
command_groups = sorted(command_groups.items())
for i, (group, commands) in enumerate(command_groups):
file.write('%s:\n' % (group or 'Commands'))
for name, command in sorted(commands.items()):
file.write(command_template % (name, command.summary))
if i + 1 < len(command_groups):
file.write('\n')
def print_known_commands(self, file=sys.stderr):
commands = self.get_commands()
command_names = sorted(commands.keys())
if not command_names:
file.write('No commands registered.\n')
return
file.write('Known commands:\n')
command_template = self.get_command_template(command_names)
for name in command_names:
file.write(command_template % (name, commands[name].summary))
def run(self, args):
options, args = self.parser.parse_args(args)
if not args:
self.print_usage()
return 0
command_name = args.pop(0)
commands = self.get_commands()
if command_name not in commands:
sys.stderr.write('Command %s not known\n\n' % command_name)
self.print_known_commands()
return 1
else:
command = commands[command_name](command_name)
if options.show_help:
return command.run(['-h'])
else:
return command.run(args)
@classmethod
def handle_command_line(cls):
try:
runner = CommandRunner()
exit_code = runner.run(sys.argv[1:])
except paste_command.BadCommand, ex:
sys.stderr.write('%s\n' % ex)
exit_code = ex.exit_code
sys.exit(exit_code)

594
pecan/commands/serve.py Normal file
View File

@ -0,0 +1,594 @@
# Code heavily borrowed from:
# (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
#
# 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 os
import errno
import sys
import time
from paste import httpserver
try:
import subprocess
except ImportError:
from paste.util import subprocess24 as subprocess
import threading
import atexit
import logging
from base import Command
MAXFD = 1024
jython = sys.platform.startswith('java')
class DaemonizeException(Exception):
pass
class ServeCommand(Command):
min_args = 0
usage = 'CONFIG_FILE [start|stop|restart|status]'
summary = "Serve the pecan web application"
description = """\
This command serves a pecan web application using the provided
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.
"""
# used by subclasses that configure apps and servers differently
requires_config_file = True
parser = Command.standard_parser(quiet=True)
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 paster.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 paster.pid file)')
if jython:
parser.add_option('--disable-jython-reloader',
action='store_true',
dest='disable_jython_reloader',
help="Disable the Jython reloader")
default_verbosity = 1
_reloader_environ_key = 'PYTHON_RELOADER_SHOULD_RUN'
_monitor_environ_key = 'PASTE_MONITOR_SHOULD_RUN'
possible_subcommands = ('start', 'stop', 'restart', 'status')
def command(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 BadCommand('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 BadCommand(
'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
vars = self.parse_vars(restvars)
if getattr(self.options, 'daemon', False):
if not self.options.pid_file:
self.options.pid_file = 'paster.pid'
if not self.options.log_file:
self.options.log_file = 'paster.log'
# Ensure the log file is writeable
if self.options.log_file:
try:
writeable_log_file = open(self.options.log_file, 'a')
except IOError, ioe:
msg = 'Error: Unable to write to log file: %s' % ioe
raise BadCommand(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, ioe:
msg = 'Error: Unable to write to pid file: %s' % ioe
raise BadCommand(msg)
writeable_pid_file.close()
if getattr(self.options, 'daemon', False):
try:
self.daemonize()
except DaemonizeException, 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)
# load the application
config = self.load_configuration(self.args[0])
app = self.load_app(config)
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:
httpserver.serve(app, config.server.host, config.server.port)
except (SystemExit, KeyboardInterrupt), 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 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 'paster.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), 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 'paster.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 BadCommand(
"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 BadCommand(
"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, 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, 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, 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, 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)

69
pecan/commands/shell.py Normal file
View File

@ -0,0 +1,69 @@
"""
PasteScript shell command for Pecan.
"""
import sys
from webtest import TestApp
from base import Command
class ShellCommand(Command):
"""
Open an interactive shell with the Pecan app loaded.
"""
# command information
usage = 'CONFIG_NAME'
summary = __doc__.strip().splitlines()[0].rstrip('.')
# command options/arguments
min_args = 1
max_args = 1
def command(self):
# load the application
config = self.load_configuration(self.args[0])
setattr(config.app, 'reload', False)
app = self.load_app(config)
# prepare the locals
locs = dict(__name__='pecan-admin')
locs['wsgiapp'] = app
locs['app'] = TestApp(app)
# find the model for the app
model = self.load_model(config)
if model:
locs['model'] = model
# insert the pecan locals
exec('from pecan import abort, conf, redirect, request, response') in locs
# prepare the banner
banner = ' The following objects are available:\n'
banner += ' %-10s - This project\'s WSGI App instance\n' % 'wsgiapp'
banner += ' %-10s - The current configuration\n' % 'conf'
banner += ' %-10s - webtest.TestApp wrapped around wsgiapp\n' % 'app'
if model:
model_name = getattr(model, '__module__', getattr(model, '__name__', 'model'))
banner += ' %-10s - Models from %s\n' % ('model', model_name)
# launch the shell, using IPython if available
try:
from IPython.Shell import IPShellEmbed
shell = IPShellEmbed(argv=self.args)
shell.set_banner(shell.IP.BANNER + '\n\n' + banner)
shell(local_ns=locs, global_ns={})
except ImportError:
import code
py_prefix = sys.platform.startswith('java') and 'J' or 'P'
shell_banner = 'Pecan Interactive Shell\n%sython %s\n\n' % \
(py_prefix, sys.version)
shell = code.InteractiveConsole(locals=locs)
try:
import readline
except ImportError:
pass
shell.interact(shell_banner + banner)

View File

@ -1,5 +1,4 @@
from configuration import _runtime_conf
from monitor import MonitorableProcess
from templating import RendererFactory
from routing import lookup_controller
@ -59,7 +58,7 @@ def error_for(field):
return request.validation_error.error_dict.get(field, '')
class Pecan(MonitorableProcess):
class Pecan(object):
def __init__(self, root,
default_renderer = 'mako',
template_path = 'templates',
@ -74,10 +73,6 @@ class Pecan(MonitorableProcess):
self.hooks = hooks
self.template_path = template_path
MonitorableProcess.__init__(self)
if getattr(_runtime_conf, 'app', None) and getattr(_runtime_conf.app, 'reload', False) is True:
self.start_monitoring()
def get_content_type(self, format):
return {
'html' : 'text/html',
@ -223,7 +218,7 @@ class Pecan(MonitorableProcess):
return
raw_namespace = result
# pull the template out based upon content type and handle overrides
template = controller.pecan.get('content_types', {}).get(state.content_type)
template = getattr(request, 'override_template', template)

View File

@ -1,77 +0,0 @@
import sys, os
import subprocess
class MonitorableProcess(object):
_reloader_environ_key = 'PYTHON_RELOADER_SHOULD_RUN'
def start_monitoring(self):
if os.environ.get(self._reloader_environ_key):
from paste import reloader
reloader.install()
else:
return self.restart_with_reloader()
def restart_with_reloader(self):
print 'Starting subprocess with file monitor'
while 1:
args = [self.quote_first_command_arg(sys.executable)] + sys.argv
new_environ = os.environ.copy()
new_environ[self._reloader_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'
raise
finally:
if (proc is not None
and hasattr(os, 'kill')):
import signal
try:
os.kill(proc.pid, signal.SIGTERM)
except (OSError, IOError):
pass
# Reloader always exits with code 3; but if we are
# a monitor, any exit code will restart
if exit_code != 3:
return exit_code
print '-'*20, 'Restarting', '-'*20
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 _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)

View File

@ -56,7 +56,7 @@ setup(
license = 'BSD',
packages = find_packages(exclude=['ez_setup', 'examples', 'tests']),
include_package_data = True,
scripts = ['bin/pecan']
scripts = ['bin/pecan'],
zip_safe = False,
cmdclass = {'test': PyTest},
install_requires = requirements,