在日常的工作中,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