|
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.4.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 manole. |
|
138
|
|
|
|
|
139
|
|
|
Args: |
|
140
|
|
|
sigmask (list of singal 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: # pylint: disable=W0702 |
|
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 cause |
|
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 specifc path for the unix domain socket (instead of ``/tmp/manhole-<pid>``). This |
|
591
|
|
|
disables ``patch_fork`` as children cannot resuse 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
|
|
|
|