levistone 0.6.1__1-cp311-cp311-win_amd64.whl → 0.9.3__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 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.93"
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,8 +1,10 @@
1
1
  import errno
2
+ import fnmatch
2
3
  import hashlib
3
4
  import logging
4
5
  import os
5
6
  import platform
7
+ import shutil
6
8
  import subprocess
7
9
  import sys
8
10
  import tempfile
@@ -11,12 +13,13 @@ from pathlib import Path
11
13
  from typing import Union
12
14
 
13
15
  import click
16
+ import importlib_resources
14
17
  import requests
18
+ import sentry_crashpad
15
19
  from packaging.version import Version
16
20
  from rich.progress import BarColumn, DownloadColumn, Progress, TextColumn, TimeRemainingColumn
17
21
 
18
22
  from endstone import __minecraft_version__ as minecraft_version
19
- from endstone import __version__ as endstone_version
20
23
 
21
24
 
22
25
  class Bootstrap:
@@ -47,13 +50,17 @@ class Bootstrap:
47
50
  def executable_path(self) -> Path:
48
51
  return self.server_path / self.executable_filename
49
52
 
53
+ @property
54
+ def config_path(self) -> Path:
55
+ return self.server_path / "endstone.toml"
56
+
50
57
  @property
51
58
  def plugin_path(self) -> Path:
52
59
  return self.server_path / "plugins"
53
60
 
54
61
  @property
55
62
  def user_agent(self) -> str:
56
- return f"Endstone/{endstone_version} (Minecraft/{minecraft_version})"
63
+ return "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/80.0.3987.149 Safari/537.36"
57
64
 
58
65
  def _validate(self) -> None:
59
66
  if platform.system().lower() != self.target_system:
@@ -67,22 +74,23 @@ class Bootstrap:
67
74
  dst = Path(dst)
68
75
 
69
76
  self._logger.info("Loading index from the remote server...")
70
- response = requests.get(self._remote)
77
+ channel = "preview" if Version(minecraft_version).is_prerelease else "release"
78
+ metadata_url = "/".join([self._remote, channel, minecraft_version, "metadata.json"])
79
+ response = requests.get(metadata_url, timeout=10)
71
80
  response.raise_for_status()
72
- server_data = response.json()
81
+ metadata = response.json()
73
82
 
74
- if minecraft_version not in server_data["binary"]:
75
- raise ValueError(f"Version v{minecraft_version} is not found in the remote server.")
83
+ if minecraft_version != metadata["version"]:
84
+ raise ValueError(f"Version mismatch, expect: {minecraft_version}, actual: {metadata['version']}")
76
85
 
77
86
  should_modify_server_properties = True
78
87
 
79
88
  with tempfile.TemporaryFile(dir=dst) as f:
80
- metadata = server_data["binary"][minecraft_version][self.target_system.lower()]
81
- url = metadata["url"]
89
+ url = metadata["binary"][self.target_system.lower()]["url"]
90
+ self._logger.info(f"Downloading server from {url}...")
82
91
  response = requests.get(url, stream=True, headers={"User-Agent": self.user_agent})
83
92
  response.raise_for_status()
84
93
  total_size = int(response.headers.get("Content-Length", 0))
85
- self._logger.info(f"Downloading server from {url}...")
86
94
  m = hashlib.sha256()
87
95
 
