Ver Fonte

feat: 适配windows版本

Tony há 1 mês atrás
pai
commit
52bcf0b8f9

+ 28 - 0
lib/app/controllers/windows/vpn_exception.dart

@@ -0,0 +1,28 @@
+sealed class VpnException implements Exception {
+  final String message;
+  VpnException(this.message);
+
+  @override
+  String toString() => message;
+}
+
+class WaitOnlineTimedOutException extends VpnException {
+  WaitOnlineTimedOutException() : super('wait service online timedout');
+}
+
+class RpcException extends VpnException {
+  RpcException(super.message);
+}
+
+class VpnInvalidParamsException extends VpnException {
+  VpnInvalidParamsException([String? message])
+      : super(message ?? 'Invalid connection parameters');
+}
+
+class VpnServiceNotOnlineException extends VpnException {
+  VpnServiceNotOnlineException() : super('vpn service not online');
+}
+
+class VpnServiceNotRunningException extends VpnException {
+  VpnServiceNotRunningException() : super('vpn service not running');
+}

+ 41 - 0
lib/app/controllers/windows/vpn_message.dart

@@ -0,0 +1,41 @@
+// 消息类型
+import 'dart:convert';
+
+class VpnMessageType {
+  VpnMessageType._();
+  static const int login = 1;
+  static const int heartbeat = 2;
+  static const int stateSync = 3;
+}
+
+// VPN 消息
+class VpnMessage {
+  final int type;
+  final Map<String, dynamic>? data;
+
+  VpnMessage(this.type, this.data);
+
+  factory VpnMessage.create(int type, Map<String, dynamic>? data) {
+    return VpnMessage(type, data);
+  }
+
+  Map<String, dynamic> toMap() {
+    return <String, dynamic>{
+      'msg_type': type,
+      'data': data,
+    };
+  }
+
+  factory VpnMessage.fromMap(Map<String, dynamic> map) {
+    return VpnMessage(
+      map['msg_type'] ?? 0,
+      map['data'],
+    );
+  }
+
+  String toJson() => json.encode(toMap());
+
+  factory VpnMessage.fromJson(String source) {
+    return VpnMessage.fromMap(json.decode(source) as Map<String, dynamic>);
+  }
+}

+ 430 - 0
lib/app/controllers/windows/vpn_windows_service.dart

