1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
//! async-std specific implementations for async Discord IPC
use std::future::Future;
use std::io;
use std::pin::Pin;
#[cfg(unix)]
use async_std::io::ReadExt as _;
#[cfg(unix)]
use async_std::io::WriteExt as _;
#[cfg(unix)]
use async_std::os::unix::net::UnixStream;
#[cfg(windows)]
use std::fs::File;
#[cfg(windows)]
use std::io::{Read, Write};
#[cfg(windows)]
use std::sync::{Arc, Mutex};
use crate::async_io::traits::{AsyncRead, AsyncWrite};
use crate::debug_println;
use crate::error::{DiscordIpcError, Result};
use crate::ipc::{constants, PipeConfig};
/// A Discord IPC connection using async-std
pub(crate) enum AsyncStdConnection {
#[cfg(unix)]
Unix(UnixStream),
#[cfg(windows)]
Windows(Arc<Mutex<File>>),
}
impl AsyncStdConnection {
/// Create a new async-std connection with pipe configuration
pub async fn new_with_config(config: Option<PipeConfig>) -> Result<Self> {
let config = config.unwrap_or_default();
#[cfg(unix)]
{
Self::connect_unix_with_config(&config).await
}
#[cfg(windows)]
{
Self::connect_windows_with_config(&config).await
}
}
/// Create a new connection with pipe configuration and timeout
pub async fn new_with_config_and_timeout(
config: Option<PipeConfig>,
timeout_ms: u64,
) -> Result<Self> {
use async_std::future::timeout;
use std::time::Duration;
#[cfg(windows)]
debug_println!(
"Attempting to connect to Discord IPC with timeout {} ms (Windows)",
timeout_ms
);
#[cfg(unix)]
debug_println!(
"Attempting to connect to Discord IPC with timeout {} ms (Unix)",
timeout_ms
);
let timeout_duration = Duration::from_millis(timeout_ms);
match timeout(timeout_duration, Self::new_with_config(config)).await {
Ok(result) => result,
Err(_) => Err(DiscordIpcError::ConnectionTimeout {
timeout_ms,
last_error: None,
}),
}
}
#[cfg(unix)]
/// Connect to Discord IPC socket on Unix systems with configuration
async fn connect_unix_with_config(config: &PipeConfig) -> Result<Self> {
match config {
PipeConfig::Auto => Self::connect_unix_auto().await,
PipeConfig::CustomPath(path) => UnixStream::connect(path)
.await
.map(Self::Unix)
.map_err(DiscordIpcError::ConnectionFailed),
}
}
#[cfg(unix)]
/// Connect to Discord IPC socket using auto-discovery
async fn connect_unix_auto() -> Result<Self> {
// Try environment variables in order of preference
let env_keys = ["XDG_RUNTIME_DIR", "TMPDIR", "TMP", "TEMP"];
let mut directories = Vec::new();
for env_key in &env_keys {
if let Ok(dir) = std::env::var(env_key) {
directories.push(dir.clone());
// Also check Flatpak Discord path if XDG_RUNTIME_DIR is set
if env_key == &"XDG_RUNTIME_DIR" {
directories.push(format!("{}/app/com.discordapp.Discord", dir));
}
}
}
// Fallback to /run/user/{uid} if no env vars found
if directories.is_empty() {
let uid = unsafe { libc::getuid() };
directories.push(format!("/run/user/{}", uid));
// Also try Flatpak path as fallback
directories.push(format!("/run/user/{}/app/com.discordapp.Discord", uid));
}
// Try each directory with each socket number
let mut last_error = None;
for dir in &directories {
for i in 0..constants::MAX_IPC_SOCKETS {
let socket_path = format!("{}/{}{}", dir, constants::IPC_SOCKET_PREFIX, i);
match UnixStream::connect(&socket_path).await {
Ok(stream) => {
return Ok(Self::Unix(stream));
}
Err(err) => {
last_error = Some(err);
continue;
}
}
}
}
// If we got here, no valid socket was found
if let Some(err) = last_error {
// Return the last error we encountered for diagnostic purposes
if err.kind() == io::ErrorKind::PermissionDenied {
Err(DiscordIpcError::ConnectionFailed(io::Error::new(
io::ErrorKind::PermissionDenied,
"Permission denied when connecting to Discord IPC socket. Check file permissions.",
)))
} else {
Err(DiscordIpcError::ConnectionFailed(err))
}
} else {
Err(DiscordIpcError::NoValidSocket)
}
}
#[cfg(windows)]
/// Connect to Discord IPC named pipe on Windows with configuration
async fn connect_windows_with_config(config: &PipeConfig) -> Result<Self> {
match config {
PipeConfig::Auto => Self::connect_windows_auto().await,
PipeConfig::CustomPath(path) => {
use std::fs::OpenOptions;
use std::os::windows::fs::OpenOptionsExt;
const FILE_FLAG_OVERLAPPED: u32 = 0x40000000;
let file = OpenOptions::new()
.read(true)
.write(true)
.custom_flags(FILE_FLAG_OVERLAPPED)
.open(path)
.map_err(DiscordIpcError::ConnectionFailed)?;
Ok(Self::Windows(Arc::new(Mutex::new(file))))
}
}
}
#[cfg(windows)]
/// Connect to Discord IPC named pipe using auto-discovery
async fn connect_windows_auto() -> Result<Self> {
use std::fs::OpenOptions;
use std::os::windows::fs::OpenOptionsExt;
const FILE_FLAG_OVERLAPPED: u32 = 0x40000000;
let mut last_error = None;
for i in 0..constants::MAX_IPC_SOCKETS {
let pipe_path = format!(r"\\.\pipe\discord-ipc-{}", i);
debug_println!("Attempting to connect to Windows named pipe: {}", pipe_path);
// Clone pipe_path for the closure
let pipe_path_clone = pipe_path.clone();
// Open the named pipe with overlapped I/O support
// We use blocking operations wrapped in async context via the blocking crate
// this can cause a perfomance loss but there was no other way i could think of
// Todo : write a better solution for the below code
let result = blocking::unblock(move || {
OpenOptions::new()
.read(true)
.write(true)
.custom_flags(FILE_FLAG_OVERLAPPED)
.open(&pipe_path_clone)
})
.await;
match result {
Ok(file) => {
debug_println!("Successfully opened named pipe: {}", pipe_path);
return Ok(Self::Windows(Arc::new(Mutex::new(file))));
}
Err(err) => {
debug_println!("Failed to connect to named pipe {}: {}", pipe_path, err);
last_error = Some(err);
continue; // Try next pipe number
}
}
}
// If we got here, no valid pipe was found
if let Some(err) = last_error {
// Return the last error we encountered for diagnostic purposes
if err.kind() == io::ErrorKind::PermissionDenied {
Err(DiscordIpcError::ConnectionFailed(io::Error::new(
io::ErrorKind::PermissionDenied,
"Permission denied when connecting to Discord IPC pipe. Is Discord running with the right permissions?",
)))
} else {
Err(DiscordIpcError::ConnectionFailed(err))
}
} else {
Err(DiscordIpcError::NoValidSocket)
}
}
}
impl AsyncRead for AsyncStdConnection {
fn read<'a>(
&'a mut self,
buf: &'a mut [u8],
) -> Pin<Box<dyn Future<Output = io::Result<usize>> + Send + 'a>> {
Box::pin(async move {
match self {
#[cfg(unix)]
Self::Unix(stream) => stream.read(buf).await,
#[cfg(windows)]
Self::Windows(pipe) => {
// Clone the Arc to pass into the blocking task
let pipe_clone = Arc::clone(pipe);
let buf_len = buf.len();
// Use blocking crate to handle synchronous I/O in async context
let result = blocking::unblock(move || {
let mut local_buf = vec![0u8; buf_len];
let mut file = match pipe_clone.lock().map_err(|e| {
io::Error::new(io::ErrorKind::Other, format!("Mutex poisoned: {}", e))
}) {
Ok(f) => f,
Err(e) => return Err(e),
};
match file.read(&mut local_buf) {
Ok(n) => Ok((n, local_buf)),
Err(e) => Err(e),
}
})
.await;
match result {
Ok((n, data)) => {
buf[..n].copy_from_slice(&data[..n]);
Ok(n)
}
Err(e) => Err(e),
}
}
}
})
}
}
impl AsyncWrite for AsyncStdConnection {
fn write<'a>(
&'a mut self,
buf: &'a [u8],
) -> Pin<Box<dyn Future<Output = io::Result<usize>> + Send + 'a>> {
Box::pin(async move {
match self {
#[cfg(unix)]
Self::Unix(stream) => stream.write(buf).await,
#[cfg(windows)]
Self::Windows(pipe) => {
// Clone the Arc to pass into the blocking task
let pipe_clone = Arc::clone(pipe);
let data = buf.to_vec();
// Use blocking crate to handle synchronous I/O in async context
blocking::unblock(move || {
let mut file = match pipe_clone.lock() {
Ok(f) => f,
Err(e) => {
return Err(io::Error::new(
io::ErrorKind::Other,
format!("Mutex poisoned: {}", e),
));
}
};
file.write(&data)
})
.await
}
}
})
}
fn flush<'a>(&'a mut self) -> Pin<Box<dyn Future<Output = io::Result<()>> + Send + 'a>> {
Box::pin(async move {
match self {
#[cfg(unix)]
Self::Unix(stream) => stream.flush().await,
#[cfg(windows)]
Self::Windows(pipe) => {
// Clone the Arc to pass into the blocking task
let pipe_clone = Arc::clone(pipe);
blocking::unblock(move || {
let mut file = match pipe_clone.lock() {
Ok(f) => f,
Err(e) => {
return Err(io::Error::new(
io::ErrorKind::Other,
format!("Mutex poisoned: {}", e),
));
}
};
file.flush()
})
.await
}
}
})
}
}
/// async-std specific implementation of AsyncDiscordIpcClient
pub mod client {
use super::AsyncStdConnection;
use crate::async_io::client::AsyncDiscordIpcClient;
use crate::error::{DiscordIpcError, Result};
use crate::ipc::PipeConfig;
use serde_json::Value;
use std::time::Duration;
/// A reconnectable async-std-based Discord IPC client
///
/// This wrapper stores the connection configuration and client ID,
/// allowing you to reconnect after connection loss.
pub struct AsyncStdDiscordIpcClient {
inner: AsyncDiscordIpcClient<AsyncStdConnection>,
client_id: String,
pipe_config: Option<PipeConfig>,
timeout_ms: Option<u64>,
}
impl AsyncStdDiscordIpcClient {
/// Creates a new reconnectable async-std-based Discord IPC client
async fn new_internal(
client_id: impl Into<String>,
pipe_config: Option<PipeConfig>,
timeout_ms: Option<u64>,
) -> Result<Self> {
let client_id = client_id.into();
let connection = if let Some(timeout) = timeout_ms {
AsyncStdConnection::new_with_config_and_timeout(pipe_config.clone(), timeout)
.await?
} else {
AsyncStdConnection::new_with_config(pipe_config.clone()).await?
};
Ok(Self {
inner: AsyncDiscordIpcClient::new(client_id.clone(), connection),
client_id,
pipe_config,
timeout_ms,
})
}
/// Performs handshake with Discord
pub async fn connect(&mut self) -> Result<Value> {
self.inner.connect().await
}
/// Sets Discord Rich Presence activity
pub async fn set_activity(&mut self, activity: &crate::activity::Activity) -> Result<()> {
self.inner.set_activity(activity).await
}
/// Clears Discord Rich Presence activity
pub async fn clear_activity(&mut self) -> Result<Value> {
self.inner.clear_activity().await
}
/// Reconnect to Discord IPC
///
/// This method closes the existing connection and establishes a new one,
/// then performs the handshake again.
pub async fn reconnect(&mut self) -> Result<Value> {
// Create a new connection with the same configuration
let connection = if let Some(timeout) = self.timeout_ms {
AsyncStdConnection::new_with_config_and_timeout(self.pipe_config.clone(), timeout)
.await?
} else {
AsyncStdConnection::new_with_config(self.pipe_config.clone()).await?
};
// Replace the inner client with a new one
self.inner = AsyncDiscordIpcClient::new(self.client_id.clone(), connection);
// Perform handshake
self.inner.connect().await
}
/// Create a new async-std-based Discord IPC client (uses auto-discovery)
pub async fn new(client_id: impl Into<String>) -> Result<Self> {
Self::new_internal(client_id, None, None).await
}
/// Create a new async-std-based Discord IPC client with pipe configuration
pub async fn new_with_config(
client_id: impl Into<String>,
config: Option<PipeConfig>,
) -> Result<Self> {
Self::new_internal(client_id, config, None).await
}
/// Create a new async-std-based Discord IPC client with a connection timeout
pub async fn new_with_timeout(
client_id: impl Into<String>,
timeout_ms: u64,
) -> Result<Self> {
Self::new_internal(client_id, None, Some(timeout_ms)).await
}
/// Create a new async-std-based Discord IPC client with pipe configuration and timeout
pub async fn new_with_config_and_timeout(
client_id: impl Into<String>,
config: Option<PipeConfig>,
timeout_ms: u64,
) -> Result<Self> {
Self::new_internal(client_id, config, Some(timeout_ms)).await
}
/// Performs handshake with Discord with a timeout
pub async fn connect_with_timeout(&mut self, timeout_duration: Duration) -> Result<Value> {
match async_std::future::timeout(timeout_duration, self.inner.connect()).await {
Ok(result) => result,
Err(_) => Err(DiscordIpcError::connection_timeout(
timeout_duration.as_millis() as u64,
None,
)),
}
}
/// Send a raw IPC message
pub async fn send_message(
&mut self,
opcode: crate::ipc::Opcode,
payload: &Value,
) -> Result<()> {
self.inner.send_message(opcode, payload).await
}
/// Receive a raw IPC message
pub async fn recv_message(&mut self) -> Result<(crate::ipc::Opcode, Value)> {
self.inner.recv_message().await
}
}
/// Helper extension trait for async-std-specific timeout operations
pub trait AsyncStdClientExt {
/// Performs handshake with Discord with a timeout
///
/// # Arguments
///
/// * `timeout_duration` - The maximum time to wait for the connection
///
/// # Returns
///
/// A `Result` containing the Discord handshake response
///
/// # Errors
///
/// Returns `DiscordIpcError::ConnectionTimeout` if the operation times out
/// Returns `DiscordIpcError::HandshakeFailed` if the handshake fails
fn connect_with_timeout(
&mut self,
timeout_duration: Duration,
) -> impl std::future::Future<Output = Result<Value>> + Send;
}
impl AsyncStdClientExt for AsyncDiscordIpcClient<AsyncStdConnection> {
async fn connect_with_timeout(&mut self, timeout_duration: Duration) -> Result<Value> {
match async_std::future::timeout(timeout_duration, self.connect()).await {
Ok(result) => result,
Err(_) => Err(DiscordIpcError::connection_timeout(
timeout_duration.as_millis() as u64,
None,
)),
}
}
}
}
pub use client::*;