88
96
  with Progress(
@@ -98,17 +106,28 @@ class Bootstrap:
98
106
  m.update(data)
99
107
 
100
108
  self._logger.info("Download complete. Verifying integrity...")
101
- if m.hexdigest() != metadata["sha256"]:
109
+ if m.hexdigest() != metadata["binary"][self.target_system.lower()]["sha256"]:
102
110
  raise ValueError("SHA256 mismatch: the downloaded file may be corrupted or tampered with.")
103
111
 
104
112
  self._logger.info(f"Integrity check passed. Extracting to {dst}...")
105
113
  dst.mkdir(parents=True, exist_ok=True)
114
+ override_patterns = [
115
+ self.executable_filename,
116
+ "behavior_packs/*",
117
+ "definitions/*",
118
+ "resource_packs/*",
119
+ "bedrock_server_how_to.html",
120
+ "profanity_filter.wlist",
121
+ "release-notes.txt",
122
+ ]
106
123
  with zipfile.ZipFile(f) as zip_ref:
107
124
  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
125
+ dest_path = dst / file
126
+ if dest_path.exists():
127
+ if not any(fnmatch.fnmatch(file, pattern) for pattern in override_patterns):
128
+ should_modify_server_properties = False
129
+ self._logger.info(f"{dest_path} already exists, skipping.")
130
+ continue
112
131
 
113
132
  zip_ref.extract(file, dst)
114
133
 
@@ -135,6 +154,13 @@ class Bootstrap:
135
154
 
136
155
  def _prepare(self) -> None:
137
156
  self.plugin_path.mkdir(parents=True, exist_ok=True)
157
+ shutil.copytree(
158
+ Path(sentry_crashpad._get_executable("crashpad_handler")).parent, self.server_path, dirs_exist_ok=True
159
+ )
160
+ if not self.config_path.exists():
161
+ ref = importlib_resources.files("endstone") / "config" / "endstone.toml"
162
+ with importlib_resources.as_file(ref) as path:
163
+ shutil.copy(path, self.config_path)
138
164
 
139
165
  def _install(self) -> None:
140
166
  """
@@ -199,8 +225,7 @@ class Bootstrap:
199
225
  self._install()
200
226
  self._validate()
201
227
  self._prepare()
202
- self._create_process()
203
- return self._wait_for_server()
228
+ return self._run()
204
229
 
205
230
  @property
206
231
  def _endstone_runtime_filename(self) -> str:
@@ -211,48 +236,27 @@ class Bootstrap:
211
236
  p = Path(__file__).parent.parent / self._endstone_runtime_filename
212
237
  return p.resolve().absolute()
213
238
 
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())
239
+ @property
240
+ def _endstone_runtime_env(self) -> dict[str, str]:
241
+ env = os.environ.copy()
227
242
  env["PATH"] = os.pathsep.join(sys.path)
228
243
  env["PYTHONPATH"] = os.pathsep.join(sys.path)
229
244
  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
- )
245
+ env["ENDSTONE_PYTHON_EXECUTABLE"] = sys.executable
246
+ return env
242
247
 
243
- def _wait_for_server(self) -> int:
248
+ def _run(self, *args, **kwargs) -> int:
244
249
  """
245
- Waits for the server process to terminate and returns its exit code.
250
+ Runs the server and returns its exit code.
246
251
 
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.
252
+ This method blocks until the server process terminates. It returns the exit code of the process, which can be
253
+ used to determine if the server shut down successfully or if there were errors.
254
+
255
+ Args:
256
+ *args: Variable length argument list.
257
+ **kwargs: Arbitrary keyword arguments.
250
258
 
251
259
  Returns:
252
- int: The exit code of the server process. Returns -1 if the process is not created or still running.
260
+ int: The exit code of the server process.
253
261
  """
254
-
255
- if self._process is None:
256
- return -1
257
-
258
- return self._process.wait()
262
+ raise NotImplementedError
@@ -1,6 +1,8 @@
1
1
  import ctypes.util
2
2
  import os
3
3
  import stat
4
+ import subprocess
5
+ import sys
4
6
  from pathlib import Path
5
7
 
6
8
  from endstone._internal.bootstrap.base import Bootstrap
@@ -23,16 +25,33 @@ class LinuxBootstrap(Bootstrap):
23
25
  def _endstone_runtime_filename(self) -> str:
24
26
  return "libendstone_runtime.so"
25
27
 
28
+ @property
29
+ def _endstone_runtime_env(self) -> dict[str, str]:
30
+ env = super()._endstone_runtime_env
31
+ env["LD_PRELOAD"] = str(self._endstone_runtime_path.absolute())
32
+ env["LD_LIBRARY_PATH"] = str(self._linked_libpython_path.parent.absolute())
33
+ return env
34
+
26
35
  def _prepare(self) -> None:
27
36
  super()._prepare()
28
37
  st = os.stat(self.executable_path)
29
38
  os.chmod(self.executable_path, st.st_mode | stat.S_IEXEC)
39
+ os.chmod(self.server_path / "crashpad_handler", st.st_mode | stat.S_IEXEC)
30
40
 
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)
41
+ def _run(self, *args, **kwargs) -> int:
42
+ process = subprocess.Popen(
43
+ [str(self.executable_path.absolute())],
44
+ stdin=sys.stdin,
45
+ stdout=sys.stdout,
46
+ stderr=subprocess.STDOUT,
47
+ text=True,
48
+ encoding="utf-8",
49
+ cwd=str(self.server_path.absolute()),
50
+ env=self._endstone_runtime_env,
51
+ *args,
52
+ **kwargs,
53
+ )
54
+ return process.wait()
36
55
 
37
56
  @property
38
57
  def _linked_libpython_path(self) -> Path:
@@ -1,86 +1,9 @@
1
1
  import ctypes
2
2
  import os
3
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
4
 
17
5
  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)
6
+ from endstone._internal.winext import start_process_with_dll
84
7
 
85
8
 
86
9
  class WindowsBootstrap(Bootstrap):
@@ -100,6 +23,18 @@ class WindowsBootstrap(Bootstrap):
100
23
  def _endstone_runtime_filename(self) -> str:
101
24
  return "endstone_runtime_loader.dll"
102
25
 
26
+ @property
27
+ def _endstone_runtime_env(self) -> dict[str, str]:
28
+ env = super()._endstone_runtime_env
29
+ symbol_path = env.get("_NT_SYMBOL_PATH", "")
30
+ symbol_path_list = symbol_path.split(os.pathsep)
31
+ symbol_path_list = [
32
+ str(self._endstone_runtime_path.parent.absolute()),
33
+ str(self.plugin_path.absolute()),
34
+ ] + symbol_path_list
35
+ env["_NT_SYMBOL_PATH"] = os.pathsep.join(symbol_path_list)
36
+ return env
37
+
103
38
  def _add_loopback_exemption(self) -> bool:
104
39
  sid = "S-1-15-2-1958404141-86561845-1752920682-3514627264-368642714-62675701-733520436"
105
40
  ret = subprocess.run(
@@ -113,95 +48,12 @@ class WindowsBootstrap(Bootstrap):
113
48
  else:
114
49
  return True
115
50
 
116
- def _create_process(self, *args, **kwargs) -> None:
51
+ def _run(self, *args, **kwargs) -> int:
117
52
  self._add_loopback_exemption()
118
53
 
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)
54
+ return start_process_with_dll(
55
+ str(self.executable_path.absolute()),
56
+ str(self._endstone_runtime_path.absolute()),
57
+ cwd=str(self.server_path.absolute()),
58
+ env=self._endstone_runtime_env,
154
59
  )
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)