在日常的工作中,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客户端,费尽心思,找了无数个开源代码研究。用这个方法来实现,真是太简单了。不是吗?下面来看看测试代码:

#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;
}

上述代码输出:



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


GitHub 加速计划 / li / linux-dash
10.39 K
1.2 K
下载
A beautiful web dashboard for Linux
最近提交(Master分支:2 个月前 )
186a802e added ecosystem file for PM2 4 年前
5def40a3 Add host customization support for the NodeJS version 4 年前
Logo

旨在为数千万中国开发者提供一个无缝且高效的云端环境,以支持学习、使用和贡献开源项目。

更多推荐