const char* S_NAME = "TEST";
const char* S_DISP = "TEST Service";
const char* S_DESC = "TEST Service Description";

// 서비스를 제어하기 위한 핸들을 저장하고 있어야한다.
SERVICE_STATUS_HANDLE srvhd = 0;

VOID _tmain_service(INT ARGC, LPSTR* ARGV);
DWORD WINAPI _tmain_service_handler(DWORD fdwControl, DWORD dwEventType, LPVOID lpEventData, LPVOID lpContext);
VOID SERVICE_STATE(DWORD dwState, DWORD dwAccept);

int main(int argc, char** argv)
{
    SERVICE_TABLE_ENTRY STE[] =
    {
        {(char*)S_NAME, (LPSERVICE_MAIN_FUNCTION)_tmain_service},
        {NULL,NULL}
    };
    
    // 서비스를 위해서 특별히 만들어진 구조의 함수 시작 부분을 시스템에 전달해야 한다.
    // 콘솔 프로그램과 다른 부분은 이렇게 등록시킨 함수가 콘솔의 main 처럼 동작한다는 점이다.
    // 일종의 콜백함수 포인터를 등록하면, 서비스 매니저가 이걸 호출해주는 방식이다.
    if(StartServiceCtrlDispatcher(STE) == FALSE)
        return -1;
   
    return 0;
}

VOID _tmain_service(INT ARGC, LPSTR* ARGV)
{
    // 서비스가 외부 제어 명령(시작, 중지, 다시 시작... etc) 을 받을 때, 그것을 받을 수 있도록 콜백형식의
    // 함수를 등록하는 것이다.

    srvhd = RegisterServiceCtrlHandlerEx(S_NAME, _tmain_service_handler, NULL);
    if (srvhd == NULL)
        return;

    // 무한 루프를 돌면서 띵띵~ 소리를 낸다.
    while(1)
    {
        ::MessageBeep(0xFFFFFFFF);
        Sleep(1000);
    }

    return;
}

// 서비스의 상태를 변경 시켜주는 함수.
VOID SERVICE_STATE(DWORD dwState, DWORD dwAccept)
{
    SERVICE_STATUS ss;
    ss.dwServiceType=SERVICE_WIN32_OWN_PROCESS;
    ss.dwCurrentState=dwState;
    ss.dwControlsAccepted=dwAccept;
    ss.dwWin32ExitCode=0;
    ss.dwServiceSpecificExitCode=0;
    ss.dwCheckPoint=0;
    ss.dwWaitHint=0;
        
    SetServiceStatus(srvhd, &ss);
}

DWORD WINAPI _tmain_service_handler(DWORD fdwControl, DWORD dwEventType, LPVOID lpEventData, LPVOID lpContext)
{
    // 서비스라 좋은점이 있네, 시스템에서 발생하는 잡다구리한 이벤트들을 별로 힘들이지 않고
    // 이곳에서 받아다가 처리할 수 있다.


    switch (fdwControl)
    {
    case SERVICE_CONTROL_PAUSE:
        SERVICE_STATE(SERVICE_PAUSE_PENDING,0);
        // 서비스를 일시 중지 시킨다.
        SERVICE_STATE(SERVICE_PAUSED);
        break;
       
    case SERVICE_CONTROL_CONTINUE:
        SERVICE_STATE(SERVICE_CONTINUE_PENDING,0);
        // 일시 중지 시킨 서비스를 재개한다.
        SERVICE_STATE(SERVICE_RUNNING);
        break;
       
    case SERVICE_CONTROL_STOP:
        SERVICE_STATE(SERVICE_STOP_PENDING, 0);
        // 서비스를 멈춘다 (즉, 종료와 같은 의미)
        SERVICE_STATE(SERVICE_STOPPED);
        break;
       
    default:
        break;
    }
   
    return NO_ERROR;
}

