diff --git a/.github/workflows/ci_suite.yml b/.github/workflows/ci_suite.yml index d22c6be2de5..b8c9a4cc0a6 100644 --- a/.github/workflows/ci_suite.yml +++ b/.github/workflows/ci_suite.yml @@ -10,12 +10,22 @@ jobs: run_linters: if: "!contains(github.event.head_commit.message, '[ci skip]')" name: Run Linters - runs-on: ubuntu-20.04 + runs-on: ubuntu-24.04 concurrency: group: run_linters-${{ github.ref }} cancel-in-progress: true steps: - uses: actions/checkout@v3 + - name: Update apt and install dependencies + run: | + sudo apt-get update + sudo apt-get install -y libgit2-dev libjpeg62 libjpeg62-dev libpng-dev + - uses: actions/setup-python@v5 + with: + python-version: '3.11' + cache: 'pip' + cache-dependency-path: 'tools/requirements.txt' + - uses: actions/checkout@v3 - name: Restore SpacemanDMM cache uses: actions/cache@v3 with: @@ -32,10 +42,9 @@ jobs: ${{ runner.os }}- - name: Install Tools run: | - pip3 install setuptools + pip install -r tools/requirements.txt bash tools/ci/install_node.sh bash tools/ci/install_spaceman_dmm.sh dreamchecker - tools/bootstrap/python -c '' - name: Run Linters run: | bash tools/ci/check_filedirs.sh cev_eris.dme @@ -56,7 +65,7 @@ jobs: compile_all_maps: if: "!contains(github.event.head_commit.message, '[ci skip]')" name: Compile Maps - runs-on: ubuntu-20.04 + runs-on: ubuntu-24.04 concurrency: group: compile_all_maps-${{ github.ref }} cancel-in-progress: true @@ -76,7 +85,7 @@ jobs: run_all_tests: if: "!contains(github.event.head_commit.message, '[ci skip]')" name: Integration Tests - runs-on: ubuntu-20.04 + runs-on: ubuntu-24.04 services: mysql: image: mysql:latest @@ -102,9 +111,6 @@ jobs: mysql -u root -proot tg_ci < schema.sql - name: Install rust-g run: | - sudo dpkg --add-architecture i386 - sudo apt update || true - sudo apt install -o APT::Immediate-Configure=false libssl1.1:i386 bash tools/ci/install_rust_g.sh - name: Compile Tests run: | @@ -127,7 +133,7 @@ jobs: if: "!contains(github.event.head_commit.message, '[ci skip]') && always()" needs: [run_all_tests] name: Compare Screenshot Tests - runs-on: ubuntu-20.04 + runs-on: ubuntu-24.04 steps: - uses: actions/checkout@v3 # If we ever add more artifacts, this is going to break, but it'll be obvious. diff --git a/cev_eris.dme b/cev_eris.dme index 95a6ad58348..beef362881d 100644 --- a/cev_eris.dme +++ b/cev_eris.dme @@ -30,6 +30,7 @@ #include "code\__DEFINES\craft.dm" #include "code\__DEFINES\cyborg_traits.dm" #include "code\__DEFINES\damage_organs.dm" +#include "code\__DEFINES\database.dm" #include "code\__DEFINES\economy.dm" #include "code\__DEFINES\error_handler.dm" #include "code\__DEFINES\flags.dm" @@ -226,6 +227,7 @@ #include "code\controllers\subsystems\chemistry.dm" #include "code\controllers\subsystems\chunks.dm" #include "code\controllers\subsystems\craft.dm" +#include "code\controllers\subsystems\database.dm" #include "code\controllers\subsystems\dcs.dm" #include "code\controllers\subsystems\economy.dm" #include "code\controllers\subsystems\evac.dm" @@ -527,7 +529,6 @@ #include "code\defines\obj.dm" #include "code\defines\procs\announce.dm" #include "code\defines\procs\AStar.dm" -#include "code\defines\procs\dbcore.dm" #include "code\defines\procs\hud.dm" #include "code\defines\procs\radio.dm" #include "code\defines\procs\sd_Alert.dm" @@ -1425,6 +1426,7 @@ #include "code\modules\admin\verbs\possess.dm" #include "code\modules\admin\verbs\pray.dm" #include "code\modules\admin\verbs\randomverbs.dm" +#include "code\modules\admin\verbs\reestablish_db_connection.dm" #include "code\modules\admin\verbs\ticklag.dm" #include "code\modules\admin\verbs\tripAI.dm" #include "code\modules\admin\verbs\SDQL_2\SDQL_2.dm" diff --git a/code/__DEFINES/database.dm b/code/__DEFINES/database.dm new file mode 100644 index 00000000000..3d20b3b9a3c --- /dev/null +++ b/code/__DEFINES/database.dm @@ -0,0 +1,6 @@ +/// When a query has been queued up for execution/is being executed +#define DB_QUERY_STARTED 0 +/// When a query is finished executing +#define DB_QUERY_FINISHED 1 +/// When there was a problem with the execution of a query. +#define DB_QUERY_BROKEN 2 diff --git a/code/__DEFINES/subsystems-priority.dm b/code/__DEFINES/subsystems-priority.dm index 2b93e424563..cad0de31b11 100644 --- a/code/__DEFINES/subsystems-priority.dm +++ b/code/__DEFINES/subsystems-priority.dm @@ -16,7 +16,8 @@ var/list/bitflags = list(1, 2, 4, 8, 16, 32, 64, 128, 256, 512, 1024, 2048, 4096 #define SS_PRIORITY_TICKER 200 // Gameticker processing. #define FIRE_PRIORITY_TGUI 110 #define FIRE_PRIORITY_EXPLOSIONS 105 // Explosions! -#define FIRE_PRIORITY_THROWING 106 // Throwing! after explosions since they influence throw direction +#define FIRE_PRIORITY_DATABASE 104 +#define FIRE_PRIORITY_THROWING 103 // Throwing! after explosions since they influence throw direction #define SS_PRIORITY_HUMAN 101 // Human Life(). #define SS_PRIORITY_MOB 100 // Non-human Mob Life(). #define SS_PRIORITY_CHAT 100 // Chat subsystem. diff --git a/code/__DEFINES/subsystems.dm b/code/__DEFINES/subsystems.dm index 0600cf513d4..0c507b6a63d 100644 --- a/code/__DEFINES/subsystems.dm +++ b/code/__DEFINES/subsystems.dm @@ -1,4 +1,13 @@ +//! ## DB defines +/** + * DB schema version + * + * Update this whenever the db schema changes + */ +#define DB_SCHEMA_VERSION 1 + + //! ## Timing subsystem /** * Don't run if there is an identical unique timer active @@ -88,6 +97,7 @@ // The numbers just define the ordering, they are meaningless otherwise. #define INIT_ORDER_GARBAGE 99 +#define INIT_ORDER_DBCORE 98 #define INIT_ORDER_EXPLOSIONS 97 #define INIT_ORDER_STATPANELS 96 #define INIT_ORDER_MAPPING 15 diff --git a/code/__HELPERS/text.dm b/code/__HELPERS/text.dm index 0a035e76bfa..65e47ae9d29 100644 --- a/code/__HELPERS/text.dm +++ b/code/__HELPERS/text.dm @@ -13,10 +13,8 @@ * SQL sanitization */ -// Run all strings to be used in an SQL query through this proc first to properly escape out injection attempts. -/proc/sanitizeSQL(var/t as text) - var/sqltext = dbcon.Quote(t); - return copytext(sqltext, 2, length(sqltext));//Quote() adds quotes around input, we already do that +/proc/format_table_name(table as text) + return sql_tableprefix + table /* * Text sanitization diff --git a/code/controllers/configuration.dm b/code/controllers/configuration.dm index e951535ed60..b9f9c0e7e21 100644 --- a/code/controllers/configuration.dm +++ b/code/controllers/configuration.dm @@ -249,6 +249,7 @@ GLOBAL_LIST_EMPTY(storyteller_cache) /datum/configuration/proc/load(filename, type = "config") //the type can also be game_options, in which case it uses a different switch. not making it separate to not copypaste code - Urist var/list/Lines = file2list(filename) + world.log << "loading [filename]" for(var/t in Lines) if(!t) continue @@ -779,6 +780,7 @@ GLOBAL_LIST_EMPTY(storyteller_cache) /datum/configuration/proc/loadsql(filename) // -- TLE var/list/Lines = file2list(filename) + world.log << "loading [filename]" for(var/t in Lines) if(!t) continue @@ -805,13 +807,25 @@ GLOBAL_LIST_EMPTY(storyteller_cache) if ("address") sqladdress = value if ("port") - sqlport = value + sqlport = text2num(value) if ("database") sqldb = value if ("login") sqllogin = value if ("password") sqlpass = value + if ("sql_tableprefix") + sql_tableprefix = value + if ("async_query_timeout") + sql_async_query_timeout = text2num(value) + if ("blocking_query_timeout") + sql_blocking_query_timeout = text2num(value) + if ("pooling_min_sql_connections") + sql_pooling_min_sql_connections = text2num(value) + if ("pooling_max_sql_connections") + sql_pooling_max_sql_connections = text2num(value) + if ("max_concurrent_queries") + sql_max_concurrent_queries = text2num(value) else log_misc("Unknown setting in configuration: '[name]'") diff --git a/code/controllers/subsystems/database.dm b/code/controllers/subsystems/database.dm new file mode 100644 index 00000000000..7095f9028f1 --- /dev/null +++ b/code/controllers/subsystems/database.dm @@ -0,0 +1,559 @@ +#define SHUTDOWN_QUERY_TIMELIMIT (1 MINUTES) +SUBSYSTEM_DEF(dbcore) + name = "Database" + flags = SS_TICKER + wait = 10 // Not seconds because we're running on SS_TICKER + runlevels = RUNLEVEL_INIT|RUNLEVEL_LOBBY|RUNLEVELS_DEFAULT + init_order = INIT_ORDER_DBCORE + priority = FIRE_PRIORITY_DATABASE + + var/schema_mismatch = 0 + var/db_version = 0 + /// Number of failed connection attempts this try. Resets after the timeout or successful connection + var/failed_connections = 0 + /// Max number of consecutive failures before a timeout (here and not a define so it can be vv'ed mid round if needed) + var/max_connection_failures = 5 + /// world.time that connection attempts can resume + var/failed_connection_timeout = 0 + /// Total number of times connections have had to be timed out. + var/failed_connection_timeout_count = 0 + + var/last_error + + var/max_concurrent_queries = 25 + + /// Number of all queries, reset to 0 when logged in SStime_track. Used by SStime_track + var/all_queries_num = 0 + /// Number of active queries, reset to 0 when logged in SStime_track. Used by SStime_track + var/queries_active_num = 0 + /// Number of standby queries, reset to 0 when logged in SStime_track. Used by SStime_track + var/queries_standby_num = 0 + + /// All the current queries that exist. + var/list/all_queries = list() + /// Queries being checked for timeouts. + var/list/processing_queries + + /// Queries currently being handled by database driver + var/list/datum/db_query/queries_active = list() + /// Queries pending execution, mapped to complete arguments + var/list/datum/db_query/queries_standby = list() + + /// We are in the process of shutting down and should not allow more DB connections + var/shutting_down = FALSE + + + var/connection // Arbitrary handle returned from rust_g. + + var/db_daemon_started = FALSE + +/datum/controller/subsystem/dbcore/Initialize() + //We send warnings to the admins during subsystem init, as the clients will be New'd and messages + //will queue properly with goonchat + switch(schema_mismatch) + if(1) + message_admins("Database schema ([db_version]) doesn't match the latest schema version ([DB_SCHEMA_VERSION]), this may lead to undefined behaviour or errors") + if(2) + message_admins("Could not get schema version from database") + + return ..() + +/datum/controller/subsystem/dbcore/OnConfigLoad() + . = ..() + + var/min_sql_connections = sql_pooling_min_sql_connections + var/max_sql_connections = sql_pooling_max_sql_connections + + if (max_sql_connections < min_sql_connections) + // Since we no longer have "modified" flags, just handle logically equivalent cases + log_misc("ERROR: POOLING_MAX_SQL_CONNECTIONS ([max_sql_connections]) is set lower than POOLING_MIN_SQL_CONNECTIONS ([min_sql_connections]), the values will be swapped.") + + // Swap values + var/tmp = min_sql_connections + sql_pooling_min_sql_connections = max_sql_connections + sql_pooling_max_sql_connections = tmp + + log_misc("ERROR: POOLING_MAX_SQL_CONNECTIONS ([sql_pooling_max_sql_connections]) is set lower than POOLING_MIN_SQL_CONNECTIONS ([sql_pooling_min_sql_connections]). Please check your config or the code defaults for sanity") + +/datum/controller/subsystem/dbcore/stat_entry(msg) + msg = "P:[length(all_queries)]|Active:[length(queries_active)]|Standby:[length(queries_standby)]" + return ..() + +/// Resets the tracking numbers on the subsystem. Used by SStime_track. +/datum/controller/subsystem/dbcore/proc/reset_tracking() + all_queries_num = 0 + queries_active_num = 0 + queries_standby_num = 0 + +/datum/controller/subsystem/dbcore/fire(resumed = FALSE) + if(!IsConnected()) + return + + if(!resumed) + if(!length(queries_active) && !length(queries_standby) && !length(all_queries)) + processing_queries = null + return + processing_queries = all_queries.Copy() + + // First handle the already running queries + for (var/datum/db_query/query in queries_active) + if(!process_query(query)) + queries_active -= query + + // Now lets pull in standby queries if we have room. + if (length(queries_standby) > 0 && length(queries_active) < max_concurrent_queries) + var/list/queries_to_activate = queries_standby.Copy(1, min(length(queries_standby), max_concurrent_queries) + 1) + + for (var/datum/db_query/query in queries_to_activate) + queries_standby.Remove(query) + create_active_query(query) + + // And finally, let check queries for undeleted queries, check ticking if there is a lot of work to do. + while(length(processing_queries)) + var/datum/db_query/query = popleft(processing_queries) + if(world.time - query.last_activity_time > (5 MINUTES)) + stack_trace("Found undeleted query, check the sql.log for the undeleted query and add a delete call to the query datum.") + log_misc("Undeleted query: \"[query.sql]\" LA: [query.last_activity] LAT: [query.last_activity_time]") + qdel(query) + if(MC_TICK_CHECK) + return + + +/// Helper proc for handling activating queued queries +/datum/controller/subsystem/dbcore/proc/create_active_query(datum/db_query/query) + PRIVATE_PROC(TRUE) + SHOULD_NOT_SLEEP(TRUE) + // if(IsAdminAdvancedProcCall()) + // return FALSE + run_query(query) + queries_active_num++ + queries_active += query + return query + +/datum/controller/subsystem/dbcore/proc/process_query(datum/db_query/query) + PRIVATE_PROC(TRUE) + SHOULD_NOT_SLEEP(TRUE) + // if(IsAdminAdvancedProcCall()) + // return FALSE + if(QDELETED(query)) + return FALSE + if(query.Process((TICKS2DS(wait)) / 10)) + queries_active -= query + return FALSE + return TRUE + +/datum/controller/subsystem/dbcore/proc/run_query_sync(datum/db_query/query) + // if(IsAdminAdvancedProcCall()) + // return + run_query(query) + UNTIL(query.Process()) + return query + +/datum/controller/subsystem/dbcore/proc/run_query(datum/db_query/query) + // if(IsAdminAdvancedProcCall()) + // return + query.job_id = rustg_sql_query_async(connection, query.sql, json_encode(query.arguments)) + +/datum/controller/subsystem/dbcore/proc/queue_query(datum/db_query/query) + // if(IsAdminAdvancedProcCall()) + // return + + if (!length(queries_standby) && length(queries_active) < max_concurrent_queries) + create_active_query(query) + return + + queries_standby_num++ + queries_standby |= query + +/datum/controller/subsystem/dbcore/Recover() + connection = SSdbcore.connection + +/datum/controller/subsystem/dbcore/Shutdown() + shutting_down = TRUE + var/msg = "Clearing DB queries standby:[length(queries_standby)] active: [length(queries_active)] all: [length(all_queries)]" + to_chat(world, span_boldannounce(msg)) + log_world(msg) + //This is as close as we can get to the true round end before Disconnect() without changing where it's called, defeating the reason this is a subsystem + var/endtime = REALTIMEOFDAY + SHUTDOWN_QUERY_TIMELIMIT + if(SSdbcore.Connect()) + //Take over control of all active queries + var/queries_to_check = queries_active.Copy() + queries_active.Cut() + + //Start all waiting queries + for(var/datum/db_query/query in queries_standby) + run_query(query) + queries_to_check += query + queries_standby -= query + + //wait for them all to finish + for(var/datum/db_query/query in queries_to_check) + UNTIL(query.Process() || REALTIMEOFDAY > endtime) + + msg = "Done clearing DB queries standby:[length(queries_standby)] active: [length(queries_active)] all: [length(all_queries)]" + to_chat(world, span_boldannounce(msg)) + log_world(msg) + if(IsConnected()) + Disconnect() + +/datum/controller/subsystem/dbcore/proc/Connect() + if(IsConnected()) + return TRUE + + if(connection) + Disconnect() //clear the current connection handle so isconnected() calls stop invoking rustg + connection = null //make sure its cleared even if runtimes happened + + if(failed_connection_timeout <= world.time) //it's been long enough since we failed to connect, reset the counter + failed_connections = 0 + failed_connection_timeout = 0 + + if(failed_connection_timeout > 0) + return FALSE + + if(!sqladdress) + return FALSE + + var/user = sqllogin + var/pass = sqlpass + var/db = sqldb + var/address = sqladdress + var/port = sqlport + var/timeout = max(sql_async_query_timeout, sql_blocking_query_timeout) + var/min_sql_connections = sql_pooling_min_sql_connections + var/max_sql_connections = sql_pooling_max_sql_connections + + var/result = json_decode(rustg_sql_connect_pool(json_encode(list( + "host" = address, + "port" = port, + "user" = user, + "pass" = pass, + "db_name" = db, + "read_timeout" = timeout, + "write_timeout" = timeout, + "min_threads" = min_sql_connections, + "max_threads" = max_sql_connections, + )))) + . = (result["status"] == "ok") + if (.) + connection = result["handle"] + else + connection = null + last_error = result["data"] + log_misc("Connect() failed | [last_error]") + ++failed_connections + //If it failed to establish a connection more than 5 times in a row, don't bother attempting to connect for a time. + if(failed_connections > max_connection_failures) + failed_connection_timeout_count++ + //basic exponential backoff algorithm + failed_connection_timeout = world.time + ((2 ** failed_connection_timeout_count) SECONDS) + +/datum/controller/subsystem/dbcore/proc/CheckSchemaVersion() + if(sqladdress) + if(Connect()) + log_world("Database connection established.") + var/datum/db_query/query_db_version = NewQuery("SELECT version FROM [format_table_name("schema_migrations")] ORDER BY version DESC LIMIT 1") + query_db_version.Execute() + if(query_db_version.NextRow()) + var/db_version = query_db_version.item[1] + if(db_version != DB_SCHEMA_VERSION) + schema_mismatch = 1 // flag admin message about mismatch + log_misc("Database schema ([db_version]) doesn't match the expected schema version ([DB_SCHEMA_VERSION]), this may lead to undefined behaviour or errors") + else + schema_mismatch = 2 // flag admin message about missing schema version + log_misc("Could not get schema version from database") + qdel(query_db_version) + else + log_misc("Your server failed to establish a connection with the database.") + else + log_misc("Database is not enabled in configuration.") + +/datum/controller/subsystem/dbcore/proc/Disconnect() + failed_connections = 0 + if (connection) + rustg_sql_disconnect_pool(connection) + connection = null + +/datum/controller/subsystem/dbcore/proc/IsConnected() + if (!sqladdress) + return FALSE + if (!connection) + return FALSE + return json_decode(rustg_sql_connected(connection))["status"] == "online" + +/datum/controller/subsystem/dbcore/proc/ErrorMsg() + if(!sqladdress) + return "Database disabled by configuration" + return last_error + +/datum/controller/subsystem/dbcore/proc/ReportError(error) + last_error = error + +/datum/controller/subsystem/dbcore/proc/NewQuery(sql_query, arguments, allow_during_shutdown=FALSE) + //If the subsystem is shutting down, disallow new queries + if(!allow_during_shutdown && shutting_down) + CRASH("Attempting to create a new db query during the world shutdown") + + // if(IsAdminAdvancedProcCall()) + // log_admin_private("ERROR: Advanced admin proc call led to sql query: [sql_query]. Query has been blocked") + // message_admins("ERROR: Advanced admin proc call led to sql query. Query has been blocked") + // return FALSE + return new /datum/db_query(connection, sql_query, arguments) + +/** + * Creates and executes a query without waiting for or tracking the results. + * Query is executed asynchronously (without blocking) and deleted afterwards - any results or errors are discarded. + * + * Arguments: + * * sql_query - The SQL query string to execute + * * arguments - List of arguments to pass to the query for parameter binding + * * allow_during_shutdown - If TRUE, allows query to be created during subsystem shutdown. Generally, only cleanup queries should set this. + */ +/datum/controller/subsystem/dbcore/proc/FireAndForget(sql_query, arguments, allow_during_shutdown = FALSE) + var/datum/db_query/query = NewQuery(sql_query, arguments, allow_during_shutdown) + if(!query) + return + spawn(-1) + query.Execute() + qdel(query) + +/** QuerySelect + Run a list of query datums in parallel, blocking until they all complete. + * queries - List of queries or single query datum to run. + * warn - Controls rather warn_execute() or Execute() is called. + * qdel - If you don't care about the result or checking for errors, you can have the queries be deleted afterwards. + This can be combined with invoke_async as a way of running queries async without having to care about waiting for them to finish so they can be deleted, + however you should probably just use FireAndForget instead if it's just a single query. +*/ +/datum/controller/subsystem/dbcore/proc/QuerySelect(list/queries, warn = FALSE, qdel = FALSE) + if (!islist(queries)) + if (!istype(queries, /datum/db_query)) + CRASH("Invalid query passed to QuerySelect: [queries]") + queries = list(queries) + else + queries = queries.Copy() //we don't want to hide bugs in the parent caller by removing invalid values from this list. + + for (var/datum/db_query/query as anything in queries) + if (!istype(query)) + queries -= query + stack_trace("Invalid query passed to QuerySelect: `[query]` [REF(query)]") + continue + + if (warn) + INVOKE_ASYNC(query, TYPE_PROC_REF(/datum/db_query, warn_execute)) + else + INVOKE_ASYNC(query, TYPE_PROC_REF(/datum/db_query, Execute)) + + for (var/datum/db_query/query as anything in queries) + query.sync() + if (qdel) + qdel(query) + +/* +Takes a list of rows (each row being an associated list of column => value) and inserts them via a single mass query. +Rows missing columns present in other rows will resolve to SQL NULL +You are expected to do your own escaping of the data, and expected to provide your own quotes for strings. +The duplicate_key arg can be true to automatically generate this part of the query + or set to a string that is appended to the end of the query +Ignore_errors instructes mysql to continue inserting rows if some of them have errors. + the erroneous row(s) aren't inserted and there isn't really any way to know why or why errored +*/ +/datum/controller/subsystem/dbcore/proc/MassInsert(table, list/rows, duplicate_key = FALSE, ignore_errors = FALSE, warn = FALSE, async = TRUE, special_columns = null) + if (!table || !rows || !istype(rows)) + return + + // Prepare column list + var/list/columns = list() + var/list/has_question_mark = list() + for (var/list/row in rows) + for (var/column in row) + columns[column] = "?" + has_question_mark[column] = TRUE + for (var/column in special_columns) + columns[column] = special_columns[column] + has_question_mark[column] = findtext(special_columns[column], "?") + + // Prepare SQL query full of placeholders + var/list/query_parts = list("INSERT") + if (ignore_errors) + query_parts += " IGNORE" + query_parts += " INTO " + query_parts += table + query_parts += "\n([columns.Join(", ")])\nVALUES" + + var/list/arguments = list() + var/has_row = FALSE + for (var/list/row in rows) + if (has_row) + query_parts += "," + query_parts += "\n (" + var/has_col = FALSE + for (var/column in columns) + if (has_col) + query_parts += ", " + if (has_question_mark[column]) + var/name = "p[arguments.len]" + query_parts += replacetext(columns[column], "?", ":[name]") + arguments[name] = row[column] + else + query_parts += columns[column] + has_col = TRUE + query_parts += ")" + has_row = TRUE + + if (duplicate_key == TRUE) + var/list/column_list = list() + for (var/column in columns) + column_list += "[column] = VALUES([column])" + query_parts += "\nON DUPLICATE KEY UPDATE [column_list.Join(", ")]" + else if (duplicate_key != FALSE) + query_parts += duplicate_key + + var/datum/db_query/Query = NewQuery(query_parts.Join(), arguments) + if (warn) + . = Query.warn_execute(async) + else + . = Query.Execute(async) + qdel(Query) + +/datum/db_query + // Inputs + var/connection + var/sql + var/arguments + + var/datum/callback/success_callback + var/datum/callback/fail_callback + + // Status information + /// Current status of the query. + var/status + /// Job ID of the query passed by rustg. + var/job_id + var/last_error + var/last_activity + var/last_activity_time + + // Output + var/list/list/rows + var/next_row_to_take = 1 + var/affected + var/last_insert_id + + var/list/item //list of data values populated by NextRow() + +/datum/db_query/New(connection, sql, arguments) + SSdbcore.all_queries += src + SSdbcore.all_queries_num++ + Activity("Created") + item = list() + + src.connection = connection + src.sql = sql + src.arguments = arguments + +/datum/db_query/Destroy() + Close() + SSdbcore.all_queries -= src + SSdbcore.queries_standby -= src + SSdbcore.queries_active -= src + return ..() + +/datum/db_query/proc/Activity(activity) + last_activity = activity + last_activity_time = world.time + +/datum/db_query/proc/warn_execute(async = TRUE) + . = Execute(async) + if(!.) + + to_chat(usr, span_danger("A SQL error occurred during this operation, check the server logs.")) + +/datum/db_query/proc/Execute(async = TRUE, log_error = TRUE) + Activity("Execute") + if(status == DB_QUERY_STARTED) + CRASH("Attempted to start a new query while waiting on the old one") + + if(!SSdbcore.IsConnected()) + last_error = "No connection!" + return FALSE + + var/start_time + if(!async) + start_time = REALTIMEOFDAY + Close() + status = DB_QUERY_STARTED + if(async) + if(!MC_RUNNING(SSdbcore.init_stage)) + SSdbcore.run_query_sync(src) + else + SSdbcore.queue_query(src) + sync() + else + var/job_result_str = rustg_sql_query_blocking(connection, sql, json_encode(arguments)) + store_data(json_decode(job_result_str)) + + . = (status != DB_QUERY_BROKEN) + var/timed_out = !. && findtext(last_error, "Operation timed out") + if(!. && log_error) + log_misc("SQL query failed: [sql] | args=[json_encode(arguments)] | error=[last_error]") + + if(!async && timed_out) + log_misc("Slow query timeout: [sql] | start=[start_time] | end=[REALTIMEOFDAY]") + slow_query_check() + +/// Sleeps until execution of the query has finished. +/datum/db_query/proc/sync() + while(status < DB_QUERY_FINISHED) + stoplag() + +/datum/db_query/Process(seconds_per_tick) + if(status >= DB_QUERY_FINISHED) + return TRUE // we are done processing after all + + status = DB_QUERY_STARTED + var/job_result = rustg_sql_check_query(job_id) + if(job_result == RUSTG_JOB_NO_RESULTS_YET) + return FALSE //no results yet + + store_data(json_decode(job_result)) + return TRUE + +/datum/db_query/proc/store_data(result) + switch(result["status"]) + if("ok") + rows = result["rows"] + affected = result["affected"] + last_insert_id = result["last_insert_id"] + status = DB_QUERY_FINISHED + return + if("err") + last_error = result["data"] + status = DB_QUERY_BROKEN + return + if("offline") + last_error = "CONNECTION OFFLINE" + status = DB_QUERY_BROKEN + return + + +/datum/db_query/proc/slow_query_check() + message_admins("HEY! A database query timed out. Did the server just hang? \[YES\]|\[NO\]") + +/datum/db_query/proc/NextRow(async = TRUE) + Activity("NextRow") + + if (rows && next_row_to_take <= rows.len) + item = rows[next_row_to_take] + next_row_to_take++ + return !!item + else + return FALSE + +/datum/db_query/proc/ErrorMsg() + return last_error + +/datum/db_query/proc/Close() + rows = null + item = null +#undef SHUTDOWN_QUERY_TIMELIMIT diff --git a/code/datums/topic/admin.dm b/code/datums/topic/admin.dm index f3af9e40f72..1ab4748577d 100644 --- a/code/datums/topic/admin.dm +++ b/code/datums/topic/admin.dm @@ -1353,6 +1353,24 @@ else error_viewer.showTo(usr, null, input["viewruntime_linear"]) +/datum/admin_topic/slowquery + keyword = "slowquery" + require_perms = list(R_ADMIN) + +/datum/admin_topic/viewruntime/Run(list/input) + if(!check_rights(R_ADMIN)) + return + + var/data = list("key" = usr.key) + var/answer = input["slowquery"] + if(answer == "yes") + if(alert(usr, "Did you just press any admin buttons?", "Query server hang report", list("Yes", "No")) == "Yes") + var/response = input(usr,"What were you just doing?","Query server hang report") as null|text + if(response) + data["response"] = response + log_misc("SQL: server hang - [json_encode(data)]") + else if(answer == "no") + log_misc("SQL: no server hang - [json_encode(data)]") /datum/admin_topic/admincaster keyword = "admincaster" diff --git a/code/defines/procs/dbcore.dm b/code/defines/procs/dbcore.dm deleted file mode 100644 index fb0f19b6e10..00000000000 --- a/code/defines/procs/dbcore.dm +++ /dev/null @@ -1,235 +0,0 @@ -//This file was auto-corrected by findeclaration.exe on 25.5.2012 20:42:31 - -//cursors -#define Default_Cursor 0 -#define Client_Cursor 1 -#define Server_Cursor 2 -//conversions -#define TEXT_CONV 1 -#define RSC_FILE_CONV 2 -#define NUMBER_CONV 3 -//column flag values: -#define IS_NUMERIC 1 -#define IS_BINARY 2 -#define IS_NOT_NULL 4 -#define IS_PRIMARY_KEY 8 -#define IS_UNSIGNED 16 -//types -#define TINYINT 1 -#define SMALLINT 2 -#define MEDIUMINT 3 -#define INTEGER 4 -#define BIGINT 5 -#define DECIMAL 6 -#define FLOAT 7 -#define DOUBLE 8 -#define DATE 9 -#define DATETIME 10 -#define TIMESTAMP 11 -#define TIME 12 -#define STRING 13 -#define BLOB 14 -// TODO: Investigate more recent type additions and see if I can handle them. - Nadrew - - -// Deprecated! See global.dm for new configuration vars -/* -var/DB_SERVER = "" // This is the location of your MySQL server (localhost is USUALLY fine) -var/DB_PORT = 3306 // This is the port your MySQL server is running on (3306 is the default) -*/ - -DBConnection - var/_db_con // This variable contains a reference to the actual database connection. - var/dbi // This variable is a string containing the DBI MySQL requires. - var/user // This variable contains the username data. - var/password // This variable contains the password data. - var/default_cursor // This contains the default database cursor data. - var/server = "" - var/port = 3306 - -DBConnection/New(dbi_handler, username, password_handler, cursor_handler) - src.dbi = dbi_handler - src.user = username - src.password = password_handler - src.default_cursor = cursor_handler - _db_con = _dm_db_new_con() - -DBConnection/proc/Connect(dbi_handler=src.dbi, user_handler=src.user, password_handler=src.password, cursor_handler) - if(!src) - return 0 - cursor_handler = src.default_cursor - if(!cursor_handler) - cursor_handler = Default_Cursor - return _dm_db_connect(_db_con, dbi_handler, user_handler, password_handler, cursor_handler, null) - -DBConnection/proc/Disconnect() - return _dm_db_close(_db_con) - -DBConnection/proc/IsConnected() - return _dm_db_is_connected(_db_con) - -DBConnection/proc/Quote(str) - return _dm_db_quote(_db_con, str) - -DBConnection/proc/ErrorMsg() - return "## MYSQL ERROR: [_dm_db_error_msg(_db_con)]" - -DBConnection/proc/SelectDB(database_name, dbi) - if(IsConnected()) - Disconnect() - return Connect("[dbi?"[dbi]":"dbi:mysql:[database_name]:[sqladdress]:[sqlport]"]", user, password) - -DBConnection/proc/NewQuery(sql_query, cursor_handler = src.default_cursor) - return new/DBQuery(sql_query, src, cursor_handler) - - -DBQuery/New(sql_query, DBConnection/connection_handler, cursor_handler) - if(sql_query) - src.sql = sql_query - if(connection_handler) - src.db_connection = connection_handler - if(cursor_handler) - src.default_cursor = cursor_handler - _db_query = _dm_db_new_query() - return ..() - - -DBQuery - var/sql // The sql query being executed. - var/default_cursor - var/list/columns //list of DB Columns populated by Columns() - var/list/conversions - var/list/item[0] //list of data values populated by NextRow() - - var/DBConnection/db_connection - var/_db_query - -DBQuery/proc/Connect(DBConnection/connection_handler) - src.db_connection = connection_handler - -DBQuery/proc/Execute(sql_query = src.sql, cursor_handler = default_cursor) - Close() - return _dm_db_execute(_db_query, sql_query, db_connection._db_con, cursor_handler, null) - -DBQuery/proc/NextRow() - return _dm_db_next_row(_db_query, item, conversions) - -DBQuery/proc/RowsAffected() - return _dm_db_rows_affected(_db_query) - -DBQuery/proc/RowCount() - return _dm_db_row_count(_db_query) - -DBQuery/proc/ErrorMsg() - return _dm_db_error_msg(_db_query) - -DBQuery/proc/Columns() - if(!columns) - columns = _dm_db_columns(_db_query, /DBColumn) - return columns - -DBQuery/proc/GetRowData() - var/list/columns = Columns() - var/list/results - if(columns.len) - results = list() - for(var/C in columns) - results.Add(C) - var/DBColumn/cur_col = columns[C] - results[C] = src.item[(cur_col.position + 1)] - return results - -DBQuery/proc/Close() - item.len = 0 - columns = null - conversions = null - return _dm_db_close(_db_query) - -DBQuery/proc/Quote(str) - return db_connection.Quote(str) - -DBQuery/proc/SetConversion(column, conversion) - if(istext(column)) - column = columns.Find(column) - if(!conversions) - conversions = new/list(column) - else if(conversions.len < column) - conversions.len = column - conversions[column] = conversion - - -DBColumn - var/name - var/table - var/position //1-based index into item data - var/sql_type - var/flags - var/length - var/max_length - -DBColumn/New(name_handler, table_handler, position_handler, type_handler, flag_handler, length_handler, max_length_handler) - src.name = name_handler - src.table = table_handler - src.position = position_handler - src.sql_type = type_handler - src.flags = flag_handler - src.length = length_handler - src.max_length = max_length_handler - return ..() - - -DBColumn/proc/SqlTypeName(type_handler = src.sql_type) - switch(type_handler) - if(TINYINT) - return "TINYINT" - if(SMALLINT) - return "SMALLINT" - if(MEDIUMINT) - return "MEDIUMINT" - if(INTEGER) - return "INTEGER" - if(BIGINT) - return "BIGINT" - if(FLOAT) - return "FLOAT" - if(DOUBLE) - return "DOUBLE" - if(DATE) - return "DATE" - if(DATETIME) - return "DATETIME" - if(TIMESTAMP) - return "TIMESTAMP" - if(TIME) - return "TIME" - if(STRING) - return "STRING" - if(BLOB) - return "BLOB" - - -#undef Default_Cursor -#undef Client_Cursor -#undef Server_Cursor -#undef TEXT_CONV -#undef RSC_FILE_CONV -#undef NUMBER_CONV -#undef IS_NUMERIC -#undef IS_BINARY -#undef IS_NOT_NULL -#undef IS_PRIMARY_KEY -#undef IS_UNSIGNED -#undef TINYINT -#undef SMALLINT -#undef MEDIUMINT -#undef INTEGER -#undef BIGINT -#undef DECIMAL -#undef FLOAT -#undef DOUBLE -#undef DATE -#undef DATETIME -#undef TIMESTAMP -#undef TIME -#undef STRING -#undef BLOB diff --git a/code/game/world.dm b/code/game/world.dm index 0f44e13a4a8..90ca10379b1 100644 --- a/code/game/world.dm +++ b/code/game/world.dm @@ -15,6 +15,7 @@ var/global/datum/global_init/init = new () */ /datum/global_init/New() + log_world("attempted init") generate_gameid() load_configuration() makeDatumRefLists() @@ -252,6 +253,7 @@ var/world_topic_spam_protect_time = world.timeofday /proc/load_configuration() + log_world("attempted load of configuration") config = new /datum/configuration() config.load("config/config.txt") config.load("config/game_options.txt", "game_options") @@ -357,51 +359,6 @@ var/world_topic_spam_protect_time = world.timeofday if (src.status != s) src.status = s -#define FAILED_DB_CONNECTION_CUTOFF 5 -var/failed_db_connections = 0 -var/failed_old_db_connections = 0 - -/hook/startup/proc/connectDB() - if(!setup_database_connection()) - log_world("Your server failed to establish a connection with the feedback database.") - else - log_world("Feedback database connection established.") - return 1 - -proc/setup_database_connection() - - if(failed_db_connections > FAILED_DB_CONNECTION_CUTOFF) //If it failed to establish a connection more than 5 times in a row, don't bother attempting to conenct anymore. - return 0 - - if(!dbcon) - dbcon = new() - - var/user = sqllogin - var/pass = sqlpass - var/db = sqldb - var/address = sqladdress - var/port = sqlport - - dbcon.Connect("dbi:mysql:[db]:[address]:[port]", "[user]", "[pass]") - . = dbcon.IsConnected() - if ( . ) - failed_db_connections = 0 //If this connection succeeded, reset the failed connections counter. - else - failed_db_connections++ //If it failed, increase the failed connections counter. - log_world(dbcon.ErrorMsg()) - - return . - -//This proc ensures that the connection to the feedback database (global variable dbcon) is established -proc/establish_db_connection() - if(failed_db_connections > FAILED_DB_CONNECTION_CUTOFF) - return 0 - - if(!dbcon || !dbcon.IsConnected()) - return setup_database_connection() - else - return 1 - /world/proc/incrementMaxZ(z_level_info) SEND_SIGNAL(SSdcs, COMSIG_WORLD_MAXZ_INCREMENTING) maxz++ diff --git a/code/global.dm b/code/global.dm index 18a0e95ca76..8efa74fbe3c 100644 --- a/code/global.dm +++ b/code/global.dm @@ -66,16 +66,18 @@ var/sqlport var/sqldb var/sqllogin var/sqlpass +var/sql_tableprefix +var/sql_async_query_timeout = 10 +var/sql_blocking_query_timeout = 5 +var/sql_pooling_min_sql_connections = 1 +var/sql_pooling_max_sql_connections = 25 +var/sql_max_concurrent_queries = 25 // For FTP requests. (i.e. downloading runtime logs.) // However it'd be ok to use for accessing attack logs and such too, which are even laggier. var/fileaccess_timer = 0 var/custom_event_msg -// Database connections. A connection is established on world creation. -// Ideally, the connection dies when the server restarts (After feedback logging.). -var/DBConnection/dbcon = new() // Feedback database (New database) - // Reference list for disposal sort junctions. Filled up by sorting junction's New() /var/list/tagger_locations = list() diff --git a/code/modules/admin/DB ban/delayed_ban.dm b/code/modules/admin/DB ban/delayed_ban.dm index 3a403110856..b62040d5cfc 100644 --- a/code/modules/admin/DB ban/delayed_ban.dm +++ b/code/modules/admin/DB ban/delayed_ban.dm @@ -24,7 +24,20 @@ GLOBAL_LIST_EMPTY(delayed_bans) banned_by_id = _banned_by_id /datum/delayed_ban/proc/execute() - var/DBQuery/query_insert = dbcon.NewQuery({"INSERT INTO bans (target_id, time, server, type, reason, job, duration, expiration_time, cid, ip, banned_by_id) VALUES ([target_id], Now(), '[server]', '[bantype_str]', '[reason]', '[job]', [(duration)?"[duration]":"0"], Now() + INTERVAL [(duration>0) ? duration : 0] MINUTE, '[computerid]', NULL, [banned_by_id])"}) + var/datum/db_query/query_insert = SSdbcore.NewQuery( + "INSERT INTO [format_table_name("bans")] \ + (target_id, time, server, type, reason, job, duration, expiration_time, cid, ip, banned_by_id) \ + VALUES (:target_id, Now(), :server, :type, :reason, :job, [duration ? "[duration]" : "0"], Now() + INTERVAL [duration > 0 ? duration : 0] MINUTE, :cid, NULL, :banned_by_id)", + list( + "target_id" = target_id, + "server" = server, + "type" = bantype_str, + "reason" = reason, + "job" = job, + "cid" = computerid, + "banned_by_id" = banned_by_id, + ) + ) query_insert.Execute() /hook/roundend/proc/explode() diff --git a/code/modules/admin/DB ban/functions.dm b/code/modules/admin/DB ban/functions.dm index 2cfe8bb1ec2..06895df1de9 100644 --- a/code/modules/admin/DB ban/functions.dm +++ b/code/modules/admin/DB ban/functions.dm @@ -5,8 +5,7 @@ datum/admins/proc/DB_ban_record(var/bantype, var/mob/banned_mob, var/duration = return - establish_db_connection() - if(!dbcon.IsConnected()) + if(!SSdbcore.Connect()) if(banned_mob.ckey) error("[key_name_admin(usr)] attempted to ban [banned_mob.ckey], but somehow server could not establish a database connection.") else @@ -45,7 +44,7 @@ datum/admins/proc/DB_ban_record(var/bantype, var/mob/banned_mob, var/duration = var/target_id var/banned_by_id - var/DBQuery/query + var/datum/db_query/query if(ismob(banned_mob)) ckey = banned_mob.ckey @@ -59,7 +58,7 @@ datum/admins/proc/DB_ban_record(var/bantype, var/mob/banned_mob, var/duration = ip = banip if(!target_id) - query = dbcon.NewQuery("SELECT id FROM players WHERE ckey = '[ckey]'") + query = SSdbcore.NewQuery("SELECT id FROM [format_table_name("players")] WHERE ckey = :ckey", list("ckey" = ckey)) query.Execute() if(!query.NextRow()) if(!banned_mob || (banned_mob && !IsGuestKey(banned_mob.key))) @@ -70,7 +69,7 @@ datum/admins/proc/DB_ban_record(var/bantype, var/mob/banned_mob, var/duration = banned_by_id = usr.client.id if(!banned_by_id) - query = dbcon.NewQuery("SELECT id FROM players WHERE ckey = '[usr.ckey]'") + query = SSdbcore.NewQuery("SELECT id FROM [format_table_name("players")] WHERE ckey = :ckey", list("ckey" = usr.ckey)) query.Execute() if(!query.NextRow()) error("[key_name_admin(usr)] attempted to ban [ckey], but somehow [key_name_admin(usr)] record does not exist in database.") @@ -80,21 +79,32 @@ datum/admins/proc/DB_ban_record(var/bantype, var/mob/banned_mob, var/duration = reason = sql_sanitize_text(reason) if(!computerid) - var/DBQuery/get_cid = dbcon.NewQuery("SELECT cid FROM players WHERE id = '[target_id]'") + var/datum/db_query/get_cid = SSdbcore.NewQuery("SELECT cid FROM [format_table_name("players")] WHERE id = :id", list("id" = target_id)) get_cid.Execute() if(get_cid.NextRow()) computerid = get_cid.item[1] - var/sql if(delayed_ban) - var/datum/delayed_ban/ban = new(target_id, server, bantype_str , reason, job, duration, computerid, banned_by_id, ip) + var/datum/delayed_ban/ban = new(target_id, server, bantype_str, reason, job, duration, computerid, banned_by_id, ip) GLOB.delayed_bans += ban return - if(banip == -1) - sql = "INSERT INTO bans (target_id, time, server, type, reason, job, duration, expiration_time, cid, ip, banned_by_id) VALUES ([target_id], Now(), '[server]', '[bantype_str]', '[reason]', '[job]', [(duration)?"[duration]":"0"], Now() + INTERVAL [(duration>0) ? duration : 0] MINUTE, '[computerid]', NULL, [banned_by_id])" - else - sql = "INSERT INTO bans (target_id, time, server, type, reason, job, duration, expiration_time, cid, ip, banned_by_id) VALUES ([target_id], Now(), '[server]', '[bantype_str]', '[reason]', '[job]', [(duration)?"[duration]":"0"], Now() + INTERVAL [(duration>0) ? duration : 0] MINUTE, '[computerid]', '[ip]', [banned_by_id])" - var/DBQuery/query_insert = dbcon.NewQuery(sql) + var/datum/db_query/query_insert = SSdbcore.NewQuery( + "INSERT INTO [format_table_name("bans")] \ + (target_id, time, server, type, reason, job, duration, expiration_time, cid, ip, banned_by_id) \ + VALUES (:target_id, Now(), :server, :type, :reason, :job, :duration, Now() + INTERVAL :duration_minutes MINUTE, :cid, '[banip == -1 ? "NULL" : ip]', :banned_by_id)", + list( + "target_id" = target_id, + "server" = server, + "type" = bantype_str, + "reason" = reason, + "job" = job, + "duration" = duration ? duration : 0, + "duration_minutes" = duration > 0 ? duration : 0, + "cid" = computerid, + "banned_by_id" = banned_by_id + ) + ) + if(!query_insert.Execute()) log_world("[key_name_admin(usr)] attempted to ban [ckey] but got error: [query_insert.ErrorMsg()].") return @@ -107,8 +117,7 @@ datum/admins/proc/DB_ban_unban(var/ckey, var/bantype, var/job = "") if(!check_rights(R_MOD) && !check_rights(R_ADMIN)) return - establish_db_connection() - if(!dbcon.IsConnected()) + if(!SSdbcore.Connect()) error("[key_name_admin(usr)] attempted to unban [ckey], but somehow server could not establish a database connection.") return @@ -139,21 +148,27 @@ datum/admins/proc/DB_ban_unban(var/ckey, var/bantype, var/job = "") else bantype_sql = "type = '[bantype_str]'" - var/DBQuery/query = dbcon.NewQuery("SELECT id FROM players WHERE ckey = '[ckey]'") + var/datum/db_query/query = SSdbcore.NewQuery("SELECT id FROM [format_table_name("players")] WHERE ckey = :ckey", list("ckey" = ckey)) query.Execute() if(!query.NextRow()) error("[key_name_admin(usr)] attempted to unban [ckey], but [ckey] has not been seen yet.") return var/target_id = query.item[1] - var/sql = "SELECT id FROM bans WHERE target_id = [target_id] AND [bantype_sql] AND (unbanned is null OR unbanned = false)" + var/sql = "SELECT id FROM [format_table_name("bans")] WHERE target_id = :target_id AND [bantype_sql] AND (unbanned IS NULL OR unbanned = FALSE)" + + if(job) + sql += " AND job = :job" + + var/params = list("target_id" = target_id) if(job) - sql += " AND job = '[job]'" + params["job"] = job var/ban_id var/ban_number = 0 //failsafe - query = dbcon.NewQuery(sql) + query = SSdbcore.NewQuery(sql, params) + if(!query.Execute()) log_world("[key_name_admin(usr)] attempted to unban [ckey], but got error: [query.ErrorMsg()].") return @@ -182,8 +197,7 @@ datum/admins/proc/DB_ban_edit(var/banid = null, var/param = null) if(!check_rights(R_MOD) && !check_rights(R_ADMIN)) return - establish_db_connection() - if(!dbcon.IsConnected()) + if(!SSdbcore.Connect()) error("[key_name_admin(usr)] attempted to edit ban record with id [banid], but somehow server could not establish a database connection.") return @@ -196,7 +210,7 @@ datum/admins/proc/DB_ban_edit(var/banid = null, var/param = null) var/duration var/reason - var/DBQuery/query = dbcon.NewQuery("SELECT target_id, duration, reason FROM bans WHERE id = [banid]") + var/datum/db_query/query = SSdbcore.NewQuery("SELECT target_id, duration, reason FROM [format_table_name("bans")] WHERE id = :id", list("id" = banid)) query.Execute() if(query.NextRow()) target_id = query.item[1] @@ -206,7 +220,7 @@ datum/admins/proc/DB_ban_edit(var/banid = null, var/param = null) error("[key_name_admin(usr)] attempted to edit ban record with id [banid], but matching record does not exist in database.") return - query = dbcon.NewQuery("SELECT ckey FROM players WHERE id = [target_id]") + query = SSdbcore.NewQuery("SELECT ckey FROM [format_table_name("players")] WHERE id = :id", list("id" = target_id)) query.Execute() if(!query.NextRow()) error("[key_name_admin(usr)] attempted to edit [ckey]'s ban, but [ckey] has not been seen yet.") @@ -224,7 +238,7 @@ datum/admins/proc/DB_ban_edit(var/banid = null, var/param = null) if(!value) to_chat(usr, "Cancelled") return - var/DBQuery/update_query = dbcon.NewQuery("UPDATE bans SET reason = '[value]', WHERE id = [banid]") + var/datum/db_query/update_query = SSdbcore.NewQuery("UPDATE [format_table_name("bans")] SET reason = :reason WHERE id = :id", list("reason" = value, "id" = banid)) if(!update_query.Execute()) log_world("[key_name_admin(usr)] tried to edit ban for [ckey] but got error: [update_query.ErrorMsg()].") return @@ -236,7 +250,7 @@ datum/admins/proc/DB_ban_edit(var/banid = null, var/param = null) if(!isnum(value) || !value) to_chat(usr, "Cancelled") return - var/DBQuery/update_query = dbcon.NewQuery("UPDATE bans SET duration = [value], expiration_time = DATE_ADD(time, INTERVAL '[value]' MINUTE) WHERE id = [banid]") + var/datum/db_query/update_query = SSdbcore.NewQuery("UPDATE [format_table_name("bans")] SET duration = :duration, expiration_time = DATE_ADD(time, INTERVAL :duration MINUTE) WHERE id = :id", list("duration" = value, "id" = banid)) if(!update_query.Execute()) log_world("[key_name_admin(usr)] tried to edit a ban duration for [ckey] but got error: [update_query.ErrorMsg()].") return @@ -258,14 +272,13 @@ datum/admins/proc/DB_ban_unban_by_id(var/id) if(!check_rights(R_MOD) && !check_rights(R_ADMIN)) return - establish_db_connection() - if(!dbcon.IsConnected()) + if(!SSdbcore.Connect()) error("[key_name_admin(usr)] attempted to remove ban record with id [id], but somehow server could not establish a database connection.") return var/ckey - var/DBQuery/query = dbcon.NewQuery("SELECT target_id FROM bans WHERE id = [id]") + var/datum/db_query/query = SSdbcore.NewQuery("SELECT target_id FROM [format_table_name("bans")] WHERE id = :id", list("id" = id)) query.Execute() if(query.NextRow()) ckey = query.item[1] @@ -276,16 +289,14 @@ datum/admins/proc/DB_ban_unban_by_id(var/id) if(!src.owner || !istype(src.owner, /client)) return - query = dbcon.NewQuery("SELECT id FROM players WHERE ckey = '[usr.ckey]'") + query = SSdbcore.NewQuery("SELECT id FROM [format_table_name("players")] WHERE ckey = :ckey", list("ckey" = usr.ckey)) query.Execute() if(!query.NextRow()) error("[key_name_admin(usr)] attempted to remove ban record with id [id], but admin database record does not exist.") return var/admin_id = query.item[1] - var/sql_update = "UPDATE bans SET unbanned = 1, unbanned_time = Now(), unbanned_by_id = [admin_id] WHERE id = [id]" - - var/DBQuery/query_update = dbcon.NewQuery(sql_update) + var/datum/db_query/query_update = SSdbcore.NewQuery("UPDATE [format_table_name("bans")] SET unbanned = 1, unbanned_time = Now(), unbanned_by_id = :admin_id WHERE id = :id", list("admin_id" = admin_id, "id" = id)) if(!query_update.Execute()) log_world("[key_name_admin(usr)] tried to unban [ckey] but got error: [query_update.ErrorMsg()].") return @@ -310,8 +321,7 @@ datum/admins/proc/DB_ban_unban_by_id(var/id) if(!check_rights(R_MOD) && !check_rights(R_ADMIN)) return - establish_db_connection() - if(!dbcon.IsConnected()) + if(!SSdbcore.Connect()) to_chat(usr, "\red Failed to establish database connection") return @@ -402,13 +412,13 @@ datum/admins/proc/DB_ban_unban_by_id(var/id) output += "" var/player_id - var/DBQuery/query = dbcon.NewQuery("SELECT id FROM players WHERE ckey='[playerckey]'") + var/datum/db_query/query = SSdbcore.NewQuery("SELECT id FROM [format_table_name("players")] WHERE ckey = :ckey", list("ckey" = playerckey)) query.Execute() if(query.NextRow()) player_id = query.item[1] var/admin_id - query = dbcon.NewQuery("SELECT id FROM players WHERE ckey='[adminckey]'") + query = SSdbcore.NewQuery("SELECT id FROM [format_table_name("players")] WHERE ckey = :ckey", list("ckey" = adminckey)) query.Execute() if(query.NextRow()) admin_id = query.item[1] @@ -451,8 +461,9 @@ datum/admins/proc/DB_ban_unban_by_id(var/id) else bantypesearch += "'PERMABAN' " - - var/DBQuery/select_query = dbcon.NewQuery("SELECT id, time, type, reason, job, duration, expiration_time, target_id, banned_by_id, unbanned, unbanned_by_id, unbanned_time, ip, cid FROM bans WHERE 1 [playersearch] [adminsearch] [ipsearch] [cidsearch] [bantypesearch] ORDER BY time DESC LIMIT 100") + var/datum/db_query/select_query = SSdbcore.NewQuery( + "SELECT id, time, type, reason, job, duration, expiration_time, target_id, banned_by_id, unbanned, unbanned_by_id, unbanned_time, ip, cid \ + FROM [format_table_name("bans")] WHERE 1 [playersearch] [adminsearch] [ipsearch] [cidsearch] [bantypesearch] ORDER BY time DESC LIMIT 100") select_query.Execute() var/now = time2text(world.realtime, "YYYY-MM-DD hh:mm:ss") // MUST BE the same format as SQL gives us the dates in, and MUST be least to most specific (i.e. year, month, day not day, month, year) @@ -476,17 +487,17 @@ datum/admins/proc/DB_ban_unban_by_id(var/id) var/banned_by_ckey var/unbanned_by_ckey - query = dbcon.NewQuery("SELECT ckey FROM players WHERE id = [target_id]") + query = SSdbcore.NewQuery("SELECT ckey FROM [format_table_name("players")] WHERE id = :id", list("id" = target_id)) query.Execute() if(query.NextRow()) target_ckey = query.item[1] - query = dbcon.NewQuery("SELECT ckey FROM players WHERE id = [banned_by_id]") + query = SSdbcore.NewQuery("SELECT ckey FROM [format_table_name("players")] WHERE id = :id", list("id" = banned_by_id)) query.Execute() if(query.NextRow()) banned_by_ckey = query.item[1] - query = dbcon.NewQuery("SELECT ckey FROM players WHERE id = [unbanned_by_id]") + query = SSdbcore.NewQuery("SELECT ckey FROM [format_table_name("players")] WHERE id = :id", list("id" = unbanned_by_id)) query.Execute() if(query.NextRow()) unbanned_by_ckey = query.item[1] diff --git a/code/modules/admin/DB_search/search.dm b/code/modules/admin/DB_search/search.dm index 3624a2c19ac..5a1814e21f9 100644 --- a/code/modules/admin/DB_search/search.dm +++ b/code/modules/admin/DB_search/search.dm @@ -2,24 +2,24 @@ var/datum/browser/panel var/empty = 1 -/datum/DB_search/verb/new_search_related(var/ckey as text) +/datum/DB_search/verb/new_search_related(ckey as text) set category = "Admin" set name = "Search related accounts" set desc = "Search players with same IP or CID" var/list/ip_related_ckeys = list() var/list/cid_related_ckeys = list() - var/DBQuery/search_query = dbcon.NewQuery("SELECT ip_related_ids, cid_related_ids FROM players WHERE ckey = '[sanitizeSQL(ckey)]'") + var/datum/db_query/search_query = SSdbcore.NewQuery("SELECT ip_related_ids, cid_related_ids FROM [format_table_name("players")] WHERE ckey = :ckey", list(ckey = ckey)) search_query.Execute() if(search_query.NextRow()) ip_related_ckeys = splittext(search_query.item[1], ",") cid_related_ckeys = splittext(search_query.item[2], ",") - search_query = dbcon.NewQuery("SELECT ckey FROM players WHERE id IN ([jointext(ip_related_ckeys, ",")])") + search_query = SSdbcore.NewQuery("SELECT ckey FROM [format_table_name("players")] WHERE id IN ([jointext(ip_related_ckeys, ",")])") search_query.Execute() ip_related_ckeys = list() while(search_query.NextRow()) ip_related_ckeys += search_query.item[1] - search_query = dbcon.NewQuery("SELECT ckey FROM players WHERE id IN ([jointext(cid_related_ckeys, ",")])") + search_query = SSdbcore.NewQuery("SELECT ckey FROM [format_table_name("players")] WHERE id IN ([jointext(cid_related_ckeys, ",")])") search_query.Execute() cid_related_ckeys = list() while(search_query.NextRow()) @@ -33,6 +33,7 @@ else to_chat(usr,"No player with ckey = [ckey] found.") + qdel(search_query) /datum/DB_search/verb/new_search() set category = "Admin" @@ -44,8 +45,7 @@ /datum/DB_search/proc/DB_players_search() - establish_db_connection() - if(!dbcon.IsConnected()) + if(!SSdbcore.Connect()) to_chat(usr, "\red Failed to establish database connection") return @@ -113,10 +113,13 @@ hsrc.empty = 1 if(dbsearchckey_search || dbsearchip_search || dbsearchcid_search) hsrc.empty = 0 - var/DBQuery/search_query = dbcon.NewQuery("SELECT ckey, ip, cid, last_seen FROM players WHERE ckey = '[sanitizeSQL(dbsearchckey_search)]' OR ip = '[sanitizeSQL(dbsearchip_search)]' OR cid = '[sanitizeSQL(dbsearchcid_search)]'") - search_query.Execute() + var/datum/db_query/search_query = SSdbcore.NewQuery( + "SELECT ckey, ip, computerid, lastseen FROM [format_table_name("players")] WHERE ckey = :ckey OR ip = :ip OR computerid = :cid", + list(ckey = dbsearchckey_search, ip = dbsearchip_search, cid = dbsearchcid_search) + ) + search_query.warn_execute() while(search_query.NextRow()) output = "
| AUTHOR | TITLE | CATEGORY | SS13BN |
| TITLE | AUTHOR | CATEGORY | |
| [author] | [title] | [category] | \[Order\] |
| [author] | [title] | [category] | \[Order\] |
- Yes.
- No.
"}
+ Yes.
+ No.
"}
//dat += "Close
"
user << browse(dat, "window=library")
onclose(user, "library")
-/obj/machinery/librarycomp/emag_act(var/remaining_charges, var/mob/user)
+/obj/machinery/librarycomp/emag_act(remaining_charges, mob/user)
if (src.density && !src.emagged)
src.emagged = 1
return 1
@@ -240,7 +235,7 @@ datum/borrowbook // Datum used to keep track of who has borrowed what when and f
var/obj/item/barcodescanner/scanner = W
scanner.computer = src
to_chat(user, "[scanner]'s associated machine has been set to [src].")
- for (var/mob/V in hearers(src))
+ for (var/mob/V in hearers(get_turf(src)))
V.show_message("[src] lets out a low, short blip.", 2)
else
..()
@@ -275,7 +270,7 @@ datum/borrowbook // Datum used to keep track of who has borrowed what when and f
bibledelay = 0
else
- for (var/mob/V in hearers(src))
+ for (var/mob/V in hearers(get_turf(src)))
V.show_message("[src]'s monitor flashes, \"Bible printer currently unavailable, please wait a moment.\"")
if("7")
@@ -319,28 +314,30 @@ datum/borrowbook // Datum used to keep track of who has borrowed what when and f
if(scanner.cache.unique)
alert("This book has been rejected from the database. Aborting!")
else
- establish_db_connection()
- if(!dbcon.IsConnected())
+ if(!SSdbcore.Connect())
alert("Connection to Archive has been severed. Aborting.")
else
/*
- var/sqltitle = dbcon.Quote(scanner.cache.name)
- var/sqlauthor = dbcon.Quote(scanner.cache.author)
- var/sqlcontent = dbcon.Quote(scanner.cache.dat)
- var/sqlcategory = dbcon.Quote(upload_category)
+ var/sqltitle = SSdbcore.Quote(scanner.cache.name)
+ var/sqlauthor = SSdbcore.Quote(scanner.cache.author)
+ var/sqlcontent = SSdbcore.Quote(scanner.cache.dat)
+ var/sqlcategory = SSdbcore.Quote(upload_category)
*/
- var/sqltitle = sanitizeSQL(scanner.cache.name)
- var/sqlauthor = sanitizeSQL(scanner.cache.author)
- var/sqlcontent = sanitizeSQL(scanner.cache.dat)
- var/sqlcategory = sanitizeSQL(upload_category)
+ var/sqltitle = scanner.cache.name
+ var/sqlauthor = scanner.cache.author
+ var/sqlcontent = scanner.cache.dat
+ var/sqlcategory = upload_category
var/author_id = null
- var/DBQuery/get_author_id = dbcon.NewQuery("SELECT id FROM players WHERE ckey='[usr.ckey]'")
+ var/datum/db_query/get_author_id = SSdbcore.NewQuery("SELECT id FROM [format_table_name("players")] WHERE ckey = :ckey", list("ckey" = usr.ckey))
get_author_id.Execute()
if(get_author_id.NextRow())
author_id = get_author_id.item[1]
- var/DBQuery/query = dbcon.NewQuery("INSERT INTO library (author, title, content, category, author_id) VALUES ('[sqlauthor]', '[sqltitle]', '[sqlcontent]', '[sqlcategory]', [author_id])")
+ var/datum/db_query/query = SSdbcore.NewQuery(
+ "INSERT INTO [format_table_name("library")] (author, title, content, category, author_id) VALUES (:sqlauthor, :sqltitle, :sqlcontent, :sqlcategory, :author_id)",
+ list("sqlauthor" = sqlauthor, "sqltitle" = sqltitle, "sqlcontent" = sqlcontent, "sqlcategory" = sqlcategory,"author_id" = author_id)
+ )
if(!query.Execute())
to_chat(usr, query.ErrorMsg())
else
@@ -349,18 +346,17 @@ datum/borrowbook // Datum used to keep track of who has borrowed what when and f
alert("Upload Complete.")
if(href_list["targetid"])
- var/sqlid = sanitizeSQL(href_list["targetid"])
- establish_db_connection()
- if(!dbcon.IsConnected())
+ var/sqlid = href_list["targetid"]
+ if(!SSdbcore.Connect())
alert("Connection to Archive has been severed. Aborting.")
if(bibledelay)
- for (var/mob/V in hearers(src))
+ for (var/mob/V in hearers(get_turf(src)))
V.show_message("[src]'s monitor flashes, \"Printer unavailable. Please allow a short time before attempting to print.\"")
else
bibledelay = 1
spawn(60)
bibledelay = 0
- var/DBQuery/query = dbcon.NewQuery("SELECT * FROM library WHERE id=[sqlid]")
+ var/datum/db_query/query = SSdbcore.NewQuery("SELECT * FROM [format_table_name("library")] WHERE id = :sqlid", list("sqlid" = sqlid))
query.Execute()
while(query.NextRow())
@@ -397,21 +393,21 @@ datum/borrowbook // Datum used to keep track of who has borrowed what when and f
density = TRUE
var/obj/item/book/cache // Last scanned book
-/obj/machinery/libraryscanner/attackby(var/obj/O as obj, var/mob/user as mob)
+/obj/machinery/libraryscanner/attackby(obj/O as obj, mob/user as mob)
if(istype(O, /obj/item/book))
user.drop_item()
O.loc = src
-/obj/machinery/libraryscanner/attack_hand(var/mob/user as mob)
+/obj/machinery/libraryscanner/attack_hand(mob/user as mob)
usr.set_machine(src)
var/dat = "