コンソール(CMD.EXE)のログを取る (不完全版)

コマンドプロンプト(CMD.EXE)の操作や出力ログを全部取りたい、と思っては見たものの、Windows標準の機能だけでは完全なログを取ることができません。リダイレクトしても入力したログが記録されないし、何より、コンソール画面に出力されないので入力しつつ、出力も表示しながら、かつすべてのログをとる、ということが標準でできません。

確かUnix系のOSなら、teeコマンドというものでできるらしいのですが・・・Windowsにはありません。そういう需要がWindowsには少ないのか、ネットで探すと、VBSスクリプトで実装しているもの(Tee.vbs)もありますが・・・。

これが Cygwin であれば、TeratermやPedorosaのようなターミナルエミュレータでCygwinに接続することですべてのログを取ることが可能ですが、残念ながらこれらのターミナルからCMD.EXEには接続できません。

というわけで、C++で強引に作ってみよう、というわけで、とりあえず動作チェック・テスト用コードを書いてみました。

仕組み自体は、CreateProcessで標準入出力をリダイレクトしたCMD.EXEのプロセスを作って、匿名パイプで出力を取得してファイルと画面に出力する、という至って普通のやり方です。

なんとか強引にコーディングして、標準出力と標準エラー出力はファイルに落とすことはできましたが、まだ入力のログがとれてません。また来週にでも完全版を書きたいと思ってます。。。

/****************************************************************

  ■CMD.EXE のログをとる不完全版テストコード
     (入力がログに落ちません)

****************************************************************/

#include <windows.h>
#include <tchar.h>
#include <stdio.h>

#define CLOSEHANDLE(X) {if(X)CloseHandle(X);}
#define SAFEDELETE(X) {if(X)delete (X);}
#define BUFFER_SIZE 1024
#define SPIN_COUNT 4000
#define LOGFILE_PATH TEXT("cmdlog.log")

//匿名パイプをラップしたもの
struct PipeHandle
{
  HANDLE hRead;
  HANDLE hWrite;
  SECURITY_ATTRIBUTES sa;

  PipeHandle() : hRead(NULL),hWrite(NULL)
    {
      sa.nLength = sizeof(SECURITY_ATTRIBUTES);
      sa.lpSecurityDescriptor = NULL;
      sa.bInheritHandle = TRUE;

      ::CreatePipe(&hRead,&hWrite,&sa,0);
    }
  ~PipeHandle()
    {
      CLOSEHANDLE(hWrite);
      CLOSEHANDLE(hRead);
    }
};

//スレッド関数に各種変数を渡すために使用するコンテキスト
struct Context
{
  PipeHandle *lpStdHandle;
  HANDLE hFile;
  HANDLE hStdio;
  LPCRITICAL_SECTION lpCriticalSection;
  PBOOL pThreadMustTerminate;

  Context(LPCRITICAL_SECTION lpcs,PBOOL ptht) 
    : lpCriticalSection(lpcs),pThreadMustTerminate(ptht)
    {}
};

//パイプから出力を読み取るスレッド関数
DWORD WINAPI ReadOutputProc(LPVOID lpParameter)
{
  Context *context = (Context*)lpParameter;

  TCHAR pBuffer[BUFFER_SIZE] = {0};
  DWORD dwRead = 0,dwWrite = 0;

  while(!(*context->pThreadMustTerminate))
    {
      BOOL b = FALSE;
      DWORD dwReadAvail = 0;
      do
        {
          b = ReadFile(context->lpStdHandle->hRead,(PVOID)pBuffer,BUFFER_SIZE,&dwRead,NULL);
          if(dwRead > 0)
            {
              EnterCriticalSection(context->lpCriticalSection);
              WriteFile(context->hFile,(PVOID)pBuffer,dwRead,&dwWrite,NULL);
              WriteFile(context->hStdio,(PVOID)pBuffer,dwRead,&dwWrite,NULL);
              LeaveCriticalSection(context->lpCriticalSection);
            }
          if(*context->pThreadMustTerminate == TRUE)
            break;
        }
      while(b == TRUE && dwRead > 0);
    }

  return 0;
}

