import 'dart:io'; import 'dart:async'; import 'dart:convert'; import 'package:dio/dio.dart'; import 'package:get/get.dart'; import 'package:naiyouwl/app/bean/ClashServiceInfo.dart'; import 'package:naiyouwl/app/common/LogHelper.dart'; import 'package:naiyouwl/app/const/const.dart'; import 'package:naiyouwl/app/controller/controllers.dart'; import 'package:naiyouwl/app/utils/logger.dart'; import 'package:naiyouwl/app/utils/shell.dart'; import 'package:naiyouwl/app/utils/system_dns.dart'; import 'package:naiyouwl/app/utils/system_proxy.dart'; import 'package:naiyouwl/app/utils/utils.dart'; import 'package:path/path.dart' as path; import 'package:flutter/foundation.dart'; import 'package:bot_toast/bot_toast.dart'; import 'package:web_socket_channel/io.dart'; final headers = {"User-Agent": "ccore-for-flutter/0.0.1"}; class ServiceController extends GetxController { late final _dio = Dio(BaseOptions(baseUrl: 'http://127.0.0.1:9899', headers: headers)); var serviceMode = false.obs; var coreStatus = RunningState.stoped.obs; var serviceStatus = RunningState.stoped.obs; Process? clashServiceProcess; Process? clashCoreProcess; bool get isRunning => serviceStatus.value == RunningState.running && coreStatus.value == RunningState.running; bool get isCanOperationService => ![RunningState.starting, RunningState.stopping].contains(serviceStatus.value) && ![RunningState.starting, RunningState.stopping].contains(coreStatus.value); bool get isCanOperationCore => serviceStatus.value == RunningState.running && ![RunningState.starting, RunningState.stopping].contains(coreStatus.value); ServiceController( ); Future initConfig() async{ _dio.options.baseUrl = 'http://127.0.0.1:${controllers.config.config.value.servicePort}'; } Future startTunService() async { try { while (true) { final data = await fetchInfo(); if( data.mode != 'service-mode') { if (serviceStatus.value == RunningState.running) { await stopService(); } await install(); } else { break; } } } catch (e) { serviceStatus.value = RunningState.error; log.debug(e.toString()); // BotToast.showText(text: e.toString()); } } Future isService() async { //controllers.global.updateMsg("判断是不是 service-mode"); //serviceStatus.value = RunningState.starting; if (Platform.isLinux) { await fixBinaryExecutePermissions(Files.assetsClashService); await fixBinaryExecutePermissions(Files.assetsClashCore); } try { final data = await fetchInfo(); if(data.mode == 'service-mode'){ serviceMode.value = true; controllers.global.updateMsg("服务模式"); //await serviceModeSwitch(false); } } catch (_) { } //serviceStatus.value = RunningState.running; } Future startService() async { //controllers.global.updateMsg("开启服务"); serviceStatus.value = RunningState.starting; if (Platform.isLinux) { await fixBinaryExecutePermissions(Files.assetsClashService); await fixBinaryExecutePermissions(Files.assetsClashCore); } // bool isAvailable = await isPortAvailable(controllers.config.servicePort.value); // if (!isAvailable) { // controllers.global.updateMsg("端口${controllers.config.servicePort.value}被占用,启动服务失败。"); // serviceStatus.value = RunningState.error; // return; // 端口被占用,返回失败 // } try { final data = await fetchInfo(); serviceMode.value = data.mode == 'service-mode'; controllers.global.updateMsg("服务模式"); } catch (e) { await startUserModeService(); controllers.global.updateMsg("开启用户服务"); if (serviceStatus.value == RunningState.error) return; } serviceStatus.value = RunningState.running; } Future fixBinaryExecutePermissions(File file) async { final stat = await file.stat(); // 0b001000000 final has = (stat.mode & 64) == 64; if (has) return; await Process.run('chmod', ['+x', file.path]); } Future startUserModeService() async { serviceMode.value = false; try { int? exitCode; clashServiceProcess = await Process.start(Files.assetsClashService.path, ['-port','${controllers.config.config.value.servicePort}','user-mode'], mode: ProcessStartMode.inheritStdio); clashServiceProcess!.exitCode.then((code) => exitCode = code); while (true) { await Future.delayed(const Duration(milliseconds: 200)); if (exitCode == 101) { BotToast.showText(text: 'clash-service exit with code: $exitCode,After 10 seconds, try to restart'); log.error('After 10 seconds, try to restart'); await Future.delayed(const Duration(seconds: 10)); await startUserModeService(); break; } else if (exitCode != null) { serviceStatus.value = RunningState.error; break; } try { await _dio.post('/info'); break; } catch (_) {} } } catch (e) { serviceStatus.value = RunningState.error; BotToast.showText(text: e.toString()); } } Future stopService() async { serviceStatus.value = RunningState.stopping; if (coreStatus.value == RunningState.running) await stopClashCore(); if (!serviceMode.value) { if (clashServiceProcess != null) { clashServiceProcess!.kill(); clashServiceProcess = null; } else if (kDebugMode) { await killProcess(path.basename(Files.assetsClashService.path)); } } serviceStatus.value = RunningState.stoped; } // for macos Future waitServiceStart() async { while (true) { await Future.delayed(const Duration(milliseconds: 100)); try { await _dio.post('/info'); break; } catch (_) {} } } // for windows Future waitServiceStop() async { while (true) { await Future.delayed(const Duration(milliseconds: 100)); try { await _dio.post('/info'); } catch (e) { break; } } } Future fetchInfo() async { final res = await _dio.post('/info'); return ClashServiceInfo.fromJson(res.data); } IOWebSocketChannel fetchLogWs() { return IOWebSocketChannel.connect(Uri.parse('ws://127.0.0.1:${controllers.config.config.value.servicePort}/logs'), headers: headers); } Future fetchStart(String name) async { await fetchStop(); final res = await _dio.post('/start', data: { "args": ['-d', Paths.config.path, '-f', path.join(Paths.config.path, name)] }); if (json.decode(res.data!)["code"] != 0) throw json.decode(res.data!)["msg"]; } Future fetchStop() async { try { await _dio.post('/stop'); } catch (e) { return; } } Future install() async { final res = await runAsAdmin(Files.assetsClashService.path, ["-port","${controllers.config.config.value.servicePort}","stop", "uninstall", "install", "start"]); await initConfig(); log.debug('install', res.stdout, res.stderr); if (res.exitCode != 0) throw res.stderr; await waitServiceStart(); } Future uninstall() async { final res = await runAsAdmin(Files.assetsClashService.path, ["stop", "uninstall"]); log.debug('uninstall', res.stdout, res.stderr); if (res.exitCode != 0) throw res.stderr; await waitServiceStop(); } Future serviceModeSwitch(bool open) async { if (serviceStatus.value == RunningState.running) await stopService(); if (coreStatus.value == RunningState.running) await stopClashCore(); try { controllers.global.updateMsg(open ? "安装服务" : "卸载服务"); open ? await install() : await uninstall(); } catch (e) { BotToast.showText(text: e.toString()); } if(open){ await startService(); await startClashCore(); }else{ serviceMode.value = false; await startClashCore(); } //await startClashCore(); } Future isPortAvailable(int port) async { try { // 尝试绑定一个socket到指定的端口 var server = await ServerSocket.bind(InternetAddress.anyIPv4, port); // 成功绑定后立即关闭 await server.close(); // 如果成功绑定并关闭了服务器,那么端口是可用的 return true; } on SocketException { // 如果绑定失败,端口被占用 return false; } } Future startClashCore() async { final timeout = const Duration(seconds: 30); // 设置超时时间为30秒 final checkInterval = const Duration(milliseconds: 200); var startTime = DateTime.now(); await controllers.config.readClashCoreApi(); // bool isAvailable = await isPortAvailable(controllers.config.mixedPort.value); // if (!isAvailable) { // controllers.global.updateMsg("端口 被占用,启动内核失败,等待几秒后重新测试。"); // return false; // 端口被占用,返回失败 // } // isAvailable = await isPortAvailable(controllers.config.ApiAddressPort.value); // if (!isAvailable) { // controllers.global.updateMsg("端口 被占用,启动内核失败,等待几秒后重新测试。"); // return false; // 端口被占用,返回失败 // } try { controllers.global.updateMsg("启动内核---${controllers.config.config.value.selected}"); if( controllers.config.config.value.selected == 'init_proxy.yaml'){ controllers.global.updateMsg("启动内核初始化"); } else { controllers.global.updateMsg("启动内核"); } coreStatus.value = RunningState.starting; if(serviceMode.value == true){ await fetchStart(controllers.config.config.value.selected); } else { int? exitCode; clashCoreProcess = await Process.start(Files.assetsCCore.path, ['-d', Paths.config.path, '-f', path.join(Paths.config.path, controllers.config.config.value.selected)], mode: ProcessStartMode.inheritStdio); clashCoreProcess!.exitCode.then((code) => exitCode = code); if (exitCode != null && exitCode != 0) { // 非零退出代码通常表示错误 controllers.global.updateMsg("启动内核错误,请重启点电脑测试"); return false; } } // log.debug("api${controllers.config.clashCoreApiAddress.value}"); controllers.core.setApi(controllers.config.clashCoreApiAddress.value, controllers.config.clashCoreApiSecret.value); while (DateTime.now().difference(startTime) < timeout) { try { controllers.global.updateMsg("等待内核启动.."); final ret = await controllers.core.fetchHello(); // 如果fetchHello成功,跳出循环 break; } catch (_) { // 如果fetchHello失败,等待200毫秒后重试 await Future.delayed(checkInterval); } } // 检查是否超时 if (DateTime.now().difference(startTime) >= timeout) { // 如果超时,更新状态并抛出异常 coreStatus.value = RunningState.error; controllers.global.updateMsg("内核启动超时,重新点击加速后尝试。"); return false; // 提前退出函数 } await controllers.core.updateConfig(); coreStatus.value = RunningState.running; controllers.global.updateMsg("内核状态:${coreStatus.value == RunningState.running} "); controllers.global.updateDate(); return true; } catch (e) { log.error("core -- $e"); controllers.global.updateMsg("启动内核错误"); //controllers.global.handleApiError(e); // BotToast.showText(text: e.toString()); coreStatus.value = RunningState.error; return false; } } Future stopClashCore() async { coreStatus.value = RunningState.stopping; await controllers.global.closeProxy(); if(serviceMode.value == true){ await fetchStop(); } else{ if(clashCoreProcess != null){ clashCoreProcess?.kill(); } } killProcess(ClashName.name); // if (Platform.isMacOS && // controllers.service.serviceMode.value && // controllers.config.clashCoreTunEnable.value && // controllers.config.clashCoreDns.isNotEmpty) { // await MacSystemDns.instance.set([]); // } //if (controllers.config.config.value.setSystemProxy) await SystemProxy.instance.set(SystemProxyConfig()); //await stopClash(); coreStatus.value = RunningState.stoped; } Future initClashCoreConfig() async { controllers.config.config.value.selected = 'init_proxy.yaml'; //await stopClashCore(); await startClashCore(); if (coreStatus.value == RunningState.error) { controllers.global.updateMsg("启动内核失败..."); //BotToast.showText(text: '重启失败'); } else { await controllers.core.updateVersion(); controllers.global.updateMsg("启动内核成功..."); //BotToast.showText(text: '重启成功'); } } Future stopClash() async { controllers.config.config.value.selected = 'init_proxy.yaml'; if( coreStatus.value == RunningState.running){ await controllers.config.readClashCoreApi(); controllers.core.setApi(controllers.config.clashCoreApiAddress.value, controllers.config.clashCoreApiSecret.value); await controllers.core.changeConfig(path.join(Paths.config.path, controllers.config.config.value.selected)); } } Future reloadClashCore() async { try { controllers.config.config.value.selected = 'proxy.yaml'; if( coreStatus.value == RunningState.running){ controllers.global.updateMsg("切换配置..."); await controllers.config.readClashCoreApi(); //controllers.global.updateMsg("${controllers.config.clashCoreApiAddress.value}..."); controllers.core.setApi(controllers.config.clashCoreApiAddress.value, controllers.config.clashCoreApiSecret.value); //controllers.global.updateMsg("setApi${controllers.config.clashCoreApiAddress.value}..."); await controllers.core.changeConfig(path.join(Paths.config.path, controllers.config.config.value.selected)); controllers.global.updateMsg("fetchReloadConfig${controllers.config.clashCoreApiAddress.value}..."); } } catch(e){ controllers.global.updateMsg("重新配置..."); await controllers.config.readClashCoreApi(); //controllers.global.updateMsg("${controllers.config.clashCoreApiAddress.value}..."); controllers.core.setApi(controllers.config.clashCoreApiAddress.value, controllers.config.clashCoreApiSecret.value); //controllers.global.updateMsg("setApi${controllers.config.clashCoreApiAddress.value}..."); await controllers.core.changeConfig(path.join(Paths.config.path, controllers.config.config.value.selected)); } //BotToast.showText(text: '正在重启 Core ……'); // controllers.global.updateMsg("停止内核..."); // await stopClashCore(); // await controllers.config.readClashCoreApi(); // await startClashCore(); // if (coreStatus.value == RunningState.error) { // controllers.global.updateMsg("启动内核失败..."); // } else { // await controllers.core.updateVersion(); // controllers.global.updateMsg("启动内核成功..."); // } } Future getFreePort() async { var server = await HttpServer.bind(InternetAddress.loopbackIPv4, 0); int port = server.port; await server.close(); return port; } }