Beispiel #1
0
class Run(object):
    """Class to handle processes.

    :ivar cmds: The ``cmds`` argument passed to the __init__ method
        (a command line passed in a list, or a list of command lines passed as
        a list of list).
    :ivar status: The exit status. As the exit status is only meaningful after
        the process has exited, its initial value is None.  When a problem
        running the command is detected and a process does not get
        created, its value gets set to the special value 127.
    :ivar out: process standard output  (if instanciated with output = PIPE)
    :ivar err: same as out but for standard error
    :ivar pid: PID. Set to -1 if the command failed to run.
    """
    def __init__(self,
                 cmds,
                 cwd=None,
                 output=PIPE,
                 error=STDOUT,
                 input=None,
                 bg=False,
                 timeout=None,
                 env=None,
                 set_sigpipe=True,
                 parse_shebang=False,
                 ignore_environ=True):
        """Spawn a process.

        :param cmds: two possibilities:
            1) a command line: a tool name and its arguments, passed
            in a list. e.g. ['ls', '-a', '.']
            2) a list of command lines (as defined in (1)): the
            different commands will be piped. This means that
            [['ps', '-a'], ['grep', 'vxsim']] will be equivalent to
            the system command line 'ps -a | grep vxsim'.
        :type cmds: list[str] | list[list[str]]
        :param cwd: directory in which the process should be executed (string
            or None). If None then current directory is used
        :type cwd: str | None
        :param output: can be PIPE (default), a filename string, a fd on an
            already opened file, a python file object or None (for stdout).
        :type output: int | str | file | None
        :param error: same as output or STDOUT, which indicates that the
            stderr data from the applications should be captured into the same
            file handle as for stdout.
        :type error: int | str | file | None
        :param input: same as output
        :type input: int | str | file | None
        :param bg: if True then run in background
        :type bg: bool
        :param timeout: limit execution time (in seconds), None means
            unlimited
        :type timeout: int | None
        :param env: dictionary for environment variables (e.g. os.environ)
        :type env: dict
        :param set_sigpipe: reset SIGPIPE handler to default value
        :type set_sigpipe: bool
        :param parse_shebang: take the #! interpreter line into account
        :type parse_shebang: bool
        :param ignore_environ: Applies only when env parameter is not None.
            When set to True (the default), the only environment variables
            passed to the program are the ones provided by the env parameter.
            Otherwise, the environment passed to the program consists of the
            environment variables currently defined (os.environ) augmented by
            the ones provided in env.
        :type ignore_environ: bool

        :raise OSError: when trying to execute a non-existent file.

        If you specify a filename for output or stderr then file content is
        reseted (equiv. to > in shell). If you prepend the filename with '+'
        then the file will be opened in append mode (equiv. to >> in shell)
        If you prepend the input with '|', then the content of input string
        will be used for process stdin.
        """
        def add_interpreter_command(cmd_line):
            """Add the interpreter defined in the #! line to cmd_line.

            If the #! line cannot be parsed, just return the cmd_line
            unchanged

            On windows, /usr/bin/env will be ignored to avoid a dependency on
            cygwin and /bin/bash & /bin/sh are replaced by $SHELL if defined.
            :param cmd_line: command line
            :type cmd_line: list[str]
            """
            if not parse_shebang:
                # nothing to do
                return cmd_line
            prog = which(cmd_line[0], default=None)
            if prog is None:
                # Not found. Do not modify the command line
                return cmd_line

            with open(prog) as f:
                try:
                    header = f.read()[0:2]
                except UnicodeDecodeError:  # py3-only
                    # unknown header - cannot decode the first two bytes
                    return cmd_line
                if header != "#!":
                    # Unknown header
                    return cmd_line
                # Header found, get the interpreter command in the first line
                f.seek(0)
                line = f.readline()
                interpreter_cmds = [
                    l.strip() for l in line[line.find('!') + 1:].split()
                ]
                # Pass the program path to the interpreter
                if len(cmd_line) > 1:
                    cmd_line = [prog] + list(cmd_line[1:])
                else:
                    cmd_line = [prog]

                if sys.platform == 'win32':  # unix: no cover
                    if interpreter_cmds[0] == '/usr/bin/env':
                        return interpreter_cmds[1:] + cmd_line
                    elif interpreter_cmds[0] in ('/bin/bash', '/bin/sh') and \
                            'SHELL' in os.environ:
                        return [os.environ['SHELL']] + cmd_line
                return interpreter_cmds + cmd_line

        # First resolve output, error and input
        self.input_file = File(input, 'r')
        self.output_file = File(output, 'w')
        self.error_file = File(error, 'w')

        self.status = None
        self.out = ''
        self.err = ''
        self.cmds = []

        if env is not None and not ignore_environ:
            # ignore_environ is False, so get a copy of the current
            # environment and update it with the env dictionnary.
            tmp = os.environ.copy()
            tmp.update(env)
            env = tmp

        rlimit_args = []
        if timeout is not None:
            rlimit = get_rlimit()
            if os.path.exists(rlimit):
                rlimit_args = [rlimit, '%d' % timeout]
            else:
                logger.warning('cannot find rlimit at %s', rlimit)
                rlimit_args = []

        try:
            if isinstance(cmds[0], basestring):
                self.cmds = rlimit_args + list(add_interpreter_command(cmds))
            else:
                self.cmds = [add_interpreter_command(c) for c in cmds]
                self.cmds[0] = rlimit_args + list(self.cmds[0])

            cmdlogger.debug('Run: cd %s; %s',
                            cwd if cwd is not None else os.getcwd(),
                            self.command_line_image())

            if isinstance(cmds[0], basestring):
                popen_args = {
                    'stdin': self.input_file.fd,
                    'stdout': self.output_file.fd,
                    'stderr': self.error_file.fd,
                    'cwd': cwd,
                    'env': env,
                    'universal_newlines': True
                }

                if sys.platform != 'win32' and \
                        set_sigpipe:  # windows: no cover
                    # preexec_fn is no supported on windows
                    popen_args['preexec_fn'] = subprocess_setup

                if WIN_NEW_PG and sys.platform == 'win32':
                    popen_args['creationflags'] = \
                        subprocess.CREATE_NEW_PROCESS_GROUP

                self.internal = Popen(self.cmds, **popen_args)

            else:
                runs = []
                for index, cmd in enumerate(self.cmds):
                    if index == 0:
                        stdin = self.input_file.fd
                    else:
                        stdin = runs[index - 1].stdout

                    # When connecting two processes using a Pipe don't use
                    # universal_newlines mode. Indeed commands transmitting
                    # binary data between them will crash
                    # (e.g. gzip -dc foo.txt | tar -xf -)
                    if index == len(self.cmds) - 1:
                        stdout = self.output_file.fd
                        txt_mode = True
                    else:
                        stdout = subprocess.PIPE
                        txt_mode = False

                    popen_args = {
                        'stdin': stdin,
                        'stdout': stdout,
                        'stderr': self.error_file.fd,
                        'cwd': cwd,
                        'env': env,
                        'universal_newlines': txt_mode
                    }

                    if sys.platform != 'win32' and \
                            set_sigpipe:  # windows: no cover
                        # preexec_fn is no supported on windows
                        popen_args['preexec_fn'] = subprocess_setup

                    if WIN_NEW_PG and sys.platform == 'win32':
                        popen_args['creationflags'] = \
                            subprocess.CREATE_NEW_PROCESS_GROUP

                    try:
                        runs.append(Popen(cmd, **popen_args))
                    except OSError:
                        logger.error('error when spawning %s', cmd)
                        # We have an error (e.g. file not found), try to kill
                        # all processes already started.
                        for p in runs:
                            p.terminate()
                        raise

                    self.internal = runs[-1]

        except Exception as e:  # defensive code
            self.__error(e, self.cmds)
            raise

        self.pid = self.internal.pid

        if not bg:
            self.wait()

    def command_line_image(self):
        """Get shell command line image of the spawned command(s).

        :rtype: str

        This just a convenient wrapper around the function of the same
        name.
        """
        return command_line_image(self.cmds)

    def close_files(self):
        """Close all file descriptors."""
        self.output_file.close()
        self.error_file.close()
        self.input_file.close()

    def __error(self, error, cmds):
        """Set pid to -1 and status to 127 before closing files."""
        self.close_files()
        logger.error(error)

        def not_found(path):
            """Raise OSError.

            :param path: path of the executable
            :type path: str
            """
            logger.error("%s not found", path)
            e3.log.debug('PATH=%s', os.environ['PATH'])
            raise OSError(errno.ENOENT,
                          'No such file or directory, %s not found' % path)

        # Try to send an helpful message if one of the executable has not
        # been found.
        if isinstance(cmds[0], basestring):
            if which(cmds[0], default=None) is None:
                not_found(cmds[0])
        else:
            for cmd in cmds:
                if which(cmd[0], default=None) is None:
                    not_found(cmd[0])

    def wait(self):
        """Wait until process ends and return its status.

        :return: exit code of the process
        :rtype: int
        """
        if self.status is not None:
            # Wait has already been called
            return self.status

        # If there is no pipe in the loop then just do a wait. Otherwise
        # in order to avoid blocked processes due to full pipes, use
        # communicate.
        if self.output_file.fd != subprocess.PIPE and \
                self.error_file.fd != subprocess.PIPE and \
                self.input_file.fd != subprocess.PIPE:
            self.status = self.internal.wait()
        else:
            tmp_input = None
            if self.input_file.fd == subprocess.PIPE:
                tmp_input = self.input_file.get_command()

            (self.out, self.err) = self.internal.communicate(tmp_input)
            self.status = self.internal.returncode

        self.close_files()
        return self.status

    def poll(self):
        """Check the process status and set self.status if available.

        This method checks whether the underlying process has exited
        or not. If it hasn't, then it just returns None immediately.
        Otherwise, it stores the process' exit code in self.status
        and then returns it.

        :return: None if the process is still alive; otherwise, returns
          the process exit status.
        :rtype: int | None
        """
        if self.status is not None:
            # Process is already terminated and wait been called
            return self.status

        result = self.internal.poll()

        if result is not None:
            # Process is finished, call wait to finalize it (closing handles,
            # ...)
            return self.wait()
        else:
            return None

    def kill(self, recursive=True, timeout=3):
        """Kill the process.

        :param recursive: if True, try to kill the complete process tree
        :type recursive: bool
        :param timeout: wait timeout (in seconds) after sending the kill
            signal (when recursive=True)
        :type timeout: int
        """
        if recursive:
            kill_process_tree(self.internal, timeout=timeout)
        else:
            self.internal.kill()

    def interrupt(self):
        """Send SIGINT to the process, kill on Windows."""
        if sys.platform == 'win32':
            self.kill()  # Ctrl-C event is unreliable on Windows
        else:
            self.internal.send_signal(signal.SIGINT)

    def is_running(self):
        """Check whether the process is running.

        :rtype: bool
        """
        if psutil is None:  # defensive code
            # psutil not imported, use our is_running function
            return is_running(self.pid)
        else:
            return self.internal.is_running()

    def children(self):
        """Return list of child processes (using psutil).

        :rtype: list[psutil.Process]
        """
        if psutil is None:  # defensive code
            raise NotImplementedError('Run.children() require psutil')
        return self.internal.children()
