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

서비스스 뿐만 아니라, 시스템이나 관리자 권한으로 생성한 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/?]

지난장에서는 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;
}

오늘이 벌써 금요일 이네요.. 평안한 주말 보내세요.. ^^;;;
지난 내용
[강좌로 보는 서비스 프로그래밍] 서비스란 무엇인가? [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/?]

지난 장에서 콘솔 응용프로그램의 main() 함수에서 서비스 메인 함수를 호출하는 부분을 진행하다가 여기로 넘어왔다.
StartServiceCtrlDispatcher 함수는 그냥 서비스를 등록하기 위해서 호출하는 함수이려니.. (속편하게 ^^;;;)


다음으로 이전 장에서 서비스에 등록한 서비스 메인 함수에 대하여 알아보자.
// SCM에 등록된 서비스 메인함수 ( 콘솔 프로그램의 main()과 구조는 동일하다.
int
service_main(INT ARGC, LPSTR* ARGV)
{
    // 편하게 작업하려고 만들어놓은 클래스이니 선언해놓고, 자주 써먹자.
    CServiceManager manager;
    CServiceUtility utility;


    // 현재 구동 상태가 서비스 모드인지 확인한다.
    
if(utility.IsServiceMode() == TRUE)
    {
        // 서비스일 경우는 SCM 으로 부터 이벤트를 받을 수 있도록 이벤트 핸들러를 등록한다.
        // 이벤트 핸들러에 사용자 정의 파리미터를 전달하고 싶으면, 3번째 인자에 넣으면 된다.

     
   manager.SetServiceHandler(RegisterServiceCtrlHandlerEx(
           
CServiceManager::m_InstallParam.lpServiceName, service_handler, NULL));

        // 등록이 실패하면 어쩔 수 없이 에러 처리.
    
    if (manager.GetServiceHandler() == NULL)
            return 0;
    }
    else
    {
        // 응용 프로그램 모드로 구동하면 적당히 메시지 뿌려놓고 바로 코딩에 들어간다.
        // 이 경우는 그냥 콘솔 프로그램이랑 100% 동일하게 동작한다.
    
    printf("run debug mode\npress any key close...\n");
    }

    // 서비스가 시작되면 먼저 시작중임을 서비스 메니저에 알린다.
    // 서비스가 동작할 때는 SCM 에 나의 현재 상태가 어떤지를 알리는 작업은 매우 중요하므로
    // 상태가 변경될 때, 혹은 외부에서 변경되었을 때, 모든 상태에 대하여 꼭 처리를 해주어야 한다.
    // 그렇지 않으면, SCM 은 응답없음과 같은 처리 불가능한 상태로 인식하게 된다.
    
manager.SetServiceRunStatus(SERVICE_START_PENDING);

    // 현재 샘플 코드이므로 별로 처리할게 없다. 그냥 Sleep 로 대충 때운다.
    // 보통 서비스가 로딩될 때 초기화 하는 작업을 여기서 처리한다.
   
Sleep(1000);

    // 자 초기화가 모두 끝났다. 이제는 외부에서 이벤트도 받을 수 있고 서비스도 정상 동작중임을 알린다.
    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;
}
위의 함수는 서비스 모드일 경우는 이벤트 핸들러를 등록하는 과정이 추가될 뿐, 일반 콘솔 응응프로그램의 main() 함수와 전혀 다르지 않다.
주 내용은 그냥 무한 루프 돌면서 띵~ 띵~ 소리만 주구 장창 내는 아주 단순한 내용이다.

RegisterServiceCtrlHandlerEx 는 SCM으로 부터 이벤트를 받을 수 있도록 해주는 함수이다.
RegisterServiceCtrlHandler 함수라는 좀더 간단한 함수가 있지만, 이거는 서비스의 시작/중지/일시중지/재시작과 같은 아주 단순한 이벤트만
받을 수 있고, 시스템에서 발생하는 다양한 정보를 제공받지 못한다.

가능하면 시작할 때 RegisterServiceCtrlHandlerEx 함수를 이용하여, 서비스가 지원해주는 다양한 이벤트를 즐겨 보도록 하자.
유틸리티 클래스에 의하여
RegisterServiceCtrlHandlerEx 에 등록되는 과정이 조금 복잡해 보이지만, 해당 함수의 리턴값을 저장하여
다른 용도로 사용하기 위하여 꽁수를 부리다 보니, 저런 모양을 가지게 되었다. 별로 이뿌지가 않다 -_-;;;



자 이제 초간단 서비스의 최종 내용에 해당하는 이벤트 핸들러에 등록된 service_handler 함수에 대하여 알아보자.
DWORD WINAPI service_handler(DWORD fdwControl, DWORD dwEventType, LPVOID lpEventData, LPVOID lpContext)
{
   // 전달되는 인자에 대하여 살펴보면 다음과 같다.
   // fdwControl  이벤트의 종류이다.
   // dwEventType 이벤트의 세부 종류이다.
   // lpEventData 이벤트에서 전달해주는 파라미터이다. 이벤트의 종류에 맞도록 타입 캐스팅하여 사용한다.
   // lpContext   등록할 당시 전달된 사용자 정의 파라미터.

 
  CServiceManager manager;

   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;

   // 실제로는 아주 많고 다양한 이벤트를 받을 수 있다. 하지만 현재 다루기에는 조금 빠르니
   // 추후에 증축 공사를 끝낸 샘플에서 좀더 다양한 이벤트를 처리하도록 할것이다.
 
  default:
       break;
   }

   return NO_ERROR;
}

