Skip to content

Commit e2ff0b1

Browse files
committed
User defined locals in manhole shell
Typically, when you add a manhole to existing application, the application was not designed for this, so finding stuff from the manhole shell is hard or even impossible. This patch adds a new locals optional argument, allowing a user to add application specific objects to manhole shell locals. Example usage: manhole.install(locals={'server', my_server}) From the manhole shell, you can now use "server": >>> server.status() When using automatic installation in child process, the locals are inherited by the child process. It is the responsibility of the user to handle object that became invalid after the fork.
1 parent a3b9e84 commit e2ff0b1

File tree

3 files changed

+58
-14
lines changed

3 files changed

+58
-14
lines changed

README.rst

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -89,7 +89,8 @@ Options
8989

9090
manhole.install(
9191
verbose=True, patch_fork=True, activate_on=None, oneshot_on=None,
92-
sigmask=manhole.ALL_SIGNALS, socket_path=None, reinstall_bind_delay=0.5)
92+
sigmask=manhole.ALL_SIGNALS, socket_path=None, reinstall_bind_delay=0.5,
93+
locals=None)
9394

9495
* ``verbose`` - set it to ``False`` to squelch the stderr ouput
9596
* ``patch_fork`` - set it to ``False`` if you don't want your ``os.fork`` and ``os.forkpy`` monkeypatched
@@ -105,6 +106,7 @@ Options
105106
``patch_fork`` as children cannot resuse the same path.
106107
* ``reinstall_bind_delay`` - Delay the unix domain socket creation *reinstall_bind_delay* seconds. This alleviates
107108
cleanup failures when using fork+exec patterns.
109+
* ``locals`` - names to add to manhole interactive shell locals.
108110

109111
What happens when you actually connect to the socket
110112
----------------------------------------------------

src/manhole.py

Lines changed: 20 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -136,7 +136,7 @@ class Manhole(_ORIGINAL_THREAD):
136136
bind_delay (float): Seconds to delay socket binding. Default: `no delay`.
137137
"""
138138

139-
def __init__(self, sigmask, start_timeout, bind_delay=None):
139+
def __init__(self, sigmask, start_timeout, bind_delay=None, locals=None):
140140
super(Manhole, self).__init__()
141141
self.daemon = True
142142
self.name = "Manhole"
@@ -146,6 +146,7 @@ def __init__(self, sigmask, start_timeout, bind_delay=None):
146146
# see: http://emptysqua.re/blog/dawn-of-the-thread/
147147
self.start_timeout = start_timeout
148148
self.bind_delay = bind_delay
149+
self.locals = locals
149150

150151
def start(self):
151152
super(Manhole, self).start()
@@ -183,7 +184,7 @@ def run(self):
183184
while True:
184185
cry("Waiting for new connection (in pid:%s) ..." % os.getpid())
185186
try:
186-
client = ManholeConnection(sock.accept()[0], self.sigmask)
187+
client = ManholeConnection(sock.accept()[0], self.sigmask, self.locals)
187188
client.start()
188189
client.join()
189190
except (InterruptedError, socket.error) as e:
@@ -199,12 +200,13 @@ class ManholeConnection(_ORIGINAL_THREAD):
199200
Manhole thread that handles the connection. This thread is a normal thread (non-daemon) - it won't exit if the
200201
main thread exits.
201202
"""
202-
def __init__(self, client, sigmask):
203+
def __init__(self, client, sigmask, locals):
203204
super(ManholeConnection, self).__init__()
204205
self.daemon = False
205206
self.client = client
206207
self.name = "ManholeConnection"
207208
self.sigmask = sigmask
209+
self.locals = locals
208210

209211
def run(self):
210212
cry('Started ManholeConnection thread. Checking credentials ...')
@@ -214,7 +216,7 @@ def run(self):
214216

215217
pid, _, _ = self.check_credentials(self.client)
216218
pthread_setname_np(self.ident, "Manhole %s" % pid)
217-
self.handle(self.client)
219+
self.handle(self.client, self.locals)
218220

219221
@staticmethod
220222
def check_credentials(client):
@@ -234,7 +236,7 @@ def check_credentials(client):
234236
return pid, uid, gid
235237

