Manhole.configure()   F
last analyzed

Complexity

Conditions 16

Size

Total Lines 40

Duplication

Lines 0
Ratio 0 %

Importance

Changes 4
Bugs 0 Features 0
Metric Value
cc 16
c 4
b 0
f 0
dl 0
loc 40
rs 2.7326

How to fix   Complexity   

Complexity

Complex classes like Manhole.configure() often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

1
from __future__ import print_function
2
3
import atexit
4
import code
5
import errno
6
import os
7
import signal
8
import socket
9
import struct
10
import sys
11
import traceback
12
from contextlib import closing
13
14
__version__ = '1.5.0'
15
16
try:
17
    import signalfd
18
except ImportError:
19
    signalfd = None
20
try:
21
    string = basestring
22
except NameError:  # python 3
23
    string = str
24
try:
25
    InterruptedError = InterruptedError
26
except NameError:  # python <= 3.2
27
    InterruptedError = OSError
28
if hasattr(sys, 'setswitchinterval'):
29
    setinterval = sys.setswitchinterval
30
    getinterval = sys.getswitchinterval
31
else:
32
    setinterval = sys.setcheckinterval
33
    getinterval = sys.getcheckinterval
34
35
try:
36
    from eventlet.patcher import original as _original
37
38
    def _get_original(mod, name):
39
        return getattr(_original(mod), name)
40
except ImportError:
41
    try:
42
        from gevent.monkey import get_original as _get_original
43
    except ImportError:
44
        def _get_original(mod, name):
45
            return getattr(__import__(mod), name)
46
47
_ORIGINAL_SOCKET = _get_original('socket', 'socket')
48
_ORIGINAL_FROMFD = _get_original('socket', 'fromfd')
49
_ORIGINAL_FDOPEN = _get_original('os', 'fdopen')
50
_ORIGINAL_DUP = _get_original('os', 'dup')
51
_ORIGINAL_DUP2 = _get_original('os', 'dup2')
52
try:
53
    _ORIGINAL_ALLOCATE_LOCK = _get_original('thread', 'allocate_lock')
54
except ImportError:  # python 3
55
    _ORIGINAL_ALLOCATE_LOCK = _get_original('_thread', 'allocate_lock')
56
_ORIGINAL_THREAD = _get_original('threading', 'Thread')
57
_ORIGINAL_EVENT = _get_original('threading', 'Event')
58
_ORIGINAL__ACTIVE = _get_original('threading', '_active')
59
_ORIGINAL_SLEEP = _get_original('time', 'sleep')
60
61
PY3 = sys.version_info[0] == 3
62
PY26 = sys.version_info[:2] == (2, 6)
63
64
try:
65
    import ctypes
66
    import ctypes.util
67
68
    libpthread_path = ctypes.util.find_library("pthread")
69
    if not libpthread_path:
70
        raise ImportError
71
    libpthread = ctypes.CDLL(libpthread_path)
72
    if not hasattr(libpthread, "pthread_setname_np"):
73
        raise ImportError
74
    _pthread_setname_np = libpthread.pthread_setname_np
75
    _pthread_setname_np.argtypes = [ctypes.c_void_p, ctypes.c_char_p]
76
    _pthread_setname_np.restype = ctypes.c_int
77
78
    def pthread_setname_np(ident, name):
79
        _pthread_setname_np(ident, name[:15].encode('utf8'))
80
except ImportError:
81
    def pthread_setname_np(ident, name):
82
        pass
83
84
if sys.platform == 'darwin' or sys.platform.startswith("freebsd"):
85
    _PEERCRED_LEVEL = getattr(socket, 'SOL_LOCAL', 0)
86
    _PEERCRED_OPTION = getattr(socket, 'LOCAL_PEERCRED', 1)
87
else:
88
    _PEERCRED_LEVEL = socket.SOL_SOCKET
89
    # TODO: Is this missing on some platforms?
90
    _PEERCRED_OPTION = getattr(socket, 'SO_PEERCRED', 17)
91
92
_ALL_SIGNALS = tuple(getattr(signal, sig) for sig in dir(signal)
93
                     if sig.startswith('SIG') and '_' not in sig)
