Windows Programming – 꿈꾸는 개발자 https://studioys.me #개발 #일상생활 #생각 Tue, 07 Jan 2025 12:05:57 +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 Programming – 꿈꾸는 개발자 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
[spdlog] spdlog 사용 방법 https://studioys.me/how-to-use-spdlog-library/ https://studioys.me/how-to-use-spdlog-library/#respond Mon, 30 Sep 2024 04:07:16 +0000 https://studioys.me/?p=901 더 보기[spdlog] spdlog 사용 방법]]>

이번 포스트는 spdlog 라이브러리를 사용할 수 있는 환경을 기반으로 합니다.
[spdlog] spdlog 소스 빌드 및 참조

spdlog 라이브러리를 사용하면 다양한 형태로 로그를 남길 수 있습니다.

이번 포스팅에서는 spdlog 사용 방법 및 예시에 대해서 알아보도록 하겠습니다.

spdlog 사용 방법

Formatting

spdlog에서 Formatting은 open source formatting library인 fmt를 사용하고 있습니다.

콘솔 화면에 간단하게 로그를 남겨 보겠습니다.

#include "spdlog/spdlog.h"
  
int main()
{
    spdlog::info("Welcome to spdlog!");
    spdlog::info("Args : {}", 42);
    spdlog::info("Between {1} and {0}", "Z", 0);
    spdlog::info("Notation Formatting int: {0:d}, hex: {0:x}, oct: {0:o}, bin: {0:b}", 42);
    spdlog::info("Padding 1 : {:8d}", 5);
    spdlog::info("Padding 2 : {:08x}", 5);
    spdlog::info("{:<30}", "left aligned");
    spdlog::info("{:>30}", "right aligned");
    spdlog::info("Floating 1 : {:08.1f}", 1.23456);
    spdlog::info("Floating 2 : {:08.2f}", 1.23456);
    spdlog::info("Floating 3 : {:8.1f}", 1.23456);
    spdlog::info("Floating 4 : {:8.2f}", 1.23456);
    
    spdlog::set_pattern("%^%l: %v%$");
 
    return 0;
}
실행 결과
[2024-09-30 12:22:58.974] [info] Welcome to spdlog!
[2024-09-30 12:22:58.975] [info] Args : 42
[2024-09-30 12:22:58.975] [info] Between 0 and Z
[2024-09-30 12:22:58.975] [info] Notation Formatting int: 42, hex: 2a, oct: 52, bin: 101010
[2024-09-30 12:22:58.976] [info] Padding 1 :        5
[2024-09-30 12:22:58.976] [info] Padding 2 : 00000005
[2024-09-30 12:22:58.976] [info] left aligned
[2024-09-30 12:22:58.977] [info]                  right aligned
[2024-09-30 12:22:58.977] [info] Floating 1 : 000001.2
[2024-09-30 12:22:58.977] [info] Floating 2 : 00001.23
[2024-09-30 12:22:58.978] [info] Floating 3 :      1.2
[2024-09-30 12:22:58.978] [info] Floating 4 :     1.23

소스와 같이 아주 간단하게 로그를 남길 수 있습니다.

5번째 라인처럼 문자열을 남길 수 있고, 

6번째 ~ 7번째 라인처럼 {}, {0}, {1} 등을 통해서 값이나 변수를 출력할 수 있고,

8번째 ~ 16번째 라인처럼 다양한 formatting을 지원하고 있습니다.

출력 패턴 변경

spdlog::set_pattern 함수를 사용하여 출력 패턴을 변경할 수 있습니다. 보다 자세한 내용은 Custom Formatting 문서를 참고하시길 바랍니다.

spdlog::set_pattern(pattern_string)

// Level: 로그 형태로 출력
spdlog::set_pattern("%^%l: %v%$");

// *** [12:53:29 +09:00] [thread 17340] 로그 ** 형태로 출력
spdlog::set_pattern("*** [%H:%M:%S %z] [thread %t] %v ***");

로그 레벨

spdlog는 다음과 같이 로그 레벨을 지원하며, 오른쪽으로 갈 수록 순위가 높습니다.

Trace – Debug – Info – Warning – Error – Critical

로그 레벨은 각각 다음 함수와 매칭됩니다.

함수매크로 함수
Tracespdlog::traceSPDLOG_TRACES
Debugspdlog::debugSPDLOG_DEBUG
Infospdlog::infoPDLOG_INFO
Warningspdlog::warnSPDLOG_WARN
Errorspdlog::errorSPDLOG_ERROR
Criticalspdlog::criticalSPDLOG_CRITICAL

spdlog::set_level 함수를 통해서 로그 레벨을 설정할 수 있으며, 설정한 로그 레벨을 포함하여 순위가 높은 로그 레벨만 출력합니다.

만약 다음과 같이 설정한다면 Error 보다 순위가 높은 ErrorCritical 만 출력합니다.

spdlog::set_level(spdlog::level::err);

간단하게 로그 레벨 로그를 남겨보겠습니다.

#include "spdlog/spdlog.h"
 
int main()
{
    spdlog::set_level(spdlog::level::err);
    spdlog::trace("Trace Level");
    spdlog::debug("Debug Level");
    spdlog::info("Info Level");
    spdlog::warn("Warn Level");
    spdlog::error("Error Level");
    spdlog::critical("Critical Level");
 
    return 0;
}
실행 결과

[2024-09-30 12:28:32.952] [error] Error Level
[2024-09-30 12:28:32.953] [critical] Critical Level

spdlog::set_level 함수를 사용하여 로그 레벨을 Err로 변경하자 Error ~ Critical 까지 출력되는 것을 확인할 수 있습니다.

만약, 로그를 남기고 싶지 않으면 다음과 같이 설정하면 됩니다.

spdlog::set_level(spdlog::level::off);

보통 spdlog::set_level 함수를 사용하여 Release 빌드일 경우에는 Info ~ Critical 까지, Debug 빌드일 경우에는 Trace ~ Critical까지 출력하도록 설정합니다.

매크로 방식으로 로그 남기기

함수를 사용 방식 대신에 매크로 방식으로 로그를 남길 수 있습니다.

#include "spdlog/spdlog.h"
 
#pragma comment(lib, "libspdlog_MD_2019_x86D_v1.10.0.lib")
 
int main()
{
    SPDLOG_TRACE("Trace Level");
    SPDLOG_DEBUG("Debug Level");
    SPDLOG_INFO("Info Level");
    SPDLOG_WARN("Warn Level");
    SPDLOG_ERROR("Error Level");
    SPDLOG_CRITICAL("Critical Level");
 
    return 0;
}

파일 로그 남기기

지금까지 화면에 로그를 남겼는데 파일로 로그를 남겨보겠습니다.