//スタートアップ
int _tmain(int argc,_TCHAR **argv)
{
  //CMD.EXEのパスを格納するバッファ
  TCHAR szCmdPath[MAX_PATH+1] = {0};
  
  //ログを書き込むファイル
  HANDLE hFile = NULL;
  
  //CreateProcessに必要な構造体
  STARTUPINFO si;
  PROCESS_INFORMATION pi;

  //パイプ
  PipeHandle *ppStdOut = new PipeHandle();
  PipeHandle *ppStdErr = new PipeHandle();

  //スレッドの同期処理に必要
  CRITICAL_SECTION cs;
  BOOL bThreadMustTerminate = FALSE;

  //標準入力コンソールハンドル
  HANDLE hStdInput = GetStdHandle(STD_INPUT_HANDLE);
  
  //スレッドに渡すコンテキスト
  Context ctxOut(&cs,&bThreadMustTerminate);
  Context ctxErr(&cs,&bThreadMustTerminate);

  //クリティカルセクションオブジェクト初期化
  InitializeCriticalSectionAndSpinCount(&cs,SPIN_COUNT);

  //CMD.EXEのパスを環境(システム)変数から得る。
  if(0 == GetEnvironmentVariable(TEXT("COMSPEC"),szCmdPath,MAX_PATH+1))
    {
      _tprintf(TEXT("環境変数 COMSPEC 取得失敗\n"));
      return 0;
    }

  //ログファイルをオープン
  if((hFile = CreateFile(LOGFILE_PATH,
                         GENERIC_WRITE,
                         0,
                         NULL,
                         CREATE_ALWAYS,
                         FILE_ATTRIBUTE_NORMAL,
                         NULL)) == INVALID_HANDLE_VALUE)
    {
      _tprintf(TEXT("CreateFile が失敗しました"));
      goto cleanup;
    }
  

  // CMD.EXEのプロセスを生成
  ZeroMemory((LPVOID)&si,sizeof(STARTUPINFO));
  si.cb = sizeof(STARTUPINFO);
  si.dwFlags    = STARTF_USESTDHANDLES;
  si.hStdInput  = hStdInput;
  si.hStdOutput = ppStdOut->hWrite; //パイプにリダイレクト
  si.hStdError  = ppStdErr->hWrite; //パイプにリダイレクト

  if(FALSE == CreateProcess(NULL,
                            szCmdPath,
                            NULL,
                            NULL,
                            TRUE,
                            NORMAL_PRIORITY_CLASS,
                            NULL,
                            NULL,
                            &si,
                            &pi))
    {	
      _tprintf(TEXT("CreateProcess 失敗 : %d"),GetLastError());
      goto cleanup;
    }
  
  //スレッドに渡すコンテキストのメンバをセット
  ctxOut.lpStdHandle = ppStdOut;
  ctxOut.hFile = hFile;
  ctxOut.hStdio = GetStdHandle(STD_OUTPUT_HANDLE);
  
  ctxErr.lpStdHandle = ppStdErr;
  ctxErr.hFile = hFile;
  ctxErr.hStdio = GetStdHandle(STD_ERROR_HANDLE);

  /* スレッド作成 */
  HANDLE hThread1 = CreateThread(NULL,0,ReadOutputProc,&ctxOut,0,NULL);
  HANDLE hThread2 = CreateThread(NULL,0,ReadOutputProc,&ctxErr,0,NULL);

  //CMDプロセス終了まで待機
  WaitForSingleObject(pi.hProcess,INFINITE);
  
  //スレッド終了通知
  bThreadMustTerminate = TRUE;

  //パイプの削除
  SAFEDELETE(ppStdOut);
  SAFEDELETE(ppStdErr);
  
  //クリティカルセクションオブジェクトを削除
  DeleteCriticalSection(&cs);

cleanup:
  //ハンドルを閉じる
  CLOSEHANDLE(hFile);
  CLOSEHANDLE(pi.hThread);
  CLOSEHANDLE(pi.hProcess);
  CLOSEHANDLE(hThread1);
  CLOSEHANDLE(hThread2);

  return 0;
}

なんか・・・汚いコードだな、相変わらず・・・。
これだけなら、

CMD.EXE 2>&1 |  xxxxxx

と変わらないなぁ・・・。


コマンドプロンプトのログを取る(完結)
コマンドプロンプトのログを取る(その3)
コマンドプロンプトのログを取る (その2)