levistone 0.6.2.dev58__1-cp39-cp39-win_amd64.whl → 0.10.5__1-cp39-cp39-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 CHANGED
@@ -1,14 +1,28 @@
1
- from endstone._internal.endstone_python import ColorFormat, GameMode, Logger, OfflinePlayer, Player, Server, Skin
1
+ from endstone._internal.endstone_python import (
2
+ ColorFormat,
3
+ EnchantmentRegistry,
4
+ GameMode,
5
+ ItemRegistry,
6
+ Logger,
7
+ NamespacedKey,
8
+ OfflinePlayer,
9
+ Player,
10
+ Server,
11
+ Skin,
12
+ )
2
13
  from endstone._internal.version import __version__
3
14
 
4
- __minecraft_version__ = "1.21.60"
15
+ __minecraft_version__ = "1.21.102"
5
16
 
6
17
  __all__ = [
7
18
  "__version__",
8
19
  "__minecraft_version__",
9
20
  "ColorFormat",
21
+ "EnchantmentRegistry",
10
22
  "GameMode",
23
+ "ItemRegistry",
11
24
  "Logger",
25
+ "NamespacedKey",
12
26
  "OfflinePlayer",
13
27
  "Player",
14
28
  "Server",
@@ -2,6 +2,7 @@ import functools
2
2
  import logging
3
3
  import platform
4
4
  import sys
5
+ import time
5
6
 
6
7
  import click
7
8
 
@@ -50,7 +51,7 @@ def catch_exceptions(func):
50
51
  @click.option(
51
52
  "-r",
52
53
  "--remote",
53
- default="https://raw.githubusercontent.com/EndstoneMC/bedrock-server-data/main/bedrock_server_data.json",
54
+ default="https://raw.githubusercontent.com/EndstoneMC/bedrock-server-data/v2",
54
55
  help="The remote URL to retrieve bedrock server data from.",
55
56
  )
56
57
  @click.version_option(__version__)
@@ -71,4 +72,8 @@ def cli(server_folder: str, no_confirm: bool, remote: str) -> None:
71
72
 
72
73
  bootstrap = cls(server_folder=server_folder, no_confirm=no_confirm, remote=remote)
73
74
  exit_code = bootstrap.run()
75
+ if exit_code != 0:
76
+ logger.error(f"Server exited with non-zero code {exit_code}.")
77
+ time.sleep(2)
78
+
74
79
  sys.exit(exit_code)
@@ -1,4 +1,5 @@
1
1
  import errno
2
+ import fnmatch
2
3
  import hashlib
3
4
  import logging
4
5
  import os
@@ -12,13 +13,14 @@ from pathlib import Path
12
13
  from typing import Union
13
14
 
14
15
  import click
16
+ import importlib_resources
15
17
  import requests
16
18
  import sentry_crashpad
19
+ import tomlkit
17
20
  from packaging.version import Version
18
21
  from rich.progress import BarColumn, DownloadColumn, Progress, TextColumn, TimeRemainingColumn
19
22
 
20
23
  from endstone import __minecraft_version__ as minecraft_version
21
- from endstone import __version__ as endstone_version
22
24
 
23
25
 
24
26
  class Bootstrap:
@@ -49,13 +51,17 @@ class Bootstrap:
49
51
  def executable_path(self) -> Path:
50
52
  return self.server_path / self.executable_filename
51
53
 
54
+ @property
55
+ def config_path(self) -> Path:
56
+ return self.server_path / "endstone.toml"
57
+
52
58
  @property
53
59
  def plugin_path(self) -> Path:
54
60
  return self.server_path / "plugins"
55
61
 
56
62
  @property
57
63
  def user_agent(self) -> str:
58
- return f"Endstone/{endstone_version} (Minecraft/{minecraft_version})"
64
+ return "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/80.0.3987.149 Safari/537.36"
59
65
 
60
66
  def _validate(self) -> None:
61
67
  if platform.system().lower() != self.target_system:
@@ -69,22 +75,23 @@ class Bootstrap:
69
75
  dst = Path(dst)
70
76
 
71
77
  self._logger.info("Loading index from the remote server...")
72
- response = requests.get(self._remote)
78
+ channel = "preview" if Version(minecraft_version).is_prerelease else "release"
79
+ metadata_url = "/".join([self._remote, channel, minecraft_version, "metadata.json"])
80
+ response = requests.get(metadata_url, timeout=10)
73
81
  response.raise_for_status()
74
- server_data = response.json()
82
+ metadata = response.json()
75
83
 
76
- if minecraft_version not in server_data["binary"]:
77
- raise ValueError(f"Version v{minecraft_version} is not found in the remote server.")
84
+ if minecraft_version != metadata["version"]:
85
+ raise ValueError(f"Version mismatch, expect: {minecraft_version}, actual: {metadata['version']}")
78
86
 
79
87
  should_modify_server_properties = True
80
88
 
81
89
  with tempfile.TemporaryFile(dir=dst) as f:
82
- metadata = server_data["binary"][minecraft_version][self.target_system.lower()]
83
- url = metadata["url"]
90
+ url = metadata["binary"][self.target_system.lower()]["url"]
91
+ self._logger.info(f"Downloading server from {url}...")
84
92
  response = requests.get(url, stream=True, headers={"User-Agent": self.user_agent})
85
93
  response.raise_for_status()
86
94
  total_size = int(response.headers.get("Content-Length", 0))
87
- self._logger.info(f"Downloading server from {url}...")
88
95
  m = hashlib.sha256()
89
96
 
90
97
  with Progress(
@@ -100,17 +107,28 @@ class Bootstrap:
100
107
  m.update(data)
101
108
 
102
109
  self._logger.info("Download complete. Verifying integrity...")
103
- if m.hexdigest() != metadata["sha256"]:
110
+ if m.hexdigest() != metadata["binary"][self.target_system.lower()]["sha256"]:
104
111
  raise ValueError("SHA256 mismatch: the downloaded file may be corrupted or tampered with.")
105
112
 
106
113
  self._logger.info(f"Integrity check passed. Extracting to {dst}...")
107
114
  dst.mkdir(parents=True, exist_ok=True)
115
+ override_patterns = [
116
+ self.executable_filename,
117
+ "behavior_packs/*",
118
+ "definitions/*",
119
+ "resource_packs/*",
120
+ "bedrock_server_how_to.html",
121
+ "profanity_filter.wlist",
122
+ "release-notes.txt",
123
+ ]
108
124
  with zipfile.ZipFile(f) as zip_ref:
109
125
  for file in zip_ref.namelist():
110
- if file in ["allowlist.json", "permissions.json", "server.properties"] and (dst / file).exists():
111
- self._logger.info(f"{file} already exists, skipping.")
112
- should_modify_server_properties = False
113
- continue
126
+ dest_path = dst / file
127
+ if dest_path.exists():
128
+ if not any(fnmatch.fnmatch(file, pattern) for pattern in override_patterns):
129
+ should_modify_server_properties = False
130
+ self._logger.info(f"{dest_path} already exists, skipping.")
131
+ continue
114
132
 
115
133
  zip_ref.extract(file, dst)
116
134
 
@@ -140,6 +158,32 @@ class Bootstrap:
140
158
  shutil.copytree(
141
159
  Path(sentry_crashpad._get_executable("crashpad_handler")).parent, self.server_path, dirs_exist_ok=True
142
160
  )
161
+ if not self.config_path.exists():
162
+ ref = importlib_resources.files("endstone") / "config" / "endstone.toml"
163
+ with importlib_resources.as_file(ref) as path:
164
+ shutil.copy(path, self.config_path)
165
+ else:
166
+ ref = importlib_resources.files("endstone") / "config" / "endstone.toml"
167
+ with importlib_resources.as_file(ref) as path:
168
+ with open(path, "r", encoding="utf-8") as f:
169
+ default_config = tomlkit.load(f)
170
+
171
+ with open(self.config_path, "r", encoding="utf-8") as f:
172
+ config = tomlkit.load(f)
173
+
174
+ def migrate_config(from_doc: tomlkit.TOMLDocument, to_doc: tomlkit.TOMLDocument) -> None:
175
+ for key, val in from_doc.items():
176
+ if key not in to_doc:
177
+ # if the user hasn’t set it, copy it (with comments!)
178
+ to_doc[key] = val
179
+ else:
180
+ # if both are tables, dive deeper
181
+ if isinstance(val, tomlkit.TOMLDocument) and isinstance(to_doc[key], tomlkit.TOMLDocument):
182
+ migrate_config(val, to_doc[key])
183
+
184
+ migrate_config(default_config, config)
185
+ with open(self.config_path, "w", encoding="utf-8") as f:
186
+ tomlkit.dump(config, f)
143
187
 
144
188
  def _install(self) -> None:
145
189
  """
@@ -204,8 +248,7 @@ class Bootstrap:
204
248
  self._install()
205
249
  self._validate()
206
250
  self._prepare()
207
- self._create_process()
208
- return self._wait_for_server()
251
+ return self._run()
209
252
 
210
253
  @property
211
254
  def _endstone_runtime_filename(self) -> str:
@@ -216,48 +259,27 @@ class Bootstrap:
216
259
  p = Path(__file__).parent.parent / self._endstone_runtime_filename
217
260
  return p.resolve().absolute()
218
261
 
219
- def _create_process(self, *args, **kwargs) -> None:
220
- """
221
- Creates a subprocess for running the server.
222
-
223
- This method initializes a subprocess.Popen object for the server executable. It sets up the necessary
224
- buffers and encodings for the process and specifies the working directory.
225
-
226
- Args:
227
- *args: Variable length argument list.
228
- **kwargs: Arbitrary keyword arguments.
229
-
230
- """
231
- env = kwargs.pop("env", os.environ.copy())
262
+ @property
263
+ def _endstone_runtime_env(self) -> dict[str, str]:
264
+ env = os.environ.copy()
232
265
  env["PATH"] = os.pathsep.join(sys.path)
233
266
  env["PYTHONPATH"] = os.pathsep.join(sys.path)
234
267
  env["PYTHONIOENCODING"] = "UTF-8"
235
- self._process = subprocess.Popen(
236
- [str(self.executable_path.absolute())],
237
- stdin=sys.stdin,
238
- stdout=sys.stdout,
239
- stderr=subprocess.STDOUT,
240
- text=True,
241
- encoding="utf-8",
242
- cwd=str(self.server_path.absolute()),
243
- env=env,
244
- *args,
245
- **kwargs,
246
- )
268
+ env["ENDSTONE_PYTHON_EXECUTABLE"] = sys.executable
269
+ return env
247
270
 
248
- def _wait_for_server(self) -> int:
271
+ def _run(self, *args, **kwargs) -> int:
249
272
  """
250
- Waits for the server process to terminate and returns its exit code.
273
+ Runs the server and returns its exit code.
251
274
 
252
- This method blocks until the server process created by _create_process terminates. It returns the
253
- exit code of the process, which can be used to determine if the server shut down successfully or if
254
- there were errors.
275
+ This method blocks until the server process terminates. It returns the exit code of the process, which can be
276
+ used to determine if the server shut down successfully or if there were errors.
277
+
278
+ Args:
279
+ *args: Variable length argument list.
280
+ **kwargs: Arbitrary keyword arguments.
255
281
 
256
282
  Returns:
257
- int: The exit code of the server process. Returns -1 if the process is not created or still running.
283
+ int: The exit code of the server process.
258
284
  """
259
-
260
- if self._process is None:
261
- return -1
262
-
263
- return self._process.wait()
285
+ raise NotImplementedError
@@ -1,6 +1,7 @@
1
1
  import ctypes.util
2
2
  import os
3
3
  import stat
4
+ import subprocess
4
5
  from pathlib import Path
5
6
 
6
7
  from endstone._internal.bootstrap.base import Bootstrap
@@ -23,17 +24,30 @@ class LinuxBootstrap(Bootstrap):
23
24
  def _endstone_runtime_filename(self) -> str:
24
25
  return "libendstone_runtime.so"
25
26
 
27
+ @property
28
+ def _endstone_runtime_env(self) -> dict[str, str]:
29
+ env = super()._endstone_runtime_env
30
+ env["LD_PRELOAD"] = str(self._endstone_runtime_path.absolute())
31
+ env["LD_LIBRARY_PATH"] = str(self._linked_libpython_path.parent.absolute())
32
+ return env
33
+
26
34
  def _prepare(self) -> None:
27
35
  super()._prepare()
28
36
  st = os.stat(self.executable_path)
29
37
  os.chmod(self.executable_path, st.st_mode | stat.S_IEXEC)
30
38
  os.chmod(self.server_path / "crashpad_handler", st.st_mode | stat.S_IEXEC)
31
39
 
32
- def _create_process(self, *args, **kwargs) -> None:
33
- env = os.environ.copy()
34
- env["LD_PRELOAD"] = str(self._endstone_runtime_path.absolute())
35
- env["LD_LIBRARY_PATH"] = str(self._linked_libpython_path.parent.absolute())
36
- super()._create_process(env=env)
40
+ def _run(self, *args, **kwargs) -> int:
41
+ process = subprocess.Popen(
42
+ [str(self.executable_path.absolute())],
43
+ text=True,
44
+ encoding="utf-8",
45
+ cwd=str(self.server_path.absolute()),
46
+ env=self._endstone_runtime_env,
47
+ *args,
48
+ **kwargs,
49
+ )
50
+ return process.wait()
37
51
 
38
52
  @property
39
53
  def _linked_libpython_path(self) -> Path:
@@ -1,86 +1,128 @@
1
+ import _winapi
1
2
  import ctypes
2
3
  import os
3
4
  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
- )
5
+ import warnings
6
+ from subprocess import STARTUPINFO, Handle, list2cmdline
16
7
 
8
+ from endstone._internal import _detours
17
9
  from endstone._internal.bootstrap.base import Bootstrap
18
10
 
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)
11
+
12
+ class PopenWithDll(subprocess.Popen):
13
+ def __init__(self, *args, **kwargs):
14
+ self.dll_names = kwargs.pop("dll_names", None)
15
+ super().__init__(*args, **kwargs)
16
+
17
+ def _execute_child(
18
+ self,
19
+ args,
20
+ executable,
21
+ preexec_fn,
22
+ close_fds,
23
+ pass_fds,
24
+ cwd,
25
+ env,
26
+ startupinfo,
27
+ creationflags,
28
+ shell,
29
+ p2cread,
30
+ p2cwrite,
31
+ c2pread,
32
+ c2pwrite,
33
+ errread,
34
+ errwrite,
35
+ unused_restore_signals,
36
+ unused_gid,
37
+ unused_gids,
38
+ unused_uid,
39
+ unused_umask,
40
+ unused_start_new_session,
41
+ unused_process_group,
42
+ ):
43
+ """Execute program (MS Windows version)"""
44
+
45
+ assert not pass_fds, "pass_fds not supported on Windows."
46
+
47
+ if isinstance(args, str):
48
+ pass
49
+ elif isinstance(args, bytes):
50
+ if shell:
51
+ raise TypeError("bytes args is not allowed on Windows")
52
+ args = list2cmdline([args])
53
+ elif isinstance(args, os.PathLike):
54
+ if shell:
55
+ raise TypeError("path-like args is not allowed when shell is true")
56
+ args = list2cmdline([args])
57
+ else:
58
+ args = list2cmdline(args)
59
+
60
+ if executable is not None:
61
+ executable = os.fsdecode(executable)
62
+
63
+ if startupinfo is None:
64
+ startupinfo = STARTUPINFO()
65
+ else:
66
+ startupinfo = startupinfo.copy()
67
+
68
+ use_std_handles = -1 not in (p2cread, c2pwrite, errwrite)
69
+ if use_std_handles:
70
+ startupinfo.dwFlags |= _winapi.STARTF_USESTDHANDLES
71
+ startupinfo.hStdInput = p2cread
72
+ startupinfo.hStdOutput = c2pwrite
73
+ startupinfo.hStdError = errwrite
74
+
75
+ attribute_list = startupinfo.lpAttributeList
76
+ have_handle_list = bool(attribute_list and "handle_list" in attribute_list and attribute_list["handle_list"])
77
+
78
+ # If we were given an handle_list or need to create one
79
+ if have_handle_list or (use_std_handles and close_fds):
80
+ if attribute_list is None:
81
+ attribute_list = startupinfo.lpAttributeList = {}
82
+ handle_list = attribute_list["handle_list"] = list(attribute_list.get("handle_list", []))
83
+
84
+ if use_std_handles:
85
+ handle_list += [int(p2cread), int(c2pwrite), int(errwrite)]
86
+
87
+ handle_list[:] = self._filter_handle_list(handle_list)
88
+
89
+ if handle_list:
90
+ if not close_fds:
91
+ warnings.warn("startupinfo.lpAttributeList['handle_list'] overriding close_fds", RuntimeWarning)
92
+
93
+ # When using the handle_list we always request to inherit
94
+ # handles but the only handles that will be inherited are
95
+ # the ones in the handle_list
96
+ close_fds = False
97
+
98
+ assert not shell
99
+
100
+ if cwd is not None:
101
+ cwd = os.fsdecode(cwd)
102
+
103
+ # Start the process
104
+ try:
105
+ hp, ht, pid, tid = _detours.CreateProcessWithDllEx(
106
+ executable,
107
+ args,
108
+ # no special security
109
+ None,
110
+ None,
111
+ not close_fds,
112
+ creationflags,
113
+ env,
114
+ cwd,
115
+ startupinfo,
116
+ dll_name=self.dll_names,
117
+ )
118
+ finally:
119
+ self._close_pipe_fds(p2cread, p2cwrite, c2pread, c2pwrite, errread, errwrite)
120
+
121
+ # Retain the process handle, but close the thread handle
122
+ self._child_created = True
123
+ self._handle = Handle(hp)
124
+ self.pid = pid
125
+ _winapi.CloseHandle(ht)
84
126
 
85
127
 
86
128
  class WindowsBootstrap(Bootstrap):
@@ -100,24 +142,9 @@ class WindowsBootstrap(Bootstrap):
100
142
  def _endstone_runtime_filename(self) -> str:
101
143
  return "endstone_runtime_loader.dll"
102
144
 
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()
145
+ @property
146
+ def _endstone_runtime_env(self) -> dict[str, str]:
147
+ env = super()._endstone_runtime_env
121
148
  symbol_path = env.get("_NT_SYMBOL_PATH", "")
122
149
  symbol_path_list = symbol_path.split(os.pathsep)
123
150
  symbol_path_list = [
@@ -125,83 +152,36 @@ class WindowsBootstrap(Bootstrap):
125
152
  str(self.plugin_path.absolute()),
126
153
  ] + symbol_path_list
127
154
  env["_NT_SYMBOL_PATH"] = os.pathsep.join(symbol_path_list)
155
+ return env
128
156
 
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
157
+ def _add_loopback_exemption(self) -> None:
158
+ sid = "S-1-15-2-1958404141-86561845-1752920682-3514627264-368642714-62675701-733520436"
159
+ ret = subprocess.run(
160
+ ["CheckNetIsolation", "LoopbackExempt", "-s", f"-p={sid}"], check=True, capture_output=True
146
161
  )
147
- if not address:
148
- raise ValueError(f"VirtualAllocEx failed with error {get_last_error()}.")
162
+ if sid not in str(ret.stdout):
163
+ ret = ctypes.windll.shell32.ShellExecuteW(
164
+ None, "runas", "CheckNetIsolation", " ".join(["LoopbackExempt", "-a", f"-p={sid}"]), None, 1
165
+ )
166
+ if ret <= 32:
167
+ raise RuntimeError(f"CheckNetIsolation LoopbackExempt -a failed with exit code {ret}.")
168
+
169
+ def _run(self, *args, **kwargs) -> int:
170
+ try:
171
+ self._add_loopback_exemption()
172
+ except Exception as e:
173
+ self._logger.warning(
174
+ f"Unable to add loopback exemption: %s. See bedrock_server_how_to.html for more details. {e}"
175
+ )
149
176
 
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)
177
+ process = PopenWithDll(
178
+ [str(self.executable_path.absolute())],
179
+ text=True,
180
+ encoding="utf-8",
181
+ cwd=str(self.server_path.absolute()),
182
+ env=self._endstone_runtime_env,
183
+ dll_names=str(self._endstone_runtime_path.absolute()),
184
+ *args,
185
+ **kwargs,
154
186
  )
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)
187
+ return process.wait()