[WinAPI] 작업 스케줄러 등록 – 관리자 권한 프로그램 부팅 시 자동 실행

최근  프로젝트를 진행하면서 발생했던 이슈에 대해 정리 할겸 글을 씁니다.

컴퓨터를 부팅했을 때 프로그램을 자동 실행 방법은 여러 가지가 있습니다.

1. 시작 프로그램 폴더에 파일 이동

Windows + R 키를 눌러 shell:startup를 입력하면 시작프로그램 폴더를 볼 수 있습니다.
여기에 exe 파일을 이동하면 부팅 시 프로그램이 실행 됩니다.

2. 레지스트리에 등록

Windows + R 키를 눌러 regedit을 입력하면 레지스트리 편집기가 실행됩니다.
HKEY_CURRENT_USER\SOFTWARE\Microsoft\Windows\CurrentVersion에 레지스트리를 등록하는 방법이 있습니다.

3. 서비스 등록

Windows + R 키를 눌러 services.msc을 입력하면 서비스가 실행됩니다.
여기에 서비스를 등록하는 방법이 있습니다.

4. 작업 스케줄러에 등록

시작 메뉴에 작업 스케줄러를 입력하여 작업 스케줄러를 실행하는 방법입니다.

첫번째와 두번째 방법은 일반 권한일 땐 사용할 순 있으나, 관리자 권한일 때는 사용할 수 없는 방식 입니다.
C++로 서비스에 등록하는 방법은 많이 있으니 네번째 작업 스케줄러에 등록하는 방법을 알아보겠습니다.
예를 들어 현재 등록되어 있는 ocam을 보면 작업스케줄러에 아래와 같이 등록되어 있습니다.

위와 같이 작업 스케줄러에 경로, 이름, 관리자 권한, 트리거, 동작을 등록해야 합니다. (트리거는 어떠한 동작을 하기 위한 조건을 의미)
우선 필요한 헤더와 라이브러리는 아래와 같습니다.

#include <wincred.h>
#include <taskschd.h>
#pragma comment(lib, "taskschd.lib")
#pragma comment(lib, "comsupp.lib")
#pragma comment(lib, "credui.lib")

코드는 아래와 같습니다.

