編寫自己的IDE

 

如何在圖形界面中實時捕獲控制檯程序的標準輸出

本文未曾在商業媒體發表過, 如需轉載, 請註明作者 [王詠剛] 和出處 [www.contextfree.net]

IDE是集成開發環境(Integrated Development Environment)的簡稱。印象裏有很多出色的IDE,比如JBuilder和Kylix,比如Visual Studio。不知大家是否留意過,大多數IDE本身只提供代碼編輯、工程管理等人機交互功能,我們在IDE中編譯代碼、調試程序時,IDE需要調用命令行的編譯器、調試器完成相應的操作。例如,使用Visual Studio編譯C++程序時,我們會在IDE下方的Output窗口中看到編譯和連接的全過程,雖然我們看不到彈出的DOS窗口,但實際上是IDE先後啓動了Microsoft C++編譯器cl.exe和連接器link.exe這兩個命令行程序,而cl.exe和link.exe的輸出又實時反映到了IDE的Output窗口中。還有,我們可以在Visual Studio中配置自己需要的工具程序(比如特殊的編譯器),然後讓Visual Studio在適當的時候運行這些工具,並將工具程序的輸出實時顯示到Output窗口中。下圖是我在Visual Studio 6.0的Output窗口中運行J2SDK的javac.exe編譯java源程序並顯示程序中語法錯誤的情形:

myide1.gif 

也就是說,大多數IDE工具都可以在集成環境中調用特定的命令行程序(WIN32裏更確切的說法是控制檯程序),然後實時捕獲它們的輸出(這多半是輸出到標準的stdout和stderr流裏的東西),並將捕獲到的信息顯示在圖形界面的窗口中。

這顯然是一種具備潛在價值的功能。利用這一技術,我們至少可以

1. 編寫出自己的IDE,如果我們有足夠的耐心的話;

2. 在我們自己的應用程序裏嵌入全文檢索功能(調用Borland C++裏的grep.exe工具),或者壓縮和解壓縮功能(調用控制檯方式的壓縮解壓程序,比如arj.exe、pkzip.exe等);

3. 連接其他人編寫的,或者我們自己很久以前編寫的控制檯程序——我經常因爲難以調用一個功能強大但又沒有源碼的控制檯程序而苦惱萬分。

這樣好的功能是如何實現的呢?

首先,如果我們想做的是用一個控制檯程序調用另一個控制檯程序,那就再簡單不過了。我們只消把父進程的stdout重定向到某個匿名管道的WRITE端,然後啓動子進程,這時,子進程的stdout因爲繼承的關係也連在了管道的WRITE端,子進程的所有標準輸出都寫入了管道,父進程則在管道的另一端隨時“偵聽”——這一技術叫做輸入輸出的重定向。

可現在的問題是,GUI方式的Windows程序根本沒有控制檯,沒有stdin、stdout之類的東西,子進程又是別人寫好的東西無法更改,這重定向該從何談起呢?

還有另外一招:我們可以直接在調用子進程時用命令行中的管道指令“>”將子進程的標準輸出重定向到一個文件,子進程運行完畢後再去讀取文件內容。這種方法當然可行,但它的問題是,我們很難實時監控子進程的輸出,如果子進程不是隨時刷新stdout的話,那我們只能等一整塊數據實際寫入文件之後才能看到運行結果;況且,訪問磁盤文件的開銷也遠比內存中的管道操作來得大。

我這裏給出的方案其實很簡單:既然控制檯程序可以調用另一個控制檯程序並完成輸入輸出的重定向,那我們完全可以編寫一箇中介程序,這個中介程序調用我們需要調用的工具程序並隨時獲取該程序的輸出信息,然後直接將信息用約定的進程間通訊方式(比如匿名管道)傳回GUI程序,就象下圖中這樣:

myide2.gif 

圖中,工具程序和中介程序都是以隱藏的方式運行的。工具程序原本輸出到stdout的信息被重定向到中介程序開闢的管道中,中介程序再利用GUI程序創建的管道將信息即時傳遞到GUI程序的一個後臺線程裏,後臺線程負責刷新GUI程序的用戶界面(使用後臺線程的原因是,只有這樣纔可以保證信息在GUI界面中隨時輸出時不影響用戶正在進行的其他操作,就象我們在Visual Studio中執行耗時較長的編譯功能那樣)。

