现在的位置: 首页 > 自动控制 > 工业·编程 > 正文

通过管道进行cmd进程输入输出重定向

2014-03-16 06:09 工业·编程 ⁄ 共 5333字 ⁄ 字号 暂无评论

在日常的工作中,shell使用比较多,尤其在软件测试过程中,但使用手工操作既麻烦,又记不住命令,关键是不能自动化。众所周知,linux或者windows系统的shell都是可以进行输入输出重定向的,利用输入输出重定向技术,把shell的输入输出映射到自己所写的进程里,这样就很方面了。比如要使用ssh2远程一个linux机器,就需要自己实现ssh2客户端的协议,使用重定向,就可以直接使用操作系统自带的ssh(linux系统),或者putty(windows下的telnet、ssh命令)。

本文所要讲述的实现方法就是利用管道,管道的概念,大家网上搜索,这里还是通过直接show代码的方式进行讲述,另外进行了简单的封装和进一步的抽象,大家可以直接使用,扩展起来也比较方便。本文是基于win32,linux实现留给大家自己当着作业吧,于此类似。

首先设计Shell类型的大概的样子如下:
#include "windows.h" 
 
class Shell 

public: 
    Shell(void); 
    ~Shell(void); 
 
    bool RunProcess(const string &process); 
    bool StopProcess(void); 
    bool GetOutput(const string &endStr, int timeout, string &outstr );//获取输出字符串 
    bool SetInput(const string &cmd);//执行命令 
private: 
    HANDLE m_hChildInputWrite;  //用于重定向子进程输入的句柄 
    HANDLE m_hChildInputRead; 
    HANDLE m_hChildOutputWrite; //用于重定向子进程输出的句柄   
    HANDLE m_hChildOutputRead; 
    PROCESS_INFORMATION m_cmdPI;//cmd进程信息 
}; 
上述代码,重定向的句柄保存起来,目的是方便用户自己扩展,同时也保存了cmd进程的信息,可以在程序推出的时候,杀掉cmd进程。接下来看看构造函数和析构函数,资源初始化和释放。
Shell::Shell(void) 

    m_hChildInputWrite = NULL; 
    m_hChildInputRead  = NULL; 
    m_hChildOutputWrite= NULL; 
    m_hChildOutputRead = NULL; 
    ZeroMemory(&m_cmdPI, sizeof(m_cmdPI));    

 
Shell::~Shell(void) 

    StopProcess();  

接下来就是创建进程(比如cmd.exe)了,便于进行输入输出操作,代码如下:

bool Shell::RunProcess( const string &process ) 

    SECURITY_ATTRIBUTES   sa;    
    sa.bInheritHandle = TRUE;    
    sa.lpSecurityDescriptor = NULL;    
    sa.nLength = sizeof(sa);  
 
    //创建子进程输出匿名管道  
    if( FALSE == ::CreatePipe(&m_hChildOutputRead, &m_hChildOutputWrite, &sa, 0) )    
    {     
        return false;    
    }   
 
    //创建子进程输入匿名管道    
    if( FALSE == CreatePipe(&m_hChildInputRead, &m_hChildInputWrite, &sa, 0) )    
    {    
        ::CloseHandle(m_hChildOutputWrite); 
        ::CloseHandle(m_hChildOutputRead); 
        ::CloseHandle(m_hChildOutputWrite); 
        ::CloseHandle(m_hChildOutputRead); 
        return false; 
    } 
 
    ZeroMemory(&m_cmdPI, sizeof(m_cmdPI));    
    STARTUPINFO  si;  
    GetStartupInfo(&si); 
 
    si.cb = sizeof(STARTUPINFO); 
    si.dwFlags = STARTF_USESTDHANDLES|STARTF_USESHOWWINDOW; 
    si.wShowWindow = SW_HIDE; 
    si.hStdInput   = m_hChildInputRead;     //重定向子进程输入    
    si.hStdOutput  = m_hChildOutputWrite;   //重定向子进程输入     
    si.hStdError   = m_hChildOutputWrite;  
 
    if( FALSE == ::CreateProcess(NULL, (process.c_str()), NULL, NULL, TRUE, NORMAL_PRIORITY_CLASS, NULL, NULL, &si, &m_cmdPI) )    
    {    
        ::CloseHandle(m_hChildInputWrite); 
        ::CloseHandle(m_hChildInputRead); 
        ::CloseHandle(m_hChildOutputWrite); 
        ::CloseHandle(m_hChildOutputRead); 
        m_hChildInputWrite = NULL; 
        m_hChildInputRead  = NULL; 
        m_hChildOutputWrite= NULL; 
        m_hChildOutputRead = NULL; 
        ZeroMemory(&m_cmdPI, sizeof(m_cmdPI));   
        return false;    
    } 
 
    return true; 

