Windows: add windows service

pull/1300/head
Kang Lin 2 years ago
parent e7c1551de1
commit abdcc189ae
  1. 2
      CMakeLists.txt
  2. 10
      src/apps/relay/CMakeLists.txt
  3. 75
      src/apps/relay/windows/CMakeLists.txt
  4. 109
      src/apps/relay/windows/Example.c
  5. 258
      src/apps/relay/windows/Service.cpp
  6. 70
      src/apps/relay/windows/Service.h
  7. 137
      src/apps/relay/windows/ServiceInstaller.cpp
  8. 74
      src/apps/relay/windows/ServiceInstaller.h

@ -6,6 +6,8 @@ project(coturn)
set(CMAKE_INCLUDE_CURRENT_DIR ON)
set(CMAKE_C_STANDARD 11)
set(CMAKE_C_STANDARD_REQUIRED ON)
set(CMAKE_CXX_STANDARD 11)
set(CMAKE_CXX_STANDARD_REQUIRED OFF)
set(CMAKE_MODULE_PATH ${CMAKE_MODULE_PATH} ${CMAKE_SOURCE_DIR}/cmake)
# TODO: Modify this when the version is released

@ -43,6 +43,16 @@ set(SOURCE_FILES
dbdrivers/dbd_redis.c
)
if(MSVC)
list(APPEND HEADER_FILES
windows/ServiceInstaller.h
windows/Service.h)
list(APPEND SOURCE_FILES
windows/ServiceInstaller.cpp
windows/Service.cpp
)
endif()
find_package(SQLite)
if(SQLite_FOUND)
list(APPEND turnserver_LIBS SQLite::sqlite)

@ -0,0 +1,75 @@
# The file is a example of windows service
# Author: Kang Lin <kl222@126.com>
#
# ### Usage:
# - Build:
#
# cd src\apps\relay\windows
# mkdir build
# cd build
# cmake ..
# cmake --build .
#
# - Programe:
#
# cd bin\Debug
# dir
#
# 2025/05/29 11:12 <DIR> .
# 2025/05/29 11:12 <DIR> ..
# 2025/05/29 11:12 148,480 coturn_example.exe
# 2025/05/29 11:12 3,543,040 coturn_example.pdb
#
# - Usage:
#
# ; Show usage
# coturn_example.exe -h
# ; Using Administrator Privileges to install service
# coturn_example.exe -install
# ; Using Administrator Privileges to remove service
# coturn_example.exe -remove
#
# ; Viewing Log Events Using the Event Manager
# ; Managing coturn services using the service manager
cmake_minimum_required(VERSION 3.5)
set(CMAKE_INCLUDE_CURRENT_DIR ON)
set(CMAKE_C_STANDARD 11)
set(CMAKE_C_STANDARD_REQUIRED ON)
set(CMAKE_CXX_STANDARD 11)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
project(coturn_example)
IF(NOT WIN32)
message(FATAL_ERROR "OS must be windows")
ENDIF()
set(HEADER_FILES
ServiceInstaller.h
Service.h
)
set(SOURCE_FILES
ServiceInstaller.cpp
Service.cpp
Example.c
)
add_executable(${PROJECT_NAME} ${SOURCE_FILES} ${HEADER_FILES})
set_target_properties(${PROJECT_NAME} PROPERTIES
RUNTIME_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/bin
)
INSTALL(TARGETS ${PROJECT_NAME}
RUNTIME DESTINATION "${CMAKE_INSTALL_BINDIR}"
COMPONENT Runtime
)
install(DIRECTORY
$<TARGET_FILE_DIR:${PROJECT_NAME}>/
DESTINATION DESTINATION "${CMAKE_INSTALL_BINDIR}"
COMPONENT Runtime
)

