#!/opt/lnxall_app/bin/lua local invoker = require "invoker" local gfmt = string.format local ltuidir = "/usr/lib/lua/5.x" if invoker.statfile(ltuidir) ~= "directory" then io.stderr:write(gfmt("Error, LTUI module directory not found: %s\n", ltuidir)) io.stderr:flush() os.exit(1) end package.path = gfmt("%s;%s/?.lua", package.path, ltuidir) package.cpath = gfmt("%s;%s/?.so", package.cpath, ltuidir) ltuidir = nil local ltui = require("ltui") local MODULE_DIR = "/app/sysdiag_modules" local HEADER_TEXT = "方向键移动, Enter 执行或进入菜单, Esc 返回, ? 查看说明, / 搜索, Ctrl-C 退出" local function sorted_keys(tab) local keys = {} for key, _ in pairs(tab) do keys[#keys + 1] = key end table.sort(keys) return keys end local function display_width(text) local width, idx, len = 0, 1, #text while idx <= len do local byte = string.byte(text, idx) if byte >= 240 then idx = idx + 4 width = width + 2 elseif byte >= 224 then idx = idx + 3 width = width + 2 elseif byte >= 192 then idx = idx + 2 width = width + 1 else idx = idx + 1 width = width + 1 end end return width end local function center_text(text, width) local pad = math.floor(math.max(0, width - display_width(text)) / 2) return string.rep(" ", pad) .. text end local function update_header(dialog) dialog:text():text_set(center_text(HEADER_TEXT, dialog:width())) end local function configs_width(configs) local width = 0 for _, config in ipairs(configs or {}) do local text = tostring(config) width = math.max(width, display_width(text), #text) end return width end local function layout_menuconf(dialog) local panel = dialog:box():panel() local menuconf = dialog:menuconf() local panel_width = panel:width() local panel_height = panel:height() local content_width = configs_width(menuconf._CONFIGS) if content_width <= 0 then content_width = panel_width end content_width = math.min(panel_width, content_width) local x = math.floor(math.max(0, panel_width - content_width) / 2) menuconf:bounds_set(ltui.rect:new(x, 0, content_width, panel_height)) for _, item in ipairs(menuconf:_items() or {}) do local bounds = item:bounds() item:bounds_set(ltui.rect:new(bounds.sx, bounds.sy, content_width, 1)) end end local function menuconf_select_index(menuconf, index) local items = menuconf:_items() or {} local total = #items if total == 0 then return end index = math.max(1, math.min(index or 1, total)) local height = math.max(1, menuconf:height()) local startindex = menuconf._STARTINDEX or 1 if index < startindex or index >= startindex + height then startindex = index local maxstart = math.max(1, total - height + 1) if startindex > maxstart then startindex = maxstart end menuconf._STARTINDEX = startindex menuconf:clear() for idx = startindex, startindex + height - 1 do local item = items[idx] if item then item:bounds_set(ltui.rect:new(0, idx - startindex, menuconf:width(), 1)) menuconf:insert(item) else break end end end local item = items[index] if item and item:parent() == menuconf then menuconf:select(item) menuconf:invalidate() end end local function restore_menuconf_selection(menuconf) local configs = menuconf._CONFIGS if not configs then return end local index = configs._SYSDIAG_SELECTED_INDEX if index then menuconf_select_index(menuconf, index) end end local function shell_quote(text) text = tostring(text or "") return "'" .. text:gsub("'", "'\\''") .. "'" end local function list_module_files(mdir) local files = {} local cmd = "find " .. shell_quote(mdir) .. " -maxdepth 1 -type f -name '*.lua' 2>/dev/null | sort" local fp = io.popen(cmd, "r") if not fp then return files end for line in fp:lines() do files[#files + 1] = line end fp:close() return files end local function basename(path) return string.match(path, "([^/]+)%.lua$") or path end local function safe_call(fn, ...) if type(fn) ~= "function" then return false, "not a function" end return pcall(fn, ...) end local function table_to_text(value, indent, visited) indent = indent or "" visited = visited or {} if type(value) ~= "table" then return tostring(value) end if visited[value] then return "" end visited[value] = true local lines = {} local numeric = true local maxn = 0 for key, _ in pairs(value) do if type(key) ~= "number" then numeric = false end if type(key) == "number" and key > maxn then maxn = key end end if numeric then for i = 1, maxn do if value[i] ~= nil then local item = value[i] if type(item) == "table" then lines[#lines + 1] = indent .. tostring(i) .. ":" lines[#lines + 1] = table_to_text(item, indent .. " ", visited) else lines[#lines + 1] = indent .. tostring(i) .. ": " .. tostring(item) end end end else for _, key in ipairs(sorted_keys(value)) do local item = value[key] if type(item) == "table" then lines[#lines + 1] = indent .. tostring(key) .. ":" lines[#lines + 1] = table_to_text(item, indent .. " ", visited) else lines[#lines + 1] = indent .. tostring(key) .. ": " .. tostring(item) end end end visited[value] = nil return #lines > 0 and table.concat(lines, "\n") or indent .. "" end local function check_module_api(id, mod) local apis = { "checkplat", "category", "description", "detailed", "suggestion", "step" } for _, api in ipairs(apis) do if type(mod[api]) ~= "function" then return false, id .. ": missing api " .. api end end return true end local function load_modules() local modules, errors = {}, {} for _, path in ipairs(list_module_files(MODULE_DIR)) do local id = basename(path) local loader, lerr = loadfile(path) if not loader then errors[#errors + 1] = id .. ": " .. tostring(lerr) else local ok, mod = pcall(loader) if not ok or type(mod) ~= "table" then errors[#errors + 1] = id .. ": failed to load module" else local valid, verr = check_module_api(id, mod) if not valid then errors[#errors + 1] = verr else local pok, matched = safe_call(mod.checkplat) if not pok then errors[#errors + 1] = id .. ": checkplat failed: " .. tostring(matched) elseif matched then modules[#modules + 1] = { id = id, path = path, handle = mod } end end end end end table.sort(modules, function (a, b) return a.id < b.id end) return modules, errors end local function get_text(mod, api, fallback) local ok, text = safe_call(mod[api]) if ok and type(text) == "string" and #text > 0 then return text end return fallback end local function run_module(entry) local mod = entry.handle local ok, result, haserr = safe_call(mod.step) if not ok then return "模块执行失败\n\n" .. tostring(result) end local lines = {} lines[#lines + 1] = "模块: " .. entry.id lines[#lines + 1] = "类别: " .. get_text(mod, "category", "Unknown") lines[#lines + 1] = "结果: " .. (haserr and "发现异常" or "未发现异常") lines[#lines + 1] = "" lines[#lines + 1] = "诊断数据:" lines[#lines + 1] = table_to_text(result or {}) if haserr then lines[#lines + 1] = "" lines[#lines + 1] = "处理建议:" lines[#lines + 1] = get_text(mod, "suggestion", "无处理建议") end return table.concat(lines, "\n") end local function build_help(entry) local mod = entry.handle return table.concat({ get_text(mod, "detailed", "无详细说明"), "", "处理建议:", get_text(mod, "suggestion", "无处理建议"), "", "模块文件:", entry.path, }, "\n") end local function build_configs(modules, errors) local groups = {} for _, entry in ipairs(modules) do local category = get_text(entry.handle, "category", "Unknown") local description = get_text(entry.handle, "description", entry.id) entry.description = description entry.category = category entry.help = build_help(entry) groups[category] = groups[category] or {} table.insert(groups[category], entry) end local function item_prompt(text) return function () return text end end local function menu_prompt(text) return function () return text end end local configs = {} for _, category in ipairs(sorted_keys(groups)) do local items = {} for _, entry in ipairs(groups[category]) do items[#items + 1] = ltui.menuconf.config { description = { entry.description, "", entry.help }, prompt = item_prompt(entry.description), module = entry, new = false } end configs[#configs + 1] = ltui.menuconf.menu { description = category, prompt = menu_prompt(category), configs = items, new = false } end if #errors > 0 then local items = {} for i, err in ipairs(errors) do items[#items + 1] = ltui.menuconf.config { description = "错误 " .. tostring(i), prompt = item_prompt("错误 " .. tostring(i)), result_text = err, new = false } end configs[#configs + 1] = ltui.menuconf.menu { description = "加载错误", prompt = menu_prompt("加载错误"), configs = items, new = false } end if #configs == 0 then configs[#configs + 1] = ltui.menuconf.config { description = "未发现可用诊断模块", prompt = item_prompt("未发现可用诊断模块"), result_text = "未在 " .. MODULE_DIR .. " 下发现可用诊断模块", new = false } end return configs end local app = ltui.application() function app:init(name, argv) ltui.application.init(self, name, argv) local modules, errors = load_modules() local dialog = ltui.mconfdialog:new("sysdiag.dialog", ltui.rect {0, 0, self:width(), self:height()}, "系统诊断") update_header(dialog) dialog:button("exit"):action_set(ltui.action.ac_on_enter, function () self:quit() return true end) dialog:box():panel():action_add(ltui.action.ac_on_resized, function () layout_menuconf(dialog) end) dialog:menuconf():action_add(ltui.action.ac_on_load, function (v) layout_menuconf(dialog) restore_menuconf_selection(v) end) dialog:menuconf():action_set(ltui.action.ac_on_selected, function (v, config) if config.module then dialog:show_result(run_module(config.module)) return true elseif config.result_text then dialog:show_result(config.result_text) return true elseif config.kind == "menu" and config.configs and #config.configs > 0 then local current = v:current() if current and v._CONFIGS then v._CONFIGS._SYSDIAG_SELECTED_INDEX = current:extra("index") end end end) dialog:load(build_configs(modules, errors)) self:insert(dialog) self._SYSDIAG_DIALOG = dialog self._SYSDIAG_MODULES = modules self._SYSDIAG_ERRORS = errors end function app:on_resize() if self._SYSDIAG_DIALOG then self._SYSDIAG_DIALOG:bounds_set(ltui.rect {0, 0, self:width(), self:height()}) ltui.application.on_resize(self) update_header(self._SYSDIAG_DIALOG) layout_menuconf(self._SYSDIAG_DIALOG) return end ltui.application.on_resize(self) end app:run("sysdiag-tui", arg)