94
95
# These (_LOG and _MANHOLE) will hold instances after install
96
_MANHOLE = None
97
_LOCK = _ORIGINAL_ALLOCATE_LOCK()
98
99
100
def force_original_socket(sock):
101
    with closing(sock):
102
        if hasattr(sock, 'detach'):
103
            return _ORIGINAL_SOCKET(sock.family, sock.type, sock.proto, sock.detach())
104
        else:
105
            assert hasattr(_ORIGINAL_SOCKET, '_sock')
106
            return _ORIGINAL_SOCKET(_sock=sock._sock)
107
108
109
def get_peercred(sock):
110
    """Gets the (pid, uid, gid) for the client on the given *connected* socket."""
111
    buf = sock.getsockopt(_PEERCRED_LEVEL, _PEERCRED_OPTION, struct.calcsize('3i'))
112
    return struct.unpack('3i', buf)
113
114
115
class AlreadyInstalled(Exception):
116
    pass
117
118
119
class NotInstalled(Exception):
120
    pass
121
122
123
class ConfigurationConflict(Exception):
124
    pass
125
126
127
class SuspiciousClient(Exception):
128
    pass
129
130
131
class ManholeThread(_ORIGINAL_THREAD):
132
    """
133
    Thread that runs the infamous "Manhole". This thread is a `daemon` thread - it will exit if the main thread
134
    exits.
135
136
    On connect, a different, non-daemon thread will be started - so that the process won't exit while there's a
137
    connection to the manhole.
138
139
    Args:
140
        sigmask (list of signal numbers): Signals to block in this thread.
141
        start_timeout (float): Seconds to wait for the thread to start. Emits a message if the thread is not running
142
            when calling ``start()``.
143
        bind_delay (float): Seconds to delay socket binding. Default: `no delay`.
144
        daemon_connection (bool): The connection thread is daemonic (dies on app exit). Default: ``False``.
145
    """
146
147
    def __init__(self,
148
                 get_socket, sigmask, start_timeout, connection_handler,
149
                 bind_delay=None, daemon_connection=False):
150
        super(ManholeThread, self).__init__()
151
        self.daemon = True
152
        self.daemon_connection = daemon_connection
153
        self.name = "Manhole"
154
        self.sigmask = sigmask
155
        self.serious = _ORIGINAL_EVENT()
156
        # time to wait for the manhole to get serious (to have a complete start)
157
        # see: http://emptysqua.re/blog/dawn-of-the-thread/
158
        self.start_timeout = start_timeout
159
        self.bind_delay = bind_delay
160
        self.connection_handler = connection_handler
161
        self.get_socket = get_socket
162
        self.should_run = False
163
164
    def stop(self):
165
        self.should_run = False
166
167
    def clone(self, **kwargs):
168
        """
169
        Make a fresh thread with the same options. This is usually used on dead threads.
170
        """
171
        return ManholeThread(
172
            self.get_socket, self.sigmask, self.start_timeout,
173
            connection_handler=self.connection_handler,
174
            daemon_connection=self.daemon_connection,
175
            **kwargs
176
        )
177
178
    def start(self):
179
        self.should_run = True
180
        super(ManholeThread, self).start()
181
        if not self.serious.wait(self.start_timeout) and not PY26:
182
            _LOG("WARNING: Waited %s seconds but Manhole thread didn't start yet :(" % self.start_timeout)
183
184
    def run(self):
185
        """
186
        Runs the manhole loop. Only accepts one connection at a time because:
187
188
        * This thread is a daemon thread (exits when main thread exists).
189
        * The connection need exclusive access to stdin, stderr and stdout so it can redirect inputs and outputs.
190
        """
191
        self.serious.set()
192
        if signalfd and self.sigmask:
193
            signalfd.sigprocmask(signalfd.SIG_BLOCK, self.sigmask)
194
        pthread_setname_np(self.ident, self.name)
195
196
        if self.bind_delay:
197
            _LOG("Delaying UDS binding %s seconds ..." % self.bind_delay)
198
            _ORIGINAL_SLEEP(self.bind_delay)
199
200
        sock = self.get_socket()
201
        while self.should_run:
202
            _LOG("Waiting for new connection (in pid:%s) ..." % os.getpid())
203
            try:
204
                client = ManholeConnectionThread(sock.accept()[0], self.connection_handler, self.daemon_connection)
205
                client.start()
