【问题标题】:How to detect when subprocess asks for input in Windows如何检测子进程何时在 Windows 中要求输入
【发布时间】:2018-08-30 16:32:28
【问题描述】:

我有一个子进程,它要么以返回码退出,要么询问某些内容并等待用户输入。

我想检测进程何时提出问题并立即退出。进程是否提出问题这一事实足以让我决定系统的状态。

问题是我无法阅读该问题,因为子进程可能不会刷新标准输出。所以我不能依赖解析subprocess.Popen().stdout:当尝试读取它时,它会阻塞,因为首先读取输入。

有点像这样

# ask.py, just asks something without printing anything if a condition is met
# here, we'll say that the condition is always met
input()

当然,实际的子进程是第三方二进制文件,我不能轻易修改它以添加必要的刷新调用,这将解决它。

我也可以尝试 Windows 等效的 unbuffer (What is the equivalent of unbuffer program on Windows?),它被称为 winpty,它(也许)可以让我检测输出并解决我当前的问题,但我想保留它很简单,我想先解决标准 input 问题...

我尝试了...好吧,很多事情都不起作用,包括尝试将假文件作为stdin 参数传递,这不起作用,因为subprocess 采用了文件的fileno,而且我们不能喂它垃圾......

p = subprocess.Popen(["python","ask.py"],...)

communicate 与字符串一起使用也不起作用,因为您无法控制何时读取字符串以将其馈送到子进程(可能通过系统管道)。

这些问题很有希望,但要么依赖于标准输出,要么仅适用于 Linux

我目前正在做的是超时运行进程,如果达到超时,然后我决定程序被阻止。但它会花费超时等待时间。如果我可以在子进程读取stdin 时立即做出决定,那就更好了。

我想知道是否有本地 python 解决方案(可能使用ctypes 和 windows 扩展)来检测从标准输入的读取。但是不使用 Python 而是使用非 Microsoft 专有语言的本机解决方案可以做到。

【问题讨论】:

  • 您对尝试检测其状态的子进程有任何控制权吗?
  • 好吧,我当然可以运行它:) 你是什么意思?
  • 控制我的意思是你是否可以修改它的代码
  • 错误,不,它是第三方二进制文件。
  • 如果可以,您应该使用pexepct 包,地址为pypi.python.org/pypi/pexpect。否则,您可能需要使用pty 包来打开一个伪终端,以使您的子进程相信它正在与终端通信而不是(无终端)子进程。或者,如果您的第三方文件是 python 脚本,您可以在调用脚本 (['python', '-u', 'ask.py']) 时使用 -u 标志强制它不缓冲输出,但您也可以对其进行编辑。

标签: python windows subprocess io-redirection


【解决方案1】:

我想确定子进程是否读取用户输入的想法是(ab)使用文件对象是有状态的这一事实:如果进程从其标准输入中读取数据,我们应该能够检测到标准输入状态的变化。

程序如下:

  1. 创建一个临时文件,用作子进程的标准输入
  2. 将一些数据写入文件
  3. 开始进程
  4. 稍等片刻让进程读取数据(或不读取数据),然后使用tell() 方法找出是否已从文件中读取任何内容

这是代码:

import os
import time
import tempfile
import subprocess

# create a file that we can use as the stdin for the subprocess
with tempfile.TemporaryFile() as proc_stdin:
    # write some data to the file for the subprocess to read
    proc_stdin.write(b'whatever\r\n')
    proc_stdin.seek(0)

    # start the thing
    cmd = ["python","ask.py"]
    proc = subprocess.Popen(cmd, stdin=proc_stdin, stdout=subprocess.PIPE)

    # wait for it to start up and do its thing
    time.sleep(1)

    # now check if the subprocess read any data from the file
    if proc_stdin.tell() == 0:
        print("it didn't take input")
    else:
        print("it took input")

理想情况下,临时文件可以用某种管道或不向磁盘写入任何数据的东西替换,但不幸的是,如果没有真正的磁盘文件,我找不到让它工作的方法。

