5 bước tạo 1 ứng dụng Windows services bằng ngôn ngữ C

Xin chào các bạn, trong bài viết này mình xin giới thiệu đến các bạn các bước viết chương trình services chạy trên Windows bằng ngôn ngữ C.

Tôi đã tìm được bài hướng dẫn viết services trên Windows bằng ngôn ngữ C++. Tuy nhiên, ngôn ngữ C++ là ngôn ngữ hướng đối tượng, việc gọi các hàm Win32 API được đóng gói bởi class.  Do vậy, mình chọn ngôn ngữ C để viết ứng dụng services sẽ giúp các bạn hiểu rõ được cơ chế hoạt động của services. Khi hiểu được rõ cơ chế hoạt động của windows services, các bạn có thể tham khảo bài viết: “Viết ứng dụng services bằng ngôn ngữ C++”

Mình sẽ minh họa bằng 1 ứng dụng services: “Truy vấn thông tin bộ nhớ và ghi thông tin ra file”.

 

Source code của ứng dụng

#include <windows.h>
#include <stdio.h>

#define SLEEP_TIME 5000
#define LOGFILE "C:\\MyService\\memstatus.log"

SERVICE_STATUS ServiceStatus; 
SERVICE_STATUS_HANDLE hStatus; 

void ServiceMain(int argc, char** argv); 
void ControlHandler(DWORD request); 
int InitService();
int WriteToLog(char* str);


void main() 
{ 
    SERVICE_TABLE_ENTRY ServiceTable[2];
    ServiceTable[0].lpServiceName = L"QueryMemory";
    ServiceTable[0].lpServiceProc = (LPSERVICE_MAIN_FUNCTION)ServiceMain;

    ServiceTable[1].lpServiceName = NULL;
    ServiceTable[1].lpServiceProc = NULL;

    // Start the control dispatcher thread for our service
    StartServiceCtrlDispatcher(ServiceTable); 
}


void ServiceMain(int argc, char** argv) 
{ 
    int error; 

    ServiceStatus.dwServiceType = SERVICE_WIN32; 
    ServiceStatus.dwCurrentState = SERVICE_START_PENDING; 
    ServiceStatus.dwControlsAccepted = SERVICE_ACCEPT_STOP | SERVICE_ACCEPT_SHUTDOWN;
    ServiceStatus.dwWin32ExitCode = 0; 
    ServiceStatus.dwServiceSpecificExitCode = 0; 
    ServiceStatus.dwCheckPoint = 0; 
    ServiceStatus.dwWaitHint = 0; 

    hStatus = RegisterServiceCtrlHandler(
              L"QueryMemory", 
              (LPHANDLER_FUNCTION)ControlHandler); 
    if (hStatus == (SERVICE_STATUS_HANDLE)0) 
    { 
        // Registering Control Handler failed
        return; 
    } 
    // Initialize Service 
    error = InitService(); 
    if (error) 
    {
        // Initialization failed
        ServiceStatus.dwCurrentState = SERVICE_STOPPED; 
        ServiceStatus.dwWin32ExitCode = -1; 
        SetServiceStatus(hStatus, &ServiceStatus); 
        return; 
    } 
    // We report the running status to SCM. 
    ServiceStatus.dwCurrentState = SERVICE_RUNNING; 
    SetServiceStatus (hStatus, &ServiceStatus);

    MEMORYSTATUS memory;
    // The worker loop of a service
    while (ServiceStatus.dwCurrentState == SERVICE_RUNNING)
    {
        char buffer[16];
        GlobalMemoryStatus(&memory);
        sprintf(buffer, "%d", memory.dwAvailPhys);
        int result = WriteToLog(buffer);
        if (result)
        {
            ServiceStatus.dwCurrentState = SERVICE_STOPPED; 
            ServiceStatus.dwWin32ExitCode = -1; 
            SetServiceStatus(hStatus, &ServiceStatus);
            return;
        }

        Sleep(SLEEP_TIME);
    }
    return; 
}

// Service initialization
int InitService() 
{ 
    int result;
    result = WriteToLog("Monitoring started.");
    return(result); 
} 

// Control handler function
void ControlHandler(DWORD request) 
{ 
    switch(request) 
    { 
    case SERVICE_CONTROL_STOP: 
        WriteToLog("Monitoring stopped.");
        ServiceStatus.dwWin32ExitCode = 0; 
        ServiceStatus.dwCurrentState = SERVICE_STOPPED; 
        SetServiceStatus (hStatus, &ServiceStatus);
        return; 

    case SERVICE_CONTROL_SHUTDOWN: 
        WriteToLog("Monitoring stopped.");
        ServiceStatus.dwWin32ExitCode = 0; 
        ServiceStatus.dwCurrentState = SERVICE_STOPPED; 
        SetServiceStatus (hStatus, &ServiceStatus);
        return; 

    default:
        break;
    } 
    // Report current status
    SetServiceStatus(hStatus, &ServiceStatus);
    return; 
} 

