diff --git a/MSLX.Daemon/Program.cs b/MSLX.Daemon/Program.cs index faad513..9972f9c 100644 --- a/MSLX.Daemon/Program.cs +++ b/MSLX.Daemon/Program.cs @@ -1,4 +1,4 @@ -using Microsoft.AspNetCore.HttpOverrides; +using Microsoft.AspNetCore.HttpOverrides; using Microsoft.AspNetCore.Mvc; using Microsoft.Extensions.FileProviders; using MSLX.Daemon.Hubs; @@ -9,9 +9,20 @@ using MSLX.Daemon.Utils.BackgroundTasks; using MSLX.Daemon.Utils.ConfigUtils; using System.Reflection; +using System.Runtime.InteropServices; System.Text.Encoding.RegisterProvider(System.Text.CodePagesEncodingProvider.Instance); +// 控制台关闭事件处理 +if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) +{ + WindowsConsoleHandler.Initialize(); +} +else if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux) || RuntimeInformation.IsOSPlatform(OSPlatform.OSX)) +{ + UnixConsoleHandler.Initialize(); +} + var builder = WebApplication.CreateBuilder(args); // 日志级别配置 diff --git a/MSLX.Daemon/Services/FrpProcessService.cs b/MSLX.Daemon/Services/FrpProcessService.cs index f1df39e..4c2b6ee 100644 --- a/MSLX.Daemon/Services/FrpProcessService.cs +++ b/MSLX.Daemon/Services/FrpProcessService.cs @@ -1,7 +1,8 @@ -using System.Collections.Concurrent; +using System.Collections.Concurrent; using System.Diagnostics; using System.IO.Compression; using System.Formats.Tar; +using System.Runtime.InteropServices; using System.Text; using Downloader; using Microsoft.AspNetCore.SignalR; @@ -30,6 +31,7 @@ public class FrpContext private readonly ConcurrentDictionary _activeProcesses = new(); private const int MaxLogLines = 200; + private int _isStopping = 0; // 防重复停止标志 (0=未停止, 1=正在停止) private readonly string _frpcExecutablePath; private readonly string _toolsDir; @@ -49,6 +51,16 @@ public FrpProcessService(ILogger logger, IHubContext GetLogs(int id) private void StopAllFrp() { - if (_activeProcesses.IsEmpty) return; + // 防重复执行检查 + if (Interlocked.CompareExchange(ref _isStopping, 1, 0) != 0) + { + _logger.LogDebug("StopAllFrp 已在执行中,跳过重复调用"); + return; + } + + if (_activeProcesses.IsEmpty) + { + _logger.LogInformation("没有正在运行的 FRP 隧道需要停止"); + return; + } + + _logger.LogInformation($"正在停止 {_activeProcesses.Count} 个 FRP 隧道..."); + int stoppedCount = 0; + int failedCount = 0; + foreach (var kvp in _activeProcesses) { + int id = kvp.Key; + var context = kvp.Value; try { - var context = kvp.Value; - if (context.Process != null && !context.Process.HasExited) context.Process.Kill(true); + if (context.Process != null && !context.Process.HasExited) + { + context.Process.Kill(true); + context.Process.WaitForExit(2000); // 等待最多2秒 + stoppedCount++; + _logger.LogInformation($"FRP 隧道 [{id}] 已停止 (PID: {context.Process.Id})"); + } + } + catch (InvalidOperationException ex) + { + // 进程已经退出 + _logger.LogDebug($"FRP 隧道 [{id}] 进程已经退出"); + } + catch (Exception ex) + { + failedCount++; + _logger.LogError(ex, $"停止 FRP 隧道 [{id}] 时发生错误"); } - catch { } } + _activeProcesses.Clear(); + _logger.LogInformation($"FRP 隧道停止完成: 成功 {stoppedCount} 个, 失败 {failedCount} 个"); + + // 重置停止标志,允许下次调用 + Interlocked.Exchange(ref _isStopping, 0); } private void OnAppStarted() diff --git a/MSLX.Daemon/Utils/UnixConsoleHandler.cs b/MSLX.Daemon/Utils/UnixConsoleHandler.cs new file mode 100644 index 0000000..2954b7a --- /dev/null +++ b/MSLX.Daemon/Utils/UnixConsoleHandler.cs @@ -0,0 +1,132 @@ +using System.Runtime.InteropServices; +using System.Diagnostics; + +namespace MSLX.Daemon.Utils; + +/// +/// Unix/Linux/macOS 控制台关闭事件处理器 +/// 用于在进程收到终止信号时执行清理操作 +/// +public static class UnixConsoleHandler +{ + private static readonly ILogger Logger; + private static readonly List CleanupActions = new(); + private static readonly object LockObj = new(); + private static bool _initialized = false; + + // 保持委托引用,防止被 GC 回收 + private static SignalHandler? _signalHandler; + private static GCHandle _signalHandlerHandle; + + // POSIX 信号常量 + private const int SIGINT = 2; // Ctrl+C + private const int SIGTERM = 15; // 终止信号 (kill 默认) + private const int SIGHUP = 1; // 挂起信号 (终端关闭) + + // 委托类型 + private delegate void SignalHandler(int signum); + + [DllImport("libc", SetLastError = true)] + private static extern IntPtr signal(int signum, SignalHandler handler); + + [DllImport("libc", SetLastError = true)] + private static extern int getpid(); + + static UnixConsoleHandler() + { + var loggerFactory = LoggerFactory.Create(builder => builder.AddConsole()); + Logger = loggerFactory.CreateLogger("UnixConsoleHandler"); + } + + /// + /// 初始化 Unix 信号处理 + /// + public static void Initialize() + { + if (_initialized) return; + + lock (LockObj) + { + if (_initialized) return; + + // 创建委托实例并保持引用 + _signalHandler = OnSignal; + _signalHandlerHandle = GCHandle.Alloc(_signalHandler, GCHandleType.Normal); + + // 注册信号处理器 + signal(SIGINT, _signalHandler); + signal(SIGTERM, _signalHandler); + signal(SIGHUP, _signalHandler); + + _initialized = true; + + Logger.LogDebug("Unix 信号处理器已初始化"); + } + } + + /// + /// 注册清理操作 + /// + public static void RegisterCleanupAction(Action action) + { + lock (LockObj) + { + CleanupActions.Add(action); + } + } + + /// + /// 取消注册清理操作 + /// + public static void UnregisterCleanupAction(Action action) + { + lock (LockObj) + { + CleanupActions.Remove(action); + } + } + + private static void OnSignal(int signum) + { + string signalName = signum switch + { + SIGINT => "SIGINT (Ctrl+C)", + SIGTERM => "SIGTERM", + SIGHUP => "SIGHUP (Terminal closed)", + _ => $"Signal({signum})" + }; + + Logger.LogInformation($"收到信号: {signalName},正在执行清理操作..."); + + // 执行所有注册的清理操作 + List actions; + lock (LockObj) + { + actions = new List(CleanupActions); + } + + foreach (var action in actions) + { + try + { + action(); + } + catch (Exception ex) + { + Logger.LogError(ex, "执行清理操作时发生错误"); + } + } + + // 根据信号类型决定是否退出 + if (signum == SIGINT) + { + // Ctrl+C 正常退出 + Environment.Exit(0); + } + else + { + // 其他信号也正常退出 + Environment.Exit(0); + } + } +} diff --git a/MSLX.Daemon/Utils/WindowsConsoleHandler.cs b/MSLX.Daemon/Utils/WindowsConsoleHandler.cs new file mode 100644 index 0000000..b3fe0ce --- /dev/null +++ b/MSLX.Daemon/Utils/WindowsConsoleHandler.cs @@ -0,0 +1,132 @@ +using System.Diagnostics; +using System.Runtime.InteropServices; + +namespace MSLX.Daemon.Utils; + +/// +/// Windows 控制台关闭事件处理器 +/// 用于在控制台窗口关闭时执行清理操作 +/// +public static class WindowsConsoleHandler +{ + private static readonly ILogger Logger; + private static readonly List CleanupActions = new(); + private static readonly object LockObj = new(); + private static bool _initialized = false; + + // 保持委托引用,防止被 GC 回收 + private static HandlerRoutine? _handlerRoutine; + + // Windows API 常量 + private const int CTRL_C_EVENT = 0; + private const int CTRL_BREAK_EVENT = 1; + private const int CTRL_CLOSE_EVENT = 2; + private const int CTRL_LOGOFF_EVENT = 5; + private const int CTRL_SHUTDOWN_EVENT = 6; + + // 委托类型 + private delegate bool HandlerRoutine(int ctrlType); + + [DllImport("kernel32.dll", SetLastError = true)] + private static extern bool SetConsoleCtrlHandler(HandlerRoutine handler, bool add); + + [DllImport("kernel32.dll", SetLastError = true)] + private static extern IntPtr GetCurrentProcess(); + + [DllImport("kernel32.dll", SetLastError = true)] + private static extern bool TerminateProcess(IntPtr hProcess, uint uExitCode); + + static WindowsConsoleHandler() + { + // 创建一个简单的日志记录器 + var loggerFactory = LoggerFactory.Create(builder => builder.AddConsole()); + Logger = loggerFactory.CreateLogger("WindowsConsoleHandler"); + } + + /// + /// 初始化控制台关闭事件处理 + /// + public static void Initialize() + { + if (_initialized) return; + + lock (LockObj) + { + if (_initialized) return; + + // 创建委托实例并保持引用,防止被 GC 回收 + _handlerRoutine = OnConsoleCtrlEvent; + SetConsoleCtrlHandler(_handlerRoutine, true); + _initialized = true; + + Logger.LogDebug("Windows 控制台关闭事件处理器已初始化"); + } + } + + /// + /// 注册清理操作 + /// + public static void RegisterCleanupAction(Action action) + { + lock (LockObj) + { + CleanupActions.Add(action); + } + } + + /// + /// 取消注册清理操作 + /// + public static void UnregisterCleanupAction(Action action) + { + lock (LockObj) + { + CleanupActions.Remove(action); + } + } + + private static bool OnConsoleCtrlEvent(int ctrlType) + { + string eventName = ctrlType switch + { + CTRL_C_EVENT => "CTRL_C", + CTRL_BREAK_EVENT => "CTRL_BREAK", + CTRL_CLOSE_EVENT => "CTRL_CLOSE", + CTRL_LOGOFF_EVENT => "CTRL_LOGOFF", + CTRL_SHUTDOWN_EVENT => "CTRL_SHUTDOWN", + _ => $"UNKNOWN({ctrlType})" + }; + + Logger.LogInformation($"收到控制台关闭事件: {eventName},正在执行清理操作..."); + + // 执行所有注册的清理操作 + List actions; + lock (LockObj) + { + actions = new List(CleanupActions); + } + + foreach (var action in actions) + { + try + { + action(); + } + catch (Exception ex) + { + Logger.LogError(ex, "执行清理操作时发生错误"); + } + } + + // 对于 CTRL_CLOSE 事件,系统只给大约 5-10 秒的时间 + // 我们需要尽快退出 + if (ctrlType == CTRL_CLOSE_EVENT) + { + // 强制终止当前进程 + var currentProcess = Process.GetCurrentProcess(); + TerminateProcess(currentProcess.Handle, 0); + } + + return true; + } +}