【讨论】:

  • 不错的尝试,但是如果允许该进程获取所有输入,它将到达文件末尾并执行我不希望它执行的任务。我需要检测一个字符读取,以便我可以终止进程。
  • 如果问题是“如何检测子进程何时要求在 Windows 中输入”,这将回答。我需要即时检测。
  • @Jean-FrançoisFabre - 我需要检测一个字符读取,以便我可以终止进程 - 如果你想终止进程,如果它从 stdin - 您需要创建大小为 0 缓冲区的异步管道。将子 stdin 连接到此管道,并将 1 个字节写入管道,并将事件作为完成。因为管道缓冲区为 0 - 直到有人未从连接的管道末端(stdin)读取或您未关闭管道句柄,操作才完成。然后您使用重定向的 stdin 启动子进程并等待事件+子退出。如果事件首先触发 - 孩子从 stdin. 开始读取
  • 不知道在pyton中,但是在c++中这很容易实现
  • time.sleep(1) - 为什么不time.sleep(748)
【解决方案2】:

如果我们不想让子进程处理用户输入,而只是在这种情况下将其杀死,则可以是下一个解决方案:

  • 启动子进程并将stdin重定向到管道。
  • 我们以异步方式创建的管道服务器端和主设置管道 缓冲区大小为 0
  • 在启动子之前 - 将 1 个字节写入此管道。
  • 因为管道缓冲区大小为 0 - 操作未完成,直到另一个 这边没有读到这个字节
  • 在我们写入这 1 个字节并且操作正在进行(待定)之后 - 启动子进程。
  • 终于开始等待什么先完成:写操作还是子进程?
  • 如果先写完成 - 这意味着子进程开始读 来自 stdin - 所以此时杀死它

c++ 上的一种可能实现:

struct ReadWriteContext : public OVERLAPPED
{
    enum OpType : char { e_write, e_read } _op;
    BOOLEAN _bCompleted;

    ReadWriteContext(OpType op) : _op(op), _bCompleted(false)
    {
    }
};

VOID WINAPI OnReadWrite(DWORD dwErrorCode, DWORD dwNumberOfBytesTransfered, OVERLAPPED* lpOverlapped)
{
    static_cast<ReadWriteContext*>(lpOverlapped)->_bCompleted = TRUE;
    DbgPrint("%u:%x %p\n", static_cast<ReadWriteContext*>(lpOverlapped)->_op, dwErrorCode, dwNumberOfBytesTransfered);
}

void nul(PCWSTR lpApplicationName)
{
    ReadWriteContext wc(ReadWriteContext::e_write), rc(ReadWriteContext::e_read);

    static const WCHAR pipename[] = L"\\\\?\\pipe\\{221B9EC9-85E6-4b64-9B70-249026EFAEAF}";

    if (HANDLE hPipe = CreateNamedPipeW(pipename, PIPE_ACCESS_DUPLEX|FILE_FLAG_OVERLAPPED, 
        PIPE_TYPE_BYTE|PIPE_READMODE_BYTE|PIPE_WAIT, 1, 0, 0, 0, 0))
    {
        static SECURITY_ATTRIBUTES sa = { sizeof(sa), 0, TRUE };
        PROCESS_INFORMATION pi;
        STARTUPINFOW si = { sizeof(si)};
        si.dwFlags = STARTF_USESTDHANDLES;
        si.hStdInput = CreateFileW(pipename, FILE_GENERIC_READ|FILE_GENERIC_WRITE, 0, &sa, OPEN_EXISTING, 0, 0);

        if (INVALID_HANDLE_VALUE != si.hStdInput)
        {
            char buf[256];

            if (WriteFileEx(hPipe, "\n", 1, &wc, OnReadWrite))
            {
                si.hStdError = si.hStdOutput = si.hStdInput;

                if (CreateProcessW(lpApplicationName, 0, 0, 0, TRUE, CREATE_NO_WINDOW, 0, 0, &si, &pi))
                {
                    CloseHandle(pi.hThread);

                    BOOLEAN bQuit = true;

                    goto __read;
                    do 
                    {
                        bQuit = true;

                        switch (WaitForSingleObjectEx(pi.hProcess, INFINITE, TRUE))
                        {
                        case WAIT_OBJECT_0:
                            DbgPrint("child terminated\n");
                            break;
                        case WAIT_IO_COMPLETION:
                            if (wc._bCompleted)
                            {
                                DbgPrint("child read from hStdInput!\n");
                                TerminateProcess(pi.hProcess, 0);
                            }
                            else if (rc._bCompleted)
                            {
__read:
                                rc._bCompleted = false;
                                if (ReadFileEx(hPipe, buf, sizeof(buf), &rc, OnReadWrite))
                                {
                                    bQuit = false;
                                }
                            }
                            break;
                        default:
                            __debugbreak();
                        }
                    } while (!bQuit);

                    CloseHandle(pi.hProcess);
                }
            }

            CloseHandle(si.hStdInput);

            // let execute pending apc
            SleepEx(0, TRUE);
        }

        CloseHandle(hPipe);
    }
}

