Gododin Gododin Documentation

Linked repository source

examples/cavern_lab/live/project/bake/bake_shell.gd

This read-only view comes from the file in the Gododin repository. It retains its design, example or evidence context.

Download the original file · Return to the guide

@tool
extends RefCounted
# The Phase 14 bake shell (bake/README.md), scripted equivalent for evidence
# runs and the reusable core of the editor panel. It saves the request,
# computes the digest, and supervises one bounded headless child. It never
# compiles, links, or stages a native artifact itself. One outstanding bake
# per request resource: launching a new bake supersedes the outstanding child,
# and cancel terminates the child without touching the destination. The shell
# verifies publication by loading the destination and comparing its recorded
# request digest.
const DIGEST := preload("res://bake/bake_digest.gd")

var godot_binary: String
var timeout_seconds := 180.0
var last_error := ""
# Set when cancel() terminated the outstanding child, so a poller can name the
# disposition honestly instead of reporting a deliberate cancel as a failure.
var cancelled := false

var _pid := 0
var _exit_code := -1
var _finalized := ""
var _output_path := ""
var _digest := ""


func _init() -> void:
	godot_binary = OS.get_executable_path()


func digest_for(request) -> String:
	var mask_image: Image = request.mask
	if mask_image == null:
		return ""
	return DIGEST.digest(
		mask_image.get_data(), mask_image.get_width(), mask_image.get_height(),
		int(request.smoothing_steps), int(request.minimum_region_size))


func save_request(request, request_path: String) -> String:
	var digest := digest_for(request)
	if digest.is_empty():
		last_error = "request has no digestable mask"
		return ""
	# ResourceSaver does not create parent directories, and a redirected HOME
	# starts without any user-space path.
	DirAccess.make_dir_recursive_absolute(ProjectSettings.globalize_path(request_path.get_base_dir()))
	var save_error := ResourceSaver.save(request, request_path)
	if save_error != OK:
		last_error = "request save error %d" % save_error
		return ""
	return digest


func outstanding() -> bool:
	return _pid != 0 and OS.is_process_running(_pid)


func launch(request, request_path: String, output_path: String) -> bool:
	_supersede_outstanding()
	# The shell saves the request immediately before spawning, so the child
	# always digests exactly the bytes this launch supervises.
	var digest := save_request(request, request_path)
	if digest.is_empty():
		return false
	var arguments := PackedStringArray([
		"--headless",
		"--path", ProjectSettings.globalize_path("res://"),
		"--script", "res://bake/bake_process.gd",
		"--", request_path, output_path, digest,
	])
	_pid = OS.create_process(godot_binary, arguments)
	if _pid == 0:
		last_error = "child spawn failed"
		return false
	_output_path = output_path
	_digest = digest
	cancelled = false
	_exit_code = -1
	_finalized = ""
	last_error = ""
	return true


func cancel() -> bool:
	if not outstanding():
		return false
	# The child may have exited since the last poll; killing an exited
	# process makes the engine print a child-tracking error.
	if OS.is_process_running(_pid):
		OS.kill(_pid)
		_wait_exit(2.0)
	# Capture the terminated child's exit code before dropping the handle:
	# poll() must verify against a nonzero code, never against a stale one.
	_exit_code = OS.get_process_exit_code(_pid)
	_pid = 0
	cancelled = true
	return true


# Non-blocking completion check for interactive callers: empty while the child
# runs, then the disposition ("published"/"failed") exactly once. Verification
# runs on the first completion and the verdict is cached, so the frame loop can
# keep rendering while the supervised bake proceeds.
func poll() -> String:
	if not _finalized.is_empty():
		return _finalized
	if outstanding():
		return ""
	if _pid != 0:
		_exit_code = OS.get_process_exit_code(_pid)
		_pid = 0
	_finalized = _verify_publication(_exit_code)
	return _finalized


func wait_until_finished(deadline_seconds := -1.0) -> String:
	if deadline_seconds < 0.0:
		deadline_seconds = timeout_seconds
	var deadline := Time.get_ticks_usec() + int(deadline_seconds * 1_000_000.0)
	while outstanding():
		if Time.get_ticks_usec() > deadline:
			last_error = "child timed out"
			OS.kill(_pid)
			_wait_exit(2.0)
			_exit_code = OS.get_process_exit_code(_pid)
			_pid = 0
			return _verify_publication(_exit_code)
		OS.delay_msec(10)
	if _pid != 0:
		_exit_code = OS.get_process_exit_code(_pid)
		_pid = 0
	return _verify_publication(_exit_code)


func _supersede_outstanding() -> void:
	if _pid == 0:
		return
	if OS.is_process_running(_pid):
		OS.kill(_pid)
		_wait_exit(2.0)
	_pid = 0


func _wait_exit(grace_seconds: float) -> void:
	var deadline := Time.get_ticks_usec() + int(grace_seconds * 1_000_000.0)
	while OS.is_process_running(_pid) and Time.get_ticks_usec() < deadline:
		OS.delay_msec(5)


func _verify_publication(exit_code: int) -> String:
	if exit_code != 0:
		last_error = "child exit code %d" % exit_code
		return "failed"
	if not FileAccess.file_exists(_output_path):
		last_error = "destination was not published"
		return "failed"
	var data = load(_output_path)
	if data == null:
		last_error = "destination is not a loadable resource"
		return "failed"
	if String(data.request_digest) != _digest:
		last_error = "destination digest differs from the request"
		return "failed"
	return "published"