#include #include "loader/component_loader.hpp" #include "mysql.hpp" #include "component/gsc.hpp" #include "component/scheduler.hpp" #include "component/scripting.hpp" #include namespace mysql { namespace { struct mysql_result_t { MYSQL_RES* result; MYSQL_STMT* stmt; uint64_t affected_rows; std::string error; }; struct task_t { scripting::object handle; mysql_result_t result{}; std::atomic_bool completed; }; std::vector> tasks; std::array connection_pool; scripting::script_value field_to_value(const MYSQL_FIELD* field, const std::string& row) { switch (field->type) { case enum_field_types::MYSQL_TYPE_INT24: case enum_field_types::MYSQL_TYPE_LONG: case enum_field_types::MYSQL_TYPE_SHORT: return std::atoi(row.data()); case enum_field_types::MYSQL_TYPE_LONGLONG: return row; case enum_field_types::MYSQL_TYPE_FLOAT: case enum_field_types::MYSQL_TYPE_DOUBLE: return static_cast(std::atof(row.data())); } return row; } scripting::script_value bind_to_value(const MYSQL_BIND* bind) { switch (bind->buffer_type) { case enum_field_types::MYSQL_TYPE_INT24: case enum_field_types::MYSQL_TYPE_LONG: case enum_field_types::MYSQL_TYPE_SHORT: return *reinterpret_cast(bind->buffer); case enum_field_types::MYSQL_TYPE_LONGLONG: return std::to_string(*reinterpret_cast(bind->buffer)); case enum_field_types::MYSQL_TYPE_FLOAT: return *reinterpret_cast(bind->buffer); case enum_field_types::MYSQL_TYPE_DOUBLE: return static_cast(*reinterpret_cast(bind->buffer)); case enum_field_types::MYSQL_TYPE_STRING: return std::string{reinterpret_cast(bind->buffer), bind->buffer_length}; } return {}; } scripting::array generate_result(MYSQL_STMT* stmt) { if (stmt == nullptr) { return {}; } utils::memory::allocator allocator; scripting::array result_arr; const auto meta = mysql_stmt_result_metadata(stmt); if (meta == nullptr) { return {}; } const auto column_count = mysql_num_fields(meta); const auto fields = mysql_fetch_fields(meta); const auto is_null = allocator.allocate_array(column_count); const auto errors = allocator.allocate_array(column_count); const auto real_lengths = allocator.allocate_array(column_count); const auto binds = allocator.allocate_array(column_count); for (auto i = 0u; i < column_count; i++) { binds[i].length = &real_lengths[i]; binds[i].is_null = &is_null[i]; binds[i].error = &errors[i]; } if (mysql_stmt_bind_result(stmt, binds) != 0 || mysql_stmt_store_result(stmt) != 0) { return {}; } while (mysql_stmt_fetch(stmt) != MYSQL_NO_DATA) { for (auto i = 0u; i < column_count; i++) { switch (fields[i].type) { case enum_field_types::MYSQL_TYPE_INT24: case enum_field_types::MYSQL_TYPE_LONG: case enum_field_types::MYSQL_TYPE_SHORT: { binds[i].buffer_type = MYSQL_TYPE_LONG; binds[i].buffer = allocator.allocate(); binds[i].buffer_length = sizeof(int); break; }; case enum_field_types::MYSQL_TYPE_LONGLONG: { binds[i].buffer_type = MYSQL_TYPE_LONGLONG; binds[i].buffer = allocator.allocate(); binds[i].buffer_length = sizeof(std::int64_t); break; }; case enum_field_types::MYSQL_TYPE_FLOAT: binds[i].buffer_type = MYSQL_TYPE_FLOAT; binds[i].buffer = allocator.allocate(); binds[i].buffer_length = sizeof(float); break; case enum_field_types::MYSQL_TYPE_DOUBLE: { binds[i].buffer_type = MYSQL_TYPE_DOUBLE; binds[i].buffer = allocator.allocate(); binds[i].buffer_length = sizeof(double); break; } default: binds[i].buffer_type = MYSQL_TYPE_STRING; binds[i].buffer = allocator.allocate_array(real_lengths[i]); binds[i].buffer_length = real_lengths[i]; break; } mysql_stmt_fetch_column(stmt, &binds[i], i, 0); } scripting::array row_arr; for (auto i = 0u; i < column_count; i++) { const auto field = &fields[i]; const std::string field_name = {field->name, field->name_length}; row_arr[field_name] = bind_to_value(&binds[i]); } result_arr.push(row_arr); } mysql_free_result(meta); return result_arr; } scripting::array generate_result(MYSQL_RES* result) { if (result == nullptr) { return {}; } scripting::array result_arr; const auto num_rows = mysql_num_rows(result); const auto num_fields = mysql_num_fields(result); const auto fields = mysql_fetch_fields(result); for (auto i = 0u; i < num_rows; i++) { scripting::array row_arr; const auto row = mysql_fetch_row(result); const auto lengths = mysql_fetch_lengths(result); for (auto f = 0u; f < num_fields; f++) { const auto field = &fields[f]; const std::string field_str = {field->name, field->name_length}; const std::string row_str = {row[f], lengths[f]}; const auto value = field_to_value(field, row_str); row_arr[field_str] = value; } result_arr.push(row_arr); } return result_arr; } template scripting::object create_mysql_query(F&& cb) { auto task = std::make_shared(); tasks.emplace_back(task); scheduler::thread_pool.push([=] { try { mysql::access([&](database_t& db) { task->result = cb(db); task->completed = true; }); } catch (const std::exception& e) { printf("^1MySQL ERROR: %s\n", e.what()); task->completed = true; } }); return task->handle; } void wait_and_clear_tasks() { for (auto& task : tasks) { while (!task->completed) { std::this_thread::sleep_for(10ms); } } tasks.clear(); } void notify_task_result(std::shared_ptr& task) { const auto result = task->result.result ? generate_result(task->result.result) : generate_result(task->result.stmt); const auto rows = static_cast(task->result.affected_rows); scripting::notify(task->handle.get_entity_id(), "done", {result, rows, task->result.error}); if (task->result.result != nullptr) { mysql_free_result(task->result.result); task->result.result = nullptr; } if (task->result.stmt != nullptr) { mysql_stmt_close(task->result.stmt); task->result.stmt = nullptr; } } void check_tasks() { for (auto i = tasks.begin(); i != tasks.end(); ) { auto& task = *i; if (!task->completed) { ++i; continue; } else { notify_task_result(task); i = tasks.erase(i); } } } template MYSQL_BIND* bind_statement_args(const T& args, std::size_t& bind_count) { bind_count = args.size(); const auto binds = utils::memory::allocate_array(bind_count); for (auto i = 0u; i < args.size(); i++) { const auto& arg = args[i]; const auto& raw_value = arg.get_raw(); switch (raw_value.type) { case game::SCRIPT_FLOAT: { binds[i].buffer = utils::memory::allocate(); binds[i].buffer_type = MYSQL_TYPE_FLOAT; *reinterpret_cast(binds[i].buffer) = raw_value.u.floatValue; break; } case game::SCRIPT_INTEGER: { binds[i].buffer = utils::memory::allocate(); binds[i].buffer_type = MYSQL_TYPE_LONG; *reinterpret_cast(binds[i].buffer) = raw_value.u.intValue; break; } case game::SCRIPT_STRING: { const auto str = arg.as(); const auto str_copy = utils::memory::duplicate_string(str); binds[i].buffer = str_copy; binds[i].buffer_length = str.size(); binds[i].buffer_type = MYSQL_TYPE_STRING; break; } default: { binds[i].buffer_type = MYSQL_TYPE_NULL; break; } } } return binds; } void cleanup_connections() { for (auto& connection : connection_pool) { connection.cleanup(); } } void free_statement_binds(MYSQL_BIND* binds, const std::size_t bind_count) { if (binds == nullptr) { return; } for (auto i = 0u; i < bind_count; i++) { utils::memory::free(binds[i].buffer); } utils::memory::free(binds); } } utils::concurrency::container& get_config() { static utils::concurrency::container config; static auto initialized = false; if (!initialized) { config.access([&](sql::connection_config& cfg) { cfg.user = "root"; cfg.password = "root"; cfg.host = "localhost"; cfg.port = 3306; cfg.database = "default"; }); initialized = true; } return config; } void connection::check() { const auto now = std::chrono::high_resolution_clock::now(); const auto diff = now - this->start_; if (this->db.get() == nullptr || !this->db->ping_server() || diff >= connection_timeout) { get_config().access([&](sql::connection_config& cfg) { this->db = std::make_unique(cfg); this->start_ = now; }); } this->last_access_ = now; } void connection::cleanup() { std::unique_lock lock(this->mutex, std::try_to_lock); if (!lock.owns_lock()) { return; } const auto now = std::chrono::high_resolution_clock::now(); const auto diff = now - this->last_access_; if (diff >= connection_timeout) { this->db.reset(); } } connection* get_connection(std::unique_lock& lock) { static thread_local connection* last_connection{}; if (last_connection != nullptr) { lock = std::unique_lock(last_connection->mutex, std::try_to_lock); if (lock.owns_lock()) { return last_connection; } } for (auto i = 0u; i < connection_pool.size(); i++) { auto connection = &connection_pool[i]; if (connection == last_connection) { continue; } lock = std::unique_lock(connection->mutex, std::try_to_lock); if (!lock.owns_lock()) { continue; } last_connection = connection; return connection; } return nullptr; } class component final : public component_interface { public: void on_shutdown([[maybe_unused]] plugin::plugin* plugin) override { wait_and_clear_tasks(); } void on_startup([[maybe_unused]] plugin::plugin* plugin) override { scripting::on_shutdown(wait_and_clear_tasks); scheduler::loop(cleanup_connections, scheduler::async, 60s); scheduler::loop(check_tasks, scheduler::server); gsc::function::add("mysql_set_config", [](const gsc::function_args& args) -> scripting::script_value { const auto config = args[0].as(); get_config().access([&](sql::connection_config& cfg) { cfg.host = config["host"].as(); cfg.user = config["user"].as(); cfg.password = config["password"].as(); cfg.port = config["port"].as(); cfg.database = config["database"].as(); }); return {}; }); gsc::function::add("mysql_query", [](const gsc::function_args& args) -> scripting::script_value { const auto query = args[0].as(); return create_mysql_query([=](database_t& db) { const auto handle = db->get_handle(); mysql_result_t result{}; if (mysql_query(handle, query.data()) != 0) { result.error = mysql_error(handle); return result; } result.result = mysql_store_result(handle); result.affected_rows = mysql_affected_rows(handle); return result; }); }); gsc::function::add("mysql_prepared_statement", [](const gsc::function_args& args) -> scripting::script_value { MYSQL_BIND* binds = nullptr; auto bind_count = 0u; const auto query = args[0].as(); auto args_vec = args.get_raw(); args_vec.erase(args_vec.begin()); try { if (args_vec.size() > 0 && args_vec[0].is()) { binds = bind_statement_args(args_vec[0].as(), bind_count); } else { binds = bind_statement_args(args_vec, bind_count); } const auto handle = create_mysql_query([binds, bind_count, query](database_t& db) { const auto _0 = gsl::finally([=] { free_statement_binds(binds, bind_count); }); mysql_result_t result{}; const auto handle = db->get_handle(); const auto stmt = mysql_stmt_init(handle); if (mysql_stmt_prepare(stmt, query.data(), query.size()) != 0 || (binds != nullptr && mysql_stmt_bind_param(stmt, binds) != 0) || mysql_stmt_execute(stmt) != 0) { result.error = mysql_stmt_error(stmt); return result; } result.stmt = stmt; result.affected_rows = mysql_stmt_affected_rows(stmt); return result; }); return handle; } catch (const std::exception& e) { free_statement_binds(binds, bind_count); throw e; } }); } }; } REGISTER_COMPONENT(mysql::component)