Windows Service – 꿈꾸는 개발자 https://studioys.me #개발 #일상생활 #생각 Tue, 07 Jan 2025 03:46:42 +0000 ko-KR hourly 1 https://wordpress.org/?v=6.8 https://studioys.me/wp-content/webpc-passthru.php?src=https://studioys.me/wp-content/uploads/2024/09/cropped-그림1-32x32.png&nocache=1 Windows Service – 꿈꾸는 개발자 https://studioys.me 32 32 [윈도우 서비스] 03. 런처 서비스 https://studioys.me/%ec%9c%88%eb%8f%84%ec%9a%b0-%ec%84%9c%eb%b9%84%ec%8a%a4-03-%eb%9f%b0%ec%b2%98-%ec%84%9c%eb%b9%84%ec%8a%a4/ https://studioys.me/%ec%9c%88%eb%8f%84%ec%9a%b0-%ec%84%9c%eb%b9%84%ec%8a%a4-03-%eb%9f%b0%ec%b2%98-%ec%84%9c%eb%b9%84%ec%8a%a4/#respond Tue, 22 Oct 2024 09:21:22 +0000 https://studioys.me/?p=946 더 보기[윈도우 서비스] 03. 런처 서비스]]> 이전 포스팅을 참고하여 윈도우 서비스 기반으로 윈도우 부팅 시 Explorer 권한으로 특정 프로그램(여기서는 메모장)을 실행하는 런처 서비스를 만들어보겠습니다.

전체 소스 코드는 https://github.com/whalec-io/LauncherService 에서 받아볼 수 있습니다.

런처 서비스

런처는 특정 프로그램을 실행하는 것으로 윈도우 서비스를 만들어 윈도우 부팅 시 Explorer 권한으로 메모장을 실행 시켜보겠습니다. Windows Session이 추가 될 때 프로그램을 실행하거나 별도 프로세스에 대해서 WatchDog을 수행하기 위해서는 런처 서비스를 기반으로 추가로 작업하면 됩니다.

Service Main

ServiceMain에서는 work_thread를 생성하여 작업을 시작합니다. work_thread 생성 후 서비스 상태를 SERVICE_START_PENDING에서 SERVICE_RUNNING으로 변경하고, work_thread 종료 되면 서비스 상태를 SERVICE_STOPPED로 변경하여 서비스를 중지 상태로 변경합니다.

void ServiceMain(DWORD argc, LPCWSTR* argv)
{
  // ...
  
	UpdateServiceStatus(SERVICE_START_PENDING, NO_ERROR, 0, 3000);

	std::thread work_thread(WorkThread);

	UpdateServiceStatus(SERVICE_RUNNING, NO_ERROR, 0, 0);
	 
	work_thread.join();
	
	UpdateServiceStatus(SERVICE_STOPPED, NO_ERROR, 0, 0);
}

BOOL UpdateServiceStatus(
  DWORD current_state, 
  DWORD exit_code, 
  DWORD specific_exit_code, 
  DWORD wait_hint
  )
{
	static DWORD check_point = 1;
	SERVICE_STATUS service_status = { 0 };
	service_status.dwServiceType = SERVICE_WIN32_OWN_PROCESS;
	service_status.dwCurrentState = current_state;
	service_status.dwControlsAccepted = (current_state == SERVICE_RUNNING) ? (SERVICE_ACCEPT_STOP | SERVICE_ACCEPT_SHUTDOWN) : 0;
	service_status.dwWin32ExitCode = (specific_exit_code == 0) ? exit_code : ERROR_SERVICE_SPECIFIC_ERROR;
	service_status.dwServiceSpecificExitCode = specific_exit_code;
	service_status.dwWaitHint = wait_hint;
	service_status.dwCheckPoint = ((current_state == SERVICE_RUNNING) || (current_state == SERVICE_STOPPED)) ? 0 : check_point++;

	return SetServiceStatus(service_state_handle, &service_status);
}

Work Thread

서비스가 상태 체크프로세스 실행을 수행합니다.
서비스 상태는 ServiceHandlerEx에서 SHUTDOWN 또는 STOP 이벤트 발생 시 전역 변수를 변경하고 Work Thread에서 해당 변수를 사용하여 서비스 중지 요청을 받을 경우 Thread를 종료합니다.

DWORD WINAPI ServiceHandlerEx(
  DWORD dwControl, 
  DWORD dwEventType, 
  LPVOID lpEventData, 
  LPVOID lpContext
)
{
	switch ( dwControl )
	{
	case SERVICE_CONTROL_SHUTDOWN:
	case SERVICE_CONTROL_STOP:
		UpdateServiceStatus(SERVICE_STOP_PENDING, NO_ERROR, 0, 0);
		is_running = FALSE;
		return NO_ERROR;
	default:
		break;
	}
	return NO_ERROR;
}

void WorkThread()
{
	const std::wstring process_path = L"C:\\windows\\system32\\notepad.exe";

	BOOL executed = FALSE;

	while ( is_running )
	{
		if ( !executed )
		{
			executed = RunAsExplorer(process_path.c_str());
		}

		Sleep(2000);
	}
}

전체 코드

런처 서비스 전체 코드는 다음과 같습니다.