자, 드디어 가장 기초적이면서도 갖출건 다 갖춘 서비스 샘플을 구성하여 보았습니다.
별 쓸때 없는 클래스를 2개씩이나 만들어서 코드 모양이 조금 이해하기 어렵게 구성되었지만, 첨부된 플젝을 열어서 보시면
크게 어렵지 않을 것입니다.

어느정도 기초적인 서비스 프로그램을 진행해보신 분이라면 첨부된 2개의 클래스에 대하여 분석해 보는 것도 나쁘지 않겠지만
처음으로 서비스 프로그램을 작성하시는 분이라면, 괜한 시간 낭비일 수도 있으므로 메인 함수만 분석하시는게 작업 속도가 빠를 것입니다.

첨언으로, VC를 이용하여 작성하였는데 생성하시는 프로젝트 타입은 콘솔 응용프로그램 으로 만드시면 됩니다. (완전히 똑같습니다.)
상위버전 VC에 포팅을 용이하게 하느라고 VC++ 6.0으로 샘플을 작성하였으니, 컨버팅하는 부담은 없을 것입니다.

지난 내용
[강좌로 보는 서비스 프로그래밍] 서비스란 무엇인가? [1/?]

[강좌로 보는 서비스 프로그래밍] 외적으로 보여지는 서비스 [2/?]
[강좌로 보는 서비스 프로그래밍] 설치와 제거 [3/?]
[강좌로 보는 서비스 프로그래밍] 가끔 쓸모있는 관련 함수들 [4/?]
[강좌로 보는 서비스 프로그래밍] 시작/중지/일시정지 [5/?]

이번 장에서는 실제로 서비스 프로그램이 어떻게 구성되어 있는지 간단한 샘플을 이용하여 살펴보고자 한다.
샘플에 구현된 기능은 다음과 같다.
1. 설치/제거
2. 시작/중지/일시중지/재시작
3. 설치시 각종 파라미터 설정
4. 디버깅을 위한 간단한 조작
5. 전체 서비스 프로그램의 흐름
6. 기능은 지난 서비스 설명시 잠깐 나온 땡땡거리는 서비스를 기반으로 한다.

소스의 구성 품목은 다음과 같다.
1. main.cpp 서비스 구조를 가진 소스
2. ServiceUtil.h / cpp 서비스를 개발할 때 간혹 쓸모있는 함수 모음 클래스
3. ServiceMgr.h / cpp  서비스의 설치나 동작을 제어하는 기능을 가진 모음 클래스

아래는 접어놓은 부분은 main.cpp 와 두개 클래스의 헤더만 설명을 위하여 올려 놓았다.
전체 샘플은 압축하여 첨부 파일로 추가할 것이다.




서비스 본체를 구성하고 있는 main.cpp 를 살펴보기로 한다.
int main(int argc, char** argv)
{
#if 0
    /////////////////////////////////////////////////////////////////////
    // #define 로 쌓인 이 구문은 첨부된 클래스 2개의 간단한 사용법과
    // 정상적인 지를 확인하기 위한 테스트 코드이다. 불필요하면 스킵해도 된다.
    
/////////////////////////////////////////////////////////////////////

    CServiceUtility u;
    u.ServiceUserBlankPassword(FALSE);

    u.UserPrivileges("Administrtor", L"SeServiceLogonRight");
    u.ProcessPrivileges(GetCurrentProcess(), SE_SHUTDOWN_NAME, TRUE);

    PWTS_PROCESS_INFO sinfo = NULL;
    DWORD count = 0;
    u.WTSEnumProcesses(sinfo, &count);
    u.WTSFree(sinfo);

    PWTS_SESSION_INFO info = NULL;
    count = 0;
    u.WTSEnumSessions(info, &count);
    u.WTSFree(info);

    CServiceUtility::CWTSSession ws(WTS_CURRENT_SERVER_HANDLE, 0);

    char buffer[512] = {0};
    if(u.GetOSDisplayString(buffer))
        printf("%s\n", buffer);

    CServiceUtility su;
    STARTUPINFO si = {0};
    PROCESS_INFORMATION pi = {0};
    u.CreateProcessToDesktop("C:\\Windows\\System32\\cmd.exe", NULL, si, pi, 0);
    WaitForSingleObject(pi.hProcess, INFINITE);

#endif

    // 서비스를 관리하는 매니저와 구현을 도와주는 유틸 클래스
    
CServiceManager manager;
    CServiceUtility utility;

    // 아래 항목은 설정하지 않으면, 파일정보를 읽어서 자동으로채운다.
    manager.ConfigServiceName("CROWBACK");
    manager.ConfigServiceDisp("CROWBACK SERVICE");
    manager.ConfigServiceDesc("CROWBACK's test service, forever ting... ting...");

    // 먼저 서비스로 구동중인지, 응용 어플리케이션 모드로 구동 중인지를 확인한다.
    if(utility.IsServiceMode() == FALSE)
    {
        // 응용 어플리케이션 모드로 돌아갈 때는 인자를 하나 받는다. 인자는 아래와 같다.
        // 인자가 없을 경우는 직접 인자를 입력 받는다. 지정된 값이 아니면 구동 상태로 돌입한다.
        char ch = 0;
        if(argc != 2)
        {
            printf("sample.exe [option: i, u, s, t, p, c]\n");
            printf(" i - install\n");
            printf(" u - uninstall\n");
            printf(" s - start\n");
            printf(" t - stop\n");
            printf(" p - pause\n");
            printf(" c - continue\n\n");

            printf("input command: ");
            ch = getch();
        }
        else
            ch = argv[1][0];

        // 인자에 따라 아래와 같은 동작을 수행한다.
    
    switch(ch)
        {
        case 'i':
            manager.Install();
            return 0;
        case 'u':
            manager.Uninstall();
            return 0;
        case 's':
            manager.Start();
            return 0;
        case 't':
            manager.Stop();
            return 0;
        case 'p':
            manager.Pause();
            return 0;
        case 'c':
            manager.Continue();
            return 0;

        // 위의 값이 아니면 그냥 응용 어플리케이션 모드로 구동한다.

        default:
            return service_main(argc, argv);
        }
    }

   // 여기로 왔다는 것은 현재 서비스 모드로 구동중인 상태인 것이다.
   SERVICE_TABLE_ENTRY STE[] =
   {
       //SCM (Service Control Manager)에 등록하기 위하여 지정된 구조체 정보를 채운다.
       { manager.m_InstallParam.lpServiceName, (LPSERVICE_MAIN_FUNCTION)service_main },
       { NULL, NULL}
   };
      
   // 해당 채워진 구조체를 이용하여 서비스 메인 함수를 SCM이 호출할 수 있도록 등록작업을 마친다.
   if(StartServiceCtrlDispatcher(STE) == FALSE)
       return -1;

   return 0;
}


