service.dart 16 KB

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