@ -0,0 +1,109 @@
/*!
* Example of use service
* \author Kang Lin <kl222@126.com>
*
* ### Usage:
* - Build:
*
* cd src\apps\relay\windows
* mkdir build
* cd build
* cmake ..
* cmake --build .
*
* - Programe:
*
* cd bin\Debug
* dir
*
* 2025/05/29 11:12 <DIR> .
* 2025/05/29 11:12 <DIR> ..
* 2025/05/29 11:12 148,480 coturn_example.exe
* 2025/05/29 11:12 3,543,040 coturn_example.pdb
*
* - Usage:
*
* ; Show usage
* coturn_example.exe -h
* ; Using Administrator Privileges to install service
* coturn_example.exe -install
* ; Using Administrator Privileges to remove service
* coturn_example.exe -remove
*
* ; Viewing Log Events Using the Event Manager
* ; Managing coturn services using the service manager
*/
#include "Service.h"
#include "ServiceInstaller.h"
#include <stdio.h>
#include <strsafe.h>
#include <tchar.h>
#include <windows.h>
static BOOL g_exit = FALSE;
unsigned long start(int argc, char *argv[]) {
char msg[1024];
sprintf_s(msg, 1024, "Start:argc:[%d]:", argc);
GetServiceLog()(msg);
for (int i = 0; i < argc; i++) {
sprintf_s(msg, 1024, " %s", argv[i]);
GetServiceLog()(msg);
}
return 0;
}
unsigned long run() {
int num = 1;
GetServiceLog()("run ...");
do {
char buf[64];
sprintf_s(buf, 64, "run %d", num++);
GetServiceLog()(buf);
Sleep(1000);
} while (!g_exit);
GetServiceLog()("run end");
return 0;
}
void stop() {
GetServiceLog()("stop");
g_exit = TRUE;
}
int main(int argc, char *argv[]) {
printf("Log file: d:\\coturn_example.log\n");
SetLogFile("d:\\coturn_example.log");
GetServiceLog()("main start");
const char *pServiceName = _T("coturn_example");
if ((argc > 1) && ((*argv[1] == '-' || (*argv[1] == '/')))) {
if (_stricmp("install", argv[1] + 1) == 0) {
printf("Install service ......\n");
// Install the service when the command is
// "-install" or "/install".
InstallService(pServiceName, // Name of service
pServiceName, // Name to display
SERVICE_AUTO_START, // Service start type
_T(""), // Dependencies, format: "dep1\0dep2\0\0"
_T("NT AUTHORITY\\LocalService"), // Service running account(local server)
NULL // Password of the account
);
} else if (_stricmp("remove", argv[1] + 1) == 0) {
printf("Remove service ......\n");
// Uninstall the service when the command is
// "-remove" or "/remove".
UninstallService(pServiceName);
} else {
printf("%s\nUsage:\n%s\n%s\n%s\n", argv[0],
"\t-install: install service requires administrator privileges",
"\t-remove: remove service requires administrator privileges",
"\t-h: help");
}
} else {
printf("Run service ......\n");
ServiceRun(pServiceName, start, run, stop);
}
GetServiceLog()("main end");
return 0;
}

