Start a task, which may involve starting zero or more processes.

        This is indicated as an internal method because tasks are really
        only ever marked as startable by the configuration.  Any task
        that should be running and is not will be started during regular
        manage() calls.  A task set to run only once will be started only
        if the _stopped attribute is None.

        If a task requires another task, it won't be started until the
        required task has started, except if the required task has 'once'
        control, then it won't be started until the 'once' task has
        stopped.

        Currently, processes are started via direct fork/exec, with
        stdin/stdout/stderr all redirected from /dev/null.  In future,
        will probably add options to redirect stdout/stderr to syslog
        or files.

        Note that processes are intentionally not detached or put in
        separate process groups or terminal groups.  The presumption is
        that "async" and "adopt" tasks will handle this themselves, and
        we need "wait" tasks to not be detached.

        Returns True to request a shorter period before the next call,
        False if nothing special is needed.

def _start(self):
        """
        Start a task, which may involve starting zero or more processes.

        This is indicated as an internal method because tasks are really
        only ever marked as startable by the configuration.  Any task
        that should be running and is not will be started during regular
        manage() calls.  A task set to run only once will be started only
        if the _stopped attribute is None.

        If a task requires another task, it won't be started until the
        required task has started, except if the required task has 'once'
        control, then it won't be started until the 'once' task has
        stopped.

        Currently, processes are started via direct fork/exec, with
        stdin/stdout/stderr all redirected from /dev/null.  In future,
        will probably add options to redirect stdout/stderr to syslog
        or files.

        Note that processes are intentionally not detached or put in
        separate process groups or terminal groups.  The presumption is
        that "async" and "adopt" tasks will handle this themselves, and
        we need "wait" tasks to not be detached.

        Returns True to request a shorter period before the next call,
        False if nothing special is needed.
    """
        log = self._params.get('log', self._discard)
        if self._stopping:
            log.debug("%s task is stopping", self._name)
            return True
        now = time.time()
        conf = self._config_running
        control = self._get(conf.get('control'))
        once = (control in self._legion.once_controls)

        #  Tasks with "event" control are immediately marked stopped as if they
        #  ran at start.  This is the only difference between "event" and "once"
        #  controls.
        #
        if control == 'event' and not self._stopped:
            self._stopped = now
        if self._stopped:
            if self._dnr:
                log.info("Task '%s' stopped and will now be deleted", self._name)
                self.close()
                return False
            elif once:
                log.debug("'%s' task %s exited %s ago", control, self._name, deltafmt(time.time() - self._stopped))
                return False
            else:
                log.debug("Restarting %s, task was stopped %s ago",
                            self._name, deltafmt(time.time() - self._stopped))
                self._reset_state()

        start_delay = self._get(conf.get('start_delay'))
        if start_delay:
            try:
                start_delay = int(start_delay)
            except Exception as e:
                log.error("Task '%s' has invalid start_delay '%s'", self._name, start_delay)
                start_delay = 0
        else:
            start_delay = 0
        if self._starting and not self._started:
            if now > self._starting + start_delay:
                log.info("%s task marked started after %s", self._name, deltafmt(now - self._starting))
                self._mark_started()
                return False
            log.debug("%s task has been starting for %s of %s",
                    self._name, deltafmt(now - self._starting), deltafmt(start_delay))
            return True

        #  Check the required state to ensure dependencies have been started.  In the case of
        #  'once' controls, the dependency must have already stopped, otherwise it must have
        #  started.
        #
        if self._started and control != 'suspend':
            log.debug("Task '%s' already started, skipping requires-check", self._name)
        else:
            for req in self.get_requires():
                if req._config_running.get('control') == 'once':
                    if not req._stopped:
                        if self._last_message + repetition_limit < time.time():
                            log.info("Task '%s' is waiting on '%s' to complete", self._name, req._name)
                            self._last_message = now
                        return True
                else:
                    if not req._started:
                        if self._last_message + repetition_limit < time.time():
                            log.info("Task '%s' is waiting on '%s' to start", self._name, req._name)
                            self._last_message = now
                        return True

        self._last_message = 0
        if once:
            #  "once" processes are immediately marked as stopping.
            #
            self._stopping = now

        try:
            start_command = None
            if 'commands' in conf:
                start_command = self._get_list(conf['commands'].get('start'))
            if not start_command:
                raise TaskError(self._name, "No 'start' command in task configuration")
            if not isinstance(start_command, list):
                start_command = list(start_command)

            if control != 'suspend':
                needed = self._get(conf.get('count'), default=1)
                running = len(self.get_pids())
                if needed < running:
                    self._shrink(needed, running)
                    return False
                elif needed == running:
                    log.debug("all %d needed process%s running", running, ses(running, 'es'))
                    return False

            self._starting = now
            if not start_delay:
                self._mark_started()

            if control == 'suspend':
                if not self._suspended:
                    log.debug("%s just moved to %r", self._name, control)
                    running = len(self.get_pids())
                    if running > 0:
                        log.debug("%s now %r, stopping running processes", self._name, control)
                        self._shrink(0, running)
                    else:
                        log.debug("%s is %r control, skipping process startup", self._name, control)
                        self._suspended = now
                return False
            else:
                if self._suspended:
                    log.debug("%s just moved to %r", self._name, control)
                self._suspended = None

            log.debug("Found %d running, %d needed, starting %d", running, needed, needed-running)
            started = 0
            for instance in range(needed):
                if instance < len(self._proc_state):
                    proc = self._proc_state[instance]
                    if proc.pid is not None:
                        log.debug("%s instance %d already started", self._name, instance)
                        continue
                    if proc.started is None:
                        proc.started = now
                    last_start_delta = now - proc.started
                    if last_start_delta < 0:
                        #  This can happen when the system clock is manually set.  As one
                        #  of the goals here is to restart ntpd when it dies due to exceeding
                        #  the panic threshold (1000 seconds), go ahead and mark the time
                        #  as now so the task restart will only be delayed slightly longer
                        #  than normal.
                        #
                        log.warning("Time flowed backwards, resetting %s instance %d start time",
                                self._name, instance)
                        proc.started = now
                        continue
                    if last_start_delta < reexec_delay:
                        log.debug("%s instance %d restart skipped, last attempt %s ago",
                                self._name, instance, deltafmt(last_start_delta))
                        continue
                else:
                    log.debug("%s growing instance %d", self._name, instance)
                    self._proc_state.append(ProcessState())
                    proc = self._proc_state[instance]

                pid = _exec_process(start_command, self._context, instance=instance, log=log)
                log.debug("Forked pid %d for '%s', %d of %d now running",
                            pid, self._name, len(self.get_pids()), needed)
                self._legion.proc_add(event_target(self, 'proc_exit', key=pid, log=log))
                proc.pid = pid
                proc.started = now
                started += 1

            log.info("Task %s: %d process%s scheduled to start%s",
                    self._name, started, ses(started, 'es'),
                    (' with time limit %s' % (deltafmt(self._limit - now),)) if self._limit else '')
        except Exception as e:
            log.error("Failed to start task '%s' -- %s", self._name, e, exc_info=log.isEnabledFor(logging.DEBUG))
        return False