levistone 0.6.1__1-cp310-cp310-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 +16 -0
- endstone/__main__.py +4 -0
- endstone/_internal/__init__.py +0 -0
- endstone/_internal/bootstrap/__init__.py +74 -0
- endstone/_internal/bootstrap/base.py +258 -0
- endstone/_internal/bootstrap/linux.py +67 -0
- endstone/_internal/bootstrap/windows.py +207 -0
- endstone/_internal/endstone_python.pyd +0 -0
- endstone/_internal/endstone_python.pyi +4082 -0
- endstone/_internal/metrics.py +101 -0
- endstone/_internal/plugin_loader.py +234 -0
- endstone/_internal/version.py +14 -0
- endstone/actor.py +3 -0
- endstone/ban.py +3 -0
- endstone/block.py +3 -0
- endstone/boss.py +3 -0
- endstone/command.py +11 -0
- endstone/damage.py +3 -0
- endstone/event.py +99 -0
- endstone/form.py +13 -0
- endstone/inventory.py +3 -0
- endstone/lang.py +3 -0
- endstone/level.py +9 -0
- endstone/network.py +3 -0
- endstone/permissions.py +9 -0
- endstone/plugin.py +137 -0
- endstone/scheduler.py +3 -0
- endstone/scoreboard.py +3 -0
- endstone/util.py +3 -0
- endstone_runtime.dll +0 -0
- endstone_runtime.pdb +0 -0
- levistone-0.6.1.dist-info/METADATA +47 -0
- levistone-0.6.1.dist-info/RECORD +37 -0
- levistone-0.6.1.dist-info/WHEEL +5 -0
- levistone-0.6.1.dist-info/entry_points.txt +2 -0
- levistone-0.6.1.dist-info/top_level.txt +2 -0
- manifest.json +6 -0
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
|
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
|