File: child.py

package info (click to toggle)
python-ptrace 0.9.9-0.3
  • links: PTS
  • area: main
  • in suites: forky, sid
  • size: 808 kB
  • sloc: python: 10,167; ansic: 263; makefile: 164
file content (173 lines) | stat: -rw-r--r-- 4,389 bytes parent folder | download | duplicates (4)
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
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
"""
Error pipe and serialization code comes from Python 2.5 subprocess module.
"""
from os import (
    fork, execvp, execvpe, waitpid,
    close, dup2, pipe,
    read, write, devnull, sysconf, set_inheritable)
from sys import exc_info
from traceback import format_exception
from ptrace.binding import ptrace_traceme
from ptrace import PtraceError
from sys import exit
from errno import EINTR
import fcntl
import pickle

try:
    MAXFD = sysconf("SC_OPEN_MAX")
except Exception:
    MAXFD = 256


class ChildError(RuntimeError):
    pass


class ChildPtraceError(ChildError):
    pass


def _set_cloexec_flag(fd):
    try:
        cloexec_flag = fcntl.FD_CLOEXEC
    except AttributeError:
        cloexec_flag = 1

    old = fcntl.fcntl(fd, fcntl.F_GETFD)
    fcntl.fcntl(fd, fcntl.F_SETFD, old | cloexec_flag)


def _waitpid_no_intr(pid, options):
    """Like os.waitpid, but retries on EINTR"""
    while True:
        try:
            return waitpid(pid, options)
        except OSError as e:
            if e.errno == EINTR:
                continue
            else:
                raise


def _read_no_intr(fd, buffersize):
    """Like os.read, but retries on EINTR"""
    while True:
        try:
            return read(fd, buffersize)
        except OSError as e:
            if e.errno == EINTR:
                continue
            else:
                raise


def _write_no_intr(fd, s):
    """Like os.write, but retries on EINTR"""
    while True:
        try:
            return write(fd, s)
        except OSError as e:
            if e.errno == EINTR:
                continue
            else:
                raise


def _createParent(pid, errpipe_read):
    # Wait for exec to fail or succeed; possibly raising exception
    data = _read_no_intr(errpipe_read, 1048576)  # Exceptions limited to 1 MB
    close(errpipe_read)
    if data:
        _waitpid_no_intr(pid, 0)
        child_exception = pickle.loads(data)
        raise child_exception


def _createChild(arguments,
                 no_stdout,
                 env,
                 errpipe_write,
                 close_fds=True,
                 pass_fds=()):
    # Child code
    try:
        ptrace_traceme()
    except PtraceError as err:
        raise ChildError(str(err))

    for fd in pass_fds:
        set_inheritable(fd, True)

    if close_fds:
        # Close all files except 0, 1, 2 and errpipe_write
        for fd in range(3, MAXFD):
            if fd == errpipe_write or (fd in pass_fds):
                continue
            try:
                close(fd)
            except OSError:
                pass

    try:
        _execChild(arguments, no_stdout, env)
    except:   # noqa: E722
        exc_type, exc_value, tb = exc_info()
        # Save the traceback and attach it to the exception object
        exc_lines = format_exception(exc_type, exc_value, tb)
        exc_value.child_traceback = ''.join(exc_lines)
        _write_no_intr(errpipe_write, pickle.dumps(exc_value))
    exit(255)


def _execChild(arguments, no_stdout, env):
    if no_stdout:
        try:
            null = open(devnull, 'wb')
            dup2(null.fileno(), 1)
            dup2(1, 2)
            null.close()
        except IOError:
            close(2)
            close(1)
    try:
        if env is not None:
            execvpe(arguments[0], arguments, env)
        else:
            execvp(arguments[0], arguments)
    except Exception as err:
        raise ChildError(str(err))


def createChild(arguments, no_stdout, env=None, close_fds=True, pass_fds=()):
    """
    Create a child process:
     - arguments: list of string where (e.g. ['ls', '-la'])
     - no_stdout: if True, use null device for stdout/stderr
     - env: environment variables dictionary

    Use:
     - env={} to start with an empty environment
     - env=None (default) to copy the environment
    """

    if pass_fds and not close_fds:
        close_fds = True

    errpipe_read, errpipe_write = pipe()
    _set_cloexec_flag(errpipe_write)

    # Fork process
    pid = fork()
    if pid:
        close(errpipe_write)
        _createParent(pid, errpipe_read)
        return pid
    else:
        close(errpipe_read)
        _createChild(arguments,
                     no_stdout,
                     env,
                     errpipe_write,
                     close_fds=close_fds,
                     pass_fds=pass_fds)