namespace ServiceLib.Manager; /// /// Core process processing class /// public class CoreManager { private static readonly Lazy _instance = new(() => new()); public static CoreManager Instance => _instance.Value; private Config _config; [SupportedOSPlatform("windows")] private WindowsJobService? _processJob; private ProcessService? _processService; private ProcessService? _processPreService; private bool _linuxSudo = false; private Func? _updateFunc; private const string _tag = "CoreHandler"; public async Task Init(Config config, Func updateFunc) { _config = config; _updateFunc = updateFunc; //Copy the bin folder to the storage location (for init) if (Environment.GetEnvironmentVariable(Global.LocalAppData) == "1") { var fromPath = Utils.GetBaseDirectory("bin"); var toPath = Utils.GetBinPath(""); if (fromPath != toPath) { FileUtils.CopyDirectory(fromPath, toPath, true, false); } } if (Utils.IsNonWindows()) { var coreInfo = CoreInfoManager.Instance.GetCoreInfo(); foreach (var it in coreInfo) { if (it.CoreType == ECoreType.v2rayN) { if (Utils.UpgradeAppExists(out var upgradeFileName)) { await Utils.SetLinuxChmod(upgradeFileName); } continue; } foreach (var name in it.CoreExes) { var exe = Utils.GetBinPath(Utils.GetExeName(name), it.CoreType.ToString()); if (File.Exists(exe)) { await Utils.SetLinuxChmod(exe); } } } } } /// Resolved main context (with pre-socks ports already merged if applicable). /// Optional pre-socks context passed to . public async Task LoadCore(CoreConfigContext? mainContext, CoreConfigContext? preContext) { if (mainContext == null) { await UpdateFunc(false, ResUI.CheckServerSettings); return; } var node = mainContext.Node; var fileName = Utils.GetBinConfigPath(Global.CoreConfigFileName); var result = await CoreConfigHandler.GenerateClientConfig(mainContext, fileName); if (result.Success != true) { await UpdateFunc(true, result.Msg); return; } await UpdateFunc(false, $"{node.GetSummary()}"); await UpdateFunc(false, $"{Utils.GetRuntimeInfo()}"); await UpdateFunc(false, string.Format(ResUI.StartService, DateTime.Now.ToString("yyyy/MM/dd HH:mm:ss"))); await CoreStop(); await Task.Delay(100); if (Utils.IsWindows() && (mainContext?.IsTunEnabled == true || preContext?.IsTunEnabled == true)) { await Task.Delay(100); await WindowsUtils.RemoveTunDevice(); } await CoreStart(mainContext); await WaitForProxyPort(preContext); await CoreStartPreService(preContext); AppManager.Instance.RunningCoreType = preContext?.RunCoreType ?? mainContext.RunCoreType; if (_processService != null) { await UpdateFunc(true, $"{node.GetSummary()}"); } } public async Task LoadCoreConfigSpeedtest(List selecteds) { var coreType = selecteds.FirstOrDefault()?.CoreType == ECoreType.sing_box ? ECoreType.sing_box : ECoreType.Xray; var fileName = string.Format(Global.CoreSpeedtestConfigFileName, Utils.GetGuid(false)); var configPath = Utils.GetBinConfigPath(fileName); var result = await CoreConfigHandler.GenerateClientSpeedtestConfig(_config, configPath, selecteds, coreType); await UpdateFunc(false, result.Msg); if (result.Success != true) { return null; } await UpdateFunc(false, string.Format(ResUI.StartService, DateTime.Now.ToString("yyyy/MM/dd HH:mm:ss"))); await UpdateFunc(false, configPath); var coreInfo = CoreInfoManager.Instance.GetCoreInfo(coreType); return await RunProcess(coreInfo, fileName, true, false); } public async Task LoadCoreConfigSpeedtest(ServerTestItem testItem) { var node = await AppManager.Instance.GetProfileItem(testItem.IndexId); if (node is null) { return null; } var fileName = string.Format(Global.CoreSpeedtestConfigFileName, Utils.GetGuid(false)); var configPath = Utils.GetBinConfigPath(fileName); var (context, _) = await CoreConfigContextBuilder.Build(_config, node); var result = await CoreConfigHandler.GenerateClientSpeedtestConfig(_config, context, testItem, configPath); if (result.Success != true) { return null; } var coreType = context.RunCoreType; var coreInfo = CoreInfoManager.Instance.GetCoreInfo(coreType); return await RunProcess(coreInfo, fileName, true, false); } public async Task CoreStop() { try { if (_linuxSudo) { await CoreAdminManager.Instance.KillProcessAsLinuxSudo(); _linuxSudo = false; } if (_processService != null) { await _processService.StopAsync(); _processService.Dispose(); _processService = null; } if (_processPreService != null) { await _processPreService.StopAsync(); _processPreService.Dispose(); _processPreService = null; } } catch (Exception ex) { Logging.SaveLog(_tag, ex); } } #region Private private async Task CoreStart(CoreConfigContext context) { var node = context.Node; var coreType = AppManager.Instance.GetCoreType(node, node.ConfigType); var coreInfo = CoreInfoManager.Instance.GetCoreInfo(coreType); var displayLog = node.ConfigType != EConfigType.Custom || node.DisplayLog; var proc = await RunProcess(coreInfo, Global.CoreConfigFileName, displayLog, true, context.IsTunEnabled); if (proc is null) { return; } _processService = proc; } private async Task CoreStartPreService(CoreConfigContext? preContext) { if (_processService is { HasExited: false } && preContext != null) { var preCoreType = preContext?.Node?.CoreType ?? ECoreType.sing_box; var fileName = Utils.GetBinConfigPath(Global.CorePreConfigFileName); var result = await CoreConfigHandler.GenerateClientConfig(preContext, fileName); if (result.Success) { var coreInfo = CoreInfoManager.Instance.GetCoreInfo(preCoreType); var proc = await RunProcess(coreInfo, Global.CorePreConfigFileName, true, true, preContext.IsTunEnabled); if (proc is null) { return; } _processPreService = proc; } } } private async Task UpdateFunc(bool notify, string msg) { await _updateFunc?.Invoke(notify, msg); } private static async Task WaitForProxyPort(CoreConfigContext? preContext) { if (preContext is null) { return; } if (!preContext.IsTunEnabled) { return; } using var rootCts = new CancellationTokenSource(Global.LocalFetch); var rootToken = rootCts.Token; var port = preContext.Node.Port; // SOCKS5 client greeting: VER=5, NMETHODS=1, METHOD=0x00 (no auth) ReadOnlyMemory greeting = new byte[] { 0x05, 0x01, 0x00 }; var buf = new byte[2]; while (!rootToken.IsCancellationRequested) { using var tcp = new TcpClient(); using var attemptCts = new CancellationTokenSource(TimeSpan.FromMilliseconds(50)); using var linkedCts = CancellationTokenSource.CreateLinkedTokenSource(rootToken, attemptCts.Token); var linkedToken = linkedCts.Token; try { await tcp.ConnectAsync(Global.Loopback, port, linkedToken); var stream = tcp.GetStream(); await stream.WriteAsync(greeting, linkedToken); var read = await stream.ReadAsync(buf.AsMemory(0, 2), linkedToken); // Server selection: VER=5, METHOD=0x00 — proxy is fully ready if (read == 2 && buf[0] == 0x05) { return; } } catch (OperationCanceledException) { if (!rootToken.IsCancellationRequested) { continue; } Logging.SaveLog($"WaitForProxyPort Timeout waiting for proxy port {port} to be ready."); return; } catch (SocketException ex) when (ex.SocketErrorCode == SocketError.ConnectionRefused) { // Connection refused, proxy not ready yet, wait 50ms before retrying try { await Task.Delay(50, rootToken); } catch (OperationCanceledException) { Logging.SaveLog($"WaitForProxyPort Timeout waiting for proxy port {port} to be ready."); return; } } catch { // Ignore other exceptions and continue } } } #endregion Private #region Process /// /// Decides whether a core launch must be elevated on non-Windows platforms. /// The TUN state comes from the immutable snapshot that /// generated the config, never from the live mutable config: the generated config and the /// launch mode must always agree, even if TUN is toggled while a reload is in flight. /// public static bool ShouldRunAsSudo(bool isTunLaunch, ECoreType? coreType, bool isNonWindows) { return isTunLaunch && coreType is ECoreType.sing_box or ECoreType.mihomo or ECoreType.Xray && isNonWindows; } private async Task RunProcess(CoreInfo? coreInfo, string configPath, bool displayLog, bool mayNeedSudo, bool isTunLaunch = false) { var fileName = CoreInfoManager.Instance.GetCoreExecFile(coreInfo, out var msg); if (fileName.IsNullOrEmpty()) { await UpdateFunc(false, msg); return null; } try { if (mayNeedSudo && ShouldRunAsSudo(isTunLaunch, coreInfo.CoreType, Utils.IsNonWindows())) { _linuxSudo = true; await CoreAdminManager.Instance.Init(_config, _updateFunc); return await CoreAdminManager.Instance.RunProcessAsLinuxSudo(fileName, coreInfo, configPath); } return await RunProcessNormal(fileName, coreInfo, configPath, displayLog); } catch (Exception ex) { Logging.SaveLog(_tag, ex); await UpdateFunc(mayNeedSudo, ex.Message); return null; } } private async Task RunProcessNormal(string fileName, CoreInfo? coreInfo, string configPath, bool displayLog) { var environmentVars = new Dictionary(); foreach (var kv in coreInfo.Environment) { environmentVars[kv.Key] = string.Format(kv.Value, coreInfo.AbsolutePath ? Utils.GetBinConfigPath(configPath).AppendQuotes() : configPath); } var procService = new ProcessService( fileName: fileName, arguments: string.Format(coreInfo.Arguments, coreInfo.AbsolutePath ? Utils.GetBinConfigPath(configPath).AppendQuotes() : configPath), workingDirectory: Utils.GetBinConfigPath(), displayLog: displayLog, redirectInput: false, environmentVars: environmentVars, updateFunc: _updateFunc ); await procService.StartAsync(); await Task.Delay(100); if (procService is null or { HasExited: true }) { throw new Exception(ResUI.FailedToRunCore); } AddProcessJob(procService.Handle); return procService; } private void AddProcessJob(nint processHandle) { if (Utils.IsWindows()) { _processJob ??= new(); try { _processJob?.AddProcess(processHandle); } catch { } } } #endregion Process }