여기서는 콘솔 응용프로그램의 main 함수에서 어떻게 서비스 메인함수를 등록하는지 간단한 절차를 처리해보았다.
하나의 페이지에 꽤 긴 내용이 들어가서 편집작업에 어려움이 있어 추가 설명과 나머지 내용을 다음장에서 정리할 것이다.
이번 장에서는 서비스의 몸통을 만들기 전에 서비스를 인스톨 하는 부분과 제거하는 부분에 대하여 알아볼 것이다.
서비스를 설계할 때 기능도 당연히 중요하지만 어떤 동작 상태를 주어 기동하게 할것인가에 대하여 생각해 보아야한다.

1. 먼저 서비스 이름이나 설명을 어떻게 설정할 것인가?
   -> 단순한 문제이지만, 서비스 이름이나 실행파일명, 그리고 이에 따른 설명은 해당 서비스가 어떠한 기능을 하게 되는지
       그리고, 사용하는 사람들이 직관적으로 알아 볼 수 있도록 명확하게 하여야 한다.

2. 서비스를 구동 시키는 계정은 어떻게 처리할 것인가?
   -> 보통 아주 일반적으로 디폴트 계정 즉, SYSTEM(Local System Account) 계정을 사용하게 된다. 
       아주 일반 적인 상황이지만, 간혹 Administrator 계정으로 설정해야 하거나, 혹은 특정 Users 계정으로 구동해야 하는
       경우도 없지않다.

       SYSTEM 계정이 아닌 다른 계정을 사용하게 되면 해당 계정에 SeServiceLogonRight 권한을 주어야한다.
       또한 암호가 비어있는 유저일 경우는 이것 외에도 보안 설정을 풀어주어야 한다.

3. 초기 구동시 자동 시작으로 할것인가? 수동 시작으로 할것인가?
   -> 보통 자동시작으로 초기 설정을 확정하는 경우가 많은데... 서비스가 순수하게 독립 기능을 수행할 경우는 모르겠지만
       먼가 연계되는 부분이 존재한다면, 사용자가 시작 시켜주기를 기다려야 할 수도 있다.
       이러한 상황을 무시하고 무조껀 자동 시작으로 해놓으면, 서비스가 구동에 실패하면서 시스템 로그에 서비스 구동 실패
       로그를 쌓을 것이다. 

4. 서비스가 구동에 실패하였을 경우, 어떠한 처리를 할것인가?
   -> 보통 정상적으로 서비스가 구동되기를 기대하지만, 아주 황당한 이유등으로 서비스 구동이 실패할 수 있다.
       주로 겪는 문제가 Network Adapter 장애로 인하여 소켓등에 문제가 생기거나, 과부하로 인해 스토리지가 깨지거나
       AD 서버 장애로 인하여 로그인에 실패하거나 DBMS가 죽어 있거나 .... 등등.. 여러가지 문제가 발생할 수 있다.

5. 특정 서비스에 의존하는 부분이 존재하는가?
   -> 특수 목적용으로 만들어진 서비스중에서는 DBMS에 의존하던가? 무선랜 서비스가 켜져 있어야 한다던가?
       원격의 관리를 위하여 여러 서비스가 활성화 되어 있어야 한다건가.. 

       편하게 이러한 시스템 에러에 대응하여 디펜던시를 걸어 놓으면 가장 편하게 에러 상황을 피해 나갈 수 있다.
       문제는 이렇게 디펜던시를 걸어 놓으면 상위 서비스에 장애가 생겼을 경우, 하위 서비스는 아예 구동 조차도
       되지 않으므로 어떠한 문제가 발생하였는지 전혀 알 수 가 없다는 것이다.

       그래서, 디펜던시를 걸어 놓는 방법은 추천하지 않는다. 특정 서비스에 종속적이라면 서비스가 초기 구동될 때
       해당 서비스들의 구동 상태를 점검하고 꺼져 있는건 시작 시켜주고 문제가 생기면 에러 로깅을 해주고 종료해야 한다.

