File: fork.py

package info (click to toggle)
python-mitogen 0.3.25~a2-1
  • links: PTS, VCS
  • area: main
  • in suites: sid, trixie
  • size: 6,220 kB
  • sloc: python: 21,989; sh: 183; makefile: 74; perl: 19; ansic: 18
file content (250 lines) | stat: -rw-r--r-- 8,436 bytes parent folder | download | duplicates (3)
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
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
# Copyright 2019, David Wilson
#
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions are met:
#
# 1. Redistributions of source code must retain the above copyright notice,
# this list of conditions and the following disclaimer.
#
# 2. Redistributions in binary form must reproduce the above copyright notice,
# this list of conditions and the following disclaimer in the documentation
# and/or other materials provided with the distribution.
#
# 3. Neither the name of the copyright holder nor the names of its contributors
# may be used to endorse or promote products derived from this software without
# specific prior written permission.
#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
# POSSIBILITY OF SUCH DAMAGE.

# !mitogen: minify_safe

import errno
import logging
import os
import random
import sys
import threading
import traceback

import mitogen.core
import mitogen.parent
from mitogen.core import b


LOG = logging.getLogger(__name__)

# Python 2.4/2.5 cannot support fork+threads whatsoever, it doesn't even fix up
# interpreter state. So 2.4/2.5 interpreters start .local() contexts for
# isolation instead. Since we don't have any crazy memory sharing problems to
# avoid, there is no virginal fork parent either. The child is started directly
# from the login/become process. In future this will be default everywhere,
# fork is brainwrong from the stone age.
FORK_SUPPORTED = sys.version_info >= (2, 6)


class Error(mitogen.core.StreamError):
    pass


def fixup_prngs():
    """
    Add 256 bits of /dev/urandom to OpenSSL's PRNG in the child, and re-seed
    the random package with the same data.
    """
    s = os.urandom(256 // 8)
    random.seed(s)
    if 'ssl' in sys.modules:
        sys.modules['ssl'].RAND_add(s, 75.0)


def reset_logging_framework():
    """
    After fork, ensure any logging.Handler locks are recreated, as a variety of
    threads in the parent may have been using the logging package at the moment
    of fork.

    It is not possible to solve this problem in general; see :gh:issue:`150`
    for a full discussion.
    """
    logging._lock = threading.RLock()

    # The root logger does not appear in the loggerDict.
    logging.Logger.manager.loggerDict = {}
    logging.getLogger().handlers = []


def on_fork():
    """
    Should be called by any program integrating Mitogen each time the process
    is forked, in the context of the new child.
    """
    reset_logging_framework()  # Must be first!
    fixup_prngs()
    mitogen.core.Latch._on_fork()
    mitogen.core.Side._on_fork()
    mitogen.core.ExternalContext.service_stub_lock = threading.Lock()

    mitogen__service = sys.modules.get('mitogen.service')
    if mitogen__service:
        mitogen__service._pool_lock = threading.Lock()


def handle_child_crash():
    """
    Respond to _child_main() crashing by ensuring the relevant exception is
    logged to /dev/tty.
    """
    tty = open('/dev/tty', 'wb')
    tty.write('\n\nFORKED CHILD PID %d CRASHED\n%s\n\n' % (
        os.getpid(),
        traceback.format_exc(),
    ))
    tty.close()
    os._exit(1)


def _convert_exit_status(status):
    """
    Convert a :func:`os.waitpid`-style exit status to a :mod:`subprocess` style
    exit status.
    """
    if os.WIFEXITED(status):
        return os.WEXITSTATUS(status)
    elif os.WIFSIGNALED(status):
        return -os.WTERMSIG(status)
    elif os.WIFSTOPPED(status):
        return -os.WSTOPSIG(status)


class Process(mitogen.parent.Process):
    def poll(self):
        try:
            pid, status = os.waitpid(self.pid, os.WNOHANG)
        except OSError:
            e = sys.exc_info()[1]
            if e.args[0] == errno.ECHILD:
                LOG.warn('%r: waitpid(%r) produced ECHILD', self, self.pid)
                return
            raise

        if not pid:
            return
        return _convert_exit_status(status)


class Options(mitogen.parent.Options):
    #: Reference to the importer, if any, recovered from the parent.
    importer = None

    #: User-supplied function for cleaning up child process state.
    on_fork = None

    def __init__(self, old_router, max_message_size, on_fork=None, debug=False,
                 profiling=False, unidirectional=False, on_start=None,
                 name=None):
        if not FORK_SUPPORTED:
            raise Error(self.python_version_msg)

        # fork method only supports a tiny subset of options.
        super(Options, self).__init__(
            max_message_size=max_message_size, debug=debug,
            profiling=profiling, unidirectional=unidirectional, name=name,
        )
        self.on_fork = on_fork
        self.on_start = on_start

        responder = getattr(old_router, 'responder', None)
        if isinstance(responder, mitogen.parent.ModuleForwarder):
            self.importer = responder.importer


class Connection(mitogen.parent.Connection):
    options_class = Options
    child_is_immediate_subprocess = True

    python_version_msg = (
        "The mitogen.fork method is not supported on Python versions "
        "prior to 2.6, since those versions made no attempt to repair "
        "critical interpreter state following a fork. Please use the "
        "local() method instead."
    )

    name_prefix = u'fork'

    def start_child(self):
        parentfp, childfp = mitogen.parent.create_socketpair()
        pid = os.fork()
        if pid:
            childfp.close()
            return Process(pid, stdin=parentfp, stdout=parentfp)
        else:
            parentfp.close()
            self._wrap_child_main(childfp)

    def _wrap_child_main(self, childfp):
        try:
            self._child_main(childfp)
        except BaseException:
            handle_child_crash()

    def get_econtext_config(self):
        config = super(Connection, self).get_econtext_config()
        config['core_src_fd'] = None
        config['importer'] = self.options.importer
        config['send_ec2'] = False
        config['setup_package'] = False
        if self.options.on_start:
            config['on_start'] = self.options.on_start
        return config

    def _child_main(self, childfp):
        on_fork()
        if self.options.on_fork:
            self.options.on_fork()
        mitogen.core.set_block(childfp.fileno())

        childfp.send(b('MITO002\n'))

        # Expected by the ExternalContext.main().
        os.dup2(childfp.fileno(), 1)
        os.dup2(childfp.fileno(), 100)

        # Overwritten by ExternalContext.main(); we must replace the
        # parent-inherited descriptors that were closed by Side._on_fork() to
        # avoid ExternalContext.main() accidentally allocating new files over
        # the standard handles.
        os.dup2(childfp.fileno(), 0)

        # Avoid corrupting the stream on fork crash by dupping /dev/null over
        # stderr. Instead, handle_child_crash() uses /dev/tty to log errors.
        devnull = os.open('/dev/null', os.O_WRONLY)
        if devnull != 2:
            os.dup2(devnull, 2)
            os.close(devnull)

        # If we're unlucky, childfp.fileno() may coincidentally be one of our
        # desired FDs. In that case closing it breaks ExternalContext.main().
        if childfp.fileno() not in (0, 1, 100):
            childfp.close()

        mitogen.core.IOLOG.setLevel(logging.INFO)

        try:
            try:
                mitogen.core.ExternalContext(self.get_econtext_config()).main()
            except Exception:
                # TODO: report exception somehow.
                os._exit(72)
        finally:
            # Don't trigger atexit handlers, they were copied from the parent.
            os._exit(0)