@ -0,0 +1,258 @@
/*!
* Service
* \author Kang Lin <kl222@126.com>
* \see https://learn.microsoft.com/windows/win32/services/service-program-tasks
*/
#include "Service.h"
#include <AtlBase.h>
#include <AtlConv.h>
#include <fstream>
#include <stdio.h>
#include <tchar.h>
#include <windows.h>
struct WindowsService {
TCHAR name[MAX_PATH]; // Service name
fnServiceStart start;
fnServiceRun run;
fnServiceStop stop;
// Current service status handle. don't need close it.
// See: https://learn.microsoft.com/windows/win32/api/winsvc/nf-winsvc-registerservicectrlhandlera?redirectedfrom=MSDN&devlangs=cpp&f1url=%3FappId%3DDev11IDEF1%26l%3DZH-CN%26k%3Dk(winsvc%252FRegisterServiceCtrlHandler)%3Bk(RegisterServiceCtrlHandler)%3Bk(DevLang-C%252B%252B)%3Bk(TargetOS-Windows)%26rd%3Dtrue
SERVICE_STATUS_HANDLE handle;
HANDLE hEvent;
SERVICE_STATUS status; // Current service status
};
static WindowsService g_Service;
static std::string g_logFile;
void SetLogFile(const char* pFile) {
if (pFile)
g_logFile = pFile;
}
void ServiceLog(char *msg) {
if (g_logFile.empty()) {
USES_CONVERSION;
OutputDebugString(A2T(msg));
} else {
std::ofstream ofs(g_logFile, std::ios_base::app);
if (ofs.is_open()) {
ofs << "[" << ::GetCurrentProcessId() << ":" << ::GetCurrentThreadId() << "] " << msg << "\n";
ofs.close();
return;
}
}
}
static fnServiceLog g_Log = ServiceLog;
fnServiceLog SetServiceLog(fnServiceLog log) {
fnServiceLog oldLog = g_Log;
if (log)
g_Log = log;
else
g_Log = ServiceLog;
return oldLog;
}
fnServiceLog GetServiceLog() { return g_Log; }
/*!
* Allows any thread to log an error message
* \param lpszFunction - name of function that failed
* \param dwErr - error code returned from the function
* \return none
* \see https://learn.microsoft.com/windows/win32/eventlog/event-logging
*/
void ServiceReportEvent(LPTSTR lpszFunction, DWORD dwErr = NO_ERROR) {
USES_CONVERSION;
HANDLE hEventSource = NULL;
LPCTSTR lpszStrings[1] = {0};
TCHAR szBuffer[256] = {0};
hEventSource = RegisterEventSource(NULL, g_Service.name);
if (hEventSource) {
WORD wType = EVENTLOG_SUCCESS;
if (NO_ERROR == dwErr) {
_stprintf_s(szBuffer, ARRAYSIZE(szBuffer), lpszFunction);
wType = EVENTLOG_INFORMATION_TYPE;
} else {
_stprintf_s(szBuffer, ARRAYSIZE(szBuffer), _T("%s [0x%08X]"), lpszFunction, dwErr);
wType = EVENTLOG_ERROR_TYPE;
}
g_Log(T2A(szBuffer));
lpszStrings[0] = szBuffer;
BOOL bRet = ReportEvent(hEventSource, // Event log handle
wType, // Event type
0, // Event category
0, // Event identifier
NULL, // No user security identifier
sizeof(lpszStrings) / sizeof(LPCTSTR), // Size of lpszStrings array
0, // No binary data
lpszStrings, // Array of strings
NULL); // No binary data
if (!bRet) {
TCHAR buf[1024] = {0};
_stprintf_s(buf, ARRAYSIZE(buf), _T("ReportEvent fail: %s [0x%08X]"), lpszFunction, dwErr);
g_Log(T2A(buf));
}
DeregisterEventSource(hEventSource);
} else {
TCHAR buf[1024] = {0};
_stprintf_s(buf, ARRAYSIZE(buf), _T("%s [0x%08X]"), lpszFunction, dwErr);
g_Log(T2A(buf));
}
}
/*!
* Sets the current service status and reports it to the SCM.
*
* \param dwCurrentState - the state of the service (see SERVICE_STATUS)
* \param dwWin32ExitCode - error code to report
* \param dwWaitHint - Estimated time for pending operation, in milliseconds
* \return none
*/
void ServiceReportStatus(DWORD dwCurrentState, DWORD dwWin32ExitCode, DWORD dwWaitHint) {
static DWORD dwCheckPoint = 1;
// Fill in the SERVICE_STATUS structure.
g_Service.status.dwCurrentState = dwCurrentState;
g_Service.status.dwWin32ExitCode = dwWin32ExitCode;
g_Service.status.dwWaitHint = dwWaitHint;
g_Service.status.dwControlsAccepted = (dwCurrentState == SERVICE_START_PENDING) ? 0 : SERVICE_ACCEPT_STOP;
g_Service.status.dwCheckPoint =
((dwCurrentState == SERVICE_RUNNING) || (dwCurrentState == SERVICE_STOPPED)) ? 0 : dwCheckPoint++;
// Report the status of the service to the SCM.
SetServiceStatus(g_Service.handle, &g_Service.status);
}
/*!
* Called by SCM whenever a control code is sent to the service
using the ControlService function.
* \param
* \param dwCtrlCode - type of control requested
*/
void WINAPI ServiceControlHandler(DWORD dwCtrl) {
// Handle the requested control code.
switch (dwCtrl) {
case SERVICE_CONTROL_STOP:
case SERVICE_CONTROL_SHUTDOWN:
// SERVICE_STOP_PENDING should be reported before setting the Stop
ServiceReportStatus(SERVICE_STOP_PENDING, NO_ERROR, 0);
if (g_Service.stop)
g_Service.stop();
ServiceReportStatus(g_Service.status.dwCurrentState, NO_ERROR, 0);
return;
case SERVICE_CONTROL_INTERROGATE:
break;
default:
break;
}
}
/*!
* Entry point for the service
* \param dwArgc - number of command line arguments
* \param lpszArgv - array of command line arguments
*/
void WINAPI ServiceMain(DWORD dwArgc, LPTSTR lpszArgv[]) {
USES_CONVERSION;
g_Log("Enter ServiceMain");
// Register the handler function for the service
g_Service.handle = RegisterServiceCtrlHandler(g_Service.name, ServiceControlHandler);
if (!g_Service.handle) {
ServiceReportEvent(_T("RegisterServiceCtrlHandler fail"), GetLastError());
return;
}
// These SERVICE_STATUS members remain as set here
g_Service.status.dwServiceType = SERVICE_WIN32_OWN_PROCESS;
g_Service.status.dwServiceSpecificExitCode = 0;
// Report initial status to the SCM
ServiceReportStatus(SERVICE_START_PENDING, NO_ERROR, 3000);
// Perform service-specific initialization
if (g_Service.start) {
char **argv = new char *[dwArgc];
if (argv) {
for (DWORD i = 0; i < dwArgc; i++) {
argv[i] = T2A(lpszArgv[i]);
}
DWORD r = g_Service.start(dwArgc, argv);
delete[] argv;
if (ERROR_SUCCESS != r) {
ServiceReportEvent(_T("Service start fail"), r);
ServiceReportStatus(SERVICE_STOPPED, r, 0);
return;
}
}
}
// Report running status when initialization is complete.
ServiceReportStatus(SERVICE_RUNNING, NO_ERROR, 0);
// Perform service-specific work.
if (g_Service.run) {
DWORD r = g_Service.run();
if (ERROR_SUCCESS != r) {
ServiceReportEvent(_T("Service run fail"), r);
ServiceReportStatus(SERVICE_STOPPED, r, 0);
return;
}
}
ServiceReportStatus(SERVICE_STOPPED, NO_ERROR, 0);
}
/*!
* Run service
* \param name: the name of service
* \param start: the callback function of start. it maybe NULL
* \param run: the callback function of run. it maybe NULL
* \param stop: the callback function of stop. it maybe NULL
*/
int ServiceRun(char *name, fnServiceStart start, fnServiceRun run, fnServiceStop stop) {
USES_CONVERSION;
int nRet = 0;
g_Log("Enter ServiceRun");
if (!name) {
g_Log("Error: The name is NULL in ServiceRun");
return -1;
}
::ZeroMemory(&g_Service, sizeof(g_Service));
g_Service.start = start;
g_Service.run = run;
g_Service.stop = stop;
if (name) {
size_t nLen = strlen(name);
if (nLen > 0)
_tcsncpy_s(g_Service.name, nLen + 1, A2T(name), nLen);
}
// You can add any additional services for the process to this table.
const SERVICE_TABLE_ENTRY dispatchTable[] = {{g_Service.name, (LPSERVICE_MAIN_FUNCTION)ServiceMain}, {NULL, NULL}};
// This call returns when the service has stopped.
// The process should simply terminate when the call returns.
if (!StartServiceCtrlDispatcher(dispatchTable)) {
ServiceReportEvent(_T("StartServiceCtrlDispatcher fail"), GetLastError());
nRet = GetLastError();
}
return nRet;
}