BOOL RegistAutoExecuteTaskSchedule(CString strPath, CString strTaskName, CString strUserName, CString strDesc)
{
    //  ------------------------------------------------------
    //  Create a name for the task.
    //  ------------------------------------------------------
    //  Initialize COM.
    HRESULT hr = CoInitializeEx(NULL, COINIT_MULTITHREADED);
    if (FAILED(hr))
    {
        TRACE(_T("\nCoInitializeEx failed: %x"), hr);
        return FALSE;
    }
    else
    {
        //  Set general COM security levels.
        hr = CoInitializeSecurity(
            NULL,
            -1,
            NULL,
            NULL,
            RPC_C_AUTHN_LEVEL_PKT_PRIVACY,
            RPC_C_IMP_LEVEL_IMPERSONATE,
            NULL,
            0,
            NULL);
        if (FAILED(hr))
        {
            TRACE(_T("\nCoInitializeSecurity failed: %x"), hr);
            CoUninitialize();
            return FALSE;
        }
    }

    //  ------------------------------------------------------
    //  Create an instance of the Task Service. 
    ITaskService *pService = NULL;
    hr = CoCreateInstance(CLSID_TaskScheduler,
                          NULL,
                          CLSCTX_INPROC_SERVER,
                          IID_ITaskService,
                          (void**)&pService);
    if (FAILED(hr))
    {
        TRACE(_T("\nFailed to create an instance of ITaskService: %x"), hr);
        CoUninitialize();
        return FALSE;
    }

    //  Connect to the task service.
    hr = pService->Connect(_variant_t(), _variant_t(), _variant_t(), _variant_t());
    if (FAILED(hr))
    {
        TRACE(_T("\nITaskService::Connect failed: %x"), hr);
        pService->Release();
        CoUninitialize();
        return FALSE;
    }

    //  ------------------------------------------------------
    //  Get the pointer to the root task folder.  This folder will hold the
    //  new task that is registered.
    ITaskFolder *pRootFolder = NULL;
    hr = pService->GetFolder(_bstr_t(_T("\\")), &pRootFolder);
    if (FAILED(hr))
    {
        TRACE(_T("\nCannot get Root Folder pointer: %x"), hr);
        pService->Release();
        CoUninitialize();
        return FALSE;
    }

    ITaskFolder *pFolder = NULL;
    hr = pRootFolder->CreateFolder(_bstr_t(strPath), CComVariant(), &pFolder);
    if (hr == 0x800700b7/*already exists*/)
        hr = pRootFolder->GetFolder(_bstr_t(strPath), &pFolder);

    if (FAILED(hr))
    {
        TRACE(_T("\nCannot create Folder pointer: %x"), hr);
        pService->Release();
        CoUninitialize();
        return FALSE;
    }
    pRootFolder->Release();

    // If the same task exists, remove it.
    IRegisteredTask **ppTask = NULL;
    hr = pFolder->GetTask(_bstr_t(strTaskName), ppTask);
    if (SUCCEEDED(hr))
        pFolder->DeleteTask(_bstr_t(strTaskName), 0);

    //  Create the task builder object to create the task.;
    ITaskDefinition *pTask = NULL;
    hr = pService->NewTask(0, &pTask);
    if (FAILED(hr))
    {
        TRACE(_T("\nFailed to CoCreate an instance of the TaskService class: %x"), hr);
        pFolder->Release();
        CoUninitialize();
        return FALSE;
    }
    pService->Release();  // COM clean up.  Pointer is no longer used.

    //  ------------------------------------------------------
    //  Get the registration info for setting the identification.
    IRegistrationInfo *pRegInfo= NULL;
    hr = pTask->get_RegistrationInfo(&pRegInfo);
    if (FAILED(hr))
    {
        TRACE(_T("\nCannot get identification pointer: %x"), hr);
        pFolder->Release();
        pTask->Release();
        CoUninitialize();
        return FALSE;
    }

    // 만든 이
    hr = pRegInfo->put_Author(_bstr_t(strUserName));
    if (FAILED(hr))
    {
        TRACE(_T("\nCannot put identification info: %x"), hr);
        pFolder->Release();
        pTask->Release();
        CoUninitialize();
        return FALSE;
    }

    // 설명
    hr = pRegInfo->put_Description(_bstr_t(strDesc));
    if (FAILED(hr))
    {
        TRACE(_T("\nCannot put Description info: %x"), hr);
        pFolder->Release();
        pTask->Release();
        CoUninitialize();
        return FALSE;
    }

    // 만든 날짜
    CTime tm = CTime::GetCurrentTime();
    CString strCurrent;
    strCurrent.Format(_T("%04d-%02d-%02dT%02d:%02d:%02d"), tm.GetYear(), tm.GetMonth(), tm.GetDay(), tm.GetHour(), tm.GetMinute(), tm.GetSecond());
    hr = pRegInfo->put_Date(_bstr_t(strCurrent));
    if (FAILED(hr))
    {
        TRACE(_T("\nCannot put Date info: %x"), hr);
        pFolder->Release();
        pTask->Release();
        CoUninitialize();
        return FALSE;
    }

    pRegInfo->Release();  // COM clean up.  Pointer is no longer used.

    //  ------------------------------------------------------
    // 일반 탭 - 보안 옵션 - 가장 높은 수준의 권한으로 실행
    IPrincipal *pPrincipal = NULL;
    hr = pTask->get_Principal(&pPrincipal);
    if (FAILED(hr))
    {
        TRACE(_T("\nCannot get pricipal info: %x"), hr);
        pFolder->Release();
        pTask->Release();
        CoUninitialize();
        return FALSE;
    }

    hr =  pPrincipal->put_RunLevel(TASK_RUNLEVEL_HIGHEST);
    if (FAILED(hr))
    {
        TRACE(_T("\nCannot pet run level highest: %x"), hr);
        pFolder->Release();
        pTask->Release();
        CoUninitialize();
        return FALSE;
    }

    //  ------------------------------------------------------
    //  Get the trigger collection to insert the daily trigger.
    // 트리거 탭
    ITriggerCollection *pTriggerCollection = NULL;
    hr = pTask->get_Triggers(&pTriggerCollection);
    if (FAILED(hr))
    {
        TRACE(_T("\nCannot get trigger collection: %x"), hr);
        pFolder->Release();
        pTask->Release();
        CoUninitialize();
        return FALSE;
    }

    //////////////////////////////////////////////////////////////////////////////////
    // 로그온 트리거
    ITrigger *pTrigger = NULL;
    hr = pTriggerCollection->Create(TASK_TRIGGER_LOGON, &pTrigger);
    pTriggerCollection->Release();
    if (FAILED(hr))
    {
        TRACE(_T("\nCannot create the trigger: %x"), hr);
        pFolder->Release();
        pTask->Release();
        CoUninitialize();
        return FALSE;
    }

    ILogonTrigger *pLogonTrigger = NULL;
    hr = pTrigger->QueryInterface(IID_ILogonTrigger, (void**)&pLogonTrigger);
    pTrigger->Release();
    if (FAILED(hr))
    {
        TRACE(_T("\nQueryInterface call on ILogonTrigger failed: %x"), hr);
        pFolder->Release();
        pTask->Release();
        CoUninitialize();
        return FALSE;
    }

    CString strTriggerID = _T("Logon Trigger 1");
    hr = pLogonTrigger->put_Id(_bstr_t(strTriggerID));
    if (FAILED(hr))
        TRACE(_T("\nCannot put trigger ID: %x"), hr);

    //  Set the task to start daily at a certain time. The time 
    //  format should be YYYY-MM-DDTHH:MM:SS(+-)(timezone).
    //  For example, the start boundary below
    hr = pLogonTrigger->put_StartBoundary(_bstr_t(strCurrent));
    if (FAILED(hr))
        TRACE(_T("\nCannot put start boundary: %x"), hr);
    //////////////////////////////////////////////////////////////////////////////////


    //  ------------------------------------------------------
    //  Add an action to the task. This task will execute notepad.exe.   
    // 동작 탭
    IActionCollection *pActionCollection = NULL;

    //  Get the task action collection pointer.
    hr = pTask->get_Actions(&pActionCollection);
    if (FAILED(hr))
    {
        TRACE(_T("\nCannot get task collection pointer: %x"), hr);
        pFolder->Release();
        pTask->Release();
        CoUninitialize();
        return FALSE;
    }

    //  Create the action, specifying that it is an executable action.
    IAction *pAction = NULL;
    hr = pActionCollection->Create(TASK_ACTION_EXEC, &pAction);
    pActionCollection->Release();
    if (FAILED(hr))
    {
        TRACE(_T("\nCannot create action: %x"), hr);
        pFolder->Release();
        pTask->Release();
        CoUninitialize();
        return FALSE;
    }

    IExecAction *pExecAction = NULL;
    hr = pAction->QueryInterface(IID_IExecAction, (void**)&pExecAction);
    pAction->Release();
    if (FAILED(hr))
    {
        TRACE(_T("\nQueryInterface call failed for IExecAction: %x"), hr);
        pFolder->Release();
        pTask->Release();
        CoUninitialize();
        return FALSE;
    }

    //  Get the windows directory and set the path to notepad.exe.
    TCHAR tzFullPathName[MAX_PATH] ={0,};
    ::GetModuleFileName((HMODULE)&__ImageBase, tzFullPathName, sizeof(tzFullPathName));
    CString strPathName = tzFullPathName;

    //  Set the path of the executable to notepad.exe.
    hr = pExecAction->put_Path(_bstr_t(strPathName));
    pExecAction->Release();
    if (FAILED(hr))
    {
        TRACE(_T("\nCannot put the executable path: %x"), hr);
        pFolder->Release();
        pTask->Release();
        CoUninitialize();
        return FALSE;
    }

    //  ------------------------------------------------------
    // 조건 탭
    ITaskSettings *pTaskSettings = NULL;
    hr = pTask->get_Settings(&pTaskSettings);
    if (FAILED(hr))
    {
        TRACE(_T("\nCannot get settings collection pointer: %x"), hr);
        pFolder->Release();
        pTask->Release();
        CoUninitialize();
        return FALSE;
    }

    // 컴퓨터의 AC 전원이 켜져 있는경우에만 작업 시작
    hr = pTaskSettings->put_DisallowStartIfOnBatteries(FALSE);
    if (FAILED(hr))
    {
        TRACE(_T("\nCannot create action: %x"), hr);
        pFolder->Release();
        pTask->Release();
        CoUninitialize();
        return FALSE;
    }

    // 컴퓨터가 배터리 전원으로 전환되는 경우 중지
    hr = pTaskSettings->put_StopIfGoingOnBatteries(FALSE);
    pTaskSettings->Release();
    if (FAILED(hr))
    {
        TRACE(_T("\nCannot create action: %x"), hr);
        pFolder->Release();
        pTask->Release();
        CoUninitialize();
        return FALSE;
    }

    //pTaskSettings->put_AllowDemandStart(FALSE); // 요청 시 작업이 실행되도록 허용
    //pTaskSettings->put_AllowHardTerminate(FALSE); // 요청할 때 실행 중인 작업이 끝나지 않으면 강제로 작업 중지
    CString strExecutionTimeLimit = _T("PT0S");
    pTaskSettings->put_ExecutionTimeLimit(_bstr_t(strExecutionTimeLimit)); // 다음 시간 이상 작업이 실행되면 중지
    //  ------------------------------------------------------

    //  ------------------------------------------------------
    //  Securely get the user name and password. The task will
    //  be created to run with the credentials from the supplied 
    //  user name and password.
    CREDUI_INFO cui;
    TCHAR pszName[CREDUI_MAX_USERNAME_LENGTH] = TEXT("");
    TCHAR pszPwd[CREDUI_MAX_PASSWORD_LENGTH] = TEXT("");

    cui.cbSize = sizeof(CREDUI_INFO);
    cui.hwndParent = NULL;
    //  Ensure that MessageText and CaptionText identify
    //  what credentials to use and which application requires them.
    cui.pszMessageText = TEXT("Account information for task registration:");
    cui.pszCaptionText = TEXT("Enter Account Information for Task Registration");
    cui.hbmBanner = NULL;

    //  ------------------------------------------------------
    //  Save the task in the root folder.
    IRegisteredTask *pRegisteredTask = NULL;
    hr = pFolder->RegisterTaskDefinition(
        _bstr_t(strTaskName),
        pTask,
        TASK_CREATE_OR_UPDATE,
        _variant_t(pszName),
        _variant_t(pszPwd),
        //TASK_LOGON_PASSWORD, // 사용자의 로그온 여부 관계없이 실행??
        TASK_LOGON_INTERACTIVE_TOKEN, // 사용자가 로그온할 때만 실행
        _variant_t(_T("")),
        &pRegisteredTask);
    if (FAILED(hr))
    {
        TRACE(_T("\nError saving the Task : %x"), hr);
        SecureZeroMemory(pszName, sizeof(pszName));
        SecureZeroMemory(pszPwd, sizeof(pszPwd));

        pFolder->Release();
        pTask->Release();

        CoUninitialize();
        return FALSE;
    }

    TRACE(_T("\nSuccess! Task successfully registered. "));

    //  Clean up
    SecureZeroMemory(pszName, sizeof(pszName));
    SecureZeroMemory(pszPwd, sizeof(pszPwd));

    pFolder->Release();
    pTask->Release();
    pRegisteredTask->Release();

    CoUninitialize();

    return TRUE;
}

// 예시
RegistAutoExecuteTaskSchedule(_T("Company\\Program Name"), _T("Task Name"), _T("User Name"), _T("Desc"));

Visual C++/MFC로 위 코드를 실행하면 아래와 같이 작업 스케줄러에 추가됩니다.

참고로 한 MSDN은 여기를 참고해주세요