zoukankan      html  css  js  c++  java
  • 重定向子进程控制台程序的输入输出

    重定向子进程控制台程序的输入输出

    重定向所做的工作都在父进程,但需要子进程遵守下面的规则:

    子进程程序在输出代码后,等待输入之前需要调用fflush(stdout)函数,这样把输出的内容放入缓冲区,父进程才能及时的读到输出数据。

    不遵守以上规则就没办法实现有效的交互了,cmd.exe是遵守这个规则的典范,大部分控制台程序都不遵守这个规则。今天我试图给Google的V8 Javascript 的Shell搞一个GUI,方便我输入Javascript程序,就遇到了v8_shell不遵守这个的问题。好在有源代码,研究了一下,加上两个fflush(stdout)就行了。

    还有一个需要注意的就是,在向子进程的stdin写内容时,一定要在最后加上" ",否则直到父进程终止时子进程才能收到这些输入,这时已经太晚了。这很奇怪,但确实如此。加上fflush(stdin)应该会更加保险。

    MSDN的CreatePipe函数的Examples的链接内容对于这个主题也有讲述,但不够清晰扼要。
    CodeProject也有个例子,能够正确运行,但建议你用cmd.exe做子进程进行试验。

    管道是一种有两个端点的通信通道。你使用管道在两个进程间或同一进程内交换数据。它有点像手提对讲机,双方一人一个就可以通话了。
    存在两种管道:匿名管道和命名管道。匿名管道是匿名的,使用时你无需知道它的名字。命名管道相反,使用时你必须知道它的名字。
    另一种分类:单向管道和双向管道。单向管道数据流式单向的,像发传真;双向管道数据流是双向的,像打电话。
    匿名管道总是单向的,命名管道可以是单向或双向的。命名管道可以用在网络环境,服务器通过它连接到几个客户。在本教材,我们详细研究匿名管道。匿名管道用于父子进程间的通信,或子进程间的通信。当你处理控制台程序时,匿名管道真的很有用。控制台程序类似于DOS窗口,但是它是完全的32位Windows程序,可以使用任何GUI函数,只不过它比通常的GUI程序多了一个控制台而已。控制台程序有三个句柄,标准输入、标准输出、错误输出。
    你可以调用GetStdHandle获得控制台程序的三个句柄。GUI程序没有控制台,如果你调用GetStdHandle,会返回错误。如果你想在GUI程序中使用控制台,那么可以使用AllocConsole,用完后请调用FreeConsole。
    匿名管道常用于重定向控制台子进程的输入输入,父进程可以是控制台或GUI程序。我们可以替换控制台程序的输入和输出,而那个控制台程序浑然不觉。引用面向对象编程的术语,这是一种多态。这种方法很强大,我们无需对子进程做任何改变。
    另外你要知道的是控制台程序从哪里获得那些标准句柄。当控制台进程被创建时,父进程有两个选择:为子进程创建一个新的控制台,或者让子进程继承它的控制台。
    介绍一下CreatePipe函数,该函数用于创建匿名管道。
    BOOL WINAPI CreatePipe(
      __out     PHANDLE hReadPipe,
      __out     PHANDLE hWritePipe,
      __in_opt  LPSECURITY_ATTRIBUTES lpPipeAttributes,
      __in      DWORD nSize
    );
    hReadPipe 接受管道读句柄的变量指针
    hWritePipe 接受管道写句柄的变量指针
    lpPipeAttributes 安全属性,决定子进程是否可以继承返回的读写句柄
    nSize  管道保留使用的缓冲大小,这仅仅是一个建议大小,你可以使用NULL表示使用缺省大小。
    如果调用成功,返回值非0,否则,返回值为0。
    如果调用成功,你会得到两个句柄,一头用于读,一头用于写。下面我列出重定向子进程标准输出到父进程的步骤,我们使用GUI做为父进程,因为这更有意义。

    1 使用CreatePipe创建一个匿名管道。不要忘记设置SECURITY_ATTRIBUTES的bInheritable成员为TRUE,这样子进程就可以继承这些句柄。
    2 调用CreateProcess创建子进程。我们需要准备CreateProcess函数的STARTUPINFO参数
          cb : STARTUPINFO结构体的大小。
          dwFlags : flag枚举。为了达到目的,我们应该使用STARTF_USESHOWWINDOW | STARTF_USESTDHANDLES
          hStdOutput and hStdError : 你希望子进程使用的标准输出和错误输出的句柄。为了达到目的,我们把这个管道的写端用于子进程的标准输出和错误输出。这样当子进程输出错误和其他信息时,实际是在向管道写入。
          wShowWindow 决定窗口的显示状态。为了达到目的,我们使用SW_HIDE来隐藏控制台窗口。
     子进程创建后,是处于休眠状态的。
    3 使用CloseHandle关闭管道的写句柄。这是必须的。父进程不使用这个句柄,如果存在多个写句柄,那么管道不会工作。但是,不要在调用CreateProcess前关闭管道的写句柄,否则管道就作废了。你应该在CreateProcess调用返回后,从读管道端读取子进程的数据之前关闭管道。
    4 使用ReadFile从读管道端读取数据。在你调用ReadFile后,子进程会开始运行。你可以把读数据想象为用吸管喝饮料。你一直使用ReadFile读数据,直到它返回0。对于读来的数据,你想怎样就怎样,比如放进一个Edit控件。
    5 最后,关闭读管道句柄。

    http://www.adintr.com/article/278

    Windows管道(Pipe)重定向stdout,stderr,stdin

    by other 
    March 25, 2012, 1:50 p.m.

    stdin是标准输入,stdout是标准输出,stderr是标准错误输出。
    大多数的命令行程序从stdin输入,输出到stdout或stderr,有时我们需要重定向stdout,stderr,stdin。比如:将输出写入文件,又或者我们要将命令行程序输出结果显示到Windows对话框中。

    在Windows编程中,重定向需要用到管道(Pipe)的概念。管道是一种用于在进程间共享数据的机制。一个管道类似于一个管子的两端,一端是写入的,一端是读出的。由一个进程从写入端写入、另一个进程从读出端读出,从而实现通信,就向一个“管道”一样。

    重定向的原理是:

    首先声明两个概念:主程序(重定向的操纵者)、子进程(被重定向的子进程)

    如果要重定位stdout的话,先生成一个管道, 管道的写入端交给子进程去写,主程序从管道的读出端读数据,然后可以把数据写成文件、显示等等。重定向stderr和stdout是相同的。

    同理,要重定向stdin的话,生成一个管道, 管道的写入端由主程序写,子进程从管道的读出端读数据。

    其中需要用到几个Windows API :  CreatePipe, DuplicateHandle, CreateProcess, ReadFile, WriteFile 等,函数详解可参见MSDN.


    一、编程实现原理 ( C语言)

    [html] view plaincopy
     
    1. #include <windows.h>  
    2.   
    3.   //定义句柄: 构成stdin管道的两端句柄  
    4.   HANDLE  hStdInRead;         //子进程用的stdin的读入端  
    5.   HANDLE  hStdInWrite;        //主程序用的stdin的读入端  
    6.   
    7.   //定义句柄: 构成stdout管道的两端句柄  
    8.   HANDLE  hStdOutRead;     ///主程序用的stdout的读入端  
    9.   HANDLE  hStdOutWrite;    ///子进程用的stdout的写入端  
    10.   
    11.   //定义句柄: 构成stderr管道的句柄,由于stderr一般等于stdout,我们没有定义hStdErrRead,直接用hStdOutRead即可  
    12.   HANDLE  hStdErrWrite;       ///子进程用的stderr的写入端  
    13.   
    14.   //定义一个用于产生子进程的STARTUPINFO结构体 (定义见CreateProcess,函数说明)  
    15.   STARTUPINFO siStartInfo;  
    16.   //定义一个用于产生子进程的PROCESS_INFORMATION结构体 (定义见CreateProcess,函数说明)  
    17.   PROCESS_INFORMATION piProcInfo;  
    18.   
    19.   
    20.   //产生一个用于stdin的管道,得到两个HANDLE:  hStdInRead用于子进程读出数据,hStdInWrite用于主程序写入数据  
    21.   //其中saAttr是一个STARTUPINFO结构体,定义见CreatePipe函数说明  
    22.   if   (!CreatePipe(&hStdInRead, &hStdInWrite,&saAttr, 0))  
    23.           return;  
    24.   
    25.   //产生一个用于stdout的管道,得到两个HANDLE:  hStdInRead用于主程序读出数据,hStdInWrite用于子程序写入数据  
    26.   if  (!CreatePipe(&hStdOutRead, &hStdOutWrite,&saAttr, 0))  
    27.         return;  
    28.   //由于stderr一般就是stdout, 直接复制句柄hStdOutWrite,得到 hStdErrWrite  
    29.    if (!DuplicateHandle(GetCurrentProcess(), hStdOutWrite, GetCurrentProcess(), &hStdErrWrite, 0, TRUE, DUPLICATE_SAME_ACCESS))  
    30.       return;  
    31.   
    32.   //对STARTUPINFO结构体赋值,对stdin,stdout,stderr的Handle设置为刚才得到的管道HANDLE  
    33.   ZeroMemory( &siStartInfo, sizeof(STARTUPINFO) );  
    34.   siStartInfo.cb = sizeof(STARTUPINFO);  
    35.   siStartInfo.dwFlags  |= STARTF_USESHOWWINDOW;  
    36.   siStartInfo.dwFlags  |= STARTF_USESTDHANDLES;  
    37.   siStartInfo.hStdOutput = hStdOutWrite;     //意思是:子进程的stdout输出到hStdOutWrite  
    38.   siStartInfo.hStdError  =  hStdErrWrite;        //意思是:子进程的stderr输出到hStdErrWrite  
    39.   siStartInfo.hStdInput  = hStdInRead;  
    40.   
    41.   // 产生子进程,具体参数说明见CreateProcess函数  
    42.   bSuccess = CreateProcess(NULL,  
    43.       CommandLine,    // 子进程的命令行  
    44.       NULL,                   // process security attributes  
    45.       NULL,                   // primary thread security attributes  
    46.       TRUE,                   // handles are inherited  
    47.       0,                          // creation flags  
    48.       NULL,                  // use parent's environment  
    49.       NULL,                  // use parent's current directory  
    50.       &siStartInfo,      // STARTUPINFO pointer  
    51.       &piProcInfo);     // receives PROCESS_INFORMATION  
    52.   
    53.    //如果失败,退出  
    54.    if (!bSuccess ) return;  
    55.   
    56.     //然后,就可以读写管道了  
    57.     //写入stdin,具体代码在一个WriteToPipe函数中  
    58.     WriteToPipe();  
    59.   
    60.    //不断子检测进程有否结束  
    61.     while (GetExitCodeProcess(piProcInfo.hProcess,&process_exit_code))  
    62.         {  
    63.             //读stdout,stderr  
    64.            ReadFromPipe();  
    65.            //如果子进程结束,退出循环  
    66.            if (process_exit_code!=STILL_ACTIVE) break;  
    67.         }  


     
    具体看一下WriteToPipe(), ReadFromPipe函数:

    [html] view plaincopy
     
    1. //写入stdin  
    2. BOOL WriteToPipe()  
    3. {  
    4.   DWORD dwWritten;  
    5.   BOOL bSuccess = FALSE;  
    6.     
    7.  //用WriteFile,从hStdInWrite写入数据,数据在in_buffer中,长度为dwSize  
    8.   bSuccess = WriteFile( hStdInWrite, in_buffer, dwSize, &dwWritten, NULL);  
    9.   return bSuccess;  
    10. }  
    [html] view plaincopy
     
    1. // 读出stdout  
    2. BOOL ReadFromPipe()  
    3. {  
    4.   char out_buffer[4096];  
    5.   DWORD dwRead;    
    6.   BOOL bSuccess = FALSE;  
    7.   
    8.    //用WriteFile,从hStdOutRead读出子进程stdout输出的数据,数据结果在out_buffer中,长度为dwRead  
    9.    bSuccess = ReadFile( hStdOutRead, out_buffer, BUFSIZE, &dwRead, NULL);  
    10.    if ((bSuccess) && (dwRead!=0))  //如果成功了,且长度>0  
    11.              {  
    12.                    // 此处加入你自己的代码  
    13.                    // 比如:将数据写入文件或显示到窗口中  
    14.              }  
    15.   return bSuccess;  
    16. }  


    OK,到此原理写完了。为简化说明原理,上述代码省略了出错处理、结束处理(如:CloseHandle等),具体可以参见我的源码。


    二、封装、实用的代码

    上述过程有些麻烦,实际使用中,我封装成几个函数:

    首先定义三个回调函数 (就是函数指针类型)

    //当stdin输入时,调用此函数。将要写的数据写入buf中,*p_size写为数据长度即可。
    typedef void FuncIn(char *buf,int *p_size);

    //当stdout,stderr输出时,调用此函数。可读取的数据在buf中,数据长度为size。
    typedef void FuncOut(char *buf,int size);

    //当子进程持续过程中,周期性调用此函数,设置p_abort可中断子进程。
    typedef void FuncProcessing(int *p_abort);

    然后定义了四个函数

    //执行一个命令行,重定向stdin, stdout,stderr。
    //OnStdOut是回调函数指针,当有输出时,OnStdOut被调用。
    //OnStdIn是回调函数指针,当输入时,OnStdIn被调用。
    int ExecCommandEx(char *szCommandLine,char *CurrentDirectory,char *Environment,unsigned short ShowWindow,
                  FuncOut *OnStdOut,FuncProcessing *OnProcessing,FuncIn *OnStdIn);

    //执行一个命令行,重定向stdout,stderr。
    //OnLineOut是回调函数指针,当有一行输出时,OnLineOut被调用。
    int ExecCommandOutput(char *szCommandLine,char *Environment,unsigned short ShowWindow,
                           FuncOut *OnLineOut,FuncProcessing *OnProcessing);

    //执行一个命令行,等待子进程结束,返回子进程的程序退出代码。不处理重定向。
    int ExecCommandWait(char *szCommandLine,unsigned short ShowWindow,FuncProcessing *OnProcessing);

    //执行一个命令行,不等待子进程结束,即返回。不处理重定向。功能相当于 Windows API  WinExec.
    int ExecCommandNoWait(char *szCommandLine,unsigned short ShowWindow);


    还定义了一个存储数据的EXEC_INFO结构体及操作它的函数。

    全部代码为C语言,在JExecution.c, JExecution.h两个文件中。只用到了Windows API,没有用MFC及其他库。


    三、使用方法

    有了JExecution.c,使用就很方便了。比如:

    [html] view plaincopy
     
    1. #include <stdio.h>  
    2. #include <windows.h>  
    3. #include "JExecution.h"  
    4.   
    5. //定义一个处理输出的回调函数  
    6. void OnLineOut(char *buf,int size)  
    7. {  
    8.    printf("%s ", buf);  
    9. }  
    10.   
    11.   
    12. int main(int argc, char* argv[])  
    13. {  
    14.   int n;  
    15.   char *command;  
    16.   
    17.   command = "cmd.exe  /r dir/w ";   //这个命令的意思就是调用DOS,执行dir命令,显示当前目前下的文件  
    18.   n=ExecCommandOutput(command,NULL,SW_HIDE,OnLineOut,NULL);  
    19.   printf("<Return:>%d",n);  
    20. }  
  • 相关阅读:
    Mybatis-Plus02 CRUD
    Idea一直卡在loading archetype list问题解决(或者报Unable to import maven project: See logs for details)
    Mybatis-Plus01 快速开始
    用PS给视频磨皮美颜
    Linux基本内容
    【maven】IDEA工程右边的maven配置中Plugins有重复的命令
    段誉身具凌波微波,动无常则,若危若安,一次能走一级台阶或者两级台阶,他要爬一段30级的山路,问有多少种走法?分析如何计算,然后编程解答。 进阶问题:当他轻功熟练度提升,一次最多可以走三级,那就结果有什么变化?后来走火入魔了,不能走一级,只能走二或三级,又有什么变化?
    小陆每天要写一份工作日报,日报标题含有日期。几年后,他翻开以前的日报,想知道两份日报的日期是否同为星期几,请编程帮助他判断。
    高性能HTML5/JS开发框架DevExtreme 新版——v20.2.7发布
    跨平台UI开发框架DevExpress XAF 拥有超强性能,你Get了吗?
  • 原文地址:https://www.cnblogs.com/luhouxiang/p/3732258.html
Copyright © 2011-2022 走看看