아주 오랜만에 글을 써봅니다.
바쁘다는 핑계로 한 번 두 번 글을 미루다 보니
몇 달이 그냥 흘러가 버리네요.
먼가 장편이나 연재글을 쓴다는게 얼마나 어려운 일인지 조금 느꼈습니다.

(작가분들 존경 스럽습니다. @.@);;;

파일 (폴더도 파일의 일종)의 보안 속성 변경

사실 아래의 코드는 원본 코드를 일부 수정한 것입니다.
그 때 당시 필요해서 이것 저것 자료를 찾아서 만든 것인데, SetFileSecurity API 자체를 잘 이해 못하고
필요한 동작만 빠르게 갖추기 위해서 만든 것이었죠. (사실 그 때는 위 API로 폴더가 되는 줄도 몰랐음)

좀 오래된 자료이긴 하지만, 그 때는 잘 썼는데..
폴더의 보안 속성을 못바꾸는 이유가.. 개념도 없이 잡탕해 놓은 소스라서 그렇다고 하니... -_-;;;
어쨋든 그런 이유로 좀더 보완하여 수정한 코드입니다.


지금 이 코드도 그리 크게 쓸만한 깔끔한 코드는 아닙니다. 단지 파일과 폴더에 특정 유저 권한을

완전히 풀어줄 때 급하게 땜빵하는 용도로는 쓰는데 무리는 없을겁니다.

제대로 만들어 지려면
1. 기존 파일/폴더의 보안 권한 목록을 얻어오고
2. 내가 원하는 사용자가 있는지 검출할 수 있어야 하며
3. 없으면 만들어서 해당 사용자를 추가 할 수 있어야 하고
4. 있으면 해당 권한을 수정해 줄 수 있어야 겠죠.

이 글을 보시는 분들 중에 미리 저러한 기능이 필요해서 만들어 두신게 있으시면..
살짝 리플달아 주세요.. 서로 나눠 먹자구요 ^^;;;






안녕하세요. 아주 오랜만에 글을 쓰게 되었습니다.
인생사 머가 그리 바쁜지, 일은 해도 해도 계속 생기네요 ㅠㅠ;;;

서비스스 뿐만 아니라, 시스템이나 관리자 권한으로 생성한 MMF나 혹은 PIPE를 유저 권한으로
읽거나 하려면 반드시라고 해도 좋을 만큼 권한에 관한 문제가 발생합니다.

   DWORD dwRes;
   PSID pEveryoneSID = NULL, pAdminSID = NULL;
   PACL pACL = NULL;
   PSECURITY_DESCRIPTOR pSD = NULL;
   EXPLICIT_ACCESS ea;
   SID_IDENTIFIER_AUTHORITY SIDAuthWorld SECURITY_WORLD_SID_AUTHORITY;
   SID_IDENTIFIER_AUTHORITY SIDAuthNT = SECURITY_NT_AUTHORITY;

  SECURITY_ATTRIBUTES sa;

   // Create a well-known SID for the Everyone group.
   if(!AllocateAndInitializeSid(&SIDAuthWorld, 1,
                    SECURITY_WORLD_RID,
                    0, 0, 0, 0, 0, 0, 0,
                    &pEveryoneSID))
   {
       printf("AllocateAndInitializeSid Error %u\n", GetLastError());
       goto Cleanup;
   }

   // Initialize an EXPLICIT_ACCESS structure for an ACE.
   // The ACE will allow Everyone read access to the key.
   ZeroMemory(&ea, sizeof(EXPLICIT_ACCESS));
   ea.grfAccessPermissions = GENERIC_READ;
   ea.grfAccessMode = SET_ACCESS;
   ea.grfInheritance= NO_INHERITANCE;
   ea.Trustee.TrusteeForm = TRUSTEE_IS_SID;
   ea.Trustee.TrusteeType = TRUSTEE_IS_USER;
   ea.Trustee.ptstrName  = (LPTSTR) pEveryoneSID;

   // Create a SID for the BUILTIN\Administrators group.
   if(! AllocateAndInitializeSid(&SIDAuthNT, 2,
                    SECURITY_BUILTIN_DOMAIN_RID,
                    DOMAIN_ALIAS_RID_ADMINS,
                    0, 0, 0, 0, 0, 0,
                    &pAdminSID))
   {
       printf("AllocateAndInitializeSid Error %u\n", GetLastError());
       goto Cleanup;
   }


   // Create a new ACL that contains the new ACEs.
   dwRes = SetEntriesInAcl(1, &ea, NULL, &pACL);
   if (ERROR_SUCCESS != dwRes)
   {
       printf("SetEntriesInAcl Error %u\n", GetLastError());
       goto Cleanup;
   }

   // Initialize a security descriptor.
   pSD = (PSECURITY_DESCRIPTOR) LocalAlloc(LPTR, SECURITY_DESCRIPTOR_MIN_LENGTH);
   if (NULL == pSD)
   {
       printf("LocalAlloc Error %u\n", GetLastError());
       goto Cleanup;
   }

   if (!InitializeSecurityDescriptor(pSD, SECURITY_DESCRIPTOR_REVISION))
   {
       printf("InitializeSecurityDescriptor Error %u\n", GetLastError());
       goto Cleanup;
   }

   // Add the ACL to the security descriptor.
   if (!SetSecurityDescriptorDacl(pSD,
           TRUE,     // bDaclPresent flag
           pACL,
           FALSE))   // not a default DACL
   {
       printf("SetSecurityDescriptorDacl Error %u\n",
               GetLastError());
       goto Cleanup;
   }

   // Initialize a security attributes structure.
   sa.nLength = sizeof (SECURITY_ATTRIBUTES);
   sa.lpSecurityDescriptor = pSD;
   sa.bInheritHandle = FALSE;

위의 코드는 보안 속성을 생성하는 코드입니다. MSDN에 나온 샘플 일부를 추려서 만든 코드인데요.
큼지막하게 생긴 애들만 주의해 보시고, 수정해 주시면 원하시는 권한을 풀어줄 수 있습니다.

핸들 = ::CreateFileMapping(파일핸들, &sa, ... 나머지 인자들)...

위 코드의 설명은 Users 그룹에 포함된 사용자에게 Read Only 권한을 주도록 만든 것입니다.

지난 내용
[강좌로 보는 서비스 프로그래밍] 서비스란 무엇인가? [1/?]
[강좌로 보는 서비스 프로그래밍] 외적으로 보여지는 서비스 [2/?]
[강좌로 보는 서비스 프로그래밍] 설치와 제거 [3/?]
[강좌로 보는 서비스 프로그래밍] 가끔 쓸모있는 관련 함수들 [4/?]
[강좌로 보는 서비스 프로그래밍] 시작/중지/일시정지 [5/?]
[강좌로 보는 서비스 프로그래밍] 서비스의 본체 코딩 [6/?]
[강좌로 보는 서비스 프로그래밍] 서비스의 본체 코딩 Cont. [6/?]
[강좌로 보는 서비스 프로그래밍] 뼈다귀 서비스 코드 [7/?]
[강좌로 보는 서비스 프로그래밍] 서비스의 디버깅 [8/?]
[강좌로 보는 서비스 프로그래밍] 내컴에 누가 왔는지 다알아 [9/?]
[강좌로 보는 서비스 프로그래밍] 환영 인사 [10/?]
[강좌로 보는 서비스 프로그래밍] 서비스의 동작 순서 [11/?]
[강좌로 보는 서비스 프로그래밍] 서비스의 종료 [12/?]

 
지금 까지의 진행한 내용을 정리하면서...

요번에 진행할 때는 좀 쓸만한 샘플을 제작해서 올려보려고 하였습니다. 하지만, 지금까지 사용한 샘플코드가 실무에 바로 적용할 수 있을 만큼
제대로 정리가 되지 못했더군요. 플젝만 복사해서 필요할 때 바로 사용할 수 있을 정도의 기본적인 정리를 하고 다음 순서를 진행하고자,
이번 장에서는 사용된 코드를 정리만 하고 넘기도록 하겠습니다. ^^;;;
(참고로 말씀드리면, VS 6.0과 VS2008 로 테스트 되었으며, 프로젝트도 해당 2가지 버전으로 빌드 가능하도록 구성되어 있습니다.)
(그리고, 여전히 땡땡 거리는 서비스입니다. -_-;;;)