Beispiel #2
0
class Run(object):
    """Class to handle processes.

    :ivar cmds: The ``cmds`` argument passed to the __init__ method
        (a command line passed in a list, or a list of command lines passed as
        a list of list).
    :ivar status: The exit status. As the exit status is only meaningful after
        the process has exited, its initial value is None.  When a problem
        running the command is detected and a process does not get
        created, its value gets set to the special value 127.
    :ivar out: process standard output  (if instanciated with output = PIPE)
    :ivar err: same as out but for standard error
    :ivar pid: PID. Set to -1 if the command failed to run.
    """

    def __init__(self, cmds, cwd=None, output=PIPE,
                 error=STDOUT, input=None, bg=False, timeout=None,
                 env=None, set_sigpipe=True, parse_shebang=False,
                 ignore_environ=True, python_executable=sys.executable):
        """Spawn a process.

        :param cmds: two possibilities:
            1) a command line: a tool name and its arguments, passed
            in a list. e.g. ['ls', '-a', '.']
            2) a list of command lines (as defined in (1)): the
            different commands will be piped. This means that
            [['ps', '-a'], ['grep', 'vxsim']] will be equivalent to
            the system command line 'ps -a | grep vxsim'.
        :type cmds: list[str] | list[list[str]]
        :param cwd: directory in which the process should be executed (string
            or None). If None then current directory is used
        :type cwd: str | None
        :param output: can be PIPE (default), a filename string, a fd on an
            already opened file, a python file object or None (for stdout).
        :type output: int | str | file | None
        :param error: same as output or STDOUT, which indicates that the
            stderr data from the applications should be captured into the same
            file handle as for stdout.
        :type error: int | str | file | None
        :param input: same as output
        :type input: int | str | file | None
        :param bg: if True then run in background
        :type bg: bool
        :param timeout: limit execution time (in seconds), None means
            unlimited
        :type timeout: int | None
        :param env: dictionary for environment variables (e.g. os.environ)
        :type env: dict
        :param set_sigpipe: reset SIGPIPE handler to default value
        :type set_sigpipe: bool
        :param parse_shebang: take the #! interpreter line into account
        :type parse_shebang: bool
        :param ignore_environ: Applies only when env parameter is not None.
            When set to True (the default), the only environment variables
            passed to the program are the ones provided by the env parameter.
            Otherwise, the environment passed to the program consists of the
            environment variables currently defined (os.environ) augmented by
            the ones provided in env.
        :type ignore_environ: bool
        :param python_executable: name or path to the python executable
        :type python_executable: str

        :raise OSError: when trying to execute a non-existent file.

        If you specify a filename for output or stderr then file content is
        reseted (equiv. to > in shell). If you prepend the filename with '+'
        then the file will be opened in append mode (equiv. to >> in shell)
        If you prepend the input with '|', then the content of input string
        will be used for process stdin.
        """
        def add_interpreter_command(cmd_line):
            """Add the interpreter defined in the #! line to cmd_line.

            If the #! line cannot be parsed, just return the cmd_line
            unchanged

            If the interpreter command line contains /usr/bin/env python it
            will be replaced by the value of python_executable

            On windows, /usr/bin/env will be ignored to avoid a dependency on
            cygwin
            :param cmd_line: command line
            :type cmd_line: list[str]
            """
            if not parse_shebang:
                # nothing to do
                return cmd_line
            prog = which(cmd_line[0], default=None)
            if prog is None:
                # Not found. Do not modify the command line
                return cmd_line

            with open(prog) as f:
                header = f.read()[0:2]
                if header != "#!":
                    # Unknown header
                    return cmd_line
                # Header found, get the interpreter command in the first line
                f.seek(0)
                line = f.readline()
                interpreter_cmds = [l.strip() for l in
                                    line[line.find('!') + 1:].split()]
                # Pass the program path to the interpreter
                if len(cmd_line) > 1:
                    cmd_line = [prog] + list(cmd_line[1:])
                else:
                    cmd_line = [prog]

                # If the interpreter is '/usr/bin/env python', use
                # python_executable instead to keep the same python executable
                if interpreter_cmds[0:2] == ['/usr/bin/env', 'python']:
                    if len(interpreter_cmds) > 2:
                        return [python_executable] + \
                            interpreter_cmds[2:] + cmd_line
                    else:
                        return [python_executable] + cmd_line
                elif sys.platform == 'win32':  # unix: no cover
                    if interpreter_cmds[0] == '/usr/bin/env':
                        return interpreter_cmds[1:] + cmd_line
                    elif interpreter_cmds[0] in ('/bin/bash', '/bin/sh') and \
                            'SHELL' in os.environ:
                        return [os.environ['SHELL']] + cmd_line
                return interpreter_cmds + cmd_line

        # First resolve output, error and input
        self.input_file = File(input, 'r')
        self.output_file = File(output, 'w')
        self.error_file = File(error, 'w')

        self.status = None
        self.out = ''
        self.err = ''
        self.cmds = []

        if env is not None and not ignore_environ:
            # ignore_environ is False, so get a copy of the current
            # environment and update it with the env dictionnary.
            tmp = os.environ.copy()
            tmp.update(env)
            env = tmp

        rlimit_args = []
        if timeout is not None:
            rlimit = get_rlimit()
            if os.path.exists(rlimit):
                rlimit_args = [rlimit, '%d' % timeout]
            else:
                logger.warning('cannot find rlimit at %s', rlimit)
                rlimit_args = []

        try:
            if isinstance(cmds[0], basestring):
                self.cmds = rlimit_args + list(add_interpreter_command(cmds))
            else:
                self.cmds = [add_interpreter_command(c) for c in cmds]
                self.cmds[0] = rlimit_args + list(self.cmds[0])

            cmdlogger.debug('Run: cd %s; %s' % (
                cwd if cwd is not None else os.getcwd(),
                self.command_line_image()))

            if isinstance(cmds[0], basestring):
                popen_args = {
                    'stdin': self.input_file.fd,
                    'stdout': self.output_file.fd,
                    'stderr': self.error_file.fd,
                    'cwd': cwd,
                    'env': env,
                    'universal_newlines': True}

                if sys.platform != 'win32' and set_sigpipe:
                    # preexec_fn is no supported on windows
                    popen_args['preexec_fn'] = subprocess_setup

                self.internal = Popen(self.cmds, **popen_args)

            else:
                runs = []
                for index, cmd in enumerate(self.cmds):
                    if index == 0:
                        stdin = self.input_file.fd
                    else:
                        stdin = runs[index - 1].stdout

                    # When connecting two processes using a Pipe don't use
                    # universal_newlines mode. Indeed commands transmitting
                    # binary data between them will crash
                    # (e.g. gzip -dc foo.txt | tar -xf -)
                    if index == len(self.cmds) - 1:
                        stdout = self.output_file.fd
                        txt_mode = True
                    else:
                        stdout = subprocess.PIPE
                        txt_mode = False

                    popen_args = {
                        'stdin': stdin,
                        'stdout': stdout,
                        'stderr': self.error_file.fd,
                        'cwd': cwd,
                        'env': env,
                        'universal_newlines': txt_mode}

                    if sys.platform != 'win32' and set_sigpipe:
                        # preexec_fn is no supported on windows
                        popen_args['preexec_fn'] = subprocess_setup

                    try:
                        runs.append(Popen(cmd, **popen_args))
                    except OSError as e:
                        logger.error('error when spawning %s', cmd)
                        # We have an error (e.g. file not found), try to kill
                        # all processes already started.
                        for p in runs:
                            p.terminate()
                        raise

                    self.internal = runs[-1]

        except Exception as e:
            self.__error(e, self.cmds)
            raise

        self.pid = self.internal.pid

        if not bg:
            self.wait()

    def command_line_image(self):
        """Get shell command line image of the spawned command(s).

        :rtype: str

        This just a convenient wrapper around the function of the same
        name.
        """
        return command_line_image(self.cmds)

    def close_files(self):
        """Close all file descriptors."""
        self.output_file.close()
        self.error_file.close()
        self.input_file.close()

    def __error(self, error, cmds):
        """Set pid to -1 and status to 127 before closing files."""
        self.close_files()
        logger.error(error)

        def not_found(path):
            """Raise OSError.

            :param path: path of the executable
            :type path: str
            """
            logger.error("%s not found", path)
            e3.log.debug('PATH=%s', os.environ['PATH'])
            raise OSError(errno.ENOENT,
                          'No such file or directory, %s not found' % path)

        # Try to send an helpful message if one of the executable has not
        # been found.
        if isinstance(cmds[0], basestring):
            if which(cmds[0], default=None) is None:
                not_found(cmds[0])
        else:
            for cmd in cmds:
                if which(cmd[0], default=None) is None:
                    not_found(cmd[0])

    def wait(self):
        """Wait until process ends and return its status.

        :return: exit code of the process
        :rtype: int
        """
        if self.status is not None:
            # Wait has already been called
            return self.status

        # If there is no pipe in the loop then just do a wait. Otherwise
        # in order to avoid blocked processes due to full pipes, use
        # communicate.
        if self.output_file.fd != subprocess.PIPE and \
                self.error_file.fd != subprocess.PIPE and \
                self.input_file.fd != subprocess.PIPE:
            self.status = self.internal.wait()
        else:
            tmp_input = None
            if self.input_file.fd == subprocess.PIPE:
                tmp_input = self.input_file.get_command()

            (self.out, self.err) = self.internal.communicate(tmp_input)
            self.status = self.internal.returncode

        self.close_files()
        return self.status

    def poll(self):
        """Check the process status and set self.status if available.

        This method checks whether the underlying process has exited
        or not. If it hasn't, then it just returns None immediately.
        Otherwise, it stores the process' exit code in self.status
        and then returns it.

        :return: None if the process is still alive; otherwise, returns
          the process exit status.
        :rtype: int | None
        """
        if self.status is not None:
            # Process is already terminated and wait been called
            return self.status

        result = self.internal.poll()

        if result is not None:
            # Process is finished, call wait to finalize it (closing handles,
            # ...)
            return self.wait()
        else:
            return None

    def kill(self):
        """Kill the process."""
        self.internal.kill()

    def interrupt(self):
        """Send SIGINT CTRL_C_EVENT to the process."""
        # On windows CTRL_C_EVENT is available and SIGINT is not;
        # and the other way around on other platforms.
        interrupt_signal = getattr(signal, 'CTRL_C_EVENT', signal.SIGINT)
        self.internal.send_signal(interrupt_signal)

    def is_running(self):
        """Check whether the process is running.

        :rtype: bool
        """
        if psutil is None:
            # psutil not imported, use our is_running function
            return is_running(self.pid)
        else:
            return self.internal.is_running()

    def children(self):
        """Return list of child processes (using psutil).

        :rtype: list[psutil.Process]
        """
        if psutil is None:
            raise NotImplementedError('Run.children() require psutil')
        return self.internal.children()
