console执行命令的操作方式,即通过shell操作电脑的方式,在Linux下的方便快捷无需言说。同样,在Windows下的cmd是一个也是一个很有用的工具,有很多命令,通过console的cmd执行后,能够快速得出结果。

在编程中,有很多应用软件或者服务软件需要执行一些windows常用的cmd作为软件辅助,不仅如此,执行有些cmd后还要得出相应的执行结果,以便于根据结果做相应逻辑调整;另一方面,执行cmd后也需要得知执行是否成功,不成功的话会是什么错误。

本篇博客使用Windows下的API CreateProcess来执行cmd,并通过创建匿名管道来获取执行结果。

1、获取cmd执行成功还是失败

经过测试发现,windows下的cmd有后缀可得出命令执行的成功结果。通过

"cmd" + "command string" + "&&" + "echo string1" + "||" + "echo string2"的命令形式,可在执行命令结束后返回结果,其中"command string"是具体的命令本身。
如果command string执行成功,则返回 string1;否则返回string2。

例如执行打开记事本命令(用例系统为win10,64位):

判断process状态 process结果_执行命令行

可看到command string 为"notepad",其后跟后缀"&&echo S_OK || echo E_FAILED",其中的"S_OK"为string1,"notepad"命令成功后的返回string1,"E_FAILED"为string2,"notepad"命令失败后返回string2。

我们的例子中返回了S_OK,说明命令执行成功,也可看到确实成功打开了记事本。

如果command执行后有字符串返回,则先返回字符串,在返回的字符串末尾返回string1或string2。例如:

判断process状态 process结果_CreateProcess_02

当然,string1和string2的字符串可自定义。

2、匿名管道通信

BOOL
WINAPI
CreatePipe(
    _Out_ PHANDLE hReadPipe,
    _Out_ PHANDLE hWritePipe,
    _In_opt_ LPSECURITY_ATTRIBUTES lpPipeAttributes,
    _In_ DWORD nSize
    );

CreatePipe是创建一个匿名管道,并从中得到读写管道的句柄。

hReadPipe[out] 返回一个可用于读管道数据的文件句柄

hWritePipe[out] 返回一个可用于写管道数据的文件句柄

lpPipeAttributes[in, optional] 传入一个SECURITY_ATTRIBUTES结构的指针,该结构用于决定该函数返回的句柄是否可被子进

程继承。如果传NULL,则返回的句柄是不可继承的。

该结构的lpSecurityDescriptor成员用于设定管道的安全属性,如果传NULL,那么该管道将获得一个默认的安全属性,该属性与

创建该管道的用户账户权限ACLs的安全令牌(token)相同。

nSize[in]

管道的缓冲区大小。但是这仅仅只是一个理想值,系统根据这个值创建大小相近的缓冲区。如果传入0 ,那么系统将使用一个默

认的缓冲区大小。

BOOL
WINAPI
PeekNamedPipe(
    _In_ HANDLE hNamedPipe,
    _Out_writes_bytes_to_opt_(nBufferSize, *lpBytesRead) LPVOID lpBuffer,
    _In_ DWORD nBufferSize,
    _Out_opt_ LPDWORD lpBytesRead,
    _Out_opt_ LPDWORD lpTotalBytesAvail,
    _Out_opt_ LPDWORD lpBytesLeftThisMessage
    );

预览一个管道中的数据,或取得与管道中的数据有关的信息。

参数


hNamedPipe [in]

管道句柄。这个参数可以是一个命名管道实例句柄,返回,由CreateNamedPipe或CreateFile函数,或者它可以是一个匿名管

道的读端句柄,返回由CREATEPIPE功能。句柄必须有GENERIC_READ权限的管道。

lpBuffer [out, optional]

接收从管道读取数据的缓冲区的指针。如果没有数据要读取,此参数可以为NULL。

nBufferSize [in]

lpBuffer参数以字节为单位,由指定的缓冲区大小。如果lpBuffer是NULL,则忽略此参数。

lpBytesRead [out, optional]

接收从管道中读取的字节数的变量的指针。此参数可以为NULL,如果没有数据要读取。

lpTotalBytesAvail [out, optional]

一个指针变量,接收从管道读取的字节总数。此参数可以为NULL,如果没有数据要读取。

