You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 
 

365 lines
12 KiB

  1. SUBSYSTEM_DEF(dbcore)
  2. name = "Database"
  3. flags = SS_BACKGROUND
  4. wait = 1 MINUTES
  5. init_order = INIT_ORDER_DBCORE
  6. var/const/FAILED_DB_CONNECTION_CUTOFF = 5
  7. var/schema_mismatch = 0
  8. var/db_minor = 0
  9. var/db_major = 0
  10. var/failed_connections = 0
  11. var/last_error
  12. var/list/active_queries = list()
  13. var/datum/BSQL_Connection/connection
  14. var/datum/BSQL_Operation/connectOperation
  15. /datum/controller/subsystem/dbcore/Initialize()
  16. //We send warnings to the admins during subsystem init, as the clients will be New'd and messages
  17. //will queue properly with goonchat
  18. switch(schema_mismatch)
  19. if(1)
  20. message_admins("Database schema ([db_major].[db_minor]) doesn't match the latest schema version ([DB_MAJOR_VERSION].[DB_MINOR_VERSION]), this may lead to undefined behaviour or errors")
  21. if(2)
  22. message_admins("Could not get schema version from database")
  23. return ..()
  24. /datum/controller/subsystem/dbcore/fire()
  25. for(var/I in active_queries)
  26. var/datum/DBQuery/Q = I
  27. if(world.time - Q.last_activity_time > (5 MINUTES))
  28. message_admins("Found undeleted query, please check the server logs and notify coders.")
  29. log_sql("Undeleted query: \"[Q.sql]\" LA: [Q.last_activity] LAT: [Q.last_activity_time]")
  30. qdel(Q)
  31. if(MC_TICK_CHECK)
  32. return
  33. /datum/controller/subsystem/dbcore/Recover()
  34. connection = SSdbcore.connection
  35. connectOperation = SSdbcore.connectOperation
  36. /datum/controller/subsystem/dbcore/Shutdown()
  37. //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
  38. if(SSdbcore.Connect())
  39. var/datum/DBQuery/query_round_shutdown = SSdbcore.NewQuery("UPDATE [format_table_name("round")] SET shutdown_datetime = Now(), end_state = '[sanitizeSQL(SSticker.end_state)]' WHERE id = [GLOB.round_id]")
  40. query_round_shutdown.Execute()
  41. qdel(query_round_shutdown)
  42. if(IsConnected())
  43. Disconnect()
  44. world.BSQL_Shutdown()
  45. //nu
  46. /datum/controller/subsystem/dbcore/can_vv_get(var_name)
  47. return var_name != NAMEOF(src, connection) && var_name != NAMEOF(src, active_queries) && var_name != NAMEOF(src, connectOperation) && ..()
  48. /datum/controller/subsystem/dbcore/vv_edit_var(var_name, var_value)
  49. if(var_name == NAMEOF(src, connection) || var_name == NAMEOF(src, connectOperation))
  50. return FALSE
  51. return ..()
  52. /datum/controller/subsystem/dbcore/proc/Connect()
  53. if(IsConnected())
  54. return TRUE
  55. if(failed_connections > FAILED_DB_CONNECTION_CUTOFF) //If it failed to establish a connection more than 5 times in a row, don't bother attempting to connect anymore.
  56. return FALSE
  57. if(!CONFIG_GET(flag/sql_enabled))
  58. return FALSE
  59. var/user = CONFIG_GET(string/feedback_login)
  60. var/pass = CONFIG_GET(string/feedback_password)
  61. var/db = CONFIG_GET(string/feedback_database)
  62. var/address = CONFIG_GET(string/address)
  63. var/port = CONFIG_GET(number/port)
  64. connection = new /datum/BSQL_Connection(BSQL_CONNECTION_TYPE_MARIADB, CONFIG_GET(number/async_query_timeout), CONFIG_GET(number/blocking_query_timeout))
  65. var/error
  66. if(QDELETED(connection))
  67. connection = null
  68. error = last_error
  69. else
  70. SSdbcore.last_error = null
  71. connectOperation = connection.BeginConnect(address, port, user, pass, db)
  72. if(SSdbcore.last_error)
  73. CRASH(SSdbcore.last_error)
  74. UNTIL(connectOperation.IsComplete())
  75. error = connectOperation.GetError()
  76. . = !error
  77. if (!.)
  78. log_sql("Connect() failed | [error]")
  79. ++failed_connections
  80. QDEL_NULL(connection)
  81. QDEL_NULL(connectOperation)
  82. /datum/controller/subsystem/dbcore/proc/CheckSchemaVersion()
  83. if(CONFIG_GET(flag/sql_enabled))
  84. if(Connect())
  85. log_world("Database connection established.")
  86. var/datum/DBQuery/query_db_version = NewQuery("SELECT major, minor FROM [format_table_name("schema_revision")] ORDER BY date DESC LIMIT 1")
  87. query_db_version.Execute()
  88. if(query_db_version.NextRow())
  89. db_major = text2num(query_db_version.item[1])
  90. db_minor = text2num(query_db_version.item[2])
  91. if(db_major != DB_MAJOR_VERSION || db_minor != DB_MINOR_VERSION)
  92. schema_mismatch = 1 // flag admin message about mismatch
  93. log_sql("Database schema ([db_major].[db_minor]) doesn't match the latest schema version ([DB_MAJOR_VERSION].[DB_MINOR_VERSION]), this may lead to undefined behaviour or errors")
  94. else
  95. schema_mismatch = 2 //flag admin message about no schema version
  96. log_sql("Could not get schema version from database")
  97. qdel(query_db_version)
  98. else
  99. log_sql("Your server failed to establish a connection with the database.")
  100. else
  101. log_sql("Database is not enabled in configuration.")
  102. /datum/controller/subsystem/dbcore/proc/SetRoundID()
  103. if(!Connect())
  104. return
  105. var/datum/DBQuery/query_round_initialize = SSdbcore.NewQuery("INSERT INTO [format_table_name("round")] (initialize_datetime, server_ip, server_port) VALUES (Now(), INET_ATON(IF('[world.internet_address]' LIKE '', '0', '[world.internet_address]')), '[world.port]')")
  106. query_round_initialize.Execute()
  107. qdel(query_round_initialize)
  108. var/datum/DBQuery/query_round_last_id = SSdbcore.NewQuery("SELECT LAST_INSERT_ID()")
  109. query_round_last_id.Execute()
  110. if(query_round_last_id.NextRow())
  111. GLOB.round_id = query_round_last_id.item[1]
  112. qdel(query_round_last_id)
  113. /datum/controller/subsystem/dbcore/proc/SetRoundStart()
  114. if(!Connect())
  115. return
  116. var/datum/DBQuery/query_round_start = SSdbcore.NewQuery("UPDATE [format_table_name("round")] SET start_datetime = Now() WHERE id = [GLOB.round_id]")
  117. query_round_start.Execute()
  118. qdel(query_round_start)
  119. /datum/controller/subsystem/dbcore/proc/SetRoundEnd()
  120. if(!Connect())
  121. return
  122. var/sql_station_name = sanitizeSQL(station_name())
  123. var/datum/DBQuery/query_round_end = SSdbcore.NewQuery("UPDATE [format_table_name("round")] SET end_datetime = Now(), game_mode_result = '[sanitizeSQL(SSticker.mode_result)]', station_name = '[sql_station_name]' WHERE id = [GLOB.round_id]")
  124. query_round_end.Execute()
  125. qdel(query_round_end)
  126. /datum/controller/subsystem/dbcore/proc/Disconnect()
  127. failed_connections = 0
  128. QDEL_NULL(connectOperation)
  129. QDEL_NULL(connection)
  130. /datum/controller/subsystem/dbcore/proc/IsConnected()
  131. if(!CONFIG_GET(flag/sql_enabled))
  132. return FALSE
  133. //block until any connect operations finish
  134. var/datum/BSQL_Connection/_connection = connection
  135. var/datum/BSQL_Operation/op = connectOperation
  136. UNTIL(QDELETED(_connection) || op.IsComplete())
  137. return !QDELETED(connection) && !op.GetError()
  138. /datum/controller/subsystem/dbcore/proc/Quote(str)
  139. if(connection)
  140. return connection.Quote(str)
  141. /datum/controller/subsystem/dbcore/proc/ErrorMsg()
  142. if(!CONFIG_GET(flag/sql_enabled))
  143. return "Database disabled by configuration"
  144. return last_error
  145. /datum/controller/subsystem/dbcore/proc/ReportError(error)
  146. last_error = error
  147. /datum/controller/subsystem/dbcore/proc/NewQuery(sql_query)
  148. if(IsAdminAdvancedProcCall())
  149. log_admin_private("ERROR: Advanced admin proc call led to sql query: [sql_query]. Query has been blocked")
  150. message_admins("ERROR: Advanced admin proc call led to sql query. Query has been blocked")
  151. return FALSE
  152. return new /datum/DBQuery(sql_query, connection)
  153. /*
  154. Takes a list of rows (each row being an associated list of column => value) and inserts them via a single mass query.
  155. Rows missing columns present in other rows will resolve to SQL NULL
  156. You are expected to do your own escaping of the data, and expected to provide your own quotes for strings.
  157. The duplicate_key arg can be true to automatically generate this part of the query
  158. or set to a string that is appended to the end of the query
  159. Ignore_errors instructes mysql to continue inserting rows if some of them have errors.
  160. the erroneous row(s) aren't inserted and there isn't really any way to know why or why errored
  161. Delayed insert mode was removed in mysql 7 and only works with MyISAM type tables,
  162. It was included because it is still supported in mariadb.
  163. It does not work with duplicate_key and the mysql server ignores it in those cases
  164. */
  165. /datum/controller/subsystem/dbcore/proc/MassInsert(table, list/rows, duplicate_key = FALSE, ignore_errors = FALSE, delayed = FALSE, warn = FALSE, async = FALSE)
  166. if (!table || !rows || !istype(rows))
  167. return
  168. var/list/columns = list()
  169. var/list/sorted_rows = list()
  170. for (var/list/row in rows)
  171. var/list/sorted_row = list()
  172. sorted_row.len = columns.len
  173. for (var/column in row)
  174. var/idx = columns[column]
  175. if (!idx)
  176. idx = columns.len + 1
  177. columns[column] = idx
  178. sorted_row.len = columns.len
  179. sorted_row[idx] = row[column]
  180. sorted_rows[++sorted_rows.len] = sorted_row
  181. if (duplicate_key == TRUE)
  182. var/list/column_list = list()
  183. for (var/column in columns)
  184. column_list += "[column] = VALUES([column])"
  185. duplicate_key = "ON DUPLICATE KEY UPDATE [column_list.Join(", ")]\n"
  186. else if (duplicate_key == FALSE)
  187. duplicate_key = null
  188. if (ignore_errors)
  189. ignore_errors = " IGNORE"
  190. else
  191. ignore_errors = null
  192. if (delayed)
  193. delayed = " DELAYED"
  194. else
  195. delayed = null
  196. var/list/sqlrowlist = list()
  197. var/len = columns.len
  198. for (var/list/row in sorted_rows)
  199. if (length(row) != len)
  200. row.len = len
  201. for (var/value in row)
  202. if (value == null)
  203. value = "NULL"
  204. sqlrowlist += "([row.Join(", ")])"
  205. sqlrowlist = " [sqlrowlist.Join(",\n ")]"
  206. var/datum/DBQuery/Query = NewQuery("INSERT[delayed][ignore_errors] INTO [table]\n([columns.Join(", ")])\nVALUES\n[sqlrowlist]\n[duplicate_key]")
  207. if (warn)
  208. . = Query.warn_execute(async)
  209. else
  210. . = Query.Execute(async)
  211. qdel(Query)
  212. /datum/DBQuery
  213. var/sql // The sql query being executed.
  214. var/list/item //list of data values populated by NextRow()
  215. var/last_activity
  216. var/last_activity_time
  217. var/last_error
  218. var/skip_next_is_complete
  219. var/in_progress
  220. var/datum/BSQL_Connection/connection
  221. var/datum/BSQL_Operation/Query/query
  222. /datum/DBQuery/New(sql_query, datum/BSQL_Connection/connection)
  223. SSdbcore.active_queries[src] = TRUE
  224. Activity("Created")
  225. item = list()
  226. src.connection = connection
  227. sql = sql_query
  228. /datum/DBQuery/Destroy()
  229. Close()
  230. SSdbcore.active_queries -= src
  231. return ..()
  232. /datum/DBQuery/CanProcCall(proc_name)
  233. //fuck off kevinz
  234. return FALSE
  235. /datum/DBQuery/proc/SetQuery(new_sql)
  236. if(in_progress)
  237. CRASH("Attempted to set new sql while waiting on active query")
  238. Close()
  239. sql = new_sql
  240. /datum/DBQuery/proc/Activity(activity)
  241. last_activity = activity
  242. last_activity_time = world.time
  243. /datum/DBQuery/proc/warn_execute(async = FALSE)
  244. . = Execute(async)
  245. if(!.)
  246. to_chat(usr, "<span class='danger'>A SQL error occurred during this operation, check the server logs.</span>")
  247. /datum/DBQuery/proc/Execute(async = FALSE, log_error = TRUE)
  248. Activity("Execute")
  249. if(in_progress)
  250. CRASH("Attempted to start a new query while waiting on the old one")
  251. if(QDELETED(connection))
  252. last_error = "No connection!"
  253. return FALSE
  254. var/start_time
  255. var/timed_out
  256. if(!async)
  257. start_time = REALTIMEOFDAY
  258. Close()
  259. query = connection.BeginQuery(sql)
  260. if(!async)
  261. timed_out = !query.WaitForCompletion()
  262. else
  263. in_progress = TRUE
  264. UNTIL(query.IsComplete())
  265. in_progress = FALSE
  266. skip_next_is_complete = TRUE
  267. var/error = QDELETED(query) ? "Query object deleted!" : query.GetError()
  268. last_error = error
  269. . = !error
  270. if(!. && log_error)
  271. log_sql("[error] | Query used: [sql]")
  272. if(!async && timed_out)
  273. log_query_debug("Query execution started at [start_time]")
  274. log_query_debug("Query execution ended at [REALTIMEOFDAY]")
  275. log_query_debug("Slow query timeout detected.")
  276. log_query_debug("Query used: [sql]")
  277. slow_query_check()
  278. /datum/DBQuery/proc/slow_query_check()
  279. message_admins("HEY! A database query timed out. Did the server just hang? <a href='?_src_=holder;[HrefToken()];slowquery=yes'>\[YES\]</a>|<a href='?_src_=holder;[HrefToken()];slowquery=no'>\[NO\]</a>")
  280. /datum/DBQuery/proc/NextRow(async)
  281. Activity("NextRow")
  282. UNTIL(!in_progress)
  283. if(!skip_next_is_complete)
  284. if(!async)
  285. query.WaitForCompletion()
  286. else
  287. in_progress = TRUE
  288. UNTIL(query.IsComplete())
  289. in_progress = FALSE
  290. else
  291. skip_next_is_complete = FALSE
  292. last_error = query.GetError()
  293. var/list/results = query.CurrentRow()
  294. . = results != null
  295. item.Cut()
  296. //populate item array
  297. for(var/I in results)
  298. item += results[I]
  299. /datum/DBQuery/proc/ErrorMsg()
  300. return last_error
  301. /datum/DBQuery/proc/Close()
  302. item.Cut()
  303. QDEL_NULL(query)
  304. /world/BSQL_Debug(message)
  305. if(!CONFIG_GET(flag/bsql_debug))
  306. return
  307. //strip sensitive stuff
  308. if(findtext(message, ": CreateConnection("))
  309. message = "CreateConnection CENSORED"
  310. log_sql("BSQL_DEBUG: [message]")