我寫的中介程序名字叫wSpawn,這個名字來自Visual Studio裏完成類似功能的中介程序VcSpawn(你可以在Visual Studio的安裝目錄中找到它)。我的wSpawn非常簡單,它利用系統調用_popen()同時完成創建子進程和輸入輸出重定向兩件工作。GUI程序則使用一種特殊的命令行方式調用wSpawn:

wspawn –h <n> <command> [arg1] [arg2] ...

其中,-h後跟的是GUI程序提供的管道句柄,由GUI程序自動將其轉換爲十進制數字,wSpawn運行時將信息寫入該句柄中,隨後的內容是GUI程序真正要執行的命令行,例如調用C++編譯器cl.exe的方式大致如下:

wspawn –h 1903 cl /Id:/myInclude Test.cpp

wspawn.cpp的程序清單如下:

#include <stdlib.h>
#include <stdio.h>
#include <string.h>
#include <string>
#include <windows.h>

using namespace std;

void exit_friendly(void)
{
    puts("請不要單獨運行wSpawn.");
    exit(0);
}

int main( int argc, char *argv[] )
{
    HANDLE  hWrite = NULL;
    DWORD   dwWrited;
    int     i = 0, ret = 0, len = 0;
    char    psBuffer[256];
    FILE*   child_output;
    string  command_line = "";

    // 檢查命令行,如存在管道句柄,則將其轉換爲HANDLE類型
    if (argc < 2)
        exit_friendly();
    if (!stricmp(argv[1], "-h"))
    {
        if (argc < 4)
            exit_friendly();
        hWrite = (HANDLE)atoi(argv[2]);
        i = 3;
    }
    else
        i = 1;

    // 提取要執行的命令
    for (; i < argc; i++)
    {
        command_line += argv[i];
        command_line += " ";
    }

    // 使用_popen創建子進程並重定向其標準輸出到文件指針中
    if( (child_output = _popen( command_line.c_str(), "rt" )) == NULL )
        exit( 1 );

    while( !feof( child_output ) )
    {
        if( fgets( psBuffer, 255, child_output ) != NULL )
        {
            if (hWrite)
            {
                // 將子進程的標準輸出寫入管道,提供給自己的父進程
                // 格式是先寫數據塊長度(0表示結束),再寫數據塊內容
                len = strlen(psBuffer);
                WriteFile(hWrite, &len, sizeof(int), &dwWrited, NULL);
                WriteFile(hWrite, psBuffer, len, &dwWrited, NULL);
            }
            else
                // 如命令行未提供管道句柄,則直接打印輸出
                printf(psBuffer);
        }
    }

    // 寫“0”表示所有數據都已寫完
    len = 0;
    if (hWrite)
        WriteFile(hWrite, &len, sizeof(int), &dwWrited, NULL);
    return _pclose( child_output );
}

下面,我們就利用wSpawn程序,寫一個簡單的“IDE”工具。我們選擇Visual Studio 6.0作爲開發環境(本文給出的代碼也在Visual Studio.NET 7.0中做過測試)。首先,創建Visual C++工程myIDE,工程類型爲MFC AppWizard(EXE)中的Dialog based類型,即創建了一個主窗口爲對話框的GUI程序。工程myIDE的主對話框類是CMyIDEDlg。現在我們要在資源編輯器中爲主對話框添加一個足夠大的多行編輯框(Edit Box),它的控制ID是IDC_EDIT1,必須爲IDC_EDIT1設置以下屬性:

Multiline, Horizontal scroll, Auto HScroll,
Vertical scroll, Auto VScroll, Want return

然後用ClassWizard爲IDC_EDIT1添加一個對應的成員變量(注意變量的類型要選CEdit型而非字符串CString型)

CEdit m_edit1;

使用ClassWizard爲“確定”按鈕添加消息響應方法OnOK(),編輯該方法:

void CMyIDEDlg::OnOK()
{
    AfxBeginThread(myThread, this);
    InvalidateRect(NULL);
    UpdateWindow();
}

也就是說,我們在“確定”按鈕按下時,啓動了後臺線程myThread(),那麼,myThread()到底做了些什麼呢?我們先在CMyIDEDlg類的頭文件myIDEDlg.h中加上一個成員函數聲明:

protected:
    static UINT myThread(LPVOID pParam);

然後,在CMyIDEDlg類的實現文件myIDEDlg.cpp裏添加myThread()的實現代碼:

