Path: blob/master/editor/debugger/editor_file_server.cpp
9902 views
/**************************************************************************/1/* editor_file_server.cpp */2/**************************************************************************/3/* This file is part of: */4/* GODOT ENGINE */5/* https://godotengine.org */6/**************************************************************************/7/* Copyright (c) 2014-present Godot Engine contributors (see AUTHORS.md). */8/* Copyright (c) 2007-2014 Juan Linietsky, Ariel Manzur. */9/* */10/* Permission is hereby granted, free of charge, to any person obtaining */11/* a copy of this software and associated documentation files (the */12/* "Software"), to deal in the Software without restriction, including */13/* without limitation the rights to use, copy, modify, merge, publish, */14/* distribute, sublicense, and/or sell copies of the Software, and to */15/* permit persons to whom the Software is furnished to do so, subject to */16/* the following conditions: */17/* */18/* The above copyright notice and this permission notice shall be */19/* included in all copies or substantial portions of the Software. */20/* */21/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */22/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */23/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. */24/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */25/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */26/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */27/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */28/**************************************************************************/2930#include "editor_file_server.h"3132#include "editor/editor_node.h"33#include "editor/export/editor_export_platform.h"34#include "editor/settings/editor_settings.h"3536#define FILESYSTEM_PROTOCOL_VERSION 137#define PASSWORD_LENGTH 3238#define MAX_FILE_BUFFER_SIZE 100 * 1024 * 1024 // 100mb max file buffer size (description of files to update, compressed).3940static void _add_file(String f, const uint64_t &p_modified_time, HashMap<String, uint64_t> &files_to_send, HashMap<String, uint64_t> &cached_files) {41f = f.replace_first("res://", ""); // remove res://42const uint64_t *cached_mt = cached_files.getptr(f);43if (cached_mt && *cached_mt == p_modified_time) {44// File is good, skip it.45cached_files.erase(f); // Erase to mark this file as existing. Remaining files not added to files_to_send will be considered erased here, so they need to be erased in the client too.46return;47}48files_to_send.insert(f, p_modified_time);49}5051void EditorFileServer::_scan_files_changed(EditorFileSystemDirectory *efd, const Vector<String> &p_tags, HashMap<String, uint64_t> &files_to_send, HashMap<String, uint64_t> &cached_files) {52for (int i = 0; i < efd->get_file_count(); i++) {53String f = efd->get_file_path(i);54if (FileAccess::exists(f + ".import")) {55// is imported, determine what to do56// Todo the modified times of remapped files should most likely be kept in EditorFileSystem to speed this up in the future.57Ref<ConfigFile> cf;58cf.instantiate();59Error err = cf->load(f + ".import");6061ERR_CONTINUE(err != OK);62{63uint64_t mt = FileAccess::get_modified_time(f + ".import");64_add_file(f + ".import", mt, files_to_send, cached_files);65}6667if (!cf->has_section("remap")) {68continue;69}7071Vector<String> remaps = cf->get_section_keys("remap");7273for (const String &remap : remaps) {74if (remap == "path") {75String remapped_path = cf->get_value("remap", remap);76uint64_t mt = FileAccess::get_modified_time(remapped_path);77_add_file(remapped_path, mt, files_to_send, cached_files);78} else if (remap.begins_with("path.")) {79String feature = remap.get_slicec('.', 1);80if (p_tags.has(feature)) {81String remapped_path = cf->get_value("remap", remap);82uint64_t mt = FileAccess::get_modified_time(remapped_path);83_add_file(remapped_path, mt, files_to_send, cached_files);84}85}86}87} else {88uint64_t mt = efd->get_file_modified_time(i);89_add_file(f, mt, files_to_send, cached_files);90}91}9293for (int i = 0; i < efd->get_subdir_count(); i++) {94_scan_files_changed(efd->get_subdir(i), p_tags, files_to_send, cached_files);95}96}9798static void _add_custom_file(const String &f, HashMap<String, uint64_t> &files_to_send, HashMap<String, uint64_t> &cached_files) {99if (!FileAccess::exists(f)) {100return;101}102_add_file(f, FileAccess::get_modified_time(f), files_to_send, cached_files);103}104105void EditorFileServer::poll() {106if (!active) {107return;108}109110if (!server->is_connection_available()) {111return;112}113114Ref<StreamPeerTCP> tcp_peer = server->take_connection();115ERR_FAIL_COND(tcp_peer.is_null());116117// Got a connection!118EditorProgress pr("updating_remote_file_system", TTR("Updating assets on target device:"), 105);119120pr.step(TTR("Syncing headers"), 0, true);121print_verbose("EFS: Connecting taken!");122char header[4];123Error err = tcp_peer->get_data((uint8_t *)&header, 4);124ERR_FAIL_COND(err != OK);125ERR_FAIL_COND(header[0] != 'G');126ERR_FAIL_COND(header[1] != 'R');127ERR_FAIL_COND(header[2] != 'F');128ERR_FAIL_COND(header[3] != 'S');129130uint32_t protocol_version = tcp_peer->get_u32();131ERR_FAIL_COND(protocol_version != FILESYSTEM_PROTOCOL_VERSION);132133char cpassword[PASSWORD_LENGTH + 1];134err = tcp_peer->get_data((uint8_t *)cpassword, PASSWORD_LENGTH);135cpassword[PASSWORD_LENGTH] = 0;136ERR_FAIL_COND(err != OK);137print_verbose("EFS: Got password: " + String(cpassword));138ERR_FAIL_COND_MSG(password != cpassword, "Client disconnected because password mismatch.");139140uint32_t tag_count = tcp_peer->get_u32();141print_verbose("EFS: Getting tags: " + itos(tag_count));142143ERR_FAIL_COND(tcp_peer->get_status() != StreamPeerTCP::STATUS_CONNECTED);144Vector<String> tags;145for (uint32_t i = 0; i < tag_count; i++) {146String tag = tcp_peer->get_utf8_string();147print_verbose("EFS: tag #" + itos(i) + ": " + tag);148ERR_FAIL_COND(tcp_peer->get_status() != StreamPeerTCP::STATUS_CONNECTED);149tags.push_back(tag);150}151152uint32_t file_buffer_decompressed_size = tcp_peer->get_32();153HashMap<String, uint64_t> cached_files;154155if (file_buffer_decompressed_size > 0) {156pr.step(TTR("Getting remote file system"), 1, true);157158// Got files cached by client.159uint32_t file_buffer_size = tcp_peer->get_32();160print_verbose("EFS: Getting file buffer: compressed - " + String::humanize_size(file_buffer_size) + " decompressed: " + String::humanize_size(file_buffer_decompressed_size));161162ERR_FAIL_COND(tcp_peer->get_status() != StreamPeerTCP::STATUS_CONNECTED);163ERR_FAIL_COND(file_buffer_size > MAX_FILE_BUFFER_SIZE);164LocalVector<uint8_t> file_buffer;165file_buffer.resize(file_buffer_size);166LocalVector<uint8_t> file_buffer_decompressed;167file_buffer_decompressed.resize(file_buffer_decompressed_size);168169err = tcp_peer->get_data(file_buffer.ptr(), file_buffer_size);170171pr.step(TTR("Decompressing remote file system"), 2, true);172173ERR_FAIL_COND(err != OK);174// Decompress the text with all the files175const int64_t decompressed_size = Compression::decompress(file_buffer_decompressed.ptr(), file_buffer_decompressed.size(), file_buffer.ptr(), file_buffer.size(), Compression::MODE_ZSTD);176ERR_FAIL_COND_MSG(decompressed_size != file_buffer_decompressed.size(), "Error decompressing file buffer. Decompressed size did not match the expected size.");177String files_text = String::utf8((const char *)file_buffer_decompressed.ptr(), file_buffer_decompressed.size());178Vector<String> files = files_text.split("\n");179180print_verbose("EFS: Total cached files received: " + itos(files.size()));181for (int i = 0; i < files.size(); i++) {182if (files[i].get_slice_count("::") != 2) {183continue;184}185String file = files[i].get_slice("::", 0);186uint64_t modified_time = files[i].get_slice("::", 1).to_int();187188cached_files.insert(file, modified_time);189}190} else {191// Client does not have any files stored.192}193194pr.step(TTR("Scanning for local changes"), 3, true);195196print_verbose("EFS: Scanning changes:");197198HashMap<String, uint64_t> files_to_send;199// Scan files to send.200_scan_files_changed(EditorFileSystem::get_singleton()->get_filesystem(), tags, files_to_send, cached_files);201// Add forced export files202Vector<String> forced_export = EditorExportPlatform::get_forced_export_files(Ref<EditorExportPreset>());203for (int i = 0; i < forced_export.size(); i++) {204_add_custom_file(forced_export[i], files_to_send, cached_files);205}206207_add_custom_file("res://project.godot", files_to_send, cached_files);208// Check which files were removed and also add them209for (KeyValue<String, uint64_t> K : cached_files) {210if (!files_to_send.has(K.key)) {211files_to_send.insert(K.key, 0); //0 means removed212}213}214215tcp_peer->put_32(files_to_send.size());216217print_verbose("EFS: Sending list of changed files.");218pr.step(TTR("Sending list of changed files:"), 4, true);219220// Send list of changed files first, to ensure that if connecting breaks, the client is not found in a broken state.221for (KeyValue<String, uint64_t> K : files_to_send) {222tcp_peer->put_utf8_string(K.key);223tcp_peer->put_64(K.value);224}225226print_verbose("EFS: Sending " + itos(files_to_send.size()) + " files.");227228int idx = 0;229for (KeyValue<String, uint64_t> K : files_to_send) {230pr.step(TTR("Sending file:") + " " + K.key.get_file(), 5 + idx * 100 / files_to_send.size(), false);231idx++;232233if (K.value == 0 || !FileAccess::exists("res://" + K.key)) { // File was removed234continue;235}236237Vector<uint8_t> array = FileAccess::_get_file_as_bytes("res://" + K.key);238tcp_peer->put_64(array.size());239tcp_peer->put_data(array.ptr(), array.size());240ERR_FAIL_COND(tcp_peer->get_status() != StreamPeerTCP::STATUS_CONNECTED);241}242243tcp_peer->put_data((const uint8_t *)"GEND", 4); // End marker.244245print_verbose("EFS: Done.");246}247248void EditorFileServer::start() {249if (active) {250stop();251}252port = EDITOR_GET("filesystem/file_server/port");253password = EDITOR_GET("filesystem/file_server/password");254Error err = server->listen(port);255ERR_FAIL_COND_MSG(err != OK, "EditorFileServer: Unable to listen on port " + itos(port));256active = true;257}258259bool EditorFileServer::is_active() const {260return active;261}262263void EditorFileServer::stop() {264if (active) {265server->stop();266active = false;267}268}269270EditorFileServer::EditorFileServer() {271server.instantiate();272}273274EditorFileServer::~EditorFileServer() {275stop();276}277278279