今天碰到一个有意思的问题,如何在GUI里调用命令行并返回处理结果信息。说是在GUI程序里调用,其实和普通进程调用命令行并返回结果的原理是一样的。一般情况下,用system或CreateProcess创建一个进程来执行一个命令行,并不关心它执行过程中的具体信息,有时甚至不在乎它何时结束,而在今天所碰到的问题中,却恰恰需要关注这两点。关于这两点,可以再强调一下:
1.在一个进程中(GUI进程或普通的Console进程,原理都是一样的)调用一个指定的命令行,并等待其返回,也就是说调用过程是阻塞的,不然没办法搜取到准确的信息。
2.调用完毕后必须得到这个命令行调用过程中的输出信息,即单独在控制台上执行该命令行所输出的信息。
好了,这就是我们的需求。我们主函数看起来是这个样子的:
1 /* Main.cpp */
2 #include "cmd_caller.h"
3 #include <iostream>
4
5 int main(int argc, char* argv[])
6
7 {
8 cmd_caller caller("netstat -ano");
9 std::string retstring;
10 if (!caller.call_cmd(retstring))
11 {
12 std::cout<<"call failed\n";
13 return 1;
14 }
15 std::cout.write(retstring.c_str(), retstring.size());
16 return 0;
17 }
cmd_caller类做些什么?简单的说就是创建一个进程,然后让该进程执行给定的命令,最后通过某种方式返回结果,而这种方式就是我们所说的管道。我们先看看Windows系统上的实现,这个实现是基于网上的一篇文章。
这个是cmd_caller的类申明,cmd_caller_private_data结构便于跨平台实现(具体请看后面):
1 /* cmd_caller.h */
2 #ifndef __CMD_CALLER_H__
3 #define __CMD_CALLER_H__
4 #include <string>
5
6 struct cmd_caller_private_data;
7 class cmd_caller
8 {
9 public:
10 cmd_caller(const char* cmdstr);
11 cmd_caller(const std::string& cmdstr);
12 virtual ~cmd_caller();
13 void set_cmd(const char* cmdstr);
14 void set_cmd(const std::string& cmdstr);
15 bool call_cmd(std::string& output);
16 protected:
17 private:
18 bool init_private_data();
19 cmd_caller_private_data* private_data_;
20 };
21 #endif
下面是cmd_caller在Windows上的一种实现:
1 #include "cmd_caller.h"
2 #include <Windows.h>
3 #include <stdio.h>
4
5 struct cmd_caller_private_data
6 {
7 std::string cmd_str;
8 HANDLE hPipeRead;
9 HANDLE hPipeWrite;
10 SECURITY_ATTRIBUTES sa;
11 STARTUPINFOA si;
12 PROCESS_INFORMATION pi;
13 };
14
15 cmd_caller::cmd_caller(const char* cmdstr)
16 {
17 private_data_ = new cmd_caller_private_data();
18 private_data_->cmd_str = std::string(cmdstr);
19 }
20
21 cmd_caller::cmd_caller(const std::string& cmdstr)
22 {
23 private_data_ = new cmd_caller_private_data();
24 private_data_->cmd_str = cmdstr;
25 }
26
27 bool cmd_caller::init_private_data()
28 {
29 private_data_->sa.nLength = sizeof(SECURITY_ATTRIBUTES);
30 private_data_->sa.lpSecurityDescriptor = NULL;
31 private_data_->sa.bInheritHandle = TRUE;
32
33 if (!CreatePipe(&private_data_->hPipeRead,
34 &private_data_->hPipeWrite,
35 &private_data_->sa,
36 0)) //创建管道)
37 {
38 return false;
39 }
40
41
42 private_data_->si.cb = sizeof(STARTUPINFOA);
43 GetStartupInfo(&private_data_->si); //设置启动信息
44 private_data_->si.hStdError = private_data_->hPipeWrite; //将子进程错误输出结果写到管道
45 private_data_->si.hStdOutput = private_data_->hPipeWrite; //将子进程标准输出结果写出管道
46 private_data_->si.wShowWindow = SW_HIDE; //隐式调用子程序,及不显示子程序窗口
47 private_data_->si.dwFlags = STARTF_USESHOWWINDOW | STARTF_USESTDHANDLES;
48
49 return true;
50 }
51
52 cmd_caller::~cmd_caller()
53 {
54 delete private_data_;
55 }
56
57 void cmd_caller::set_cmd(const char* cmdstr)
58 {
59 private_data_->cmd_str = std::string(cmdstr);
60 }
61
62 void cmd_caller::set_cmd(const std::string& cmdstr)
63 {
64 private_data_->cmd_str = cmdstr;
65 }
66
67 bool cmd_caller::call_cmd(std::string& output)
68 {
69 if(!init_private_data())
70 {
71 return false;
72 }
73
74 //根据设定的参数创建并执行子进程
75 if(!CreateProcessA(NULL,
76 (LPSTR)private_data_->cmd_str.c_str(),
77 NULL,
78 NULL,
79 TRUE,
80 NULL,
81 NULL,
82 NULL,
83 &private_data_->si,
84 &private_data_->pi))
85 {
86 return false;
87 }
88
89 //等待子进程退出
90 if(WAIT_FAILED == WaitForSingleObject(private_data_->pi.hProcess, INFINITE))
91 {
92 return false;
93 }
94
95 //关闭子进程的相关句柄,释放资源
96 CloseHandle(private_data_->pi.hProcess);
97 CloseHandle(private_data_->pi.hThread);
98
99 //Note:We must close the write handle of pipe, else the ReadFile call will pending infinitely.
100 CloseHandle(private_data_->hPipeWrite);
101
102 char buf[1024];
103 DWORD dwRead;
104 output.clear();
105
106 for (;;)
107 {
108 BOOL result = ReadFile(private_data_->hPipeRead, buf, 1024, &dwRead, NULL);
109
110 if(!result || dwRead == 0)
111 {
112 break;
113 }
114 else
115 {
116 output.append(buf, dwRead);
117 }
118 }
119 return true;
120 }
下面我们再来看一下Linux下的实现:
1 #include "cmd_caller.h"
2 #include <stdio.h>
3 #include <unistd.h>
4
5 struct cmd_caller_private_data
6 {
7 std::string cmd_str;
8 int pipefd[2];
9 };
10
11 cmd_caller::cmd_caller(const char *cmdstr)
12 {
13 private_data_ = new cmd_caller_private_data();
14 private_data_->cmd_str = std::string(cmdstr);
15 }
16
17 cmd_caller::cmd_caller(const std::string &cmdstr)
18 {
19 private_data_ = new cmd_caller_private_data();
20 private_data_->cmd_str = cmdstr;
21 }
22
23 cmd_caller::~cmd_caller()
24 { delete private_data_;
25 }
26
27 void cmd_caller::set_cmd(const char* cmdstr)
28 {
29 private_data_->cmd_str = std::string(cmdstr);
30 }
31
32 void cmd_caller::set_cmd(const std::string& cmdstr)
33 {
34 private_data_->cmd_str = cmdstr;
35 }
36
37 bool cmd_caller::init_private_data()
38 {
39 return (-1 != pipe(private_data_->pipefd));
40 }
41
42
43 bool cmd_caller::call_cmd(std::string &output)
44 {
45 if(!init_private_data())
46 return false;
47
48 int pid = fork();
49 if(-1 == pid)
50 {
51 return false;
52 }
53 else if(0 == pid)
54 {
55 //在子进程中,将管道的写入文件描述符重定向到标准输出和标准错误输出
56 //这样命令执行的结果将通过管道发送到父进程
57 dup2(private_data_->pipefd[1], STDOUT_FILENO);
58 dup2(private_data_->pipefd[1], STDERR_FILENO);
59
60 //在子进程中,我们不需要用到读管道,把他关掉
61 close(private_data_->pipefd[0]);
62
63 if(-1 == execlp(private_data_->cmd_str.c_str(), private_data_->cmd_str.c_str(), NULL))
64 return false;
65
66 //写管道已经用完,不需要在往管道中写了,我们把它关了
67 close(private_data_->pipefd[1]);
68
69 return true;
70 }
71 else
72 {
73 char buf[1024];
74 size_t readsize;
75 //在父进程中我们不需要用到写管道,可以关掉
76 close(private_data_->pipefd[1]);
77 while((readsize = read(private_data_->pipefd[0], buf, 1024)) > 0)
78 {
79 output.append(buf, readsize);
80 }
81 //读管道已经用完,可以把它关掉
82 close(private_data_->pipefd[0]);
83 return true;
84 }
85 }
到这里时,任务已近完成,然而,还有一种更简单的调用方法,就是popen/pclose函数(Windows上是_popen/_pclose函数),下面就是利用这种方法的实现:
1 /*
2 *其他实现不变
3 */
4
5 bool cmd_caller::init_private_data()
6 {
7 return true;
8 }
9
10 bool cmd_caller::call_cmd(std::string &output)
11 {
12 char buffer[128];
13 FILE *fpipe;
14
15 if( (fpipe = popen(private_data_->cmd_str.c_str(), "r" )) == NULL )
16 return false;
17
18 //Read pipe until end of file, or an error occurs.
19
20 output.clear();
21 while(fgets(buffer, 128, fpipe))
22 {
23 output.append(buffer);
24 }
25
26 //Close pipe and print return value of fpipe.
27 if (feof(fpipe))
28 {
29 pclose(fpipe);
30 return true;
31 }
32 else
33 {
34 pclose(fpipe);
35 return false;
36 }
37 }
同样,在Windows上
:
1 /*
2 *其他实现不变
3 */
4
5 bool cmd_caller::init_private_data()
6 {
7 return true;
8 }
9
10 bool cmd_caller::call_cmd(std::string &output)
11 {
12 char psBuffer[128];
13 FILE *pPipe;
14
15 if( (pPipe = _popen(private_data_->cmd_str.c_str(), "rt" )) == NULL )
16 return false;
17
18 //Read pipe until end of file, or an error occurs.
19
20 output.clear();
21 while(fgets(psBuffer, 128, pPipe))
22 {
23 output.append(psBuffer);
24 }
25
26 //Close pipe and print return value of pPipe.
27 if (feof( pPipe))
28 {
29 _pclose(pPipe);
30 return true;
31 }
32 else
33 {
34 _pclose(pPipe);
35 return false;
36 }
37 }
38
值得注意的是,以上两种方法有下面一些不同:
1.第一种实现直接将子进程的标准输出和错误输出重定向到管道,而第二种实现却需要在命令中指出重定向。
2.调用方式。如果命令是一个路径,并且路径中间有特殊字符如空格,那么两种实现的调用方法可能有些区别,前一种方式更直接,而后一种方式需要加转义。最后以Windows上的一个例子来说明这两种调用之间的区别:
以第一种实现调用
cmd_caller caller("E:\\Program Files\\Nmap\\nmap.exe -T4 -A -v -PE -PS22,25,80 -PA21,23,80,3389 www.baidu.com")
对应于以第二种实现调用
cmd_caller caller("\"E:\\Program Files\\Nmap\\nmap.exe\" -T4 -A -v -PE -PS22,25,80 -PA21,23,80,3389 www.baidu.com 2>&1")