. 한 번만 실행하기

윈도우즈용 응용 프로그램은 원칙적으로 여러 번 실행할 수 있다. A.txt B.txt를 동시에 편집하고 싶다면 메모장 두 개를 실행할 수 있으며 당근도 마찬가지로 두 번 실행하는 것이 가능하다. 멀티태스킹이 가능한 운영체제이므로 이것은 지극히 당연한 현상이다. 그러나 프로그램의 특성상 두 개 이상의 인스턴스가 실행될 수 없다거나 가능하다 하더라도 바람직하지 못한 경우가 있다. 예를 들어 이벤트 로그나 SQL 서버 같은 서비스 프로그램은 컴퓨터 한대에 하나의 서비스를 제공하므로 두 번 실행할 필요가 전혀 없다.

MP3 파일을 연주하는 WinAmp같이 독점적인 자원을 사용하는 프로그램도 두 번 실행할 수 없다. 어차피 시스템에 사운드 카드는 하나밖에 없고 설사 복수 개의 소리를 믹싱해서 들려줄 수 있다 하더라도 그 소리를 듣는 사람이 각각의 소리를 분리해서 듣지 못하기 때문이다. 스타크래프트같이 전체 화면을 사용하는 게임도 일반적으로 딱 한 번만 실행될 수 있다.

그렇다면 우리가 지금 만들고 있는 당근 편집기의 경우는 어떨까? 시스템 서비스를 제공하는 것도 아니고 특별히 독점적인 자원을 사용하는 것도 아니므로 여러 번 실행해도 별 무리는 없다. 하지만 한꺼번에 복수 개의 파일을 편집할 수 있는 MDI 프로그램이기 때문에 여러 번 실행하는 것이 별 의미가 없는 것도 사실이다. 오히려 복수 개의 인스턴스를 허용할 경우 잠재적으로 데이터를 잃을 위험이 있다. 두 개의 인스턴스가 같은 파일을 편집하고 있을 경우 서로 이 파일을 덮어쓰게 되므로 먼저 저장한 인스턴스의 편집 결과가 손상될 것이다.

복수 개의 인스턴스가 별로 바람직하지 않지만 그렇다고 꼭 강제로 막아야 할 필요까지는 없다. 이 정책은 사용자의 취향에 따를 문제이므로 옵션으로 제공하는 것이 가장 무난하다. Option구조체의 bAllowMulti 변수가 복수 개의 인스턴스를 허용할 것인가 아닌가를 지정하며 이 값이 FALSE이면 딱 한 번만 실행하도록 한다. TRUE일 경우 별다른 조치를 취할 필요없이 아무것도 하지 않으면 자연스럽게 복수 개의 인스턴스가 실행된다.

한 번만 실행되도록 하는 방법은 아주 간단하다. 실행될 때 이미 실행중인 인스턴스가 있는지 보고 만약 있다면 더 이상 실행하지 말고 종료하면 된다. 결국 이 문제는 같은 프로그램의 이전 인스턴스를 어떻게 찾을 것인가의 문제이다. 가장 쉬운 방법은 FindWindow 함수를 사용하는 것인데 당근은 고정된 윈도우 클래스 이름을 가지고 있으므로 이 방법을 사용할 수 있다. 이외에 파일 매핑이나 뮤텍스같이 시스템 전역적으로 유일한 커널 객체를 이전 인스턴스 식별에 사용할 수 있는데 여기서는 뮤텍스를 사용해보도록 하자. WinMain에 다음 코드를 작성한다.

 

int APIENTRY WinMain(HINSTANCE hInstance,HINSTANCE hPrevInstance

            ,LPSTR lpszCmdParam,int nCmdShow)

