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) 동작도 처리해보자.

간단하게 서비스를 설치하는 기능에 대하여 설명하여 보았습니다.
서비스를 설치할 때 CreateService에 들어가는 인자들을 살펴보면 더 많은 무언가가 있어보이지만..
그건 진행하면서 필요할 때 하나씩 추가하는 걸로 해보죠..

자, 이제 서비스를 설치해봤으니, 제거를 해봐야 겠죠?
설치는 간단하지만, 제거는 좀더 복잡한 과정을 거쳐야합니다.

일단, 주의 사항으로 서비스는 제거하기 전에 꼭 멈추고 제거시켜야 한다는 것을 명심하세요.
단계는 서비스 열고, 중지 시키고, 제거한다.


DWORD ServiceUninstall()
{
    // 에러 처리를 위한 간단한 매크로
    #define ERROR_RETURN { RET = GetLastError(); goto ERROR_LABEL; }
    
    SC_HANDLE hSrv;
    DWORD RET = ERROR_SUCCESS;
    DWORD dwType;
    int loop = 0;
    SERVICE_STATUS ss;

    // SCM 을 열어서 서비스에 작업을 진행할 수 있도록 핸들을 하나 달라고 하자.
    SC_HANDLE hScm = OpenSCManager(NULL, NULL, SC_MANAGER_CREATE_SERVICE);
    
    // 열지 못하면 에러다. 머 어쩔 수 없다. 왜 그런지 에러 코드나 리턴한다.
    if (hScm == NULL)
        return GetLastError();
   
   
    // 서비스 관리자를 열었으니, 이제 서비스를 직접 열어보자.
    hSrv = OpenService(hScm, S_NAME, SERVICE_ALL_ACCESS);
    if (hSrv == NULL)
        ERROR_RETURN;
   
   
    // 관리자를 열어으면, 이젠 서비스의 상태를 읽어오자.
    // 서비스를 제거하려면 먼저, 서비스를 멈추어야 한다.

    loop = 0;
    do
    {
        ControlService(hSrv, SERVICE_CONTROL_INTERROGATE, &ss);
        Sleep(1);
    } while (++loop <10);
    dwType = ss.dwCurrentState;
   
   
    // 만약 서비스의 상태가 멈춤이 아니면, 서비스를 중지 시키자.
    if(dwType != SERVICE_STOPPED)
    {
        if(!ControlService(hSrv, SERVICE_CONTROL_STOP, &ss))
            ERROR_RETURN;
    }
   
   
    // 아주 무식한 방법으로, 서비스가 멈출 때 까지 기다린다.
    // 하지만 서비스를 멈춰야 하는데, 멈추지 않는다면?
    // 서비스를 제작한 사람을 쪼아야지.. 불쌍한 서비스 제거
    // 프로그램이 먹통되었다고 화내면 건강에 해롭다.
   
    // 만약 서비스를 멈추지 않고, 동작중인데 DeleteService를
    // 이용하여 제거하면, 컴터를 리부팅 하기 전까지는 다시
    // 설치가 불가능하다. (서비스 내부 디비엔트리에 먼가 찌꺼기가 남아서 안된다는데..흠...)

    do
    {
        Sleep(100);
       
        loop = 0;
        do
        {
            ControlService(hSrv, SERVICE_CONTROL_INTERROGATE, &ss);
            Sleep(1);
        } while (++loop <10);
        dwType = ss.dwCurrentState;
    } while(dwType != SERVICE_STOPPED);
   
   
    // 멈추어졌으면 제거한다.

    if(!DeleteService(hSrv))
        ERROR_RETURN;
   
ERROR_LABEL:
    if(hSrv)
        CloseServiceHandle(hSrv);
    if(hScm)
        CloseServiceHandle(hScm);
   
    return RET;
}

와우!!! 잘 살펴보면 알겠지만, 서비스를 제거할 때 필요한 것은 서비스를 제거할 수 있는 권한과, 서비스 이름만 알면된다.

개념도 없이, 아무것도 없이 갑자기 웬? 서비스의 설치? 코딩 한줄도 못만들어 봤는데.. -_-???

작성한 서비스 프로그램을 돌려 보려면, 일단 서비스를 설치해서 구동해야 한다.
그냥 콘솔 상태에서 아무리 디버깅해봐야 실제로 서비스 레이어에서 구동할 때 정상적인지...
실제로 서비스로 돌아는 가는지 확인해 봐야 할것 아닌가?

일단 아주 심플한 서비스 설치용 함수이다.
인자 중에 NULL 라고 된것들은 첨에 알아봐야 머리만 아프고
별로 쓸일이 없는 경우이다. 예제 만들다 필요하면 설명이 추가될 수 도 있을것 같은데 잘 모르겠다.

해당 소스를 사용하려면
   헤더파일 : #include <windows.h>
   라이브러리 : Advapi32.lib
                   : 코드로 라이브러리를 추가하려면 #pragma comment(lib, "Advapi32.lib")
가 있어야 한다.

