File: execute_process_nopty.py

package info (click to toggle)
ros-osrf-pycommon 2.1.7-1
  • links: PTS, VCS
  • area: main
  • in suites: sid
  • size: 360 kB
  • sloc: python: 1,726; makefile: 146; xml: 16
file content (143 lines) | stat: -rw-r--r-- 5,543 bytes parent folder | download | duplicates (2)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
# Copyright 2014 Open Source Robotics Foundation, Inc.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#     http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

import errno
import os
import select

from subprocess import PIPE
from subprocess import Popen
from subprocess import STDOUT

import sys

_is_linux = sys.platform.lower().startswith('linux')
_is_windows = sys.platform.lower().startswith('win')


def _process_incoming_lines(incoming, left_over):
    # This function takes the new data, the left over data from last time
    # and returns a list of complete lines (separated by sep) as well as
    # any sep trailing data for the next iteration
    # This function takes and returns bytes only
    combined = (left_over + incoming)
    lines = combined.splitlines(True)
    if not lines:
        return None, left_over
    # Use splitlines because it is magic
    # comparing against os.linesep is not sufficient
    if lines[-1].splitlines() != [lines[-1]]:
        data = b''.join(lines)
        left_over = b''
    else:
        data = b''.join(lines[:-1])
        left_over = lines[-1]
    return data, left_over


def _close_fds(fds_to_close):
    # This function is used to close (if not already closed) any fds used
    for s in fds_to_close:
        if s is None:
            continue
        try:
            os.close(s)
        except OSError as exc:
            # This could raise "OSError: [Errno 9] Bad file descriptor"
            # If it has already been closed, but that's ok
            if "Bad file descriptor" not in "{0}".format(exc):
                raise


def _yield_data(p, fds, left_overs, linesep, fds_to_close=None):
    # This function uses select and subprocess.Popen.poll to collect out
    # from a subprocess until it has finished, yielding it as it goes
    fds_to_close = [] if fds_to_close is None else fds_to_close

    def yield_to_stream(data, stream):
        if stream == fds[0]:
            return data, None, None
        else:
            return None, data, None

    try:
        while p.poll() is None:
            # If Windows
            if _is_windows:
                for stream in fds:
                    # This will not produce the best results, but at least
                    # it will function on Windows. A True IOCP implementation
                    # would be required to get streaming from Windows streams.
                    data = stream.readline()
                    if data:
                        yield yield_to_stream(data, stream)
                continue
            # Otherwise Unix
            try:
                rlist, wlist, xlist = select.select(fds, [], [])
            except select.error as exc:
                # Ignore EINTR
                try:
                    errnum = exc.errno
                except AttributeError:
                    errnum = exc[0]
                if errnum == errno.EINTR:
                    continue
                raise
            for stream in rlist:
                left_over = left_overs[stream]
                fileno = getattr(stream, 'fileno', lambda: stream)()
                try:
                    incoming = os.read(fileno, 1024)
                except OSError as exc:
                    # On Linux, when using a pty, in order to get select
                    # to return when the subprocess finishes, os.close
                    # must be called on the slave pty fd after forking
                    # the subprocess with popen. On some versions of
                    # the Linux kernel this causes an Errno 5 OSError,
                    # "Input/output error". Therefore, I am explicitly
                    # catching and passing on this error. In my testing
                    # this error does not occur repeatedly (it does not
                    # become a busy wait). See:
                    #   http://stackoverflow.com/a/12207447/671658
                    if _is_linux and "Input/output error" in "{0}".format(exc):
                        continue
                    raise
                if not incoming:
                    # In this case, EOF has been reached, see docs for os.read
                    if left_over:
                        yield yield_to_stream(left_over, stream)
                    continue
                data, left_over = _process_incoming_lines(incoming, left_over)
                left_overs[stream] = left_over
                yield yield_to_stream(data, stream)
        # Done
        yield None, None, p.returncode
    finally:
        # Make sure we don't leak file descriptors
        _close_fds(fds_to_close)


def _execute_process_nopty(cmd, cwd, env, shell, stderr_to_stdout=True):
    stderr = STDOUT if stderr_to_stdout else PIPE
    with Popen(
        cmd, stdin=PIPE, stdout=PIPE, stderr=stderr,
        cwd=cwd, env=env, shell=shell, close_fds=False
    ) as p:
        # Left over data from read which isn't a complete line yet
        left_overs = {p.stdout: b'', p.stderr: b''}

        fds = list(filter(None, [p.stdout, p.stderr]))

        yield from _yield_data(p, fds, left_overs, os.linesep)