Files
scientific-surfing/scientific_surfing/windows_service_wrapper.py

261 lines
9.5 KiB
Python

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