editor_import_blend_runner.cpp 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394
  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. #include "core/io/http_client.h"
  32. #include "editor/editor_node.h"
  33. #include "editor/file_system/editor_file_system.h"
  34. #include "editor/settings/editor_settings.h"
  35. static constexpr char PYTHON_SCRIPT_RPC[] = R"(
  36. import bpy, sys, threading
  37. from xmlrpc.server import SimpleXMLRPCServer
  38. req = threading.Condition()
  39. res = threading.Condition()
  40. info = None
  41. export_err = None
  42. def xmlrpc_server():
  43. server = SimpleXMLRPCServer(('127.0.0.1', %d))
  44. server.register_function(export_gltf)
  45. server.serve_forever()
  46. def export_gltf(opts):
  47. with req:
  48. global info
  49. info = ('export_gltf', opts)
  50. req.notify()
  51. with res:
  52. res.wait()
  53. if export_err:
  54. raise export_err
  55. # Important to return a value to prevent the error 'cannot marshal None unless allow_none is enabled'.
  56. return 'BLENDER_GODOT_EXPORT_SUCCESSFUL'
  57. if bpy.app.version < (3, 0, 0):
  58. print('Blender 3.0 or higher is required.', file=sys.stderr)
  59. threading.Thread(target=xmlrpc_server).start()
  60. while True:
  61. with req:
  62. while info is None:
  63. req.wait()
  64. method, opts = info
  65. if method == 'export_gltf':
  66. try:
  67. export_err = None
  68. bpy.ops.wm.open_mainfile(filepath=opts['path'])
  69. if opts['unpack_all']:
  70. bpy.ops.file.unpack_all(method='USE_LOCAL')
  71. bpy.ops.export_scene.gltf(**opts['gltf_options'])
  72. except Exception as e:
  73. export_err = e
  74. info = None
  75. with res:
  76. res.notify()
  77. )";
  78. static constexpr char PYTHON_SCRIPT_DIRECT[] = R"(
  79. import bpy, sys
  80. opts = %s
  81. if bpy.app.version < (3, 0, 0):
  82. print('Blender 3.0 or higher is required.', file=sys.stderr)
  83. bpy.ops.wm.open_mainfile(filepath=opts['path'])
  84. if opts['unpack_all']:
  85. bpy.ops.file.unpack_all(method='USE_LOCAL')
  86. bpy.ops.export_scene.gltf(**opts['gltf_options'])
  87. )";
  88. String dict_to_python(const Dictionary &p_dict) {
  89. String entries;
  90. for (const KeyValue<Variant, Variant> &kv : p_dict) {
  91. const String &key = kv.key;
  92. String value;
  93. const Variant &raw_value = kv.value;
  94. switch (raw_value.get_type()) {
  95. case Variant::Type::BOOL: {
  96. value = raw_value ? "True" : "False";
  97. break;
  98. }
  99. case Variant::Type::STRING:
  100. case Variant::Type::STRING_NAME: {
  101. value = raw_value;
  102. value = vformat("'%s'", value.c_escape());
  103. break;
  104. }
  105. case Variant::Type::DICTIONARY: {
  106. value = dict_to_python(raw_value);
  107. break;
  108. }
  109. default: {
  110. ERR_FAIL_V_MSG("", vformat("Unhandled Variant type %s for python dictionary", Variant::get_type_name(raw_value.get_type())));
  111. }
  112. }
  113. entries += vformat("'%s': %s,", key, value);
  114. }
  115. return vformat("{%s}", entries);
  116. }
  117. String dict_to_xmlrpc(const Dictionary &p_dict) {
  118. String members;
  119. for (const KeyValue<Variant, Variant> &kv : p_dict) {
  120. const String &key = kv.key;
  121. String value;
  122. const Variant &raw_value = kv.value;
  123. switch (raw_value.get_type()) {
  124. case Variant::Type::BOOL: {
  125. value = vformat("<boolean>%d</boolean>", raw_value ? 1 : 0);
  126. break;
  127. }
  128. case Variant::Type::STRING:
  129. case Variant::Type::STRING_NAME: {
  130. value = raw_value;
  131. value = vformat("<string>%s</string>", value.xml_escape());
  132. break;
  133. }
  134. case Variant::Type::DICTIONARY: {
  135. value = dict_to_xmlrpc(raw_value);
  136. break;
  137. }
  138. default: {
  139. ERR_FAIL_V_MSG("", vformat("Unhandled Variant type %s for XMLRPC", Variant::get_type_name(raw_value.get_type())));
  140. }
  141. }
  142. members += vformat("<member><name>%s</name><value>%s</value></member>", key, value);
  143. }
  144. return vformat("<struct>%s</struct>", members);
  145. }
  146. Error EditorImportBlendRunner::start_blender(const String &p_python_script, bool p_blocking) {
  147. String blender_path = EDITOR_GET("filesystem/import/blender/blender_path");
  148. List<String> args;
  149. args.push_back("--background");
  150. args.push_back("--python-exit-code");
  151. args.push_back("1");
  152. args.push_back("--python-expr");
  153. args.push_back(p_python_script);
  154. Error err;
  155. String str;
  156. if (p_blocking) {
  157. int exitcode = 0;
  158. err = OS::get_singleton()->execute(blender_path, args, &str, &exitcode, true);
  159. if (exitcode != 0) {
  160. print_error(vformat("Blender import failed: %s.", str));
  161. return FAILED;
  162. }
  163. } else {
  164. err = OS::get_singleton()->create_process(blender_path, args, &blender_pid);
  165. }
  166. return err;
  167. }
  168. Error EditorImportBlendRunner::do_import(const Dictionary &p_options) {
  169. if (is_using_rpc()) {
  170. Error err = do_import_rpc(p_options);
  171. if (err != OK) {
  172. // Retry without using RPC (slow, but better than the import failing completely).
  173. if (err == ERR_CONNECTION_ERROR) {
  174. // Disable RPC if the connection could not be established.
  175. 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));
  176. EditorSettings::get_singleton()->set_manually("filesystem/import/blender/rpc_port", 0);
  177. rpc_port = 0;
  178. }
  179. if (err != ERR_QUERY_FAILED) {
  180. err = do_import_direct(p_options);
  181. }
  182. }
  183. return err;
  184. } else {
  185. return do_import_direct(p_options);
  186. }
  187. }
  188. HTTPClient::Status EditorImportBlendRunner::connect_blender_rpc(const Ref<HTTPClient> &p_client, int p_timeout_usecs) {
  189. p_client->connect_to_host("127.0.0.1", rpc_port);
  190. HTTPClient::Status status = p_client->get_status();
  191. int attempts = 1;
  192. int wait_usecs = 1000;
  193. bool done = false;
  194. while (!done) {
  195. OS::get_singleton()->delay_usec(wait_usecs);
  196. status = p_client->get_status();
  197. switch (status) {
  198. case HTTPClient::STATUS_RESOLVING:
  199. case HTTPClient::STATUS_CONNECTING: {
  200. p_client->poll();
  201. break;
  202. }
  203. case HTTPClient::STATUS_CONNECTED: {
  204. done = true;
  205. break;
  206. }
  207. default: {
  208. if (attempts * wait_usecs < p_timeout_usecs) {
  209. p_client->connect_to_host("127.0.0.1", rpc_port);
  210. } else {
  211. return status;
  212. }
  213. }
  214. }
  215. }
  216. return status;
  217. }
  218. Error EditorImportBlendRunner::do_import_rpc(const Dictionary &p_options) {
  219. kill_timer->stop();
  220. // Start Blender if not already running.
  221. if (!is_running()) {
  222. // Start an XML RPC server on the given port.
  223. String python = vformat(PYTHON_SCRIPT_RPC, rpc_port);
  224. Error err = start_blender(python, false);
  225. if (err != OK || blender_pid == 0) {
  226. return FAILED;
  227. }
  228. }
  229. // Convert options to XML body.
  230. String xml_options = dict_to_xmlrpc(p_options);
  231. String xml_body = vformat("<?xml version=\"1.0\"?><methodCall><methodName>export_gltf</methodName><params><param><value>%s</value></param></params></methodCall>", xml_options);
  232. // Connect to RPC server.
  233. Ref<HTTPClient> client = HTTPClient::create();
  234. HTTPClient::Status status = connect_blender_rpc(client, 1000000);
  235. if (status != HTTPClient::STATUS_CONNECTED) {
  236. ERR_FAIL_V_MSG(ERR_CONNECTION_ERROR, vformat("Unexpected status during RPC connection: %d", status));
  237. }
  238. // Send XML request.
  239. PackedByteArray xml_buffer = xml_body.to_utf8_buffer();
  240. Error err = client->request(HTTPClient::METHOD_POST, "/", Vector<String>(), xml_buffer.ptr(), xml_buffer.size());
  241. if (err != OK) {
  242. ERR_FAIL_V_MSG(err, vformat("Unable to send RPC request: %d", err));
  243. }
  244. // Wait for response.
  245. bool done = false;
  246. PackedByteArray response;
  247. while (!done) {
  248. status = client->get_status();
  249. switch (status) {
  250. case HTTPClient::STATUS_REQUESTING: {
  251. client->poll();
  252. break;
  253. }
  254. case HTTPClient::STATUS_BODY: {
  255. client->poll();
  256. response.append_array(client->read_response_body_chunk());
  257. break;
  258. }
  259. case HTTPClient::STATUS_CONNECTED: {
  260. done = true;
  261. break;
  262. }
  263. default: {
  264. ERR_FAIL_V_MSG(ERR_CONNECTION_ERROR, vformat("Unexpected status during RPC response: %d", status));
  265. }
  266. }
  267. }
  268. String response_text = "No response from Blender.";
  269. if (response.size() > 0) {
  270. response_text = String::utf8((const char *)response.ptr(), response.size());
  271. }
  272. if (client->get_response_code() != HTTPClient::RESPONSE_OK) {
  273. ERR_FAIL_V_MSG(ERR_QUERY_FAILED, vformat("Error received from Blender - status code: %s, error: %s", client->get_response_code(), response_text));
  274. } else if (response_text.find("BLENDER_GODOT_EXPORT_SUCCESSFUL") < 0) {
  275. // Previous versions of Godot used a Python script where the RPC function did not return
  276. // a value, causing the error 'cannot marshal None unless allow_none is enabled'.
  277. // If an older version of Godot is running and has started Blender with this script,
  278. // we will receive the error, but there's a good chance that the import was successful.
  279. // We are discarding this error to maintain backward compatibility and prevent situations
  280. // where the user needs to close the older version of Godot or kill Blender.
  281. if (response_text.find("cannot marshal None unless allow_none is enabled") < 0) {
  282. String error_message;
  283. if (_extract_error_message_xml(response, error_message)) {
  284. ERR_FAIL_V_MSG(ERR_QUERY_FAILED, vformat("Blender exportation failed: %s", error_message));
  285. } else {
  286. ERR_FAIL_V_MSG(ERR_QUERY_FAILED, vformat("Blender exportation failed: %s", response_text));
  287. }
  288. }
  289. }
  290. return OK;
  291. }
  292. bool EditorImportBlendRunner::_extract_error_message_xml(const Vector<uint8_t> &p_response_data, String &r_error_message) {
  293. // Based on RPC Xml spec from: https://xmlrpc.com/spec.md
  294. Ref<XMLParser> parser = memnew(XMLParser);
  295. Error err = parser->open_buffer(p_response_data);
  296. if (err) {
  297. return false;
  298. }
  299. r_error_message = String();
  300. while (parser->read() == OK) {
  301. if (parser->get_node_type() == XMLParser::NODE_TEXT) {
  302. if (parser->get_node_data().size()) {
  303. if (r_error_message.size()) {
  304. r_error_message += " ";
  305. }
  306. r_error_message += parser->get_node_data().trim_suffix("\n");
  307. }
  308. }
  309. }
  310. return r_error_message.size();
  311. }
  312. Error EditorImportBlendRunner::do_import_direct(const Dictionary &p_options) {
  313. // Export glTF directly.
  314. String python = vformat(PYTHON_SCRIPT_DIRECT, dict_to_python(p_options));
  315. Error err = start_blender(python, true);
  316. if (err != OK) {
  317. return err;
  318. }
  319. return OK;
  320. }
  321. void EditorImportBlendRunner::_resources_reimported(const PackedStringArray &p_files) {
  322. if (is_running()) {
  323. // After a batch of imports is done, wait a few seconds before trying to kill blender,
  324. // in case of having multiple imports trigger in quick succession.
  325. kill_timer->start();
  326. }
  327. }
  328. void EditorImportBlendRunner::_kill_blender() {
  329. kill_timer->stop();
  330. if (is_running()) {
  331. OS::get_singleton()->kill(blender_pid);
  332. }
  333. blender_pid = 0;
  334. }
  335. void EditorImportBlendRunner::_notification(int p_what) {
  336. switch (p_what) {
  337. case NOTIFICATION_PREDELETE: {
  338. _kill_blender();
  339. break;
  340. }
  341. }
  342. }
  343. EditorImportBlendRunner *EditorImportBlendRunner::singleton = nullptr;
  344. EditorImportBlendRunner::EditorImportBlendRunner() {
  345. ERR_FAIL_COND_MSG(singleton != nullptr, "EditorImportBlendRunner already created.");
  346. singleton = this;
  347. rpc_port = EDITOR_GET("filesystem/import/blender/rpc_port");
  348. kill_timer = memnew(Timer);
  349. add_child(kill_timer);
  350. kill_timer->set_one_shot(true);
  351. kill_timer->set_wait_time(EDITOR_GET("filesystem/import/blender/rpc_server_uptime"));
  352. kill_timer->connect("timeout", callable_mp(this, &EditorImportBlendRunner::_kill_blender));
  353. EditorFileSystem::get_singleton()->connect("resources_reimported", callable_mp(this, &EditorImportBlendRunner::_resources_reimported));
  354. }