levistone 0.6.1__1-cp311-cp311-win_amd64.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.

Potentially problematic release.


This version of levistone might be problematic. Click here for more details.

endstone/__init__.py ADDED
@@ -0,0 +1,16 @@
1
+ from endstone._internal.endstone_python import ColorFormat, GameMode, Logger, OfflinePlayer, Player, Server, Skin
2
+ from endstone._internal.version import __version__
3
+
4
+ __minecraft_version__ = "1.21.60"
5
+
6
+ __all__ = [
7
+ "__version__",
8
+ "__minecraft_version__",
9
+ "ColorFormat",
10
+ "GameMode",
11
+ "Logger",
12
+ "OfflinePlayer",
13
+ "Player",
14
+ "Server",
15
+ "Skin",
16
+ ]
endstone/__main__.py ADDED
@@ -0,0 +1,4 @@
1
+ from endstone._internal.bootstrap import cli
2
+
3
+ if __name__ == "__main__":
4
+ cli()
File without changes
@@ -0,0 +1,74 @@
1
+ import functools
2
+ import logging
3
+ import platform
4
+ import sys
5
+
6
+ import click
7
+
8
+ from endstone._internal.version import __version__
9
+
10
+ logging.basicConfig(
11
+ level=logging.INFO,
12
+ format="[%(asctime)s.%(msecs)03d %(levelname)s] [%(name)s] %(message)s",
13
+ datefmt="%Y-%m-%d %H:%M:%S",
14
+ )
15
+ logger = logging.getLogger(__name__)
16
+
17
+ __all__ = ["cli"]
18
+
19
+
20
+ def catch_exceptions(func):
21
+ """Decorator to catch and log exceptions."""
22
+
23
+ @functools.wraps(func)
24
+ def wrapper(*args, **kwargs):
25
+ try:
26
+ return func(*args, **kwargs)
27
+ except Exception as e:
28
+ logger.exception(e)
29
+ sys.exit(-1)
30
+
31
+ return wrapper
32
+
33
+
34
+ @click.command(help="Starts an endstone server.")
35
+ @click.option(
36
+ "-s",
37
+ "--server-folder",
38
+ default="bedrock_server",
39
+ help="Specify the folder for the bedrock server. Defaults to 'bedrock_server'.",
40
+ )
41
+ @click.option(
42
+ "-y",
43
+ "--no-confirm",
44
+ "--yes",
45
+ default=False,
46
+ is_flag=True,
47
+ show_default=True,
48
+ help="Assume yes as answer to all prompts",
49
+ )
50
+ @click.option(
51
+ "-r",
52
+ "--remote",
53
+ default="https://raw.githubusercontent.com/EndstoneMC/bedrock-server-data/main/bedrock_server_data.json",
54
+ help="The remote URL to retrieve bedrock server data from.",
55
+ )
56
+ @click.version_option(__version__)
57
+ @catch_exceptions
58
+ def cli(server_folder: str, no_confirm: bool, remote: str) -> None:
59
+ system = platform.system()
60
+ if system == "Windows":
61
+ from endstone._internal.bootstrap.windows import WindowsBootstrap
62
+
63
+ cls = WindowsBootstrap
64
+
65
+ elif system == "Linux":
66
+ from endstone._internal.bootstrap.linux import LinuxBootstrap
67
+
68
+ cls = LinuxBootstrap
69
+ else:
70
+ raise NotImplementedError(f"{system} is not supported.")
71
+
72
+ bootstrap = cls(server_folder=server_folder, no_confirm=no_confirm, remote=remote)
73
+ exit_code = bootstrap.run()
74
+ sys.exit(exit_code)
@@ -0,0 +1,258 @@
1
+ import errno
2
+ import hashlib
3
+ import logging
4
+ import os
5
+ import platform
6
+ import subprocess
7
+ import sys
8
+ import tempfile
9
+ import zipfile
10
+ from pathlib import Path
11
+ from typing import Union
12
+
13
+ import click
14
+ import requests
15
+ from packaging.version import Version
16
+ from rich.progress import BarColumn, DownloadColumn, Progress, TextColumn, TimeRemainingColumn
17
+
18
+ from endstone import __minecraft_version__ as minecraft_version
19
+ from endstone import __version__ as endstone_version
20
+
21
+
22
+ class Bootstrap:
23
+ def __init__(self, server_folder: str, no_confirm: bool, remote: str) -> None:
24
+ self._server_path = Path(server_folder).absolute()
25
+ self._no_confirm = no_confirm
26
+ self._remote = remote
27
+ self._logger = logging.getLogger(self.name)
28
+ self._process: subprocess.Popen
29
+
30
+ @property
31
+ def name(self) -> str:
32
+ return __name__
33
+
34
+ @property
35
+ def target_system(self) -> str:
36
+ raise NotImplementedError
37
+
38
+ @property
39
+ def executable_filename(self) -> str:
40
+ raise NotImplementedError
41
+
42
+ @property
43
+ def server_path(self) -> Path:
44
+ return self._server_path
45
+
46
+ @property
47
+ def executable_path(self) -> Path:
48
+ return self.server_path / self.executable_filename
49
+
50
+ @property
51
+ def plugin_path(self) -> Path:
52
+ return self.server_path / "plugins"
53
+
54
+ @property
55
+ def user_agent(self) -> str:
56
+ return f"Endstone/{endstone_version} (Minecraft/{minecraft_version})"
57
+
58
+ def _validate(self) -> None:
59
+ if platform.system().lower() != self.target_system:
60
+ raise NotImplementedError(f"{platform.system()} is not supported by this bootstrap.")
61
+ if not self.executable_path.exists():
62
+ raise FileNotFoundError(errno.ENOENT, os.strerror(errno.ENOENT), str(self.executable_path))
63
+ if not self._endstone_runtime_path.exists():
64
+ raise FileNotFoundError(errno.ENOENT, os.strerror(errno.ENOENT), str(self._endstone_runtime_path))
65
+
66
+ def _download(self, dst: Union[str, os.PathLike]) -> None:
67
+ dst = Path(dst)
68
+
69
+ self._logger.info("Loading index from the remote server...")
70
+ response = requests.get(self._remote)
71
+ response.raise_for_status()
72
+ server_data = response.json()
73
+
74
+ if minecraft_version not in server_data["binary"]:
75
+ raise ValueError(f"Version v{minecraft_version} is not found in the remote server.")
76
+
77
+ should_modify_server_properties = True
78
+
79
+ with tempfile.TemporaryFile(dir=dst) as f:
80
+ metadata = server_data["binary"][minecraft_version][self.target_system.lower()]
81
+ url = metadata["url"]
82
+ response = requests.get(url, stream=True, headers={"User-Agent": self.user_agent})
83
+ response.raise_for_status()
84
+ total_size = int(response.headers.get("Content-Length", 0))
85
+ self._logger.info(f"Downloading server from {url}...")
86
+ m = hashlib.sha256()
87
+
88
+ with Progress(
89
+ TextColumn("[progress.description]{task.description}"),
90
+ BarColumn(),
91
+ DownloadColumn(),
92
+ TimeRemainingColumn(),
93
+ ) as progress:
94
+ task = progress.add_task("[bold blue]Downloading...", total=total_size)
95
+ for data in response.iter_content(chunk_size=1024):
96
+ progress.update(task, advance=len(data))
97
+ f.write(data)
98
+ m.update(data)
99
+
100
+ self._logger.info("Download complete. Verifying integrity...")
101
+ if m.hexdigest() != metadata["sha256"]:
102
+ raise ValueError("SHA256 mismatch: the downloaded file may be corrupted or tampered with.")
103
+
104
+ self._logger.info(f"Integrity check passed. Extracting to {dst}...")
105
+ dst.mkdir(parents=True, exist_ok=True)
106
+ with zipfile.ZipFile(f) as zip_ref:
107
+ for file in zip_ref.namelist():
108
+ if file in ["allowlist.json", "permissions.json", "server.properties"] and (dst / file).exists():
109
+ self._logger.info(f"{file} already exists, skipping.")
110
+ should_modify_server_properties = False
111
+ continue
112
+
113
+ zip_ref.extract(file, dst)
114
+
115
+ if should_modify_server_properties:
116
+ properties = dst / "server.properties"
117
+ with properties.open("r", encoding="utf-8") as file:
118
+ in_lines = file.readlines()
119
+
120
+ out_lines = []
121
+ for line in in_lines:
122
+ if line.strip() == "server-name=Dedicated Server":
123
+ out_lines.append("server-name=Endstone Server\n")
124
+ elif line.strip() == "client-side-chunk-generation-enabled=true":
125
+ out_lines.append("client-side-chunk-generation-enabled=false\n")
126
+ else:
127
+ out_lines.append(line)
128
+
129
+ with properties.open("w", encoding="utf-8") as file:
130
+ file.writelines(out_lines)
131
+
132
+ version_file = dst / "version.txt"
133
+ with version_file.open("w", encoding="utf-8") as file:
134
+ file.writelines(minecraft_version)
135
+
136
+ def _prepare(self) -> None:
137
+ self.plugin_path.mkdir(parents=True, exist_ok=True)
138
+
139
+ def _install(self) -> None:
140
+ """
141
+ Installs the server if not already installed.
142
+ """
143
+
144
+ if self.executable_path.exists():
145
+ self._update()
146
+ return
147
+
148
+ if not self._no_confirm:
149
+ download = click.confirm(
150
+ f"Bedrock Dedicated Server (v{minecraft_version}) "
151
+ f"is not found in {str(self.executable_path.parent)}. "
152
+ f"Would you like to download it now?",
153
+ default=True,
154
+ )
155
+ else:
156
+ download = True
157
+
158
+ if not download:
159
+ sys.exit(1)
160
+
161
+ self.server_path.mkdir(parents=True, exist_ok=True)
162
+ self._download(self.server_path)
163
+
164
+ def _update(self) -> None:
165
+ current_version = Version("0.0.0")
166
+ supported_version = Version(minecraft_version)
167
+
168
+ version_file = self.server_path / "version.txt"
169
+ if version_file.exists():
170
+ with version_file.open("r", encoding="utf-8") as file:
171
+ current_version = Version(file.readline())
172
+
173
+ if current_version == supported_version:
174
+ return
175
+
176
+ if current_version > supported_version:
177
+ raise RuntimeError(
178
+ f"A newer version of Bedrock Dedicated Server (v{current_version}) "
179
+ f"is found in {str(self.executable_path.parent)}. Please update your Endstone server."
180
+ )
181
+
182
+ if not self._no_confirm:
183
+ update = click.confirm(
184
+ f"An older version of Bedrock Dedicated Server (v{current_version}) "
185
+ f"is found in {str(self.executable_path.parent)}. "
186
+ f"Would you like to update to v{minecraft_version} now?",
187
+ default=True,
188
+ )
189
+ else:
190
+ update = True
191
+
192
+ if not update:
193
+ sys.exit(1)
194
+
195
+ self._logger.info(f"Updating server from v{current_version} to v{minecraft_version}...")
196
+ self._download(self.server_path)
197
+
198
+ def run(self) -> int:
199
+ self._install()
200
+ self._validate()
201
+ self._prepare()
202
+ self._create_process()
203
+ return self._wait_for_server()
204
+
205
+ @property
206
+ def _endstone_runtime_filename(self) -> str:
207
+ raise NotImplementedError
208
+
209
+ @property
210
+ def _endstone_runtime_path(self) -> Path:
211
+ p = Path(__file__).parent.parent / self._endstone_runtime_filename
212
+ return p.resolve().absolute()
213
+
214
+ def _create_process(self, *args, **kwargs) -> None:
215
+ """
216
+ Creates a subprocess for running the server.
217
+
218
+ This method initializes a subprocess.Popen object for the server executable. It sets up the necessary
219
+ buffers and encodings for the process and specifies the working directory.
220
+
221
+ Args:
222
+ *args: Variable length argument list.
223
+ **kwargs: Arbitrary keyword arguments.
224
+
225
+ """
226
+ env = kwargs.pop("env", os.environ.copy())
227
+ env["PATH"] = os.pathsep.join(sys.path)
228
+ env["PYTHONPATH"] = os.pathsep.join(sys.path)
229
+ env["PYTHONIOENCODING"] = "UTF-8"
230
+ self._process = subprocess.Popen(
231
+ [str(self.executable_path.absolute())],
232
+ stdin=sys.stdin,
233
+ stdout=sys.stdout,
234
+ stderr=subprocess.STDOUT,
235
+ text=True,
236
+ encoding="utf-8",
237
+ cwd=str(self.server_path.absolute()),
238
+ env=env,
239
+ *args,
240
+ **kwargs,
241
+ )
242
+
243
+ def _wait_for_server(self) -> int:
244
+ """
245
+ Waits for the server process to terminate and returns its exit code.
246
+
247
+ This method blocks until the server process created by _create_process terminates. It returns the
248
+ exit code of the process, which can be used to determine if the server shut down successfully or if
249
+ there were errors.
250
+
251
+ Returns:
252
+ int: The exit code of the server process. Returns -1 if the process is not created or still running.
253
+ """
254
+
255
+ if self._process is None:
256
+ return -1
257
+
258
+ return self._process.wait()
@@ -0,0 +1,67 @@
1
+ import ctypes.util
2
+ import os
3
+ import stat
4
+ from pathlib import Path
5
+
6
+ from endstone._internal.bootstrap.base import Bootstrap
7
+
8
+
9
+ class LinuxBootstrap(Bootstrap):
10
+ @property
11
+ def name(self) -> str:
12
+ return "LinuxBootstrap"
13
+
14
+ @property
15
+ def target_system(self) -> str:
16
+ return "linux"
17
+
18
+ @property
19
+ def executable_filename(self) -> str:
20
+ return "bedrock_server"
21
+
22
+ @property
23
+ def _endstone_runtime_filename(self) -> str:
24
+ return "libendstone_runtime.so"
25
+
26
+ def _prepare(self) -> None:
27
+ super()._prepare()
28
+ st = os.stat(self.executable_path)
29
+ os.chmod(self.executable_path, st.st_mode | stat.S_IEXEC)
30
+
31
+ def _create_process(self, *args, **kwargs) -> None:
32
+ env = os.environ.copy()
33
+ env["LD_PRELOAD"] = str(self._endstone_runtime_path.absolute())
34
+ env["LD_LIBRARY_PATH"] = str(self._linked_libpython_path.parent.absolute())
35
+ super()._create_process(env=env)
36
+
37
+ @property
38
+ def _linked_libpython_path(self) -> Path:
39
+ """
40
+ Find the path of the linked libpython on Unix systems.
41
+
42
+ From https://gist.github.com/tkf/d980eee120611604c0b9b5fef5b8dae6
43
+
44
+ Returns:
45
+ (Path): Path object representing the path of the linked libpython.
46
+ """
47
+
48
+ class DlInfo(ctypes.Structure):
49
+ # https://www.man7.org/linux/man-pages/man3/dladdr.3.html
50
+ _fields_ = [
51
+ ("dli_fname", ctypes.c_char_p),
52
+ ("dli_fbase", ctypes.c_void_p),
53
+ ("dli_sname", ctypes.c_char_p),
54
+ ("dli_saddr", ctypes.c_void_p),
55
+ ]
56
+
57
+ libdl = ctypes.CDLL(ctypes.util.find_library("dl"))
58
+ libdl.dladdr.argtypes = [ctypes.c_void_p, ctypes.POINTER(DlInfo)]
59
+ libdl.dladdr.restype = ctypes.c_int
60
+
61
+ dlinfo = DlInfo()
62
+ retcode = libdl.dladdr(ctypes.cast(ctypes.pythonapi.Py_GetVersion, ctypes.c_void_p), ctypes.pointer(dlinfo))
63
+ if retcode == 0:
64
+ raise ValueError("dladdr cannot match the address of ctypes.pythonapi.Py_GetVersion to a shared object")
65
+
66
+ path = Path(dlinfo.dli_fname.decode()).resolve()
67
+ return path
@@ -0,0 +1,207 @@
1
+ import ctypes
2
+ import os
3
+ import subprocess
4
+ from ctypes import get_last_error
5
+ from ctypes.wintypes import (
6
+ BOOL,
7
+ DWORD,
8
+ HANDLE,
9
+ HMODULE,
10
+ LPCSTR,
11
+ LPCVOID,
12
+ LPCWSTR,
13
+ LPDWORD,
14
+ LPVOID,
15
+ )
16
+
17
+ from endstone._internal.bootstrap.base import Bootstrap
18
+
19
+ SIZE_T = ctypes.c_size_t
20
+ PSIZE_T = ctypes.POINTER(SIZE_T)
21
+
22
+
23
+ class THREADENTRY32(ctypes.Structure):
24
+ _fields_ = [
25
+ ("dwSize", DWORD),
26
+ ("cntUsage", DWORD),
27
+ ("th32ThreadID", DWORD),
28
+ ("th32OwnerProcessID", DWORD),
29
+ ("tpBasePri", DWORD),
30
+ ("tpDeltaPri", DWORD),
31
+ ("dwFlags", DWORD),
32
+ ]
33
+
34
+
35
+ # Kernel32 Functions
36
+ kernel32 = ctypes.WinDLL("kernel32", use_last_error=True)
37
+
38
+ # Constants
39
+ CREATE_SUSPENDED = 0x00000004 # https://learn.microsoft.com/en-us/windows/win32/procthread/process-creation-flags
40
+ MAX_PATH = 260 # https://learn.microsoft.com/en-us/windows/win32/fileio/maximum-file-path-limitation
41
+ MEM_COMMIT = 0x00001000 # https://learn.microsoft.com/en-us/windows/win32/api/memoryapi/nf-memoryapi-virtualalloc
42
+ MEM_RESERVE = 0x00002000 # https://learn.microsoft.com/en-us/windows/win32/api/memoryapi/nf-memoryapi-virtualalloc
43
+ PAGE_READWRITE = 0x04 # https://learn.microsoft.com/en-us/windows/win32/memory/memory-protection-constants
44
+ INFINITE = 0xFFFFFFFF
45
+ TH32CS_SNAPTHREAD = 0x00000004
46
+ THREAD_ALL_ACCESS = 0x000F0000 | 0x00100000 | 0xFFFF
47
+ INVALID_HANDLE_VALUE = ctypes.c_void_p(-1).value
48
+ DONT_RESOLVE_DLL_REFERENCES = 0x00000001
49
+
50
+ # https://learn.microsoft.com/en-us/windows/win32/api/memoryapi/nf-memoryapi-virtualallocex
51
+ VirtualAllocEx = kernel32.VirtualAllocEx
52
+ VirtualAllocEx.restype = LPVOID
53
+ VirtualAllocEx.argtypes = (HANDLE, LPVOID, SIZE_T, DWORD, DWORD)
54
+
55
+ # https://learn.microsoft.com/en-us/windows/win32/api/memoryapi/nf-memoryapi-writeprocessmemory
56
+ WriteProcessMemory = kernel32.WriteProcessMemory
57
+ WriteProcessMemory.restype = BOOL
58
+ WriteProcessMemory.argtypes = (HANDLE, LPVOID, LPCVOID, SIZE_T, PSIZE_T)
59
+
60
+ # https://learn.microsoft.com/en-us/windows/win32/api/processthreadsapi/nf-processthreadsapi-createremotethread
61
+ CreateRemoteThread = kernel32.CreateRemoteThread
62
+ CreateRemoteThread.restype = HANDLE
63
+ CreateRemoteThread.argtypes = (HANDLE, LPVOID, SIZE_T, LPVOID, LPVOID, DWORD, LPDWORD)
64
+
65
+ # https://learn.microsoft.com/en-us/windows/win32/api/processthreadsapi/nf-processthreadsapi-getexitcodethread
66
+ GetExitCodeThread = kernel32.GetExitCodeThread
67
+ GetExitCodeThread.restype = BOOL
68
+ GetExitCodeThread.argtypes = (HANDLE, LPDWORD)
69
+
70
+ # https://learn.microsoft.com/en-us/windows/win32/api/libloaderapi/nf-libloaderapi-getmodulehandlew
71
+ GetModuleHandle = kernel32.GetModuleHandleW
72
+ GetModuleHandle.restype = HMODULE
73
+ GetModuleHandle.argtypes = (LPCWSTR,)
74
+
75
+ # https://learn.microsoft.com/en-us/windows/win32/api/libloaderapi/nf-libloaderapi-getprocaddress
76
+ GetProcAddress = kernel32.GetProcAddress
77
+ GetProcAddress.restype = LPVOID
78
+ GetProcAddress.argtypes = (HMODULE, LPCSTR)
79
+
80
+ # https://learn.microsoft.com/en-us/windows/win32/api/libloaderapi/nf-libloaderapi-loadlibraryexw
81
+ LoadLibraryExW = kernel32.LoadLibraryExW
82
+ LoadLibraryExW.restype = HMODULE
83
+ LoadLibraryExW.argtypes = (LPCWSTR, HANDLE, DWORD)
84
+
85
+
86
+ class WindowsBootstrap(Bootstrap):
87
+ @property
88
+ def name(self) -> str:
89
+ return "WindowsBootstrap"
90
+
91
+ @property
92
+ def target_system(self) -> str:
93
+ return "windows"
94
+
95
+ @property
96
+ def executable_filename(self) -> str:
97
+ return "bedrock_server.exe"
98
+
99
+ @property
100
+ def _endstone_runtime_filename(self) -> str:
101
+ return "endstone_runtime_loader.dll"
102
+
103
+ def _add_loopback_exemption(self) -> bool:
104
+ sid = "S-1-15-2-1958404141-86561845-1752920682-3514627264-368642714-62675701-733520436"
105
+ ret = subprocess.run(
106
+ ["CheckNetIsolation", "LoopbackExempt", "-s", f"-p={sid}"], check=True, capture_output=True
107
+ )
108
+ if sid not in str(ret.stdout):
109
+ ret = ctypes.windll.shell32.ShellExecuteW(
110
+ None, "runas", "CheckNetIsolation", " ".join(["LoopbackExempt", "-a", f"-p={sid}"]), None, 1
111
+ )
112
+ return ret > 32
113
+ else:
114
+ return True
115
+
116
+ def _create_process(self, *args, **kwargs) -> None:
117
+ self._add_loopback_exemption()
118
+
119
+ # Add paths for symbol lookup
120
+ env = os.environ.copy()
121
+ symbol_path = env.get("_NT_SYMBOL_PATH", "")
122
+ symbol_path_list = symbol_path.split(os.pathsep)
123
+ symbol_path_list = [
124
+ str(self._endstone_runtime_path.parent.absolute()),
125
+ str(self.plugin_path.absolute()),
126
+ ] + symbol_path_list
127
+ env["_NT_SYMBOL_PATH"] = os.pathsep.join(symbol_path_list)
128
+
129
+ # Create the process is a suspended state
130
+ super()._create_process(creationflags=CREATE_SUSPENDED, env=env)
131
+ handle_proc = int(self._process._handle)
132
+ lib_path = str(self._endstone_runtime_path.absolute())
133
+
134
+ # Validate dll
135
+ dll = kernel32.LoadLibraryExW(lib_path, None, DONT_RESOLVE_DLL_REFERENCES)
136
+ if not dll:
137
+ raise ValueError(f"LoadLibraryExW failed with error {get_last_error()}.")
138
+
139
+ # Allocate memory for lib_path
140
+ address = kernel32.VirtualAllocEx(
141
+ handle_proc, # hProcess
142
+ 0, # lpAddress
143
+ MAX_PATH * 2 + 1, # dwSize
144
+ DWORD(MEM_COMMIT | MEM_RESERVE), # flAllocationType
145
+ DWORD(PAGE_READWRITE), # flProtect
146
+ )
147
+ if not address:
148
+ raise ValueError(f"VirtualAllocEx failed with error {get_last_error()}.")
149
+
150
+ # Write lib_path into the allocated memory
151
+ size_written = SIZE_T(0)
152
+ result = kernel32.WriteProcessMemory(
153
+ handle_proc, address, lib_path, len(lib_path) * 2 + 1, ctypes.byref(size_written)
154
+ )
155
+ if not result:
156
+ raise ValueError(f"WriteProcessMemory failed with error {get_last_error()}.")
157
+
158
+ # Get module handle of kernel32
159
+ handle_kernel32 = kernel32.GetModuleHandleW("kernel32.dll")
160
+ if not handle_kernel32:
161
+ raise ValueError(f"GetModuleHandleW failed with error {get_last_error()}.")
162
+
163
+ # Get address of LoadLibraryW
164
+ load_library = kernel32.GetProcAddress(handle_kernel32, b"LoadLibraryW")
165
+ if not load_library:
166
+ raise ValueError(f"GetProcAddress failed with error {get_last_error()}.")
167
+
168
+ # Start a new thread in the process, starting at LoadLibraryW with address as argument
169
+ remote_thread = kernel32.CreateRemoteThread(handle_proc, None, 0, load_library, address, 0, None)
170
+ if not remote_thread:
171
+ raise ValueError(f"CreateRemoteThread failed with error {get_last_error()}.")
172
+
173
+ # Wait for the remote thread to finish
174
+ if kernel32.WaitForSingleObject(remote_thread, INFINITE) == 0xFFFFFFFF:
175
+ raise ValueError(f"WaitForSingleObject failed with error {get_last_error()}.")
176
+
177
+ # Check dll load result
178
+ exit_code = DWORD()
179
+ result = kernel32.GetExitCodeThread(remote_thread, ctypes.byref(exit_code))
180
+ if result == 0:
181
+ raise ValueError(f"LoadLibrary failed with thread exit code: {exit_code.value}")
182
+
183
+ # Reopen the handle to process thread (which was closed by subprocess.Popen)
184
+ snapshot = kernel32.CreateToolhelp32Snapshot(TH32CS_SNAPTHREAD, 0)
185
+ if snapshot == INVALID_HANDLE_VALUE:
186
+ raise ValueError(f"CreateToolhelp32Snapshot failed with error {get_last_error()}.")
187
+
188
+ thread_entry = THREADENTRY32()
189
+ thread_entry.dwSize = ctypes.sizeof(thread_entry)
190
+ success = kernel32.Thread32First(snapshot, ctypes.byref(thread_entry))
191
+ if not success:
192
+ raise ValueError(f"Thread32First failed with error {get_last_error()}.")
193
+
194
+ handle_thread = None
195
+ while success:
196
+ if thread_entry.th32OwnerProcessID == self._process.pid:
197
+ handle_thread = kernel32.OpenThread(THREAD_ALL_ACCESS, None, thread_entry.th32ThreadID)
198
+ break
199
+
200
+ success = kernel32.Thread32Next(snapshot, ctypes.byref(thread_entry))
201
+
202
+ if handle_thread is None:
203
+ raise ValueError(f"OpenThread failed with error {get_last_error()}.")
204
+
205
+ # Resume main thread execution
206
+ kernel32.ResumeThread(handle_thread)
207
+ kernel32.CloseHandle(handle_thread)
Binary file