DWORD ServiceInstall()
{
    DWORD RET = ERROR_SUCCESS;
    SC_HANDLE hSrv;

    // SCM 을 열어서 서비스에 작업을 진행할 수 있도록 핸들을 하나 달라고 하자.
    // 지금 할 작업은 새로운 서비스를 만들어 등록하는 작업이다.
    SC_HANDLE hScm = OpenSCManager(NULL, NULL, SC_MANAGER_CREATE_SERVICE);
        
    // 열지 못하면 에러다. 머 어쩔 수 없다. 왜 그런지 에러 코드나 리턴한다.
    if (hScm == NULL)
        return GetLastError();
           
    // SCM 을 서비스 생성 권한으로 열었으니, 서비스를 만들어야지..
    // 함수에 필요한 인자들을 채워서 넣어주면 된다. 
    // 첫번째 인자는 SCM 에서 넘겨준 핸들이고 나머지는 밑에다가 추가적으로 설명을 단다.

    hSrv = CreateService(
        hScm,
        (1)서비스 이름,
        (2)서비스 표시 이름,
        (3)서비스 액세스 권한,
        (4)서버스 타입,
        (5)서비스 시작 타입,
        (6)서비스 에러 제어,
        (7)서비스 파일 경로,
        NULL,
        NULL,
        NULL,
        NULL,
        NULL);
    
    // 서비스 생성이 실패하면 별수 없다. 여러가지 이유가 있을 테니까..
    // 에러 코드나 리턴하자.

    if (hSrv == NULL)
    {
        RET = GetLastError();
        CloseServiceHandle(hScm);
        return RET;
    }
    else
    {
        // 와우 서비스 생성에 성공했네...
        // 서비스 관리자 (services.msc) 를 열어보면 해당 서비스마다, 친절하게 이건 무신 무신 서비스 입니다.
        // 라는 설명을 보았을 것이다. 그와 같은 설명을 달아주는 부분이다.
        // 구지 설명 달아줄 필요가 없으면 건너 띄어도 무방하다.
        lpDes.lpDescription=(8)서비스의 세부 설명;
        if(!ChangeServiceConfig2(hSrv, SERVICE_CONFIG_DESCRIPTION, &lpDes))
        {
            RET = GetLastError();
            CloseServiceHandle(hScm);
            CloseServiceHandle(hSrv);
            return RET;
        }
        
        CloseServiceHandle(hScm);     
        return CloseServiceHandle(hSrv) ? ERROR_SUCCESS : GetLastError();
    }
}

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

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

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


서비스 프로그램을 작성하다 보면..
간혹 데스크탑과 연동해야 하는 일이 발생한다.

입력없는 콘솔 프로그램 같은 경우는 그냥 서비스에서 바로 수행하면 되지만
입력을 받는 콘솔이나, GUI를 가진 프로그램을 띄워야할 경우는
프로세스 목록에만 나오고.. 깜깜 무소식이다.

일반 응용프로그램에서야 WTSRegisterSessionNotification 함수를 이용하여 아주 쉽게
로그온된 시점을 구할 수 있지만 불행하게도 서비스에서는 사용할 수 없다.


또한 서비스 시작시에 응용 프로그램을 수행시키면, 아직 로그온 되기 전이기때문에
저런 설정을 다 해놓는다 하여도 로그온된 화면에는 아무것도 보이지 않을 수 있다.

1. 꼭 서비스로 관리해야하는가?
2. 입력을 가지는 콘솔이나 GUI를 가졌는가?
3. 서비스 구동시에 시작해야 하는가?
4. 로그온 된 데스크탑에 보여주어야 하는가?

위 사항들을 잘 판단하고 그래도 꼭 필요하다면 다음과 같은 정보를 구해야한다.
1. 서비스로 개발하기
2. 데스크탑과 연동하는 옵션
3. 로그온된 시점 구하기

1. 서비스로 개발하기
위 사항은 이미 자료가 많이 나와있고, 대부분 알려진 자료이므로 그냥 좋은 자료를 찾는다 ^^;

2. 데스크탑과 연동하는 옵션
       첫번째로 가장 쉬운 방법은 서비스 관리자를 이용하는 방법이다.

사용자 삽입 이미지

     그림에서 보이는것 처럼 로그온 설정시에 서비스와 데스크탑 상호 작용 허용에 첵크를
     해줌으로써 아주 간단하게 해결할 수 있다.

     두번째는 서비스를 인스톨할 때, 즉 CreateService 함수를 이용하여 서비스를 설치할 때
     다섯번째 인자에 SERVICE_INTERACTIVE_PROCESS 를 함께 넣어주는 것이다.
     이것은 첫번째 방법을 프로그램적으로 해결하는 것이다.

3. 로그온된 시점 구하기
로그온된 시점을 구하는 방법도 그리 어렵지 않다. 하지만 제약사항이 많아서..
특히나 지금 처리하는 방법은 Windows XP 이상, Windows Server 2003 이상에서만
동작하는 방법이므로, 하위 버전 윈도우를 처리하는 경우에는 해당사항이 없다. -_-;;;
(예전에는 이벤트 로그를 뒤적거려서 처리했었다...)

