与长时间运行的Python脚本交互

6

我有一个长时间运行的Python脚本,从Twitter收集推文,我想偶尔知道它的运行情况。

目前,我正在使用signal库来捕获中断,在这种情况下调用我的打印函数。如下所示:

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))

每次当我想要查看信息时,我会打开一个新的终端并输入我的中断命令:
$kill -USR1 <pid>

有更好的方法吗?我知道可以按计划间隔运行脚本,但我更想了解按需求运行,并可能发出其他命令的方法。


这似乎是一个适用的信号库。你为什么认为有更好的方法呢? - MrAlias
你可以创建一个线程监听套接字以便连接,当客户端连接时向套接字写入信息。 - larsks
@MrAlias,我计划拥有不止一个“print”方法,并且据我所知,我可以使用有限的中断,因此我想也许有一种不同的方式可以在运行时与我的程序交互。 - Tyler
如果只有一个或几个命令,我认为信号就足够了。但是如果你有很多不同的命令,我更喜欢使用一个线程监听套接字或者只读取文件来接受命令。 - WKPlus
知道 signal.signal 发送信号编号和帧会很有帮助。您可以从帧中提取信息,例如全局和局部变量(可能是 tweet_counts 和其他所需状态对象...)。另外,如果您喜欢将信息保存到文件中的想法,logging 模块是一个好选择。 - MrAlias
rsync使用USR1方法,供参考。你的问题实际上是当你需要多种交互方式时如何交互,还是只是获取状态?问题文本似乎与标题不符。 - Daenyth
6个回答

2

向进程发送信号会中断该进程。下面是一种使用专用线程模拟Python控制台的方法。该控制台以Unix套接字的形式公开。

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

示例程序:

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

示例会话:

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:~$ 

这是一个非常强大的工具,可用于以下操作:
  • 转储线程,
  • 检查进程内存,
  • 按需附加调试器,pydev调试器(适用于eclipse和pycharm),
  • 强制GC,
  • 动态猴子补丁函数定义。
甚至更多。

很好。对我来说非常有用的片段。投了赞成票,但需要在Python 3中使用。 - xendi
这是我认为可以工作的Python 3版本:https://hastebin.com/eketewaxes.py - xendi
AttributeError: module 'socket' has no attribute '_fileobject' - xendi

0
这是一个示例长时间运行的程序,同时还维护着一个状态套接字。当客户端连接到套接字时,脚本会向套接字写入一些状态信息。
#!/usr/bin/python

import os
import sys
import argparse
import random
import threading
import socket
import time
import select

val1 = 0
val2 = 0
lastupdate = 0
quit = False

# This function runs in a separate thread.  When a client connects,
# we write out some basic status information, close the client socket,
# and wait for the next connection.
def connection_handler(sock):
    global val1, val2, lastupdate, quit

    while not quit:
        # We use select() with a timeout here so that we are able to catch the
        # quit flag in a timely manner.
        rlist, wlist, xlist = select.select([sock],[],[], 0.5)
        if not rlist:
            continue

        client, clientaddr = sock.accept()
        client.send('%s %s %s\n' % (lastupdate, val1, val2))
        client.close()

# This function starts the listener thread.
def start_listener():
    sock = socket.socket(socket.AF_UNIX)

    try:
        os.unlink('/var/tmp/myprog.socket')
    except OSError:
        pass

    sock.bind('/var/tmp/myprog.socket')
    sock.listen(5)

    t = threading.Thread(
        target=connection_handler,
        args=(sock,))
    t.start()

def main():
    global val1, val2, lastupdate

    start_listener()

    # Here is the part of our script that actually does "work".
    while True:
        print 'updating...'
        lastupdate = time.time()
        val1 = val1 + random.randint(1,10)
        val2 = val2 + random.randint(100,200)
        print 'sleeping...'
        time.sleep(5)

if __name__ == '__main__':
    try:
        main()
    except (Exception,KeyboardInterrupt,SystemExit):
        quit=True
        raise

你可以编写一个简单的Python客户端来连接套接字,或者你可以使用类似于socat的工具:

$ socat - unix:/var/tmp/myprog.sock
1403061693.06 6 152

0

您的问题涉及进程间通信。您可以通过使用Unix套接字或TCP端口进行通信,使用共享内存,或使用消息队列或缓存系统(如RabbitMQ和Redis)来实现。

这篇文章讨论了使用mmap实现共享内存进程间通信的方法。

以下是如何开始使用redisRabbitMQ,两者都相当简单易用。


0

我个人喜欢将信息写入文件以便日后查看,但这样做的劣势是它可能会略微变慢,因为每次或每几次检索推文时都需要写入文件。

无论如何,如果你将其写入一个名为 "output.txt" 的文件中,你可以打开 bash 并键入 tail output.txt 查看文件中最新的10行数据,或者键入 tail -f output.txt 以持续更新终端提示所写入的内容。如果你想停止,只需键入Ctrl-C


0

我之前写过类似的应用。

这是我的做法:

当只有几个所需命令时,我就像你一样使用信号,以免使它过于复杂。命令是指您想要应用程序执行的操作,例如您帖子中的print_info

但是当应用程序更新后,需要更多不同的命令,我开始使用特殊线程监听套接字端口或读取本地文件以接受命令。假设应用程序需要支持prinf_info1 print_info2 print_info3,那么您可以使用客户端连接到目标端口并编写print_info1以使应用程序执行print_info1(如果您正在使用读取本地文件机制,则只需将print_info1编写到本地文件中)。

使用套接字端口监听机制的缺点是编写客户端以提供命令需要更多的工作量,但优点是可以在任何地方下达命令。

当使用读取本地文件机制时,缺点是您必须使线程在循环中检查文件,并且它会使用一些资源,优点是给出指令非常简单(只需将字符串写入文件),而且您不需要编写客户端和套接字监听服务器。


0

rpyc 是完成此任务的完美工具。

简而言之,您需要定义一个rpyc.Service类,该类公开您想要公开的命令,并启动一个rpyc.Server线程。

然后,您的客户端连接到您的进程,并调用映射到服务公开的命令的方法。

就是这么简单和干净。无需担心套接字、信号、对象序列化等问题。

它还有其他很酷的功能,例如协议是对称的。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接