10 Commits

Author SHA1 Message Date
299408d10e Update Makefile to use x86_64-w64-mingw32-gcc 2024-03-07 06:48:54 +02:00
8b356b65b8 Add macOS support 2024-03-07 06:48:50 +02:00
66a20f51c4 Update platform check and add help option 2024-03-07 06:48:29 +02:00
d5af2277c5 Prevent installing as service on macOS 2024-03-07 06:47:56 +02:00
16291ec0fd Add macOS instructions 2024-03-07 06:47:35 +02:00
56168a6d3d Add socket system call constants 2024-01-26 03:35:34 +02:00
d2b23eb2e8 Check if it's running under Linux 2024-01-26 03:34:00 +02:00
cba4fa1e2b Update README.md 2023-12-17 01:25:19 +02:00
8c7b336c0e Update README.md 2023-10-13 19:20:39 +03:00
334c4e0479 Update README.md 2023-10-13 04:21:42 +03:00
5 changed files with 247 additions and 59 deletions

View File

@ -14,11 +14,11 @@ install: build
build: $(C_OBJECTS) build: $(C_OBJECTS)
$(info Linking) $(info Linking)
i686-w64-mingw32-gcc $(C_OBJECTS) $(LFLAGS) $(DBGFLAGS) -o build/bridge.exe x86_64-w64-mingw32-gcc $(C_OBJECTS) $(LFLAGS) $(DBGFLAGS) -o build/bridge.exe
%.o: %.c %.o: %.c
$(info Compiling $<) $(info Compiling $<)
i686-w64-mingw32-gcc $(CFLAGS) $(DBGFLAGS) -c $< -o $@ x86_64-w64-mingw32-gcc $(CFLAGS) $(DBGFLAGS) -c $< -o $@
clean: clean:
rm -f $(C_OBJECTS) build/bridge.exe rm -f $(C_OBJECTS) build/bridge.exe

View File