프로젝트의 구성...


기본적인 서비스 골격을 가진 main.cpp
실제로 서비스 프로그래밍을 하면서 내부적으로 갖추어야할 기본적인 main() 함수부터 서비스 메인, 서비스 이벤트 핸들러 및 초기 구성에 필요한
모든 내용을 가지고 있는 소스입니다. 소스에는 아래의 6개 함수가 포함되어 있습니다.

main() 콘솔 응용 프로그램의 엔트리 함수 입니다. 전달받은 파라미터를 처리하는 기능을 담당하고, 필요한 경우 도움말을 보여줍니다.
service_main() 서비스에 등록되는 엔트리 함수 입니다. main에서 StartServiceCtrlDispatcher() 함수에 의하여 등록됩니다.
service_handler() 서비스의 이벤트를 받는 함수입니다. service_main 에서 RegisterServiceCtrlHandlerEx 함수에 의하여 등록됩니다.
EventServiceStop() 서비스가 중지되거나 종료될 때 호출되는 이벤트 함수입니다. service_handler 에서 호출됩니다.
EventServicePause() 서비스가 일시 중지될 때 호출되는 이벤트 함수입니다. service_handler 에서 호출됩니다.
EventServiceContinue() 일시중지된 상에서 서비스가 재개될 때 호출되는 이벤트 함수입니다. service_handler 에서 호출됩니다.

서비스 시스템을 관리하는데 필요한 기능을 가진 servicemgr.h 와 servicemgr.cpp
main.cpp 소스는 내부적으로 순수하게 서비스의 기능 구현에 필요한 루틴을 처리한다면, servicemgr 은 실제 서비스를 등록, 제거, 시작, 종료
등과 같은 서비스를 매니징하는데 필요한 모든 기능을 내부적으로 구현하고 있습니다. 해당 기능을 이용하면, services.msc 와 같은 매니저를
이용하지 않고서도 코딩으로 모든 서비스를 제어할 수 있습니다.

서비스에 필요는 하지만, 서비스 자체의 기능과는 먼 유틸리티 serviceutil.h 와 serviceutil.cpp
서비스는 응용 프로그램과는 다른 레이어에서 구동하고, LocalSystem 권한을 가진상태에서 동작하기 때문에, 의외로 제약 사항들이 있습니다.
이러한 부분을 처리하고, 터미널과 같은 상태에서의 동작 처리를 위한 몇몇가지 기능을 가지고 있습니다.

주어진 기능을 테스트하기 위한 코드 readme.txt
위에서 나열된 기능을 하나씩 테스트하기 위하여 제작했던 샘플 코드를 소스에 넣어두니, 상당히 지저분해서... ㅎ~ readme.txt 에 모아 두었습니다.

서비스 매니징을 위한 파리미터 처리 및 도움말 ( -help, -? )
소스를 보시면 아시겠지만, 위 프로젝트로 서비스를 제작하면 설치부터 제거까지 해당 실행파일 내부에 모두 탑재하게 됩니다. 이러한 기능은
작성한 서비스 뿐만 아니라, -srvname 이라는 옵션을 이용하여, 다른 서비스 이름을 옵션으로 전달해주면 시스템에 설치되어 있는 모든 서비스에
그대로 적용되도록 구성되어 있습니다.

만약 불필요한 서비스가 시스템에 존재한다면, 이를 제거할 때 아래과 같이 제거할 수 있습니다.
예) 서비스프로그램.exe -srvname=쓰레기서비스 -uninstall
D:\Personal\Kwon\서비스\SRVAPP\Debug>SRVAPP.exe -srvname=TEST -uninstall
Uninstall: [TEST] Success


중지되어 있는 다른 서비스를 프로그램적으로 시작해야 할경우, 서비스 내부 코드를 이용할 수도 있지만 다음과 같이 가능합니다.
(아래의 예를 system(), ShellExecute(), ShellExecuteEx(), CreateProcess() 등을 사용하여 구동시킬 수 있습니다.)
예) 서비스프로그램.exe -srvname=해당서비스 -start
D:\Personal\Kwon\서비스\SRVAPP\Debug>SRVAPP.exe -srvname=WebClient -start
Start: [WebClient] Success


만약 다른 서비스의 상태를 살펴볼 경우 아래와 같이 수행하여 살펴볼 수 있습니다.
예) 서비스프로그램.exe -srvname=다른서비스 -status
D:\Personal\Kwon\서비스\SRVAPP\Debug>SRVAPP.exe -srvname=Browser -status
Selected Service Status
   Service Name         > Browser
   Service Display Name > Computer Browser
   Service Descriptipn  >
Maintains an updated list of computers on the network and supplies this list to
computers designated as browsers. If this service is stopped, this list will not
 be updated or maintained. If this service is disabled, any services that explic
itly depend on it will fail to start.

   Service Start User   > LocalSystem
   Service Module Path  > C:\WINDOWS\system32\svchost.exe -k netsvcs
   Service Dependency   > LanmanWorkstation
   Service Start Type   > SERVICE_AUTO_START
   Service Run State    > SERVICE_RUNNING




서비스의 구동...

서비스를 처음으로 다루어 보는 분들을 위하여, 현재 만들어져 가고 있는 서비스와 이미 구동되고 있는 서비스들의 동작 방식을 다이어 그램으로
설명해 보려 합니다. 여기서 제시하는 방식은 콘솔에서 실행파일 상태로 바로 실행시켜 볼 수도 있도록 구성되어 있으므로, 기존 서비스와는 조금
다른 로직을 타기는 하지만 전체적인 흐름에 따른 차이는 크지 않습니다. 푸른색으로 명시된 함수명들은 main.cpp 에서 설명된 6개의 함수입니다.



나머지 자세한 사항들은 소스를 참조하시면 될꺼구요, 혹시나 서비스를 구현하는데 따른 문제점이나, 샘플로 제작하기 알맞은 내용등이
있으면 리플남겨 주세요.. ^^;;;
(요즘 서버군이 대부분 64비트머신이라, 테스트도 할겸 플젝내부에 64비트 빌드도 추가로 포함시켰습니다.)

SRVAPP.zip
지난 내용
[강좌로 보는 서비스 프로그래밍] 서비스란 무엇인가? [1/?]
[강좌로 보는 서비스 프로그래밍] 외적으로 보여지는 서비스 [2/?]
[강좌로 보는 서비스 프로그래밍] 설치와 제거 [3/?]
[강좌로 보는 서비스 프로그래밍] 가끔 쓸모있는 관련 함수들 [4/?]
[강좌로 보는 서비스 프로그래밍] 시작/중지/일시정지 [5/?]
[강좌로 보는 서비스 프로그래밍] 서비스의 본체 코딩 [6/?]
[강좌로 보는 서비스 프로그래밍] 서비스의 본체 코딩 Cont. [6/?]
[강좌로 보는 서비스 프로그래밍] 뼈다귀 서비스 코드 [7/?]
[강좌로 보는 서비스 프로그래밍] 서비스의 디버깅 [8/?]
[강좌로 보는 서비스 프로그래밍] 내컴에 누가 왔는지 다알아 [9/?]
[강좌로 보는 서비스 프로그래밍] 환영 인사 [10/?]
[강좌로 보는 서비스 프로그래밍] 서비스의 동작 순서 [11/?]

지난 장에서는 서비스의 동작순서를 개략적으로 살펴보았습니다. 윈도우 2008 서버나, Vista 부터는 좀더 색다른
시작 방식과 안정적인 종료를 지원한다는 것을 공부해 보았는데, 종료 플래그와 관련된 설정이 마무리 되지 않아
정상적으로 종료가 되지를 않더군요. (물론 종료시 처리작업은 마무리가 된듯한...)

서비스의 중지가 아닌, 머신 종료에 따른 서비스가 종료될 때는 다음과 같은 특징이 발견되었네요.
1. SERVICE_CONTROL_SHUTDOWN 와 SERVICE_CONTROL_PRESHUTDOWN 에서 서비스가 정상적으로
   종료되었음을 알리려면 작업이 끝난 뒤에 꼭, SERVICE_STOPPED 상태를 변경 시켜주어야 SCM
   이 정상적으로 
서비스가 종료되었음을 인지하여, 강제로 킬하지 않게 된다.

