283 lines
		
	
	
		
			7.4 KiB
		
	
	
	
		
			Dart
		
	
	
	
	
	
			
		
		
	
	
			283 lines
		
	
	
		
			7.4 KiB
		
	
	
	
		
			Dart
		
	
	
	
	
	
import 'dart:async';
 | 
						|
import 'dart:convert';
 | 
						|
import 'dart:io';
 | 
						|
import 'dart:typed_data';
 | 
						|
import 'package:dart_ipc/dart_ipc.dart';
 | 
						|
import 'package:island/talker.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);
 | 
						|
      talker.log('IPC listening at $ipcPath');
 | 
						|
 | 
						|
      _serverSubscription = serverSocket.listen((socket) {
 | 
						|
        final socketWrapper = MultiPlatformIpcSocketWrapper(socket);
 | 
						|
        addSocket(socketWrapper);
 | 
						|
        talker.log('New IPC connection!');
 | 
						|
        _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) {
 | 
						|
        talker.log('Error closing IPC socket: $e');
 | 
						|
      }
 | 
						|
    }
 | 
						|
    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;
 | 
						|
        talker.log('Read data took $readDuration microseconds');
 | 
						|
 | 
						|
        final packets = socket.readPackets();
 | 
						|
        for (final packet in packets) {
 | 
						|
          handlePacket?.call(socket, packet, {});
 | 
						|
        }
 | 
						|
      },
 | 
						|
      onDone: () {
 | 
						|
        talker.log('IPC connection closed');
 | 
						|
        socket.close();
 | 
						|
      },
 | 
						|
      onError: (e) {
 | 
						|
        talker.log('IPC data error: $e');
 | 
						|
        socket.closeWithCode(IpcCloseCodes.closeUnsupported, e.toString());
 | 
						|
      },
 | 
						|
    );
 | 
						|
    final totalDuration = DateTime.now().difference(startTime).inMicroseconds;
 | 
						|
    talker.log('_handleIpcData took $totalDuration microseconds');
 | 
						|
  }
 | 
						|
 | 
						|
  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) {
 | 
						|
        talker.log('Failed to get macOS system temp dir: $e');
 | 
						|
      }
 | 
						|
    }
 | 
						|
 | 
						|
    // 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 (_) {}
 | 
						|
          talker.log('IPC socket will be created at: $socketPath');
 | 
						|
          return socketPath;
 | 
						|
        } catch (e) {
 | 
						|
          if (i == 0) {
 | 
						|
            talker.log('IPC path $socketPath not available: $e');
 | 
						|
          }
 | 
						|
          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) {
 | 
						|
    talker.log('IPC sending: $msg');
 | 
						|
    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();
 | 
						|
  }
 | 
						|
}
 |