lpBytesLeftThisMessage [out, optional]

指向剩余的字节数的变量的指针消息。此参数将是零字节类型的命名管道或匿名管道。此参数可以为NULL,如果没有数据要读取。

返回值

如果函数成功,返回值为非零。

如果函数失败,返回值是零。为了获得更多错误信息,调用GetLastError。


BOOL
WINAPI
ReadFile(
    _In_ HANDLE hFile,
    _Out_writes_bytes_to_opt_(nNumberOfBytesToRead, *lpNumberOfBytesRead) __out_data_source(FILE) LPVOID lpBuffer,
    _In_ DWORD nNumberOfBytesToRead,
    _Out_opt_ LPDWORD lpNumberOfBytesRead,
    _Inout_opt_ LPOVERLAPPED lpOverlapped
    );

从文件指针指向的位置开始将数据读出到一个文件中, 且支持同步和异步操作,



参数说明



HANDLE hFile, 需要读入数据的文件指针,这个指针指向的文件必须是GENERIC_READ 访问属性的文件。

LPVOID lpBuffer,接收数据的缓冲区。

DWORD nNumberOfBytesToRead,指定要读取的字节数。

LPDWORD lpNumberOfBytesRead,指向一个DWORD类型变量的指针,用来接收读取的字节数。如果下一个参数为NULL,那么一定要传入这个参数。

LPOVERLAPPED lpOverlapped OVERLAPPED结构体指针,如果文件是以FILE_FLAG_OVERLAPPED方式打开的话,那么这个指针就不能为NULL。

FILE_FLAG_OVERLAPPED 允许对文件进行重叠操作



返回值

调用成功返回非0,否则返回0




3、实现

对执行命令并匿名管道读取结果,本博客封装了一个类,提供接口供外部调用。

以下是封装类的头文件:

/* ====================================================
* Copyright: 275659704@qq.com
* Author: wfy
* Date: 2018-4-15
* Description: 提供CreateProcess()执行
* 命令并通过匿名管道获取的封装
==================================================== */

#ifndef __CMD_HANDLER_H__
#define __CMD_HANDLER_H__

#include <Windows.h>

/* buffer的最大长度 */
#define PIPE_BUFFER_SIZE 1024

/* 命令参数 */
typedef struct _CHCmdParam CHCmdParam;

struct _CHCmdParam
{
    /* 初始化为 sizeof(CommandParam) */
    int iSize;

    /* 外部命令,不需要用则设置为-1, 提供给外部使用
    */
    int iCommand;

    /* 超时时间,单位秒 */
    int iTimeOut;
    
    /* 命令行要执行的命令 */
    TCHAR szCommand[MAX_PATH];

    /* 用户数据 */
    void* pUserData;

    /* 命令执行后的回调 */
    void (*OnCmdEvent)(const CHCmdParam* pParam, HRESULT hResultCode, char* szResult);
};

class CCmdHandler
{
private:
    BOOL m_bInit;
    STARTUPINFO m_startupInfo;
    PROCESS_INFORMATION m_processInfo;
	SECURITY_ATTRIBUTES m_saOutPipe;
    DWORD m_dwErrorCode;
	HANDLE m_hPipeRead;
	HANDLE m_hPipeWrite;
    CHCmdParam m_CommandParam;
    TCHAR m_szReadBuffer[PIPE_BUFFER_SIZE];
    TCHAR m_szWriteBuffer[PIPE_BUFFER_SIZE];
    char m_szPipeOut[PIPE_BUFFER_SIZE];
    HRESULT ExecuteCmdWait();
public:
    CCmdHandler();
    ~CCmdHandler();
    /*
    * 初始化接口,调用其余接口之前调用
    * 成功返回S_OK
    */
    HRESULT Initalize();
    /*
    * 结束接口
    */
    HRESULT Finish();
    /*
    * 执行命令接口,接口调用成功返回S_OK
    * param[in] pCommmandParam: 指向一个CHCmdParam命令参数结构的指针
    */
    HRESULT HandleCommand(CHCmdParam* pCommmandParam);
    /*
    * 返回错误码,便于差距接口调用失败后产生什么错误
    */
    DWORD GetErrorCode() { return m_dwErrorCode; }
};

