사용자 도구

사이트 도구


kb:windowsservice

Service

윈도우즈계의 데몬(daemon). 즉 백그라운드에서 항상 떠있으면서 다른 프로그램 또는 클라이언트의 요청을 처리하는 프로그램이다. 즉 클라이언트보다는 서버 쪽과 관련이 깊다.

일반적인 프로그램과의 차이는 대강 다음과 같다.

  • 기본적으로 GUI를 사용할 수 없다. (완전히 사용할 수 없는 것은 아니다.)
  • 시스템에 사용자가 로그인하지 않은 상태에서도 실행이 가능하다.
  • 크래시된 경우 실행할 행동을 지정할 수 있다.
  • SCM(Service Control Manager)과의 통신을 통해, 실행을 시작/중지/일시중지/계속할 수 있다. 다른 컴퓨터에 있는 서비스를 실행하는 것도 가능하다.

두가지 모드로 실행하기

하나의 실행 파일을 어쩔 때는 그냥 커맨드 라인 프로그램으로, 어쩔 때는 서비스로 실행하기에 관한 내용이다.

일단 실제 메인 루프가 들어가는 함수를 따로 분리한다. 이 함수를 usermain 함수라고 하자.

그 다음 커맨드 라인 인수를 이용해 일반 프로그램으로 실행할 것인지, 아니면 서비스로 실행할 것인지의 여부를 알아낸다. 일반 프로그램으로 실행한다면 usermain 함수를 그냥 호출해주면 된다. 서비스로 실행한다면, RegisterServiceCtrlHandlerEx 호출 후에, StartServiceCtrlDispatcher 함수에 넘기는 구조체에다가 usermain 함수 포인터 주소를 채워넣으면 된다.

디버깅의 편이성을 위해, 콘솔 처리를 해준다. 즉 일반 프로그램일 경우에는 표준출력(stdout)을 사용하고, 서비스일 경우에는 명시적으로 콘솔을 할당해서 사용하도록 만드는 것이다. 이 콘솔은 서비스가 SERVICE_INTERACTIVE_PROCESS 속성을 가지고 있어야만 보인다. 속성을 설정해주지 않으면 보이지도 않는 가상의 스테이션에 콘솔이 떠있게 된다.

가장 간단한 콘솔 출력, 실제로는 스레드 문제를 고려해줘야 한다.

extern HANDLE g_hConsole; // 콘솔 핸들
 
...
 
// 콘솔 초기화하기
void InitConsole()
{
    // 일반 콘솔 모드 프로그램, 즉 콘솔이 이미 존재하는 프로그램이라면,
    // AllocConsole 함수는 실패한다. 그러나 성공/실패 여부는 별 상관 없다.
    // 새로 생성한 콘솔이건 원래 있던 콘솔이건 GetStdHandle 함수 호출을
    // 통해 핸들을 얻어올 수 있기 때문이다.
    ::AllocConsole();
    g_hConsole = ::GetStdHandle(STD_OUTPUT_HANDLE);
}
 
// 콘솔에 출력하기
void WriteToConsole(const char* buf)
{
    if (g_hConsole != INVALID_HANDLE_VALUE)
    {
        DWORD dwBytes = 0; 
        ::WriteFile(g_hConsole, buf, (DWORD)::strlen(buf), &dwBytes, NULL); 
    }
    else
    {
        std::cout << buf;
    }
}

서비스 계정

서비스는 따로 설정해주지 않는 이상 기본적으로 “Local System” 계정으로 실행되는데, 이 넘은 몇가지 제한 사항을 가지고 있다.

  • HKEY_CURRENT_USER 밑의 레지스트리는 액세스하기가 애매모호하다. 기본 프로파일 값을 사용하게 되는데, 그냥 속편하게 HKEY_LOCAL_MACHINE을 사용하는 것이 낫다.
  • 랜에 연결되어 있는 다른 컴퓨터에 접근하기가 어렵다. 로컬 시스템 계정은 자신의 컴퓨터에서는 “Users” 그룹과 같은 권한을 가지지만, 네트워크로 나가면 널 세션을 사용하게 된다. 널 세션은 credential이 전혀 없는 세션이다. 윈도우즈 보안 쪽으로 자세히 모르는 관계로 credential이 정확하게 뭔지는 잘 모르겠다만, 중요한 건 일단 네트워크로 나가면 다른 컴퓨터에서는 guest 정도 취급이라는 것이다.

이러한 여러 제약 사항을 피하기 위해서는 서비스 실행 계정을 애초에 지정해주거나, impersonation을 이용해야 한다. 서비스 계정 지정하는 거야 서비스 스냅인에서 처리하거나, sc 같은 프로그램을 이용해 등록해주면 되고, impersonation은 대충 아래와 같은 코드를 통해 처리할 수 있다.

bool ImpersonateUser(LPCTSTR user, LPCTSTR domain, LPCTSTR password)
{
    HANDLE token = NULL;
    HANDLE duplicated = NULL;
 
    if (!LogonUser(user, domain, password, LOGON32_LOGON_INTERACTIVE, LOGON32_PROVIDER_DEFAULT, &token))
        return false;
 
    if (!DuplicateToken(token, SecurityImpersonation, &duplicated))
    {
        CloseHandle(token);
        return false;
    }
 
    if (!ImpersonateLoggedOnUser(duplicated)) 
    {
        CloseHandle(token);
        CloseHandle(duplicated);
        return false;
    }
 
    CloseHandle(token);
    CloseHandle(duplicated);
 
    return true;
}

Access Control 부분은 자세히 아는 것이 아니라, 이 코드가 맞는 건지는 잘 모르겠다만, 테스트해보니 돌아가긴 한다.

링크

kb/windowsservice.txt · 마지막으로 수정됨: 2014/11/12 15:55 저자 excel96