service.dart 9.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296
  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. bool get isRunning => serviceStatus.value == RunningState.running && coreStatus.value == RunningState.running;
  27. bool get isCanOperationService =>
  28. ![RunningState.starting, RunningState.stopping].contains(serviceStatus.value) &&
  29. ![RunningState.starting, RunningState.stopping].contains(coreStatus.value);
  30. bool get isCanOperationCore =>
  31. serviceStatus.value == RunningState.running && ![RunningState.starting, RunningState.stopping].contains(coreStatus.value);
  32. ServiceController(
  33. );
  34. Future<void> initConfig() async{
  35. _dio.options.baseUrl = 'http://127.0.0.1:${controllers.config.config.value.servicePort}';
  36. }
  37. Future<void> startTunService() async {
  38. try {
  39. while (true) {
  40. final data = await fetchInfo();
  41. if( data.mode != 'service-mode')
  42. {
  43. if (serviceStatus.value == RunningState.running) {
  44. await stopService();
  45. }
  46. await install();
  47. } else {
  48. break;
  49. }
  50. }
  51. } catch (e) {
  52. serviceStatus.value = RunningState.error;
  53. log.debug(e.toString());
  54. // BotToast.showText(text: e.toString());
  55. }
  56. }
  57. Future<void> startService() async {
  58. serviceStatus.value = RunningState.starting;
  59. if (Platform.isLinux) {
  60. await fixBinaryExecutePermissions(Files.assetsClashService);
  61. await fixBinaryExecutePermissions(Files.assetsClashCore);
  62. }
  63. try {
  64. final data = await fetchInfo();
  65. serviceMode.value = data.mode == 'service-mode';
  66. fetchStop();
  67. } catch (e) {
  68. //await install();
  69. await startUserModeService();
  70. if (serviceStatus.value == RunningState.error) return;
  71. }
  72. serviceStatus.value = RunningState.running;
  73. }
  74. Future<void> fixBinaryExecutePermissions(File file) async {
  75. final stat = await file.stat();
  76. // 0b001000000
  77. final has = (stat.mode & 64) == 64;
  78. if (has) return;
  79. await Process.run('chmod', ['+x', file.path]);
  80. }
  81. Future<void> startUserModeService() async {
  82. serviceMode.value = false;
  83. try {
  84. int? exitCode;
  85. clashServiceProcess = await Process.start(Files.assetsClashService.path, ['-port','${controllers.config.config.value.servicePort}','user-mode'], mode: ProcessStartMode.inheritStdio);
  86. clashServiceProcess!.exitCode.then((code) => exitCode = code);
  87. while (true) {
  88. await Future.delayed(const Duration(milliseconds: 200));
  89. if (exitCode == 101) {
  90. BotToast.showText(text: 'clash-service exit with code: $exitCode,After 10 seconds, try to restart');
  91. log.error('After 10 seconds, try to restart');
  92. await Future.delayed(const Duration(seconds: 10));
  93. await startUserModeService();
  94. break;
  95. } else if (exitCode != null) {
  96. serviceStatus.value = RunningState.error;
  97. break;
  98. }
  99. try {
  100. await _dio.post('/info');
  101. break;
  102. } catch (_) {}
  103. }
  104. } catch (e) {
  105. serviceStatus.value = RunningState.error;
  106. BotToast.showText(text: e.toString());
  107. }
  108. }
  109. Future<void> stopService() async {
  110. serviceStatus.value = RunningState.stopping;
  111. if (coreStatus.value == RunningState.running) await stopClashCore();
  112. if (!serviceMode.value) {
  113. if (clashServiceProcess != null) {
  114. clashServiceProcess!.kill();
  115. clashServiceProcess = null;
  116. } else if (kDebugMode) {
  117. await killProcess(path.basename(Files.assetsClashService.path));
  118. }
  119. }
  120. serviceStatus.value = RunningState.stoped;
  121. }
  122. // for macos
  123. Future<void> waitServiceStart() async {
  124. while (true) {
  125. await Future.delayed(const Duration(milliseconds: 100));
  126. try {
  127. await _dio.post('/info');
  128. break;
  129. } catch (_) {}
  130. }
  131. }
  132. // for windows
  133. Future<void> waitServiceStop() async {
  134. while (true) {
  135. await Future.delayed(const Duration(milliseconds: 100));
  136. try {
  137. await _dio.post('/info');
  138. } catch (e) {
  139. break;
  140. }
  141. }
  142. }
  143. Future<ClashServiceInfo> fetchInfo() async {
  144. final res = await _dio.post('/info');
  145. return ClashServiceInfo.fromJson(res.data);
  146. }
  147. IOWebSocketChannel fetchLogWs() {
  148. return IOWebSocketChannel.connect(Uri.parse('ws://127.0.0.1:${controllers.config.config.value.servicePort}/logs'), headers: headers);
  149. }
  150. Future<void> fetchStart(String name) async {
  151. await fetchStop();
  152. final res = await _dio.post<String>('/start', data: {
  153. "args": ['-d', Paths.config.path, '-f', path.join(Paths.config.path, name)]
  154. });
  155. if (json.decode(res.data!)["code"] != 0) throw json.decode(res.data!)["msg"];
  156. }
  157. Future<void> fetchStop() async {
  158. try {
  159. await _dio.post('/stop');
  160. } catch (e) {
  161. return;
  162. }
  163. }
  164. Future<void> install() async {
  165. final res = await runAsAdmin(Files.assetsClashService.path, ["-port","${controllers.config.config.value.servicePort}","stop", "uninstall", "install", "start"]);
  166. await initConfig();
  167. log.debug('install', res.stdout, res.stderr);
  168. if (res.exitCode != 0) throw res.stderr;
  169. await waitServiceStart();
  170. }
  171. Future<void> uninstall() async {
  172. final res = await runAsAdmin(Files.assetsClashService.path, ["stop", "uninstall"]);
  173. log.debug('uninstall', res.stdout, res.stderr);
  174. if (res.exitCode != 0) throw res.stderr;
  175. await waitServiceStop();
  176. }
  177. Future<void> serviceModeSwitch(bool open) async {
  178. if (serviceStatus.value == RunningState.running) await stopService();
  179. try {
  180. open ? await install() : await uninstall();
  181. } catch (e) {
  182. BotToast.showText(text: e.toString());
  183. }
  184. await startService();
  185. //await startClashCore();
  186. }
  187. Future<void> startClashCore() async {
  188. try {
  189. coreStatus.value = RunningState.starting;
  190. await fetchStart(controllers.config.config.value.selected);
  191. controllers.core.setApi(controllers.config.clashCoreApiAddress.value, controllers.config.clashCoreApiSecret.value);
  192. while (true) {
  193. await Future.delayed(const Duration(milliseconds: 200));
  194. final info = await fetchInfo();
  195. print("core --- info $info");
  196. if (info.status == 'running') {
  197. try {
  198. await controllers.core.fetchHello();
  199. break;
  200. } catch (_) {}
  201. } else {
  202. throw 'clash-core start error';
  203. }
  204. }
  205. await controllers.core.updateConfig();
  206. if (Platform.isMacOS &&
  207. controllers.service.serviceMode.value &&
  208. controllers.config.clashCoreTunEnable.value &&
  209. controllers.config.clashCoreDns.isNotEmpty) {
  210. await MacSystemDns.instance.set([controllers.config.clashCoreDns.value]);
  211. }
  212. if (controllers.config.config.value.setSystemProxy) await SystemProxy.instance.set(controllers.core.proxyConfig);
  213. coreStatus.value = RunningState.running;
  214. } catch (e) {
  215. log.error("core -- $e");
  216. // BotToast.showText(text: e.toString());
  217. coreStatus.value = RunningState.error;
  218. }
  219. }
  220. Future<void> stopClashCore() async {
  221. coreStatus.value = RunningState.stopping;
  222. if (Platform.isMacOS &&
  223. controllers.service.serviceMode.value &&
  224. controllers.config.clashCoreTunEnable.value &&
  225. controllers.config.clashCoreDns.isNotEmpty) {
  226. await MacSystemDns.instance.set([]);
  227. }
  228. if (controllers.config.config.value.setSystemProxy) await SystemProxy.instance.set(SystemProxyConfig());
  229. await fetchStop();
  230. coreStatus.value = RunningState.stoped;
  231. }
  232. Future<void> initClashCoreConfig() async {
  233. //await stopClashCore();
  234. await startClashCore();
  235. if (coreStatus.value == RunningState.error) {
  236. //BotToast.showText(text: '重启失败');
  237. } else {
  238. await controllers.core.updateVersion();
  239. //BotToast.showText(text: '重启成功');
  240. }
  241. }
  242. Future<void> stopClash() async {
  243. controllers.config.config.value.selected = 'init_proxy.yaml';
  244. if( coreStatus.value == RunningState.running){
  245. await controllers.core.fetchReloadConfig({"path": path.join(Paths.config.path, controllers.config.config.value.selected),"payload": ""});
  246. }
  247. }
  248. Future<void> reloadClashCore() async {
  249. controllers.config.config.value.selected = 'proxy.yaml';
  250. if( coreStatus.value == RunningState.running){
  251. await controllers.core.fetchReloadConfig({"path": path.join(Paths.config.path, controllers.config.config.value.selected),"payload":""});
  252. }
  253. }
  254. Future<int> getFreePort() async {
  255. var server = await HttpServer.bind(InternetAddress.loopbackIPv4, 0);
  256. int port = server.port;
  257. await server.close();
  258. return port;
  259. }
  260. }