int WriteToLog(char* str)
{
    FILE* fpLog;
    fpLog = fopen(LOGFILE, "a+");
    if (fpLog == NULL)
        return -1;
    if(fprintf(fpLog, "%s\n", str) < strlen(str))
    {
        fclose(fpLog);
        return -1;
    }
    fclose(fpLog);
    return 0;
}

Giải thích:

Cấu trúc chương trình service gồm:

  • Hàm main()
  • Hàm ServiceMain()
  • Hàm Control Hanlder

Bước 1: Hàm main() và biến toàn cục

#define SLEEP_TIME 5000
#define LOGFILE "C:\\MyService\\memstatus.log"

SLEEP_TIME: là khoảng thời gian giữa 2 lần truy vấn thông tin memory.

LOGFILE: là đường dẫn lưu file

 

SERVICE_STATUS ServiceStatus;

Biến struct ServiceStatus bao gồm các thông tin trạng thái cho service. Service sẽ dùng struct SERVICE_STATUS trong hàm SetServiceStatus để report status hiện tại cho trình quản lí điều khiển service (SCM).

SERVICE_STATUS_HANDLE hStatus;

Giá trị trỏ tới thông tin status của service. Giá trị trả về của hàm RegisterServiceCtrlHandlerEx

void main()
{
    SERVICE_TABLE_ENTRY ServiceTable[2];
    ServiceTable[0].lpServiceName = L"QueryMemory";
    ServiceTable[0].lpServiceProc = (LPSERVICE_MAIN_FUNCTION)ServiceMain;

    ServiceTable[1].lpServiceName = NULL;
    ServiceTable[1].lpServiceProc = NULL;

    // Start the control dispatcher thread for our service
    StartServiceCtrlDispatcher(ServiceTable);
}

Chương trình service là 1 ứng dụng console. Do vậy, chương trình cần có hàm main(). Hàm main() là điểm bắt đầu của chương trình.

Một chương trình có thể gồm nhiều service. Thông tin về mỗi service được định nghĩa trong struct SERVICE_TABLE_ENTRY gồm có:

  • lpServiceName: tên services
  • lpServiceProc: con trỏ tới hàm ServiceMain

Trong bài viết này, chương trình chỉ đăng kí 1 service nên phần tử thứ 2 của mảng struct SERVICE_TABLE_ENTRY phải gán bằng NULL.

Trình quản lí service (SCM: Services Control Manager) quản lý tất cả các service của ứng dụng và hệ điều hành. Khi SCM start service, hàm main() là điểm bắt đầu của service(gọi là main thread), gọi hàm StartServiceCtrlDispatcher (trong vòng 30s) và trở thành control dispatcher.

  • Dispatcher tạo thêm 1 thread (ngoài thread main()) để run hàm ServiceMain của mỗi service (trong ví dụ này: sử dụng 1 service duy nhất).
  • Dispatcher cũng giám sát việc thực thi của tất cả các service được định nghĩa trong chương trình.
  • Dispatcher sẽ gửi “control request” từ SCM tới service.

Hàm StartServiceCtrlDispatcher trả về khi tất cả các service của chương trình kết thúc. (ví dụ: người dùng stop services hoặc xảy ra lỗi,..)

 

Bước 2: Hàm ServiceMain

Hàm ServiceMain là điểm bắt đầu của service, chạy trong 1 thread được tạo bởi control dispatcher.

hStatus = RegisterServiceCtrlHandler(
L"QueryMemory", (LPHANDLER_FUNCTION)ControlHandler);

Hàm ServiceMain gọi hàm RegisterServiceCtrlHandler để đăng kí hàm ControlHandler xử lý  các request do “control dispatcher” gửi về từ SCM. Hàm RegisterServiceCtrlHandler trả về status handle.

ServiceStatus.dwServiceType = SERVICE_WIN32;
ServiceStatus.dwCurrentState = SERVICE_START_PENDING;
ServiceStatus.dwControlsAccepted = SERVICE_ACCEPT_STOP | SERVICE_ACCEPT_SHUTDOWN;
ServiceStatus.dwWin32ExitCode = 0;
ServiceStatus.dwServiceSpecificExitCode = 0;
ServiceStatus.dwCheckPoint = 0;
ServiceStatus.dwWaitHint = 0;

Khởi tạo cho service bằng việc khai báo các đặc tính và status hiện tại của service.

  • dwServiceType: chỉ ra loại service
  • dwCurrentState: trạng thái hiện tại của service.
  • dwControlsAccepted: thông báo cho SCM biết những request mà service cho phép nhận. 

