feat: Windows 服务 安装,启动,停止 功能
This commit is contained in:
261
scientific_surfing/windows_service_wrapper.py
Normal file
261
scientific_surfing/windows_service_wrapper.py
Normal file
@ -0,0 +1,261 @@
|
||||
import os
|
||||
import json
|
||||
import signal
|
||||
import sys
|
||||
import time
|
||||
import win32serviceutil
|
||||
import win32service
|
||||
import servicemanager # Simple setup and logging
|
||||
from typing import Optional
|
||||
import subprocess
|
||||
from dataclasses import dataclass
|
||||
import win32event
|
||||
import logging
|
||||
import traceback
|
||||
from threading import Thread
|
||||
|
||||
|
||||
@dataclass
|
||||
class WindowServiceConfig:
|
||||
name: str
|
||||
display_name: str
|
||||
working_dir: str
|
||||
bin_path: str
|
||||
description: Optional[str] = None
|
||||
args: Optional[str] = None
|
||||
env: Optional[dict[str, str]] = None
|
||||
|
||||
|
||||
def log_stream(stream, logger_func):
|
||||
"""Read lines from stream and log them using the provided logger function"""
|
||||
for line in iter(stream.readline, ''):
|
||||
if line.strip(): # Only log non-empty lines
|
||||
logger_func(line.strip())
|
||||
stream.close()
|
||||
|
||||
class WindowsServiceFramework(win32serviceutil.ServiceFramework):
|
||||
|
||||
# required
|
||||
_svc_name_ = "mihomo"
|
||||
_svc_display_name_ = "Scentific Surfing Service"
|
||||
|
||||
_config: WindowServiceConfig = None
|
||||
|
||||
stop_requested: bool = False
|
||||
|
||||
def __init__(self, args):
|
||||
super().__init__(args)
|
||||
self.setup_logging()
|
||||
self.hWaitStop = win32event.CreateEvent(None, 0, 0, None)
|
||||
self.process = None
|
||||
WindowsServiceFramework.load_service_config()
|
||||
|
||||
def setup_logging(self):
|
||||
"""Setup logging to both Event Viewer and desktop file"""
|
||||
try:
|
||||
log_file_path = f"Y:/{self._svc_name_}_service.log"
|
||||
|
||||
# Create logger
|
||||
self.log = logging.getLogger(self._svc_name_)
|
||||
self.log.setLevel(logging.DEBUG)
|
||||
|
||||
# Clear existing handlers
|
||||
self.log.handlers.clear()
|
||||
|
||||
# File handler for desktop
|
||||
file_handler = logging.FileHandler(log_file_path, mode='w')
|
||||
file_handler.setLevel(logging.DEBUG)
|
||||
file_formatter = logging.Formatter(
|
||||
'%(asctime)s - %(name)s - %(levelname)s - %(message)s'
|
||||
)
|
||||
file_handler.setFormatter(file_formatter)
|
||||
|
||||
# Console handler for Event Viewer
|
||||
console_handler = logging.StreamHandler()
|
||||
console_handler.setLevel(logging.INFO)
|
||||
console_formatter = logging.Formatter('%(levelname)s: %(message)s')
|
||||
console_handler.setFormatter(console_formatter)
|
||||
|
||||
# Add handlers
|
||||
self.log.addHandler(file_handler)
|
||||
self.log.addHandler(console_handler)
|
||||
|
||||
self.log.info(f"Logging initialized. Log file: {log_file_path}")
|
||||
|
||||
except Exception as e:
|
||||
# Fallback to servicemanager logging
|
||||
servicemanager.LogInfoMsg(f"Failed to setup file logging: {e}")
|
||||
self.log = servicemanager
|
||||
|
||||
@classmethod
|
||||
def load_service_config(cls):
|
||||
config_path = sys.argv[1]
|
||||
with open(config_path, 'r', encoding="utf-8") as f:
|
||||
config_data = json.load(f)
|
||||
service_config = WindowServiceConfig(**config_data)
|
||||
cls._config = service_config
|
||||
cls._svc_name_ = service_config.name
|
||||
cls._svc_display_name_ = service_config.display_name
|
||||
|
||||
def SvcStop(self):
|
||||
"""Stop the service"""
|
||||
self.log.info("Service stop requested via SvcStop")
|
||||
self.ReportServiceStatus(win32service.SERVICE_STOP_PENDING)
|
||||
self.stop()
|
||||
self.ReportServiceStatus(win32service.SERVICE_STOPPED)
|
||||
self.log.info("Service stopped successfully")
|
||||
|
||||
def SvcDoRun(self):
|
||||
"""Start the service; does not return until stopped"""
|
||||
self.log.info("Service starting via SvcDoRun")
|
||||
self.ReportServiceStatus(win32service.SERVICE_START_PENDING)
|
||||
self.ReportServiceStatus(win32service.SERVICE_RUNNING)
|
||||
self.log.info("Service status set to RUNNING")
|
||||
# Run the service
|
||||
try:
|
||||
self.run()
|
||||
except Exception as e:
|
||||
self.log.error(f"Service crashed with exception: {e}")
|
||||
self.log.error(f"Traceback: {traceback.format_exc()}")
|
||||
raise
|
||||
finally:
|
||||
self.ReportServiceStatus(win32service.SERVICE_STOPPED)
|
||||
self.log.info("Service status set to STOPPED")
|
||||
|
||||
def stop(self):
|
||||
"""Stop the service"""
|
||||
self.log.info("Stop method called")
|
||||
win32event.SetEvent(self.hWaitStop)
|
||||
self.stop_requested = True
|
||||
self.log.info("Service stop requested flag set")
|
||||
|
||||
# Terminate the subprocess
|
||||
if self.process and self.process.poll() is None:
|
||||
self.log.info(f"Terminating process with PID: {self.process.pid}")
|
||||
try:
|
||||
# self.process.terminate()
|
||||
self.process.send_signal(signal.CTRL_C_EVENT)
|
||||
time.sleep(1)
|
||||
self.process.terminate()
|
||||
self.log.info("Process termination signal sent")
|
||||
# Give process time to terminate gracefully
|
||||
try:
|
||||
self.process.wait(timeout=30)
|
||||
self.log.info("Process terminated gracefully within timeout")
|
||||
except subprocess.TimeoutExpired:
|
||||
self.log.warning("Process did not terminate gracefully, forcing kill")
|
||||
self.process.kill()
|
||||
self.process.wait()
|
||||
self.log.info("Process killed forcefully")
|
||||
|
||||
self.log.info("Wrapped process terminated successfully")
|
||||
except Exception as e:
|
||||
self.log.error(f"Error terminating wrapped process: {e}")
|
||||
self.log.error(f"Traceback: {traceback.format_exc()}")
|
||||
|
||||
|
||||
def run(self):
|
||||
"""Main service loop. This is where work is done!"""
|
||||
self.log.info("Starting service run method")
|
||||
|
||||
# Log configuration details
|
||||
self.log.info(f"Service configuration: {self._config}")
|
||||
|
||||
env = os.environ.copy()
|
||||
env['PYTHONIOENCODING'] = 'utf-8'
|
||||
if self._config.env:
|
||||
env.update(self._config.env)
|
||||
self.log.info(f"Environment variables updated: {list(self._config.env.keys())}")
|
||||
|
||||
cmd = self._config.bin_path
|
||||
if self._config.args:
|
||||
cmd = f"{cmd} {self._config.args}"
|
||||
|
||||
self.log.info(f"Command to execute: {' '.join(cmd)}")
|
||||
self.log.info(f"Working directory: {self._config.working_dir}")
|
||||
|
||||
try:
|
||||
# Launch the process
|
||||
self.log.info("Launching subprocess...")
|
||||
self.process = subprocess.Popen(
|
||||
cmd,
|
||||
cwd=self._config.working_dir,
|
||||
env=env,
|
||||
stdout=subprocess.PIPE,
|
||||
stderr=subprocess.PIPE,
|
||||
universal_newlines=True,
|
||||
shell=True,
|
||||
encoding="utf-8",
|
||||
)
|
||||
self.log.info(f"Process started successfully with PID: {self.process.pid}")
|
||||
except Exception as e:
|
||||
self.log.error(f"Failed to launch executable: {e}")
|
||||
self.log.error(f"Traceback: {traceback.format_exc()}")
|
||||
raise
|
||||
|
||||
# redirect logs
|
||||
self.stdout_thread = Thread(target=log_stream, args=(self.process.stdout, self.log.info))
|
||||
self.stderr_thread = Thread(target=log_stream, args=(self.process.stderr, self.log.error))
|
||||
self.stdout_thread.start()
|
||||
self.stderr_thread.start()
|
||||
|
||||
self.log.info("Entering process wait loop...")
|
||||
self._wait_for_process()
|
||||
|
||||
def _wait_for_process(self):
|
||||
"""Wait for the wrapped process to complete."""
|
||||
self.log.info("Starting process wait loop")
|
||||
loop_count = 0
|
||||
|
||||
while self.process.poll() is None and not self.stop_requested:
|
||||
loop_count += 1
|
||||
if loop_count % 10 == 0: # Log every 5 seconds
|
||||
self.log.debug(f"Process still running... (check #{loop_count})")
|
||||
|
||||
# Check for stop requests with short timeout (500ms for responsiveness)
|
||||
result = win32event.WaitForSingleObject(self.hWaitStop, 500)
|
||||
if result == win32event.WAIT_OBJECT_0:
|
||||
self.stop_requested = True
|
||||
self.log.info("Stop signal received via WaitForSingleObject")
|
||||
break
|
||||
|
||||
self.log.info(f"Exited wait loop. Process poll: {self.process.poll()}, stop_requested: {self.stop_requested}")
|
||||
|
||||
# Process has terminated or stop was requested
|
||||
if self.process and self.process.poll() is None:
|
||||
self.log.info("Process still running, initiating termination")
|
||||
try:
|
||||
self.process.terminate()
|
||||
self.log.info("Termination signal sent to process")
|
||||
self.process.wait(timeout=10)
|
||||
self.log.info("Process terminated gracefully")
|
||||
except subprocess.TimeoutExpired:
|
||||
self.log.warning("Process did not terminate gracefully, forcing kill")
|
||||
self.process.kill()
|
||||
self.process.wait()
|
||||
self.log.info("Process killed forcefully")
|
||||
except Exception as e:
|
||||
self.log.error(f"Error terminating process: {e}")
|
||||
self.log.error(f"Traceback: {traceback.format_exc()}")
|
||||
|
||||
# Capture final output
|
||||
if self.process:
|
||||
self.log.info("Capturing final process output...")
|
||||
self.stdout_thread.join()
|
||||
self.stderr_thread.join()
|
||||
self.log.info(f"Process terminated with exit code: {self.process.returncode}")
|
||||
|
||||
|
||||
def main():
|
||||
if len(sys.argv) < 2:
|
||||
print(f"Usage: {sys.argv[0]} /path/to/service_config.json")
|
||||
return
|
||||
|
||||
|
||||
servicemanager.Initialize()
|
||||
servicemanager.PrepareToHostSingle(WindowsServiceFramework)
|
||||
servicemanager.StartServiceCtrlDispatcher()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Reference in New Issue
Block a user