editor_import_blend_runner.cpp 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396
  1. /**************************************************************************/
  2. /* editor_import_blend_runner.cpp */
  3. /**************************************************************************/
  4. /* This file is part of: */
  5. /* GODOT ENGINE */
  6. /* https://godotengine.org */
  7. /**************************************************************************/
  8. /* Copyright (c) 2014-present Godot Engine contributors (see AUTHORS.md). */
  9. /* Copyright (c) 2007-2014 Juan Linietsky, Ariel Manzur. */
  10. /* */
  11. /* Permission is hereby granted, free of charge, to any person obtaining */
  12. /* a copy of this software and associated documentation files (the */
  13. /* "Software"), to deal in the Software without restriction, including */
  14. /* without limitation the rights to use, copy, modify, merge, publish, */
  15. /* distribute, sublicense, and/or sell copies of the Software, and to */
  16. /* permit persons to whom the Software is furnished to do so, subject to */
  17. /* the following conditions: */
  18. /* */
  19. /* The above copyright notice and this permission notice shall be */
  20. /* included in all copies or substantial portions of the Software. */
  21. /* */
  22. /* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */
  23. /* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */
  24. /* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. */
  25. /* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */
  26. /* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */
  27. /* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */
  28. /* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */
  29. /**************************************************************************/
  30. #include "editor_import_blend_runner.h"
  31. #ifdef TOOLS_ENABLED
  32. #include "core/io/http_client.h"
  33. #include "editor/editor_file_system.h"
  34. #include "editor/editor_node.h"
  35. #include "editor/editor_settings.h"
  36. static constexpr char PYTHON_SCRIPT_RPC[] = R"(
  37. import bpy, sys, threading
  38. from xmlrpc.server import SimpleXMLRPCServer
  39. req = threading.Condition()
  40. res = threading.Condition()
  41. info = None
  42. export_err = None
  43. def xmlrpc_server():
  44. server = SimpleXMLRPCServer(('127.0.0.1', %d))
  45. server.register_function(export_gltf)
  46. server.serve_forever()
  47. def export_gltf(opts):
  48. with req:
  49. global info
  50. info = ('export_gltf', opts)
  51. req.notify()
  52. with res:
  53. res.wait()
  54. if export_err:
  55. raise export_err
  56. # Important to return a value to prevent the error 'cannot marshal None unless allow_none is enabled'.
  57. return 'BLENDER_GODOT_EXPORT_SUCCESSFUL'
  58. if bpy.app.version < (3, 0, 0):
  59. print('Blender 3.0 or higher is required.', file=sys.stderr)
  60. threading.Thread(target=xmlrpc_server).start()
  61. while True:
  62. with req:
  63. while info is None:
  64. req.wait()
  65. method, opts = info
  66. if method == 'export_gltf':
  67. try:
  68. export_err = None
  69. bpy.ops.wm.open_mainfile(filepath=opts['path'])
  70. if opts['unpack_all']:
  71. bpy.ops.file.unpack_all(method='USE_LOCAL')
  72. bpy.ops.export_scene.gltf(**opts['gltf_options'])
  73. except Exception as e:
  74. export_err = e
  75. info = None
  76. with res:
  77. res.notify()
  78. )";
  79. static constexpr char PYTHON_SCRIPT_DIRECT[] = R"(
  80. import bpy, sys
  81. opts = %s
  82. if bpy.app.version < (3, 0, 0):
  83. print('Blender 3.0 or higher is required.', file=sys.stderr)
  84. bpy.ops.wm.open_mainfile(filepath=opts['path'])
  85. if opts['unpack_all']:
  86. bpy.ops.file.unpack_all(method='USE_LOCAL')
  87. bpy.ops.export_scene.gltf(**opts['gltf_options'])
  88. )";
  89. String dict_to_python(const Dictionary &p_dict) {
  90. String entries;
  91. Array dict_keys = p_dict.keys();
  92. for (int i = 0; i < dict_keys.size(); i++) {
  93. const String key = dict_keys[i];
  94. String value;
  95. Variant raw_value = p_dict[key];
  96. switch (raw_value.get_type()) {
  97. case Variant::Type::BOOL: {
  98. value = raw_value ? "True" : "False";
  99. break;
  100. }
  101. case Variant::Type::STRING:
  102. case Variant::Type::STRING_NAME: {
  103. value = raw_value;
  104. value = vformat("'%s'", value.c_escape());
  105. break;
  106. }
  107. case Variant::Type::DICTIONARY: {
  108. value = dict_to_python(raw_value);
  109. break;
  110. }
  111. default: {
  112. ERR_FAIL_V_MSG("", vformat("Unhandled Variant type %s for python dictionary", Variant::get_type_name(raw_value.get_type())));
  113. }
  114. }
  115. entries += vformat("'%s': %s,", key, value);
  116. }
  117. return vformat("{%s}", entries);
  118. }
  119. String dict_to_xmlrpc(const Dictionary &p_dict) {
  120. String members;
  121. Array dict_keys = p_dict.keys();
  122. for (int i = 0; i < dict_keys.size(); i++) {
  123. const String key = dict_keys[i];
  124. String value;
  125. Variant raw_value = p_dict[key];
  126. switch (raw_value.get_type()) {
  127. case Variant::Type::BOOL: {
  128. value = vformat("<boolean>%d</boolean>", raw_value ? 1 : 0);
  129. break;
  130. }
  131. case Variant::Type::STRING:
  132. case Variant::Type::STRING_NAME: {
  133. value = raw_value;
  134. value = vformat("<string>%s</string>", value.xml_escape());
  135. break;
  136. }
  137. case Variant::Type::DICTIONARY: {
  138. value = dict_to_xmlrpc(raw_value);
  139. break;
  140. }
  141. default: {
  142. ERR_FAIL_V_MSG("", vformat("Unhandled Variant type %s for XMLRPC", Variant::get_type_name(raw_value.get_type())));
  143. }
  144. }
  145. members += vformat("<member><name>%s</name><value>%s</value></member>", key, value);
  146. }
  147. return vformat("<struct>%s</struct>", members);
  148. }
  149. Error EditorImportBlendRunner::start_blender(const String &p_python_script, bool p_blocking) {
  150. String blender_path = EDITOR_GET("filesystem/import/blender/blender_path");
  151. List<String> args;
  152. args.push_back("--background");
  153. args.push_back("--python-expr");
  154. args.push_back(p_python_script);
  155. Error err;
  156. if (p_blocking) {
  157. int exitcode = 0;
  158. err = OS::get_singleton()->execute(blender_path, args, nullptr, &exitcode);
  159. if (exitcode != 0) {
  160. return FAILED;
  161. }
  162. } else {
  163. err = OS::get_singleton()->create_process(blender_path, args, &blender_pid);
  164. }
  165. return err;
  166. }
  167. Error EditorImportBlendRunner::do_import(const Dictionary &p_options) {
  168. if (is_using_rpc()) {
  169. Error err = do_import_rpc(p_options);
  170. if (err != OK) {
  171. // Retry without using RPC (slow, but better than the import failing completely).
  172. if (err == ERR_CONNECTION_ERROR) {
  173. // Disable RPC if the connection could not be established.
  174. print_error(vformat("Failed to connect to Blender via RPC, switching to direct imports of .blend files. Check your proxy and firewall settings, then RPC can be re-enabled by changing the editor setting `filesystem/import/blender/rpc_port` to %d.", rpc_port));
  175. EditorSettings::get_singleton()->set_manually("filesystem/import/blender/rpc_port", 0);
  176. rpc_port = 0;
  177. }
  178. if (err != ERR_QUERY_FAILED) {
  179. err = do_import_direct(p_options);
  180. }
  181. }
  182. return err;
  183. } else {
  184. return do_import_direct(p_options);
  185. }
  186. }
  187. HTTPClient::Status EditorImportBlendRunner::connect_blender_rpc(const Ref<HTTPClient> &p_client, int p_timeout_usecs) {
  188. p_client->connect_to_host("127.0.0.1", rpc_port);
  189. HTTPClient::Status status = p_client->get_status();
  190. int attempts = 1;
  191. int wait_usecs = 1000;
  192. bool done = false;
  193. while (!done) {
  194. OS::get_singleton()->delay_usec(wait_usecs);
  195. status = p_client->get_status();
  196. switch (status) {
  197. case HTTPClient::STATUS_RESOLVING:
  198. case HTTPClient::STATUS_CONNECTING: {
  199. p_client->poll();
  200. break;
  201. }
  202. case HTTPClient::STATUS_CONNECTED: {
  203. done = true;
  204. break;
  205. }
  206. default: {
  207. if (attempts * wait_usecs < p_timeout_usecs) {
  208. p_client->connect_to_host("127.0.0.1", rpc_port);
  209. } else {
  210. return status;
  211. }
  212. }
  213. }
  214. }
  215. return status;
  216. }
  217. Error EditorImportBlendRunner::do_import_rpc(const Dictionary &p_options) {
  218. kill_timer->stop();
  219. // Start Blender if not already running.
  220. if (!is_running()) {
  221. // Start an XML RPC server on the given port.
  222. String python = vformat(PYTHON_SCRIPT_RPC, rpc_port);
  223. Error err = start_blender(python, false);
  224. if (err != OK || blender_pid == 0) {
  225. return FAILED;
  226. }
  227. }
  228. // Convert options to XML body.
  229. String xml_options = dict_to_xmlrpc(p_options);
  230. String xml_body = vformat("<?xml version=\"1.0\"?><methodCall><methodName>export_gltf</methodName><params><param><value>%s</value></param></params></methodCall>", xml_options);
  231. // Connect to RPC server.
  232. Ref<HTTPClient> client = HTTPClient::create();
  233. HTTPClient::Status status = connect_blender_rpc(client, 1000000);
  234. if (status != HTTPClient::STATUS_CONNECTED) {
  235. ERR_FAIL_V_MSG(ERR_CONNECTION_ERROR, vformat("Unexpected status during RPC connection: %d", status));
  236. }
  237. // Send XML request.
  238. PackedByteArray xml_buffer = xml_body.to_utf8_buffer();
  239. Error err = client->request(HTTPClient::METHOD_POST, "/", Vector<String>(), xml_buffer.ptr(), xml_buffer.size());
  240. if (err != OK) {
  241. ERR_FAIL_V_MSG(err, vformat("Unable to send RPC request: %d", err));
  242. }
  243. // Wait for response.
  244. bool done = false;
  245. PackedByteArray response;
  246. while (!done) {
  247. status = client->get_status();
  248. switch (status) {
  249. case HTTPClient::STATUS_REQUESTING: {
  250. client->poll();
  251. break;
  252. }
  253. case HTTPClient::STATUS_BODY: {
  254. client->poll();
  255. response.append_array(client->read_response_body_chunk());
  256. break;
  257. }
  258. case HTTPClient::STATUS_CONNECTED: {
  259. done = true;
  260. break;
  261. }
  262. default: {
  263. ERR_FAIL_V_MSG(ERR_CONNECTION_ERROR, vformat("Unexpected status during RPC response: %d", status));
  264. }
  265. }
  266. }
  267. String response_text = "No response from Blender.";
  268. if (response.size() > 0) {
  269. response_text = String::utf8((const char *)response.ptr(), response.size());
  270. }
  271. if (client->get_response_code() != HTTPClient::RESPONSE_OK) {
  272. ERR_FAIL_V_MSG(ERR_QUERY_FAILED, vformat("Error received from Blender - status code: %s, error: %s", client->get_response_code(), response_text));
  273. } else if (response_text.find("BLENDER_GODOT_EXPORT_SUCCESSFUL") < 0) {
  274. // Previous versions of Godot used a Python script where the RPC function did not return
  275. // a value, causing the error 'cannot marshal None unless allow_none is enabled'.
  276. // If an older version of Godot is running and has started Blender with this script,
  277. // we will receive the error, but there's a good chance that the import was successful.
  278. // We are discarding this error to maintain backward compatibility and prevent situations
  279. // where the user needs to close the older version of Godot or kill Blender.
  280. if (response_text.find("cannot marshal None unless allow_none is enabled") < 0) {
  281. String error_message;
  282. if (_extract_error_message_xml(response, error_message)) {
  283. ERR_FAIL_V_MSG(ERR_QUERY_FAILED, vformat("Blender exportation failed: %s", error_message));
  284. } else {
  285. ERR_FAIL_V_MSG(ERR_QUERY_FAILED, vformat("Blender exportation failed: %s", response_text));
  286. }
  287. }
  288. }
  289. return OK;
  290. }
  291. bool EditorImportBlendRunner::_extract_error_message_xml(const Vector<uint8_t> &p_response_data, String &r_error_message) {
  292. // Based on RPC Xml spec from: https://xmlrpc.com/spec.md
  293. Ref<XMLParser> parser = memnew(XMLParser);
  294. Error err = parser->open_buffer(p_response_data);
  295. if (err) {
  296. return false;
  297. }
  298. r_error_message = String();
  299. while (parser->read() == OK) {
  300. if (parser->get_node_type() == XMLParser::NODE_TEXT) {
  301. if (parser->get_node_data().size()) {
  302. if (r_error_message.size()) {
  303. r_error_message += " ";
  304. }
  305. r_error_message += parser->get_node_data().trim_suffix("\n");
  306. }
  307. }
  308. }
  309. return r_error_message.size();
  310. }
  311. Error EditorImportBlendRunner::do_import_direct(const Dictionary &p_options) {
  312. // Export glTF directly.
  313. String python = vformat(PYTHON_SCRIPT_DIRECT, dict_to_python(p_options));
  314. Error err = start_blender(python, true);
  315. if (err != OK) {
  316. return err;
  317. }
  318. return OK;
  319. }
  320. void EditorImportBlendRunner::_resources_reimported(const PackedStringArray &p_files) {
  321. if (is_running()) {
  322. // After a batch of imports is done, wait a few seconds before trying to kill blender,
  323. // in case of having multiple imports trigger in quick succession.
  324. kill_timer->start();
  325. }
  326. }
  327. void EditorImportBlendRunner::_kill_blender() {
  328. kill_timer->stop();
  329. if (is_running()) {
  330. OS::get_singleton()->kill(blender_pid);
  331. }
  332. blender_pid = 0;
  333. }
  334. void EditorImportBlendRunner::_notification(int p_what) {
  335. switch (p_what) {
  336. case NOTIFICATION_PREDELETE: {
  337. _kill_blender();
  338. break;
  339. }
  340. }
  341. }
  342. EditorImportBlendRunner *EditorImportBlendRunner::singleton = nullptr;
  343. EditorImportBlendRunner::EditorImportBlendRunner() {
  344. ERR_FAIL_COND_MSG(singleton != nullptr, "EditorImportBlendRunner already created.");
  345. singleton = this;
  346. rpc_port = EDITOR_GET("filesystem/import/blender/rpc_port");
  347. kill_timer = memnew(Timer);
  348. add_child(kill_timer);
  349. kill_timer->set_one_shot(true);
  350. kill_timer->set_wait_time(EDITOR_GET("filesystem/import/blender/rpc_server_uptime"));
  351. kill_timer->connect("timeout", callable_mp(this, &EditorImportBlendRunner::_kill_blender));
  352. EditorFileSystem::get_singleton()->connect("resources_reimported", callable_mp(this, &EditorImportBlendRunner::_resources_reimported));
  353. }
  354. #endif // TOOLS_ENABLED