6. 데스크탑 유저들과 연동해야 하는 부분이 존재하는가?
   -> 서비스는 백그라운드에서 구동되기 때문에 사실상 로그온된 상태의 유저들에게 화면상으로 정보를 전달하는게 불가능하다.
       보통 IPC를 통하여 사용자 프로그램과 연동하지만, 서비스에서 GUI 어플리케이션을 실행 시켜 줄 수도 있다.
 
       데스크탑과 연동이라는 옵션을 이용하면, 이러한 부분을 일부 커버할 수 있지만, 이건 순수하게 Console Session 만 해당한다.
       즉, 터미널 유저에게는 영향을 못미친다는 것이다.

       서비스와 연동하는 GUI에서 특정 정보를 전달하고 종료했을 때, 서비스 구동에 문제가 생기면 로그에만 남겨야할까?


      
머 저렇게 하나씩 준비해서 서비스의 설치단계를 진행하는 사람은 없겠지만, 한번 쯤은 점검해볼 필요는 있지 않을까?
사실상 개발할 때 문제가 되거나 가장 귀찬은 영역이 4, 5, 6 번 영역이다... -_-;;;

다음의 예제 코드는 아주 간단한 서비스 설치 코드이다. 설정을 최소화한 상태에서는 저렇게 쉬울 수가 없다.
SC_HANDLE hScm = OpenSCManager(NULL, NULL, SC_MANAGER_ALL_ACCESS);
   if (hScm == NULL)
       return GetLastError();

   SC_HANDLE hSrv = CreateService(
       hScm,
       "서비스의 고유 이름",
       "서비스의 외부 표시 이름",
       SERVICE_ALL_ACCESS,
       SERVICE_WIN32_OWN_PROCESS,
       SERVICE_AUTO_START,
       SERVICE_ERROR_NORMAL,
       "서비스 실행파일 경로",
       NULLNULLNULL, NULL, NULL); 

   if(hSrv )
       CloseServiceHandle(hSrv);
   CloseServiceHandle(hScm);

자, 아주 쉽게 서비스의 설치가 완료되었다. 간단하게 이름을 주고, 자동시작하고, 에러는 적당히 시스템로그에 남기고 끝이다.
보통 대부분의 서비스의 설치는 저 상태라도 전혀 문제가 없다...

1. 시스템을 관리하기 위하여 특정한 SP_USER 라는게 생성되었다. 모든 동작이 해당 계정에서 처리되며, 각종 레지스트리도 저 유저 계정하에서
    동작되도록 처리해야 한다. 해당 계정으로 만들어진 대부분은 Administrator 의 접근을 배제 시켜 놓았다.

작업의 편의성을 위해서라도 서비스의 구동 계정을 SP_USER 라고 맞출 필요가 방금 생긴것이다. 왜? 방금 생각해 낸것이니까 ㅡ.,ㅡ
그렇다면 SP_USER가 암호를 가지고 있는가? 아닌가에 따라 작업이 또 달라진다.

SP_USER 가 암호를 가진 유저라면 해당 계정에  SeServiceLogonRight 권한만 주면 문제가 없겠지만, 아쉽게도 암호가 비어있을 경우는
또다른 문제를 생성시킨다. 즉, 기본적으로 서비스를 구동시키는 계정은 암호를 비워둘 수가 없다.

그냥 서비스 계정을 SYSTEM 으로 만들면 아주 편한데, 저거 하나 때문에 골머리 썩어보신 분이 있을 겄이다. (없으면 나만 그랬나.. ㅡㅜ);
(물론 서비스 매니저에서 마우스로 클릭해주고, 보안 관리자에서 보안을 손으로 풀어주면 아주 쉽게 해결되는 부분이기도 하다. 아주 아주 쉽다...)
(항상 코딩으로 먼가를 해결해야 할때가 문제인거다 -_-;;;)

서비스 설치에 저런게 있다는거고 모두 저런 자질구래한 것에 신경 쓸 필요는 없다. 필요한 사람에게 아주 가끔 도움이 될 수 있을까?
세세한 설치방법과 에러 처리에 관한 부분은 샘플 코드를 작성하는 부분으로 넘긴다.

자 다음으로는 서비스를 제거해봐야 겠다.
   SC_HANDLE hScm = OpenSCManager(NULL, NULL, SC_MANAGER_CREATE_SERVICE);
   if (hScm == NULL)
       return GetLastError();

   SC_HANDLE hSrv = OpenService(hScm, "서비스 이름", SERVICE_ALL_ACCESS);
   if (hSrv == NULL)
       goto ERROR_LABEL;

   if(!DeleteService(hSrv))
       goto ERROR_LABEL;

   ERROR_LABEL:
   if(hSrv)
       CloseServiceHandle(hSrv);
   if(hScm)
       CloseServiceHandle(hScm);
이전 설치와 마찬가지로 아주 쉽다. 더불어 서비스를 제거할 경우는 '서비스 이름' 이라는 단 하나만 알고 있으면 된다.
별도의 에러처리는 실제 프로그램을 하면서 하나씩 진행하면 되므로, 특별한 에러처리나 이런것은 배제하였다.

다음으로는 여기서 중간 중간 설명하던 권한이나, 서비스 상태에서 데스크탑에 GUI를 띄워주거나 이런 잘잘한 기능을 구현한 함수들을
일부 소개한다. 앞으로 가끔 쓰일것이므로, 미리 포스팅하여 참고할 수 있도록 할것이다.



