프로그램을 작성하다 보면, 처음에 제일 문제였던것이 bio 설정에 관한 것이었다.
컴파일해서 잘 돌아가다가 bio 에 멀 쓰던가 하면 OPENSSL_Uplink(00509010,07): no OPENSSL_Applink 
위와 같은 에러가 발생하여 사람 참~ 난감하게 만든다.

처음에는 저걸 해결하지 못해서, bio 쓰는 부분을 몽땅 털어버리는 무식한 노가다를 하기도 했었다.

혹시나 비슷한 에러로 고생하는 사람이 없도록 http://www.openssl.org/support/faq.html 에 나와 있는 내용중에
위 에러를 해결하기 위한 방법을 적어본다.

아래는 해당 사이트 원문이다.
결국은 프로그램을 설정할 때 스레드 모델을 아래 사항에 맞도록 설정해 주면 아주 미끈하게 잘 돌아간다 -_-;;;
디버깅 하느라 날려버린 내 아까운 시간 흐~~

2. I've compiled a program under Windows and it crashes: why?

This is usually because you've missed the comment in INSTALL.W32. Your application must link against the same version of the Win32 C-Runtime against which your openssl libraries were linked. The default version for OpenSSL is /MD - "Multithreaded DLL".

If you are using Microsoft Visual C++'s IDE (Visual Studio), in many cases, your new project most likely defaulted to "Debug Singlethreaded" - /ML. This is NOT interchangeable with /MD and your program will crash, typically on the first BIO related read or write operation.

For each of the six possible link stage configurations within Win32, your application must link against the same by which OpenSSL was built. If you are using MS Visual C++ (Studio) this can be changed by:

 1. Select Settings... from the Project Menu.
 2. Select the C/C++ Tab.
 3. Select "Code Generation from the "Category" drop down list box
 4. Select the Appropriate library (see table below) from the "Use
    run-time library" drop down list box.  Perform this step for both
    your debug and release versions of your application (look at the
    top left of the settings panel to change between the two)

Single Threaded /ML - MS VC++ often defaults to this for the release version of a new project. Debug Single Threaded /MLd - MS VC++ often defaults to this for the debug version of a new project. Multithreaded /MT Debug Multithreaded /MTd Multithreaded DLL /MD - OpenSSL defaults to this. Debug Multithreaded DLL /MDd

Note that debug and release libraries are NOT interchangeable. If you built OpenSSL with /MD your application must use /MD and cannot use /MDd.

As per 0.9.8 the above limitation is eliminated for .DLLs. OpenSSL .DLLs compiled with some specific run-time option [we insist on the default /MD] can be deployed with application compiled with different option or even different compiler. But there is a catch! Instead of re-compiling OpenSSL toolkit, as you would have to with prior versions, you have to compile small C snippet with compiler and/or options of your choice. The snippet gets installed as <install-root>/include/openssl/applink.c and should be either added to your application project or simply #include-d in one [and only one] of your application source files. Failure to link this shim module into your application manifests itself as fatal "no OPENSSL_Applink" run-time error. An explicit reminder is due that in this situation [mixing compiler options] it is as important to add CRYPTO_malloc_init prior first call to OpenSSL.


소스 컴파일을 마치고, 필요한 프로젝트에 넣어서 사용하는데는 별 문제가 없지만..
역시나 문제가 생기면 디버깅을 해야죠.

ms\do_ms.bat 를 보시면 다음과 같이 나와있습니다.

perl util\mkfiles.pl >MINFO
perl util\mk1mf.pl no-asm VC-WIN32 >ms\nt.mak
perl util\mk1mf.pl dll no-asm VC-WIN32 >ms\ntdll.mak
perl util\mk1mf.pl no-asm VC-CE >ms\ce.mak
perl util\mk1mf.pl dll no-asm VC-CE >ms\cedll.mak
perl util\mkdef.pl 32 libeay > ms\libeay32.def
perl util\mkdef.pl 32 ssleay > ms\ssleay32.def

위의 환경은 일반적인 디버깅이 빠진 상태의 메이크 파일을 만드는 과정이고..
아래와 같이 debug 라고 하는 키워드를 추가하시면 디버깅이 가능한 메이크 파일을 생성해 줍니다.

perl util\mkfiles.pl >MINFO
perl util\mk1mf.pl no-asm VC-WIN32 >ms\nt.mak
perl util\mk1mf.pl dll debug no-asm VC-WIN32 >ms\ntdll.mak
perl util\mk1mf.pl no-asm VC-CE >ms\ce.mak
perl util\mk1mf.pl dll no-asm VC-CE >ms\cedll.mak
perl util\mkdef.pl 32 libeay > ms\libeay32.def
perl util\mkdef.pl 32 ssleay > ms\ssleay32.def

