GodotPythonJSONRPC/addons/KiriPythonRPCWrapper/KiriPythonWrapperInstance.gd
2024-07-14 12:58:20 -07:00

253 lines
7.3 KiB
GDScript

extends RefCounted
class_name KiriPythonWrapperInstance
enum KiriPythonWrapperStatus {
STATUS_RUNNING,
STATUS_STOPPED
}
class KiriPythonWrapperActiveRequest:
enum KiriPythonWrapperActiveRequestState {
STATE_WAITING_TO_SEND,
STATE_SENT,
STATE_RESPONSE_RECEIVED
}
var id : int
var method_name : String
var arguments : Variant # Dictionary or Array
var callback # Callable or null
var state : KiriPythonWrapperActiveRequestState
var response # Return value from the call
var error_response = ""
var _active_request_queue = {}
var _request_counter = 0
var _server_packet_socket : KiriPacketSocket = null
var communication_packet_socket : KiriPacketSocket = null
var python_script_path : String = ""
var _build_wrangler : KiriPythonBuildWrangler = null
var _external_process_pid = -1
func _init(python_file_path : String):
_build_wrangler = KiriPythonBuildWrangler.new()
python_script_path = python_file_path
func _get_python_executable():
return _build_wrangler.get_runtime_python_executable_system_path()
func _get_wrapper_script():
# FIXME: Paths will be different for builds.
var script_path = self.get_script().get_path()
var script_dirname = script_path.get_base_dir()
return ProjectSettings.globalize_path( \
script_dirname + "/KiriPythonRPCWrapper_start.py")
func _get_wrapper_cache_path() -> String:
return _build_wrangler._get_cache_path_godot().path_join("KiriPythonRPCWrapper")
func _get_wrapper_script_cache_path() -> String:
return _get_wrapper_cache_path().path_join("addons/KiriPythonRPCWrapper/KiriPythonRPCWrapper/__init__.py")
func setup_python():
# Unpack base Python build.
_build_wrangler.unpack_python(false)
# Unpack Python wrapper.
var extra_scripts = _build_wrangler.get_extra_scripts_list()
print(extra_scripts)
for extra_script : String in extra_scripts:
# Chop off the "res://".
var extra_script_relative : String = extra_script.substr(len("res://"))
# Some other path wrangling.
var extraction_path : String = _get_wrapper_cache_path().path_join(extra_script_relative)
var extraction_path_dir : String = extraction_path.get_base_dir()
# Make the dir.
DirAccess.make_dir_recursive_absolute(extraction_path_dir)
# Extract the file.
var bytes : PackedByteArray = FileAccess.get_file_as_bytes(extra_script)
FileAccess.open(extraction_path, FileAccess.WRITE).store_buffer(bytes)
func get_status():
if _external_process_pid == -1:
return KiriPythonWrapperStatus.STATUS_STOPPED
if not OS.is_process_running(_external_process_pid):
return KiriPythonWrapperStatus.STATUS_STOPPED
return KiriPythonWrapperStatus.STATUS_RUNNING
func start_process():
# FIXME: Make sure we don't have one running.
var open_port = 9500
assert(not _server_packet_socket)
_server_packet_socket = KiriPacketSocket.new()
while true:
_server_packet_socket.start_server(["127.0.0.1", open_port])
# Wait for the server to start.
while _server_packet_socket.get_state() == KiriPacketSocket.KiriSocketState.SERVER_STARTING:
OS.delay_usec(1)
# If we're successfully listening, then we found a port to use and we
# don't need to loop anymore.
if _server_packet_socket.get_state() == KiriPacketSocket.KiriSocketState.SERVER_LISTENING:
break
# This port is busy. Try the next one.
_server_packet_socket.stop()
open_port += 1
print("Port: ", open_port)
var python_exe_path : String = _get_python_executable()
var wrapper_script_path : String = \
ProjectSettings.globalize_path(_get_wrapper_script_cache_path())
var startup_command : Array = [
"xterm", "-e",
python_exe_path,
wrapper_script_path,
"--script", python_script_path,
"--port", open_port,
"--parent_pid", OS.get_process_id()]
print("startup command: ", startup_command)
_external_process_pid = OS.create_process(
startup_command[0], startup_command.slice(1), true)
print("external process: ", _external_process_pid)
func stop_process():
if _external_process_pid != -1:
OS.kill(_external_process_pid)
_external_process_pid = -1
# Clean up server and communication sockets.
if _server_packet_socket:
_server_packet_socket.stop()
_server_packet_socket = null
if communication_packet_socket:
communication_packet_socket.stop()
communication_packet_socket = null
func call_rpc_async(method : String, args : Variant, callback = null) -> int:
assert((args is Dictionary) or (args is Array))
assert((callback == null) or (callback is Callable))
var new_request = KiriPythonWrapperActiveRequest.new()
new_request.id = _request_counter
_request_counter += 1
new_request.method_name = method
new_request.arguments = args
new_request.callback = callback
_active_request_queue[new_request.id] = new_request
return new_request.id
func call_rpc_sync(method : String, args : Variant):
# Kinda hacky. We're using arrays because we can change the contents.
# Without the array or something else mutable we'd just end up with the
# internal pointer pointing to different values without affecting these
# ones.
var done_array = [false]
var response_list = []
var request_id = call_rpc_async(method, args, func(request_ob):
done_array[0] = true
response_list.append(request_ob.response)
)
# Wait (block) until we get a response.
while not done_array[0]:
# Bail out if something happened to our instance or connection to it.
if communication_packet_socket:
if communication_packet_socket.is_disconnected_or_error():
break
poll()
OS.delay_usec(1)
if len(response_list):
return response_list[0]
return null
func poll():
# Hand-off between listening socket and actual communications socket.
if _server_packet_socket:
communication_packet_socket = _server_packet_socket.get_next_server_connection()
if communication_packet_socket:
_server_packet_socket.stop()
_server_packet_socket = null
if communication_packet_socket:
# Send all waiting requests
for request_id in _active_request_queue:
var request : KiriPythonWrapperActiveRequest = _active_request_queue[request_id]
if request.state == request.KiriPythonWrapperActiveRequestState.STATE_WAITING_TO_SEND:
var request_dict = {
"jsonrpc": "2.0",
"method": request.method_name,
"params": request.arguments,
"id": request.id
}
var request_dict_json = JSON.stringify(request_dict)
communication_packet_socket.send_packet(request_dict_json.to_utf8_buffer())
request.state = request.KiriPythonWrapperActiveRequestState.STATE_SENT
# Check for responses.
var packet = communication_packet_socket.get_next_packet()
while packet != null:
var packet_dict = JSON.parse_string(packet.get_string_from_utf8())
if packet_dict:
if packet_dict.has("id"):
var request_id = packet_dict["id"]
# floats aren't even allowed in JSON RPC as an id. Probably
# meant it to be an int.
if request_id is float:
request_id = int(request_id)
if _active_request_queue.has(request_id):
var request : KiriPythonWrapperActiveRequest = \
_active_request_queue[request_id]
if "result" in packet_dict:
request.response = packet_dict["result"]
else:
request.error_response = "Couldn't find result on packet."
if request.callback:
request.callback.call(request)
# Clean up request.
_active_request_queue.erase(request_id)
packet = communication_packet_socket.get_next_packet()