diff --git a/lib/pods/activity/activity_rpc.dart b/lib/pods/activity/activity_rpc.dart index 6e93a9c1..3a98f8ed 100644 --- a/lib/pods/activity/activity_rpc.dart +++ b/lib/pods/activity/activity_rpc.dart @@ -12,8 +12,6 @@ import 'package:web_socket_channel/web_socket_channel.dart'; // Conditional imports for IPC server - use web stubs on web platform import 'ipc_server.dart' if (dart.library.html) 'ipc_server.web.dart'; -import 'ipc_server.windows.dart' if (dart.library.html) 'ipc_server.web.dart'; -import 'ipc_server.unix.dart' if (dart.library.html) 'ipc_server.web.dart'; const String kRpcLogPrefix = 'arRPC.websocket'; const String kRpcIpcLogPrefix = 'arRPC.ipc'; @@ -112,11 +110,7 @@ class ActivityRpcServer { final shouldStartIpc = !Platform.isMacOS && !kIsWeb; if (shouldStartIpc) { try { - if (Platform.isWindows) { - _ipcServer = WindowsIpcServer(); - } else { - _ipcServer = UnixIpcServer(); - } + _ipcServer = MultiPlatformIpcServer(); // Set up IPC handlers _ipcServer!.handlePacket = (socket, packet, _) { diff --git a/lib/pods/activity/ipc_server.dart b/lib/pods/activity/ipc_server.dart index 503151c2..5d724dcc 100644 --- a/lib/pods/activity/ipc_server.dart +++ b/lib/pods/activity/ipc_server.dart @@ -1,6 +1,10 @@ import 'dart:async'; import 'dart:convert'; +import 'dart:developer' as developer; +import 'dart:io'; import 'dart:typed_data'; +import 'package:dart_ipc/dart_ipc.dart'; +import 'package:path/path.dart' as path; const String kRpcIpcLogPrefix = 'arRPC.ipc'; @@ -116,3 +120,178 @@ abstract class IpcSocketWrapper { return packets; } } + +// Multiplatform IPC Server implementation using dart_ipc +class MultiPlatformIpcServer extends IpcServer { + StreamSubscription? _serverSubscription; + + @override + Future start() async { + try { + final ipcPath = Platform.isWindows + ? r'\\.\pipe\discord-ipc-0' + : await _findAvailableUnixIpcPath(); + + final serverSocket = await bind(ipcPath); + developer.log( + 'IPC listening at $ipcPath', + name: kRpcIpcLogPrefix, + ); + + _serverSubscription = serverSocket.listen((socket) { + final socketWrapper = MultiPlatformIpcSocketWrapper(socket); + addSocket(socketWrapper); + developer.log( + 'New IPC connection!', + name: kRpcIpcLogPrefix, + ); + _handleIpcData(socketWrapper); + }); + } catch (e) { + throw Exception('Failed to start IPC server: $e'); + } + } + + @override + Future stop() async { + for (var socket in sockets) { + try { + socket.close(); + } catch (e) { + developer.log('Error closing IPC socket: $e', name: kRpcIpcLogPrefix); + } + } + sockets.clear(); + _serverSubscription?.cancel(); + } + + // Handle incoming IPC data + void _handleIpcData(MultiPlatformIpcSocketWrapper socket) { + final startTime = DateTime.now(); + socket.socket.listen((data) { + final readStart = DateTime.now(); + socket.addData(data); + final readDuration = DateTime.now().difference(readStart).inMicroseconds; + developer.log( + 'Read data took $readDuration microseconds', + name: kRpcIpcLogPrefix, + ); + + final packets = socket.readPackets(); + for (final packet in packets) { + handlePacket?.call(socket, packet, {}); + } + }, onDone: () { + developer.log('IPC connection closed', name: kRpcIpcLogPrefix); + socket.close(); + }, onError: (e) { + developer.log('IPC data error: $e', name: kRpcIpcLogPrefix); + socket.closeWithCode(IpcCloseCodes.closeUnsupported, e.toString()); + }); + final totalDuration = DateTime.now().difference(startTime).inMicroseconds; + developer.log( + '_handleIpcData took $totalDuration microseconds', + name: kRpcIpcLogPrefix, + ); + } + + Future _getMacOsSystemTmpDir() async { + final result = await Process.run('getconf', ['DARWIN_USER_TEMP_DIR']); + return (result.stdout as String).trim(); + } + + // Find available IPC socket path for Unix-like systems + Future _findAvailableUnixIpcPath() async { + // Build list of directories to try, with macOS-specific handling + final baseDirs = []; + + if (Platform.isMacOS) { + try { + final macTempDir = await _getMacOsSystemTmpDir(); + if (macTempDir.isNotEmpty) { + baseDirs.add(macTempDir); + } + } catch (e) { + developer.log( + 'Failed to get macOS system temp dir: $e', + name: kRpcIpcLogPrefix, + ); + } + } + + // Add other standard directories + final otherDirs = [ + Platform.environment['XDG_RUNTIME_DIR'], + Platform.environment['TMPDIR'], + Platform.environment['TMP'], + Platform.environment['TEMP'], + '/tmp', + ]; + + baseDirs.addAll( + otherDirs.where((dir) => dir != null && dir.isNotEmpty).cast(), + ); + + for (final baseDir in baseDirs) { + for (int i = 0; i < 10; i++) { + final socketPath = path.join(baseDir, 'discord-ipc-$i'); + try { + final socket = await bind(socketPath); + socket.close(); + try { + await File(socketPath).delete(); + } catch (_) {} + developer.log( + 'IPC socket will be created at: $socketPath', + name: kRpcIpcLogPrefix, + ); + return socketPath; + } catch (e) { + if (i == 0) { + developer.log( + 'IPC path $socketPath not available: $e', + name: kRpcIpcLogPrefix, + ); + } + continue; + } + } + } + throw Exception( + 'No available IPC socket paths found in any temp directory', + ); + } +} + +// Multiplatform IPC Socket Wrapper +class MultiPlatformIpcSocketWrapper extends IpcSocketWrapper { + final dynamic socket; + + MultiPlatformIpcSocketWrapper(this.socket); + + @override + void send(Map msg) { + developer.log('IPC sending: $msg', name: kRpcIpcLogPrefix); + final packet = IpcServer.encodeIpcPacket(IpcTypes.frame, msg); + socket.add(packet); + } + + @override + void sendPong(dynamic data) { + final packet = IpcServer.encodeIpcPacket(IpcTypes.pong, data ?? {}); + socket.add(packet); + } + + @override + void close() { + socket.close(); + } + + @override + void closeWithCode(int code, [String message = '']) { + final closeData = {'code': code, 'message': message}; + final packet = IpcServer.encodeIpcPacket(IpcTypes.close, closeData); + socket.add(packet); + socket.close(); + } +} diff --git a/lib/pods/activity/ipc_server.unix.dart b/lib/pods/activity/ipc_server.unix.dart deleted file mode 100644 index 22dacfe7..00000000 --- a/lib/pods/activity/ipc_server.unix.dart +++ /dev/null @@ -1,172 +0,0 @@ -import 'dart:async'; -import 'dart:developer' as developer; -import 'dart:io'; -import 'package:path/path.dart' as path; -import 'ipc_server.dart'; - -class UnixIpcServer extends IpcServer { - ServerSocket? _ipcServer; - - @override - Future start() async { - final ipcPath = await _findAvailableIpcPath(); - _ipcServer = await ServerSocket.bind( - InternetAddress(ipcPath, type: InternetAddressType.unix), - 0, - ); - developer.log('IPC listening at $ipcPath', name: kRpcIpcLogPrefix); - - _ipcServer!.listen((Socket socket) { - _onIpcConnection(socket); - }); - } - - @override - Future stop() async { - for (var socket in sockets) { - try { - socket.close(); - } catch (e) { - developer.log('Error closing IPC socket: $e', name: kRpcIpcLogPrefix); - } - } - sockets.clear(); - await _ipcServer?.close(); - } - - // Handle new IPC connection - void _onIpcConnection(Socket socket) { - developer.log('New IPC connection!', name: kRpcIpcLogPrefix); - - final socketWrapper = UnixIpcSocketWrapper(socket); - addSocket(socketWrapper); - - socket.listen( - (data) => _onIpcData(socketWrapper, data), - onError: (e) { - developer.log('IPC socket error: $e', name: kRpcIpcLogPrefix); - socket.close(); - }, - onDone: () { - developer.log('IPC socket closed', name: kRpcIpcLogPrefix); - removeSocket(socketWrapper); - }, - ); - } - - // Handle incoming IPC data - void _onIpcData(UnixIpcSocketWrapper socket, List data) { - try { - socket.addData(data); - final packets = socket.readPackets(); - for (final packet in packets) { - handlePacket?.call(socket, packet, {}); - } - } catch (e) { - developer.log('IPC data error: $e', name: kRpcIpcLogPrefix); - socket.closeWithCode(IpcCloseCodes.closeUnsupported, e.toString()); - } - } - - Future _getMacOsSystemTmpDir() async { - final result = await Process.run('getconf', ['DARWIN_USER_TEMP_DIR']); - return (result.stdout as String).trim(); - } - - // Find available IPC socket path - Future _findAvailableIpcPath() async { - // Build list of directories to try, with macOS-specific handling - final baseDirs = []; - - if (Platform.isMacOS) { - try { - final macTempDir = await _getMacOsSystemTmpDir(); - if (macTempDir.isNotEmpty) { - baseDirs.add(macTempDir); - } - } catch (e) { - developer.log( - 'Failed to get macOS system temp dir: $e', - name: kRpcIpcLogPrefix, - ); - } - } - - // Add other standard directories - final otherDirs = [ - Platform.environment['XDG_RUNTIME_DIR'], - Platform.environment['TMPDIR'], - Platform.environment['TMP'], - Platform.environment['TEMP'], - '/tmp', - ]; - - baseDirs.addAll( - otherDirs.where((dir) => dir != null && dir.isNotEmpty).cast(), - ); - - for (final baseDir in baseDirs) { - for (int i = 0; i < 10; i++) { - final socketPath = path.join(baseDir, 'discord-ipc-$i'); - try { - final socket = await ServerSocket.bind( - InternetAddress(socketPath, type: InternetAddressType.unix), - 0, - ); - socket.close(); - try { - await File(socketPath).delete(); - } catch (_) {} - developer.log( - 'IPC socket will be created at: $socketPath', - name: kRpcIpcLogPrefix, - ); - return socketPath; - } catch (e) { - if (i == 0) { - developer.log( - 'IPC path $socketPath not available: $e', - name: kRpcIpcLogPrefix, - ); - } - continue; - } - } - } - throw Exception( - 'No available IPC socket paths found in any temp directory', - ); - } -} - -class UnixIpcSocketWrapper extends IpcSocketWrapper { - final Socket socket; - - UnixIpcSocketWrapper(this.socket); - - @override - void send(Map msg) { - developer.log('IPC sending: $msg', name: kRpcIpcLogPrefix); - final packet = IpcServer.encodeIpcPacket(IpcTypes.frame, msg); - socket.add(packet); - } - - @override - void sendPong(dynamic data) { - final packet = IpcServer.encodeIpcPacket(IpcTypes.pong, data ?? {}); - socket.add(packet); - } - - @override - void close() { - socket.close(); - } - - @override - void closeWithCode(int code, [String message = '']) { - final closeData = {'code': code, 'message': message}; - final packet = IpcServer.encodeIpcPacket(IpcTypes.close, closeData); - socket.add(packet); - socket.close(); - } -} diff --git a/lib/pods/activity/ipc_server.web.dart b/lib/pods/activity/ipc_server.web.dart index 71c50d31..c955bbf0 100644 --- a/lib/pods/activity/ipc_server.web.dart +++ b/lib/pods/activity/ipc_server.web.dart @@ -56,8 +56,6 @@ class IpcSocketWrapper { List readPackets() => []; } -class WindowsIpcServer extends IpcServer {} +class MultiPlatformIpcServer extends IpcServer {} -class UnixIpcServer extends IpcServer {} - -class WindowsIpcSocketWrapper extends IpcSocketWrapper {} +class MultiPlatformIpcSocketWrapper extends IpcSocketWrapper {} diff --git a/lib/pods/activity/ipc_server.windows.dart b/lib/pods/activity/ipc_server.windows.dart deleted file mode 100644 index ce861caa..00000000 --- a/lib/pods/activity/ipc_server.windows.dart +++ /dev/null @@ -1,243 +0,0 @@ -import 'dart:async'; -import 'dart:developer' as developer; -import 'dart:ffi'; -import 'dart:io'; -import 'dart:isolate'; -import 'package:ffi/ffi.dart'; -import 'package:win32/win32.dart'; -import 'ipc_server.dart'; - -class WindowsIpcServer extends IpcServer { - int? _pipeHandle; - Timer? _ipcTimer; - - @override - Future start() async { - final pipeName = r'\\.\pipe\discord-ipc-0'.toNativeUtf16(); - try { - _pipeHandle = CreateNamedPipe( - pipeName, - PIPE_ACCESS_DUPLEX, - PIPE_TYPE_MESSAGE | PIPE_READMODE_MESSAGE | PIPE_WAIT, - PIPE_UNLIMITED_INSTANCES, - 4096, // Output buffer size - 4096, // Input buffer size - 0, // Default timeout - nullptr, // Security attributes - ); - - if (_pipeHandle == INVALID_HANDLE_VALUE) { - final error = GetLastError(); - throw Exception('Failed to create named pipe: error code $error'); - } - - developer.log( - r'IPC named pipe created at \\.\pipe\discord-ipc-0', - name: kRpcIpcLogPrefix, - ); - - // Start listening for connections in a separate isolate - _listenWindowsIpc(); - } finally { - free(pipeName); - } - } - - @override - Future stop() async { - for (var socket in sockets) { - try { - socket.close(); - } catch (e) { - developer.log('Error closing IPC socket: $e', name: kRpcIpcLogPrefix); - } - } - sockets.clear(); - - if (_pipeHandle != null) { - try { - CloseHandle(_pipeHandle!); - } catch (e) { - developer.log('Error closing named pipe: $e', name: kRpcIpcLogPrefix); - } - _pipeHandle = null; - } - _ipcTimer?.cancel(); - } - - // Listen for Windows IPC connections in an isolate - void _listenWindowsIpc() async { - final receivePort = ReceivePort(); - await Isolate.spawn(_windowsIpcIsolate, receivePort.sendPort); - - receivePort.listen((message) { - developer.log(message.toString(), name: kRpcIpcLogPrefix); - if (message is int) { - final socketWrapper = WindowsIpcSocketWrapper(message); - addSocket(socketWrapper); - developer.log( - 'New IPC connection on named pipe', - name: kRpcIpcLogPrefix, - ); - _handleWindowsIpcData(socketWrapper); - start(); // Create new pipe for next connection - } - }); - } - - static void _windowsIpcIsolate(SendPort sendPort) { - while (true) { - final pipeHandle = CreateNamedPipe( - r'\\.\pipe\discord-ipc-0'.toNativeUtf16(), - PIPE_ACCESS_DUPLEX, - PIPE_TYPE_MESSAGE | PIPE_READMODE_MESSAGE | PIPE_WAIT, - PIPE_UNLIMITED_INSTANCES, - 4096, - 4096, - 0, - nullptr, - ); - if (pipeHandle == INVALID_HANDLE_VALUE) { - developer.log( - 'Failed to create named pipe: ${GetLastError()}', - name: kRpcIpcLogPrefix, - ); - break; - } - final connected = ConnectNamedPipe(pipeHandle, nullptr); - if (connected != 0 || GetLastError() == ERROR_PIPE_CONNECTED) { - sendPort.send(pipeHandle); - } - // Avoid tight loop - sleep(Duration(milliseconds: 500)); - } - } - - // Handle Windows IPC data - void _handleWindowsIpcData(WindowsIpcSocketWrapper socket) async { - final startTime = DateTime.now(); - final buffer = malloc.allocate(4096); - final bytesRead = malloc.allocate(4); - try { - while (socket.pipeHandle != null) { - final readStart = DateTime.now(); - final success = ReadFile( - socket.pipeHandle!, - buffer.cast(), - 4096, - bytesRead, - nullptr, - ); - final readDuration = - DateTime.now().difference(readStart).inMicroseconds; - developer.log( - 'ReadFile took $readDuration microseconds', - name: kRpcIpcLogPrefix, - ); - - if (success == FALSE && GetLastError() != ERROR_MORE_DATA) { - developer.log( - 'IPC read error: ${GetLastError()}', - name: kRpcIpcLogPrefix, - ); - socket.close(); - break; - } - - final data = buffer.asTypedList(0); - socket.addData(data); - final packets = socket.readPackets(); - for (final packet in packets) { - handlePacket?.call(socket, packet, {}); - } - } - } catch (e) { - developer.log('IPC data error: $e', name: kRpcIpcLogPrefix); - socket.closeWithCode(IpcCloseCodes.closeUnsupported, e.toString()); - } finally { - malloc.free(buffer); - malloc.free(bytesRead); - final totalDuration = DateTime.now().difference(startTime).inMicroseconds; - developer.log( - 'handleWindowsIpcData took $totalDuration microseconds', - name: kRpcIpcLogPrefix, - ); - } - } -} - -class WindowsIpcSocketWrapper extends IpcSocketWrapper { - final int? pipeHandle; - - WindowsIpcSocketWrapper(this.pipeHandle); - - @override - void send(Map msg) { - developer.log('IPC sending: $msg', name: kRpcIpcLogPrefix); - final packet = IpcServer.encodeIpcPacket(IpcTypes.frame, msg); - final buffer = malloc.allocate(packet.length); - buffer.asTypedList(packet.length).setAll(0, packet); - final bytesWritten = malloc.allocate(4); // DWORD is 4 bytes - try { - WriteFile( - pipeHandle!, - buffer.cast(), - packet.length, - bytesWritten, - nullptr, - ); - } finally { - malloc.free(buffer); - malloc.free(bytesWritten); - } - } - - @override - void sendPong(dynamic data) { - final packet = IpcServer.encodeIpcPacket(IpcTypes.pong, data ?? {}); - final buffer = malloc.allocate(packet.length); - buffer.asTypedList(packet.length).setAll(0, packet); - final bytesWritten = malloc.allocate(4); // DWORD is 4 bytes - try { - WriteFile( - pipeHandle!, - buffer.cast(), - packet.length, - bytesWritten, - nullptr, - ); - } finally { - malloc.free(buffer); - malloc.free(bytesWritten); - } - } - - @override - void close() { - if (pipeHandle != null) { - CloseHandle(pipeHandle!); - } - } - - @override - void closeWithCode(int code, [String message = '']) { - final closeData = {'code': code, 'message': message}; - final packet = IpcServer.encodeIpcPacket(IpcTypes.close, closeData); - final buffer = malloc.allocate(packet.length); - buffer.asTypedList(packet.length).setAll(0, packet); - final bytesWritten = malloc.allocate(4); // DWORD is 4 bytes - try { - WriteFile( - pipeHandle!, - buffer.cast(), - packet.length, - bytesWritten, - nullptr, - ); - } finally { - malloc.free(buffer); - malloc.free(bytesWritten); - } - CloseHandle(pipeHandle!); - } -} diff --git a/lib/pods/userinfo.dart b/lib/pods/userinfo.dart index 3081070a..8e7e98ab 100644 --- a/lib/pods/userinfo.dart +++ b/lib/pods/userinfo.dart @@ -30,7 +30,7 @@ class UserInfoNotifier extends StateNotifier> { final user = SnAccount.fromJson(response.data); state = AsyncValue.data(user); - if (kIsWeb || !Platform.isLinux) { + if (kIsWeb || !(Platform.isLinux || Platform.isWindows)) { FirebaseAnalytics.instance.setUserId(id: user.id); } } catch (error, stackTrace) { @@ -44,7 +44,7 @@ class UserInfoNotifier extends StateNotifier> { : 'failedToLoadUserInfoNetwork') .tr() .trim(), - '${error.response!.statusCode}\n${error.response?.headers}', + '${error.response?.statusCode ?? 'Network Error'}\n${error.response?.headers}', jsonEncode(error.response?.data), ].join('\n\n'), iconStyle: IconStyle.error, @@ -87,7 +87,7 @@ class UserInfoNotifier extends StateNotifier> { final prefs = _ref.read(sharedPreferencesProvider); await prefs.remove(kTokenPairStoreKey); _ref.invalidate(tokenProvider); - if (kIsWeb || !Platform.isLinux) { + if (kIsWeb || !(Platform.isLinux || Platform.isWindows)) { FirebaseAnalytics.instance.setUserId(id: null); } } diff --git a/pubspec.lock b/pubspec.lock index 30c3414f..fa2695e4 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -401,6 +401,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.0.0+7.7.0" + dart_ipc: + dependency: "direct main" + description: + name: dart_ipc + sha256: "6cad558cda5304017c1f581df4c96fd4f8e4ee212aae7bfa4357716236faa9ba" + url: "https://pub.dev" + source: hosted + version: "1.0.1" dart_style: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index 1b86184f..08436edf 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -150,6 +150,7 @@ dependencies: windows_notification: ^1.3.0 win32: ^5.14.0 ffi: ^2.1.4 + dart_ipc: ^1.0.0 dev_dependencies: flutter_test: diff --git a/windows/flutter/generated_plugin_registrant.cc b/windows/flutter/generated_plugin_registrant.cc index 661c9404..65268343 100644 --- a/windows/flutter/generated_plugin_registrant.cc +++ b/windows/flutter/generated_plugin_registrant.cc @@ -8,6 +8,7 @@ #include #include +#include #include #include #include @@ -38,6 +39,8 @@ void RegisterPlugins(flutter::PluginRegistry* registry) { registry->GetRegistrarForPlugin("BitsdojoWindowPlugin")); ConnectivityPlusWindowsPluginRegisterWithRegistrar( registry->GetRegistrarForPlugin("ConnectivityPlusWindowsPlugin")); + DartIpcPluginCApiRegisterWithRegistrar( + registry->GetRegistrarForPlugin("DartIpcPluginCApi")); FileSaverPluginRegisterWithRegistrar( registry->GetRegistrarForPlugin("FileSaverPlugin")); FileSelectorWindowsRegisterWithRegistrar( diff --git a/windows/flutter/generated_plugins.cmake b/windows/flutter/generated_plugins.cmake index be1b3c07..332b7ad7 100644 --- a/windows/flutter/generated_plugins.cmake +++ b/windows/flutter/generated_plugins.cmake @@ -5,6 +5,7 @@ list(APPEND FLUTTER_PLUGIN_LIST bitsdojo_window_windows connectivity_plus + dart_ipc file_saver file_selector_windows firebase_core