@@ -0,0 +1,430 @@
+import 'dart:async';
+import 'dart:convert';
+import 'dart:io';
+import 'package:dio/dio.dart';
+import 'package:shelf/shelf_io.dart' as shelf_io;
+import 'package:shelf_web_socket/shelf_web_socket.dart';
+
+import '../../../utils/crypto.dart';
+import '../../../utils/log/logger.dart';
+import '../../../utils/misc.dart';
+import '../../constants/enums.dart';
+import 'vpn_exception.dart';
+import 'vpn_message.dart';
+
+class _RpcResult {
+  final bool success;
+  final String message;
+  final dynamic data;
+  _RpcResult(this.success, this.message, this.data);
+}
+
+class VpnWindowsService {
+  static const _tag = 'VpnWindowsService';
+
+  static const _rpcPort = 29764;
+  static const _rpcFileName = 'ixrpc.exe';
+  static const _rpcHeartbeatTimeout = 5;
+  static const _rpcBaseUrl = 'http://127.0.0.1:$_rpcPort';
+  static const String _accessKeySecret =
+      '{C5E098D8-5487-4F8A-8B23-7F1F839A3A0B}';
+
+  final _rpcDio = Dio();
+
+  ConnectionState _status = ConnectionState.disconnected;
+  final _ecStatus = StreamController<ConnectionState>.broadcast();
+
+  bool _isOnline = false;
+  final _ecOnline = StreamController<bool>.broadcast();
+
+  Process? _process;
+  Timer? _refreshTimer;
+  Timer? _startupTimer;
+  String? _logPath;
+  bool _vpnDebug = false;
+  bool _isStarted = false;
+  int _rpcHostPort = 0;
+  DateTime _lastAliveTime = DateTime.now();
+  bool _needRecoverConnection = false;
+
+  Map<String, dynamic>? _connectionParams;
+
+  void _setStatus(ConnectionState value) {
+    if (_status != value) {
+      _status = value;
+      _ecStatus.add(_status);
+    }
+  }
+
+  void _setIsOnline(bool value) {
+    if (_isOnline != value) {
+      _isOnline = value;
+      _ecOnline.add(_isOnline);
+    }
+  }
+
+  Future<_RpcResult> _rpcGet(String action) async {
+    final url = Uri.parse('$_rpcBaseUrl/api/$action').toString();
+    final response = await _rpcDio.get(url);
+    if (response.statusCode != 200) {
+      log(_tag, 'rpcGet HTTP error: ${response.statusCode}');
+      return _RpcResult(false, 'HTTP ${response.statusCode}', null);
+    }
+    final json = response.data as Map<String, dynamic>;
+    final int code = json['code'] as int;
+    final String msg = json['msg'] as String;
+    return _RpcResult(code == 200, msg, json);
+  }
+
+  Future<_RpcResult> _rpcPost(
+    String action,
+    Map<String, dynamic> params,
+  ) async {
+    if (!isOnline) {
+      throw RpcException('Vpn rpc service not available');
+    }
+    final url = Uri.parse('$_rpcBaseUrl/api/action').toString();
+    final data = jsonEncode({'action': action, 'data': params});
+
+    try {
+      final timestamp = DateTime.now().millisecondsSinceEpoch;
+      final signature = await Crypto.signature(
+        '$data:$timestamp',
+        _accessKeySecret,
+      );
+
+      log(_tag, '[VpnWindowsService] url:$url');
+      log(_tag, '[VpnWindowsService] >>:$data');
+
+      final response = await _rpcDio.post(
+        url,
+        data: data,
+        options: Options(
+          headers: {
+            'Accept': 'application/json',
+            'Timestamp': '$timestamp',
+            'Signature': signature,
+          },
+        ),
+      );
+
+      log(_tag, '[VpnWindowsService] <<:${response.data}');
+
+      if (response.statusCode != 200) {
+        log(_tag, 'rpcPost HTTP error: ${response.statusCode}');
+        return _RpcResult(false, 'HTTP ${response.statusCode}', null);
+      }
+
+      final json = response.data as Map<String, dynamic>; // Dio 自动反序列化JSON的数据
+      final int code = json['code'] as int;
+      final String msg = json['msg'] as String;
+      return _RpcResult(code == 200, msg, json);
+    } on DioException catch (e, stack) {
+      log(_tag, 'DioException: ${e.message}\nStack: $stack');
+      throw RpcException('DioException: ${e.message}');
+    } catch (e, stack) {
+      log(_tag, 'request error: $e\nStack: $stack');
+      throw RpcException('request error: $e');
+    }
+  }
+
+  Future<void> _check() {
+    return _rpcGet('ping');
+  }
+
+  Future<void> _resetProcess(int hostPort, bool debug, {int? rpcPort}) async {
+    try {
+      _killProcess();
+      final List<String> args = [];
+      args.add('-l');
+      args.add('127.0.0.1:$hostPort');
+      if (rpcPort != null) {
+        args.add('-p');
+        args.add(rpcPort.toString());
+      }
+      if (_logPath != null) {
+        args.add('-d');
+        args.add(_logPath!);
+      }
+      log(_tag, '_resetProcess: starting $_rpcFileName with args: $args');
+      _process = await Process.start(
+        _rpcFileName,
+        args,
+        runInShell: false,
+        mode: ProcessStartMode.detached,
+      );
+      // _process?.stdout.drain();
+      // _process?.stderr.drain();
+      log(_tag, '_resetProcess: process started successfully.');
+    } catch (e, stack) {
+      log(_tag, '_resetProcess error: $e\nStack: $stack');
+    }
+  }
+
+  void _killProcess() {
+    log(_tag, 'VpnWindowsService kill process');
+    _process?.kill();
+    _process = null;
+    _setIsOnline(false);
+  }
+
+  void dispose() async {
+    log(_tag, 'VpnWindowsService dispose start.');
+    await _shutdown();
+    //_ecTraffic.close();
+    _ecStatus.close();
+    _ecOnline.close();
+  }
+
+  Future<void> start(Map<String, dynamic> params) async {
+    _connectionParams = null;
+
+    // 通知正在连接
+    _setStatus(ConnectionState.connecting);
+
+    try {
+      final result = await _rpcPost('connect', params);
+      if (!result.success) {
+        log(_tag, 'start call error: ${result.message}');
+        _setStatus(ConnectionState.error);
+      } else {
+        _connectionParams = params;
+        log(_tag, 'start call success.');
+      }
+    } catch (e, stack) {
+      log(_tag, 'start call exception: $e\nStack: $stack');
+      // 通知连接失败
+      _setStatus(ConnectionState.error);
+    }
+  }
+
+  Future<void> stop() async {
+    _needRecoverConnection = false;
+    try {
+      final result = await _rpcPost('disconnect', {});
+      if (result.success) {
+        log(_tag, 'disconnect call success.');
+      } else {
+        log(_tag, 'disconnect call error: ${result.message}');
+      }
+    } catch (e, stack) {
+      log(_tag, 'disconnect call exception: $e\nStack: $stack');
+    }
+  }
+
+  Future<String> getLocationName() async {
+    return '';
+  }
+
+  Future<String> getRemoteAddress() async {
+    try {
+      final result = await _rpcPost('get_remote_ip', {});
+      if (!result.success) {
+        log(_tag, 'getRemoteAddress error: ${result.message}');
+        return '';
+      }
+      return result.message;
+    } catch (e) {
+      log(_tag, 'getRemoteAddress error: $e');
+      return '';
+    }
+  }
+
+  Future<bool> setBypassAddresses(List<String> list) async {
+    try {
+      if (list.isEmpty) {
+        log(_tag, 'setBypassAddresses: empty list');
+        return false;
+      }
+      final result = await _rpcPost('set_pass_ips', {'ips': list.join(',')});
+      if (!result.success) {
+        log(_tag, 'setBypassAddresses error: ${result.message}');
+        return false;
+      }
+      return true;
+    } catch (e) {
+      log(_tag, 'setBypassAddresses error: $e');
+      return false;
+    }
+  }
+
+  Future<void> _startup({bool debug = false}) async {
+    log(_tag, 'startup called. debug: $debug');
+    _vpnDebug = debug;
+    if (_isStarted) {
+      log(_tag, 'startup abort! _isStarted: $_isStarted');
+      return;
+    }
+    try {
+      // 处理VPN 消息
+      final handler = webSocketHandler((webSocket, _) {
+        webSocket.stream.listen((message) {
+          _lastAliveTime = DateTime.now();
+          try {
+            final vpnMessage = VpnMessage.fromMap(jsonDecode(message));
+            switch (vpnMessage.type) {
+              case VpnMessageType.login:
+                log(_tag, 'login message got');
+                // rpc已经上线
+                _setIsOnline(true);
+                // 保存重连状态
+                try {
+                  if (_status == ConnectionState.connected ||
+                      _status == ConnectionState.connecting) {
+                    _needRecoverConnection = true;
+                  }
+                } catch (e, stack) {
+                  log(_tag, 'login Exception: $e\nStack: $stack');
+                }
+                break;
+              case VpnMessageType.heartbeat:
+                log(_tag, 'heartbeat message got');
+                // 应答心跳
+                webSocket.sink.add(
+                  VpnMessage.create(VpnMessageType.heartbeat, null).toJson(),
+                );
+                break;
+              case VpnMessageType.stateSync:
+                log(_tag, 'stateSync message got');
+                try {
+                  final data = vpnMessage.data;
+                  if (data == null) {
+                    return;
+                  }
+                  final int state = data['state'] as int;
+                  //final dynamic param = data['param']; // 错误code
+                  _setStatus(ConnectionState.values[state]);
+
+                  // 恢复连接
+                  if (_needRecoverConnection) {
+                    if (_connectionParams != null) {
+                      start(_connectionParams!);
+                    }
+                    _needRecoverConnection = false;
+                  }
+                } catch (e, stack) {
+                  log(_tag, 'stateSync Exception: $e\nStack: $stack');
+                }
+                break;
+              default:
+            }
+          } catch (e, stack) {
+            log(_tag, 'webSocketHandler Exception: $e\nStack: $stack');
+          }
+        });
+      });
+
+      shelf_io.serve(handler, '127.0.0.1', 35461).then((server) async {
+        _isStarted = true;
+        log(_tag, 'startup finished. _isStarted: $_isStarted');
+        log(_tag, 'Serving at ws://${server.address.host}:${server.port}');
+
+        // websocket 服务器端
+        _rpcHostPort = server.port;
+        // 启动vpn客户端进程
+        await _resetProcess(_rpcHostPort, rpcPort: _rpcPort, _vpnDebug);
+        // 启动守护定时器
+        _refreshTimer = Timer.periodic(const Duration(seconds: 1), (_) async {
+          final dt = DateTime.now().difference(_lastAliveTime).inSeconds;
+          if (dt > _rpcHeartbeatTimeout) {
+            // 尝试check一下
+            try {
+              await _check();
+              _lastAliveTime = DateTime.now();
+              log(_tag, 'RPC process has been woken up, continuing...');
+            } catch (e) {
+              log(_tag, 'RPC process has timedout, need reset.');
+              _lastAliveTime = DateTime.now();
+              await _resetProcess(_rpcHostPort, rpcPort: _rpcPort, _vpnDebug);
+            }
+          }
+        });
+      });
+    } catch (e, stack) {
+      log(_tag, 'startup error: $e\nStack: $stack');
+    }
+  }
+
+  Future<void> _shutdown() async {
+    log(_tag, 'shutdown called.');
+    if (!_isStarted) {
+      log(_tag, 'shutdown abort! _isStarted: $_isStarted');
+      return;
+    }
+    try {
+      // 断开连接
+      await stop();
+    } catch (e, stack) {
+      log(_tag, 'shutdown disconnect error: $e\nStack: $stack');
+    }
+    try {
+      // 关闭守护定时器
+      _startupTimer?.cancel();
+    } catch (e, stack) {
+      log(_tag, 'shutdown _startupTimer cancel error: $e\nStack: $stack');
+    }
+    try {
+      _refreshTimer?.cancel();
+    } catch (e, stack) {
+      log(_tag, 'shutdown _refreshTimer cancel error: $e\nStack: $stack');
+    }
+    try {
+      // 关闭vpn客户端进程
+      _killProcess();
+    } catch (e, stack) {
+      log(_tag, 'shutdown _killProcess error: $e\nStack: $stack');
+    }
+
+    _isStarted = false;
+    _needRecoverConnection = false;
+    _setStatus(ConnectionState.disconnected);
+    log(_tag, 'shutdown finished. _isStarted: $_isStarted');
+  }
+
+  Future<void> initialize(int timedout, bool debug) async {
+    log(_tag, 'vpn service initialize...');
+
+    _logPath = (await logFileDirectory()).path;
+
+    final startTime = DateTime.now();
+    bool isTimedout = false;
+
+    // 启动服务
+    await _startup(debug: debug);
+
+    // 等待vpn客户端就绪信号
+    if (!isOnline) {
+      final completer = Completer();
+      Timer.periodic(const Duration(milliseconds: 500), (timer) {
+        // 客户端就绪
+        if (isOnline) {
+          log(_tag, 'vpn service start successed.');
+          completer.complete();
+          timer.cancel();
+        }
+        // 等待超时
+        final dt = DateTime.now().difference(startTime).inSeconds;
+        if (dt > timedout) {
+          log(_tag, 'vpn service start timedout.');
+          completer.complete();
+          timer.cancel();
+          isTimedout = true;
+        }
+      });
+      await completer.future;
+      log(_tag, 'vpn service initialize finished.');
+    }
+    if (isTimedout) {
+      _shutdown();
+      throw WaitOnlineTimedOutException();
+    }
+  }
+
+  bool get isOnline => _isOnline;
+
+  ConnectionState get status => _status;
+
+  Stream<bool> get onOnlineChanged => _ecOnline.stream;
+
+  Stream<ConnectionState> get onStatusChanged => _ecStatus.stream;
+}

