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. 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. if (coreStatus.value == RunningState.running) await stopClashCore();
  206. try {
  207. controllers.global.updateMsg(open ? "安装服务" : "卸载服务");
  208. open ? await install() : await uninstall();
  209. } catch (e) {
  210. BotToast.showText(text: e.toString());
  211. }
  212. if(open){
  213. await startService();
  214. await startClashCore();
  215. }else{
  216. serviceMode.value = false;
  217. await startClashCore();
  218. }
  219. //await startClashCore();
  220. }
  221. Future<bool> isPortAvailable(int port) async {
  222. try {
  223. // 尝试绑定一个socket到指定的端口
  224. var server = await ServerSocket.bind(InternetAddress.anyIPv4, port);
  225. // 成功绑定后立即关闭
  226. await server.close();
  227. // 如果成功绑定并关闭了服务器,那么端口是可用的
  228. return true;
  229. } on SocketException {
  230. // 如果绑定失败,端口被占用
  231. return false;
  232. }
  233. }
  234. Future<bool> startClashCore() async {
  235. final timeout = const Duration(seconds: 30); // 设置超时时间为30秒
  236. final checkInterval = const Duration(milliseconds: 200);
  237. var startTime = DateTime.now();
  238. await controllers.config.readClashCoreApi();
  239. // bool isAvailable = await isPortAvailable(controllers.config.mixedPort.value);
  240. // if (!isAvailable) {
  241. // controllers.global.updateMsg("端口 被占用,启动内核失败,等待几秒后重新测试。");
  242. // return false; // 端口被占用,返回失败
  243. // }
  244. // isAvailable = await isPortAvailable(controllers.config.ApiAddressPort.value);
  245. // if (!isAvailable) {
  246. // controllers.global.updateMsg("端口 被占用,启动内核失败,等待几秒后重新测试。");
  247. // return false; // 端口被占用,返回失败
  248. // }
  249. try {
  250. controllers.global.updateMsg("启动内核---${controllers.config.config.value.selected}");
  251. if( controllers.config.config.value.selected == 'init_proxy.yaml'){
  252. controllers.global.updateMsg("启动内核初始化");
  253. } else {
  254. controllers.global.updateMsg("启动内核");
  255. }
  256. coreStatus.value = RunningState.starting;
  257. if(serviceMode.value == true){
  258. await fetchStart(controllers.config.config.value.selected);
  259. }
  260. else
  261. {
  262. int? exitCode;
  263. 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);
  264. clashCoreProcess!.exitCode.then((code) => exitCode = code);
  265. if (exitCode != null && exitCode != 0) {
  266. // 非零退出代码通常表示错误
  267. controllers.global.updateMsg("启动内核错误,请重启点电脑测试");
  268. return false;
  269. }
  270. }
  271. //
  272. log.debug("api${controllers.config.clashCoreApiAddress.value}");
  273. controllers.core.setApi(controllers.config.clashCoreApiAddress.value, controllers.config.clashCoreApiSecret.value);
  274. while (DateTime.now().difference(startTime) < timeout) {
  275. try {
  276. controllers.global.updateMsg("等待内核启动..");
  277. final ret = await controllers.core.fetchHello();
  278. // 如果fetchHello成功,跳出循环
  279. break;
  280. } catch (_) {
  281. // 如果fetchHello失败,等待200毫秒后重试
  282. await Future.delayed(checkInterval);
  283. }
  284. }
  285. // 检查是否超时
  286. if (DateTime.now().difference(startTime) >= timeout) {
  287. // 如果超时,更新状态并抛出异常
  288. coreStatus.value = RunningState.error;
  289. controllers.global.updateMsg("内核启动超时,重新点击加速后尝试。");
  290. return false; // 提前退出函数
  291. }
  292. await controllers.core.updateConfig();
  293. coreStatus.value = RunningState.running;
  294. controllers.global.updateMsg("内核状态:${coreStatus.value == RunningState.running} ");
  295. controllers.global.updateDate();
  296. return true;
  297. } catch (e) {
  298. log.error("core -- $e");
  299. controllers.global.updateMsg("启动内核错误");
  300. //controllers.global.handleApiError(e);
  301. // BotToast.showText(text: e.toString());
  302. coreStatus.value = RunningState.error;
  303. return false;
  304. }
  305. }
  306. Future<void> stopClashCore() async {
  307. coreStatus.value = RunningState.stopping;
  308. await controllers.global.closeProxy();
  309. if(serviceMode.value == true){
  310. await fetchStop();
  311. }
  312. else{
  313. if(clashCoreProcess != null){
  314. clashCoreProcess?.kill();
  315. }
  316. }
  317. killProcess(ClashName.name);
  318. // if (Platform.isMacOS &&
  319. // controllers.service.serviceMode.value &&
  320. // controllers.config.clashCoreTunEnable.value &&
  321. // controllers.config.clashCoreDns.isNotEmpty) {
  322. // await MacSystemDns.instance.set([]);
  323. // }
  324. //if (controllers.config.config.value.setSystemProxy) await SystemProxy.instance.set(SystemProxyConfig());
  325. //await stopClash();
  326. coreStatus.value = RunningState.stoped;
  327. }
  328. Future<void> initClashCoreConfig() async {
  329. controllers.config.config.value.selected = 'init_proxy.yaml';
  330. //await stopClashCore();
  331. await startClashCore();
  332. if (coreStatus.value == RunningState.error) {
  333. controllers.global.updateMsg("启动内核失败...");
  334. //BotToast.showText(text: '重启失败');
  335. } else {
  336. await controllers.core.updateVersion();
  337. controllers.global.updateMsg("启动内核成功...");
  338. //BotToast.showText(text: '重启成功');
  339. }
  340. }
  341. Future<void> stopClash() async {
  342. controllers.config.config.value.selected = 'init_proxy.yaml';
  343. if( coreStatus.value == RunningState.running){
  344. await controllers.config.readClashCoreApi();
  345. controllers.core.setApi(controllers.config.clashCoreApiAddress.value, controllers.config.clashCoreApiSecret.value);
  346. await controllers.core.changeConfig(path.join(Paths.config.path, controllers.config.config.value.selected));
  347. }
  348. }
  349. Future<void> reloadClashCore() async {
  350. try
  351. {
  352. controllers.config.config.value.selected = 'proxy.yaml';
  353. if( coreStatus.value == RunningState.running){
  354. controllers.global.updateMsg("切换配置...");
  355. await controllers.config.readClashCoreApi();
  356. //controllers.global.updateMsg("${controllers.config.clashCoreApiAddress.value}...");
  357. controllers.core.setApi(controllers.config.clashCoreApiAddress.value, controllers.config.clashCoreApiSecret.value);
  358. //controllers.global.updateMsg("setApi${controllers.config.clashCoreApiAddress.value}...");
  359. await controllers.core.changeConfig(path.join(Paths.config.path, controllers.config.config.value.selected));
  360. controllers.global.updateMsg("fetchReloadConfig${controllers.config.clashCoreApiAddress.value}...");
  361. }
  362. } catch(e){
  363. controllers.global.updateMsg("重新配置...");
  364. await controllers.config.readClashCoreApi();
  365. //controllers.global.updateMsg("${controllers.config.clashCoreApiAddress.value}...");
  366. controllers.core.setApi(controllers.config.clashCoreApiAddress.value, controllers.config.clashCoreApiSecret.value);
  367. //controllers.global.updateMsg("setApi${controllers.config.clashCoreApiAddress.value}...");
  368. await controllers.core.changeConfig(path.join(Paths.config.path, controllers.config.config.value.selected));
  369. }
  370. //BotToast.showText(text: '正在重启 Core ……');
  371. // controllers.global.updateMsg("停止内核...");
  372. // await stopClashCore();
  373. // await controllers.config.readClashCoreApi();
  374. // await startClashCore();
  375. // if (coreStatus.value == RunningState.error) {
  376. // controllers.global.updateMsg("启动内核失败...");
  377. // } else {
  378. // await controllers.core.updateVersion();
  379. // controllers.global.updateMsg("启动内核成功...");
  380. // }
  381. }
  382. Future<int> getFreePort() async {
  383. var server = await HttpServer.bind(InternetAddress.loopbackIPv4, 0);
  384. int port = server.port;
  385. await server.close();
  386. return port;
  387. }
  388. }