저렇게 수정하신 후 command line 에서 ms\do_ms.bat 를 돌리시면 새로운 환경 설정이 적용된 ntdll.mak 파일 생성됩니다.
해당 파일을 열어 보시면 다음과 같이 바껴 있는걸 볼 수 있습니다.

CFLAG= /MDd /Od -DDEBUG -D_DEBUG /W3 /WX /Gs0 /GF /Gy /nologo -DOPENSSL_SYSNAME_WIN32 -DWIN32_LEAN_AND_MEAN -DL_ENDIAN -DDSO_WIN32 -D_CRT_SECURE_NO_DEPRECATE -D_CRT_NONSTDC_NO_DEPRECATE -DOPENSSL_USE_APPLINK -I. /Fdout32dll -DOPENSSL_NO_CAMELLIA -DOPENSSL_NO_SEED -DOPENSSL_NO_RC5 -DOPENSSL_NO_MDC2 -DOPENSSL_NO_TLSEXT -DOPENSSL_NO_CMS -DOPENSSL_NO_CAPIENG -DOPENSSL_NO_KRB5 -DOPENSSL_NO_DYNAMIC_ENGINE    

일단 저렇게 만들어서 컴파일을 해보니 out32dll.dbg tmp32dll.dbg 처럼 디버깅용 폴더가 생기면서 필요한 파일이 작성됩니다.
그런데, 메뉴얼 대로 다 해서 소스와 물려 돌려봤는데, -_-??? 역시나 디버깅시 openssl 함수 호출부를 추적하면 내부로 못들어 가네요.

CFLAG= /nologo /MDd /W3 /Gm /GX /ZI /Od /I /FR"Debug/" -DDEBUG -D_DEBUG   -DOPENSSL_SYSNAME_WIN32 -DWIN32_LEAN_AND_MEAN -DL_ENDIAN -DDSO_WIN32 -D_CRT_SECURE_NO_DEPRECATE -D_CRT_NONSTDC_NO_DEPRECATE -DOPENSSL_USE_APPLINK -I. /Fdout32dll -DOPENSSL_NO_RC5  -DOPENSSL_NO_MDC2 -DOPENSSL_NO_KRB5 -DOPENSSL_NO_DYNAMIC_ENGINE -DOPENSSL_NO_TLSEXT -DOPENSSL_NO_CMS -DOPENSSL_NO_CAPIENG -DOPENSSL_NO_CAMELLIA -DOPENSSL_NO_SEED

위에 꺼는 이전에 맨땅에 해딩하면서 만들었던 (0.9.8b 버전 당시..) 내용을 추려서, 0.9.8i 버전에 맞도록 수정한 내용입니다.

이렇게 수정해 놓고, 다시 몽땅지우고 재컴파일 한 후, 새로 생성된 라이브러리와 dll을 복사해놓고.. 
작업 프로그램을 빌드 한 후, 소스 추적에 들어가니 ssl 소스로도 잘 추적이 들어가네요.. 

수고하세요..

PS. 다 만들어진 라이브러리를 작업하는 프로젝트에 첨부하고 빌드했을 때, unresolved external symbol 류의 에러가 발생하면
      ssleay32.def 에 해당 함수를 추가 하시면 됩니다. 참고로 ssleay32.def의  마지막 오디너리 넘버가 293번 이니까..
      294번 부터 추가하세요.. 그리고, openssl 을 다시 한번 빌드하셔서 복사하시고 사용하시면 됩니다.


 

지금까지 0.9.8b 버전을 사용하다가, 추가된 cipher를 지원하기 위하여 0.9.8i 버전을 사용하게 되었습니다.
이거 받아서 컴파일 하다보니, 이전 버전과는 또 다른 문제가 튀어나오네요..ㅎㅎ;
이런 종류의 라이브러리 받아서 처음 컴파일하다보면 당황스런 경우가 많은데요 도움이 되셨으면 합니다.

1. ActivePerl 다운 받아서 설치하세요.
   http://www.activestate.com/Products/activeperl/index.mhtml
   경로에서 다운 받고, 그냥 인스톨 하시면 됩니다.

2. Perl 의 사용상 편의를 위해서 Perl의 설치 경로를 환경 변수에 등록해 주시면
   지속적으로 사용하는데 아주 편합니다.

   
   제어판->시스템->고급 탭을 선택합니다.
 


  
  환경변수를 클릭한다.
 


  편집을 클릭한다.
 


  제일 끝에, 세미콜론을 하나 찍고, 펄의 bin 경로를 추가한다.
  이렇게 되면 어떤 경로에서든 Perl을 바로 실행할 수 있다.