+ 114 - 10
lib/app/controllers/windows_core_api.dart

@@ -1,16 +1,33 @@
 import 'dart:async';
+import 'dart:convert';
 import 'dart:io';
 
 import 'package:flutter/services.dart';
+import 'package:path/path.dart' as path;
+import 'package:path_provider/path_provider.dart';
 
+import '../../utils/log/logger.dart';
+import '../constants/enums.dart';
 import 'base_core_api.dart';
+import 'windows/vpn_windows_service.dart';
 
 /// Windows 实现
 class WindowsCoreApi implements BaseCoreApi {
+  static const _tag = 'WindowsCoreApi';
+
   WindowsCoreApi._() {
     _initEventChannel();
+    // 初始化vpn服务
+    _initVpnService();
   }
 
+  // 创建vpn服务
+  static final _vpn = VpnWindowsService();
+
+  StreamSubscription? _ssStatus;
+
+  bool _isVpnInited = false;
+
   /// 内部构造方法,供 BaseCoreApi 工厂使用
   factory WindowsCoreApi.create() => WindowsCoreApi._();
 
@@ -46,7 +63,51 @@ class WindowsCoreApi implements BaseCoreApi {
     }
   }
 
-  // TODO: 实现 Windows 特定的逻辑
+  void _initVpnService() {
+    if (_isVpnInited) {
+      return;
+    }
+    _isVpnInited = true;
+
+    // 初始化vpn服务 10秒超时
+    _vpn.initialize(10, false);
+
+    // 监听VPN服务状态
+    _ssStatus = _vpn.onStatusChanged.listen((event) async {
+      switch (event) {
+        case ConnectionState.connecting:
+          // 正在连接
+          _eventController.add(
+            '{"type":"vpn_status","status":1,"code":0,"message":""}',
+          );
+          break;
+
+        // 已经连接
+        case ConnectionState.connected:
+          _eventController.add(
+            '{"type":"vpn_status","status":2,"code":0,"message":""}',
+          );
+          break;
+
+        // 连接错误
+        case ConnectionState.error:
+          _eventController.add(
+            '{"type":"vpn_status","status":3,"code":0,"message":""}',
+          );
+          _vpn.stop();
+          break;
+
+        // 已经断开
+        case ConnectionState.disconnected:
+          _eventController.add(
+            '{"type":"vpn_status","status":0,"code":0,"message":""}',
+          );
+          break;
+      }
+
+      // TODO: 通知状态变更
+    });
+  }
 
   @override
   Future<String?> getApps() async {
@@ -56,7 +117,6 @@ class WindowsCoreApi implements BaseCoreApi {
 
   @override
   Future<String?> getSystemLocale() async {
-    // TODO: 实现 Windows 获取系统语言
     return Platform.localeName;
   }
 
@@ -79,20 +139,54 @@ class WindowsCoreApi implements BaseCoreApi {
     String params,
     int peekTimeInterval,
   ) async {
-    // TODO: 实现 Windows 连接逻辑
-    throw UnimplementedError('Windows connect not implemented yet');
+    try {
+      String geoPath = await _getGeoDirectory();
+      log(_tag, 'geoPath: $geoPath');
+
+      final selfExecutable = Platform.resolvedExecutable;
+      //List<String> apps = isSplitTunnelEnabled ? splitTunnelApps : [];
+
+      List<String> allowExes = [];
+      List<String> disallowExes = [selfExecutable];
+
+      // 连接vpn
+      Map<String, dynamic> params = {
+        'sessionId': sessionId,
+        'connectOptions': jsonEncode({
+          'geoPath': geoPath,
+          'nodesConfig': configJson,
+        }),
+        'allowExes': allowExes,
+        'disallowExes': disallowExes,
+      };
+
+      log(_tag, jsonEncode(params));
+
+      // 连接vpn
+      _vpn.start(params);
+      return true;
+    } catch (e) {
+      // 通知连接出错
+      _eventController.add(
+        '{"type":"vpn_status","status":3,"code":0,"message":""}',
+      );
+      //logger.e('get nodes error: $e');
+
+      return false;
+    }
   }
 
   @override
   Future<bool?> disconnect() async {
-    // TODO: 实现 Windows 断开连接逻辑
-    throw UnimplementedError('Windows disconnect not implemented yet');
+    // 实现 Windows 断开连接逻辑
+    await _vpn.stop();
+    return true;
   }
 
   @override
   Future<String?> getRemoteIp() async {
-    // TODO: 实现 Windows 获取远程 IP
-    throw UnimplementedError('Windows getRemoteIp not implemented yet');
+    // 实现 Windows 获取远程 IP
+    return await _vpn.getRemoteAddress();
   }
 
   @override
@@ -109,8 +203,7 @@ class WindowsCoreApi implements BaseCoreApi {
 
   @override
   Future<bool?> isConnected() async {
-    // TODO: 实现 Windows 连接状态检查
-    throw UnimplementedError('Windows isConnected not implemented yet');
+    return _vpn.isOnline && _vpn.status == ConnectionState.connected;
   }
 
   @override
@@ -152,4 +245,15 @@ class WindowsCoreApi implements BaseCoreApi {
   static void dispose() {
     _eventController.close();
   }
+
+  /// 获取 geo 文件目录
+  Future<String> _getGeoDirectory() async {
+    try {
+      final appDir = await getApplicationSupportDirectory();
+      final geoDir = Directory(path.join(appDir.path, 'geo'));
+      return geoDir.path;
+    } catch (_) {
+      return '';
+    }
+  }
 }

+ 6 - 0
lib/utils/crypto.dart

@@ -98,4 +98,10 @@ class Crypto {
   static Future<String> md5File(String filename) {
     return foundation.compute(_md5File, filename);
   }
+
+  static Future<String> signature(String input, String secret) async {
+    final hmacSha1 = crypto.Hmac(crypto.sha1, utf8.encode(secret));
+    final digest = hmacSha1.convert(utf8.encode(input));
+    return digest.toString();
+  }
 }

+ 1 - 1
lib/utils/geo_downloader.dart

@@ -36,7 +36,7 @@ class GeoDownloader {
 
   /// 获取文件目录
   Future<Directory> getFilesDir() async {
-    if (Platform.isAndroid) {
+    if (Platform.isAndroid || Platform.isWindows) {
       return await getApplicationSupportDirectory();
     } else {
       // iOS fallback

+ 18 - 0
lib/utils/misc.dart

@@ -0,0 +1,18 @@
+import 'dart:io';
+
+import 'package:flutter/foundation.dart';
+import 'package:path_provider/path_provider.dart';
+
+bool get isDesktop {
+  if (kIsWeb) return false;
+  return [
+    TargetPlatform.windows,
+    TargetPlatform.linux,
+    TargetPlatform.macOS,
+  ].contains(defaultTargetPlatform);
+}
+
+Future<Directory> logFileDirectory() async {
+  final temporaryDirectory = await getApplicationSupportDirectory();
+  return Directory('${temporaryDirectory.path}/logs').create(recursive: true);
+}

+ 2 - 0
pubspec.yaml

@@ -85,6 +85,8 @@ dependencies:
   pull_to_refresh_flutter3: ^2.0.2 # 下拉刷新
   awesome_notifications: ^0.10.1 # 通知
   flutter_app_group_directory: ^1.1.0 # App Group目录
+  shelf_web_socket: ^3.0.0 # windows 进程通信
+  shelf: ^1.4.2 # windows 进程通信
   # hive_ce: ^2.17.0 # 本地缓存
 
 dev_dependencies:

+ 3 - 3
windows/runner/main.cpp

@@ -7,7 +7,7 @@
 #include "utils.h"
 #include "dump.h"
 
-#define APP_MUTEX_NAME L"nomo.mutex"
+#define APP_MUTEX_NAME L"nomo_win.mutex"
 #define APP_WINDOW_NAME L"NOMO"
 
 UINT (*MyGetDpiForWindow) (HWND) = [] (HWND) { return 96u; };
@@ -114,7 +114,7 @@ int APIENTRY wWinMain(_In_ HINSTANCE instance, _In_opt_ HINSTANCE prev,
   }
 
   // 程序窗口大小
-  UINT windowWidth = 375, windowHeight = 650;
+  UINT windowWidth = 340, windowHeight = 640;
   Win32Window::Size size(windowWidth, windowHeight);
 
   // 计算程序显示坐标原点(屏幕中间)
@@ -130,7 +130,7 @@ int APIENTRY wWinMain(_In_ HINSTANCE instance, _In_opt_ HINSTANCE prev,
   // int minWidth = static_cast<int>(360 * scale_factor_x);
   // int minHeight = static_cast<int>(640 * scale_factor_y);
   // window.SetMinimumSize({minWidth, minHeight});
-  window.SetMinimumSize(SIZE{ 375, 650 });
+  window.SetMinimumSize(SIZE{ 340, 640 });
 
   // 设置窗口不可最大化以及不可缩放
   HWND hWnd = window.GetHandle();

+ 1 - 1
windows/runner/win32_window.cpp

@@ -16,7 +16,7 @@ namespace {
 #define DWMWA_USE_IMMERSIVE_DARK_MODE 20
 #endif
 
-#define APP_MUTEX_NAME L"nomo.mutex"
+#define APP_MUTEX_NAME L"nomo_win.mutex"
 #define APP_WINDOW_NAME L"NOMO"
 
 constexpr const wchar_t kWindowClassName[] = L"FLUTTER_RUNNER_WIN32_WINDOW";