360 lines
10 KiB
Lua
360 lines
10 KiB
Lua
--
|
|
-- File System Logger
|
|
--
|
|
|
|
local util = require("scada-common.util")
|
|
|
|
-- constant strings for speed
|
|
local DBG_TAG, INF_TAG, WRN_TAG, ERR_TAG, FTL_TAG = "[DBG] ", "[INF] ", "[WRN] ", "[ERR] ", "[FTL] "
|
|
local COLON, FUNC, ARROW = ":", "():", " > "
|
|
|
|
local MIN_SPACE = 512
|
|
local OUT_OF_SPACE = "Out of space"
|
|
local TIME_FMT = "%F %T "
|
|
|
|
---@class logger
|
|
local log = {}
|
|
|
|
---@enum LOG_MODE
|
|
local MODE = { APPEND = 0, NEW = 1 }
|
|
|
|
log.MODE = MODE
|
|
|
|
local logger = {
|
|
not_ready = true,
|
|
path = "/log.txt",
|
|
mode = MODE.APPEND,
|
|
debug = false,
|
|
file = nil, ---@type table|nil
|
|
dmesg_out = nil, ---@type Redirect|nil
|
|
dmesg_restore_coord = { 1, 1 },
|
|
dmesg_scroll_count = 0
|
|
}
|
|
|
|
---@type function
|
|
local free_space = fs.getFreeSpace
|
|
|
|
-----------------------
|
|
-- PRIVATE FUNCTIONS --
|
|
-----------------------
|
|
|
|
-- check if the provided error indicates out of space or if insufficient space available
|
|
---@param err_msg string|nil error message
|
|
---@return boolean out_of_space
|
|
local function check_out_of_space(err_msg)
|
|
return (free_space(logger.path) < MIN_SPACE) or ((err_msg ~= nil) and (string.find(err_msg, OUT_OF_SPACE) ~= nil))
|
|
end
|
|
|
|
-- private log write function
|
|
---@param msg_bits any[]
|
|
local function _log(msg_bits)
|
|
if logger.not_ready then return end
|
|
|
|
local time_stamp = os.date(TIME_FMT)
|
|
local stamped = util.c(time_stamp, table.unpack(msg_bits))
|
|
|
|
-- attempt to write log
|
|
local status, result = pcall(function ()
|
|
logger.file.writeLine(stamped)
|
|
logger.file.flush()
|
|
end)
|
|
|
|
-- if we don't have space, we need to create a new log file
|
|
if check_out_of_space() then
|
|
-- delete the old log file before opening a new one
|
|
logger.file.close()
|
|
fs.delete(logger.path)
|
|
|
|
-- re-init logger and pass dmesg_out so that it doesn't change
|
|
log.init(logger.path, logger.mode, logger.debug, logger.dmesg_out)
|
|
|
|
-- log the message and recycle warning
|
|
logger.file.writeLine(time_stamp .. WRN_TAG .. "recycled log file")
|
|
logger.file.writeLine(stamped)
|
|
logger.file.flush()
|
|
elseif (not status) and (result ~= nil) then
|
|
util.println("unexpected error writing to the log file: " .. result)
|
|
end
|
|
end
|
|
|
|
----------------------
|
|
-- PUBLIC FUNCTIONS --
|
|
----------------------
|
|
|
|
-- initialize logger
|
|
---@param path string file path
|
|
---@param write_mode LOG_MODE file write mode
|
|
---@param include_debug boolean whether or not to include debug logs
|
|
---@param dmesg_redirect? Redirect terminal/window to direct dmesg to
|
|
function log.init(path, write_mode, include_debug, dmesg_redirect)
|
|
local err_msg = nil
|
|
|
|
logger.path = path
|
|
logger.mode = write_mode
|
|
logger.debug = include_debug
|
|
logger.file, err_msg = fs.open(path, util.trinary(logger.mode == MODE.APPEND, "a", "w"))
|
|
|
|
if dmesg_redirect then
|
|
logger.dmesg_out = dmesg_redirect
|
|
else
|
|
logger.dmesg_out = term.current()
|
|
end
|
|
|
|
-- check for space issues
|
|
local out_of_space = check_out_of_space(err_msg)
|
|
|
|
-- try to handle problems
|
|
if logger.file == nil or out_of_space then
|
|
if out_of_space then
|
|
if fs.exists(logger.path) then
|
|
fs.delete(logger.path)
|
|
|
|
logger.file, err_msg = fs.open(path, util.trinary(logger.mode == MODE.APPEND, "a", "w"))
|
|
|
|
if logger.file then
|
|
logger.file.writeLine(os.date(TIME_FMT) .. WRN_TAG .. "init recycled log file")
|
|
logger.file.flush()
|
|
else error("failed to setup the log file: " .. err_msg) end
|
|
else error("failed to make space for the log file, please delete unused files") end
|
|
else error("unexpected error setting up the log file: " .. err_msg) end
|
|
end
|
|
|
|
logger.not_ready = false
|
|
end
|
|
|
|
-- close the log file handle
|
|
function log.close() logger.file.close() end
|
|
|
|
-- direct dmesg output to a monitor/window
|
|
---@param window Window window or terminal reference
|
|
function log.direct_dmesg(window) logger.dmesg_out = window end
|
|
|
|
-- dmesg style logging for boot because I like linux-y things
|
|
---@param msg any message
|
|
---@param tag? string log tag
|
|
---@param tag_color? integer log tag color
|
|
---@return dmesg_ts_coord coordinates line area to place working indicator
|
|
function log.dmesg(msg, tag, tag_color)
|
|
---@class dmesg_ts_coord
|
|
local ts_coord = { x1 = 2, x2 = 3, y = 1 }
|
|
|
|
msg = util.strval(msg)
|
|
tag = util.strval(tag or "")
|
|
|
|
local t_stamp = string.format("%12.2f", os.clock())
|
|
local out = logger.dmesg_out
|
|
|
|
if out ~= nil then
|
|
local out_w, out_h = out.getSize()
|
|
|
|
local lines = { msg }
|
|
|
|
-- wrap if needed
|
|
if string.len(msg) > out_w then
|
|
local remaining = true
|
|
local s_start = 1
|
|
local s_end = out_w
|
|
local i = 1
|
|
|
|
lines = {}
|
|
|
|
while remaining do
|
|
local line = string.sub(msg, s_start, s_end)
|
|
|
|
if line == "" then
|
|
remaining = false
|
|
else
|
|
lines[i] = line
|
|
|
|
s_start = s_end + 1
|
|
s_end = s_end + out_w
|
|
i = i + 1
|
|
end
|
|
end
|
|
end
|
|
|
|
-- start output with tag and time, assuming we have enough width for this to be on one line
|
|
local cur_x, cur_y = out.getCursorPos()
|
|
|
|
if cur_x > 1 then
|
|
if cur_y == out_h then
|
|
out.scroll(1)
|
|
out.setCursorPos(1, cur_y)
|
|
logger.dmesg_scroll_count = logger.dmesg_scroll_count + 1
|
|
else
|
|
out.setCursorPos(1, cur_y + 1)
|
|
end
|
|
end
|
|
|
|
-- colored time
|
|
local initial_color = out.getTextColor()
|
|
out.setTextColor(colors.white)
|
|
out.write("[")
|
|
out.setTextColor(colors.lightGray)
|
|
out.write(t_stamp)
|
|
ts_coord.x2, ts_coord.y = out.getCursorPos()
|
|
ts_coord.x2 = ts_coord.x2 - 1
|
|
out.setTextColor(colors.white)
|
|
out.write("] ")
|
|
|
|
-- print optionally colored tag
|
|
if tag ~= "" then
|
|
out.write("[")
|
|
if tag_color then out.setTextColor(tag_color) end
|
|
out.write(tag)
|
|
out.setTextColor(colors.white)
|
|
out.write("] ")
|
|
end
|
|
|
|
out.setTextColor(initial_color)
|
|
|
|
-- output message
|
|
for i = 1, #lines do
|
|
cur_x, cur_y = out.getCursorPos()
|
|
|
|
if i > 1 and cur_x > 1 then
|
|
if cur_y == out_h then
|
|
out.scroll(1)
|
|
out.setCursorPos(1, cur_y)
|
|
logger.dmesg_scroll_count = logger.dmesg_scroll_count + 1
|
|
else
|
|
out.setCursorPos(1, cur_y + 1)
|
|
end
|
|
end
|
|
|
|
out.write(lines[i])
|
|
end
|
|
|
|
logger.dmesg_restore_coord = { out.getCursorPos() }
|
|
|
|
_log{"[", t_stamp, "] [", tag, "] ", msg}
|
|
end
|
|
|
|
return ts_coord
|
|
end
|
|
|
|
-- print a dmesg message, but then show remaining seconds instead of timestamp
|
|
---@nodiscard
|
|
---@param msg string message
|
|
---@param tag? string log tag
|
|
---@param tag_color? integer log tag color
|
|
---@return function update, function done
|
|
function log.dmesg_working(msg, tag, tag_color)
|
|
local ts_coord = log.dmesg(msg, tag, tag_color)
|
|
local initial_scroll = logger.dmesg_scroll_count
|
|
|
|
local out = logger.dmesg_out
|
|
local width = (ts_coord.x2 - ts_coord.x1) + 1
|
|
|
|
if out ~= nil then
|
|
local initial_color = out.getTextColor()
|
|
|
|
local counter = 0
|
|
|
|
local function update(sec_remaining)
|
|
local new_y = ts_coord.y - (logger.dmesg_scroll_count - initial_scroll)
|
|
if new_y < 1 then return end
|
|
|
|
local time = util.sprintf("%ds", sec_remaining)
|
|
local available = width - (string.len(time) + 2)
|
|
local progress = ""
|
|
|
|
out.setCursorPos(ts_coord.x1, new_y)
|
|
out.write(" ")
|
|
|
|
if counter % 4 == 0 then
|
|
progress = "|"
|
|
elseif counter % 4 == 1 then
|
|
progress = "/"
|
|
elseif counter % 4 == 2 then
|
|
progress = "-"
|
|
elseif counter % 4 == 3 then
|
|
progress = "\\"
|
|
end
|
|
|
|
out.setTextColor(colors.blue)
|
|
out.write(progress)
|
|
out.setTextColor(colors.lightGray)
|
|
out.write(util.spaces(available) .. time)
|
|
out.setTextColor(initial_color)
|
|
|
|
counter = counter + 1
|
|
|
|
out.setCursorPos(table.unpack(logger.dmesg_restore_coord))
|
|
end
|
|
|
|
local function done(ok)
|
|
local new_y = ts_coord.y - (logger.dmesg_scroll_count - initial_scroll)
|
|
if new_y < 1 then return end
|
|
|
|
out.setCursorPos(ts_coord.x1, new_y)
|
|
|
|
if ok or ok == nil then
|
|
out.setTextColor(colors.green)
|
|
out.write(util.pad("DONE", width))
|
|
else
|
|
out.setTextColor(colors.red)
|
|
out.write(util.pad("FAIL", width))
|
|
end
|
|
|
|
out.setTextColor(initial_color)
|
|
|
|
out.setCursorPos(table.unpack(logger.dmesg_restore_coord))
|
|
end
|
|
|
|
return update, done
|
|
else
|
|
return function () end, function () end
|
|
end
|
|
end
|
|
|
|
-- log debug messages
|
|
---@param msg any message
|
|
---@param trace? boolean include file trace
|
|
function log.debug(msg, trace)
|
|
if logger.debug then
|
|
if trace then
|
|
local info = debug.getinfo(2)
|
|
|
|
if info.name ~= nil then
|
|
_log{DBG_TAG, info.short_src, COLON, info.name, FUNC, info.currentline, ARROW, msg}
|
|
else
|
|
_log{DBG_TAG, info.short_src, COLON, info.currentline, ARROW, msg}
|
|
end
|
|
else
|
|
_log{DBG_TAG, msg}
|
|
end
|
|
end
|
|
end
|
|
|
|
-- log info messages
|
|
---@param msg any message
|
|
function log.info(msg) _log{INF_TAG, msg} end
|
|
|
|
-- log warning messages
|
|
---@param msg any message
|
|
function log.warning(msg) _log{WRN_TAG, msg} end
|
|
|
|
-- log error messages
|
|
---@param msg any message
|
|
---@param trace? boolean include file trace
|
|
function log.error(msg, trace)
|
|
if trace then
|
|
local info = debug.getinfo(2)
|
|
|
|
if info.name ~= nil then
|
|
_log{ERR_TAG, info.short_src, COLON, info.name, FUNC, info.currentline, ARROW, msg}
|
|
else
|
|
_log{ERR_TAG, info.short_src, COLON, info.currentline, ARROW, msg}
|
|
end
|
|
else
|
|
_log{ERR_TAG, msg}
|
|
end
|
|
end
|
|
|
|
-- log fatal errors
|
|
---@param msg any message
|
|
function log.fatal(msg) _log{FTL_TAG, msg} end
|
|
|
|
return log
|