♻️ Splitting up ipc servers in unix and windows
This commit is contained in:
460
lib/pods/activity/activity_rpc.dart
Normal file
460
lib/pods/activity/activity_rpc.dart
Normal file
@@ -0,0 +1,460 @@
|
|||||||
|
import 'dart:async';
|
||||||
|
import 'dart:convert';
|
||||||
|
import 'dart:developer' as developer;
|
||||||
|
import 'dart:io';
|
||||||
|
import 'package:flutter/foundation.dart';
|
||||||
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
|
import 'package:island/pods/network.dart';
|
||||||
|
import 'package:shelf/shelf.dart';
|
||||||
|
import 'package:shelf/shelf_io.dart' as shelf_io;
|
||||||
|
import 'package:shelf_web_socket/shelf_web_socket.dart';
|
||||||
|
import 'package:web_socket_channel/web_socket_channel.dart';
|
||||||
|
import 'ipc_server.dart';
|
||||||
|
import 'ipc_server.windows.dart';
|
||||||
|
import 'ipc_server.unix.dart';
|
||||||
|
|
||||||
|
const String kRpcLogPrefix = 'arRPC.websocket';
|
||||||
|
const String kRpcIpcLogPrefix = 'arRPC.ipc';
|
||||||
|
|
||||||
|
// IPC Constants
|
||||||
|
const String kIpcBasePath = 'discord-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;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Reference https://github.com/OpenAsar/arrpc/blob/main/src/transports/ipc.js
|
||||||
|
class ActivityRpcServer {
|
||||||
|
static const List<int> portRange = [6463, 6472]; // Ports 6463–6472
|
||||||
|
Map<String, Function>
|
||||||
|
handlers; // {connection: (socket), message: (socket, data), close: (socket)}
|
||||||
|
HttpServer? _httpServer;
|
||||||
|
IpcServer? _ipcServer;
|
||||||
|
final List<WebSocketChannel> _wsSockets = [];
|
||||||
|
|
||||||
|
ActivityRpcServer(this.handlers);
|
||||||
|
|
||||||
|
void updateHandlers(Map<String, Function> newHandlers) {
|
||||||
|
handlers = newHandlers;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Start the server
|
||||||
|
Future<void> start() async {
|
||||||
|
int port = portRange[0];
|
||||||
|
bool wsSuccess = false;
|
||||||
|
|
||||||
|
// Start WebSocket server
|
||||||
|
while (port <= portRange[1]) {
|
||||||
|
developer.log('Trying port $port', name: kRpcLogPrefix);
|
||||||
|
try {
|
||||||
|
_httpServer = await HttpServer.bind(InternetAddress.loopbackIPv4, port);
|
||||||
|
developer.log('Listening on $port', name: kRpcLogPrefix);
|
||||||
|
|
||||||
|
shelf_io.serveRequests(_httpServer!, (Request request) async {
|
||||||
|
developer.log('New request', name: kRpcLogPrefix);
|
||||||
|
if (request.headers['upgrade']?.toLowerCase() == 'websocket') {
|
||||||
|
final handler = webSocketHandler((WebSocketChannel channel, _) {
|
||||||
|
_wsSockets.add(channel);
|
||||||
|
_onWsConnection(channel, request);
|
||||||
|
});
|
||||||
|
return handler(request);
|
||||||
|
}
|
||||||
|
developer.log(
|
||||||
|
'New request disposed due to not websocket',
|
||||||
|
name: kRpcLogPrefix,
|
||||||
|
);
|
||||||
|
return Response.notFound('Not a WebSocket request');
|
||||||
|
});
|
||||||
|
wsSuccess = true;
|
||||||
|
break;
|
||||||
|
} catch (e) {
|
||||||
|
if (e is SocketException && e.osError?.errorCode == 98) {
|
||||||
|
developer.log('$port in use!', name: kRpcLogPrefix);
|
||||||
|
} else {
|
||||||
|
developer.log('HTTP error: $e', name: kRpcLogPrefix);
|
||||||
|
}
|
||||||
|
port++;
|
||||||
|
await Future.delayed(Duration(milliseconds: 100)); // Add delay
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!wsSuccess) {
|
||||||
|
throw Exception(
|
||||||
|
'Failed to bind to any port in range ${portRange[0]}–${portRange[1]}',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Start IPC server
|
||||||
|
final shouldStartIpc = !Platform.isMacOS && !kIsWeb;
|
||||||
|
if (shouldStartIpc) {
|
||||||
|
try {
|
||||||
|
if (Platform.isWindows) {
|
||||||
|
_ipcServer = WindowsIpcServer();
|
||||||
|
} else {
|
||||||
|
_ipcServer = UnixIpcServer();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set up IPC handlers
|
||||||
|
_ipcServer!.handlePacket = (socket, packet, _) {
|
||||||
|
_handleIpcPacket(socket, packet);
|
||||||
|
};
|
||||||
|
|
||||||
|
await _ipcServer!.start();
|
||||||
|
} catch (e) {
|
||||||
|
developer.log('IPC server error: $e', name: kRpcIpcLogPrefix);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
developer.log(
|
||||||
|
'IPC server disabled on macOS or web in production mode',
|
||||||
|
name: kRpcIpcLogPrefix,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Stop the server
|
||||||
|
Future<void> stop() async {
|
||||||
|
// Stop WebSocket server
|
||||||
|
for (var socket in _wsSockets) {
|
||||||
|
try {
|
||||||
|
await socket.sink.close();
|
||||||
|
} catch (e) {
|
||||||
|
developer.log('Error closing WebSocket: $e', name: kRpcLogPrefix);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_wsSockets.clear();
|
||||||
|
await _httpServer?.close(force: true);
|
||||||
|
|
||||||
|
// Stop IPC server
|
||||||
|
await _ipcServer?.stop();
|
||||||
|
|
||||||
|
developer.log('Servers stopped', name: kRpcLogPrefix);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle new WebSocket connection
|
||||||
|
void _onWsConnection(WebSocketChannel socket, Request request) {
|
||||||
|
final uri = request.url;
|
||||||
|
final params = uri.queryParameters;
|
||||||
|
final ver = int.tryParse(params['v'] ?? '1') ?? 1;
|
||||||
|
final encoding = params['encoding'] ?? 'json';
|
||||||
|
final clientId = params['client_id'] ?? '';
|
||||||
|
final origin = request.headers['origin'] ?? '';
|
||||||
|
|
||||||
|
developer.log(
|
||||||
|
'New WS connection! origin: $origin, params: $params',
|
||||||
|
name: kRpcLogPrefix,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (origin.isNotEmpty &&
|
||||||
|
![
|
||||||
|
'https://discord.com',
|
||||||
|
'https://ptb.discord.com',
|
||||||
|
'https://canary.discord.com',
|
||||||
|
].contains(origin)) {
|
||||||
|
developer.log('Disallowed origin: $origin', name: kRpcLogPrefix);
|
||||||
|
socket.sink.close();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (encoding != 'json') {
|
||||||
|
developer.log(
|
||||||
|
'Unsupported encoding requested: $encoding',
|
||||||
|
name: kRpcLogPrefix,
|
||||||
|
);
|
||||||
|
socket.sink.close();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (ver != 1) {
|
||||||
|
developer.log('Unsupported version requested: $ver', name: kRpcLogPrefix);
|
||||||
|
socket.sink.close();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
final socketWithMeta = _WsSocketWrapper(socket, clientId, encoding);
|
||||||
|
|
||||||
|
socket.stream.listen(
|
||||||
|
(data) => _onWsMessage(socketWithMeta, data),
|
||||||
|
onError: (e) {
|
||||||
|
developer.log('WS socket error: $e', name: kRpcLogPrefix);
|
||||||
|
},
|
||||||
|
onDone: () {
|
||||||
|
developer.log('WS socket closed', name: kRpcLogPrefix);
|
||||||
|
handlers['close']?.call(socketWithMeta);
|
||||||
|
_wsSockets.remove(socket);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
handlers['connection']?.call(socketWithMeta);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle incoming WebSocket message
|
||||||
|
Future<void> _onWsMessage(_WsSocketWrapper socket, dynamic data) async {
|
||||||
|
if (data is! String) {
|
||||||
|
developer.log(
|
||||||
|
'Invalid WebSocket message: not a string',
|
||||||
|
name: kRpcLogPrefix,
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
final jsonData = await compute(jsonDecode, data);
|
||||||
|
if (jsonData is! Map<String, dynamic>) {
|
||||||
|
developer.log(
|
||||||
|
'Invalid WebSocket message: not a JSON object',
|
||||||
|
name: kRpcLogPrefix,
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
developer.log('WS message: $jsonData', name: kRpcLogPrefix);
|
||||||
|
handlers['message']?.call(socket, jsonData);
|
||||||
|
} catch (e) {
|
||||||
|
developer.log('WS message parse error: $e', name: kRpcLogPrefix);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle IPC packet
|
||||||
|
void _handleIpcPacket(IpcSocketWrapper socket, IpcPacket packet) {
|
||||||
|
switch (packet.type) {
|
||||||
|
case IpcTypes.ping:
|
||||||
|
developer.log('IPC ping received', name: kRpcIpcLogPrefix);
|
||||||
|
socket.sendPong(packet.data);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case IpcTypes.pong:
|
||||||
|
developer.log('IPC pong received', name: kRpcIpcLogPrefix);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case IpcTypes.handshake:
|
||||||
|
if (socket.handshook) {
|
||||||
|
throw Exception('Already handshook');
|
||||||
|
}
|
||||||
|
socket.handshook = true;
|
||||||
|
_onIpcHandshake(socket, packet.data);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case IpcTypes.frame:
|
||||||
|
if (!socket.handshook) {
|
||||||
|
throw Exception('Need to handshake first');
|
||||||
|
}
|
||||||
|
developer.log('IPC frame: ${packet.data}', name: kRpcIpcLogPrefix);
|
||||||
|
handlers['message']?.call(socket, packet.data);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case IpcTypes.close:
|
||||||
|
socket.close();
|
||||||
|
break;
|
||||||
|
|
||||||
|
default:
|
||||||
|
throw Exception('Invalid packet type: ${packet.type}');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle IPC handshake
|
||||||
|
void _onIpcHandshake(IpcSocketWrapper socket, Map<String, dynamic> params) {
|
||||||
|
developer.log('IPC handshake: $params', name: kRpcIpcLogPrefix);
|
||||||
|
|
||||||
|
final ver = int.tryParse(params['v']?.toString() ?? '1') ?? 1;
|
||||||
|
final clientId = params['client_id']?.toString() ?? '';
|
||||||
|
|
||||||
|
if (ver != 1) {
|
||||||
|
developer.log(
|
||||||
|
'IPC unsupported version requested: $ver',
|
||||||
|
name: kRpcIpcLogPrefix,
|
||||||
|
);
|
||||||
|
socket.closeWithCode(IpcErrorCodes.invalidVersion);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (clientId.isEmpty) {
|
||||||
|
developer.log('IPC client ID required', name: kRpcIpcLogPrefix);
|
||||||
|
socket.closeWithCode(IpcErrorCodes.invalidClientId);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
socket.clientId = clientId;
|
||||||
|
|
||||||
|
handlers['connection']?.call(socket);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// WebSocket wrapper
|
||||||
|
class _WsSocketWrapper {
|
||||||
|
final WebSocketChannel channel;
|
||||||
|
final String clientId;
|
||||||
|
final String encoding;
|
||||||
|
|
||||||
|
_WsSocketWrapper(this.channel, this.clientId, this.encoding);
|
||||||
|
|
||||||
|
void send(Map<String, dynamic> msg) {
|
||||||
|
developer.log('WS sending: $msg', name: kRpcLogPrefix);
|
||||||
|
channel.sink.add(jsonEncode(msg));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// State management for server status and activities
|
||||||
|
class ServerState {
|
||||||
|
final String status;
|
||||||
|
final List<String> activities;
|
||||||
|
|
||||||
|
ServerState({required this.status, this.activities = const []});
|
||||||
|
|
||||||
|
ServerState copyWith({String? status, List<String>? activities}) {
|
||||||
|
return ServerState(
|
||||||
|
status: status ?? this.status,
|
||||||
|
activities: activities ?? this.activities,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class ServerStateNotifier extends StateNotifier<ServerState> {
|
||||||
|
final ActivityRpcServer server;
|
||||||
|
|
||||||
|
ServerStateNotifier(this.server)
|
||||||
|
: super(ServerState(status: 'Server not started'));
|
||||||
|
|
||||||
|
Future<void> start() async {
|
||||||
|
if (!Platform.isAndroid && !Platform.isIOS && !kIsWeb) {
|
||||||
|
try {
|
||||||
|
await server.start();
|
||||||
|
state = state.copyWith(status: 'Server running');
|
||||||
|
} catch (e) {
|
||||||
|
state = state.copyWith(status: 'Server failed: $e');
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
state = state.copyWith(status: 'Server disabled on mobile/web');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void updateStatus(String status) {
|
||||||
|
state = state.copyWith(status: status);
|
||||||
|
}
|
||||||
|
|
||||||
|
void addActivity(String activity) {
|
||||||
|
state = state.copyWith(activities: [...state.activities, activity]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Providers
|
||||||
|
final rpcServerStateProvider =
|
||||||
|
StateNotifierProvider<ServerStateNotifier, ServerState>((ref) {
|
||||||
|
final server = ActivityRpcServer({});
|
||||||
|
final notifier = ServerStateNotifier(server);
|
||||||
|
server.updateHandlers({
|
||||||
|
'connection': (socket) {
|
||||||
|
final clientId =
|
||||||
|
socket is _WsSocketWrapper
|
||||||
|
? socket.clientId
|
||||||
|
: (socket as IpcSocketWrapper).clientId;
|
||||||
|
notifier.updateStatus('Client connected (ID: $clientId)');
|
||||||
|
socket.send({
|
||||||
|
'cmd': 'DISPATCH',
|
||||||
|
'data': {
|
||||||
|
'v': 1,
|
||||||
|
'config': {
|
||||||
|
'cdn_host': 'fake.cdn',
|
||||||
|
'api_endpoint': '//fake.api',
|
||||||
|
'environment': 'dev',
|
||||||
|
},
|
||||||
|
'user': {
|
||||||
|
'id': 'fake_user_id',
|
||||||
|
'username': 'FakeUser',
|
||||||
|
'discriminator': '0001',
|
||||||
|
'avatar': null,
|
||||||
|
'bot': false,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
'evt': 'READY',
|
||||||
|
'nonce': '12345',
|
||||||
|
});
|
||||||
|
},
|
||||||
|
'message': (socket, dynamic data) async {
|
||||||
|
if (data['cmd'] == 'SET_ACTIVITY') {
|
||||||
|
notifier.addActivity(
|
||||||
|
'Activity: ${data['args']['activity']['details'] ?? 'Unknown'}',
|
||||||
|
);
|
||||||
|
final label = data['args']['activity']['details'] ?? 'Unknown';
|
||||||
|
final appId = socket.clientId;
|
||||||
|
try {
|
||||||
|
await setRemoteActivityStatus(ref, label, appId);
|
||||||
|
} catch (e) {
|
||||||
|
developer.log(
|
||||||
|
'Failed to set remote activity status: $e',
|
||||||
|
name: kRpcLogPrefix,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
socket.send({
|
||||||
|
'cmd': 'SET_ACTIVITY',
|
||||||
|
'data': data['args']['activity'],
|
||||||
|
'evt': null,
|
||||||
|
'nonce': data['nonce'],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
'close': (socket) async {
|
||||||
|
notifier.updateStatus('Client disconnected');
|
||||||
|
final appId = socket.clientId;
|
||||||
|
try {
|
||||||
|
await unsetRemoteActivityStatus(ref, appId);
|
||||||
|
} catch (e) {
|
||||||
|
developer.log(
|
||||||
|
'Failed to unset remote activity status: $e',
|
||||||
|
name: kRpcLogPrefix,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
});
|
||||||
|
return notifier;
|
||||||
|
});
|
||||||
|
|
||||||
|
final rpcServerProvider = Provider<ActivityRpcServer>((ref) {
|
||||||
|
final notifier = ref.watch(rpcServerStateProvider.notifier);
|
||||||
|
return notifier.server;
|
||||||
|
});
|
||||||
|
|
||||||
|
Future<void> setRemoteActivityStatus(
|
||||||
|
Ref ref,
|
||||||
|
String label,
|
||||||
|
String appId,
|
||||||
|
) async {
|
||||||
|
final apiClient = ref.read(apiClientProvider);
|
||||||
|
await apiClient.post(
|
||||||
|
'/id/accounts/me/statuses',
|
||||||
|
data: {
|
||||||
|
'is_invisible': false,
|
||||||
|
'is_not_disturb': false,
|
||||||
|
'is_automated': true,
|
||||||
|
'label': label,
|
||||||
|
'app_identifier': appId,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> unsetRemoteActivityStatus(Ref ref, String appId) async {
|
||||||
|
final apiClient = ref.read(apiClientProvider);
|
||||||
|
await apiClient.delete(
|
||||||
|
'/id/accounts/me/statuses',
|
||||||
|
queryParameters: {'app': appId},
|
||||||
|
);
|
||||||
|
}
|
118
lib/pods/activity/ipc_server.dart
Normal file
118
lib/pods/activity/ipc_server.dart
Normal file
@@ -0,0 +1,118 @@
|
|||||||
|
import 'dart:async';
|
||||||
|
import 'dart:convert';
|
||||||
|
import 'dart:typed_data';
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
203
lib/pods/activity/ipc_server.unix.dart
Normal file
203
lib/pods/activity/ipc_server.unix.dart
Normal file
@@ -0,0 +1,203 @@
|
|||||||
|
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());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle IPC handshake
|
||||||
|
void _onIpcHandshake(
|
||||||
|
IpcSocketWrapper socket,
|
||||||
|
Map<String, dynamic> params,
|
||||||
|
Map<String, Function> handlers,
|
||||||
|
) {
|
||||||
|
developer.log('IPC handshake: $params', name: kRpcIpcLogPrefix);
|
||||||
|
|
||||||
|
final ver = int.tryParse(params['v']?.toString() ?? '1') ?? 1;
|
||||||
|
final clientId = params['client_id']?.toString() ?? '';
|
||||||
|
|
||||||
|
if (ver != 1) {
|
||||||
|
developer.log(
|
||||||
|
'IPC unsupported version requested: $ver',
|
||||||
|
name: kRpcIpcLogPrefix,
|
||||||
|
);
|
||||||
|
socket.closeWithCode(IpcErrorCodes.invalidVersion);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (clientId.isEmpty) {
|
||||||
|
developer.log('IPC client ID required', name: kRpcIpcLogPrefix);
|
||||||
|
socket.closeWithCode(IpcErrorCodes.invalidClientId);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
socket.clientId = clientId;
|
||||||
|
|
||||||
|
handlers['connection']?.call(socket);
|
||||||
|
}
|
||||||
|
|
||||||
|
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();
|
||||||
|
}
|
||||||
|
}
|
273
lib/pods/activity/ipc_server.windows.dart
Normal file
273
lib/pods/activity/ipc_server.windows.dart
Normal file
@@ -0,0 +1,273 @@
|
|||||||
|
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'.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(
|
||||||
|
'IPC named pipe created at \\\\.\\pipe\\discord-ipc',
|
||||||
|
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) {
|
||||||
|
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'.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: 100));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle IPC handshake
|
||||||
|
void _onIpcHandshake(
|
||||||
|
IpcSocketWrapper socket,
|
||||||
|
Map<String, dynamic> params,
|
||||||
|
Map<String, Function> handlers,
|
||||||
|
) {
|
||||||
|
developer.log('IPC handshake: $params', name: kRpcIpcLogPrefix);
|
||||||
|
|
||||||
|
final ver = int.tryParse(params['v']?.toString() ?? '1') ?? 1;
|
||||||
|
final clientId = params['client_id']?.toString() ?? '';
|
||||||
|
|
||||||
|
if (ver != 1) {
|
||||||
|
developer.log(
|
||||||
|
'IPC unsupported version requested: $ver',
|
||||||
|
name: kRpcIpcLogPrefix,
|
||||||
|
);
|
||||||
|
socket.closeWithCode(IpcErrorCodes.invalidVersion);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (clientId.isEmpty) {
|
||||||
|
developer.log('IPC client ID required', name: kRpcIpcLogPrefix);
|
||||||
|
socket.closeWithCode(IpcErrorCodes.invalidClientId);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
socket.clientId = clientId;
|
||||||
|
|
||||||
|
handlers['connection']?.call(socket);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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!);
|
||||||
|
}
|
||||||
|
}
|
@@ -1,839 +0,0 @@
|
|||||||
import 'dart:async';
|
|
||||||
import 'dart:convert';
|
|
||||||
import 'dart:developer' as developer;
|
|
||||||
import 'dart:ffi';
|
|
||||||
import 'dart:io';
|
|
||||||
import 'dart:isolate';
|
|
||||||
import 'package:flutter/foundation.dart';
|
|
||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
|
||||||
import 'package:island/pods/network.dart';
|
|
||||||
import 'package:shelf/shelf.dart';
|
|
||||||
import 'package:shelf/shelf_io.dart' as shelf_io;
|
|
||||||
import 'package:shelf_web_socket/shelf_web_socket.dart';
|
|
||||||
import 'package:web_socket_channel/web_socket_channel.dart';
|
|
||||||
import 'package:path/path.dart' as path;
|
|
||||||
import 'package:win32/win32.dart';
|
|
||||||
import 'package:ffi/ffi.dart';
|
|
||||||
|
|
||||||
const String kRpcLogPrefix = 'arRPC.websocket';
|
|
||||||
const String kRpcIpcLogPrefix = 'arRPC.ipc';
|
|
||||||
|
|
||||||
// IPC Constants
|
|
||||||
const String kIpcBasePath = 'discord-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;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Reference https://github.com/OpenAsar/arrpc/blob/main/src/transports/ipc.js
|
|
||||||
class ActivityRpcServer {
|
|
||||||
static const List<int> portRange = [6463, 6472]; // Ports 6463–6472
|
|
||||||
Map<String, Function> handlers; // {connection: (socket), message: (socket, data), close: (socket)}
|
|
||||||
HttpServer? _httpServer;
|
|
||||||
ServerSocket? _ipcServer;
|
|
||||||
int? _pipeHandle; // For Windows named pipe
|
|
||||||
Timer? _ipcTimer; // Store timer for cancellation
|
|
||||||
final List<WebSocketChannel> _wsSockets = [];
|
|
||||||
final List<_IpcSocketWrapper> _ipcSockets = [];
|
|
||||||
|
|
||||||
ActivityRpcServer(this.handlers);
|
|
||||||
|
|
||||||
void updateHandlers(Map<String, Function> newHandlers) {
|
|
||||||
handlers = newHandlers;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 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<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 {
|
|
||||||
if (Platform.isWindows) return r'\\.\pipe\discord-ipc';
|
|
||||||
|
|
||||||
// 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, '$kIpcBasePath-$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',
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Start the server
|
|
||||||
Future<void> start() async {
|
|
||||||
int port = portRange[0];
|
|
||||||
bool wsSuccess = false;
|
|
||||||
|
|
||||||
// Start WebSocket server
|
|
||||||
while (port <= portRange[1]) {
|
|
||||||
developer.log('Trying port $port', name: kRpcLogPrefix);
|
|
||||||
try {
|
|
||||||
_httpServer = await HttpServer.bind(InternetAddress.loopbackIPv4, port);
|
|
||||||
developer.log('Listening on $port', name: kRpcLogPrefix);
|
|
||||||
|
|
||||||
shelf_io.serveRequests(_httpServer!, (Request request) async {
|
|
||||||
developer.log('New request', name: kRpcLogPrefix);
|
|
||||||
if (request.headers['upgrade']?.toLowerCase() == 'websocket') {
|
|
||||||
final handler = webSocketHandler((WebSocketChannel channel, _) {
|
|
||||||
_wsSockets.add(channel);
|
|
||||||
_onWsConnection(channel, request);
|
|
||||||
});
|
|
||||||
return handler(request);
|
|
||||||
}
|
|
||||||
developer.log(
|
|
||||||
'New request disposed due to not websocket',
|
|
||||||
name: kRpcLogPrefix,
|
|
||||||
);
|
|
||||||
return Response.notFound('Not a WebSocket request');
|
|
||||||
});
|
|
||||||
wsSuccess = true;
|
|
||||||
break;
|
|
||||||
} catch (e) {
|
|
||||||
if (e is SocketException && e.osError?.errorCode == 98) {
|
|
||||||
developer.log('$port in use!', name: kRpcLogPrefix);
|
|
||||||
} else {
|
|
||||||
developer.log('HTTP error: $e', name: kRpcLogPrefix);
|
|
||||||
}
|
|
||||||
port++;
|
|
||||||
await Future.delayed(Duration(milliseconds: 100)); // Add delay
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!wsSuccess) {
|
|
||||||
throw Exception(
|
|
||||||
'Failed to bind to any port in range ${portRange[0]}–${portRange[1]}',
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Start IPC server
|
|
||||||
final shouldStartIpc = !Platform.isMacOS && !kIsWeb;
|
|
||||||
if (shouldStartIpc) {
|
|
||||||
if (Platform.isWindows) {
|
|
||||||
await _startWindowsIpcServer();
|
|
||||||
} else {
|
|
||||||
try {
|
|
||||||
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);
|
|
||||||
});
|
|
||||||
} catch (e) {
|
|
||||||
developer.log('IPC server error: $e', name: kRpcIpcLogPrefix);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
developer.log(
|
|
||||||
'IPC server disabled on macOS or web in production mode',
|
|
||||||
name: kRpcIpcLogPrefix,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Start Windows-specific IPC server using Winsock2 named pipe
|
|
||||||
Future<void> _startWindowsIpcServer() async {
|
|
||||||
final pipeName = r'\\.\pipe\discord-ipc'.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('IPC named pipe created at \\\\.\\pipe\\discord-ipc', name: kRpcIpcLogPrefix);
|
|
||||||
|
|
||||||
// Start listening for connections in a separate isolate
|
|
||||||
_listenWindowsIpc();
|
|
||||||
} finally {
|
|
||||||
free(pipeName);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Listen for Windows IPC connections in an isolate
|
|
||||||
void _listenWindowsIpc() async {
|
|
||||||
final receivePort = ReceivePort();
|
|
||||||
await Isolate.spawn(_windowsIpcIsolate, receivePort.sendPort);
|
|
||||||
|
|
||||||
receivePort.listen((message) {
|
|
||||||
if (message is int) {
|
|
||||||
final socketWrapper = _IpcSocketWrapper(message);
|
|
||||||
_ipcSockets.add(socketWrapper);
|
|
||||||
developer.log('New IPC connection on named pipe', name: kRpcIpcLogPrefix);
|
|
||||||
_handleWindowsIpcData(socketWrapper);
|
|
||||||
_startWindowsIpcServer(); // Create new pipe for next connection
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
static void _windowsIpcIsolate(SendPort sendPort) {
|
|
||||||
while (true) {
|
|
||||||
final pipeHandle = CreateNamedPipe(
|
|
||||||
r'\\.\pipe\discord-ipc'.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: 100));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Handle Windows IPC data
|
|
||||||
void _handleWindowsIpcData(_IpcSocketWrapper socket) async {
|
|
||||||
final startTime = DateTime.now();
|
|
||||||
final buffer = malloc.allocate<Uint8>(4096);
|
|
||||||
final bytesRead = malloc.allocate<Uint32>(sizeOf<Uint32>());
|
|
||||||
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(bytesRead.value);
|
|
||||||
socket.addData(data);
|
|
||||||
final packets = socket.readPackets();
|
|
||||||
for (final packet in packets) {
|
|
||||||
_handleIpcPacket(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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Stop the server
|
|
||||||
Future<void> stop() async {
|
|
||||||
// Stop WebSocket server
|
|
||||||
for (var socket in _wsSockets) {
|
|
||||||
try {
|
|
||||||
await socket.sink.close();
|
|
||||||
} catch (e) {
|
|
||||||
developer.log('Error closing WebSocket: $e', name: kRpcLogPrefix);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
_wsSockets.clear();
|
|
||||||
await _httpServer?.close(force: true);
|
|
||||||
|
|
||||||
// Stop IPC server
|
|
||||||
for (var socket in _ipcSockets) {
|
|
||||||
try {
|
|
||||||
socket.close();
|
|
||||||
} catch (e) {
|
|
||||||
developer.log('Error closing IPC socket: $e', name: kRpcIpcLogPrefix);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
_ipcSockets.clear();
|
|
||||||
if (Platform.isWindows && _pipeHandle != null) {
|
|
||||||
try {
|
|
||||||
CloseHandle(_pipeHandle!);
|
|
||||||
} catch (e) {
|
|
||||||
developer.log('Error closing named pipe: $e', name: kRpcIpcLogPrefix);
|
|
||||||
}
|
|
||||||
_pipeHandle = null;
|
|
||||||
}
|
|
||||||
_ipcTimer?.cancel();
|
|
||||||
await _ipcServer?.close();
|
|
||||||
|
|
||||||
developer.log('Servers stopped', name: kRpcLogPrefix);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Handle new WebSocket connection
|
|
||||||
void _onWsConnection(WebSocketChannel socket, Request request) {
|
|
||||||
final uri = request.url;
|
|
||||||
final params = uri.queryParameters;
|
|
||||||
final ver = int.tryParse(params['v'] ?? '1') ?? 1;
|
|
||||||
final encoding = params['encoding'] ?? 'json';
|
|
||||||
final clientId = params['client_id'] ?? '';
|
|
||||||
final origin = request.headers['origin'] ?? '';
|
|
||||||
|
|
||||||
developer.log(
|
|
||||||
'New WS connection! origin: $origin, params: $params',
|
|
||||||
name: kRpcLogPrefix,
|
|
||||||
);
|
|
||||||
|
|
||||||
if (origin.isNotEmpty &&
|
|
||||||
![
|
|
||||||
'https://discord.com',
|
|
||||||
'https://ptb.discord.com',
|
|
||||||
'https://canary.discord.com',
|
|
||||||
].contains(origin)) {
|
|
||||||
developer.log('Disallowed origin: $origin', name: kRpcLogPrefix);
|
|
||||||
socket.sink.close();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (encoding != 'json') {
|
|
||||||
developer.log(
|
|
||||||
'Unsupported encoding requested: $encoding',
|
|
||||||
name: kRpcLogPrefix,
|
|
||||||
);
|
|
||||||
socket.sink.close();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (ver != 1) {
|
|
||||||
developer.log('Unsupported version requested: $ver', name: kRpcLogPrefix);
|
|
||||||
socket.sink.close();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
final socketWithMeta = _WsSocketWrapper(socket, clientId, encoding);
|
|
||||||
|
|
||||||
socket.stream.listen(
|
|
||||||
(data) => _onWsMessage(socketWithMeta, data),
|
|
||||||
onError: (e) {
|
|
||||||
developer.log('WS socket error: $e', name: kRpcLogPrefix);
|
|
||||||
},
|
|
||||||
onDone: () {
|
|
||||||
developer.log('WS socket closed', name: kRpcLogPrefix);
|
|
||||||
handlers['close']?.call(socketWithMeta);
|
|
||||||
_wsSockets.remove(socket);
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
handlers['connection']?.call(socketWithMeta);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Handle new IPC connection
|
|
||||||
void _onIpcConnection(Socket socket) {
|
|
||||||
developer.log('New IPC connection!', name: kRpcIpcLogPrefix);
|
|
||||||
|
|
||||||
final socketWrapper = _IpcSocketWrapper.fromSocket(socket);
|
|
||||||
_ipcSockets.add(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);
|
|
||||||
handlers['close']?.call(socketWrapper);
|
|
||||||
_ipcSockets.remove(socketWrapper);
|
|
||||||
},
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Handle incoming WebSocket message
|
|
||||||
Future<void> _onWsMessage(_WsSocketWrapper socket, dynamic data) async {
|
|
||||||
if (data is! String) {
|
|
||||||
developer.log('Invalid WebSocket message: not a string', name: kRpcLogPrefix);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
final jsonData = await compute(jsonDecode, data);
|
|
||||||
if (jsonData is! Map<String, dynamic>) {
|
|
||||||
developer.log('Invalid WebSocket message: not a JSON object', name: kRpcLogPrefix);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
developer.log('WS message: $jsonData', name: kRpcLogPrefix);
|
|
||||||
handlers['message']?.call(socket, jsonData);
|
|
||||||
} catch (e) {
|
|
||||||
developer.log('WS message parse error: $e', name: kRpcLogPrefix);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Handle incoming IPC data
|
|
||||||
void _onIpcData(_IpcSocketWrapper socket, List<int> data) {
|
|
||||||
try {
|
|
||||||
socket.addData(data);
|
|
||||||
final packets = socket.readPackets();
|
|
||||||
for (final packet in packets) {
|
|
||||||
_handleIpcPacket(socket, packet);
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
developer.log('IPC data error: $e', name: kRpcIpcLogPrefix);
|
|
||||||
socket.closeWithCode(IpcCloseCodes.closeUnsupported, e.toString());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Handle IPC packet
|
|
||||||
void _handleIpcPacket(_IpcSocketWrapper socket, _IpcPacket packet) {
|
|
||||||
switch (packet.type) {
|
|
||||||
case IpcTypes.ping:
|
|
||||||
developer.log('IPC ping received', name: kRpcIpcLogPrefix);
|
|
||||||
socket.sendPong(packet.data);
|
|
||||||
break;
|
|
||||||
|
|
||||||
case IpcTypes.pong:
|
|
||||||
developer.log('IPC pong received', name: kRpcIpcLogPrefix);
|
|
||||||
break;
|
|
||||||
|
|
||||||
case IpcTypes.handshake:
|
|
||||||
if (socket.handshook) {
|
|
||||||
throw Exception('Already handshook');
|
|
||||||
}
|
|
||||||
socket.handshook = true;
|
|
||||||
_onIpcHandshake(socket, packet.data);
|
|
||||||
break;
|
|
||||||
|
|
||||||
case IpcTypes.frame:
|
|
||||||
if (!socket.handshook) {
|
|
||||||
throw Exception('Need to handshake first');
|
|
||||||
}
|
|
||||||
developer.log('IPC frame: ${packet.data}', name: kRpcIpcLogPrefix);
|
|
||||||
handlers['message']?.call(socket, packet.data);
|
|
||||||
break;
|
|
||||||
|
|
||||||
case IpcTypes.close:
|
|
||||||
socket.close();
|
|
||||||
break;
|
|
||||||
|
|
||||||
default:
|
|
||||||
throw Exception('Invalid packet type: ${packet.type}');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Handle IPC handshake
|
|
||||||
void _onIpcHandshake(_IpcSocketWrapper socket, Map<String, dynamic> params) {
|
|
||||||
developer.log('IPC handshake: $params', name: kRpcIpcLogPrefix);
|
|
||||||
|
|
||||||
final ver = int.tryParse(params['v']?.toString() ?? '1') ?? 1;
|
|
||||||
final clientId = params['client_id']?.toString() ?? '';
|
|
||||||
|
|
||||||
if (ver != 1) {
|
|
||||||
developer.log(
|
|
||||||
'IPC unsupported version requested: $ver',
|
|
||||||
name: kRpcIpcLogPrefix,
|
|
||||||
);
|
|
||||||
socket.closeWithCode(IpcErrorCodes.invalidVersion);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (clientId.isEmpty) {
|
|
||||||
developer.log('IPC client ID required', name: kRpcIpcLogPrefix);
|
|
||||||
socket.closeWithCode(IpcErrorCodes.invalidClientId);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
socket.clientId = clientId;
|
|
||||||
|
|
||||||
handlers['connection']?.call(socket);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// WebSocket wrapper
|
|
||||||
class _WsSocketWrapper {
|
|
||||||
final WebSocketChannel channel;
|
|
||||||
final String clientId;
|
|
||||||
final String encoding;
|
|
||||||
|
|
||||||
_WsSocketWrapper(this.channel, this.clientId, this.encoding);
|
|
||||||
|
|
||||||
void send(Map<String, dynamic> msg) {
|
|
||||||
developer.log('WS sending: $msg', name: kRpcLogPrefix);
|
|
||||||
channel.sink.add(jsonEncode(msg));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// IPC wrapper
|
|
||||||
class _IpcSocketWrapper {
|
|
||||||
final Socket? socket;
|
|
||||||
final int? pipeHandle;
|
|
||||||
String clientId = '';
|
|
||||||
bool handshook = false;
|
|
||||||
final List<int> _buffer = [];
|
|
||||||
|
|
||||||
_IpcSocketWrapper(this.pipeHandle) : socket = null;
|
|
||||||
_IpcSocketWrapper.fromSocket(this.socket) : pipeHandle = null;
|
|
||||||
|
|
||||||
void addData(List<int> data) {
|
|
||||||
_buffer.addAll(data);
|
|
||||||
}
|
|
||||||
|
|
||||||
void send(Map<String, dynamic> msg) {
|
|
||||||
developer.log('IPC sending: $msg', name: kRpcIpcLogPrefix);
|
|
||||||
final packet = ActivityRpcServer.encodeIpcPacket(IpcTypes.frame, msg);
|
|
||||||
if (Platform.isWindows && pipeHandle != null) {
|
|
||||||
final buffer = malloc.allocate<Uint8>(packet.length);
|
|
||||||
buffer.asTypedList(packet.length).setAll(0, packet);
|
|
||||||
final bytesWritten = malloc.allocate<Uint32>(sizeOf<Uint32>());
|
|
||||||
try {
|
|
||||||
WriteFile(
|
|
||||||
pipeHandle!,
|
|
||||||
buffer.cast(),
|
|
||||||
packet.length,
|
|
||||||
bytesWritten,
|
|
||||||
nullptr,
|
|
||||||
);
|
|
||||||
} finally {
|
|
||||||
malloc.free(buffer);
|
|
||||||
malloc.free(bytesWritten);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
socket?.add(packet);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void sendPong(dynamic data) {
|
|
||||||
final packet = ActivityRpcServer.encodeIpcPacket(IpcTypes.pong, data ?? {});
|
|
||||||
if (Platform.isWindows && pipeHandle != null) {
|
|
||||||
final buffer = malloc.allocate<Uint8>(packet.length);
|
|
||||||
buffer.asTypedList(packet.length).setAll(0, packet);
|
|
||||||
final bytesWritten = malloc.allocate<Uint32>(sizeOf<Uint32>());
|
|
||||||
try {
|
|
||||||
WriteFile(
|
|
||||||
pipeHandle!,
|
|
||||||
buffer.cast(),
|
|
||||||
packet.length,
|
|
||||||
bytesWritten,
|
|
||||||
nullptr,
|
|
||||||
);
|
|
||||||
} finally {
|
|
||||||
malloc.free(buffer);
|
|
||||||
malloc.free(bytesWritten);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
socket?.add(packet);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void close() {
|
|
||||||
if (Platform.isWindows && pipeHandle != null) {
|
|
||||||
CloseHandle(pipeHandle!);
|
|
||||||
} else {
|
|
||||||
socket?.close();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void closeWithCode(int code, [String message = '']) {
|
|
||||||
final closeData = {'code': code, 'message': message};
|
|
||||||
final packet = ActivityRpcServer.encodeIpcPacket(IpcTypes.close, closeData);
|
|
||||||
if (Platform.isWindows && pipeHandle != null) {
|
|
||||||
final buffer = malloc.allocate<Uint8>(packet.length);
|
|
||||||
buffer.asTypedList(packet.length).setAll(0, packet);
|
|
||||||
final bytesWritten = malloc.allocate<Uint32>(sizeOf<Uint32>());
|
|
||||||
try {
|
|
||||||
WriteFile(
|
|
||||||
pipeHandle!,
|
|
||||||
buffer.cast(),
|
|
||||||
packet.length,
|
|
||||||
bytesWritten,
|
|
||||||
nullptr,
|
|
||||||
);
|
|
||||||
} finally {
|
|
||||||
malloc.free(buffer);
|
|
||||||
malloc.free(bytesWritten);
|
|
||||||
}
|
|
||||||
CloseHandle(pipeHandle!);
|
|
||||||
} else {
|
|
||||||
socket?.add(packet);
|
|
||||||
socket?.close();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// IPC Packet structure
|
|
||||||
class _IpcPacket {
|
|
||||||
final int type;
|
|
||||||
final Map<String, dynamic> data;
|
|
||||||
|
|
||||||
_IpcPacket(this.type, this.data);
|
|
||||||
}
|
|
||||||
|
|
||||||
// State management for server status and activities
|
|
||||||
class ServerState {
|
|
||||||
final String status;
|
|
||||||
final List<String> activities;
|
|
||||||
|
|
||||||
ServerState({required this.status, this.activities = const []});
|
|
||||||
|
|
||||||
ServerState copyWith({String? status, List<String>? activities}) {
|
|
||||||
return ServerState(
|
|
||||||
status: status ?? this.status,
|
|
||||||
activities: activities ?? this.activities,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class ServerStateNotifier extends StateNotifier<ServerState> {
|
|
||||||
final ActivityRpcServer server;
|
|
||||||
|
|
||||||
ServerStateNotifier(this.server)
|
|
||||||
: super(ServerState(status: 'Server not started'));
|
|
||||||
|
|
||||||
Future<void> start() async {
|
|
||||||
if (!Platform.isAndroid && !Platform.isIOS && !kIsWeb) {
|
|
||||||
try {
|
|
||||||
await server.start();
|
|
||||||
state = state.copyWith(status: 'Server running');
|
|
||||||
} catch (e) {
|
|
||||||
state = state.copyWith(status: 'Server failed: $e');
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
state = state.copyWith(status: 'Server disabled on mobile/web');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void updateStatus(String status) {
|
|
||||||
state = state.copyWith(status: status);
|
|
||||||
}
|
|
||||||
|
|
||||||
void addActivity(String activity) {
|
|
||||||
state = state.copyWith(activities: [...state.activities, activity]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Providers
|
|
||||||
final rpcServerStateProvider =
|
|
||||||
StateNotifierProvider<ServerStateNotifier, ServerState>((ref) {
|
|
||||||
final server = ActivityRpcServer({});
|
|
||||||
final notifier = ServerStateNotifier(server);
|
|
||||||
server.updateHandlers({
|
|
||||||
'connection': (socket) {
|
|
||||||
final clientId =
|
|
||||||
socket is _WsSocketWrapper
|
|
||||||
? socket.clientId
|
|
||||||
: (socket as _IpcSocketWrapper).clientId;
|
|
||||||
notifier.updateStatus('Client connected (ID: $clientId)');
|
|
||||||
socket.send({
|
|
||||||
'cmd': 'DISPATCH',
|
|
||||||
'data': {
|
|
||||||
'v': 1,
|
|
||||||
'config': {
|
|
||||||
'cdn_host': 'fake.cdn',
|
|
||||||
'api_endpoint': '//fake.api',
|
|
||||||
'environment': 'dev',
|
|
||||||
},
|
|
||||||
'user': {
|
|
||||||
'id': 'fake_user_id',
|
|
||||||
'username': 'FakeUser',
|
|
||||||
'discriminator': '0001',
|
|
||||||
'avatar': null,
|
|
||||||
'bot': false,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
'evt': 'READY',
|
|
||||||
'nonce': '12345',
|
|
||||||
});
|
|
||||||
},
|
|
||||||
'message': (socket, dynamic data) async {
|
|
||||||
if (data['cmd'] == 'SET_ACTIVITY') {
|
|
||||||
notifier.addActivity(
|
|
||||||
'Activity: ${data['args']['activity']['details'] ?? 'Unknown'}',
|
|
||||||
);
|
|
||||||
final label = data['args']['activity']['details'] ?? 'Unknown';
|
|
||||||
final appId = socket.clientId;
|
|
||||||
try {
|
|
||||||
await setRemoteActivityStatus(ref, label, appId);
|
|
||||||
} catch (e) {
|
|
||||||
developer.log(
|
|
||||||
'Failed to set remote activity status: $e',
|
|
||||||
name: kRpcLogPrefix,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
socket.send({
|
|
||||||
'cmd': 'SET_ACTIVITY',
|
|
||||||
'data': data['args']['activity'],
|
|
||||||
'evt': null,
|
|
||||||
'nonce': data['nonce'],
|
|
||||||
});
|
|
||||||
}
|
|
||||||
},
|
|
||||||
'close': (socket) async {
|
|
||||||
notifier.updateStatus('Client disconnected');
|
|
||||||
final appId = socket.clientId;
|
|
||||||
try {
|
|
||||||
await unsetRemoteActivityStatus(ref, appId);
|
|
||||||
} catch (e) {
|
|
||||||
developer.log(
|
|
||||||
'Failed to unset remote activity status: $e',
|
|
||||||
name: kRpcLogPrefix,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
});
|
|
||||||
return notifier;
|
|
||||||
});
|
|
||||||
|
|
||||||
final rpcServerProvider = Provider<ActivityRpcServer>((ref) {
|
|
||||||
final notifier = ref.watch(rpcServerStateProvider.notifier);
|
|
||||||
return notifier.server;
|
|
||||||
});
|
|
||||||
|
|
||||||
Future<void> setRemoteActivityStatus(
|
|
||||||
Ref ref,
|
|
||||||
String label,
|
|
||||||
String appId,
|
|
||||||
) async {
|
|
||||||
final apiClient = ref.read(apiClientProvider);
|
|
||||||
await apiClient.post(
|
|
||||||
'/id/accounts/me/statuses',
|
|
||||||
data: {
|
|
||||||
'is_invisible': false,
|
|
||||||
'is_not_disturb': false,
|
|
||||||
'is_automated': true,
|
|
||||||
'label': label,
|
|
||||||
'app_identifier': appId,
|
|
||||||
},
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<void> unsetRemoteActivityStatus(Ref ref, String appId) async {
|
|
||||||
final apiClient = ref.read(apiClientProvider);
|
|
||||||
await apiClient.delete(
|
|
||||||
'/id/accounts/me/statuses',
|
|
||||||
queryParameters: {'app': appId},
|
|
||||||
);
|
|
||||||
}
|
|
@@ -3,7 +3,7 @@ import 'package:bitsdojo_window/bitsdojo_window.dart';
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_hooks/flutter_hooks.dart';
|
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
import 'package:island/pods/activity_rpc.dart';
|
import 'package:island/pods/activity/activity_rpc.dart';
|
||||||
import 'package:island/pods/websocket.dart';
|
import 'package:island/pods/websocket.dart';
|
||||||
import 'package:island/screens/tray_manager.dart';
|
import 'package:island/screens/tray_manager.dart';
|
||||||
import 'package:island/services/notify.dart';
|
import 'package:island/services/notify.dart';
|
||||||
|
Reference in New Issue
Block a user