levistone 0.6.2.dev58__1-cp312-cp312-win_amd64.whl → 0.10.5__1-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.
Potentially problematic release.
This version of levistone might be problematic. Click here for more details.
- endstone/__init__.py +16 -2
- endstone/_internal/bootstrap/__init__.py +6 -1
- endstone/_internal/bootstrap/base.py +74 -52
- endstone/_internal/bootstrap/linux.py +19 -5
- endstone/_internal/bootstrap/windows.py +151 -171
- endstone/_internal/endstone_python.cp312-win_amd64.pyd +0 -0
- endstone/_internal/endstone_python.pyi +1330 -589
- endstone/_internal/metrics.py +2 -2
- endstone/_internal/plugin_loader.py +18 -2
- endstone/_internal/version.py +2 -2
- endstone/actor.py +2 -2
- endstone/command.py +2 -2
- endstone/config/endstone.toml +9 -0
- endstone/enchantments.py +3 -0
- endstone/event.py +44 -4
- endstone/form.py +17 -1
- endstone/inventory.py +20 -2
- endstone/map.py +3 -0
- endstone/permissions.py +9 -1
- endstone/plugin.py +6 -0
- endstone_runtime.dll +0 -0
- endstone_runtime.pdb +0 -0
- levistone-0.10.5.dist-info/METADATA +49 -0
- levistone-0.10.5.dist-info/RECORD +40 -0
- {levistone-0.6.2.dev58.dist-info → levistone-0.10.5.dist-info}/WHEEL +1 -1
- manifest.json +1 -1
- endstone/_internal/endstone_python.pyd +0 -0
- endstone/network.py +0 -3
- levistone-0.6.2.dev58.dist-info/METADATA +0 -249
- levistone-0.6.2.dev58.dist-info/RECORD +0 -38
- {levistone-0.6.2.dev58.dist-info → levistone-0.10.5.dist-info}/entry_points.txt +0 -0
- {levistone-0.6.2.dev58.dist-info → levistone-0.10.5.dist-info/licenses}/LICENSE +0 -0
- {levistone-0.6.2.dev58.dist-info → levistone-0.10.5.dist-info}/top_level.txt +0 -0
endstone/__init__.py
CHANGED
|
@@ -1,14 +1,28 @@
|
|
|
1
|
-
from endstone._internal.endstone_python import
|
|
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.
|
|
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/
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
82
|
+
metadata = response.json()
|
|
75
83
|
|
|
76
|
-
if minecraft_version
|
|
77
|
-
raise ValueError(f"Version
|
|
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
|
-
|
|
83
|
-
|
|
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
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
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.
|
|
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
|
-
|
|
220
|
-
|
|
221
|
-
|
|
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
|
-
|
|
236
|
-
|
|
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
|
|
271
|
+
def _run(self, *args, **kwargs) -> int:
|
|
249
272
|
"""
|
|
250
|
-
|
|
273
|
+
Runs the server and returns its exit code.
|
|
251
274
|
|
|
252
|
-
This method blocks until the server process
|
|
253
|
-
|
|
254
|
-
|
|
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.
|
|
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
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
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
|
-
|
|
5
|
-
from
|
|
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
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
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
|
-
|
|
104
|
-
|
|
105
|
-
|
|
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
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
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
|
|
148
|
-
|
|
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
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
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
|
-
|
|
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()
|
|
Binary file
|