Process execution tool.

    The forks and execs a process with args formatted according to a context.
    This is implemented as a module function to make it available to
    event_targets, legion and tasks.

    The args are:

    cmd_list    - The path and arg vector
    context     - Task's context
    instance    - An integer instance number used with multi-process tasks
    log     - Logging object (default is nothing logged).

    The context is used to format command args.  In addition, these values will
    be used to change the process execution environment:

    procname    - Changes the process name of the executed command (but not the path executed).
    user        - Does a setuid for the process
    group       - Does a setgid for the process
    cwd     - Does a chdir before executing

    The passed context is extended to include these specific runtime values which
    are only available for cmd_list substitution.

    context_prefix+'pid'        -  The process ID of the child process
    context_prefix+'instance'   -  The instance number (0 if not provided)
    context_prefix+'uid'        -  The numeric uid (based on 'user' if set, getuid() otherwise)
    context_prefix+'gid'        -  The numeric gid (based on 'group' if set, getgid() otherwise)

def _exec_process(cmd_list, base_context, instance=0, log=None):
    """
    Process execution tool.

    The forks and execs a process with args formatted according to a context.
    This is implemented as a module function to make it available to
    event_targets, legion and tasks.

    The args are:

    cmd_list    - The path and arg vector
    context     - Task's context
    instance    - An integer instance number used with multi-process tasks
    log     - Logging object (default is nothing logged).

    The context is used to format command args.  In addition, these values will
    be used to change the process execution environment:

    procname    - Changes the process name of the executed command (but not the path executed).
    user        - Does a setuid for the process
    group       - Does a setgid for the process
    cwd     - Does a chdir before executing

    The passed context is extended to include these specific runtime values which
    are only available for cmd_list substitution.

    context_prefix+'pid'        -  The process ID of the child process
    context_prefix+'instance'   -  The instance number (0 if not provided)
    context_prefix+'uid'        -  The numeric uid (based on 'user' if set, getuid() otherwise)
    context_prefix+'gid'        -  The numeric gid (based on 'group' if set, getgid() otherwise)
"""
    if not log:                                                                                 # pragma: no cover
        log = logging.getLogger(__name__)
        log.addHandler(logging.NullHandler())

    #  Get a copy of the context so changes here will not affect the
    #  task's base context.
    #
    context = base_context.copy()

    #  Make sure we have a normalized clone of the cmd_list
    #
    cmd_list = list(cmd_list)
    name = context.get(context_prefix+'name', cmd_list[0])
    log.debug("Starting %s instance %d", name, instance)

    procname = _fmt_context(context.get(context_prefix+'procname'), context)
    user = _fmt_context(context.get(context_prefix+'user'), context)
    group = _fmt_context(context.get(context_prefix+'group'), context)
    cwd = _fmt_context(context.get(context_prefix+'cwd'), context)

    #  Do the user setup early so we can throw an Exception on failure.
    #  Identity errors are considered fatal as we do not want to run
    #  a process at a higher priv if it was explicitly set to something
    #  else.
    #
    proc_uid = os.geteuid()
    proc_gid = os.getegid()
    do_setuid = (proc_uid != os.getuid())
    do_setgid = (proc_gid != os.getgid())
    if user is not None:
        pw = None
        try:
            uid = int(user)
            try: pw = pwd.getpwuid(uid)
            except: pass                                                                        # pragma: no cover
        except: pass
        if pw is None:
            try:
                pw = pwd.getpwnam(user)
            except Exception as e:
                raise TaskError(name, "Bad user %r -- %s" % (user, e))
        if proc_uid != pw.pw_uid:
            proc_uid = pw.pw_uid
            do_setuid = True
        if proc_gid != pw.pw_gid:
            proc_gid = pw.pw_gid
            do_setgid = True
    if group is not None:
        gr = None
        try:
            gid = int(group)
            try: gr = grp.getgrgid(gid)
            except: pass
        except: pass
        if gr is None:
            try:
                gr = grp.getgrnam(group)
            except Exception as e:
                raise TaskError(name, "Bad group %r -- %s" % (group, e))
        if proc_uid is not None and proc_gid != gr.gr_gid:
            log.info("gid for user %r (%d) overridden by group %r (%d)", user, proc_gid, group, gr.gr_gid)
            proc_gid = gr.gr_gid
            do_setgid = True

    if cwd is not None and not os.path.isdir(cwd):
        raise TaskError(name, "Directory for cwd setting '%s' does not exist" % (cwd,))

    #  Add in per-process context
    #
    context[context_prefix+'instance'] = instance
    context[context_prefix+'started'] = time.time()
    context[context_prefix+'uid'] = proc_uid
    context[context_prefix+'gid'] = proc_gid

    pid = os.fork()

    #  Parent just returns pid
    if pid > 0:
        return pid

    #  This section is processing the child.  Exceptions from this point must
    #  never escape to outside handlers or we might create zombie init tasks.
    #
    try:
        # Add the pid to the context now that we have it.
        #
        context[context_prefix+'pid'] = os.getpid()

        #  Set up the requested process environment
        #
        if do_setgid:
            try:
                os.setgid(proc_gid)
                log.debug("Setgid to %d succeeded in child '%s', instance %d", proc_gid, name, instance)
            except Exception as e:
                log.error("Setgid to %d failed in child %r, instance %d -- %s",
                        proc_gid, name, instance, e, exc_info=log.isEnabledFor(logging.DEBUG))
                os._exit(81)
        if do_setuid:
            try:
                os.setuid(proc_uid)
                log.debug("Setuid to %d succeeded in child '%s', instance %d", proc_uid, name, instance)
            except Exception as e:
                log.error("Setuid to %d failed in child %r, instance %d -- %s",
                        proc_uid, name, instance, e, exc_info=log.isEnabledFor(logging.DEBUG))
                os._exit(82)
        if cwd is not None:
            try:
                os.chdir(cwd)
                log.debug("Chdir to '%s' succeeded in child '%s', instance %d", cwd, name, instance)
            except Exception as e:
                log.error("Chdir to '%s' failed in child %r, instance %d -- %s",
                        cwd, name, instance, e, exc_info=log.isEnabledFor(logging.DEBUG))
                os._exit(83)

        #  Build formatted command
        #
        prog = _fmt_context(cmd_list[0], context)
        cmd = []
        if procname:
            cmd_list.pop(0)
            cmd.append(_fmt_context(context['procname'], context))
        for a in cmd_list:
            cmd.append(_fmt_context(a, context))

        log.info("child, Execing: %s <%s>", prog, utils.format_cmd(cmd))
    except Exception as e:
        #  Log any exceptions here while we still can.  After the closeall,
        #  bets are off.
        #
        log.error("Child processing failed for task %r, instance %d -- %s",
                name, instance, e, exc_info=log.isEnabledFor(logging.DEBUG))
        os._exit(84)
    try:
        retain_fds = [0,1,2]
        for log_fd in utils.log_filenos(log):
            if log_fd not in retain_fds:
                retain_fds.append(log_fd)
        utils.closeall(exclude=retain_fds)
        fd = None
        try: os.close(0)
        except: pass
        try:
            fd = os.open(std_process_dest, os.O_RDONLY)
        except Exception as e:
            log.error("child read open of %s failed -- %s", std_process_dest, e)
        if fd != 0:
            log.error("child failed to redirect stdin to %s", std_process_dest)

        try: os.close(1)
        except: pass
        try:
            fd = os.open('/dev/null', os.O_WRONLY)
        except Exception as e:
            log.error("child write open of %s failed -- %s", std_process_dest, e)
        if fd != 1:
            log.error("child failed to redirect stdout to %s", std_process_dest)

        #  Build a fresh environment based on context, with None values excluded and
        #  all other values as strings, formatted where appropriate:
        #
        env = {}
        for tag, val in context.items():
            if val is None:
                continue
            val = _fmt_context(str(val), context)
            if val is not None:
                env[tag] = val
    except Exception as e:
        #  At this point we can still send logs to stderr, so log these
        #  too, just in case.
        #
        log.error("Child processing failed for task %r, instance %d -- %s",
            name, instance, e, exc_info=log.isEnabledFor(logging.DEBUG))
        os._exit(85)
    try:
        try: os.close(2)
        except: pass
        try: os.dup(1)
        except: pass

        os.execvpe(prog, cmd, env)
    except:
        pass
    #  There is no way to report an exception here, so hopefully the exit code will
    #  be evidence enough.  When child output logging is supported, this can be reworked.
    #
    os._exit(86)