206
                client.join()
207
            except (InterruptedError, socket.error) as e:
208
                if e.errno != errno.EINTR:
209
                    raise
210
                continue
211
            finally:
212
                client = None
213
214
215
class ManholeConnectionThread(_ORIGINAL_THREAD):
216
    """
217
    Manhole thread that handles the connection. This thread is a normal thread (non-daemon) - it won't exit if the
218
    main thread exits.
219
    """
220
221
    def __init__(self, client, connection_handler, daemon=False):
222
        super(ManholeConnectionThread, self).__init__()
223
        self.daemon = daemon
224
        self.client = force_original_socket(client)
225
        self.connection_handler = connection_handler
226
        self.name = "ManholeConnectionThread"
227
228
    def run(self):
229
        _LOG('Started ManholeConnectionThread thread. Checking credentials ...')
230
        pthread_setname_np(self.ident, "Manhole -------")
231
        pid, _, _ = check_credentials(self.client)
232
        pthread_setname_np(self.ident, "Manhole < PID:%s" % pid)
233
        try:
234
            self.connection_handler(self.client)
235
        except BaseException as exc:
236
            _LOG("ManholeConnectionThread failure: %r" % exc)
237
238
239
def check_credentials(client):
240
    """
241
    Checks credentials for given socket.
242
    """
243
    pid, uid, gid = get_peercred(client)
244
245
    euid = os.geteuid()
246
    client_name = "PID:%s UID:%s GID:%s" % (pid, uid, gid)
247
    if uid not in (0, euid):
248
        raise SuspiciousClient("Can't accept client with %s. It doesn't match the current EUID:%s or ROOT." % (
249
            client_name, euid
250
        ))
251
252
    _LOG("Accepted connection on fd:%s from %s" % (client.fileno(), client_name))
253
    return pid, uid, gid
254
255
256
def handle_connection_exec(client):
257
    """
258
    Alternate connection handler. No output redirection.
259
    """
260
    class ExitExecLoop(Exception):
261
        pass
262
263
    def exit():
264
        raise ExitExecLoop()
265
266
    client.settimeout(None)
267
    fh = os.fdopen(client.detach() if hasattr(client, 'detach') else client.fileno())
268
269
    with closing(client):
270
        with closing(fh):
271
            try:
272
                payload = fh.readline()
273
                while payload:
274
                    _LOG("Running: %r." % payload)
275
                    eval(compile(payload, '<manhole>', 'exec'), {'exit': exit}, _MANHOLE.locals)
276
                    payload = fh.readline()
277
            except ExitExecLoop:
278
                _LOG("Exiting exec loop.")
279
280
281
def handle_connection_repl(client):
282
    """
283
    Handles connection.
284
    """
285
    client.settimeout(None)
286
    # # disable this till we have evidence that it's needed
287
    # client.setsockopt(socket.SOL_SOCKET, socket.SO_SNDBUF, 0)
288
    # # Note: setting SO_RCVBUF on UDS has no effect, see: http://man7.org/linux/man-pages/man7/unix.7.html
289
290
    backup = []
291
    old_interval = getinterval()
292
    patches = [('r', ('stdin', '__stdin__')), ('w', ('stdout', '__stdout__'))]
293
    if _MANHOLE.redirect_stderr:
294
        patches.append(('w', ('stderr', '__stderr__')))
295
    try:
296
        client_fd = client.fileno()
297
        for mode, names in patches:
298
            for name in names:
299
                backup.append((name, getattr(sys, name)))
300
                setattr(sys, name, _ORIGINAL_FDOPEN(client_fd, mode, 1 if PY3 else 0))
301
        try:
302
            handle_repl(_MANHOLE.locals)
303
        except Exception as exc:
304
            _LOG("REPL failed with %r." % exc)
305
        _LOG("DONE.")
306
    finally:
307
        try:
308
            # Change the switch/check interval to something ridiculous. We don't want to have other thread try
309
            # to write to the redirected sys.__std*/sys.std* - it would fail horribly.
310
            setinterval(2147483647)
311
            try:
312
                client.close()  # close before it's too late. it may already be dead
313
            except IOError:
314
                pass
315
            junk = []  # keep the old file objects alive for a bit
316
            for name, fh in backup:
317
                junk.append(getattr(sys, name))
