Commit 7b7467cb authored by Luke Macken's avatar Luke Macken

Merge branch 'release/2.0'

parents 3098b008 35e04fce
Luke Macken <lmacken@redhat.com>
David Malcolm <dmalcolm@redhat.com>
...@@ -3,7 +3,7 @@ pyrasite ...@@ -3,7 +3,7 @@ pyrasite
.. split here .. split here
Pyrasite lets you to inject arbitrary code into running Python processes. Tools for injecting arbitrary code into running Python processes.
:homepage: http://pyrasite.com :homepage: http://pyrasite.com
:documentation: http://pyrasite.rtfd.org :documentation: http://pyrasite.rtfd.org
...@@ -36,9 +36,12 @@ The graphical interface can be found here: https://github.com/lmacken/pyrasite-g ...@@ -36,9 +36,12 @@ The graphical interface can be found here: https://github.com/lmacken/pyrasite-g
Authors Authors
~~~~~~~ ~~~~~~~
`Luke Macken <http://twitter.com/lmacken>`_ Created by `Luke Macken <http://twitter.com/lmacken>`_ with the help of
`David Malcolm <http://dmalcolm.livejournal.com>`_ and many other
`contributors <https://github.com/lmacken/pyrasite/contributors>`_.
.. image:: http://api.coderwall.com/lmacken/endorsecount.png License
:target: http://coderwall.com/lmacken ~~~~~~~
David Malcolm <dmalcolm@redhat.com> .. image:: https://www.gnu.org/graphics/gplv3-127x51.png
:target: https://www.gnu.org/licenses/gpl.txt
pyrasite - A command-line interface for injecting code into a running Python process ``pyrasite`` - Inject arbitrary code into a running Python process
==================================================================================== ==================================================================
:: ::
......
Development
===========
Running from git
----------------
::
git clone git://github.com/lmacken/pyrasite.git
cd pyrasite
python pyrasite/main.py
::
git clone git://github.com/lmacken/pyrasite-gui.git
cd pyrasite-gui
python pyrasite_gui/gui.py
Git flow
--------
Use `git-flow <https://github.com/nvie/gitflow.git>`_.
Style
-----
* `PEP8 <www.python.org/dev/peps/pep-0008>`_
pyrasite-gui - A graphical interface for Pyrasite ``pyrasite-gui`` - A graphical interface for Pyrasite
================================================= =====================================================
The GUI has been moved into it's own repository: The pyrasite-gui is a graphical interface for Pyrasite that lets you
easily monitor, analyze, introspect, and alter running Python programs.
https://github.com/lmacken/pyrasite-gui :source: https://github.com/lmacken/pyrasite-gui
:download: http://pypi.python.org/pypi/pyrasite-gui
Requirements
------------
- Python debuginfo (needed for live object inspection)
- PyGObject3 Introspection bindings
- WebKitGTK3
- `meliae <https://launchpad.net/meliae>`_ (easy_install/pip may not work for this install. If not, use the tarball from the distribution website. You may need to install `Cython <http://cython.org>`_ in order to get meliae to build)
- `pycallgraph <http://pycallgraph.slowchop.com>`_
- `psutil <http://code.google.com/p/psutil>`_
Distribution-specific instructions
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
.. code-block:: bash
# Fedora
yum --enablerepo=updates-testing install python-psutil python-debuginfo python-pycallgraph pygobject3 webkitgtk3 python-meliae
# Ubuntu:
apt-get install python-dbg python-pycallgraph python-gobject-dev gir1.2-webkit-3.0 python-meliae python-psutil
# Arch
pacman -S python2-psutil python2-gobject python2-pycallgraph libwebkit3 python2-meliae
Screenshots
-----------
.. image:: http://lewk.org/img/pyrasite/pyrasite-info.png .. image:: http://lewk.org/img/pyrasite/pyrasite-info.png
......
...@@ -4,62 +4,28 @@ Installing ...@@ -4,62 +4,28 @@ Installing
Requirements Requirements
~~~~~~~~~~~~ ~~~~~~~~~~~~
Core * `gdb <https://www.gnu.org/s/gdb>`_ (version 7.3+)
----
* `gdb <https://www.gnu.org/s/gdb>`_ (version 7.3+)
GUI Python Compatiblity
--- ~~~~~~~~~~~~~~~~~~~
- `Pyrasite <https://github.com/lmacken/pyrasite>`_ Pyrasite works with Python 2.4 and newer. Injection works between versions
- Python debuginfo (needed for live object inspection) as well, so you can run Pyrasite under Python 3 and inject into 2, and
vice versa.
- Fedora: python-debuginfo, Ubuntu: python-dbg Installing
~~~~~~~~~~
- PyGObject3 Introspection bindings
- Fedora: pygobject3, Ubuntu: python-gobject-dev, Arch: python2-gobject
- WebKitGTK3
- Fedora: webkitgtk3, Ubuntu: gir1.2-webkit-3.0, Arch: libwebkit3
- `meliae <https://launchpad.net/meliae>`_
- easy_install/pip may not work for this install. If not, use the tarball from the distribution website. You may need to install `Cython <http://cython.org>`_ in order to get meliae to build.
- Fedora: python-meliae, Ubuntu: python-meliae, Arch: python2-meliae
- `pycallgraph <http://pycallgraph.slowchop.com>`_
- Fedora: python-pycallgraph, Ubuntu: python-pycallgraph, Arch: python2-pycallgraph
- `psutil <http://code.google.com/p/psutil>`_
- Fedora: python-psutil, Ubuntu: python-psutil, Arch: python2-psutil
Download
~~~~~~~~
Download the latest stable release from PyPi: http://pypi.python.org/pypi/pyrasite
::
pip install pyrasite
Running from git You can download the latest tarballs, RPMs, and debs from `PyPi <http://pypi.python.org/pypi/pyrasite>`_. Installing the package specific to your distribution is recommended. However, you
~~~~~~~~~~~~~~~~ can also install it using ``pip`` if you wish
:: ::
git clone git://github.com/lmacken/pyrasite.git pip install pyrasite pyrasite-gui
cd pyrasite
python -m pyrasite.main
.. note::
If you're on Python 2.4, you can run pyrasite by doing .. seealso:: `pyrasite-gui <http://pyrasite.readthedocs.org/en/latest/GUI.html>`_ for instructions on installing the graphical interface
``PYTHONPATH=$(pwd) python pyrasite/main.py``
Additional installation notes Additional installation notes
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
...@@ -67,20 +33,6 @@ Additional installation notes ...@@ -67,20 +33,6 @@ Additional installation notes
Fedora Fedora
------ ------
pyrasite and pycallgraph are currently not available in Fedora. You can run
pyrasite on Fedora by doing the following:
.. code-block:: bash
sudo yum -y install python-meliae python-devel python-psutil pygobject3 graphviz python-virtualenv git-core gcc
sudo yum -y --enablerepo=\*-debuginfo install python-debuginfo
git clone -b develop git://github.com/lmacken/pyrasite.git
cd pyrasite
virtualenv [--system-site-packages if on F16+] env
source env/bin/activate
python setup.py develop
pyrasite-gui
If you're using Fedora 17 or later, you'll need to disable an SELinux boolean to allow ptrace. If you're using Fedora 17 or later, you'll need to disable an SELinux boolean to allow ptrace.
.. code-block:: bash .. code-block:: bash
...@@ -111,4 +63,5 @@ Since version 10.10, Ubuntu ships with a `controversial patch <https://lkml.org/ ...@@ -111,4 +63,5 @@ Since version 10.10, Ubuntu ships with a `controversial patch <https://lkml.org/
echo 0 > /proc/sys/kernel/yama/ptrace_scope echo 0 > /proc/sys/kernel/yama/ptrace_scope
You can make this change permanent by setting ``ptrace_scope`` to ``0`` in
``/etc/sysctl.d/10-ptrace.conf``.
``pyrasite-memory-viewer`` - View the largest objects in your process
=====================================================================
Pyrasite provides a tool to view object memory usage statistics, and the
live value, of largest objects in your process. This requires `urwid
<http://pypi.python.org/pypi/urwid>`_ and `meliae
<https://launchpad.net/meliae>`_ to be installed.
::
$ pyrasite-memory-viewer <PID>
.. image:: http://lewk.org/img/pyrasite-memory-viewer.png
This tool automatically injects the following payload:
.. literalinclude:: ../pyrasite/payloads/dump_memory.py
:language: python
:start-after: html
You can easily dump the object memory usage JSON data by hand, if you wish:
::
$ pyrasite <PID> pyrasite/payloads/dump_memory.py
Payloads Example Payloads
======== ================
Viewing the largest objects in your process These payloads can be found in the `pyrasite/payloads
------------------------------------------- <https://github.com/lmacken/pyrasite/tree/master/pyrasite/payloads>`_
directory.
This payload uses `meliae <https://launchpad.net/meliae>`_ to dump all of Dumping thread stacks
the objects in your process to an `objects.json` file (currently dumped in ---------------------
the working directory of your process).
We recommend using python-meliae from your OS distribution, if available.
If it is not, you will need to first install Cython, and then meliae
seperately. If pip/easy_install does not work, you may need to use the
tarball from the upstream website.
.. literalinclude:: ../pyrasite/payloads/dump_memory.py .. literalinclude:: ../pyrasite/payloads/dump_stacks.py
:language: python :language: python
:start-after: html
::
$ pyrasite <PID> pyrasite/payloads/dump_memory.py
Pyrasite also provides a tool to view the values of largest objects in your
process. This requires `urwid <http://pypi.python.org/pypi/urwid>`_ to be
installed.
::
$ pyrasite-memory-viewer <PID> objects.json
Viewing loaded modules
----------------------
.. image:: http://lewk.org/img/pyrasite-memory-viewer.png .. literalinclude:: ../pyrasite/payloads/dump_modules.py
:language: python
Call Graph Call Graph
---------- ----------
...@@ -43,48 +26,28 @@ graph using `pycallgraph <http://pycallgraph.slowchop.com>`_. ...@@ -43,48 +26,28 @@ graph using `pycallgraph <http://pycallgraph.slowchop.com>`_.
.. literalinclude:: ../pyrasite/payloads/start_callgraph.py .. literalinclude:: ../pyrasite/payloads/start_callgraph.py
:language: python :language: python
:start-after: http :start-after: http
.. literalinclude:: ../pyrasite/payloads/stop_callgraph.py .. literalinclude:: ../pyrasite/payloads/stop_callgraph.py
:language: python :language: python
:start-after: http :start-after: http
::
$ pyrasite <PID> pyrasite/payloads/start_callgraph.py
$ pyrasite <PID> pyrasite/payloads/stop_callgraph.py
The callgraph is then generated using `graphviz <http://www.graphviz.org>`_ and The callgraph is then generated using `graphviz <http://www.graphviz.org>`_ and
saved to `callgraph.png`. You can see an example callgraph `here <http://pycallgraph.slowchop.com/pycallgraph/wiki/RegExpExample>`_. saved to `callgraph.png`. You can see an example callgraph `here <http://pycallgraph.slowchop.com/pycallgraph/wiki/RegExpExample>`_.
Viewing loaded modules
----------------------
.. literalinclude:: ../pyrasite/payloads/dump_modules.py
:language: python
::
$ pyrasite <PID> pyrasite/payloads/dump_modules.py
Dumping thread stacks
---------------------
.. literalinclude:: ../pyrasite/payloads/dump_stacks.py
:language: python
::
$ pyrasite <PID> pyrasite/payloads/dump_stacks.py
Forcing garbage collection Forcing garbage collection
--------------------------- ---------------------------
.. literalinclude:: ../pyrasite/payloads/force_garbage_collection.py .. literalinclude:: ../pyrasite/payloads/force_garbage_collection.py
:language: python :language: python
:: Dumping out object memory usage statistics
------------------------------------------
.. literalinclude:: ../pyrasite/payloads/dump_memory.py
:language: python
$ pyrasite <PID> pyrasite/payloads/force_garbage_collection.py .. seealso:: :doc:`MemoryViewer`
Reverse Subprocess Shell Reverse Subprocess Shell
------------------------ ------------------------
......
pyrasite-shell - Give it a pid, get a shell ``pyrasite-shell`` - Give it a pid, get a shell
=========================================== ===============================================
You can easily drop into a shell and execute commands in a running process You can easily drop into a shell and execute commands in a running process
using the ``pyrasite-shell``. using the ``pyrasite-shell``.
...@@ -11,11 +11,11 @@ using the ``pyrasite-shell``. ...@@ -11,11 +11,11 @@ using the ``pyrasite-shell``.
.. code-block:: bash .. code-block:: bash
$ pyrasite-shell $(pgrep -f "python -v") $ pyrasite-shell $(pgrep -f "ipython")
pyrasite shell 2.0beta7 Pyrasite Shell 2.0beta9
Connected to 'ipython'
Python 2.7.2 (default, Oct 27 2011, 01:40:22) Python 2.7.2 (default, Oct 27 2011, 01:40:22)
[GCC 4.6.1 20111003 (Red Hat 4.6.1-10)] on linux2 [GCC 4.6.1 20111003 (Red Hat 4.6.1-10)] on linux2
>>> print(x) >>> print(x)
foo foo
......
...@@ -20,6 +20,8 @@ Contents ...@@ -20,6 +20,8 @@ Contents
Installing Installing
CLI CLI
Shell Shell
MemoryViewer
GUI GUI
Payloads Payloads
API API
Development
%global betaver beta9
Name: pyrasite Name: pyrasite
Version: 2.0 Version: 2.0
Release: 0.1.%{betaver}%{?dist} Release: 1%{?dist}
Summary: Code injection and monitoring of running Python processes Summary: Code injection and monitoring of running Python processes
Group: Development/Languages Group: Development/Languages
License: GPLv3 License: GPLv3
URL: http://pyrasite.com URL: http://pyrasite.com
Source0: http://pypi.python.org/packages/source/p/%{name}/%{name}-%{version}%{betaver}.tar.gz Source0: http://pypi.python.org/packages/source/p/%{name}/%{name}-%{version}.tar.gz
BuildArch: noarch BuildArch: noarch
BuildRequires: python-devel BuildRequires: python-devel
BuildRequires: python-setuptools-devel BuildRequires: python-setuptools-devel
BuildRequires: python-nose BuildRequires: python-nose
BuildRequires: python-sphinx
Requires: gdb
Requires: gdb >= 7.3 %if 0%{?rhel} <= 6
BuildRequires: python-argparse
Requires: python-argparse
%endif
%description %description
Pyrasite uses the GNU debugger to inject code into a running Python process. Pyrasite uses the GNU debugger to inject code into a running Python process.
...@@ -23,25 +27,29 @@ It is comprised of a command-line tool, and a Python API. This package ...@@ -23,25 +27,29 @@ It is comprised of a command-line tool, and a Python API. This package
also comes with a variety of example payloads. also comes with a variety of example payloads.
%prep %prep
%setup -q -n %{name}-%{version}%{betaver} %setup -q
%build %build
%{__python} setup.py build %{__python} setup.py build
make -C docs man
%check %check
%{__python} setup.py test %{__python} setup.py test
%install %install
%{__python} setup.py install -O1 --skip-build --root $RPM_BUILD_ROOT %{__python} setup.py install -O1 --skip-build --root %{buildroot}
mkdir -p %{buildroot}%{_mandir}/man1
gzip -c docs/_build/man/pyrasite.1 > %{buildroot}/%{_mandir}/man1/pyrasite.1.gz
%files %files
%defattr(-,root,root,-) %defattr(-,root,root,-)
%doc README.rst %doc README.rst LICENSE
%doc %{_mandir}/man1/pyrasite.1.gz
%{_bindir}/pyrasite %{_bindir}/pyrasite
%{_bindir}/pyrasite-memory-viewer %{_bindir}/pyrasite-memory-viewer
%{_bindir}/pyrasite-shell %{_bindir}/pyrasite-shell
%{python_sitelib}/* %{python_sitelib}/*
%changelog %changelog
* Mon Mar 12 2012 Luke Macken <lmacken@redhat.com> 2.0-0.1.beta1 * Mon Mar 12 2012 Luke Macken <lmacken@redhat.com> 2.0-1
- Initial package for Fedora - Initial package for Fedora
__version__ = '2.0beta9' __version__ = '2.0'
__all__ = ('inject', 'inspect', 'PyrasiteIPC', __all__ = ('inject', 'inspect', 'PyrasiteIPC',
'ReverseConnection', 'ReversePythonConnection') 'ReverseConnection', 'ReversePythonConnection')
__license__ = """\ __license__ = """\
......
...@@ -23,10 +23,12 @@ import os ...@@ -23,10 +23,12 @@ import os
import socket import socket
import struct import struct
import tempfile import tempfile
import pyrasite import subprocess
from os.path import dirname, abspath, join from os.path import dirname, abspath, join
import pyrasite
class PyrasiteIPC(object): class PyrasiteIPC(object):
"""Pyrasite Inter-Python Communication. """Pyrasite Inter-Python Communication.
...@@ -59,13 +61,29 @@ class PyrasiteIPC(object): ...@@ -59,13 +61,29 @@ class PyrasiteIPC(object):
# shell payloads with netcat. # shell payloads with netcat.
reliable = True reliable = True
def __init__(self, pid): def __init__(self, pid, reverse='ReversePythonConnection'):
super(PyrasiteIPC, self).__init__() super(PyrasiteIPC, self).__init__()
self.pid = pid self.pid = pid
self.sock = None self.sock = None
self.server_sock = None self.server_sock = None
self.hostname = None self.hostname = None
self.port = None self.port = None
self.reverse = reverse
def __enter__(self):
self.connect()
return self
def __exit__(self, *args, **kwargs):
self.close()
@property
def title(self):
if not getattr(self, '_title', None):
p = subprocess.Popen('ps --no-heading -o cmd= -p %d' % self.pid,
stdout=subprocess.PIPE, shell=True)
self._title = p.communicate()[0].decode('utf-8')
return self._title.strip()
def connect(self): def connect(self):
""" """
...@@ -116,7 +134,7 @@ class PyrasiteIPC(object): ...@@ -116,7 +134,7 @@ class PyrasiteIPC(object):
line = line.replace('reliable = True', 'reliable = False') line = line.replace('reliable = True', 'reliable = False')
tmp.write(line) tmp.write(line)
tmp.write('ReversePythonConnection().start()\n') tmp.write('%s().start()\n' % self.reverse)
tmp.close() tmp.close()
payload.close() payload.close()
......
...@@ -40,24 +40,48 @@ def ptrace_check(): ...@@ -40,24 +40,48 @@ def ptrace_check():
p = subprocess.Popen([getsebool, 'deny_ptrace'], p = subprocess.Popen([getsebool, 'deny_ptrace'],
stdout=subprocess.PIPE, stderr=subprocess.PIPE) stdout=subprocess.PIPE, stderr=subprocess.PIPE)
out, err = p.communicate() out, err = p.communicate()
if out.decode('utf-8') == u'deny_ptrace --> on\n': if str(out) == 'deny_ptrace --> on\n':
print("WARNING: ptrace is disabled. Injection will not work.") print("WARNING: ptrace is disabled. Injection will not work.")
print("You can enable it by running the following:") print("You can enable it by running the following:")
print("sudo setsebool -P deny_ptrace=off") print("sudo setsebool -P deny_ptrace=off")
print("") print("")
def get_payload_dir():
return os.path.join(os.path.dirname(pyrasite.__file__), 'payloads')
def list_payloads():
return sorted(fn for fn in os.listdir(get_payload_dir())
if fn.endswith('.py') and not fn.startswith('_'))
def expand_payload(payload):
"""If a standard payload with this name exists, return its full path.
Otherwise return the input value unchanged.
"""
if os.path.sep not in payload:
fn = os.path.join(get_payload_dir(), payload)
if os.path.isfile(fn):
return fn
return payload
def main(): def main():
ptrace_check() ptrace_check()
parser = argparse.ArgumentParser( parser = argparse.ArgumentParser(
description='pyrasite - inject code into a running python process', description='pyrasite - inject code into a running python process',
epilog="For updates, visit https://github.com/lmacken/pyrasite") epilog="For updates, visit https://github.com/lmacken/pyrasite")
parser.add_argument('pid',
parser.add_argument('pid', nargs='?',
help="The ID of the process to inject code into") help="The ID of the process to inject code into")
parser.add_argument('filename', parser.add_argument('payload', nargs='?', default='',
help="The Python script to be executed inside the" help="The Python script to be executed inside the"
" running process, also referred to as 'payload'") " running process. Can be one of the standard"
" payloads (see --list-payloads) or a filname.")
parser.add_argument('-l', '--list-payloads', help='List standard payloads',
default=False, action='store_const', const=True)
parser.add_argument('--gdb-prefix', dest='gdb_prefix', parser.add_argument('--gdb-prefix', dest='gdb_prefix',
help='GDB prefix (if specified during installation)', help='GDB prefix (if specified during installation)',
default="") default="")
...@@ -70,19 +94,25 @@ def main(): ...@@ -70,19 +94,25 @@ def main():
args = parser.parse_args() args = parser.parse_args()
if args.list_payloads:
print("Available payloads:")
for payload in list_payloads():
print(" %s" % payload)
sys.exit()
try: try:
pid = int(args.pid) pid = int(args.pid)
except ValueError: except ValueError:
print("Error: The first argument must be a pid") print("Error: The first argument must be a pid")
sys.exit(2) sys.exit(2)
filename = args.filename filename = expand_payload(args.payload)
if filename: if filename:
if not os.path.exists(filename): if not os.path.exists(filename):
print("Error: Invalid path or file doesn't exist") print("Error: Invalid path or file doesn't exist")
sys.exit(3) sys.exit(3)
else: else:
print("Error: The second argument must be a filename") print("Error: The second argument must be a filename or a payload name")
sys.exit(4) sys.exit(4)
pyrasite.inject(pid, filename, verbose=args.verbose, pyrasite.inject(pid, filename, verbose=args.verbose,
......
...@@ -5,5 +5,5 @@ ...@@ -5,5 +5,5 @@
# https://launchpad.net/meliae # https://launchpad.net/meliae
# http://jam-bazaar.blogspot.com/2009/11/memory-debugging-with-meliae.html # http://jam-bazaar.blogspot.com/2009/11/memory-debugging-with-meliae.html
from meliae import scanner import os, meliae.scanner
scanner.dump_all_objects('objects.json') meliae.scanner.dump_all_objects('/tmp/pyrasite-%d-objects.json' % os.getpid())
...@@ -23,6 +23,7 @@ import sys ...@@ -23,6 +23,7 @@ import sys
import socket import socket
import traceback import traceback
import threading import threading
from code import InteractiveConsole
if sys.version_info[0] == 3: if sys.version_info[0] == 3:
from io import StringIO from io import StringIO
...@@ -113,3 +114,78 @@ class ReversePythonConnection(ReverseConnection): ...@@ -113,3 +114,78 @@ class ReversePythonConnection(ReverseConnection):
buffer.close() buffer.close()
self.send(output) self.send(output)
return True return True
class DistantInteractiveConsole(InteractiveConsole):
def __init__(self, ipc):
InteractiveConsole.__init__(self, globals())
self.ipc = ipc
self.set_buffer()
def set_buffer(self):
self.out_buffer = StringIO()
sys.stdout = sys.stderr = self.out_buffer
def unset_buffer(self):
sys.stdout, sys.stderr = sys.__stdout__, sys.__stderr__
value = self.out_buffer.getvalue()
self.out_buffer.close()
return value
def raw_input(self, prompt=""):
output = self.unset_buffer()
# payload format: 'prompt' ? '\n' 'output'
self.ipc.send('\n'.join((prompt, output)))
cmd = self.ipc.recv()
self.set_buffer()
return cmd
class ReversePythonShell(threading.Thread, pyrasite.PyrasiteIPC):
"""A reverse Python shell that behaves like Python interactive interpreter.
"""
host = 'localhost'
port = 9001
reliable = True
def __init__(self, host=None, port=None):
super(ReversePythonShell, self).__init__()
def run(self):
try:
for res in socket.getaddrinfo(self.host, self.port,
socket.AF_UNSPEC, socket.SOCK_STREAM):
af, socktype, proto, canonname, sa = res
try:
self.sock = socket.socket(af, socktype, proto)
try:
self.sock.connect(sa)
except socket.error:
self.sock.close()
self.sock = None
continue
except socket.error:
self.sock = None
continue
break
if not self.sock:
raise Exception('pyrasite cannot establish reverse ' +
'connection to %s:%d' % (self.host, self.port))
DistantInteractiveConsole(self).interact()
except SystemExit:
pass
except:
traceback.print_exc(file=sys.__stderr__)
sys.stdout, sys.stderr = sys.__stdout__, sys.__stderr__
self.close()
""" This is kept in a separate file so that python2.4 never picks it up. """
import pyrasite
def context_manager_business(case):
# Check that the context manager injects ipc correctly.
with pyrasite.PyrasiteIPC(case.p.pid) as ipc:
assert ipc.cmd('print("mu")') == 'mu\n'
# Check that the context manager closes the ipc correctly.
try:
ipc.cmd('print("mu")')
assert False, "The connection was not closed."
except IOError as e:
assert "Bad file descriptor" in str(e)
...@@ -17,12 +17,12 @@ ...@@ -17,12 +17,12 @@
import os import os
import sys import sys
import unittest import subprocess
import pyrasite import pyrasite
from pyrasite.tests.utils import generate_program, run_program, stop_program, \ from pyrasite.tests.utils import generate_program, run_program, stop_program, \
interpreters interpreters, unittest
class TestCodeInjection(unittest.TestCase): class TestCodeInjection(unittest.TestCase):
...@@ -47,8 +47,8 @@ class TestCodeInjection(unittest.TestCase): ...@@ -47,8 +47,8 @@ class TestCodeInjection(unittest.TestCase):
os.unlink(program) os.unlink(program)
def test_many_payloads_into_program_with_many_threads(self): def test_many_payloads_into_program_with_many_threads(self):
program = generate_program(threads=50) program = generate_program(threads=25)
num_payloads = 50 num_payloads = 25
try: try:
for exe in interpreters(): for exe in interpreters():
p = run_program(program, exe=exe) p = run_program(program, exe=exe)
...@@ -65,6 +65,21 @@ class TestCodeInjection(unittest.TestCase): ...@@ -65,6 +65,21 @@ class TestCodeInjection(unittest.TestCase):
finally: finally:
os.unlink(program) os.unlink(program)
def test_pyrasite_script(self):
program = generate_program()
try:
for exe in interpreters():
print("sys.executable = %s" % sys.executable)
print("injecting into %s" % exe)
p = run_program(program, exe=exe)
subprocess.call([sys.executable, 'pyrasite/main.py',
str(p.pid), 'pyrasite/payloads/helloworld.py'],
env={'PYTHONPATH': os.getcwd()})
stop_program(p)
stdout, stderr = p.communicate()
self.assert_output_contains(stdout, stderr, 'Hello World!')
finally:
os.unlink(program)
if __name__ == '__main__': if __name__ == '__main__':
unittest.main() unittest.main()
...@@ -16,10 +16,34 @@ ...@@ -16,10 +16,34 @@
# Copyright (C) 2011, 2012 Red Hat, Inc. # Copyright (C) 2011, 2012 Red Hat, Inc.
import os import os
import unittest import sys
import pyrasite import pyrasite
from pyrasite.tests.utils import run_program, generate_program, stop_program from pyrasite.tests.utils import run_program, generate_program, stop_program, unittest
class TestIPCContextManager(unittest.TestCase):
def setUp(self):
self.prog = generate_program()
self.p = run_program(self.prog)
def tearDown(self):
stop_program(self.p)
def test_context_manager(self):
# Check that we're on a version of python that
# supports context managers
info = sys.version_info
major, minor = info[0], info[1]
if major <= 2 and minor <= 5:
self.skipTest("Context Managers not supported on Python<=2.5")
# Otherwise import a module which contains modern syntax.
# It really contains our test case, but we have pushed it out into
# another module so that python 2.4 never sees it.
import pyrasite.tests.context_manager_case
pyrasite.tests.context_manager_case.context_manager_business(self)
class TestIPC(unittest.TestCase): class TestIPC(unittest.TestCase):
......
...@@ -16,11 +16,17 @@ ...@@ -16,11 +16,17 @@
# Copyright (C) 2011-2012 Red Hat, Inc., Luke Macken <lmacken@redhat.com> # Copyright (C) 2011-2012 Red Hat, Inc., Luke Macken <lmacken@redhat.com>
import os import os
import sys
import glob import glob
import time import time
import textwrap import textwrap
import tempfile import tempfile
import subprocess import subprocess
import unittest
if sys.version_info[0] == 2:
if sys.version_info[1] < 7:
import unittest2 as unittest
def generate_program(threads=1): def generate_program(threads=1):
...@@ -30,21 +36,20 @@ def generate_program(threads=1): ...@@ -30,21 +36,20 @@ def generate_program(threads=1):
import os, time, threading import os, time, threading
running = True running = True
pidfile = '/tmp/pyrasite_%d' % os.getpid() pidfile = '/tmp/pyrasite_%d' % os.getpid()
open(pidfile, 'w').close()
def cpu_bound(): def cpu_bound():
i = 2 i = 0
y = 0
def fib(n):
return fib(n - 1) + fib(n - 2)
while running: while running:
y += fib(i)
i += 1 i += 1
while os.path.exists(pidfile):
time.sleep(0.1)
""") """)
# CPU-bound threads # CPU-bound threads
for t in range(threads): for t in range(threads):
script += "threading.Thread(target=cpu_bound).start()\n" script += "threading.Thread(target=cpu_bound).start()\n"
script += "open(pidfile, 'w').close()\n" script += textwrap.dedent("""
while os.path.exists(pidfile):
time.sleep(0.1)
running = False
""")
tmp.write(script) tmp.write(script)
tmp.close() tmp.close()
return filename return filename
......
#!/usr/bin/python
# This file is part of pyrasite. # This file is part of pyrasite.
# #
# pyrasite is free software: you can redistribute it and/or modify # pyrasite is free software: you can redistribute it and/or modify
...@@ -30,8 +29,9 @@ import urwid ...@@ -30,8 +29,9 @@ import urwid
import urwid.raw_display import urwid.raw_display
from meliae import loader from meliae import loader
from os.path import join, abspath, dirname
from pyrasite.inspect import ObjectInspector import pyrasite
class PyrasiteMemoryViewer(object): class PyrasiteMemoryViewer(object):
...@@ -47,7 +47,7 @@ class PyrasiteMemoryViewer(object): ...@@ -47,7 +47,7 @@ class PyrasiteMemoryViewer(object):
] ]
def __init__(self, pid, objects): def __init__(self, pid, objects):
self.inspector = ObjectInspector(pid) self.pid = pid
self.objects = objects self.objects = objects
self.summary = objects.summarize() self.summary = objects.summarize()
...@@ -67,7 +67,7 @@ class PyrasiteMemoryViewer(object): ...@@ -67,7 +67,7 @@ class PyrasiteMemoryViewer(object):
def display_object(self, w, state): def display_object(self, w, state):
if state: if state:
value = self.inspector.inspect(w.obj.max_address) value = pyrasite.inspect(self.pid, w.obj.max_address)
self.object_output.set_text(value) self.object_output.set_text(value)
def get_object_buttons(self, group=[]): def get_object_buttons(self, group=[]):
...@@ -134,16 +134,19 @@ class PyrasiteMemoryViewer(object): ...@@ -134,16 +134,19 @@ class PyrasiteMemoryViewer(object):
def main(): def main():
if len(sys.argv) != 3: if len(sys.argv) != 2:
print "[ pyrasite memory viewer ]\n" print("[ pyrasite memory viewer ]\n")
print "Usage: %s <pid> <objects.json>" % sys.argv[0] print("Usage: %s <pid> <objects.json>" % sys.argv[0])
print "\n pid - the running process id" print("\n pid - the running process id")
print " objects.json - the output of the dump-memory payload" print("")
print
sys.exit(1) sys.exit(1)
pid = int(sys.argv[1]) pid = int(sys.argv[1])
filename = sys.argv[2] payload = abspath(join(dirname(__file__), '..',
'payloads', 'dump_memory.py'))
pyrasite.inject(pid, payload)
filename = '/tmp/pyrasite-%d-objects.json' % pid
objects = loader.load(filename) objects = loader.load(filename)
objects.compute_referrers() objects.compute_referrers()
......
...@@ -26,17 +26,50 @@ def shell(): ...@@ -26,17 +26,50 @@ def shell():
print("Usage: pyrasite-shell <PID>") print("Usage: pyrasite-shell <PID>")
sys.exit(1) sys.exit(1)
ipc = pyrasite.PyrasiteIPC(int(sys.argv[1])) ipc = pyrasite.PyrasiteIPC(int(sys.argv[1]), 'ReversePythonShell')
ipc.connect() ipc.connect()
print("pyrasite shell %s" % pyrasite.__version__) print("Pyrasite Shell %s" % pyrasite.__version__)
print(ipc.cmd('import sys; print("Python " + sys.version + ' + print("Connected to '%s'" % ipc.title)
'" on " + sys.platform)'))
prompt, payload = ipc.recv().split('\n', 1)
print(payload)
try:
import readline
except ImportError:
pass
# py3k compat
try:
input_ = raw_input
except NameError:
input_ = input
try: try:
while True: while True:
print(ipc.cmd(raw_input('>>> '))) try:
input_line = input_(prompt)
except EOFError:
input_line = 'exit()'
print('')
except KeyboardInterrupt:
input_line = 'None'
print('')
ipc.send(input_line)
payload = ipc.recv()
if payload is None:
break
prompt, payload = payload.split('\n', 1)
if payload != '':
print(payload)
except: except:
pass print('')
raise
ipc.close() ipc.close()
print()
if __name__ == '__main__':
shell()
import sys
from setuptools import setup, find_packages from setuptools import setup, find_packages
version = '2.0beta9' try:
# These imports are not used, but make
# tests pass smoothly on python2.7
import multiprocessing
import logging
except Exception:
pass
version = '2.0'
f = open('README.rst') f = open('README.rst')
long_description = f.read().split('split here')[1] long_description = f.read().split('split here')[1]
f.close() f.close()
requirements = []
if sys.version_info[0] == 3:
if sys.version_info[1] < 2:
requirements.append('argparse')
elif sys.version_info[0] == 2:
if sys.version_info[1] < 7:
requirements.append('argparse')
tests_require = ['nose']
if sys.version_info[0] == 2:
if sys.version_info[1] < 7:
tests_require.append('unittest2')
setup(name='pyrasite', setup(name='pyrasite',
version=version, version=version,
description="Inject code into a running Python process", description="Inject code into a running Python process",
...@@ -18,8 +40,8 @@ setup(name='pyrasite', ...@@ -18,8 +40,8 @@ setup(name='pyrasite',
packages=find_packages(exclude=['ez_setup', 'examples', 'tests']), packages=find_packages(exclude=['ez_setup', 'examples', 'tests']),
include_package_data=True, include_package_data=True,
zip_safe=False, zip_safe=False,
install_requires=[], install_requires=requirements,
tests_require=['nose'], tests_require=tests_require,
test_suite='nose.collector', test_suite='nose.collector',
entry_points=""" entry_points="""
[console_scripts] [console_scripts]
......
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