{

    HANDLE hMutex;

     ....

    Option.bAllowMulti=SHRegReadInt(SHCU,KEY"Setting","bAllowMulti",0);

    if (Option.bAllowMulti==FALSE) {

#ifdef _DEBUG

        hMutex=CreateMutex(NULL,FALSE,"Dangeun_Mutex_For_Debug");

#else

        hMutex=CreateMutex(NULL,FALSE,"Dangeun_Mutex_For_Allow_Unique_Instance");

#endif

        if (GetLastError() == ERROR_ALREADY_EXISTS) {

           ActivateBrood();

           return 0;

        }

    }

 

     hMenu1=LoadMenu(g_hInst,MAKEINTRESOURCE(IDR_MENU1));

     hMenu2=LoadMenu(g_hInst,MAKEINTRESOURCE(IDR_MENU2));

     ....

    if (hMutex) {

        CloseHandle(hMutex);

    }

     return (int)Message.wParam;

}

 

bAllowMulti FALSE일 때는 전역 뮤텍스를 생성하되 다른 뮤텍스트와 충돌하지 않도록 충분히 긴 이름을 주었다. 이때 뮤텍스가 이미 있다면, 즉 이전 인스턴스가 뮤텍스를 만들어 놓았다면 이 프로그램은 실행되지 말아야 한다. 그래서 아예 메인 윈도우도 만들지 않고 return 0;로 종료하였다. 이 뮤텍스는 메시지 루프가 종료되었을 때 파괴되며 뮤텍스가 파괴된 후에 다른 인스턴스가 실행될 수 있다.

한 번만 실행을 허용하기 위해 뮤텍스를 사용하는 방법은 일반화된 방법이므로 뮤텍스의 정의만 알고 있다면 기법 자체는 별로 어렵지 않을 것이다. 이 코드에는 몇 가지 특이한 점이 있는데 특이점들에 대해서만 연구해보도록 하자. 우선 디버그 버전일 때와 릴리즈 버전일 때의 뮤텍스 이름이 다른데 이는 개발중에는 릴리즈 버전의 당근을 아예 다른 프로그램으로 인식하도록 하기 위해서이다. 이렇게 하지 않으면 당근 개발중에는 당근을 쓸 수 없다는 묘한 제약이 생겨 버린다. 이 코드는 단순히 개발자의 편의를 위해 존재할 뿐 다른 이유는 없다.

다음은 bAllowMulti 옵션을 읽는 시점에 대해 생각해보자. Option 구조체 자체는 OnCreate에서 읽혀지는데 이 옵션은 WinMain의 선두에서 적용하기 때문에 미리 먼저 읽어야 한다. 다른 옵션과는 달리 프로그램 실행을 계속할 것인가 아닌가를 결정하는 워낙 특수한 옵션이라 메인 윈도우가 생성되고 난 다음에 읽어서는 너무 늦다. 잠시 후에 다시 보게 되겠지만 이 옵션을 저장하는 시점도 아주 특이하다.

이미 실행중인 인스턴스가 있을 때는 return 0; WinMain을 종료하되 그 전에 실행중인 인스턴스를 활성화시켜 주도록 했다. 그냥 조용히 종료하면 사용자는 무엇이 잘못되었는지 알 수 없으므로 혼란스러울 것이다. 그래서 자신은 죽지만 대신 자기 형을 소개하도록 했으며 이 일을 하는 함수가 ActivateBrood 함수이다. 이 함수는 유틸리티 함수가 아니므로 Dangeun.cpp에 작성한다.

 

void ActivateBrood()

{

     HWND hBrood;

     ATOM hAtom;

 

     hBrood=FindWindow(lpszClass,NULL);

     if (hBrood==NULL) {

          return;

     }

 

     if (__argc >1 ) {

          hAtom=GlobalAddAtom(__argv[1]);

          SendMessage(hBrood,WM_USER+1,(WPARAM)hAtom,0);

          GlobalDeleteAtom(hAtom);

     } else {

          SendMessage(hBrood,WM_USER+1,0,0);

     }

 

     SetForegroundWindow(hBrood);

}

 

이전 인스턴스의 메인 윈도우를 FindWindow로 검색했다. 윈도우 클래스명이 Dangeun이라는 고유한 이름으로 되어 있으므로 쉽게 메인 윈도우를 찾을 수 있다. 이렇게 찾은 윈도우를 활성화시켜주되 만약 인수가 있다면 이 인수도 전달해야 한다. 예를 들어 첫 번째 인스턴스가 실행중이고 탐색기 팝업메뉴에서 Readme.txt를 열었다면 두 번째 인스턴스가 이 파일명을 받아서 첫 번째 인스턴스에게 대신 열어 달라고 부탁을 하는 것이다.

