class_name ServerProcess ## Manages the Rust simulation server as a subprocess (D-020). var _pid: int = -1 ## Populated only by start_with_pipe() — the stdout FileAccess handle used to ## read the LISTENING:{port} signal (D-254 §1/§3). Null after plain start(). var _stdio: FileAccess = null ## Start the server process. Returns the PID, or -1 on failure. ## Validates that server_path exists and is accessible before spawning. func start(server_path: String, args: Array = []) -> int: if _pid > 0 and is_alive(): push_warning("ServerProcess: server already running (pid %d)" % _pid) return _pid if not FileAccess.file_exists(server_path): push_error("ServerProcess: server binary not found at %s" % server_path) return -1 _pid = OS.create_process(server_path, args) if _pid <= 0: push_error("ServerProcess: failed to start server at %s" % server_path) return _pid ## Start the server process with its stdout piped back to this process ## (D-254 §1/§3: the companion app's --port 0 spawn-mode, which needs to read ## the LISTENING:{port} signal main.rs prints — see server/src/main.rs). Unlike ## start()/OS.create_process, this uses OS.execute_with_pipe, which does not ## block (the shell precedent, tests/run-visual, achieves the same read via a ## redirected log file + `grep` from a wrapper script; this is the Godot-native ## equivalent for a caller with no shell wrapper). Returns the PID, or -1 on ## failure. Call read_stdout_line() from a per-frame poll to drain the pipe. func start_with_pipe(server_path: String, args: Array = []) -> int: if _pid > 0 and is_alive(): push_warning("ServerProcess: server already running (pid %d)" % _pid) return _pid if not FileAccess.file_exists(server_path): push_error("ServerProcess: server binary not found at %s" % server_path) return -1 var result: Dictionary = OS.execute_with_pipe(server_path, args) if not result.has("pid") or int(result.get("pid", -1)) <= 0: push_error("ServerProcess: failed to start server (pipe) at %s" % server_path) return -1 _pid = int(result["pid"]) _stdio = result.get("stdio", null) return _pid ## Non-blocking read of one line from the piped server's stdout, or "" if no ## complete line is buffered yet (FileAccess.get_line() on a pipe never ## blocks — it returns immediately with whatever is currently available). ## Only meaningful after start_with_pipe(); returns "" if _stdio was never set ## (plain start()) or the process has exited (stream at EOF). func read_stdout_line() -> String: if _stdio == null: return "" if _stdio.get_error() != OK: return "" return _stdio.get_line() ## Check if the server process is still running. ## NOTE: OS.is_process_running() doesn't distinguish starting/healthy/zombie. ## TODO: Add health check (e.g. TCP readiness probe) when server supports it. func is_alive() -> bool: if _pid <= 0: return false return OS.is_process_running(_pid) ## Stop the server process. ## TODO: Send graceful shutdown signal before SIGKILL when server supports it. func stop() -> void: if _pid > 0 and is_alive(): OS.kill(_pid) _pid = -1 ## Get the server PID (-1 if not started). func get_pid() -> int: return _pid ## Clean up on destruction — kill the server if still running. func _notification(what: int) -> void: if what == NOTIFICATION_PREDELETE: stop()