使用 Nuitka 将 Python 程序编译为 Windows 服务

原来 Nuitka 商业版通过插件已经实现了编译 Windows service 的功能,但开源版本不提供此功能。掏钱是不可能掏钱的。本人 fork 了 Nuitka 项目,加入了编译 Windows service 的功能,只要在编译时加入 --windows-service 参数就将 Python 程序能构建成 Windows service 了:

shellpip install nuitka-winsvc
nuitka --onefile --windows-service --windows-service-name=myservice main.py

项目地址: https://github.com/tabris17/Nuitka-winsvc

在前文《Nuitka 编译时注入自定义 C 代码》中介绍了在 Nuitka 编译时注入自定义代码的方法。本文将介绍如何通过这种方法将 Python 程序编译成 Windows 服务。

首先,假设我们用下面的命令将 Python 程序编译成普通的 Onefile 可执行文件:

shellnuitka --standalone --output-dir=build --onefile main.py

接下来,需要修改 OnefileBootstrap.cpp 的代码。有两种方法:一是修改编译时创建的 build\main.onefile-build\static_src\OnefileBootstrap.cpp ;二是直接修改 Nuitka 源代码中的 nuitka/build/static_src/OnefileBootstrap.c 模板文件。定位到该文件的第 778 行(代码所在行数可能因不同版本的 Nuitka 而有所不同),代码如下:

c++#if defined(_WIN32)
int wmain(int argc, wchar_t **argv) {
#else

将这行代码替换为:

c++#pragma comment(lib, "advapi32.lib")
#define SERVICE_NAME L"YourServiceName"
SERVICE_STATUS_HANDLE hSvcStatus;
SERVICE_STATUS svcStatus;
int startup(int, wchar_t**);

DWORD PrintError(const wchar_t* fnName)
{
    LPWSTR errorMessage = NULL;
    DWORD errorCode = GetLastError();
    FormatMessageW(
        FORMAT_MESSAGE_ALLOCATE_BUFFER |
        FORMAT_MESSAGE_FROM_SYSTEM |
        FORMAT_MESSAGE_IGNORE_INSERTS,
        NULL,
        errorCode,
        MAKELANGID(LANG_ENGLISH, SUBLANG_ENGLISH_US),
        (LPWSTR)&errorMessage,
        0, NULL);
    wprintf(L"%s failed (%d): %s", fnName, errorCode, errorMessage);
    LocalFree(errorMessage);
    return errorCode;
}

DWORD SvcInstall(LPCWSTR progName)
{
    SC_HANDLE hSCManager = OpenSCManagerW(NULL, NULL, SC_MANAGER_ALL_ACCESS);
    if (NULL == hSCManager) {
        return PrintError(L"OpenSCManager");
    }
    SC_HANDLE hService = CreateServiceW(
        hSCManager,
        SERVICE_NAME,
        SERVICE_NAME,
        SERVICE_ALL_ACCESS,
        SERVICE_WIN32_OWN_PROCESS,
        SERVICE_DEMAND_START,
        SERVICE_ERROR_NORMAL,
        progName,
        NULL, NULL, NULL, NULL, NULL);
    if (NULL == hService) {
        CloseServiceHandle(hSCManager);
        return PrintError(L"CreateService");
    }
    else wprintf(L"Service installed successfully\n");
    CloseServiceHandle(hService);
    CloseServiceHandle(hSCManager);
    return NO_ERROR;
}

DWORD SvcUninstall()
{
    SC_HANDLE hSCManager = OpenSCManagerW(NULL, NULL, SC_MANAGER_ALL_ACCESS);
    if (NULL == hSCManager) {
        return PrintError(L"OpenSCManager");
    }
    SC_HANDLE hService = OpenServiceW(hSCManager, SERVICE_NAME, DELETE);
    if (hService == NULL) {
        CloseServiceHandle(hSCManager);
        return PrintError(L"OpenService");
    }
    if (!DeleteService(hService)) {
        CloseServiceHandle(hService);
        return PrintError(L"DeleteService");
    }
    else wprintf(L"Service uninstalled successfully\n");
    CloseServiceHandle(hService);
    CloseServiceHandle(hSCManager);
    return NO_ERROR;
}

VOID ReportSvcStatus(DWORD dwCurrentState, DWORD dwWin32ExitCode, DWORD dwWaitHint)
{
    static DWORD dwCheckPoint = 1;
    svcStatus.dwCurrentState = dwCurrentState;
    svcStatus.dwWin32ExitCode = dwWin32ExitCode;
    svcStatus.dwWaitHint = dwWaitHint;

    if (dwCurrentState == SERVICE_START_PENDING)
        svcStatus.dwControlsAccepted = 0;
    else svcStatus.dwControlsAccepted = SERVICE_ACCEPT_STOP;

    if ((dwCurrentState == SERVICE_RUNNING) ||
        (dwCurrentState == SERVICE_STOPPED))
        svcStatus.dwCheckPoint = 0;
    else svcStatus.dwCheckPoint = dwCheckPoint++;
    SetServiceStatus(hSvcStatus, &svcStatus);
}

VOID WINAPI SvcCtrlHandler(DWORD dwCtrl)
{
    switch (dwCtrl) {
    case SERVICE_CONTROL_STOP:
        ReportSvcStatus(SERVICE_STOP_PENDING, NO_ERROR, 0);
        cleanupChildProcess(true);
        ReportSvcStatus(svcStatus.dwCurrentState, NO_ERROR, 0);
        return;
    case SERVICE_CONTROL_INTERROGATE:
        break;
    default:
        break;
    }
}

VOID WINAPI SvcMain(DWORD dwArgc, LPSTR* lpszArgv)
{
    hSvcStatus = RegisterServiceCtrlHandlerW(SERVICE_NAME, SvcCtrlHandler);
    if (!hSvcStatus) {
        return;
    }
    svcStatus.dwServiceType = SERVICE_WIN32_OWN_PROCESS;
    svcStatus.dwServiceSpecificExitCode = 0;
    ReportSvcStatus(SERVICE_START_PENDING, NO_ERROR, 100);

    ReportSvcStatus(SERVICE_RUNNING, NO_ERROR, 0);
    startup(dwArgc, (wchar_t **)lpszArgv);
    ReportSvcStatus(SERVICE_STOPPED, NO_ERROR, 0);
}

int wmain(int argc, wchar_t** argv)
{
    if (argc == 2) {
        if (wcscmp(argv[1], L"install") == 0) {
            return SvcInstall(argv[0]);
        }
        else if (wcscmp(argv[1], L"uninstall") == 0) {
            return SvcUninstall();
        }
        return wprintf(L"Invalid argument %s", argv[1]);
    }

    SERVICE_TABLE_ENTRYW serviceTable[] = {
        { (LPWSTR)SERVICE_NAME, (LPSERVICE_MAIN_FUNCTIONW)SvcMain },
        { NULL, NULL }
    };
    StartServiceCtrlDispatcherW(serviceTable);
}

int startup(int argc, wchar_t **argv) {

以上代码因为使用了 #pragam 指令,所以必须使用 MSVC 进行编译。可以按需求将 "YourServiceName" 修改成自己的服务名称。

最后,使用相同的 nuitka 命令参数进行编译,得到 main.exe 可执行文件。该程序有两个参数,分别是 install 和 uninstall 。无法向 Python 脚本传递原本的命令行参数,可以自行修改代码实现。

安装服务:

shell.\main.exe install 

删除服务:

shell.\main.exe uninstall