diff --git a/3RD-PARTY-LICENSE.txt b/3RD-PARTY-LICENSE.txt new file mode 100644 index 0000000..350c3be --- /dev/null +++ b/3RD-PARTY-LICENSE.txt @@ -0,0 +1,30 @@ +3RD PARTY LICENSES + +* dolphin-memory-engine (modified) + (https://github.com/aldelaro5/Dolphin-memory-engine) +# SPDX-License-Identifier: MIT +# Copyright (c) 2017 aldelaro5 + +############################################################################# +LICENSE TEXTS +############################################################################# + +MIT License + +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/CHANGELOG.md b/CHANGELOG.md index 63b574b..60aea3e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,4 +1,6 @@ # Change Log +## 0.1.3 (2023/04/16) +- Added Older Dolphin support (e.g. Lua-Core) on windows ## 0.1.2 (2023/03/27) - Fixed MEM2 range ## 0.1.1 (2023/03/27) diff --git a/setup.cfg b/setup.cfg index 374cd2c..bd674e9 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,6 +1,6 @@ [metadata] name = supDolphinWS-server -version = 0.1.2 +version = 0.1.3 author = sup39 author_email = sms@sup39.dev description = A WebSocket server for accessing memory of emulated games in Dolphin diff --git a/src/supDolphinWS/server/dolphin.py b/src/supDolphinWS/server/dolphin.py index a56fbf2..d637992 100644 --- a/src/supDolphinWS/server/dolphin.py +++ b/src/supDolphinWS/server/dolphin.py @@ -5,6 +5,11 @@ import psutil import struct from multiprocessing.shared_memory import SharedMemory +if os.name == 'nt': + from .memory_windows import try_get_memory as try_get_memory_direct +else: + try_get_memory_direct = None + MEM1_START = 0x80000000 MEM1_END = 0x81800000 MEM2_START = 0x90000000 @@ -16,14 +21,23 @@ dolphinProcNames = \ else {'dolphin-emu', 'dolphin-emu-qt2', 'dolphin-emu-wx'} def try_get_memory(pid): - try: return SharedMemory('dolphin-emu.'+str(pid)) - except FileNotFoundError: return None + # newer Dolphin => SharedMemory + try: + return SharedMemory('dolphin-emu.'+str(pid)) + except FileNotFoundError: pass + # old Dolphin (on windows) => direct memory access via win32 api + if try_get_memory_direct: + try: + return try_get_memory_direct(pid) + except: + import traceback + print(traceback.format_exc()) def find_memory(exclude_pid={}): try: return next( (p.pid, m) for p in psutil.process_iter(['pid', 'name']) - if p.pid not in exclude_pid and p.name() in dolphinProcNames + if p.pid not in exclude_pid and p.name() in dolphinProcNames and p.status() == 'running' for m in [try_get_memory(p.pid)] if m is not None ) diff --git a/src/supDolphinWS/server/memory_windows.py b/src/supDolphinWS/server/memory_windows.py new file mode 100644 index 0000000..6015177 --- /dev/null +++ b/src/supDolphinWS/server/memory_windows.py @@ -0,0 +1,92 @@ +# SPDX-License-Identifier: MIT +# Copyright (c) 2023 sup39 +''' +The `try_get_memory` function is based on + `WindowsDolphinProcess::obtainEmuRAMInformations()` + (https://github.com/aldelaro5/Dolphin-memory-engine/blob/master/Source/DolphinProcess/Windows/WindowsDolphinProcess.cpp#L47) +from aldelaro5's Dolphin memory engine + (https://github.com/aldelaro5/Dolphin-memory-engine) +# SPDX-License-Identifier: MIT +# Copyright (c) 2017 aldelaro5 +''' + +import ctypes +from ctypes import wintypes +wintypes.SIZE_T = ctypes.c_uint32 if ctypes.sizeof(ctypes.c_void_p) == 4 else ctypes.c_uint64 + +PROCESS_QUERY_INFORMATION = 0x0400 +PROCESS_VM_READ = 0x0010 +PROCESS_VM_WRITE = 0x0020 +MEM_MAPPED = 0x40000 + +class MEMORY_BASIC_INFORMATION(ctypes.Structure): + _fields_ = [ + ("BaseAddress", wintypes.LPVOID), + ("AllocationBase", wintypes.LPVOID), + ("AllocationProtect", wintypes.DWORD), + ("RegionSize", wintypes.SIZE_T), + ("State", wintypes.DWORD), + ("Protect", wintypes.DWORD), + ("Type", wintypes.DWORD) + ] + +kernel32 = ctypes.WinDLL("kernel32.dll") +virtual_query_ex = kernel32.VirtualQueryEx +virtual_query_ex.argtypes = [wintypes.HANDLE, wintypes.LPCVOID, ctypes.POINTER(MEMORY_BASIC_INFORMATION), wintypes.SIZE_T] +virtual_query_ex.restype = wintypes.SIZE_T + +class FakeMemorySlice(bytes): + def tobytes(self): return self + +class ProcessMemoryView: + def __init__(self, hProcess, baseAddr): + self.hProcess = hProcess + self.baseAddr = baseAddr + def __getitem__(self, rg): + if type(rg) is not slice: raise TypeError('Index must be `slice`') + assert rg.start is not None and rg.stop is not None, 'Start and stop of the slice must be given' + addr = rg.start + size = rg.stop - rg.start + buf = ctypes.create_string_buffer(size) + nbyte = ctypes.c_size_t(0) + assert kernel32.ReadProcessMemory( + self.hProcess, + ctypes.c_void_p(self.baseAddr+addr), + buf, + ctypes.c_size_t(size), + ctypes.byref(nbyte), + ), f'Failed to read process memory. Error code: {kernel32.GetLastError()}' + return FakeMemorySlice(buf[slice(None, None, rg.step)]) + def __setitem__(self, rg, payload): + if type(rg) is not slice: raise TypeError('Index must be `slice`') + assert rg.start is not None, 'Start of the slice must be given' + addr = rg.start + nbyte = ctypes.c_size_t(0) + assert kernel32.WriteProcessMemory( + self.hProcess, + ctypes.c_void_p(self.baseAddr+addr), + bytes(payload), + ctypes.c_size_t(len(payload)), + ctypes.byref(nbyte) + ), f'Failed to write process memory. Error code: {kernel32.GetLastError()}' + return nbyte.value + def __len__(self): + return 0x2000000 # TODO + +class FakeSharedMemory: + def __init__(self, hProcess, baseAddr): + self.hProcess = hProcess + self.buf = ProcessMemoryView(hProcess, baseAddr) + def __del__(self): + self.close() + def close(self): + kernel32.CloseHandle(self.hProcess) + +def try_get_memory(pid): # TODO MEM2 + hProcess = kernel32.OpenProcess(PROCESS_QUERY_INFORMATION | PROCESS_VM_READ | PROCESS_VM_WRITE, False, pid) + baseAddr = 0 + memory_info = MEMORY_BASIC_INFORMATION() + while virtual_query_ex(hProcess, baseAddr, ctypes.byref(memory_info), ctypes.sizeof(memory_info)): + if memory_info.RegionSize == 0x2000000 and memory_info.Type == MEM_MAPPED: + return FakeSharedMemory(hProcess, baseAddr) + baseAddr += memory_info.RegionSize