318
                setattr(sys, name, fh)
319
            del backup
320
            for fh in junk:
321
                try:
322
                    if hasattr(fh, 'detach'):
323
                        fh.detach()
324
                    else:
325
                        fh.close()
326
                except IOError:
327
                    pass
328
                del fh
329
            del junk
330
        finally:
331
            setinterval(old_interval)
332
            _LOG("Cleaned up.")
333
334
335
_CONNECTION_HANDLER_ALIASES = {
336
    'repl': handle_connection_repl,
337
    'exec': handle_connection_exec
338
}
339
340
341
class ManholeConsole(code.InteractiveConsole):
342
    def __init__(self, *args, **kw):
343
        code.InteractiveConsole.__init__(self, *args, **kw)
344
        if _MANHOLE.redirect_stderr:
345
            self.file = sys.stderr
346
        else:
347
            self.file = sys.stdout
348
349
    def write(self, data):
350
        self.file.write(data)
351
352
353
def handle_repl(locals):
354
    """
355
    Dumps stacktraces and runs an interactive prompt (REPL).
356
    """
357
    dump_stacktraces()
358
    namespace = {
359
        'dump_stacktraces': dump_stacktraces,
360
        'sys': sys,
361
        'os': os,
362
        'socket': socket,
363
        'traceback': traceback,
364
    }
365
    if locals:
366
        namespace.update(locals)
367
    ManholeConsole(namespace).interact()
368
369
370
class Logger(object):
371
    """
372
    Internal object used for logging.
373
374
    Initially this is not configured. Until you call ``manhole.install()``, this logger object won't work (will raise
375
    ``NotInstalled``).
376
    """
377
    time = _get_original('time', 'time')
378
    enabled = True
379
    destination = None
380
381
    def configure(self, enabled, destination):
382
        self.enabled = enabled
383
        self.destination = destination
384
385
    def release(self):
386
        self.enabled = True
387
        self.destination = None
388
389
    def __call__(self, message):
390
        """
391
        Fail-ignorant logging function.
392
        """
393
        if self.enabled:
394
            if self.destination is None:
395
                raise NotInstalled("Manhole is not installed!")
396
            try:
397
                full_message = "Manhole[%s:%.4f]: %s\n" % (os.getpid(), self.time(), message)
398
399
                if isinstance(self.destination, int):
400
                    os.write(self.destination, full_message.encode('ascii', 'ignore'))
401
                else:
402
                    self.destination.write(full_message)
403
            except Exception:
404
                pass
405
406
407
_LOG = Logger()
408
409
410
class Manhole(object):
411
    # Manhole core configuration
412
    # These are initialized when manhole is installed.
413
    daemon_connection = False
414
    locals = None
415
    original_os_fork = None
416
    original_os_forkpty = None
417
    redirect_stderr = True
418
    reinstall_delay = 0.5
419
    should_restart = None
420
    sigmask = _ALL_SIGNALS
421
    socket_path = None
422
    start_timeout = 0.5
423
    connection_handler = None
424
    previous_signal_handlers = None
425
    _thread = None
426
427
    def configure(self,
428
                  patch_fork=True, activate_on=None, sigmask=_ALL_SIGNALS, oneshot_on=None, thread=True,
429
                  start_timeout=0.5, socket_path=None, reinstall_delay=0.5, locals=None, daemon_connection=False,
430
                  redirect_stderr=True, connection_handler=handle_connection_repl):
431
        self.socket_path = socket_path
432
        self.reinstall_delay = reinstall_delay
433
        self.redirect_stderr = redirect_stderr
434
        self.locals = locals
435
        self.sigmask = sigmask
436
        self.daemon_connection = daemon_connection
437
        self.start_timeout = start_timeout
438
        self.previous_signal_handlers = {}
439
        self.connection_handler = _CONNECTION_HANDLER_ALIASES.get(connection_handler, connection_handler)
440
441
        if oneshot_on is None and activate_on is None and thread:
442
            self.thread.start()
443
            self.should_restart = True
444
445
        if oneshot_on is not None:
446
            oneshot_on = getattr(signal, 'SIG' + oneshot_on) if isinstance(oneshot_on, string) else oneshot_on
447
            self.previous_signal_handlers.setdefault(oneshot_on, signal.signal(oneshot_on, self.handle_oneshot))
