如何在Windows中检测子进程需要输入的情况

10

我有一个子进程,要么以返回代码的形式退出,要么询问并等待用户输入。

我想在进程询问问题并立即退出时检测到。对于我决定系统状态的问题是否被提出已经足够了。

问题是我无法读取问题,因为子进程可能不会刷新标准输出。所以我不能依赖解析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什么是Windows上unbuffer程序的等效物?),它被称为winpty,这可能会允许我检测输出并解决我的当前问题,但我想保持简单,而且我想先解决标准输入问题...

我尝试了很多不起作用的东西,包括尝试将假文件作为stdin参数传递,这不起作用,因为subprocess获取文件的fileno,我们不能向其中输入垃圾...

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

使用字符串与communicate一起使用也不行,因为你无法控制何时读取字符串以提供给子进程(可能通过系统管道)。

这些问题很有前途,但要么依赖于标准输出,要么只适用于Linux

我目前正在运行具有超时的过程,如果到达超时,则决定程序已被阻止。 但是这会耗费超时等待时间。 如果我能够在子进程读取stdin时立即决定,那就更好了。

我想知道是否有本地python解决方案(可能使用ctypes和Windows扩展),以检测从stdin读取。但是,不使用Python而是使用非微软专有语言的本地解决方案也可以。


通过控制,我的意思是你是否可以修改它的代码 - Ma0
错误,不是我们的二进制文件,而是第三方的。 - Jean-François Fabre
如果可能的话,您应该使用位于https://pypi.python.org/pypi/pexpect上的“pexepct”软件包。否则,您可能需要使用“pty”软件包来打开伪终端,让您的子进程相信它正在与终端通信而不是作为(无终端)子进程。或者,如果您的第三方文件是Python脚本,则可以在调用脚本时使用“-u”标志强制其不缓冲输出(['python', '-u', 'ask.py']),但这样您也可能编辑它。 - JohanL
我将尝试使用pexpect。在Windows上无法导入pty。无法在subprocess上使用“-u”选项:这不是Python...感谢您的建议。 - Jean-François Fabre
如果这是一个控制台应用程序并且没有向stdout写入太多内容,我建议传递一个新的控制台屏幕缓冲区,初始化为所有NULs,以便轻松读取行作为空终止字符串。然后轮询缓冲区以获取输出或等待控制台WinEvents。毫无疑问,winpty的工作方式类似,因为Windows没有pty设备。但是,如果您的需求很简单,那么您可以不使用它。您只需要ctypes或PyWin32即可。 - Eryk Sun
显示剩余6条评论
2个回答

4

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

  • 使用重定向的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);
    }
}

另一种代码变体 - 使用事件完成(event completion),而不是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);
    }
}

@Jean-FrançoisFabre - 在我的测试中,使用cmd.exe/notepad (xp-win10)这个方法很好用。当然可以稍微改变代码,使用重叠的事件完成代替apc完成。但这不会影响结果。 - RbMm
@Jean-FrançoisFabre - DbgPrint 这只是用于调试输出。你可以完全删除它(不需要替换),它没有任何功能点。我使用的是UNICODE(W)API,而你似乎在尝试ansi。我使用的是CL.EXE(msvc)编译器。 - RbMm
DbgPrint或print - 帮助测试,但总是在调试器下进行最佳测试。 - RbMm
@Jean-FrançoisFabre - 我添加了第二个代码变体(带有事件完成)。也许对您来说这会更简单。但是这不会改变主要思想,只是实现细节略有不同。无论如何,两种变体都可以,或者在同一位置失败。 - RbMm
标准输入读取检测。但这并不重要,因为一个普通的C程序可以正常工作。 - Jean-François Fabre
显示剩余6条评论

3
我想要找出子进程是否读取用户输入的想法是(滥用)文件对象是有状态的这一事实:如果进程从其stdin读取数据,我们应该能够检测到stdin状态的变化。
步骤如下:
  1. 创建一个临时文件,它将被用作子进程的stdin
  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")

理想情况下,临时文件可以被一些不会向磁盘写入任何数据的管道或其他东西所替换,但不幸的是,我找不到一种不使用真实磁盘文件就能使其正常工作的方法。

很不错的尝试,但如果允许进程获取所有输入,它将到达文件结尾并执行我不想要的任务。我需要检测一个字符读取,以便我可以终止该进程。 - Jean-François Fabre
这会回答问题:如何在Windows中检测子进程何时“请求”输入。我需要立即检测。 - Jean-François Fabre
@Jean-FrançoisFabre - 我需要检测一个字符读取,以便我可以终止进程 - 如果您想要终止进程,如果它开始从stdin读取 - 您需要创建一个带有0大小缓冲区的异步管道。将子进程的stdin连接到此管道,并使用事件作为完成写入1个字节到管道。因为管道缓冲区为0 - 操作不会完成,直到有人从连接的管道端(stdin)读取或者您关闭管道句柄。然后,您可以重定向stdin并等待事件+子进程退出来启动子进程。如果事件先触发,则子进程开始从stdin读取。 - RbMm
不知道Python怎么做,但在C++中实现这个非常容易。 - RbMm
time.sleep(1) - 为什么不是 time.sleep(748) 呢? - RbMm
显示剩余3条评论

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