service.dart 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420
  1. import 'dart:io';
  2. import 'dart:async';
  3. import 'dart:convert';
  4. import 'package:dio/dio.dart';
  5. import 'package:get/get.dart';
  6. import 'package:naiyouwl/app/bean/ClashServiceInfo.dart';
  7. import 'package:naiyouwl/app/common/LogHelper.dart';
  8. import 'package:naiyouwl/app/const/const.dart';
  9. import 'package:naiyouwl/app/controller/controllers.dart';
  10. import 'package:naiyouwl/app/utils/logger.dart';
  11. import 'package:naiyouwl/app/utils/shell.dart';
  12. import 'package:naiyouwl/app/utils/system_dns.dart';
  13. import 'package:naiyouwl/app/utils/system_proxy.dart';
  14. import 'package:naiyouwl/app/utils/utils.dart';
  15. import 'package:path/path.dart' as path;
  16. import 'package:flutter/foundation.dart';
  17. import 'package:bot_toast/bot_toast.dart';
  18. import 'package:web_socket_channel/io.dart';
  19. final headers = {"User-Agent": "ccore-for-flutter/0.0.1"};
  20. class ServiceController extends GetxController {
  21. late final _dio = Dio(BaseOptions(baseUrl: 'http://127.0.0.1:9899', headers: headers));
  22. var serviceMode = false.obs;
  23. var coreStatus = RunningState.stoped.obs;
  24. var serviceStatus = RunningState.stoped.obs;
  25. Process? clashServiceProcess;
  26. Process? clashCoreProcess;
  27. bool get isRunning => serviceStatus.value == RunningState.running && coreStatus.value == RunningState.running;
  28. bool get isCanOperationService =>
  29. ![RunningState.starting, RunningState.stopping].contains(serviceStatus.value) &&
  30. ![RunningState.starting, RunningState.stopping].contains(coreStatus.value);
  31. bool get isCanOperationCore =>
  32. serviceStatus.value == RunningState.running && ![RunningState.starting, RunningState.stopping].contains(coreStatus.value);
  33. ServiceController(
  34. );
  35. Future<void> initConfig() async{
  36. _dio.options.baseUrl = 'http://127.0.0.1:${controllers.config.config.value.servicePort}';
  37. }
  38. Future<void> startTunService() async {
  39. try {
  40. while (true) {
  41. final data = await fetchInfo();
  42. if( data.mode != 'service-mode')
  43. {
  44. if (serviceStatus.value == RunningState.running) {
  45. await stopService();
  46. }
  47. await install();
  48. } else {
  49. break;
  50. }
  51. }
  52. } catch (e) {
  53. serviceStatus.value = RunningState.error;
  54. log.debug(e.toString());
  55. // BotToast.showText(text: e.toString());
  56. }
  57. }
  58. Future<void> isService() async {
  59. //controllers.global.updateMsg("判断是不是 service-mode");
  60. //serviceStatus.value = RunningState.starting;
  61. if (Platform.isLinux) {
  62. await fixBinaryExecutePermissions(Files.assetsClashService);
  63. await fixBinaryExecutePermissions(Files.assetsClashCore);
  64. }
  65. try {
  66. final data = await fetchInfo();
  67. if(data.mode == 'service-mode'){
  68. serviceMode.value = true;
  69. controllers.global.updateMsg("服务模式");
  70. //await serviceModeSwitch(false);
  71. }
  72. } catch (_) {
  73. }
  74. //serviceStatus.value = RunningState.running;
  75. }
  76. Future<void> startService() async {
  77. //controllers.global.updateMsg("开启服务");
  78. serviceStatus.value = RunningState.starting;
  79. if (Platform.isLinux) {
  80. await fixBinaryExecutePermissions(Files.assetsClashService);
  81. await fixBinaryExecutePermissions(Files.assetsClashCore);
  82. }
  83. // bool isAvailable = await isPortAvailable(controllers.config.servicePort.value);
  84. // if (!isAvailable) {
  85. // controllers.global.updateMsg("端口${controllers.config.servicePort.value}被占用,启动服务失败。");
  86. // serviceStatus.value = RunningState.error;
  87. // return; // 端口被占用,返回失败
  88. // }
  89. try {
  90. final data = await fetchInfo();
  91. serviceMode.value = data.mode == 'service-mode';
  92. controllers.global.updateMsg("服务模式");
  93. } catch (e) {
  94. await startUserModeService();
  95. controllers.global.updateMsg("开启用户服务");
  96. if (serviceStatus.value == RunningState.error) return;
  97. }
  98. serviceStatus.value = RunningState.running;
  99. }
  100. Future<void> fixBinaryExecutePermissions(File file) async {
  101. final stat = await file.stat();
  102. // 0b001000000
  103. final has = (stat.mode & 64) == 64;
  104. if (has) return;
  105. await Process.run('chmod', ['+x', file.path]);
  106. }
  107. Future<void> startUserModeService() async {
  108. serviceMode.value = false;
  109. try {
  110. int? exitCode;
  111. clashServiceProcess = await Process.start(Files.assetsClashService.path, ['-port','${controllers.config.config.value.servicePort}','user-mode'], mode: ProcessStartMode.inheritStdio);
  112. clashServiceProcess!.exitCode.then((code) => exitCode = code);
  113. while (true) {
  114. await Future.delayed(const Duration(milliseconds: 200));
  115. if (exitCode == 101) {
  116. BotToast.showText(text: 'clash-service exit with code: $exitCode,After 10 seconds, try to restart');
  117. log.error('After 10 seconds, try to restart');
  118. await Future.delayed(const Duration(seconds: 10));
  119. await startUserModeService();
  120. break;
  121. } else if (exitCode != null) {
  122. serviceStatus.value = RunningState.error;
  123. break;
  124. }
  125. try {
  126. await _dio.post('/info');
  127. break;
  128. } catch (_) {}
  129. }
  130. } catch (e) {
  131. serviceStatus.value = RunningState.error;
  132. BotToast.showText(text: e.toString());
  133. }
  134. }
  135. Future<void> stopService() async {
  136. serviceStatus.value = RunningState.stopping;
  137. if (coreStatus.value == RunningState.running) await stopClashCore();
  138. if (!serviceMode.value) {
  139. if (clashServiceProcess != null) {
  140. clashServiceProcess!.kill();
  141. clashServiceProcess = null;
  142. } else if (kDebugMode) {
  143. await killProcess(path.basename(Files.assetsClashService.path));
  144. }
  145. }
  146. serviceStatus.value = RunningState.stoped;
  147. }
  148. // for macos
  149. Future<void> waitServiceStart() async {
  150. while (true) {
  151. await Future.delayed(const Duration(milliseconds: 100));
  152. try {
  153. await _dio.post('/info');
  154. break;
  155. } catch (_) {}
  156. }
  157. }
  158. // for windows
  159. Future<void> waitServiceStop() async {
  160. while (true) {
  161. await Future.delayed(const Duration(milliseconds: 100));
  162. try {
  163. await _dio.post('/info');
  164. } catch (e) {
  165. break;
  166. }
  167. }
  168. }
  169. Future<ClashServiceInfo> fetchInfo() async {
  170. final res = await _dio.post('/info');
  171. return ClashServiceInfo.fromJson(res.data);
  172. }
  173. IOWebSocketChannel fetchLogWs() {
  174. return IOWebSocketChannel.connect(Uri.parse('ws://127.0.0.1:${controllers.config.config.value.servicePort}/logs'), headers: headers);
  175. }
  176. Future<void> fetchStart(String name) async {
  177. await fetchStop();
  178. final res = await _dio.post<String>('/start', data: {
  179. "args": ['-d', Paths.config.path, '-f', path.join(Paths.config.path, name)]
  180. });
  181. if (json.decode(res.data!)["code"] != 0) throw json.decode(res.data!)["msg"];
  182. }
  183. Future<void> fetchStop() async {
  184. try {
  185. await _dio.post('/stop');
  186. } catch (e) {
  187. return;
  188. }
  189. }
  190. Future<void> install() async {
  191. final res = await runAsAdmin(Files.assetsClashService.path, ["-port","${controllers.config.config.value.servicePort}","stop", "uninstall", "install", "start"]);
  192. await initConfig();
  193. log.debug('install', res.stdout, res.stderr);
  194. if (res.exitCode != 0) throw res.stderr;
  195. await waitServiceStart();
  196. }
  197. Future<void> uninstall() async {
  198. final res = await runAsAdmin(Files.assetsClashService.path, ["stop", "uninstall"]);
  199. log.debug('uninstall', res.stdout, res.stderr);
  200. if (res.exitCode != 0) throw res.stderr;
  201. await waitServiceStop();
  202. }
  203. Future<void> serviceModeSwitch(bool open) async {
  204. if (serviceStatus.value == RunningState.running) await stopService();
  205. try {
  206. controllers.global.updateMsg(open ? "安装服务" : "卸载服务");
  207. open ? await install() : await uninstall();
  208. } catch (e) {
  209. BotToast.showText(text: e.toString());
  210. }
  211. if(open){
  212. await startService();
  213. }else{
  214. serviceMode.value = false;
  215. }
  216. //await startClashCore();
  217. }
  218. Future<bool> isPortAvailable(int port) async {
  219. try {
  220. // 尝试绑定一个socket到指定的端口
  221. var server = await ServerSocket.bind(InternetAddress.anyIPv4, port);
  222. // 成功绑定后立即关闭
  223. await server.close();
  224. // 如果成功绑定并关闭了服务器,那么端口是可用的
  225. return true;
  226. } on SocketException {
  227. // 如果绑定失败,端口被占用
  228. return false;
  229. }
  230. }
  231. Future<bool> startClashCore() async {
  232. final timeout = const Duration(seconds: 30); // 设置超时时间为30秒
  233. final checkInterval = const Duration(milliseconds: 200);
  234. var startTime = DateTime.now();
  235. await controllers.config.readClashCoreApi();
  236. // bool isAvailable = await isPortAvailable(controllers.config.mixedPort.value);
  237. // if (!isAvailable) {
  238. // controllers.global.updateMsg("端口 被占用,启动内核失败,等待几秒后重新测试。");
  239. // return false; // 端口被占用,返回失败
  240. // }
  241. // isAvailable = await isPortAvailable(controllers.config.ApiAddressPort.value);
  242. // if (!isAvailable) {
  243. // controllers.global.updateMsg("端口 被占用,启动内核失败,等待几秒后重新测试。");
  244. // return false; // 端口被占用,返回失败
  245. // }
  246. try {
  247. controllers.global.updateMsg("启动内核---${controllers.config.config.value.selected}");
  248. if( controllers.config.config.value.selected == 'init_proxy.yaml'){
  249. controllers.global.updateMsg("启动内核初始化");
  250. } else {
  251. controllers.global.updateMsg("启动内核");
  252. }
  253. coreStatus.value = RunningState.starting;
  254. if(serviceMode.value == true){
  255. await fetchStart(controllers.config.config.value.selected);
  256. }
  257. else
  258. {
  259. int? exitCode;
  260. 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);
  261. clashCoreProcess!.exitCode.then((code) => exitCode = code);
  262. if (exitCode != null && exitCode != 0) {
  263. // 非零退出代码通常表示错误
  264. controllers.global.updateMsg("启动内核错误,请重启点电脑测试");
  265. return false;
  266. }
  267. }
  268. //
  269. log.debug("api${controllers.config.clashCoreApiAddress.value}");
  270. controllers.core.setApi(controllers.config.clashCoreApiAddress.value, controllers.config.clashCoreApiSecret.value);
  271. while (DateTime.now().difference(startTime) < timeout) {
  272. try {
  273. controllers.global.updateMsg("等待内核启动..");
  274. final ret = await controllers.core.fetchHello();
  275. // 如果fetchHello成功,跳出循环
  276. break;
  277. } catch (_) {
  278. // 如果fetchHello失败,等待200毫秒后重试
  279. await Future.delayed(checkInterval);
  280. }
  281. }
  282. // 检查是否超时
  283. if (DateTime.now().difference(startTime) >= timeout) {
  284. // 如果超时,更新状态并抛出异常
  285. coreStatus.value = RunningState.error;
  286. controllers.global.updateMsg("内核启动超时,重新点击加速后尝试。");
  287. return false; // 提前退出函数
  288. }
  289. await controllers.core.updateConfig();
  290. coreStatus.value = RunningState.running;
  291. controllers.global.updateMsg("内核状态:${coreStatus.value == RunningState.running} ");
  292. controllers.global.updateDate();
  293. return true;
  294. } catch (e) {
  295. log.error("core -- $e");
  296. controllers.global.updateMsg("启动内核错误");
  297. //controllers.global.handleApiError(e);
  298. // BotToast.showText(text: e.toString());
  299. coreStatus.value = RunningState.error;
  300. return false;
  301. }
  302. }
  303. Future<void> stopClashCore() async {
  304. coreStatus.value = RunningState.stopping;
  305. await controllers.global.closeProxy();
  306. if(serviceMode.value == true){
  307. await fetchStop();
  308. }
  309. else{
  310. if(clashCoreProcess != null){
  311. clashCoreProcess?.kill();
  312. }
  313. }
  314. // killProcess(ClashName.name);
  315. // if (Platform.isMacOS &&
  316. // controllers.service.serviceMode.value &&
  317. // controllers.config.clashCoreTunEnable.value &&
  318. // controllers.config.clashCoreDns.isNotEmpty) {
  319. // await MacSystemDns.instance.set([]);
  320. // }
  321. //if (controllers.config.config.value.setSystemProxy) await SystemProxy.instance.set(SystemProxyConfig());
  322. //await stopClash();
  323. coreStatus.value = RunningState.stoped;
  324. }
  325. Future<void> initClashCoreConfig() async {
  326. controllers.config.config.value.selected = 'init_proxy.yaml';
  327. //await stopClashCore();
  328. await startClashCore();
  329. if (coreStatus.value == RunningState.error) {
  330. controllers.global.updateMsg("启动内核失败...");
  331. //BotToast.showText(text: '重启失败');
  332. } else {
  333. await controllers.core.updateVersion();
  334. controllers.global.updateMsg("启动内核成功...");
  335. //BotToast.showText(text: '重启成功');
  336. }
  337. }
  338. Future<void> stopClash() async {
  339. controllers.config.config.value.selected = 'init_proxy.yaml';
  340. if( coreStatus.value == RunningState.running){
  341. await controllers.config.readClashCoreApi();
  342. controllers.core.setApi(controllers.config.clashCoreApiAddress.value, controllers.config.clashCoreApiSecret.value);
  343. await controllers.core.fetchReloadConfig({"path": path.join(Paths.config.path, controllers.config.config.value.selected),"payload": ""});
  344. }
  345. }
  346. Future<void> reloadClashCore() async {
  347. controllers.config.config.value.selected = 'proxy.yaml';
  348. if( coreStatus.value == RunningState.running){
  349. controllers.global.updateMsg("重新配置...");
  350. await controllers.config.readClashCoreApi();
  351. controllers.global.updateMsg("${controllers.config.clashCoreApiAddress.value}...");
  352. controllers.core.setApi(controllers.config.clashCoreApiAddress.value, controllers.config.clashCoreApiSecret.value);
  353. controllers.global.updateMsg("setApi${controllers.config.clashCoreApiAddress.value}...");
  354. await controllers.core.fetchReloadConfig({"path": path.join(Paths.config.path, controllers.config.config.value.selected),"payload":""});
  355. controllers.global.updateMsg("fetchReloadConfig${controllers.config.clashCoreApiAddress.value}...");
  356. }
  357. //BotToast.showText(text: '正在重启 Core ……');
  358. // controllers.global.updateMsg("停止内核...");
  359. // await stopClashCore();
  360. // await controllers.config.readClashCoreApi();
  361. // await startClashCore();
  362. // if (coreStatus.value == RunningState.error) {
  363. // controllers.global.updateMsg("启动内核失败...");
  364. // } else {
  365. // await controllers.core.updateVersion();
  366. // controllers.global.updateMsg("启动内核成功...");
  367. // }
  368. }
  369. Future<int> getFreePort() async {
  370. var server = await HttpServer.bind(InternetAddress.loopbackIPv4, 0);
  371. int port = server.port;
  372. await server.close();
  373. return port;
  374. }
  375. }