editor_import_blend_runner.cpp 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344
  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. 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 bpy.app.version < (3, 0, 0):
  54. print('Blender 3.0 or higher is required.', file=sys.stderr)
  55. threading.Thread(target=xmlrpc_server).start()
  56. while True:
  57. with req:
  58. while info is None:
  59. req.wait()
  60. method, opts = info
  61. if method == 'export_gltf':
  62. try:
  63. bpy.ops.wm.open_mainfile(filepath=opts['path'])
  64. if opts['unpack_all']:
  65. bpy.ops.file.unpack_all(method='USE_LOCAL')
  66. bpy.ops.export_scene.gltf(**opts['gltf_options'])
  67. except:
  68. pass
  69. info = None
  70. with res:
  71. res.notify()
  72. )";
  73. static constexpr char PYTHON_SCRIPT_DIRECT[] = R"(
  74. import bpy, sys
  75. opts = %s
  76. if bpy.app.version < (3, 0, 0):
  77. print('Blender 3.0 or higher is required.', file=sys.stderr)
  78. bpy.ops.wm.open_mainfile(filepath=opts['path'])
  79. if opts['unpack_all']:
  80. bpy.ops.file.unpack_all(method='USE_LOCAL')
  81. bpy.ops.export_scene.gltf(**opts['gltf_options'])
  82. )";
  83. String dict_to_python(const Dictionary &p_dict) {
  84. String entries;
  85. Array dict_keys = p_dict.keys();
  86. for (int i = 0; i < dict_keys.size(); i++) {
  87. const String key = dict_keys[i];
  88. String value;
  89. Variant raw_value = p_dict[key];
  90. switch (raw_value.get_type()) {
  91. case Variant::Type::BOOL: {
  92. value = raw_value ? "True" : "False";
  93. break;
  94. }
  95. case Variant::Type::STRING:
  96. case Variant::Type::STRING_NAME: {
  97. value = raw_value;
  98. value = vformat("'%s'", value.c_escape());
  99. break;
  100. }
  101. case Variant::Type::DICTIONARY: {
  102. value = dict_to_python(raw_value);
  103. break;
  104. }
  105. default: {
  106. ERR_FAIL_V_MSG("", vformat("Unhandled Variant type %s for python dictionary", Variant::get_type_name(raw_value.get_type())));
  107. }
  108. }
  109. entries += vformat("'%s': %s,", key, value);
  110. }
  111. return vformat("{%s}", entries);
  112. }
  113. String dict_to_xmlrpc(const Dictionary &p_dict) {
  114. String members;
  115. Array dict_keys = p_dict.keys();
  116. for (int i = 0; i < dict_keys.size(); i++) {
  117. const String key = dict_keys[i];
  118. String value;
  119. Variant raw_value = p_dict[key];
  120. switch (raw_value.get_type()) {
  121. case Variant::Type::BOOL: {
  122. value = vformat("<boolean>%d</boolean>", raw_value ? 1 : 0);
  123. break;
  124. }
  125. case Variant::Type::STRING:
  126. case Variant::Type::STRING_NAME: {
  127. value = raw_value;
  128. value = vformat("<string>%s</string>", value.xml_escape());
  129. break;
  130. }
  131. case Variant::Type::DICTIONARY: {
  132. value = dict_to_xmlrpc(raw_value);
  133. break;
  134. }
  135. default: {
  136. ERR_FAIL_V_MSG("", vformat("Unhandled Variant type %s for XMLRPC", Variant::get_type_name(raw_value.get_type())));
  137. }
  138. }
  139. members += vformat("<member><name>%s</name><value>%s</value></member>", key, value);
  140. }
  141. return vformat("<struct>%s</struct>", members);
  142. }
  143. Error EditorImportBlendRunner::start_blender(const String &p_python_script, bool p_blocking) {
  144. String blender_path = EDITOR_GET("filesystem/import/blender/blender3_path");
  145. #ifdef WINDOWS_ENABLED
  146. blender_path = blender_path.path_join("blender.exe");
  147. #else
  148. blender_path = blender_path.path_join("blender");
  149. #endif
  150. List<String> args;
  151. args.push_back("--background");
  152. args.push_back("--python-expr");
  153. args.push_back(p_python_script);
  154. Error err;
  155. if (p_blocking) {
  156. int exitcode = 0;
  157. err = OS::get_singleton()->execute(blender_path, args, nullptr, &exitcode);
  158. if (exitcode != 0) {
  159. return FAILED;
  160. }
  161. } else {
  162. err = OS::get_singleton()->create_process(blender_path, args, &blender_pid);
  163. }
  164. return err;
  165. }
  166. Error EditorImportBlendRunner::do_import(const Dictionary &p_options) {
  167. if (is_using_rpc()) {
  168. Error err = do_import_rpc(p_options);
  169. if (err != OK) {
  170. // Retry without using RPC (slow, but better than the import failing completely).
  171. if (err == ERR_CONNECTION_ERROR) {
  172. // Disable RPC if the connection could not be established.
  173. 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));
  174. EditorSettings::get_singleton()->set_manually("filesystem/import/blender/rpc_port", 0);
  175. rpc_port = 0;
  176. }
  177. err = do_import_direct(p_options);
  178. }
  179. return err;
  180. } else {
  181. return do_import_direct(p_options);
  182. }
  183. }
  184. HTTPClient::Status EditorImportBlendRunner::connect_blender_rpc(const Ref<HTTPClient> &p_client, int p_timeout_usecs) {
  185. p_client->connect_to_host("127.0.0.1", rpc_port);
  186. HTTPClient::Status status = p_client->get_status();
  187. int attempts = 1;
  188. int wait_usecs = 1000;
  189. bool done = false;
  190. while (!done) {
  191. OS::get_singleton()->delay_usec(wait_usecs);
  192. status = p_client->get_status();
  193. switch (status) {
  194. case HTTPClient::STATUS_RESOLVING:
  195. case HTTPClient::STATUS_CONNECTING: {
  196. p_client->poll();
  197. break;
  198. }
  199. case HTTPClient::STATUS_CONNECTED: {
  200. done = true;
  201. break;
  202. }
  203. default: {
  204. if (attempts * wait_usecs < p_timeout_usecs) {
  205. p_client->connect_to_host("127.0.0.1", rpc_port);
  206. } else {
  207. return status;
  208. }
  209. }
  210. }
  211. }
  212. return status;
  213. }
  214. Error EditorImportBlendRunner::do_import_rpc(const Dictionary &p_options) {
  215. kill_timer->stop();
  216. // Start Blender if not already running.
  217. if (!is_running()) {
  218. // Start an XML RPC server on the given port.
  219. String python = vformat(PYTHON_SCRIPT_RPC, rpc_port);
  220. Error err = start_blender(python, false);
  221. if (err != OK || blender_pid == 0) {
  222. return FAILED;
  223. }
  224. }
  225. // Convert options to XML body.
  226. String xml_options = dict_to_xmlrpc(p_options);
  227. String xml_body = vformat("<?xml version=\"1.0\"?><methodCall><methodName>export_gltf</methodName><params><param><value>%s</value></param></params></methodCall>", xml_options);
  228. // Connect to RPC server.
  229. Ref<HTTPClient> client = HTTPClient::create();
  230. HTTPClient::Status status = connect_blender_rpc(client, 1000000);
  231. if (status != HTTPClient::STATUS_CONNECTED) {
  232. ERR_FAIL_V_MSG(ERR_CONNECTION_ERROR, vformat("Unexpected status during RPC connection: %d", status));
  233. }
  234. // Send XML request.
  235. PackedByteArray xml_buffer = xml_body.to_utf8_buffer();
  236. Error err = client->request(HTTPClient::METHOD_POST, "/", Vector<String>(), xml_buffer.ptr(), xml_buffer.size());
  237. if (err != OK) {
  238. ERR_FAIL_V_MSG(err, vformat("Unable to send RPC request: %d", err));
  239. }
  240. // Wait for response.
  241. bool done = false;
  242. while (!done) {
  243. status = client->get_status();
  244. switch (status) {
  245. case HTTPClient::STATUS_REQUESTING: {
  246. client->poll();
  247. break;
  248. }
  249. case HTTPClient::STATUS_BODY: {
  250. client->poll();
  251. // Parse response here if needed. For now we can just ignore it.
  252. done = true;
  253. break;
  254. }
  255. default: {
  256. ERR_FAIL_V_MSG(ERR_CONNECTION_ERROR, vformat("Unexpected status during RPC response: %d", status));
  257. }
  258. }
  259. }
  260. return OK;
  261. }
  262. Error EditorImportBlendRunner::do_import_direct(const Dictionary &p_options) {
  263. // Export glTF directly.
  264. String python = vformat(PYTHON_SCRIPT_DIRECT, dict_to_python(p_options));
  265. Error err = start_blender(python, true);
  266. if (err != OK) {
  267. return err;
  268. }
  269. return OK;
  270. }
  271. void EditorImportBlendRunner::_resources_reimported(const PackedStringArray &p_files) {
  272. if (is_running()) {
  273. // After a batch of imports is done, wait a few seconds before trying to kill blender,
  274. // in case of having multiple imports trigger in quick succession.
  275. kill_timer->start();
  276. }
  277. }
  278. void EditorImportBlendRunner::_kill_blender() {
  279. kill_timer->stop();
  280. if (is_running()) {
  281. OS::get_singleton()->kill(blender_pid);
  282. }
  283. blender_pid = 0;
  284. }
  285. void EditorImportBlendRunner::_notification(int p_what) {
  286. switch (p_what) {
  287. case NOTIFICATION_PREDELETE: {
  288. _kill_blender();
  289. break;
  290. }
  291. }
  292. }
  293. EditorImportBlendRunner *EditorImportBlendRunner::singleton = nullptr;
  294. EditorImportBlendRunner::EditorImportBlendRunner() {
  295. ERR_FAIL_COND_MSG(singleton != nullptr, "EditorImportBlendRunner already created.");
  296. singleton = this;
  297. rpc_port = EDITOR_GET("filesystem/import/blender/rpc_port");
  298. kill_timer = memnew(Timer);
  299. add_child(kill_timer);
  300. kill_timer->set_one_shot(true);
  301. kill_timer->set_wait_time(EDITOR_GET("filesystem/import/blender/rpc_server_uptime"));
  302. kill_timer->connect("timeout", callable_mp(this, &EditorImportBlendRunner::_kill_blender));
  303. EditorFileSystem::get_singleton()->connect("resources_reimported", callable_mp(this, &EditorImportBlendRunner::_resources_reimported));
  304. }
  305. #endif // TOOLS_ENABLED