diff --git a/code/__HELPERS/files.dm b/code/__HELPERS/files.dm index c773b8e9b762..6435f865b28d 100644 --- a/code/__HELPERS/files.dm +++ b/code/__HELPERS/files.dm @@ -5,6 +5,7 @@ */ GLOBAL_VAR_INIT(fileaccess_timer, 0) +/* monkestation edit: replaced in [monkestation\code\__HELPERS\files.dm] /client/proc/browse_files(root_type=BROWSE_ROOT_ALL_LOGS, max_iterations=10, list/valid_extensions=list("txt","log","htm", "html", "gz", "json")) // wow why was this ever a parameter var/root = "data/logs/" @@ -51,6 +52,7 @@ GLOBAL_VAR_INIT(fileaccess_timer, 0) return return path +*/ #define FTPDELAY 200 //200 tick delay to discourage spam #define ADMIN_FTPDELAY_MODIFIER 0.5 //Admins get to spam files faster since we ~trust~ them! diff --git a/code/modules/admin/admin_verbs.dm b/code/modules/admin/admin_verbs.dm index 34990daf4118..b6d545ce7d5d 100644 --- a/code/modules/admin/admin_verbs.dm +++ b/code/modules/admin/admin_verbs.dm @@ -253,6 +253,10 @@ GLOBAL_PROTECT(admin_verbs_debug) /client/proc/validate_cards, /client/proc/validate_puzzgrids, /client/proc/view_runtimes, + // monkestation verbs: debugger tools + /client/proc/log_viewer_new, + /client/proc/getserverlogs_debug, + /client/proc/getcurrentlogs_debug, ) GLOBAL_LIST_INIT(admin_verbs_possess, list(/proc/possess, /proc/release)) GLOBAL_PROTECT(admin_verbs_possess) diff --git a/code/modules/admin/verbs/getlogs.dm b/code/modules/admin/verbs/getlogs.dm index 6abbbe6829b6..0b33e01bb8a8 100644 --- a/code/modules/admin/verbs/getlogs.dm +++ b/code/modules/admin/verbs/getlogs.dm @@ -11,8 +11,9 @@ set desc = "View/retrieve logfiles for the current round." set category = "Admin.Logging" - browseserverlogs(current=TRUE) + browseserverlogs(current = TRUE) +/* monkestation edit: replaced in [monkestation\code\modules\admin\verbs\getlogs.dm] /client/proc/browseserverlogs(current=FALSE) var/path = browse_files(current ? BROWSE_ROOT_CURRENT_LOGS : BROWSE_ROOT_ALL_LOGS) if(!path) @@ -33,3 +34,4 @@ return to_chat(src, "Attempting to send [path], this may take a fair few minutes if the file is very large.", confidential = TRUE) return +*/ diff --git a/monkestation/code/__HELPERS/files.dm b/monkestation/code/__HELPERS/files.dm new file mode 100644 index 000000000000..be454635ab42 --- /dev/null +++ b/monkestation/code/__HELPERS/files.dm @@ -0,0 +1,63 @@ +/client/proc/browse_files(root_type = BROWSE_ROOT_ALL_LOGS, max_iterations = 10, list/valid_extensions = list("txt", "log", "htm", "html", "gz", "json"), list/whitelist = null, allow_folder = TRUE) + var/regex/valid_ext_regex = new("\\.(?:[regex_quote_list(valid_extensions)])$", "i") + var/regex/whitelist_regex + if(whitelist) + // try not to look at it too hard. yes i wrote this by hand. + whitelist_regex = new("(?:\[\\/\\\\\]$|(?:^|\\\\|\\/)(?:[regex_quote_list(whitelist)])\\.(?:[regex_quote_list(valid_extensions)])$)", "i") + + // wow why was this ever a parameter + var/root = "data/logs/" + switch(root_type) + if(BROWSE_ROOT_ALL_LOGS) + root = "data/logs/" + if(BROWSE_ROOT_CURRENT_LOGS) + root = "[GLOB.log_directory]/" + var/path = root + + for(var/i in 1 to max_iterations) + var/list/choices + if(whitelist_regex) + choices = list() + for(var/listed_path in flist(path)) + if(whitelist_regex.Find(listed_path)) + choices += listed_path + else + choices = flist(path) + if(path != root) + choices.Insert(1, "/") + choices = sort_list(choices) + if(allow_folder) + choices += "Download Folder" + + var/choice = tgui_input_list(src, "Choose a file to access", "Download", choices) + if(!choice) + return + switch(choice) + if("/") + path = root + continue + if("Download Folder") + if(!allow_folder) + return + var/list/comp_flist = flist(path) + var/confirmation = input(src, "Are you SURE you want to download all the files in this folder? (This will open [length(comp_flist)] prompt[length(comp_flist) == 1 ? "" : "s"])", "Confirmation") in list("Yes", "No") + if(confirmation != "Yes") + continue + for(var/file in comp_flist) + src << ftp(path + file) + return + path += choice + + if(copytext_char(path, -1) != "/") //didn't choose a directory, no need to iterate again + break + if(!fexists(path) || !valid_ext_regex.Find(path)) + to_chat(src, "Error: browse_files(): File not found/Invalid file([path]).") + return + + return path + +/proc/regex_quote_list(list/input) as text + var/list/sanitized = list() + for(var/thingy in input) + sanitized += REGEX_QUOTE(thingy) + return jointext(sanitized, "|") diff --git a/monkestation/code/modules/admin/verbs/getlogs.dm b/monkestation/code/modules/admin/verbs/getlogs.dm new file mode 100644 index 000000000000..c60bacbaaec2 --- /dev/null +++ b/monkestation/code/modules/admin/verbs/getlogs.dm @@ -0,0 +1,53 @@ +GLOBAL_LIST(debug_logfile_names) +GLOBAL_PROTECT(debug_logfile_names) + +/client/proc/getserverlogs_debug() + set name = "Get Server Logs (Debug)" + set desc = "View/retrieve debug-related logfiles." + set category = "Debug" + if(!check_rights_for(src, R_DEBUG)) + return + get_debug_logfiles() + if(!GLOB.debug_logfile_names) + return + browseserverlogs(whitelist = GLOB.debug_logfile_names, allow_folder = FALSE) + +/client/proc/getcurrentlogs_debug() + set name = "Get Current Logs (Debug)" + set desc = "View/retrieve debug-related logfiles for the current round." + set category = "Debug" + if(!check_rights_for(src, R_DEBUG)) + return + get_debug_logfiles() + if(!GLOB.debug_logfile_names) + return + browseserverlogs(current = TRUE, whitelist = GLOB.debug_logfile_names, allow_folder = FALSE) + +/client/proc/browseserverlogs(current = FALSE, list/whitelist = null, allow_folder = TRUE) + var/path = browse_files(current ? BROWSE_ROOT_CURRENT_LOGS : BROWSE_ROOT_ALL_LOGS, whitelist = whitelist, allow_folder = allow_folder) + if(!path || !fexists(path)) + return + + if(file_spam_check()) + return + + message_admins("[key_name_admin(src)] accessed file: [path]") + switch(tgui_alert(usr, "View (in game), Open (in your system's text editor), or Download?", path, list("View", "Open", "Download"))) + if ("View") + src << browse("
[html_encode(file2text(file(path)))]
", list2params(list("window" = "viewfile.[path]"))) + if ("Open") + src << run(file(path)) + if ("Download") + src << ftp(file(path)) + else + return + to_chat(src, span_boldnotice("Attempting to send [path], this may take a fair few minutes if the file is very large."), confidential = TRUE) + return + +/proc/get_debug_logfiles() + if(!logger.initialized || GLOB.debug_logfile_names) + return + for(var/datum/log_category/category as anything in logger.log_categories) + category = logger.log_categories[category] + if(is_category_debug_visible(category)) + LAZYOR(GLOB.debug_logfile_names, get_category_logfile(category)) diff --git a/monkestation/code/modules/logging/log_category.dm b/monkestation/code/modules/logging/log_category.dm new file mode 100644 index 000000000000..cb82001d2e36 --- /dev/null +++ b/monkestation/code/modules/logging/log_category.dm @@ -0,0 +1,49 @@ +/datum/log_category + /// If non-admin debuggers (+DEBUG without +ADMIN) can see logs from this category or not. + /// When set to null, it will assume the value of the parent category (which will default to FALSE if not set). + var/debugger_visible = null + +/datum/log_category/debug + debugger_visible = TRUE + +/datum/log_category/debug_sql + debugger_visible = FALSE + +/datum/log_category/debug_href + debugger_visible = FALSE + +/datum/log_category/debug_runtime + debugger_visible = TRUE + +/// Recursively checks to see if a log category or any of its parent categories is marked as "debugger_visible" or not. +/// Caches the result. +/proc/is_category_debug_visible(datum/log_category/category) + var/static/list/cached_visibility + if(!cached_visibility) + cached_visibility = list(/datum/log_category = FALSE) + var/datum/log_category/category_path = ispath(category) ? category : (istext(category) ? logger.log_categories[category]?.type : category?.type) + if(!category) + return FALSE + if(!isnull(cached_visibility[category_path])) + . = cached_visibility[category_path] + else if(!isnull(category_path::debugger_visible)) + . = cached_visibility[category_path] = category_path::debugger_visible + else if(!isnull(category_path::master_category) && category_path::master_category != category_path) // safety check to prevent infinite recursion, prolly not needed but better safe than sorry + . = cached_visibility[category_path] = is_category_debug_visible(category_path::master_category) + else + . = cached_visibility[category_path] = FALSE + +/proc/get_category_logfile(datum/log_category/category) + var/static/list/cached_filenames + if(!cached_filenames) + cached_filenames = list() + if(!category) + return FALSE + var/datum/log_category/category_path = ispath(category) ? category : (istext(category) ? logger.log_categories[category].type : category.type) + if(!isnull(cached_filenames[category_path])) + . = cached_filenames[category_path] + else if(!isnull(category_path::master_category)) + var/datum/log_category/master_category = category_path::master_category + . = cached_filenames[master_category] = cached_filenames[category_path] = master_category::category + else + . = cached_filenames[category_path] = category_path::category diff --git a/monkestation/code/modules/logging/log_holder.dm b/monkestation/code/modules/logging/log_holder.dm new file mode 100644 index 000000000000..962274fc6813 --- /dev/null +++ b/monkestation/code/modules/logging/log_holder.dm @@ -0,0 +1,85 @@ +/datum/log_holder + /// Cached ui_data for debuggers + var/list/debug_data_cache = list() + +/datum/log_holder/ui_interact(mob/user, datum/tgui/ui) + if(!check_rights_for(user.client, R_ADMIN | R_DEBUG)) + return + + ui = SStgui.try_update_ui(user, src, ui) + if(isnull(ui)) + ui = new(user, src, "LogViewer") + ui.set_autoupdate(FALSE) + ui.open() + +/datum/log_holder/ui_status(mob/user, datum/ui_state/state) + return check_rights_for(user.client, R_ADMIN | R_DEBUG) ? UI_INTERACTIVE : UI_CLOSE + +/datum/log_holder/ui_static_data(mob/user) + var/list/data = list( + "round_id" = GLOB.round_id, + "logging_start_timestamp" = logging_start_timestamp, + ) + var/debug_user = is_user_debug_only(user) + + var/list/tree = list() + data["tree"] = tree + var/list/enabled_categories = list() + for(var/enabled in log_categories) + if(debug_user && !is_category_debug_visible(log_categories[enabled])) + continue + enabled_categories += enabled + tree["enabled"] = enabled_categories + + var/list/disabled_categories = list() + for(var/disabled in src.disabled_categories) + if(debug_user && !is_category_debug_visible(disabled)) + continue + disabled_categories += disabled + tree["disabled"] = disabled_categories + + return data + +/datum/log_holder/ui_data(mob/user) + if(!last_data_update || (world.time - last_data_update) > LOG_UPDATE_TIMEOUT) + cache_ui_data() + return is_user_debug_only(user) ? debug_data_cache : data_cache + +/datum/log_holder/cache_ui_data() + var/list/category_map = list() + var/list/debug_category_map = list() + for(var/datum/log_category/category as anything in log_categories) + category = log_categories[category] + var/list/category_data = list() + + var/list/entries = list() + for(var/datum/log_entry/entry as anything in category.entries) + entries += list(list( + "id" = entry.id, + "message" = entry.message, + "timestamp" = entry.timestamp, + "data" = entry.data, + "semver" = entry.semver_store, + )) + category_data["entries"] = entries + category_data["entry_count"] = category.entry_count + + category_map[category.category] = category_data + if(is_category_debug_visible(category)) + debug_category_map[category.category] = category_data + + data_cache.Cut() + debug_data_cache.Cut() + last_data_update = world.time + + data_cache["categories"] = category_map + data_cache["last_data_update"] = last_data_update + + debug_data_cache["categories"] = debug_category_map + debug_data_cache["last_data_update"] = last_data_update + +/// Checks to see if a user has +DEBUG without +ADMIN permissions. +/// Used to give +DEBUG holders "limited" versions of some admin commands for debugging purposes. +/proc/is_user_debug_only(mob/user) + var/client/client = user.client + return client.holder && check_rights_for(client, R_DEBUG) && !check_rights_for(client, R_ADMIN) diff --git a/tgstation.dme b/tgstation.dme index d34c24b4735d..ffa5a0548e06 100644 --- a/tgstation.dme +++ b/tgstation.dme @@ -5713,6 +5713,7 @@ #include "monkestation\code\__DEFINES\projectile.dm" #include "monkestation\code\__HELPERS\_lists.dm" #include "monkestation\code\__HELPERS\anime.dm" +#include "monkestation\code\__HELPERS\files.dm" #include "monkestation\code\__HELPERS\reagents.dm" #include "monkestation\code\__HELPERS\turfs.dm" #include "monkestation\code\_onclick\hud\alert.dm" @@ -5848,6 +5849,7 @@ #include "monkestation\code\modules\admin\smites\dagothstripsmite.dm" #include "monkestation\code\modules\admin\smites\phobia_christian_minecraft.dm" #include "monkestation\code\modules\admin\smites\where_are_your_fingers.dm" +#include "monkestation\code\modules\admin\verbs\getlogs.dm" #include "monkestation\code\modules\admin\verbs\kick_player_by_ckey.dm" #include "monkestation\code\modules\aesthetics\airlock\airlock.dm" #include "monkestation\code\modules\aesthetics\items\clothing.dm" @@ -6543,6 +6545,8 @@ #include "monkestation\code\modules\loadouts\items\under\under.dm" #include "monkestation\code\modules\loafing\code\loaf.dm" #include "monkestation\code\modules\loafing\code\loafer.dm" +#include "monkestation\code\modules\logging\log_category.dm" +#include "monkestation\code\modules\logging\log_holder.dm" #include "monkestation\code\modules\logging\categories\log_category_game.dm" #include "monkestation\code\modules\mapping\access_helpers.dm" #include "monkestation\code\modules\mapping\mapping_helpers.dm"