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 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
+ ]
@@ -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