[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