파일 로그도 아주 간단합니다. 헤더 파일을 추가하고, 파일 로거를 생성하고 기본 로거를 파일 로거로 지정하면 됩니다.

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

int main()
{
    auto logger = spdlog::basic_logger_mt("basic_logger", "logs/basic.txt");
    
    spdlog::set_default_logger(logger);
    spdlog::info("Basic File Logger");
    
    return 0;
}

최대 크기 지정

로그가 최대 크기가 넘어가면 다른 파일로 로그를 남기도록 설정할 수 있습니다.

#include "spdlog/spdlog.h"
#include "spdlog/sinks/rotating_file_sink.h"
 
int main()
{
    auto max_size = 5 * 1024 * 1024;
    auto max_files = 3;
    
    // 5MB 넘어갈 경우 파일을 생성. 3개 파일을 반복하여 사용
    auto logger = spdlog::rotating_logger_mt("some_logger_name", "logs/rotating.txt", max_size, max_files);

    spdlog::set_default_logger(logger);
    spdlog::info("Rotating File Logger");
 
    return 0;
}

일자별 파일 로그 남기기 

일자별로 로그를 남기도록 설정할 수 있습니다.

#include "spdlog/spdlog.h"
#include "spdlog/sinks/daily_file_sink.h"
 
int main()
{
    auto hour = 2;
    auto min = 30;
    
    // daily_YYYY-MM-DD.txt 파일 생성 ( 새벽 2시 30분에 )
    auto logger = spdlog::daily_logger_mt("daily_logger", "logs/daily.txt", hour, min);
 
    spdlog::set_default_logger(logger);
    spdlog::info("Daily File Logger");
 
    return 0;
}

flush

파일 로그 생성 시 로그가 실시간으로 기록되지 않을 수 있습니다. flush가 되지 않아서 발생하는 것으로 flush 주기 및 레벨을 설정할 수 있습니다.

spdlog::flush_every(std::chrono::seconds(3));  // 3초마다 갱신
spdlog::flush_on(spdlog::level::trace);        // trace 이상 항상 갱신

Backtrace 지원

Backtrace를 지원합니다. spdlog::enable_backtrace 함수에서 지정한 크기 만큼 메모리 상으로 로그를 가지고 있다가 spdlog::dump_backtrace() 사용 시 출력합니다.

spdlog::enable_backtrace(10);

for(int i = 0; i < 100; i++)
{
  spdlog::debug("Backtrace message {}", i); // not logged yet..
}

spdlog::dump_backtrace();
실행 결과
[2024-09-30 12:57:14.187] [info] ****************** Backtrace Start ******************
[2024-09-30 12:57:14.187] [debug] Backtrace message 90
[2024-09-30 12:57:14.187] [debug] Backtrace message 91
[2024-09-30 12:57:14.187] [debug] Backtrace message 92
[2024-09-30 12:57:14.187] [debug] Backtrace message 93
[2024-09-30 12:57:14.187] [debug] Backtrace message 94
[2024-09-30 12:57:14.187] [debug] Backtrace message 95
[2024-09-30 12:57:14.187] [debug] Backtrace message 96
[2024-09-30 12:57:14.187] [debug] Backtrace message 97
[2024-09-30 12:57:14.187] [debug] Backtrace message 98
[2024-09-30 12:57:14.187] [debug] Backtrace message 99
[2024-09-30 12:57:14.191] [info] ****************** Backtrace End ********************

마치며

spdlog 사용 방법에 대해서 알아보았습니다. spdlog를 사용하면 원하는 형태로 쉽게 로그를 남길 수 있습니다. 레지스트리나 설정 파일을 사용하여 로그 레벨을 조정하도록 설정하면 원하는 시점에 로그를 확인할 수 있습니다.

간단하게 사용할 수 있으므로 로그를 남긴다면 spdlog 로그를 사용해보세요. 🙏

참고 자료 및 사이트

]]>
https://studioys.me/how-to-use-spdlog-library/feed/ 0
부모 프로세스 ID 구하기 https://studioys.me/%eb%b6%80%eb%aa%a8-%ed%94%84%eb%a1%9c%ec%84%b8%ec%8a%a4-id-%ea%b5%ac%ed%95%98%ea%b8%b0/ https://studioys.me/%eb%b6%80%eb%aa%a8-%ed%94%84%eb%a1%9c%ec%84%b8%ec%8a%a4-id-%ea%b5%ac%ed%95%98%ea%b8%b0/#respond Sun, 29 Sep 2024 13:04:52 +0000 https://studioys.me/?p=793

부모 프로세스 ID는 NtQueryInformationProcess 함수를 통해서 쉽게 구할 수 있습니다.

구문

NtQueryInformationProcess 함수 원형은 다음과 같습니다.

__kernel_entry NTSTATUS NtQueryInformationProcess(
  [in]            HANDLE           ProcessHandle,
  [in]            PROCESSINFOCLASS ProcessInformationClass,
  [out]           PVOID            ProcessInformation,
  [in]            ULONG            ProcessInformationLength,
  [out, optional] PULONG           ReturnLength
);
C++

NtQueryInformationProcess 함수에서 Output 파라미터로 ProcessInformation 구조체를 반환하는데 해당 구조체의 InheritedFromUniqueProcessId 변수가 부모 프로세스 ID 입니다.

ProcessInformation 구조체는 다음과 같습니다.

typedef struct _PROCESS_BASIC_INFORMATION {
    NTSTATUS ExitStatus;
    PPEB PebBaseAddress;
    ULONG_PTR AffinityMask;
    KPRIORITY BasePriority;
    ULONG_PTR UniqueProcessId;
    ULONG_PTR InheritedFromUniqueProcessId;
} PROCESS_BASIC_INFORMATION;
C++

코드

부모 프로세스 ID를 구하는 코드는 다음과 같습니다.

#include <iostream>
#include <windows.h>
#include <tchar.h>

ULONG_PTR GetParentProcessId()
{
	BOOL loaded = FALSE;
	ULONG_PTR parent_process_id = 0;
	ULONG_PTR pbi[6];
	ULONG size = 0;

	HMODULE handle_ntdll = GetModuleHandle(_T("ntdll.dll"));

	if (handle_ntdll == NULL)
	{
		loaded = TRUE;
		handle_ntdll = LoadLibrary(_T("ntdll.dll"));
	}

	typedef NTSTATUS(WINAPI* _LPNTQUERYINFORMATIONPROCESS)(HANDLE, ULONG, PVOID, ULONG, PULONG);
	_LPNTQUERYINFORMATIONPROCESS _NtQueryInformationProcess = reinterpret_cast<_LPNTQUERYINFORMATIONPROCESS>(GetProcAddress(handle_ntdll, "NtQueryInformationProcess"));

	if (_NtQueryInformationProcess)
	{
		if (_NtQueryInformationProcess(GetCurrentProcess(), 0, &pbi, sizeof(pbi), &size) >= 0 && size == sizeof(pbi))
		{
			parent_process_id = pbi[5];
		}
	}

	if (handle_ntdll != NULL && loaded)
	{
		FreeLibrary(handle_ntdll);
	}

	return parent_process_id;
}