#endif // !__CMD_HANDLER_H__

以下是实现文件:

#include "CmdHandler.h"
#include <tchar.h>

#define EXCEPTIION_STATE_CHECK \
    if (!m_bInit) return E_NOTIMPL

CCmdHandler::CCmdHandler()
    : m_bInit(FALSE)
    , m_dwErrorCode(0)
    , m_hPipeRead(NULL)
    , m_hPipeWrite(NULL)
{
    ZeroMemory(m_szReadBuffer, sizeof(m_szReadBuffer));
    ZeroMemory(m_szWriteBuffer, sizeof(m_szWriteBuffer));
    ZeroMemory(&m_CommandParam, sizeof(m_CommandParam));
}
CCmdHandler::~CCmdHandler()
{
}

HRESULT CCmdHandler::Initalize()
{
    // 初始化,创建匿名管道
    if (m_bInit) return S_OK;
    m_bInit = TRUE;
    ZeroMemory(m_szReadBuffer, sizeof(m_szReadBuffer));
    ZeroMemory(&m_saOutPipe, sizeof(m_saOutPipe));
    m_saOutPipe.nLength = sizeof(SECURITY_ATTRIBUTES);
    m_saOutPipe.lpSecurityDescriptor = NULL;
    m_saOutPipe.bInheritHandle = TRUE;
    ZeroMemory(&m_startupInfo, sizeof(STARTUPINFO));
    ZeroMemory(&m_processInfo, sizeof(PROCESS_INFORMATION));
    if (!CreatePipe(&m_hPipeRead, &m_hPipeWrite, &m_saOutPipe, PIPE_BUFFER_SIZE))
    {
        m_dwErrorCode = GetLastError();
        return E_FAIL;
    }
    return S_OK;
}
HRESULT CCmdHandler::Finish()
{
    EXCEPTIION_STATE_CHECK;
    if (m_hPipeRead)
    {
        CloseHandle(m_hPipeRead);
        m_hPipeRead = NULL;
    }
    if (m_hPipeWrite)
    {
        CloseHandle(m_hPipeWrite);
        m_hPipeWrite = NULL;
    }
    return S_OK;
}
HRESULT CCmdHandler::HandleCommand(CHCmdParam* pCommmandParam)
{
    EXCEPTIION_STATE_CHECK;
    if (!pCommmandParam || pCommmandParam->iSize != sizeof(CHCmdParam))
        return E_INVALIDARG;
    if (_tcslen(pCommmandParam->szCommand) <= 0)
        return E_UNEXPECTED;
    memset(&m_CommandParam, 0, sizeof(m_CommandParam));
    m_CommandParam = *pCommmandParam;
    return ExecuteCmdWait();
}
HRESULT CCmdHandler::ExecuteCmdWait()
{
    EXCEPTIION_STATE_CHECK;
    HRESULT hResult = E_FAIL;
    DWORD dwReadLen = 0;
    DWORD dwStdLen = 0;
    m_startupInfo.cb = sizeof(STARTUPINFO);
    m_startupInfo.dwFlags = STARTF_USESTDHANDLES | STARTF_USESHOWWINDOW;
    m_startupInfo.hStdOutput = m_hPipeWrite;
    m_startupInfo.hStdError = m_hPipeWrite;
    m_startupInfo.wShowWindow = SW_HIDE;
    DWORD dTimeOut = m_CommandParam.iTimeOut >= 3000 ? m_CommandParam.iTimeOut : 5000;
    do
    {
        if (!CreateProcess(NULL, m_CommandParam.szCommand, 
            NULL, NULL, TRUE, CREATE_NEW_CONSOLE, NULL, NULL,
            &m_startupInfo, &m_processInfo))
        {
            m_dwErrorCode = GetLastError();
            hResult = E_FAIL;
            break;
        }
        if (WAIT_TIMEOUT == WaitForSingleObject(m_processInfo.hProcess, dTimeOut))
        {
            m_dwErrorCode = GetLastError();
            hResult = CO_E_SERVER_START_TIMEOUT;
            if (m_CommandParam.OnCmdEvent)
                m_CommandParam.OnCmdEvent(&m_CommandParam, CO_E_SERVER_START_TIMEOUT, "");
            break;
        }
        // 预览管道中数据的内容
        if (!PeekNamedPipe(m_hPipeRead, NULL, 0, NULL, &dwReadLen, NULL) 
            || dwReadLen <= 0)
        {
            m_dwErrorCode = GetLastError();
            hResult = E_FAIL;
            break;
        }
        else
        {
            ZeroMemory(m_szPipeOut, sizeof(m_szPipeOut));
            // 读取管道中的数据
            if (ReadFile(m_hPipeRead, m_szPipeOut, dwReadLen, &dwStdLen, NULL))
            {
                hResult = S_OK;
                if (m_CommandParam.OnCmdEvent)
                    m_CommandParam.OnCmdEvent(&m_CommandParam, S_OK, m_szPipeOut);
                break;
            }
            else
            {
                m_dwErrorCode = GetLastError();
                break;
            }
        }
    } while (0);
    if (m_processInfo.hThread)
    {
        CloseHandle(m_processInfo.hThread);
        m_processInfo.hThread = NULL;
    }
    if (m_processInfo.hProcess)
    {
        CloseHandle(m_processInfo.hProcess);
        m_processInfo.hProcess = NULL;
    }
    return hResult;
}