파일명은 문자열이므로 프로세스끼리 이 문자열을 전달하기 위해서는 IPC를 사용해야 한다. WM_COPYDATA, 파일 매핑, 파이프 등의 여러 가지 IPC 방법이 있지만 여기서는 가장 간단한 글로벌 아톰을 사용했다. 파일명을 글로벌 아톰으로 등록하고 그 핸들을 WM_USER+1 wParam으로 전달했다. 그리고 아톰을 삭제한 후 이전 인스턴스를 활성화시키면 된다. 이 함수가 하는 일을 좀 익살스럽게 표현하자면 우리 형 어디 있나? 여기 있군! 형 나 갑니다. 대신 이 파일을 좀 열어 줘요 하는 꼴이다. 만약 형이 자고 있다면(최소화) 깨어주기까지 한다.

그렇다면 이전 인스턴스인 형은 동생이 전달하는 파일명을 받아 이 파일을 열어야 할 의무가 있다. WM_USER+1 메시지(=형 일어나) 핸들러를 만들고 이 핸들러에서 wParam으로 전달되는 아톰 핸들로부터 파일명을 추출하여 열어 주면 된다. 물론 wParam NULL이면 파일을 열 필요가 없다.

 

LRESULT CALLBACK DGWndProc(HWND hWnd,UINT iMessage,WPARAM wParam,LPARAM lParam)

{

     switch(iMessage) {

          ....

        case WM_USER+1:OnUser1(hWnd,wParam,lParam);return 0;

     }

     return(DefFrameProc(hWnd,g_hMDIClient,iMessage,wParam,lParam));

}

 

void OnUser1(HWND hWnd,WPARAM wParam,LPARAM lParam)

{

     TCHAR Path[MAX_PATH];

 

     if (wParam) {

          GlobalGetAtomName((ATOM)wParam,Path,MAX_PATH);

          OpenFromFile(Path);

     }

     if (IsIconic(hWnd)) {

          ShowWindow(hWnd,SW_RESTORE);

     }

}

 

아톰에 등록된 문자열은 대상 파일의 절대경로이므로 OpenFromFile 함수만 호출하면 된다. 아톰은 현재 인스턴스가 만든 것이 아니므로 여기서 파괴하지 말아야 한다. 만약 자신이 최소화되어 있는 상황이라면 복구하여 사용자가 지시한 파일을 제대로 열었음을 명확히 보여주도록 했다.

bAllowMulti 옵션도 사용자가 취향에 따라 선택할 수 있는 옵션이므로 설정 대화상자에서 값을 변경할 수 있다. 이 옵션이 기록되는 시점은 다른 옵션과는 달리 아주 특이하다. ApplyNow에 다음 코드를 작성하도록 하자.

 

void ApplyNow()

{

     HWND hChild;

     SInfo *pSi;

     HDC hdc;

     LOGFONT tFont;

 

    if (Option.bAllowMulti != NewOption.bAllowMulti) {

        SHRegWriteInt(SHCU,KEY"Setting","bAllowMulti",NewOption.bAllowMulti);

    }

     ....

 

사용자가 이 옵션을 변경했으면 그 즉시 레지스트리에 기록해 버린다. 왜냐하면 이 옵션의 효과는 현재 인스턴스에만 국한되는 것이 아니라 이후부터 실행될 모든 인스턴스가 참조해야 하기 때문이다. 옵션 변경의 효과가 즉시 나타나기 위해서는 Option::Save 함수가 호출될 때까지 기다릴 수 없다. ApplyNow에서 기록을 하므로 Save 함수는 이 옵션을 기록하지도 않는다. 적용 시점, 저장시점, 읽는 시점에 있어서 다른 옵션들과는 정말 많이 다른 특이 옵션이다.