#include <windows.h>
#include <winsvc.h>
#include <Tlhelp32.h>
#include <process.h>
#include <psapi.h>
#include <sddl.h>

#include <iomanip>
#include <iostream>
#include <map>
#include <mutex>
#include <thread>

#include "spdlog/sinks/basic_file_sink.h"
#include "spdlog/spdlog.h"

static WCHAR kServiceName[] = L"StudioYS Launcher Service";

SERVICE_STATUS_HANDLE service_state_handle;
BOOL is_running = FALSE;

std::mutex mutex_lock;

SC_HANDLE OpenSCM(DWORD desired_access)
{
	SC_HANDLE service_manager = OpenSCManagerW(NULL, NULL, desired_access);

	if ( service_manager == NULL )
	{
		spdlog::error("OpenSCManager Failed: 0x{:08x}", GetLastError());
	}

	return service_manager;
}

BOOL GetExplorerToken(HANDLE& hToken)
{
	spdlog::info("[GetExplorerToken] Start");

	HANDLE hSnapshot = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0);

	if ( hSnapshot == INVALID_HANDLE_VALUE )
	{
		spdlog::error("[GetExplorerToken] CreateToolhelp32Snapshot Failed : 0x{:08x}", GetLastError());
		return FALSE;
	}

	DWORD explorerPID = 0;
	PROCESSENTRY32 pe;
	pe.dwSize = sizeof(PROCESSENTRY32);

	// Find explorer.exe process
	if ( Process32First(hSnapshot, &pe) )
	{
		do
		{
			if ( _wcsicmp(pe.szExeFile, L"explorer.exe") == 0 )
			{
				explorerPID = pe.th32ProcessID;
				spdlog::debug("[GetExplorerToken] Found Explorer PID : {}", explorerPID);
				break;
			}
		} while ( Process32Next(hSnapshot, &pe) );
	}

	CloseHandle(hSnapshot);

	if ( explorerPID == 0 )
	{
		spdlog::error("[GetExplorerToken] Explorer process not found.");
		return FALSE;
	}

	// Open process and get token
	HANDLE hProcess = OpenProcess(PROCESS_QUERY_INFORMATION, FALSE, explorerPID);
	if ( hProcess == NULL )
	{
		spdlog::error("[GetExplorerToken] OpenProcess failed for Explorer PID: 0x{:08x}", GetLastError());
		return FALSE;
	}

	if ( !OpenProcessToken(hProcess, TOKEN_DUPLICATE | TOKEN_ASSIGN_PRIMARY | TOKEN_QUERY, &hToken) )
	{
		CloseHandle(hProcess);
		spdlog::error("[GetExplorerToken] OpenProcessToken failed: 0x{:08x}", GetLastError());
		return FALSE;
	}

	CloseHandle(hProcess);
	spdlog::info("[GetExplorerToken] Successfully retrieved explorer token.");
	return TRUE;
}

BOOL RunAsExplorer(LPCWSTR lpApplicationName)
{
	HANDLE hToken;
	if ( !GetExplorerToken(hToken) )
	{
		spdlog::error("[RunAsExplorer] Failed to get explorer token");
		return FALSE;
	}

	HANDLE hNewToken;
	if ( !DuplicateTokenEx(hToken, MAXIMUM_ALLOWED, NULL, SecurityImpersonation, TokenPrimary, &hNewToken) )
	{
		CloseHandle(hToken);
		spdlog::error("[RunAsExplorer] Failed to duplicate token: 0x{:08x}", GetLastError());
		return FALSE;
	}

	STARTUPINFOW si = { sizeof(STARTUPINFOW) };
	PROCESS_INFORMATION pi = { 0 };

	BOOL result = CreateProcessAsUserW(hNewToken, lpApplicationName, NULL, NULL, NULL, FALSE, 0, NULL, NULL, &si, &pi);

	if ( result )
	{
		spdlog::info("[RunAsExplorer] Successfully created process.");
		CloseHandle(pi.hProcess);
		CloseHandle(pi.hThread);
	}
	else
	{
		spdlog::error("[RunAsExplorer] Failed to create process: 0x{:08x}", GetLastError());
	}

	CloseHandle(hToken);
	CloseHandle(hNewToken);
	return result;
}

void WorkThread()
{
	spdlog::info("[WorkThread] Start");

	const std::wstring process_path = L"C:\\windows\\system32\\notepad.exe";

	BOOL executed = FALSE;

	while ( is_running )
	{
		if ( !executed )
		{
			if ( executed = RunAsExplorer(process_path.c_str()) )
			{
				spdlog::info("[WorkThread] run process");
			}
			else
			{
				spdlog::error("[WorkThread] Failed to run process");
			}
		}

		Sleep(2000);
	}

	spdlog::info("[WorkThread] Finish");
}

