874b6f20c2
Основа — Horizon (MIT). История апстрима не переносится: этот репозиторий ведёт свою.
120 lines
4.6 KiB
Python
120 lines
4.6 KiB
Python
#!/usr/bin/env python3
|
|
"""Run the vanilla 26.3 dedicated server headless on a world folder, optionally
|
|
send console commands (`sleep SECONDS` pauses between them; `login NAME
|
|
SECONDS` joins with tools/vanilla_login.py in the background), then stop it
|
|
cleanly and report problems from its log.
|
|
|
|
vanilla_world_check.py --jar server.jar --dir RUN_DIR [--world NAME]
|
|
[--seed N] [--port P] [--command CMD ...]
|
|
[--wait-after SECONDS]
|
|
|
|
RUN_DIR receives eula.txt, server.properties and logs/. An existing
|
|
RUN_DIR/NAME world is opened as is (this is how Horizon saves are checked); a
|
|
missing one is generated by vanilla (this is how reference worlds are made).
|
|
Exit status 1 when the log contains errors, upgrade/datafix work or chunk
|
|
load failures.
|
|
"""
|
|
import argparse
|
|
import os
|
|
import re
|
|
import subprocess
|
|
import sys
|
|
import threading
|
|
import time
|
|
|
|
try:
|
|
from . import lab
|
|
except ImportError:
|
|
import lab
|
|
|
|
SUSPICIOUS = re.compile(
|
|
r'(ERROR|login failed|Exception|Couldn\'t load|Failed to|Skipping|Invalid|Unknown|corrupt|'
|
|
r'Recreating|upgrad|DataFix|out of bounds|missing|Mismatch)', re.IGNORECASE)
|
|
# Benign lines the server prints on every start.
|
|
BENIGN = re.compile(r'(Unpacking |OFFLINE/INSECURE|online-mode|authenticate usernames|hackers|'
|
|
r'Failed to load properties|Failed to load eula)')
|
|
|
|
|
|
def main():
|
|
p = argparse.ArgumentParser()
|
|
p.add_argument('--jar', required=True)
|
|
p.add_argument('--dir', required=True)
|
|
p.add_argument('--world', default='world')
|
|
p.add_argument('--seed', default='12345')
|
|
p.add_argument('--port', type=int, default=0, help='default: a free port')
|
|
p.add_argument('--command', action='append', default=[])
|
|
p.add_argument('--wait-after', type=float, default=1.0)
|
|
p.add_argument('--timeout', type=float, default=240)
|
|
args = p.parse_args()
|
|
port = args.port or lab.free_port()
|
|
os.makedirs(args.dir, exist_ok=True)
|
|
with open(os.path.join(args.dir, 'eula.txt'), 'w') as f:
|
|
f.write('eula=true\n')
|
|
props = {
|
|
'server-port': port, 'online-mode': 'false', 'level-seed': args.seed,
|
|
'level-name': args.world, 'spawn-protection': '0', 'view-distance': '4',
|
|
'simulation-distance': '4', 'sync-chunk-writes': 'true', 'enable-query': 'false',
|
|
'enable-rcon': 'false', 'management-server-enabled': 'false',
|
|
'pause-when-empty-seconds': '-1', 'white-list': 'false', 'enforce-whitelist': 'false',
|
|
}
|
|
with open(os.path.join(args.dir, 'server.properties'), 'w') as f:
|
|
f.writelines(f'{k}={v}\n' for k, v in props.items())
|
|
proc = subprocess.Popen(
|
|
['java', '-Xmx2G', '-jar', os.path.abspath(args.jar), '--nogui'], cwd=args.dir,
|
|
stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, text=True)
|
|
lines = []
|
|
done = threading.Event()
|
|
|
|
def pump():
|
|
for line in proc.stdout:
|
|
lines.append(line.rstrip('\n'))
|
|
if 'Done (' in line:
|
|
done.set()
|
|
done.set()
|
|
|
|
threading.Thread(target=pump, daemon=True).start()
|
|
if not done.wait(args.timeout) or proc.poll() is not None:
|
|
proc.kill()
|
|
print('\n'.join(lines))
|
|
print('server did not start', file=sys.stderr)
|
|
return 1
|
|
for command in args.command:
|
|
if command.startswith('sleep '):
|
|
time.sleep(float(command.split()[1]))
|
|
continue
|
|
if command.startswith('login '):
|
|
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
|
|
import vanilla_login
|
|
_, name, seconds = command.split()
|
|
|
|
def run(name=name, seconds=float(seconds)):
|
|
try:
|
|
vanilla_login.join('127.0.0.1', port, name, seconds)
|
|
except Exception as e: # reported in the log check below
|
|
lines.append(f'login failed: {e}')
|
|
|
|
threading.Thread(target=run, daemon=True).start()
|
|
continue
|
|
proc.stdin.write(command + '\n')
|
|
proc.stdin.flush()
|
|
time.sleep(0.5)
|
|
time.sleep(args.wait_after)
|
|
proc.stdin.write('stop\n')
|
|
proc.stdin.flush()
|
|
try:
|
|
proc.wait(args.timeout)
|
|
except subprocess.TimeoutExpired:
|
|
proc.kill()
|
|
time.sleep(0.2)
|
|
problems = [l for l in lines if SUSPICIOUS.search(l) and not BENIGN.search(l)]
|
|
with open(os.path.join(args.dir, 'check.log'), 'w') as f:
|
|
f.write('\n'.join(lines) + '\n')
|
|
for l in problems:
|
|
print(l)
|
|
print(f'{len(lines)} log lines, {len(problems)} suspicious')
|
|
return 1 if problems else 0
|
|
|
|
|
|
if __name__ == '__main__':
|
|
sys.exit(main())
|