Parcourir la source

修 Windows 服务外壳两个真缺陷 (远端实逮: sc query 永远 START_PENDING + 起不来)

1. **状态报不上去**: ctypes 不声明 restype 时按 c_int 处理返回值, 而 RegisterServiceCtrlHandlerExW
   返回的是 HANDLE(指针) ⇒ 64 位 Python 上被截成 32 位拿到无效句柄 ⇒ 之后每次 SetServiceStatus
   都失败(ERROR_INVALID_HANDLE), SCM 收不到任何状态, 停在默认等待(CHECKPOINT=0/WAIT_HINT=2000),
   服务永远 START_PENDING 且 NOT_STOPPABLE(组件其实在跑) —— 现场表现"服务在跑但 net stop 停不掉"。
   修: 显式声明 argtypes/restype(句柄按 c_void_p), 并把 SetServiceStatus 失败**写进 logs/service.log**
   (不再静默)。
2. **顺序**: 原先 SW.log → 起守护线程 → 最后才报 RUNNING; 远端那次 sc start 卡在日志落盘上 ⇒ SCM
   等不到 RUNNING, 30 s 后直接 1053。改为: 报 START_PENDING → **立刻报 RUNNING**(控制协议就绪即运行态),
   再记日志/起守护线程。
3. 自己引入又当场逮到的第三个坑(写在这儿免得后人再踩): argtypes 写成 POINTER(SERVICE_TABLE_ENTRY)
   却传 `byref(数组)` ⇒ ctypes 抛 ArgumentError, 服务**根本起不来**(sc start=1053)。现按"数组 cast 成
   指针"传, 并用 `python scripts\win_service.py`(非 SCM 启动 ⇒ 应打印 WinError 1063 提示) 做本机自证。

远端复核: sc query guanlan → STATE=4 RUNNING (STOPPABLE, ACCEPTS_SHUTDOWN)。
zhouyang.xie il y a 3 semaines
Parent
commit
4851bc4d13
1 fichiers modifiés avec 30 ajouts et 4 suppressions
  1. 30 4
      scripts/win_service.py

+ 30 - 4
scripts/win_service.py

@@ -54,7 +54,14 @@ ERROR_FAILED_SERVICE_CONTROLLER_CONNECT = 1063
 
 advapi32 = ctypes.windll.advapi32 if hasattr(ctypes, 'windll') else None
 
-
+# ★★2026-09-19 远端实逮 (服务器 sc query guanlan 永远 START_PENDING, 组件却在跑):
+#   ctypes **不声明 restype 时按 c_int 处理返回值**, 而 `RegisterServiceCtrlHandlerExW` 返回的是
+#   **HANDLE(指针)** ⇒ 64 位 Python 上被截成 32 位, 拿到的是无效句柄; 于是后面每一次
+#   `SetServiceStatus` 都失败(ERROR_INVALID_HANDLE), SCM 收不到任何状态 ⇒ 它停在"默认等待"
+#   (CHECKPOINT=0 / WAIT_HINT=2000), 服务永远显示 START_PENDING 且 NOT_STOPPABLE ——
+#   表现是"服务在跑但 `net stop guanlan` 停不掉、状态栏看着像没起完"。
+#   修法: 显式声明 argtypes/restype (句柄按 c_void_p 传), 并把 SetServiceStatus 的失败**报出来**
+#   (失败不静默: 写进 logs/service.log)。
 class SERVICE_STATUS(ctypes.Structure):
     _fields_ = [('dwServiceType', ctypes.c_ulong), ('dwCurrentState', ctypes.c_ulong),
                 ('dwControlsAccepted', ctypes.c_ulong), ('dwWin32ExitCode', ctypes.c_ulong),
@@ -69,16 +76,32 @@ class SERVICE_TABLE_ENTRY(ctypes.Structure):
 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))
 
+if advapi32 is not None:
+    advapi32.RegisterServiceCtrlHandlerExW.restype = ctypes.c_void_p
+    advapi32.RegisterServiceCtrlHandlerExW.argtypes = [ctypes.c_wchar_p, HANDLER, ctypes.c_void_p]
+    advapi32.SetServiceStatus.restype = ctypes.c_int
+    advapi32.SetServiceStatus.argtypes = [ctypes.c_void_p, ctypes.POINTER(SERVICE_STATUS)]
+    advapi32.StartServiceCtrlDispatcherW.restype = ctypes.c_int
+    # ★argtypes 别写成 POINTER(SERVICE_TABLE_ENTRY) 却传 `byref(数组)` —— ctypes 会直接抛
+    #   ArgumentError("expected LP_SERVICE_TABLE_ENTRY instance instead of pointer to
+    #   SERVICE_TABLE_ENTRY_Array_2"), 于是服务**根本起不来**(sc start 回 1053)。
+    #   2026-09-19 我自己先踩了这个坑(远端实逮), 现在按"数组 cast 成指针"传。
+    advapi32.StartServiceCtrlDispatcherW.argtypes = [ctypes.POINTER(SERVICE_TABLE_ENTRY)]
+
 _status_handle = None
 _stop_evt = threading.Event()
 
 
 def _report(state: int, hint: int = 0, cp: int = 0) -> None:
     if not _status_handle:
+        SW.log(f'[{V.SERVICE_NAME}] 没有服务状态句柄 —— 状态 {state} 报不上去 (非 SCM 启动时属正常)', 'ERROR')
         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))
+    if not advapi32.SetServiceStatus(_status_handle, ctypes.byref(st)):
+        err = ctypes.windll.kernel32.GetLastError()
+        SW.log(f'[{V.SERVICE_NAME}] SetServiceStatus(state={state}) 失败, WinError {err} '
+               f'—— SCM 会看不到本服务状态 (服务状态栏显示异常/停不掉时先查这条)', 'ERROR')
 
 
 def _handler(control, event_type, event_data, context):
@@ -98,10 +121,13 @@ def _service_main(argc, argv) -> None:
     if not _status_handle:
         return
     _report(SERVICE_START_PENDING, 30000, 1)
+    # ★先把 RUNNING 报给 SCM, 再干重活 (2026-09-19 远端实逮: 原来先 SW.log 再起线程最后才报 RUNNING,
+    #   而 `sc start` 那次在日志落盘上卡住 ⇒ SCM 等不到 RUNNING, 30 s 后直接 1053 "服务未及时响应")
+    #   ⇒ 控制协议就绪 = 服务就是 RUNNING; 组件起没起由守护线程负责, 看 /healthz 与 logs/service.log。
+    _report(SERVICE_RUNNING)
     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)
@@ -150,7 +176,7 @@ def main() -> int:
     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))
+    ok = advapi32.StartServiceCtrlDispatcherW(ctypes.cast(table, ctypes.POINTER(SERVICE_TABLE_ENTRY)))
     if not ok:
         err = ctypes.windll.kernel32.GetLastError()
         if err in (ERROR_FAILED_SERVICE_CONTROLLER_CONNECT, 0):