diff --git a/features/server.lua b/features/server.lua index 020916b5..0ea371b6 100644 --- a/features/server.lua +++ b/features/server.lua @@ -6,6 +6,7 @@ local Event = require 'utils.event' local Game = require 'utils.game' local Timestamp = require 'utils.timestamp' local Print = require('utils.print_override') +local ErrorLogging = require 'utils.error_logging' local serialize = serpent.serialize local concat = table.concat @@ -18,11 +19,13 @@ local serialize_options = {sparse = true, compact = true} local Public = {} local server_time = {secs = nil, tick = 0} +ErrorLogging.server_time = server_time Global.register( server_time, function(tbl) server_time = tbl + ErrorLogging.server_time = tbl end ) @@ -299,6 +302,7 @@ local function data_set_changed(data) local success, err = pcall(handler, data) if not success then log(err) + ErrorLogging.generate_error_report(err) error(err, 2) end end @@ -307,6 +311,7 @@ local function data_set_changed(data) local success, err = pcall(handler, data) if not success then log(err) + ErrorLogging.generate_error_report(err) end end end diff --git a/utils/command.lua b/utils/command.lua index 01cb5f5c..4540180b 100644 --- a/utils/command.lua +++ b/utils/command.lua @@ -2,6 +2,7 @@ local Event = require 'utils.event' local Game = require 'utils.game' local Utils = require 'utils.core' local Timestamp = require 'utils.timestamp' +local ErrorLogging = require 'utils.error_logging' local Rank = require 'features.rank_system' local Donator = require 'features.donator' local Server = require 'features.server' @@ -247,11 +248,14 @@ function Command.add(command_name, options, callback) if _DEBUG then print(format("%s triggered an error running a command and has been logged: '%s' with arguments %s", player_name, command_name, serialized_arguments)) print(error) + ErrorLogging.generate_error_report(error) return end print(format('There was an error running %s, it has been logged.', command_name)) - log(format("Error while running '%s' with arguments %s: %s", command_name, serialized_arguments, error)) + local err = format("Error while running '%s' with arguments %s: %s", command_name, serialized_arguments, error) + log(err) + ErrorLogging.generate_error_report(err) end end) end diff --git a/utils/error_logging.lua b/utils/error_logging.lua new file mode 100644 index 00000000..d3af104e --- /dev/null +++ b/utils/error_logging.lua @@ -0,0 +1,86 @@ +--[[ + This module creates a file of just trapped lua errors. It is possible that this module misses errors, therefore it is advised + that users also verify their server/game logs. +]] +-- Dependencies +local Timestamp = require 'utils.timestamp' + +-- Localized functions +local floor = math.floor +local format = string.format +local insert = table.insert +local concat = table.concat + +-- Local constants +local minutes_to_ticks = 60 * 60 +local hours_to_ticks = 60 * 60 * 60 +local ticks_to_minutes = 1 / minutes_to_ticks +local ticks_to_hours = 1 / hours_to_ticks +local warning = '\n\n\n\nTHIS LOG IS NOT ALL-INCLUSIVE AND CAN MISS ERRORS. IF THERE ARE ANY SUSPICIONS OF ERRORS CHECK THE LOGS.\n\n\n\n' + +-- Local vars +local Public = { + server_time = {secs = nil, tick = 0} +} +local first_error = true + +--- Copied from utils.core, turns ticks into a human-readable time. +local function format_time(ticks) + local result = {} + + local hours = floor(ticks * ticks_to_hours) + if hours > 0 then + ticks = ticks - hours * hours_to_ticks + insert(result, hours) + if hours == 1 then + insert(result, 'hour') + else + insert(result, 'hours') + end + end + + local minutes = floor(ticks * ticks_to_minutes) + insert(result, minutes) + if minutes == 1 then + insert(result, 'minute') + else + insert(result, 'minutes') + end + + return concat(result, ' ') +end + +--- Takes the given string and generates an entry in the error file. +function Public.generate_error_report(str) + Debug.print() + local server_time = Public.server_time.secs + + local server_time_str = '(Server time: unavailable)' + local file_name = 'redmew_errors.log' + if server_time then + server_time_str = format('(Server time: %s)', Timestamp.to_string(server_time)) + file_name = Timestamp.to_date_string(server_time) .. '_' .. file_name + else + game.write_file(file_name, '', false, 0) + end + + if first_error then + server_time_str = warning .. server_time_str + first_error = nil + end + + local tick = 'pre-game' + if game then + tick = format_time(game.tick) + end + tick = 'Time of error: ' .. tick + + local redmew_version = global.redmew_version or 'Unknown' + redmew_version = 'RedMew version: ' .. redmew_version + + local output = concat({server_time_str, tick, redmew_version, str, '\n'}, '\n') + + game.write_file(file_name, output, true, 0) +end + +return Public diff --git a/utils/event_core.lua b/utils/event_core.lua index 6ea0dac0..76e5eaf9 100644 --- a/utils/event_core.lua +++ b/utils/event_core.lua @@ -1,5 +1,6 @@ -- This module exists to break the circular dependency between event.lua and global.lua. -- It is not expected that any user code would require this module instead event.lua should be required. +local ErrorLogging = require 'utils.error_logging' local Public = {} @@ -16,18 +17,22 @@ local log = log local script_on_event = script.on_event local script_on_nth_tick = script.on_nth_tick -local function call_handlers(handlers, event) - if _DEBUG then +local call_handlers +if _DEBUG then + function call_handlers(handlers, event) for i = 1, #handlers do local handler = handlers[i] handler(event) end - else + end +else + function call_handlers(handlers, event) for i = 1, #handlers do local handler = handlers[i] local success, error = pcall(handler, event) if not success then log(error) + ErrorLogging.generate_error_report(error) end end end diff --git a/utils/task.lua b/utils/task.lua index a43c3174..a1d30ab9 100644 --- a/utils/task.lua +++ b/utils/task.lua @@ -8,6 +8,7 @@ local Queue = require 'utils.queue' local PriorityQueue = require 'utils.priority_queue' local Event = require 'utils.event' local Token = require 'utils.token' +local ErrorLogging = require 'utils.error_logging' local Task = {} @@ -45,6 +46,7 @@ local function on_tick() error(result) else log(result) + ErrorLogging.generate_error_report(result) end Queue.pop(queue) global.total_task_weight = global.total_task_weight - task.weight @@ -64,6 +66,7 @@ local function on_tick() error(result) else log(result) + ErrorLogging.generate_error_report(result) end end PriorityQueue.pop(callbacks, comp) diff --git a/utils/timestamp.lua b/utils/timestamp.lua index 9af76e5d..ccf9b7bd 100644 --- a/utils/timestamp.lua +++ b/utils/timestamp.lua @@ -119,15 +119,15 @@ local function normalise(year, month, day, hour, min, sec) end --- Converts unix epoch timestamp into table {year: number, month: number, day: number, hour: number, min: number, sec: number} --- @param sec unix epoch timestamp --- @return {year: number, month: number, day: number, hour: number, min: number, sec: number} +-- @param sec unix epoch timestamp +-- @return {year: number, month: number, day: number, hour: number, min: number, sec: number} function Public.to_timetable(secs) return normalise(1970, 1, 1, 0, 0, secs) end --- Converts timetable into unix epoch timestamp --- @param timetable
{year: number, month: number, day: number, hour: number, min: number, sec: number} --- @return number +-- @param
timetable {year: number, month: number, day: number, hour: number, min: number, sec: number} +-- @return function Public.from_timetable(timetable) local tt = normalise(timetable.year, timetable.month, timetable.day, timetable.hour, timetable.min, timetable.sec) @@ -142,11 +142,19 @@ function Public.from_timetable(timetable) end --- Converts unix epoch timestamp into human readable string. --- @param secs unix epoch timestamp --- @return string +-- @param secs unix epoch timestamp +-- @return function Public.to_string(secs) local tt = normalise(1970, 1, 1, 0, 0, secs) return strformat('%04u-%02u-%02u %02u:%02u:%02d', tt.year, tt.month, tt.day, tt.hour, tt.min, tt.sec) end +--- Converts unix epoch timestamp into a date string. +-- @param secs unix epoch timestamp +-- @return With data in format YYYY-MM-DD +function Public.to_date_string(secs) + local tt = normalise(1970, 1, 1, 0, 0, secs) + return strformat('%04u-%02u-%02u', tt.year, tt.month, tt.day) +end + return Public