본문으로 바로가기

[C#] 외부 프로그램 실행시키고,종료이벤트 처리

category 개발언어/C# 2017. 11. 9. 12:05
C#으로 응용프로그램 프로세스를 실행시키고, 에러가 발생했을 때 강제 종료 시키고 다시 응용프로그램을  실행시키도록 Launcher 프로그램을 만들었다. 그런데 Tray Icon으로 숨겨지는 프로그램, Dialog Base의 응용프로그램의 경우와 에러가 발생했을 때 Message Box가 떠 있는 경우는 상태 파악이 되지 않는 난감한 문제에 직면하게 되었다. 이문제를 해결하는 과정을 메모 해둔다.
C#으로 외부 프로그램을 실행시키기C#에서 외부 프로그램을 실행시키기 위해서 System.Diagnostics.Process 클래스를 이용하면 된다.
System.Diagnostics.Process ps = new System.Diagnostics.Process();
ps.StartInfo.FileName = "실행할 파일명(xxx.exe)";
ps.StartInfo.WorkingDirectory = "작업 폴더(c:\\프로그램폴더)";
ps.StartInfo.WindowStyle = System.Diagnostics.ProcessWindowStyle.Normal;
//위와 같이 StartInfo에 실행할 프로그램의 정보를 설정한 후 Start()를 실행하면 된다.
ps.Start();
ps.WaitForExit(1000);//프로세서가 끝 나기를 기다림 1초(1000밀리)
WaitForExit(1000) 의 1000 은 1초를 기다린다는 뜻입니다.파라메타가 없으면 무기한 대기합니다.이 무기한 대기가 사용되면, 호출된 프로세스가 너무 오래 걸릴 경우 끝났는지 아닌지 확인이 안되고 죽어 버리는 문제가 경향이 있으므로 루프롤 돌면서 수시로 체크해야 한다. 또 다른 방법은 종료 이벤트를 확인하는 방법이 있다.
private void RunNotePad()
{
  Process p1 = new Process("notepad.exe");
  p1.EnableRaisingEvents=true;//종료 이벤트를 활성화 
  p1.Exited += new EventHandler(ProcessExited);//이벤트 핸들러 지정
  p1.Start();
}
public void ProcessExited(object source, EventArgs e)
{
// 프로세스가 종료 되었을 때 처리 작업을 ...
}
프로세스가 이미 실행되고 있는지 파악해서 이미 실행되고 있다면 에러가 없는지 확인하고 에러가 있으면 종료시키고 다시 실행 시키고 싶다면 어떻게 처리해야 할까? 기본적인 개념은 프로세스가 실행되고 있는지 확인하고, Main윈도우가 정상적으로 Active 되어 있는지 확인하면 된다. 에러가 있으면 Main Window가 닫히고 에러창이 떠게 되니까..
기본 개념에 대한 코드
 [DllImport("user32.dll")]
public static extern int SendMessage(IntPtr hWnd, uint Msg, int wParam, int lParam);
void Main()///프로세스를 감시하는 thread를 실행시키고
{
 bool bThredAlive = true;
 Thread hThread = new Thread(delegate () { MonitoringProcee(); });
 hThread.Priority = ThreadPriority.Lowest;
 hThread.IsBackground = true;
 hThread.Start(); 
}
/// Process의 상태를 감시 하도록 루틴을 만듦
public void MonitoringProcee()
{//너무 복잡한 조건을 상정하니까 계속 어려워 진다.
  while (bThredAlive)
  {
    Process[] processs = GetProcessesByName(clsName);//실행 프로그램의 Class이름으로 프로세서 찾기 에:)clsName.exe
    if (processs.Length <= 0)//실행되고 있지 않으면 실행이 되도록 한다
    { 
      Start();//Process를 실행시키는 루틴     
    }
    else //이미 프로세스가 실행되고 있으면 에러만 처리하도록, 프로세스를 조사하지 않으면 무한 반복으로 응용프로그램을 실행시킨다.
    {     
       foreach (Process Pr in processs)
       { 
         if(CheckError())//에러를 검출하고 에러가 있을 때 윈도우를 종료 시킨다
           SendMessage(Pr.MainWindowHandle, 0x0010, -1, -1);//윈도우를 종료하게 만든다
            /// 에러가 있을 때 Process클래스의 Close,CloseMainWindow 는 정상작동하지 않는다 ///////////////
            //Pr.Close();
            //Pr.CloseMainWindow();             
       }
   }
}
프로세스 클래스를 이용하여 외부 프로그램을 실행시키는 것은 해결 되었다.그러나 에러로 인해 응용프로그램이 자동으로 종료되어버리는 경우 에러창이 남는 경우도 있고, 사용자가 에러창을 수동으로 닫아야만 응용프로그램이 종료되는 경우도 있다.종료된후 에러가 있었다고 메세지를 띄우고 있는 경우 거추장 스런 창들을 자동으로 닫아 줄 필요가 있다.  


에러창을 확인하고 닫아야 프로그램이 종료되는 경우, 실제로 응용프로그램은 정상동작 되지 않고 있음에도 불구하고 프로세스를 조사해 보면 해당프로세스가 실행되고 있다는 결과를 얻게 되고 의도했던 에러발생 후 재실행 할 수 있는 효과를 기대할 수 없게 된다. 결국 에러창을 찾아서 닫을 수 있는 어떤 동작이 필요하다. 프로세스 Check 만으로 응용프로그램의 실행여부를 판단할 수 있지만 에러창을 찾아서 닫는 방법은 조금 다르게 처리 되어야한다.예상되는 윈도우를("윈도우 창의 이름")모두 검색해서 일일이 닫아야 한다는 것이다. 그러기 위해서는 정상적인 응용프로그램인지 에러창인지 판단 할 수 있어야 한다. 그래서 MainWindow의 상태를 먼저 파악하고 나서 MainWindow가 아닌 에러창들을 닫도록 해야 하는데.... 몇몇 문제가 있다.

Process 클래스의 MainWindowHandle 이용의 문제점(예외상황)의도는 MainWindow가 비활성화 되면 에러로 처리하려는 목적이었지만 실제 동작결과는 다소 의외다. Process.MainWindowHandle는 에러메세지 박스가 떠 있는 것과 상관없이 유효한 핸들값을 반환한다. 오혀혀 Tray Icon으로 숨겨진 경우 0을 반환하는 예상밖의 결과를 보여준다.
이렇게 동작된다면 Process.MainWindowHandle를 사용할 수 없다. 그러나 Window Api의 FindWindow의 경우는 TrayIcon으로 처리된 윈도우의 핸들을 반환하므로 FindWindow를 사용하도록 한다. IntPtr pWnd=FindWindow(null,"윈도우네임"); API 사용하는 방법-- [DllImport("user32.dll", CharSet = CharSet.Auto)] public static extern IntPtr FindWindow(string strClassName, string strWindowName);
Process 클래스의 MainWindowHandle를 이용하여 에러여부를 감지하기 적절치 않지만,API의 FindWindow를 사용하면 TrayIcon으로 처리된 윈도우 일지라도 핸들을 찾을 수 있다. 그리고 API를 이용해서 Error Dialog와 구분해서 판단하는 방법을 살펴보자. KillErrorProcess Method에서 Application이름으로 응용프로그램을 모두 찾아서 프로세스 ID가 같은 것은 남겨두고 에러 메세지박스 혹은 다이얼로그만 닫는다
public void MonitoringProcee()
{
  while (bThredAlive)
  {
    Process[] processs = GetProcessesByName(clsName);//실행 프로그램의 Class이름으로 프로세서 찾기 에:)clsName.exe
    if (processs.Length <= 0)//실행되고 있지 않으면 실행이 되도록 한다
    { 
      Start();//Process를 실행시키는 루틴     
    }
    else //이미 프로세스가 실행되고 있으면 에러만 처리하도록, 프로세스를 조사하지 않으면 무한 반복으로 응용프로그램을 실행시킨다.
    {     
        foreach (Process Pr in processs)
        {
          if (!Pr.EnableRaisingEvents)//종료이벤트 수신이 되어 있지 않으면 이벤트를 수신하도록 설정
          {
            Pr.Exited += ProcessExited;
            Pr.EnableRaisingEvents = true;//종료 이벤트 수락   
          }
          KillErrorProcess(Pr.ProcessName, Pr.Id);//에러창을 강제로 닫기
        }
       Thread.Sleep(5000);
   }
}
//// 에러발생을 알리는 Dialog 또는 MessageBox를 닫자
bool KillErrorProcess(String wndTitle, int ValidprocessID)
{
  uint processID = 0;          
  StringBuilder Title = new StringBuilder(256);
  StringBuilder lpClassName = new StringBuilder(256);
  IntPtr tempHwnd = WinApi.FindWindow(null, null); // 최상위 윈도우 핸들 찾기               
  
  while (tempHwnd.ToInt32() != 0)//에러 Dialog를 직접 찾을 방법이 없으로로 윈도우를 모두 뒤져야 한다
  {
   tempHwnd = WinApi.GetWindow(tempHwnd, WinApi.GW_HWNDNEXT); // 다음 윈도우 핸들 찾기   
   WinApi.GetWindowText(tempHwnd, Title, Title.Capacity + 1); //윈도우 Title 찾기
  
   if (Title.ToString().IndexOf(wndTitle) >= 0)
   {
       WinApi.GetWindowThreadProcessId(tempHwnd, out processID);//윈도우의 프로세스 ID 찾기                 
       if (processID != ValidprocessID)//에러 메세지 박스, 다이알로그 등 윈도우 타이틀로 찾아서 Launch시킨것과 같은지 확인
       {
         WinApi.GetClassName(tempHwnd, lpClassName, lpClassName.Capacity + 1);        
         if (lpClassName.ToString().CompareTo("#32770") == 0)//Dialog,MessageBox인 것은 닫고 다른 응용프로그램이면 닫지 않는다
         {
               WinApi.SendMessage(tempHwnd, 0x0010, -1, -1);
         }
       }
   }
  }
}
//API를 사용하려면 Dll을 Import하도록 해주어야한다.
[DllImport("User32.dll", SetLastError = true, CharSet = CharSet.Auto)]
public static extern int GetWindowText(IntPtr hWnd, StringBuilder lpWindowText, int nMaxCount);

[DllImport("user32.dll", SetLastError = true)]
public static extern uint GetWindowThreadProcessId(IntPtr hWnd, out uint lpdwProcessId);

[DllImport("User32.dll", SetLastError = true, CharSet = CharSet.Auto)]
public static extern int GetClassName(IntPtr hwnd, StringBuilder lpClassName, int nMaxCount);