nethackers 0.6.22__py3-none-any.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.
- nethackers/__init__.py +6 -0
- nethackers/_runtime_pin.py +3 -0
- nethackers/bootstrap.py +507 -0
- nethackers/cli.py +1949 -0
- nethackers/constants.py +89 -0
- nethackers/eval/__init__.py +1 -0
- nethackers/eval/evaluation.py +1087 -0
- nethackers/eval/runner.py +600 -0
- nethackers/eval/trace_supervisor.py +61 -0
- nethackers/harness/__init__.py +49 -0
- nethackers/harness/command.py +620 -0
- nethackers/harness/conversation.py +381 -0
- nethackers/harness/evolution/__init__.py +1 -0
- nethackers/harness/evolution/agent.py +471 -0
- nethackers/harness/evolution/driver.py +766 -0
- nethackers/harness/evolution/kernel.py +295 -0
- nethackers/harness/evolution/session.py +358 -0
- nethackers/harness/evolution/workflow.py +1916 -0
- nethackers/harness/history.py +396 -0
- nethackers/harness/protocol.py +955 -0
- nethackers/harness/recovery.py +874 -0
- nethackers/harness/runner.py +3352 -0
- nethackers/harness/search.py +367 -0
- nethackers/harness/server.py +218 -0
- nethackers/harness/spec.py +552 -0
- nethackers/harness/supervisor.py +125 -0
- nethackers/repository.py +680 -0
- nethackers/sdk/__init__.py +46 -0
- nethackers/sdk/candidate.py +388 -0
- nethackers/sdk/github.py +1106 -0
- nethackers/sdk/objective.py +211 -0
- nethackers/sdk/starting_conditions.py +254 -0
- nethackers/sdk/submission.py +783 -0
- nethackers/sdk/verification.py +118 -0
- nethackers-0.6.22.dist-info/METADATA +181 -0
- nethackers-0.6.22.dist-info/RECORD +40 -0
- nethackers-0.6.22.dist-info/WHEEL +5 -0
- nethackers-0.6.22.dist-info/entry_points.txt +2 -0
- nethackers-0.6.22.dist-info/licenses/LICENSE +202 -0
- nethackers-0.6.22.dist-info/top_level.txt +1 -0
nethackers/__init__.py
ADDED
nethackers/bootstrap.py
ADDED
|
@@ -0,0 +1,507 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import hashlib
|
|
4
|
+
import os
|
|
5
|
+
import re
|
|
6
|
+
import shutil
|
|
7
|
+
import signal
|
|
8
|
+
import subprocess
|
|
9
|
+
import sys
|
|
10
|
+
import tarfile
|
|
11
|
+
import tempfile
|
|
12
|
+
import urllib.error
|
|
13
|
+
import urllib.request
|
|
14
|
+
from pathlib import Path, PurePosixPath
|
|
15
|
+
|
|
16
|
+
from rich.console import Console
|
|
17
|
+
from rich.panel import Panel
|
|
18
|
+
|
|
19
|
+
from nethackers import __version__
|
|
20
|
+
from nethackers._runtime_pin import SOURCE_RUNTIME_DIGEST
|
|
21
|
+
|
|
22
|
+
SOURCE_REPOSITORY = "https://github.com/dunnolab/nethackers.git"
|
|
23
|
+
SOURCE_REVISION = f"v{__version__}"
|
|
24
|
+
RUNTIME_ARCHIVE_URL = (
|
|
25
|
+
f"https://nethackers.dunnolab.ai/dist/nethackers-runtime-{__version__}.tar.gz"
|
|
26
|
+
)
|
|
27
|
+
_RUNTIME_PIN_PATH = "src/nethackers/_runtime_pin.py"
|
|
28
|
+
_DIGEST_RE = re.compile(r"^sha256:[0-9a-f]{64}$")
|
|
29
|
+
_MAX_RUNTIME_ARCHIVE_BYTES = 16 * 1024 * 1024
|
|
30
|
+
_MAX_RUNTIME_MEMBERS = 512
|
|
31
|
+
_MAX_RUNTIME_UNPACKED_BYTES = 64 * 1024 * 1024
|
|
32
|
+
_COMMANDS = {
|
|
33
|
+
"ascend",
|
|
34
|
+
"baseline",
|
|
35
|
+
"candidate",
|
|
36
|
+
"evaluate",
|
|
37
|
+
"join",
|
|
38
|
+
"login",
|
|
39
|
+
"logs",
|
|
40
|
+
"logout",
|
|
41
|
+
"search",
|
|
42
|
+
"session",
|
|
43
|
+
"status",
|
|
44
|
+
"stop",
|
|
45
|
+
"verify",
|
|
46
|
+
"whoami",
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
class BootstrapError(RuntimeError):
|
|
51
|
+
"""Raised when the compact runtime checkout cannot be prepared."""
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def _is_runtime_checkout(path: Path) -> bool:
|
|
55
|
+
return all(
|
|
56
|
+
(path / relative).is_file()
|
|
57
|
+
for relative in (
|
|
58
|
+
"roots/autoascend/agent.py",
|
|
59
|
+
"harness/evolution/problem/task.txt",
|
|
60
|
+
"template/README.md",
|
|
61
|
+
"pyproject.toml",
|
|
62
|
+
)
|
|
63
|
+
)
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def _source_checkout() -> Path | None:
|
|
67
|
+
candidate = Path(__file__).resolve().parents[2]
|
|
68
|
+
if _is_runtime_checkout(candidate):
|
|
69
|
+
return candidate.resolve()
|
|
70
|
+
return None
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def _runtime_home() -> Path:
|
|
74
|
+
configured = os.environ.get("NETHACKERS_HOME")
|
|
75
|
+
if configured:
|
|
76
|
+
return Path(configured).expanduser().resolve()
|
|
77
|
+
cache_home = Path(os.environ.get("XDG_CACHE_HOME", Path.home() / ".cache"))
|
|
78
|
+
return (cache_home / "nethackers").resolve()
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def _state_home() -> Path:
|
|
82
|
+
configured = os.environ.get("NETHACKERS_STATE_DIR")
|
|
83
|
+
if configured:
|
|
84
|
+
return Path(configured).expanduser().resolve()
|
|
85
|
+
state_home = Path(os.environ.get("XDG_STATE_HOME", Path.home() / ".local" / "state"))
|
|
86
|
+
return (state_home / "nethackers").resolve()
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def _clone_command(source: str, revision: str, destination: Path) -> list[str]:
|
|
90
|
+
github_source = source.rstrip("/").removesuffix(".git")
|
|
91
|
+
gh_authenticated = False
|
|
92
|
+
if github_source == "https://github.com/dunnolab/nethackers" and shutil.which("gh"):
|
|
93
|
+
try:
|
|
94
|
+
status = subprocess.run(
|
|
95
|
+
["gh", "auth", "status", "--hostname", "github.com"],
|
|
96
|
+
check=False,
|
|
97
|
+
capture_output=True,
|
|
98
|
+
text=True,
|
|
99
|
+
)
|
|
100
|
+
gh_authenticated = status.returncode == 0
|
|
101
|
+
except OSError:
|
|
102
|
+
pass
|
|
103
|
+
if gh_authenticated:
|
|
104
|
+
return [
|
|
105
|
+
"gh",
|
|
106
|
+
"repo",
|
|
107
|
+
"clone",
|
|
108
|
+
"dunnolab/nethackers",
|
|
109
|
+
str(destination),
|
|
110
|
+
"--",
|
|
111
|
+
"--branch",
|
|
112
|
+
revision,
|
|
113
|
+
"--depth",
|
|
114
|
+
"1",
|
|
115
|
+
"--filter=blob:none",
|
|
116
|
+
]
|
|
117
|
+
return [
|
|
118
|
+
"git",
|
|
119
|
+
"clone",
|
|
120
|
+
"--branch",
|
|
121
|
+
revision,
|
|
122
|
+
"--depth",
|
|
123
|
+
"1",
|
|
124
|
+
"--filter=blob:none",
|
|
125
|
+
source,
|
|
126
|
+
str(destination),
|
|
127
|
+
]
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
def _run_checked(command: list[str]) -> None:
|
|
131
|
+
try:
|
|
132
|
+
result = subprocess.run(command, capture_output=True, text=True, check=False)
|
|
133
|
+
except OSError as exc:
|
|
134
|
+
raise BootstrapError(f"could not run {command[0]!r}: {exc}") from exc
|
|
135
|
+
if result.returncode == 0:
|
|
136
|
+
return
|
|
137
|
+
detail = (result.stderr or result.stdout).strip().splitlines()
|
|
138
|
+
reason = detail[-1] if detail else f"exit status {result.returncode}"
|
|
139
|
+
raise BootstrapError(f"{command[0]} failed: {reason}")
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
def runtime_digest(path: Path) -> str:
|
|
143
|
+
"""Digest the exact Git index while excluding only the generated pin itself."""
|
|
144
|
+
try:
|
|
145
|
+
result = subprocess.run(
|
|
146
|
+
["git", "-C", str(path), "ls-files", "--stage", "-z"],
|
|
147
|
+
capture_output=True,
|
|
148
|
+
check=False,
|
|
149
|
+
)
|
|
150
|
+
except OSError as exc:
|
|
151
|
+
raise BootstrapError(f"could not inspect the source runtime: {exc}") from exc
|
|
152
|
+
if result.returncode != 0:
|
|
153
|
+
detail = result.stderr.decode(errors="replace").strip().splitlines()
|
|
154
|
+
reason = detail[-1] if detail else f"exit status {result.returncode}"
|
|
155
|
+
raise BootstrapError(f"could not inspect the source runtime: {reason}")
|
|
156
|
+
digest = hashlib.sha256()
|
|
157
|
+
entries = 0
|
|
158
|
+
for record in result.stdout.split(b"\0"):
|
|
159
|
+
if not record:
|
|
160
|
+
continue
|
|
161
|
+
metadata, separator, path_bytes = record.partition(b"\t")
|
|
162
|
+
fields = metadata.split()
|
|
163
|
+
if not separator or len(fields) != 3:
|
|
164
|
+
raise BootstrapError("source runtime has an invalid Git index")
|
|
165
|
+
try:
|
|
166
|
+
relative = path_bytes.decode("utf-8")
|
|
167
|
+
except UnicodeDecodeError as exc:
|
|
168
|
+
raise BootstrapError("source runtime contains a non-UTF-8 path") from exc
|
|
169
|
+
if relative == _RUNTIME_PIN_PATH:
|
|
170
|
+
continue
|
|
171
|
+
mode, object_id, stage = fields
|
|
172
|
+
if stage != b"0" or mode not in {b"100644", b"100755"}:
|
|
173
|
+
raise BootstrapError("source runtime contains an unsupported Git index entry")
|
|
174
|
+
digest.update(mode + b"\0" + object_id + b"\0" + path_bytes + b"\0")
|
|
175
|
+
entries += 1
|
|
176
|
+
if entries < 1:
|
|
177
|
+
raise BootstrapError("source runtime Git index is empty")
|
|
178
|
+
return f"sha256:{digest.hexdigest()}"
|
|
179
|
+
|
|
180
|
+
|
|
181
|
+
def runtime_directory_digest(path: Path) -> str:
|
|
182
|
+
"""Digest a release tree with the same entries used by Git's index."""
|
|
183
|
+
digest = hashlib.sha256()
|
|
184
|
+
entries = 0
|
|
185
|
+
for item in sorted(
|
|
186
|
+
path.rglob("*"),
|
|
187
|
+
key=lambda value: value.relative_to(path).as_posix(),
|
|
188
|
+
):
|
|
189
|
+
relative = item.relative_to(path).as_posix()
|
|
190
|
+
if relative == _RUNTIME_PIN_PATH:
|
|
191
|
+
continue
|
|
192
|
+
if item.is_symlink():
|
|
193
|
+
raise BootstrapError(f"source runtime contains a symbolic link: {relative}")
|
|
194
|
+
if item.is_dir():
|
|
195
|
+
continue
|
|
196
|
+
if not item.is_file():
|
|
197
|
+
raise BootstrapError(f"source runtime contains an unsupported entry: {relative}")
|
|
198
|
+
try:
|
|
199
|
+
path_bytes = relative.encode("utf-8")
|
|
200
|
+
except UnicodeEncodeError as exc:
|
|
201
|
+
raise BootstrapError("source runtime contains a non-UTF-8 path") from exc
|
|
202
|
+
payload = item.read_bytes()
|
|
203
|
+
header = f"blob {len(payload)}\0".encode()
|
|
204
|
+
object_id = hashlib.sha1(
|
|
205
|
+
header + payload,
|
|
206
|
+
usedforsecurity=False,
|
|
207
|
+
).hexdigest().encode()
|
|
208
|
+
mode = b"100755" if item.stat().st_mode & 0o111 else b"100644"
|
|
209
|
+
digest.update(mode + b"\0" + object_id + b"\0" + path_bytes + b"\0")
|
|
210
|
+
entries += 1
|
|
211
|
+
if entries < 1:
|
|
212
|
+
raise BootstrapError("source runtime directory is empty")
|
|
213
|
+
return f"sha256:{digest.hexdigest()}"
|
|
214
|
+
|
|
215
|
+
|
|
216
|
+
def _download_runtime_archive(source: str, destination: Path) -> None:
|
|
217
|
+
if not source.startswith("https://"):
|
|
218
|
+
raise BootstrapError("runtime archive URL must use HTTPS")
|
|
219
|
+
request = urllib.request.Request(
|
|
220
|
+
source,
|
|
221
|
+
headers={"User-Agent": f"nethackers/{__version__}"},
|
|
222
|
+
)
|
|
223
|
+
try:
|
|
224
|
+
response = urllib.request.urlopen(request, timeout=60)
|
|
225
|
+
except (OSError, urllib.error.URLError) as exc:
|
|
226
|
+
raise BootstrapError(f"could not download the NetHackers runtime: {exc}") from exc
|
|
227
|
+
with response:
|
|
228
|
+
final_url = response.geturl()
|
|
229
|
+
if not final_url.startswith("https://"):
|
|
230
|
+
raise BootstrapError("runtime archive redirected away from HTTPS")
|
|
231
|
+
content_length = response.headers.get("Content-Length")
|
|
232
|
+
if content_length is not None:
|
|
233
|
+
try:
|
|
234
|
+
declared_size = int(content_length)
|
|
235
|
+
except ValueError as exc:
|
|
236
|
+
raise BootstrapError("runtime archive has an invalid Content-Length") from exc
|
|
237
|
+
if not 0 < declared_size <= _MAX_RUNTIME_ARCHIVE_BYTES:
|
|
238
|
+
raise BootstrapError("runtime archive exceeds the download limit")
|
|
239
|
+
downloaded = 0
|
|
240
|
+
with destination.open("xb") as output:
|
|
241
|
+
while chunk := response.read(1024 * 1024):
|
|
242
|
+
downloaded += len(chunk)
|
|
243
|
+
if downloaded > _MAX_RUNTIME_ARCHIVE_BYTES:
|
|
244
|
+
raise BootstrapError("runtime archive exceeds the download limit")
|
|
245
|
+
output.write(chunk)
|
|
246
|
+
if downloaded == 0:
|
|
247
|
+
raise BootstrapError("runtime archive is empty")
|
|
248
|
+
|
|
249
|
+
|
|
250
|
+
def _archive_relative_path(name: str) -> Path | None:
|
|
251
|
+
if "\\" in name or "\0" in name or ":" in name:
|
|
252
|
+
raise BootstrapError("runtime archive contains an unsafe path")
|
|
253
|
+
pure = PurePosixPath(name)
|
|
254
|
+
if pure.is_absolute() or ".." in pure.parts:
|
|
255
|
+
raise BootstrapError("runtime archive contains an unsafe path")
|
|
256
|
+
parts = tuple(part for part in pure.parts if part not in {"", "."})
|
|
257
|
+
return Path(*parts) if parts else None
|
|
258
|
+
|
|
259
|
+
|
|
260
|
+
def _extract_runtime_archive(archive: Path, destination: Path) -> None:
|
|
261
|
+
try:
|
|
262
|
+
destination.mkdir(mode=0o700)
|
|
263
|
+
with tarfile.open(archive, mode="r|gz") as bundle:
|
|
264
|
+
member_count = 0
|
|
265
|
+
unpacked_size = 0
|
|
266
|
+
normalized: set[Path] = set()
|
|
267
|
+
for member in bundle:
|
|
268
|
+
member_count += 1
|
|
269
|
+
if member_count > _MAX_RUNTIME_MEMBERS:
|
|
270
|
+
raise BootstrapError("runtime archive contains too many entries")
|
|
271
|
+
relative = _archive_relative_path(member.name)
|
|
272
|
+
if relative is None:
|
|
273
|
+
if not member.isdir():
|
|
274
|
+
raise BootstrapError("runtime archive contains an invalid root entry")
|
|
275
|
+
continue
|
|
276
|
+
if relative in normalized:
|
|
277
|
+
raise BootstrapError("runtime archive contains duplicate paths")
|
|
278
|
+
normalized.add(relative)
|
|
279
|
+
target = destination / relative
|
|
280
|
+
if member.isdir():
|
|
281
|
+
target.mkdir(parents=True, exist_ok=True, mode=0o755)
|
|
282
|
+
target.chmod(0o755)
|
|
283
|
+
continue
|
|
284
|
+
if not member.isfile() or (member.mode & 0o777) not in {0o644, 0o755}:
|
|
285
|
+
raise BootstrapError("runtime archive contains an unsupported entry")
|
|
286
|
+
unpacked_size += member.size
|
|
287
|
+
if unpacked_size > _MAX_RUNTIME_UNPACKED_BYTES:
|
|
288
|
+
raise BootstrapError("runtime archive exceeds the unpacked size limit")
|
|
289
|
+
target.parent.mkdir(parents=True, exist_ok=True, mode=0o755)
|
|
290
|
+
source = bundle.extractfile(member)
|
|
291
|
+
if source is None:
|
|
292
|
+
raise BootstrapError("runtime archive file cannot be read")
|
|
293
|
+
with source, target.open("xb") as output:
|
|
294
|
+
shutil.copyfileobj(source, output)
|
|
295
|
+
target.chmod(member.mode & 0o777)
|
|
296
|
+
except BootstrapError:
|
|
297
|
+
raise
|
|
298
|
+
except (OSError, tarfile.TarError) as exc:
|
|
299
|
+
raise BootstrapError(f"runtime archive is invalid: {exc}") from exc
|
|
300
|
+
|
|
301
|
+
|
|
302
|
+
def _verify_runtime(path: Path) -> None:
|
|
303
|
+
if not _DIGEST_RE.fullmatch(SOURCE_RUNTIME_DIGEST):
|
|
304
|
+
raise BootstrapError("this NetHackers wheel has no immutable source-runtime pin")
|
|
305
|
+
actual = (
|
|
306
|
+
runtime_digest(path)
|
|
307
|
+
if (path / ".git").exists()
|
|
308
|
+
else runtime_directory_digest(path)
|
|
309
|
+
)
|
|
310
|
+
if actual != SOURCE_RUNTIME_DIGEST:
|
|
311
|
+
raise BootstrapError(
|
|
312
|
+
f"source runtime digest is {actual}, expected {SOURCE_RUNTIME_DIGEST}"
|
|
313
|
+
)
|
|
314
|
+
runtime_pin = path / _RUNTIME_PIN_PATH
|
|
315
|
+
installed_pin = Path(__file__).with_name("_runtime_pin.py").read_bytes()
|
|
316
|
+
if not runtime_pin.is_file() or runtime_pin.read_bytes() != installed_pin:
|
|
317
|
+
raise BootstrapError("source runtime pin does not match the installed wheel")
|
|
318
|
+
|
|
319
|
+
|
|
320
|
+
def prepare_runtime(*, console: Console | None = None) -> Path:
|
|
321
|
+
override = os.environ.get("NETHACKERS_RUNTIME_ROOT")
|
|
322
|
+
if override:
|
|
323
|
+
runtime = Path(override).expanduser().resolve()
|
|
324
|
+
if not _is_runtime_checkout(runtime):
|
|
325
|
+
raise BootstrapError(f"NETHACKERS_RUNTIME_ROOT is not a NetHackers checkout: {runtime}")
|
|
326
|
+
return runtime
|
|
327
|
+
|
|
328
|
+
source_checkout = _source_checkout()
|
|
329
|
+
if source_checkout is not None:
|
|
330
|
+
return source_checkout
|
|
331
|
+
|
|
332
|
+
runtime = _runtime_home() / "runtime" / __version__
|
|
333
|
+
if _is_runtime_checkout(runtime):
|
|
334
|
+
try:
|
|
335
|
+
_verify_runtime(runtime)
|
|
336
|
+
except BootstrapError:
|
|
337
|
+
shutil.rmtree(runtime)
|
|
338
|
+
else:
|
|
339
|
+
return runtime
|
|
340
|
+
if runtime.exists():
|
|
341
|
+
shutil.rmtree(runtime)
|
|
342
|
+
|
|
343
|
+
source = os.environ.get("NETHACKERS_SOURCE_URL")
|
|
344
|
+
archive_source = os.environ.get("NETHACKERS_RUNTIME_URL", RUNTIME_ARCHIVE_URL)
|
|
345
|
+
revision = os.environ.get("NETHACKERS_SOURCE_REVISION", SOURCE_REVISION)
|
|
346
|
+
runtime.parent.mkdir(parents=True, exist_ok=True)
|
|
347
|
+
temporary = Path(tempfile.mkdtemp(prefix=f"{__version__}-", dir=runtime.parent))
|
|
348
|
+
shutil.rmtree(temporary)
|
|
349
|
+
status = (console or Console()).status(
|
|
350
|
+
"[bold green]Preparing the compact AutoAscend evolution runtime..."
|
|
351
|
+
)
|
|
352
|
+
archive = temporary.with_name(f"{temporary.name}.tar.gz")
|
|
353
|
+
try:
|
|
354
|
+
with status:
|
|
355
|
+
if source is not None:
|
|
356
|
+
if shutil.which("git") is None:
|
|
357
|
+
raise BootstrapError("git is required for NETHACKERS_SOURCE_URL")
|
|
358
|
+
clone_command = _clone_command(source, revision, temporary)
|
|
359
|
+
try:
|
|
360
|
+
_run_checked(clone_command)
|
|
361
|
+
except BootstrapError:
|
|
362
|
+
if clone_command[0] != "gh":
|
|
363
|
+
raise
|
|
364
|
+
shutil.rmtree(temporary, ignore_errors=True)
|
|
365
|
+
_run_checked(
|
|
366
|
+
[
|
|
367
|
+
"git",
|
|
368
|
+
"clone",
|
|
369
|
+
"--branch",
|
|
370
|
+
revision,
|
|
371
|
+
"--depth",
|
|
372
|
+
"1",
|
|
373
|
+
"--filter=blob:none",
|
|
374
|
+
source,
|
|
375
|
+
str(temporary),
|
|
376
|
+
]
|
|
377
|
+
)
|
|
378
|
+
else:
|
|
379
|
+
_download_runtime_archive(archive_source, archive)
|
|
380
|
+
_extract_runtime_archive(archive, temporary)
|
|
381
|
+
if not temporary.exists():
|
|
382
|
+
raise BootstrapError("runtime preparation produced no checkout")
|
|
383
|
+
_verify_runtime(temporary)
|
|
384
|
+
if runtime.exists():
|
|
385
|
+
shutil.rmtree(temporary)
|
|
386
|
+
else:
|
|
387
|
+
temporary.replace(runtime)
|
|
388
|
+
except Exception:
|
|
389
|
+
shutil.rmtree(temporary, ignore_errors=True)
|
|
390
|
+
raise
|
|
391
|
+
finally:
|
|
392
|
+
archive.unlink(missing_ok=True)
|
|
393
|
+
if not _is_runtime_checkout(runtime):
|
|
394
|
+
raise BootstrapError("the downloaded NetHackers runtime is incomplete")
|
|
395
|
+
return runtime
|
|
396
|
+
|
|
397
|
+
|
|
398
|
+
def _run_without_runtime(arguments: list[str]) -> bool:
|
|
399
|
+
if not arguments:
|
|
400
|
+
return False
|
|
401
|
+
if any(argument in {"-h", "--help"} for argument in arguments):
|
|
402
|
+
return True
|
|
403
|
+
if arguments[0] == "session":
|
|
404
|
+
return len(arguments) > 1 and arguments[1] != "start"
|
|
405
|
+
return arguments[0] in {
|
|
406
|
+
"-h",
|
|
407
|
+
"--help",
|
|
408
|
+
"--version",
|
|
409
|
+
"logs",
|
|
410
|
+
"login",
|
|
411
|
+
"logout",
|
|
412
|
+
"search",
|
|
413
|
+
"status",
|
|
414
|
+
"stop",
|
|
415
|
+
"whoami",
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
|
|
419
|
+
def _normalize_arguments(arguments: list[str]) -> list[str]:
|
|
420
|
+
if not arguments:
|
|
421
|
+
return ["session", "start"]
|
|
422
|
+
if arguments[0] in _COMMANDS or arguments[0] in {"-h", "--help", "--version"}:
|
|
423
|
+
return arguments
|
|
424
|
+
return ["session", "start", *arguments]
|
|
425
|
+
|
|
426
|
+
|
|
427
|
+
def _signal_runtime(process: subprocess.Popen[bytes], signal_number: int) -> None:
|
|
428
|
+
try:
|
|
429
|
+
if os.name == "posix":
|
|
430
|
+
os.killpg(process.pid, signal_number)
|
|
431
|
+
else:
|
|
432
|
+
process.send_signal(signal_number)
|
|
433
|
+
except ProcessLookupError:
|
|
434
|
+
pass
|
|
435
|
+
|
|
436
|
+
|
|
437
|
+
def _wait_for_runtime(process: subprocess.Popen[bytes], console: Console) -> int:
|
|
438
|
+
try:
|
|
439
|
+
returncode = process.wait()
|
|
440
|
+
except KeyboardInterrupt:
|
|
441
|
+
console.print(
|
|
442
|
+
"[yellow][STOP][/] Finishing and publishing the best checkpoint; "
|
|
443
|
+
"press Ctrl+C again to force exit."
|
|
444
|
+
)
|
|
445
|
+
_signal_runtime(process, signal.SIGINT)
|
|
446
|
+
try:
|
|
447
|
+
returncode = process.wait()
|
|
448
|
+
except KeyboardInterrupt:
|
|
449
|
+
console.print("[red][STOP][/] Forcing the NetHackers runtime to exit.")
|
|
450
|
+
_signal_runtime(process, signal.SIGTERM)
|
|
451
|
+
try:
|
|
452
|
+
returncode = process.wait(timeout=10)
|
|
453
|
+
except subprocess.TimeoutExpired:
|
|
454
|
+
_signal_runtime(process, signal.SIGKILL)
|
|
455
|
+
returncode = process.wait()
|
|
456
|
+
return 130
|
|
457
|
+
return 130 if returncode < 0 else returncode
|
|
458
|
+
|
|
459
|
+
|
|
460
|
+
def main(argv: list[str] | None = None) -> int:
|
|
461
|
+
if argv is None and "NETHACKERS_LAUNCHER" not in os.environ:
|
|
462
|
+
resolved_launcher = shutil.which(sys.argv[0])
|
|
463
|
+
if resolved_launcher is not None:
|
|
464
|
+
os.environ["NETHACKERS_LAUNCHER"] = str(
|
|
465
|
+
Path(resolved_launcher).expanduser().resolve()
|
|
466
|
+
)
|
|
467
|
+
arguments = _normalize_arguments(list(sys.argv[1:] if argv is None else argv))
|
|
468
|
+
invocation_directory = Path.cwd().resolve()
|
|
469
|
+
if _run_without_runtime(arguments):
|
|
470
|
+
from nethackers.cli import main as cli_main
|
|
471
|
+
|
|
472
|
+
return cli_main(arguments)
|
|
473
|
+
|
|
474
|
+
source_checkout = _source_checkout()
|
|
475
|
+
console = Console()
|
|
476
|
+
try:
|
|
477
|
+
runtime = prepare_runtime(console=console)
|
|
478
|
+
except BootstrapError as exc:
|
|
479
|
+
console.print(Panel(str(exc), title="[bold red]NETHACKERS SETUP FAILED[/]"))
|
|
480
|
+
return 2
|
|
481
|
+
|
|
482
|
+
if source_checkout is not None and runtime == source_checkout:
|
|
483
|
+
from nethackers.cli import main as cli_main
|
|
484
|
+
|
|
485
|
+
return cli_main(arguments)
|
|
486
|
+
|
|
487
|
+
environment = dict(os.environ)
|
|
488
|
+
environment["NETHACKERS_RUNTIME_ROOT"] = str(runtime)
|
|
489
|
+
environment["NETHACKERS_STATE_DIR"] = str(_state_home())
|
|
490
|
+
environment.setdefault("NETHACKERS_WORKSPACE", str(invocation_directory))
|
|
491
|
+
try:
|
|
492
|
+
process = subprocess.Popen(
|
|
493
|
+
[sys.executable, "-I", "-m", "nethackers.cli", *arguments],
|
|
494
|
+
cwd=invocation_directory,
|
|
495
|
+
env=environment,
|
|
496
|
+
start_new_session=os.name == "posix",
|
|
497
|
+
)
|
|
498
|
+
except OSError as exc:
|
|
499
|
+
console.print(
|
|
500
|
+
Panel(str(exc), title="[bold red]NETHACKERS START FAILED[/]")
|
|
501
|
+
)
|
|
502
|
+
return 2
|
|
503
|
+
return _wait_for_runtime(process, console)
|
|
504
|
+
|
|
505
|
+
|
|
506
|
+
if __name__ == "__main__":
|
|
507
|
+
raise SystemExit(main())
|