간단하게 샘플을 완성해 보려고 했더니, 좀더 알고 있어야 하는 사항들이 자꾸 생기네요..

   _tmain_service_handler 에 추가된 내용

   각각의 서비스 상태 변화에 맞추어  SERVICE_STATE 라는 함수를 호출합니다.

   위 함수의 기능은 현재 이 서비스는 이러한 상태입니다.... 라고 서비스 관리자 (SCM)에
   전달해 주는 기능을 합니다. 이것을 빼놓고 동작만 구현하면 서비스 관리자에서 해당
   서비스의 상태를 정상적으로 보여 줄 수 없기 때문에 잘못된 동작을 일으킬 수 있습니다.

   서비스는 상태를 변화 시키기 이전에 "상태 변화를 준비중입니다". 라고 하는 PENDDING
   상태로 먼저 만들고, 필요한 작업을 한 후 마지막에 해당 상태로 변환 되었음을 알려주어야 합니다.


지금까지는 전체 소스 샘플없이, 부분 코드만으로 작업을 진행했습니다만, 다음 진행되는 부분 부터는
프로젝트를 만들어서 소스를 업데이트 시키고, 파일로 첨부하겠습니다.

const char* S_NAME = "TEST";
const char* S_DISP = "TEST Service";
const char* S_DESC = "TEST Service Description";

VOID _tmain_service(INT ARGC, LPSTR* ARGV);
DWORD WINAPI _tmain_service_handler(DWORD fdwControl, DWORD dwEventType, LPVOID lpEventData, LPVOID lpContext);

int main(int argc, char** argv)
{
    SERVICE_TABLE_ENTRY STE[] =
    {
        {(char*)S_NAME, (LPSERVICE_MAIN_FUNCTION)_tmain_service},
        {NULL,NULL}
    };
    
    // 서비스를 위해서 특별히 만들어진 구조의 함수 시작 부분을 시스템에 전달해야 한다.
    // 콘솔 프로그램과 다른 부분은 이렇게 등록시킨 함수가 콘솔의 main 처럼 동작한다는 점이다.
    // 일종의 콜백함수 포인터를 등록하면, 서비스 매니저가 이걸 호출해주는 방식이다.
    if(StartServiceCtrlDispatcher(STE) == FALSE)
        return -1;
   
    return 0;
}

VOID _tmain_service(INT ARGC, LPSTR* ARGV)
{
    // 서비스가 외부 제어 명령(시작, 중지, 다시 시작... etc) 을 받을 때, 그것을 받을 수 있도록 콜백형식의
    // 함수를 등록하는 것이다.

    SERVICE_STATUS_HANDLE hd = RegisterServiceCtrlHandlerEx(S_NAME, _tmain_service_handler, NULL);
    if (hd == 0)
        return;

    // 요기가 콘솔의 main() 함수 역활을 하는 곳이다.
    // 먼가 기능을 넣으려면 이곳에다가 만들면 된다.  
  

    return;
}

DWORD WINAPI _tmain_service_handler(DWORD fdwControl, DWORD dwEventType, LPVOID lpEventData, LPVOID lpContext)
{
    // 서비스라 좋은점이 있네, 시스템에서 발생하는 잡다구리한 이벤트들을 별로 힘들이지 않고
    // 이곳에서 받아다가 처리할 수 있다.


    return 0;
}

살펴보면, 간단한 Windows Console Application 에다가 기본 main() 함수를 잠깐 개조한것 정도로 밖에는 보이지 않는다.
이렇게 처리하면서 서비스용 메인함수서비스 제어용 제어함수 2개를 등록하면 기본 서비스 구조는 끝이다.

서비스의 장점은 일단 윈도우가 로그온/로그오프 상태에서도 정상적으로 동작한다는 것이고,
단점은, 일반적인 어플리케이션에서 사용하는 많은 기능이 제약된다는 것이다. 디버깅도 좀 취약하다.
           특히나, 서버계열에서는 간단한 EnumWindows 같은 함수도 원하는데로 동작하지 않는다. -_-;;;;
           즉, 특정 터미널 세션이나, 데스크톱 윈도우의 제어 기능(일반 어플들의 기능)같은 것들을 처리할라믄 꽤 고달프다는 말이다.
           메인 함수야 제대로 블록해놓고 구성해도 되지만 제어기능의 내부는 블록되면 다른 서비스에도 영향을 미친다.

자, 이렇게 기본 서비스의 구조를 살펴봤으니, 다음장에서는 간단하게 띵띵~ 소리를 내는 서비스를 만들어 보자.
서비스의 시작(START), 중지(STOP), 일시 정지(PAUSE), 이어가기(CONTINUE) 동작도 처리해보자.