2. manager.SetPreShutdownInfo(밀리초); 와 같은 형태로 사용할 수 있으며
   위 함수가 작동하려면 SERVICE_CONTROL_PRESHUTDOWN 설정이 미리 되어 있어야 한다.

   그러나, 시간값을 아무리 조절해도 50초를 못넘기고 강제종료된다. 이것을 연장하려면 어떻게 해야할까?..
   MSDN에서는 기본값이 3분이라는데.. 무슨짓을 해도 50초 이상을 못넘긴다. 왜일까?
   (이 문제는 차근 차근 알아보자.. msdn에도 google 에도 더이상의 자료는 찾지 못하였다.)

그리고, 다음으로 이전에는 서비스가 이벤트를 모두 받을 수 있도록 기본값으로 설정하였는데
    manager.SetServiceEventAcceptor(SERVICE_ACCEPT_STOP, TRUE);
    manager.SetServiceEventAcceptor(SERVICE_ACCEPT_PAUSE_CONTINUE, TRUE);
    manager.SetServiceEventAcceptor(SERVICE_ACCEPT_SHUTDOWN, TRUE);
    manager.SetServiceEventAcceptor(SERVICE_ACCEPT_PARAMCHANGE, TRUE);
    manager.SetServiceEventAcceptor(SERVICE_ACCEPT_NETBINDCHANGE, TRUE);
    manager.SetServiceEventAcceptor(SERVICE_ACCEPT_HARDWAREPROFILECHANGE, TRUE);
    manager.SetServiceEventAcceptor(SERVICE_ACCEPT_POWEREVENT, TRUE);
    manager.SetServiceEventAcceptor(SERVICE_ACCEPT_SESSIONCHANGE, TRUE);
    manager.SetServiceEventAcceptor(SERVICE_ACCEPT_PRESHUTDOWN, TRUE);
위에 코드 처럼 SetServiceEventAcceptor 라고 하는 함수를 추가하여, 원하는 이벤트를 임으로 추가할 수 있도록 별도로 처리하여 놓았다.

만약 SERVICE_ACCEPT_STOP 을 추가하여 놓지 않은 상태에서 서비스를 시작 시키면, 서비스 매니저에 중지 버튼이 비활성화 되어
프로세스를 킬 하지 않는 이상 서비스를 중지 시킬 수 없도록
할 수 도 있다.
같은 이야기로 SERVICE_ACCEPT_PAUSE_CONTINUE 를 빼 놓으면, 일시중지 버튼이 비활성화 된다.

아래는 최종 수정된 서비스의 메인 함수이다.
int service_main(INT ARGC, LPSTR* ARGV)
{
    CServiceManager manager;
    CServiceUtility utility;
    if(utility.IsServiceMode() == TRUE)
    {
        manager.SetServiceHandler(RegisterServiceCtrlHandlerEx(
            CServiceManager::m_InstallParam.lpServiceName, service_handler, NULL));

        if (manager.GetServiceHandler() == NULL)
            return 0;
    }
    else
    {
        printf("run debug mode\npress any key close...\n");
    }


    manager.SetServiceRunStatus(SERVICE_START_PENDING);

    manager.SetServiceEventAcceptor(SERVICE_ACCEPT_STOP, TRUE);
    manager.SetServiceEventAcceptor(SERVICE_ACCEPT_PAUSE_CONTINUE, TRUE);
    manager.SetServiceEventAcceptor(SERVICE_ACCEPT_SHUTDOWN, TRUE);
    manager.SetServiceEventAcceptor(SERVICE_ACCEPT_PARAMCHANGE, TRUE);
    manager.SetServiceEventAcceptor(SERVICE_ACCEPT_NETBINDCHANGE, TRUE);
    manager.SetServiceEventAcceptor(SERVICE_ACCEPT_HARDWAREPROFILECHANGE, TRUE);
    manager.SetServiceEventAcceptor(SERVICE_ACCEPT_POWEREVENT, TRUE);
    manager.SetServiceEventAcceptor(SERVICE_ACCEPT_SESSIONCHANGE, TRUE);
    manager.SetServiceEventAcceptor(SERVICE_ACCEPT_PRESHUTDOWN, TRUE);

    utility.ProcessPrivileges(GetCurrentProcess(), "SeShutdownPrivilege", TRUE);
    utility.ProcessPrivileges(GetCurrentProcess(), "SeDebugPrivilege", TRUE);

    manager.SetPreShutdownInfo(180000);

    // start ok, i'm ready receive event
    manager.SetServiceRunStatus(SERVICE_RUNNING);


   // 무한 루프를 돌면서 띵띵~ 소리를 낸다.
   while(manager.GetServiceRunStatus() != SERVICE_STOPPED)
   {
       if(manager.GetServiceRunStatus() == SERVICE_PAUSED)
       {
           Sleep(1000);
           continue;
       }

        if(utility.IsServiceMode() == FALSE && _kbhit())
        {
            printf("stop debug mode\n");
            break;
        }

        ::MessageBeep(0xFFFFFFFF);
        printf("ting...\n");
        Sleep(1000);
   }

   return 0;
}

아래는 최종 수정된 서비스의 이벤트 핸들러 이다.
DWORD WINAPI service_handler(DWORD fdwControl, DWORD dwEventType, LPVOID lpEventData, LPVOID lpContext)
{
    CServiceManager manager;
    switch (fdwControl)
    {
        // some event code.
  
  case bla bla bla ...:
       break;

        // 윈도우에서 세션에 변경 사항이 발생하였을 경우, 발생하는 이벤트.
    case SERVICE_CONTROL_SHUTDOWN:
        manager.SetServiceRunStatus(SERVICE_STOP_PENDING);
        for(int i=0; i<30; i++)
        {
            Sleep(1000);
            PRTTIME(FileLog);
        }
        manager.SetServiceRunStatus(SERVICE_STOPPED);
        break;

        // 윈도우에서 세션에 변경 사항이 발생하였을 경우, 발생하는 이벤트.
    case SERVICE_CONTROL_PRESHUTDOWN:
        manager.SetServiceRunStatus(SERVICE_STOP_PENDING);
        for(int i=0; i<30; i++)
        {
            Sleep(1000);
            PRTTIME(FileLog);
        }
        manager.SetServiceRunStatus(SERVICE_STOPPED);
        break;

    default
:
       break;
   }

    return NO_ERROR;
}

서비스의 종료 이벤트시 지연시간을 원하는 만큼 충분히 늘여보고자 하였는데... 제대로 마무리가 되지 못하였다.
(기왕 만들어주는거 어디서 어떻게 호출하면, 그리고 어떤 권한이 필요한지 설명을 달아주면 좋을 텐데.. 전무하다 -_-;;;)

나름 대로 열심히 테스트를 진행하고, 자료를 찾아보았지만... 마무리가 않되는 군요..
이거 괜히 건드려서 일만 키운게 아닌지..
(혹시 이쪽 다뤄보신분 있으시면.. 리플좀..  ~~)


아직까지 아무 기능없는 땡땡~ 서비스 이지만 다음에는 먼가 기능을 넣어 보도록 하겠습니다.

