From 42627946689d20869cbd195a716d4070eb57d166 Mon Sep 17 00:00:00 2001 From: isJuhn Date: Sat, 18 Jun 2022 21:30:38 +0200 Subject: [PATCH] Implement PINE IPC Server --- 3rdparty/pine/pine_server.h | 622 ++++++++++++++++++++++++++ rpcs3.sln | 6 + rpcs3/Emu/CMakeLists.txt | 2 + rpcs3/Emu/Cell/PPUModule.cpp | 2 + rpcs3/Emu/IPC_config.cpp | 70 +++ rpcs3/Emu/IPC_config.h | 23 + rpcs3/Emu/IPC_socket.cpp | 114 +++++ rpcs3/Emu/IPC_socket.h | 59 +++ rpcs3/Emu/System.cpp | 8 + rpcs3/Emu/System.h | 8 + rpcs3/emucore.vcxproj | 4 + rpcs3/emucore.vcxproj.filters | 12 + rpcs3/rpcs3.vcxproj | 17 + rpcs3/rpcs3.vcxproj.filters | 15 + rpcs3/rpcs3qt/CMakeLists.txt | 1 + rpcs3/rpcs3qt/ipc_settings_dialog.cpp | 66 +++ rpcs3/rpcs3qt/ipc_settings_dialog.h | 10 + rpcs3/rpcs3qt/main_window.cpp | 7 + rpcs3/rpcs3qt/main_window.ui | 9 + 19 files changed, 1055 insertions(+) create mode 100644 3rdparty/pine/pine_server.h create mode 100644 rpcs3/Emu/IPC_config.cpp create mode 100644 rpcs3/Emu/IPC_config.h create mode 100644 rpcs3/Emu/IPC_socket.cpp create mode 100644 rpcs3/Emu/IPC_socket.h create mode 100644 rpcs3/rpcs3qt/ipc_settings_dialog.cpp create mode 100644 rpcs3/rpcs3qt/ipc_settings_dialog.h diff --git a/3rdparty/pine/pine_server.h b/3rdparty/pine/pine_server.h new file mode 100644 index 0000000000..fd26f07542 --- /dev/null +++ b/3rdparty/pine/pine_server.h @@ -0,0 +1,622 @@ +// Based on https://github.com/PCSX2/pcsx2/blob/edeb0d7bd7258c58273cc4a88a9f9a823d71e48c/pcsx2/IPC.h +// and https://github.com/PCSX2/pcsx2/blob/edeb0d7bd7258c58273cc4a88a9f9a823d71e48c/pcsx2/IPC.cpp +// Relicensed as GPLv2 for use in RPCS3 with permission from copyright owner (Govanify). + +#pragma once + +//#include "Utilities/Thread.h" +#include +#include "stdafx.h" +#include +#include +#include +#include +#if _WIN32 +#define read_portable(a, b, c) (recv(a, b, c, 0)) +#define write_portable(a, b, c) (send(a, b, c, 0)) +#define close_portable(a) (closesocket(a)) +#define bzero(b, len) (memset((b), '\0', (len)), (void)0) +#include +#include +#else +#define read_portable(a, b, c) (read(a, b, c)) +#define write_portable(a, b, c) (write(a, b, c)) +#define close_portable(a) (close(a)) +#include +#include +#if defined(__DragonFly__) || defined(__FreeBSD__) || defined(__NetBSD__) || defined(__OpenBSD__) || defined(__APPLE__) +#include +#endif +#endif + +namespace pine +{ + /** + * Emulator status enum. @n + * A list of possible emulator statuses. @n + */ + enum EmuStatus : uint32_t + { + Running = 0, /**< Game is running */ + Paused = 1, /**< Game is paused */ + Shutdown = 2, /**< Game is shutdown */ + }; + + typedef unsigned long long SOCKET; + + template + class pine_server : public Impl + { + public: +#ifdef _WIN32 + // windows claim to have support for AF_UNIX sockets but that is a blatant lie, + // their SDK won't even run their own examples, so we go on TCP sockets. + SOCKET m_sock; + // the message socket used in thread's accept(). + SOCKET m_msgsock; +#else + // absolute path of the socket. Stored in XDG_RUNTIME_DIR, if unset /tmp + std::string m_socket_name; + int m_sock = 0; + // the message socket used in thread's accept(). + int m_msgsock = 0; +#endif + + /** + * Maximum memory used by an IPC message request. + * Equivalent to 50,000 Write64 requests. + */ + #define MAX_IPC_SIZE 650000 + + /** + * Maximum memory used by an IPC message reply. + * Equivalent to 50,000 Read64 replies. + */ + #define MAX_IPC_RETURN_SIZE 450000 + + /** + * IPC return buffer. + * A preallocated buffer used to store all IPC replies. + * to the size of 50.000 MsgWrite64 IPC calls. + */ + char* m_ret_buffer; + + /** + * IPC messages buffer. + * A preallocated buffer used to store all IPC messages. + */ + char* m_ipc_buffer; + + /** + * IPC Command messages opcodes. + * A list of possible operations possible by the IPC. + * Each one of them is what we call an "opcode" and is the first + * byte sent by the IPC to differentiate between commands. + */ + enum IPCCommand : unsigned char + { + MsgRead8 = 0, /**< Read 8 bit value to memory. */ + MsgRead16 = 1, /**< Read 16 bit value to memory. */ + MsgRead32 = 2, /**< Read 32 bit value to memory. */ + MsgRead64 = 3, /**< Read 64 bit value to memory. */ + MsgWrite8 = 4, /**< Write 8 bit value to memory. */ + MsgWrite16 = 5, /**< Write 16 bit value to memory. */ + MsgWrite32 = 6, /**< Write 32 bit value to memory. */ + MsgWrite64 = 7, /**< Write 64 bit value to memory. */ + MsgVersion = 8, /**< Returns RPCS3 version. */ + MsgTitle = 0xB, /**< Returns the game title. */ + MsgID = 0xC, /**< Returns the game ID. */ + MsgUUID = 0xD, /**< Returns the game UUID. */ + MsgGameVersion = 0xE, /**< Returns the game verion. */ + MsgStatus = 0xF, /**< Returns the emulator status. */ + MsgUnimplemented = 0xFF /**< Unimplemented IPC message. */ + }; + + /** + * IPC message buffer. + * A list of all needed fields to store an IPC message. + */ + struct IPCBuffer + { + int size; /**< Size of the buffer. */ + char* buffer; /**< Buffer. */ + }; + + /** + * IPC result codes. + * A list of possible result codes the IPC can send back. + * Each one of them is what we call an "opcode" or "tag" and is the + * first byte sent by the IPC to differentiate between results. + */ + enum IPCResult : unsigned char + { + IPC_OK = 0, /**< IPC command successfully completed. */ + IPC_FAIL = 0xFF /**< IPC command failed to complete. */ + }; + + /** + * Internal function, Parses an IPC command. + * buf: buffer containing the IPC command. + * buf_size: size of the buffer announced. + * ret_buffer: buffer that will be used to send the reply. + * return value: IPCBuffer containing a buffer with the result + * of the command and its size. + */ + IPCBuffer ParseCommand(char* buf, char* ret_buffer, u32 buf_size) + { + u32 ret_cnt = 5; + u32 buf_cnt = 0; + + while (buf_cnt < buf_size) + { + if (!SafetyChecks(buf_cnt, 1, ret_cnt, 0, buf_size)) + return IPCBuffer{ 5, MakeFailIPC(ret_buffer) }; + buf_cnt++; + // example IPC messages: MsgRead/Write + // refer to the client doc for more info on the format + // IPC Message event (1 byte) + // | Memory address (4 byte) + // | | argument (VLE) + // | | | + // format: XX YY YY YY YY ZZ ZZ ZZ ZZ + // reply code: 00 = OK, FF = NOT OK + // | return value (VLE) + // | | + // reply: XX ZZ ZZ ZZ ZZ + IPCCommand command = std::bit_cast(buf[buf_cnt - 1]); + switch (command) + { + case MsgRead8: + { + if (!SafetyChecks(buf_cnt, 4, ret_cnt, 1, buf_size)) + goto error; + const u32 a = FromArray(&buf[buf_cnt], 0); + if (!Impl::template check_addr(a)) + goto error; + const u8 res = Impl::read8(a); + ToArray(ret_buffer, res, ret_cnt); + ret_cnt += 1; + buf_cnt += 4; + break; + } + case MsgRead16: + { + if (!SafetyChecks(buf_cnt, 4, ret_cnt, 2, buf_size)) + goto error; + const u32 a = FromArray(&buf[buf_cnt], 0); + if (!Impl::template check_addr<2>(a)) + goto error; + const u16 res = Impl::read16(a); + ToArray(ret_buffer, res, ret_cnt); + ret_cnt += 2; + buf_cnt += 4; + break; + } + case MsgRead32: + { + if (!SafetyChecks(buf_cnt, 4, ret_cnt, 4, buf_size)) + goto error; + const u32 a = FromArray(&buf[buf_cnt], 0); + if (!Impl::template check_addr<4>(a)) + goto error; + const u32 res = Impl::read32(a); + ToArray(ret_buffer, res, ret_cnt); + ret_cnt += 4; + buf_cnt += 4; + break; + } + case MsgRead64: + { + if (!SafetyChecks(buf_cnt, 4, ret_cnt, 8, buf_size)) + goto error; + const u32 a = FromArray(&buf[buf_cnt], 0); + if (!Impl::template check_addr<8>(a)) + goto error; + u64 res = Impl::read64(a); + ToArray(ret_buffer, res, ret_cnt); + ret_cnt += 8; + buf_cnt += 4; + break; + } + case MsgWrite8: + { + if (!SafetyChecks(buf_cnt, 1 + 4, ret_cnt, 0, buf_size)) + goto error; + const u32 a = FromArray(&buf[buf_cnt], 0); + if (!Impl::template check_addr(a, vm::page_writable)) + goto error; + Impl::write8(a, FromArray(&buf[buf_cnt], 4)); + buf_cnt += 5; + break; + } + case MsgWrite16: + { + if (!SafetyChecks(buf_cnt, 2 + 4, ret_cnt, 0, buf_size)) + goto error; + const u32 a = FromArray(&buf[buf_cnt], 0); + if (!Impl::template check_addr<2>(a, vm::page_writable)) + goto error; + Impl::write16(a, FromArray(&buf[buf_cnt], 4)); + buf_cnt += 6; + break; + } + case MsgWrite32: + { + if (!SafetyChecks(buf_cnt, 4 + 4, ret_cnt, 0, buf_size)) + goto error; + const u32 a = FromArray(&buf[buf_cnt], 0); + if (!Impl::template check_addr<4>(a, vm::page_writable)) + goto error; + Impl::write32(a, FromArray(&buf[buf_cnt], 4)); + buf_cnt += 8; + break; + } + case MsgWrite64: + { + if (!SafetyChecks(buf_cnt, 8 + 4, ret_cnt, 0, buf_size)) + goto error; + const u32 a = FromArray(&buf[buf_cnt], 0); + if (!Impl::template check_addr<8>(a, vm::page_writable)) + goto error; + Impl::write64(a, FromArray(&buf[buf_cnt], 4)); + buf_cnt += 12; + break; + } + case MsgVersion: + { + char version[256] = {}; + sprintf(version, "RPCS3 %s", Impl::get_version_and_branch().c_str()); + const u32 size = strlen(version) + 1; + version[size] = 0x00; + if (!SafetyChecks(buf_cnt, 0, ret_cnt, size + 4, buf_size)) + goto error; + ToArray(ret_buffer, size, ret_cnt); + ret_cnt += 4; + memcpy(&ret_buffer[ret_cnt], version, size); + ret_cnt += size; + break; + } + case MsgStatus: + { + if (!SafetyChecks(buf_cnt, 0, ret_cnt, 4, buf_size)) + goto error; + EmuStatus status = Impl::get_status(); + ToArray(ret_buffer, status, ret_cnt); + ret_cnt += 4; + buf_cnt += 4; + break; + } + case MsgTitle: + { + const auto title_string = Impl::get_title(); + const auto size = title_string.size() + 1; + char* title = new char[size]; + sprintf(title, "%s", title_string.c_str()); + title[size] = 0x00; + if (!SafetyChecks(buf_cnt, 0, ret_cnt, size + 4, buf_size)) + goto error; + ToArray(ret_buffer, size, ret_cnt); + ret_cnt += 4; + memcpy(&ret_buffer[ret_cnt], title, size); + ret_cnt += size; + delete[] title; + break; + } + case MsgID: + { + const auto title_id_string = Impl::get_title_ID(); + const auto size = title_id_string.size() + 1; + char* title_id = new char[size]; + sprintf(title_id, "%s", title_id_string.c_str()); + title_id[size] = 0x00; + if (!SafetyChecks(buf_cnt, 0, ret_cnt, size + 4, buf_size)) + goto error; + ToArray(ret_buffer, size, ret_cnt); + ret_cnt += 4; + memcpy(&ret_buffer[ret_cnt], title_id, size); + ret_cnt += size; + delete[] title_id; + break; + } + case MsgUUID: + { + const auto hash_string = Impl::get_executable_hash(); + const auto size = hash_string.size() + 1; + char* hash = new char[size]; + sprintf(hash, "%s", hash_string.c_str()); + hash[size] = 0x00; + if (!SafetyChecks(buf_cnt, 0, ret_cnt, size + 4, buf_size)) + goto error; + ToArray(ret_buffer, size, ret_cnt); + ret_cnt += 4; + memcpy(&ret_buffer[ret_cnt], hash, size); + ret_cnt += size; + delete[] hash; + break; + } + case MsgGameVersion: + { + const auto game_version_string = Impl::get_app_version(); + const auto size = game_version_string.size() + 1; + char* game_version = new char[size]; + sprintf(game_version, "%s", game_version_string.c_str()); + game_version[size] = 0x00; + if (!SafetyChecks(buf_cnt, 0, ret_cnt, size, buf_size)) + goto error; + ToArray(ret_buffer, size, ret_cnt); + ret_cnt += 4; + memcpy(&ret_buffer[ret_cnt], game_version, size); + ret_cnt += size; + delete[] game_version; + break; + } + default: + { + error: + return IPCBuffer{ 5, MakeFailIPC(ret_buffer) }; + } + } + } + return IPCBuffer{ static_cast(ret_cnt), MakeOkIPC(ret_buffer, ret_cnt) }; + } + + /** + * Formats an IPC buffer + * ret_buffer: return buffer to use. + * size: size of the IPC buffer. + * return value: buffer containing the status code allocated of size + */ + static inline char* MakeOkIPC(char* ret_buffer, uint32_t size = 5) + { + ToArray(ret_buffer, size, 0); + ret_buffer[4] = IPC_OK; + return ret_buffer; + } + + static inline char* MakeFailIPC(char* ret_buffer, uint32_t size = 5) + { + ToArray(ret_buffer, size, 0); + ret_buffer[4] = IPC_FAIL; + return ret_buffer; + } + + /** + * Initializes an open socket for IPC communication. + * return value: -1 if a fatal failure happened, 0 otherwise. + */ + int StartSocket() + { + m_msgsock = accept(m_sock, 0, 0); + + if (m_msgsock == -1) + { + // everything else is non recoverable in our scope + // we also mark as recoverable socket errors where it would block a + // non blocking socket, even though our socket is blocking, in case + // we ever have to implement a non blocking socket. +#ifdef _WIN32 + int errno_w = WSAGetLastError(); + if (!(errno_w == WSAECONNRESET || errno_w == WSAEINTR || errno_w == WSAEINPROGRESS || errno_w == WSAEMFILE || errno_w == WSAEWOULDBLOCK)) + { +#else + if (!(errno == ECONNABORTED || errno == EINTR || errno == EAGAIN || errno == EWOULDBLOCK)) + { +#endif + Impl::error("IPC: An unrecoverable error happened! Shutting down..."); + return -1; + } + } + return 0; + } + + // Thread used to relay IPC commands. + void operator()() + { + // we allocate once buffers to not have to do mallocs for each IPC + // request, as malloc is expansive when we optimize for µs. + m_ret_buffer = new char[MAX_IPC_RETURN_SIZE]; + m_ipc_buffer = new char[MAX_IPC_SIZE]; + + if (StartSocket() < 0) + return; + + while (thread_ctrl::state() != thread_state::aborting) + { + // either int or ssize_t depending on the platform, so we have to + // use a bunch of auto + auto receive_length = 0; + auto end_length = 4; + + // while we haven't received the entire packet, maybe due to + // socket datagram splittage, we continue to read + while (receive_length < end_length) + { + auto tmp_length = read_portable(m_msgsock, &m_ipc_buffer[receive_length], MAX_IPC_SIZE - receive_length); + + // we recreate the socket if an error happens + if (tmp_length <= 0) + { + receive_length = 0; + if (StartSocket() < 0) + return; + break; + } + + receive_length += tmp_length; + + // if we got at least the final size then update + if (end_length == 4 && receive_length >= 4) + { + end_length = FromArray(m_ipc_buffer, 0); + // we'd like to avoid a client trying to do OOB + if (end_length > MAX_IPC_SIZE || end_length < 4) + { + receive_length = 0; + break; + } + } + } + + // we remove 4 bytes to get the message size out of the IPC command + // size in ParseCommand. + // also, if we got a failed command, let's reset the state so we don't + // end up deadlocking by getting out of sync, eg when a client + // disconnects + if (receive_length != 0) + { + pine_server::IPCBuffer res = ParseCommand(&m_ipc_buffer[4], m_ret_buffer, static_cast(end_length) - 4); + + // if we cannot send back our answer restart the socket + if (write_portable(m_msgsock, res.buffer, res.size) < 0) + { + if (StartSocket() < 0) + return; + } + } + } + } + + /** + * Converts an uint to an char* in little endian + * res_array: the array to modify + * res: the value to convert + * i: when to insert it into the array + * return value: res_array + * NB: implicitely inlined + */ + template + static char* ToArray(char* res_array, T res, int i) + { + memcpy((res_array + i), reinterpret_cast(&res), sizeof(T)); + return res_array; + } + + /** + * Converts a char* to an uint in little endian + * arr: the array to convert + * i: when to load it from the array + * return value: the converted value + * NB: implicitely inlined + */ + template + static T FromArray(char* arr, int i) + { + return *reinterpret_cast(arr + i); + } + + /** + * Ensures an IPC message isn't too big. + * return value: false if checks failed, true otherwise. + */ + static inline bool SafetyChecks(u32 command_len, int command_size, u32 reply_len, int reply_size = 0, u32 buf_size = MAX_IPC_SIZE - 1) + { + bool res = ((command_len + command_size) > buf_size || + (reply_len + reply_size) >= MAX_IPC_RETURN_SIZE); + if (res) [[unlikely]] + return false; + return true; + } + + public: + /* Initializers */ + pine_server() noexcept + { +#ifdef _WIN32 + WSADATA wsa; + struct sockaddr_in server; + m_sock = INVALID_SOCKET; + m_msgsock = INVALID_SOCKET; + + if (WSAStartup(MAKEWORD(2, 2), &wsa) != 0) + { + Impl::error("IPC: Cannot initialize winsock! Shutting down..."); + return; + } + + if ((m_sock = socket(AF_INET, SOCK_STREAM, 0)) == INVALID_SOCKET) + { + Impl::error("IPC: Cannot open socket! Shutting down..."); + return; + } + + // yes very good windows s/sun/sin/g sure is fine + server.sin_family = AF_INET; + // localhost only + server.sin_addr.s_addr = inet_addr("127.0.0.1"); + server.sin_port = htons(Impl::get_port()); + + if (bind(m_sock, (struct sockaddr*)&server, sizeof(server)) == SOCKET_ERROR) + { + Impl::error("IPC: Error while binding to socket! Shutting down..."); + return; + } + +#else + char* runtime_dir = nullptr; +#ifdef __APPLE__ + runtime_dir = std::getenv("TMPDIR"); +#else + runtime_dir = std::getenv("XDG_RUNTIME_DIR"); +#endif + // fallback in case macOS or other OSes don't implement the XDG base + // spec + if (runtime_dir == nullptr) + m_socket_name = "/tmp/rpcs3.sock"; + else + { + m_socket_name = runtime_dir; + m_socket_name += "/rpcs3.sock"; + } + + m_socket_name = fmt::format("%s.%d", m_socket_name, Impl::get_port()); + + struct sockaddr_un server; + + m_sock = socket(AF_UNIX, SOCK_STREAM, 0); + if (m_sock < 0) + { + Impl::error("IPC: Cannot open socket! Shutting down..."); + return; + } + server.sun_family = AF_UNIX; + strcpy(server.sun_path, m_socket_name.c_str()); + + // we unlink the socket so that when releasing this thread the socket gets + // freed even if we didn't close correctly the loop + unlink(m_socket_name.c_str()); + if (bind(m_sock, std::bit_cast(&server), sizeof(struct sockaddr_un))) + { + Impl::error("IPC: Error while binding to socket! Shutting down..."); + return; + } +#endif + + // maximum queue of 4096 commands before refusing, approximated to the + // nearest legal value. We do not use SOMAXCONN as windows have this idea + // that a "reasonable" value is 5, which is not. + listen(m_sock, 4096); + } + + pine_server(const pine_server&) = delete; + pine_server& operator=(const pine_server&) = delete; + + void Cleanup() + { +#ifdef _WIN32 + WSACleanup(); +#else + unlink(m_socket_name.c_str()); +#endif + close_portable(m_sock); + close_portable(m_msgsock); + } + + ~pine_server() + { + Cleanup(); + delete[] m_ret_buffer; + delete[] m_ipc_buffer; + } + + }; // class pine_server +} diff --git a/rpcs3.sln b/rpcs3.sln index 67d979a987..23bdba5992 100644 --- a/rpcs3.sln +++ b/rpcs3.sln @@ -85,6 +85,11 @@ Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "libcubeb", "3rdparty\cubeb\ EndProject Project("{508c291a-3d18-49f5-b25d-f7c8db92cb21}") = "soundtouch", "3rdparty\SoundTouch\soundtouch.vcxproj", "{508c291a-3d18-49f5-b25d-f7c8db92cb21}" EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "pine", "pine", "{A55DA1B5-CC17-4525-BE7F-1659CE17BB56}" + ProjectSection(SolutionItems) = preProject + 3rdparty\pine\pine_server.h = 3rdparty\pine\pine_server.h + EndProjectSection +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|x64 = Debug|x64 @@ -202,6 +207,7 @@ Global {9610627D-20FE-4B07-8CE3-9FF68A5F1EC2} = {10FBF193-D532-4CCF-B875-4C7091A7F6C2} {FDA7B080-03B0-48C8-B24F-88118981422A} = {6C3B64A0-8F8A-4DC4-8C0B-D71EBEED7FA8} {508c291a-3d18-49f5-b25d-f7c8db92cb21} = {6C3B64A0-8F8A-4DC4-8C0B-D71EBEED7FA8} + {A55DA1B5-CC17-4525-BE7F-1659CE17BB56} = {6C3B64A0-8F8A-4DC4-8C0B-D71EBEED7FA8} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {06CC7920-E085-4B81-9582-8DE8AAD42510} diff --git a/rpcs3/Emu/CMakeLists.txt b/rpcs3/Emu/CMakeLists.txt index 36135d5b46..02af5463ee 100644 --- a/rpcs3/Emu/CMakeLists.txt +++ b/rpcs3/Emu/CMakeLists.txt @@ -13,6 +13,8 @@ add_library(rpcs3_emu title.cpp perf_meter.cpp perf_monitor.cpp + IPC_config.cpp + IPC_socket.cpp ) # prevent WolfSSL from warning about not having harden options diff --git a/rpcs3/Emu/Cell/PPUModule.cpp b/rpcs3/Emu/Cell/PPUModule.cpp index 1d2bfce920..9d6f697849 100644 --- a/rpcs3/Emu/Cell/PPUModule.cpp +++ b/rpcs3/Emu/Cell/PPUModule.cpp @@ -1455,6 +1455,8 @@ bool ppu_load_exec(const ppu_exec_object& elf) hash[5 + i * 2] = pal[_main.sha1[i] & 15]; } + Emu.SetExecutableHash(hash); + // Apply the patch auto applied = g_fxo->get().apply(hash, vm::g_base_addr); diff --git a/rpcs3/Emu/IPC_config.cpp b/rpcs3/Emu/IPC_config.cpp new file mode 100644 index 0000000000..c276645730 --- /dev/null +++ b/rpcs3/Emu/IPC_config.cpp @@ -0,0 +1,70 @@ +#include "stdafx.h" +#include "IPC_config.h" + +cfg_ipc g_cfg_ipc; + +LOG_CHANNEL(IPC); + +void cfg_ipc::load() +{ + const std::string path = cfg_ipc::get_path(); + + fs::file cfg_file(path, fs::read); + if (cfg_file) + { + IPC.notice("Loading IPC config. Path: %s", path); + from_string(cfg_file.to_string()); + } + else + { + IPC.notice("IPC config missing. Using default settings. Path: %s", path); + from_default(); + } +} + +void cfg_ipc::save() const +{ +#ifdef _WIN32 + const std::string path_to_cfg = fs::get_config_dir() + "config/"; + if (!fs::create_path(path_to_cfg)) + { + IPC.error("Could not create path: %s", path_to_cfg); + } +#endif + + fs::pending_file cfg_file(cfg_ipc::get_path()); + + if (!cfg_file.file || (cfg_file.file.write(to_string()), !cfg_file.commit())) + { + IPC.error("Could not save config: %s", cfg_ipc::get_path()); + } +} + +std::string cfg_ipc::get_path() +{ +#ifdef _WIN32 + return fs::get_config_dir() + "config/ipc.yml"; +#else + return fs::get_config_dir() + "ipc.yml"; +#endif +} + +bool cfg_ipc::get_server_enabled() const +{ + return ipc_server_enabled.get(); +} + +int cfg_ipc::get_port() const +{ + return ipc_port; +} + +void cfg_ipc::set_server_enabled(const bool enabled) +{ + this->ipc_server_enabled.set(enabled); +} + +void cfg_ipc::set_port(const int port) +{ + this->ipc_port.set(port); +} diff --git a/rpcs3/Emu/IPC_config.h b/rpcs3/Emu/IPC_config.h new file mode 100644 index 0000000000..3209257d2d --- /dev/null +++ b/rpcs3/Emu/IPC_config.h @@ -0,0 +1,23 @@ +#pragma once + +#include "Utilities/Config.h" + +struct cfg_ipc : cfg::node +{ + cfg::_bool ipc_server_enabled{ this, "IPC Server enabled", false }; + cfg::_int<1025, 65535> ipc_port{ this, "IPC Port", 28012 }; + + void load(); + void save() const; + + bool get_server_enabled() const; + int get_port() const; + + void set_server_enabled(const bool enabled); + void set_port(const int port); + +private: + static std::string get_path(); +}; + +extern cfg_ipc g_cfg_ipc; diff --git a/rpcs3/Emu/IPC_socket.cpp b/rpcs3/Emu/IPC_socket.cpp new file mode 100644 index 0000000000..844a023538 --- /dev/null +++ b/rpcs3/Emu/IPC_socket.cpp @@ -0,0 +1,114 @@ +#include "stdafx.h" +#include "System.h" +#include "Emu/IPC_config.h" +#include "IPC_socket.h" +#include "rpcs3_version.h" + + +namespace IPC_socket +{ + const u8& IPC_impl::read8(u32 addr) + { + return vm::read8(addr); + } + + void IPC_impl::write8(u32 addr, u8 value) + { + vm::write8(addr, value); + } + + const be_t& IPC_impl::read16(u32 addr) + { + return vm::read16(addr); + } + + void IPC_impl::write16(u32 addr, be_t value) + { + vm::write16(addr, value); + } + + const be_t& IPC_impl::read32(u32 addr) + { + return vm::read32(addr); + } + + void IPC_impl::write32(u32 addr, be_t value) + { + vm::write32(addr, value); + } + + const be_t& IPC_impl::read64(u32 addr) + { + return vm::read64(addr); + } + + void IPC_impl::write64(u32 addr, be_t value) + { + vm::write64(addr, value); + } + + const int IPC_impl::get_port() + { + return g_cfg_ipc.get_port(); + } + + pine::EmuStatus IPC_impl::get_status() + { + switch (Emu.GetStatus()) + { + case system_state::running: + return pine::EmuStatus::Running; + case system_state::paused: + return pine::EmuStatus::Paused; + default: + return pine::EmuStatus::Shutdown; + } + } + + const std::string& IPC_impl::get_title() + { + return Emu.GetTitle(); + } + + const std::string& IPC_impl::get_title_ID() + { + return Emu.GetTitleID(); + } + + const std::string& IPC_impl::get_executable_hash() + { + return Emu.GetExecutableHash(); + } + + const std::string& IPC_impl::get_app_version() + { + return Emu.GetAppVersion(); + } + + const std::string IPC_impl::get_version_and_branch() + { + return rpcs3::get_version_and_branch(); + } + + IPC_impl& IPC_impl::operator=(thread_state) + { + return *this; + } + + void IPC_server_manager::set_server_enabled(bool enabled) + { + if (enabled) + { + int port = g_cfg_ipc.get_port(); + if (!m_ipc_server || port != m_old_port) + { + m_ipc_server = std::make_unique(); + m_old_port = port; + } + } + else if (!enabled && m_ipc_server) + { + m_ipc_server.reset(nullptr); + } + } +} diff --git a/rpcs3/Emu/IPC_socket.h b/rpcs3/Emu/IPC_socket.h new file mode 100644 index 0000000000..92aeb22493 --- /dev/null +++ b/rpcs3/Emu/IPC_socket.h @@ -0,0 +1,59 @@ +#pragma once + +#include "Utilities/Thread.h" +#include "util/logs.hpp" +#include "Emu/Memory/vm.h" +#include "../pine/pine_server.h" + +LOG_CHANNEL(IPC); + +namespace IPC_socket +{ + class IPC_impl + { + protected: + template + static bool check_addr(u32 addr, u8 flags = vm::page_readable) + { + return vm::check_addr(addr, flags); + } + + static const u8& read8(u32 addr); + static void write8(u32 addr, u8 value); + static const be_t& read16(u32 addr); + static void write16(u32 addr, be_t value); + static const be_t& read32(u32 addr); + static void write32(u32 addr, be_t value); + static const be_t& read64(u32 addr); + static void write64(u32 addr, be_t value); + + template + static void error(const const_str& fmt, const Args&&... args) + { + IPC.error(fmt, std::forward(args)...); + } + + static const int get_port(); + static pine::EmuStatus get_status(); + static const std::string& get_title(); + static const std::string& get_title_ID(); + static const std::string& get_executable_hash(); + static const std::string& get_app_version(); + static const std::string get_version_and_branch(); + + public: + static auto constexpr thread_name = "IPC Server"sv; + IPC_impl& operator=(thread_state); + }; + + class IPC_server_manager + { + using IPC_server = named_thread>; + + std::unique_ptr m_ipc_server; + int m_old_port = 0; + + public: + void set_server_enabled(bool enabled); + }; +} diff --git a/rpcs3/Emu/System.cpp b/rpcs3/Emu/System.cpp index f2a3d97d20..6f256f6988 100644 --- a/rpcs3/Emu/System.cpp +++ b/rpcs3/Emu/System.cpp @@ -8,6 +8,7 @@ #include "Emu/perf_meter.hpp" #include "Emu/perf_monitor.hpp" #include "Emu/vfs_config.h" +#include "Emu/IPC_config.h" #include "Emu/Cell/ErrorCodes.h" #include "Emu/Cell/PPUThread.h" @@ -43,6 +44,8 @@ #include "display_sleep_control.h" +#include "Emu/IPC_socket.h" + #if defined(HAVE_VULKAN) #include "Emu/RSX/VK/VulkanAPI.h" #endif @@ -408,6 +411,11 @@ void Emulator::Init(bool add_only) } } } + + // Load IPC config + g_cfg_ipc.load(); + sys_log.notice("Using IPC config:\n%s", g_cfg_ipc.to_string()); + g_fxo->get().set_server_enabled(g_cfg_ipc.get_server_enabled()); } void Emulator::SetUsr(const std::string& user) diff --git a/rpcs3/Emu/System.h b/rpcs3/Emu/System.h index 9a64134100..802bb4684a 100644 --- a/rpcs3/Emu/System.h +++ b/rpcs3/Emu/System.h @@ -105,6 +105,7 @@ class Emulator final std::string m_title_id; std::string m_title; std::string m_app_version; + std::string m_hash; std::string m_cat; std::string m_dir; std::string m_sfo_dir; @@ -213,6 +214,13 @@ public: return m_app_version; } + const std::string& GetExecutableHash() const + { + return m_hash; + } + + void SetExecutableHash(std::string hash) { m_hash = std::move(hash); } + const std::string& GetCat() const { return m_cat; diff --git a/rpcs3/emucore.vcxproj b/rpcs3/emucore.vcxproj index 74b1b6ef32..d5a7238134 100644 --- a/rpcs3/emucore.vcxproj +++ b/rpcs3/emucore.vcxproj @@ -71,6 +71,8 @@ + + @@ -481,6 +483,8 @@ + + diff --git a/rpcs3/emucore.vcxproj.filters b/rpcs3/emucore.vcxproj.filters index 24bc4d564b..d113d6b9e3 100644 --- a/rpcs3/emucore.vcxproj.filters +++ b/rpcs3/emucore.vcxproj.filters @@ -1072,6 +1072,12 @@ Emu\GPU\RSX + + Emu + + + Emu + @@ -2131,6 +2137,12 @@ Emu\GPU\RSX + + Emu + + + Emu + diff --git a/rpcs3/rpcs3.vcxproj b/rpcs3/rpcs3.vcxproj index fa75ee7604..eab3c38693 100644 --- a/rpcs3/rpcs3.vcxproj +++ b/rpcs3/rpcs3.vcxproj @@ -255,6 +255,9 @@ true + + true + true @@ -471,6 +474,9 @@ true + + true + true @@ -622,6 +628,7 @@ + @@ -1123,6 +1130,16 @@ .\QTGeneratedFiles\$(ConfigurationName)\moc_%(Filename).cpp "$(QTDIR)\bin\moc.exe" "%(FullPath)" -o ".\QTGeneratedFiles\$(ConfigurationName)\moc_%(Filename).cpp" -D_WINDOWS -DUNICODE -DWIN32 -DWIN64 -DWITH_DISCORD_RPC -DQT_NO_DEBUG -DQT_WIDGETS_LIB -DQT_GUI_LIB -DQT_CORE_LIB -DNDEBUG -DQT_WINEXTRAS_LIB -DQT_CONCURRENT_LIB -D%(PreprocessorDefinitions) "-I.\..\3rdparty\wolfssl\wolfssl" "-I.\..\3rdparty\curl\curl\include" "-I.\..\3rdparty\libusb\libusb\libusb" "-I$(VULKAN_SDK)\Include" "-I.\..\3rdparty\XAudio2Redist\include" "-I$(QTDIR)\include" "-I$(QTDIR)\include\QtWidgets" "-I$(QTDIR)\include\QtGui" "-I$(QTDIR)\include\QtANGLE" "-I$(QTDIR)\include\QtCore" "-I.\release" "-I$(QTDIR)\mkspecs\win32-msvc2015" "-I.\QTGeneratedFiles\$(ConfigurationName)" "-I.\QTGeneratedFiles" "-I$(QTDIR)\include\QtWinExtras" "-I$(QTDIR)\include\QtConcurrent" + + $(QTDIR)\bin\moc.exe;%(FullPath);$(QTDIR)\bin\moc.exe;%(FullPath);$(QTDIR)\bin\moc.exe;%(FullPath) + Moc%27ing %(Identity)... + .\QTGeneratedFiles\$(ConfigurationName)\moc_%(Filename).cpp + "$(QTDIR)\bin\moc.exe" "%(FullPath)" -o ".\QTGeneratedFiles\$(ConfigurationName)\moc_%(Filename).cpp" -D_WINDOWS -DUNICODE -DWIN32 -DWIN64 -DWIN32_LEAN_AND_MEAN -DHAVE_VULKAN -DQT_WIDGETS_LIB -DQT_GUI_LIB -DQT_CORE_LIB -DQT_WINEXTRAS_LIB -DQT_CONCURRENT_LIB -DQT_MULTIMEDIA_LIB -DQT_MULTIMEDIAWIDGETS_LIB -DQT_SVG_LIB -D%(PreprocessorDefinitions) "-I.\..\3rdparty\SoundTouch\soundtouch\include" "-I.\..\3rdparty\cubeb\extra" "-I.\..\3rdparty\cubeb\cubeb\include" "-I.\..\3rdparty\flatbuffers\include" "-I.\..\3rdparty\wolfssl\wolfssl" "-I.\..\3rdparty\curl\curl\include" "-I.\..\3rdparty\libusb\libusb\libusb" "-I$(VULKAN_SDK)\Include" "-I.\..\3rdparty\XAudio2Redist\include" "-I$(QTDIR)\include" "-I$(QTDIR)\include\QtWidgets" "-I$(QTDIR)\include\QtGui" "-I$(QTDIR)\include\QtANGLE" "-I$(QTDIR)\include\QtCore" "-I.\debug" "-I$(QTDIR)\mkspecs\win32-msvc2015" "-I.\QTGeneratedFiles\$(ConfigurationName)" "-I.\QTGeneratedFiles" "-I$(QTDIR)\include\QtWinExtras" "-I$(QTDIR)\include\QtConcurrent" "-I$(QTDIR)\include\QtMultimedia" "-I$(QTDIR)\include\QtMultimediaWidgets" "-I$(QTDIR)\include\QtSvg" + $(QTDIR)\bin\moc.exe;%(FullPath) + Moc%27ing %(Identity)... + .\QTGeneratedFiles\$(ConfigurationName)\moc_%(Filename).cpp + "$(QTDIR)\bin\moc.exe" "%(FullPath)" -o ".\QTGeneratedFiles\$(ConfigurationName)\moc_%(Filename).cpp" -D_WINDOWS -DUNICODE -DWIN32 -DWIN64 -DWIN32_LEAN_AND_MEAN -DHAVE_VULKAN -DWITH_DISCORD_RPC -DQT_NO_DEBUG -DQT_WIDGETS_LIB -DQT_GUI_LIB -DQT_CORE_LIB -DNDEBUG -DQT_WINEXTRAS_LIB -DQT_CONCURRENT_LIB -DQT_MULTIMEDIA_LIB -DQT_MULTIMEDIAWIDGETS_LIB -DQT_SVG_LIB -D%(PreprocessorDefinitions) "-I.\..\3rdparty\SoundTouch\soundtouch\include" "-I.\..\3rdparty\cubeb\extra" "-I.\..\3rdparty\cubeb\cubeb\include" "-I.\..\3rdparty\flatbuffers\include" "-I.\..\3rdparty\wolfssl\wolfssl" "-I.\..\3rdparty\curl\curl\include" "-I.\..\3rdparty\libusb\libusb\libusb" "-I$(VULKAN_SDK)\Include" "-I.\..\3rdparty\XAudio2Redist\include" "-I$(QTDIR)\include" "-I$(QTDIR)\include\QtWidgets" "-I$(QTDIR)\include\QtGui" "-I$(QTDIR)\include\QtANGLE" "-I$(QTDIR)\include\QtCore" "-I.\release" "-I$(QTDIR)\mkspecs\win32-msvc2015" "-I.\QTGeneratedFiles\$(ConfigurationName)" "-I.\QTGeneratedFiles" "-I$(QTDIR)\include\QtWinExtras" "-I$(QTDIR)\include\QtConcurrent" "-I$(QTDIR)\include\QtMultimedia" "-I$(QTDIR)\include\QtMultimediaWidgets" "-I$(QTDIR)\include\QtSvg" + diff --git a/rpcs3/rpcs3.vcxproj.filters b/rpcs3/rpcs3.vcxproj.filters index a6f3e03dde..ed4efaf2a1 100644 --- a/rpcs3/rpcs3.vcxproj.filters +++ b/rpcs3/rpcs3.vcxproj.filters @@ -139,6 +139,9 @@ {44c3b8d8-cc4d-4d5f-8681-21241ab220d0} + + {91389c5b-9ebd-43fe-a288-1bbad775e528} + @@ -849,6 +852,15 @@ Generated Files\Release + + Gui\ipc + + + Generated Files\Debug + + + Generated Files\Release + @@ -1246,6 +1258,9 @@ Gui\vfs + + Gui\ipc + diff --git a/rpcs3/rpcs3qt/CMakeLists.txt b/rpcs3/rpcs3qt/CMakeLists.txt index 53b805a226..a74f3c9140 100644 --- a/rpcs3/rpcs3qt/CMakeLists.txt +++ b/rpcs3/rpcs3qt/CMakeLists.txt @@ -32,6 +32,7 @@ set(SRC_FILES gui_settings.cpp input_dialog.cpp instruction_editor_dialog.cpp + ipc_settings_dialog.cpp kernel_explorer.cpp localized.cpp localized_emu.h diff --git a/rpcs3/rpcs3qt/ipc_settings_dialog.cpp b/rpcs3/rpcs3qt/ipc_settings_dialog.cpp new file mode 100644 index 0000000000..d5a57680ab --- /dev/null +++ b/rpcs3/rpcs3qt/ipc_settings_dialog.cpp @@ -0,0 +1,66 @@ +#include +#include +#include +#include +#include +#include +#include +#include + +#include "ipc_settings_dialog.h" +#include "Emu/IPC_config.h" + +ipc_settings_dialog::ipc_settings_dialog(QWidget* parent) + : QDialog(parent) +{ + setWindowTitle(tr("IPC Settings")); + setObjectName("ipc_settings_dialog"); + setMinimumSize(QSize(200, 100)); + + QVBoxLayout* vbox_global = new QVBoxLayout(); + + QCheckBox* checkbox_server_enabled = new QCheckBox(tr("Enable IPC Server")); + + QGroupBox* group_server_port = new QGroupBox(tr("IPC Server Port")); + QHBoxLayout* hbox_group_port = new QHBoxLayout(); + QLineEdit* line_edit_server_port = new QLineEdit(); + line_edit_server_port->setValidator(new QIntValidator(1025, 65535, this)); + + QDialogButtonBox* buttons = new QDialogButtonBox(QDialogButtonBox::Cancel | QDialogButtonBox::Save); + buttons->button(QDialogButtonBox::Save)->setDefault(true); + + hbox_group_port->addWidget(line_edit_server_port); + group_server_port->setLayout(hbox_group_port); + + vbox_global->addWidget(checkbox_server_enabled); + vbox_global->addWidget(group_server_port); + vbox_global->addWidget(buttons); + + setLayout(vbox_global); + + connect(buttons, &QDialogButtonBox::accepted, this, [this, checkbox_server_enabled, line_edit_server_port]() + { + bool ok = true; + const bool server_enabled = checkbox_server_enabled->isChecked(); + const int server_port = line_edit_server_port->text().toInt(&ok); + + if (!ok || server_port < 1025 || server_port > 65535) + { + QMessageBox::critical(this, tr("Invalid port"), tr("The server port must be an integer in the range 1025 - 65535!"), QMessageBox::Ok); + return; + } + + g_cfg_ipc.set_server_enabled(server_enabled); + g_cfg_ipc.set_port(server_port); + g_cfg_ipc.save(); + + accept(); + }); + + connect(buttons, &QDialogButtonBox::rejected, this, &QDialog::reject); + + g_cfg_ipc.load(); + + checkbox_server_enabled->setChecked(g_cfg_ipc.get_server_enabled()); + line_edit_server_port->setText(QString::number(g_cfg_ipc.get_port())); +} diff --git a/rpcs3/rpcs3qt/ipc_settings_dialog.h b/rpcs3/rpcs3qt/ipc_settings_dialog.h new file mode 100644 index 0000000000..7e8981223c --- /dev/null +++ b/rpcs3/rpcs3qt/ipc_settings_dialog.h @@ -0,0 +1,10 @@ +#pragma once + +#include + +class ipc_settings_dialog : public QDialog +{ + Q_OBJECT +public: + ipc_settings_dialog(QWidget* parent = nullptr); +}; diff --git a/rpcs3/rpcs3qt/main_window.cpp b/rpcs3/rpcs3qt/main_window.cpp index 48df435342..528e62e147 100644 --- a/rpcs3/rpcs3qt/main_window.cpp +++ b/rpcs3/rpcs3qt/main_window.cpp @@ -29,6 +29,7 @@ #include "gui_settings.h" #include "input_dialog.h" #include "camera_settings_dialog.h" +#include "ipc_settings_dialog.h" #include #include @@ -2084,6 +2085,12 @@ void main_window::CreateConnects() dlg.exec(); }); + connect(ui->confIPCAct, &QAction::triggered, this, [this]() + { + ipc_settings_dialog dlg(this); + dlg.exec(); + }); + connect(ui->confAutopauseManagerAct, &QAction::triggered, this, [this]() { auto_pause_settings_dialog dlg(this); diff --git a/rpcs3/rpcs3qt/main_window.ui b/rpcs3/rpcs3qt/main_window.ui index 66d3c263e5..54f0271a2f 100644 --- a/rpcs3/rpcs3qt/main_window.ui +++ b/rpcs3/rpcs3qt/main_window.ui @@ -241,6 +241,7 @@ + @@ -1148,6 +1149,14 @@ Configure RPCN + + + IPC + + + Configure IPC + + Log Viewer