BOOL UpdateServiceStatus(DWORD current_state, DWORD exit_code, DWORD specific_exit_code, DWORD wait_hint)
{
	static DWORD check_point = 1;
	SERVICE_STATUS service_status = { 0 };
	service_status.dwServiceType = SERVICE_WIN32_OWN_PROCESS;
	service_status.dwCurrentState = current_state;
	service_status.dwControlsAccepted = (current_state == SERVICE_RUNNING) ? (SERVICE_ACCEPT_STOP | SERVICE_ACCEPT_SHUTDOWN) : 0;
	service_status.dwWin32ExitCode = (specific_exit_code == 0) ? exit_code : ERROR_SERVICE_SPECIFIC_ERROR;
	service_status.dwServiceSpecificExitCode = specific_exit_code;
	service_status.dwWaitHint = wait_hint;
	service_status.dwCheckPoint = ((current_state == SERVICE_RUNNING) || (current_state == SERVICE_STOPPED)) ? 0 : check_point++;

	return SetServiceStatus(service_state_handle, &service_status);
}

DWORD WINAPI ServiceHandlerEx(DWORD dwControl, DWORD dwEventType, LPVOID lpEventData, LPVOID lpContext)
{
	switch ( dwControl )
	{
	case SERVICE_CONTROL_SHUTDOWN:
	case SERVICE_CONTROL_STOP:
		spdlog::info("SERVICE_CONTROL_STOP received");
		UpdateServiceStatus(SERVICE_STOP_PENDING, NO_ERROR, 0, 0);
		is_running = FALSE;
		return NO_ERROR;
	default:
		break;
	}
	return NO_ERROR;
}

void ServiceMain(DWORD argc, LPCWSTR* argv)
{
	spdlog::info("[ServiceMain] Start");

	service_state_handle = RegisterServiceCtrlHandlerEx(kServiceName, ServiceHandlerEx, NULL);
	if ( service_state_handle == NULL )
	{
		spdlog::error("[ServiceMain] RegisterServiceCtrlHandlerEx failed: 0x{:08x}", GetLastError());
		return;
	}

	UpdateServiceStatus(SERVICE_START_PENDING, NO_ERROR, 0, 3000);

	is_running = TRUE;
	std::thread work_thread(WorkThread);

	UpdateServiceStatus(SERVICE_RUNNING, NO_ERROR, 0, 0);
	 
	work_thread.join();
	UpdateServiceStatus(SERVICE_STOPPED, NO_ERROR, 0, 0);
}

void InstallMyService()
{
	SC_HANDLE service_manager = OpenSCM(SC_MANAGER_CREATE_SERVICE);
	if ( service_manager == NULL ) return;

	WCHAR file_path[MAX_PATH] = { 0 };
	GetModuleFileNameW(NULL, file_path, _countof(file_path));

	SC_HANDLE service_handle = CreateServiceW(
		service_manager, kServiceName, kServiceName, SERVICE_ALL_ACCESS,
		SERVICE_WIN32_OWN_PROCESS, SERVICE_AUTO_START, SERVICE_ERROR_NORMAL,
		file_path, NULL, NULL, NULL, NULL, NULL);

	if ( service_handle == NULL )
	{
		spdlog::error("[InstallMyService] CreateService Failed: 0x{:08x}", GetLastError());
		CloseServiceHandle(service_manager);
		return;
	}

	WCHAR description[] = L"StudioYS Launcher Service Description";
	SERVICE_DESCRIPTION sd = { description };
	ChangeServiceConfig2W(service_handle, SERVICE_CONFIG_DESCRIPTION, &sd);

	CloseServiceHandle(service_handle);
	CloseServiceHandle(service_manager);

	spdlog::info("[InstallMyService] Service installed successfully.");
}

void UninstallMyService()
{
	SC_HANDLE service_manager = OpenSCM(SC_MANAGER_ALL_ACCESS);
	if ( service_manager == NULL ) return;

	SC_HANDLE service_handle = OpenService(service_manager, kServiceName, SERVICE_ALL_ACCESS);
	if ( service_handle == NULL )
	{
		spdlog::error("[UninstallMyService] OpenService Failed: 0x{:08x}", GetLastError());
		CloseServiceHandle(service_manager);
		return;
	}

	if ( !DeleteService(service_handle) )
	{
		spdlog::error("[UninstallMyService] DeleteService Failed: 0x{:08x}", GetLastError());
	}

	CloseServiceHandle(service_handle);
	CloseServiceHandle(service_manager);

	spdlog::info("[UninstallMyService] Service uninstalled successfully.");
}

void StartMyService()
{
	SC_HANDLE service_manager = OpenSCM(SC_MANAGER_ALL_ACCESS);
	if ( service_manager == NULL ) return;

	SC_HANDLE service_handle = OpenService(service_manager, kServiceName, SERVICE_ALL_ACCESS);
	if ( service_handle == NULL )
	{
		spdlog::error("[StartMyService] OpenService Failed: 0x{:08x}", GetLastError());
		CloseServiceHandle(service_manager);
		return;
	}

	if ( !StartService(service_handle, 0, NULL) )
	{
		spdlog::error("[StartMyService] StartService Failed: 0x{:08x}", GetLastError());
		CloseServiceHandle(service_handle);
		CloseServiceHandle(service_manager);
		return;
	}

	SERVICE_STATUS service_status;
	QueryServiceStatus(service_handle, &service_status);

	while ( service_status.dwCurrentState != SERVICE_RUNNING )
	{
		Sleep(service_status.dwWaitHint);
		QueryServiceStatus(service_handle, &service_status);
	}

	CloseServiceHandle(service_handle);
	CloseServiceHandle(service_manager);
	spdlog::info("[StartMyService] Service started successfully.");
}