448
449
        if activate_on is not None:
450
            activate_on = getattr(signal, 'SIG' + activate_on) if isinstance(activate_on, string) else activate_on
451
            if activate_on == oneshot_on:
452
                raise ConfigurationConflict('You cannot do activation of the Manhole thread on the same signal '
453
                                            'that you want to do oneshot activation !')
454
            self.previous_signal_handlers.setdefault(activate_on, signal.signal(activate_on, self.activate_on_signal))
455
456
        atexit.register(self.remove_manhole_uds)
457
        if patch_fork:
458
            if activate_on is None and oneshot_on is None and socket_path is None:
459
                self.patch_os_fork_functions()
460
            else:
461
                if activate_on:
462
                    _LOG("Not patching os.fork and os.forkpty. Activation is done by signal %s" % activate_on)
463
                elif oneshot_on:
464
                    _LOG("Not patching os.fork and os.forkpty. Oneshot activation is done by signal %s" % oneshot_on)
465
                elif socket_path:
466
                    _LOG("Not patching os.fork and os.forkpty. Using user socket path %s" % socket_path)
467
468
    def release(self):
469
        if self._thread:
470
            self._thread.stop()
471
            self._thread = None
472
        self.remove_manhole_uds()
473
        self.restore_os_fork_functions()
474
        for sig, handler in self.previous_signal_handlers.items():
475
            signal.signal(sig, handler)
476
        self.previous_signal_handlers.clear()
477
478
    @property
479
    def thread(self):
480
        if self._thread is None:
481
            self._thread = ManholeThread(
482
                self.get_socket, self.sigmask, self.start_timeout, self.connection_handler,
483
                daemon_connection=self.daemon_connection
484
            )
485
        return self._thread
486
487
    @thread.setter
488
    def thread(self, value):
489
        self._thread = value
490
491
    def get_socket(self):
492
        sock = _ORIGINAL_SOCKET(socket.AF_UNIX, socket.SOCK_STREAM)
493
        name = self.remove_manhole_uds()
494
        sock.bind(name)
495
        sock.listen(5)
496
        _LOG("Manhole UDS path: " + name)
497
        return sock
498
499
    def reinstall(self):
500
        """
501
        Reinstalls the manhole. Checks if the thread is running. If not, it starts it again.
502
        """
503
        with _LOCK:
504
            if not (self.thread.is_alive() and self.thread in _ORIGINAL__ACTIVE):
505
                self.thread = self.thread.clone(bind_delay=self.reinstall_delay)
506
                if self.should_restart:
507
                    self.thread.start()
508
509
    def handle_oneshot(self, _signum=None, _frame=None):
510
        try:
511
            try:
512
                sock = self.get_socket()
513
                _LOG("Waiting for new connection (in pid:%s) ..." % os.getpid())
514
                client = force_original_socket(sock.accept()[0])
515
                check_credentials(client)
516
                self.connection_handler(client)
517
            finally:
518
                self.remove_manhole_uds()
519
        except BaseException as exc:  # pylint: disable=W0702
520
            # we don't want to let any exception out, it might make the application misbehave
521
            _LOG("Oneshot failure: %r" % exc)
522
523
    def remove_manhole_uds(self):
524
        name = self.uds_name
525
        if os.path.exists(name):
526
            os.unlink(name)
527
        return name
528
529
    @property
530
    def uds_name(self):
531
        if self.socket_path is None:
532
            return "/tmp/manhole-%s" % os.getpid()
533
        return self.socket_path
534
535
    def patched_fork(self):
536
        """Fork a child process."""
537
        pid = self.original_os_fork()
538
        if not pid:
539
            _LOG('Fork detected. Reinstalling Manhole.')
540
            self.reinstall()
541
        return pid
542
543
    def patched_forkpty(self):
544
        """Fork a new process with a new pseudo-terminal as controlling tty."""
545
        pid, master_fd = self.original_os_forkpty()
546
        if not pid:
547
            _LOG('Fork detected. Reinstalling Manhole.')
548
            self.reinstall()
549
        return pid, master_fd
550
551
    def patch_os_fork_functions(self):
552
        self.original_os_fork, os.fork = os.fork, self.patched_fork
