'''
We have faced an issue with forking processes on RHEL 5.10.
When parent process consumes a realy big amount of memory fork call becomes very slow
because it needs to copy memory pages from parent to child process.
To overcome the issue we use posix_spawn function. The function calls vfork followed by
execve call. This avoids copying of memory pages and saves time but it also prevents us
from redirecting standard input and output because when it is requested posix_spawn
function uses fork instead of vfork and we have the slowdown again.

To make the IO redirection we do not start a requested sub-process immidiately but
use intermediate python script which redirects the IO. The whole call chain looks like this:

Popen("/foo/bar") -> fork -> execve(["python", __file__]) -> execve("/foo/bar")

All parameters needed for I/O redirection are passed in pickle via temporary file.
'''

import sys
if sys.platform in ('linux2', 'darwin'):
    import types
    import time
    import os
    import cPickle as pickle
    import fcntl

    try:
        cloexec_flag = fcntl.FD_CLOEXEC
    except AttributeError:
        cloexec_flag = 1

    def _set_cloexec_flag(fd, cloexec=True):
        '''
        Utility function to modify close-on-exec flag of specified file descriptor
        '''
        old = fcntl.fcntl(fd, fcntl.F_GETFD)
        if cloexec:
            fcntl.fcntl(fd, fcntl.F_SETFD, old | cloexec_flag)
        else:
            fcntl.fcntl(fd, fcntl.F_SETFD, old & ~cloexec_flag)

    if __name__ == '__main__':
        try:
            MAXFD = os.sysconf("SC_OPEN_MAX")
        except:
            MAXFD = 256

        try:
            from os import closerange
        except ImportError:
            def _close_fds():
                '''
                Closes all open file descriptors
                '''
                for i in xrange(3, MAXFD):
                    try:
                        os.close(i)
                    except:
                        pass
        else:
            _close_fds = lambda: closerange(3, MAXFD)

        def do_execv(args=None, executable=None, close_fds=None, cwd=None, env=None,
                shell=None, p2cread=None, p2cwrite=None, c2pread=None, c2pwrite=None,
                errread=None, errwrite=None, ):
            '''
            The function redirects IO, prepares command-line, sets up environment and
            executes execve function.
            '''

            if isinstance(args, types.StringTypes):
                args = [args]
            else:
                args = list(args)

            if shell:
                args = [executable or "/bin/sh", "-c"] + args

            if executable is None:
                executable = args[0]

            # Close parent's pipe ends
            for parent_end, child_end in ((p2cwrite, p2cread), (c2pread, c2pwrite),
                    (errread, errwrite)):
                if parent_end is not None:
                    os.close(parent_end)
                if child_end is not None:
                    _set_cloexec_flag(True)

            # When duping fds, if there arises a situation
            # where one of the fds is either 0, 1 or 2, it
            # is possible that it is overwritten (#12607).
            if c2pwrite == 0:
                c2pwrite = os.dup(c2pwrite)
            if errwrite == 0 or errwrite == 1:
                errwrite = os.dup(errwrite)

            # Dup fds for child
            def _dup2(a, b):
                # dup2() removes the CLOEXEC flag but
                # we must do it ourselves if dup2()
                # would be a no-op (issue #10806).
                if a == b:
                    _set_cloexec_flag(a, False)
                elif a is not None:
                    os.dup2(a, b)
            _dup2(p2cread, 0)
            _dup2(c2pwrite, 1)
            _dup2(errwrite, 2)

            # Close pipe fds.  Make sure we don't close the
            # same fd more than once, or standard fds.
            closed = set([ None ])
            for fd in [p2cread, c2pwrite, errwrite]:
                if fd not in closed and fd > 2:
                    os.close(fd)
                    closed.add(fd)

            # Close all other fds, if asked for
            if close_fds:
                _close_fds()

            if cwd is not None:
                os.chdir(cwd)

            if env is None:
                os.execvp(executable, args)
            else:
                os.execvpe(executable, args, env)

            # This exitcode won't be reported to applications, so it
            # really doesn't matter what we return.
            os._exit(255)

        with open(sys.argv[1], 'rb') as input_data:
            params = pickle.load(input_data)
        os.unlink(sys.argv[1])
        do_execv(**params)

    else: # __name__ <> '__main__'
        import tempfile
        import ctypes

        # We support only Linux and Mac OS X now.
        libc = ctypes.CDLL("libc.so.6" if 'linux' in sys.platform else 'libc.dylib', use_errno=True)
        posix_spawn = libc.posix_spawn
        posix_spawn.restype = ctypes.c_int
        posix_spawn.argtypes = (
                ctypes.POINTER(ctypes.c_int), # ppid
                ctypes.c_char_p, # executable
                ctypes.c_void_p, # file_actions
                ctypes.c_void_p, # attrp
                ctypes.POINTER(ctypes.c_char_p), #argv
                ctypes.POINTER(ctypes.c_char_p), #env
        )


        def _wrap_execute_child(klass):
            wrapped_execute_child = klass._execute_child
            def _execute_child(self, args, executable, preexec_fn, close_fds,
                                   cwd, env, universal_newlines,
                                   startupinfo, creationflags, shell,
                                   p2cread, p2cwrite,
                                   c2pread, c2pwrite,
                                   errread, errwrite):

                if preexec_fn:
                    return wrapped_execute_child(self, args, executable, preexec_fn, close_fds,
                                   cwd, env, universal_newlines,
                                   startupinfo, creationflags, shell,
                                   p2cread, p2cwrite,
                                   c2pread, c2pwrite,
                                   errread, errwrite)
                for fd in (p2cread, p2cwrite,
                                   c2pread, c2pwrite,
                                   errread, errwrite):
                    if fd is not None:
                        _set_cloexec_flag(fd, False)

                data = dict(
                        args=args, executable=executable, close_fds=close_fds,
                        cwd=cwd, env=env, shell=shell,
                        p2cread=p2cread, p2cwrite=p2cwrite,
                        c2pread=c2pread, c2pwrite=c2pwrite,
                        errread=errread, errwrite=errwrite,
                        )

                with tempfile.NamedTemporaryFile(delete=False) as the_file:
                    pickle.dump(data, the_file, pickle.HIGHEST_PROTOCOL)

                argv = (ctypes.c_char_p * 4)(
                        sys.executable,
                        __file__,
                        the_file.name,
                        0)
                pid = ctypes.c_int()

                try:
                    if posix_spawn(ctypes.byref(pid), ctypes.c_char_p(sys.executable),
                            ctypes.c_void_p(), ctypes.c_void_p(),
                            ctypes.cast(argv, ctypes.POINTER(ctypes.c_char_p)),
                            ctypes.POINTER(ctypes.c_char_p)()):
                        errno = ctypes.get_errno()
                        try:
                            os.unlink(the_file.name)
                        except:
                            pass
                        raise OSError(errno, os.strerror(errno))
                finally:
                    if p2cread is not None and p2cwrite is not None:
                        os.close(p2cread)
                    if c2pwrite is not None and c2pread is not None:
                        os.close(c2pwrite)
                    if errwrite is not None and errread is not None:
                        os.close(errwrite)


                self.pid = pid.value
                self._child_created = True
            klass._execute_child = _execute_child


        import subprocess
        _wrap_execute_child(subprocess.Popen)

# vim: set et ts=4 sw=4 ai :

