service.dart 21 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586
  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. import 'package:http/http.dart' as http;
  20. final headers = {"User-Agent": "ccore-for-flutter/0.0.1"};
  21. class ServiceController extends GetxController {
  22. //late final _dio = Dio(BaseOptions(baseUrl: 'http://127.0.0.1:9899', headers: headers));
  23. var ip = "127.0.0.1";
  24. var url = "";
  25. var client = http.Client();
  26. var serviceMode = false.obs;
  27. var coreStatus = RunningState.stoped.obs;
  28. var serviceStatus = RunningState.stoped.obs;
  29. Process? clashServiceProcess;
  30. Process? clashCoreProcess;
  31. bool get isRunning => serviceStatus.value == RunningState.running && coreStatus.value == RunningState.running;
  32. bool get isCanOperationService =>
  33. ![RunningState.starting, RunningState.stopping].contains(serviceStatus.value) &&
  34. ![RunningState.starting, RunningState.stopping].contains(coreStatus.value);
  35. bool get isCanOperationCore =>
  36. serviceStatus.value == RunningState.running && ![RunningState.starting, RunningState.stopping].contains(coreStatus.value);
  37. bool get coreIsRuning => coreStatus.value == RunningState.running;
  38. bool get serviceIsRuning => serviceStatus.value == RunningState.running;
  39. ServiceController(
  40. );
  41. Future<void> initConfig() async{
  42. url = "$ip:${controllers.config.config.value.servicePort}";
  43. controllers.config.setSerivcePort(controllers.config.config.value.servicePort);
  44. }
  45. Future<void> startTunService() async {
  46. try {
  47. while (true) {
  48. final data = await fetchInfo() ?? ClashServiceInfo.fromJson({'code': -1 , 'mode' : '' , 'status': '-1','version' : '-1'});
  49. if( data.mode != 'service-mode')
  50. {
  51. if (serviceStatus.value == RunningState.running) {
  52. await stopService();
  53. }
  54. await install();
  55. } else {
  56. break;
  57. }
  58. }
  59. } catch (e) {
  60. serviceStatus.value = RunningState.error;
  61. log.debug(e.toString());
  62. // BotToast.showText(text: e.toString());
  63. }
  64. }
  65. Future<void> isService() async {
  66. try {
  67. final data = await fetchInfo() ?? ClashServiceInfo.fromJson({'code': -1 , 'mode' : '' , 'status': '-1','version' : '-1'});
  68. if(data.mode == 'service-mode'){
  69. serviceMode.value = true;
  70. controllers.global.updateMsg("服务模式");
  71. } else {
  72. serviceMode.value = false;
  73. controllers.global.updateMsg("用户模式");
  74. }
  75. } catch (_) {
  76. }
  77. }
  78. Future<void> startService() async {
  79. serviceStatus.value = RunningState.starting;
  80. try {
  81. final data = await fetchInfo();
  82. if(data == null){
  83. await startUserModeService();
  84. controllers.global.updateMsg("开启用户服务");
  85. return;
  86. }
  87. serviceMode.value = data.mode == 'service-mode';
  88. controllers.global.updateMsg("服务模式");
  89. await fetchStop();
  90. } catch (e) {
  91. await startUserModeService();
  92. controllers.global.updateMsg("开启用户服务");
  93. if (serviceStatus.value == RunningState.error) return;
  94. }
  95. serviceStatus.value = RunningState.running;
  96. }
  97. Future<void> fixBinaryExecutePermissions(File file) async {
  98. final stat = await file.stat();
  99. // 0b001000000
  100. final has = (stat.mode & 64) == 64;
  101. if (has) return;
  102. await Process.run('chmod', ['+x', file.path]);
  103. }
  104. Future<void> startUserModeService() async {
  105. serviceStatus.value = RunningState.stopping;
  106. serviceMode.value = false;
  107. final timeout = const Duration(seconds: 30); // 设置超时时间为30秒
  108. final checkInterval = const Duration(milliseconds: 200);
  109. var startTime = DateTime.now();
  110. try {
  111. final isRun = await controllers.global.onIsProcessRunning(Files.assetsClashService.path);
  112. if(isRun == true){
  113. await isService();
  114. await fetchStart();
  115. serviceStatus.value = RunningState.running;
  116. return;
  117. }
  118. int? exitCode;
  119. clashServiceProcess = await Process.start(Files.assetsClashService.path, ['-port','${controllers.config.config.value.servicePort}','user-mode'], mode: ProcessStartMode.inheritStdio);
  120. clashServiceProcess!.exitCode.then((code) => exitCode = code);
  121. while (DateTime.now().difference(startTime) < timeout) {
  122. try {
  123. controllers.global.updateMsg("等待内核启动..");
  124. final ret = await fetchInfo();
  125. if(ret != null){
  126. if(ret.code == 0){
  127. break;
  128. }
  129. }
  130. break;
  131. } catch (_) {
  132. // 如果fetchHello失败,等待200毫秒后重试
  133. await Future.delayed(checkInterval);
  134. }
  135. }
  136. // 检查是否超时
  137. if (DateTime.now().difference(startTime) >= timeout) {
  138. // 如果超时,更新状态并抛出异常
  139. coreStatus.value = RunningState.error;
  140. controllers.global.updateMsg("内核启动超时,重新点击加速后尝试。");
  141. return; // 提前退出函数
  142. }
  143. await fetchStartInit();
  144. serviceStatus.value = RunningState.running;
  145. } catch (e) {
  146. serviceStatus.value = RunningState.error;
  147. //BotToast.showText(text: e.toString());
  148. print(e.toString());
  149. }
  150. }
  151. Future<void> stopService() async {
  152. serviceStatus.value = RunningState.stopping;
  153. if (coreStatus.value == RunningState.running) await fetchStop();
  154. if (!serviceMode.value) {
  155. if (clashServiceProcess != null) {
  156. clashServiceProcess!.kill();
  157. clashServiceProcess = null;
  158. } else if (kDebugMode) {
  159. await killProcess(path.basename(Files.assetsClashService.path));
  160. }
  161. }
  162. serviceStatus.value = RunningState.stoped;
  163. }
  164. // for macos
  165. Future<void> waitServiceStart() async {
  166. while (true) {
  167. await Future.delayed(const Duration(milliseconds: 100));
  168. try {
  169. await client.post(Uri.http(url,'info'),headers: headers);
  170. break;
  171. } catch (_) {}
  172. }
  173. }
  174. // for windows
  175. Future<void> waitServiceStop() async {
  176. while (true) {
  177. await Future.delayed(const Duration(milliseconds: 100));
  178. try {
  179. await client.post(Uri.http(url,'info'),headers: headers);
  180. } catch (e) {
  181. break;
  182. }
  183. }
  184. }
  185. Future<ClashServiceInfo?> fetchInfo() async {
  186. try {
  187. final res = await client.post(Uri.http(url,'info'),headers: headers);
  188. var jsonResponse =
  189. jsonDecode(res.body) as Map<String, dynamic>;
  190. return ClashServiceInfo.fromJson(jsonResponse);
  191. } on http.ClientException catch (e) {
  192. // 处理客户端异常,例如没有网络连接
  193. print('Client Exception: ${e.message}');
  194. return null;
  195. } on Exception catch (e) {
  196. // 处理其他类型的异常
  197. print('Exception: $e');
  198. return null;
  199. }
  200. return null;
  201. }
  202. IOWebSocketChannel fetchLogWs() {
  203. return IOWebSocketChannel.connect(Uri.parse('ws://127.0.0.1:${controllers.config.config.value.servicePort}/logs'), headers: headers);
  204. }
  205. Future<void> fetchStartInit() async {
  206. controllers.config.config.value.selected = 'init_proxy.yaml';
  207. controllers.global.updateMsg("启动内核---${controllers.config.config.value.selected}");
  208. if( controllers.config.config.value.selected == 'init_proxy.yaml'){
  209. controllers.global.updateMsg("启动内核初始化");
  210. } else {
  211. controllers.global.updateMsg("启动内核");
  212. }
  213. await fetchStop();
  214. try{
  215. var ut = Uri.http(url,'start');
  216. final body = json.encode({
  217. "args": [
  218. '-d',
  219. Paths.config.path,
  220. '-f',
  221. path.join(Paths.config.path, controllers.config.config.value.selected)
  222. ]
  223. });
  224. final res = await client.post(ut,body: body,headers: headers);
  225. var jsonResponse =
  226. jsonDecode(res.body) as Map<String, dynamic>;
  227. if (jsonResponse["code"] != 0) {
  228. coreStatus.value = RunningState.error;
  229. throw jsonResponse["msg"];
  230. }
  231. coreStatus.value = RunningState.running;
  232. } on http.ClientException catch (e) {
  233. // 处理客户端异常,例如没有网络连接
  234. print('Client Exception: ${e.message}');
  235. coreStatus.value = RunningState.error;
  236. } on Exception catch (e) {
  237. // 处理其他类型的异常
  238. print('Exception: $e');
  239. coreStatus.value = RunningState.error;
  240. }
  241. }
  242. Future<void> fetchStart() async {
  243. controllers.config.config.value.selected = 'proxy.yaml';
  244. controllers.global.updateMsg("启动内核---${controllers.config.config.value.selected}");
  245. if( controllers.config.config.value.selected == 'init_proxy.yaml'){
  246. controllers.global.updateMsg("启动内核初始化");
  247. } else {
  248. controllers.global.updateMsg("启动内核");
  249. }
  250. await fetchStop();
  251. try{
  252. var ut = Uri.http(url,'start');
  253. final res = await client.post(ut,body: {"args": ['-d', Paths.config.path, '-f', path.join(Paths.config.path, controllers.config.config.value.selected)]},headers: headers);
  254. var jsonResponse =
  255. jsonDecode(res.body) as Map<String, dynamic>;
  256. if (jsonResponse["code"] != 0) throw jsonResponse["msg"];
  257. } on http.ClientException catch (e) {
  258. // 处理客户端异常,例如没有网络连接
  259. print('Client Exception: ${e.message}');
  260. } on Exception catch (e) {
  261. // 处理其他类型的异常
  262. print('Exception: $e');
  263. }
  264. }
  265. Future<void> fetchSetProxy() async {
  266. try {
  267. var dns_ip = "";
  268. if(controllers.global.routeModesSelect.value == "tun"){
  269. dns_ip = "198.18.0.2";
  270. }
  271. var ut = Uri.http(url,'on');
  272. final body = json.encode({
  273. "http_port" : controllers.config.mixedPort.value,
  274. "https_port" : controllers.config.mixedPort.value,
  275. "socks_port" :controllers.config.mixedPort.value,
  276. "bypass": "127.0.0.1, 192.168.0.0/16, 10.0.0.0/8, 172.16.0.0/12, 100.64.0.0/10, 17.0.0.0/8, localhost, *.local, *.crashlytics.com, seed-sequoia.siri.apple.com, sequoia.apple.com,xd.apple.com",
  277. "dns_ip": dns_ip,
  278. "dns_type":"",
  279. "address":"127.0.0.1"
  280. });
  281. await client.post(ut,body: body,headers: headers);
  282. } on http.ClientException catch (e) {
  283. // 处理客户端异常,例如没有网络连接
  284. print('Client Exception: ${e.message}');
  285. } on Exception catch (e) {
  286. // 处理其他类型的异常
  287. print('Exception: $e');
  288. }
  289. }
  290. Future<void> fetchSetDnsProxy() async {
  291. try {
  292. var dns_ip = "";
  293. if(controllers.global.routeModesSelect.value == "tun"){
  294. dns_ip = "198.18.0.2";
  295. }
  296. var ut = Uri.http(url,'on');
  297. final body = json.encode({
  298. "http_port" : 0,
  299. "https_port" : 0,
  300. "socks_port" : 0,
  301. "bypass": "",
  302. "dns_ip": dns_ip,
  303. "dns_type":"",
  304. "address":"127.0.0.1"
  305. });
  306. await client.post(ut,body: body,headers: headers);
  307. } on http.ClientException catch (e) {
  308. // 处理客户端异常,例如没有网络连接
  309. print('Client Exception: ${e.message}');
  310. } on Exception catch (e) {
  311. // 处理其他类型的异常
  312. print('Exception: $e');
  313. }
  314. }
  315. Future<void> fetchSetProxyStop() async {
  316. try {
  317. var dnsIp = "";
  318. if(controllers.global.routeModesSelect.value == "tun"){
  319. dnsIp = "223.5.5.5";
  320. }
  321. var ut = Uri.http(url,'off');
  322. final body = json.encode({
  323. "bypass": "",
  324. "dns_ip": dnsIp
  325. });
  326. await client.post(ut,body:body,headers: headers);
  327. } on http.ClientException catch (e) {
  328. // 处理客户端异常,例如没有网络连接
  329. print('Client Exception: ${e.message}');
  330. } on Exception catch (e) {
  331. // 处理其他类型的异常
  332. print('Exception: $e');
  333. }
  334. }
  335. Future<void> fetchStop() async {
  336. try {
  337. var ut = Uri.http(url,'stop');
  338. await client.post(ut,headers:headers);
  339. coreStatus.value = RunningState.stoped;
  340. } on http.ClientException catch (e) {
  341. // 处理客户端异常,例如没有网络连接
  342. print('Client Exception: ${e.message}');
  343. } on Exception catch (e) {
  344. // 处理其他类型的异常
  345. print('Exception: $e');
  346. }
  347. }
  348. Future<void> install() async {
  349. await initConfig();
  350. final res = await runAsAdmin(Files.assetsClashService.path, ["-port","${controllers.config.config.value.servicePort}","stop", "uninstall", "install", "start"]);
  351. log.debug('install', res.stdout, res.stderr);
  352. if (res.exitCode != 0) throw res.stderr;
  353. await waitServiceStart();
  354. }
  355. Future<void> uninstall() async {
  356. final res = await runAsAdmin(Files.assetsClashService.path, ["stop", "uninstall"]);
  357. log.debug('uninstall', res.stdout, res.stderr);
  358. if (res.exitCode != 0) throw res.stderr;
  359. await waitServiceStop();
  360. }
  361. Future<void> serviceModeSwitch(bool open) async {
  362. if (serviceStatus.value == RunningState.running) await stopService();
  363. if (coreStatus.value == RunningState.running) await stopClashCore();
  364. try {
  365. controllers.global.updateMsg(open ? "安装服务" : "卸载服务");
  366. open ? await install() : await uninstall();
  367. } catch (e) {
  368. BotToast.showText(text: e.toString());
  369. }
  370. await startService();
  371. await fetchStartInit();
  372. }
  373. Future<bool> isPortAvailable(int port) async {
  374. try {
  375. // 尝试绑定一个socket到指定的端口
  376. var server = await ServerSocket.bind(InternetAddress.anyIPv4, port);
  377. // 成功绑定后立即关闭
  378. await server.close();
  379. // 如果成功绑定并关闭了服务器,那么端口是可用的
  380. return true;
  381. } on SocketException {
  382. // 如果绑定失败,端口被占用
  383. return false;
  384. }
  385. }
  386. Future<bool> startClashCore() async {
  387. final timeout = const Duration(seconds: 30); // 设置超时时间为30秒
  388. final checkInterval = const Duration(milliseconds: 200);
  389. var startTime = DateTime.now();
  390. await controllers.config.readClashCoreApi();
  391. try {
  392. controllers.global.updateMsg("启动内核---${controllers.config.config.value.selected}");
  393. if( controllers.config.config.value.selected == 'init_proxy.yaml'){
  394. controllers.global.updateMsg("启动内核初始化");
  395. } else {
  396. controllers.global.updateMsg("启动内核");
  397. }
  398. coreStatus.value = RunningState.starting;
  399. if(serviceMode.value == true){
  400. await fetchStart();
  401. }
  402. else
  403. {
  404. int? exitCode;
  405. 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);
  406. clashCoreProcess!.exitCode.then((code) => exitCode = code);
  407. if (exitCode != null && exitCode != 0) {
  408. // 非零退出代码通常表示错误
  409. controllers.global.updateMsg("启动内核错误,请重启点电脑测试");
  410. return false;
  411. }
  412. }
  413. //
  414. log.debug("api${controllers.config.clashCoreApiAddress.value}");
  415. controllers.core.setApi(controllers.config.clashCoreApiAddress.value, controllers.config.clashCoreApiSecret.value);
  416. while (DateTime.now().difference(startTime) < timeout) {
  417. try {
  418. controllers.global.updateMsg("等待内核启动..");
  419. final ret = await controllers.core.fetchHello();
  420. // 如果fetchHello成功,跳出循环
  421. break;
  422. } catch (_) {
  423. // 如果fetchHello失败,等待200毫秒后重试
  424. await Future.delayed(checkInterval);
  425. }
  426. }
  427. // 检查是否超时
  428. if (DateTime.now().difference(startTime) >= timeout) {
  429. // 如果超时,更新状态并抛出异常
  430. coreStatus.value = RunningState.error;
  431. controllers.global.updateMsg("内核启动超时,重新点击加速后尝试。");
  432. return false; // 提前退出函数
  433. }
  434. await controllers.core.updateConfig();
  435. coreStatus.value = RunningState.running;
  436. controllers.global.updateMsg("内核状态:${coreStatus.value == RunningState.running} ");
  437. return true;
  438. } catch (e) {
  439. log.error("core -- $e");
  440. controllers.global.updateMsg("启动内核错误");
  441. //controllers.global.handleApiError(e);
  442. // BotToast.showText(text: e.toString());
  443. coreStatus.value = RunningState.error;
  444. return false;
  445. }
  446. }
  447. Future<void> stopClashCore() async {
  448. coreStatus.value = RunningState.stopping;
  449. await controllers.global.closeProxy();
  450. if(serviceMode.value == true){
  451. await fetchStop();
  452. }
  453. else{
  454. if(clashCoreProcess != null){
  455. clashCoreProcess?.kill();
  456. }
  457. }
  458. killProcess(ClashName.name);
  459. // if (Platform.isMacOS &&
  460. // controllers.service.serviceMode.value &&
  461. // controllers.config.clashCoreTunEnable.value &&
  462. // controllers.config.clashCoreDns.isNotEmpty) {
  463. // await MacSystemDns.instance.set([]);
  464. // }
  465. //if (controllers.config.config.value.setSystemProxy) await SystemProxy.instance.set(SystemProxyConfig());
  466. //await stopClash();
  467. coreStatus.value = RunningState.stoped;
  468. }
  469. Future<void> initClashCoreConfig() async {
  470. controllers.config.config.value.selected = 'init_proxy.yaml';
  471. //await stopClashCore();
  472. await startClashCore();
  473. if (coreStatus.value == RunningState.error) {
  474. controllers.global.updateMsg("启动内核失败...");
  475. //BotToast.showText(text: '重启失败');
  476. } else {
  477. await controllers.core.updateVersion();
  478. controllers.global.updateMsg("启动内核成功...");
  479. //BotToast.showText(text: '重启成功');
  480. }
  481. }
  482. Future<void> stopClash() async {
  483. controllers.config.config.value.selected = 'init_proxy.yaml';
  484. if( coreStatus.value == RunningState.running){
  485. await controllers.config.readClashCoreApi();
  486. controllers.core.setApi(controllers.config.clashCoreApiAddress.value, controllers.config.clashCoreApiSecret.value);
  487. await controllers.core.changeConfig(path.join(Paths.config.path, controllers.config.config.value.selected));
  488. }
  489. }
  490. Future<void> reloadClashCore() async {
  491. try
  492. {
  493. controllers.config.config.value.selected = 'proxy.yaml';
  494. if( coreStatus.value == RunningState.running){
  495. controllers.global.updateMsg("切换配置...");
  496. await controllers.config.readClashCoreApi();
  497. //controllers.global.updateMsg("${controllers.config.clashCoreApiAddress.value}...");
  498. controllers.core.setApi(controllers.config.clashCoreApiAddress.value, controllers.config.clashCoreApiSecret.value);
  499. //controllers.global.updateMsg("setApi${controllers.config.clashCoreApiAddress.value}...");
  500. await controllers.core.changeConfig(path.join(Paths.config.path, controllers.config.config.value.selected));
  501. controllers.global.updateMsg("fetchReloadConfig${controllers.config.clashCoreApiAddress.value}...");
  502. }
  503. } catch(e){
  504. controllers.global.updateMsg("重新配置...");
  505. await controllers.config.readClashCoreApi();
  506. //controllers.global.updateMsg("${controllers.config.clashCoreApiAddress.value}...");
  507. controllers.core.setApi(controllers.config.clashCoreApiAddress.value, controllers.config.clashCoreApiSecret.value);
  508. //controllers.global.updateMsg("setApi${controllers.config.clashCoreApiAddress.value}...");
  509. await controllers.core.changeConfig(path.join(Paths.config.path, controllers.config.config.value.selected));
  510. }
  511. //BotToast.showText(text: '正在重启 Core ……');
  512. // controllers.global.updateMsg("停止内核...");
  513. // await stopClashCore();
  514. // await controllers.config.readClashCoreApi();
  515. // await startClashCore();
  516. // if (coreStatus.value == RunningState.error) {
  517. // controllers.global.updateMsg("启动内核失败...");
  518. // } else {
  519. // await controllers.core.updateVersion();
  520. // controllers.global.updateMsg("启动内核成功...");
  521. // }
  522. }
  523. Future<int> getFreePort() async {
  524. var server = await HttpServer.bind(InternetAddress.loopbackIPv4, 0);
  525. int port = server.port;
  526. await server.close();
  527. return port;
  528. }
  529. }