代码的另一种变体 - 使用事件完成,而不是 apc。但是这不会影响最终结果。这种代码变体给出了与第一个相同的结果:

void nul(PCWSTR lpApplicationName)
{
    OVERLAPPED ovw = {}, ovr = {};

    if (ovr.hEvent = CreateEvent(0, 0, 0, 0))
    {
        if (ovw.hEvent = CreateEvent(0, 0, 0, 0))
        {
            static const WCHAR pipename[] = L"\\\\?\\pipe\\{221B9EC9-85E6-4b64-9B70-249026EFAEAF}";

            if (HANDLE hPipe = CreateNamedPipeW(pipename, PIPE_ACCESS_DUPLEX|FILE_FLAG_OVERLAPPED, 
                PIPE_TYPE_BYTE|PIPE_READMODE_BYTE|PIPE_WAIT, 1, 0, 0, 0, 0))
            {
                static SECURITY_ATTRIBUTES sa = { sizeof(sa), 0, TRUE };
                PROCESS_INFORMATION pi;
                STARTUPINFOW si = { sizeof(si)};
                si.dwFlags = STARTF_USESTDHANDLES;
                si.hStdInput = CreateFileW(pipename, FILE_GENERIC_READ|FILE_GENERIC_WRITE, 0, &sa, OPEN_EXISTING, 0, 0);

                if (INVALID_HANDLE_VALUE != si.hStdInput)
                {
                    char buf[256];

                    if (!WriteFile(hPipe, "\n", 1, 0, &ovw) && GetLastError() == ERROR_IO_PENDING)
                    {
                        si.hStdError = si.hStdOutput = si.hStdInput;

                        if (CreateProcessW(lpApplicationName, 0, 0, 0, TRUE, CREATE_NO_WINDOW, 0, 0, &si, &pi))
                        {
                            CloseHandle(pi.hThread);

                            BOOLEAN bQuit = true;

                            HANDLE h[] = { ovr.hEvent, ovw.hEvent, pi.hProcess };

                            goto __read;
                            do 
                            {
                                bQuit = true;

                                switch (WaitForMultipleObjects(3, h, false, INFINITE))
                                {
                                case WAIT_OBJECT_0 + 0://read completed
__read:
                                    if (ReadFile(hPipe, buf, sizeof(buf), 0, &ovr) || GetLastError() == ERROR_IO_PENDING)
                                    {
                                        bQuit = false;
                                    }
                                    break;
                                case WAIT_OBJECT_0 + 1://write completed
                                    DbgPrint("child read from hStdInput!\n");
                                    TerminateProcess(pi.hProcess, 0);
                                    break;
                                case WAIT_OBJECT_0 + 2://process terminated
                                    DbgPrint("child terminated\n");
                                    break;
                                default:
                                    __debugbreak();
                                }
                            } while (!bQuit);

                            CloseHandle(pi.hProcess);
                        }
                    }

                    CloseHandle(si.hStdInput);
                }

                CloseHandle(hPipe);
                // all pending operation completed here.
            }

            CloseHandle(ovw.hEvent);
        }

        CloseHandle(ovr.hEvent);
    }
}

【讨论】:

  • 我想我什至不会尝试将它移植到 python。我将在一个将命令作为参数的小工具 exe 中按原样使用代码,这样我就可以从我的 python 代码中运行它。谢谢。我很快就会对此进行测试。
  • @Jean-FrançoisFabre - 在我使用 cmd.exe/notepad (xp-win10) 进行的测试中,这个效果很好。当然可以很少更改代码,用于代替 apc 完成,重叠的事件完成。但这不会影响结果
  • @Jean-FrançoisFabre - DbgPrint 这仅用于调试输出。您完全可以删除它(无需替换)-它没有任何功能点。我使用 UNICODE (W) api。你看起来像尝试ansi。我使用 CL.EXE (msvc) 编译器
  • @Jean-FrançoisFabre - 直接声明 STARTUPINFOWW。我只是使用定义的 unicode 并自动为我添加。改为STARTUPINFOW si = { sizeof(si)};
  • DbgPrint 或打印 - 帮助测试,但始终是调试器下的最佳测试。
猜你喜欢
  • 2015-06-09
  • 1970-01-01
  • 2013-08-09
  • 2021-05-28
  • 1970-01-01
  • 1970-01-01
  • 2011-08-30
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多