@ -0,0 +1,70 @@
/*!
* Service
* \author Kang Lin <kl222@126.com>
*/
#ifndef __SERVICE_H_KL_2023_10_20__
#define __SERVICE_H_KL_2023_10_20__
#pragma once
#ifdef __cplusplus
extern "C" {
#endif
/*! Perform service-specific initialization.
* \param arg: the number of the command-line arguments passed in from the SCM(service control manager)
* \param argv: the array of the command-line arguments passed in from the SCM(service control manager)
* \return
* - 0: success
* - other: error code
*/
typedef long (*fnServiceStart)(int arg, char *argv[]);
/*!Perform service work. Block until stopped.
* \return
* - 0: success
* - other: error code
*/
typedef long (*fnServiceRun)();
/*!
* Stop service. The function should return as quickly as possible;
* if it does not return within 30 seconds, the SCM returns an error.
* \see https://learn.microsoft.com/windows/win32/api/winsvc/nc-winsvc-lphandler_function
*/
typedef void (*fnServiceStop)();
/*!
* Run service
* \param name: the name of service
* \param start: the callback function of start. it maybe NULL
* \param run: the callback function of run. it maybe NULL
* \param stop: the callback function of stop. it maybe NULL
*/
int ServiceRun(char *name, fnServiceStart start, fnServiceRun run, fnServiceStop stop);
/*!
* log function
*/
typedef void (*fnServiceLog)(char *msg);
/*!
* Set service log
* \param log: the callback function of log. it maybe NULL
* \return the old log function
*/
fnServiceLog SetServiceLog(fnServiceLog log);
/*!
* Get log function
*/
fnServiceLog GetServiceLog();
void SetLogFile(const char *pFile);
#ifdef __cplusplus
}
#endif
#endif //__SERVICE_H_KL_2023_10_20__