지난 내용
[강좌로 보는 서비스 프로그래밍] 서비스란 무엇인가? [1/?]
[강좌로 보는 서비스 프로그래밍] 외적으로 보여지는 서비스 [2/?]
[강좌로 보는 서비스 프로그래밍] 설치와 제거 [3/?]
[강좌로 보는 서비스 프로그래밍] 가끔 쓸모있는 관련 함수들 [4/?]
[강좌로 보는 서비스 프로그래밍] 시작/중지/일시정지 [5/?]
[강좌로 보는 서비스 프로그래밍] 서비스의 본체 코딩 [6/?]
[강좌로 보는 서비스 프로그래밍] 서비스의 본체 코딩 Cont. [6/?]
[강좌로 보는 서비스 프로그래밍] 뼈다귀 서비스 코드 [7/?]
[강좌로 보는 서비스 프로그래밍] 서비스의 디버깅 [8/?]
[강좌로 보는 서비스 프로그래밍] 내컴에 누가 왔는지 다알아 [9/?]
[강좌로 보는 서비스 프로그래밍] 환영 인사 [10/?

서비스로 좀더 깊게 들어가기 이전에, 컴퓨터가 켜져서 꺼질때까지 서비스가 어떻게 동작하는지 알아보자.

서비스는 OS에 따라 동작 순서와 특성에 따른 차이가 존재한다.
Windows 2000 및 95, 98 등에 관해서는 다루지 않을 것이며, 최소 Windows XP 를 기준으로 다룬다.

Windows XP와 Windows Server 2003은 _WIN32_WINNT 의 버전 표기가 0x0500 부터 시작한다.
Windows Vista, Windiow 7, Windows 2008은 표기가 0x0600 부터 시작한다.
이렇게 두가지로 나누어서 동작을 살펴보고자 한다. XP나 2003을 구형 OS, 그 이상을 신형 OS 라고 표기할것이다.

동작은 전체적으로 머신 부팅 -> 서비스 로딩 -> 콘솔세션 로그온 -> 머신 종료 순으로 다룰 것이다.
(로그온 과정이 빠지면 아래중에 로그온과 로그오프는 배제될 것이다.)
그리고, 동작 순서의 표기는 서비스가 받을 수 있는 메시지를 기준으로 표기한다.

왼쪽은 구형 OS이고, 오른쪽은 신형 OS의 동작 순서이다.



시작하는 거야, 컴퓨터가 켜질때 같이 시작하거나 ( StartAuto), 나중에 사용자가 켜주거나 (Manual)
하는 방식의 차이만 있을뿐, 무언가가 서비스를 시작 시키면 전혀 차이점이 존해하지 않는다.

하지만, 종료하는 것에는 차이가 있다.

위의 그림을 살펴보면 SERVICE_CONTROL_STOP 이 않보인다. 서비스 매니저를 이용해서 서비스를 중지 시키면
SERVICE_CONTROL_STOP 메시지가 전달되는데, 시스템이 종료될 때는 발생을 하지 않는것이다.
어짜피 시스템이 종료되기 때문에, 서비스의 정상종료 여부에 크게 관여치 않는 경우는 문제가 없겠지만
서비스가 종료될 때 중요한 정보를 저장해야 하거나 어딘가에 통보해야 하거나 할 경우, 정확한 종료 시점에
필요한 작업을 할수 있는 충분한 시간을 확보해야 한다면 난감해 질 수 있다.

우선 살펴본바와 같이 서비스는 아래의 3개의 이벤트를 받으면 종료되는 것으로 인식하면 된다.
그리고, 아래의 이벤트는 절대 종료시점에 중복되어 호출되지 않으므로, 겹치는 문제는 생각하지 않아도 된다.

1. SERVICE_CONTROL_STOP
    서비스가 SERVICE_ACCEPT_STOP 플래그를 가지고 있어야 한다.
    사용자나 특정 프로그램에 의하여 서비스를 임의로 중지 시켰을 경우 받는 메시지이다.

    당연히 프로세스를 강제로 킬하거나 하면 발생하지 않는다.
    머신이 종료될 때는 이 이벤트는 발생하지 않는다.
    이 이벤트 핸들러에서는 아무리 많은 시간을 잡아먹어도  OS가 불평하지 안는다. 관여도 하지 않는다.
    (단 종료를 오래할 경우 외부에서 보기 좋도록 SERVICE_STOP_PENDING 상태만 출력해주면 된다.)

2. SERVICE_CONTROL_SHUTDOWN
    서비스가 SERVICE_ACCEPT_SHUTDOWN 플래그를 가지고 있어야 한다.
    하위나, 상위 OS 모두 지원하는 이벤트로, 특별한 일이 없으면 머신이 종료될 때 저 이벤트를 발생시킨다.
    서비스가 종료되거나, 머신이 꺼질 때 먼가 해야하는 작업이 있다면 저 이벤트 핸들러에서 처리한다.
    (즉, SERVICE_CONTROL_STOP 서 하는 작업을 이 이벤트에서도 처리해 주어야 한다.)
    현재 머신이 종료중에 있기 때문에 여기서는 오랜시간 작업을 할 수 없다. 대략 20초 정도가 최대인데
    시스템 상황에 따라 다소 차이가 나기는 하지만 30초를 넘기지 못한다. 시간이 지나면 강제로 Kill 된다.   

3. SERVICE_CONTROL_PRESHUTDOWN (상위 OS 부터 지원 됨)
    서비스가 SERVICE_ACCEPT_PRESHUTDOWN 플래그를 가지고 있어야 한다.
    상위 OS 부터 지원되기 시작한 이벤트이다. 시스템이 종료에 들어가자 마자 저 이벤트 핸들러를 등록해 놓은

    서비스들 부터 먼저 이벤트를 보낸다. 즉, SERVICE_CONTROL_SHUTDOWN 만 등록해 놓은 것들은 
    저것들이 다 스스로 혹은 강제로 끝나야 호출되기 시작한다.
    내가 작업을 진행하는데 오래걸리면, 시간이 오래걸린다고 지연시간을 시스템에 요청할 수 있으며
    해당 시스템은 저 요청을 받으면 해당 서비스가 종료중임을 알리고, 강제 종료를 지연 시킨다
.


신형 OS에 해당하는 Windows Vista, Windows Server 2008, Windows 7은 서비스의 동작 상태가 몇가지 추가되었다.
위의 내용과 연계하여 추가된 내용을 살펴보면 다음과 같다.

1. Delayed AutoStart Service
   보통은 서비스가 시작될 때, 커널이 올라온후, 적당한 디펜던시에 의해 순서가 정해지고 서비스란 서비스는
   몽땅 한꺼번에 올라온다. 이렇게 되면 초기 시작되는 시점에 상당한 부하가 발생하고 부팅 시간이 길어진다.

   저 자동 시작 지연 기능을 이용하면, 지연되지 않은 서비스들이 모두 올라오고 대략적인 부팅이 마무리된 후
   10 - 30 초 정도 지연된 이후에 서비스가 시작된다.
   (MSDN 설명을 살펴보면 스레드 우선순위를 낮추었다가, 나중에 서비스가 시작되면 순위를 복원한다는 표현을 사용한다.)

   해당 기능은 매니저.SetServiceStartType(SERVICE_DELAYED); 와같이 사용할 수 있다.

2. Clear Service Shutdown
   서비스를 중지 시킬경우는 SERVICE_CONTROL_STOP 를 받아서 정상적인 종료 처리를 충분한 시간 동안 할 수 있다.
   하지만 머신이 종료될 경우는 SERVICE_CONTROL_STOP 는 받지 못하고, SERVICE_CONTROL_SHUTDOWN 를
   받게 되는데, 만약 서비스 종료시 디스크에 먼가를 기록하는등 많은 시간을 소요하는 작업을 진행한다면...
   종료 작업중에 머신이 강제 종료될 것이다. 대략 디폴트로 주는 시간이 20초 정도 되고, 넘어가면 강제 종료 당한다.

   서비스의 상태를 변경할 때 SERVICE_ACCEPT_PRESHUTDOWN 를 SERVICE_STATUS::dwControlsAccepted 에
   추가하는 작업으로 SERVICE_CONTROL_PRESHUTDOWN 이벤트를 받을 수 있게된다.
해당 이벤트를 받으면
   시스템의 화면이 다음과 같이 변경된다.
   이 때는 SERVICE_CONTROL_SHUTDOWN 는 더이상 받지 않게된다.
  

   지연시간이 기본적으로 3분이라고 하는데, 서비스가 특정작업을 진행하지 않으면 보통 1분 정도에 강제 종료된다.
   해당 지연시간을 설정하기 위하여 매니저.SetPreShutdownInfo(지연시간 /*밀리초*/); 를 지원한다.
   해당 함수에 0을 넣으면 비활성화 시키고, 값을 입력하면 활성화 시키도록 설계 되었다.

3. Service Shutdown Order
   세번째로 서비스를 종료시킬 때, 어떤 순서대로 종료시킬 지를 설정할 수 있는 방법을 제공한다.
   보통 디펜던시가 걸려있을 경우는 해당 순서대로 처리가 되지만, 그렇지 못할 경우는 무작위로 종료가 되는데...
   이를 좀더 개선된 방식으로 지원해주는 것이다. (보통 디펜던시를 걸게되면 까다로운 작업이 생긴다. ==;;;)
 

  위의 그림처처럼 해당 레지스트리키에 서비스의 이름을 기록하게 되면, 저 순서대로 종료된다.
  서비스의 종료 순서에 영향을 받을 경우, 2번과 병행하여 처리하면 안전하게 서비스를 종료할 수 있다.
  (해당 작업은 단순히 레지스트리에 기입하는 방식이므로 별도의 함수를 제공하지는 않는다.)

아래는 추가된 함수의 목록이다. 물론 위에서 나열된 기능은 신형 OS 에서만 동작한다.
    // change start : auto, demand, disable, boot, system, delayed
    // if select delayed: windows vista and server 2008 over
    /*  #define SERVICE_BOOT_START 0x0000
        #define SERVICE_SYSTEM_START            0x0001
        #define SERVICE_AUTO_START              0x0002
        #define SERVICE_DEMAND_START            0x0003
        #define SERVICE_DISABLED                0x0004
        #define SERVICE_DELAYED                 0x0010 */

    DWORD SetServiceStartType(DWORD dwControl);

    /*
        typedef struct _QUERY_SERVICE_CONFIGA {
            DWORD   dwServiceType;
            DWORD   dwStartType;
            DWORD   dwErrorControl;
            LPSTR   lpBinaryPathName;
            LPSTR   lpLoadOrderGroup;
            DWORD   dwTagId;
            LPSTR   lpDependencies;
            LPSTR   lpServiceStartName;
            LPSTR   lpDisplayName;
    } QUERY_SERVICE_CONFIGA, LLPQUERY_SERVICE_CONFIGA; */

    DWORD GetServiceConfig(QUERY_SERVICE_CONFIG*& pConfig);
    VOID FreeServiceConfig(QUERY_SERVICE_CONFIG*& pConfig);

    // change service desc string
    DWORD SetServiceDescription(LPTSTR lpszStr);

    // change service startup user
    // user : SetServiceUser(".\\aaa", "aaa");
    // system : SetServiceUser(".\\LocalSystem", NULL);
    DWORD SetServiceUser(LPTSTR lpszUser, LPTSTR lpszPass);

    // change interact mode
    DWORD SetServiceInteractMode(BOOL bActive);

    // this value enabled, service receive SERVICE_CONTROL_PRESHUTDOWN message
    // and, get 3 (default) minutes wired.
    DWORD SetPreShutdownInfo(DWORD dwPreshutdownTimeout /* ms <default:18000> */);

토요일이네요.. 주말 즐겁게 보내세요.. ^^;;;








지난 내용
[강좌로 보는 서비스 프로그래밍] 서비스란 무엇인가? [1/?]
[강좌로 보는 서비스 프로그래밍] 외적으로 보여지는 서비스 [2/?]
[강좌로 보는 서비스 프로그래밍] 설치와 제거 [3/?]
[강좌로 보는 서비스 프로그래밍] 가끔 쓸모있는 관련 함수들 [4/?]
[강좌로 보는 서비스 프로그래밍] 시작/중지/일시정지 [5/?]
[강좌로 보는 서비스 프로그래밍] 서비스의 본체 코딩 [6/?]
[강좌로 보는 서비스 프로그래밍] 서비스의 본체 코딩 Cont. [6/?]
[강좌로 보는 서비스 프로그래밍] 뼈다귀 서비스 코드 [7/?]
[강좌로 보는 서비스 프로그래밍] 서비스의 디버깅 [8/?]
[강좌로 보는 서비스 프로그래밍] 내컴에 누가 왔는지 다알아 [9/?]

지난장에서는 SERVICE_CONTROL_SESSIONCHANGE 를 받아서, 어떠한 이벤트가 떨어지는지 그리고
그러한 정보를 추출해서 필요한 로그를 남기는 작업을 진행하여 보았다.

그렇다면, 이번에는 누군가 컴퓨터에 로그인을 하면, 해당 정보를 출력해주는 환영인사를 남겨보자.
아래 그림은 MessageBox.exe 라는 유틸성 다이알로그를 하나 만들어 놓고, 사용자가 로그인을 하면 해당 정보를
추출하여, 메시지 박스를 띄워 주도록 구성되었다.


동작을 그려보면 다음과 같다.


대충 보면, 윈도우 시작프로그램에 MessageBox.exe를 등록해 놓고 뿌려주는것과 기능은 같다고 할 수 있겠지만
실제로 구현하는 레벨이 좀 다르다. 아무튼 여기서는 CreateProcessToDesktop 라고 하는 유틸성 함수를 제공해
주기 때문에 아래처럼 등록만 해주면, 서비스에서도  GUI 를 가진 프로그램을 원하는 세션에 띄워줄 수 있다.
DWORD WINAPI service_handler(DWORD fdwControl, DWORD dwEventType, LPVOID lpEventData, LPVOID lpContext)
{
   CServiceManager manager;

#ifdef _DEBUG
    if(fdwControl != SERVICE_CONTROL_INTERROGATE)
    {
        fprintf(FileLog, "EVENT> %s\n", stname[fdwControl].text);
        FileLog.flush();
    }
#endif

    switch (fdwControl)
   {
   case SERVICE_CONTROL_PAUSE:
       manager.SetServiceRunStatus(SERVICE_PAUSE_PENDING);
        // 서비스를 일시 중지 시킨다.
       manager.SetServiceRunStatus(SERVICE_PAUSED);
       break;

   case SERVICE_CONTROL_CONTINUE:
       manager.SetServiceRunStatus(SERVICE_CONTINUE_PENDING);
       // 일시 중지 시킨 서비스를 재개한다.
       manager.SetServiceRunStatus(SERVICE_RUNNING);
       break;

   case SERVICE_CONTROL_STOP:
       manager.SetServiceRunStatus(SERVICE_STOP_PENDING);
       // 서비스를 멈춘다 (즉, 종료와 같은 의미)
       // 서비스를 종료하면, service_main 는 절대로 리턴하지 않는다.
       // 그러므로 해제하려면 작업이 있으면 모든것을 이곳에서 처리한다.
       manager.SetServiceRunStatus(SERVICE_STOPPED);
       break;

        // 윈도우에서 세션에 변경 사항이 발생하였을 경우, 발생하는 이벤트.
    case SERVICE_CONTROL_SESSIONCHANGE:
        {
            // 세션에 관련된 이벤트 정보는 아래의 구조체에 복사하여 사용한다.
            WTSSESSION_NOTIFICATION wtsnoti = {0};
            memcpy(&wtsnoti, lpEventData, sizeof(WTSSESSION_NOTIFICATION));

            CServiceUtility::CWTSSession ws(WTS_CURRENT_SERVER_HANDLE, wtsnoti.dwSessionId);
#ifdef _DEBUG
            fprintf(FileLog, " WTS[%s <%d>] => IP(%s), Client(%s), Domain(%s), User(%s), WinStation(%s)\n",
                wtsname[dwEventType].text,
                wtsnoti.dwSessionId,
                ws.IPAddress,
                ws.ClientName,
                ws.DomainName,
                ws.UserName,
                ws.WinStation);
            FileLog.flush();
#endif

            if(dwEventType == WTS_SESSION_LOGON)
            {
                char Path[MAX_PATH] = {0}, Message[MAX_PATH] = {0};
                strcpy(Path, manager.m_InstallParam.lpBinaryPathName);
                GetClearModulePath(Path);
                strcat(Path, "MessageBox.exe");
                sprintf(Message, "%s 에서 오신 %s님\n\n반갑습니다.", ws.IPAddress, ws.UserName);

                CServiceUtility su;
                STARTUPINFO si = {0};
                si.cb = sizeof(STARTUPINFO);
                PROCESS_INFORMATION pi = {0};
                Sleep(1000);
                su.CreateProcessToDesktop(Path, Message, si, pi, wtsnoti.dwSessionId);
            }
        }

   default:
       break;
   }

    return NO_ERROR;
}

추가로, CreateProcessToDesktop 함수가 좀더 깔끔하고 단순하게 변경되었다. 기존에 만들 때는 이것 저것 끼워 맞추어서 모양도
동작상태도 깔끔하지 못했는데.... 이번에는 좀 제대로 나왔다.
BOOL CServiceUtility::CreateProcessToDesktop(char* pszExecute, char* args, STARTUPINFO& si, PROCESS_INFORMATION& pi, UINT SessionID)
{
    BOOL bRet = FALSE;
    HANDLE hTokenNew = NULL, hTokenDup = NULL;
    DWORD dwCreationFlag = NORMAL_PRIORITY_CLASS | CREATE_NEW_CONSOLE;
    void* EnvBlock = NULL;

       // 해당 세션의 유저 토큰을 아래 함수로 바로 가져올 수 있엇다.
    if(WTSQueryUserToken(SessionID, &hTokenNew) == FALSE)
        return FALSE;

   if(DuplicateTokenEx(hTokenNew,MAXIMUM_ALLOWED,NULL,SecurityIdentification,TokenPrimary,&hTokenDup) == NULL)
    {
        CloseHandle(hTokenNew);
        return FALSE;
    }

    CloseHandle(hTokenNew);
    if(CreateEnvironmentBlock(&EnvBlock, hTokenDup, FALSE))
        dwCreationFlag |= CREATE_UNICODE_ENVIRONMENT;

    bRet = ::CreateProcessAsUser(hTokenDup,
        pszExecute,
        args,
        NULL,
        NULL,
        FALSE,
        dwCreationFlag,
        EnvBlock,
        NULL,
        &si,
        &pi);

    if(dwCreationFlag | CREATE_UNICODE_ENVIRONMENT)
        DestroyEnvironmentBlock(EnvBlock);
    CloseHandle(hTokenDup);
    return bRet;
}

오늘이 벌써 금요일 이네요.. 평안한 주말 보내세요.. ^^;;;
해당 업무와 연관된 작업을 하는 관계로, FreeTDS Downloads (C libraries) 를 빌드 할 일이 생겼네요..
다운로드는 http://www.freetds.org/software.html 에서 받으시면 되고요..

리눅스 계열에서 오픈소스 컴파일 안해보신 분들이라면, 저렇게 오픈된 소스가 있어도 빌드해서 사용하는데
상당한 어려움들이 존재합니다.

또한, 요기 개발자들이 윈도우 지원에 관해서는 거의 신경을 않쓰기 때문에, 문서대로 한다고 빌드가 되는것도 아니고..
실제로 정상적인 빌드가 되도록 작성하는 것은 문서 내용으로 꽤 많아지기 때문에...
빌드가 되도록 수정한 버전을 통째로 올리겠습니다.

설치경로\freetds-stable\win32\msvc6 에 보시면 다음과 같이 라이브러리 프로젝트가 4개 있습니다.
원래는 3개만 있는데, 하나는 만들어서 넣은 것입니다. (ctlib 프로젝트)


FreeTDS.dsw 를 여시면 저렇게 4개의 플젝이 있는데, 배치빌드로 몽땅 돌리시면 해당 경로에 lib와 dll 이 생성됩니다.
FreeTDS 를 제외한 나머지는 스태틱 빌드이므로, lib만 생성됩니다.

아래는 리눅스용으로 올라와 있는 샘플테스트 소스인데, 요놈을 윈도우에서 구동 가능하도록 프로젝트로 만들었습니다.


라이브러리를 모두 빌드 하신후, lib 파일과 dll을 dyntest 플젝이 있는 폴더에 복사해 넣으시고, 해당 dyntest 를
바로 디버그 모드로 테스팅 가능합니다.

좋은 하루 되세요.. ^^;;;

지난 내용
[강좌로 보는 서비스 프로그래밍] 서비스란 무엇인가? [1/?]
[강좌로 보는 서비스 프로그래밍] 외적으로 보여지는 서비스 [2/?]
[강좌로 보는 서비스 프로그래밍] 설치와 제거 [3/?]
[강좌로 보는 서비스 프로그래밍] 가끔 쓸모있는 관련 함수들 [4/?]
[강좌로 보는 서비스 프로그래밍] 시작/중지/일시정지 [5/?]
[강좌로 보는 서비스 프로그래밍] 서비스의 본체 코딩 [6/?]
[강좌로 보는 서비스 프로그래밍] 서비스의 본체 코딩 Cont. [6/?]
[강좌로 보는 서비스 프로그래밍] 뼈다귀 서비스 코드 [7/?]
[강좌로 보는 서비스 프로그래밍] 서비스의 디버깅 [8/?]

이번에는 서비스 메인에 등록된 이벤트 핸들러에 대하여 진행해 보고자 한다.
서비스에서 제공 받을 수 있는 이벤트의 목록은 다음과 같다.
#define SERVICE_CONTROL_STOP                   0x00000001
#define SERVICE_CONTROL_PAUSE                  0x00000002
#define SERVICE_CONTROL_CONTINUE               0x00000003
#define SERVICE_CONTROL_INTERROGATE            0x00000004
#define SERVICE_CONTROL_SHUTDOWN               0x00000005
#define SERVICE_CONTROL_PARAMCHANGE            0x00000006
#define SERVICE_CONTROL_NETBINDADD             0x00000007
#define SERVICE_CONTROL_NETBINDREMOVE          0x00000008
#define SERVICE_CONTROL_NETBINDENABLE          0x00000009
#define SERVICE_CONTROL_NETBINDDISABLE         0x0000000A
#define SERVICE_CONTROL_DEVICEEVENT            0x0000000B
#define SERVICE_CONTROL_HARDWAREPROFILECHANGE  0x0000000C
#define SERVICE_CONTROL_POWEREVENT             0x0000000D
#define SERVICE_CONTROL_SESSIONCHANGE          0x0000000E

오늘은 그 중에서 SERVICE_CONTROL_SESSIONCHANGE 이벤트에 대하여 알아보자.
위의 이벤트는 컴퓨터에서 발생하는 세션의 변화를 감지하여 알려준다. 세션의 변화라는 것은 내 컴터에 터미널로 접속하는것
혹은 로그인, 로그오프... 등등등... 현재 모니터링 되고 있는 대상 컴퓨터에 접속정보를 알려주는 것이다.

윈도우즈의 이벤트 뷰어 (eventvwr.msc) 를 열어보면 나오는 보안 페이지 (security ) 의 정보와 같은 것을 해당 서비스에서
실시간으로 받을 수 있고, 그런 정보를 분석하여 필요한 추가 작업을 진행해 줄 수 있다.


아래의 구조체는 제공되는 이벤트를 문자열로 출력해 보기 위하여 정의 해놓은 부분이다.
////////////////////////////////////////////////////////////////////
// 아래의 구조체는 서비스에서 제공받는 이벤트를 시각적으로 출력할 때

// 사용하기 위하여 이벤트와 이름을 매핑하여 놓은 것이다.
////////////////////////////////////////////////////////////////////
typedef struct _STNAME
{
    const char* text;
    DWORD id;
} STNAME;

STNAME stname[] = {
    {"",                                      0x00000000},
    {"SERVICE_CONTROL_STOP",                  0x00000001},
    {"SERVICE_CONTROL_PAUSE",                 0x00000002},
    {"SERVICE_CONTROL_CONTINUE",              0x00000003},
    {"SERVICE_CONTROL_INTERROGATE",           0x00000004},
    {"SERVICE_CONTROL_SHUTDOWN",              0x00000005},
    {"SERVICE_CONTROL_PARAMCHANGE",           0x00000006},
    {"SERVICE_CONTROL_NETBINDADD",            0x00000007},
    {"SERVICE_CONTROL_NETBINDREMOVE",         0x00000008},
    {"SERVICE_CONTROL_NETBINDENABLE",         0x00000009},
    {"SERVICE_CONTROL_NETBINDDISABLE",        0x0000000A},
    {"SERVICE_CONTROL_DEVICEEVENT",           0x0000000B},
    {"SERVICE_CONTROL_HARDWAREPROFILECHANGE", 0x0000000C},
    {"SERVICE_CONTROL_POWEREVENT",            0x0000000D},
    {"SERVICE_CONTROL_SESSIONCHANGE",         0x0000000E},
    {"SERVICE_CONTROL_PRESHUTDOWN",           0x0000000F},
};

// 세션 체인지 이벤트에서는 하부 이벤트 타입을 아래 처럼 제공해준다.
STNAME wtsname[] = {
    {"",                                     0x00000000},
    {"CONSOLE_CONNECT",                      0x00000001},
    {"CONSOLE_DISCONNECT",                   0x00000002},
    {"REMOTE_CONNECT",                       0x00000003},
    {"REMOTE_DISCONNECT",                    0x00000004},
    {"LOGON",                                0x00000005},
    {"LOGOFF",                               0x00000006},
    {"LOCK",                                 0x00000007},
    {"UNLOCK",                               0x00000008},
    {"SESSION_REMOTE_CONTROL",               0x00000009},
};

다음은 이벤트 핸들러에 해당 이벤트처리 부분을 추가한 것이다.
CServiceUtility::CSimpleLog FileLog("C:\\srv.log");

DWORD WINAPI service_handler(DWORD fdwControl, DWORD dwEventType, LPVOID lpEventData, LPVOID lpContext)
{
   CServiceManager manager;

#ifdef _DEBUG
    if(fdwControl != SERVICE_CONTROL_INTERROGATE)
    {
        fprintf(FileLog, "EVENT> %s\n", stname[fdwControl].text);
        FileLog.flush();
    }
#endif

   switch (fdwControl)
   {
   case SERVICE_CONTROL_PAUSE:
       manager.SetServiceRunStatus(SERVICE_PAUSE_PENDING);
        // 서비스를 일시 중지 시킨다.
       manager.SetServiceRunStatus(SERVICE_PAUSED);
       break;

   case SERVICE_CONTROL_CONTINUE:
       manager.SetServiceRunStatus(SERVICE_CONTINUE_PENDING);
       // 일시 중지 시킨 서비스를 재개한다.
       manager.SetServiceRunStatus(SERVICE_RUNNING);
       break;

   case SERVICE_CONTROL_STOP:
       manager.SetServiceRunStatus(SERVICE_STOP_PENDING);
       // 서비스를 멈춘다 (즉, 종료와 같은 의미)
        // 서비스를 종료하면, service_main 는 절대로 리턴하지 않는다.
        // 그러므로 해제하려면 작업이 있으면 모든것을 이곳에서 처리한다.
       manager.SetServiceRunStatus(SERVICE_STOPPED);
       break;

        // 윈도우에서 세션에 변경 사항이 발생하였을 경우, 발생하는 이벤트.
    case SERVICE_CONTROL_SESSIONCHANGE:
        {
            // 세션에 관련된 이벤트 정보는 아래의 구조체에 복사하여 사용한다.
            WTSSESSION_NOTIFICATION wtsnoti = {0};
            memcpy(&wtsnoti, lpEventData, sizeof(WTSSESSION_NOTIFICATION));

            // 유틸리티 CWTSSession 을 이용하여, 현재 검출된 세션의 정보를 읽어온다.
            // 소스를 열어보면, 검출되는 정보에는 어떤것이 있는지 알 수 있다.
            CServiceUtility::CWTSSession ws(WTS_CURRENT_SERVER_HANDLE, wtsnoti.dwSessionId);

            // FileLog는 서비스는 결과를 화면에 출력할 수 없기 때문에, 아주 심플하게 파일로 찍고자 임시로 만듬.
            // 이벤트 타입 dwEventType 를 이용하여 해당 이벤트때 필요한 작업을 하도록 처리해주면 된다.
            // 현재는 정보만 출력해준다.

#ifdef _DEBUG
            fprintf(FileLog, " WTS[%s <%d>] => IP(%s), Client(%s), Domain(%s), User(%s), WinStation(%s)",
                wtsname[dwEventType].text,
               
wtsnoti.dwSessionId,
               
ws.IPAddress,
               
ws.ClientName,
               
ws.DomainName,
               
ws.UserName, 
                
ws.WinStation);
            FileLog.flush();
#endif
        }

   default:
       break;
   }

   return NO_ERROR;
}

서비스를 띄워 놓고, 로그온/로그오프.. 혹은 외부에서 터미널로 접속하거나 하면 다음과 같이 파일로그에 출력된다.
(필요하다면 시간 정보를 같이 출력해주면 나중에 로깅할 때 도움을 받을 수 도 있다.)
EVENT> SERVICE_CONTROL_STOP
EVENT> SERVICE_CONTROL_SESSIONCHANGE
   WTS[REMOTE_CONNECT <1>] => IP(127.0.0.1), ClientName(JINHO2003), DomainName(), UserName(), WinStation(RDP-Tcp#3)
EVENT> SERVICE_CONTROL_SESSIONCHANGE

   WTS[LOGON <1>] => IP(127.0.0.1), ClientName(JINHO2003), DomainName(JINHO2003), UserName(aaa), WinStation(RDP-Tcp#3)
EVENT> SERVICE_CONTROL_PAUSE

EVENT> SERVICE_CONTROL_STOP
EVENT> SERVICE_CONTROL_SESSIONCHANGE
   WTS[LOGOFF <1>] => IP(127.0.0.1), ClientName(JINHO2003), DomainName(), UserName(), WinStation(RDP-Tcp#3)
EVENT> SERVICE_CONTROL_STOP


해당 소스는 MSVS 6.0 과 2008 버전으로 빌드 가능하도록 구성하였다.

지난 내용
[강좌로 보는 서비스 프로그래밍] 서비스란 무엇인가? [1/?]
[강좌로 보는 서비스 프로그래밍] 외적으로 보여지는 서비스 [2/?]
[강좌로 보는 서비스 프로그래밍] 설치와 제거 [3/?]
[강좌로 보는 서비스 프로그래밍] 가끔 쓸모있는 관련 함수들 [4/?]
[강좌로 보는 서비스 프로그래밍] 시작/중지/일시정지 [5/?]
[강좌로 보는 서비스 프로그래밍] 서비스의 본체 코딩 [6/?]
[강좌로 보는 서비스 프로그래밍] 서비스의 본체 코딩 Cont. [6/?]
[강좌로 보는 서비스 프로그래밍] 뼈다귀 서비스 코드 [7/?]

6장에서 서비스의 본문을 작성할 때 IsServiceMode() 라는 유틸성 함수를 이용하여, 비주얼 스튜디오에서 서비스 메인을
SCM에 등록하지 않고, 서비스 메인을 직접 호출하는 방식으로 콘솔 응용 프로그램 처럼 디버깅하는 방법을 제시하였다.

작업을 진행할 때 가장 빠르고, 편안하게 디버깅을 할 수 있기 때문에 동작 테스트는 거의 위 방식으로 진행을 한다.
하지만 실제로 서비스로 구동중이지 않으므로, 서비스의 이벤트를 제공받거나 하는 등의 테스트는 불가능하다.

기본적인 동작 테스트가 마친 상태에서 서비스로 등록하여 놓고, 구동 중인 상태에서의 디버깅 방법을 살펴본다면,
이미 아시는 분들도 계시겠지만, MSVS는 Process Attach 방식의 디버깅을 제공해 주므로 이것을 이용하는 것이다.

현재 MSVS 6.0 과, MSVS 2008 두가지 버전으로 개발툴을 사용하는 관계로 해당 두가지 툴로 구동중인 프로세스를
디버깅하는 방법을 처리해 보도록 할것이다.

1. MSVS 6.0 으로의 구동중인 프로세스 디버깅 ( 당연히 소스를 가지고 있는 상태를 기준으로 )
   A. 디버그 모드로 빌드된 것을 서비스에 등록하여 구동중인 것은 별도의 설정이 필요 없다.
   B. 릴리즈 모드로 빌드된 것이 서비스로 구동중일 경우는 다음과 같은 절차를 거쳐야 디버깅이 가능하다.

       가. 먼저 서비스를 중지 시킨다.

       나. 프로젝트의 속성 중에서 컴파일 부분의 다음과 같은 속성을 변경시킨다.
          



       다. 프로젝트의 속성 중에서 링크 부분의 다음과 같은 속성을 변경시킨다.
         
   

    라. 프로젝트를 리빌드 한 후, 빌드된 프로세스로 서비스를 등록 한 후 구동 시킨다.

C. 구동중인 프로세스를 디버깅 해보자.
    가. 메뉴에서 프로세스 어태칭 메뉴를 선택한다.
        


    나. 화면에서 현재 구동되고 있는 서비스 프로세스를 선택한다.
       


    다. 아래처럼 프로젝트 상태로 어태칭된 화면을 볼 수 있을 것이다.
       


   라. 위 그림 상태에서 Break Execution 버튼을 누르면, 아래처럼 콜스택이 활성화 되면서 실제 디버깅 모드로 진입한다.
       


   마. 콜스택을 이용하여, 소스 상태 디버깅과 마찬가지로 모든 작업을 진행할 수 있다.
       

2. MSVS 2008 로의 구동중인 프로세스 디버깅 ( 당연히 소스를 가지고 있는 상태를 기준으로 )
   A. 디버그 모드로 빌드된 것을 서비스에 등록하여 구동중인 것은 별도의 설정이 필요 없다.
   B. 릴리즈 모드로 빌드된 것이 서비스로 구동중일 경우는 다음과 같은 절차를 거쳐야 디버깅이 가능하다.

       가. 먼저 서비스를 중지 시킨다.

       나. 프로젝트의 속성 중에서 컴파일 부분과 링크 부분을 다음과 같이 변경시킨다.
         

   
       다. 프로젝트를 리빌드 한 후, 빌드된 프로세스로 서비스를 등록 한 후 구동 시킨다.

   C. 구동 중인 프로세스를 디버깅 해보자. 
      가. 메뉴에서 프로세스 어태칭 메뉴를 선택한다.
        


    나. 화면에서 현재 구동되고 있는 서비스 프로세스를 선택한다.
        


    다. 아래처럼 바로 디버깅 가능한 상태로 화면이 펼쳐진다. 2008은 이렇게 어태치한 프로세스를 디버깅에 사용하고,
         필요한 작업을 마치면 디태칭하여, 다시 복원 시키는 것도 가능하다 (  MSVS 6.0은 디태치 기능이 없음)
       


이 기능은 서비스 뿐만 아니라, 본인의 개발툴로 개발된 프로세스를 테스트할 때 언제든지 사용할 수 있는 방법입니다.

이러하게 프로세스를 가지고 디버깅 하게 되면, 실제로 외부에서 서비스에 중지 신호나, 일시 중지 등을 시도 하였을 때, 소스 내부에서
어떻게 반응하는지, 나의 처리가 제대로 되었는지.. 모든것이 가능해 집니다.

아래 그림은 서비스 매니저에서, 해당 서비스를 일시 중지 시켰을 때, 소스에 걸린 브레이크 포인가 동작하는 화면입니다.












지난 내용
[강좌로 보는 서비스 프로그래밍] 서비스란 무엇인가? [1/?]
[강좌로 보는 서비스 프로그래밍] 외적으로 보여지는 서비스 [2/?]
[강좌로 보는 서비스 프로그래밍] 설치와 제거 [3/?]
[강좌로 보는 서비스 프로그래밍] 가끔 쓸모있는 관련 함수들 [4/?]
[강좌로 보는 서비스 프로그래밍] 시작/중지/일시정지 [5/?]
[강좌로 보는 서비스 프로그래밍] 서비스의 본체 코딩 [6/?]
[강좌로 보는 서비스 프로그래밍] 서비스의 본체 코딩 Cont. [6/?]

이전 장에서 다룬 서비스 본체 코딩이라고 한 부분은 유틸성 클래스를 이용하여, 본체를 구성하여 보았다.
다음 주제로 넘어가기 이전에, 잡다한거 다 제거하고 순수하게.. API 만으로 작성한 코드를 살펴보면서
차이점이 무엇인지? 구지 저런거 다 넣고 진행할 필요가 있는지. 급할 때는 기본 코드만으로 쓱~~ ^^;;;

아래의 코드는 이전 샘플과 거의 동일한 기능을 제공한다. 그리고, 실제로도 대부분 저렇게 구성하는 경우가 많다.

#include <windows.h>
#include <winsvc.h>

#define SRVNAME "CROWBACK Service"
SERVICE_STATUS_HANDLE hss = NULL;
SERVICE_STATUS ss = {SERVICE_WIN32_OWN_PROCESS, 0, 0xFF, 0, 0, 0, 0};

DWORD GetServiceRunStatus(SERVICE_STATUS& ss);
DWORD GetServiceRunStatus();
DWORD WINAPI service_handler(DWORD fdwCtrl, DWORD dwEvent, LPVOID lpData, LPVOID lParam);
int service_main(int argc, char** argv);

// 1. 원래의 메인 함수
int main(int argc, char** argv)
{
    SERVICE_TABLE_ENTRY STE[] =
   {
       {SRVNAME, (LPSERVICE_MAIN_FUNCTION)service_main},
       {NULL,NULL}
   };

   if(StartServiceCtrlDispatcher(STE) == FALSE)
       return -1;

    return 0;
}

// 2. 서비스의 메인 함수
int service_main(int argc, char** argv)
{
    hss = RegisterServiceCtrlHandlerEx(SRVNAME, service_handler, NULL);
    if (hss == NULL)
        return -1;

    ss.dwCurrentState = SERVICE_START_PENDING;
    SetServiceStatus(hss, &ss);

    // bla bla...

    ss.dwCurrentState = SERVICE_RUNNING;
    SetServiceStatus(hss, &ss);

    while(GetServiceRunStatus() != SERVICE_STOPPED)
    {
       if(GetServiceRunStatus() == SERVICE_PAUSED)
       {
           Sleep(1000);
           continue;
       }

        ::MessageBeep(0xFFFFFFFF);
       Sleep(1000);
    }

    return 0;
}

// 3. 이벤트 핸들러 함수.
DWORD
WINAPI service_handler(DWORD fdwCtrl, DWORD dwEvent, LPVOID lpData, LPVOID lParam)
{
    switch (fdwCtrl)
   {
   case SERVICE_CONTROL_PAUSE:
        ss.dwCurrentState = SERVICE_PAUSE_PENDING;
        SetServiceStatus(hss, &ss);
        // to do
        ss.dwCurrentState = SERVICE_PAUSED;
        SetServiceStatus(hss, &ss);
        break;

   case SERVICE_CONTROL_CONTINUE:
        ss.dwCurrentState = SERVICE_CONTINUE_PENDING;
        SetServiceStatus(hss, &ss);
        // to do
        ss.dwCurrentState = SERVICE_RUNNING;
        SetServiceStatus(hss, &ss);
        break;

   case SERVICE_CONTROL_STOP:
        ss.dwCurrentState = SERVICE_STOP_PENDING;
        SetServiceStatus(hss, &ss);
        // to do
        ss.dwCurrentState = SERVICE_STOPPED;
        SetServiceStatus(hss, &ss);
        break;

   default:
        break;
   }

    return NO_ERROR;
}

// 4. 서비스의 상태를 읽어오는 함수.
DWORD GetServiceRunStatus(SERVICE_STATUS& ss)
{
    SC_HANDLE hScm = OpenSCManager(NULL, NULL, SC_MANAGER_ALL_ACCESS);
    if(hScm == NULL)
        return ERROR_INVALID_HANDLE;

    SC_HANDLE hSrv = OpenService(hScm, SRVNAME, SERVICE_INTERROGATE);
    if (hSrv == NULL)
        return GetLastError();

    if(ControlService(hSrv, SERVICE_CONTROL_INTERROGATE, &ss) == 0)
    {
        DWORD dw = GetLastError();
        if(dw == ERROR_SERVICE_NOT_ACTIVE)
        {
            ss.dwCurrentState = SERVICE_STOPPED;
            return ERROR_SUCCESS;
        }

        return dw;
    }

       CloseServiceHandle(hSrv);
    CloseServiceHandle(hScm);

    return ERROR_SUCCESS;
}

DWORD GetServiceRunStatus()
{
    SERVICE_STATUS ss = {0};
    if(GetServiceRunStatus(ss) != ERROR_SUCCESS)
        return ERROR_SUCCESS;

    return ss.dwCurrentState;
}

위의 소스를 빌드하면, 바로 서비스 프로그램이 생성된다. 단, 저건 스스로 설치할 수 없다, 스스로 제거할 수 도 없다.
외부에서 해당 서비스의 이름을 가지고, 이전 장에서 주욱 나열해왔던 설치부터 실행 제거까지 전부 외부에 의존해야 한다.

이전에 설명했던거 다 필요없이 저거면 서비스 프로그램이 되는 것이다. 급할 때 헤딩하지 말고, Ctrl+C, Ctrl+V 신공을 발휘하여 
빠르게 진행할 때 사용하기 위해서, 만들어둔 템플리트 처럼 사용하고 있는 코드이기도 하다. 

+ Recent posts