summaryrefslogtreecommitdiff
path: root/pyramid/scripts/pserve.py
diff options
context:
space:
mode:
Diffstat (limited to 'pyramid/scripts/pserve.py')
-rw-r--r--pyramid/scripts/pserve.py224
1 files changed, 187 insertions, 37 deletions
diff --git a/pyramid/scripts/pserve.py b/pyramid/scripts/pserve.py
index ea125a0dd..431afe6f4 100644
--- a/pyramid/scripts/pserve.py
+++ b/pyramid/scripts/pserve.py
@@ -14,18 +14,22 @@ import errno
import logging
import optparse
import os
+import py_compile
import re
import subprocess
import sys
+import tempfile
import textwrap
import threading
import time
import traceback
+import webbrowser
from paste.deploy import loadserver
from paste.deploy import loadapp
+from paste.deploy.loadwsgi import loadcontext, SERVER
-from pyramid.compat import PY3
+from pyramid.compat import PY2
from pyramid.compat import WIN
from pyramid.paster import setup_logging
@@ -34,6 +38,11 @@ from pyramid.scripts.common import parse_vars
MAXFD = 1024
+try:
+ import termios
+except ImportError: # pragma: no cover
+ termios = None
+
if WIN and not hasattr(os, 'kill'): # pragma: no cover
# py 2.6 on windows
def kill(pid, sig=None):
@@ -62,6 +71,7 @@ class PServeCommand(object):
If start/stop/restart is given, then --daemon is implied, and it will
start (normal operation), stop (--stop-daemon), or do both.
+ Note: Daemonization features are deprecated.
You can also include variable assignments like 'http_port=8080'
and then use %(http_port)s in your config files.
@@ -93,18 +103,18 @@ class PServeCommand(object):
'--daemon',
dest="daemon",
action="store_true",
- help="Run in daemon (background) mode")
+ help="Run in daemon (background) mode [DEPRECATED]")
parser.add_option(
'--pid-file',
dest='pid_file',
metavar='FILENAME',
help=("Save PID to file (default to pyramid.pid if running in "
- "daemon mode)"))
+ "daemon mode) [DEPRECATED]"))
parser.add_option(
'--log-file',
dest='log_file',
metavar='LOG_FILE',
- help="Save output to the given log file (redirects stdout)")
+ help="Save output to the given log file (redirects stdout) [DEPRECATED]")
parser.add_option(
'--reload',
dest='reload',
@@ -120,18 +130,24 @@ class PServeCommand(object):
'--monitor-restart',
dest='monitor_restart',
action='store_true',
- help="Auto-restart server if it dies")
+ help="Auto-restart server if it dies [DEPRECATED]")
+ parser.add_option(
+ '-b', '--browser',
+ dest='browser',
+ action='store_true',
+ help="Open a web browser to server url")
parser.add_option(
'--status',
action='store_true',
dest='show_status',
- help="Show the status of the (presumably daemonized) server")
+ help=("Show the status of the (presumably daemonized) server "
+ "[DEPRECATED]"))
parser.add_option(
'-v', '--verbose',
default=default_verbosity,
dest='verbose',
action='count',
- help="Set verbose level (default "+str(default_verbosity)+")")
+ help="Set verbose level (default " + str(default_verbosity) + ")")
parser.add_option(
'-q', '--quiet',
action='store_const',
@@ -157,7 +173,7 @@ class PServeCommand(object):
dest='stop_daemon',
action='store_true',
help=('Stop a daemonized server (given a PID file, or default '
- 'pyramid.pid file)'))
+ 'pyramid.pid file) [DEPRECATED]'))
_scheme_re = re.compile(r'^[a-z][a-z]+:', re.I)
@@ -176,16 +192,19 @@ class PServeCommand(object):
print(msg)
def get_options(self):
- if (len(self.args) > 1
- and self.args[1] in self.possible_subcommands):
+ if (
+ len(self.args) > 1 and
+ self.args[1] in self.possible_subcommands
+ ):
restvars = self.args[2:]
else:
restvars = self.args[1:]
return parse_vars(restvars)
- def run(self): # pragma: no cover
+ def run(self): # pragma: no cover
if self.options.stop_daemon:
+ self._warn_daemon_deprecated()
return self.stop_daemon()
if not hasattr(self.options, 'set_user'):
@@ -193,21 +212,31 @@ class PServeCommand(object):
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.options.set_user or self.options.set_group:
+ self.change_user_group(
+ self.options.set_user, self.options.set_group)
if not self.args:
self.out('You must give a config file')
return 2
app_spec = self.args[0]
- if (len(self.args) > 1
- and self.args[1] in self.possible_subcommands):
+ if (
+ len(self.args) > 1 and
+ self.args[1] in self.possible_subcommands
+ ):
cmd = self.args[1]
else:
cmd = None
if self.options.reload:
+ if (
+ getattr(self.options, 'daemon', False) or
+ cmd in ('start', 'stop', 'restart')
+ ):
+ self.out(
+ 'Error: Cannot use reloading while running as a dameon.')
+ return 2
if os.environ.get(self._reloader_environ_key):
if self.options.verbose > 1:
self.out('Running reloading file monitor')
@@ -223,9 +252,11 @@ class PServeCommand(object):
return 2
if cmd == 'status' or self.options.show_status:
+ self._warn_daemon_deprecated()
return self.show_status()
- if cmd == 'restart' or cmd == 'stop':
+ if cmd in ('restart', 'stop'):
+ self._warn_daemon_deprecated()
result = self.stop_daemon()
if result:
if cmd == 'restart':
@@ -255,6 +286,10 @@ class PServeCommand(object):
server_spec = app_spec
base = os.getcwd()
+ # warn before setting a default
+ if self.options.pid_file or self.options.log_file:
+ self._warn_daemon_deprecated()
+
if getattr(self.options, 'daemon', False):
if not self.options.pid_file:
self.options.pid_file = 'pyramid.pid'
@@ -279,7 +314,22 @@ class PServeCommand(object):
raise ValueError(msg)
writeable_pid_file.close()
- if getattr(self.options, 'daemon', False):
+ # warn before forking
+ if (
+ self.options.monitor_restart and
+ not os.environ.get(self._monitor_environ_key)
+ ):
+ self.out('''\
+--monitor-restart has been deprecated in Pyramid 1.6. It will be removed
+in a future release per Pyramid's deprecation policy. Please consider using
+a real process manager for your processes like Systemd, Circus, or Supervisor.
+''')
+
+ if (
+ getattr(self.options, 'daemon', False) and
+ not os.environ.get(self._monitor_environ_key)
+ ):
+ self._warn_daemon_deprecated()
try:
self.daemonize()
except DaemonizeException as ex:
@@ -287,13 +337,18 @@ class PServeCommand(object):
self.out(str(ex))
return 2
- if (self.options.monitor_restart
- and not os.environ.get(self._monitor_environ_key)):
- return self.restart_with_monitor()
-
- if self.options.pid_file:
+ if (
+ not os.environ.get(self._monitor_environ_key) and
+ self.options.pid_file
+ ):
self.record_pid(self.options.pid_file)
+ if (
+ self.options.monitor_restart and
+ not os.environ.get(self._monitor_environ_key)
+ ):
+ return self.restart_with_monitor()
+
if self.options.log_file:
stdout_log = LazyWriter(self.options.log_file, 'a')
sys.stdout = stdout_log
@@ -334,6 +389,17 @@ class PServeCommand(object):
msg = ''
self.out('Exiting%s (-v to see traceback)' % msg)
+ if self.options.browser:
+ def open_browser():
+ context = loadcontext(SERVER, app_spec, name=server_name, relative_to=base,
+ global_conf=vars)
+ url = 'http://127.0.0.1:{port}/'.format(**context.config())
+ time.sleep(1)
+ webbrowser.open(url)
+ t = threading.Thread(target=open_browser)
+ t.setDaemon(True)
+ t.start()
+
serve()
def loadapp(self, app_spec, name, relative_to, **kw): # pragma: no cover
@@ -363,6 +429,19 @@ class PServeCommand(object):
arg = win32api.GetShortPathName(arg)
return arg
+ def find_script_path(self, name): # pragma: no cover
+ """
+ Return the path to the script being invoked by the python interpreter.
+
+ There's an issue on Windows when running the executable from
+ a console_script causing the script name (sys.argv[0]) to
+ not end with .exe or .py and thus cannot be run via popen.
+ """
+ if sys.platform == 'win32':
+ if not name.endswith('.exe') and not name.endswith('.py'):
+ name += '.exe'
+ return name
+
def daemonize(self): # pragma: no cover
pid = live_pidfile(self.options.pid_file)
if pid:
@@ -508,7 +587,10 @@ class PServeCommand(object):
else:
self.out('Starting subprocess with monitor parent')
while 1:
- args = [self.quote_first_command_arg(sys.executable)] + sys.argv
+ args = [
+ self.quote_first_command_arg(sys.executable),
+ self.find_script_path(sys.argv[0]),
+ ] + sys.argv[1:]
new_environ = os.environ.copy()
if reloader:
new_environ[self._reloader_environ_key] = 'true'
@@ -543,9 +625,16 @@ class PServeCommand(object):
self.out('%s %s %s' % ('-' * 20, 'Restarting', '-' * 20))
def change_user_group(self, user, group): # pragma: no cover
- if not user and not group:
- return
- import pwd, grp
+ import pwd
+ import grp
+
+ self.out('''\
+The --user and --group options have been deprecated in Pyramid 1.6. They will
+be removed in a future release per Pyramid's deprecation policy. Please
+consider using a real process manager for your processes like Systemd, Circus,
+or Supervisor, all of which support process security.
+''')
+
uid = gid = None
if group:
try:
@@ -579,6 +668,16 @@ class PServeCommand(object):
if uid:
os.setuid(uid)
+ def _warn_daemon_deprecated(self):
+ self.out('''\
+The daemon options have been deprecated in Pyramid 1.6. They will be removed
+in a future release per Pyramid's deprecation policy. Please consider using
+a real process manager for your processes like Systemd, Circus, or Supervisor.
+
+The following commands are deprecated:
+ [start,stop,restart,status] --daemon, --stop-server, --status, --pid-file, --log-file
+''')
+
class LazyWriter(object):
"""
@@ -691,15 +790,23 @@ def _turn_sigterm_into_systemexit(): # pragma: no cover
raise SystemExit
signal.signal(signal.SIGTERM, handle_term)
+def ensure_echo_on(): # pragma: no cover
+ if termios:
+ fd = sys.stdin
+ if fd.isatty():
+ attr_list = termios.tcgetattr(fd)
+ if not attr_list[3] & termios.ECHO:
+ attr_list[3] |= termios.ECHO
+ termios.tcsetattr(fd, termios.TCSANOW, attr_list)
+
def install_reloader(poll_interval=1, extra_files=None): # pragma: no cover
"""
Install the reloading monitor.
On some platforms server threads may not terminate when the main
- thread does, causing ports to remain open/locked. The
- ``raise_keyboard_interrupt`` option creates a unignorable signal
- which causes the whole application to shut-down (rudely).
+ thread does, causing ports to remain open/locked.
"""
+ ensure_echo_on()
mon = Monitor(poll_interval=poll_interval)
if extra_files is None:
extra_files = []
@@ -731,11 +838,12 @@ class _methodwrapper(object):
self.type = type
def __call__(self, *args, **kw):
- assert not 'self' in kw and not 'cls' in kw, (
+ assert 'self' not in kw and 'cls' not in kw, (
"You cannot use 'self' or 'cls' arguments to a "
"classinstancemethod")
return self.func(*((self.obj, self.type) + args), **kw)
+
class Monitor(object): # pragma: no cover
"""
A file monitor and server restarter.
@@ -762,7 +870,7 @@ class Monitor(object): # pragma: no cover
if %errorlevel% == 3 goto repeat
or run a monitoring process in Python (``pserve --reload`` does
- this).
+ this).
Use the ``watch_file(filename)`` function to cause a reload/restart for
other non-Python files (e.g., configuration files). If you have
@@ -785,9 +893,19 @@ class Monitor(object): # pragma: no cover
self.poll_interval = poll_interval
self.extra_files = list(self.global_extra_files)
self.instances.append(self)
+ self.syntax_error_files = set()
+ self.pending_reload = False
self.file_callbacks = list(self.global_file_callbacks)
+ temp_pyc_fp = tempfile.NamedTemporaryFile(delete=False)
+ self.temp_pyc = temp_pyc_fp.name
+ temp_pyc_fp.close()
def _exit(self):
+ try:
+ os.unlink(self.temp_pyc)
+ except IOError:
+ # not worried if the tempfile can't be removed
+ pass
# use os._exit() here and not sys.exit() since within a
# thread sys.exit() just closes the given thread and
# won't kill the process; note os._exit does not call
@@ -818,6 +936,7 @@ class Monitor(object): # pragma: no cover
continue
if filename is not None:
filenames.append(filename)
+ new_changes = False
for filename in filenames:
try:
stat = os.stat(filename)
@@ -829,11 +948,42 @@ class Monitor(object): # pragma: no cover
continue
if filename.endswith('.pyc') and os.path.exists(filename[:-1]):
mtime = max(os.stat(filename[:-1]).st_mtime, mtime)
- if not filename in self.module_mtimes:
- self.module_mtimes[filename] = mtime
- elif self.module_mtimes[filename] < mtime:
- print("%s changed; reloading..." % filename)
- return False
+ pyc = True
+ else:
+ pyc = False
+ old_mtime = self.module_mtimes.get(filename)
+ self.module_mtimes[filename] = mtime
+ if old_mtime is not None and old_mtime < mtime:
+ new_changes = True
+ if pyc:
+ filename = filename[:-1]
+ is_valid = True
+ if filename.endswith('.py'):
+ is_valid = self.check_syntax(filename)
+ if is_valid:
+ print("%s changed ..." % filename)
+ if new_changes:
+ self.pending_reload = True
+ if self.syntax_error_files:
+ for filename in sorted(self.syntax_error_files):
+ print("%s has a SyntaxError; NOT reloading." % filename)
+ if self.pending_reload and not self.syntax_error_files:
+ self.pending_reload = False
+ return False
+ return True
+
+ def check_syntax(self, filename):
+ # check if a file has syntax errors.
+ # If so, track it until it's fixed.
+ try:
+ py_compile.compile(filename, cfile=self.temp_pyc, doraise=True)
+ except py_compile.PyCompileError as ex:
+ print(ex.msg)
+ self.syntax_error_files.add(filename)
+ return False
+ else:
+ if filename in self.syntax_error_files:
+ self.syntax_error_files.remove(filename)
return True
def watch_file(self, cls, filename):
@@ -961,7 +1111,7 @@ def cherrypy_server_runner(
server = wsgiserver.CherryPyWSGIServer(bind_addr, app,
server_name=server_name, **kwargs)
if ssl_pem is not None:
- if not PY3:
+ if PY2:
server.ssl_certificate = server.ssl_private_key = ssl_pem
else:
# creates wsgiserver.ssl_builtin as side-effect