levistone 0.10.15__cp312-cp312-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.
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.124"
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,91 @@
1
+ import functools
2
+ import logging
3
+ import platform
4
+ import sys
5
+ import time
6
+
7
+ import click
8
+ import colorlog
9
+
10
+ from endstone._internal.version import __version__
11
+
12
+ handler = colorlog.StreamHandler()
13
+ handler.setFormatter(
14
+ colorlog.ColoredFormatter(
15
+ fmt="%(log_color)s[%(asctime)s.%(msecs)03d %(levelname)s] [%(name)s] %(message)s",
16
+ datefmt="%Y-%m-%d %H:%M:%S",
17
+ reset=True,
18
+ log_colors={
19
+ "DEBUG": "cyan",
20
+ "INFO": "reset",
21
+ "WARNING": "bold_yellow",
22
+ "ERROR": "bold_red",
23
+ "CRITICAL": "bold,bg_red",
24
+ },
25
+ )
26
+ )
27
+ logging.basicConfig(level=logging.INFO, handlers=[handler])
28
+ logger = logging.getLogger(__name__)
29
+
30
+ __all__ = ["cli"]
31
+
32
+
33
+ def catch_exceptions(func):
34
+ """Decorator to catch and log exceptions."""
35
+
36
+ @functools.wraps(func)
37
+ def wrapper(*args, **kwargs):
38
+ try:
39
+ return func(*args, **kwargs)
40
+ except Exception as e:
41
+ logger.exception(e)
42
+ sys.exit(-1)
43
+
44
+ return wrapper
45
+
46
+
47
+ @click.command(help="Starts an endstone server.")
48
+ @click.option(
49
+ "-s",
50
+ "--server-folder",
51
+ default="bedrock_server",
52
+ help="Specify the folder for the bedrock server. Defaults to 'bedrock_server'.",
53
+ )
54
+ @click.option(
55
+ "-y",
56
+ "--no-confirm",
57
+ "--yes",
58
+ default=False,
59
+ is_flag=True,
60
+ show_default=True,
61
+ help="Assume yes as answer to all prompts",
62
+ )
63
+ @click.option(
64
+ "-r",
65
+ "--remote",
66
+ default="https://raw.githubusercontent.com/EndstoneMC/bedrock-server-data/v2",
67
+ help="The remote URL to retrieve bedrock server data from.",
68
+ )
69
+ @click.version_option(__version__)
70
+ @catch_exceptions
71
+ def cli(server_folder: str, no_confirm: bool, remote: str) -> None:
72
+ system = platform.system()
73
+ if system == "Windows":
74
+ from endstone._internal.bootstrap.windows import WindowsBootstrap
75
+
76
+ cls = WindowsBootstrap
77
+
78
+ elif system == "Linux":
79
+ from endstone._internal.bootstrap.linux import LinuxBootstrap
80
+
81
+ cls = LinuxBootstrap
82
+ else:
83
+ raise NotImplementedError(f"{system} is not supported.")
84
+
85
+ bootstrap = cls(server_folder=server_folder, no_confirm=no_confirm, remote=remote)
86
+ exit_code = bootstrap.run()
87
+ if exit_code != 0:
88
+ logger.error(f"Server exited with non-zero code {exit_code}.")
89
+ time.sleep(2)
90
+
91
+ sys.exit(exit_code)
@@ -0,0 +1,301 @@
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
+ import tomlkit
20
+ from packaging.version import Version
21
+ from rich.progress import BarColumn, DownloadColumn, Progress, TextColumn, TimeRemainingColumn
22
+
23
+ from endstone import __minecraft_version__ as minecraft_version
24
+
25
+
26
+ class Bootstrap:
27
+ def __init__(self, server_folder: str, no_confirm: bool, remote: str) -> None:
28
+ self._server_path = Path(server_folder).absolute()
29
+ self._no_confirm = no_confirm
30
+ self._remote = remote
31
+ self._logger = logging.getLogger(self.name)
32
+ self._process: subprocess.Popen
33
+
34
+ @property
35
+ def name(self) -> str:
36
+ return __name__
37
+
38
+ @property
39
+ def target_system(self) -> str:
40
+ raise NotImplementedError
41
+
42
+ @property
43
+ def executable_filename(self) -> str:
44
+ raise NotImplementedError
45
+
46
+ @property
47
+ def server_path(self) -> Path:
48
+ return self._server_path
49
+
50
+ @property
51
+ def executable_path(self) -> Path:
52
+ return self.server_path / self.executable_filename
53
+
54
+ @property
55
+ def config_path(self) -> Path:
56
+ return self.server_path / "endstone.toml"
57
+
58
+ @property
59
+ def plugin_path(self) -> Path:
60
+ return self.server_path / "plugins"
61
+
62
+ @property
63
+ def user_agent(self) -> str:
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"
65
+
66
+ def _validate(self) -> None:
67
+ if platform.system().lower() != self.target_system:
68
+ raise NotImplementedError(f"{platform.system()} is not supported by this bootstrap.")
69
+ if not self.executable_path.exists():
70
+ raise FileNotFoundError(errno.ENOENT, os.strerror(errno.ENOENT), str(self.executable_path))
71
+ if not self._endstone_runtime_path.exists():
72
+ raise FileNotFoundError(errno.ENOENT, os.strerror(errno.ENOENT), str(self._endstone_runtime_path))
73
+
74
+ def _download(self, dst: Union[str, os.PathLike]) -> None:
75
+ dst = Path(dst)
76
+
77
+ self._logger.info("Loading index from the remote server...")
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)
81
+ response.raise_for_status()
82
+ metadata = response.json()
83
+
84
+ if minecraft_version != metadata["version"]:
85
+ raise ValueError(f"Version mismatch, expect: {minecraft_version}, actual: {metadata['version']}")
86
+
87
+ should_modify_server_properties = True
88
+
89
+ with tempfile.TemporaryFile(dir=dst) as f:
90
+ url = metadata["binary"][self.target_system.lower()]["url"]
91
+ self._logger.info(f"Downloading server from {url}...")
92
+ response = requests.get(url, stream=True, headers={"User-Agent": self.user_agent})
93
+ response.raise_for_status()
94
+ total_size = int(response.headers.get("Content-Length", 0))
95
+ m = hashlib.sha256()
96
+
97
+ with Progress(
98
+ TextColumn("[progress.description]{task.description}"),
99
+ BarColumn(),
100
+ DownloadColumn(),
101
+ TimeRemainingColumn(),
102
+ ) as progress:
103
+ task = progress.add_task("[bold blue]Downloading...", total=total_size)
104
+ for data in response.iter_content(chunk_size=1024):
105
+ progress.update(task, advance=len(data))
106
+ f.write(data)
107
+ m.update(data)
108
+
109
+ self._logger.info("Download complete. Verifying integrity...")
110
+ if m.hexdigest() != metadata["binary"][self.target_system.lower()]["sha256"]:
111
+ raise ValueError("SHA256 mismatch: the downloaded file may be corrupted or tampered with.")
112
+
113
+ self._logger.info(f"Integrity check passed. Extracting to {dst}...")
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
+ ]
124
+ with zipfile.ZipFile(f) as zip_ref:
125
+ for file in zip_ref.namelist():
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
132
+
133
+ zip_ref.extract(file, dst)
134
+
135
+ if should_modify_server_properties:
136
+ properties = dst / "server.properties"
137
+ with properties.open("r", encoding="utf-8") as file:
138
+ in_lines = file.readlines()
139
+
140
+ out_lines = []
141
+ for line in in_lines:
142
+ if line.strip() == "server-name=Dedicated Server":
143
+ out_lines.append("server-name=Endstone Server\n")
144
+ elif line.strip() == "client-side-chunk-generation-enabled=true":
145
+ out_lines.append("client-side-chunk-generation-enabled=false\n")
146
+ else:
147
+ out_lines.append(line)
148
+
149
+ with properties.open("w", encoding="utf-8") as file:
150
+ file.writelines(out_lines)
151
+
152
+ version_file = dst / "version.txt"
153
+ with version_file.open("w", encoding="utf-8") as file:
154
+ file.writelines(minecraft_version)
155
+
156
+ def _prepare(self) -> None:
157
+ # ensure the plugin folder exists
158
+ self.plugin_path.mkdir(parents=True, exist_ok=True)
159
+
160
+ # prepare the crashpad handler
161
+ src_dir, dst_dir = Path(sentry_crashpad._get_executable("crashpad_handler")).parent, self.server_path
162
+ for s in src_dir.rglob("*"):
163
+ if s.is_dir():
164
+ continue
165
+ rel = s.relative_to(src_dir)
166
+ d = dst_dir / rel
167
+ if d.exists() and d.is_dir():
168
+ raise RuntimeError(f"Destination path is a directory: {d}")
169
+ d.parent.mkdir(parents=True, exist_ok=True)
170
+ if not d.exists():
171
+ try:
172
+ shutil.copy2(s, d)
173
+ except Exception as e:
174
+ raise RuntimeError(f"Failed to copy {s} -> {d}: {e}")
175
+
176
+ # create or update the config file
177
+ if not self.config_path.exists():
178
+ ref = importlib_resources.files("endstone") / "config" / "endstone.toml"
179
+ with importlib_resources.as_file(ref) as path:
180
+ shutil.copy(path, self.config_path)
181
+ else:
182
+ ref = importlib_resources.files("endstone") / "config" / "endstone.toml"
183
+ with importlib_resources.as_file(ref) as path:
184
+ with open(path, "r", encoding="utf-8") as f:
185
+ default_config = tomlkit.load(f)
186
+
187
+ with open(self.config_path, "r", encoding="utf-8") as f:
188
+ config = tomlkit.load(f)
189
+
190
+ def migrate_config(from_doc: tomlkit.TOMLDocument, to_doc: tomlkit.TOMLDocument) -> None:
191
+ for key, val in from_doc.items():
192
+ if key not in to_doc:
193
+ # if the user hasn’t set it, copy it (with comments!)
194
+ to_doc[key] = val
195
+ else:
196
+ # if both are tables, dive deeper
197
+ if isinstance(val, tomlkit.TOMLDocument) and isinstance(to_doc[key], tomlkit.TOMLDocument):
198
+ migrate_config(val, to_doc[key])
199
+
200
+ migrate_config(default_config, config)
201
+ with open(self.config_path, "w", encoding="utf-8") as f:
202
+ tomlkit.dump(config, f)
203
+
204
+ def _install(self) -> None:
205
+ """
206
+ Installs the server if not already installed.
207
+ """
208
+
209
+ if self.executable_path.exists():
210
+ self._update()
211
+ return
212
+
213
+ if not self._no_confirm:
214
+ download = click.confirm(
215
+ f"Bedrock Dedicated Server (v{minecraft_version}) "
216
+ f"is not found in {str(self.executable_path.parent)}. "
217
+ f"Would you like to download it now?",
218
+ default=True,
219
+ )
220
+ else:
221
+ download = True
222
+
223
+ if not download:
224
+ sys.exit(1)
225
+
226
+ self.server_path.mkdir(parents=True, exist_ok=True)
227
+ self._download(self.server_path)
228
+
229
+ def _update(self) -> None:
230
+ current_version = Version("0.0.0")
231
+ supported_version = Version(minecraft_version)
232
+
233
+ version_file = self.server_path / "version.txt"
234
+ if version_file.exists():
235
+ with version_file.open("r", encoding="utf-8") as file:
236
+ current_version = Version(file.readline())
237
+
238
+ if current_version == supported_version:
239
+ return
240
+
241
+ if current_version > supported_version:
242
+ raise RuntimeError(
243
+ f"A newer version of Bedrock Dedicated Server (v{current_version}) "
244
+ f"is found in {str(self.executable_path.parent)}. Please update your Endstone server."
245
+ )
246
+
247
+ if not self._no_confirm:
248
+ update = click.confirm(
249
+ f"An older version of Bedrock Dedicated Server (v{current_version}) "
250
+ f"is found in {str(self.executable_path.parent)}. "
251
+ f"Would you like to update to v{minecraft_version} now?",
252
+ default=True,
253
+ )
254
+ else:
255
+ update = True
256
+
257
+ if not update:
258
+ sys.exit(1)
259
+
260
+ self._logger.info(f"Updating server from v{current_version} to v{minecraft_version}...")
261
+ self._download(self.server_path)
262
+
263
+ def run(self) -> int:
264
+ self._install()
265
+ self._validate()
266
+ self._prepare()
267
+ return self._run()
268
+
269
+ @property
270
+ def _endstone_runtime_filename(self) -> str:
271
+ raise NotImplementedError
272
+
273
+ @property
274
+ def _endstone_runtime_path(self) -> Path:
275
+ p = Path(__file__).parent.parent / self._endstone_runtime_filename
276
+ return p.resolve().absolute()
277
+
278
+ @property
279
+ def _endstone_runtime_env(self) -> dict[str, str]:
280
+ env = os.environ.copy()
281
+ env["PATH"] = os.pathsep.join(sys.path)
282
+ env["PYTHONPATH"] = os.pathsep.join(sys.path)
283
+ env["PYTHONIOENCODING"] = "UTF-8"
284
+ env["ENDSTONE_PYTHON_EXECUTABLE"] = sys.executable
285
+ return env
286
+
287
+ def _run(self, *args, **kwargs) -> int:
288
+ """
289
+ Runs the server and returns its exit code.
290
+
291
+ This method blocks until the server process terminates. It returns the exit code of the process, which can be
292
+ used to determine if the server shut down successfully or if there were errors.
293
+
294
+ Args:
295
+ *args: Variable length argument list.
296
+ **kwargs: Arbitrary keyword arguments.
297
+
298
+ Returns:
299
+ int: The exit code of the server process.
300
+ """
301
+ raise NotImplementedError
@@ -0,0 +1,82 @@
1
+ import ctypes.util
2
+ import os
3
+ import stat
4
+ import subprocess
5
+ from pathlib import Path
6
+
7
+ from endstone._internal.bootstrap.base import Bootstrap
8
+
9
+
10
+ class LinuxBootstrap(Bootstrap):
11
+ @property
12
+ def name(self) -> str:
13
+ return "LinuxBootstrap"
14
+
15
+ @property
16
+ def target_system(self) -> str:
17
+ return "linux"
18
+
19
+ @property
20
+ def executable_filename(self) -> str:
21
+ return "bedrock_server"
22
+
23
+ @property
24
+ def _endstone_runtime_filename(self) -> str:
25
+ return "libendstone_runtime.so"
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
+
34
+ def _prepare(self) -> None:
35
+ super()._prepare()
36
+ st = os.stat(self.executable_path)
37
+ os.chmod(self.executable_path, st.st_mode | stat.S_IEXEC)
38
+ os.chmod(self.server_path / "crashpad_handler", st.st_mode | stat.S_IEXEC)
39
+
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()
51
+
52
+ @property
53
+ def _linked_libpython_path(self) -> Path:
54
+ """
55
+ Find the path of the linked libpython on Unix systems.
56
+
57
+ From https://gist.github.com/tkf/d980eee120611604c0b9b5fef5b8dae6
58
+
59
+ Returns:
60
+ (Path): Path object representing the path of the linked libpython.
61
+ """
62
+
63
+ class DlInfo(ctypes.Structure):
64
+ # https://www.man7.org/linux/man-pages/man3/dladdr.3.html
65
+ _fields_ = [
66
+ ("dli_fname", ctypes.c_char_p),
67
+ ("dli_fbase", ctypes.c_void_p),
68
+ ("dli_sname", ctypes.c_char_p),
69
+ ("dli_saddr", ctypes.c_void_p),
70
+ ]
71
+
72
+ libdl = ctypes.CDLL(ctypes.util.find_library("dl"))
73
+ libdl.dladdr.argtypes = [ctypes.c_void_p, ctypes.POINTER(DlInfo)]
74
+ libdl.dladdr.restype = ctypes.c_int
75
+
76
+ dlinfo = DlInfo()
77
+ retcode = libdl.dladdr(ctypes.cast(ctypes.pythonapi.Py_GetVersion, ctypes.c_void_p), ctypes.pointer(dlinfo))
78
+ if retcode == 0:
79
+ raise ValueError("dladdr cannot match the address of ctypes.pythonapi.Py_GetVersion to a shared object")
80
+
81
+ path = Path(dlinfo.dli_fname.decode()).resolve()
82
+ return path