지난 내용
[강좌로 보는 서비스 프로그래밍] 서비스란 무엇인가? [1/?]


지난 내용에서 서비스가 구동되는 순서는 어떠한지, 또한 서비스 프로그래밍의 장/단점은 어떠한지에
대하여 간략하게 살펴보았다. 자세한 기술 관점으로 넘어가기전에 서비스 관리자에서 (services.msc)
살펴볼 수 있는 항목들에 대하여 간략하게 설명하고, 이러한 항목들이 프로그래밍되는 API 와 연관되는지
살펴보도록 하겠다.

전체적으로 외향적인 부분을 살펴보는 것이므로, 주의깊게 살펴보지 않아도 작업하는데 지장은 없다.

<윈도우에서 제공하는 서비스 관리자의 모습>

가장 일반적으로 접하게 되는 GUI로써, 서비스를 시작/중지/일시 정지 시킬 수 있고 서비스의 현재 동작
상태를 살펴볼 수 있다. 해당 서비스가 어떠한 계정으로 구동중인지도 확인이 가능하다.


1. General (일반 속성)

A. 서비스 이름 (Service name)
    외부에서 서비스를 제어하기 위한 고유한 이름이다.
    모든 Pirntable Charcter는 모두 사용가능하다.

B. 표시 이름 (Display name)
    서비스 이름은 고유명사 형태의 간략화된 상태로
    사용되는 이름이지만, 표시 이름은 그 서비스의 특성을
    알려주는데 사용되는 별칭과 같은 형태이다.

C. 설명 (Description)
    해당 서비스의 사용 목적이나 기능에 대하여 알려주는
    내용으로, 핵심적인 내용을 요약하여 보여준다.

D. 실행파일 경로 (Path to executable)
    해당 서비스가 설치되어 있는 전체 경로를 나타낸다.
    단, 실제로 구동될 경우는 Windows\System32 경로에서
    구동되므로, 구동시 상대 경로 설정에 주의해야 한다.

위의 4가지 속성중, 서비스 이름과 실행 경로는 필수 정보이고
나머지 2가지 정보는 부가 정보이므로 생략이 가능하다.

 E. 시작 형태 (Startup type)
    머신이 구동될 때, 해당 서비스가 자동으로 시작될것인지
    사용자에 의하여 수동으로 시작될 것인지를 나타낸다.
    (Disable은 사용을 임시로 막아 놓은 상태)

   F.  서비스 상태 (Servces status)
       해당 서비스의 동작 상태를 시각적으로 보여준다. 다음과 같이 여러단계를 가진다.
       시작중, 시작됨, 중지중, 중지됨, 일시중지중, 일시중지됨, 재시작중...

       //
       // Service State -- for CurrentState
       //
       #define SERVICE_STOPPED                        0x00000001
       #define SERVICE_START_PENDING            0x00000002
       #define SERVICE_STOP_PENDING              0x00000003
       #define SERVICE_RUNNING                        0x00000004
       #define SERVICE_CONTINUE_PENDING     0x00000005
       #define SERVICE_PAUSE_PENDING            0x00000006
       #define SERVICE_PAUSED                           0x00000007

  G. 동작 제어 버튼
      서비스의 동작 상태를 변경하는데 사용되는 버튼들이다.
      SetServiceStatus API를 이용하여, 간단하게 서비스의 동작을 제어할 수 있다.


  H. 시작 파라미터 (Start parameter)
      서비스를 서비스 관리자에서 시작시킬 때 서비스의 메인 함수에 인자를 전달할 수 있도록 방법을 제공해주는
      부분이다. 해당 위치에 적절한 파라미터를 위치 시킨 후에 시작 시키면 서비스는 해당 파라미터를 인식하여
      필요한 처리를 할 수 있다.
      단, 주의 할점으로는 휘발성이므로, 한번 적용하고 나서 다시 시작할 때 적용되지 않는다는 것에 주의하자.

여기까지 서비스의 속성창 중에서 첫번째에 보여지는 일반 속성에 대하여 간략하게 알아보았다.
그렇다면, 위에 나열된 것들이 프로그램적으로는 어디에서 사용되는지를 간단하게 살펴보고자 한다.

그럼 처음으로 CreateService Function 함수에 대하여, 살펴보고 위에 나온 내용들이 어디에 사용되는지 확인하자.

SC_HANDLE WINAPI CreateService(
  __in       SC_HANDLE hSCManager,              서비스 매니저의 핸들
  __in       LPCTSTR lpServiceName,                A. 서비스 이름
  __in_opt   LPCTSTR lpDisplayName,            B. 표시 이름
  __in       DWORD dwDesiredAccess,               서비스의 액세스 권한
  __in       DWORD dwServiceType,                    서비스의 종류(파일 드라이버, 커널 드라이버, 독립실행 서비스, 프로세스공유 서비스)
  __in       DWORD dwStartType,                         E. 시작 형태 (자동/수동/사용안함)
  __in       DWORD dwErrorControl,                    서비스 시작 실패시의 후속처리(시스템 재시작, 무시, 무시 및 이벤트로그 기록)
  __in_opt   LPCTSTR lpBinaryPathName,      F. 실행파일 경로
  __in_opt   LPCTSTR lpLoadOrderGroup,      @.@ (1)
  __out_opt  LPDWORD lpdwTagId,                  @.@ (2)
  __in_opt   LPCTSTR lpDependencies,           다른 서비스가 시작된 후에 구동할 필요가 있을 경우, 먼저 실행되어야할 서비스 이름들.. (3)
  __in_opt   LPCTSTR lpServiceStartName,    서비스를 시작시킬 때 사용될 유저 이름 (4)
  __in_opt   LPCTSTR lpPassword ,                   유저 이름이 지정되었을 경우 해당 유저의 암호

)