int main()
{
	std::cout << "Current PID : " << GetCurrentProcessId() << " / Parent PID : " << GetParentProcessId() << "\n";

	return 0;
}
C++
]]>
https://studioys.me/%eb%b6%80%eb%aa%a8-%ed%94%84%eb%a1%9c%ec%84%b8%ec%8a%a4-id-%ea%b5%ac%ed%95%98%ea%b8%b0/feed/ 0
[spdlog] 소스 빌드 및 참조 https://studioys.me/how-to-build-and-use-the-spdlog-source/ https://studioys.me/how-to-build-and-use-the-spdlog-source/#respond Fri, 27 Sep 2024 04:31:59 +0000 https://studioys.me/?p=704 더 보기[spdlog] 소스 빌드 및 참조]]> spglog는 C++용 고속 로깅 라이브러리로 헤더 파일만 추가하여 사용할 수 있도록 설계되어 라이브러리 컴파일 없이 헤더 파일을 추가하여 사용할 수 있습니다.

이번 포스팅에서는 Visual Studio 환경에서 spdlog 소스를 다운로드 받아 헤더 버전 또는 빌드 후 라이브러리 버전으로 사용하는 방법에 대해서 알아보도록 하겠습니다.

필수 소프트웨어 설치

spdlog는 헤더 전용 라이브러리로 반드시 빌드해서 사용해야 하는 건 아니지만, 공식 페이지에서는 프로젝트 컴파일 시간을 단축하기 위해 빌드 후 사용하는 것을 권장하고 있습니다.

Visual Studio 환경에서 spdlog 라이브러리를 사용하거나 빌드하기 위해서는 다음 도구들이 설치되어 있어야 합니다.

  • CMake : CMake를 통해서 MSVC 프로젝트를 생성하여 빌드할 수 있습니다.
  • Visual Studio (MSVC 컴파일러 포함) : spdlog 사용 및 빌드 시 필요합니다. C++ 11을 지원하는 MSVC 버전 필요

MSVC 버전 별로 지원하는 C++ 버전은 아래 사이트에서 확인할 수 있습니다.
Visual Studio 버전의 Microsoft C/C++ 언어 규칙

소스 다운로드

spdlog GitHub에서 소스 코드를 다운로드 받을 수 있습니다. 저장소를 복제하거나, 압축 파일을 다운로드 후 사용합니다.

저는 저장소를 복사해서 사용하겠습니다.

git clone https://github.com/gabime/spdlog.git

헤더 전용 버전으로 사용하기

spdlog 소스 폴더 안에 있는 include 폴더를 프로젝트 폴더에 추가합니다. ( 직접 링크를 걸거나 프로젝트 폴더 내 복사 )

spdlog include 폴더 이미지
spdlog 소스폴더 하위에 include 폴더

정적 라이브러리로 사용하기

spdlog 프로젝트를 정적 라이브러리로 빌드하여 사용해보겠습니다.

CMake를 통해 Visual Studio 솔루션 생성

CMake를 사용하여 Visusl Studio 솔루션을 생성해야 합니다. spdlog 소스 폴더로 이동 후 빌드 폴더를 생성합니다.

저는 x86 및 x64 빌드 프로젝트를 생성해보겠습니다.

# spdlog 소스 폴더로 이동
cd spdlog

# x86용 build 폴더 생성
mkdir build

# x64용 build 폴더 생성
mkdir build64

명령 프롬프트에 아래 명령을 실행하여 Visual Studio 솔루션 파일을 생성합니다.

# CMake를 사용하여 빌드 프로젝트 생성하기
cmake CMakeLists.txt -B [빌드 프로젝트 경로] -G [Generator] -A [Architecture]
 
# Visual Studio 2022 - x86
cmake CMakeLists.txt -B build -G "Visual Studio 17 2022" -A Win32
 
# Visual Studio 2022 - x64
cmake CMakeLists.txt -B build64 -G "Visual Studio 17 2022" -A x64

만약 MSVC 버전을 사용한다면 [Generator] 부분을 해당 버전에 맞게 변경합니다.

# MSVC Generators 예시

Visual Studio 17 2022 = Generates Visual Studio 2022 project files.
                        Use -A option to specify architecture.
Visual Studio 16 2019 = Generates Visual Studio 2019 project files.
                        Use -A option to specify architecture.
Visual Studio 15 2017 [arch] = Generates Visual Studio 2017 project files.
                               Optional [arch] can be "Win64" or "ARM".
Visual Studio 14 2015 [arch] = Generates Visual Studio 2015 project files.
                               Optional [arch] can be "Win64" or "ARM".
Visual Studio 12 2013 [arch] = Generates Visual Studio 2013 project files.
                               Optional [arch] can be "Win64" or "ARM".
Visual Studio 11 2012 [arch] = Generates Visual Studio 2012 project files.
                               Optional [arch] can be "Win64" or "ARM".
Visual Studio 10 2010 [arch] = Deprecated. Generates Visual Studio 2010 project files.
                               Optional [arch] can be "Win64" or "IA64".
Visual Studio 9 2008 [arch] = Generates Visual Studio 2008 project files.
                              Optional [arch] can be "Win64" or "IA64".

CMake가 성공적으로 실행되면, buildbuild64 폴더 안에 spdlog.sln과 같이 Visual Studio 솔루션 파일이 생성됩니다.

build 및 build 폴더 생성
build 및 build64 폴더 생성

프로젝트 빌드 ( 정적 라이브러리 생성 )

정적 라이브러리를 만들어 보겠습니다. 저는 아래와 같이 MSVC 플랫폼 및 구성에 맞게 총 8개의 정적 라이브러리를 만들어서 사용하고 있습니다.

구분x86x64
MT – Debuglibspdlog_MT_2022_x86D_v1.14.1.liblibspdlog_MT_2022_x64D_v1.14.1.lib
MT – Releaselibspdlog_MT_2022_x86_v1.14.1.liblibspdlog_MT_2022_x64_v1.14.1.lib
MD – Debuglibspdlog_MD_2022_x86D_v1.14.1.liblibspdlog_MD_2022_x64D_v1.14.1.lib
MD – Relaselibspdlog_MD_2022_x86_v1.14.1.liblibspdlog_MD_2022_x64_v1.14.1.lib

