GodotPythonJSONRPC/addons/KiriPythonRPCWrapper/KiriPythonWrapperInstance.gd

314 lines
9.2 KiB
GDScript3
Raw Permalink Normal View History

extends RefCounted
class_name KiriPythonWrapperInstance
2024-07-14 12:58:20 -07:00
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
2024-07-14 12:58:20 -07:00
var _server_packet_socket : KiriPacketSocket = null
var communication_packet_socket : KiriPacketSocket = null
var python_script_path : String = ""
2024-07-14 12:58:20 -07:00
var _build_wrangler : KiriPythonBuildWrangler = null
var _external_process_pid = -1
2024-07-14 18:40:49 -07:00
signal _rpc_async_response_received
func _init(python_file_path : String):
2024-07-14 12:58:20 -07:00
_build_wrangler = KiriPythonBuildWrangler.new()
python_script_path = python_file_path
func _get_python_executable():
2024-07-14 12:58:20 -07:00
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")
2024-07-14 12:58:20 -07:00
func _get_wrapper_cache_path() -> String:
2024-07-14 18:40:49 -07:00
return _build_wrangler._get_cache_path_godot().path_join("packaged_scripts")
2024-07-14 12:58:20 -07:00
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():
2024-07-14 12:58:20 -07:00
if _external_process_pid == -1:
return KiriPythonWrapperStatus.STATUS_STOPPED
2024-07-14 12:58:20 -07:00
if not OS.is_process_running(_external_process_pid):
return KiriPythonWrapperStatus.STATUS_STOPPED
return KiriPythonWrapperStatus.STATUS_RUNNING
2024-07-15 07:39:58 -07:00
func run_python_command(
args : PackedStringArray,
output : Array = [],
read_stderr : bool = false,
open_console : bool = false):
2024-07-15 07:39:58 -07:00
var python_exe_path : String = _get_python_executable()
2024-07-15 07:39:58 -07:00
# Do a little switcheroo on Linux to open a console.
if open_console:
if OS.get_name() == "Linux":
args = PackedStringArray(["-e", python_exe_path]) + args
python_exe_path = "xterm"
return OS.execute(python_exe_path, args, output, read_stderr, open_console)
func convert_cache_item_to_real_path(path : String):
var real_python_script_path = path
2024-07-14 18:40:49 -07:00
if real_python_script_path.begins_with("res://"):
real_python_script_path = _build_wrangler._get_script_cache_path_system().path_join(
real_python_script_path.substr(len("res://")))
else:
real_python_script_path = ProjectSettings.globalize_path(
real_python_script_path)
2024-07-15 07:39:58 -07:00
return real_python_script_path
func start_process(open_terminal : bool = false):
# FIXME: Make sure we don't have one running.
var open_port = 9500
var real_python_script_path = convert_cache_item_to_real_path(
python_script_path)
2024-07-14 18:40:49 -07:00
2024-07-14 12:58:20 -07:00
assert(not _server_packet_socket)
_server_packet_socket = KiriPacketSocket.new()
while true:
2024-07-14 12:58:20 -07:00
_server_packet_socket.start_server(["127.0.0.1", open_port])
# Wait for the server to start.
2024-07-14 12:58:20 -07:00
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.
2024-07-14 12:58:20 -07:00
if _server_packet_socket.get_state() == KiriPacketSocket.KiriSocketState.SERVER_LISTENING:
break
# This port is busy. Try the next one.
2024-07-14 12:58:20 -07:00
_server_packet_socket.stop()
open_port += 1
var python_exe_path : String = _get_python_executable()
2024-07-14 12:58:20 -07:00
var wrapper_script_path : String = \
ProjectSettings.globalize_path(_get_wrapper_script_cache_path())
var startup_command : Array = [
python_exe_path,
wrapper_script_path,
2024-07-14 18:40:49 -07:00
"--script", real_python_script_path,
2024-07-14 14:49:44 -07:00
"--port", open_port]
2024-07-14 18:40:49 -07:00
if open_terminal:
if OS.get_name() == "Linux":
startup_command = ["xterm", "-e"] + startup_command
#print("startup command: ", startup_command)
2024-07-14 12:58:20 -07:00
_external_process_pid = OS.create_process(
2024-07-14 18:40:49 -07:00
startup_command[0], startup_command.slice(1),
open_terminal)
2024-07-14 18:40:49 -07:00
#print("external process: ", _external_process_pid)
func stop_process():
2024-07-14 12:58:20 -07:00
if _external_process_pid != -1:
OS.kill(_external_process_pid)
_external_process_pid = -1
# Clean up server and communication sockets.
2024-07-14 12:58:20 -07:00
if _server_packet_socket:
_server_packet_socket.stop()
_server_packet_socket = null
if communication_packet_socket:
communication_packet_socket.stop()
communication_packet_socket = null
2024-07-14 18:40:49 -07:00
func call_rpc_callback(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
2024-07-14 18:40:49 -07:00
func call_rpc_async(method : String, args : Variant):
var request_id = call_rpc_callback(method, args, func(request_ob):
_rpc_async_response_received.emit(request_ob)
)
# Wait (block) until we get a response.
while true:
var rpc_response = await _rpc_async_response_received
if not rpc_response:
push_error("Error happened while waiting for RPC response in async call.")
break
if rpc_response.id == request_id:
return rpc_response.response
return null
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 = []
2024-07-14 18:40:49 -07:00
var request_id = call_rpc_callback(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():
2024-07-14 18:40:49 -07:00
push_error("Disconnected from RPC client while waiting for response.")
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.
2024-07-14 12:58:20 -07:00
if _server_packet_socket:
communication_packet_socket = _server_packet_socket.get_next_server_connection()
if communication_packet_socket:
2024-07-14 12:58:20 -07:00
_server_packet_socket.stop()
_server_packet_socket = null
if communication_packet_socket:
2024-07-14 18:40:49 -07:00
if communication_packet_socket.is_disconnected_or_error():
# Tell any awaiting async calls that they're never getting an
# answer. So sad.
_rpc_async_response_received.emit(null)
stop_process()
push_error("poll(): Disconnected from RPC client.")
return
# 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"]
2024-07-14 18:40:49 -07:00
elif "error" in packet_dict:
push_error(packet_dict["error"])
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()