서비스를 생성할 때 위처럼 복잡하게 많은 인자들이 전달되지만 대부분이 생략 가능한 것들이다.
위에서 파란색으로 표기된 인자들은 필수 인자들인데, 설정하는데 전혀 어려울것이 없다.
나머지 아래의 5개가 조금 헷갈리는 것들인데

(1), (2) 은 나도 잘 모르겠다. 항목을 살펴보면 서비스를 그룹으로 묶을 수 있고, 해당 그룹 목록을 작성할 수 있는것을 보면
먼가가 있어보이는데.. 값을 넣거나, NULL로 해놔도 먼가 변화를 찾아볼 수 없다 -_-;;;

아래는 해당 관련된 정보를 담고 있는 레지스트리 키 이름이다.
HKEY_LOCAL_MACHINE\SYSTEM\ControlSet001\Control\ServiceGroupOrder
HKEY_LOCAL_MACHINE\SYSTEM\ControlSet001\Control\GroupOrderList

(3) 번은 "LanmanWorkstation\0LanmanServer\0LmHosts\0\0" 과 같이 여러개의 서비스 사이를 \0으로 구분하고 문자열의
마지막을 \0\\으로 구분해 주면 된다.


               <위의 설정을 이용하여 적용된 디펜던시 상태>

(4) 번은 사용자의 컴퓨터 계정 이름을 넣어주면 되는데, 도메인 사용자는 "도메인이름\사용자이름",
로컬 머신 계정은 ".\사용자이름" 과 같이 도메인을 구분해서 넣어주어야 한다.


2. Log on (시작 계정)
일반 속성에서는 말 그대로 서비스를 사용하는데 필요한 몇가지 필수 요소들의 정보를 가지고 있다. 또한 이렇게 생성된
서비스를 제어할 수 있는 방법을 제공한다. 아래의 그림은 시작 계정을 시스템 계정이 아닌 사용자 정의 계정을 적용
시켜놓은 상태의 모습이다. 하나씩 그 의미를 살펴보자.
A. Log on as: Local System account
   위 Radio Button 을 첵크하면 기본적으로 시스템
   (SYSTEM) 계정으로 서비스가 구동된다. 프로그램
   적으로는 계정을 설정 하지 않고 CreateService에서
   NULL 을 입력하면 되는것이다. 일반적으로 서비스는
   SYSTEM 계정으로 구동하길 권장한다.

B. Allow service to interact with desktop
   제목과 같이 서비스가 사용자가 로그온한 테스크탑과
   서로 연동되도록 설정하는 항목이다. SYSTEM 계정일
   때만 사용이 가능하다.

C. This account
   사용자가 계정을 직접 입력하여 해당 서비스가, 그 계정
   하에서 만 구동하도록 설정하는 것이다.

D. Hardware profile enable/disable
   해당 서비스에 연결가능한 하드웨어 프로파일을 사용하게
   하거나 못하게 하는 것이다. Disable 을 선택하면 서비스의
   사용이 불가능해진다는 점에서 서비스의 시작 타입을
   Disable 시켜 놓은 것과 동일하다.

이번 시작계정 챕터에는 항목은 작지만, 의외로 다루기 까다로운 설정들이 존재하는데... 프로그래밍의 어려움도 있지만
의미를 이해하기 쉽지않은 부분들이 존재한다.

Allow service to interact with desktop (데스크탑과 상호 연동)
해석해보면 데스크탑(사용자가 로그온한 상태의 윈도우 환경)과 연동이 가능하도록 지원해준다는 것이다.
일반적으로 서비스가 구동하게 되면, 어떠한 어플리케이션을 실행시키더라도 화면에 보여주지 못하는 background session 에서 구동된다.
즉, CreateProcess나 ShellExecute 혹은 ShellExecuteEx 를 이용하여, 콘솔이던  GUI이던 실행시키더라도 화면에 보여지지 않는다.
(물론 프로그램은 정상적으로 동작하지만 눈에 보이지 않으니, 프로세스가 떠있다는것 말고는 동작 상태를 확인하기 어렵다.)

 
코딩으로 보면 CreateService 함수에 인자를 전달할 때 SERVICE_WIN32_OWN_PROCESS | SERVICE_INTERACTIVE_PROCESS
를 전달하기만 하면 위의 그림처럼 첵크되어 해당 기능을 활성화 할 수 있다. 이 자체는 해당 기능을 활성화한것 뿐이지 이것으로
먼가가 자동으로 되는것은 없다.

저 옵션을 사용하면, 프로그램이 실행은 되지만 Console Session 에만 보여진다.
즉, 유저가 로그온한 세션에 프로그램을 띄워주는건 저 방법으로는 불가능하다는 것이다.

그렇다면, 서비스에서 로그온된 유저를 찾아 해당 유저의 세션에 프로그램을 띄워주는 것은 불가능한 것인가?
그 해법으로 CreateProcessAsUser 를 이용하는 방법이 있다. 이 챕터의 설명 범위를 넘어가는 것이기 때문에..
추후에 진행되는 예제중에 해당 처리 방법에 대한 것을 논의 하겠다.