이전 장에서 나온 인자에 대하여 간단하게 설명을 추가하도록 한다.
        (1)서비스 이름,
        (2)서비스 표시 이름,
        (3)서비스 액세스 권한,
        (4)서버스 타입,
        (5)서비스 시작 타입,
        (6)서비스 에러 제어,
        (7)서비스 파일 경로,
        (8)서비스의 세부 설명

대략 살펴보아야 할 인자에 대하여 번호를 붙혀두었다. 그림을 보면서 살펴보자.

서비스 관리자에 나오는 화면을 캡처하고, 인자 번호를 부여하여 보았다. 구지 추가 적인 설명을
달지 않더라도 어떤 의미인지 확실하게 이해할 수 있을거라 생각한다.
 
나머지 인자에 대한 설명을 달아보자.

(3)서비스 액세스 권한 - 말 그대로 서비스를 핸들링할 수 있는 세부 권한을 말한다.
  별일 없으면 그냥 SERVICE_ALL_ACCESS 때려 넣는다.

더보기
SERVICE_ALL_ACCESS (0xF01FF) Includes STANDARD_RIGHTS_REQUIRED in addition to all access rights in this table.
SERVICE_CHANGE_CONFIG (0x0002) Required to call the ChangeServiceConfig or ChangeServiceConfig2 function to change the service configuration. Because this grants the caller the right to change the executable file that the system runs, it should be granted only to administrators.
SERVICE_ENUMERATE_DEPENDENTS (0x0008) Required to call the EnumDependentServices function to enumerate all the services dependent on the service.
SERVICE_INTERROGATE (0x0080) Required to call the ControlService function to ask the service to report its status immediately.
SERVICE_PAUSE_CONTINUE (0x0040) Required to call the ControlService function to pause or continue the service.
SERVICE_QUERY_CONFIG (0x0001) Required to call the QueryServiceConfig and QueryServiceConfig2 functions to query the service configuration.
SERVICE_QUERY_STATUS (0x0004) Required to call the QueryServiceStatusEx function to ask the service control manager about the status of the service.
SERVICE_START (0x0010) Required to call the StartService function to start the service.
SERVICE_STOP (0x0020) Required to call the ControlService function to stop the service.
SERVICE_USER_DEFINED_CONTROL(0x0100) Required to call the ControlService function to specify a user-defined control code.

(4)서버스 타입 - 윈도우즈에서 말하는 서비스라는건 여러가지 의미로 사용되는데 여기서는 현재 설치하려는 무언가가
  무엇인지를 알려주어야 한다. 파일시스템 드라이버(SERVICE_FILE_SYSTEM_DRIVER) 일 수도 있고,
  디바이스 드라이버(SERVICE_KERNEL_DRIVER) 일 수 도 있으며 지금 만들려고 하는 일반 응용프로그램
  수준의 서비스 프로그램(SERVICE_WIN32_OWN_PROCESS) 일 수 도 있다.

(6)서비스 에러 제어 - 만약 시스템 초기에 서비스가 구동하다가 에러가 발생하면 어떻게 할것인가? 에러난 서비스가 파일    시스템  이라면? 키보드 드라이버라면? 내가 만든 간단한 응용 서비스라면? 과 같이.. 서비스의 특성과 중요도에 따라,
  재부팅을 할 수도, 그냥 무시( SERVICE_ERROR_IGNORE) 할 수도 있다.

더보기
SERVICE_ERROR_CRITICAL
0x00000003
The startup program logs the error in the event log, if possible. If the last-known-good configuration is being started, the startup operation fails. Otherwise, the system is restarted with the last-known good configuration.
SERVICE_ERROR_IGNORE
0x00000000
The startup program ignores the error and continues the startup operation.
SERVICE_ERROR_NORMAL
0x00000001
The startup program logs the error in the event log but continues the startup operation.
SERVICE_ERROR_SEVERE
0x00000002
The startup program logs the error in the event log. If the last-known-good configuration is being started, the startup operation continues. Otherwise, the system is restarted with the last-known-good configuration.

아무것도 없는, 서비스 인스톨 함수 딸랑 하나 있는 Windows Console Application 이다.
아래 샘플 소스에 하나씩 하나씩 추가하여 간단한 서비스를 만들어 보도록 하자.

TestService.zip
0.01MB

