summaryrefslogtreecommitdiffstats
path: root/platform/web/export
diff options
context:
space:
mode:
Diffstat (limited to 'platform/web/export')
-rw-r--r--platform/web/export/editor_http_server.cpp255
-rw-r--r--platform/web/export/editor_http_server.h203
-rw-r--r--platform/web/export/export_plugin.cpp355
-rw-r--r--platform/web/export/export_plugin.h28
-rw-r--r--platform/web/export/logo.svg2
-rw-r--r--platform/web/export/run_icon.svg2
6 files changed, 573 insertions, 272 deletions
diff --git a/platform/web/export/editor_http_server.cpp b/platform/web/export/editor_http_server.cpp
new file mode 100644
index 0000000000..9cf862eb1e
--- /dev/null
+++ b/platform/web/export/editor_http_server.cpp
@@ -0,0 +1,255 @@
+/**************************************************************************/
+/* editor_http_server.cpp */
+/**************************************************************************/
+/* This file is part of: */
+/* GODOT ENGINE */
+/* https://godotengine.org */
+/**************************************************************************/
+/* Copyright (c) 2014-present Godot Engine contributors (see AUTHORS.md). */
+/* Copyright (c) 2007-2014 Juan Linietsky, Ariel Manzur. */
+/* */
+/* Permission is hereby granted, free of charge, to any person obtaining */
+/* a copy of this software and associated documentation files (the */
+/* "Software"), to deal in the Software without restriction, including */
+/* without limitation the rights to use, copy, modify, merge, publish, */
+/* distribute, sublicense, and/or sell copies of the Software, and to */
+/* permit persons to whom the Software is furnished to do so, subject to */
+/* the following conditions: */
+/* */
+/* The above copyright notice and this permission notice shall be */
+/* included in all copies or substantial portions of the Software. */
+/* */
+/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */
+/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */
+/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. */
+/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */
+/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */
+/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */
+/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */
+/**************************************************************************/
+
+#include "editor_http_server.h"
+
+void EditorHTTPServer::_server_thread_poll(void *data) {
+ EditorHTTPServer *web_server = static_cast<EditorHTTPServer *>(data);
+ while (!web_server->server_quit.is_set()) {
+ OS::get_singleton()->delay_usec(6900);
+ {
+ MutexLock lock(web_server->server_lock);
+ web_server->_poll();
+ }
+ }
+}
+
+void EditorHTTPServer::_clear_client() {
+ peer = Ref<StreamPeer>();
+ tls = Ref<StreamPeerTLS>();
+ tcp = Ref<StreamPeerTCP>();
+ memset(req_buf, 0, sizeof(req_buf));
+ time = 0;
+ req_pos = 0;
+}
+
+void EditorHTTPServer::_set_internal_certs(Ref<Crypto> p_crypto) {
+ const String cache_path = EditorPaths::get_singleton()->get_cache_dir();
+ const String key_path = cache_path.path_join("html5_server.key");
+ const String crt_path = cache_path.path_join("html5_server.crt");
+ bool regen = !FileAccess::exists(key_path) || !FileAccess::exists(crt_path);
+ if (!regen) {
+ key = Ref<CryptoKey>(CryptoKey::create());
+ cert = Ref<X509Certificate>(X509Certificate::create());
+ if (key->load(key_path) != OK || cert->load(crt_path) != OK) {
+ regen = true;
+ }
+ }
+ if (regen) {
+ key = p_crypto->generate_rsa(2048);
+ key->save(key_path);
+ cert = p_crypto->generate_self_signed_certificate(key, "CN=godot-debug.local,O=A Game Dev,C=XXA", "20140101000000", "20340101000000");
+ cert->save(crt_path);
+ }
+}
+
+void EditorHTTPServer::_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.");
+
+ const int query_index = req[1].find_char('?');
+ const String path = (query_index == -1) ? req[1] : req[1].substr(0, query_index);
+
+ const String req_file = path.get_file();
+ const String req_ext = path.get_extension();
+ const String cache_path = EditorPaths::get_singleton()->get_cache_dir().path_join("web");
+ const String filepath = cache_path.path_join(req_file);
+
+ if (!mimes.has(req_ext) || !FileAccess::exists(filepath)) {
+ String s = "HTTP/1.1 404 Not Found\r\n";
+ s += "Connection: Close\r\n";
+ s += "\r\n";
+ CharString cs = s.utf8();
+ peer->put_data((const uint8_t *)cs.get_data(), cs.size() - 1);
+ return;
+ }
+ const String ctype = mimes[req_ext];
+
+ Ref<FileAccess> f = FileAccess::open(filepath, FileAccess::READ);
+ ERR_FAIL_COND(f.is_null());
+ String s = "HTTP/1.1 200 OK\r\n";
+ s += "Connection: Close\r\n";
+ s += "Content-Type: " + ctype + "\r\n";
+ s += "Access-Control-Allow-Origin: *\r\n";
+ s += "Cross-Origin-Opener-Policy: same-origin\r\n";
+ s += "Cross-Origin-Embedder-Policy: require-corp\r\n";
+ s += "Cache-Control: no-store, max-age=0\r\n";
+ s += "\r\n";
+ CharString cs = s.utf8();
+ Error err = peer->put_data((const uint8_t *)cs.get_data(), cs.size() - 1);
+ if (err != OK) {
+ ERR_FAIL();
+ }
+
+ while (true) {
+ uint8_t bytes[4096];
+ uint64_t read = f->get_buffer(bytes, 4096);
+ if (read == 0) {
+ break;
+ }
+ err = peer->put_data(bytes, read);
+ if (err != OK) {
+ ERR_FAIL();
+ }
+ }
+}
+
+void EditorHTTPServer::_poll() {
+ if (!server->is_listening()) {
+ return;
+ }
+ if (tcp.is_null()) {
+ if (!server->is_connection_available()) {
+ return;
+ }
+ tcp = server->take_connection();
+ peer = tcp;
+ time = OS::get_singleton()->get_ticks_usec();
+ }
+ if (OS::get_singleton()->get_ticks_usec() - time > 1000000) {
+ _clear_client();
+ return;
+ }
+ if (tcp->get_status() != StreamPeerTCP::STATUS_CONNECTED) {
+ return;
+ }
+
+ if (use_tls) {
+ if (tls.is_null()) {
+ tls = Ref<StreamPeerTLS>(StreamPeerTLS::create());
+ peer = tls;
+ if (tls->accept_stream(tcp, TLSOptions::server(key, cert)) != OK) {
+ _clear_client();
+ return;
+ }
+ }
+ tls->poll();
+ if (tls->get_status() == StreamPeerTLS::STATUS_HANDSHAKING) {
+ // Still handshaking, keep waiting.
+ return;
+ }
+ if (tls->get_status() != StreamPeerTLS::STATUS_CONNECTED) {
+ _clear_client();
+ 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 = peer->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;
+ }
+}
+
+void EditorHTTPServer::stop() {
+ server_quit.set();
+ if (server_thread.is_started()) {
+ server_thread.wait_to_finish();
+ }
+ if (server.is_valid()) {
+ server->stop();
+ }
+ _clear_client();
+}
+
+Error EditorHTTPServer::listen(int p_port, IPAddress p_address, bool p_use_tls, String p_tls_key, String p_tls_cert) {
+ MutexLock lock(server_lock);
+ if (server->is_listening()) {
+ return ERR_ALREADY_IN_USE;
+ }
+ use_tls = p_use_tls;
+ if (use_tls) {
+ Ref<Crypto> crypto = Crypto::create();
+ if (crypto.is_null()) {
+ return ERR_UNAVAILABLE;
+ }
+ if (!p_tls_key.is_empty() && !p_tls_cert.is_empty()) {
+ key = Ref<CryptoKey>(CryptoKey::create());
+ Error err = key->load(p_tls_key);
+ ERR_FAIL_COND_V(err != OK, err);
+ cert = Ref<X509Certificate>(X509Certificate::create());
+ err = cert->load(p_tls_cert);
+ ERR_FAIL_COND_V(err != OK, err);
+ } else {
+ _set_internal_certs(crypto);
+ }
+ }
+ Error err = server->listen(p_port, p_address);
+ if (err == OK) {
+ server_quit.clear();
+ server_thread.start(_server_thread_poll, this);
+ }
+ return err;
+}
+
+bool EditorHTTPServer::is_listening() const {
+ MutexLock lock(server_lock);
+ return server->is_listening();
+}
+
+EditorHTTPServer::EditorHTTPServer() {
+ mimes["html"] = "text/html";
+ mimes["js"] = "application/javascript";
+ mimes["json"] = "application/json";
+ mimes["pck"] = "application/octet-stream";
+ mimes["png"] = "image/png";
+ mimes["svg"] = "image/svg";
+ mimes["wasm"] = "application/wasm";
+ server.instantiate();
+ stop();
+}
+
+EditorHTTPServer::~EditorHTTPServer() {
+ stop();
+}
diff --git a/platform/web/export/editor_http_server.h b/platform/web/export/editor_http_server.h
index 3f87288537..4437492b5a 100644
--- a/platform/web/export/editor_http_server.h
+++ b/platform/web/export/editor_http_server.h
@@ -51,199 +51,24 @@ private:
uint8_t req_buf[4096];
int req_pos = 0;
- void _clear_client() {
- peer = Ref<StreamPeer>();
- tls = Ref<StreamPeerTLS>();
- tcp = Ref<StreamPeerTCP>();
- memset(req_buf, 0, sizeof(req_buf));
- time = 0;
- req_pos = 0;
- }
+ SafeFlag server_quit;
+ Mutex server_lock;
+ Thread server_thread;
- void _set_internal_certs(Ref<Crypto> p_crypto) {
- const String cache_path = EditorPaths::get_singleton()->get_cache_dir();
- const String key_path = cache_path.path_join("html5_server.key");
- const String crt_path = cache_path.path_join("html5_server.crt");
- bool regen = !FileAccess::exists(key_path) || !FileAccess::exists(crt_path);
- if (!regen) {
- key = Ref<CryptoKey>(CryptoKey::create());
- cert = Ref<X509Certificate>(X509Certificate::create());
- if (key->load(key_path) != OK || cert->load(crt_path) != OK) {
- regen = true;
- }
- }
- if (regen) {
- key = p_crypto->generate_rsa(2048);
- key->save(key_path);
- cert = p_crypto->generate_self_signed_certificate(key, "CN=godot-debug.local,O=A Game Dev,C=XXA", "20140101000000", "20340101000000");
- cert->save(crt_path);
- }
- }
+ void _clear_client();
+ void _set_internal_certs(Ref<Crypto> p_crypto);
+ void _send_response();
+ void _poll();
-public:
- EditorHTTPServer() {
- mimes["html"] = "text/html";
- mimes["js"] = "application/javascript";
- mimes["json"] = "application/json";
- mimes["pck"] = "application/octet-stream";
- mimes["png"] = "image/png";
- mimes["svg"] = "image/svg";
- mimes["wasm"] = "application/wasm";
- server.instantiate();
- stop();
- }
-
- void stop() {
- server->stop();
- _clear_client();
- }
-
- Error listen(int p_port, IPAddress p_address, bool p_use_tls, String p_tls_key, String p_tls_cert) {
- use_tls = p_use_tls;
- if (use_tls) {
- Ref<Crypto> crypto = Crypto::create();
- if (crypto.is_null()) {
- return ERR_UNAVAILABLE;
- }
- if (!p_tls_key.is_empty() && !p_tls_cert.is_empty()) {
- key = Ref<CryptoKey>(CryptoKey::create());
- Error err = key->load(p_tls_key);
- ERR_FAIL_COND_V(err != OK, err);
- cert = Ref<X509Certificate>(X509Certificate::create());
- err = cert->load(p_tls_cert);
- ERR_FAIL_COND_V(err != OK, err);
- } else {
- _set_internal_certs(crypto);
- }
- }
- 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.");
-
- const int query_index = req[1].find_char('?');
- const String path = (query_index == -1) ? req[1] : req[1].substr(0, query_index);
+ static void _server_thread_poll(void *data);
- const String req_file = path.get_file();
- const String req_ext = path.get_extension();
- const String cache_path = EditorPaths::get_singleton()->get_cache_dir().path_join("web");
- const String filepath = cache_path.path_join(req_file);
-
- if (!mimes.has(req_ext) || !FileAccess::exists(filepath)) {
- String s = "HTTP/1.1 404 Not Found\r\n";
- s += "Connection: Close\r\n";
- s += "\r\n";
- CharString cs = s.utf8();
- peer->put_data((const uint8_t *)cs.get_data(), cs.size() - 1);
- return;
- }
- const String ctype = mimes[req_ext];
-
- Ref<FileAccess> f = FileAccess::open(filepath, FileAccess::READ);
- ERR_FAIL_COND(f.is_null());
- String s = "HTTP/1.1 200 OK\r\n";
- s += "Connection: Close\r\n";
- s += "Content-Type: " + ctype + "\r\n";
- s += "Access-Control-Allow-Origin: *\r\n";
- s += "Cross-Origin-Opener-Policy: same-origin\r\n";
- s += "Cross-Origin-Embedder-Policy: require-corp\r\n";
- s += "Cache-Control: no-store, max-age=0\r\n";
- s += "\r\n";
- CharString cs = s.utf8();
- Error err = peer->put_data((const uint8_t *)cs.get_data(), cs.size() - 1);
- if (err != OK) {
- ERR_FAIL();
- }
-
- while (true) {
- uint8_t bytes[4096];
- uint64_t read = f->get_buffer(bytes, 4096);
- if (read == 0) {
- break;
- }
- err = peer->put_data(bytes, read);
- if (err != OK) {
- ERR_FAIL();
- }
- }
- }
-
- void poll() {
- if (!server->is_listening()) {
- return;
- }
- if (tcp.is_null()) {
- if (!server->is_connection_available()) {
- return;
- }
- tcp = server->take_connection();
- peer = tcp;
- time = OS::get_singleton()->get_ticks_usec();
- }
- if (OS::get_singleton()->get_ticks_usec() - time > 1000000) {
- _clear_client();
- return;
- }
- if (tcp->get_status() != StreamPeerTCP::STATUS_CONNECTED) {
- return;
- }
-
- if (use_tls) {
- if (tls.is_null()) {
- tls = Ref<StreamPeerTLS>(StreamPeerTLS::create());
- peer = tls;
- if (tls->accept_stream(tcp, TLSOptions::server(key, cert)) != OK) {
- _clear_client();
- return;
- }
- }
- tls->poll();
- if (tls->get_status() == StreamPeerTLS::STATUS_HANDSHAKING) {
- // Still handshaking, keep waiting.
- return;
- }
- if (tls->get_status() != StreamPeerTLS::STATUS_CONNECTED) {
- _clear_client();
- 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;
- }
+public:
+ EditorHTTPServer();
+ ~EditorHTTPServer();
- int read = 0;
- ERR_FAIL_COND(req_pos >= 4096);
- Error err = peer->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;
- }
- }
+ void stop();
+ Error listen(int p_port, IPAddress p_address, bool p_use_tls, String p_tls_key, String p_tls_cert);
+ bool is_listening() const;
};
#endif // WEB_EDITOR_HTTP_SERVER_H
diff --git a/platform/web/export/export_plugin.cpp b/platform/web/export/export_plugin.cpp
index a62ccdc2aa..d83e465e8e 100644
--- a/platform/web/export/export_plugin.cpp
+++ b/platform/web/export/export_plugin.cpp
@@ -34,11 +34,11 @@
#include "run_icon_svg.gen.h"
#include "core/config/project_settings.h"
-#include "editor/editor_scale.h"
#include "editor/editor_settings.h"
#include "editor/editor_string_names.h"
#include "editor/export/editor_export.h"
#include "editor/import/resource_importer_texture_settings.h"
+#include "editor/themes/editor_scale.h"
#include "scene/resources/image_texture.h"
#include "modules/modules_enabled.gen.h" // For mono and svg.
@@ -112,7 +112,7 @@ Error EditorExportPlatformWeb::_write_or_error(const uint8_t *p_content, int p_s
return OK;
}
-void EditorExportPlatformWeb::_replace_strings(HashMap<String, String> p_replaces, Vector<uint8_t> &r_template) {
+void EditorExportPlatformWeb::_replace_strings(const HashMap<String, String> &p_replaces, Vector<uint8_t> &r_template) {
String str_template = String::utf8(reinterpret_cast<const char *>(r_template.ptr()), r_template.size());
String out;
Vector<String> lines = str_template.split("\n");
@@ -150,14 +150,15 @@ void EditorExportPlatformWeb::_fix_html(Vector<uint8_t> &p_html, const Ref<Edito
config["executable"] = p_name;
config["args"] = args;
config["fileSizes"] = p_file_sizes;
+ config["ensureCrossOriginIsolationHeaders"] = (bool)p_preset->get("progressive_web_app/ensure_cross_origin_isolation_headers");
String head_include;
if (p_preset->get("html/export_icon")) {
- head_include += "<link id='-gd-engine-icon' rel='icon' type='image/png' href='" + p_name + ".icon.png' />\n";
- head_include += "<link rel='apple-touch-icon' href='" + p_name + ".apple-touch-icon.png'/>\n";
+ head_include += "<link id=\"-gd-engine-icon\" rel=\"icon\" type=\"image/png\" href=\"" + p_name + ".icon.png\" />\n";
+ head_include += "<link rel=\"apple-touch-icon\" href=\"" + p_name + ".apple-touch-icon.png\"/>\n";
}
if (p_preset->get("progressive_web_app/enabled")) {
- head_include += "<link rel='manifest' href='" + p_name + ".manifest.json'>\n";
+ head_include += "<link rel=\"manifest\" href=\"" + p_name + ".manifest.json\">\n";
config["serviceWorker"] = p_name + ".service.worker.js";
}
@@ -169,6 +170,14 @@ void EditorExportPlatformWeb::_fix_html(Vector<uint8_t> &p_html, const Ref<Edito
replaces["$GODOT_PROJECT_NAME"] = GLOBAL_GET("application/config/name");
replaces["$GODOT_HEAD_INCLUDE"] = head_include + custom_head_include;
replaces["$GODOT_CONFIG"] = str_config;
+ replaces["$GODOT_SPLASH"] = p_name + ".png";
+
+ if (p_preset->get("variant/thread_support")) {
+ replaces["$GODOT_THREADS_ENABLED"] = "true";
+ } else {
+ replaces["$GODOT_THREADS_ENABLED"] = "false";
+ }
+
_replace_strings(replaces, p_html);
}
@@ -215,10 +224,12 @@ Error EditorExportPlatformWeb::_build_pwa(const Ref<EditorExportPreset> &p_prese
const String dir = p_path.get_base_dir();
const String name = p_path.get_file().get_basename();
bool extensions = (bool)p_preset->get("variant/extensions_support");
+ bool ensure_crossorigin_isolation_headers = (bool)p_preset->get("progressive_web_app/ensure_cross_origin_isolation_headers");
HashMap<String, String> replaces;
- replaces["@GODOT_VERSION@"] = String::num_int64(OS::get_singleton()->get_unix_time()) + "|" + String::num_int64(OS::get_singleton()->get_ticks_usec());
- replaces["@GODOT_NAME@"] = proj_name.substr(0, 16);
- replaces["@GODOT_OFFLINE_PAGE@"] = name + ".offline.html";
+ replaces["___GODOT_VERSION___"] = String::num_int64(OS::get_singleton()->get_unix_time()) + "|" + String::num_int64(OS::get_singleton()->get_ticks_usec());
+ replaces["___GODOT_NAME___"] = proj_name.substr(0, 16);
+ replaces["___GODOT_OFFLINE_PAGE___"] = name + ".offline.html";
+ replaces["___GODOT_ENSURE_CROSSORIGIN_ISOLATION_HEADERS___"] = ensure_crossorigin_isolation_headers ? "true" : "false";
// Files cached during worker install.
Array cache_files;
@@ -231,7 +242,7 @@ Error EditorExportPlatformWeb::_build_pwa(const Ref<EditorExportPreset> &p_prese
}
cache_files.push_back(name + ".worker.js");
cache_files.push_back(name + ".audio.worklet.js");
- replaces["@GODOT_CACHE@"] = Variant(cache_files).to_json_string();
+ replaces["___GODOT_CACHE___"] = Variant(cache_files).to_json_string();
// Heavy files that are cached on demand.
Array opt_cache_files;
@@ -243,7 +254,7 @@ Error EditorExportPlatformWeb::_build_pwa(const Ref<EditorExportPreset> &p_prese
opt_cache_files.push_back(p_shared_objects[i].path.get_file());
}
}
- replaces["@GODOT_OPT_CACHE@"] = Variant(opt_cache_files).to_json_string();
+ replaces["___GODOT_OPT_CACHE___"] = Variant(opt_cache_files).to_json_string();
const String sw_path = dir.path_join(name + ".service.worker.js");
Vector<uint8_t> sw;
@@ -259,6 +270,7 @@ Error EditorExportPlatformWeb::_build_pwa(const Ref<EditorExportPreset> &p_prese
_replace_strings(replaces, sw);
Error err = _write_or_error(sw.ptr(), sw.size(), dir.path_join(name + ".service.worker.js"));
if (err != OK) {
+ // Message is supplied by the subroutine method.
return err;
}
@@ -291,16 +303,19 @@ Error EditorExportPlatformWeb::_build_pwa(const Ref<EditorExportPreset> &p_prese
const String icon144_path = p_preset->get("progressive_web_app/icon_144x144");
err = _add_manifest_icon(p_path, icon144_path, 144, icons_arr);
if (err != OK) {
+ // Message is supplied by the subroutine method.
return err;
}
const String icon180_path = p_preset->get("progressive_web_app/icon_180x180");
err = _add_manifest_icon(p_path, icon180_path, 180, icons_arr);
if (err != OK) {
+ // Message is supplied by the subroutine method.
return err;
}
const String icon512_path = p_preset->get("progressive_web_app/icon_512x512");
err = _add_manifest_icon(p_path, icon512_path, 512, icons_arr);
if (err != OK) {
+ // Message is supplied by the subroutine method.
return err;
}
manifest["icons"] = icons_arr;
@@ -308,6 +323,7 @@ Error EditorExportPlatformWeb::_build_pwa(const Ref<EditorExportPreset> &p_prese
CharString cs = Variant(manifest).to_json_string().utf8();
err = _write_or_error((const uint8_t *)cs.get_data(), cs.length(), dir.path_join(name + ".manifest.json"));
if (err != OK) {
+ // Message is supplied by the subroutine method.
return err;
}
@@ -318,10 +334,14 @@ void EditorExportPlatformWeb::get_preset_features(const Ref<EditorExportPreset>
if (p_preset->get("vram_texture_compression/for_desktop")) {
r_features->push_back("s3tc");
}
-
if (p_preset->get("vram_texture_compression/for_mobile")) {
r_features->push_back("etc2");
}
+ if (p_preset->get("variant/thread_support").operator bool()) {
+ r_features->push_back("threads");
+ } else {
+ r_features->push_back("nothreads");
+ }
r_features->push_back("wasm32");
}
@@ -329,7 +349,8 @@ void EditorExportPlatformWeb::get_export_options(List<ExportOption> *r_options)
r_options->push_back(ExportOption(PropertyInfo(Variant::STRING, "custom_template/debug", PROPERTY_HINT_GLOBAL_FILE, "*.zip"), ""));
r_options->push_back(ExportOption(PropertyInfo(Variant::STRING, "custom_template/release", PROPERTY_HINT_GLOBAL_FILE, "*.zip"), ""));
- r_options->push_back(ExportOption(PropertyInfo(Variant::BOOL, "variant/extensions_support"), false)); // Export type.
+ r_options->push_back(ExportOption(PropertyInfo(Variant::BOOL, "variant/extensions_support"), false)); // GDExtension support.
+ r_options->push_back(ExportOption(PropertyInfo(Variant::BOOL, "variant/thread_support"), false)); // Thread support (i.e. run with or without COEP/COOP headers).
r_options->push_back(ExportOption(PropertyInfo(Variant::BOOL, "vram_texture_compression/for_desktop"), true)); // S3TC
r_options->push_back(ExportOption(PropertyInfo(Variant::BOOL, "vram_texture_compression/for_mobile"), false)); // ETC or ETC2, depending on renderer
@@ -340,6 +361,7 @@ void EditorExportPlatformWeb::get_export_options(List<ExportOption> *r_options)
r_options->push_back(ExportOption(PropertyInfo(Variant::BOOL, "html/focus_canvas_on_start"), true));
r_options->push_back(ExportOption(PropertyInfo(Variant::BOOL, "html/experimental_virtual_keyboard"), false));
r_options->push_back(ExportOption(PropertyInfo(Variant::BOOL, "progressive_web_app/enabled"), false));
+ r_options->push_back(ExportOption(PropertyInfo(Variant::BOOL, "progressive_web_app/ensure_cross_origin_isolation_headers"), true));
r_options->push_back(ExportOption(PropertyInfo(Variant::STRING, "progressive_web_app/offline_page", PROPERTY_HINT_FILE, "*.html"), ""));
r_options->push_back(ExportOption(PropertyInfo(Variant::INT, "progressive_web_app/display", PROPERTY_HINT_ENUM, "Fullscreen,Standalone,Minimal UI,Browser"), 1));
r_options->push_back(ExportOption(PropertyInfo(Variant::INT, "progressive_web_app/orientation", PROPERTY_HINT_ENUM, "Any,Landscape,Portrait"), 0));
@@ -372,10 +394,11 @@ bool EditorExportPlatformWeb::has_valid_export_configuration(const Ref<EditorExp
String err;
bool valid = false;
bool extensions = (bool)p_preset->get("variant/extensions_support");
+ bool thread_support = (bool)p_preset->get("variant/thread_support");
// Look for export templates (first official, and if defined custom templates).
- bool dvalid = exists_export_template(_get_template_name(extensions, true), &err);
- bool rvalid = exists_export_template(_get_template_name(extensions, false), &err);
+ bool dvalid = exists_export_template(_get_template_name(extensions, thread_support, true), &err);
+ bool rvalid = exists_export_template(_get_template_name(extensions, thread_support, false), &err);
if (p_preset->get("custom_template/debug") != "") {
dvalid = FileAccess::exists(p_preset->get("custom_template/debug"));
@@ -439,16 +462,18 @@ Error EditorExportPlatformWeb::export_project(const Ref<EditorExportPreset> &p_p
const String base_path = p_path.get_basename();
const String base_name = p_path.get_file().get_basename();
+ if (!DirAccess::exists(base_dir)) {
+ add_message(EXPORT_MESSAGE_ERROR, TTR("Export"), vformat(TTR("Target folder does not exist or is inaccessible: \"%s\""), base_dir));
+ return ERR_FILE_BAD_PATH;
+ }
+
// Find the correct template
String template_path = p_debug ? custom_debug : custom_release;
template_path = template_path.strip_edges();
if (template_path.is_empty()) {
bool extensions = (bool)p_preset->get("variant/extensions_support");
- template_path = find_export_template(_get_template_name(extensions, p_debug));
- }
-
- if (!DirAccess::exists(base_dir)) {
- return ERR_FILE_BAD_PATH;
+ bool thread_support = (bool)p_preset->get("variant/thread_support");
+ template_path = find_export_template(_get_template_name(extensions, thread_support, p_debug));
}
if (!template_path.is_empty() && !FileAccess::exists(template_path)) {
@@ -480,6 +505,7 @@ Error EditorExportPlatformWeb::export_project(const Ref<EditorExportPreset> &p_p
// Extract templates.
error = _extract_template(template_path, base_dir, base_name, pwa);
if (error) {
+ // Message is supplied by the subroutine method.
return error;
}
@@ -510,6 +536,7 @@ Error EditorExportPlatformWeb::export_project(const Ref<EditorExportPreset> &p_p
_fix_html(html, p_preset, base_name, p_debug, p_flags, shared_objects, file_sizes);
Error err = _write_or_error(html.ptr(), html.size(), p_path);
if (err != OK) {
+ // Message is supplied by the subroutine method.
return err;
}
html.resize(0);
@@ -543,6 +570,7 @@ Error EditorExportPlatformWeb::export_project(const Ref<EditorExportPreset> &p_p
if (pwa) {
err = _build_pwa(p_preset, p_path, shared_objects);
if (err != OK) {
+ // Message is supplied by the subroutine method.
return err;
}
}
@@ -561,34 +589,233 @@ bool EditorExportPlatformWeb::poll_export() {
}
}
- int prev = menu_options;
- menu_options = preset.is_valid();
- if (server->is_listening()) {
- if (menu_options == 0) {
- MutexLock lock(server_lock);
- server->stop();
- } else {
- menu_options += 1;
+ RemoteDebugState prev_remote_debug_state = remote_debug_state;
+ remote_debug_state = REMOTE_DEBUG_STATE_UNAVAILABLE;
+
+ if (preset.is_valid()) {
+ const bool debug = true;
+ // Throwaway variables to pass to `can_export`.
+ String err;
+ bool missing_templates;
+
+ if (can_export(preset, err, missing_templates, debug)) {
+ if (server->is_listening()) {
+ remote_debug_state = REMOTE_DEBUG_STATE_SERVING;
+ } else {
+ remote_debug_state = REMOTE_DEBUG_STATE_AVAILABLE;
+ }
}
}
- return menu_options != prev;
+
+ if (remote_debug_state != REMOTE_DEBUG_STATE_SERVING && server->is_listening()) {
+ server->stop();
+ }
+
+ return remote_debug_state != prev_remote_debug_state;
}
Ref<ImageTexture> EditorExportPlatformWeb::get_option_icon(int p_index) const {
- return p_index == 1 ? stop_icon : EditorExportPlatform::get_option_icon(p_index);
+ Ref<ImageTexture> play_icon = EditorExportPlatform::get_option_icon(p_index);
+
+ switch (remote_debug_state) {
+ case REMOTE_DEBUG_STATE_UNAVAILABLE: {
+ return nullptr;
+ } break;
+
+ case REMOTE_DEBUG_STATE_AVAILABLE: {
+ switch (p_index) {
+ case 0:
+ case 1:
+ return play_icon;
+ default:
+ ERR_FAIL_V(nullptr);
+ }
+ } break;
+
+ case REMOTE_DEBUG_STATE_SERVING: {
+ switch (p_index) {
+ case 0:
+ return play_icon;
+ case 1:
+ return restart_icon;
+ case 2:
+ return stop_icon;
+ default:
+ ERR_FAIL_V(nullptr);
+ }
+ } break;
+ }
+
+ return nullptr;
}
int EditorExportPlatformWeb::get_options_count() const {
- return menu_options;
+ switch (remote_debug_state) {
+ case REMOTE_DEBUG_STATE_UNAVAILABLE: {
+ return 0;
+ } break;
+
+ case REMOTE_DEBUG_STATE_AVAILABLE: {
+ return 2;
+ } break;
+
+ case REMOTE_DEBUG_STATE_SERVING: {
+ return 3;
+ } break;
+ }
+
+ return 0;
+}
+
+String EditorExportPlatformWeb::get_option_label(int p_index) const {
+ String run_in_browser = TTR("Run in Browser");
+ String start_http_server = TTR("Start HTTP Server");
+ String reexport_project = TTR("Re-export Project");
+ String stop_http_server = TTR("Stop HTTP Server");
+
+ switch (remote_debug_state) {
+ case REMOTE_DEBUG_STATE_UNAVAILABLE:
+ return "";
+
+ case REMOTE_DEBUG_STATE_AVAILABLE: {
+ switch (p_index) {
+ case 0:
+ return run_in_browser;
+ case 1:
+ return start_http_server;
+ default:
+ ERR_FAIL_V("");
+ }
+ } break;
+
+ case REMOTE_DEBUG_STATE_SERVING: {
+ switch (p_index) {
+ case 0:
+ return run_in_browser;
+ case 1:
+ return reexport_project;
+ case 2:
+ return stop_http_server;
+ default:
+ ERR_FAIL_V("");
+ }
+ } break;
+ }
+
+ return "";
+}
+
+String EditorExportPlatformWeb::get_option_tooltip(int p_index) const {
+ String run_in_browser = TTR("Run exported HTML in the system's default browser.");
+ String start_http_server = TTR("Start the HTTP server.");
+ String reexport_project = TTR("Export project again to account for updates.");
+ String stop_http_server = TTR("Stop the HTTP server.");
+
+ switch (remote_debug_state) {
+ case REMOTE_DEBUG_STATE_UNAVAILABLE:
+ return "";
+
+ case REMOTE_DEBUG_STATE_AVAILABLE: {
+ switch (p_index) {
+ case 0:
+ return run_in_browser;
+ case 1:
+ return start_http_server;
+ default:
+ ERR_FAIL_V("");
+ }
+ } break;
+
+ case REMOTE_DEBUG_STATE_SERVING: {
+ switch (p_index) {
+ case 0:
+ return run_in_browser;
+ case 1:
+ return reexport_project;
+ case 2:
+ return stop_http_server;
+ default:
+ ERR_FAIL_V("");
+ }
+ } break;
+ }
+
+ return "";
}
Error EditorExportPlatformWeb::run(const Ref<EditorExportPreset> &p_preset, int p_option, int p_debug_flags) {
- if (p_option == 1) {
- MutexLock lock(server_lock);
- server->stop();
- return OK;
+ const uint16_t bind_port = EDITOR_GET("export/web/http_port");
+ // Resolve host if needed.
+ const String bind_host = EDITOR_GET("export/web/http_host");
+ const bool use_tls = EDITOR_GET("export/web/use_tls");
+
+ switch (remote_debug_state) {
+ case REMOTE_DEBUG_STATE_UNAVAILABLE: {
+ return FAILED;
+ } break;
+
+ case REMOTE_DEBUG_STATE_AVAILABLE: {
+ switch (p_option) {
+ // Run in Browser.
+ case 0: {
+ Error err = _export_project(p_preset, p_debug_flags);
+ if (err != OK) {
+ return err;
+ }
+ err = _start_server(bind_host, bind_port, use_tls);
+ if (err != OK) {
+ return err;
+ }
+ return _launch_browser(bind_host, bind_port, use_tls);
+ } break;
+
+ // Start HTTP Server.
+ case 1: {
+ Error err = _export_project(p_preset, p_debug_flags);
+ if (err != OK) {
+ return err;
+ }
+ return _start_server(bind_host, bind_port, use_tls);
+ } break;
+
+ default: {
+ ERR_FAIL_V_MSG(FAILED, vformat(R"(Invalid option "%s" for the current state.)", p_option));
+ }
+ }
+ } break;
+
+ case REMOTE_DEBUG_STATE_SERVING: {
+ switch (p_option) {
+ // Run in Browser.
+ case 0: {
+ Error err = _export_project(p_preset, p_debug_flags);
+ if (err != OK) {
+ return err;
+ }
+ return _launch_browser(bind_host, bind_port, use_tls);
+ } break;
+
+ // Re-export Project.
+ case 1: {
+ return _export_project(p_preset, p_debug_flags);
+ } break;
+
+ // Stop HTTP Server.
+ case 2: {
+ return _stop_server();
+ } break;
+
+ default: {
+ ERR_FAIL_V_MSG(FAILED, vformat(R"(Invalid option "%s" for the current state.)", p_option));
+ }
+ }
+ } break;
}
+ return FAILED;
+}
+
+Error EditorExportPlatformWeb::_export_project(const Ref<EditorExportPreset> &p_preset, int p_debug_flags) {
const String dest = EditorPaths::get_singleton()->get_cache_dir().path_join("web");
Ref<DirAccess> da = DirAccess::create(DirAccess::ACCESS_FILESYSTEM);
if (!da->dir_exists(dest)) {
@@ -615,39 +842,40 @@ Error EditorExportPlatformWeb::run(const Ref<EditorExportPreset> &p_preset, int
DirAccess::remove_file_or_error(basepath + ".wasm");
DirAccess::remove_file_or_error(basepath + ".icon.png");
DirAccess::remove_file_or_error(basepath + ".apple-touch-icon.png");
- return err;
}
+ return err;
+}
- const uint16_t bind_port = EDITOR_GET("export/web/http_port");
- // Resolve host if needed.
- const String bind_host = EDITOR_GET("export/web/http_host");
+Error EditorExportPlatformWeb::_launch_browser(const String &p_bind_host, const uint16_t p_bind_port, const bool p_use_tls) {
+ OS::get_singleton()->shell_open(String((p_use_tls ? "https://" : "http://") + p_bind_host + ":" + itos(p_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;
+}
+
+Error EditorExportPlatformWeb::_start_server(const String &p_bind_host, const uint16_t p_bind_port, const bool p_use_tls) {
IPAddress bind_ip;
- if (bind_host.is_valid_ip_address()) {
- bind_ip = bind_host;
+ if (p_bind_host.is_valid_ip_address()) {
+ bind_ip = p_bind_host;
} else {
- bind_ip = IP::get_singleton()->resolve_hostname(bind_host);
+ bind_ip = IP::get_singleton()->resolve_hostname(p_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'.");
+ ERR_FAIL_COND_V_MSG(!bind_ip.is_valid(), ERR_INVALID_PARAMETER, "Invalid editor setting 'export/web/http_host': '" + p_bind_host + "'. Try using '127.0.0.1'.");
- const bool use_tls = EDITOR_GET("export/web/use_tls");
const String tls_key = EDITOR_GET("export/web/tls_key");
const String tls_cert = EDITOR_GET("export/web/tls_certificate");
// Restart server.
- {
- MutexLock lock(server_lock);
-
- server->stop();
- err = server->listen(bind_port, bind_ip, use_tls, tls_key, tls_cert);
- }
+ server->stop();
+ Error err = server->listen(p_bind_port, bind_ip, p_use_tls, tls_key, tls_cert);
if (err != OK) {
add_message(EXPORT_MESSAGE_ERROR, TTR("Run"), vformat(TTR("Error starting HTTP server: %d."), err));
- return err;
}
+ return err;
+}
- OS::get_singleton()->shell_open(String((use_tls ? "https://" : "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.
+Error EditorExportPlatformWeb::_stop_server() {
+ server->stop();
return OK;
}
@@ -655,21 +883,9 @@ Ref<Texture2D> EditorExportPlatformWeb::get_run_icon() const {
return run_icon;
}
-void EditorExportPlatformWeb::_server_thread_poll(void *data) {
- EditorExportPlatformWeb *ej = static_cast<EditorExportPlatformWeb *>(data);
- while (!ej->server_quit) {
- OS::get_singleton()->delay_usec(6900);
- {
- MutexLock lock(ej->server_lock);
- ej->server->poll();
- }
- }
-}
-
EditorExportPlatformWeb::EditorExportPlatformWeb() {
if (EditorNode::get_singleton()) {
server.instantiate();
- server_thread.start(_server_thread_poll, this);
#ifdef MODULE_SVG_ENABLED
Ref<Image> img = memnew(Image);
@@ -685,18 +901,13 @@ EditorExportPlatformWeb::EditorExportPlatformWeb() {
Ref<Theme> theme = EditorNode::get_singleton()->get_editor_theme();
if (theme.is_valid()) {
stop_icon = theme->get_icon(SNAME("Stop"), EditorStringName(EditorIcons));
+ restart_icon = theme->get_icon(SNAME("Reload"), EditorStringName(EditorIcons));
} else {
stop_icon.instantiate();
+ restart_icon.instantiate();
}
}
}
EditorExportPlatformWeb::~EditorExportPlatformWeb() {
- if (server.is_valid()) {
- server->stop();
- }
- server_quit = true;
- if (server_thread.is_started()) {
- server_thread.wait_to_finish();
- }
}
diff --git a/platform/web/export/export_plugin.h b/platform/web/export/export_plugin.h
index 887000ac45..2f67d8107f 100644
--- a/platform/web/export/export_plugin.h
+++ b/platform/web/export/export_plugin.h
@@ -46,21 +46,28 @@
class EditorExportPlatformWeb : public EditorExportPlatform {
GDCLASS(EditorExportPlatformWeb, EditorExportPlatform);
+ enum RemoteDebugState {
+ REMOTE_DEBUG_STATE_UNAVAILABLE,
+ REMOTE_DEBUG_STATE_AVAILABLE,
+ REMOTE_DEBUG_STATE_SERVING,
+ };
+
Ref<ImageTexture> logo;
Ref<ImageTexture> run_icon;
Ref<ImageTexture> stop_icon;
- int menu_options = 0;
+ Ref<ImageTexture> restart_icon;
+ RemoteDebugState remote_debug_state = REMOTE_DEBUG_STATE_UNAVAILABLE;
Ref<EditorHTTPServer> server;
- bool server_quit = false;
- Mutex server_lock;
- Thread server_thread;
- String _get_template_name(bool p_extension, bool p_debug) const {
+ String _get_template_name(bool p_extension, bool p_thread_support, bool p_debug) const {
String name = "web";
if (p_extension) {
name += "_dlink";
}
+ if (!p_thread_support) {
+ name += "_nothreads";
+ }
if (p_debug) {
name += "_debug.zip";
} else {
@@ -90,13 +97,16 @@ class EditorExportPlatformWeb : public EditorExportPlatform {
}
Error _extract_template(const String &p_template, const String &p_dir, const String &p_name, bool pwa);
- void _replace_strings(HashMap<String, String> p_replaces, Vector<uint8_t> &r_template);
+ void _replace_strings(const HashMap<String, String> &p_replaces, Vector<uint8_t> &r_template);
void _fix_html(Vector<uint8_t> &p_html, const Ref<EditorExportPreset> &p_preset, const String &p_name, bool p_debug, int p_flags, const Vector<SharedObject> p_shared_objects, const Dictionary &p_file_sizes);
Error _add_manifest_icon(const String &p_path, const String &p_icon, int p_size, Array &r_arr);
Error _build_pwa(const Ref<EditorExportPreset> &p_preset, const String p_path, const Vector<SharedObject> &p_shared_objects);
Error _write_or_error(const uint8_t *p_content, int p_len, String p_path);
- static void _server_thread_poll(void *data);
+ Error _export_project(const Ref<EditorExportPreset> &p_preset, int p_debug_flags);
+ Error _launch_browser(const String &p_bind_host, uint16_t p_bind_port, bool p_use_tls);
+ Error _start_server(const String &p_bind_host, uint16_t p_bind_port, bool p_use_tls);
+ Error _stop_server();
public:
virtual void get_preset_features(const Ref<EditorExportPreset> &p_preset, List<String> *r_features) const override;
@@ -114,8 +124,8 @@ public:
virtual bool poll_export() override;
virtual int get_options_count() const override;
- virtual String get_option_label(int p_index) const override { return p_index ? TTR("Stop HTTP Server") : TTR("Run in Browser"); }
- virtual String get_option_tooltip(int p_index) const override { return p_index ? TTR("Stop HTTP Server") : TTR("Run exported HTML in the system's default browser."); }
+ virtual String get_option_label(int p_index) const override;
+ virtual String get_option_tooltip(int p_index) const override;
virtual Ref<ImageTexture> get_option_icon(int p_index) const override;
virtual Error run(const Ref<EditorExportPreset> &p_preset, int p_option, int p_debug_flags) override;
virtual Ref<Texture2D> get_run_icon() const override;
diff --git a/platform/web/export/logo.svg b/platform/web/export/logo.svg
index 567b6f3c77..8d874a143b 100644
--- a/platform/web/export/logo.svg
+++ b/platform/web/export/logo.svg
@@ -1 +1 @@
-<svg height="32" width="32" xmlns="http://www.w3.org/2000/svg"><path d="M7 5h18v21H7z" fill="#fff"/><path d="M3.143 1 5.48 27.504 15.967 31l10.553-3.496L28.857 1zM23.78 9.565H11.473l.275 3.308h11.759l-.911 9.937-6.556 1.808v.02h-.073l-6.61-1.828-.402-5.076h3.195l.234 2.552 3.583.97 3.595-.97.402-4.165H8.788L7.93 6.37h16.145z" fill="#eb6428"/></svg>
+<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32"><path fill="#fff" d="M7 5h18v21H7z"/><path fill="#eb6428" d="M3.143 1 5.48 27.504 15.967 31l10.553-3.496L28.857 1zM23.78 9.565H11.473l.275 3.308h11.759l-.911 9.937-6.556 1.808v.02h-.073l-6.61-1.828-.402-5.076h3.195l.234 2.552 3.583.97 3.595-.97.402-4.165H8.788L7.93 6.37h16.145z"/></svg> \ No newline at end of file
diff --git a/platform/web/export/run_icon.svg b/platform/web/export/run_icon.svg
index fa95e64e79..395db8e597 100644
--- a/platform/web/export/run_icon.svg
+++ b/platform/web/export/run_icon.svg
@@ -1 +1 @@
-<svg height="16" width="16" xml:space="preserve" xmlns="http://www.w3.org/2000/svg"><path d="m2 1 1.09 12.357 4.9 1.63 4.9-1.63L13.98 1zm9.622 3.994h-5.74l.129 1.541h5.482l-.424 4.634-3.057.843v.01h-.033l-3.082-.853-.187-2.367h1.489l.11 1.19 1.67.452 1.676-.453.187-1.942h-5.21l-.4-4.546h7.527z" fill="#eb6428" style="fill:#e0e0e0;fill-opacity:1"/></svg>
+<svg xmlns="http://www.w3.org/2000/svg" xml:space="preserve" width="16" height="16"><path fill="#e0e0e0" d="m2 1 1.09 12.357 4.9 1.63 4.9-1.63L13.98 1zm9.622 3.994h-5.74l.129 1.541h5.482l-.424 4.634-3.057.843v.01h-.033l-3.082-.853-.187-2.367h1.489l.11 1.19 1.67.452 1.676-.453.187-1.942h-5.21l-.4-4.546h7.527z"/></svg> \ No newline at end of file