하드웨어 프로파일에 관한건 별 쓸모가 없는 관계로 자세하게 다루지 않을 것이다.

3. Recovery
서비스를 수행하다보면, 여러가지 이유에 의해서 구동이 실패할 수 있다. 컴퓨터가 부팅되는 과정중에 수행되는 서비스의
실패를 어떻게 다룰것인가? 어떠한 이유로 실패하였고, 실패하였다면 그 후속 처리를 어떻게 할 것인가.. 등등.
성공하면 상관없지만, 실패하면 상관있는 그런 항목이다. 중요도가 상당히 높지만 자주 사용되지는 않는다.
사실상 윈도우의 시스템 서비스를 살펴보더라도 99% 쓰지 않거나, 쓴다고 하더라도 서비스 재시작이 고작이다.

왼쪽 그림에서 설명하는 내용도, 서비스가 실패
하였을 경우,

A. 그냥 무시
B. 서비스 재시작
C. 특정 프로그램 실행
D. 컴퓨터 재시작

저기서 C의 특정 프로그램 실행이란 항목을
선택하면 어떤 프로그램을 실행시킬건지 하고
인자를 기입할 수 있는 항목이 생성된다.

최대 3번 까지 재시도 할 수 있도록 제공하고

머 나머지는 실패했을 때 숫자세는데 무한정
셀수는 없으니까.. 언제 초기화 할것인가이다.

99.9%의 대열에 합류하면 이 항목은 전혀 의미
없는 항목이 되겠고, 먼지 몰라도 일단 해본다...
라고 하면, 헤딩을 좀 해야한다.


저기 항목에 있는 것을 설정하려면 ChangeServiceConfig2 API를 이용하여 편집하는 것이 가능하다. 단, 다른 것은
별 문제가 없는데 Restart th Computer 를 처리하려면, 즉, 문제가 생겼을 때 컴퓨터를 재 시작하려면 한가지 권한을
 필요로 한다. 머신을 리부팅 시킬수 있는 권한 즉, SE_SHUTDOWN_NAME 를 필요로 한다는 것이다.

서비스는 SYSTEM 계정으로  구동하므로 필요하다면 언제든지 저 권한을 할당할 수 있으므로 저것이 필요하다는 것을
알고만 있으면 별 문제 없다. AdjustTokenPrivileges 를 이용하여 추가하면 그만이다. 복잡하게 이야기 하면 끝이 없고
간단하게 샘플코드 일부를 정리함으로 이내용은 나중 예제를 위하여 남겨 놓는다.

  SERVICE_FAILURE_ACTIONS sfa = {0};
  SC_ACTION sa[3];
  
  sa[0].Delay = 2000;
  sa[0].Type = SC_ACTION_RESTART;
  sa[1].Delay = 2000;
  sa[1].Type = SC_ACTION_RUN_COMMAND;
  sa[2].Delay = 2000;
  sa[2].Type = SC_ACTION_REBOOT;

  sfa.dwResetPeriod = 600;
  sfa.lpRebootMsg = "";
  sfa.lpCommand = "";
  sfa.cActions = 3;
  sfa.lpsaActions = (SC_ACTION *)sa;

  ChangeServiceConfig2(hSrv, SERVICE_CONFIG_FAILURE_ACTIONS, &sfa);

정말 간단하게 끝나넹.. 요걸 수행하기 이전에  SE_SHUTDOWN_NAME 권한만 주면 되는 것이다.
알고서 진행하면 별거 아닌 것이지만, 모르고 찾을 때는 별 귀찬은 문제가 다 덤비는 것이 이 세계인가 보다.

4. Dependencies
머 이름만 거창하게 디펜던시즈라고 되어있는데.. 짧게 간추려 보면, 내가 만든 프로그램이 특정 서비스에 영향을 받는가? 혹은 주는가?
이러한 것이 있을 경우는 정확하게 명시해라.. 머 이런 의미이다.

내 프로그램을 특정 서비스 동작 이후에 구동되도록 하고 싶다면..
1. General 에서 설명한 (3) 번 처럼 영향을 받을 서비스 목록을 그냥 주르륵 넣어서 서비스를 생성하면 된다. 아주 쉽다.

그렇다면 내 서비스 프로그램에 디펜던시가 걸려있는 다른 서비스 목록이 있다면 이걸 어떻게 찾을 것인가?
EnumDependentServices 를 이용하여 그냥 긁어오면 된다.

이러한 작업이 왜 필요하냐면.. 내가 만약 특정 서비스를 중지 시켜야할 일이 있다고 가정했을 때, 디펜던시 걸린 서비스를 확인하지 않고
그냥 그 서비스를 중지 시키면 절대 죽지 않는다. 프로세스를 kill 한다면 모를까..
디펜던시가 걸린 하부 서비스를 모두 확인하고 그 서비스의 하부를 확인해서 아래부터 차례로 하나식 죽이고 마지막에 목표로한 서비스를
중지시켜야 정상적으로 중지된다.

-----------------------------------------------------------------------------------------------------------------------

지금 까지 서비스 매니저에서 볼 수 있는 몇몇 항목에 대하여 간단하게 기술하여 보았습니다. 모르고 넘어가도 되는 항목들도
많이 있으나, 그런게 있다는걸 알아두면 나중에 도움이 될까 싶어서 그림과 함께 적어 놓았으니 참고가 되셨으면 좋겠네요..

