PAYATHON 2026

Windows 服务声明接受停止,为何 ControlService 仍返回 1061?

数据小陈

现有代码的回调声明有误,应先修正 ServiceHandler 的函数签名,并在 ControlService() 失败后立即保存 GetLastError()。但仅凭这些片段,还不能断定回调声明错误就是错误 1061 的唯一原因。

dwControlsAccepted 包含 SERVICE_ACCEPT_STOP,只表明服务声明支持停止控制;SCM 还会检查服务状态。观察到的 SERVICE_RUNNING 与错误 1061 之间存在矛盾,需要进一步核实,不能仅凭该标志位判断原因。

1. 修正控制处理函数的签名

原代码写成了:

LPHANDLER_FUNCTION ServiceHandler(DWORD dwCtrl)

LPHANDLER_FUNCTION 是函数指针类型,不能用来表示处理函数的返回类型。与 RegisterServiceCtrlHandler() 配套的回调应返回 void,并使用 WINAPI 调用约定:

void WINAPI ServiceHandler(DWORD dwCtrl)
{
    switch (dwCtrl) {
    case SERVICE_CONTROL_STOP:
        SetEvent(gServiceStopEvent);
        return;

    case SERVICE_CONTROL_INTERROGATE:
        return;

    default:
        return;
    }
}

这段代码只展示了回调签名和停止事件通知,完整的状态上报还需要按后面的流程补齐。普通 Handler 没有返回值,应删除 return(NO_ERROR) 和用于返回 ERROR_CALL_NOT_IMPLEMENTED 的类型转换。Microsoft 的回调文档规定了这一接口。

注册时直接传入 ServiceHandler,让编译器检查类型,不要用强制转换掩盖类型不匹配。缺少 WINAPI 在依赖不同调用约定的构建环境中可能导致参数或栈处理错误,尤其需要检查 32 位构建。

如果实际使用的是 RegisterServiceCtrlHandlerEx(),则必须改用对应的 HandlerEx 签名,不能混用两套接口。

2. 立即保存错误码,避免日志干扰判断

原代码在读取 GetLastError() 之前已经执行了流输出操作,不能依赖这些操作一定保留线程的最后错误值。应先保存错误码,再输出日志,同时使用独立的状态变量:

SERVICE_STATUS returnedStatus{};

if (!ControlService(service_handle, SERVICE_CONTROL_STOP,
                    &returnedStatus)) {
    const DWORD error = GetLastError();

    std::cerr << "ControlService failed: " << error
              << ", state=" << returnedStatus.dwCurrentState
              << ", accepted=" << returnedStatus.dwControlsAccepted
              << '\n';

    CloseServiceHandle(service_handle);
    return 0;
}

调用失败时,只有在文档规定会填充输出结构的错误条件下,才应解释上面的状态字段。对于这里关注的 ERROR_SERVICE_CANNOT_ACCEPT_CTRL,ControlService() 文档明确允许读取输出状态,因此不能简单认定调用失败后最后一个参数一定无效。其他错误需要分别判断。ControlService 文档列出了适用条件。

原片段打开的是 service_handle,关闭的却是 service_schandle。如果这不是粘贴时的笔误,应改为关闭实际打开的句柄。

3. 核对 SCM 中的状态和每次上报结果

Microsoft 对错误 1061 的说明涉及服务处于 SERVICE_STOPPED、SERVICE_START_PENDING 或 SERVICE_STOP_PENDING 等无法接收该控制的情况。这与“没有声明接受该控制”的 ERROR_INVALID_SERVICE_CONTROL 需要区分。错误码说明

建议在调用停止前后,通过同一个服务句柄调用 QueryServiceStatusEx(),记录:

  • dwCurrentState 和 dwControlsAccepted。
  • 处于 SERVICE_RUNNING 时的 dwProcessId,用于核对调试的进程。
  • 时间戳、服务内部名称,以及立即保存的停止错误码。

QueryServiceStatusEx() 返回的是服务最近向 SCM 上报的状态,查询与停止调用之间仍可能发生状态变化。QueryServiceStatusEx 文档

服务端还需要检查未展示的 ReportSvcStatus():它是否调用了 SetServiceStatus(),是否检查了返回值,以及失败时是否立即记录了错误。打印出“Service is now running.”只能证明程序执行到了日志位置,不能证明 SCM 已成功接收 SERVICE_RUNNING。

同时核对 RegisterServiceCtrlHandler() 是否成功、使用的服务名是否正确,以及 StatusHandle 是否来自这一次注册。客户端的状态查询结果和服务端用于上报的状态结构应分别命名;还要确认原代码中同名的全局变量和引用参数是否指向预期对象。

如果修正错误码捕获后,仍持续得到“1061,同时返回 SERVICE_RUNNING 和 SERVICE_ACCEPT_STOP”,需要补充完整的注册代码、ReportSvcStatus() 实现和连续状态日志,才能继续定位。

4. 按停止流程处理事件和清理工作

建议按以下流程处理停止请求:

  1. 初始化期间上报 SERVICE_START_PENDING,暂不接受控制。
  2. 停止事件和工作资源准备好后,上报 SERVICE_RUNNING,并设置 SERVICE_ACCEPT_STOP。
  3. Handler 收到停止请求后,尽快上报 SERVICE_STOP_PENDING,清除接受控制的标志,设置停止事件并返回。
  4. 工作线程收到事件后结束任务、回收资源,最后上报一次 SERVICE_STOPPED。

Handler 中的 Sleep(1000) 应删除。控制处理函数应尽快返回,较长的清理工作交给工作线程。控制回调说明

原来的 while (1) 使用零超时反复检查事件,会形成忙等。如果该线程没有其他任务,可以阻塞等待:

const DWORD result = WaitForSingleObject(gServiceStopEvent, INFINITE);

if (result == WAIT_OBJECT_0) {
    // Finish work and release resources before reporting SERVICE_STOPPED.
} else if (result == WAIT_FAILED) {
    const DWORD error = GetLastError();
    // Record the error and enter the failure cleanup path.
}

这与 Microsoft 服务示例使用停止事件唤醒工作流程的方式一致。创建事件时也必须检查 CreateEvent() 是否返回 NULL。ServiceMain 示例

如果状态结构由多个线程修改,需要同步访问,避免一个线程开始清理后,另一个线程又上报 SERVICE_RUNNING。还应检查 SetServiceStatus() 的返回值,在上报 SERVICE_STOPPED 前完成所有必要清理,并且只上报一次停止状态。SetServiceStatus 注意事项

5. 确认 winkey.exe 与服务的关系

如果 winkey.exe 就是承载服务自身的进程,调用 TerminateProcess() 终止它会截断后续清理和状态上报。应让工作流程正常返回,并由服务程序完成退出。

如果它是服务启动的独立子进程,应保存创建时获得的进程句柄,通过约定的退出机制通知它结束,再等待退出。仅凭进程名查找可能选中其他同名实例。确实需要强制终止时,也必须检查句柄和 API 返回值,不能无条件打印“terminated successfully”。

这部分代码需要修正,但它只有在停止事件触发后才会执行,不能单独解释“控制处理函数从未进入”的现象。

备注:内容仅供参考。