"""隔離ラボ(_lab2)の BDS をプレイヤー無しで動かし、tickingarea でチャンク読み込みを大量に発生させて クラッシュ(0xC0000409)が再現するか調べる。 使い方: python labrun.py [key=value ...] 例: python labrun.py mt2 10 max-threads=2 python labrun.py mt0 10 max-threads=0 key=value は server.properties を書き換える(実行前)。 結果: /lab_.log に全出力、最後に SUMMARY 行。 """ import os, random, re, subprocess, sys, threading, time from pathlib import Path LAB = Path(r"C:\Users\PC_User\Desktop\AOIROSERVER2\_lab2") tag = sys.argv[1] minutes = float(sys.argv[2]) overrides = dict(kv.split("=", 1) for kv in sys.argv[3:]) # server.properties 書き換え props = (LAB / "server.properties").read_text(encoding="utf-8").splitlines() seen = set() for i, line in enumerate(props): m = re.match(r"^([A-Za-z0-9_.-]+)=", line) if m and m.group(1) in overrides: props[i] = f"{m.group(1)}={overrides[m.group(1)]}" seen.add(m.group(1)) for k, v in overrides.items(): if k not in seen: props.append(f"{k}={v}") (LAB / "server.properties").write_text("\n".join(props) + "\n", encoding="utf-8") cur = {m.group(1): m.group(2) for m in (re.match(r"^([A-Za-z0-9_.-]+)=(.*)$", l) for l in props) if m} print("設定:", {k: cur.get(k) for k in ("view-distance", "tick-distance", "max-threads", "client-side-chunk-generation-enabled", "server-port")}) log = open(LAB / f"lab_{tag}.log", "w", encoding="utf-8", errors="replace") random.seed(int(time.time())) p = subprocess.Popen([str(LAB / "bedrock_server.exe")], cwd=str(LAB), stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, bufsize=0) started = threading.Event() t0 = time.time() nlines = [0] def reader(): for raw in iter(p.stdout.readline, b""): s = raw.decode("utf-8", "replace").rstrip() nlines[0] += 1 log.write(f"[{time.time()-t0:7.1f}] {s}\n") if "Server started." in s: started.set() if re.search(r"ERROR|Assert|crash|Exception", s, re.I) and "Scripting" not in s: print(f" ! {s[:160]}") log.flush() threading.Thread(target=reader, daemon=True).start() def send(cmd): try: p.stdin.write((cmd + "\n").encode()); p.stdin.flush() log.write(f"[{time.time()-t0:7.1f}] >>> {cmd}\n") except Exception: pass if not started.wait(180): print("起動待ちタイムアウト"); send("stop"); p.wait(30); sys.exit(2) print(f"起動 {time.time()-t0:.0f} 秒 / チャンク読み込みループ開始 ({minutes} 分)") # 生成済み範囲(チャンク x -224..319, z -4096..450) の中からランダムに tickingarea を張る deadline = t0 + minutes * 60 rounds = 0 DWELL = float(overrides.get("lab.dwell", "7")) # 張ってから外すまでの秒数 RECT = overrides.get("lab.rect", "0") == "1" # 1 なら 10x10 チャンクの矩形(100チャンク)で張る FILL = int(overrides.get("lab.fill", "0")) # 1周あたりの fill コマンド数(0で無し) while time.time() < deadline and p.poll() is None: rounds += 1 spots = [] for i in range(10): cx = random.randint(-220, 315); cz = random.randint(-4090, 445) # 半分は活動域(プレイヤーの居た辺り)に寄せる if i % 2 == 0: cx = random.randint(40, 100); cz = random.randint(20, 60) if RECT: send(f"tickingarea add {cx*16} 64 {cz*16} {cx*16+159} 64 {cz*16+159} lab{i}") else: send(f"tickingarea add circle {cx*16} 64 {cz*16} 4 lab{i}") time.sleep(0.2) spots.append((cx * 16, cz * 16)) if FILL: # 張った範囲の中で 32x32x32 の fill を繰り返し、メインスレッドのブロック更新と # ワーカーの再照明(Chunk Relight)を同時に走らせる time.sleep(1.0) for k in range(FILL): bx, bz = random.choice(spots) bx += random.randint(0, 96); bz += random.randint(0, 96) by = random.randint(40, 90) blk = random.choice(("stone", "air", "glass", "sea_lantern")) send(f"fill {bx} {by} {bz} {bx+31} {by+31} {bz+31} {blk}") time.sleep(0.15) time.sleep(DWELL) send("tickingarea remove_all") time.sleep(1) if rounds % 5 == 0: print(f" {rounds} 周 / {time.time()-t0:.0f} 秒 / 出力 {nlines[0]} 行 / 生存中") alive = p.poll() is None if alive: send("stop") try: p.wait(60) except Exception: p.kill() rc = p.returncode elapsed = time.time() - t0 verdict = "生存(正常停止)" if alive else f"死亡 exit={rc} (0x{rc & 0xffffffff:08X})" line = f"SUMMARY tag={tag} rounds={rounds} elapsed={elapsed:.0f}s result={verdict}" print(line); log.write(line + "\n"); log.close()