Visual Studio 2022에서 플랫폼에 맞게 build 또는 build64 폴더에 있는 솔루션 파일을 열고, 구성에 맞게 프로젝트를 빌드합니다.

출력 폴더에 정적 라이브러리가 생성된 것을 확인할 수 있습니다.

정적 라이브러리 생성

spdlog 라이브러리 참조

이제 console 프로젝트를 생성 후 spdlog 헤더 파일과 생성한 라이브러리를 사용하여 파일 로그를 남겨보겠습니다.

console 프로젝트 생성

간단하게 콘솔 애플리케이션을 만들어 spdlog를 테스트 해보겠습니다.

spdlog 헤더 파일 및 라이브러리 복사

spdlog 소스 폴더에 있는 include 폴더정적 라이브러리를 프로젝트에 맞게 솔루션 내 적절한 위치에 복사합니다.

저는 다음과 같이 솔루션 파일이 있는 위치에 include 폴더lib 폴더를 생성하였습니다.

include 및 lib 폴더 생성
헤더 파일 및 정적 라이브러리 복사

console 프로젝트에 참조 폴더 추가

프로젝트 속성 페이지에서 추가 포함 디렉터리에 spdlog 헤더 파일을 복사한 폴더 경로를 입력합니다.

프로젝트 속성 페이지에서 추가 라이브러리 디렉토리를 경로를 입력하고, 추가 종속성에 라이브러리를 추가합니다.

spdlog 추가 라이브러리 디렉토리 입력
spdlog 추가 족송성 입력

테스트

이제 다음과 같이 main 함수 작성 후 실행 시 콘솔에서 로그가 정상적으로 보인다면 헤더와 라이브러리를 정상적으로 참조한 것입니다.

#include <iostream>
#include "spdlog/spdlog.h"
  
int main()
{
    spdlog::info("Hello World!\n");
 
    return 0;
}
[2022-07-16 16:09:24.870] [info] Hello World!
실행 결과

마치며

spdlog 소스 빌드 방법에 대해서 알아보았습니다. 이제 spdlog 사용 방법에 대해서 알아보도록 하겠습니다.

참조 자료 및 관련 사이트

# 관련 포스트

]]>
https://studioys.me/how-to-build-and-use-the-spdlog-source/feed/ 0
ID3D11Texture2D를 이미지로 저장하기 https://studioys.me/id3d11texture2d%eb%a5%bc-%ec%9d%b4%eb%af%b8%ec%a7%80%eb%a1%9c-%ec%a0%80%ec%9e%a5%ed%95%98%ea%b8%b0/ https://studioys.me/id3d11texture2d%eb%a5%bc-%ec%9d%b4%eb%af%b8%ec%a7%80%eb%a1%9c-%ec%a0%80%ec%9e%a5%ed%95%98%ea%b8%b0/#respond Mon, 16 Sep 2024 03:01:24 +0000 https://studioys.me/?p=491 더 보기ID3D11Texture2D를 이미지로 저장하기]]> 보통 DirectX에서 Texture를 처리하는데 DirectXTex 라이브러리를 사용합니다. DirectXTex 라이브러리는 Visual Studio 2019 이상이 필요하기 때문에 구버전에서 사용할 수 없습니다. 이번 포스팅에서는 DirectXTex 라이브러리없이 ID3D11Texture2D를 이미지로 저장하는 방법에 대해서 알아보도록 하겠습니다.

개발에 앞서

저는 업무 특성 상 애플리케이션을 후킹하여 보안을 적용하기 위한 다양한 작업을 합니다. 그 중 하나는 캡쳐 시 화면 데이터를 보호하는 것으로 애플리케이션이 화면을 캡처하여 메모리로 저장할 때 데이터를 변조하여 화면의 특정 영역을 가리거나 복사를 불가능하게 만듭니다.

일반적으로 DirectX를 사용하여 화면을 캡처하는 경우 IDXGIOutputDuplication Interface를 사용합니다. CreateDXGIFactory 부터 IDXGIOutputDuplication 까지의 과정을 잘 파악하고 있으면 캡처 데이터를 제어하는데 어려움은 없습니다.

하지만 일부 앱은 Shared Resource 기술을 사용하여 리소스를 공유하고 메모리에 저장하는 방식을 사용하고 있어 IDXGIOutputDuplication에서 제어가 불가능한 경우가 존재합니다. 이 경우는 Shared Resource 영역을 후킹하여 캡처 데이터를 확보해야 하는데 Shared Resource 관련 함수는 리소스와 관련한 부분에서 다 사용할 수 있어 관련 함수를 분석하고 적용하는데 입력 리소스 또는 출력 리소스 그리고 변조한 데이터가 정상적으로 반영되었는지 화면이 아니라 파일로 확인하는 경우가 존재합니다.

ID3D11Texture2D를 이미지로 저장하기

아래 소스코드는 ID3D11Texture2D를 이미지로 저장하는 소스코드입니다. DDSDirectDraw Surface 포맷으로 이미지를 저장하고 있습니다.

#include <windows.h>
#include <d3d11.h>

#include <memory>
#include <string>
#include <algorithm>

#pragma pack(push,1)

#define DDS_MAGIC 0x20534444 // "DDS "

#define DDS_FOURCC      0x00000004  // DDPF_FOURCC
#define DDS_RGB         0x00000040  // DDPF_RGB
#define DDS_RGBA        0x00000041  // DDPF_RGB | DDPF_ALPHAPIXELS
#define DDS_LUMINANCE   0x00020000  // DDPF_LUMINANCE
#define DDS_LUMINANCEA  0x00020001  // DDPF_LUMINANCE | DDPF_ALPHAPIXELS
#define DDS_ALPHA       0x00000002  // DDPF_ALPHA
#define DDS_BUMPDUDV    0x00080000  // DDPF_BUMPDUDV

#define DDS_HEADER_FLAGS_TEXTURE        0x00001007  // DDSD_CAPS | DDSD_HEIGHT | DDSD_WIDTH | DDSD_PIXELFORMAT
#define DDS_HEADER_FLAGS_MIPMAP         0x00020000  // DDSD_MIPMAPCOUNT
#define DDS_HEADER_FLAGS_PITCH          0x00000008  // DDSD_PITCH
#define DDS_HEADER_FLAGS_LINEARSIZE     0x00080000  // DDSD_LINEARSIZE

#define DDS_SURFACE_FLAGS_TEXTURE 0x00001000 // DDSCAPS_TEXTURE

struct DDS_PIXELFORMAT {
  DWORD dwSize;
  DWORD dwFlags;
  DWORD dwFourCC;
  DWORD dwRGBBitCount;
  DWORD dwRBitMask;
  DWORD dwGBitMask;
  DWORD dwBBitMask;
  DWORD dwABitMask;
};