4、测试

针对以上的封装做出测试,就使用dir命令来测试好了。以下文件为main函数所在文件:

#include <Windows.h>
#include <stdio.h>
#include <tchar.h>
#include <string>
#include "CmdHandler.h"

#define SYSTEM_PAUSE system("pause")

void OnCommandEvent(const CHCmdParam* pParam, HRESULT hResultCode, char* szResult);

int main(int argc, TCHAR** argv)
{
    CHCmdParam cmdParam;
    CCmdHandler cmdHandler;
    HRESULT cmdResult = S_OK;

    ZeroMemory(&cmdParam, sizeof(cmdParam));
    cmdParam.iSize = sizeof(CHCmdParam);
    // 这里测试F磁盘Test目录下,命令格式为 cmd.exe /C + 命令
    TCHAR* szCmd = _T("cmd.exe /C dir F:\\Test&& echo S_OK || echo E_FAIL");
    _tcscpy_s(cmdParam.szCommand, szCmd);
    cmdParam.OnCmdEvent = OnCommandEvent;
    cmdParam.iTimeOut = 3000;

    cmdResult = cmdHandler.Initalize();
    if (cmdResult != S_OK)
    {
        printf("cmd handler 初始化失败\n");
        SYSTEM_PAUSE;
        return 0;
    }
    cmdResult = cmdHandler.HandleCommand(&cmdParam);
    if (cmdResult != S_OK)
    {
        printf("cmd handler 执行命令接口调用失败\n");
        cmdHandler.Finish();
        SYSTEM_PAUSE;
        return 0;
    }
    system("pause");
    return 0;
}

void OnCommandEvent(const CHCmdParam* pParam, HRESULT hResultCode, char* szResult)
{
    if (!szResult || !szResult[0]) return;
    if (!pParam || hResultCode != S_OK) return;
    printf("============== 回调 ==============\n");
    std::string echo_data(szResult);
    std::string s_ok("S_OK");
    std::string::size_type pos = echo_data.find(s_ok);
    if (pos != std::string::npos)
        printf("命令执行成功\n");
    else
        printf("命令执行失败\n");
    printf("执行返回的结构:\n");
    printf("========================================\n");
    printf("%s\n", szResult);
}

在本机上运行以上代码,测试结果为:

判断process状态 process结果_判断process状态_03

可看到打印的测试结果后跟后缀“S_OK”。

注意:

1、在ExecuteCmdWait()函数中,WaitForSingleObject那一行传入了一个默认的超时时间至少在3秒以上。其实对于有些cmd例如打开记事本的"notepad",则只有记事本退出时,WaitForSingleObject()函数才返回,因此有必要将超时时间设置成INFINITE,否则ExecuteCmdWait()会返回超时;

2、传入的cmdParam中的OnCmdEvent()回调不可阻塞,否则ExecuteCmdWait()将无法返回;

3、针对2,如果需要可随意对实现代码进行修改,在不同的线程中执行。