3. 다음으로 VC-WIN32 환경에서 컴파일 할수 있도록 컴파일 환경을 설정한다.
   => perl Configure VC-WIN32 --prefix=c:/some/openssl/dir
   => 위의 예에서 처럼 오픈SSL을 압축 풀어서 놓은 경로를 지정하여 환경을 설정한다.

4. 필요한 Makefile과 관련 오브젝트를 생성한다.
   openssl의 경로로 가서, 콘솔창에 아래의 명령중에 하나를 입력한다.
   별문제 없이 넘어가고 싶으면 ms\do_ms 를 추천한다.

   => masm 이 있으면 > ms\do_masm
   => nasm 이 있으면 > ms\do_nasm
   => 어셈블러 없으면> ms\do_ms

5. 이제 본격적으로 컴파일을 해보자.

   => nmake -f ms\ntdll.mak
   => 처음에 컴파일을 하면 제일 먼저 만나는 에러다, 에러 문구는 이것 저것 복잡한게 있지만 필요한 내용은 이거다.
  
  .\crypto\cversion.c(105) : warning C4129: 'k' : unrecognized character escape sequence

   => 해당 파일을 소스를 에디트 할수 있는 툴을 이용하여 열어보자.
   => 저위에서 'k'는 프로그램의 설치경로에 따라 각각 다른 캐릭터로 나타날 수 있다.

   #ifdef OPENSSLDIR
      return "OPENSSLDIR: \"" OPENSSLDIR "\"";
   #else
      return "OPENSSLDIR: N/A";
   #endif

   위의 빨간색 부분을 아래와 같이 바꾼다.

   #ifdef OPENSSLDIR
      return "OPENSSLDIR: \" OPENSSLDIR \"";
   #else
      return "OPENSSLDIR: N/A";

  대충 원인을 살펴보면, 프로그램(openssl) 설치 경로를 를 나타내는데 "가 앞뒤로 하나 더 붙어있어서 발생하는 문제이다.
  아마도 윈도우와 유닉스의 경로 구분문자 차이 때문에 발생하는 것으로 추측된다.

  이걸 잘 처리하고 컴파일을 다시 진행하면 다음과 같은 또다른 비슷한 유형의 에러를 만나게 된다.

  .\crypto\x509\x509_def.c(65) : warning C4129: 'K' : unrecognized character escape sequence
   => 열심히 해당 파일을 열어서 살펴보자.

   => ./crypto/x509/x509_def.c
   아래의 빨간색 항목을 주목하자, 윈도우즈에서는 저걸 프로그램 코드에 밖기 위해서는 역슬래시 두개로 바꾸어 주어야한다.
   #ifndef OPENSSL_SYS_VMS
   #define X509_CERT_AREA  OPENSSLDIR
   #define X509_CERT_DIR  OPENSSLDIR "/certs"
   #define X509_CERT_FILE  OPENSSLDIR "/cert.pem"
   #define X509_PRIVATE_DIR OPENSSLDIR "/private"
  
   => 자 다음과 같이 바꾸자 (웹에디터 때문인지, 글자 아래 밑줄이 보이는데 무시하자.)
   #ifndef OPENSSL_SYS_VMS
   #define X509_CERT_AREA  OPENSSLDIR
   #define X509_CERT_DIR  OPENSSLDIR "\\certs"
   #define X509_CERT_FILE  OPENSSLDIR "\\cert.pem"
   #define X509_PRIVATE_DIR OPENSSLDIR "\\private"

   위에서 살펴보면 OPENSSLDIR 라는 디파인이 보인다. 이것도 무언지 살펴보면 또다른 문제가 나타난다.

   => ./crypto/opensslconf.h
   => 아래는 컴파일 환경설정 경로이기 때문에, 사용자 설치 경로에 따라 다를 수 있다.
   #if !(defined(VMS) || defined(__VMS)) /* VMS uses logical names instead */
   #if defined(HEADER_CRYPTLIB_H) && !defined(OPENSSLDIR)
   #define ENGINESDIR "D:\K4WORK\2_SRC_LIBRARY\k4lib_openss0.98i/lib/engines"
   #define OPENSSLDIR "D:\K4WORK\2_SRC_LIBRARY\k4lib_openss0.98i/ssl"

   #endif

   => 요거를 아래꺼로 변경
   #if !(defined(VMS) || defined(__VMS)) /* VMS uses logical names instead */
   #if defined(HEADER_CRYPTLIB_H) && !defined(OPENSSLDIR)
   #define ENGINESDIR "D:\\K4WORK\\2_SRC_LIBRARY\\k4lib_openss0.98i\\lib\\engines"
   #define OPENSSLDIR "D:\\K4WORK\\2_SRC_LIBRARY\\k4lib_openss0.98i\\ssl"

   #endif

   이렇게 모든 작업이 끝나면 한방에 컴파일이 완료된다.
   자 이제 컴파일을 하면 끝...

