253 lines
7.3 KiB
GDScript
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()
|
|
|
|
|
|
|