void StopMyService()
{
	SC_HANDLE service_manager = OpenSCM(SC_MANAGER_ALL_ACCESS);
	if ( service_manager == NULL ) return;

	SC_HANDLE service_handle = OpenService(service_manager, kServiceName, SERVICE_ALL_ACCESS);
	if ( service_handle == NULL )
	{
		spdlog::error("[StopMyService] OpenService Failed: 0x{:08x}", GetLastError());
		CloseServiceHandle(service_manager);
		return;
	}

	SERVICE_STATUS service_status;
	QueryServiceStatus(service_handle, &service_status);

	if ( service_status.dwCurrentState != SERVICE_STOPPED )
	{
		if ( !ControlService(service_handle, SERVICE_CONTROL_STOP, &service_status) )
		{
			spdlog::error("[StopMyService] ControlService Failed: 0x{:08x}", GetLastError());
			CloseServiceHandle(service_handle);
			CloseServiceHandle(service_manager);
			return;
		}

		Sleep(2000);
	}

	CloseServiceHandle(service_handle);
	CloseServiceHandle(service_manager);
	spdlog::info("[StopMyService] Service stopped successfully.");
}

int main(int argc, char* argv[])
{
	const std::string log_file_path = "log/Launcher Service.log";
	auto logger = spdlog::basic_logger_mt("basic_logger", log_file_path);
	spdlog::set_level(spdlog::level::trace);
	spdlog::set_default_logger(logger);
	spdlog::flush_on(spdlog::level::trace);

	spdlog::info("Main Start");

	SERVICE_TABLE_ENTRYW service_table[] = {
		{kServiceName, (LPSERVICE_MAIN_FUNCTIONW)ServiceMain},
		{nullptr, nullptr}
	};

	if ( argc >= 2 )
	{
		if ( _stricmp(argv[1], "--install") == 0 )
		{
			InstallMyService();
		}
		else if ( _stricmp(argv[1], "--uninstall") == 0 )
		{
			UninstallMyService();
		}
		else if ( _stricmp(argv[1], "--start") == 0 )
		{
			StartMyService();
		}
		else if ( _stricmp(argv[1], "--stop") == 0 )
		{
			StopMyService();
		}
	}
	else
	{
		StartServiceCtrlDispatcherW(service_table);
	}

	spdlog::info("Main Finish");
	return 0;
}

마치며

윈도우 서비스를 사용하여 런처 서비스를 구현해보았습니다. 만약 다른 권한으로 프로세스를 실행하거나, 와치독이 필요한 경우에는 추가로 수정하여 사용할 수 있습니다.

]]>
https://studioys.me/%ec%9c%88%eb%8f%84%ec%9a%b0-%ec%84%9c%eb%b9%84%ec%8a%a4-03-%eb%9f%b0%ec%b2%98-%ec%84%9c%eb%b9%84%ec%8a%a4/feed/ 0
[윈도우 서비스] 02. 윈도우 서비스 동작 과정 https://studioys.me/%ec%9c%88%eb%8f%84%ec%9a%b0-%ec%84%9c%eb%b9%84%ec%8a%a4-02-%ec%9c%88%eb%8f%84%ec%9a%b0-%ec%84%9c%eb%b9%84%ec%8a%a4-%eb%8f%99%ec%9e%91-%ea%b3%bc%ec%a0%95/ https://studioys.me/%ec%9c%88%eb%8f%84%ec%9a%b0-%ec%84%9c%eb%b9%84%ec%8a%a4-02-%ec%9c%88%eb%8f%84%ec%9a%b0-%ec%84%9c%eb%b9%84%ec%8a%a4-%eb%8f%99%ec%9e%91-%ea%b3%bc%ec%a0%95/#respond Mon, 14 Oct 2024 03:46:45 +0000 https://studioys.me/?p=939 더 보기[윈도우 서비스] 02. 윈도우 서비스 동작 과정]]> 윈도우 서비스는 운영 체제의 부팅부터 종료까지 백그라운드에서 동작할 수 있는 응용 프로그램입니다. 이전 포스팅에서 간단하게 윈도우 서비스 애플리케이션을 만들고 서비스 제어 관리자를 통해 서비스를 제어해보았습니다. 이번 포스팅에서는 실행 부터 종료까지 간단하게 윈도우 서비스 동작 과정에 대해서 알아보겠습니다.

윈도우 서비스 동작 과정

  1. 서비스 프로세스가 시작되고 main 함수에서 StartServiceCtrlDispatcher를 호출
  2. StartServiceCtrlDispatcher가 서비스 제어 관리자와 연결되고, 서비스의 ServiceMain 함수가 호출됨
  3. ServiceMain에서 서비스가 해야 할 작업을 수행하며, SetServiceStatus로 상태를 계속 서비스 제어 관리자에 보고
  4. 서비스 제어 관리자에서 발생하는 제어 요청을 ServiceCtrlHandler를 통해 처리
  5. 서비스가 종료될 때 SetServiceStatus를 통해 최종 상태를 보고하고 서비스 종료

이와 같은 흐름으로 동작하며, 서비스 상태 및 제어는 모두 서비스 제어 관리자와 통신하여 관리합니다.

