298 lines
7.8 KiB
Dart
298 lines
7.8 KiB
Dart
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';
|
|
|
|
// IPC Packet Types
|
|
class IpcTypes {
|
|
static const int handshake = 0;
|
|
static const int frame = 1;
|
|
static const int close = 2;
|
|
static const int ping = 3;
|
|
static const int pong = 4;
|
|
}
|
|
|
|
// IPC Close Codes
|
|
class IpcCloseCodes {
|
|
static const int closeNormal = 1000;
|
|
static const int closeUnsupported = 1003;
|
|
static const int closeAbnormal = 1006;
|
|
}
|
|
|
|
// IPC Error Codes
|
|
class IpcErrorCodes {
|
|
static const int invalidClientId = 4000;
|
|
static const int invalidOrigin = 4001;
|
|
static const int rateLimited = 4002;
|
|
static const int tokenRevoked = 4003;
|
|
static const int invalidVersion = 4004;
|
|
static const int invalidEncoding = 4005;
|
|
}
|
|
|
|
// IPC Packet structure
|
|
class IpcPacket {
|
|
final int type;
|
|
final Map<String, dynamic> data;
|
|
|
|
IpcPacket(this.type, this.data);
|
|
}
|
|
|
|
// Abstract base class for IPC server
|
|
abstract class IpcServer {
|
|
final List<IpcSocketWrapper> _sockets = [];
|
|
|
|
// Encode IPC packet
|
|
static Uint8List encodeIpcPacket(int type, Map<String, dynamic> data) {
|
|
final jsonData = jsonEncode(data);
|
|
final dataBytes = utf8.encode(jsonData);
|
|
final dataSize = dataBytes.length;
|
|
|
|
final buffer = ByteData(8 + dataSize);
|
|
buffer.setInt32(0, type, Endian.little);
|
|
buffer.setInt32(4, dataSize, Endian.little);
|
|
buffer.buffer.asUint8List().setRange(8, 8 + dataSize, dataBytes);
|
|
|
|
return buffer.buffer.asUint8List();
|
|
}
|
|
|
|
Future<void> start();
|
|
Future<void> stop();
|
|
|
|
void addSocket(IpcSocketWrapper socket) {
|
|
_sockets.add(socket);
|
|
}
|
|
|
|
void removeSocket(IpcSocketWrapper socket) {
|
|
_sockets.remove(socket);
|
|
}
|
|
|
|
List<IpcSocketWrapper> get sockets => _sockets;
|
|
|
|
void Function(
|
|
IpcSocketWrapper socket,
|
|
IpcPacket packet,
|
|
Map<String, Function> handlers,
|
|
)?
|
|
handlePacket;
|
|
}
|
|
|
|
// Abstract base class for IPC socket wrapper
|
|
abstract class IpcSocketWrapper {
|
|
String clientId = '';
|
|
bool handshook = false;
|
|
final List<int> _buffer = [];
|
|
|
|
void addData(List<int> data) {
|
|
_buffer.addAll(data);
|
|
}
|
|
|
|
void send(Map<String, dynamic> msg);
|
|
void sendPong(dynamic data);
|
|
void close();
|
|
void closeWithCode(int code, [String message = '']);
|
|
|
|
List<IpcPacket> readPackets() {
|
|
final packets = <IpcPacket>[];
|
|
|
|
while (_buffer.length >= 8) {
|
|
final buffer = Uint8List.fromList(_buffer);
|
|
final byteData = ByteData.view(buffer.buffer);
|
|
|
|
final type = byteData.getInt32(0, Endian.little);
|
|
final dataSize = byteData.getInt32(4, Endian.little);
|
|
|
|
if (_buffer.length < 8 + dataSize) break;
|
|
|
|
final dataBytes = _buffer.sublist(8, 8 + dataSize);
|
|
final jsonStr = utf8.decode(dataBytes);
|
|
final jsonData = jsonDecode(jsonStr);
|
|
|
|
packets.add(IpcPacket(type, jsonData));
|
|
|
|
_buffer.removeRange(0, 8 + dataSize);
|
|
}
|
|
|
|
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();
|
|
}
|
|
}
|