Commit 0cc9da87 authored by Luke Macken's avatar Luke Macken

Implement a much cleaner pyrasite.{inject,inspect} API.

Inspired by Jack Diederich PyCon talk "Stop Writing Classes".
parent 492ff2a4
Pyrasite API
============
.. autoclass:: pyrasite.CodeInjector
:members:
.. autoclass:: pyrasite.ObjectInspector
:members:
.. autofunction:: pyrasite.inject
.. autofunction:: pyrasite.inspect
.. autoclass:: pyrasite.PyrasiteIPC
:members:
.. autoclass:: pyrasite.ReverseConnection
:members:
.. autoclass:: pyrasite.ReversePythonConnection
:members:
__version__ = '2.0beta6'
__all__ = ('CodeInjector', 'ObjectInspector', 'PyrasiteIPC',
__all__ = ('inject', 'inspect', 'PyrasiteIPC',
'ReverseConnection', 'ReversePythonConnection')
__license__ = """\
pyrasite is free software: you can redistribute it and/or modify
......@@ -17,7 +17,7 @@ along with pyrasite. If not, see <http://www.gnu.org/licenses/>.\
"""
__copyright__ = "Copyright (C) 2011, 2012 Red Hat, Inc."
from pyrasite.inject import CodeInjector
from pyrasite.inspect import ObjectInspector
from pyrasite.injector import inject
from pyrasite.inspector import inspect
from pyrasite.ipc import PyrasiteIPC
from pyrasite.reverse import ReverseConnection, ReversePythonConnection
# This file is part of pyrasite.
#
# pyrasite is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# pyrasite is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with pyrasite. If not, see <http://www.gnu.org/licenses/>.
#
# Copyright (C) 2011, 2012 Red Hat, Inc.
"""
pyrasite
========
Inject code into a running python process.
http://pyrasite.com
Authors:
Luke Macken <lmacken@redhat.com>
David Malcolm <dmalcolm@redhat.com>
"""
import os
import warnings
import subprocess
class CodeInjector(object):
"""Injects code into a running Python process"""
def __init__(self, pid, filename=None, verbose=False, gdb_prefix=""):
self.pid = pid
self.verbose = verbose
self.gdb_prefix = gdb_prefix
if filename:
warnings.warn('Passing the payload in via the constructor is '
'deprecated. Please pass it to the "inject" method '
'instead.')
self.filename = os.path.abspath(filename)
def inject(self, filename=None):
"""Inject a given file into `self.pid` using gdb"""
if filename:
self.filename = os.path.abspath(filename)
gdb_cmds = [
'PyGILState_Ensure()',
'PyRun_SimpleString("'
'import sys; sys.path.insert(0, \\"%s\\"); '
'sys.path.insert(0, \\"%s\\"); '
'exec(open(\\"%s\\").read())")' %
(os.path.dirname(self.filename),
os.path.abspath(os.path.join(os.path.dirname(__file__), '..')),
self.filename),
'PyGILState_Release($1)',
]
p = subprocess.Popen('%sgdb -p %d -batch %s' % (self.gdb_prefix, self.pid,
' '.join(["-eval-command='call %s'" % cmd for cmd in gdb_cmds])),
shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
out, err = p.communicate()
if self.verbose:
print(out)
print(err)
# This file is part of pyrasite.
#
# pyrasite is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# pyrasite is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with pyrasite. If not, see <http://www.gnu.org/licenses/>.
#
# Copyright (C) 2011, 2012 Red Hat, Inc., Luke Macken <lmacken@redhat.com>
import os
import subprocess
def inject(pid, filename, verbose=False, gdb_prefix=''):
"""Executes a file in a running Python process."""
filename = os.path.abspath(filename)
gdb_cmds = [
'PyGILState_Ensure()',
'PyRun_SimpleString("'
'import sys; sys.path.insert(0, \\"%s\\"); '
'sys.path.insert(0, \\"%s\\"); '
'exec(open(\\"%s\\").read())")' %
(os.path.dirname(filename),
os.path.abspath(os.path.join(os.path.dirname(__file__), '..')),
filename),
'PyGILState_Release($1)',
]
p = subprocess.Popen('%sgdb -p %d -batch %s' % (gdb_prefix, pid,
' '.join(["-eval-command='call %s'" % cmd for cmd in gdb_cmds])),
shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
out, err = p.communicate()
if verbose:
print(out)
print(err)
......@@ -13,24 +13,17 @@
# You should have received a copy of the GNU General Public License
# along with pyrasite. If not, see <http://www.gnu.org/licenses/>.
#
# Copyright (C) 2011, 2012 Red Hat, Inc.
# Copyright (C) 2011, 2012 Red Hat, Inc., Luke Macken <lmacken@redhat.com>
import subprocess
class ObjectInspector(object):
"""Inspects objects in a running Python program"""
def __init__(self, pid):
self.pid = pid
def inspect(self, address):
"""Return the value of an object at a given address"""
cmd = ' '.join([
'gdb --quiet -p %s -batch' % self.pid,
'-eval-command="print (PyObject *)%s"' % address,
])
p = subprocess.Popen(cmd, shell=True, stdout=subprocess.PIPE)
for line in p.communicate()[0].split('\n'):
if line.startswith('$1 = '):
return line[5:]
def inspect(pid, address):
"Return the value of an object in a given process at the specified address"
cmd = ' '.join([
'gdb --quiet -p %s -batch' % pid,
'-eval-command="print (PyObject *)%s"' % address,
])
p = subprocess.Popen(cmd, shell=True, stdout=subprocess.PIPE)
for line in p.communicate()[0].split('\n'):
if line.startswith('$1 = '):
return line[5:]
......@@ -119,8 +119,7 @@ class PyrasiteIPC(object):
def inject(self):
"""Inject the payload into the process."""
filename = self.create_payload()
injector = pyrasite.CodeInjector(self.pid)
injector.inject(filename)
pyrasite.inject(self.pid, filename)
os.unlink(filename)
def wait(self):
......
......@@ -58,9 +58,8 @@ def main():
print("Error: The second argument must be a filename")
sys.exit(4)
injector = pyrasite.CodeInjector(pid, verbose=args.verbose,
gdb_prefix=args.gdb_prefix)
injector.inject(filename)
pyrasite.inject(pid, filename, verbose=args.verbose,
gdb_prefix=args.gdb_prefix)
if __name__ == '__main__':
......
......@@ -28,10 +28,11 @@ if sys.version_info[0] == 3:
from io import StringIO
else:
from StringIO import StringIO
from pyrasite.ipc import PyrasiteIPC
import pyrasite
class ReverseConnection(threading.Thread, PyrasiteIPC):
class ReverseConnection(threading.Thread, pyrasite.PyrasiteIPC):
"""A payload that connects to a given host:port and receives commands"""
host = 'localhost'
......
......@@ -18,7 +18,7 @@
import unittest
import subprocess
from pyrasite.inject import CodeInjector
import pyrasite
class TestCodeInjection(unittest.TestCase):
......@@ -28,8 +28,7 @@ class TestCodeInjection(unittest.TestCase):
p = subprocess.Popen(cmd, shell=True, stdout=subprocess.PIPE,
stderr=subprocess.PIPE)
ci = CodeInjector(p.pid, verbose=True)
ci.inject('pyrasite/payloads/helloworld.py')
pyrasite.inject(p.pid, 'pyrasite/payloads/helloworld.py', verbose=True)
stdout, stderr = p.communicate()
assert 'Hello World!' in stdout.decode('utf-8'), \
......@@ -44,8 +43,7 @@ class TestCodeInjection(unittest.TestCase):
p = subprocess.Popen('python -c "%s"' % ';'.join(cmd), shell=True,
stdout=subprocess.PIPE, stderr=subprocess.PIPE)
ci = CodeInjector(p.pid, verbose=True)
ci.inject('pyrasite/payloads/helloworld.py')
pyrasite.inject(p.pid, 'pyrasite/payloads/helloworld.py', verbose=True)
stdout, stderr = p.communicate()
assert 'Hello World!' in stdout.decode('utf-8'), \
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment