win_service.py 9.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195
  1. #!/usr/bin/env python3
  2. # -*- coding: utf-8 -*-
  3. r"""Windows 服务外壳 (2026-09-17 用户令 1: "windows 下为 windows 服务")。
  4. ## 为什么用 ctypes 直接调 SCM,而不是 pywin32
  5. 真 Windows 服务**必须**实现 SCM 的控制协议(`StartServiceCtrlDispatcher` + 控制处理回调),
  6. `sc create` 一个 `python.exe xxx.py` 是**不行的**(cmd/python 不跟 SCM 握手 ⇒ 起服务报 1053)。
  7. 实现它有两条路:
  8. · `pywin32`(`win32serviceutil`)—— 要往包里加一个新依赖(本包离线轮子的原则是"能不加就不加");
  9. · **ctypes 直连 advapi32**(本文件)—— 零依赖、只用标准库,与"单包离线"的口径一致。
  10. ## 用法
  11. python scripts\win_service.py # 由服务控制管理器拉起(正常路径,别手工跑)
  12. python scripts\win_service.py --debug # 前台调试:控制台里跑守护循环,Ctrl+C 退出
  13. python scripts\win_service.py --debug --once # 只跑一轮巡检就退出(无权限也能自证)
  14. python scripts\win_service.py --selftest # 只做前置条件自检
  15. # 注册/卸载走 service_ctl.py(需要管理员):
  16. python scripts\service_ctl.py install --dry-run
  17. python scripts\service_ctl.py install
  18. ## 与 SCM 的约定
  19. SERVICE_WIN32_OWN_PROCESS · 接受 STOP/SHUTDOWN/INTERROGATE
  20. 启动过程中报 START_PENDING(带 wait hint),就绪后报 RUNNING,收到 STOP 后停服务再报 STOPPED
  21. 服务崩溃的自动重启交给 SCM 的恢复策略(`sc failure ... actions= restart/60000/...`,见 service_ctl.py)
  22. """
  23. from __future__ import annotations
  24. import ctypes
  25. import pathlib
  26. import sys
  27. import threading
  28. import time
  29. ROOT = pathlib.Path(__file__).resolve().parents[1]
  30. sys.path.insert(0, str(ROOT))
  31. from src import version as V # noqa: E402
  32. import service_worker as SW # noqa: E402 (同目录)
  33. SERVICE_WIN32_OWN_PROCESS = 0x00000010
  34. SERVICE_ACCEPT_STOP = 0x00000001
  35. SERVICE_ACCEPT_SHUTDOWN = 0x00000004
  36. SERVICE_CONTROL_STOP = 0x00000001
  37. SERVICE_CONTROL_INTERROGATE = 0x00000004
  38. SERVICE_CONTROL_SHUTDOWN = 0x00000005
  39. SERVICE_STOPPED, SERVICE_START_PENDING, SERVICE_STOP_PENDING = 0x00000001, 0x00000002, 0x00000003
  40. SERVICE_RUNNING, SERVICE_CONTINUE_PENDING = 0x00000004, 0x00000005
  41. NO_ERROR = 0
  42. ERROR_FAILED_SERVICE_CONTROLLER_CONNECT = 1063
  43. advapi32 = ctypes.windll.advapi32 if hasattr(ctypes, 'windll') else None
  44. # ★★2026-09-19 远端实逮 (服务器 sc query guanlan 永远 START_PENDING, 组件却在跑):
  45. # ctypes **不声明 restype 时按 c_int 处理返回值**, 而 `RegisterServiceCtrlHandlerExW` 返回的是
  46. # **HANDLE(指针)** ⇒ 64 位 Python 上被截成 32 位, 拿到的是无效句柄; 于是后面每一次
  47. # `SetServiceStatus` 都失败(ERROR_INVALID_HANDLE), SCM 收不到任何状态 ⇒ 它停在"默认等待"
  48. # (CHECKPOINT=0 / WAIT_HINT=2000), 服务永远显示 START_PENDING 且 NOT_STOPPABLE ——
  49. # 表现是"服务在跑但 `net stop guanlan` 停不掉、状态栏看着像没起完"。
  50. # 修法: 显式声明 argtypes/restype (句柄按 c_void_p 传), 并把 SetServiceStatus 的失败**报出来**
  51. # (失败不静默: 写进 logs/service.log)。
  52. class SERVICE_STATUS(ctypes.Structure):
  53. _fields_ = [('dwServiceType', ctypes.c_ulong), ('dwCurrentState', ctypes.c_ulong),
  54. ('dwControlsAccepted', ctypes.c_ulong), ('dwWin32ExitCode', ctypes.c_ulong),
  55. ('dwServiceSpecificExitCode', ctypes.c_ulong), ('dwCheckPoint', ctypes.c_ulong),
  56. ('dwWaitHint', ctypes.c_ulong)]
  57. class SERVICE_TABLE_ENTRY(ctypes.Structure):
  58. _fields_ = [('lpServiceName', ctypes.c_wchar_p), ('lpServiceProc', ctypes.c_void_p)]
  59. HANDLER = ctypes.WINFUNCTYPE(ctypes.c_ulong, ctypes.c_ulong, ctypes.c_ulong, ctypes.c_void_p, ctypes.c_void_p)
  60. MAIN = ctypes.WINFUNCTYPE(None, ctypes.c_ulong, ctypes.POINTER(ctypes.c_wchar_p))
  61. if advapi32 is not None:
  62. advapi32.RegisterServiceCtrlHandlerExW.restype = ctypes.c_void_p
  63. advapi32.RegisterServiceCtrlHandlerExW.argtypes = [ctypes.c_wchar_p, HANDLER, ctypes.c_void_p]
  64. advapi32.SetServiceStatus.restype = ctypes.c_int
  65. advapi32.SetServiceStatus.argtypes = [ctypes.c_void_p, ctypes.POINTER(SERVICE_STATUS)]
  66. advapi32.StartServiceCtrlDispatcherW.restype = ctypes.c_int
  67. # ★argtypes 别写成 POINTER(SERVICE_TABLE_ENTRY) 却传 `byref(数组)` —— ctypes 会直接抛
  68. # ArgumentError("expected LP_SERVICE_TABLE_ENTRY instance instead of pointer to
  69. # SERVICE_TABLE_ENTRY_Array_2"), 于是服务**根本起不来**(sc start 回 1053)。
  70. # 2026-09-19 我自己先踩了这个坑(远端实逮), 现在按"数组 cast 成指针"传。
  71. advapi32.StartServiceCtrlDispatcherW.argtypes = [ctypes.POINTER(SERVICE_TABLE_ENTRY)]
  72. _status_handle = None
  73. _stop_evt = threading.Event()
  74. def _report(state: int, hint: int = 0, cp: int = 0) -> None:
  75. if not _status_handle:
  76. SW.log(f'[{V.SERVICE_NAME}] 没有服务状态句柄 —— 状态 {state} 报不上去 (非 SCM 启动时属正常)', 'ERROR')
  77. return
  78. st = SERVICE_STATUS(SERVICE_WIN32_OWN_PROCESS, state,
  79. SERVICE_ACCEPT_STOP | SERVICE_ACCEPT_SHUTDOWN, NO_ERROR, 0, cp, hint)
  80. if not advapi32.SetServiceStatus(_status_handle, ctypes.byref(st)):
  81. err = ctypes.windll.kernel32.GetLastError()
  82. SW.log(f'[{V.SERVICE_NAME}] SetServiceStatus(state={state}) 失败, WinError {err} '
  83. f'—— SCM 会看不到本服务状态 (服务状态栏显示异常/停不掉时先查这条)', 'ERROR')
  84. def _handler(control, event_type, event_data, context):
  85. """SCM 控制回调:只处理 STOP/SHUTDOWN/INTERROGATE,其余直接确认。"""
  86. if control in (SERVICE_CONTROL_STOP, SERVICE_CONTROL_SHUTDOWN):
  87. _report(SERVICE_STOP_PENDING, 30000, 1)
  88. _stop_evt.set()
  89. elif control == SERVICE_CONTROL_INTERROGATE:
  90. _report(SERVICE_RUNNING)
  91. return NO_ERROR
  92. @MAIN
  93. def _service_main(argc, argv) -> None:
  94. global _status_handle
  95. _status_handle = advapi32.RegisterServiceCtrlHandlerExW(V.SERVICE_NAME, _HANDLER_REF, None)
  96. if not _status_handle:
  97. return
  98. _report(SERVICE_START_PENDING, 30000, 1)
  99. # ★先把 RUNNING 报给 SCM, 再干重活 (2026-09-19 远端实逮: 原来先 SW.log 再起线程最后才报 RUNNING,
  100. # 而 `sc start` 那次在日志落盘上卡住 ⇒ SCM 等不到 RUNNING, 30 s 后直接 1053 "服务未及时响应")
  101. # ⇒ 控制协议就绪 = 服务就是 RUNNING; 组件起没起由守护线程负责, 看 /healthz 与 logs/service.log。
  102. _report(SERVICE_RUNNING)
  103. SW.log(f'服务已由 SCM 拉起({V.SERVICE_DISPLAY})')
  104. t = threading.Thread(target=_body, name='guanlan-supervisor', daemon=True)
  105. t.start()
  106. _stop_evt.wait()
  107. SW.stop()
  108. _report(SERVICE_STOPPED)
  109. _HANDLER_REF = HANDLER(_handler)
  110. def _body() -> None:
  111. """服务主体:起组件 → 守护。异常必须自己吞掉并记 ERROR,否则线程静默死掉。"""
  112. try:
  113. SW.start()
  114. SW.supervise(_stop_evt)
  115. except Exception as e:
  116. SW.log(f'服务主体异常: {type(e).__name__}: {e}', 'ERROR')
  117. def run_debug(once: bool = False) -> int:
  118. """前台调试(不需要 SCM):--once 只跑一轮,用于无权限环境自证。"""
  119. if once:
  120. SW.log('调试模式:跑一轮巡检')
  121. ok = SW.ensure()
  122. SW.log(f'一轮巡检结果: 网关{"在听" if ok else "未就绪"}')
  123. return 0
  124. SW.log('调试模式:Ctrl+C 退出')
  125. try:
  126. SW.start()
  127. SW.supervise(_stop_evt)
  128. except KeyboardInterrupt:
  129. pass
  130. finally:
  131. SW.stop()
  132. return 0
  133. def main() -> int:
  134. if '--selftest' in sys.argv:
  135. return SW.selftest()
  136. if '--debug' in sys.argv:
  137. return run_debug(once='--once' in sys.argv)
  138. if advapi32 is None:
  139. print('[X] 本文件只在 Windows 上有效(Linux 用 scripts/service_main.py + systemd)')
  140. return 2
  141. table = (SERVICE_TABLE_ENTRY * 2)()
  142. table[0].lpServiceName = V.SERVICE_NAME
  143. table[0].lpServiceProc = ctypes.cast(_service_main, ctypes.c_void_p)
  144. table[1].lpServiceName = None
  145. table[1].lpServiceProc = None
  146. ok = advapi32.StartServiceCtrlDispatcherW(ctypes.cast(table, ctypes.POINTER(SERVICE_TABLE_ENTRY)))
  147. if not ok:
  148. err = ctypes.windll.kernel32.GetLastError()
  149. if err in (ERROR_FAILED_SERVICE_CONTROLLER_CONNECT, 0):
  150. # 这条**正是无权限自证**:能走到这里说明 SCM 接口调用正确,只是本进程不是被 SCM 拉起的。
  151. print(f'[{V.SERVICE_NAME}] 不是由服务控制管理器启动的(WinError {ERROR_FAILED_SERVICE_CONTROLLER_CONNECT})。')
  152. print(' 想注册成服务(需管理员): python scripts/service_ctl.py install')
  153. print(' 想前台调试: python scripts/win_service.py --debug [--once]')
  154. print(' 只看前置条件: python scripts/win_service.py --selftest')
  155. else:
  156. print(f'[X] StartServiceCtrlDispatcher 失败, WinError {err}')
  157. return 1
  158. return 0
  159. if __name__ == '__main__':
  160. sys.exit(main())