bool Shell::StopProcess( void ) 

    ::CloseHandle(m_hChildInputWrite); 
    ::CloseHandle(m_hChildInputRead); 
    ::CloseHandle(m_hChildOutputWrite); 
    ::CloseHandle(m_hChildOutputRead); 
    m_hChildInputWrite = NULL; 
    m_hChildInputRead  = NULL; 
    m_hChildOutputWrite= NULL; 
    m_hChildOutputRead = NULL; 
    ::TerminateProcess(m_cmdPI.hProcess, -1); 
    ::CloseHandle(m_cmdPI.hProcess);    
    ::CloseHandle(m_cmdPI.hThread);  
    ZeroMemory(&m_cmdPI, sizeof(m_cmdPI)); 

如果你想通过putty来实现telnet、ssh操作进行,一是把cmd.exe替换为putty命令行的形式,另外一种形式,就是把putty当着普通的shell命令执行即可。进程创建好了,下面来看看读写函数的实现:
bool Console::GetOutput( const string &endStr, int timeout, string &outstr ) 

    if( NULL == m_hChildOutputRead ) 
    { 
        return false; 
    } 
 
    outstr = ""; 
    char buffer[4096] = {0}; 
    DWORD readBytes = 0; 
    while( timeout > 0 ) 
    { 
        //对管道数据进行读,但不会删除管道里的数据,如果没有数据,就立即返回 
        if( FALSE == PeekNamedPipe( m_hChildOutputRead, buffer, sizeof(buffer) - 1, &readBytes, 0, NULL ) ) 
        { 
            return false; 
        } 
 
        //检测是否读到数据,如果没有数据,继续等待 
        if( 0 == readBytes ) 
        { 
            ::Sleep(200); 
            timeout -= 200; 
            continue; 
        } 
         
        readBytes = 0; 
        if( ::ReadFile( m_hChildOutputRead, buffer, sizeof(buffer) - 1, &readBytes, NULL) ) 
        { 
            outstr.insert( outstr.end(), buffer, buffer + readBytes ); 
            size_t pos = outstr.rfind(endStr); 
            if( string::npos == pos ) 
            { 
                continue; 
            } 
            if( pos == outstr.size() - endStr.size() ) 
            { 
                return true;//找到数据 
            } 
        } 
        else 
        { 
            return false; 
        } 
    } 
 
    return false; 

 
bool Shell::SetInput( const string &cmd ) 

    if( NULL == m_hChildInputWrite ) 
    { 
        return ""; 
    } 
 
    string tmp = cmd + "\r\n"; 
    DWORD writeBytes = 0; 
    if( FALSE == ::WriteFile( m_hChildInputWrite, tmp.c_str(), tmp.size(), &writeBytes, NULL ) ) 
    { 
        return false; 
    } 
    return true; 

写函数实在是平淡无奇,就是调用WriteFile,只要把句柄传递对了就行,这几个句柄是容易混淆,大家仔细阅读代码,仔细理解。在读函数中,使用了PeekNamedPipe函数,对管道进行数据读,这个函数有个作用,即使管道中没有数据,但会立即返回,不会读阻塞。这个时候再调用ReadFile就不会阻塞了。同时,使用Sleep函数就能简单的实现超时读的功能。这个是ReadFile办不到的。

到此为止,代码已经实现完了,很简单吧,曾经为了实现ssh2客户端,费尽心思,找了无数个开源代码研究。用这个方法来实现,真是太简单了。不是吗?下面来看看测试代码:
[cpp] view plaincopy
#include "console.h" 
#include <iostream> 
using namespace std; 
 
 
int _tmain(int argc, _TCHAR* argv[]) 

    Console console; 
    if( false == console.RunProcess("cmd.exe") ) 
    { 
        cout<<"create cmd.exe process fail"<<endl; 
        return -1; 
    } 
    string outstr; 
    console.GetOutput(">", 3000, outstr); 
    cout<<outstr<<endl; 
    console.SetInput("dir"); 
    console.GetOutput(">", 3000, outstr); 
    cout<<outstr<<endl; 
 
    return 0; 

上述代码输出:

大功告成,代码实现没有用到什么高级技巧,但简单适用,大家不妨直接借用,也许能帮助大家解决日常的工作问题。

来源:KiteRunner

给我留言

留言无头像?