Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Interacting with long-running python script

Tags:

python

I have a long running Python script which collect tweets from Twitter, and I would like to know how its doing every once in awhile.

Currently, I am using the signal library to catch interrupts, at which point I call my print function. Something like this:

import signal

def print_info(count):
    print "#Tweets:", count

#Print out the process ID so I can interrupt it for info
print 'PID:', os.getpid()

#Start listening for interrupts
signal.signal(signal.SIGUSR1, functools.partial(print_info, tweet_count))

And whenever I want my info, I open up a new terminal and issue my interrupt:

$kill -USR1 <pid>

Is there a better way to do this? I am aware I could have my script something at scheduled intervals, but I am more interested in knowing on demand, and potentially issuing other commands as well.

like image 275
Tyler Avatar asked Jun 18 '14 02:06

Tyler


1 Answers

Sending a signal to process would interrupt the process. Below you will find an approach that uses dedicated thread to emulate python console. The console is exposed as a unix socket.

import traceback
import importlib
from code import InteractiveConsole
import sys
import socket
import os
import threading
from logging import getLogger

# template used to generate file name
SOCK_FILE_TEMPLATE = '%(dir)s/%(prefix)s-%(pid)d.socket'

log = getLogger(__name__)


class SocketConsole(object):
    '''
    Ported form :eventlet.backdoor.SocketConsole:.
    '''
    def __init__(self, locals, conn, banner=None):  # pylint: diable=W0622
        self.locals = locals
        self.desc = _fileobject(conn)
        self.banner = banner
        self.saved = None

    def switch(self):
        self.saved = sys.stdin, sys.stderr, sys.stdout
        sys.stdin = sys.stdout = sys.stderr = self.desc

    def switch_out(self):
        sys.stdin, sys.stderr, sys.stdout = self.saved

    def finalize(self):
        self.desc = None

    def _run(self):
        try:
            console = InteractiveConsole(self.locals)
            # __builtins__ may either be the __builtin__ module or
            # __builtin__.__dict__ in the latter case typing
            # locals() at the backdoor prompt spews out lots of
            # useless stuff
            import __builtin__
            console.locals["__builtins__"] = __builtin__
            console.interact(banner=self.banner)
        except SystemExit:  # raised by quit()
            sys.exc_clear()
        finally:
            self.switch_out()
            self.finalize()


class _fileobject(socket._fileobject):
    def write(self, data):
        self._sock.sendall(data)

    def isatty(self):
        return True

    def flush(self):
        pass

    def readline(self, *a):
        return socket._fileobject.readline(self, *a).replace("\r\n", "\n")


def make_threaded_backdoor(prefix=None):
    '''
    :return: started daemon thread running :main_loop:
    '''
    socket_file_name = _get_filename(prefix)

    db_thread = threading.Thread(target=main_loop, args=(socket_file_name,))
    db_thread.setDaemon(True)
    db_thread.start()
    return db_thread


def _get_filename(prefix):
    return SOCK_FILE_TEMPLATE % {
        'dir': '/var/run',
        'prefix': prefix,
        'pid': os.getpid(),
    }


def main_loop(socket_filename):
    try:
        log.debug('Binding backdoor socket to %s', socket_filename)
        check_socket(socket_filename)

        sockobj = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
        sockobj.bind(socket_filename)
        sockobj.listen(5)
    except Exception, e:
        log.exception('Failed to init backdoor socket %s', e)
        return

    while True:
        conn = None
        try:
            conn, _ = sockobj.accept()
            console = SocketConsole(locals=None, conn=conn, banner=None)
            console.switch()
            console._run()
        except IOError:
            log.debug('IOError closing connection')
        finally:
            if conn:
                conn.close()


def check_socket(socket_filename):
    try:
        os.unlink(socket_filename)
    except OSError:
        if os.path.exists(socket_filename):
            raise

Example program:

make_threaded_backdoor(prefix='test')
while True:
    pass

Example session:

mmatczuk@cactus:~$ rlwrap nc -U /var/run/test-3196.socket
Python 2.7.6 (default, Mar 22 2014, 22:59:56) 
[GCC 4.8.2] on linux2
Type "help", "copyright", "credits" or "license" for more information.
(InteractiveConsole)
>>> import os
>>> os.getpid()
3196
>>> quit()
mmatczuk@cactus:~$ 

This is a pretty robust tool that can be used to:

  • dump threads,
  • inspect process memory,
  • attach debugger on demand, pydev debugger (work for both eclipse and pycharm),
  • force GC,
  • monkeypatch function definition on the fly

and even more.

like image 125
mmatczuk Avatar answered Nov 14 '22 23:11

mmatczuk