1. 서비스 메인 함수

Windows 서비스는 콘솔 애플리케이션과 달리 서비스 시작 시 메인 함수(통상 main 또는 wmain)가 아니라 서비스 전용 함수가 호출됩니다. 일반적으로 ServiceMain 이름으로 함수를 생성합니다.

void WINAPI ServiceMain(DWORD argc, LPTSTR* argv);

이 함수는 서비스 제어 관리자에 의해 호출되며, 서비스의 실제 작업을 처리합니다.

2. StartServiceCtrlDispatcher 함수

서비스가 시작될 때 가장 먼저 호출해야 할 함수는 StartServiceCtrlDispatcher입니다. 이 함수는 서비스와 서비스 제어 관리자 간의 통신을 설정하며, 서비스 메인 함수를 지정합니다.

BOOL WINAPI StartServiceCtrlDispatcher(
  const SERVICE_TABLE_ENTRY* lpServiceTable
);

SERVICE_TABLE_ENTRY

서비스 이름과 ServiceMain 함수를 지정할 수 있는 구조체입니다. SERVICE_TABLE_ENTRY 구조체 배열을 StartServiceCtrlDispatcher 함수에 넘기면 서비스가 시작되고, 구조체의 ServiceMain 함수를 실행합니다.

int _tmain(int argc, TCHAR* argv[]) 
{
    SERVICE_TABLE_ENTRY ServiceTable[] = 
    {
        {TEXT("StudioYS Service"), (LPSERVICE_MAIN_FUNCTION) ServiceMain},
        {NULL, NULL}
    };

    // Start the control dispatcher
    if (!StartServiceCtrlDispatcher(ServiceTable)) 
    {
        // Handle error
    }
}

StartServiceCtrlDispatcher는 서비스 프로세스의 시작점이 되는 함수로, 이 함수가 호출되어야 비로소 서비스 제어 관리자와 연결되어 서비스 상태를 관리할 수 있습니다.

3. ServiceMain 함수

이 함수 내에서 서비스가 해야 할 작업을 정의합니다. 일반적으로 서비스의 상태를 나타내는 구조체(SERVICE_STATUS_HANDLE)를 사용하고, SetServiceStatus 함수를 통해 상태를 SCM에 보고합니다. 또한 서비스가 언제 중지되거나 일시 중지되는지와 같은 이벤트를 처리할 수 있도록 서비스 제어 핸들러를 등록합니다.

void WINAPI ServiceMain(DWORD argc, LPTSTR* argv) 
{
    // 1. 서비스 상태 등록
    SERVICE_STATUS_HANDLE hServiceStatus = RegisterServiceCtrlHandler(
        SERVICE_NAME,
        ServiceCtrlHandler
    );

    if (hServiceStatus == NULL) 
    {
        // Error handling
        return;
    }

    // 2. 서비스 상태 보고
    SERVICE_STATUS serviceStatus;
    serviceStatus.dwServiceType = SERVICE_WIN32_OWN_PROCESS;
    serviceStatus.dwCurrentState = SERVICE_START_PENDING;
    serviceStatus.dwControlsAccepted = SERVICE_ACCEPT_STOP | SERVICE_ACCEPT_PAUSE_CONTINUE;
    // ...

    SetServiceStatus(hServiceStatus, &serviceStatus);

    // 3. 서비스 동작 수행
    // To Do Something
}

4. 서비스 제어 핸들러

ServiceCtrlHandler는 서비스 제어 관리자가 서비스에 전달하는 제어 요청을 처리합니다. 예를 들어, 서비스 중지, 일시 중지, 계속 등을 처리하는 함수입니다.

void WINAPI ServiceCtrlHandler(DWORD dwCtrl) 
{
    switch (dwCtrl) {
        case SERVICE_CONTROL_STOP:
            // 서비스 중지 처리
            break;
        case SERVICE_CONTROL_PAUSE:
            // 서비스 일시 중지 처리
            break;
        case SERVICE_CONTROL_CONTINUE:
            // 서비스 재개 처리
            break;
        default:
            break;
    }
}

5. 서비스 상태 보고

서비스는 SetServiceStatus를 사용하여 서비스 제어 관리자에 현재 상태를 보고합니다. 서비스가 시작 중, 실행 중, 일시 중지, 중지 등 다양한 상태를 서비스 제어 관리자에 알림으로써 적절한 처리를 할 수 있도록 합니다.

BOOL SetServiceStatus(
  SERVICE_STATUS_HANDLE hServiceStatus,
  LPSERVICE_STATUS lpServiceStatus
);

마치며

