commit fa88e36aeea56f1bb98d267b846aec1d834a125c Author: EnderIce2 Date: Fri Oct 13 03:54:34 2023 +0300 Initial commit diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..0393442 --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +*.o +*.exe \ No newline at end of file diff --git a/.vscode/c_cpp_properties.json b/.vscode/c_cpp_properties.json new file mode 100644 index 0000000..da0ec7f --- /dev/null +++ b/.vscode/c_cpp_properties.json @@ -0,0 +1,15 @@ +{ + "configurations": [ + { + "name": "Windows", + "includePath": [ + "${workspaceFolder}/**" + ], + "defines": [], + "cStandard": "c17", + "cppStandard": "gnu++17", + "intelliSenseMode": "windows-gcc-x86" + } + ], + "version": 4 +} \ No newline at end of file diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..eb0dcdb --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,8 @@ +{ + "C_Cpp.default.compilerPath": "/usr/bin/i686-w64-mingw32-gcc", + "files.associations": { + "*.su": "tsv", + "windows.h": "c", + "namedpipeapi.h": "c" + } +} \ No newline at end of file diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..02b29c3 --- /dev/null +++ b/LICENSE @@ -0,0 +1,19 @@ +Copyright (c) 2023 EnderIce2 + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..6c43bb6 --- /dev/null +++ b/Makefile @@ -0,0 +1,24 @@ +C_SOURCES = $(shell find ./ -type f -name '*.c') +C_OBJECTS = $(C_SOURCES:.c=.o) + +CFLAGS = -std=c17 -Wno-int-conversion +LFLAGS = + +# DBGFLAGS = -Wl,--export-all-symbols -g -O0 -ggdb3 -Wall + +all: build + +# This is valid only if this directory is a subdirectory of drive_c +install: build + cp build/bridge.exe ../bridge.exe + +build: $(C_OBJECTS) + $(info Linking) + i686-w64-mingw32-gcc $(C_OBJECTS) $(LFLAGS) $(DBGFLAGS) -o build/bridge.exe + +%.o: %.c + $(info Compiling $<) + i686-w64-mingw32-gcc $(CFLAGS) $(DBGFLAGS) -c $< -o $@ + +clean: + rm -f $(C_OBJECTS) build/bridge.exe diff --git a/README.md b/README.md new file mode 100644 index 0000000..bef3aff --- /dev/null +++ b/README.md @@ -0,0 +1,123 @@ +# Discord RPC Bridge for Wine + +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`. + +This bridge takes advantage of the Windows service implementation in Wine, eliminating the need to manually run any programs. + +--- + +## Table of Contents + +- [Discord RPC Bridge for Wine](#discord-rpc-bridge-for-wine) + - [Table of Contents](#table-of-contents) + - [Installation \& Usage](#installation--usage) + - [Installing inside a prefix](#installing-inside-a-prefix) + - [Wine (~/.wine)](#wine-wine) + - [Lutris](#lutris) + - [Steam](#steam) + - [If you use Flatpak](#if-you-use-flatpak) + - [Run without installing the service](#run-without-installing-the-service) + - [Compiling from source](#compiling-from-source) + - [Command line arguments](#command-line-arguments) + - [Debugging](#debugging) + - [Demo](#demo) + - [Credits](#credits) + +## Installation & Usage + +Installation will place `bridge.exe` to `C:\bridge.exe` (if it's not already there) and create a Windows service. This service will automatically start when the prefix is used. + +If you prefer not to use the service for any reason, please refer to the [Run without installing the service](#run-without-installing-the-service) section. + +#### Installing inside a prefix + +##### Wine (~/.wine) + +- Open terminal in `build` +- Run `$ wine cmd` and `C:\> install.bat` + - ![wine install](imgs/wine_install.gif) +- To remove, run `C:\> remove.bat` + - ![wine remove](imgs/wine_remove.gif) +- Note: Copying files are not required here. + +##### Lutris + +- Right click on the game and select `Browse files` + - ![lutris browse](imgs/lutris_browse.gif) +- Copy contents of `build` to the game's prefix `drive_c` + - ![lutris copy](imgs/lutris_copy.gif) +- To install open the console + - ![lutris console](imgs/lutris_console.gif) +- And run `C:\> install.bat` (make sure you are in `C:\`!) + - ![lutris install](imgs/lutris_install.gif) + +##### Steam + +- Open [Protontricks](https://github.com/Matoking/protontricks) and select the game you want to install the bridge to +- Select `Select the default wineprefix` +- Select `Browse files` and copy contents of `build` to the game's prefix `drive_c` +- Select `Run a Wine cmd shell` and run `C:\> install.bat` + - If you are not in `C:\`, type `c:` and press enter +- ![protontricks](imgs/steam_protontricks.png) + +#### If you use Flatpak + +- If you are running Steam, Lutris, etc. in a Flatpak, you will need to allow the bridge to access the `/run/user/1000/discord-ipc-0` file. + - ##### By using [Flatseal](https://flathub.org/apps/details/com.github.tchx84.Flatseal) + - Add `xdg-run/discord-ipc-0` under `Filesystems` category + - ![flatseal](imgs/flatseal_permission.png) + - ##### By using the terminal + - Per application + - `flatpak override --filesystem=xdg-run/discord-ipc-0 ` + - Globally + - `flatpak override --user --filesystem=xdg-run/discord-ipc-0` + +## Run without installing the service + +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. + +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. +Example: +- Without bridge: + - Executable `/mnt/games/lutris/league-of-legends/drive_c/Riot Games/League of Legends/LeagueClient.exe` + - Arguments `--locale=en_US --launch-product=league_of_legends --launch-patchline=live` +- With bridge: + - Executable `/mnt/games/lutris/league-of-legends/drive_c/bridge.exe` + - Arguments `"C:\Riot Games\League of Legends\LeagueClient.exe" --locale=en_US --launch-product=league_of_legends --launch-patchline=live` + +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. + +## Compiling from source + +- Install the `wine`, `i686-w64-mingw32-gcc` and `make` packages. +- Open a terminal in the directory that contains this file and run `make`. +- The compiled executable will be located in `build/bridge.exe`. + +## Command line arguments + +- `--install` - Installs the service +- `--remove` - Removes the service +- `--service` - Reserved for the service +- `[Target Executable]` - Starts the bridge and the game + - Example: `bridge.exe "C:\Riot Games\League of Legends\LeagueClient.exe" --locale=en_US --launch-product=league_of_legends --launch-patchline=live` + - Note: The game executable must be enclosed in quotes. The rest of the arguments are passed to the target executable. + +## Debugging + +The bridge will write the logs in `C:\bridge.log`. + +## Demo + +![League of Legends running under Wine](imgs/lutris_lol.png) +![Among Us running from Steam](imgs/steam_amongus.png) + +## Credits + +This project is inspired by [wine-discord-ipc-bridge](https://github.com/0e4ef622/wine-discord-ipc-bridge). + +--- diff --git a/bridge.c b/bridge.c new file mode 100644 index 0000000..76f0b4f --- /dev/null +++ b/bridge.c @@ -0,0 +1,438 @@ +#include +#include +#include +#include +#include + +#define __NR_read 3 +#define __NR_write 4 +#define __NR_open 5 +#define __NR_close 6 +#define __NR_socketcall 102 + +#define O_RDONLY 00 + +#define likely(expr) (__builtin_expect(!!(expr), 1)) +#define unlikely(expr) (__builtin_expect(!!(expr), 0)) + +#define force_inline \ + __inline__ \ + __attribute__((__always_inline__, __gnu_inline__)) + +typedef unsigned short sa_family_t; +struct sockaddr_un +{ + sa_family_t sun_family; /* AF_UNIX */ + char sun_path[108]; /* Pathname */ +}; + +typedef struct +{ + int fd; + HANDLE hPipe; +} bridge_thread; + +void print(char const *fmt, ...); +LPTSTR GetErrorMessage(); +extern BOOL RunningAsService; +BOOL RetryNewConnection; + +static force_inline int syscall(int num, + intptr_t arg1, intptr_t arg2, intptr_t arg3, + intptr_t arg4, intptr_t arg5, intptr_t arg6) +{ + int ret; + __asm__ __volatile__( + "int $0x80\n" + : "=a"(ret) + : "0"(num), "b"(arg1), "c"(arg2), + "d"(arg3), "S"(arg4), "D"(arg5) + : "memory"); + + return ret; +} + +static inline int sys_read(int fd, void *buf, size_t count) +{ + return syscall(__NR_read, fd, (intptr_t)buf, count, 0, 0, 0); +} + +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); +} + +static inline int sys_open(const char *pathname, int flags, int mode) +{ + return syscall(__NR_open, (intptr_t)pathname, flags, mode, 0, 0, 0); +} + +static inline int sys_close(int fd) +{ + return syscall(__NR_close, fd, 0, 0, 0, 0, 0); +} + +static inline int sys_socketcall(int call, unsigned long *args) +{ + return syscall(__NR_socketcall, call, (intptr_t)args, 0, 0, 0, 0); +} + +char *linux_getenv(const char *name) +{ + int fd = sys_open("/proc/self/environ", O_RDONLY, 0); + if (fd < 0) + { + print("Failed to open /proc/self/environ: %d\n", fd); + return NULL; + } + + char buffer[4096]; + char *result = NULL; + int bytesRead; + + while ((bytesRead = sys_read(fd, buffer, sizeof(buffer) - 1)) > 0) + { + buffer[bytesRead] = '\0'; + char *env = buffer; + while (*env) + { + if (strstr(env, name) == env) + { + env += strlen(name); + if (*env == '=') + { + env++; + result = strdup(env); + break; + } + } + env += strlen(env) + 1; + } + + if (result) + break; + } + + sys_close(fd); + return result; +} + +void ConnectToSocket(int fd) +{ + print("Connecting to socket\n"); + const char *runtime = linux_getenv("XDG_RUNTIME_DIR"); + if (runtime == NULL) + { + print("XDG_RUNTIME_DIR not set\n"); + if (!RunningAsService) + { + MessageBox(NULL, + "XDG_RUNTIME_DIR not set", + "Environment variable not set", + MB_OK | MB_ICONSTOP); + } + ExitProcess(1); + } + + print("XDG_RUNTIME_DIR: %s\n", runtime); + + const char *discordUnixPipes[] = { + "/discord-ipc-0", + "/snap.discord/discord-ipc-0", + "/app/com.discordapp.Discord/discord-ipc-0", + }; + + struct sockaddr_un socketAddr; + socketAddr.sun_family = AF_UNIX; + char *pipePath = NULL; + int sockRet = -1; + + for (int i = 0; i < sizeof(discordUnixPipes) / sizeof(discordUnixPipes[0]); i++) + { + pipePath = malloc(strlen(runtime) + strlen(discordUnixPipes[i]) + 1); + strcpy(pipePath, runtime); + strcat(pipePath, discordUnixPipes[i]); + strcpy_s(socketAddr.sun_path, sizeof(socketAddr.sun_path), pipePath); + + print("Connecting to %s\n", pipePath); + + unsigned long socketArgs[] = { + (unsigned long)fd, + (unsigned long)&socketAddr, + sizeof(socketAddr)}; + + sockRet = sys_socketcall(3, socketArgs); + + free(pipePath); + if (sockRet >= 0) + break; + } + + if (sockRet < 0) + { + print("socketcall failed for: %d\n", sockRet); + if (!RunningAsService) + { + MessageBox(NULL, + "Failed to connect to Discord", + "Socket Connection failed", + MB_OK | MB_ICONSTOP); + } + ExitProcess(1); + } +} + +void PipeBufferInThread(LPVOID lpParam) +{ + bridge_thread *bt = (bridge_thread *)lpParam; + print("In thread started using fd %d and pipe %#x\n", + bt->fd, bt->hPipe); + while (TRUE) + { + char buffer[1024]; + int read = sys_read(bt->fd, buffer, sizeof(buffer)); + + if (unlikely(read < 0)) + { + print("Failed to read from unix pipe: %d\n", + GetErrorMessage()); + Sleep(1000); + continue; + } + + if (unlikely(read == 0)) + { + print("EOF\n"); + Sleep(1000); + continue; + } + + print("Reading %d bytes from unix pipe: \"", read); + for (int i = 0; i < read; i++) + print("%c", buffer[i]); + print("\"\n"); + + DWORD dwWritten; + if (unlikely(!WriteFile(bt->hPipe, buffer, read, + &dwWritten, NULL))) + { + if (GetLastError() == ERROR_BROKEN_PIPE) + { + RetryNewConnection = TRUE; + print("In Broken pipe\n"); + break; + } + + print("Failed to read from pipe: %d\n", + GetErrorMessage()); + Sleep(1000); + continue; + } + + if (unlikely(dwWritten < 0)) + { + print("Failed to write to pipe: %d\n", + GetErrorMessage()); + Sleep(1000); + continue; + } + + while (dwWritten < read) + { + int last_written = dwWritten; + if (unlikely(!WriteFile(bt->hPipe, buffer + dwWritten, + read - dwWritten, &dwWritten, NULL))) + { + if (GetLastError() == ERROR_BROKEN_PIPE) + { + RetryNewConnection = TRUE; + print("In Broken pipe\n"); + break; + } + + print("Failed to read from pipe: %d\n", + GetErrorMessage()); + Sleep(1000); + continue; + } + + if (unlikely(last_written == dwWritten)) + { + print("Failed to write to pipe: %d\n", + GetErrorMessage()); + Sleep(1000); + continue; + } + } + } +} + +void PipeBufferOutThread(LPVOID lpParam) +{ + bridge_thread *bt = (bridge_thread *)lpParam; + print("Out thread started using fd %d and pipe %#x\n", + bt->fd, bt->hPipe); + while (TRUE) + { + char buffer[1024]; + DWORD dwRead; + + if (unlikely(!ReadFile(bt->hPipe, buffer, sizeof(buffer), + &dwRead, NULL))) + { + if (GetLastError() == ERROR_BROKEN_PIPE) + { + RetryNewConnection = TRUE; + print("Out Broken pipe\n"); + break; + } + + print("Failed to read from pipe: %d\n", + GetErrorMessage()); + Sleep(1000); + continue; + } + + print("Writing %d bytes to unix pipe: \"", dwRead); + for (int i = 0; i < dwRead; i++) + print("%c", buffer[i]); + print("\"\n"); + + int written = sys_write(bt->fd, buffer, dwRead); + if (unlikely(written < 0)) + { + print("Failed to write to socket: %d\n", + written); + continue; + } + + while (written < dwRead) + { + int last_written = written; + written += sys_write(bt->fd, buffer + written, dwRead - written); + if (unlikely(last_written == written)) + { + print("Failed to write to socket: %d\n", + GetErrorMessage()); + Sleep(1000); + continue; + } + } + } +} + +void CreateBridge() +{ + LPCTSTR lpszPipename = TEXT("\\\\.\\pipe\\discord-ipc-0"); + +NewConnection: + if (GetNamedPipeInfo((HANDLE)lpszPipename, + NULL, NULL, + NULL, NULL)) + { + if (!RunningAsService) + MessageBox(NULL, GetErrorMessage(), + NULL, MB_OK | MB_ICONSTOP); + print("Pipe already exists: %s\n", + GetErrorMessage()); + ExitProcess(1); + } + + HANDLE hPipe = + CreateNamedPipe("\\\\.\\pipe\\discord-ipc-0", + PIPE_ACCESS_DUPLEX, + PIPE_TYPE_BYTE | PIPE_READMODE_BYTE | PIPE_WAIT, + 1, 1024, 1024, 0, NULL); + + if (hPipe == INVALID_HANDLE_VALUE) + { + if (!RunningAsService) + MessageBox(NULL, GetErrorMessage(), + NULL, MB_OK | MB_ICONSTOP); + print("Failed to create pipe: %s\n", + GetErrorMessage()); + ExitProcess(1); + } + + print("Pipe %s(%#x) created\n", lpszPipename, hPipe); + print("Waiting for pipe connection\n"); + if (!ConnectNamedPipe(hPipe, NULL)) + { + if (!RunningAsService) + MessageBox(NULL, GetErrorMessage(), + NULL, MB_OK | MB_ICONSTOP); + print("Failed to connect to pipe: %s\n", + GetErrorMessage()); + ExitProcess(1); + } + print("Pipe connected\n"); + + unsigned long socketArgs[] = { + (unsigned long)AF_UNIX, + (unsigned long)SOCK_STREAM, + 0}; + + int fd = sys_socketcall(1, socketArgs); + + print("Socket %d created\n", fd); + + if (fd < 0) + { + if (!RunningAsService) + MessageBox(NULL, GetErrorMessage(), + NULL, MB_OK | MB_ICONSTOP); + print("Failed to create socket: %d\n", fd); + ExitProcess(1); + } + + ConnectToSocket(fd); + print("Connected to Discord\n"); + + bridge_thread bt = {fd, hPipe}; + + HANDLE hIn = CreateThread(NULL, 0, + (LPTHREAD_START_ROUTINE)PipeBufferInThread, + (LPVOID)&bt, + 0, NULL); + + HANDLE hOut = CreateThread(NULL, 0, + (LPTHREAD_START_ROUTINE)PipeBufferOutThread, + (LPVOID)&bt, + 0, NULL); + + if (hIn == NULL || hOut == NULL) + { + if (!RunningAsService) + MessageBox(NULL, GetErrorMessage(), NULL, MB_OK | MB_ICONSTOP); + print("Failed to create threads: %s\n", GetErrorMessage()); + ExitProcess(1); + } + + print("Waiting for threads to exit\n"); + WaitForSingleObject(hOut, INFINITE); + print("Buffer out thread exited\n"); + + if (RetryNewConnection) + { + RetryNewConnection = FALSE; + print("Retrying new connection\n"); + if (!TerminateThread(hIn, 0)) + print("Failed to terminate thread: %s\n", + GetErrorMessage()); + + if (!TerminateThread(hOut, 0)) + print("Failed to terminate thread: %s\n", + GetErrorMessage()); + + sys_close(fd); + CloseHandle(hOut); + CloseHandle(hIn); + CloseHandle(hPipe); + Sleep(1000); + goto NewConnection; + } + + WaitForSingleObject(hIn, INFINITE); + print("Buffer in thread exited\n"); + CloseHandle(hPipe); +} diff --git a/build/install.bat b/build/install.bat new file mode 100644 index 0000000..f050ed3 --- /dev/null +++ b/build/install.bat @@ -0,0 +1,3 @@ +@echo off +START /WAIT bridge.exe --install + diff --git a/build/remove.bat b/build/remove.bat new file mode 100644 index 0000000..565203a --- /dev/null +++ b/build/remove.bat @@ -0,0 +1,3 @@ +@echo off +START /WAIT bridge.exe --uninstall + diff --git a/game.c b/game.c new file mode 100644 index 0000000..88dc11d --- /dev/null +++ b/game.c @@ -0,0 +1,145 @@ +#include +#include +#include +#include +#include +#include + +void print(char const *fmt, ...); +LPTSTR GetErrorMessage(); + +BOOL IsChildProcess(DWORD parentID, DWORD childID) +{ + HANDLE hSnapshot = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0); + PROCESSENTRY32 pe32; + + if (hSnapshot == INVALID_HANDLE_VALUE) + return FALSE; + + pe32.dwSize = sizeof(PROCESSENTRY32); + + if (Process32First(hSnapshot, &pe32)) + { + do + { + if (pe32.th32ParentProcessID == parentID && pe32.th32ProcessID == childID) + { + CloseHandle(hSnapshot); + return TRUE; + } + } while (Process32Next(hSnapshot, &pe32)); + } + + CloseHandle(hSnapshot); + return FALSE; +} + +BOOL FileExists(LPCTSTR szPath) +{ + DWORD dwAttrib = GetFileAttributes(szPath); + return (dwAttrib != INVALID_FILE_ATTRIBUTES && + !(dwAttrib & FILE_ATTRIBUTE_DIRECTORY)); +} + +void LaunchGame(int argc, char **argv) +{ + char *gamePath = ""; + int startArg = 1; + if (argc > 1) + { + print("Checking if %s is a valid file\n", argv[1]); + if (FileExists(argv[1])) + { + print("Checking if %s is a valid executable\n", argv[1]); + DWORD dwBinaryType; + if (!GetBinaryType(argv[1], &dwBinaryType)) + { + MessageBox(NULL, GetErrorMessage(), + NULL, MB_OK | MB_ICONSTOP); + ExitProcess(1); + } + print("Executable type: %d\n", dwBinaryType); + + gamePath = argv[1]; + startArg = 2; + } + else + { + MessageBox(NULL, "Invalid game path specified", + NULL, MB_OK | MB_ICONSTOP); + print("%s is not a valid file\n", argv[1]); + ExitProcess(1); + } + } + else if (argc == 1) + { + print("No game path specified. Idling...\n"); + while (TRUE) + Sleep(1000); + } + else + { + MessageBox(NULL, "No game path specified", + NULL, MB_OK | MB_ICONSTOP); + ExitProcess(1); + } + + STARTUPINFO game_si; + PROCESS_INFORMATION game_pi; + + ZeroMemory(&game_si, sizeof(STARTUPINFO)); + game_si.cb = sizeof(STARTUPINFO); + game_si.hStdOutput = INVALID_HANDLE_VALUE; + game_si.hStdError = INVALID_HANDLE_VALUE; + game_si.dwFlags |= STARTF_USESTDHANDLES; + + char *gameArgs = LocalAlloc(LPTR, 512); + for (int i = startArg; i < argc; i++) + { + assert(strlen(gameArgs) + strlen(argv[i]) < 512); + gameArgs = strcat(gameArgs, argv[i]); + gameArgs = strcat(gameArgs, " "); + } + print("Launching \"%s\" with arguments \"%s\"\n", gamePath, gameArgs); + if (!CreateProcess(gamePath, gameArgs, NULL, NULL, FALSE, + 0, NULL, NULL, &game_si, &game_pi)) + { + MessageBox(NULL, GetErrorMessage(), NULL, MB_OK | MB_ICONSTOP); + ExitProcess(1); + } + LocalFree(gameArgs); + DWORD parentID = game_pi.dwProcessId; + print("Waiting for PID %d to exit...\n", game_pi.dwProcessId); + WaitForSingleObject(game_pi.hProcess, INFINITE); + print("PID %d exited\n", game_pi.dwProcessId); + + HANDLE hSnapshot = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0); + PROCESSENTRY32 pe32; + + if (hSnapshot != INVALID_HANDLE_VALUE) + { + pe32.dwSize = sizeof(PROCESSENTRY32); + if (Process32First(hSnapshot, &pe32)) + { + do + { + if (IsChildProcess(parentID, pe32.th32ProcessID)) + { + WaitForSingleObject(OpenProcess(SYNCHRONIZE, FALSE, + pe32.th32ProcessID), + INFINITE); + print("Waiting for PID %d\n", pe32.th32ProcessID); + } + } while (Process32Next(hSnapshot, &pe32)); + } + CloseHandle(hSnapshot); + } + else + { + MessageBox(NULL, GetErrorMessage(), NULL, MB_OK | MB_ICONSTOP); + ExitProcess(0); + } + + CloseHandle(game_pi.hProcess); + print("Game exited\n"); +} diff --git a/imgs/flatseal_permission.png b/imgs/flatseal_permission.png new file mode 100644 index 0000000..fd81fb0 Binary files /dev/null and b/imgs/flatseal_permission.png differ diff --git a/imgs/lutris_browse.gif b/imgs/lutris_browse.gif new file mode 100644 index 0000000..c008170 Binary files /dev/null and b/imgs/lutris_browse.gif differ diff --git a/imgs/lutris_console.gif b/imgs/lutris_console.gif new file mode 100644 index 0000000..7e05393 Binary files /dev/null and b/imgs/lutris_console.gif differ diff --git a/imgs/lutris_copy.gif b/imgs/lutris_copy.gif new file mode 100644 index 0000000..61c9707 Binary files /dev/null and b/imgs/lutris_copy.gif differ diff --git a/imgs/lutris_install.gif b/imgs/lutris_install.gif new file mode 100644 index 0000000..8cb56ab Binary files /dev/null and b/imgs/lutris_install.gif differ diff --git a/imgs/lutris_lol.png b/imgs/lutris_lol.png new file mode 100644 index 0000000..61d9b54 Binary files /dev/null and b/imgs/lutris_lol.png differ diff --git a/imgs/steam_amongus.png b/imgs/steam_amongus.png new file mode 100644 index 0000000..c8b4377 Binary files /dev/null and b/imgs/steam_amongus.png differ diff --git a/imgs/steam_protontricks.png b/imgs/steam_protontricks.png new file mode 100644 index 0000000..531c228 Binary files /dev/null and b/imgs/steam_protontricks.png differ diff --git a/imgs/wine_install.gif b/imgs/wine_install.gif new file mode 100644 index 0000000..d6de19a Binary files /dev/null and b/imgs/wine_install.gif differ diff --git a/imgs/wine_remove.gif b/imgs/wine_remove.gif new file mode 100644 index 0000000..77ddfff Binary files /dev/null and b/imgs/wine_remove.gif differ diff --git a/main.c b/main.c new file mode 100644 index 0000000..cc77d48 --- /dev/null +++ b/main.c @@ -0,0 +1,114 @@ +#include +#include +#include +#include + +FILE *g_logFile = NULL; +BOOL RunningAsService = FALSE; + +void CreateBridge(); +void LaunchGame(int argc, char **argv); +void ServiceMain(int argc, char *argv[]); +void InstallService(); +void RemoveService(); + +LPTSTR GetErrorMessage() +{ + DWORD err = GetLastError(); + if (err == 0) + return "Error"; + + WORD wLangID = MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT); + + LPSTR buffer = NULL; + size_t size = FormatMessage(FORMAT_MESSAGE_ALLOCATE_BUFFER | + FORMAT_MESSAGE_FROM_SYSTEM | + FORMAT_MESSAGE_IGNORE_INSERTS, + NULL, err, wLangID, (LPSTR)&buffer, 0, NULL); + + LPTSTR message = NULL; + if (size > 0) + { + message = (LPTSTR)LocalAlloc(LPTR, (size + 1) * sizeof(TCHAR)); + if (message != NULL) + { + memcpy(message, buffer, size * sizeof(TCHAR)); + message[size] = '\0'; + } + LocalFree(buffer); + } + + return message; +} + +void DetectWine() +{ + HMODULE hNTdll = GetModuleHandle("ntdll.dll"); + if (!hNTdll) + ExitProcess(1); + if (!GetProcAddress(hNTdll, "wine_get_version")) + { + MessageBox(NULL, "This program is only intended to run under Wine.", + GetErrorMessage(), MB_OK | MB_ICONINFORMATION); + ExitProcess(1); + } +} + +void print(char const *fmt, ...) +{ + va_list args; + va_start(args, fmt); + vprintf(fmt, args); + va_end(args); + va_start(args, fmt); + vfprintf(g_logFile, fmt, args); + va_end(args); +} + +int main(int argc, char *argv[]) +{ + DetectWine(); + char *logFilePath = "C:\\bridge.log"; + g_logFile = fopen(logFilePath, "w"); + if (g_logFile == NULL) + { + printf("Failed to open logs file: %ld\n", + GetLastError()); + ExitProcess(1); + } + + if (argc > 1) + { + if (strcmp(argv[1], "--service") == 0) + { + RunningAsService = TRUE; + print("Running as service\n"); + + SERVICE_TABLE_ENTRY ServiceTable[] = + { + {"rpc-bridge", (LPSERVICE_MAIN_FUNCTION)ServiceMain}, + {NULL, NULL}, + }; + + if (StartServiceCtrlDispatcher(ServiceTable) == FALSE) + { + print("Service failed to start\n"); + return GetLastError(); + } + return 0; + } + + if (strcmp(argv[1], "--install") == 0) + InstallService(); + else if (strcmp(argv[1], "--uninstall") == 0) + RemoveService(); + } + + CreateThread(NULL, 0, (LPTHREAD_START_ROUTINE)CreateBridge, + NULL, 0, NULL); + Sleep(500); + LaunchGame(argc, argv); + + fclose(g_logFile); + ExitProcess(0); +} diff --git a/service.c b/service.c new file mode 100644 index 0000000..49adea7 --- /dev/null +++ b/service.c @@ -0,0 +1,188 @@ +#include +#include +#include + +SERVICE_STATUS g_ServiceStatus; +SERVICE_STATUS_HANDLE g_StatusHandle = NULL; + +void print(char const *fmt, ...); +void CreateBridge(); +LPTSTR GetErrorMessage(); + +void WINAPI ServiceCtrlHandler(DWORD CtrlCode) +{ + switch (CtrlCode) + { + case SERVICE_CONTROL_STOP: + case SERVICE_ACCEPT_SHUTDOWN: + { + g_ServiceStatus.dwCurrentState = SERVICE_STOP_PENDING; + SetServiceStatus(g_StatusHandle, &g_ServiceStatus); + + print("Stopping service\n"); + + /* ... */ + + g_ServiceStatus.dwCurrentState = SERVICE_STOPPED; + SetServiceStatus(g_StatusHandle, &g_ServiceStatus); + break; + } + default: + { + print("Unrecognized service control code %d\n", CtrlCode); + } + } +} + +DWORD WINAPI ServiceWorkerThread(LPVOID lpParam) +{ + print("Service started\n"); + CreateBridge(); + return ERROR_SUCCESS; +} + +void ServiceMain(DWORD argc, LPTSTR *argv) +{ + print("Starting service\n"); + g_StatusHandle = RegisterServiceCtrlHandler("rpc-bridge", + ServiceCtrlHandler); + if (g_StatusHandle == NULL) + { + print("Failed to register service control handler\n"); + return; + } + + ZeroMemory(&g_ServiceStatus, sizeof(g_ServiceStatus)); + + g_ServiceStatus.dwServiceType = SERVICE_WIN32_OWN_PROCESS; + g_ServiceStatus.dwCurrentState = SERVICE_START_PENDING; + SetServiceStatus(g_StatusHandle, &g_ServiceStatus); + + g_ServiceStatus.dwServiceType = SERVICE_WIN32_OWN_PROCESS; + g_ServiceStatus.dwCurrentState = SERVICE_START_PENDING; + SetServiceStatus(g_StatusHandle, &g_ServiceStatus); + + g_ServiceStatus.dwControlsAccepted = SERVICE_ACCEPT_STOP | + SERVICE_ACCEPT_SHUTDOWN; + g_ServiceStatus.dwCurrentState = SERVICE_RUNNING; + g_ServiceStatus.dwWin32ExitCode = 0; + g_ServiceStatus.dwCheckPoint = 0; + SetServiceStatus(g_StatusHandle, &g_ServiceStatus); + + HANDLE hThread = CreateThread(NULL, 0, ServiceWorkerThread, + NULL, 0, NULL); + WaitForSingleObject(hThread, INFINITE); + + g_ServiceStatus.dwControlsAccepted = 0; + g_ServiceStatus.dwCurrentState = SERVICE_STOPPED; + g_ServiceStatus.dwWin32ExitCode = 0; + g_ServiceStatus.dwCheckPoint = 3; + SetServiceStatus(g_StatusHandle, &g_ServiceStatus); + + print("Service stopped.\n"); + return; +} + +void InstallService() +{ + print("Registering to run on startup\n"); + + SC_HANDLE schSCManager, schService; + DWORD dwTagId; + + schSCManager = OpenSCManager(NULL, NULL, SC_MANAGER_CREATE_SERVICE); + if (schSCManager == NULL) + { + MessageBox(NULL, GetErrorMessage(), + NULL, MB_OK | MB_ICONSTOP); + ExitProcess(1); + } + + schService = + CreateService(schSCManager, + "rpc-bridge", "Wine RPC Bridge", + SERVICE_ALL_ACCESS, SERVICE_WIN32_OWN_PROCESS, + SERVICE_AUTO_START, SERVICE_ERROR_NORMAL, + "C:\\bridge.exe --service", + NULL, &dwTagId, NULL, NULL, NULL); + + if (schService == NULL) + { + MessageBox(NULL, GetErrorMessage(), + NULL, MB_OK | MB_ICONSTOP); + ExitProcess(1); + } + else + { + char filename[MAX_PATH]; + GetModuleFileName(NULL, filename, MAX_PATH); + CopyFile(filename, "C:\\bridge.exe", FALSE); + + print("Service installed successfully\n"); + CloseServiceHandle(schService); + } + + CloseServiceHandle(schSCManager); + ExitProcess(0); +} + +void RemoveService() +{ + print("Unregistering from startup\n"); + + SC_HANDLE schSCManager, schService; + SERVICE_STATUS ssSvcStatus; + + schSCManager = OpenSCManager(NULL, NULL, SC_MANAGER_CONNECT); + if (schSCManager == NULL) + { + MessageBox(NULL, GetErrorMessage(), + NULL, MB_OK | MB_ICONSTOP); + ExitProcess(1); + } + + schService = OpenService(schSCManager, "rpc-bridge", + SERVICE_STOP | SERVICE_QUERY_STATUS | DELETE); + + if (schService == NULL) + { + MessageBox(NULL, GetErrorMessage(), + NULL, MB_OK | MB_ICONSTOP); + ExitProcess(1); + } + + if (ControlService(schService, SERVICE_CONTROL_STOP, &ssSvcStatus)) + { + print("Stopping service\n"); + Sleep(1000); + + while (QueryServiceStatus(schService, &ssSvcStatus)) + { + if (ssSvcStatus.dwCurrentState == SERVICE_STOP_PENDING) + { + print("Waiting for service to stop\n"); + Sleep(1000); + } + else + break; + } + + if (ssSvcStatus.dwCurrentState == SERVICE_STOPPED) + print("Service stopped\n"); + else + print("Service failed to stop\n"); + } + + if (!DeleteService(schService)) + { + MessageBox(NULL, GetErrorMessage(), + NULL, MB_OK | MB_ICONSTOP); + ExitProcess(1); + } + + DeleteFile("C:\\bridge.exe"); + print("Service removed successfully\n"); + CloseServiceHandle(schService); + CloseServiceHandle(schSCManager); + ExitProcess(0); +}