extends RefCounted class_name KiriPythonWrapperInstance var external_process_pid = -1 var server_packet_socket : KiriPacketSocket = null var communication_packet_socket : KiriPacketSocket = null var python_script_path : String = "" enum KiriPythonWrapperStatus { STATUS_RUNNING, STATUS_STOPPED } func _init(python_file_path : String): python_script_path = python_file_path func _get_python_executable(): # FIXME: Adjust per-OS. Maybe test a few locations. return "/usr/bin/python3" 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_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 = _get_wrapper_script() 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 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 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()