@ -1,5 +1,9 @@
# Discord RPC Bridge for Wine # Discord RPC Bridge for Wine
![GitHub](https://img.shields.io/github/license/EnderIce2/rpc-bridge)
![GitHub All Releases](https://img.shields.io/github/downloads/EnderIce2/rpc-bridge/total)
![GitHub release (latest by date)](https://img.shields.io/github/v/release/EnderIce2/rpc-bridge)
Simple bridge that allows you to use Discord Rich Presence with Wine games/software. Simple bridge that allows you to use Discord Rich Presence with Wine games/software.
Works by running a small program in the background that creates a [named pipe](https://learn.microsoft.com/en-us/windows/win32/ipc/named-pipes) `\\.\pipe\discord-ipc-0` inside the prefix and forwards all data to the pipe `/run/user/1000/discord-ipc-0`. Works by running a small program in the background that creates a [named pipe](https://learn.microsoft.com/en-us/windows/win32/ipc/named-pipes) `\\.\pipe\discord-ipc-0` inside the prefix and forwards all data to the pipe `/run/user/1000/discord-ipc-0`.
@ -13,12 +17,13 @@ This bridge takes advantage of the Windows service implementation in Wine, elimi
- [Discord RPC Bridge for Wine](#discord-rpc-bridge-for-wine) - [Discord RPC Bridge for Wine](#discord-rpc-bridge-for-wine)
- [Table of Contents](#table-of-contents) - [Table of Contents](#table-of-contents)
- [Installation \& Usage](#installation--usage) - [Installation \& Usage](#installation--usage)
- [Installing inside a prefix](#installing-inside-a-prefix) - [Installing inside a prefix](#installing-inside-a-prefix)
- [Wine (~/.wine)](#wine-wine) - [Wine (~/.wine)](#wine-wine)
- [Lutris](#lutris) - [Lutris](#lutris)
- [Steam](#steam) - [Steam](#steam)
- [If you use Flatpak](#if-you-use-flatpak) - [If you use Flatpak](#if-you-use-flatpak)
- [Run without installing the service](#run-without-installing-the-service) - [Run without installing the service](#run-without-installing-the-service)
- [About macOS](#about-macos)
- [Compiling from source](#compiling-from-source) - [Compiling from source](#compiling-from-source)
- [Command line arguments](#command-line-arguments) - [Command line arguments](#command-line-arguments)
- [Debugging](#debugging) - [Debugging](#debugging)
@ -76,7 +81,7 @@ If you prefer not to use the service for any reason, please refer to the [Run wi
## Run without installing the service ## Run without installing the service
If you prefer not to use the service, you can manually run `bridge.exe`` within the Wine prefix. If you prefer not to use the service, you can manually run `bridge.exe` within the Wine prefix.
This method is compatible with both Wine and Lutris. This method is compatible with both Wine and Lutris.
In Lutris, you can achieve this by adding the path to `bridge.exe` in the `Executable` field under `Game options`. In `Arguments` field, be sure to include the _Windows_ path to the game's executable. In Lutris, you can achieve this by adding the path to `bridge.exe` in the `Executable` field under `Game options`. In `Arguments` field, be sure to include the _Windows_ path to the game's executable.
@ -92,9 +97,15 @@ In Wine, all you need to do is run `bridge.exe`.
- When running the program manually without providing any arguments, it will simply initiate the bridge and wait indefinitely until it's closed. - When running the program manually without providing any arguments, it will simply initiate the bridge and wait indefinitely until it's closed.
## About macOS
The bridge works similarly on macOS as it does on Linux, but it can't be registered as a service due to TMPDIR limitations. macOS users must manually run `bridge.exe` when needed.
To run `bridge.exe` on macOS, navigate to its directory in the terminal and execute `wine bridge.exe`. (or double click it in the Finder)
## Compiling from source ## Compiling from source
- Install the `wine`, `i686-w64-mingw32-gcc` and `make` packages. - Install the `wine`, `x86_64-w64-mingw32-gcc` and `make` packages.
- Open a terminal in the directory that contains this file and run `make`. - Open a terminal in the directory that contains this file and run `make`.
- The compiled executable will be located in `build/bridge.exe`. - The compiled executable will be located in `build/bridge.exe`.

229
bridge.c
View File

@ -4,22 +4,48 @@
#include <assert.h> #include <assert.h>
#include <stdio.h> #include <stdio.h>
#define __NR_read 3 #define __linux_read 3
#define __NR_write 4 #define __linux_write 4
#define __NR_open 5 #define __linux_open 5
#define __NR_close 6 #define __linux_close 6
#define __NR_socketcall 102 #define __linux_munmap 91
#define __linux_socketcall 102
#define __linux_mmap2 192
#define __linux_socket 359
#define __linux_connect 362
#define __darwin_read 0x2000003
#define __darwin_write 0x2000004
#define __darwin_open 0x2000005
#define __darwin_close 0x2000006
#define __darwin_socket 0x2000061
#define __darwin_connect 0x2000062
#define __darwin_fcntl 0x200005C
#define __darwin_sysctl 0x20000CA
#define O_RDONLY 00 #define O_RDONLY 00
#define PROT_READ 1
#define PROT_WRITE 2
#define MAP_PRIVATE 0x02
#define MAP_FIXED 0x10
#define MAP_ANON 0x20
#define MAP_FAILED ((void *)-1)
#define SYS_SOCKET 1
#define SYS_CONNECT 3
#define likely(expr) (__builtin_expect(!!(expr), 1)) #define likely(expr) (__builtin_expect(!!(expr), 1))
#define unlikely(expr) (__builtin_expect(!!(expr), 0)) #define unlikely(expr) (__builtin_expect(!!(expr), 0))
#define force_inline \ #define force_inline \
__inline__ \ __inline__ \
__attribute__((__always_inline__, __gnu_inline__)) __attribute__((__always_inline__, __gnu_inline__))
#define naked __attribute__((naked))
typedef unsigned short sa_family_t; typedef unsigned short sa_family_t;
typedef char *caddr_t;
typedef unsigned socklen_t;
struct sockaddr_un struct sockaddr_un
{ {
sa_family_t sun_family; /* AF_UNIX */ sa_family_t sun_family; /* AF_UNIX */
@ -36,10 +62,11 @@ void print(char const *fmt, ...);
LPTSTR GetErrorMessage(); LPTSTR GetErrorMessage();
extern BOOL RunningAsService; extern BOOL RunningAsService;
BOOL RetryNewConnection; BOOL RetryNewConnection;
BOOL IsLinux;
static force_inline int syscall(int num, static force_inline int linux_syscall(int num,
intptr_t arg1, intptr_t arg2, intptr_t arg3, int arg1, int arg2, int arg3,
intptr_t arg4, intptr_t arg5, intptr_t arg6) int arg4, int arg5, int arg6)
{ {
int ret; int ret;
__asm__ __volatile__( __asm__ __volatile__(
@ -48,38 +75,124 @@ static force_inline int syscall(int num,
: "0"(num), "b"(arg1), "c"(arg2), : "0"(num), "b"(arg1), "c"(arg2),
"d"(arg3), "S"(arg4), "D"(arg5) "d"(arg3), "S"(arg4), "D"(arg5)
: "memory"); : "memory");
return ret; return ret;
} }
static naked int darwin_syscall(int num,
long arg1, long arg2, long arg3,
long arg4, long arg5, long arg6)
{
register long r10 __asm__("r10") = arg4;
register long r8 __asm__("r8") = arg5;
register long r9 __asm__("r9") = arg6;
__asm__ __volatile__(
"syscall\n"
"jae noerror\n"
"negq %%rax\n"
"noerror:\n"
"ret\n"
: "=a"(num)
: "a"(num), "D"(arg1), "S"(arg2), "d"(arg3), "r"(r10), "r"(r8), "r"(r9)
: "memory");
}
static inline int sys_read(int fd, void *buf, size_t count) static inline int sys_read(int fd, void *buf, size_t count)
{ {
return syscall(__NR_read, fd, (intptr_t)buf, count, 0, 0, 0); if (IsLinux)
return linux_syscall(__linux_read, fd, buf, count, 0, 0, 0);
else
return darwin_syscall(__darwin_read, fd, buf, count, 0, 0, 0);
} }
static inline int sys_write(int fd, const void *buf, size_t count) static inline int sys_write(int fd, const void *buf, size_t count)
{ {
return syscall(__NR_write, fd, (intptr_t)buf, count, 0, 0, 0); if (IsLinux)
return linux_syscall(__linux_write, fd, buf, count, 0, 0, 0);
else
return darwin_syscall(__darwin_write, fd, buf, count, 0, 0, 0);
} }
static inline int sys_open(const char *pathname, int flags, int mode) static inline int sys_open(const char *pathname, int flags, int mode)
{ {
return syscall(__NR_open, (intptr_t)pathname, flags, mode, 0, 0, 0); if (IsLinux)
return linux_syscall(__linux_open, pathname, flags, mode, 0, 0, 0);
else
return darwin_syscall(__darwin_open, pathname, flags, mode, 0, 0, 0);
} }
static inline int sys_close(int fd) static inline int sys_close(int fd)
{ {
return syscall(__NR_close, fd, 0, 0, 0, 0, 0); if (IsLinux)
return linux_syscall(__linux_close, fd, 0, 0, 0, 0, 0);
else
return darwin_syscall(__darwin_close, fd, 0, 0, 0, 0, 0);
}
static inline unsigned int *sys_mmap(unsigned int *addr, size_t length, int prot, int flags, int fd, off_t offset)
{
assert(IsLinux);
return linux_syscall(__linux_mmap2, addr, length, prot, flags, fd, offset);
}
static inline int sys_munmap(unsigned int *addr, size_t length)
{
assert(IsLinux);
return linux_syscall(__linux_munmap, addr, length, 0, 0, 0, 0);
} }
static inline int sys_socketcall(int call, unsigned long *args) static inline int sys_socketcall(int call, unsigned long *args)
{ {
return syscall(__NR_socketcall, call, (intptr_t)args, 0, 0, 0, 0); assert(IsLinux);
return linux_syscall(__linux_socketcall, call, args, 0, 0, 0, 0);
} }
char *linux_getenv(const char *name) static inline int sys_socket(int domain, int type, int protocol)
{ {
int fd = sys_open("/proc/self/environ", O_RDONLY, 0); if (IsLinux)
return linux_syscall(__linux_socket, domain, type, protocol, 0, 0, 0);
else
return darwin_syscall(__darwin_socket, domain, type, protocol, 0, 0, 0);
}
static inline int sys_connect(int s, caddr_t name, socklen_t namelen)
{
if (IsLinux)
return linux_syscall(__linux_connect, s, name, namelen, 0, 0, 0);
else
return darwin_syscall(__darwin_connect, s, name, namelen, 0, 0, 0);
}
char *native_getenv(const char *name)
{
if (!IsLinux)
{
char *value = getenv(name);
if (value == NULL)
{
print("Failed to get environment variable: %s\n", name);
return NULL;
}
return value;
}
/* I hope the 0x10000 is okay */
void *environStr = sys_mmap(0x10000, 4096, PROT_READ | PROT_WRITE,
MAP_PRIVATE | MAP_ANON | MAP_FIXED, -1, 0);
if (environStr == MAP_FAILED)
{
print("Failed to allocate environ string: %d\n", (intptr_t)environStr);
return NULL;
}
if ((uintptr_t)environStr > 0x7effffff)
print("Warning: environStr %p is above 2GB\n", environStr);
const char *linux_environ = "/proc/self/environ";
memcpy(environStr, linux_environ, strlen(linux_environ) + 1);
int fd = sys_open(environStr, O_RDONLY, 0);
sys_munmap(environStr, 4096);
if (fd < 0) if (fd < 0)
{ {
print("Failed to open /proc/self/environ: %d\n", fd); print("Failed to open /proc/self/environ: %d\n", fd);
@ -120,22 +233,24 @@ char *linux_getenv(const char *name)
void ConnectToSocket(int fd) void ConnectToSocket(int fd)
{ {
print("Connecting to socket\n"); print("Connecting to socket\n");
const char *runtime = linux_getenv("XDG_RUNTIME_DIR"); const char *runtime;
if (IsLinux)
runtime = native_getenv("XDG_RUNTIME_DIR");
else
runtime = native_getenv("TMPDIR");
if (runtime == NULL) if (runtime == NULL)
{ {
print("XDG_RUNTIME_DIR not set\n"); print("IPC directory not set\n");
if (!RunningAsService) if (!RunningAsService)
{ MessageBox(NULL, "IPC directory not set",
MessageBox(NULL,
"XDG_RUNTIME_DIR not set",
"Environment variable not set", "Environment variable not set",
MB_OK | MB_ICONSTOP); MB_OK | MB_ICONSTOP);
}
ExitProcess(1); ExitProcess(1);
} }
print("XDG_RUNTIME_DIR: %s\n", runtime); print("IPC directory: %s\n", runtime);
/* TODO: check for multiple discord instances and create a pipe for each */
const char *discordUnixPipes[] = { const char *discordUnixPipes[] = {
"/discord-ipc-0", "/discord-ipc-0",
"/snap.discord/discord-ipc-0", "/snap.discord/discord-ipc-0",
@ -156,12 +271,17 @@ void ConnectToSocket(int fd)
print("Connecting to %s\n", pipePath); print("Connecting to %s\n", pipePath);
unsigned long socketArgs[] = { if (IsLinux)
(unsigned long)fd, {
(unsigned long)&socketAddr, unsigned long socketArgs[] = {
sizeof(socketAddr)}; (unsigned long)fd,
(unsigned long)(intptr_t)&socketAddr,
sizeof(socketAddr)};
sockRet = sys_socketcall(3, socketArgs); sockRet = sys_socketcall(SYS_CONNECT, socketArgs);
}
else
sockRet = sys_connect(fd, (caddr_t)&socketAddr, sizeof(socketAddr));
free(pipePath); free(pipePath);
if (sockRet >= 0) if (sockRet >= 0)
@ -172,12 +292,9 @@ void ConnectToSocket(int fd)
{ {
print("socketcall failed for: %d\n", sockRet); print("socketcall failed for: %d\n", sockRet);
if (!RunningAsService) if (!RunningAsService)
{ MessageBox(NULL, "Failed to connect to Discord",
MessageBox(NULL,
"Failed to connect to Discord",
"Socket Connection failed", "Socket Connection failed",
MB_OK | MB_ICONSTOP); MB_OK | MB_ICONSTOP);
}
ExitProcess(1); ExitProcess(1);
} }
} }
@ -323,6 +440,17 @@ void PipeBufferOutThread(LPVOID lpParam)
void CreateBridge() void CreateBridge()
{ {
static void(CDECL * wine_get_host_version)(const char **sysname, const char **release);
HMODULE hntdll = GetModuleHandle("ntdll.dll");
wine_get_host_version = (void *)GetProcAddress(hntdll, "wine_get_host_version");
assert(wine_get_host_version);
const char *__sysname;
const char *__release;
wine_get_host_version(&__sysname, &__release);
IsLinux = strcmp(__sysname, "Linux") == 0;
print("Running on %s\n", __sysname);
LPCTSTR lpszPipename = TEXT("\\\\.\\pipe\\discord-ipc-0"); LPCTSTR lpszPipename = TEXT("\\\\.\\pipe\\discord-ipc-0");
NewConnection: NewConnection:
@ -330,11 +458,11 @@ NewConnection:
NULL, NULL, NULL, NULL,
NULL, NULL)) NULL, NULL))
{ {
print("Pipe already exists: %s\n",
GetErrorMessage());
if (!RunningAsService) if (!RunningAsService)
MessageBox(NULL, GetErrorMessage(), MessageBox(NULL, GetErrorMessage(),
NULL, MB_OK | MB_ICONSTOP); NULL, MB_OK | MB_ICONSTOP);
print("Pipe already exists: %s\n",
GetErrorMessage());
ExitProcess(1); ExitProcess(1);
} }
@ -346,11 +474,11 @@ NewConnection:
if (hPipe == INVALID_HANDLE_VALUE) if (hPipe == INVALID_HANDLE_VALUE)
{ {
print("Failed to create pipe: %s\n",
GetErrorMessage());
if (!RunningAsService) if (!RunningAsService)
MessageBox(NULL, GetErrorMessage(), MessageBox(NULL, GetErrorMessage(),
NULL, MB_OK | MB_ICONSTOP); NULL, MB_OK | MB_ICONSTOP);
print("Failed to create pipe: %s\n",
GetErrorMessage());
ExitProcess(1); ExitProcess(1);
} }
@ -358,30 +486,35 @@ NewConnection:
print("Waiting for pipe connection\n"); print("Waiting for pipe connection\n");
if (!ConnectNamedPipe(hPipe, NULL)) if (!ConnectNamedPipe(hPipe, NULL))
{ {
print("Failed to connect to pipe: %s\n",
GetErrorMessage());
if (!RunningAsService) if (!RunningAsService)
MessageBox(NULL, GetErrorMessage(), MessageBox(NULL, GetErrorMessage(),
NULL, MB_OK | MB_ICONSTOP); NULL, MB_OK | MB_ICONSTOP);
print("Failed to connect to pipe: %s\n",
GetErrorMessage());
ExitProcess(1); ExitProcess(1);
} }
print("Pipe connected\n"); print("Pipe connected\n");
unsigned long socketArgs[] = { int fd;
(unsigned long)AF_UNIX, if (IsLinux)
(unsigned long)SOCK_STREAM, {
0}; unsigned long socketArgs[] = {
(unsigned long)AF_UNIX,
int fd = sys_socketcall(1, socketArgs); (unsigned long)SOCK_STREAM,
0};
fd = sys_socketcall(SYS_SOCKET, socketArgs);
}
else
fd = sys_socket(AF_UNIX, SOCK_STREAM, 0);
print("Socket %d created\n", fd); print("Socket %d created\n", fd);
if (fd < 0) if (fd < 0)
{ {
if (!RunningAsService)
MessageBox(NULL, GetErrorMessage(),
NULL, MB_OK | MB_ICONSTOP);
print("Failed to create socket: %d\n", fd); print("Failed to create socket: %d\n", fd);
if (!RunningAsService)
MessageBox(NULL, "Failed to create socket",
NULL, MB_OK | MB_ICONSTOP);
ExitProcess(1); ExitProcess(1);
} }
@ -402,9 +535,9 @@ NewConnection:
if (hIn == NULL || hOut == NULL) if (hIn == NULL || hOut == NULL)
{ {
print("Failed to create threads: %s\n", GetErrorMessage());
if (!RunningAsService) if (!RunningAsService)
MessageBox(NULL, GetErrorMessage(), NULL, MB_OK | MB_ICONSTOP); MessageBox(NULL, GetErrorMessage(), NULL, MB_OK | MB_ICONSTOP);
print("Failed to create threads: %s\n", GetErrorMessage());
ExitProcess(1); ExitProcess(1);
} }

29
main.c
View File

@ -52,6 +52,23 @@ void DetectWine()
GetErrorMessage(), MB_OK | MB_ICONINFORMATION); GetErrorMessage(), MB_OK | MB_ICONINFORMATION);
ExitProcess(1); ExitProcess(1);
} }
static void(CDECL * wine_get_host_version)(const char **sysname, const char **release);
wine_get_host_version = (void *)GetProcAddress(hNTdll, "wine_get_host_version");
assert(wine_get_host_version);
const char *__sysname;
const char *__release;
wine_get_host_version(&__sysname, &__release);
if (strcmp(__sysname, "Linux") != 0 && strcmp(__sysname, "Darwin") != 0)
{
int result = MessageBox(NULL, "This program is designed for Linux and macOS only!\nDo you want to proceed?",
NULL, MB_YESNO | MB_ICONQUESTION);
if (result == IDYES)
return;
else if (result == IDNO)
ExitProcess(1);
}
} }
void print(char const *fmt, ...) void print(char const *fmt, ...)
@ -97,11 +114,19 @@ int main(int argc, char *argv[])
} }
return 0; return 0;
} }
else if (strcmp(argv[1], "--install") == 0)
if (strcmp(argv[1], "--install") == 0)
InstallService(); InstallService();
else if (strcmp(argv[1], "--uninstall") == 0) else if (strcmp(argv[1], "--uninstall") == 0)
RemoveService(); RemoveService();
else if (strcmp(argv[1], "--help") == 0)
{
printf("Usage:\n");
printf(" %s [args]\n", argv[0]);
printf(" --install - Install service\n");
printf(" --uninstall - Uninstall service\n");
printf(" --help - Show this help\n");
ExitProcess(0);
}
} }
CreateThread(NULL, 0, (LPTHREAD_START_ROUTINE)CreateBridge, CreateThread(NULL, 0, (LPTHREAD_START_ROUTINE)CreateBridge,

View File

@ -83,10 +83,29 @@ void ServiceMain(DWORD argc, LPTSTR *argv)
return; return;
} }
void DetectDarwin()
{
static void(CDECL * wine_get_host_version)(const char **sysname, const char **release);
wine_get_host_version = (void *)GetProcAddress(GetModuleHandle("ntdll.dll"),
"wine_get_host_version");
const char *__sysname;
const char *__release;
wine_get_host_version(&__sysname, &__release);
if (strcmp(__sysname, "Darwin") == 0)
{
/* FIXME: I don't know how to get the TMPDIR without getenv */
MessageBox(NULL, "Registering as a service is not supported on macOS at the moment.",
"Unsupported", MB_OK | MB_ICONINFORMATION);
ExitProcess(1);
}
}
void InstallService() void InstallService()
{ {
print("Registering to run on startup\n"); print("Registering to run on startup\n");
DetectDarwin();
SC_HANDLE schSCManager, schService; SC_HANDLE schSCManager, schService;
DWORD dwTagId; DWORD dwTagId;