서비스를 핸들하기 위하여 RegisterServiceCtrlHandler 함수 대신에 좀더 구체적인 정보를
전달해주는 RegisterServiceCtrlHandlerEx 를 이용하는 방법이다.

RegisterServiceCtrlHandlerEx API는 시스템의 세션정보 변화를 감지하는 이벤트인
WM_WTSSESSION_CHANGE 를 처리할 수 있도록 해준다.

SetServiceStatus 에서 SERVICE_STATUS 값의 dwControlsAccepted 값에 꼭
SERVICE_ACCEPT_SESSIONCHANGE 를 함께 넣어주도록 하자.

이렇게 RegisterServiceCtrlHandlerEx 에 전달된 콜백함수에는 다음과 같은 인자가 있다.
DWORD WINAPI HandlerEx(
  [in]                 DWORD dwControl,
  [in]                 DWORD dwEventType,
  [in]                 LPVOID lpEventData,
  [in]                 LPVOID lpContext
);

저기서 첫번째 인자에 SERVICE_CONTROL_SESSIONCHANGE 가 넘어오면 로그온 세션에
무언가 변화가 생긴것이다.

두번째 인자로 어떤 변화가 발생하였는지를 감지할 수 있다. MSDN의 설명을 참고해보면...
Value Meaning

WTS_CONSOLE_CONNECT
0x1

A session was connected to the console terminal.

WTS_CONSOLE_DISCONNECT
0x2

A session was disconnected from the console terminal.

WTS_REMOTE_CONNECT
0x3

A session was connected to the remote terminal.

WTS_REMOTE_DISCONNECT
0x4

A session was disconnected from the remote terminal.

WTS_SESSION_LOGON
0x5

A user has logged on to the session.

WTS_SESSION_LOGOFF
0x6

A user has logged off the session.

WTS_SESSION_LOCK
0x7

A session has been locked.

WTS_SESSION_UNLOCK
0x8

A session has been unlocked.

WTS_SESSION_REMOTE_CONTROL
0x9

A session has changed its remote controlled status. To determine the status, call GetSystemMetrics and check the SM_REMOTECONTROL metric.


그러므로 위시점에서 WTS_SESSION_LOGON 일 경우에 프로그램을 기동시키면 정확하게
로그온된 시점에 프로그램이 구동하게 된다.

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

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

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

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

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

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

이것 때문에 헤맨 시간을 생각하면.. ㅇㅎㅎ ㅠㅠ~~

윈도우즈 서비스로 제작한 프로그램을 서비스에 등록할 경우..
로컬시스템 계정이나 암호를 가진 유저는 잘 등록이 되고
동작도 잘됩니다.

하지만 빈문자열로 암호를 가지지 않은 계정은 서비스가 로그온에 실패하여
서비스가 정상적으로 수행되지 못합니다.

이걸 프로그램적으로 해결하기 위해 백방으로 찾아봤지만..
해결을 못했었는데 이렇게 허무할 수가 ㅠㅠ;

http://support.microsoft.com/kb/303846/ko


오류 메시지: 계정 제한 때문에 로그온할 수 없습니다

기술 자료 ID : 303846
마지막 검토 : 2001년 11월 21일 수요일
수정 : 1.0
이 문서는 이전에 다음 ID로 출판되었음: KR303846

현상

원격 데스크톱 도구를 사용하여 Windows XP 기반 컴퓨터에 연결하려고 하면 아래의 오류 메시지가 나타날 수도 있습니다.
계정 제한 때문에 로그온할 수 없습니다.

위로 가기

원인

연결하는 데 사용한 계정에 암호가 없으면 이러한 문제가 발생할 수 있습니다. 암호가 없는 계정을 사용할 때는 원격 데스크톱 연결을 설정할 수 없습니다.

위로 가기

해결 방법

원격 데스크톱 연결을 설정할 수 있도록 이 문제를 해결하려면 해당 컴퓨터의 콘솔에서 로그온한 다음 해당 사용자 계정에 대해 암호를 설정하십시오.

위로 가기

현재 상태

이것은 의도적으로 설계된 동작입니다.

위로 가기

추가 정보

정책을 사용하여 빈 암호 제한을 해제할 수 있습니다. 이 정책을 찾아서 변경하려면 다음과 같이 하십시오.
1. 시작을 누르고, 실행을 누르고, gpedit.msc를 입력한 다음 확인을 눌러 그룹 정책 편집기를 시작합니다.
2. 컴퓨터 구성\Windows 설정\보안 설정\로컬 정책\보안 옵션\계정: 콘솔 로그온 시 로컬 계정에서 빈 암호 사용 제한을 엽니다.
3. 콘솔 로그온 시 로컬 계정에서 빈 암호 사용 제한을 두 번 누릅니다.
4. 사용 안함을 누른 다음 확인을 누릅니다.
5. 그룹 정책 편집기를 종료합니다.
참고: 기본적으로 이 정책은 사용함으로 설정되어 있습니다.

+ Recent posts