typedef struct {
  DWORD           dwSize;
  DWORD           dwFlags;
  DWORD           dwHeight;
  DWORD           dwWidth;
  DWORD           dwPitchOrLinearSize;
  DWORD           dwDepth;
  DWORD           dwMipMapCount;
  DWORD           dwReserved1[11];
  DDS_PIXELFORMAT ddspf;
  DWORD           dwCaps;
  DWORD           dwCaps2;
  DWORD           dwCaps3;
  DWORD           dwCaps4;
  DWORD           dwReserved2;
} DDS_HEADER;

typedef struct {
  DXGI_FORMAT              dxgiFormat;
  D3D10_RESOURCE_DIMENSION resourceDimension;
  UINT                     miscFlag;
  UINT                     arraySize;
  UINT                     miscFlags2;
} DDS_HEADER_DXT10;

#pragma pack(pop)

const DDS_PIXELFORMAT DDSPF_A8R8G8B8 =
{ sizeof(DDS_PIXELFORMAT), DDS_RGBA, 0, 32, 0x00ff0000, 0x0000ff00, 0x000000ff, 0xff000000 };

struct com_deleter { void operator()(IUnknown* p) { if ( p ) { p->Release(); } } };
struct handle_closer { void operator()(HANDLE h) { if ( h != INVALID_HANDLE_VALUE ) CloseHandle(h); } };

template <typename T> using ScopedInterface = std::unique_ptr<T, com_deleter>;
template <typename T> ScopedInterface<T> make_com_ptr(T* p) { return ScopedInterface<T>(p); }

using ScopedHandle = std::unique_ptr<void, handle_closer>;
inline HANDLE safe_handle(HANDLE h) { return (h == INVALID_HANDLE_VALUE) ? nullptr : h; }

HRESULT SaveDDSFile(ID3D11DeviceContext* device_context, ID3D11Resource* source, const wchar_t* file_name)
{
  if ( nullptr == device_context || nullptr == source || nullptr == file_name )
  {
    return E_INVALIDARG;
  }

  D3D11_RESOURCE_DIMENSION resource_type = D3D11_RESOURCE_DIMENSION_UNKNOWN;
  source->GetType(&resource_type);

  if ( resource_type != D3D11_RESOURCE_DIMENSION_TEXTURE2D )
  {
    return HRESULT_FROM_WIN32(ERROR_NOT_SUPPORTED);
  }

  ID3D11Texture2D* source_texture = nullptr;

  HRESULT hr = source->QueryInterface(__uuidof(ID3D11Texture2D), reinterpret_cast<void**>(&source_texture));

  if ( FAILED(hr) )
  {
    return hr;
  }

  auto _source_texture = make_com_ptr(source_texture);

  D3D11_TEXTURE2D_DESC desc;
  source_texture->GetDesc(&desc);

  ID3D11Device* device = nullptr;
  device_context->GetDevice(&device);

  auto _device = make_com_ptr(device);

  if ( desc.SampleDesc.Count != 1 && desc.Format != DXGI_FORMAT_B8G8R8A8_UNORM )
  {
    return HRESULT_FROM_WIN32(ERROR_NOT_SUPPORTED);
  }

  ID3D11Texture2D* staging_texture = nullptr;

  ScopedInterface<ID3D11Texture2D> _staging_texture;

  if ( (desc.Usage == D3D11_USAGE_STAGING) && (desc.CPUAccessFlags & D3D11_CPU_ACCESS_READ) )
  {
    staging_texture = source_texture;
    _staging_texture = std::move(_source_texture);
  }
  else
  {
    desc.BindFlags = 0;
    desc.MiscFlags = 0;
    desc.CPUAccessFlags = D3D11_CPU_ACCESS_READ;
    desc.Usage = D3D11_USAGE_STAGING;

    hr = device->CreateTexture2D(&desc, nullptr, &staging_texture);

    if ( FAILED(hr) )
    {
      return hr;
    }

    device_context->CopyResource(staging_texture, source_texture);

    _staging_texture = make_com_ptr(staging_texture);
  }

  const DWORD kMaxHeaderSize = sizeof(DWORD) + sizeof(DDS_HEADER) + sizeof(DDS_HEADER_DXT10);
  BYTE file_header[kMaxHeaderSize] = {};

  *reinterpret_cast<DWORD*>(&file_header[0]) = DDS_MAGIC;

  auto header = reinterpret_cast<DDS_HEADER*>(&file_header[0] + sizeof(DWORD));
  size_t header_size = sizeof(DWORD) + sizeof(DDS_HEADER);
  header->dwSize = sizeof(DDS_HEADER);
  header->dwFlags = DDS_HEADER_FLAGS_TEXTURE | DDS_HEADER_FLAGS_MIPMAP;
  header->dwHeight = desc.Height;
  header->dwWidth = desc.Width;
  header->dwMipMapCount = 1;
  header->dwCaps = DDS_SURFACE_FLAGS_TEXTURE;

  DDS_HEADER_DXT10* ext_header = nullptr;
  memcpy_s(&header->ddspf, sizeof(header->ddspf), &DDSPF_A8R8G8B8, sizeof(DDS_PIXELFORMAT));

  DWORD row_pitch = (DWORD(desc.Width) * 32 + 7u) / 8u;
  DWORD row_count = (DWORD(desc.Height));
  DWORD row_bytes = row_pitch * row_count;

  header->dwFlags |= DDS_HEADER_FLAGS_PITCH;
  header->dwPitchOrLinearSize = static_cast<DWORD>(row_pitch);

  std::unique_ptr<BYTE[]> pixels(new (std::nothrow) BYTE[row_bytes]);

  if ( !pixels )
  {
    return E_OUTOFMEMORY;
  }

  D3D11_MAPPED_SUBRESOURCE resource;

  hr = device_context->Map(staging_texture, 0, D3D11_MAP_READ, 0, &resource);

  if ( FAILED(hr) )
  {
    return hr;
  }

  auto src_data = static_cast<const BYTE*>(resource.pData);

  if ( !src_data )
  {
    device_context->Unmap(staging_texture, 0);
    return E_POINTER;
  }

  BYTE* dst_ptr = pixels.get();

  const DWORD min_size = std::min<DWORD>(row_pitch, resource.RowPitch);

  for ( DWORD h = 0; h < row_count; ++h )
  {
    memcpy_s(dst_ptr, row_pitch, src_data, min_size);
    src_data += resource.RowPitch;
    dst_ptr += row_pitch;
  }

  device_context->Unmap(staging_texture, 0);

  ScopedHandle hFile(safe_handle(CreateFileW(file_name, GENERIC_WRITE | DELETE, 0, nullptr, CREATE_ALWAYS, FILE_ATTRIBUTE_NORMAL, nullptr)));

  if ( !hFile )
  {
    return HRESULT_FROM_WIN32(GetLastError());
  }

  DWORD bytesWritten;
  if ( !WriteFile(hFile.get(), file_header, static_cast<DWORD>(header_size), &bytesWritten, nullptr) )
  {
    return HRESULT_FROM_WIN32(GetLastError());
  }

  if ( bytesWritten != header_size )
  {
    return E_FAIL;
  }

  if ( !WriteFile(hFile.get(), pixels.get(), static_cast<DWORD>(row_bytes), &bytesWritten, nullptr) )
  {
    return HRESULT_FROM_WIN32(GetLastError());
  }

  if ( bytesWritten != row_bytes )
  {
    return E_FAIL;
  }

  return S_OK;
}

