levistone 0.9.3__2-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 ADDED
@@ -0,0 +1,30 @@
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
+ )
13
+ from endstone._internal.version import __version__
14
+
15
+ __minecraft_version__ = "1.21.93"
16
+
17
+ __all__ = [
18
+ "__version__",
19
+ "__minecraft_version__",
20
+ "ColorFormat",
21
+ "EnchantmentRegistry",
22
+ "GameMode",
23
+ "ItemRegistry",
24
+ "Logger",
25
+ "NamespacedKey",
26
+ "OfflinePlayer",
27
+ "Player",
28
+ "Server",
29
+ "Skin",
30
+ ]
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,79 @@
1
+ import functools
2
+ import logging
3
+ import platform
4
+ import sys
5
+ import time
6
+
7
+ import click
8
+
9
+ from endstone._internal.version import __version__
10
+
11
+ logging.basicConfig(
12
+ level=logging.INFO,
13
+ format="[%(asctime)s.%(msecs)03d %(levelname)s] [%(name)s] %(message)s",
14
+ datefmt="%Y-%m-%d %H:%M:%S",
15
+ )
16
+ logger = logging.getLogger(__name__)
17
+
18
+ __all__ = ["cli"]
19
+
20
+
21
+ def catch_exceptions(func):
22
+ """Decorator to catch and log exceptions."""
23
+
24
+ @functools.wraps(func)
25
+ def wrapper(*args, **kwargs):
26
+ try:
27
+ return func(*args, **kwargs)
28
+ except Exception as e:
29
+ logger.exception(e)
30
+ sys.exit(-1)
31
+
32
+ return wrapper
33
+
34
+
35
+ @click.command(help="Starts an endstone server.")
36
+ @click.option(
37
+ "-s",
38
+ "--server-folder",
39
+ default="bedrock_server",
40
+ help="Specify the folder for the bedrock server. Defaults to 'bedrock_server'.",
41
+ )
42
+ @click.option(
43
+ "-y",
44
+ "--no-confirm",
45
+ "--yes",
46
+ default=False,
47
+ is_flag=True,
48
+ show_default=True,
49
+ help="Assume yes as answer to all prompts",
50
+ )
51
+ @click.option(
52
+ "-r",
53
+ "--remote",
54
+ default="https://raw.githubusercontent.com/EndstoneMC/bedrock-server-data/v2",
55
+ help="The remote URL to retrieve bedrock server data from.",
56
+ )
57
+ @click.version_option(__version__)
58
+ @catch_exceptions
59
+ def cli(server_folder: str, no_confirm: bool, remote: str) -> None:
60
+ system = platform.system()
61
+ if system == "Windows":
62
+ from endstone._internal.bootstrap.windows import WindowsBootstrap
63
+
64
+ cls = WindowsBootstrap
65
+
66
+ elif system == "Linux":
67
+ from endstone._internal.bootstrap.linux import LinuxBootstrap
68
+
69
+ cls = LinuxBootstrap
70
+ else:
71
+ raise NotImplementedError(f"{system} is not supported.")
72
+
73
+ bootstrap = cls(server_folder=server_folder, no_confirm=no_confirm, remote=remote)
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
+
79
+ sys.exit(exit_code)
@@ -0,0 +1,262 @@
1
+ import errno
2
+ import fnmatch
3
+ import hashlib
4
+ import logging
5
+ import os
6
+ import platform
7
+ import shutil
8
+ import subprocess
9
+ import sys
10
+ import tempfile
11
+ import zipfile
12
+ from pathlib import Path
13
+ from typing import Union
14
+
15
+ import click
16
+ import importlib_resources
17
+ import requests
18
+ import sentry_crashpad
19
+ from packaging.version import Version
20
+ from rich.progress import BarColumn, DownloadColumn, Progress, TextColumn, TimeRemainingColumn
21
+
22
+ from endstone import __minecraft_version__ as minecraft_version
23
+
24
+
25
+ class Bootstrap:
26
+ def __init__(self, server_folder: str, no_confirm: bool, remote: str) -> None:
27
+ self._server_path = Path(server_folder).absolute()
28
+ self._no_confirm = no_confirm
29
+ self._remote = remote
30
+ self._logger = logging.getLogger(self.name)
31
+ self._process: subprocess.Popen
32
+
33
+ @property
34
+ def name(self) -> str:
35
+ return __name__
36
+
37
+ @property
38
+ def target_system(self) -> str:
39
+ raise NotImplementedError
40
+
41
+ @property
42
+ def executable_filename(self) -> str:
43
+ raise NotImplementedError
44
+
45
+ @property
46
+ def server_path(self) -> Path:
47
+ return self._server_path
48
+
49
+ @property
50
+ def executable_path(self) -> Path:
51
+ return self.server_path / self.executable_filename
52
+
53
+ @property
54
+ def config_path(self) -> Path:
55
+ return self.server_path / "endstone.toml"
56
+
57
+ @property
58
+ def plugin_path(self) -> Path:
59
+ return self.server_path / "plugins"
60
+
61
+ @property
62
+ def user_agent(self) -> str:
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"
64
+
65
+ def _validate(self) -> None:
66
+ if platform.system().lower() != self.target_system:
67
+ raise NotImplementedError(f"{platform.system()} is not supported by this bootstrap.")
68
+ if not self.executable_path.exists():
69
+ raise FileNotFoundError(errno.ENOENT, os.strerror(errno.ENOENT), str(self.executable_path))
70
+ if not self._endstone_runtime_path.exists():
71
+ raise FileNotFoundError(errno.ENOENT, os.strerror(errno.ENOENT), str(self._endstone_runtime_path))
72
+
73
+ def _download(self, dst: Union[str, os.PathLike]) -> None:
74
+ dst = Path(dst)
75
+
76
+ self._logger.info("Loading index from the remote server...")
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)
80
+ response.raise_for_status()
81
+ metadata = response.json()
82
+
83
+ if minecraft_version != metadata["version"]:
84
+ raise ValueError(f"Version mismatch, expect: {minecraft_version}, actual: {metadata['version']}")
85
+
86
+ should_modify_server_properties = True
87
+
88
+ with tempfile.TemporaryFile(dir=dst) as f:
89
+ url = metadata["binary"][self.target_system.lower()]["url"]
90
+ self._logger.info(f"Downloading server from {url}...")
91
+ response = requests.get(url, stream=True, headers={"User-Agent": self.user_agent})
92
+ response.raise_for_status()
93
+ total_size = int(response.headers.get("Content-Length", 0))
94
+ m = hashlib.sha256()
95
+
96
+ with Progress(
97
+ TextColumn("[progress.description]{task.description}"),
98
+ BarColumn(),
99
+ DownloadColumn(),
100
+ TimeRemainingColumn(),
101
+ ) as progress:
102
+ task = progress.add_task("[bold blue]Downloading...", total=total_size)
103
+ for data in response.iter_content(chunk_size=1024):
104
+ progress.update(task, advance=len(data))
105
+ f.write(data)
106
+ m.update(data)
107
+
108
+ self._logger.info("Download complete. Verifying integrity...")
109
+ if m.hexdigest() != metadata["binary"][self.target_system.lower()]["sha256"]:
110
+ raise ValueError("SHA256 mismatch: the downloaded file may be corrupted or tampered with.")
111
+
112
+ self._logger.info(f"Integrity check passed. Extracting to {dst}...")
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
+ ]
123
+ with zipfile.ZipFile(f) as zip_ref:
124
+ for file in zip_ref.namelist():
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
131
+
132
+ zip_ref.extract(file, dst)
133
+
134
+ if should_modify_server_properties:
135
+ properties = dst / "server.properties"
136
+ with properties.open("r", encoding="utf-8") as file:
137
+ in_lines = file.readlines()
138
+
139
+ out_lines = []
140
+ for line in in_lines:
141
+ if line.strip() == "server-name=Dedicated Server":
142
+ out_lines.append("server-name=Endstone Server\n")
143
+ elif line.strip() == "client-side-chunk-generation-enabled=true":
144
+ out_lines.append("client-side-chunk-generation-enabled=false\n")
145
+ else:
146
+ out_lines.append(line)
147
+
148
+ with properties.open("w", encoding="utf-8") as file:
149
+ file.writelines(out_lines)
150
+
151
+ version_file = dst / "version.txt"
152
+ with version_file.open("w", encoding="utf-8") as file:
153
+ file.writelines(minecraft_version)
154
+
155
+ def _prepare(self) -> None:
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)
164
+
165
+ def _install(self) -> None:
166
+ """
167
+ Installs the server if not already installed.
168
+ """
169
+
170
+ if self.executable_path.exists():
171
+ self._update()
172
+ return
173
+
174
+ if not self._no_confirm:
175
+ download = click.confirm(
176
+ f"Bedrock Dedicated Server (v{minecraft_version}) "
177
+ f"is not found in {str(self.executable_path.parent)}. "
178
+ f"Would you like to download it now?",
179
+ default=True,
180
+ )
181
+ else:
182
+ download = True
183
+
184
+ if not download:
185
+ sys.exit(1)
186
+
187
+ self.server_path.mkdir(parents=True, exist_ok=True)
188
+ self._download(self.server_path)
189
+
190
+ def _update(self) -> None:
191
+ current_version = Version("0.0.0")
192
+ supported_version = Version(minecraft_version)
193
+
194
+ version_file = self.server_path / "version.txt"
195
+ if version_file.exists():
196
+ with version_file.open("r", encoding="utf-8") as file:
197
+ current_version = Version(file.readline())
198
+
199
+ if current_version == supported_version:
200
+ return
201
+
202
+ if current_version > supported_version:
203
+ raise RuntimeError(
204
+ f"A newer version of Bedrock Dedicated Server (v{current_version}) "
205
+ f"is found in {str(self.executable_path.parent)}. Please update your Endstone server."
206
+ )
207
+
208
+ if not self._no_confirm:
209
+ update = click.confirm(
210
+ f"An older version of Bedrock Dedicated Server (v{current_version}) "
211
+ f"is found in {str(self.executable_path.parent)}. "
212
+ f"Would you like to update to v{minecraft_version} now?",
213
+ default=True,
214
+ )
215
+ else:
216
+ update = True
217
+
218
+ if not update:
219
+ sys.exit(1)
220
+
221
+ self._logger.info(f"Updating server from v{current_version} to v{minecraft_version}...")
222
+ self._download(self.server_path)
223
+
224
+ def run(self) -> int:
225
+ self._install()
226
+ self._validate()
227
+ self._prepare()
228
+ return self._run()
229
+
230
+ @property
231
+ def _endstone_runtime_filename(self) -> str:
232
+ raise NotImplementedError
233
+
234
+ @property
235
+ def _endstone_runtime_path(self) -> Path:
236
+ p = Path(__file__).parent.parent / self._endstone_runtime_filename
237
+ return p.resolve().absolute()
238
+
239
+ @property
240
+ def _endstone_runtime_env(self) -> dict[str, str]:
241
+ env = os.environ.copy()
242
+ env["PATH"] = os.pathsep.join(sys.path)
243
+ env["PYTHONPATH"] = os.pathsep.join(sys.path)
244
+ env["PYTHONIOENCODING"] = "UTF-8"
245
+ env["ENDSTONE_PYTHON_EXECUTABLE"] = sys.executable
246
+ return env
247
+
248
+ def _run(self, *args, **kwargs) -> int:
249
+ """
250
+ Runs the server and returns its exit code.
251
+
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.
258
+
259
+ Returns:
260
+ int: The exit code of the server process.
261
+ """
262
+ raise NotImplementedError
@@ -0,0 +1,86 @@
1
+ import ctypes.util
2
+ import os
3
+ import stat
4
+ import subprocess
5
+ import sys
6
+ from pathlib import Path
7
+
8
+ from endstone._internal.bootstrap.base import Bootstrap
9
+
10
+
11
+ class LinuxBootstrap(Bootstrap):
12
+ @property
13
+ def name(self) -> str:
14
+ return "LinuxBootstrap"
15
+
16
+ @property
17
+ def target_system(self) -> str:
18
+ return "linux"
19
+
20
+ @property
21
+ def executable_filename(self) -> str:
22
+ return "bedrock_server"
23
+
24
+ @property
25
+ def _endstone_runtime_filename(self) -> str:
26
+ return "libendstone_runtime.so"
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
+
35
+ def _prepare(self) -> None:
36
+ super()._prepare()
37
+ st = os.stat(self.executable_path)
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)
40
+
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()
55
+
56
+ @property
57
+ def _linked_libpython_path(self) -> Path:
58
+ """
59
+ Find the path of the linked libpython on Unix systems.
60
+
61
+ From https://gist.github.com/tkf/d980eee120611604c0b9b5fef5b8dae6
62
+
63
+ Returns:
64
+ (Path): Path object representing the path of the linked libpython.
65
+ """
66
+
67
+ class DlInfo(ctypes.Structure):
68
+ # https://www.man7.org/linux/man-pages/man3/dladdr.3.html
69
+ _fields_ = [
70
+ ("dli_fname", ctypes.c_char_p),
71
+ ("dli_fbase", ctypes.c_void_p),
72
+ ("dli_sname", ctypes.c_char_p),
73
+ ("dli_saddr", ctypes.c_void_p),
74
+ ]
75
+
76
+ libdl = ctypes.CDLL(ctypes.util.find_library("dl"))
77
+ libdl.dladdr.argtypes = [ctypes.c_void_p, ctypes.POINTER(DlInfo)]
78
+ libdl.dladdr.restype = ctypes.c_int
79
+
80
+ dlinfo = DlInfo()
81
+ retcode = libdl.dladdr(ctypes.cast(ctypes.pythonapi.Py_GetVersion, ctypes.c_void_p), ctypes.pointer(dlinfo))
82
+ if retcode == 0:
83
+ raise ValueError("dladdr cannot match the address of ctypes.pythonapi.Py_GetVersion to a shared object")
84
+
85
+ path = Path(dlinfo.dli_fname.decode()).resolve()
86
+ return path
@@ -0,0 +1,59 @@
1
+ import ctypes
2
+ import os
3
+ import subprocess
4
+
5
+ from endstone._internal.bootstrap.base import Bootstrap
6
+ from endstone._internal.winext import start_process_with_dll
7
+
8
+
9
+ class WindowsBootstrap(Bootstrap):
10
+ @property
11
+ def name(self) -> str:
12
+ return "WindowsBootstrap"
13
+
14
+ @property
15
+ def target_system(self) -> str:
16
+ return "windows"
17
+
18
+ @property
19
+ def executable_filename(self) -> str:
20
+ return "bedrock_server.exe"
21
+
22
+ @property
23
+ def _endstone_runtime_filename(self) -> str:
24
+ return "endstone_runtime_loader.dll"
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
+
38
+ def _add_loopback_exemption(self) -> bool:
39
+ sid = "S-1-15-2-1958404141-86561845-1752920682-3514627264-368642714-62675701-733520436"
40
+ ret = subprocess.run(
41
+ ["CheckNetIsolation", "LoopbackExempt", "-s", f"-p={sid}"], check=True, capture_output=True
42
+ )
43
+ if sid not in str(ret.stdout):
44
+ ret = ctypes.windll.shell32.ShellExecuteW(
45
+ None, "runas", "CheckNetIsolation", " ".join(["LoopbackExempt", "-a", f"-p={sid}"]), None, 1
46
+ )
47
+ return ret > 32
48
+ else:
49
+ return True
50
+
51
+ def _run(self, *args, **kwargs) -> int:
52
+ self._add_loopback_exemption()
53
+
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,
59
+ )