はじめに
以前、Godot 4.6 で作っている RPG(kibi-kingdom)の動作確認を、AI にヘッドレス起動のテストスクリプトで回させていた。その途中で Mac のメモリが逼迫し、全プロセスを強制終了して再起動することになった。
原因は、テストを流すたびに起動した Godot が終わらずに居座っていたことだった。テストスクリプトは最後まで走り、結果も出力していた。止まっていなかったのは Godot のほうだった。
手元の Godot 4.6.3 で、同じ形を最小構成で再現した。
quit() の有無だけを変える
SceneTree を継承したスクリプトを --script で渡すと、シーンなしで GDScript を実行できる。違いは最後の quit() だけ。
# noquit.gd
extends SceneTree
func _init():
print("test done")
# quit.gd
extends SceneTree
func _init():
print("test done")
quit()
空のプロジェクト(project.godot だけ)を置いたディレクトリで、それぞれ起動した。
G=/path/to/Godot.app/Contents/MacOS/Godot
$G --headless --path . --script quit.gd
# Godot Engine v4.6.3.stable.official.7d41c59c4 - https://godotengine.org
#
# test done
echo $? # 0
quit.gd はすぐ終わる。noquit.gd は test done を出したあと、プロンプトに戻ってこない。
3回流すと3つ残る
テストを何度か流す状況に近づけて、noquit.gd をバックグラウンドで3回起動し、30秒後に見た。
for i in 1 2 3; do $G --headless --path . --script noquit.gd > n$i.log 2>&1 & done
sleep 30
ps -o pid=,etime=,rss=,command= -p $(pgrep -d, -f "Godot --headless")
94755 00:30 113744 .../Godot --headless --path . --script noquit.gd
94756 00:30 114048 .../Godot --headless --path . --script noquit.gd
94757 00:30 114224 .../Godot --headless --path . --script noquit.gd
3つとも生きていて、空のプロジェクトでも1つあたり約111MB。ログには test done が出ている。出力だけを見れば、テストは成功して終わったように見える。
ヘッドレスでも、Godot はメインループを回し続ける。_init() が終わっても、quit() を呼ぶまでエンジンは止まらない。今回は空のプロジェクトで測ったので、アセットを読み込む実プロジェクトの1つあたりの量は測っていない。テストを直しては流すのを繰り返すと、そのたびに1つずつ増えていく。
timeout で止めようとすると
外から時間を切ろうとして timeout を使うと、macOS の zsh には入っていない。
timeout 3 sleep 1
# zsh:1: command not found: timeout
echo $? # 127
timeout が無いと Godot は起動されない。ただ、エラーを無視して先に進む書き方だと、テストが走っていないことに気づかないまま進んでしまう。Homebrew の coreutils を入れれば gtimeout が使えるが、手元には入っていなかった。
macOS に標準で入っている perl の alarm で代わりになった。
perl -e 'alarm shift; exec @ARGV' 5 $G --headless --path . --script noquit.gd
echo $? # 142(SIGALRM で止まった・5秒)
止め方は3つ
-
スクリプトの最後で
quit()を呼ぶ。 失敗時にquit(1)を呼べば、終了コードが1になった(echo $?で確認)。合否を終了コードで取れる。 -
起動オプションで終わらせる。
--quitは1フレーム目のあとで終わる。--quit-after <フレーム数>で長さも指定できる。
$G --headless --path . --quit --script noquit.gd; echo $?
# 0(すぐ終わった)
$G --headless --path . --quit-after 60 --script noquit.gd; echo $?
# 0
-
後始末を必ず入れる。 途中で例外が出て
quit()まで届かないこともあるので、流し終わったら残りを確かめて消す。
pkill -f "Godot --headless"
pgrep -fl Godot || echo none # none
PID を覚えておいて kill するより、pkill -f でまとめて消すほうが取りこぼしがなかった。エディタで開いている Godot まで消さないように、--headless を条件に入れている。
テストの出力には "test done" と出ていた。終わったのはテストで、Godot ではなかった。