마치며

ID3D11Texture2D를 이미지로 저장하는 소스코드에 대해서 알아보았습니다. DDS 포맷은 별도 이미지 뷰어를 설치해서 확인할 수 있으며, JPEG, PNG 등의 이미지로 변경하는 부분에 대해서는 차후 기회가 된다면 추가하도록 하겠습니다.

참고 자료

]]>
https://studioys.me/id3d11texture2d%eb%a5%bc-%ec%9d%b4%eb%af%b8%ec%a7%80%eb%a1%9c-%ec%a0%80%ec%9e%a5%ed%95%98%ea%b8%b0/feed/ 0
HANDLE을 FILE*로 변환하는 방법 https://studioys.me/handle%ec%9d%84-file%eb%a1%9c-%eb%b3%80%ed%99%98%ed%95%98%eb%8a%94-%eb%b0%a9%eb%b2%95/ https://studioys.me/handle%ec%9d%84-file%eb%a1%9c-%eb%b3%80%ed%99%98%ed%95%98%eb%8a%94-%eb%b0%a9%eb%b2%95/#respond Sun, 15 Sep 2024 10:28:40 +0000 https://studioys.me/?p=488 더 보기HANDLE을 FILE*로 변환하는 방법]]> 윈도우에서 파일을 처리할 때 Win32 API와 C 런타임 라이브러리(C Runtime Library, CRT)를 함께 사용하는 경우가 종종 있습니다. 이를 위해서는 Win32 API에서 생성한 HANDLE을 C 런타임의 FILE*로 변환하는 방법이 필요합니다. 이 포스팅에서 HANDLE을 FILE*로 변환하는 방법과 그 과정에서 발생할 수 있는 오류 및 주의사항을 다루고자 합니다.

HANDLEFILE*로 변환하기

Win32 API의 CreateFile 함수로 파일 핸들을 얻고, 이를 C 표준 입출력 함수에서 사용하려면 C 런타임에서 사용 가능한 파일 디스크립터(File Descriptor)로 변환해야 합니다. 파일 디스크립터로 변환하기 위해 _open_osfhandle 함수를 사용하여 파일 디스크립터를 구하고, 그 결과를 FILE*로 연결하기 위해 _fdopen을 사용합니다.

#include <windows.h>
#include <io.h>
#include <fcntl.h>

#include <iostream>
#include <string>

int main()
{
    std::wstring file_path = L"C:\\Test\\Test.txt";

    // 1. CreateFile을 이용해 Win32 API의 HANDLE 생성
    HANDLE file_handle = CreateFileW(file_path.c_str(), GENERIC_READ | GENERIC_WRITE,
        FILE_SHARE_READ | FILE_SHARE_WRITE, nullptr, CREATE_ALWAYS, 
        FILE_ATTRIBUTE_NORMAL, nullptr);

    if (file_handle == INVALID_HANDLE_VALUE)
    {
        std::cout << "CreateFileW Failed : " << GetLastError() << "\n";
        return -1;
    }

    // 2. HANDLE을 파일 디스크립터로 변환
    int fd = _open_osfhandle((long)file_handle, _O_BINARY);

    if (fd == -1)
    {
        std::cout << "_open_osfhandle Failed : \n";
        CloseHandle(file_handle);
        return -1;
    }

    // 3. 파일 디스크립터를 FILE*로 변환
    FILE* fp = _fdopen(fd, "wb");

    if (nullptr == fp)
    {
        std::cout << "_fdopen Failed : " << errno << "\n";
        _close(fd);
        return -1;
    }

    // 4. 파일에 데이터 쓰기
    fwrite("whalec.io", 9, 1, fp);

    // 5. FILE* 닫기
    fclose(fp);

    return 0;
}

위 코드는 Win32 API를 사용해 파일 핸들을 생성하고, 그 핸들을 _open_osfhandle로 파일 디스크립터로 변환한 후, _fdopen을 통해 FILE*로 변환하여 데이터를 쓰는 예제입니다.

주의사항

파일 객체를 생성하고 닫는 방식은 사용하는 함수에 따라 달라집니다. HANDLE, 파일 디스크립터, 그리고 FILE*는 각각 다른 유형의 파일 객체를 나타내며, 이에 따라 각각의 종료 함수도 다릅니다.

파일 객체 종료 함수 표

파일 생성 함수객체 유형종료 함수
CreateFileFile HandleCloseHandle
_open_osfhandleFile Descriptor_close
_fdopenFILE*fclose

파일을 처리할 때, 파일 객체가 어떤 방식으로 생성 되었는지에 따라 적절한 종료 함수를 사용해야 합니다. 이 과정을 제대로 처리하지 않으면 파일 리소스를 올바르게 해제하지 못해 메모리 누수나 파일 리소스 부족 오류가 발생할 수 있습니다.

잘못된 종료 함수 사용 시 발생하는 문제

아래 코드는 위와 같은 방식으로 파일을 1000번 생성하는 루프를 돌리는 예제입니다. 여기서 종료 함수의 사용에 따른 문제점을 살펴보겠습니다.

#include <windows.h>
#include <io.h>
#include <fcntl.h>

#include <iostream>
#include <string>

