♻️ Refactor ICP server to make it available across platform

This commit is contained in:
2025-09-13 20:32:39 +08:00
parent be414891ec
commit 55c0e355f1
10 changed files with 198 additions and 429 deletions

View File

@@ -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, _) {

View File

@@ -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<void> 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<void> 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<String> _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<String> _findAvailableUnixIpcPath() async {
// Build list of directories to try, with macOS-specific handling
final baseDirs = <String>[];
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<String>(),
);
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<String, dynamic> 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();
}
}

View File

@@ -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<void> 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<void> 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<int> 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<String> _getMacOsSystemTmpDir() async {
final result = await Process.run('getconf', ['DARWIN_USER_TEMP_DIR']);
return (result.stdout as String).trim();
}
// Find available IPC socket path
Future<String> _findAvailableIpcPath() async {
// Build list of directories to try, with macOS-specific handling
final baseDirs = <String>[];
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<String>(),
);
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<String, dynamic> 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();
}
}

View File

@@ -56,8 +56,6 @@ class IpcSocketWrapper {
List<dynamic> readPackets() => [];
}
class WindowsIpcServer extends IpcServer {}
class MultiPlatformIpcServer extends IpcServer {}
class UnixIpcServer extends IpcServer {}
class WindowsIpcSocketWrapper extends IpcSocketWrapper {}
class MultiPlatformIpcSocketWrapper extends IpcSocketWrapper {}

View File

@@ -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<void> 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<void> 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<BYTE>(4096);
final bytesRead = malloc.allocate<DWORD>(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<String, dynamic> msg) {
developer.log('IPC sending: $msg', name: kRpcIpcLogPrefix);
final packet = IpcServer.encodeIpcPacket(IpcTypes.frame, msg);
final buffer = malloc.allocate<BYTE>(packet.length);
buffer.asTypedList(packet.length).setAll(0, packet);
final bytesWritten = malloc.allocate<DWORD>(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<BYTE>(packet.length);
buffer.asTypedList(packet.length).setAll(0, packet);
final bytesWritten = malloc.allocate<DWORD>(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<BYTE>(packet.length);
buffer.asTypedList(packet.length).setAll(0, packet);
final bytesWritten = malloc.allocate<DWORD>(4); // DWORD is 4 bytes
try {
WriteFile(
pipeHandle!,
buffer.cast(),
packet.length,
bytesWritten,
nullptr,
);
} finally {
malloc.free(buffer);
malloc.free(bytesWritten);
}
CloseHandle(pipeHandle!);
}
}

View File

@@ -30,7 +30,7 @@ class UserInfoNotifier extends StateNotifier<AsyncValue<SnAccount?>> {
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<AsyncValue<SnAccount?>> {
: '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<AsyncValue<SnAccount?>> {
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);
}
}