UINT CMyIDEDlg::myThread(LPVOID pParam)
{
    PROCESS_INFORMATION pi;
    STARTUPINFO siStartInfo;
    SECURITY_ATTRIBUTES saAttr;
    CString Output, tmp;
    char command_line[200];
    DWORD dwRead;
    char* buf; int len;
    HANDLE hRead, hWrite;

    CMyIDEDlg* pDlg = (CMyIDEDlg*)pParam;

    // 創建與wSpawn.exe通訊的可繼承的匿名管道
    saAttr.nLength = sizeof(SECURITY_ATTRIBUTES);
    saAttr.bInheritHandle = TRUE;
    saAttr.lpSecurityDescriptor = NULL;
    if (!CreatePipe(&hRead, &hWrite, &saAttr, 0))
    {
        AfxMessageBox("創建管道失敗");
        return 0;
    }

    // 準備wSpawn的命令行,在命令行給出寫管道句柄和要wSpawn執行的命令
    memset(&pi, 0, sizeof(pi));
    sprintf(command_line, "wspawn -h %d cl /?", (unsigned int)hWrite);

    // 子進程以隱藏方式運行
    ZeroMemory( &siStartInfo, sizeof(STARTUPINFO) );
    siStartInfo.cb = sizeof(STARTUPINFO);
    siStartInfo.wShowWindow = SW_HIDE;
    siStartInfo.dwFlags = STARTF_USESHOWWINDOW;

    // 創建wSpawn子進程
    if (!CreateProcess( NULL, command_line, NULL, NULL, TRUE,
                        0, NULL, NULL, &siStartInfo, &pi))
    {
        AfxMessageBox("調用wSpawn時失敗");
        return 0;
    }

    // 讀管道,並顯示wSpawn從管道中返回的輸出信息
    if(!ReadFile( hRead, &len, sizeof(int), &dwRead, NULL) || dwRead == 0)
        return 0;

    while(len)
    {
        buf = new char[len + 1];
        memset(buf, 0, len + 1);
        if(!ReadFile( hRead, buf, len, &dwRead, NULL) || dwRead == 0)
            return 0;

        // 將返回信息中的"/n"替換爲Edit Box可識別的"/r/n"
        tmp = buf;
        tmp.Replace("/n", "/r/n");
        Output += tmp;

        // 將結果顯示在Edit Box中,並刷新對話框
        pDlg->m_edit1.SetWindowText(Output);
        pDlg->InvalidateRect(NULL);
        pDlg->UpdateWindow();

        delete[] buf;
        if(!ReadFile( hRead, &len, sizeof(int), &dwRead, NULL) || dwRead == 0)
            return 0;
    }

    // 等待wSpawn結束
    WaitForSingleObject(pi.hProcess, 30000);
    // 關閉管道句柄
    CloseHandle(hRead);
    CloseHandle(hWrite);
    return 0;
}

很簡單,不是嗎?後臺線程創建一個匿名管道,然後以隱藏方式啓動wSpawn.exe並將管道句柄通過命令行傳給wSpawn.exe,接下來只要從管道里讀取信息就可以了。現在我們可以試着編譯運行myIDE.exe了,記住要把myIDE.exe和wSpawn.exe放在同一目錄下。還有,我在myThread()函數中寫死了傳給wSpawn.exe的待執行的命令行是“cl /?”,這模擬了一次典型的編譯過程,如果你不打算改變這一行代碼的話,那一定要注意在你的計算機上,C++編譯器cl.exe必須位於環境變量PATH指明的路徑裏,否則wSpawn.exe可就找不到cl.exe了。下面是myIDE程序的運行結果:

myide3.gif 

補充一點,上面給出的wSpawn利用_popen()完成子進程創建和輸入輸出重定向,這一方法雖然簡單,但只能重定向子進程的stdout或stdin,如果還需要重定向子進程的stderr的話(Java編譯器javac就利用stderr輸出結果信息),那我們就不能這麼投機取巧了。根據以上討論,你一定可以使用傳統的_pipe()、_dup()等系統調用,寫出功能更完整的新版wSpawn來,我這裏就不再羅嗦了。

 

[王詠剛,2002年5月]

 

 

補充:相反方向的信息傳遞

上面這篇文章在網上發佈後,引起了一些反響。很多網友來信詢問這樣一個問題:上文中演示的是圖形界面程序實時捕獲控制檯程序的輸出;但有不少控制檯程序是交互式運行的(如ftp客戶端程序),需要人們在控制界面輸入特定的指令才能完成相應的功能——能不能用類似的辦法,讓圖形界面程序向控制檯程序輸入特定的命令行指令呢?