int main()
{
    for (int i = 0; i < 1000; i++)
    {
        std::cout << "Loop #" << i << "\n";

        std::wstring file_path = L"C:\\Test\\Test" + std::to_wstring(i) + L".txt";

        HANDLE file_handle = CreateFileW(file_path.c_str(),
            GENERIC_READ | GENERIC_WRITE, FILE_SHARE_READ | FILE_SHARE_WRITE,
            nullptr, CREATE_ALWAYS, FILE_ATTRIBUTE_NORMAL, nullptr);

        if (file_handle == INVALID_HANDLE_VALUE)
        {
            std::cout << "CreateFileW Failed : " << GetLastError() << "\n";
            return -1;
        }

        int fd = _open_osfhandle((long)file_handle, _O_BINARY);

        if (fd == -1)
        {
            std::cout << "_open_osfhandle Failed : \n";
            CloseHandle(file_handle);
            return -1;
        }

        FILE* fp = _fdopen(fd, "wb");

        if (NULL == fp)
        {
            std::cout << "_fdopen Failed : " << errno << "\n";
            _close(fd);
            return -1;
        }

        fwrite("whalec.io", 9, 1, fp);

        // 잘못된 종료 방식 - fclose(fp)로 해야 함
        _close(fd); // 오류 발생 가능성

        std::cout << "Loop #" << i << " Success \n";
    }

    return 0;
}

위 코드에서 50번째 라인에서 fclose 대신 _close를 사용하면 파일 객체가 정상적으로 닫히지 않아서 errno 24 (“Too many open files”) 오류가 발생할 수 있습니다. 그 이유는 _fdopen으로 변환된 FILE*는 파일 스트림으로 관리되며, 스트림 객체를 닫기 위해서는 반드시 fclose를 사용해야 하기 때문입니다.

_close(fd)는 파일 디스크립터만 닫을 뿐, FILE* 객체는 여전히 열려 있으므로 리소스가 제대로 해제되지 않습니다. 따라서, 파일을 닫을 때는 반드시 fclose(fp)를 사용해야 합니다.

마치며

HANDLE을 FILE*로 변환하는 방법에 대해서 알아보았습니다. 변환하는 방법은 그리 어렵지 않으나 리소스 누수 현상이 발생할 수 있으니, 각 단계에서 리소스 생성과 종료 함수를 잘 맞게 사용해야 합니다.

참고 자료

]]>
https://studioys.me/handle%ec%9d%84-file%eb%a1%9c-%eb%b3%80%ed%99%98%ed%95%98%eb%8a%94-%eb%b0%a9%eb%b2%95/feed/ 0
정적 라이브러리 명명 규칙 https://studioys.me/%ec%a0%95%ec%a0%81-%eb%9d%bc%ec%9d%b4%eb%b8%8c%eb%9f%ac%eb%a6%ac-%eb%aa%85%eb%aa%85-%ea%b7%9c%ec%b9%99/ https://studioys.me/%ec%a0%95%ec%a0%81-%eb%9d%bc%ec%9d%b4%eb%b8%8c%eb%9f%ac%eb%a6%ac-%eb%aa%85%eb%aa%85-%ea%b7%9c%ec%b9%99/#respond Sun, 15 Sep 2024 03:41:59 +0000 https://studioys.me/?p=472 더 보기정적 라이브러리 명명 규칙]]> 일반적으로 솔루션 빌드할 때, 정적 라이브러리도 솔루션에 포함하여 빌드하기 때문에 문제없이 사용할 수 있습니다. 하지만 외부에서 만들어진 정적 라이브러리를 솔루션에 링크할 경우, 현재 솔루션의 빌드 환경과 정적 라이브러리의 빌드 환경이 다를 수 있어 링크 오류가 발생할 수 있습니다. 이러한 문제는 빌드가 제대로 되지 않는 주요 원인이 될 수 있습니다.

이를 방지하기 위해, 정적 라이브러리를 생성할 때 컴파일러 버전, 빌드 환경 등의 정보를 파일 이름에 포함시켜 명명 규칙을 정하는 것이 좋습니다. 이를 통해 개발 환경에 맞는 정적 라이브러리를 쉽게 식별하고 사용할 수 있습니다.

정적 라이브러리 명명 규칙

제가 주로 사용하는 정적 라이브러리 명명 규칙은 다음과 같습니다.
예시로 StudioYS라는 프로젝트 이름을 사용하겠습니다.

PlatformRumtime LibraryMSVCBuild ConfigurationOutput
x86MDVisual Studio 2019ReleaselibStudioYS_MD_2019_x86.lib
x64MDVisual Studio 2019ReleaselibStudioYS_MD_2019_x64.lib
x86MDVisual Studio 2019DebuglibStudioYS_MD_2019_x86D.lib
x64MDVisual Studio 2019DebuglibStudioYS_MD_2019_x64D.lib
x86MTVisual Studio 2019ReleaselibStudioYS_MT_2019_x86.lib
x64MTVisual Studio 2019ReleaselibStudioYS_MT_2019_x64.lib
x86MTVisual Studio 2019DebuglibStudioYS_MT_2019_x86D.lib
x64MtVisual Studio 2019DebuglibStudioYS_MT_2019_x64D.lib

각 항목 설명

  • libStudioYS :
    라이브러리 이름입니다. 이 부분은 프로젝트나 라이브러리의 이름을 나타냅니다. 여기서는 프로젝트 이름이 StudioYS이므로 libStudioYS로 설정되었습니다.
  • MD/MT :
    런타임 라이브러리 설정을 나타냅니다.
    • MD: 다중 스레드를 사용하는 DLL과 함께 C 런타임 라이브러리를 동적으로 링크하는 경우를 의미합니다. (Multithreaded DLL)
    • MT: 다중 스레드를 사용하는 C 런타임 라이브러리를 정적으로 링크하는 경우를 의미합니다. (Multithreaded static)
  • 2019:
    사용된 컴파일러 버전을 나타냅니다. 이 경우 Visual Studio 2019에서 빌드되었음을 의미합니다. 이는 컴파일러 버전 차이로 인해 발생할 수 있는 호환성 문제를 예방하는 데 도움을 줍니다.
  • x86/x64:
    플랫폼(Platform)을 나타냅니다.
    • x86: 32비트 아키텍처용 라이브러리
    • x64: 64비트 아키텍처용 라이브러리
  • D (옵션):
    빌드 구성(Build Configuration)을 나타냅니다. DDebug 빌드를 나타내며, 디버깅에 필요한 심볼 정보가 포함되어 있음을 의미합니다. 릴리즈 빌드에서는 이 접미사를 생략합니다.

예시

  • libgs_MD_2019_x86.lib:
    32비트 플랫폼(x86)에서 Visual Studio 2019를 사용해 다중 스레드 DLL 방식으로 동적 링크된 릴리즈 빌드입니다.
  • libgs_MT_2019_x64D.lib:
    64비트 플랫폼(x64)에서 Visual Studio 2019를 사용해 다중 스레드 정적 링크 방식으로 디버그 빌드된 라이브러리입니다.

이러한 명명 규칙을 따르면, 빌드 환경에 맞는 정적 라이브러리를 쉽게 선택하여 사용할 수 있으며, 빌드 오류나 호환성 문제를 예방할 수 있습니다.