자, 다음으로는 구체적인 서비스보다는 서비스를 설치/제거/제어(시작-중지)에 대하여 정리하여 보도록 하겠습니다.
위 항목도 API를 최대한 간소하게 사용하면 아주 쉽고요, API의 인자들을 모두 채우려면 꽤나 복잡하겠죠? ^^;;;

간단하게 할겁니다 ㅡ,.ㅡ;;;

윈도우 서비스 카테고리에서 이것 저것 서비스 프로그래밍을 하면서 필요한 것들을 하나 둘 씩 진행해 보았다.

이런 것들을 정리하여, 서비스는 왜 필요한 것이며? 무엇 때문에 사용하는가?
어떻게 동작되며, 과연 어느 순간에 실행되는지...
서비스는 일반 콘솔 프로그램과 무엇이 다른지 하나씩 정리를 해보려한다.


일반적으로 윈도우가 기동되는 과정을 단순화 시키면 아래 그림과 같다.




즉, 서비스 프로그램은 윈도우가 부팅된 이후에, 로그온 되지 않은 상태에서도 구동되어진다는 큰 장점을 가지게 된다.

또한 일반적으로 Local System Account 로 구동되기 때문에 시스템을 제어하는데 필요한 가장 강력한
권한을 가진다고 말할 수 있다. 서비스를 설치할 때 혹은 설치한 이후에 특별하게 로그온에 필요한 사용자를
변경하지 않는 이상 강력한 권한을 행사할 수 있다.

두 번째와 같은 이유로, 비스타 커널 부터 적용되는
시스템 경로에 파일쓰기, 프로그램 파일즈 경로에
파일 쓰기, 로컬 시스템 레지스트리 액세스와 같은
모든 작업에 영향을 받지 않고...


특히나 UAC에 영향을 받지 않는다.


이렇게나 많은 장점이 있는데도 불구하고, 응용 프로그램을 서비스로 작성하지 않는 이유는 무엇일까?

1. GUI를 가질 수 없다.
    -> 서비스 프로그램은 콘솔 어플리케이션과 같은 방식으로 프로그래밍이 되면서도 stdin, stdout, stderr 와 같은
        콘솔 핸들을 가지지 못한다. 즉, 화면상의 출력이란것은 불가능하다.

2. 데스크탑과의 연계가 어렵다.
   -> 로그온 되기 이전에 SYSTEM 계정으로 실행되기 때문에, 특정 로그온된 유저와 연계되어 어떤 작업을 진행
        하려면 까다로운 절차를 거쳐야 한다. 보통 이런걸 무시하기 위해서 보통 SOCKET를 이용해서 통신하기도 하는데
        하여튼 서버계열에서 로그온된 여러 유저와 개별적으로 분리된 통신을 하는건 상당히 피곤한 일이다.

3. 디버깅이 난해하다.
   -> 서비스로 실행되기 때문에 기본적으로 개발툴에서 직접 디버깅하는 것은 않된다.
        이를 위하여, 프로그램에 인자를 주어 debug 모드로 구동될 경우 서비스 핸들러를 거치지 않고 바로
        서비스 메인 함수를 호출하는 방식으로 콘솔 프로그램 처럼 디버깅이 가능하도록 할 수 는 있으나...
        서비스의 동작 특성에 맞도록 완전하게 디버깅 하는 것은 불가능하다.

        그래서, 일반적으로 디버깅을 위해서 별도의 로깅 장치를 만들어 놓고 열심히 써서 디버깅 하기도 한다.

4. 파일 처리나 MMF의 생성 등과 같은 작업에 제약을 받는다.
  -> 서비스에서 파일을 생성하면, Users 그룹은 읽기와 수행 권한만
      기본적으로 제공된다. 
      즉, 수정이나 쓰기 권한은 배재된다.

  -> MMF (Memory Mapped File) 의 경우 서비스에서 생성하게되면
      User 권한에서는 읽기 전용으로 여는 것조차 불가능하다.
     
      CreateFileMapping() 함수의 2번 째 인자..
      LPSECURITY_ATTRIBUTES lpAttributes에 적당한 권한을 채워서
      생성해 주어야 한다. 

      그냥 생성했을 경우, Users 그룹에서 해당 MMF를 오픈하게 되면
      ACCESS DENIED 에러를 만나게 될것이다.

      클라이언트 OS나, 일반 서버용 프로그램들은 거의 Administrators
      그룹에 속한 형태로 실행되기는 하지만, 모두 그러한 것은 아니니까


 


서비스에는 위에서 설명한 장/단점을 포함하여 조금더 많은 제약사항을 가지며, 각종 상태 표시의 까다로움, 난해한 에러 처리... 등등
간단하게 작성하면 이전의 샘플 코드 처럼 아주 쉽게 작성할 수 있고..  세밀하게 처리하려면 윈도우라고 하는 OS 에 대하여 꽤나 많은
지식을 필요로 하게 된다.

-------------------------------------------------------------------------------------------------------------------------

오랜시간 하나의 커다란 프로젝트를 진행하다 보니, 많이 지치고 너무도 구태의연해지는 내 모습에 새로운 활기를 불어 넣어 보고 싶어
강좌라는 이름으로 새롭게 각오를 다지고자 이 글을 쓰게 되었습니다.

머 특출나게 할줄 아는것도 없고, 한가지 분야에 대해 내세울만한 것도 없고.. 이것 저것 생각해 보다 선택한게.. ^^;;;
짬짬히 시간 만들어서 하나씩 진행해 보도록 하겠습니다.






+ Recent posts