From 194f298346f4d12c6a042014cf595154ca1f4f92 Mon Sep 17 00:00:00 2001 From: lbl8603 <49143209+lbl8603@users.noreply.github.com> Date: Wed, 3 Jul 2024 22:09:46 +0800 Subject: [PATCH] =?UTF-8?q?=E6=94=AF=E6=8C=81=E7=BB=84=E5=A4=9A=E4=B8=AA?= =?UTF-8?q?=E7=BD=91=E7=BB=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- lib/connected_page.dart | 40 ++-- lib/main.dart | 238 +++++++++++++-------- lib/vnt/{vnt_api.dart => vnt_manager.dart} | 185 ++++++++-------- 3 files changed, 269 insertions(+), 194 deletions(-) rename lib/vnt/{vnt_api.dart => vnt_manager.dart} (60%) diff --git a/lib/connected_page.dart b/lib/connected_page.dart index 7e666ae..bfc3b08 100644 --- a/lib/connected_page.dart +++ b/lib/connected_page.dart @@ -3,13 +3,16 @@ import 'connect_log.dart'; import 'network_config.dart'; import 'custom_app_bar.dart'; import 'src/rust/api/vnt_api.dart'; -import 'vnt/vnt_api.dart'; import 'package:json2yaml/json2yaml.dart'; +import 'vnt/vnt_manager.dart'; + class ConnectDetailPage extends StatefulWidget { final NetworkConfig config; + final VntBox vntBox; - const ConnectDetailPage({super.key, required this.config}); + const ConnectDetailPage( + {super.key, required this.config, required this.vntBox}); @override _ConnectDetailPageState createState() => _ConnectDetailPageState(); @@ -48,9 +51,9 @@ class _ConnectDetailPageState extends State { } List> _fetchDeviceList() { - var list = VntApiUtils.peerDeviceList(); + var list = widget.vntBox.peerDeviceList(); return list.map((item) { - var route = VntApiUtils.route(item.virtualIp); + var route = widget.vntBox.route(item.virtualIp); var p2pRelay = ''; var rt = ''; if (route != null) { @@ -68,7 +71,7 @@ class _ConnectDetailPageState extends State { } List> _fetchRouteList() { - var list = VntApiUtils.routeList(); + var list = widget.vntBox.routeList(); List> rs = []; for ((String, List) x in list) { for (var route in x.$2) { @@ -98,7 +101,7 @@ class _ConnectDetailPageState extends State { Navigator.push( context, MaterialPageRoute( - builder: (context) => ConnectLogPage(), + builder: (context) => LogPage(), ), ); }, @@ -130,9 +133,10 @@ class _ConnectDetailPageState extends State { title: const Text('是否断开连接'), actions: [ TextButton( - onPressed: () { + onPressed: () async { Navigator.of(context).pop(); Navigator.pop(context, true); + await vntManager.remove(widget.config.itemKey); }, style: TextButton.styleFrom( foregroundColor: Colors.white, @@ -141,7 +145,7 @@ class _ConnectDetailPageState extends State { child: const Text('断开连接'), ), TextButton( - onPressed: () { + onPressed: () async { Navigator.of(context).pop(); }, child: const Icon(Icons.close), @@ -179,13 +183,16 @@ class _ConnectDetailPageState extends State { List _buildWidgetOptions() { return [ - DeviceList(deviceList: deviceList), + DeviceList( + deviceList: deviceList, + vntBox: widget.vntBox, + ), RouteList(routeList: routeList), ]; } void _showConfigDialog() { - var conf = json2yaml(VntApiUtils.getNetConfig()!.toJsonSimple()); + var conf = json2yaml(widget.vntBox.getNetConfig()!.toJsonSimple()); showDialog( context: context, builder: (BuildContext context) { @@ -206,10 +213,10 @@ class _ConnectDetailPageState extends State { } void _showCurrentDeviceDialog() { - Map map = VntApiUtils.currentDevice(); + Map map = widget.vntBox.currentDevice(); map.addEntries({ - "upStream": VntApiUtils.upStream(), - "downStream": VntApiUtils.downStream(), + "upStream": widget.vntBox.upStream(), + "downStream": widget.vntBox.downStream(), }.entries); var info = json2yaml(map); showDialog( @@ -233,9 +240,10 @@ class _ConnectDetailPageState extends State { } class DeviceList extends StatelessWidget { + final VntBox vntBox; final List> deviceList; - const DeviceList({super.key, required this.deviceList}); + const DeviceList({super.key, required this.deviceList, required this.vntBox}); @override Widget build(BuildContext context) { @@ -271,8 +279,8 @@ class DeviceList extends StatelessWidget { } void _showPeerInfoDialog(BuildContext context, String ip, String name) { - var natInfo = VntApiUtils.peerNatInfo(ip); - var allRouteList = VntApiUtils.routeList(); + var natInfo = vntBox.peerNatInfo(ip); + var allRouteList = vntBox.routeList(); List? routeList; for ((String, List) routes in allRouteList) { if (routes.$1 == ip) { diff --git a/lib/main.dart b/lib/main.dart index d78aab1..28d96e9 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -1,6 +1,7 @@ import 'dart:isolate'; import 'package:flutter/material.dart'; import 'package:json2yaml/json2yaml.dart'; +import 'package:vnt_app/vnt/vnt_manager.dart'; import 'connected_page.dart'; import 'network_config_input_page.dart'; import 'custom_app_bar.dart'; @@ -12,7 +13,6 @@ import 'settings_page.dart'; import 'src/rust/api/vnt_api.dart'; import 'widgets/color_changing_button.dart'; import 'package:vnt_app/src/rust/frb_generated.dart'; -import 'vnt/vnt_api.dart'; import 'dart:io'; import 'package:flutter/services.dart' show rootBundle; import 'package:system_tray/system_tray.dart'; @@ -28,6 +28,11 @@ Future main() async { } catch (e) { debugPrint('copyLogConfig catch $e'); } + try { + await copyAppropriateDll(); + } catch (e) { + debugPrint('copyAppropriateDll catch $e'); + } await RustLib.init(); // 初始化Rust库 if (Platform.isWindows || Platform.isMacOS || Platform.isLinux) { await windowManager.ensureInitialized(); @@ -116,16 +121,15 @@ class HomePage extends StatefulWidget { class _HomePageState extends State with WindowListener { final DataPersistence _dataPersistence = DataPersistence(); + // 所有网络配置 List _configs = []; - bool _connected = !VntApiUtils.isClosed(); - NetworkConfig? connectedConfig; + bool _connected = vntManager.hasConnection(); bool rememberChoice = false; @override void initState() { super.initState(); if (Platform.isWindows || Platform.isMacOS || Platform.isLinux) { - copyAppropriateDll(); initSystemTray(); DataPersistence().loadCloseApp().then((isClose) { if (!(isClose ?? false)) { @@ -141,6 +145,12 @@ class _HomePageState extends State with WindowListener { }); } + void loadConnectState() { + setState(() { + _connected = vntManager.hasConnection(); + }); + } + void loadConnect() async { var connectItemKey = await _dataPersistence.loadAutoConnect(); if (connectItemKey != null && connectItemKey.isNotEmpty) { @@ -243,10 +253,10 @@ class _HomePageState extends State with WindowListener { } void _addOrEditConfig(NetworkConfig? config, int index) async { - if (_connected && config != null && connectedConfig != null) { - if (config.itemKey == connectedConfig?.itemKey) { + if (config != null) { + if (vntManager.hasConnectionItem(config.itemKey)) { ScaffoldMessenger.of(context).showSnackBar( - const SnackBar(content: Text('请先断开连接再编辑配置')), + const SnackBar(content: Text('已连接的配置不能编辑')), ); return; } @@ -271,37 +281,74 @@ class _HomePageState extends State with WindowListener { } void _connect(NetworkConfig config) { - if (_connected) { - // 不能重复连接 - showDialog( - context: context, - builder: (BuildContext context) { - return AlertDialog( - title: Text('连接配置项[${connectedConfig?.configName}]'), - content: const Text("已经建立了连接,如需多开请开多个窗口,并注意网段、虚拟网卡不要冲突"), - actions: [ - TextButton( - onPressed: () { - Navigator.of(context).pop(); - if (connectedConfig != null) { - connectDetailPage(connectedConfig!); - } - }, - child: const Text('查看连接'), - ), - TextButton( - onPressed: () { - Navigator.of(context).pop(); - }, - child: const Icon(Icons.close), - ), - ], - ); - }, - ); + if (vntManager.hasConnectionItem(config.itemKey)) { + connectDetailPage(config); + return; + } + if (vntManager.hasConnection()) { + if (!vntManager.supportMultiple()) { + var lastConnectedConfig = vntManager.getOne()?.networkConfig; + // 不能重复连接 + showDialog( + context: context, + builder: (BuildContext context) { + return AlertDialog( + title: Text('连接配置项[${lastConnectedConfig?.configName}]'), + content: const Text("已经建立了连接"), + actions: [ + TextButton( + onPressed: () { + Navigator.of(context).pop(); + if (lastConnectedConfig != null) { + connectDetailPage(lastConnectedConfig); + } + }, + child: const Text('查看连接'), + ), + TextButton( + onPressed: () { + Navigator.of(context).pop(); + }, + child: const Icon(Icons.close), + ), + ], + ); + }, + ); + } else { + // 可以多开,但是要提醒 + showDialog( + context: context, + builder: (BuildContext context) { + return AlertDialog( + title: Text('已经建立了${vntManager.size()}个连接,是否要继续组网'), + content: const Text("注意虚拟IP、虚拟网段、网卡名称均不能冲突"), + actions: [ + TextButton( + onPressed: () { + Navigator.of(context).pop(); + connectVntAndSetBackground(config); + }, + child: const Text('组网'), + ), + TextButton( + onPressed: () { + Navigator.of(context).pop(); + }, + child: const Icon(Icons.close), + ), + ], + ); + }, + ); + } + return; } - connectedConfig = config; + connectVntAndSetBackground(config); + } + + void connectVntAndSetBackground(NetworkConfig config) { showDialog( context: context, barrierDismissible: false, @@ -316,7 +363,7 @@ class _HomePageState extends State with WindowListener { const SizedBox(height: 20), // 添加一些垂直间距 ElevatedButton( onPressed: () { - VntApiUtils.close(); + vntManager.remove(config.itemKey); }, child: const Text('取消'), ), @@ -333,6 +380,8 @@ class _HomePageState extends State with WindowListener { void _connectVnt(NetworkConfig config) async { var onece = true; ReceivePort receivePort = ReceivePort(); + var itemKey = config.itemKey; + var configName = config.configName; receivePort.listen((msg) async { if (msg is String) { if (msg == 'success') { @@ -342,13 +391,13 @@ class _HomePageState extends State with WindowListener { connectDetailPage(config); } } else if (msg == 'stop') { - _closeVnt(); + _closeVnt(itemKey); if (onece) { onece = false; Navigator.of(context).pop(); } ScaffoldMessenger.of(context).showSnackBar( - const SnackBar(content: Text('VNT服务停止')), + SnackBar(content: Text('VNT服务停止[$configName]')), ); } } else if (msg is RustErrorInfo) { @@ -356,63 +405,65 @@ class _HomePageState extends State with WindowListener { //没成功就失败的,就断开不重试了 onece = false; Navigator.of(context).pop(); - _closeVnt(); + _closeVnt(itemKey); } switch (msg.code) { case RustErrorType.tokenError: - _closeVnt(); + _closeVnt(itemKey); ScaffoldMessenger.of(context).showSnackBar( - const SnackBar(content: Text('token错误')), + SnackBar(content: Text('token错误[$configName]')), ); break; case RustErrorType.disconnect: //断开连接 break; case RustErrorType.addressExhausted: - _closeVnt(); + _closeVnt(itemKey); ScaffoldMessenger.of(context).showSnackBar( - const SnackBar(content: Text('IP地址用尽')), + SnackBar(content: Text('IP地址用尽[$configName]')), ); break; case RustErrorType.ipAlreadyExists: - _closeVnt(); + _closeVnt(itemKey); ScaffoldMessenger.of(context).showSnackBar( - const SnackBar(content: Text('和其他设备的虚拟IP冲突')), + SnackBar(content: Text('和其他设备的虚拟IP冲突[$configName]')), ); break; case RustErrorType.invalidIp: - _closeVnt(); + _closeVnt(itemKey); ScaffoldMessenger.of(context).showSnackBar( - const SnackBar(content: Text('虚拟IP地址无效')), + SnackBar(content: Text('虚拟IP地址无效[$configName]')), ); break; case RustErrorType.localIpExists: - _closeVnt(); + _closeVnt(itemKey); ScaffoldMessenger.of(context).showSnackBar( - const SnackBar(content: Text('虚拟IP地址和本地IP冲突')), + SnackBar(content: Text('虚拟IP地址和本地IP冲突[$configName]')), ); break; default: - _closeVnt(); + _closeVnt(itemKey); ScaffoldMessenger.of(context).showSnackBar( - SnackBar(content: Text('未知错误 ${msg.msg}')), + SnackBar(content: Text('未知错误 ${msg.msg} [$configName]')), ); } } else if (msg is RustConnectInfo) { if (onece && msg.count > BigInt.from(60)) { onece = false; Navigator.of(context).pop(); - _closeVnt(); + _closeVnt(itemKey); ScaffoldMessenger.of(context).showSnackBar( - SnackBar(content: Text('连接超时 ${msg.address}')), + SnackBar(content: Text('连接超时 ${msg.address} [$configName]')), ); } } }); try { - await VntApiUtils.open(config, receivePort.sendPort); + await vntManager.create(config, receivePort.sendPort); } catch (e) { debugPrint('dart catch e: $e'); + if (!mounted) return; + Navigator.of(context).pop(); var msg = e.toString(); ScaffoldMessenger.of(context).showSnackBar( @@ -428,26 +479,22 @@ class _HomePageState extends State with WindowListener { } void connectDetailPage(NetworkConfig config) async { - final result = await Navigator.push( + var vntBox = vntManager.get(config.itemKey); + if (vntBox == null) { + return; + } + await Navigator.push( context, MaterialPageRoute( - builder: (context) => ConnectDetailPage(config: config), + builder: (context) => ConnectDetailPage(config: config, vntBox: vntBox), ), ); - if (result == null || (result is bool && !result)) { - setState(() { - _connected = true; - }); - } else { - _closeVnt(); - } + loadConnectState(); } - void _closeVnt() { - VntApiUtils.close(); - setState(() { - _connected = false; - }); + void _closeVnt(String itemKey) { + vntManager.remove(itemKey); + loadConnectState(); } void _seeConnected() { @@ -455,28 +502,29 @@ class _HomePageState extends State with WindowListener { context: context, builder: (BuildContext context) { return AlertDialog( - title: Text('连接配置项[${connectedConfig?.configName}]'), + title: Text('连接数[${vntManager.size()}]'), actions: [ TextButton( - onPressed: () { - _closeVnt(); + onPressed: () async { + await vntManager.removeAll(); + loadConnectState(); Navigator.of(context).pop(); }, style: TextButton.styleFrom( foregroundColor: Colors.white, backgroundColor: Colors.red, ), - child: const Text('断开连接'), - ), - TextButton( - onPressed: () { - Navigator.of(context).pop(); - if (connectedConfig != null) { - connectDetailPage(connectedConfig!); - } - }, - child: const Text('查看连接'), + child: const Text('全部断开'), ), + // TextButton( + // onPressed: () { + // Navigator.of(context).pop(); + // if (connectedConfig != null) { + // connectDetailPage(connectedConfig!); + // } + // }, + // child: const Text('查看连接'), + // ), ], ); }, @@ -484,13 +532,11 @@ class _HomePageState extends State with WindowListener { } void _deleteConfig(int index) { - if (_connected && connectedConfig != null) { - if (_configs[index].itemKey == connectedConfig?.itemKey) { - ScaffoldMessenger.of(context).showSnackBar( - const SnackBar(content: Text('请先断开连接再删除')), - ); - return; - } + if (vntManager.hasConnectionItem(_configs[index].itemKey)) { + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar(content: Text('已连接的配置不能删除')), + ); + return; } showDialog( context: context, @@ -556,7 +602,9 @@ class _HomePageState extends State with WindowListener { Widget build(BuildContext context) { return Scaffold( appBar: CustomAppBar( - title: const Text('', style: TextStyle(color: Colors.white)), + title: Text( + vntManager.hasConnection() ? '已连接:${vntManager.size()}' : '', + style: const TextStyle(fontSize: 16, color: Colors.white)), backgroundColor: Colors.teal, actions: [ Padding( @@ -622,6 +670,8 @@ class _HomePageState extends State with WindowListener { : ListView.builder( itemCount: _configs.length, itemBuilder: (context, index) { + var item = _configs[index]; + var connected = vntManager.hasConnectionItem(item.itemKey); return Container( color: index % 2 == 0 ? Colors.grey[200] : Colors.white, child: ListTile( @@ -658,9 +708,13 @@ class _HomePageState extends State with WindowListener { Padding( padding: const EdgeInsets.only(right: 3.0), child: Tooltip( - message: '连接', + message: connected ? '已连接' : '连接', child: IconButton( - icon: const Icon(Icons.link), + icon: Icon( + Icons.link, + color: + connected ? Colors.green : Colors.black, + ), onPressed: () => _connect(_configs[index]), ))), Padding( diff --git a/lib/vnt/vnt_api.dart b/lib/vnt/vnt_manager.dart similarity index 60% rename from lib/vnt/vnt_api.dart rename to lib/vnt/vnt_manager.dart index 2d523dd..8816072 100644 --- a/lib/vnt/vnt_api.dart +++ b/lib/vnt/vnt_manager.dart @@ -1,3 +1,4 @@ +import 'dart:async'; import 'dart:collection'; import 'dart:io'; import 'dart:isolate'; @@ -5,26 +6,24 @@ import 'dart:typed_data'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; - -import '../network_config.dart'; -import '../src/rust/api/vnt_api.dart'; -import '../utils/ip_utils.dart'; import 'package:synchronized/synchronized.dart'; +import 'package:vnt_app/network_config.dart'; +import 'package:vnt_app/src/rust/api/vnt_api.dart'; +import 'package:vnt_app/utils/ip_utils.dart'; -class VntApiUtils { - static VntApi? vntApi; - static VntConfig? vntConfig; - static NetworkConfig? networkConfig; - static Queue logQueue = Queue(); - static final lock = Lock(); - static NetworkConfig? getNetConfig() { - return networkConfig; - } +final VntManager vntManager = VntManager(); - static Future open(NetworkConfig config, SendPort uiCall) async { - addLog(ConnectLogEntry(message: '连接vnts ${config.serverAddress}')); - networkConfig = config; - vntConfig = VntConfig( +class VntBox { + final VntApi vntApi; + final VntConfig vntConfig; + final NetworkConfig networkConfig; + VntBox({ + required this.vntApi, + required this.vntConfig, + required this.networkConfig, + }); + static Future create(NetworkConfig config, SendPort uiCall) async { + var vntConfig = VntConfig( tap: false, token: config.token, deviceId: config.deviceID, @@ -56,35 +55,22 @@ class VntApiUtils { portMappingList: config.portMappings, compressor: config.compressor.isEmpty ? 'none' : config.compressor, ); - var vntCall = VntApiCallback(successFn: () { uiCall.send('success'); }, createTunFn: (info) { - lock.synchronized(() { - addLog( - ConnectLogEntry(message: '创建虚拟网卡 ${info.name} ${info.version}')); - }); // uiCall.send(info); }, connectFn: (info) { - lock.synchronized(() { - addLog(ConnectLogEntry(message: '第${info.count}次连接目标 ${info.address}')); - }); uiCall.send(info); }, handshakeFn: (info) { - addLog(ConnectLogEntry( - message: '握手成功 vnts版本 ${info.version} vnts指纹 ${info.finger}')); // uiCall.send(info); return true; }, registerFn: (info) { - addLog(ConnectLogEntry( - message: - '注册成功 虚拟IP ${info.virtualIp} ${info.virtualGateway}/${info.virtualNetmask}')); // uiCall.send(info); return true; }, generateTunFn: (info) async { //创建vpn try { - int fd = await VntAppCall.startVpn(info, vntConfig?.mtu ?? 1400); + int fd = await VntAppCall.startVpn(info, vntConfig.mtu ?? 1400); return fd; } catch (e) { debugPrint('创建vpn异常 $e'); @@ -95,38 +81,34 @@ class VntApiUtils { // uiCall.send(info); }, errorFn: (info) { debugPrint('服务异常 类型 ${info.code.name} ${info.msg ?? ''}'); - addLog(ConnectLogEntry( - message: '服务异常 类型 ${info.code.name} ${info.msg ?? ''}')); uiCall.send(info); }, stopFn: () { uiCall.send('stop'); }); - vntApi = await vntInit(vntConfig: vntConfig!, call: vntCall); + var vntApi = await vntInit(vntConfig: vntConfig, call: vntCall); + + return VntBox(vntApi: vntApi, vntConfig: vntConfig, networkConfig: config); } - static close() async { - if (vntApi == null) { - return; - } - vntApi?.stop(); - vntApi = null; + Future close() async { + vntApi.stop(); if (Platform.isAndroid) { await VntAppCall.stopVpn(); } - addLog(ConnectLogEntry(message: '关闭vnt连接')); } - static bool isClosed() { - return vntApi == null || vntApi!.isStopped(); + bool isClosed() { + return vntApi.isStopped(); } - static Map currentDevice() { - if (vntApi == null) { - return {}; - } - var currentDevice = vntApi!.currentDevice(); + NetworkConfig? getNetConfig() { + return networkConfig; + } + + Map currentDevice() { + var currentDevice = vntApi.currentDevice(); - var natInfo = vntApi!.natInfo(); + var natInfo = vntApi.natInfo(); return { 'virtualIp': currentDevice.virtualIp, 'virtualNetmask': currentDevice.virtualNetmask, @@ -142,62 +124,93 @@ class VntApiUtils { }; } - static List peerDeviceList() { - if (vntApi == null) { - return List.empty(); - } - return vntApi!.deviceList(); + List peerDeviceList() { + return vntApi.deviceList(); } - static List<(String, List)> routeList() { - if (vntApi == null) { - return List.empty(); - } - return vntApi!.routeList(); + List<(String, List)> routeList() { + return vntApi.routeList(); } - static RustRoute? route(String ip) { - if (vntApi == null) { - return null; + RustRoute? route(String ip) { + return vntApi.route(ip: ip); + } + + RustNatInfo? peerNatInfo(String ip) { + return vntApi.peerNatInfo(ip: ip); + } + + String downStream() { + return vntApi.downStream(); + } + + String upStream() { + return vntApi.upStream(); + } +} + +class VntManager { + HashMap map = HashMap(); + Future create(NetworkConfig config, SendPort uiCall) async { + var key = config.itemKey; + if (map.containsKey(key)) { + return map[key]!; } - return vntApi!.route(ip: ip); + var vntBox = await VntBox.create(config, uiCall); + map[key] = vntBox; + return vntBox; } - static RustNatInfo? peerNatInfo(String ip) { - if (vntApi == null) { - return null; + VntBox? get(String key) { + var vntBox = map[key]; + if (vntBox != null && !vntBox.isClosed()) { + return vntBox; } - return vntApi!.peerNatInfo(ip: ip); + return null; } - static String downStream() { - if (vntApi == null) { - return ''; + Future remove(String key) async { + var vnt = map.remove(key); + if (vnt != null) { + await vnt.close(); } - return vntApi!.downStream(); } - static String upStream() { - if (vntApi == null) { - return ''; + Future removeAll() async { + for (var element in map.entries) { + await element.value.close(); } - return vntApi!.upStream(); + map.clear(); + } + + bool hasConnectionItem(String key) { + var vntBox = map[key]; + return vntBox != null && !vntBox.isClosed(); } - static void addLog(ConnectLogEntry log) { - logQueue.addFirst(log); - if (logQueue.length > 100) { - logQueue.removeLast(); + bool hasConnection() { + if (map.isEmpty) { + return false; } + map.removeWhere((key, val) => val.isClosed()); + return map.isNotEmpty; + } + + int size() { + map.removeWhere((key, val) => val.isClosed()); + return map.length; } -} -class ConnectLogEntry { - final DateTime date; - final String message; + bool supportMultiple() { + return !Platform.isAndroid; + } - ConnectLogEntry({DateTime? date, required this.message}) - : date = date ?? DateTime.now(); + VntBox? getOne() { + if (map.isEmpty) { + return null; + } + return map.entries.first.value; + } } class VntAppCall { @@ -206,7 +219,7 @@ class VntAppCall { channel.setMethodCallHandler((MethodCall call) async { switch (call.method) { case 'stopVnt': - VntApiUtils.close(); + await vntManager.removeAll(); default: throw PlatformException( code: 'Unimplemented',