553
        self.original_os_forkpty, os.forkpty = os.forkpty, self.patched_forkpty
554
        _LOG("Patched %s and %s." % (self.original_os_fork, self.original_os_fork))
555
556
    def restore_os_fork_functions(self):
557
        if self.original_os_fork:
558
            os.fork = self.original_os_fork
559
        if self.original_os_forkpty:
560
            os.forkpty = self.original_os_forkpty
561
562
    def activate_on_signal(self, _signum, _frame):
563
        self.thread.start()
564
565
566
def install(verbose=True,
567
            verbose_destination=sys.__stderr__.fileno() if hasattr(sys.__stderr__, 'fileno') else sys.__stderr__,
568
            strict=True,
569
            **kwargs):
570
    """
571
    Installs the manhole.
572
573
    Args:
574
        verbose (bool): Set it to ``False`` to squelch the logging.
575
        verbose_destination (file descriptor or handle): Destination for verbose messages. Default is unbuffered stderr
576
            (stderr ``2`` file descriptor).
577
        patch_fork (bool): Set it to ``False`` if you don't want your ``os.fork`` and ``os.forkpy`` monkeypatched
578
        activate_on (int or signal name): set to ``"USR1"``, ``"USR2"`` or some other signal name, or a number if you
579
            want the Manhole thread to start when this signal is sent. This is desireable in case you don't want the
580
            thread active all the time.
581
        oneshot_on (int or signal name): Set to ``"USR1"``, ``"USR2"`` or some other signal name, or a number if you
582
            want the Manhole to listen for connection in the signal handler. This is desireable in case you don't want
583
            threads at all.
584
        thread (bool): Start the always-on ManholeThread. Default: ``True``. Automatically switched to ``False`` if
585
            ``oneshort_on`` or ``activate_on`` are used.
586
        sigmask (list of ints or signal names): Will set the signal mask to the given list (using
587
            ``signalfd.sigprocmask``). No action is done if ``signalfd`` is not importable.
588
            **NOTE**: This is done so that the Manhole thread doesn't *steal* any signals; Normally that is fine because
589
            Python will force all the signal handling to be run in the main thread but signalfd doesn't.
590
        socket_path (str): Use a specific path for the unix domain socket (instead of ``/tmp/manhole-<pid>``). This
591
            disables ``patch_fork`` as children cannot reuse the same path.
592
        reinstall_delay (float): Delay the unix domain socket creation *reinstall_delay* seconds. This
593
            alleviates cleanup failures when using fork+exec patterns.
594
        locals (dict): Names to add to manhole interactive shell locals.
595
        daemon_connection (bool): The connection thread is daemonic (dies on app exit). Default: ``False``.
596
        redirect_stderr (bool): Redirect output from stderr to manhole console. Default: ``True``.
597
        connection_handler (function): Connection handler to use. Use ``"exec"`` for simple implementation without
598
            output redirection or your own function. (warning: this is for advanced users). Default: ``"repl"``.
599
    """
600
    # pylint: disable=W0603
601
    global _MANHOLE
602
603
    with _LOCK:
604
        if _MANHOLE is None:
605
            _MANHOLE = Manhole()
606
        else:
607
            if strict:
608
                raise AlreadyInstalled("Manhole already installed!")
609
            else:
610
                _LOG.release()
611
                _MANHOLE.release()  # Threads might be started here
612
613
    _LOG.configure(verbose, verbose_destination)
614
    _MANHOLE.configure(**kwargs)  # Threads might be started here
615
    return _MANHOLE
616
617
618
def dump_stacktraces():
619
    """
620
    Dumps thread ids and tracebacks to stdout.
621
    """
622
    lines = []
623
    for thread_id, stack in sys._current_frames().items():  # pylint: disable=W0212
624
        lines.append("\n######### ProcessID=%s, ThreadID=%s #########" % (
625
            os.getpid(), thread_id
626
        ))
627
        for filename, lineno, name, line in traceback.extract_stack(stack):
628
            lines.append('File: "%s", line %d, in %s' % (filename, lineno, name))
629
            if line:
630
                lines.append("  %s" % (line.strip()))
631
    lines.append("#############################################\n\n")
632
633
    print('\n'.join(lines), file=sys.stderr if _MANHOLE.redirect_stderr else sys.stdout)
634