본문 바로가기

.NET(C#,ASP)

[C#]프로그램 중복 실행 방지(뮤텍스 사용 안함)

우리는 보통 프로그램 중복 실행을 방지한다고 표현하는데 외국에서는 중복 인스턴스 실행을 방지한다고 표현하나보다. 여기저기 뒤진 끝에 외국 소스를 찾았는데 싱글 인스턴스를 유지하는 방법이라고 써 있었다.

아무튼 프로그램 중복 실행을 방지한다는 것은 뭐 다 알다시피 메모장을 실행시킨 후 다시 메모장을 실행시켜도 이미 열려있는 메모장이 있으면 또 열리지 않는다는 것을 의미한다.

처음에 데브피아나 여러 곳에서 중복실행 방지에 대해서 찾았었는데, 뮤텍스 방법 위주로만 나오길래 무슨 방법인가 했다. 예전에는 프로그램명으로 검색해서 같은게 떠있으면 두 번째 실행을 방지하는 방법을 썼었는데 말이다.

아무튼 뮤텍스가 뭔가 해서 대충 찾아보니(일과시간에 잠깐 짬내서 찾느라고 자세하게는 알아보지 못했지만) 프로그램의 실행상태를 시스템에 알려주고 같은 프로그램을 실행시키면 시스템에 물어봐서 그 프로그램이 실행중인지 따져보고 실행할 지 안할지 판단하는 방법이라고 한다.
그러나 이 방법은 프로그램이 종료될 때 반드시 시스템에 프로그램이 실행중이었다는 사실을 해제해야 하는 것 같았다. 그렇다면 비정상 종료시에는 시스템에 프로그램이 종료되었다는 사실을 알려주지 못할텐데 그 때는 문제가 생길거 같아서 이 방법은 일단 배제하기로 했다.
(다시 알아보니 시스템 커널이 관리하는 뮤텍스라는 놈이 있는데 그놈은 공유자원을 관리하는 놈이란다. 그래서 공유자원을 다 썼으면 반드시 뮤텍스에게 다 썼다고 알려줘야 하는것이다.)

역시 예전에 사용하던 방법이 좋은거같다.

그렇다면 작업관리자에 들어가 있는 실행파일명을 가지고 판단할 것인가, 아니면 작업표시줄에서 보여지는 프로그램 이름(캡션명)을 사용 할 것인가를 정해야 한다.

그러다가 찾아낸 소스가 작업표시줄에 보여지는 이름을 가지고 판단하는 것이었으며 이 소스는 중복 프로그램 실행 시 이미 실행중이 프로그램이 최소화 상태로 있던가 뒤에 숨어있던가 하면 앞으로 꺼내주는 역할까지 해준다. 마침 내가 원하는 소스였다.

소스를 처음 만든 사람의 뜻을 존중해서 그 사람이 사용한 변수명이나 클래스명은 고치지 않았으며 출처도 안에 그대로 두었다.(ㅋㅋ 사실은 귀찮아서 그냥 그대로 사용한건데..)

이렇게 보니 사설이 너무 길었다.
바로  소스 설명에 들어가자면..
ProcessChecker.cs라는 클래스 파일을 하나 만들어서 그 안에서 이미 실행중인 프로그램의 캡션을 조사하여 이미 실행중인지 아닌지 여부를 알려준다.
C# 윈폼 프로그램의 시작점인 Program.cs의 Main에서 ProcessChecker.cs에게 xxx프로그램이 실행중인지 물어보고 실행할지 말지 판단하도록 한다.

먼저 ProcessChecker.cs파일의 소스를 보자면,

using System;
using System.Diagnostics;
using System.Runtime.InteropServices;
using System.Text;
using System.Windows.Forms;

namespace SISMonitor
{
    /// <SUMMARY>
    /// Check running processes for an already-running instance. Implements a simple and
    /// always effective algorithm to find currently running processes with a main window
    /// matching a given substring and focus it.
    /// Combines code written by Lion Shi (MS) and Sam Allen.
    /// 
    /// 사용법:Program.cs파일의 다음 부분에 아래와 같이 소스를 수정한다.
    ///        "Program Window Text"은 폼의 타이틀 이름이다.
    ///        (띄어쓰기가 있는 
    /// [STAThread]
    /// static void Main()
    /// {
    ///    if (ProcessChecker.IsOnlyProcess("Program Window Text"))
    ///    {
    ///        Application.EnableVisualStyles();
    ///        Application.SetCompatibleTextRenderingDefault(false);
    ///        Application.Run(new TextWindow());
    ///    }
    /// }
    /// 
    /// 출처:http://dotnetperls.com/single-instance-windows-form
    /// </SUMMARY>
    static class ProcessChecker
    {
        /// <SUMMARY>
        /// 찾아야 할 캡션
        /// </SUMMARY>
        static string _requiredString;

        /// <SUMMARY>
        /// Contains signatures for C++ DLLs using interop.
        /// </SUMMARY>
        internal static class NativeMethods
        {
            /// <SUMMARY>
            /// 현재 실행중인 윈도우의 상태를 보여준다.
            /// </SUMMARY>
            /// <PARAM name="hWnd"></PARAM>
            /// <PARAM name="nCmdShow"></PARAM>
            /// <RETURNS></RETURNS>
            [DllImport("user32.dll")]
            public static extern bool ShowWindowAsync(IntPtr hWnd, int nCmdShow);

            /// <SUMMARY>
            /// 선택한 윈도우를 뒤에 숨어있었으면 앞으로, 최소화상태였으면 원래상태로 되돌려놓으며 활성화시킨다.
            /// </SUMMARY>
            /// <PARAM name="hWnd"></PARAM>
            /// <RETURNS></RETURNS>
            [DllImport("user32.dll")]
            public static extern bool SetForegroundWindow(IntPtr hWnd);

            /// <SUMMARY>
            /// EnumWindows 함수는 모든 최상위 윈도우를 검색해서 그 핸들을 콜백함수로 전달하되
            /// 모든 윈도우를 다 찾거나 콜백함수가 FALSE를 리턴할 때까지 검색을 계속한다.
            /// 콜백함수는 검색된 윈도우의 핸들을 전달받으므로 모든 윈도우에 대해 모든 작업을 다 할 수 있다.
            /// EnumWindows 함수는 차일드 윈도우는 검색에서 제외된다.
            /// 단 시스템이 생성한 일부 최상위 윈도우는 WS_CHILD 스타일을 가지고 있더라도 예외적으로 검색에 포함된다.
            /// </SUMMARY>
            /// <PARAM name="lpEnumFunc">EnumWindows의 실행 결과를 받아줄 콜백함수이다.
            /// EnumWindows는 이 함수 결과가 false가 될 때까지 계속 윈도우를 검색하게 된다.</PARAM>
            /// <PARAM name="lParam"></PARAM>
            /// <RETURNS></RETURNS>
            [DllImport("user32.dll")]
            public static extern bool EnumWindows(EnumWindowsProcDel lpEnumFunc, Int32 lParam);

            /// <SUMMARY>
            /// HWND 값을 이용하여 프로세스 ID를 알려주는 함수이다.
            /// </SUMMARY>
            /// <PARAM name="hWnd"></PARAM>
            /// <PARAM name="lpdwProcessId"></PARAM>
            /// <RETURNS></RETURNS>
            [DllImport("user32.dll")]
            public static extern int GetWindowThreadProcessId(IntPtr hWnd, ref Int32 lpdwProcessId);

            /// <SUMMARY>
            /// 윈도우의 캡션을 가져온다.
            /// </SUMMARY>
            /// <PARAM name="hWnd"></PARAM>
            /// <PARAM name="lpString"></PARAM>
            /// <PARAM name="nMaxCount"></PARAM>
            /// <RETURNS></RETURNS>
            [DllImport("user32.dll")]
            public static extern int GetWindowText(IntPtr hWnd, StringBuilder lpString, Int32 nMaxCount);

            //윈도우의 상태를 normal로 하게 하는 상수
            public const int SW_SHOWNORMAL = 1;
        }

        /// <SUMMARY>
        /// EnumWindows의 실행 결과를 받아줄 콜백함수이다.
        /// EnumWindows는 이 함수 결과가 false가 될 때까지 계속 윈도우를 검색하게 된다.
        /// </SUMMARY>
        /// <PARAM name="hWnd"></PARAM>
        /// <PARAM name="lParam"></PARAM>
        /// <RETURNS></RETURNS>
        public delegate bool EnumWindowsProcDel(IntPtr hWnd, Int32 lParam);

        /// <SUMMARY>
        /// Perform finding and showing of running window.
        /// 모든 실행중인 윈도우를 검색하며 찾고자 하는 캡션의 윈도우를 발견하면 활성화시킨다.
        /// </SUMMARY>
        /// <RETURNS>Bool, which is important and must be kept to match up
        /// with system call.</RETURNS>
        static private bool EnumWindowsProc(IntPtr hWnd, Int32 lParam)
        {
            int processId = 0;
            NativeMethods.GetWindowThreadProcessId(hWnd, ref processId);

            StringBuilder caption = new StringBuilder(1024);
            NativeMethods.GetWindowText(hWnd, caption, 1024); //방금 검색한 윈도우의 캡션을 가져온다.

            //찾을 윈도우명과 가져온 캡션이 일치한다면,
            if (processId == lParam && (caption.ToString().IndexOf(_requiredString, StringComparison.OrdinalIgnoreCase) != -1))
            {
                //윈도우를 normal 상태로 바꾸고 제일 앞으로 가져온다.
                NativeMethods.ShowWindowAsync(hWnd, NativeMethods.SW_SHOWNORMAL);
                NativeMethods.SetForegroundWindow(hWnd);
            }
            return true; //왜 계속 true만 반환해야 할까???
        }

        /// <SUMMARY>
        /// 지금 실행하려는 프로그램이 이미 실행중인지 아닌지 찾아보고 결과를 알려준다.
        /// </SUMMARY>
        /// <PARAM name="forceTitle">찾으려는 윈도우의 캡션, 즉 프로그램 타이틀</PARAM>
        /// <RETURNS>해당 캡션의 윈도우가 이미 실행중이라면 False,
        /// 처음 실행하는 것이라면 True를 반환한다.</RETURNS>
        static public bool IsOnlyProcess(string forceTitle)
        {
            _requiredString = forceTitle;
            //먼저 실행파일의 이름으로 이름이 같은 프로세스를 검색해본다.
            foreach (Process proc in Process.GetProcessesByName(Application.ProductName))
            {
                if (proc.Id != Process.GetCurrentProcess().Id)
                {
                    NativeMethods.EnumWindows(new EnumWindowsProcDel(EnumWindowsProc), proc.Id);
                    return false;
                }
            }
            return true;
        }
    }
}

아래의 첨부파일의 확장자를 cs로 고친 후 프로그램에 적용시키면 된다.

그리고 소스의 앞부분 주석에 써 놓은 것처럼 C#프로그램의 진입점인 Program.cs 파일에

[STAThread]
static void Main()
{
   if (ProcessChecker.IsOnlyProcess("Program Window Text"))
   {
       Application.EnableVisualStyles();
       Application.SetCompatibleTextRenderingDefault(false);
       Application.Run(new TextWindow());
   }
}

이렇게 소스를 넣으면 잘 작동된다.