@ -0,0 +1,137 @@
/*!
* Install service
* \author Kang Lin <kl222@126.com>
* \see https://learn.microsoft.com/windows/win32/services/service-configuration-program-tasks
*/
#include "ServiceInstaller.h"
#include <stdio.h>
#include <strsafe.h>
#include <tchar.h>
#include <windows.h>
#include <AtlBase.h>
#include <AtlConv.h>
#include <string>
std::string ErrorString(DWORD nErr = GetLastError()) {
USES_CONVERSION;
std::string msg;
LPVOID lpMsgBuf;
FormatMessage(FORMAT_MESSAGE_ALLOCATE_BUFFER | FORMAT_MESSAGE_FROM_SYSTEM | FORMAT_MESSAGE_IGNORE_INSERTS,
NULL,
nErr, MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT), // Default language
(LPTSTR)&lpMsgBuf, 0, NULL);
if (lpMsgBuf) {
msg = CT2A((LPCTSTR)lpMsgBuf);
}
LocalFree(lpMsgBuf);
return msg;
}
int InstallService(LPTSTR pszServiceName, LPTSTR pszDisplayName, DWORD dwStartType, LPTSTR pszDependencies,
LPTSTR pszAccount, LPTSTR pszPassword) {
SC_HANDLE schSCManager;
SC_HANDLE schService;
TCHAR szUnquotedPath[MAX_PATH];
if (!GetModuleFileName(NULL, szUnquotedPath, MAX_PATH)) {
printf("Cannot install service: [%d] %s\n", GetLastError(), ErrorString().c_str());
return GetLastError();
}
// In case the path contains a space, it must be quoted so that
// it is correctly interpreted. For example,
// "d:\my share\myservice.exe" should be specified as
// ""d:\my share\myservice.exe"".
TCHAR szPath[MAX_PATH];
StringCbPrintf(szPath, MAX_PATH, TEXT("\"%s\""), szUnquotedPath);
printf(_T("Path: %s\n"), szPath);
// Open the local default service control manager database
schSCManager = OpenSCManager(NULL, // local computer
NULL, // ServicesActive database
SC_MANAGER_CONNECT | SC_MANAGER_CREATE_SERVICE);
if (NULL == schSCManager) {
printf("OpenSCManager failed: [%d] %s\n", GetLastError(), ErrorString().c_str());
return GetLastError();
}
// Install the service into SCM by calling CreateService
schService = CreateService(schSCManager, // SCManager database
pszServiceName, // Name of service
pszDisplayName, // Name to display
SERVICE_QUERY_STATUS, // Desired access
SERVICE_WIN32_OWN_PROCESS, // Service type
dwStartType, // Service start type
SERVICE_ERROR_NORMAL, // Error control type
szPath, // Service's binary
NULL, // No load ordering group
NULL, // No tag identifier
pszDependencies, // Dependencies
pszAccount, // Service running account
pszPassword // Password of the account
);
if (schService == NULL) {
printf("CreateService failed: [%d] %s\n", GetLastError(), ErrorString().c_str());
CloseServiceHandle(schSCManager);
return GetLastError();
} else
printf("Service installed successfully\n");
// Centralized cleanup for all allocated resources.
CloseServiceHandle(schService);
CloseServiceHandle(schSCManager);
return 0;
}
int UninstallService(LPTSTR pszServiceName) {
SC_HANDLE schSCManager = NULL;
SC_HANDLE schService = NULL;
SERVICE_STATUS ssSvcStatus = {};
// Open the local default service control manager database
schSCManager = OpenSCManager(NULL, NULL, SC_MANAGER_CONNECT);
if (schSCManager == NULL) {
printf("OpenSCManager failed: [%d] %s\n", GetLastError(), ErrorString().c_str());
return GetLastError();
}
// Open the service with delete, stop, and query status permissions
schService = OpenService(schSCManager, pszServiceName, SERVICE_STOP | SERVICE_QUERY_STATUS | DELETE);
if (schService == NULL) {
printf("OpenService failed: [%d] %s\n", GetLastError(), ErrorString().c_str());
CloseServiceHandle(schSCManager);
return GetLastError();
}
// Try to stop the service
if (ControlService(schService, SERVICE_CONTROL_STOP, &ssSvcStatus)) {
printf(_T("Stopping %s."), pszServiceName);
int nCount = 100;
while (QueryServiceStatus(schService, &ssSvcStatus) && nCount-- > 0) {
if (ssSvcStatus.dwCurrentState == SERVICE_STOP_PENDING) {
printf(".");
Sleep(100);
} else
break;
}
if (ssSvcStatus.dwCurrentState == SERVICE_STOPPED) {
printf(_T("\n%s is stopped.\n"), pszServiceName);
} else {
printf(_T("\n%s failed to stop. state: 0x%X\n"), pszServiceName, ssSvcStatus.dwCurrentState);
}
}
// Now remove the service by calling DeleteService.
if (!DeleteService(schService)) {
printf("DeleteService failed: [%d] %s\n", GetLastError(), ErrorString().c_str());
} else {
printf(_T("Service deleted %s successfully\n"), pszServiceName);
}
CloseServiceHandle(schService);
CloseServiceHandle(schSCManager);
return 0;
}