이전에도 언급한적이 있지만, 역시나 VS에 물려서 디버깅은 않됩니다.. 그냥 돌아만 갈뿐...
아마도 이버전도 디버거에서 openssl 소스를 추적해 들어가려고 하면, ntdll.mak 파일을 수정해서
디버깅이 가능하도록 하는 버전과, release 에서 배포할 때 성능향상을 할 수 있도록 2가지 버전으로
모두 수정해서 처리해야 할거 같네요.

디버깅 버전을 생성하는건 다음에 시간나면 또 한번 올려보도록 하겠습니다.

서비스를 설치 하고, 제거 할 수 있으며 서비스를 시작하면 1초마다 윈도우즈 기본 에러메시지.. ~ 이 나옵니다.
서비스를 일시 중지 시키면 동작이 멈추고, 재시작하면 다시 띵~ 이러한 구조로 흘러갑니다.
간단하게 util.h util.cpp 를 만들어서 구조와 크게 상관없는 함수들을 모아놓았습니다.

#include "util.h"
#pragma comment(lib, "Advapi32.lib")

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

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

// 서비스의 상태를 저장하는 플래그
DWORD  srvst = SERVICE_STOPPED;

int main(int argc, char** argv)
{
    // 실행파일의 경로를 구한다.
    char S_BINARY[MAX_PATH] = {0};
    ::GetModuleFileName(NULL, S_BINARY, MAX_PATH);

    // 인자가 2개이고, 두번째 인자가 'i' 이면 인스톨
    if(argc == 2 && argv[1][0] == 'i')
    {
        ServiceInstall();
        return 0;
    }

    // 인자가 2개이고, 두번째 인자가 'u' 이면 언인스톨
    else if(argc == 2 && argv[1][0] == 'u')
    {
        ServiceUninstall();
        return 0;
    }

    SERVICE_TABLE_ENTRY STE[] =   
    {
        {(char*)S_NAME, (LPSERVICE_MAIN_FUNCTION)_tmain_service},
        {NULL,NULL}
    };
    // 서비스 메인 함수를 등록한다.
    if(StartServiceCtrlDispatcher(STE) == FALSE)
        return -1;

    return 0;
}

 VOID _tmain_service(INT ARGC, LPSTR* ARGV)
{
    srvhd = RegisterServiceCtrlHandlerEx(S_NAME, _tmain_service_handler, NULL);
    if (srvhd == NULL)
        return;       

    // 정상적으로 서비스가 시작되었다.
    // 서비스의 기본적인 작업이 끝나고 메인 루프로 들어가기 전에 꼭 상태를
    // 런닝 상태로 만들어주어야 한다
    // 이걸 처리해놓지 않으면, 서비스 관리자에서 정상적으로 시작된건을 인지하지 못한다.
    SET_SERVICE_STATE(srvhd, SERVICE_RUNNING);

    // 무한 루프를 돌면서 띵띵~ 소리를 낸다.
    while(GET_SERVICE_STATE() != SERVICE_STOPPED)
    {
        // 일시 중지 상태일 때는 그냥 별동작 없이 대기를 반복한다.
        if(GET_SERVICE_STATE() == SERVICE_PAUSED)
        {
            Sleep(1000);
            continue;
        }

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

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

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

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

    case SERVICE_CONTROL_STOP:
        SET_SERVICE_STATE(srvhd, SERVICE_STOP_PENDING, 0);
        // 서비스를 멈춘다 (, 종료와 같은 의미)
        SET_SERVICE_STATE(srvhd, SERVICE_STOPPED);
        break;     

    default:
        break;
    }

    return NO_ERROR;
}

// 아래 파일은 샘플 프로젝트입니다.

TestService.zip
0.01MB


다음에는 개인용 피씨나, 서버 피씨에 어떤 사용자가 접속하고, 종료했는지 터미널 서버로 들어왔다면
어떤 IP주소로 접속했는지 등등등.. '내컴퓨터 누가 건드렸어..' 서비스를 만들어 보도록 하겠습니다.

출처: https://crowback.tistory.com/184?category=86281 [까막's 블로그:티스토리]


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

간단하게 서비스를 설치하는 기능에 대하여 설명하여 보았습니다.
서비스를 설치할 때 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;
}

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

이전 장에서 나온 인자에 대하여 간단하게 설명을 추가하도록 한다.
        (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

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

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

일단 아주 심플한 서비스 설치용 함수이다.
인자 중에 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();
    }
}
윈도우에서 프로그램을 개발하다 보면, 응용 어플리케이션으로 만들었을 경우..
이런 저런 한계에 때문에 고민해 보신 분들이 있을 겁니다.

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

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

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

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



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

+ Recent posts