Beispiel #3
0
class Run:
    """Class to handle processes.

    :ivar cmds: The ``cmds`` argument passed to the __init__ method
        (a command line passed in a list, or a list of command lines passed as
        a list of list).
    :ivar status: The exit status. As the exit status is only meaningful after
        the process has exited, its initial value is None.  When a problem
        running the command is detected and a process does not get
        created, its value gets set to the special value 127.
    :ivar raw_out: process standard output as bytes (if instanciated with
        output = PIPE). Use self.out to get a decoded string.
    :ivar raw_err: same as raw_out but for standard error.
    :ivar pid: PID. Set to -1 if the command failed to run.
    """
    def __init__(
        self,
        cmds: AnyCmdLine,
        cwd: Optional[str] = None,
        output: STDOUT_VALUE | DEVNULL_VALUE | PIPE_VALUE | str | IO
        | None = PIPE,
        error: STDOUT_VALUE | DEVNULL_VALUE | PIPE_VALUE | str | IO
        | None = STDOUT,
        input: DEVNULL_VALUE | PIPE_VALUE | str | IO
        | None = None,  # noqa: A002
        bg: bool = False,
        timeout: Optional[int] = None,
        env: Optional[dict] = None,
        set_sigpipe: bool = True,
        parse_shebang: bool = False,
        ignore_environ: bool = True,
    ) -> None:
        """Spawn a process.

        :param cmds: two possibilities:
            1) a command line: a tool name and its arguments, passed
            in a list. e.g. ['ls', '-a', '.']
            2) a list of command lines (as defined in (1)): the
            different commands will be piped. This means that
            [['ps', '-a'], ['grep', 'vxsim']] will be equivalent to
            the system command line 'ps -a | grep vxsim'.
        :param cwd: directory in which the process should be executed (string
            or None). If None then current directory is used
        :param output: can be PIPE (default), a filename string, a fd on an
            already opened file, a python file object or None (for stdout).
        :param error: same as output or STDOUT, which indicates that the
            stderr data from the applications should be captured into the same
            file handle as for stdout.
        :param input: same as output
        :param bg: if True then run in background
        :param timeout: limit execution time (in seconds), None means
            unlimited
        :param env: dictionary for environment variables (e.g. os.environ)
        :param set_sigpipe: reset SIGPIPE handler to default value
        :param parse_shebang: take the #! interpreter line into account
        :param ignore_environ: Applies only when env parameter is not None.
            When set to True (the default), the only environment variables
            passed to the program are the ones provided by the env parameter.
            Otherwise, the environment passed to the program consists of the
            environment variables currently defined (os.environ) augmented by
            the ones provided in env.

        :raise OSError: when trying to execute a non-existent file.

        If you specify a filename for output or stderr then file content is
        reseted (equiv. to > in shell). If you prepend the filename with '+'
        then the file will be opened in append mode (equiv. to >> in shell)
        If you prepend the input with '|', then the content of input string
        will be used for process stdin.
        """
        def add_interpreter_command(cmd_line: CmdLine) -> CmdLine:
            """Add the interpreter defined in the #! line to cmd_line.

            If the #! line cannot be parsed, just return the cmd_line
            unchanged

            On windows, /usr/bin/env will be ignored to avoid a dependency on
            cygwin and /bin/bash & /bin/sh are replaced by $SHELL if defined.
            :param cmd_line: command line
            """
            if not parse_shebang:
                # nothing to do
                return cmd_line
            prog = which(cmd_line[0], default=None)
            if prog is None:
                # Not found. Do not modify the command line
                return cmd_line

            with open(prog) as f:
                try:
                    header = f.read()[0:2]
                except UnicodeDecodeError:
                    # unknown header - cannot decode the first two bytes
                    return cmd_line
                if header != "#!":
                    # Unknown header
                    return cmd_line
                # Header found, get the interpreter command in the first line
                f.seek(0)
                line = f.readline()
                interpreter_cmds = [
                    word.strip() for word in line[line.find("!") + 1:].split()
                ]
                # Pass the program path to the interpreter
                if len(cmd_line) > 1:
                    cmd_line = [prog] + list(cmd_line[1:])
                else:
                    cmd_line = [prog]

                if sys.platform == "win32":  # unix: no cover
                    if interpreter_cmds[0] == "/usr/bin/env":
                        # On windows be sure that PATH is taken into account by
                        # using which. In some cases involving python
                        # interpreter, the python interpreter used to run this
                        # module has been used rather than the first one on the
                        # path.
                        interpreter_cmds[1] = which(
                            interpreter_cmds[1], default=interpreter_cmds[1])
                        return interpreter_cmds[1:] + cmd_line
                    elif (interpreter_cmds[0] in ("/bin/bash", "/bin/sh")
                          and "SHELL" in os.environ):
                        return [os.environ["SHELL"]] + cmd_line
                return interpreter_cmds + cmd_line

        # First resolve output, error and input
        self.input_file = File(input, "r")
        self.output_file = File(output, "w")
        self.error_file = File(error, "w")

        self.status: Optional[int] = None
        self.raw_out = b""
        self.raw_err = b""
        self.cmds = []

        if env is not None:
            if ignore_environ:
                if sys.platform == "win32":
                    # On Windows not all environment variables can be
                    # discarded. At least SYSTEMDRIVE, SYSTEMROOT should be
                    # set. In order to be portable propagate their value in
                    # case the user does not pass them in env when
                    # ignore_environ is set to True.
                    tmp = {}
                    for var in ("SYSTEMDRIVE", "SYSTEMROOT"):
                        if var not in env and var in os.environ:
                            tmp[var] = os.environ[var]
                    tmp.update(env)
                    env = tmp
            else:
                # ignore_environ is False, so get a copy of the current
                # environment and update it with the env dictionary.
                tmp = os.environ.copy()
                tmp.update(env)
                env = tmp

        rlimit_args = []
        if timeout is not None:
            rlimit = get_rlimit()
            if os.path.exists(rlimit):
                rlimit_args = [rlimit, "%d" % timeout]
            else:
                logger.warning("cannot find rlimit at %s", rlimit)
                rlimit_args = []

        try:
            self.cmds = [
                add_interpreter_command(c) for c in to_cmd_lines(cmds)
            ]
            self.cmds[0] = rlimit_args + list(self.cmds[0])

            cmdlogger.debug(
                "Run: cd %s; %s",
                cwd if cwd is not None else os.getcwd(),
                self.command_line_image(),
            )

            if len(self.cmds) == 1:
                popen_args = {
                    "stdin": self.input_file.fd,
                    "stdout": self.output_file.fd,
                    "stderr": self.error_file.fd,
                    "cwd": cwd,
                    "env": env,
                    "universal_newlines": False,
                }

                if sys.platform != "win32" and set_sigpipe:  # windows: no cover
                    # preexec_fn is no supported on windows
                    popen_args["preexec_fn"] = subprocess_setup  # type: ignore

                if sys.platform == "win32":
                    popen_args[
                        "creationflags"] = subprocess.CREATE_NEW_PROCESS_GROUP

                self.internal = Popen(self.cmds[0], **popen_args)

            else:
                runs: list[subprocess.Popen] = []
                for index, cmd in enumerate(self.cmds):
                    if index == 0:
                        stdin: int | IO[Any] = self.input_file.fd
                    else:
                        previous_stdout = runs[index - 1].stdout
                        assert previous_stdout is not None
                        stdin = previous_stdout

                    # When connecting two processes using a Pipe don't use
                    # universal_newlines mode. Indeed commands transmitting
                    # binary data between them will crash
                    # (e.g. gzip -dc foo.txt | tar -xf -)
                    if index == len(self.cmds) - 1:
                        stdout = self.output_file.fd
                    else:
                        stdout = subprocess.PIPE

                    popen_args = {
                        "stdin": stdin,
                        "stdout": stdout,
                        "stderr": self.error_file.fd,
                        "cwd": cwd,
                        "env": env,
                        "universal_newlines": False,
                    }

                    if sys.platform != "win32" and set_sigpipe:  # windows: no cover
                        # preexec_fn is no supported on windows
                        popen_args[
                            "preexec_fn"] = subprocess_setup  # type: ignore

                    if sys.platform == "win32":
                        popen_args[
                            "creationflags"] = subprocess.CREATE_NEW_PROCESS_GROUP

                    try:
                        runs.append(Popen(cmd, **popen_args))
                    except OSError:
                        logger.error("error when spawning %s", cmd)
                        # We have an error (e.g. file not found), try to kill
                        # all processes already started.
                        for p in runs:
                            p.terminate()
                        raise

                    self.internal = runs[-1]

        except Exception as e:  # defensive code
            self.__error(e, self.cmds)
            raise

        self.pid = self.internal.pid

        if not bg:
            self.wait()

    @property
    def out(self) -> str:
        """Process output as string.

        Attempt is done to decode as utf-8 the output. If the output is not in
        utf-8 a string representation will be returned
        (see e3.text.bytes_as_str).
        """
        return bytes_as_str(self.raw_out)

    @property
    def err(self) -> str:
        """Process error as string.

        Attempt is done to decode as utf-8 the output. If the output is not in
        utf-8 a string representation will be returned
        (see e3.text.bytes_as_str).
        """
        return bytes_as_str(self.raw_err)

    def command_line_image(self) -> str:
        """Get shell command line image of the spawned command(s).

        This just a convenient wrapper around the function of the same
        name.
        """
        return command_line_image(self.cmds)

    def close_files(self) -> None:
        """Close all file descriptors."""
        self.output_file.close()
        self.error_file.close()
        self.input_file.close()

    def __error(self, error: Exception, cmds: list[CmdLine]) -> None:
        """Set pid to -1 and status to 127 before closing files."""
        self.close_files()
        logger.error(error)

        def not_found(path: str) -> NoReturn:
            """Raise OSError.

            :param path: path of the executable
            """
            logger.error("%s not found", path)
            e3.log.debug("PATH=%s", os.environ["PATH"])
            raise OSError(errno.ENOENT,
                          f"No such file or directory, {path} not found")

        # Try to send an helpful message if one of the executable has not
        # been found.
        for cmd in cmds:
            if which(cmd[0], default=None) is None:
                not_found(cmd[0])

    def wait(self) -> int:
        """Wait until process ends and return its status.

        :return: exit code of the process
        """
        if self.status is not None:
            # Wait has already been called
            return self.status

        # If there is no pipe in the loop then just do a wait. Otherwise
        # in order to avoid blocked processes due to full pipes, use
        # communicate.
        if (self.output_file.fd != subprocess.PIPE
                and self.error_file.fd != subprocess.PIPE
                and self.input_file.fd != subprocess.PIPE):
            self.status = self.internal.wait()
        else:
            tmp_input: Optional[str | bytes] = None
            if self.input_file.fd == subprocess.PIPE:
                tmp_input = self.input_file.get_command()

            if isinstance(tmp_input, str):
                tmp_input = tmp_input.encode("utf-8")

            (self.raw_out, self.raw_err) = self.internal.communicate(tmp_input)
            self.status = self.internal.returncode

        self.close_files()
        return self.status

    def poll(self) -> Optional[int]:
        """Check the process status and set self.status if available.

        This method checks whether the underlying process has exited
        or not. If it hasn't, then it just returns None immediately.
        Otherwise, it stores the process' exit code in self.status
        and then returns it.

        :return: None if the process is still alive; otherwise, returns
          the process exit status.
        """
        if self.status is not None:
            # Process is already terminated and wait been called
            return self.status

        result = self.internal.poll()

        if result is not None:
            # Process is finished, call wait to finalize it (closing handles,
            # ...)
            return self.wait()
        else:
            return None

    def kill(self, recursive: bool = True, timeout: int = 3) -> None:
        """Kill the process.

        :param recursive: if True, try to kill the complete process tree
        :param timeout: wait timeout (in seconds) after sending the kill
            signal (when recursive=True)
        """
        if recursive:
            kill_process_tree(self.internal, timeout=timeout)
        else:
            self.internal.kill()

    def interrupt(self) -> None:
        """Send SIGINT to the process, kill on Windows."""
        if sys.platform == "win32":
            self.kill()  # Ctrl-C event is unreliable on Windows
        else:
            self.internal.send_signal(signal.SIGINT)

    def is_running(self) -> bool:
        """Check whether the process is running."""
        if psutil is None:  # defensive code
            # psutil not imported, use our is_running function
            return is_running(self.pid)
        else:
            return self.internal.is_running()

    def children(self) -> list[Any]:
        """Return list of child processes (using psutil)."""
        if psutil is None:  # defensive code
            raise NotImplementedError("Run.children() require psutil")
        return self.internal.children()
Beispiel #4
0
    config['general']['random_bind_ip'] = False
if not args.use_tor:
    config['transports']['tor'] = False
if not args.animated_background:
    config['ui']['animated_background'] = False
if args.keep_log_on_exit:
    config['log']['file']['remove_on_exit'] = True
else:
    config['log']['file']['remove_on_exit'] = False

config['general']['upload_mixing'] = False
if args.use_upload_mixing:
    config['general']['upload_mixing'] = True
config['general']['display_header'] = False
config['general']['security_level'] = args.security_level

with open(config_file, 'w') as cf:
    cf.write(ujson.dumps(config, reject_bytes=False))

if args.open_ui:
    p = Popen([sub_script, 'start'], stdout=DEVNULL)
    sleep(2)
    Popen([sub_script, 'openhome'], stdout=DEVNULL)
else:
    p = Popen([sub_script, 'start'], stdout=DEVNULL)

p = p.children()[0]
if args.show_stats:
    Thread(target=show_info, args=[p], daemon=True).start()
p.wait()