如何使python脚本可从另一个脚本停止?

时间:2019-07-11 19:36:28

标签: python

TL; DR::如果您的程序应运行不确定的时间,那么当用户确定时间到了时,如何编写代码以使其停止? (没有KeyboardInterrupt或杀死任务)

-

我最近发布了以下问题:How to make my code stopable? (Not killing/interrupting) 答案确实解决了我的问题,但是从终止/中断的角度来看,这并不是我真正想要的。 (尽管我的问题并没有明确说明) 所以,我改写它。

出于示例目的,我创建了一个通用脚本。因此,我有一个此类,该类从通用API收集数据并将数据写入csv。通过在终端窗口上输入python main.py来启动代码。

import time,csv

import GenericAPI

class GenericDataCollector:
    def __init__(self):
        self.generic_api = GenericAPI()
        self.loop_control = True

    def collect_data(self):
        while self.loop_control: #Can this var be changed from outside of the class? (Maybe one solution)
            data = self.generic_api.fetch_data() #Returns a JSON with some data
            self.write_on_csv(data)
            time.sleep(1)

    def write_on_csv(self, data):
        with open('file.csv','wt') as f:
            writer = csv.writer(f)
            writer.writerow(data)

def run():
    obj = GenericDataCollector()
    obj.collect_data()

if __name__ == "__main__":
    run()

该脚本应该永久运行,或者直到我命令停止运行。。我知道我可以KeyboardInterrupt (Ctrl+C)或突然终止该任务。 这不是我想要的。我想要一种“软”的方式告诉脚本停止的时间,这不仅是因为中断是无法预测的,而且也是一种艰难的停止方式。

例如,如果该脚本正在docker容器上运行,则除非您碰巧位于docker内部的终端/ bash中,否则您将无法Ctrl+C

或者另一种情况:如果该脚本是为客户创建的,我认为告诉客户不是可以的,只需使用Ctrl+C / kill任务来停止它。绝对违反直觉,特别是如果它是非技术人员。

我正在寻找编码另一个脚本(假设这是一个可能的解决方案)的方法,该脚本将更改为False属性obj.loop_control,并在完成后完成循环。可以通过在(不同的)终端python stop_script.py上键入内容来运行某些内容。

不一定非要这样。也可以接受其他解决方案,只要它不涉及KeyboardInterrupt或Killing任务即可。如果我可以在类中使用方法,那很好,只要我可以从另一个终端/脚本中调用它即可。

有没有办法做到这一点?

如果您的程序应运行不确定的时间,那么当用户确定时间到了时,如何编写代码以使其停止?

2 个答案:

答案 0 :(得分:4)

通常,有两种主要方法(据我所知)。第一个是使您的脚本检查可以从外部修改的某些条件(例如某些文件/套接字的存在或内容)。或如@Green Cloak Guy所述,使用管道是进程间通信的一种形式。

第二个方法是使用运行Python的每个操作系统中都存在的built in mechanism for interprocess communication called signals。当用户按下Ctrl + C时,终端会向前台的进程发送特定信号。但是您可以通过编程方式(即从另一个脚本)发送相同(或另一个)信号。

阅读其他问题的答案,我想说的是,解决此问题所缺少的是一种向已经运行的进程发送适当信号的方法。本质上,这可以by using the os.kill() function完成。请注意,尽管该函数称为“ kill”,但它可以发送任何信号(不仅是SIGKILL)。

为此,您需要具有正在运行的进程的进程ID。一种了解这种情况的常用方法是使脚本在启动到公共位置存储的文件中时保存其进程ID。要获取当前的进程ID,请can use the os.getpid() function

因此,总而言之,我想说的是实现您想要的步骤:

  1. 修改当前脚本以将其进程ID(可通过使用os.getpid()获取)存储到公共位置(例如/tmp/myscript.pid)的文件中。请注意,如果希望脚本稳定运行,则需要以一种在非Unix之类的操作系统(如Windows)中工作的方式来解决此问题。
  2. 选择一个信号(通常为SIGINTSIGSTOPSIGTERM)并修改脚本以使用signal.signal()注册自定义处理程序,以解决脚本的正常终止。
  3. 创建另一个(请注意,它可能是具有某些命令行参数的同一脚本)脚本,该脚本从已知文件(aka /tmp/myscript.pid)中读取进程ID,并发送选定的信号使用os.kill()完成该过程。

请注意,使用信号而不是外部方式(文件,管道等)来实现此目的的一个优点是,用户仍然可以按Ctrl + C(如果选择了SIGINT),它将产生行为与“停止脚本”相同。

答案 1 :(得分:2)

您真正要寻找的是将信号从一个程序发送到另一个独立程序的任何方法。一种实现方法是使用进程间管道。 Python has a module for this(诚然,它确实需要兼容POSIX的外壳,但是大多数主要的操作系统都应该提供它)。

您需要做的是事先在运行程序(例如main.py)和停止程序(例如stop.sh)之间建立一个文件路径。然后,您可能使主程序运行,直到有人向该管道输入内容为止。

import pipes
...
t = pipes.Template()
# create a pipe in the first place
t.open("/tmp/pipefile", "w")
# create a lasting pipe to read from that
pipefile = t.open("/tmp/pipefile", "r")
...

现在,在程序内部,将循环条件更改为“只要该文件没有输入-除非有人向它写入内容,否则.read()将返回一个空字符串:

while not pipefile.read():
    # do stuff

要停止它,请放置另一个文件或脚本或将写入该文件的内容。这是使用Shell脚本最简单的方法:

#!/usr/bin/env sh
echo STOP >> /tmp/pipefile

如果要对其进行容器化,则可以放入/usr/bin并将其命名为stop,至少赋予其0111权限,并告诉用户“停止程序,只需执行docker exec containername stop”。

(使用>>代替>很重要,因为我们只想追加到管道,而不是覆盖管道)。


我的python控制台上的概念证明:

>>> import pipes
>>> t = pipes.Template()
>>> t.open("/tmp/file1", "w")
<_io.TextIOWrapper name='/tmp/file1' mode='w' encoding='UTF-8'>
>>> pipefile = t.open("/tmp/file1", "r")
>>> i = 0
>>> while not pipefile.read():
...     i += 1
... 

这时我转到另一个终端选项卡,然后执行

$ echo "Stop" >> /tmp/file1

然后,我返回python选项卡,并且while循环不再执行,因此我可以检查离开后i发生了什么。

>>> print(i)
1704312