Ví dụ: ServiceStatus.dwControlsAccepted   = SERVICE_ACCEPT_STOP | SERVICE_ACCEPT_SHUTDOWN, service sẽ chỉ nhận request “stop” và “shutdown” từ SCM.

  • dwWin32ExitCode và dwServiceSpecificExitCode: rất hữu ích trong trường hợp bạn kết thúc chương trình service và muốn có exit code.
  • dwCheckPoint và dwWaitHint: 
error = InitService();

Hàm ghi thời điểm bắt đầu vào file.

if (error)
{
    // Initialization failed
    ServiceStatus.dwCurrentState = SERVICE_STOPPED;
    ServiceStatus.dwWin32ExitCode = -1;
    SetServiceStatus(hStatus, &ServiceStatus);
    return;
}

Nếu việc ghi file không thành công, thông báo cho SCM biết trạng thái của service là stop và kết thúc hàm ServiceMain.

ServiceStatus.dwCurrentState = SERVICE_RUNNING;
SetServiceStatus (hStatus, &ServiceStatus);

Thông báo cho SCM biết trạng thái của service là running.

while (ServiceStatus.dwCurrentState == SERVICE_RUNNING)
{
    char buffer[16];
    GlobalMemoryStatus(&memory);
    sprintf(buffer, "%d", memory.dwAvailPhys);
    int result = WriteToLog(buffer);
    if (result)
    {
        ServiceStatus.dwCurrentState = SERVICE_STOPPED;
        ServiceStatus.dwWin32ExitCode = -1;
        SetServiceStatus(hStatus, &ServiceStatus);
        return;
    }
    Sleep(SLEEP_TIME);
}

Vòng lặp thực hiện việc truy vấn bộ nhớ và ghi thông tin vào file. Nếu quá trình ghi file bị lỗi, chương trình sẽ thông báo trạng thái của service là “stop” tới SCM.

 

Bước 3: Hàm xử lí control request từ SCM

Trong bước 2, hàm ServiceMain gọi hàm RegisterServiceCtrlHandler để đăng kí hàm ControlHandler xử lý  các request do “control dispatcher” gửi về từ SCM. Hàm ControlHandler tương tự với hàm WindowProc trong chuyên đề lập trình Win32.


void ControlHandler(DWORD request)
{
    switch(request)
    {
    case SERVICE_CONTROL_STOP:
        WriteToLog("Monitoring stopped.");

        ServiceStatus.dwWin32ExitCode = 0;
        ServiceStatus.dwCurrentState = SERVICE_STOPPED;
        SetServiceStatus (hStatus, &ServiceStatus);
        return;

    case SERVICE_CONTROL_SHUTDOWN:
        WriteToLog("Monitoring stopped.");

        ServiceStatus.dwWin32ExitCode = 0;
        ServiceStatus.dwCurrentState = SERVICE_STOPPED;
        SetServiceStatus (hStatus, &ServiceStatus);
        return;

    default:
        break;
    }

    // Report current status
    SetServiceStatus(hStatus, &ServiceStatus);
    return;
}

Khi SCM stop service (người dùng stop hoặc xảy ra lỗi), STOP request sẽ được gửi đi. Và hàm ControlHandler làm nhiệm vụ xử lí các request mà SCM gửi đến.

 

Bước 4: Cài đặt và cấu hình service

  • Sau khi code xong, các bạn build chương trình trên với mode Release hoặc Debug.

Kết quả: output là file QueryMemory.exe

  • Copy file QueryMemory.exe vào thư mục C:\MyService.

Sử dụng tiện ích sc.exe có sẵn trên Windows để cài đặt service.

  • Chạy cmd.exe command line ở chế độ Administrator để đảm bảo đủ quyền tạo mới service.
Chạy cmd dưới quyền Admin
Chạy cmd dưới quyền Admin
  • Chạy lệnh sau: sc.exe create MemoryStatus binpath= C:\MyService\QueryMemory.exe

Dòng lệnh  SUCCESS thông báo việc cài đặt service thành công.

Add service vào service applet
Add service vào service applet

 

Sau khi cài đặt service thành công,bạn gõ lệnh services.msc để mở service applet. Service “MemoryStatus” đã được add vào service applet.

Service applet
Service applet

 Chú ý: để xóa service, chạy lệnh: sc delete MemoryStatus

Bước 5: Start/stop service.

Sau khi service “MemoryStatus” được add vào service applet, chúng ta start service và stop service để test việc ghi thông tin bộ nhớ ra file.

Start service
Start service

Chờ 1 thời gian, sau đó stop service. Kiểm tra file memstatus.log

memstatus.log
memstatus.log

 

 

 

Be the first to comment

Leave a Reply