rcserver.py
1025 lines
| 32.2 KiB
| text/x-python
|
PythonLexer
/ rhodecode / rcserver.py
r1 | # (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 atexit | ||||
import errno | ||||
import fnmatch | ||||
import logging | ||||
import optparse | ||||
import os | ||||
import re | ||||
Martin Bornhold
|
r1007 | import subprocess32 | ||
r1 | import sys | |||
import textwrap | ||||
import threading | ||||
import time | ||||
import traceback | ||||
from logging.config import fileConfig | ||||
import ConfigParser as configparser | ||||
from paste.deploy import loadserver | ||||
from paste.deploy import loadapp | ||||
import rhodecode | ||||
from rhodecode.lib.compat import kill | ||||
def make_web_build_callback(filename): | ||||
Martin Bornhold
|
r1007 | p = subprocess32.Popen('make web-build', shell=True, | ||
stdout=subprocess32.PIPE, | ||||
stderr=subprocess32.PIPE, | ||||
r1 | cwd=os.path.dirname(os.path.dirname(__file__))) | |||
stdout, stderr = p.communicate() | ||||
stdout = ''.join(stdout) | ||||
stderr = ''.join(stderr) | ||||
if stdout: | ||||
r3057 | print(stdout) | |||
r1 | if stderr: | |||
r3057 | print('%s %s %s' % ('-' * 20, 'ERRORS', '-' * 20)) | |||
print(stderr) | ||||
r1 | ||||
MAXFD = 1024 | ||||
HERE = os.path.dirname(os.path.abspath(__file__)) | ||||
SERVER_RUNNING_FILE = None | ||||
# watch those extra files for changes, server gets restarted if file changes | ||||
GLOBAL_EXTRA_FILES = { | ||||
'rhodecode/public/css/*.less': make_web_build_callback, | ||||
'rhodecode/public/js/src/**/*.js': make_web_build_callback, | ||||
} | ||||
## HOOKS - inspired by gunicorn # | ||||
def when_ready(server): | ||||
""" | ||||
Called just after the server is started. | ||||
""" | ||||
def _remove_server_running_file(): | ||||
if os.path.isfile(SERVER_RUNNING_FILE): | ||||
os.remove(SERVER_RUNNING_FILE) | ||||
if SERVER_RUNNING_FILE: | ||||
with open(SERVER_RUNNING_FILE, 'wb') as f: | ||||
f.write(str(os.getpid())) | ||||
# register cleanup of that file when server exits | ||||
atexit.register(_remove_server_running_file) | ||||
def setup_logging(config_uri, fileConfig=fileConfig, | ||||
configparser=configparser): | ||||
""" | ||||
Set up logging via the logging module's fileConfig function with the | ||||
filename specified via ``config_uri`` (a string in the form | ||||
``filename#sectionname``). | ||||
ConfigParser defaults are specified for the special ``__file__`` | ||||
and ``here`` variables, similar to PasteDeploy config loading. | ||||
""" | ||||
path, _ = _getpathsec(config_uri, None) | ||||
parser = configparser.ConfigParser() | ||||
parser.read([path]) | ||||
if parser.has_section('loggers'): | ||||
config_file = os.path.abspath(path) | ||||
return fileConfig( | ||||
config_file, | ||||
{'__file__': config_file, 'here': os.path.dirname(config_file)} | ||||
) | ||||
def set_rhodecode_is_test(config_uri): | ||||
"""If is_test is defined in the config file sets rhodecode.is_test.""" | ||||
path, _ = _getpathsec(config_uri, None) | ||||
parser = configparser.ConfigParser() | ||||
parser.read(path) | ||||
rhodecode.is_test = ( | ||||
parser.has_option('app:main', 'is_test') and | ||||
parser.getboolean('app:main', 'is_test')) | ||||
def _getpathsec(config_uri, name): | ||||
if '#' in config_uri: | ||||
path, section = config_uri.split('#', 1) | ||||
else: | ||||
path, section = config_uri, 'main' | ||||
if name: | ||||
section = name | ||||
return path, section | ||||
def parse_vars(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 _match_pattern(filename): | ||||
for pattern in GLOBAL_EXTRA_FILES: | ||||
if fnmatch.fnmatch(filename, pattern): | ||||
return pattern | ||||
return False | ||||
def generate_extra_file_list(): | ||||
extra_list = [] | ||||
for root, dirs, files in os.walk(HERE, topdown=True): | ||||
for fname in files: | ||||
stripped_src = os.path.join( | ||||
'rhodecode', os.path.relpath(os.path.join(root, fname), HERE)) | ||||
if _match_pattern(stripped_src): | ||||
extra_list.append(stripped_src) | ||||
return extra_list | ||||
def run_callback_for_pattern(filename): | ||||
pattern = _match_pattern(filename) | ||||
if pattern: | ||||
_file_callback = GLOBAL_EXTRA_FILES.get(pattern) | ||||
if callable(_file_callback): | ||||
_file_callback(filename) | ||||
class DaemonizeException(Exception): | ||||
pass | ||||
class RcServerCommand(object): | ||||
usage = '%prog config_uri [start|stop|restart|status] [var=value]' | ||||
description = """\ | ||||
This command serves a web application that uses a PasteDeploy | ||||
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. | ||||
""" | ||||
default_verbosity = 1 | ||||
parser = optparse.OptionParser( | ||||
usage, | ||||
description=textwrap.dedent(description) | ||||
) | ||||
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)")) | ||||
parser.add_option( | ||||
'--with-vcsserver', | ||||
dest='vcs_server', | ||||
action='store_true', | ||||
help=("Start the vcsserver instance together with the RhodeCode server")) | ||||
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( | ||||
'--running-file', | ||||
dest='running_file', | ||||
metavar='RUNNING_FILE', | ||||
help="Create a running file after the server is initalized with " | ||||
"stored PID of process") | ||||
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") | ||||
parser.add_option( | ||||
'-v', '--verbose', | ||||
default=default_verbosity, | ||||
dest='verbose', | ||||
action='count', | ||||
help="Set verbose level (default "+str(default_verbosity)+")") | ||||
parser.add_option( | ||||
'-q', '--quiet', | ||||
action='store_const', | ||||
const=0, | ||||
dest='verbose', | ||||
help="Suppress verbose output") | ||||
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)')) | ||||
_scheme_re = re.compile(r'^[a-z][a-z]+:', re.I) | ||||
_reloader_environ_key = 'PYTHON_RELOADER_SHOULD_RUN' | ||||
_monitor_environ_key = 'PASTE_MONITOR_SHOULD_RUN' | ||||
possible_subcommands = ('start', 'stop', 'restart', 'status') | ||||
def __init__(self, argv, quiet=False): | ||||
self.options, self.args = self.parser.parse_args(argv[1:]) | ||||
if quiet: | ||||
self.options.verbose = 0 | ||||
def out(self, msg): # pragma: no cover | ||||
if self.options.verbose > 0: | ||||
print(msg) | ||||
def get_options(self): | ||||
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 | ||||
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 not self.args: | ||||
self.out('Please provide configuration file as first argument, ' | ||||
'most likely it should be production.ini') | ||||
return 2 | ||||
app_spec = self.args[0] | ||||
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 os.environ.get(self._reloader_environ_key): | ||||
if self.options.verbose > 1: | ||||
self.out('Running reloading file monitor') | ||||
install_reloader(int(self.options.reload_interval), | ||||
[app_spec] + generate_extra_file_list()) | ||||
# if self.requires_config_file: | ||||
# watch_file(self.args[0]) | ||||
else: | ||||
return self.restart_with_reloader() | ||||
if cmd not in (None, 'start', 'stop', 'restart', 'status'): | ||||
self.out( | ||||
'Error: must give start|stop|restart (not %s)' % cmd) | ||||
return 2 | ||||
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': | ||||
self.out("Could not stop daemon; aborting") | ||||
else: | ||||
self.out("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.get_options() | ||||
if self.options.vcs_server: | ||||
vars['vcs.start_server'] = 'true' | ||||
if self.options.running_file: | ||||
global SERVER_RUNNING_FILE | ||||
SERVER_RUNNING_FILE = self.options.running_file | ||||
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.options.verbose > 0: | ||||
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: | ||||
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) | ||||
setup_logging(log_fn) | ||||
set_rhodecode_is_test(log_fn) | ||||
server = self.loadserver(server_spec, name=server_name, | ||||
relative_to=base, global_conf=vars) | ||||
# starting hooks | ||||
app = self.loadapp(app_spec, name=app_name, relative_to=base, | ||||
global_conf=vars) | ||||
if self.options.verbose > 0: | ||||
if hasattr(os, 'getpid'): | ||||
msg = 'Starting %s in PID %i.' % (__name__, os.getpid()) | ||||
else: | ||||
msg = 'Starting %s.' % (__name__,) | ||||
self.out(msg) | ||||
if SERVER_RUNNING_FILE: | ||||
self.out('PID file written as %s' % (SERVER_RUNNING_FILE, )) | ||||
elif not self.options.pid_file: | ||||
self.out('No PID file written by default.') | ||||
try: | ||||
when_ready(server) | ||||
server(app) | ||||
except (SystemExit, KeyboardInterrupt) as e: | ||||
if self.options.verbose > 1: | ||||
raise | ||||
if str(e): | ||||
msg = ' ' + str(e) | ||||
else: | ||||
msg = '' | ||||
self.out('Exiting%s (-v to see traceback)' % msg) | ||||
def loadapp(self, app_spec, name, relative_to, **kw): # pragma: no cover | ||||
return loadapp(app_spec, name=name, relative_to=relative_to, **kw) | ||||
def loadserver(self, server_spec, name, relative_to, **kw): # pragma:no cover | ||||
return loadserver( | ||||
server_spec, name=name, relative_to=relative_to, **kw) | ||||
def quote_first_command_arg(self, arg): # pragma: no cover | ||||
""" | ||||
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): # pragma: no cover | ||||
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.options.verbose > 0: | ||||
self.out('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 _remove_pid_file(self, 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 | ||||
with open(filename) as f: | ||||
content = f.read().strip() | ||||
try: | ||||
pid_in_file = int(content) | ||||
except ValueError: | ||||
pass | ||||
else: | ||||
if pid_in_file != current_pid: | ||||
msg = "PID file %s contains %s, not expected PID %s" | ||||
self.out(msg % (filename, pid_in_file, current_pid)) | ||||
return | ||||
if verbosity > 0: | ||||
self.out("Removing PID file %s" % filename) | ||||
try: | ||||
os.unlink(filename) | ||||
return | ||||
except OSError as e: | ||||
# Record, but don't give traceback | ||||
self.out("Cannot remove PID file: (%s)" % e) | ||||
# well, at least lets not leave the invalid PID around... | ||||
try: | ||||
with open(filename, 'w') as f: | ||||
f.write('') | ||||
except OSError as e: | ||||
self.out('Stale PID left in file: %s (%s)' % (filename, e)) | ||||
else: | ||||
self.out('Stale PID removed') | ||||
def record_pid(self, pid_file): | ||||
pid = os.getpid() | ||||
if self.options.verbose > 1: | ||||
self.out('Writing PID %s to %s' % (pid, pid_file)) | ||||
with open(pid_file, 'w') as f: | ||||
f.write(str(pid)) | ||||
atexit.register(self._remove_pid_file, pid, pid_file, self.options.verbose) | ||||
def stop_daemon(self): # pragma: no cover | ||||
pid_file = self.options.pid_file or 'pyramid.pid' | ||||
if not os.path.exists(pid_file): | ||||
self.out('No PID file exists in %s' % pid_file) | ||||
return 1 | ||||
pid = read_pidfile(pid_file) | ||||
if not pid: | ||||
self.out("Not a valid PID file in %s" % pid_file) | ||||
return 1 | ||||
pid = live_pidfile(pid_file) | ||||
if not pid: | ||||
self.out("PID in %s is not valid (deleting)" % pid_file) | ||||
try: | ||||
os.unlink(pid_file) | ||||
except (OSError, IOError) as e: | ||||
self.out("Could not delete: %s" % e) | ||||
return 2 | ||||
return 1 | ||||
for j in range(10): | ||||
if not live_pidfile(pid_file): | ||||
break | ||||
import signal | ||||
kill(pid, signal.SIGTERM) | ||||
time.sleep(1) | ||||
else: | ||||
self.out("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): # pragma: no cover | ||||
pid_file = self.options.pid_file or 'pyramid.pid' | ||||
if not os.path.exists(pid_file): | ||||
self.out('No PID file %s' % pid_file) | ||||
return 1 | ||||
pid = read_pidfile(pid_file) | ||||
if not pid: | ||||
self.out('No PID in file %s' % pid_file) | ||||
return 1 | ||||
pid = live_pidfile(pid_file) | ||||
if not pid: | ||||
self.out('PID %s in %s is not running' % (pid, pid_file)) | ||||
return 1 | ||||
self.out('Server running in PID %s' % pid) | ||||
return 0 | ||||
def restart_with_reloader(self): # pragma: no cover | ||||
self.restart_with_monitor(reloader=True) | ||||
def restart_with_monitor(self, reloader=False): # pragma: no cover | ||||
if self.options.verbose > 0: | ||||
if reloader: | ||||
self.out('Starting subprocess with file monitor') | ||||
else: | ||||
self.out('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() | ||||
Martin Bornhold
|
r1007 | proc = subprocess32.Popen(args, env=new_environ) | ||
r1 | exit_code = proc.wait() | |||
proc = None | ||||
except KeyboardInterrupt: | ||||
self.out('^C caught in monitor process') | ||||
if self.options.verbose > 1: | ||||
raise | ||||
return 1 | ||||
finally: | ||||
if proc is not None: | ||||
import signal | ||||
try: | ||||
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.options.verbose > 0: | ||||
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 | ||||
import grp | ||||
uid = gid = None | ||||
if group: | ||||
try: | ||||
gid = int(group) | ||||
group = grp.getgrgid(gid).gr_name | ||||
except ValueError: | ||||
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.options.verbose > 0: | ||||
self.out('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: | ||||
with self.lock: | ||||
self.fileobj = open(self.filename, self.mode) | ||||
return self.fileobj | ||||
def close(self): | ||||
fileobj = self.fileobj | ||||
if fileobj is not None: | ||||
fileobj.close() | ||||
def __del__(self): | ||||
self.close() | ||||
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): # pragma: no cover | ||||
""" | ||||
(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: | ||||
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: | ||||
with open(filename) as f: | ||||
content = f.read() | ||||
return int(content.strip()) | ||||
except (ValueError, IOError): | ||||
return None | ||||
else: | ||||
return None | ||||
def ensure_port_cleanup( | ||||
bound_addresses, maxtries=30, sleeptime=2): # pragma: no cover | ||||
""" | ||||
This makes sure any open ports are closed. | ||||
Does this by connecting to them until they give connection | ||||
refused. Servers should call like:: | ||||
ensure_port_cleanup([80, 443]) | ||||
""" | ||||
atexit.register(_cleanup_ports, bound_addresses, maxtries=maxtries, | ||||
sleeptime=sleeptime) | ||||
def _cleanup_ports( | ||||
bound_addresses, maxtries=30, sleeptime=2): # pragma: no cover | ||||
# 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(): # pragma: no cover | ||||
""" | ||||
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) | ||||
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). | ||||
""" | ||||
mon = Monitor(poll_interval=poll_interval) | ||||
if extra_files is None: | ||||
extra_files = [] | ||||
mon.extra_files.extend(extra_files) | ||||
t = threading.Thread(target=mon.periodic_reload) | ||||
t.setDaemon(True) | ||||
t.start() | ||||
class classinstancemethod(object): | ||||
""" | ||||
Acts like a class method when called from a class, like an | ||||
instance method when called by an instance. The method should | ||||
take two arguments, 'self' and 'cls'; one of these will be None | ||||
depending on how the method was called. | ||||
""" | ||||
def __init__(self, func): | ||||
self.func = func | ||||
self.__doc__ = func.__doc__ | ||||
def __get__(self, obj, type=None): | ||||
return _methodwrapper(self.func, obj=obj, type=type) | ||||
class _methodwrapper(object): | ||||
def __init__(self, func, obj, type): | ||||
self.func = func | ||||
self.obj = obj | ||||
self.type = type | ||||
def __call__(self, *args, **kw): | ||||
assert not 'self' in kw and not 'cls' 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. | ||||
Use this like: | ||||
..code-block:: Python | ||||
install_reloader() | ||||
Then make sure your server is installed with a shell script like:: | ||||
err=3 | ||||
while test "$err" -eq 3 ; do | ||||
python server.py | ||||
err="$?" | ||||
done | ||||
or is run from this .bat file (if you use Windows):: | ||||
@echo off | ||||
:repeat | ||||
python server.py | ||||
if %errorlevel% == 3 goto repeat | ||||
or run a monitoring process in Python (``pserve --reload`` does | ||||
this). | ||||
Use the ``watch_file(filename)`` function to cause a reload/restart for | ||||
other non-Python files (e.g., configuration files). If you have | ||||
a dynamic set of files that grows over time you can use something like:: | ||||
def watch_config_files(): | ||||
return CONFIG_FILE_CACHE.keys() | ||||
add_file_callback(watch_config_files) | ||||
Then every time the reloader polls files it will call | ||||
``watch_config_files`` and check all the filenames it returns. | ||||
""" | ||||
instances = [] | ||||
global_extra_files = [] | ||||
global_file_callbacks = [] | ||||
def __init__(self, poll_interval): | ||||
self.module_mtimes = {} | ||||
self.keep_running = True | ||||
self.poll_interval = poll_interval | ||||
self.extra_files = list(self.global_extra_files) | ||||
self.instances.append(self) | ||||
self.file_callbacks = list(self.global_file_callbacks) | ||||
def _exit(self): | ||||
# 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 | ||||
# any atexit callbacks, nor does it do finally blocks, | ||||
# flush open files, etc. In otherwords, it is rude. | ||||
os._exit(3) | ||||
def periodic_reload(self): | ||||
while True: | ||||
if not self.check_reload(): | ||||
self._exit() | ||||
break | ||||
time.sleep(self.poll_interval) | ||||
def check_reload(self): | ||||
filenames = list(self.extra_files) | ||||
for file_callback in self.file_callbacks: | ||||
try: | ||||
filenames.extend(file_callback()) | ||||
except: | ||||
print( | ||||
"Error calling reloader callback %r:" % file_callback) | ||||
traceback.print_exc() | ||||
for module in list(sys.modules.values()): | ||||
try: | ||||
filename = module.__file__ | ||||
except (AttributeError, ImportError): | ||||
continue | ||||
if filename is not None: | ||||
filenames.append(filename) | ||||
for filename in filenames: | ||||
try: | ||||
stat = os.stat(filename) | ||||
if stat: | ||||
mtime = stat.st_mtime | ||||
else: | ||||
mtime = 0 | ||||
except (OSError, IOError): | ||||
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) | ||||
run_callback_for_pattern(filename) | ||||
return False | ||||
return True | ||||
def watch_file(self, cls, filename): | ||||
"""Watch the named file for changes""" | ||||
filename = os.path.abspath(filename) | ||||
if self is None: | ||||
for instance in cls.instances: | ||||
instance.watch_file(filename) | ||||
cls.global_extra_files.append(filename) | ||||
else: | ||||
self.extra_files.append(filename) | ||||
watch_file = classinstancemethod(watch_file) | ||||
def add_file_callback(self, cls, callback): | ||||
"""Add a callback -- a function that takes no parameters -- that will | ||||
return a list of filenames to watch for changes.""" | ||||
if self is None: | ||||
for instance in cls.instances: | ||||
instance.add_file_callback(callback) | ||||
cls.global_file_callbacks.append(callback) | ||||
else: | ||||
self.file_callbacks.append(callback) | ||||
add_file_callback = classinstancemethod(add_file_callback) | ||||
watch_file = Monitor.watch_file | ||||
add_file_callback = Monitor.add_file_callback | ||||
def main(argv=sys.argv, quiet=False): | ||||
command = RcServerCommand(argv, quiet=quiet) | ||||
return command.run() | ||||
if __name__ == '__main__': # pragma: no cover | ||||
sys.exit(main() or 0) | ||||