@ -0,0 +1,74 @@
/*!
* Install service
* \author Kang Lin <kl222@126.com>
*
* Complete the create and delete of the SC command
* \see https://learn.microsoft.com/zh-cn/windows/win32/services/configuring-a-service-using-sc#syntax
*/
#ifndef __SERVICEINSTALLER_H_KL_2023_10_20__
#define __SERVICEINSTALLER_H_KL_2023_10_20__
#pragma once
#include <windows.h>
#ifdef __cplusplus
extern "C" {
#endif
/*!
* \brief Install service
*
* \details Install the current application as a service to the local
* service control manager database.
*
* \param pszServiceName - the name of the service to be installed
* \param pszDisplayName - the display name of the service
* \param dwStartType - the service start option. This parameter can be one of
* the following values:
* - SERVICE_AUTO_START
* - SERVICE_BOOT_START
* - SERVICE_DEMAND_START
* - SERVICE_DISABLED
* - SERVICE_SYSTEM_START.
* \param pszDependencies - a pointer to a double null-terminated array of null-
* separated names of services or load ordering groups that the system
* must start before this service.
* \param pszAccount - the name of the account under which the service runs.
* - local account: "NT AUTHORITY\\LocalService"
* - network account: "NT AUTHORITY\NetworkService"
* - local system account: ".\LocalSystem"
* \see https://learn.microsoft.com/windows/win32/services/service-user-accounts
* \param pszPassword - the password to the account name.
* \return 0 is success. other is fail
*
* \note If the function fails to install the service, it prints the error
* in the standard output stream for users to diagnose the problem.
* \see https://learn.microsoft.com/windows/win32/services/service-configuration-programs
* \see https://learn.microsoft.com/windows/win32/services/installing-a-service
*/
int InstallService(LPTSTR pszServiceName, LPTSTR pszDisplayName, DWORD dwStartType, LPTSTR pszDependencies,
LPTSTR pszAccount, LPTSTR pszPassword);
/*!
* \brief Uninstall service
*
* \details Stop and remove the service from the local service control
* manager database.
*
* \param pszServiceName - the name of the service to be removed.
* \return 0 is success. other is fail
*
* \note If the function fails to uninstall the service, it prints the
* error in the standard output stream for users to diagnose the problem.
*
* \see https://learn.microsoft.com/windows/win32/services/deleting-a-service
*/
int UninstallService(LPTSTR pszServiceName);
#ifdef __cplusplus
}
#endif
#endif //__SERVICEINSTALLER_H_KL_2023_10_20__
Loading…
Cancel
Save