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()