간단하게 윈도우 서비스 동작 과정에 대해서 알아보았습니다. 이제 위 내용을 기반으로 하여 윈도우 서비스의 예시로 런처 서비스를 만들어 윈도우 부팅 시 메모장을 실행하는 서비스를 만들어보겠습니다.

]]>
https://studioys.me/%ec%9c%88%eb%8f%84%ec%9a%b0-%ec%84%9c%eb%b9%84%ec%8a%a4-02-%ec%9c%88%eb%8f%84%ec%9a%b0-%ec%84%9c%eb%b9%84%ec%8a%a4-%eb%8f%99%ec%9e%91-%ea%b3%bc%ec%a0%95/feed/ 0
[윈도우 서비스] 01. 서비스 제어하기 https://studioys.me/%ec%9c%88%eb%8f%84%ec%9a%b0-%ec%84%9c%eb%b9%84%ec%8a%a4-%ea%b5%ac%ed%98%84-01-%ec%84%9c%eb%b9%84%ec%8a%a4-%ec%a0%9c%ec%96%b4%ed%95%98%ea%b8%b0/ https://studioys.me/%ec%9c%88%eb%8f%84%ec%9a%b0-%ec%84%9c%eb%b9%84%ec%8a%a4-%ea%b5%ac%ed%98%84-01-%ec%84%9c%eb%b9%84%ec%8a%a4-%ec%a0%9c%ec%96%b4%ed%95%98%ea%b8%b0/#respond Mon, 07 Oct 2024 10:36:54 +0000 https://studioys.me/?p=921 더 보기[윈도우 서비스] 01. 서비스 제어하기]]> 윈도우 서비스(Windows Service)는 백그라운드에서 실행되는 프로그램으로, 일반적인 사용자 인터페이스(User Interface)를 제공하지 않고 있으며, 보통 사용자의 직접적인 개입 없이 일정한 작업을 수행하도록 설계된 프로그램입니다. 주로 웹 서버, 데이터베이스 서버, 보안 솔루션 등에 널리 사용합니다. 이번 포스팅에서는 Visual C++로 윈도우 서비스 애플리케이션을 만들어보겠습니다.

프로젝트 생성

VC++을 사용해 윈도우 서비스 애플리케이션을 만들기 위해 콘솔 애플리케이션 프로젝트를 생성합니다. 이후 서비스 제어 관리자를 통해 서비스의 시작, 중지 등 제어 작업을 수행할 수 있습니다.

윈도우 서비스 애플리케이션 프로젝트 생성

서비스 제어 관리자(Service Control Manager)

서비스 제어 관리자(Service Control Manager, SCM)는 윈도우 서비스의 시작과 정지 등을 제어하고 관리하는 시스템 프로세스입니다. 윈도우 서비스를 제어하기 위해서는 반드시 서비스 제어 관리자를 통해 서비스를 제어해야합니다.

서비스 제어 관리자의 주요 기능은 다음과 같습니다. :

  • 서비스의 추가/제거
  • 윈도우 부팅 시 서비스 자동 시작
  • 서비스의 시작, 종료, 일시 중지 등의 제어
  • 설치된 서비스 조회
  • 실행 중인 서비스에 대한 상태 정보 조회

서비스 제어 관리자는 레지스트리를 통해 윈도우 서비스 데이터를 관리하고 있으며, 서비스 또는 services.msc 명령으로 다음과 같이 서비스 제어 관리자를 확인할 수 있습니다.
( 레지스트리 키 위치 : HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Services )

서비스 제어 관리자

서비스 제어 관리자 열기

윈도우 서비스를 제어하려면 서비스 제어 관리자 핸들(SC_HANDLE)이 필요합니다. SCM 핸들을 구하기 위해 OpenSCManager 함수를 사용합니다. 핸들을 사용한 후에는 반드시 CloseServiceHandle 함수를 통해 자원을 해제해야 합니다.

// winsvc.h (include windows.h)
SC_HANDLE OpenSCManager(
  [in, optional] LPCTSTR lpMachineName,  // 대상 컴퓨터의 이름. 로컬 PC의 경우 NULL 입력
  [in, optional] LPCTSTR lpDatabaseName, // 서비스 DB 이름 - 일반적으로 NULL
  [in]           DWORD   dwDesiredAccess // 액세스 권한 - MSDN 참고
);
SC_HANDLE service_manager = OpenSCManagerW(NULL, NULL, SC_MANAGER_ALL_ACCESS);

if ( service_manager != NULL )
{
  // To Do Something

  CloseServiceHandle(service_manager);
}

윈도우 서비스 제어

OpenSCManager 함수로 구한 SCM 핸들을 통해 윈도우 서비스 제어 작업을 수행할 수 있습니다.

윈도우 서비스 등록

CreateService 함수를 통해 새로운 윈도우 서비스를 등록할 수 있습니다. 서비스가 시작되면 main 함수가 실행되고, 서비스 엔트리 함수(Service Entry Function)가 호출됩니다. 이때 수동 시작 시에는 인수를 넘길 수 있지만, 자동 시작일 경우 부팅 시점에서 자동 실행되므로 사용자가 인수를 넘기지 못합니다. 이런 경우 애플리케이션 경로(lpBinaryPathName)에 인수를 포함하여 전달할 수 있습니다.