윈도우에서 프로그램을 개발하다 보면, 응용 어플리케이션으로 만들었을 경우..
이런 저런 한계에 때문에 고민해 보신 분들이 있을 겁니다.

터미널 서버에서 프로그램을 구동시켜 놓았는데, 누군가 세션을 로그 오프 시켜버리면 꺼진다든지
아니면, 서버가 부팅되었는데 아무도 로그온을 시켜 놓지 않은 상태에서 (즉, 부팅만 된 상태)
돌아가야 한다던지.. 기타 등등 서버 계통에서는 많은 부분을 서비스 프로그램으로 개발하게 된다.

또한, 비스타 계열에서는 UAC 때문에 의외로 까탈스런 부분들이 많은데, 시스템을 건드리는 대부분을
서비스에 몰아놓고, 응응프로그램에서는 서비스와 통신하여 불필요한 경고창이 뜨지 않도록 할 수도 있다.

대충 아주 기본적인걸 살펴보면, 설치->시작->중지->제거 형태로 이루어지고 실제적인 수행은 일반 어플과
크게 다르지 않다. 몇몇 제약사항도 있고, 장점도 존재하므로 이런 것들은 나중에 살펴보도록 한다.

위의 과정을 나열하기 전에 알아두어야할 개념으로 SCM (Service Control Manager) 이라는 용어에 익숙해지자.
윈도우즈 시스템에서 서비스를 제어하는 관리자이며 필요한 대부분의 API를 제공해주는 키워드이다.



다음 장 부터, 서비스의 설치, 시작/중지, 서비스의 제거 형태로 먼저 진행을 한 후에..
기본적인 서비스의 틀을 가진 템플릿 형태의 프로그램을 제작하는 단계로 진행해 볼까 한다.

기존에 서비스 프로그램을 다루면서, 프로그램적으로 서비스를 멈추는 것은
단순하게 ControlService(핸들, SERVICE_CONTROL_STOP, 개체포인터);
이런식으로 처리가 가능하다.

그런데? 중지 시키려고 하는 서비스의 종속된 서비스가 존재한다면?
그냥 중지 시키면 ERROR_DEPENDENT_SERVICES_RUNNING 메시지를 받게된다.
즉, 종속된 서비스가 존재하기 때문에 해당 서비스를 중지 할 수 없다.

그럴 경우 EnumDependentServices API를 이용하여, 종속 서비스 목록을 얻어와서
해당 서비스를 하나 하나 전부 종료 시켜주어야 한다.


서비스 프로그램을 작성하다 보면, 디버깅을 할 때 현재 어플리케이션으로
구동중인지 서비스로 구동중인지를 내부적으로 구분할 수 있어야한다.
(여러가지 방법이 있겠지만, 아는게 없어서 ㅜㅜ)

이를 쉽게 해결하는 방법은 다음과 같다.

메인 쯔음해서 GetConsoleWindow() 함수를 호출한 결과를 가지고 여부를
자동으로 구분할 수 있다.

응용 프로그램 모드로 구동하게 되면 위 함수가 HWND 타입의 핸들을 리턴하게 된다.
하지만 시스템 서비스 모드로 구동중이라면 NULL을 리턴한다.

즉, 프로그램 main에서 저 핸들값을 이용하여 StartServiceCtrlDispatcher 를 호출할 것인지
디버그용 메인을 호출할 것인지를 결정할 수 있는 것이다.

곁들여서 GetUserName() 를 이용하여 현재 어떤 계정으로 구동하는지도
첨부하면 금상첨화겠죠?

윈도우즈 서비스 프로그램을 작성할 때
두가지 형태의 파라미터를 전달 받을 수 있습니다.

1. 서비스파일.exe  -install -u:aaa -p:pass 기타등등
2. 서비스 관리자 -> 시작 매개변수

이렇게 두가지로 전달 받을 수 있는데요..
1번과 같은 경우는 일반적인 콘솔 프로그램과 다를바가 없지요.
즉, main 의 argc, argv 파라미터를 바로 처리하면 됩니다.

2번도 사실 1번과 거의 동일합니다.
다만 파라미터를 검출하는 위치만 다를뿐 100% 같은 방식을 사용합니다.
차이점이라면 StartServiceCtrlDispatcher 에서 등록한 서비스 메인 함수에서
파라미터를 처리한다는 차이점이 있습니다. ^^;

+ Recent posts