236238
@staticmethod
237-
def handle(client):
239+
def handle(client, locals):
238240
"""
239241
Handles connection. This is a static method so it can be used without a thread (eg: from a signal handler -
240242
`oneshot_on`).
@@ -265,7 +267,7 @@ def handle(client):
265267
for name in names:
266268
backup.append((name, getattr(sys, name)))
267269
setattr(sys, name, _ORIGINAL_FDOPEN(client_fd, mode, 1 if PY3 else 0))
268-
run_repl()
270+
run_repl(locals)
269271
cry("DONE.")
270272
finally:
271273
try:
@@ -294,27 +296,31 @@ def handle(client):
294296
cry(traceback.format_exc())
295297

296298

297-
def run_repl():
299+
def run_repl(locals):
298300
"""
299301
Dumps stacktraces and runs an interactive prompt (REPL).
300302
"""
301303
dump_stacktraces()
302-
code.InteractiveConsole({
304+
namespace = {
303305
'dump_stacktraces': dump_stacktraces,
304306
'sys': sys,
305307
'os': os,
306308
'socket': socket,
307309
'traceback': traceback,
308-
}).interact()
310+
}
311+
if locals:
312+
namespace.update(locals)
313+
code.InteractiveConsole(namespace).interact()
309314

310315

311316
def _handle_oneshot(_signum, _frame):
317+
assert _INST, "Manhole wasn't installed !"
312318
try:
313319
sock = Manhole.get_socket()
314320
cry("Waiting for new connection (in pid:%s) ..." % os.getpid())
315321
client, _ = sock.accept()
316322
ManholeConnection.check_credentials(client)
317-
ManholeConnection.handle(client)
323+
ManholeConnection.handle(client, _INST.locals)
318324
except: # pylint: disable=W0702
319325
# we don't want to let any exception out, it might make the application missbehave
320326
cry("Manhole oneshot connection failed:")
@@ -377,7 +383,7 @@ def _activate_on_signal(_signum, _frame):
377383

378384

379385
def install(verbose=True, patch_fork=True, activate_on=None, sigmask=ALL_SIGNALS, oneshot_on=None, start_timeout=0.5,
380-
socket_path=None, reinstall_bind_delay=0.5):
386+
socket_path=None, reinstall_bind_delay=0.5, locals=None):
381387
"""
382388
Installs the manhole.
383389
@@ -398,6 +404,7 @@ def install(verbose=True, patch_fork=True, activate_on=None, sigmask=ALL_SIGNALS
398404
disables ``patch_fork`` as children cannot resuse the same path.
399405
reinstall_bind_delay(float): Delay the unix domain socket creation *reinstall_bind_delay* seconds. This alleviates
400406
cleanup failures when using fork+exec patterns.
407+
locals(dict): names to add to manhole interactive shell locals.
401408
"""
402409
global _STDERR, _INST, _SHOULD_RESTART # pylint: disable=W0603
403410
global VERBOSE, START_TIMEOUT, SOCKET_PATH, REINSTALL_BIND_DELAY # pylint: disable=W0603
@@ -408,7 +415,7 @@ def install(verbose=True, patch_fork=True, activate_on=None, sigmask=ALL_SIGNALS
408415
SOCKET_PATH = socket_path
409416
_STDERR = sys.__stderr__
410417
if not _INST:
411-
_INST = Manhole(sigmask, start_timeout)
418+
_INST = Manhole(sigmask, start_timeout, locals=locals)
412419
if oneshot_on is not None:
413420
oneshot_on = getattr(signal, 'SIG'+oneshot_on) if isinstance(oneshot_on, string) else oneshot_on
414421
signal.signal(oneshot_on, _handle_oneshot)
@@ -444,7 +451,7 @@ def reinstall():
444451
assert _INST
445452
with _INST_LOCK:
446453
if not (_INST.is_alive() and _INST in _ORIGINAL__ACTIVE):
447-
_INST = Manhole(_INST.sigmask, START_TIMEOUT, bind_delay=REINSTALL_BIND_DELAY)
454+
_INST = Manhole(_INST.sigmask, START_TIMEOUT, bind_delay=REINSTALL_BIND_DELAY, locals=_INST.locals)
448455
if _SHOULD_RESTART:
449456
_INST.start()
450457

tests/test_manhole.py

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,34 @@ def test_simple():
7272
assert_manhole_running(proc, uds_path)
7373

7474

75+
def test_locals():
76+
with TestProcess(sys.executable, __file__, 'daemon', 'test_locals') as proc:
77+
with dump_on_error(proc.read):
78+
wait_for_strings(proc.read, TIMEOUT, 'Waiting for new connection')
79+
check_locals(SOCKET_PATH)
80+
81+
82+
def test_locals_after_fork():
83+
with TestProcess(sys.executable, __file__, 'daemon', 'test_locals_after_fork') as proc:
84+
with dump_on_error(proc.read):
85+
wait_for_strings(proc.read, TIMEOUT, 'Fork detected')
86+
proc.reset()
87+
wait_for_strings(proc.read, TIMEOUT, '/tmp/manhole-')
88+
child_uds_path = re.findall(r"(/tmp/manhole-\d+)", proc.read())[0]
89+
wait_for_strings(proc.read, TIMEOUT, 'Waiting for new connection')
90+
check_locals(child_uds_path)
91+
92+
93+
def check_locals(uds_path):
94+
sock = connect_to_manhole(uds_path)
95+
with TestSocket(sock) as client:
96+
with dump_on_error(client.read):
97+
wait_for_strings(client.read, 1, ">>>")
98+
sock.send(b"from __future__ import print_function\n"
99+
b"print(k1, k2)\n")
100+
wait_for_strings(client.read, 1, "v1 v2")
101+
102+
75103
def test_fork_exec():
76104
with TestProcess(sys.executable, __file__, 'daemon', 'test_fork_exec') as proc:
77105
with dump_on_error(proc.read):
@@ -438,6 +466,13 @@ def handle_usr2(_sig, _frame):
438466
manhole.install(socket_path=SOCKET_PATH)
439467
time.sleep(1)
440468
do_fork()
469+
elif test_name == 'test_locals':
470+
manhole.install(socket_path=SOCKET_PATH,
471+
locals={'k1': 'v1', 'k2': 'v2'})
472+
time.sleep(1)
473+
elif test_name == 'test_locals_after_fork':
474+
manhole.install(locals={'k1': 'v1', 'k2': 'v2'})
475+
do_fork()
441476
else:
442477
manhole.install()
443478
time.sleep(0.3) # give the manhole a bit enough time to start

0 commit comments

Comments
 (0)