261 lines
9.5 KiB
Python
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() |