#!/usr/bin/env python3 # -*- coding: utf-8 -*- r"""Windows 服务外壳 (2026-09-17 用户令 1: "windows 下为 windows 服务")。 ## 为什么用 ctypes 直接调 SCM,而不是 pywin32 真 Windows 服务**必须**实现 SCM 的控制协议(`StartServiceCtrlDispatcher` + 控制处理回调), `sc create` 一个 `python.exe xxx.py` 是**不行的**(cmd/python 不跟 SCM 握手 ⇒ 起服务报 1053)。 实现它有两条路: · `pywin32`(`win32serviceutil`)—— 要往包里加一个新依赖(本包离线轮子的原则是"能不加就不加"); · **ctypes 直连 advapi32**(本文件)—— 零依赖、只用标准库,与"单包离线"的口径一致。 ## 用法 python scripts\win_service.py # 由服务控制管理器拉起(正常路径,别手工跑) python scripts\win_service.py --debug # 前台调试:控制台里跑守护循环,Ctrl+C 退出 python scripts\win_service.py --debug --once # 只跑一轮巡检就退出(无权限也能自证) python scripts\win_service.py --selftest # 只做前置条件自检 # 注册/卸载走 service_ctl.py(需要管理员): python scripts\service_ctl.py install --dry-run python scripts\service_ctl.py install ## 与 SCM 的约定 SERVICE_WIN32_OWN_PROCESS · 接受 STOP/SHUTDOWN/INTERROGATE 启动过程中报 START_PENDING(带 wait hint),就绪后报 RUNNING,收到 STOP 后停服务再报 STOPPED 服务崩溃的自动重启交给 SCM 的恢复策略(`sc failure ... actions= restart/60000/...`,见 service_ctl.py) """ from __future__ import annotations import ctypes import pathlib import sys import threading import time ROOT = pathlib.Path(__file__).resolve().parents[1] sys.path.insert(0, str(ROOT)) from src import version as V # noqa: E402 import service_worker as SW # noqa: E402 (同目录) SERVICE_WIN32_OWN_PROCESS = 0x00000010 SERVICE_ACCEPT_STOP = 0x00000001 SERVICE_ACCEPT_SHUTDOWN = 0x00000004 SERVICE_CONTROL_STOP = 0x00000001 SERVICE_CONTROL_INTERROGATE = 0x00000004 SERVICE_CONTROL_SHUTDOWN = 0x00000005 SERVICE_STOPPED, SERVICE_START_PENDING, SERVICE_STOP_PENDING = 0x00000001, 0x00000002, 0x00000003 SERVICE_RUNNING, SERVICE_CONTINUE_PENDING = 0x00000004, 0x00000005 NO_ERROR = 0 ERROR_FAILED_SERVICE_CONTROLLER_CONNECT = 1063 advapi32 = ctypes.windll.advapi32 if hasattr(ctypes, 'windll') else None class SERVICE_STATUS(ctypes.Structure): _fields_ = [('dwServiceType', ctypes.c_ulong), ('dwCurrentState', ctypes.c_ulong), ('dwControlsAccepted', ctypes.c_ulong), ('dwWin32ExitCode', ctypes.c_ulong), ('dwServiceSpecificExitCode', ctypes.c_ulong), ('dwCheckPoint', ctypes.c_ulong), ('dwWaitHint', ctypes.c_ulong)] class SERVICE_TABLE_ENTRY(ctypes.Structure): _fields_ = [('lpServiceName', ctypes.c_wchar_p), ('lpServiceProc', ctypes.c_void_p)] HANDLER = ctypes.WINFUNCTYPE(ctypes.c_ulong, ctypes.c_ulong, ctypes.c_ulong, ctypes.c_void_p, ctypes.c_void_p) MAIN = ctypes.WINFUNCTYPE(None, ctypes.c_ulong, ctypes.POINTER(ctypes.c_wchar_p)) _status_handle = None _stop_evt = threading.Event() def _report(state: int, hint: int = 0, cp: int = 0) -> None: if not _status_handle: return st = SERVICE_STATUS(SERVICE_WIN32_OWN_PROCESS, state, SERVICE_ACCEPT_STOP | SERVICE_ACCEPT_SHUTDOWN, NO_ERROR, 0, cp, hint) advapi32.SetServiceStatus(_status_handle, ctypes.byref(st)) def _handler(control, event_type, event_data, context): """SCM 控制回调:只处理 STOP/SHUTDOWN/INTERROGATE,其余直接确认。""" if control in (SERVICE_CONTROL_STOP, SERVICE_CONTROL_SHUTDOWN): _report(SERVICE_STOP_PENDING, 30000, 1) _stop_evt.set() elif control == SERVICE_CONTROL_INTERROGATE: _report(SERVICE_RUNNING) return NO_ERROR @MAIN def _service_main(argc, argv) -> None: global _status_handle _status_handle = advapi32.RegisterServiceCtrlHandlerExW(V.SERVICE_NAME, _HANDLER_REF, None) if not _status_handle: return _report(SERVICE_START_PENDING, 30000, 1) SW.log(f'服务已由 SCM 拉起({V.SERVICE_DISPLAY})') t = threading.Thread(target=_body, name='guanlan-supervisor', daemon=True) t.start() _report(SERVICE_RUNNING) _stop_evt.wait() SW.stop() _report(SERVICE_STOPPED) _HANDLER_REF = HANDLER(_handler) def _body() -> None: """服务主体:起组件 → 守护。异常必须自己吞掉并记 ERROR,否则线程静默死掉。""" try: SW.start() SW.supervise(_stop_evt) except Exception as e: SW.log(f'服务主体异常: {type(e).__name__}: {e}', 'ERROR') def run_debug(once: bool = False) -> int: """前台调试(不需要 SCM):--once 只跑一轮,用于无权限环境自证。""" if once: SW.log('调试模式:跑一轮巡检') ok = SW.ensure() SW.log(f'一轮巡检结果: 网关{"在听" if ok else "未就绪"}') return 0 SW.log('调试模式:Ctrl+C 退出') try: SW.start() SW.supervise(_stop_evt) except KeyboardInterrupt: pass finally: SW.stop() return 0 def main() -> int: if '--selftest' in sys.argv: return SW.selftest() if '--debug' in sys.argv: return run_debug(once='--once' in sys.argv) if advapi32 is None: print('[X] 本文件只在 Windows 上有效(Linux 用 scripts/service_main.py + systemd)') return 2 table = (SERVICE_TABLE_ENTRY * 2)() table[0].lpServiceName = V.SERVICE_NAME table[0].lpServiceProc = ctypes.cast(_service_main, ctypes.c_void_p) table[1].lpServiceName = None table[1].lpServiceProc = None ok = advapi32.StartServiceCtrlDispatcherW(ctypes.byref(table)) if not ok: err = ctypes.windll.kernel32.GetLastError() if err in (ERROR_FAILED_SERVICE_CONTROLLER_CONNECT, 0): # 这条**正是无权限自证**:能走到这里说明 SCM 接口调用正确,只是本进程不是被 SCM 拉起的。 print(f'[{V.SERVICE_NAME}] 不是由服务控制管理器启动的(WinError {ERROR_FAILED_SERVICE_CONTROLLER_CONNECT})。') print(' 想注册成服务(需管理员): python scripts/service_ctl.py install') print(' 想前台调试: python scripts/win_service.py --debug [--once]') print(' 只看前置条件: python scripts/win_service.py --selftest') else: print(f'[X] StartServiceCtrlDispatcher 失败, WinError {err}') return 1 return 0 if __name__ == '__main__': sys.exit(main())