如果我們想輸入到控制檯程序的指令序列是固定的,那完全可以使用更簡單的辦法:把命令序列存儲在一個文本文件中,然後使用下面這樣的重定向指令運行控制檯程序:

foo.exe < commands.txt

但如果想輸入到控制檯的指令序列是由用戶在操作圖形界面程序時決定的,或是根據控制檯程序的輸出來決定的,我們就需要使用與上面文章中類似的管道法解決問題了。這一思路基本上和上文相同,只不過信息的傳遞方向顛倒了過來:圖形界面程序在需要時將指令序列作爲字符串傳遞給中介程序,中介程序將該字符串寫入控制檯程序的標準輸入。

實現這種相反功能的中介程序proxy的代碼如下:

#include <stdlib.h>
#include <stdio.h>
#include <string.h>
#include <string>
#include <windows.h>

using namespace std;

void exit_friendly(void)
{
    puts("請不要單獨運行proxy.");
    exit(0);
}

int main( int argc, char *argv[] )
{
    HANDLE  hRead = NULL;
    DWORD   dwReaded;
    int     i = 0, ret = 0, len = 0;
    const int BUFFER_LEN = 256;
    char    psBuffer[BUFFER_LEN];
    FILE*   child_input;
    string  command_line = "";

    // 檢查命令行,如存在管道句柄,則將其轉換爲HANDLE類型
    if (argc < 2)
        exit_friendly();
    if (!stricmp(argv[1], "-h"))
    {
        if (argc < 4)
            exit_friendly();
        hRead = (HANDLE)atoi(argv[2]);
        i = 3;
    }
    else
        i = 1;

    // 提取要執行的命令
    for (; i < argc; i++)
    {
        command_line += argv[i];
        command_line += " ";
    }

    // 使用_popen創建子進程並重定向其標準輸入
    if( (child_input = _popen( command_line.c_str(), "wt" )) == NULL )
        exit( 1 );

    if (hRead)
    {
        while(1)
        {
            memset(psBuffer, 0, BUFFER_LEN);
            if (ReadFile(hRead, psBuffer, BUFFER_LEN, &dwReaded, NULL)
                    && dwReaded > 0)
            {
                fputs(psBuffer, child_input);
                fflush(child_input);

                psBuffer[4] = 0;
                if (!stricmp(psBuffer, "quit"))
                    break;
            }
        }
    }
    return _pclose( child_input );
}

圖形界面程序中,創建管道並啓動中介程序的示例代碼如下:

    HANDLE hRead, hWrite;
    HANDLE hProcess;

    PROCESS_INFORMATION pi;
    STARTUPINFO siStartInfo;
    SECURITY_ATTRIBUTES saAttr;
    CString Output, tmp;
    char command_line[200];

    // 創建與proxy.exe通訊的可繼承的匿名管道
    saAttr.nLength = sizeof(SECURITY_ATTRIBUTES);
    saAttr.bInheritHandle = TRUE;
    saAttr.lpSecurityDescriptor = NULL;
    if (!CreatePipe(&hRead, &hWrite, &saAttr, 0))
    {
        AfxMessageBox("創建管道失敗");
        EndDialog(IDCANCEL);
        return FALSE;
    }

    // 準備proxy.exe的命令行,在命令行給出寫管道句柄和要proxy.exe執行的命令
    memset(&pi, 0, sizeof(pi));
    sprintf(command_line, "proxy -h %d ftp ...", (unsigned int)hRead);

    ZeroMemory( &siStartInfo, sizeof(STARTUPINFO) );
    //siStartInfo.cb = sizeof(STARTUPINFO);
    //siStartInfo.wShowWindow = SW_HIDE;
    //siStartInfo.dwFlags = STARTF_USESHOWWINDOW;
    if (!CreateProcess( NULL, command_line, NULL, NULL, TRUE,
                        0, NULL, NULL, &siStartInfo, &pi))
    {
        AfxMessageBox("調用proxy.exe時失敗");
        EndDialog(IDCANCEL);
        return FALSE;
    }
    hProcess = pi.hProcess;

圖形界面程序中,向控制檯程序發送某特定命令的示例代碼如下:

    char command = "help";
    DWORD dwWritten;
    WriteFile(hWrite, command, strlen(command), &dwWritten, NULL);

顯然,利用這兩種方向的管道,我們很容易爲一個純控制檯界面的程序加上一層圖形用戶界面的漂亮外殼。

[王詠剛,2003年12月]

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章