openspeedy 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- openspeedy/__init__.py +87 -0
- openspeedy/_constants.py +130 -0
- openspeedy/_core.py +240 -0
- openspeedy/_dll_resolver.py +82 -0
- openspeedy/_eject.py +139 -0
- openspeedy/_exceptions.py +111 -0
- openspeedy/_inject.py +209 -0
- openspeedy/_process.py +345 -0
- openspeedy/_speeddll.py +157 -0
- openspeedy/_types.py +46 -0
- openspeedy/_winapi.py +515 -0
- openspeedy/data/speedpatch32.dll +0 -0
- openspeedy/data/speedpatch64.dll +0 -0
- openspeedy/py.typed +0 -0
- openspeedy/tests/__init__.py +0 -0
- openspeedy/tests/conftest.py +113 -0
- openspeedy/tests/test_core.py +132 -0
- openspeedy/tests/test_exceptions.py +79 -0
- openspeedy/tests/test_types.py +58 -0
- openspeedy-0.1.0.dist-info/LICENSE +674 -0
- openspeedy-0.1.0.dist-info/METADATA +537 -0
- openspeedy-0.1.0.dist-info/RECORD +24 -0
- openspeedy-0.1.0.dist-info/WHEEL +5 -0
- openspeedy-0.1.0.dist-info/top_level.txt +1 -0
openspeedy/__init__.py
ADDED
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
"""
|
|
2
|
+
OpenSpeedy — Python bindings for the OpenSpeedy game speed controller.
|
|
3
|
+
|
|
4
|
+
This library wraps the OpenSpeedy speedpatch DLL injection mechanism,
|
|
5
|
+
allowing Python scripts to accelerate or decelerate Windows games by
|
|
6
|
+
intercepting time-related API functions in target processes.
|
|
7
|
+
|
|
8
|
+
**Windows only.** On any other platform, importing this package raises
|
|
9
|
+
``ImportError`` immediately.
|
|
10
|
+
|
|
11
|
+
Quick start
|
|
12
|
+
-----------
|
|
13
|
+
|
|
14
|
+
::
|
|
15
|
+
|
|
16
|
+
from openspeedy import SpeedController
|
|
17
|
+
|
|
18
|
+
sc = SpeedController()
|
|
19
|
+
|
|
20
|
+
# List running processes
|
|
21
|
+
for proc in sc.list_processes():
|
|
22
|
+
print(f"PID {proc.pid}: {proc.name} ({proc.arch})")
|
|
23
|
+
|
|
24
|
+
# Inject into a process and set 2x speed
|
|
25
|
+
sc.inject(1234)
|
|
26
|
+
sc.set_speed(2.0)
|
|
27
|
+
|
|
28
|
+
# Check status
|
|
29
|
+
print(f"Speed: {sc.get_speed()}x, enabled: {sc.is_enabled(1234)}")
|
|
30
|
+
|
|
31
|
+
# Clean up
|
|
32
|
+
sc.close()
|
|
33
|
+
|
|
34
|
+
License
|
|
35
|
+
-------
|
|
36
|
+
|
|
37
|
+
GPL v3. See the LICENSE file for full text.
|
|
38
|
+
|
|
39
|
+
The bundled ``speedpatch*.dll`` files incorporate MinHook, which is
|
|
40
|
+
licensed under the BSD 2-Clause License.
|
|
41
|
+
"""
|
|
42
|
+
|
|
43
|
+
import sys
|
|
44
|
+
|
|
45
|
+
if sys.platform != "win32":
|
|
46
|
+
raise ImportError(
|
|
47
|
+
"openspeedy is Windows-only. "
|
|
48
|
+
"It requires Windows APIs for DLL injection, process manipulation, "
|
|
49
|
+
"and shared memory. "
|
|
50
|
+
f"Current platform: {sys.platform}"
|
|
51
|
+
)
|
|
52
|
+
|
|
53
|
+
from openspeedy._core import SpeedController
|
|
54
|
+
from openspeedy._exceptions import (
|
|
55
|
+
DLLNotFoundError,
|
|
56
|
+
EjectionError,
|
|
57
|
+
InjectionError,
|
|
58
|
+
OpenSpeedyError,
|
|
59
|
+
PlatformNotSupportedError,
|
|
60
|
+
ProcessAccessDeniedError,
|
|
61
|
+
ProcessArchitectureMismatch,
|
|
62
|
+
ProcessNotFoundError,
|
|
63
|
+
SpeedControlError,
|
|
64
|
+
SpeedRangeError,
|
|
65
|
+
)
|
|
66
|
+
from openspeedy._types import ModuleInfo, ProcessInfo
|
|
67
|
+
|
|
68
|
+
__version__ = "0.1.0"
|
|
69
|
+
|
|
70
|
+
__all__ = [
|
|
71
|
+
# Main class
|
|
72
|
+
"SpeedController",
|
|
73
|
+
# Data types
|
|
74
|
+
"ProcessInfo",
|
|
75
|
+
"ModuleInfo",
|
|
76
|
+
# Exceptions
|
|
77
|
+
"OpenSpeedyError",
|
|
78
|
+
"PlatformNotSupportedError",
|
|
79
|
+
"DLLNotFoundError",
|
|
80
|
+
"ProcessAccessDeniedError",
|
|
81
|
+
"ProcessNotFoundError",
|
|
82
|
+
"ProcessArchitectureMismatch",
|
|
83
|
+
"InjectionError",
|
|
84
|
+
"EjectionError",
|
|
85
|
+
"SpeedRangeError",
|
|
86
|
+
"SpeedControlError",
|
|
87
|
+
]
|
openspeedy/_constants.py
ADDED
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Win32 API constants used across the openspeedy package.
|
|
3
|
+
|
|
4
|
+
Grouped by API surface for readability. All values are from the Windows SDK.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
# ---------------------------------------------------------------------------
|
|
8
|
+
# Process access rights
|
|
9
|
+
# ---------------------------------------------------------------------------
|
|
10
|
+
PROCESS_TERMINATE = 0x0001
|
|
11
|
+
PROCESS_CREATE_THREAD = 0x0002
|
|
12
|
+
PROCESS_SET_SESSIONID = 0x0004
|
|
13
|
+
PROCESS_VM_OPERATION = 0x0008
|
|
14
|
+
PROCESS_VM_READ = 0x0010
|
|
15
|
+
PROCESS_VM_WRITE = 0x0020
|
|
16
|
+
PROCESS_DUP_HANDLE = 0x0040
|
|
17
|
+
PROCESS_CREATE_PROCESS = 0x0080
|
|
18
|
+
PROCESS_SET_QUOTA = 0x0100
|
|
19
|
+
PROCESS_SET_INFORMATION = 0x0200
|
|
20
|
+
PROCESS_QUERY_INFORMATION = 0x0400
|
|
21
|
+
PROCESS_SUSPEND_RESUME = 0x0800
|
|
22
|
+
PROCESS_QUERY_LIMITED_INFORMATION = 0x1000
|
|
23
|
+
PROCESS_ALL_ACCESS = 0x1FFFFF
|
|
24
|
+
|
|
25
|
+
# Commonly needed combination for DLL injection
|
|
26
|
+
PROCESS_INJECT_ACCESS = (
|
|
27
|
+
PROCESS_CREATE_THREAD
|
|
28
|
+
| PROCESS_QUERY_INFORMATION
|
|
29
|
+
| PROCESS_VM_OPERATION
|
|
30
|
+
| PROCESS_VM_WRITE
|
|
31
|
+
| PROCESS_VM_READ
|
|
32
|
+
)
|
|
33
|
+
|
|
34
|
+
PROCESS_EJECT_ACCESS = (
|
|
35
|
+
PROCESS_CREATE_THREAD
|
|
36
|
+
| PROCESS_QUERY_INFORMATION
|
|
37
|
+
| PROCESS_VM_OPERATION
|
|
38
|
+
)
|
|
39
|
+
|
|
40
|
+
# ---------------------------------------------------------------------------
|
|
41
|
+
# Memory allocation
|
|
42
|
+
# ---------------------------------------------------------------------------
|
|
43
|
+
MEM_COMMIT = 0x00001000
|
|
44
|
+
MEM_RESERVE = 0x00002000
|
|
45
|
+
MEM_RELEASE = 0x00008000
|
|
46
|
+
MEM_DECOMMIT = 0x00004000
|
|
47
|
+
PAGE_READWRITE = 0x04
|
|
48
|
+
PAGE_EXECUTE_READWRITE = 0x40
|
|
49
|
+
|
|
50
|
+
# ---------------------------------------------------------------------------
|
|
51
|
+
# ToolHelp snapshots
|
|
52
|
+
# ---------------------------------------------------------------------------
|
|
53
|
+
TH32CS_SNAPPROCESS = 0x00000002
|
|
54
|
+
TH32CS_SNAPMODULE = 0x00000008
|
|
55
|
+
TH32CS_SNAPTHREAD = 0x00000004
|
|
56
|
+
|
|
57
|
+
# ---------------------------------------------------------------------------
|
|
58
|
+
# File mapping
|
|
59
|
+
# ---------------------------------------------------------------------------
|
|
60
|
+
FILE_MAP_ALL_ACCESS = 0x000F001F
|
|
61
|
+
FILE_MAP_READ = 0x00000004
|
|
62
|
+
FILE_MAP_WRITE = 0x00000002
|
|
63
|
+
SECTION_QUERY = 0x0001
|
|
64
|
+
SECTION_MAP_WRITE = 0x0002
|
|
65
|
+
SECTION_MAP_READ = 0x0004
|
|
66
|
+
SECTION_MAP_EXECUTE = 0x0008
|
|
67
|
+
SECTION_EXTEND_SIZE = 0x0010
|
|
68
|
+
SECTION_ALL_ACCESS = 0x000F001F
|
|
69
|
+
STANDARD_RIGHTS_REQUIRED = 0x000F0000
|
|
70
|
+
INVALID_HANDLE_VALUE_SIGNED = -1
|
|
71
|
+
|
|
72
|
+
# ---------------------------------------------------------------------------
|
|
73
|
+
# Wait / synchronization
|
|
74
|
+
# ---------------------------------------------------------------------------
|
|
75
|
+
INFINITE = 0xFFFFFFFF
|
|
76
|
+
WAIT_OBJECT_0 = 0x00000000
|
|
77
|
+
WAIT_TIMEOUT = 0x00000102
|
|
78
|
+
WAIT_FAILED = 0xFFFFFFFF
|
|
79
|
+
WAIT_ABANDONED_0 = 0x00000080
|
|
80
|
+
|
|
81
|
+
# ---------------------------------------------------------------------------
|
|
82
|
+
# Token / security
|
|
83
|
+
# ---------------------------------------------------------------------------
|
|
84
|
+
TOKEN_QUERY = 0x0008
|
|
85
|
+
TOKEN_ADJUST_PRIVILEGES = 0x0020
|
|
86
|
+
TokenElevation = 20 # TOKEN_INFORMATION_CLASS
|
|
87
|
+
|
|
88
|
+
# ---------------------------------------------------------------------------
|
|
89
|
+
# Process name format
|
|
90
|
+
# ---------------------------------------------------------------------------
|
|
91
|
+
PROCESS_NAME_WIN32 = 0
|
|
92
|
+
PROCESS_NAME_NATIVE = 1
|
|
93
|
+
|
|
94
|
+
# ---------------------------------------------------------------------------
|
|
95
|
+
# Module filter flags for EnumProcessModulesEx
|
|
96
|
+
# ---------------------------------------------------------------------------
|
|
97
|
+
LIST_MODULES_DEFAULT = 0x00
|
|
98
|
+
LIST_MODULES_32BIT = 0x01
|
|
99
|
+
LIST_MODULES_64BIT = 0x02
|
|
100
|
+
LIST_MODULES_ALL = 0x03
|
|
101
|
+
|
|
102
|
+
# ---------------------------------------------------------------------------
|
|
103
|
+
# Error codes
|
|
104
|
+
# ---------------------------------------------------------------------------
|
|
105
|
+
ERROR_SUCCESS = 0
|
|
106
|
+
ERROR_ACCESS_DENIED = 5
|
|
107
|
+
ERROR_INVALID_PARAMETER = 87
|
|
108
|
+
ERROR_PIPE_CONNECTED = 535
|
|
109
|
+
ERROR_BROKEN_PIPE = 109
|
|
110
|
+
ERROR_NO_DATA = 232
|
|
111
|
+
ERROR_PIPE_NOT_CONNECTED = 233
|
|
112
|
+
ERROR_MORE_DATA = 234
|
|
113
|
+
|
|
114
|
+
# ---------------------------------------------------------------------------
|
|
115
|
+
# Window enumeration
|
|
116
|
+
# ---------------------------------------------------------------------------
|
|
117
|
+
GW_OWNER = 4
|
|
118
|
+
GW_HWNDFIRST = 0
|
|
119
|
+
GW_HWNDLAST = 1
|
|
120
|
+
GW_HWNDNEXT = 2
|
|
121
|
+
GW_HWNDPREV = 3
|
|
122
|
+
GW_CHILD = 5
|
|
123
|
+
GW_ENABLEDPOPUP = 6
|
|
124
|
+
|
|
125
|
+
# ---------------------------------------------------------------------------
|
|
126
|
+
# Speed factor limits
|
|
127
|
+
# ---------------------------------------------------------------------------
|
|
128
|
+
SPEED_MIN = 0.001
|
|
129
|
+
SPEED_MAX = 1000.0
|
|
130
|
+
SPEED_DEFAULT = 1.0
|
openspeedy/_core.py
ADDED
|
@@ -0,0 +1,240 @@
|
|
|
1
|
+
"""
|
|
2
|
+
High-level ``SpeedController`` — the main entry point for the openspeedy
|
|
3
|
+
library.
|
|
4
|
+
|
|
5
|
+
Aggregates process enumeration, DLL injection/ejection, speed factor
|
|
6
|
+
control, and per-process enable/disable into a single, easy-to-use class.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
import threading
|
|
10
|
+
from contextlib import contextmanager
|
|
11
|
+
from typing import Iterator, List, Set
|
|
12
|
+
|
|
13
|
+
from ._constants import SPEED_DEFAULT
|
|
14
|
+
from ._eject import eject_dll as _eject_dll
|
|
15
|
+
from ._inject import inject_dll as _inject_dll
|
|
16
|
+
from ._process import list_processes as _list_processes
|
|
17
|
+
from ._speeddll import (
|
|
18
|
+
disable as _disable,
|
|
19
|
+
enable as _enable,
|
|
20
|
+
get_speed as _get_speed,
|
|
21
|
+
is_enabled as _is_enabled,
|
|
22
|
+
set_speed as _set_speed,
|
|
23
|
+
)
|
|
24
|
+
from ._types import ProcessInfo
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class SpeedController:
|
|
28
|
+
"""Control game speed by injecting OpenSpeedy's speedpatch DLL into
|
|
29
|
+
target Windows processes.
|
|
30
|
+
|
|
31
|
+
Usage::
|
|
32
|
+
|
|
33
|
+
from openspeedy import SpeedController
|
|
34
|
+
|
|
35
|
+
sc = SpeedController()
|
|
36
|
+
processes = sc.list_processes()
|
|
37
|
+
|
|
38
|
+
# Inject into a game
|
|
39
|
+
sc.inject(pid)
|
|
40
|
+
|
|
41
|
+
# Set 2x speed
|
|
42
|
+
sc.set_speed(2.0)
|
|
43
|
+
|
|
44
|
+
# Temporarily boost to 5x
|
|
45
|
+
with sc.speed_context(5.0):
|
|
46
|
+
... # game runs at 5x
|
|
47
|
+
|
|
48
|
+
# Clean up
|
|
49
|
+
sc.close()
|
|
50
|
+
|
|
51
|
+
Thread safety
|
|
52
|
+
-------------
|
|
53
|
+
|
|
54
|
+
Injection/ejection operations are serialized with an internal lock.
|
|
55
|
+
Speed factor read/write is inherently atomic (the DLL uses
|
|
56
|
+
``std::atomic<double>`` in a shared PE section) and requires no
|
|
57
|
+
additional synchronization at the Python level.
|
|
58
|
+
|
|
59
|
+
.. note::
|
|
60
|
+
|
|
61
|
+
**v1 architecture limitation**: A 64-bit Python interpreter can
|
|
62
|
+
only inject into 64-bit processes; a 32-bit interpreter can only
|
|
63
|
+
inject into 32-bit processes. This is because ``LoadLibraryW``
|
|
64
|
+
must be at the same virtual address in both the calling and target
|
|
65
|
+
processes.
|
|
66
|
+
"""
|
|
67
|
+
|
|
68
|
+
def __init__(self) -> None:
|
|
69
|
+
self._injected_pids: Set[int] = set()
|
|
70
|
+
self._lock = threading.Lock()
|
|
71
|
+
|
|
72
|
+
# ── Process enumeration ─────────────────────────────────────────────
|
|
73
|
+
|
|
74
|
+
def list_processes(self, fast: bool = True) -> List[ProcessInfo]:
|
|
75
|
+
"""Enumerate all running Windows processes.
|
|
76
|
+
|
|
77
|
+
Args:
|
|
78
|
+
fast: If ``True`` (default), use snapshot-only enumeration
|
|
79
|
+
(faster but less detail). If ``False``, open each process
|
|
80
|
+
to collect window title, memory, path, and admin status.
|
|
81
|
+
|
|
82
|
+
Returns:
|
|
83
|
+
List of :class:`ProcessInfo` objects.
|
|
84
|
+
"""
|
|
85
|
+
return _list_processes(fast=fast)
|
|
86
|
+
|
|
87
|
+
# ── Injection / ejection ────────────────────────────────────────────
|
|
88
|
+
|
|
89
|
+
def inject(self, pid: int) -> None:
|
|
90
|
+
"""Inject the speedpatch DLL into a target process and enable
|
|
91
|
+
speed control for it.
|
|
92
|
+
|
|
93
|
+
Args:
|
|
94
|
+
pid: Target process ID.
|
|
95
|
+
|
|
96
|
+
Raises:
|
|
97
|
+
ProcessNotFoundError: The PID does not exist.
|
|
98
|
+
ProcessAccessDeniedError: Cannot open the process (protected
|
|
99
|
+
or insufficient privileges).
|
|
100
|
+
ProcessArchitectureMismatch: Python and target architectures
|
|
101
|
+
differ.
|
|
102
|
+
InjectionError: The injection itself failed.
|
|
103
|
+
"""
|
|
104
|
+
with self._lock:
|
|
105
|
+
_inject_dll(pid)
|
|
106
|
+
_enable(pid)
|
|
107
|
+
self._injected_pids.add(pid)
|
|
108
|
+
|
|
109
|
+
def eject(self, pid: int) -> None:
|
|
110
|
+
"""Unload the speedpatch DLL from a target process.
|
|
111
|
+
|
|
112
|
+
Args:
|
|
113
|
+
pid: Target process ID.
|
|
114
|
+
|
|
115
|
+
Raises:
|
|
116
|
+
EjectionError: The DLL was not found or ``FreeLibrary`` failed.
|
|
117
|
+
"""
|
|
118
|
+
with self._lock:
|
|
119
|
+
_eject_dll(pid)
|
|
120
|
+
self._injected_pids.discard(pid)
|
|
121
|
+
|
|
122
|
+
# ── Global speed factor ─────────────────────────────────────────────
|
|
123
|
+
|
|
124
|
+
def set_speed(self, factor: float) -> None:
|
|
125
|
+
"""Set the global speed multiplier.
|
|
126
|
+
|
|
127
|
+
Applies to **all** processes that have the speedpatch DLL injected
|
|
128
|
+
(via the shared PE data section).
|
|
129
|
+
|
|
130
|
+
Args:
|
|
131
|
+
factor: Speed multiplier (1.0 = normal, 2.0 = double,
|
|
132
|
+
0.5 = half). Must be in ``[0.001, 1000.0]``.
|
|
133
|
+
|
|
134
|
+
Raises:
|
|
135
|
+
SpeedRangeError: The factor is out of the valid range.
|
|
136
|
+
"""
|
|
137
|
+
_set_speed(factor)
|
|
138
|
+
|
|
139
|
+
def get_speed(self) -> float:
|
|
140
|
+
"""Get the current global speed multiplier.
|
|
141
|
+
|
|
142
|
+
Returns:
|
|
143
|
+
The speed factor (defaults to 1.0).
|
|
144
|
+
"""
|
|
145
|
+
return _get_speed()
|
|
146
|
+
|
|
147
|
+
# ── Per-process enable / disable ────────────────────────────────────
|
|
148
|
+
|
|
149
|
+
def enable(self, pid: int) -> None:
|
|
150
|
+
"""Enable (or re-enable) speed control for a previously injected
|
|
151
|
+
process.
|
|
152
|
+
|
|
153
|
+
When a process is disabled, the injected DLL still intercepts time
|
|
154
|
+
API calls but returns unmodified values (factor = 1.0). Calling
|
|
155
|
+
``enable()`` restores speed modification.
|
|
156
|
+
|
|
157
|
+
Args:
|
|
158
|
+
pid: Target process ID.
|
|
159
|
+
"""
|
|
160
|
+
_enable(pid)
|
|
161
|
+
|
|
162
|
+
def disable(self, pid: int) -> None:
|
|
163
|
+
"""Temporarily disable speed control for a process.
|
|
164
|
+
|
|
165
|
+
The process still has the DLL loaded, but time APIs are not
|
|
166
|
+
affected. Call :meth:`enable` to re-apply speed control.
|
|
167
|
+
|
|
168
|
+
Args:
|
|
169
|
+
pid: Target process ID.
|
|
170
|
+
"""
|
|
171
|
+
_disable(pid)
|
|
172
|
+
|
|
173
|
+
def is_enabled(self, pid: int) -> bool:
|
|
174
|
+
"""Check whether speed control is currently active for a process.
|
|
175
|
+
|
|
176
|
+
Args:
|
|
177
|
+
pid: Target process ID.
|
|
178
|
+
|
|
179
|
+
Returns:
|
|
180
|
+
``True`` if speed control is enabled for the process.
|
|
181
|
+
"""
|
|
182
|
+
return _is_enabled(pid)
|
|
183
|
+
|
|
184
|
+
# ── Context manager ─────────────────────────────────────────────────
|
|
185
|
+
|
|
186
|
+
@contextmanager
|
|
187
|
+
def speed_context(self, factor: float) -> Iterator[None]:
|
|
188
|
+
"""Temporarily set a speed factor, restoring the previous value on
|
|
189
|
+
exit.
|
|
190
|
+
|
|
191
|
+
Usage::
|
|
192
|
+
|
|
193
|
+
with sc.speed_context(3.0):
|
|
194
|
+
# everything runs at 3x
|
|
195
|
+
...
|
|
196
|
+
# speed restored
|
|
197
|
+
|
|
198
|
+
Args:
|
|
199
|
+
factor: Temporary speed multiplier.
|
|
200
|
+
"""
|
|
201
|
+
old = _get_speed()
|
|
202
|
+
_set_speed(factor)
|
|
203
|
+
try:
|
|
204
|
+
yield
|
|
205
|
+
finally:
|
|
206
|
+
_set_speed(old)
|
|
207
|
+
|
|
208
|
+
# ── Cleanup ─────────────────────────────────────────────────────────
|
|
209
|
+
|
|
210
|
+
def close(self) -> None:
|
|
211
|
+
"""Eject from all processes that were injected by this controller
|
|
212
|
+
instance.
|
|
213
|
+
|
|
214
|
+
This is a best-effort operation — some ejections may fail
|
|
215
|
+
(e.g. if the target process has already exited). Failures are
|
|
216
|
+
logged but do not prevent ejecting the remaining processes.
|
|
217
|
+
|
|
218
|
+
The controller can still be used after ``close()``; the tracking
|
|
219
|
+
set is simply cleared.
|
|
220
|
+
"""
|
|
221
|
+
with self._lock:
|
|
222
|
+
pids = list(self._injected_pids)
|
|
223
|
+
for pid in pids:
|
|
224
|
+
try:
|
|
225
|
+
_eject_dll(pid)
|
|
226
|
+
except Exception:
|
|
227
|
+
pass # best-effort
|
|
228
|
+
self._injected_pids.clear()
|
|
229
|
+
|
|
230
|
+
def __enter__(self) -> "SpeedController":
|
|
231
|
+
return self
|
|
232
|
+
|
|
233
|
+
def __exit__(self, *args: object) -> None:
|
|
234
|
+
self.close()
|
|
235
|
+
|
|
236
|
+
def __repr__(self) -> str:
|
|
237
|
+
return (
|
|
238
|
+
f"<SpeedController injected={len(self._injected_pids)} "
|
|
239
|
+
f"speed={_get_speed():.2f}>"
|
|
240
|
+
)
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Locate the speedpatch DLLs on disk.
|
|
3
|
+
|
|
4
|
+
Resolution order:
|
|
5
|
+
1. ``OPENSPEEDY_DLL_DIR`` environment variable (directory containing the DLLs)
|
|
6
|
+
2. Package data directory ``openspeedy/data/``
|
|
7
|
+
3. Current working directory
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
import os
|
|
11
|
+
import struct
|
|
12
|
+
from pathlib import Path
|
|
13
|
+
from typing import Optional
|
|
14
|
+
|
|
15
|
+
from ._exceptions import DLLNotFoundError
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def _get_data_dir() -> Path:
|
|
19
|
+
"""Return the absolute path to ``openspeedy/data/``."""
|
|
20
|
+
return Path(__file__).resolve().parent / "data"
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def _is_python_64bit() -> bool:
|
|
24
|
+
"""Return ``True`` if the running Python interpreter is 64-bit."""
|
|
25
|
+
return struct.calcsize("P") == 8
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def _get_own_dll_path() -> Path:
|
|
29
|
+
"""Return the path to the speedpatch DLL matching the Python
|
|
30
|
+
interpreter's own architecture."""
|
|
31
|
+
dll_name = "speedpatch64.dll" if _is_python_64bit() else "speedpatch32.dll"
|
|
32
|
+
return _resolve_dll_path(dll_name)
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def _get_target_dll_path(is_64bit: bool) -> Path:
|
|
36
|
+
"""Return the path to the speedpatch DLL for a target of the given
|
|
37
|
+
architecture."""
|
|
38
|
+
dll_name = "speedpatch64.dll" if is_64bit else "speedpatch32.dll"
|
|
39
|
+
return _resolve_dll_path(dll_name)
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def _resolve_dll_path(dll_name: str) -> Path:
|
|
43
|
+
"""Resolve a DLL by name using the standard search order.
|
|
44
|
+
|
|
45
|
+
Raises:
|
|
46
|
+
DLLNotFoundError: If the DLL cannot be found at any search location.
|
|
47
|
+
"""
|
|
48
|
+
# 1. Environment variable override
|
|
49
|
+
env_dir = os.environ.get("OPENSPEEDY_DLL_DIR")
|
|
50
|
+
if env_dir:
|
|
51
|
+
candidate = Path(env_dir) / dll_name
|
|
52
|
+
if candidate.is_file():
|
|
53
|
+
return candidate
|
|
54
|
+
|
|
55
|
+
# 2. Package data directory
|
|
56
|
+
candidate = _get_data_dir() / dll_name
|
|
57
|
+
if candidate.is_file():
|
|
58
|
+
return candidate
|
|
59
|
+
|
|
60
|
+
# 3. Current working directory
|
|
61
|
+
candidate = Path.cwd() / dll_name
|
|
62
|
+
if candidate.is_file():
|
|
63
|
+
return candidate
|
|
64
|
+
|
|
65
|
+
raise DLLNotFoundError(str(candidate))
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def _find_all_dll_paths() -> dict:
|
|
69
|
+
"""Find paths for both DLLs if available.
|
|
70
|
+
|
|
71
|
+
Returns:
|
|
72
|
+
Dict mapping arch name to Path, e.g.
|
|
73
|
+
``{"x64": Path(...), "x86": Path(...)}``.
|
|
74
|
+
Missing entries are simply omitted.
|
|
75
|
+
"""
|
|
76
|
+
result: dict = {}
|
|
77
|
+
for arch, dll_name in [("x64", "speedpatch64.dll"), ("x86", "speedpatch32.dll")]:
|
|
78
|
+
try:
|
|
79
|
+
result[arch] = _resolve_dll_path(dll_name)
|
|
80
|
+
except DLLNotFoundError:
|
|
81
|
+
pass
|
|
82
|
+
return result
|
openspeedy/_eject.py
ADDED
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
"""
|
|
2
|
+
DLL ejection from a target Windows process.
|
|
3
|
+
|
|
4
|
+
Re-implements the Rust ``do_eject()`` function from ``src-bridge/src/main.rs``
|
|
5
|
+
in pure Python via ``ctypes``.
|
|
6
|
+
|
|
7
|
+
Ejection flow
|
|
8
|
+
-------------
|
|
9
|
+
|
|
10
|
+
1. Determine the DLL name from the target architecture
|
|
11
|
+
2. ``CreateToolhelp32Snapshot(TH32CS_SNAPMODULE, pid)``
|
|
12
|
+
3. ``Module32FirstW`` / ``Module32NextW`` loop to find the speedpatch DLL
|
|
13
|
+
4. Extract ``hModule`` (the module base address)
|
|
14
|
+
5. ``OpenProcess`` on the target
|
|
15
|
+
6. Get ``FreeLibrary`` address from local ``kernel32.dll``
|
|
16
|
+
7. ``CreateRemoteThread`` with ``FreeLibrary(hModule)`` as entry point
|
|
17
|
+
8. ``WaitForSingleObject``
|
|
18
|
+
9. Cleanup handles
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
import ctypes
|
|
22
|
+
|
|
23
|
+
from ._constants import (
|
|
24
|
+
PROCESS_EJECT_ACCESS,
|
|
25
|
+
PROCESS_QUERY_INFORMATION,
|
|
26
|
+
TH32CS_SNAPMODULE,
|
|
27
|
+
WAIT_OBJECT_0,
|
|
28
|
+
)
|
|
29
|
+
from ._exceptions import (
|
|
30
|
+
EjectionError,
|
|
31
|
+
ProcessAccessDeniedError,
|
|
32
|
+
)
|
|
33
|
+
from ._process import _detect_arch_for_pid
|
|
34
|
+
from ._winapi import (
|
|
35
|
+
MODULEENTRY32W,
|
|
36
|
+
kernel32,
|
|
37
|
+
wintypes,
|
|
38
|
+
get_freelibrary_address,
|
|
39
|
+
)
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def eject_dll(pid: int) -> None:
|
|
43
|
+
"""Unload the speedpatch DLL from a target process.
|
|
44
|
+
|
|
45
|
+
Args:
|
|
46
|
+
pid: Target process ID.
|
|
47
|
+
|
|
48
|
+
Raises:
|
|
49
|
+
EjectionError: The DLL was not found in the target or
|
|
50
|
+
``FreeLibrary`` failed.
|
|
51
|
+
ProcessAccessDeniedError: Cannot open the process.
|
|
52
|
+
"""
|
|
53
|
+
# 1. Determine DLL name from target architecture
|
|
54
|
+
arch = _detect_arch_for_pid(pid)
|
|
55
|
+
dll_name = "speedpatch64.dll" if arch == "x64" else "speedpatch32.dll"
|
|
56
|
+
dll_name_lower = dll_name.lower()
|
|
57
|
+
|
|
58
|
+
# 2. Enumerate modules in the target
|
|
59
|
+
h_module = _find_dll_module(pid, dll_name_lower)
|
|
60
|
+
if h_module is None:
|
|
61
|
+
raise EjectionError(pid, f"module '{dll_name}' not found in process")
|
|
62
|
+
|
|
63
|
+
# 3. Open the target process
|
|
64
|
+
h_proc = kernel32.OpenProcess(PROCESS_EJECT_ACCESS, False, pid)
|
|
65
|
+
if not h_proc:
|
|
66
|
+
err = ctypes.get_last_error()
|
|
67
|
+
raise ProcessAccessDeniedError(pid, win_error=err)
|
|
68
|
+
|
|
69
|
+
try:
|
|
70
|
+
# 4. Get FreeLibrary address
|
|
71
|
+
lp_free = get_freelibrary_address()
|
|
72
|
+
if not lp_free:
|
|
73
|
+
raise EjectionError(pid, "cannot get FreeLibrary address")
|
|
74
|
+
|
|
75
|
+
# 5. Create remote thread calling FreeLibrary(h_module)
|
|
76
|
+
thread_id = wintypes.DWORD(0)
|
|
77
|
+
h_thread = kernel32.CreateRemoteThread(
|
|
78
|
+
h_proc,
|
|
79
|
+
None,
|
|
80
|
+
0,
|
|
81
|
+
lp_free,
|
|
82
|
+
h_module,
|
|
83
|
+
0,
|
|
84
|
+
ctypes.byref(thread_id),
|
|
85
|
+
)
|
|
86
|
+
if not h_thread:
|
|
87
|
+
err = ctypes.get_last_error()
|
|
88
|
+
raise EjectionError(
|
|
89
|
+
pid, "CreateRemoteThread for FreeLibrary failed", win_error=err
|
|
90
|
+
)
|
|
91
|
+
|
|
92
|
+
try:
|
|
93
|
+
ret = kernel32.WaitForSingleObject(h_thread, 5000)
|
|
94
|
+
if ret != WAIT_OBJECT_0:
|
|
95
|
+
raise EjectionError(
|
|
96
|
+
pid, "WaitForSingleObject timed out or failed"
|
|
97
|
+
)
|
|
98
|
+
finally:
|
|
99
|
+
kernel32.CloseHandle(h_thread)
|
|
100
|
+
|
|
101
|
+
finally:
|
|
102
|
+
kernel32.CloseHandle(h_proc)
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
# ---------------------------------------------------------------------------
|
|
106
|
+
# Internal helpers
|
|
107
|
+
# ---------------------------------------------------------------------------
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
def _find_dll_module(pid: int, dll_name_lower: str) -> int | None:
|
|
111
|
+
"""Find the HMODULE (base address) of a DLL loaded in a target process.
|
|
112
|
+
|
|
113
|
+
Args:
|
|
114
|
+
pid: Target process ID.
|
|
115
|
+
dll_name_lower: Lower-cased DLL name to search for.
|
|
116
|
+
|
|
117
|
+
Returns:
|
|
118
|
+
Module handle value, or ``None`` if not found.
|
|
119
|
+
"""
|
|
120
|
+
snapshot = kernel32.CreateToolhelp32Snapshot(TH32CS_SNAPMODULE, pid)
|
|
121
|
+
if not snapshot or snapshot == ctypes.c_void_p(-1).value:
|
|
122
|
+
return None
|
|
123
|
+
|
|
124
|
+
try:
|
|
125
|
+
entry = MODULEENTRY32W()
|
|
126
|
+
entry.dwSize = ctypes.sizeof(MODULEENTRY32W)
|
|
127
|
+
|
|
128
|
+
if kernel32.Module32FirstW(snapshot, ctypes.byref(entry)):
|
|
129
|
+
while True:
|
|
130
|
+
mod_name = entry.szModule.rstrip("\x00").lower()
|
|
131
|
+
if mod_name == dll_name_lower:
|
|
132
|
+
return entry.hModule
|
|
133
|
+
|
|
134
|
+
if not kernel32.Module32NextW(snapshot, ctypes.byref(entry)):
|
|
135
|
+
break
|
|
136
|
+
finally:
|
|
137
|
+
kernel32.CloseHandle(snapshot)
|
|
138
|
+
|
|
139
|
+
return None
|