浏览代码

Merge pull request #33001 from Faless/js/http_run_server

Implement HTTP server for HTML5 "run" export
Rémi Verschelde 5 年之前
父节点
当前提交
1968f0129c

+ 7 - 1
editor/editor_export.cpp

@@ -376,6 +376,12 @@ Error EditorExportPlatform::_save_zip_file(void *p_userdata, const String &p_pat
 	return OK;
 }
 
+Ref<ImageTexture> EditorExportPlatform::get_option_icon(int p_index) const {
+	Ref<Theme> theme = EditorNode::get_singleton()->get_editor_theme();
+	ERR_FAIL_COND_V(theme.is_null(), Ref<ImageTexture>());
+	return theme->get_icon("Play", "EditorIcons");
+}
+
 String EditorExportPlatform::find_export_template(String template_file_name, String *err) const {
 
 	String current_version = VERSION_FULL_CONFIG;
@@ -1403,7 +1409,7 @@ bool EditorExport::poll_export_platforms() {
 
 	bool changed = false;
 	for (int i = 0; i < export_platforms.size(); i++) {
-		if (export_platforms.write[i]->poll_devices()) {
+		if (export_platforms.write[i]->poll_export()) {
 			changed = true;
 		}
 	}

+ 6 - 4
editor/editor_export.h

@@ -243,10 +243,12 @@ public:
 	Error save_pack(const Ref<EditorExportPreset> &p_preset, const String &p_path, Vector<SharedObject> *p_so_files = NULL, bool p_embed = false, int64_t *r_embedded_start = NULL, int64_t *r_embedded_size = NULL);
 	Error save_zip(const Ref<EditorExportPreset> &p_preset, const String &p_path);
 
-	virtual bool poll_devices() { return false; }
-	virtual int get_device_count() const { return 0; }
-	virtual String get_device_name(int p_device) const { return ""; }
-	virtual String get_device_info(int p_device) const { return ""; }
+	virtual bool poll_export() { return false; }
+	virtual int get_options_count() const { return 0; }
+	virtual String get_options_tooltip() const { return ""; }
+	virtual Ref<ImageTexture> get_option_icon(int p_index) const;
+	virtual String get_option_label(int p_device) const { return ""; }
+	virtual String get_option_tooltip(int p_device) const { return ""; }
 
 	enum DebugFlags {
 		DEBUG_FLAG_DUMB_CLIENT = 1,

+ 4 - 2
editor/editor_node.cpp

@@ -371,7 +371,7 @@ void EditorNode::_notification(int p_what) {
 
 		case EditorSettings::NOTIFICATION_EDITOR_SETTINGS_CHANGED: {
 			scene_tabs->set_tab_close_display_policy((bool(EDITOR_GET("interface/scene_tabs/always_show_close_button")) ? Tabs::CLOSE_BUTTON_SHOW_ALWAYS : Tabs::CLOSE_BUTTON_SHOW_ACTIVE_ONLY));
-			Ref<Theme> theme = create_editor_theme(theme_base->get_theme());
+			theme = create_editor_theme(theme_base->get_theme());
 
 			theme_base->set_theme(theme);
 			gui_base->set_theme(theme);
@@ -5640,6 +5640,9 @@ EditorNode::EditorNode() {
 	editor_export = memnew(EditorExport);
 	add_child(editor_export);
 
+	// Exporters might need the theme
+	theme = create_custom_theme();
+
 	register_exporters();
 
 	GLOBAL_DEF("editor/main_run_args", "");
@@ -5681,7 +5684,6 @@ EditorNode::EditorNode() {
 	theme_base->add_child(gui_base);
 	gui_base->set_anchors_and_margins_preset(Control::PRESET_WIDE);
 
-	Ref<Theme> theme = create_custom_theme();
 	theme_base->set_theme(theme);
 	gui_base->set_theme(theme);
 	gui_base->add_style_override("panel", gui_base->get_stylebox("Background", "EditorStyles"));

+ 6 - 8
editor/editor_run_native.cpp

@@ -75,20 +75,18 @@ void EditorRunNative::_notification(int p_what) {
 
 				Ref<EditorExportPlatform> eep = EditorExport::get_singleton()->get_export_platform(E->key());
 				MenuButton *mb = E->get();
-				int dc = eep->get_device_count();
+				int dc = eep->get_options_count();
 
 				if (dc == 0) {
 					mb->hide();
 				} else {
 					mb->get_popup()->clear();
 					mb->show();
-					if (dc == 1) {
-						mb->set_tooltip(eep->get_device_name(0) + "\n\n" + eep->get_device_info(0).strip_edges());
-					} else {
-						mb->set_tooltip("Select device from the list");
+					mb->set_tooltip(eep->get_options_tooltip());
+					if (dc > 1) {
 						for (int i = 0; i < dc; i++) {
-							mb->get_popup()->add_icon_item(get_icon("Play", "EditorIcons"), eep->get_device_name(i));
-							mb->get_popup()->set_item_tooltip(mb->get_popup()->get_item_count() - 1, eep->get_device_info(i).strip_edges());
+							mb->get_popup()->add_icon_item(eep->get_option_icon(i), eep->get_option_label(i));
+							mb->get_popup()->set_item_tooltip(mb->get_popup()->get_item_count() - 1, eep->get_option_tooltip(i));
 						}
 					}
 				}
@@ -111,7 +109,7 @@ void EditorRunNative::_run_native(int p_idx, int p_platform) {
 	ERR_FAIL_COND(eep.is_null());
 
 	if (p_idx == -1) {
-		if (eep->get_device_count() == 1) {
+		if (eep->get_options_count() == 1) {
 			menus[p_platform]->get_popup()->hide();
 			p_idx = 0;
 		} else {

+ 19 - 8
platform/android/export/export.cpp

@@ -1345,7 +1345,7 @@ public:
 		return logo;
 	}
 
-	virtual bool poll_devices() {
+	virtual bool poll_export() {
 
 		bool dc = devices_changed;
 		if (dc) {
@@ -1355,7 +1355,7 @@ public:
 		return dc;
 	}
 
-	virtual int get_device_count() const {
+	virtual int get_options_count() const {
 
 		device_lock->lock();
 		int dc = devices.size();
@@ -1364,20 +1364,31 @@ public:
 		return dc;
 	}
 
-	virtual String get_device_name(int p_device) const {
+	virtual String get_options_tooltip() const {
 
-		ERR_FAIL_INDEX_V(p_device, devices.size(), "");
+		return TTR("Select device from the list");
+	}
+
+	virtual String get_option_label(int p_index) const {
+
+		ERR_FAIL_INDEX_V(p_index, devices.size(), "");
 		device_lock->lock();
-		String s = devices[p_device].name;
+		String s = devices[p_index].name;
 		device_lock->unlock();
 		return s;
 	}
 
-	virtual String get_device_info(int p_device) const {
+	virtual String get_option_tooltip(int p_index) const {
 
-		ERR_FAIL_INDEX_V(p_device, devices.size(), "");
+		ERR_FAIL_INDEX_V(p_index, devices.size(), "");
 		device_lock->lock();
-		String s = devices[p_device].description;
+		String s = devices[p_index].description;
+		if (devices.size() == 1) {
+			// Tooltip will be:
+			// Name
+			// Description
+			s = devices[p_index].name + "\n\n" + s;
+		}
 		device_lock->unlock();
 		return s;
 	}

+ 227 - 15
platform/javascript/export/export.cpp

@@ -28,6 +28,7 @@
 /* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.                */
 /*************************************************************************/
 
+#include "core/io/tcp_server.h"
 #include "core/io/zip_io.h"
 #include "editor/editor_export.h"
 #include "editor/editor_node.h"
@@ -38,16 +39,153 @@
 #define EXPORT_TEMPLATE_WEBASSEMBLY_RELEASE "webassembly_release.zip"
 #define EXPORT_TEMPLATE_WEBASSEMBLY_DEBUG "webassembly_debug.zip"
 
+class EditorHTTPServer : public Reference {
+
+private:
+	Ref<TCP_Server> server;
+	Ref<StreamPeerTCP> connection;
+	uint64_t time;
+	uint8_t req_buf[4096];
+	int req_pos;
+
+	void _clear_client() {
+		connection = Ref<StreamPeerTCP>();
+		memset(req_buf, 0, sizeof(req_buf));
+		time = 0;
+		req_pos = 0;
+	}
+
+public:
+	EditorHTTPServer() {
+		server.instance();
+		stop();
+	}
+
+	void stop() {
+		server->stop();
+		_clear_client();
+	}
+
+	Error listen(int p_port, IP_Address p_address) {
+		return server->listen(p_port, p_address);
+	}
+
+	bool is_listening() const {
+		return server->is_listening();
+	}
+
+	void _send_response() {
+		Vector<String> psa = String((char *)req_buf).split("\r\n");
+		int len = psa.size();
+		ERR_FAIL_COND_MSG(len < 4, "Not enough response headers, got: " + itos(len) + ", expected >= 4.");
+
+		Vector<String> req = psa[0].split(" ", false);
+		ERR_FAIL_COND_MSG(req.size() < 2, "Invalid protocol or status code.");
+
+		// Wrong protocol
+		ERR_FAIL_COND_MSG(req[0] != "GET" || req[2] != "HTTP/1.1", "Invalid method or HTTP version.");
+
+		String filepath = EditorSettings::get_singleton()->get_cache_dir().plus_file("tmp_js_export");
+		String basereq = "/tmp_js_export";
+		if (req[1] == basereq + ".html") {
+			filepath += ".html";
+		} else if (req[1] == basereq + ".js") {
+			filepath += ".js";
+		} else if (req[1] == basereq + ".pck") {
+			filepath += ".pck";
+		} else if (req[1] == basereq + ".png") {
+			filepath += ".png";
+		} else if (req[1] == basereq + ".wasm") {
+			filepath += ".wasm";
+		} else {
+			String s = "HTTP/1.1 404 Not Found\r\n";
+			s += "Connection: Close\r\n";
+			s += "\r\n";
+			CharString cs = s.utf8();
+			connection->put_data((const uint8_t *)cs.get_data(), cs.size() - 1);
+			return;
+		}
+		FileAccess *f = FileAccess::open(filepath, FileAccess::READ);
+		ERR_FAIL_COND(!f);
+		String s = "HTTP/1.1 200 OK\r\n";
+		s += "Connection: Close\r\n";
+		s += "\r\n";
+		CharString cs = s.utf8();
+		Error err = connection->put_data((const uint8_t *)cs.get_data(), cs.size() - 1);
+		ERR_FAIL_COND(err != OK);
+
+		while (true) {
+			uint8_t bytes[4096];
+			int read = f->get_buffer(bytes, 4096);
+			if (read < 1) {
+				break;
+			}
+			err = connection->put_data(bytes, read);
+			ERR_FAIL_COND(err != OK);
+		}
+	}
+
+	void poll() {
+		if (!server->is_listening())
+			return;
+		if (connection.is_null()) {
+			if (!server->is_connection_available())
+				return;
+			connection = server->take_connection();
+			time = OS::get_singleton()->get_ticks_usec();
+		}
+		if (OS::get_singleton()->get_ticks_usec() - time > 1000000) {
+			_clear_client();
+			return;
+		}
+		if (connection->get_status() != StreamPeerTCP::STATUS_CONNECTED)
+			return;
+
+		while (true) {
+
+			char *r = (char *)req_buf;
+			int l = req_pos - 1;
+			if (l > 3 && r[l] == '\n' && r[l - 1] == '\r' && r[l - 2] == '\n' && r[l - 3] == '\r') {
+				_send_response();
+				_clear_client();
+				return;
+			}
+
+			int read = 0;
+			ERR_FAIL_COND(req_pos >= 4096);
+			Error err = connection->get_partial_data(&req_buf[req_pos], 1, read);
+			if (err != OK) {
+				// Got an error
+				_clear_client();
+				return;
+			} else if (read != 1) {
+				// Busy, wait next poll
+				return;
+			}
+			req_pos += read;
+		}
+	}
+};
+
 class EditorExportPlatformJavaScript : public EditorExportPlatform {
 
 	GDCLASS(EditorExportPlatformJavaScript, EditorExportPlatform);
 
 	Ref<ImageTexture> logo;
 	Ref<ImageTexture> run_icon;
-	bool runnable_when_last_polled;
+	Ref<ImageTexture> stop_icon;
+	int menu_options;
 
 	void _fix_html(Vector<uint8_t> &p_html, const Ref<EditorExportPreset> &p_preset, const String &p_name, bool p_debug);
 
+private:
+	Ref<EditorHTTPServer> server;
+	bool server_quit;
+	Mutex *server_lock;
+	Thread *server_thread;
+
+	static void _server_thread_poll(void *data);
+
 public:
 	virtual void get_preset_features(const Ref<EditorExportPreset> &p_preset, List<String> *r_features);
 
@@ -61,11 +199,12 @@ public:
 	virtual List<String> get_binary_extensions(const Ref<EditorExportPreset> &p_preset) const;
 	virtual Error export_project(const Ref<EditorExportPreset> &p_preset, bool p_debug, const String &p_path, int p_flags = 0);
 
-	virtual bool poll_devices();
-	virtual int get_device_count() const;
-	virtual String get_device_name(int p_device) const { return TTR("Run in Browser"); }
-	virtual String get_device_info(int p_device) const { return TTR("Run exported HTML in the system's default browser."); }
-	virtual Error run(const Ref<EditorExportPreset> &p_preset, int p_device, int p_debug_flags);
+	virtual bool poll_export();
+	virtual int get_options_count() const;
+	virtual String get_option_label(int p_index) const { return p_index ? TTR("Stop HTTP Server") : TTR("Run in Browser"); }
+	virtual String get_option_tooltip(int p_index) const { return p_index ? TTR("Stop HTTP Server") : TTR("Run exported HTML in the system's default browser."); }
+	virtual Ref<ImageTexture> get_option_icon(int p_index) const;
+	virtual Error run(const Ref<EditorExportPreset> &p_preset, int p_option, int p_debug_flags);
 	virtual Ref<Texture> get_run_icon() const;
 
 	virtual void get_platform_features(List<String> *r_features) {
@@ -78,6 +217,7 @@ public:
 	}
 
 	EditorExportPlatformJavaScript();
+	~EditorExportPlatformJavaScript();
 };
 
 void EditorExportPlatformJavaScript::_fix_html(Vector<uint8_t> &p_html, const Ref<EditorExportPreset> &p_preset, const String &p_name, bool p_debug) {
@@ -337,7 +477,7 @@ Error EditorExportPlatformJavaScript::export_project(const Ref<EditorExportPrese
 	return OK;
 }
 
-bool EditorExportPlatformJavaScript::poll_devices() {
+bool EditorExportPlatformJavaScript::poll_export() {
 
 	Ref<EditorExportPreset> preset;
 
@@ -350,17 +490,37 @@ bool EditorExportPlatformJavaScript::poll_devices() {
 		}
 	}
 
-	bool prev = runnable_when_last_polled;
-	runnable_when_last_polled = preset.is_valid();
-	return runnable_when_last_polled != prev;
+	int prev = menu_options;
+	menu_options = preset.is_valid();
+	if (server->is_listening()) {
+		if (menu_options == 0) {
+			server_lock->lock();
+			server->stop();
+			server_lock->unlock();
+		} else {
+			menu_options += 1;
+		}
+	}
+	return menu_options != prev;
+}
+
+Ref<ImageTexture> EditorExportPlatformJavaScript::get_option_icon(int p_index) const {
+	return p_index == 1 ? stop_icon : EditorExportPlatform::get_option_icon(p_index);
 }
 
-int EditorExportPlatformJavaScript::get_device_count() const {
+int EditorExportPlatformJavaScript::get_options_count() const {
 
-	return runnable_when_last_polled;
+	return menu_options;
 }
 
-Error EditorExportPlatformJavaScript::run(const Ref<EditorExportPreset> &p_preset, int p_device, int p_debug_flags) {
+Error EditorExportPlatformJavaScript::run(const Ref<EditorExportPreset> &p_preset, int p_option, int p_debug_flags) {
+
+	if (p_option == 1) {
+		server_lock->lock();
+		server->stop();
+		server_lock->unlock();
+		return OK;
+	}
 
 	String basepath = EditorSettings::get_singleton()->get_cache_dir().plus_file("tmp_js_export");
 	String path = basepath + ".html";
@@ -374,7 +534,26 @@ Error EditorExportPlatformJavaScript::run(const Ref<EditorExportPreset> &p_prese
 		DirAccess::remove_file_or_error(basepath + ".wasm");
 		return err;
 	}
-	OS::get_singleton()->shell_open(String("file://") + path);
+
+	IP_Address bind_ip;
+	uint16_t bind_port = EDITOR_GET("export/web/http_port");
+	// Resolve host if needed.
+	String bind_host = EDITOR_GET("export/web/http_host");
+	if (bind_host.is_valid_ip_address()) {
+		bind_ip = bind_host;
+	} else {
+		bind_ip = IP::get_singleton()->resolve_hostname(bind_host);
+	}
+	ERR_FAIL_COND_V_MSG(!bind_ip.is_valid(), ERR_INVALID_PARAMETER, "Invalid editor setting 'export/web/http_host': '" + bind_host + "'. Try using '127.0.0.1'.");
+
+	// Restart server.
+	server_lock->lock();
+	server->stop();
+	err = server->listen(bind_port, bind_ip);
+	server_lock->unlock();
+	ERR_FAIL_COND_V_MSG(err != OK, err, "Unable to start HTTP server.");
+
+	OS::get_singleton()->shell_open(String("http://" + bind_host + ":" + itos(bind_port) + "/tmp_js_export.html"));
 	// FIXME: Find out how to clean up export files after running the successfully
 	// exported game. Might not be trivial.
 	return OK;
@@ -385,8 +564,23 @@ Ref<Texture> EditorExportPlatformJavaScript::get_run_icon() const {
 	return run_icon;
 }
 
+void EditorExportPlatformJavaScript::_server_thread_poll(void *data) {
+	EditorExportPlatformJavaScript *ej = (EditorExportPlatformJavaScript *)data;
+	while (!ej->server_quit) {
+		OS::get_singleton()->delay_usec(1000);
+		ej->server_lock->lock();
+		ej->server->poll();
+		ej->server_lock->unlock();
+	}
+}
+
 EditorExportPlatformJavaScript::EditorExportPlatformJavaScript() {
 
+	server.instance();
+	server_quit = false;
+	server_lock = Mutex::create();
+	server_thread = Thread::create(_server_thread_poll, this);
+
 	Ref<Image> img = memnew(Image(_javascript_logo));
 	logo.instance();
 	logo->create_from_image(img);
@@ -395,11 +589,29 @@ EditorExportPlatformJavaScript::EditorExportPlatformJavaScript() {
 	run_icon.instance();
 	run_icon->create_from_image(img);
 
-	runnable_when_last_polled = false;
+	Ref<Theme> theme = EditorNode::get_singleton()->get_editor_theme();
+	if (theme.is_valid())
+		stop_icon = theme->get_icon("Stop", "EditorIcons");
+	else
+		stop_icon.instance();
+
+	menu_options = 0;
+}
+
+EditorExportPlatformJavaScript::~EditorExportPlatformJavaScript() {
+	server->stop();
+	server_quit = true;
+	Thread::wait_to_finish(server_thread);
+	memdelete(server_lock);
+	memdelete(server_thread);
 }
 
 void register_javascript_exporter() {
 
+	EDITOR_DEF("export/web/http_host", "localhost");
+	EDITOR_DEF("export/web/http_port", 8060);
+	EditorSettings::get_singleton()->add_property_hint(PropertyInfo(Variant::INT, "export/web/http_port", PROPERTY_HINT_RANGE, "1,65535,1"));
+
 	Ref<EditorExportPlatformJavaScript> platform;
 	platform.instance();
 	EditorExport::get_singleton()->add_export_platform(platform);