// winsvc.h (include windows.h)
SC_HANDLE CreateService(
  [in]            SC_HANDLE hSCManager,          // Service Contrl Manager 핸들
  [in]            LPCTSTR   lpServiceName,       // 서비스 이름 ( key )
  [in, optional]  LPCTSTR   lpDisplayName,       // 서비스 이름 ( 화면에 표시 )
  [in]            DWORD     dwDesiredAccess,     // 액세스 권한 ( MSDN 참고 )
  [in]            DWORD     dwServiceType,       // 서비스 종류 ( MSDN 참고 )
  [in]            DWORD     dwStartType,         // 시작 유형 ( MSDN 참고 )
  [in]            DWORD     dwErrorControl,
  [in, optional]  LPCTSTR   lpBinaryPathName,    // 애플리케이션 경로 ( argument 포함 가능 )
  [in, optional]  LPCTSTR   lpLoadOrderGroup,
  [out, optional] LPDWORD   lpdwTagId,
  [in, optional]  LPCTSTR   lpDependencies,
  [in, optional]  LPCTSTR   lpServiceStartName,
  [in, optional]  LPCTSTR   lpPassword
);
SC_HANDLE service_handle = CreateServiceW(
  service_manager, 
  L"StudioYS Service",
  L"StudioYS Service", 
  SERVICE_ALL_ACCESS, 
  SERVICE_WIN32_SHARE_PROCESS,
  SERVICE_AUTO_START, 
  SERVICE_ERROR_NORMAL, 
  file_path, 
  NULL, NULL, NULL, NULL, NULL);

if ( service_handle != NULL )
{
  // Do Something

  CloseServiceHandle(service_handle);
}

윈도우 서비스 제거

서비스 제거는 OpenService 함수로 서비스 핸들을 얻은 후 DeleteService 함수를 사용하여 서비스를 삭제합니다.

// winsvc.h (include windows.h)
SC_HANDLE OpenService(
  [in] SC_HANDLE hSCManager,       // Service Contrl Manager 핸들
  [in] LPCTSTR   lpServiceName,    // 서비스 이름 ( Key )
  [in] DWORD     dwDesiredAccess   // 액세스 권한 ( MSDN 참고 )
);
// winsvc.h (include windows.h)
BOOL DeleteService(
  [in] SC_HANDLE hService  // Service Handle
);
SC_HANDLE service_handle = OpenService(service_manager, L"StudioYS Service", 
  SERVICE_ALL_ACCESS);

if ( service_handle != NULL )
{
  DeleteService(service_handle);
  CloseServiceHandle(service_handle);
}

윈도우 서비스 시작

서비스를 시작하려면 OpenService 함수로 핸들을 얻은 후 StartService 함수를 사용합니다.

// winsvc.h (include windows.h)
BOOL StartService(
  [in]           SC_HANDLE hService,             // service handle
  [in]           DWORD     dwNumServiceArgs,     // lpServiceArgVectors 배열의 수
  [in, optional] LPCTSTR   *lpServiceArgVectors  // ServiceMain args ( null-terminated )
);
// 관련 코드
SC_HANDLE service_handle = OpenService(service_manager, L"StudioYS Service", 
  SERVICE_ALL_ACCESS);

if ( service_handle != NULL )
{
  StartService(service_handle, 0, nullptr);
  CloseServiceHandle(service_handle);
}

윈도우 서비스 상태 제어

서비스의 상태를 제어하려면 OpenService 함수로 핸들을 얻고, ControlService 함수를 사용하여 제어 명령을 전달합니다.

// winsvc.h (include windows.h)
BOOL ControlService(
  [in]  SC_HANDLE        hService,         // service handle
  [in]  DWORD            dwControl,        // control codes ( MSDN 참고 )
  [out] LPSERVICE_STATUS lpServiceStatus   // SERVICE_STATUS structure pointer
);
SC_HANDLE service_handle = OpenService(service_manager, L"StudioYS Service", 
  SERVICE_ALL_ACCESS);

if ( service_handle != NULL )
{
  SERVICE_STATUS service_status;
  ControlService(service_handle, SERVICE_CONTROL_STOP, &service_status);
  CloseServiceH
}

예제 코드

내 자신을 서비스로 등록/제거/시작/종료하는 프로그램을 만들어보겠습니다. 해당 예제는 spdlog 라이브러리를 사용하여 로그를 기록합니다.

#include <windows.h>
#include "spdlog/spdlog.h"
#include "spdlog/sinks/basic_file_sink.h"

static WCHAR kServiceName[] = L"StudioYS Service";

SC_HANDLE OpenServiceManager(DWORD desiredAccess)
{
    SC_HANDLE service_manager = OpenSCManagerW(NULL, NULL, desiredAccess);
    if ( service_manager == NULL )
    {
        spdlog::error("OpenSCManagerW Failed: 0x{:08X}", GetLastError());
    }
    return service_manager;
}

SC_HANDLE OpenServiceHandle(SC_HANDLE service_manager, DWORD desiredAccess)
{
    SC_HANDLE service_handle = OpenService(service_manager, kServiceName, desiredAccess);
    if ( service_handle == NULL )
    {
        spdlog::error("OpenService Failed: 0x{:08X}", GetLastError());
    }
    return service_handle;
}