마치며

위 정적라이브러리 명명 규칙은 제가 주로 사용하는 환경에 맞춘 것입니다. 각자 개발 환경에 맞게 명명 규칙을 설정하는 것이 좋습니다. 명명 규칙은 프로젝트와 빌드 환경에 따라 달라질 수 있습니다.

또한, 부스트 라이브러리에서도 명명 규칙을 참고할 수 있습니다. 부스트의 명명 규칙은 다음 링크에서 확인하실 수 있습니다.

]]>
https://studioys.me/%ec%a0%95%ec%a0%81-%eb%9d%bc%ec%9d%b4%eb%b8%8c%eb%9f%ac%eb%a6%ac-%eb%aa%85%eb%aa%85-%ea%b7%9c%ec%b9%99/feed/ 0
윈도우가 다른 윈도우에 가려졌는지 확인하는 방법 – CombineRgn https://studioys.me/%ec%9c%88%eb%8f%84%ec%9a%b0%ea%b0%80-%eb%8b%a4%eb%a5%b8-%ec%9c%88%eb%8f%84%ec%9a%b0%ec%97%90-%ea%b0%80%eb%a0%a4%ec%a1%8c%eb%8a%94%ec%a7%80-%ed%99%95%ec%9d%b8%ed%95%98%eb%8a%94-%eb%b0%a9%eb%b2%95-com/ https://studioys.me/%ec%9c%88%eb%8f%84%ec%9a%b0%ea%b0%80-%eb%8b%a4%eb%a5%b8-%ec%9c%88%eb%8f%84%ec%9a%b0%ec%97%90-%ea%b0%80%eb%a0%a4%ec%a1%8c%eb%8a%94%ec%a7%80-%ed%99%95%ec%9d%b8%ed%95%98%eb%8a%94-%eb%b0%a9%eb%b2%95-com/#respond Fri, 13 Sep 2024 01:50:22 +0000 https://studioys.me/?p=241 더 보기윈도우가 다른 윈도우에 가려졌는지 확인하는 방법 – CombineRgn]]> 윈도우 화면을 제어하는 프로그램을 개발할 때, 내가 만든 윈도우 또는 제어 대상 윈도우가 다른 윈도우에 가려졌는지 확인하는 상황이 발생할 수 있습니다. 사용자 관점에서는 다른 윈도우에 완전히 가려진 윈도우는 보이지 않기 때문에, 이런 경우 해당 윈도우를 없는 것처럼 처리하는 것이 좋습니다. 이번 포스팅에서는 CombineRgn과 GetNextWindow Win32 API를 활용하여 윈도우가 다른 윈도우에 가려졌는지 확인하는 방법을 살펴보겠습니다.

윈도우가 겹치는 유형

Win32API에서 윈도우 핸들(HWND) 기반으로 윈도우가 최소화 되었는지, 또는 현재 화면에 보여지고 있는 윈도우인지 여부를 쉽게 파악할 수 있습니다. 그러나 윈도우가 다른 윈도우에 의해 가려졌는지 여부를 직접적으로 확인하는 기능은 제공되지 않습니다.

일반적으로 윈도우가 겹치는 경우는 다음 그림과 같이 4개 유형으로 나누어 살펴볼 수 있습니다

  • Case #1: 다른 윈도우와 겹치는 않는 경우
  • Case #2: 다른 윈도우에 일부 겹친 경우
  • Case #3: 다른 윈도우에 가려져 보이지 않는 경우
  • Case #4: 다른 윈도우에 가려졌으나 윈도우가 투명도가 존재하여 화면이어서 보이는 경우

윈도우가 다른 윈도우에 가려졌는지 확인하는 방법

윈도우가 다른 윈도우에 가려졌는지 확인하려면, 대상 윈도우의 영역(Region)에서 상위 윈도우의 영역을 빼는 방식으로 계산할 수 있습니다. 이때, 투명 속성을 가진 윈도우는 계산에서 제외해야 합니다. 이를 통해 대상 윈도우 영역에서 남은 영역이 없으면, 해당 윈도우가 다른 윈도우에 완전히 가려진 것으로 판단할 수 있습니다.

이렇게 Region을 계산을 할 경우 아래 그림과 같이 표현할 수 있습니다.

예시 코드 ( CombineRgn 활용 )

아래 소스코드는 윈도우가 다른 윈도우에 의해 가려졌는지 확인하는 소스 코드입니다.

BOOL IsObscuredWindow(HWND hwnd)
{
    if (hwnd == NULL) return FALSE;

    BOOL is_obscured = FALSE;
    HWND current_window = hwnd;

    // 대상 윈도우의 영역 가져오기
    RECT target_rect;
    if (!GetWindowRect(hwnd, &target_rect)) return FALSE;
    HRGN target_region = CreateRectRgnIndirect(&target_rect);
    if (target_region == NULL) return FALSE;

    // 상위 윈도우를 탐색하면서 영역 비교
    while ((current_window = GetNextWindow(current_window, GW_HWNDPREV)) != NULL)
    {
        // 상위 윈도우가 보이지 않으면 건너뛴다
        if (!IsWindowVisible(current_window)) continue;

        // 상위 윈도우의 확장 스타일 가져오기
        LONG_PTR style = GetWindowLongPtrW(current_window, GWL_EXSTYLE);

        // 투명 또는 레이어된 윈도우는 무시
        if (style & (WS_EX_LAYERED | WS_EX_NOREDIRECTIONBITMAP | WS_EX_TRANSPARENT)) continue;

        // 상위 윈도우의 영역 가져오기
        RECT current_rect;
        if (!GetWindowRect(current_window, current_rect)) continue;

        // 영역 생성 및 비교
        HRGN current_region = CreateRectRgnIndirect(current_rect);
        if (current_region == NULL) continue;

        // 두 윈도우 영역 비교
        int combine_result = CombineRgn(target_region, target_region, current_region, RGN_DIFF);
        DeleteObject(current_region);

        // 대상 윈도우가 완전히 가려졌을 때
        if (combine_result == NULLREGION)
        {
            is_obscured = TRUE;
            break;
        }
    }

    // 메모리 해제
    DeleteObject(target_region);

    return is_obscured;
}

참고 자료

]]>
https://studioys.me/%ec%9c%88%eb%8f%84%ec%9a%b0%ea%b0%80-%eb%8b%a4%eb%a5%b8-%ec%9c%88%eb%8f%84%ec%9a%b0%ec%97%90-%ea%b0%80%eb%a0%a4%ec%a1%8c%eb%8a%94%ec%a7%80-%ed%99%95%ec%9d%b8%ed%95%98%eb%8a%94-%eb%b0%a9%eb%b2%95-com/feed/ 0