123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433 |
- 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<void> initConfig() async{
- _dio.options.baseUrl = 'http://127.0.0.1:${controllers.config.config.value.servicePort}';
- }
- Future<void> 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<void> 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<void> 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<void> 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<void> 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<void> 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<void> waitServiceStart() async {
- while (true) {
- await Future.delayed(const Duration(milliseconds: 100));
- try {
- await _dio.post('/info');
- break;
- } catch (_) {}
- }
- }
- // for windows
- Future<void> waitServiceStop() async {
- while (true) {
- await Future.delayed(const Duration(milliseconds: 100));
- try {
- await _dio.post('/info');
- } catch (e) {
- break;
- }
- }
- }
- Future<ClashServiceInfo> 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<void> fetchStart(String name) async {
- await fetchStop();
- final res = await _dio.post<String>('/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<void> fetchStop() async {
- try {
- await _dio.post('/stop');
- } catch (e) {
- return;
- }
- }
- Future<void> 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<void> 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<void> 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<bool> isPortAvailable(int port) async {
- try {
- // 尝试绑定一个socket到指定的端口
- var server = await ServerSocket.bind(InternetAddress.anyIPv4, port);
- // 成功绑定后立即关闭
- await server.close();
- // 如果成功绑定并关闭了服务器,那么端口是可用的
- return true;
- } on SocketException {
- // 如果绑定失败,端口被占用
- return false;
- }
- }
- Future<bool> 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<void> 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<void> 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<void> 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<void> 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<int> getFreePort() async {
- var server = await HttpServer.bind(InternetAddress.loopbackIPv4, 0);
- int port = server.port;
- await server.close();
- return port;
- }
- }
|