3

我一直在开发一个需要管理外部进程的 gui 应用程序。使用外部流程会导致许多问题,这些问题会使程序员的生活变得困难。我觉得这个应用程序的维护需要很长的时间。我一直在尝试列出使使用外部流程变得困难的事情,以便我可以想出减轻痛苦的方法。这变成了一种咆哮,我想我会在这里发布,以获得一些反馈,并为任何考虑驶入这些非常混浊的水域的人提供一些指导。这是我到目前为止所得到的:

  1. 孩子的输出可能会与父母的输出混淆。这会使两个输出都具有误导性并且难以阅读。很难说什么是从哪里来的。当事情是异步的时,很难弄清楚发生了什么。这是一个人为的例子:

    import textwrap, os, time
    from subprocess import Popen
    test_path = 'test_file.py'
    
    with open(test_path, 'w') as file:
        file.write(textwrap.dedent('''
            import time
            for i in range(3):
                print 'Hello %i' % i
                time.sleep(1)'''))
    
    proc = Popen('python -B "%s"' % test_path)
    
    for i in range(3):
        print 'Hello %i' % i
        time.sleep(1)
    
    os.remove(test_path)
    

    输出:

    Hello 0
    Hello 0
    Hello 1
    Hello 1
    Hello 2
    Hello 2
    

    我想我可以让子进程将其输出写入文件。但是每次我想查看打印语句的结果时都必须打开一个文件可能会很烦人。

    如果我有子进程的代码,我可以添加一个标签,比如print 'child: Hello %i',但是每次打印都这样做会很烦人。它给输出增加了一些噪音。当然,如果我无法访问代码,我就无法做到这一点。

    我可以手动管理流程输出。但随后你打开了一大罐带有线程和轮询之类的蠕虫。

    一个简单的解决方案是将进程视为同步函数,即在进程完成之前不执行进一步的代码。换句话说,使进程阻塞。但是,如果您正在构建一个 gui 应用程序,那将不起作用。这让我想到了下一个问题......

  2. 阻塞进程会导致 gui 变得无响应。

    import textwrap, sys, os
    from subprocess import Popen
    
    from PyQt4.QtGui import *
    from PyQt4.QtCore import *
    
    test_path = 'test_file.py'
    with open(test_path, 'w') as file:
        file.write(textwrap.dedent('''
            import time
            for i in range(3):
                print 'Hello %i' % i
                time.sleep(1)'''))
    
    app = QApplication(sys.argv)
    button = QPushButton('Launch process')
    def launch_proc():
        # Can't move the window until process completes
        proc = Popen('python -B "%s"' % test_path)
        proc.communicate()
    button.connect(button, SIGNAL('clicked()'), launch_proc)
    button.show()
    app.exec_() 
    os.remove(test_path)
    

    Qt 提供了一个自己的进程包装器QProcess,可以帮助解决这个问题。您可以将函数连接到信号以相对轻松地捕获输出。这是我目前正在使用的。但是我发现所有这些信号的行为都像goto语句一样可疑,并且可能导致意大利面条式代码。我想我想通过让来自 QProcess 的“完成”信号调用一个包含进程调用之后的所有代码的函数来获得某种阻塞行为。我认为这应该可行,但我对细节仍然有点模糊......

  3. 当您从子进程返回父进程时,堆栈跟踪会中断。如果一个正常的函数搞砸了,你会得到一个带有文件名和行号的完整堆栈跟踪。如果一个子进程搞砸了,如果你得到任何输出,你会很幸运。每次出现问题时,您最终都必须做更多的侦探工作。

  4. 说到这一点,在处理外部流程时,输出有一种消失的方式。就像你通过 windows 'cmd' 命令运行某些东西一样,控制台会弹出,执行代码,然后在你有机会看到输出之前消失。您必须传递 /k 标志才能使其保持不变。类似的问题似乎一直在出现。

    我想问题 3 和 4 都有相同的根本原因:没有异常处理。异常处理旨在与函数一起使用,它不适用于进程。也许有一些方法可以获得诸如进程异常处理之类的东西?我想这就是 stderr 的用途吗?但是处理两个不同的流本身就很烦人。也许我应该更多地研究这个......

  5. 进程可能会在您不知不觉中徘徊在后台。所以你最终会对你的电脑大喊大叫,因为它运行得太慢了,直到你最终打开你的任务管理器并看到 30 个相同进程的实例在后台挂起。

    此外,挂起的后台进程可以以各种有趣的方式与进程的其他实例进行交互,例如通过持有文件的句柄或类似的方式导致权限错误。

    如果子进程没有自行关闭,似乎一个简单的解决方案是让父进程在退出时杀死子进程。但是如果进程崩溃,清理代码可能不会被调用,并且子进程可能会挂起。

    另外,如果父级等待子级完成,而子级处于无限循环之类的状态,则最终可能会出现两个挂起的进程。

    这个问题可以与问题 2 相关联以获得额外的乐趣,导致您的 gui 完全停止响应并迫使您使用任务管理器杀死所有内容。

  6. 他妈的报价

    参数通常需要传递给进程。这本身就是一个令人头疼的问题。特别是如果您正在处理文件路径。说...'C:/我的文档/无论如何/'。如果您没有引号,则字符串通常会在空格处拆分并解释为两个参数。如果需要嵌套引号,可以使用'和"。但如果需要使用多于两层的引号,则必须进行一些讨厌的转义,例如:"cmd /k 'python \'path 1\' \'路径 2\''"。

    这个问题的一个很好的解决方案是将参数作为列表而不是单个字符串传递。Subprocess 允许您执行此操作。

  7. 无法轻松地从子流程返回数据。

    你当然可以使用标准输出。但是,如果您想在其中打印以进行调试怎么办?如果它期望输出以某种方式格式化,那将会搞砸父级。在函数中,您可以打印一个字符串并返回另一个字符串,一切正常。

  8. 晦涩的命令行标志和糟糕的基于终端的帮助系统。

    这些是我在使用操作系统级别的应用程序时经常遇到的问题。就像我提到的 /k 标志一样,打开一个 cmd 窗口,这是谁的主意?Unix 应用程序在这方面往往不太友好。希望您可以使用 google 或 StackOverflow 找到您需要的答案。但如果不是这样,你就有很多无聊的阅读和令人沮丧的反复试验要做。

  9. 外在因素。

    这个有点模糊 但是当您离开自己脚本的相对隐蔽的港湾来处理外部进程时,您会发现自己不得不在更大程度上处理“外部世界”。那是一个可怕的地方。各种各样的事情都可能出错。举一个随机的例子:运行进程的 cwd 可以修改它的行为。

可能还有其他问题,但这些是我迄今为止写下来的。您还想添加任何其他障碍吗?对于处理这些问题有什么建议吗?

4

1 回答 1

2

查看子流程模块。它应该有助于输出分离。我看不到任何单独的输出流或单个流中的某种输出标记的方法。

悬挂过程问题也很困难。我能够做出的唯一解决方案是在外部进程上放置一个计时器,如果它没有在分配的时间内返回,则将其杀死。粗鲁,讨厌,如果其他人有一个好的解决方案,我很想听听,所以我也可以使用它。

您可以做的一件事来帮助处理完全非托管关闭的问题,那就是保留一个 pid 文件目录。每当您启动一个外部进程时,将一个文件写入您的 pid 文件目录,其名称是该进程的 pid。当您知道进程已干净退出时,请擦除 pid 文件。您可以使用 pid 目录中的内容来帮助清理崩溃或重新启动。

这可能无法提供任何令人满意或有用的答案,但也许这是一个开始。

于 2010-05-23T20:48:52.813 回答