From d2c70b077bf9a5c91b013824f49c5dd6cf24f3b5 Mon Sep 17 00:00:00 2001 From: Levi Date: Tue, 7 May 2024 19:19:57 +0800 Subject: [PATCH] feat: check API port is in use (#524) --- .../app/controllers/app_controller.dart | 10 ++-- .../modules/setting/views/setting_view.dart | 51 +++++++++++++++++-- ui/flutter/lib/i18n/langs/en_us.dart | 1 + ui/flutter/lib/i18n/langs/zh_cn.dart | 1 + ui/flutter/lib/i18n/langs/zh_tw.dart | 1 + 5 files changed, 56 insertions(+), 8 deletions(-) diff --git a/ui/flutter/lib/app/modules/app/controllers/app_controller.dart b/ui/flutter/lib/app/modules/app/controllers/app_controller.dart index 2c3555f81..f5551ae36 100644 --- a/ui/flutter/lib/app/modules/app/controllers/app_controller.dart +++ b/ui/flutter/lib/app/modules/app/controllers/app_controller.dart @@ -274,7 +274,9 @@ class AppController extends GetxController with WindowListener, TrayListener { } Future _toCreate(Uri uri) async { - final path = (uri.scheme == "magnet" || uri.scheme == "http" || uri.scheme == "https") + final path = (uri.scheme == "magnet" || + uri.scheme == "http" || + uri.scheme == "https") ? uri.toString() : (await toFile(uri.toString())).path; await Get.rootDelegate.offAndToNamed(Routes.CREATE, arguments: path); @@ -305,15 +307,16 @@ class AppController extends GetxController with WindowListener, TrayListener { return _defaultStartConfig!; } - Future loadStartConfig() async { + Future loadStartConfig() async { final defaultCfg = await _initDefaultStartConfig(); final saveCfg = Database.instance.getStartConfig(); startConfig.value.network = saveCfg?.network ?? defaultCfg.network; startConfig.value.address = saveCfg?.address ?? defaultCfg.address; startConfig.value.apiToken = saveCfg?.apiToken ?? defaultCfg.apiToken; + return startConfig.value; } - Future loadDownloaderConfig() async { + Future loadDownloaderConfig() async { try { downloaderConfig.value = await getConfig(); } catch (e) { @@ -321,6 +324,7 @@ class AppController extends GetxController with WindowListener, TrayListener { downloaderConfig.value = DownloaderConfig(); } await _initDownloaderConfig(); + return downloaderConfig.value; } Future trackerUpdate() async { diff --git a/ui/flutter/lib/app/modules/setting/views/setting_view.dart b/ui/flutter/lib/app/modules/setting/views/setting_view.dart index d52a5a56f..03a20a34c 100644 --- a/ui/flutter/lib/app/modules/setting/views/setting_view.dart +++ b/ui/flutter/lib/app/modules/setting/views/setting_view.dart @@ -1,4 +1,5 @@ import 'dart:async'; +import 'dart:io'; import 'package:badges/badges.dart' as badges; import 'package:flutter/material.dart'; @@ -8,6 +9,7 @@ import 'package:intl/intl.dart'; import 'package:url_launcher/url_launcher.dart'; import '../../../../api/model/downloader_config.dart'; +import '../../../../database/database.dart'; import '../../../../i18n/message.dart'; import '../../../../util/input_formatter.dart'; import '../../../../util/locale_manager.dart'; @@ -34,13 +36,22 @@ class SettingView extends GetView { final startCfg = appController.startConfig; Timer? timer; - debounceSave({bool needRestart = false}) { - var completer = Completer(); + Future debounceSave( + {Future Function()? check, bool needRestart = false}) { + var completer = Completer(); timer?.cancel(); - timer = Timer(const Duration(milliseconds: 1000), () { + timer = Timer(const Duration(milliseconds: 1000), () async { + if (check != null) { + final checkResult = await check(); + if (checkResult.isNotEmpty) { + showErrorMessage(checkResult); + completer.complete(false); + return; + } + } appController .saveConfig() - .then(completer.complete) + .then((_) => completer.complete(true)) .onError(completer.completeError); if (needRestart) { showMessage('tip'.tr, 'effectAfterRestart'.tr); @@ -639,11 +650,41 @@ class SettingView extends GetView { final ipController = TextEditingController(text: ip); final portController = TextEditingController(text: port); updateAddress() async { + if (ipController.text.isEmpty || portController.text.isEmpty) { + return; + } final newAddress = '${ipController.text}:${portController.text}'; if (newAddress != startCfg.value.address) { startCfg.value.address = newAddress; - await debounceSave(needRestart: true); + final saved = await debounceSave( + check: () async { + // Check if address already in use + final configIp = ipController.text; + final configPort = int.parse(portController.text); + if (configPort == 0) { + return ''; + } + try { + final socket = await Socket.connect(configIp, configPort, + timeout: const Duration(seconds: 3)); + socket.close(); + return 'portInUse' + .trParams({'port': configPort.toString()}); + } catch (e) { + return ''; + } + }, + needRestart: true); + + // If save failed, restore the old address + if (!saved) { + final oldAddress = + (await appController.loadStartConfig()).address; + startCfg.update((val) async { + val!.address = oldAddress; + }); + } } } diff --git a/ui/flutter/lib/i18n/langs/en_us.dart b/ui/flutter/lib/i18n/langs/en_us.dart index 22aa8a75e..d33ebc443 100644 --- a/ui/flutter/lib/i18n/langs/en_us.dart +++ b/ui/flutter/lib/i18n/langs/en_us.dart @@ -58,6 +58,7 @@ const enUS = { 'apiToken': 'API Token', 'notSet': 'NS', 'set': 'SET', + 'portInUse': 'Port [@port] is in use, please change the port', 'effectAfterRestart': 'Effect after restart', 'startAll': 'Start All', 'pauseAll': 'Pause All', diff --git a/ui/flutter/lib/i18n/langs/zh_cn.dart b/ui/flutter/lib/i18n/langs/zh_cn.dart index 188d3eab6..3c409143e 100644 --- a/ui/flutter/lib/i18n/langs/zh_cn.dart +++ b/ui/flutter/lib/i18n/langs/zh_cn.dart @@ -56,6 +56,7 @@ const zhCN = { 'apiToken': '接口令牌', 'notSet': '未设置', 'set': '已设置', + 'portInUse': '端口[@port]已被占用,请更换端口', 'effectAfterRestart': '此配置项将在重启应用后生效', 'startAll': '全部开始', 'pauseAll': '全部暂停', diff --git a/ui/flutter/lib/i18n/langs/zh_tw.dart b/ui/flutter/lib/i18n/langs/zh_tw.dart index 044227f26..a1c9cfb19 100644 --- a/ui/flutter/lib/i18n/langs/zh_tw.dart +++ b/ui/flutter/lib/i18n/langs/zh_tw.dart @@ -56,6 +56,7 @@ const zhTW = { 'apiToken': 'API令牌', 'notSet': '未設定', 'set': '已設定', + 'portInUse': '端口[@port]已被占用,請更換端口', 'effectAfterRestart': '重啟後生效', 'startAll': '全部開始', 'pauseAll': '全部暫停',