void InstallMyService()
{
    spdlog::debug("InstallMyService Start");

    SC_HANDLE service_manager = OpenServiceManager(SC_MANAGER_CREATE_SERVICE);

    if ( service_manager == NULL ) return;

    WCHAR file_path[MAX_PATH] = { 0 };
    GetModuleFileNameW(NULL, file_path, _countof(file_path));

    SC_HANDLE service_handle = CreateServiceW(
        service_manager, kServiceName, kServiceName, SERVICE_ALL_ACCESS,
        SERVICE_WIN32_OWN_PROCESS, SERVICE_AUTO_START, SERVICE_ERROR_NORMAL,
        file_path, NULL, NULL, NULL, NULL, NULL
    );

    if ( service_handle == NULL )
    {
        spdlog::error("CreateServiceW Failed: 0x{:08X}", GetLastError());
        CloseServiceHandle(service_manager);
        return;
    }

    // 서비스 설명 설정
    WCHAR description[] = L"이 서비스는 테스트를 위한 서비스입니다.";
    SERVICE_DESCRIPTION sd = { description };
    ChangeServiceConfig2W(service_handle, SERVICE_CONFIG_DESCRIPTION, &sd);

    // 핸들 닫기
    CloseServiceHandle(service_handle);
    CloseServiceHandle(service_manager);

    spdlog::debug("InstallMyService Finish");
}

void UninstallMyService()
{
    spdlog::debug("UninstallMyService Start");

    SC_HANDLE service_manager = OpenServiceManager(SC_MANAGER_ALL_ACCESS);
    if ( service_manager == NULL ) return;

    SC_HANDLE service_handle = OpenServiceHandle(service_manager, SERVICE_ALL_ACCESS);
    if ( service_handle == NULL )
    {
        CloseServiceHandle(service_manager);
        return;
    }

    if ( !DeleteService(service_handle) )
    {
        spdlog::error("DeleteService Failed: 0x{:08X}", GetLastError());
    }

    CloseServiceHandle(service_handle);
    CloseServiceHandle(service_manager);

    spdlog::debug("UninstallMyService Finish");
}

void StartMyService()
{
    spdlog::debug("StartMyService Start");

    SC_HANDLE service_manager = OpenServiceManager(SC_MANAGER_ALL_ACCESS);
    if ( service_manager == NULL ) return;

    SC_HANDLE service_handle = OpenServiceHandle(service_manager, SERVICE_START);
    if ( service_handle == NULL )
    {
        CloseServiceHandle(service_manager);
        return;
    }

    if ( !StartService(service_handle, 0, NULL) )
    {
        spdlog::error("StartService Failed: 0x{:08X}", GetLastError());
        CloseServiceHandle(service_handle);
        CloseServiceHandle(service_manager);
        return;
    }

    SERVICE_STATUS service_status;
    BOOL query = QueryServiceStatus(service_handle, &service_status);

    while ( query && service_status.dwCurrentState != SERVICE_RUNNING )
    {
        Sleep(service_status.dwWaitHint);
        query = QueryServiceStatus(service_handle, &service_status);
    }

    CloseServiceHandle(service_handle);
    CloseServiceHandle(service_manager);

    spdlog::debug("StartMyService Finish");
}

void StopMyService()
{
    spdlog::debug("StopMyService Start");

    SC_HANDLE service_manager = OpenServiceManager(SC_MANAGER_ALL_ACCESS);

    if ( service_manager == NULL ) return;

    SC_HANDLE service_handle = OpenServiceHandle(service_manager, SERVICE_STOP);

    if ( service_handle == NULL )
    {
        CloseServiceHandle(service_manager);
        return;
    }

    SERVICE_STATUS service_status;

    BOOL query = QueryServiceStatus(service_handle, &service_status);

    if ( query && service_status.dwCurrentState != SERVICE_STOPPED )
    {
        if ( !ControlService(service_handle, SERVICE_CONTROL_STOP, &service_status) )
        {
            spdlog::error("ControlService Failed: 0x{:08X}", GetLastError());
        }
        Sleep(2000);
    }

    CloseServiceHandle(service_handle);
    CloseServiceHandle(service_manager);

    spdlog::debug("StopMyService Finish");
}

int wmain(int argc, wchar_t* argv[])
{
    // logger 설정
    auto logger = spdlog::basic_logger_mt("StudioYS Service", "log/StudioYS_Service.log");
    spdlog::set_default_logger(logger);
    spdlog::set_pattern("%Y-%m-%d %H:%M:%S.%e (P:%P T:%t) [%L][%n] %v");
    spdlog::set_level(spdlog::level::debug);
    spdlog::flush_on(spdlog::level::debug);

    if ( argc >= 2 )
    {
        if ( _wcsicmp(argv[1], L"--install") == 0 )
        {
            InstallMyService();
        }
        else if ( _wcsicmp(argv[1], L"--uninstall") == 0 )
        {
            UninstallMyService();
        }
        else if ( _wcsicmp(argv[1], L"--start") == 0 )
        {
            StartMyService();
        }
        else if ( _wcsicmp(argv[1], L"--stop") == 0 )
        {
            StopMyService();
        }
        else
        {
            spdlog::error("Unknown command");
        }
    }
    else
    {
        spdlog::error("No command provided.");
    }

    return 0;
}

참고 자료

]]>
https://studioys.me/%ec%9c%88%eb%8f%84%ec%9a%b0-%ec%84%9c%eb%b9%84%ec%8a%a4-%ea%b5%ac%ed%98%84-01-%ec%84%9c%eb%b9%84%ec%8a%a4-%ec%a0%9c%ec%96%b4%ed%95%98%ea%b8%b0/feed/ 0