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 signal _rpc_async_response_received 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("packaged_scripts") 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(open_terminal : bool = false): # FIXME: Make sure we don't have one running. var open_port = 9500 # Convert Python script path into a real path on the system. var real_python_script_path = python_script_path 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) print("REAL PATH: ", real_python_script_path) 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 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 = [ python_exe_path, wrapper_script_path, "--script", real_python_script_path, "--port", open_port] if open_terminal: if OS.get_name() == "Linux": startup_command = ["xterm", "-e"] + startup_command #print("startup command: ", startup_command) _external_process_pid = OS.create_process( startup_command[0], startup_command.slice(1), open_terminal) #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_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 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 = [] 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(): 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. 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: 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"] 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()