[v0.1.0] init
This commit is contained in:
commit
82f82eba3e
13 changed files with 342 additions and 0 deletions
4
.gitignore
vendored
Normal file
4
.gitignore
vendored
Normal file
|
@ -0,0 +1,4 @@
|
||||||
|
/dist
|
||||||
|
.venv/
|
||||||
|
*.egg-info/
|
||||||
|
__pycache__/
|
21
LICENSE
Normal file
21
LICENSE
Normal file
|
@ -0,0 +1,21 @@
|
||||||
|
MIT License
|
||||||
|
|
||||||
|
Copyright (c) 2023 sup39
|
||||||
|
|
||||||
|
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.
|
23
README.md
Normal file
23
README.md
Normal file
|
@ -0,0 +1,23 @@
|
||||||
|
# supDolphinWSServer
|
||||||
|
A WebSocket server for accessing memory of emulated games in Dolphin.
|
||||||
|
|
||||||
|
This tool only runs on Windows currently.
|
||||||
|
|
||||||
|
## Prerequisite
|
||||||
|
- [Dolphin beta/dev version](https://dolphin-emu.org/download/) (stable version is NOT available)
|
||||||
|
- Python >= 3.8
|
||||||
|
|
||||||
|
## Installation/Upgrade
|
||||||
|
```
|
||||||
|
pip install -U supDolphinWS-server
|
||||||
|
```
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
```
|
||||||
|
python -m supDolphinWS.server
|
||||||
|
```
|
||||||
|
|
||||||
|
Use the following command to see the available options:
|
||||||
|
```
|
||||||
|
python -m supDolphinWS.server --help
|
||||||
|
```
|
3
pyproject.toml
Normal file
3
pyproject.toml
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
[build-system]
|
||||||
|
requires = ["setuptools>=42"]
|
||||||
|
build-backend = "setuptools.build_meta"
|
27
setup.cfg
Normal file
27
setup.cfg
Normal file
|
@ -0,0 +1,27 @@
|
||||||
|
[metadata]
|
||||||
|
name = supDolphinWS-server
|
||||||
|
version = 0.1.0
|
||||||
|
author = sup39
|
||||||
|
author_email = sms@sup39.dev
|
||||||
|
description = A WebSocket server for accessing memory of emulated games in Dolphin
|
||||||
|
long_description = file: README.md
|
||||||
|
long_description_content_type = text/markdown
|
||||||
|
url = https://github.com/sup39/supDolphinWS-server
|
||||||
|
license = MIT
|
||||||
|
project_urls =
|
||||||
|
Bug Tracker = https://github.com/sup39/supDolphinWS-server/issues
|
||||||
|
classifiers =
|
||||||
|
Programming Language :: Python :: 3
|
||||||
|
License :: OSI Approved :: MIT License
|
||||||
|
Operating System :: Microsoft :: Windows
|
||||||
|
|
||||||
|
[options]
|
||||||
|
include_package_data = True
|
||||||
|
packages = find:
|
||||||
|
python_requires = >=3.8
|
||||||
|
install_requires =
|
||||||
|
wsocket >= 2.1.0
|
||||||
|
psutil >= 5.9.4
|
||||||
|
|
||||||
|
[options.packages.find]
|
||||||
|
where = src
|
0
src/supDolphinWS/__init__.py
Normal file
0
src/supDolphinWS/__init__.py
Normal file
0
src/supDolphinWS/server/__init__.py
Normal file
0
src/supDolphinWS/server/__init__.py
Normal file
17
src/supDolphinWS/server/__main__.py
Normal file
17
src/supDolphinWS/server/__main__.py
Normal file
|
@ -0,0 +1,17 @@
|
||||||
|
# SPDX-License-Identifier: MIT
|
||||||
|
# Copyright (c) 2023 sup39
|
||||||
|
import argparse
|
||||||
|
from .server import run, VERSION
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
parser = argparse.ArgumentParser(prog='python -m supDolphinWS.server', add_help=False)
|
||||||
|
parser.add_argument('-h', '--host', default='localhost', help='the IP address that the server will listen on')
|
||||||
|
parser.add_argument('-p', '--port', default=35353, type=int, help='the port number that the server will listen on')
|
||||||
|
parser.add_argument('--exclude-pids', default=[], nargs='*', help='the pid list to exclude when finding dolphin')
|
||||||
|
parser.add_argument('-?', '--help', action='help', default=argparse.SUPPRESS, help='show this help message and exit')
|
||||||
|
parser.add_argument('--version', action='store_true', help='show the version of the server')
|
||||||
|
args = parser.parse_args()
|
||||||
|
if args.version:
|
||||||
|
print(VERSION)
|
||||||
|
else:
|
||||||
|
run(args)
|
72
src/supDolphinWS/server/api.py
Normal file
72
src/supDolphinWS/server/api.py
Normal file
|
@ -0,0 +1,72 @@
|
||||||
|
# SPDX-License-Identifier: MIT
|
||||||
|
# Copyright (c) 2023 sup39
|
||||||
|
VERSION = '0.1.0'
|
||||||
|
|
||||||
|
import struct
|
||||||
|
from .err import abort, ERR_DOLPHIN, ERR_BAD_REQ
|
||||||
|
from .dolphin import Dolphin
|
||||||
|
|
||||||
|
_d = Dolphin()
|
||||||
|
def get_dolphin():
|
||||||
|
if _d.m is None:
|
||||||
|
r = _d.hook()
|
||||||
|
if r is None: abort(ERR_DOLPHIN, 'Fail to find shared memory. Is the game running on Dolphin?')
|
||||||
|
return _d
|
||||||
|
|
||||||
|
def get_version(body):
|
||||||
|
return VERSION.encode()
|
||||||
|
|
||||||
|
def do_hook(body):
|
||||||
|
_d.unhook()
|
||||||
|
get_dolphin()
|
||||||
|
return b''
|
||||||
|
|
||||||
|
'''
|
||||||
|
u32 size
|
||||||
|
u32 base
|
||||||
|
s32[] addr segments
|
||||||
|
'''
|
||||||
|
def read_ram(body):
|
||||||
|
nbody = len(body)
|
||||||
|
if nbody < 4 or nbody % 4: abort(ERR_BAD_REQ, 'Bad payload')
|
||||||
|
## args
|
||||||
|
size, = struct.unpack('>I', body[:4])
|
||||||
|
addr = [
|
||||||
|
MEM1_START if nbody < 8 else struct.unpack('>I', body[4:8])[0],
|
||||||
|
*(p[0] for p in struct.iter_unpack('>i', body[8:])),
|
||||||
|
]
|
||||||
|
## access
|
||||||
|
d = get_dolphin()
|
||||||
|
return d.read_bytes(addr, size) or b''
|
||||||
|
|
||||||
|
'''
|
||||||
|
u32 size
|
||||||
|
u8[] content
|
||||||
|
u32 base
|
||||||
|
s32[] addr segments
|
||||||
|
'''
|
||||||
|
def write_ram(body):
|
||||||
|
nbody = len(body)
|
||||||
|
if nbody < 4: abort(ERR_BAD_REQ, 'Bad payload')
|
||||||
|
## size
|
||||||
|
size, = struct.unpack('>I', body[:4])
|
||||||
|
i = 4+size
|
||||||
|
if nbody < i: abort(ERR_BAD_REQ, 'Bad payload')
|
||||||
|
content = body[4:i]
|
||||||
|
## addr
|
||||||
|
if (nbody-i) % 4: abort(ERR_BAD_REQ, 'Bad payload')
|
||||||
|
addr = [
|
||||||
|
MEM1_START if nbody == i else struct.unpack('>I', body[i:i+4])[0],
|
||||||
|
*(p[0] for p in struct.iter_unpack('>i', body[i+4:])),
|
||||||
|
]
|
||||||
|
## access
|
||||||
|
d = get_dolphin()
|
||||||
|
a = d.write_bytes(addr, content)
|
||||||
|
return b'' if a is None else struct.pack('>I', a)
|
||||||
|
|
||||||
|
handlers = {
|
||||||
|
0x00: get_version,
|
||||||
|
0x01: do_hook,
|
||||||
|
0x02: read_ram,
|
||||||
|
0x03: write_ram,
|
||||||
|
}
|
93
src/supDolphinWS/server/dolphin.py
Normal file
93
src/supDolphinWS/server/dolphin.py
Normal file
|
@ -0,0 +1,93 @@
|
||||||
|
# SPDX-License-Identifier: MIT
|
||||||
|
# Copyright (c) 2023 sup39
|
||||||
|
import os
|
||||||
|
import psutil
|
||||||
|
import struct
|
||||||
|
from multiprocessing.shared_memory import SharedMemory
|
||||||
|
|
||||||
|
MEM1_START = 0x80000000
|
||||||
|
MEM1_END = 0x81800000
|
||||||
|
MEM2_START = 0x90000000
|
||||||
|
MEM2_END = 0x93000000
|
||||||
|
MEM2_OFFSET = 0x4040000
|
||||||
|
|
||||||
|
dolphinProcNames = \
|
||||||
|
{'Dolphin.exe', 'DolphinQt2.exe', 'DolphinWx.exe'} if os.name == 'nt' \
|
||||||
|
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
|
||||||
|
|
||||||
|
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
|
||||||
|
for m in [try_get_memory(p.pid)]
|
||||||
|
if m is not None
|
||||||
|
)
|
||||||
|
except StopIteration: return None
|
||||||
|
|
||||||
|
class Dolphin():
|
||||||
|
def __init__(self):
|
||||||
|
self.pid = None
|
||||||
|
self.m = None
|
||||||
|
def hook(self, exclude_pid={}):
|
||||||
|
r = find_memory(exclude_pid)
|
||||||
|
if r is None: return
|
||||||
|
## success
|
||||||
|
self.pid, self.m = r
|
||||||
|
return r
|
||||||
|
def unhook(self):
|
||||||
|
self.pid = None
|
||||||
|
if self.m is not None:
|
||||||
|
self.m.close()
|
||||||
|
self.m = None
|
||||||
|
def _read_bytes(self, addr, size):
|
||||||
|
'''
|
||||||
|
addr: int
|
||||||
|
size: int
|
||||||
|
'''
|
||||||
|
if MEM1_START <= addr <= MEM1_END-size:
|
||||||
|
idx = addr-MEM1_START
|
||||||
|
elif MEM2_START <= addr <= MEM2_END-size and len(self.m) > MEM2_OFFSET:
|
||||||
|
idx = MEM2_OFFSET + addr-MEM2_START
|
||||||
|
else: return None
|
||||||
|
return self.m.buf[idx:idx+size].tobytes()
|
||||||
|
def _write_bytes(self, addr, payload):
|
||||||
|
'''
|
||||||
|
addr: int
|
||||||
|
payload: bytes
|
||||||
|
'''
|
||||||
|
size = len(payload)
|
||||||
|
if MEM1_START <= addr <= MEM1_END-size:
|
||||||
|
idx = addr-MEM1_START
|
||||||
|
elif MEM2_START <= addr <= MEM2_END-size and len(self.m) > MEM2_OFFSET:
|
||||||
|
idx = MEM2_OFFSET + addr-MEM2_START
|
||||||
|
else: return None
|
||||||
|
self.m.buf[idx:idx+size] = payload
|
||||||
|
return addr
|
||||||
|
def resolve_addr(self, addr):
|
||||||
|
'''
|
||||||
|
addr: int|int[]
|
||||||
|
'''
|
||||||
|
offs = [addr] if isinstance(addr, int) else addr
|
||||||
|
addr = 0
|
||||||
|
for off in offs[:-1]:
|
||||||
|
raw = self._read_bytes(addr+off, 4)
|
||||||
|
if raw is None: return None
|
||||||
|
addr, = struct.unpack('>I', raw)
|
||||||
|
return addr + (offs[-1] if len(offs) else 0)
|
||||||
|
def read_bytes(self, addr, size):
|
||||||
|
'''
|
||||||
|
addr: int|int[]
|
||||||
|
size: int
|
||||||
|
'''
|
||||||
|
return self._read_bytes(self.resolve_addr(addr), size)
|
||||||
|
def write_bytes(self, addr, payload):
|
||||||
|
'''
|
||||||
|
addr: int|int[]
|
||||||
|
payload: bytes
|
||||||
|
'''
|
||||||
|
return self._write_bytes(self.resolve_addr(addr), payload)
|
14
src/supDolphinWS/server/err.py
Normal file
14
src/supDolphinWS/server/err.py
Normal file
|
@ -0,0 +1,14 @@
|
||||||
|
# SPDX-License-Identifier: MIT
|
||||||
|
# Copyright (c) 2023 sup39
|
||||||
|
|
||||||
|
ERR_UNKNOWN = 255
|
||||||
|
ERR_CMD_NOT_FOUND = 254
|
||||||
|
ERR_DOLPHIN = 253
|
||||||
|
ERR_BAD_REQ = 252
|
||||||
|
|
||||||
|
class ErrorResponse(Exception):
|
||||||
|
def __init__(self, code, payload):
|
||||||
|
self.code = code
|
||||||
|
self.payload = payload
|
||||||
|
def abort(code, payload=''):
|
||||||
|
raise ErrorResponse(code, payload)
|
51
src/supDolphinWS/server/server.py
Normal file
51
src/supDolphinWS/server/server.py
Normal file
|
@ -0,0 +1,51 @@
|
||||||
|
# SPDX-License-Identifier: MIT
|
||||||
|
# Copyright (c) 2023 sup39
|
||||||
|
from wsocket import WSocketApp, run as _run
|
||||||
|
from .utils import tryParseInt, parseAddrPath
|
||||||
|
from .api import VERSION, handlers, _d
|
||||||
|
from .err import *
|
||||||
|
|
||||||
|
'''
|
||||||
|
u32 id
|
||||||
|
u8 cmdtype
|
||||||
|
'''
|
||||||
|
def on_message(msg, client):
|
||||||
|
if type(msg) != bytearray: return
|
||||||
|
if len(msg) < 5: return
|
||||||
|
ID, cmdtype, body = msg[:4], msg[4], msg[5:]
|
||||||
|
handler = handlers.get(cmdtype)
|
||||||
|
if handler is None:
|
||||||
|
return client.send(ID+struct.pack('>B', ERR_CMD_NOT_FOUND))
|
||||||
|
try:
|
||||||
|
res = handler(body)
|
||||||
|
code = 0
|
||||||
|
if res is None:
|
||||||
|
code = ERR_UNKNOWN
|
||||||
|
res = b''
|
||||||
|
except ErrorResponse as err:
|
||||||
|
code, res = err.code, err.payload.encode()
|
||||||
|
client.send(ID+code.to_bytes(1, 'big')+res)
|
||||||
|
|
||||||
|
class MyWSocketApp(WSocketApp):
|
||||||
|
def wsgi(self, environ, start_response):
|
||||||
|
wsock = environ.get("wsgi.websocket")
|
||||||
|
## plain HTTP
|
||||||
|
if not wsock:
|
||||||
|
start_response()
|
||||||
|
return '<h1>supDolphinWSServer</h1>'
|
||||||
|
## WebSocket
|
||||||
|
self.onconnect(wsock)
|
||||||
|
while True:
|
||||||
|
try:
|
||||||
|
message = wsock.receive()
|
||||||
|
if message != None:
|
||||||
|
self.onmessage(message, wsock)
|
||||||
|
except WebSocketError as e:
|
||||||
|
break
|
||||||
|
return []
|
||||||
|
|
||||||
|
def run(args):
|
||||||
|
app = WSocketApp()
|
||||||
|
app.onmessage += on_message
|
||||||
|
_d.hook()
|
||||||
|
_run(app, host=args.host, port=args.port)
|
17
src/supDolphinWS/server/utils.py
Normal file
17
src/supDolphinWS/server/utils.py
Normal file
|
@ -0,0 +1,17 @@
|
||||||
|
# SPDX-License-Identifier: MIT
|
||||||
|
# Copyright (c) 2023 sup39
|
||||||
|
def tryParseInt(x, radix=10):
|
||||||
|
try:
|
||||||
|
return int(x, radix)
|
||||||
|
except ValueError:
|
||||||
|
return None
|
||||||
|
|
||||||
|
def parseAddrPath(s):
|
||||||
|
if s == '': return [MEM1_START]
|
||||||
|
try:
|
||||||
|
return [
|
||||||
|
int(ch, 16)
|
||||||
|
for ch in s.split('/')
|
||||||
|
]
|
||||||
|
except ValueError:
|
||||||
|
return None
|
Reference in a new issue