581 lines
17 KiB
Python
581 lines
17 KiB
Python
#
|
|
# Module for starting a process object using os.fork() or CreateProcess()
|
|
#
|
|
# multiprocessing/forking.py
|
|
#
|
|
# Copyright (c) 2006-2008, R Oudkerk
|
|
# Licensed to PSF under a Contributor Agreement.
|
|
#
|
|
|
|
from __future__ import absolute_import
|
|
|
|
import os
|
|
import sys
|
|
import signal
|
|
import warnings
|
|
|
|
from pickle import load, HIGHEST_PROTOCOL
|
|
from billiard import util
|
|
from billiard import process
|
|
from billiard.five import int_types
|
|
from .reduction import dump
|
|
from .compat import _winapi as win32
|
|
|
|
__all__ = ['Popen', 'assert_spawning', 'exit',
|
|
'duplicate', 'close']
|
|
|
|
try:
|
|
WindowsError = WindowsError # noqa
|
|
except NameError:
|
|
class WindowsError(Exception): # noqa
|
|
pass
|
|
|
|
W_OLD_DJANGO_LAYOUT = """\
|
|
Will add directory %r to path! This is necessary to accommodate \
|
|
pre-Django 1.4 layouts using setup_environ.
|
|
You can skip this warning by adding a DJANGO_SETTINGS_MODULE=settings \
|
|
environment variable.
|
|
"""
|
|
|
|
#
|
|
# Choose whether to do a fork or spawn (fork+exec) on Unix.
|
|
# This affects how some shared resources should be created.
|
|
#
|
|
|
|
_forking_is_enabled = sys.platform != 'win32'
|
|
|
|
#
|
|
# Check that the current thread is spawning a child process
|
|
#
|
|
|
|
|
|
def assert_spawning(self):
|
|
if not Popen.thread_is_spawning():
|
|
raise RuntimeError(
|
|
'%s objects should only be shared between processes'
|
|
' through inheritance' % type(self).__name__
|
|
)
|
|
|
|
|
|
#
|
|
# Unix
|
|
#
|
|
|
|
if sys.platform != 'win32':
|
|
try:
|
|
import thread
|
|
except ImportError:
|
|
import _thread as thread # noqa
|
|
import select
|
|
|
|
WINEXE = False
|
|
WINSERVICE = False
|
|
|
|
exit = os._exit
|
|
duplicate = os.dup
|
|
close = os.close
|
|
_select = util._eintr_retry(select.select)
|
|
|
|
#
|
|
# We define a Popen class similar to the one from subprocess, but
|
|
# whose constructor takes a process object as its argument.
|
|
#
|
|
|
|
class Popen(object):
|
|
|
|
_tls = thread._local()
|
|
|
|
def __init__(self, process_obj):
|
|
# register reducers
|
|
from billiard import connection # noqa
|
|
_Django_old_layout_hack__save()
|
|
sys.stdout.flush()
|
|
sys.stderr.flush()
|
|
self.returncode = None
|
|
r, w = os.pipe()
|
|
self.sentinel = r
|
|
|
|
if _forking_is_enabled:
|
|
self.pid = os.fork()
|
|
if self.pid == 0:
|
|
os.close(r)
|
|
if 'random' in sys.modules:
|
|
import random
|
|
random.seed()
|
|
code = process_obj._bootstrap()
|
|
os._exit(code)
|
|
else:
|
|
from_parent_fd, to_child_fd = os.pipe()
|
|
cmd = get_command_line() + [str(from_parent_fd)]
|
|
|
|
self.pid = os.fork()
|
|
if self.pid == 0:
|
|
os.close(r)
|
|
os.close(to_child_fd)
|
|
os.execv(sys.executable, cmd)
|
|
|
|
# send information to child
|
|
prep_data = get_preparation_data(process_obj._name)
|
|
os.close(from_parent_fd)
|
|
to_child = os.fdopen(to_child_fd, 'wb')
|
|
Popen._tls.process_handle = self.pid
|
|
try:
|
|
dump(prep_data, to_child, HIGHEST_PROTOCOL)
|
|
dump(process_obj, to_child, HIGHEST_PROTOCOL)
|
|
finally:
|
|
del(Popen._tls.process_handle)
|
|
to_child.close()
|
|
|
|
# `w` will be closed when the child exits, at which point `r`
|
|
# will become ready for reading (using e.g. select()).
|
|
os.close(w)
|
|
util.Finalize(self, os.close, (r,))
|
|
|
|
def poll(self, flag=os.WNOHANG):
|
|
if self.returncode is None:
|
|
try:
|
|
pid, sts = os.waitpid(self.pid, flag)
|
|
except os.error:
|
|
# Child process not yet created. See #1731717
|
|
# e.errno == errno.ECHILD == 10
|
|
return None
|
|
if pid == self.pid:
|
|
if os.WIFSIGNALED(sts):
|
|
self.returncode = -os.WTERMSIG(sts)
|
|
else:
|
|
assert os.WIFEXITED(sts)
|
|
self.returncode = os.WEXITSTATUS(sts)
|
|
return self.returncode
|
|
|
|
def wait(self, timeout=None):
|
|
if self.returncode is None:
|
|
if timeout is not None:
|
|
r = _select([self.sentinel], [], [], timeout)[0]
|
|
if not r:
|
|
return None
|
|
# This shouldn't block if select() returned successfully.
|
|
return self.poll(os.WNOHANG if timeout == 0.0 else 0)
|
|
return self.returncode
|
|
|
|
def terminate(self):
|
|
if self.returncode is None:
|
|
try:
|
|
os.kill(self.pid, signal.SIGTERM)
|
|
except OSError:
|
|
if self.wait(timeout=0.1) is None:
|
|
raise
|
|
|
|
@staticmethod
|
|
def thread_is_spawning():
|
|
if _forking_is_enabled:
|
|
return False
|
|
else:
|
|
return getattr(Popen._tls, 'process_handle', None) is not None
|
|
|
|
@staticmethod
|
|
def duplicate_for_child(handle):
|
|
return handle
|
|
|
|
#
|
|
# Windows
|
|
#
|
|
|
|
else:
|
|
try:
|
|
import thread
|
|
except ImportError:
|
|
import _thread as thread # noqa
|
|
import msvcrt
|
|
try:
|
|
import _subprocess
|
|
except ImportError:
|
|
import _winapi as _subprocess # noqa
|
|
|
|
#
|
|
#
|
|
#
|
|
|
|
TERMINATE = 0x10000
|
|
WINEXE = (sys.platform == 'win32' and getattr(sys, 'frozen', False))
|
|
WINSERVICE = sys.executable.lower().endswith("pythonservice.exe")
|
|
|
|
exit = win32.ExitProcess
|
|
close = win32.CloseHandle
|
|
|
|
#
|
|
#
|
|
#
|
|
|
|
def duplicate(handle, target_process=None, inheritable=False):
|
|
if target_process is None:
|
|
target_process = _subprocess.GetCurrentProcess()
|
|
h = _subprocess.DuplicateHandle(
|
|
_subprocess.GetCurrentProcess(), handle, target_process,
|
|
0, inheritable, _subprocess.DUPLICATE_SAME_ACCESS
|
|
)
|
|
if sys.version_info[0] < 3 or (
|
|
sys.version_info[0] == 3 and sys.version_info[1] < 3):
|
|
h = h.Detach()
|
|
return h
|
|
|
|
#
|
|
# We define a Popen class similar to the one from subprocess, but
|
|
# whose constructor takes a process object as its argument.
|
|
#
|
|
|
|
class Popen(object):
|
|
'''
|
|
Start a subprocess to run the code of a process object
|
|
'''
|
|
_tls = thread._local()
|
|
|
|
def __init__(self, process_obj):
|
|
_Django_old_layout_hack__save()
|
|
# create pipe for communication with child
|
|
rfd, wfd = os.pipe()
|
|
|
|
# get handle for read end of the pipe and make it inheritable
|
|
rhandle = duplicate(msvcrt.get_osfhandle(rfd), inheritable=True)
|
|
os.close(rfd)
|
|
|
|
# start process
|
|
cmd = get_command_line() + [rhandle]
|
|
cmd = ' '.join('"%s"' % x for x in cmd)
|
|
hp, ht, pid, tid = _subprocess.CreateProcess(
|
|
_python_exe, cmd, None, None, 1, 0, None, None, None
|
|
)
|
|
close(ht) if isinstance(ht, int_types) else ht.Close()
|
|
(close(rhandle) if isinstance(rhandle, int_types)
|
|
else rhandle.Close())
|
|
|
|
# set attributes of self
|
|
self.pid = pid
|
|
self.returncode = None
|
|
self._handle = hp
|
|
self.sentinel = int(hp)
|
|
|
|
# send information to child
|
|
prep_data = get_preparation_data(process_obj._name)
|
|
to_child = os.fdopen(wfd, 'wb')
|
|
Popen._tls.process_handle = int(hp)
|
|
try:
|
|
dump(prep_data, to_child, HIGHEST_PROTOCOL)
|
|
dump(process_obj, to_child, HIGHEST_PROTOCOL)
|
|
finally:
|
|
del Popen._tls.process_handle
|
|
to_child.close()
|
|
|
|
@staticmethod
|
|
def thread_is_spawning():
|
|
return getattr(Popen._tls, 'process_handle', None) is not None
|
|
|
|
@staticmethod
|
|
def duplicate_for_child(handle):
|
|
return duplicate(handle, Popen._tls.process_handle)
|
|
|
|
def wait(self, timeout=None):
|
|
if self.returncode is None:
|
|
if timeout is None:
|
|
msecs = _subprocess.INFINITE
|
|
else:
|
|
msecs = max(0, int(timeout * 1000 + 0.5))
|
|
|
|
res = _subprocess.WaitForSingleObject(int(self._handle), msecs)
|
|
if res == _subprocess.WAIT_OBJECT_0:
|
|
code = _subprocess.GetExitCodeProcess(self._handle)
|
|
if code == TERMINATE:
|
|
code = -signal.SIGTERM
|
|
self.returncode = code
|
|
|
|
return self.returncode
|
|
|
|
def poll(self):
|
|
return self.wait(timeout=0)
|
|
|
|
def terminate(self):
|
|
if self.returncode is None:
|
|
try:
|
|
_subprocess.TerminateProcess(int(self._handle), TERMINATE)
|
|
except WindowsError:
|
|
if self.wait(timeout=0.1) is None:
|
|
raise
|
|
|
|
#
|
|
#
|
|
#
|
|
|
|
if WINSERVICE:
|
|
_python_exe = os.path.join(sys.exec_prefix, 'python.exe')
|
|
else:
|
|
_python_exe = sys.executable
|
|
|
|
|
|
def set_executable(exe):
|
|
global _python_exe
|
|
_python_exe = exe
|
|
|
|
|
|
def is_forking(argv):
|
|
'''
|
|
Return whether commandline indicates we are forking
|
|
'''
|
|
if len(argv) >= 2 and argv[1] == '--billiard-fork':
|
|
assert len(argv) == 3
|
|
os.environ["FORKED_BY_MULTIPROCESSING"] = "1"
|
|
return True
|
|
else:
|
|
return False
|
|
|
|
|
|
def freeze_support():
|
|
'''
|
|
Run code for process object if this in not the main process
|
|
'''
|
|
if is_forking(sys.argv):
|
|
main()
|
|
sys.exit()
|
|
|
|
|
|
def get_command_line():
|
|
'''
|
|
Returns prefix of command line used for spawning a child process
|
|
'''
|
|
if process.current_process()._identity == () and is_forking(sys.argv):
|
|
raise RuntimeError('''
|
|
Attempt to start a new process before the current process
|
|
has finished its bootstrapping phase.
|
|
|
|
This probably means that have forgotten to use the proper
|
|
idiom in the main module:
|
|
|
|
if __name__ == '__main__':
|
|
freeze_support()
|
|
...
|
|
|
|
The "freeze_support()" line can be omitted if the program
|
|
is not going to be frozen to produce a Windows executable.''')
|
|
|
|
if getattr(sys, 'frozen', False):
|
|
return [sys.executable, '--billiard-fork']
|
|
else:
|
|
prog = 'from billiard.forking import main; main()'
|
|
return [_python_exe, '-c', prog, '--billiard-fork']
|
|
|
|
|
|
def _Django_old_layout_hack__save():
|
|
if 'DJANGO_PROJECT_DIR' not in os.environ:
|
|
try:
|
|
settings_name = os.environ['DJANGO_SETTINGS_MODULE']
|
|
except KeyError:
|
|
return # not using Django.
|
|
|
|
conf_settings = sys.modules.get('django.conf.settings')
|
|
configured = conf_settings and conf_settings.configured
|
|
try:
|
|
project_name, _ = settings_name.split('.', 1)
|
|
except ValueError:
|
|
return # not modified by setup_environ
|
|
|
|
project = __import__(project_name)
|
|
try:
|
|
project_dir = os.path.normpath(_module_parent_dir(project))
|
|
except AttributeError:
|
|
return # dynamically generated module (no __file__)
|
|
if configured:
|
|
warnings.warn(UserWarning(
|
|
W_OLD_DJANGO_LAYOUT % os.path.realpath(project_dir)
|
|
))
|
|
os.environ['DJANGO_PROJECT_DIR'] = project_dir
|
|
|
|
|
|
def _Django_old_layout_hack__load():
|
|
try:
|
|
sys.path.append(os.environ['DJANGO_PROJECT_DIR'])
|
|
except KeyError:
|
|
pass
|
|
|
|
|
|
def _module_parent_dir(mod):
|
|
dir, filename = os.path.split(_module_dir(mod))
|
|
if dir == os.curdir or not dir:
|
|
dir = os.getcwd()
|
|
return dir
|
|
|
|
|
|
def _module_dir(mod):
|
|
if '__init__.py' in mod.__file__:
|
|
return os.path.dirname(mod.__file__)
|
|
return mod.__file__
|
|
|
|
|
|
def main():
|
|
'''
|
|
Run code specifed by data received over pipe
|
|
'''
|
|
global _forking_is_enabled
|
|
_Django_old_layout_hack__load()
|
|
|
|
assert is_forking(sys.argv)
|
|
_forking_is_enabled = False
|
|
|
|
handle = int(sys.argv[-1])
|
|
if sys.platform == 'win32':
|
|
fd = msvcrt.open_osfhandle(handle, os.O_RDONLY)
|
|
else:
|
|
fd = handle
|
|
from_parent = os.fdopen(fd, 'rb')
|
|
|
|
process.current_process()._inheriting = True
|
|
preparation_data = load(from_parent)
|
|
prepare(preparation_data)
|
|
# Huge hack to make logging before Process.run work.
|
|
try:
|
|
os.environ["MP_MAIN_FILE"] = sys.modules["__main__"].__file__
|
|
except KeyError:
|
|
pass
|
|
except AttributeError:
|
|
pass
|
|
loglevel = os.environ.get("_MP_FORK_LOGLEVEL_")
|
|
logfile = os.environ.get("_MP_FORK_LOGFILE_") or None
|
|
format = os.environ.get("_MP_FORK_LOGFORMAT_")
|
|
if loglevel:
|
|
from billiard import util
|
|
import logging
|
|
logger = util.get_logger()
|
|
logger.setLevel(int(loglevel))
|
|
if not logger.handlers:
|
|
logger._rudimentary_setup = True
|
|
logfile = logfile or sys.__stderr__
|
|
if hasattr(logfile, "write"):
|
|
handler = logging.StreamHandler(logfile)
|
|
else:
|
|
handler = logging.FileHandler(logfile)
|
|
formatter = logging.Formatter(
|
|
format or util.DEFAULT_LOGGING_FORMAT,
|
|
)
|
|
handler.setFormatter(formatter)
|
|
logger.addHandler(handler)
|
|
|
|
self = load(from_parent)
|
|
process.current_process()._inheriting = False
|
|
|
|
from_parent.close()
|
|
|
|
exitcode = self._bootstrap()
|
|
exit(exitcode)
|
|
|
|
|
|
def get_preparation_data(name):
|
|
'''
|
|
Return info about parent needed by child to unpickle process object
|
|
'''
|
|
from billiard.util import _logger, _log_to_stderr
|
|
|
|
d = dict(
|
|
name=name,
|
|
sys_path=sys.path,
|
|
sys_argv=sys.argv,
|
|
log_to_stderr=_log_to_stderr,
|
|
orig_dir=process.ORIGINAL_DIR,
|
|
authkey=process.current_process().authkey,
|
|
)
|
|
|
|
if _logger is not None:
|
|
d['log_level'] = _logger.getEffectiveLevel()
|
|
|
|
if not WINEXE and not WINSERVICE:
|
|
main_path = getattr(sys.modules['__main__'], '__file__', None)
|
|
if not main_path and sys.argv[0] not in ('', '-c'):
|
|
main_path = sys.argv[0]
|
|
if main_path is not None:
|
|
if (not os.path.isabs(main_path) and
|
|
process.ORIGINAL_DIR is not None):
|
|
main_path = os.path.join(process.ORIGINAL_DIR, main_path)
|
|
d['main_path'] = os.path.normpath(main_path)
|
|
|
|
return d
|
|
|
|
#
|
|
# Prepare current process
|
|
#
|
|
|
|
old_main_modules = []
|
|
|
|
|
|
def prepare(data):
|
|
'''
|
|
Try to get current process ready to unpickle process object
|
|
'''
|
|
old_main_modules.append(sys.modules['__main__'])
|
|
|
|
if 'name' in data:
|
|
process.current_process().name = data['name']
|
|
|
|
if 'authkey' in data:
|
|
process.current_process()._authkey = data['authkey']
|
|
|
|
if 'log_to_stderr' in data and data['log_to_stderr']:
|
|
util.log_to_stderr()
|
|
|
|
if 'log_level' in data:
|
|
util.get_logger().setLevel(data['log_level'])
|
|
|
|
if 'sys_path' in data:
|
|
sys.path = data['sys_path']
|
|
|
|
if 'sys_argv' in data:
|
|
sys.argv = data['sys_argv']
|
|
|
|
if 'dir' in data:
|
|
os.chdir(data['dir'])
|
|
|
|
if 'orig_dir' in data:
|
|
process.ORIGINAL_DIR = data['orig_dir']
|
|
|
|
if 'main_path' in data:
|
|
main_path = data['main_path']
|
|
main_name = os.path.splitext(os.path.basename(main_path))[0]
|
|
if main_name == '__init__':
|
|
main_name = os.path.basename(os.path.dirname(main_path))
|
|
|
|
if main_name == '__main__':
|
|
main_module = sys.modules['__main__']
|
|
main_module.__file__ = main_path
|
|
elif main_name != 'ipython':
|
|
# Main modules not actually called __main__.py may
|
|
# contain additional code that should still be executed
|
|
import imp
|
|
|
|
if main_path is None:
|
|
dirs = None
|
|
elif os.path.basename(main_path).startswith('__init__.py'):
|
|
dirs = [os.path.dirname(os.path.dirname(main_path))]
|
|
else:
|
|
dirs = [os.path.dirname(main_path)]
|
|
|
|
assert main_name not in sys.modules, main_name
|
|
file, path_name, etc = imp.find_module(main_name, dirs)
|
|
try:
|
|
# We would like to do "imp.load_module('__main__', ...)"
|
|
# here. However, that would cause 'if __name__ ==
|
|
# "__main__"' clauses to be executed.
|
|
main_module = imp.load_module(
|
|
'__parents_main__', file, path_name, etc
|
|
)
|
|
finally:
|
|
if file:
|
|
file.close()
|
|
|
|
sys.modules['__main__'] = main_module
|
|
main_module.__name__ = '__main__'
|
|
|
|
# Try to make the potentially picklable objects in
|
|
# sys.modules['__main__'] realize they are in the main
|
|
# module -- somewhat ugly.
|
|
for obj in list(main_module.__dict__.values()):
|
|
try:
|
|
if obj.__module__ == '__parents_main__':
|
|
obj.__module__ = '__main__'
|
|
except Exception:
|
|
pass
|