#!/usr/bin/env lua -- tz.lua - a system status prober written in plain Lua 5.1 for CGI. -- -- This is a rewrite of tz.php with no PHP and no third-party module: every -- value is read from /proc, /sys or a single shell command, so the script runs -- anywhere a lua5.1 (or luajit) interpreter is reachable from a cgi-bin, e.g. -- -- nginx + fcgiwrap, apache mod_cgid, busybox httpd, lighttpd mod_cgi, ... -- -- Routes, taken from the query string (or from the first command line argument -- when the script is executed by hand): -- -- (nothing) HTML prober page -- method=sysinfo JSON document, polled once a second by the page -- method=luainfo text/plain runtime report -- method=raw the script itself, as a download -- -- (method=phpinfo is still accepted as an alias of method=luainfo so that the -- old bookmarks of the PHP version keep working.) -- -- CGI response headers are written only when a CGI environment is detected, so -- the file can also be used straight from a shell: -- -- lua5.1 tz.lua | less -- lua5.1 tz.lua method=sysinfo | python3 -m json.tool -- -- Rendering follows the node-exporter.lua tricks: -- * stdout is fully buffered (one write(2) per response) and the page is -- streamed as write() calls whose arguments are constant string literals, -- so rendering allocates nothing but the escaped values; the JSON routes -- know their body up front and therefore announce Content-Length. -- * /proc and /sys are parsed with a single string.match per line and the -- captures land in locals; no token tables are built for the hot paths -- (stat, cpuinfo, meminfo, sockstat, netdev). -- * escaping helpers test with string.find first and only call gsub when a -- value really contains a special character. -- * filesystem space uses a statvfs binding (nixio or luaposix) when one is -- installed and falls back to one LC_ALL=C df run otherwise; the kernel -- version, architecture and/or uptime are each read at most once. local type, tonumber, tostring, rawget, pcall = type, tonumber, tostring, rawget, pcall local format, match, find, gmatch, gsub, sub = string.format, string.match, string.find, string.gmatch, string.gsub, string.sub local concat = table.concat local floor = math.floor local getenv = os.getenv local date, time, clock, exit, setlocale = os.date, os.time, os.clock, os.exit, os.setlocale local write = io.write local stdout = io.stdout -- Fully buffered stdout: the page is ~20 KB, so the kernel only sees one write. stdout:setvbuf("full", 65536) local function flush() stdout:flush() end local function finish() flush() exit(0) end -- Number formatting must not depend on the locale (%.2f has to use a dot). local locale_ctype = "C" pcall(function() locale_ctype = setlocale(nil, "ctype") or "C" end) pcall(setlocale, "C") local lua_jit = rawget(_G, "jit") local lua_version = match(_VERSION or "Lua", "%d+%.%d+") or _VERSION or "5.1" local lua_vm = lua_jit and (lua_jit.version or "LuaJIT") or "Lua " .. lua_version --------------------------------------------------------------------------- -- Environment --------------------------------------------------------------------------- local gateway = getenv("GATEWAY_INTERFACE") or "" local request_method = getenv("REQUEST_METHOD") or "GET" local is_cgi = find(gateway, "^CGI/") and true or false local function urldecode(value) if not find(value, "%%") and not find(value, "+", 1, true) then return value end value = gsub(value, "+", " ") value = gsub(value, "%%(%x%x)", function(hex) return string.char(tonumber(hex, 16)) end) return value end local function parse_query(text) local query = {} if not text or text == "" then return query end for pair in gmatch(text, "[^&;]+") do local key, value = match(pair, "^([^=]*)=(.*)$") if key then query[urldecode(key)] = urldecode(value) else query[urldecode(pair)] = "" end end return query end -- The route is either the "method" GET parameter or, for manual runs, the -- first argument: "lua5.1 tz.lua method=sysinfo". local method = parse_query(getenv("QUERY_STRING")).method if not method and arg and arg[1] then local key, value = match(arg[1], "^([^=]*)=(.*)$") method = (key == "method" and value) or arg[1] end local zh = sub(getenv("HTTP_ACCEPT_LANGUAGE") or "", 1, 2) == "zh" local messages = { ['Download'] = '文件下载', ['Gateway'] = '网关管理', ['Log Analysis'] = '日志分析', ['Monitor'] = '性能监控', ['Server Information'] = '服务器参数', ['IP Address'] = 'IP 地址', ['Client IP'] = '客户端 IP', ['Uname'] = '内核标识', ['OS'] = '操作系统', ['Server Software'] = '服务器软件', ['Language'] = '语言', ['Port'] = '端口', ['User'] = '用户', ['Hostname'] = '主机名称', ['Server Port'] = '端口', ['Prober Path'] = '探针路径', ['Server Realtime Data'] = '服务器实时数据', ['Time'] = '当前时间', ['Uptime'] = '已运行时间', ['CPU Model'] = 'CPU 型号', ['L2 Cache'] = '二级缓存', ['Frequency'] = '频率', ['CPU Instruction Set'] = 'CPU 指令集', ['BIOS Version'] = 'BIOS 版本', ['Board Vendor'] = '主板厂商', ['HardDisk Model'] = '硬盘型号', ['CPU Usage'] = 'CPU 使用状况', ['CPU Temperature'] = 'CPU 温度', ['GPU Temperature'] = 'GPU 温度', ['Memory Usage'] = '内存使用状况', ['Physical Memory'] = '物理内存', ['Used'] = '已用', ['Cached'] = '已缓存', ['Free'] = '空闲', ['Percent'] = '使用率', ['Total Space'] = '总空间', ['Disk Usage'] = '硬盘使用状况', ['Loadavg'] = '系统平均负载', ['Sockets'] = '网络连接数', ['Network Usage'] = '网络使用状况', ['Tx'] = '出网', ['Rx'] = '入网', ['Realtime'] = '实时', ['Type'] = '类型', ['Device'] = '设备', ['Lua Information'] = 'Lua 信息', ['Version'] = '版本', ['Virtual Machine'] = '虚拟机', ['Interpreter'] = '解释器', ['Environment'] = '运行环境', ['Modules'] = '模块', ['Package Path'] = '包路径', ['Package CPath'] = 'C 包路径', ['Prober'] = '探针', ['Turbo Version'] = '极速版', ['Back to top'] = '返回顶部', } local function __(message) if zh and messages[message] then return messages[message] end return message end -- HTML escaping, node-exporter style: only touch strings that need it. local function h(value) if value == nil then return "" end if type(value) ~= "string" then value = tostring(value) end if not find(value, '[&<>"]') then return value end value = gsub(value, "&", "&") value = gsub(value, "<", "<") value = gsub(value, ">", ">") value = gsub(value, '"', """) return value end local function send_headers(content_type, extra) if is_cgi then write("Content-Type: ", content_type, "\r\nCache-Control: no-cache, must-revalidate\r\n") if extra then for i = 1, #extra do write(extra[i], "\r\n") end end write("\r\n") end -- A HEAD response carries the headers only. if request_method == "HEAD" then finish() end end --------------------------------------------------------------------------- -- Small helpers --------------------------------------------------------------------------- local function read_line(path) local file = io.open(path, "r") if not file then return nil end local line = file:read("*l") file:close() return line end local function read_all(path) local file = io.open(path, "r") if not file then return nil end local content = file:read("*a") file:close() return content end local function each_line(path, callback) local file = io.open(path, "r") if not file then return false end for line in file:lines() do callback(line) end file:close() return true end local function file_exists(path) local file = io.open(path, "r") if not file then return false end file:close() return true end local function run(command) local pipe = io.popen(command, "r") if not pipe then return nil end local output = pipe:read("*a") pipe:close() return output end local function trim(value) if not value then return nil end return (gsub(value, "^%s*(.-)%s*$", "%1")) end -- Numeric value of "488.28 M", i.e. what PHP's ($string > 0) would compare. local function leading_number(value) return tonumber(match(tostring(value or ""), "^%-?%d+%.?%d*")) or 0 end -- PHP's round($value, $digits). local function round(value, digits) local multiplier = 10 ^ (digits or 0) return floor(value * multiplier + 0.5) / multiplier end -- Render a rounded float the way PHP's strval() would ("10", "10.5"). local function trimnum(value, digits) local text = format("%." .. (digits or 2) .. "f", value) if find(text, ".", 1, true) then text = gsub(text, "0+$", "") text = gsub(text, "%.$", "") end return text end -- /proc/uptime is wanted by both the uptime row and the netdev timestamps; -- read it once per request. local uptime_cached local function uptime_seconds() if uptime_cached == nil then local line = read_line("/proc/uptime") uptime_cached = (line and tonumber(match(line, "^%s*(%S+)"))) or false end return uptime_cached or nil end local function script_path() local path = getenv("SCRIPT_FILENAME") if path and path ~= "" and file_exists(path) then return path end local root, name = getenv("DOCUMENT_ROOT"), getenv("SCRIPT_NAME") if root and name then path = root .. "/" .. gsub(name, "^/", "") if file_exists(path) then return path end end return (arg and arg[0]) or "tz.lua" end local function json_encode(value) local kind = type(value) if value == nil then return "null" end if kind == "boolean" then return value and "true" or "false" end if kind == "number" then if value ~= value or value == math.huge or value == -math.huge then return "null" end if value == floor(value) and value >= -2 ^ 53 and value <= 2 ^ 53 then -- "%.0f" instead of "%d": some Lua 5.1 builds (OpenWrt's, which -- adds an int32 integer type) refuse "%d"/"%x" for a double that -- does not fit their integer type, e.g. a 13 GB netdev counter. return format("%.0f", value) end return format("%.14g", value) end if kind == "string" then if find(value, '[\\"\n\r\t\b\f]') or find(value, "%c") then value = gsub(value, "[\\\"\b\f\n\r\t%z\1-\31]", function(char) local escapes = { ['"'] = '\\"', ["\\"] = "\\\\", ["\b"] = "\\b", ["\f"] = "\\f", ["\n"] = "\\n", ["\r"] = "\\r", ["\t"] = "\\t" } return escapes[char] or format("\\u%04x", string.byte(char)) end) end return '"' .. value .. '"' end if kind ~= "table" then return "null" end local count, is_list = 0, true for key in pairs(value) do count = count + 1 if type(key) ~= "number" or key ~= floor(key) or key < 1 then is_list = false end end if count == 0 then return "[]" end local parts = {} if is_list then for i = 1, count do parts[i] = json_encode(value[i]) end return "[" .. concat(parts, ",") .. "]" end for key, item in pairs(value) do parts[#parts + 1] = json_encode(tostring(key)) .. ":" .. json_encode(item) end return "{" .. concat(parts, ",") .. "}" end -- "1 M 181 K 791 B ", same shape as the PHP human_filesize() helper. local human_units = { "B", "K", "M", "G", "T" } local function human_filesize(bytes) bytes = tonumber(bytes) or 0 if bytes == 0 then return "0 B" end local size, unit = "", 1 while bytes > 0 and unit <= #human_units do size = format("%d %s %s", bytes % 1024, human_units[unit], size) bytes = floor(bytes / 1024) unit = unit + 1 end return size end --------------------------------------------------------------------------- -- Collectors --------------------------------------------------------------------------- local function get_remote_addr() local address = getenv("HTTP_X_REAL_IP") if address and address ~= "" then return address end local forwarded = getenv("HTTP_X_FORWARDED_FOR") if forwarded and forwarded ~= "" then address = match(forwarded, "^.*,%s*(.*)$") if address then return address end return forwarded end return getenv("REMOTE_ADDR") or "" end local function ip_part(address) address = trim(address) or "" local bracketed = match(address, "^%[([^%]]+)%]") if bracketed then return bracketed end local ipv4 = match(address, "^(%d+%.%d+%.%d+%.%d+)") if ipv4 then return ipv4 end local ipv6 = match(address, "^([%x:%.]+)") if ipv6 and find(ipv6, ":", 1, true) then return ipv6 end return "" end local function get_server_addr() local address = getenv("SERVER_ADDR") if address and address ~= "" and address ~= "127.0.0.1" then return address end -- PHP used gethostbyname(php_uname('n')); lua5.1 has no resolver, but the -- hostname tool is in every base image that ships a shell. local output = run("LC_ALL=C hostname -I 2>/dev/null") if output then local first = match(output, "^%s*(%S+)") if first then return first end end return address and address ~= "" and address or "127.0.0.1" end -- /proc/stat, "cpu" line without its first field. local function get_stat() local line = read_line("/proc/stat") if not line then return {} end local stat, index = {}, 0 for value in gmatch(line, "%S+") do if index > 0 then stat[index] = tonumber(value) or 0 end index = index + 1 end return stat end local function get_sockstat() local info = {} each_line("/proc/net/sockstat", function(line) local key, values = match(line, "^([^:]+):%s*(.*)$") if not key then return end local entry = {} -- "TCP: inuse 13 orphan 0 tw 60 alloc 347 mem 3" -> name/count pairs. for name, value in gmatch(values, "(%S+)%s+(%S+)") do entry[name] = tonumber(value) or value end info[trim(key)] = entry end) return info end -- https://github.com/util-linux/util-linux/blob/master/sys-utils/lscpu-arm.c local arm_part = { ['0x810'] = "ARM810", ['0x920'] = "ARM920", ['0x922'] = "ARM922", ['0x926'] = "ARM926", ['0x940'] = "ARM940", ['0x946'] = "ARM946", ['0x966'] = "ARM966", ['0xa20'] = "ARM1020", ['0xa22'] = "ARM1022", ['0xa26'] = "ARM1026", ['0xb02'] = "ARM11 MPCore", ['0xb36'] = "ARM1136", ['0xb56'] = "ARM1156", ['0xb76'] = "ARM1176", ['0xc05'] = "Cortex-A5", ['0xc07'] = "Cortex-A7", ['0xc08'] = "Cortex-A8", ['0xc09'] = "Cortex-A9", ['0xc0d'] = "Cortex-A17", ['0xc0f'] = "Cortex-A15", ['0xc0e'] = "Cortex-A17", ['0xc14'] = "Cortex-R4", ['0xc15'] = "Cortex-R5", ['0xc17'] = "Cortex-R7", ['0xc18'] = "Cortex-R8", ['0xc20'] = "Cortex-M0", ['0xc21'] = "Cortex-M1", ['0xc23'] = "Cortex-M3", ['0xc24'] = "Cortex-M4", ['0xc27'] = "Cortex-M7", ['0xc60'] = "Cortex-M0+", ['0xd01'] = "Cortex-A32", ['0xd02'] = "Cortex-A34", ['0xd03'] = "Cortex-A53", ['0xd04'] = "Cortex-A35", ['0xd05'] = "Cortex-A55", ['0xd06'] = "Cortex-A65", ['0xd07'] = "Cortex-A57", ['0xd08'] = "Cortex-A72", ['0xd09'] = "Cortex-A73", ['0xd0a'] = "Cortex-A75", ['0xd0b'] = "Cortex-A76", ['0xd0c'] = "Neoverse-N1", ['0xd0d'] = "Cortex-A77", ['0xd0e'] = "Cortex-A76AE", ['0xd13'] = "Cortex-R52", ['0xd15'] = "Cortex-R82", ['0xd20'] = "Cortex-M23", ['0xd21'] = "Cortex-M33", ['0xd40'] = "Neoverse-V1", ['0xd41'] = "Cortex-A78", ['0xd42'] = "Cortex-A78AE", ['0xd43'] = "Cortex-A65AE", ['0xd44'] = "Cortex-X1", ['0xd46'] = "Cortex-A510", ['0xd47'] = "Cortex-A710", ['0xd48'] = "Cortex-X2", ['0xd49'] = "Neoverse-N2", ['0xd4a'] = "Neoverse-E1", ['0xd4b'] = "Cortex-A78C", ['0xd4c'] = "Cortex-X1C", ['0xd4d'] = "Cortex-A715", ['0xd4e'] = "Cortex-X3", ['0xd4f'] = "Neoverse-V2", } local function get_cpuinfo() local info = {} local processors, model, hardware, implementer, part = 0, nil, nil, nil, nil local frequency, cache_size, bogomips, flags if not each_line("/proc/cpuinfo", function(line) -- One match per line; the captures are already trimmed, so no table and -- no trim() call are needed. local key, value = match(line, "^(.-)%s*:%s*(.-)%s*$") if key == "processor" then processors = processors + 1 elseif key == "model name" and not model then model = value elseif key == "Hardware" and not hardware then hardware = value elseif key == "CPU implementer" and not implementer then implementer = value elseif key == "CPU part" and not part then part = value elseif key == "cpu MHz" and not frequency then frequency = match(value, "^([%d%.]+)") elseif key == "cache size" and not cache_size then cache_size = value elseif key == "bogomips" or key == "BogoMIPS" then bogomips = bogomips or match(value, "^([%d%.]+)") elseif key == "flags" or key == "Features" then flags = flags or value end end) then return info end if not frequency then local max_freq = read_line("/sys/devices/system/cpu/cpu0/cpufreq/cpuinfo_max_freq") if max_freq then frequency = format("%.3f", (tonumber(max_freq) or 0) / 1000) end end info.num = processors if implementer == "0x41" and part and arm_part[part] then info.model = "ARM " .. arm_part[part] elseif model then info.model = model elseif hardware then info.model = hardware end info.frequency = frequency info.bogomips = bogomips info.l2cache = cache_size info.flags = flags return info end local function get_uptime() local seconds = uptime_seconds() if not seconds then return "" end local minutes = seconds / 60 local hours = minutes / 60 local days = floor(hours / 24) hours = floor(hours - days * 24) minutes = floor(minutes - days * 60 * 24 - hours * 60) local day_unit = zh and "天" or (" day" .. (days > 1 and "s " or " ")) local hour_unit = zh and "小时" or (" hour" .. (hours > 1 and "s " or " ")) local minute_unit = zh and "分钟" or (" minute" .. (minutes > 1 and "s " or " ")) local uptime = "" if days ~= 0 then uptime = uptime .. days .. day_unit end if hours ~= 0 then uptime = uptime .. hours .. hour_unit end return uptime .. minutes .. minute_unit end local function get_tempinfo() local info, misses = {}, 0 for zone = 0, 9 do local value = read_line("/sys/class/thermal/thermal_zone" .. zone .. "/temp") local temperature = value and tonumber(value) if temperature then misses = 0 info.cpu = temperature / 1000 -- The first zone that is actually warm is the CPU one. if info.cpu > 30 then break end else -- node-exporter style: tolerate a hole in the zone numbering. misses = misses + 1 if misses >= 2 then break end end end -- if (value = read_line('/sys/class/thermal/thermal_zone10/temp')) then -- info.gpu = tonumber(value) / 1000 -- end return info end local function get_meminfo() local total, free, buffers, cached, swaptotal, swapfree if not each_line("/proc/meminfo", function(line) local key, value = match(line, "^(%a+):%s*(%d+)") if key == "MemTotal" then total = tonumber(value) elseif key == "MemFree" then free = tonumber(value) elseif key == "Buffers" then buffers = tonumber(value) elseif key == "Cached" then cached = cached or tonumber(value) elseif key == "SwapTotal" then swaptotal = tonumber(value) elseif key == "SwapFree" then swapfree = tonumber(value) end end) or not total then return nil end -- All values are in MB, exactly like the PHP version. local info = {} info.memTotal = round(total / 1024, 2) info.memFree = round((free or 0) / 1024, 2) info.memBuffers = round((buffers or 0) / 1024, 2) info.memCached = round((cached or 0) / 1024, 2) info.memUsed = round(info.memTotal - info.memFree - info.memBuffers - info.memCached, 2) info.memUsedPercent = round(info.memUsed / info.memTotal * 100, 2) info.memBuffersPercent = round(info.memBuffers / info.memTotal * 100, 2) info.memCachedPercent = round(info.memCached / info.memTotal * 100, 2) info.swapTotal = round((swaptotal or 0) / 1024, 2) info.swapFree = round((swapfree or 0) / 1024, 2) info.swapUsed = round(info.swapTotal - info.swapFree, 2) info.swapPercent = info.swapTotal ~= 0 and round(info.swapUsed / info.swapTotal * 100, 2) or 0 for key, value in pairs(info) do if not find(key, "Percent", 1, true) then if value < 1024 then info[key] = trimnum(value, 2) .. " M" else info[key] = trimnum(round(value / 1024, 3), 3) .. " G" end end end return info end local function get_loadavg() local values = { match(read_line("/proc/loadavg") or "", "^(%S+)%s+(%S+)%s+(%S+)%s+(%S+)") } if not values[1] then return "" end return format("%s %s %s %s", values[1], values[2], values[3], values[4]) end local function get_distname() for _, name in ipairs({ "redhat", "centos", "system" }) do local line = read_line("/etc/" .. name .. "-release") if line and line ~= "" then return line end end local description, pretty each_line("/etc/os-release", function(line) local key, value = match(line, "^([%w_]+)=(.*)$") if not key then return end if find(value, '^".*"$') then value = sub(value, 2, -2) end if key == "DISTRIB_DESCRIPTION" then description = description or value end if key == "PRETTY_NAME" then pretty = pretty or value end end) if description then return description end if pretty then return pretty end local issue each_line("/etc/issue", function(line) if not issue and find(line, "elcome to ") then issue = sub(line, 12) end end) if issue then return issue end return trim((read_line("/proc/sys/kernel/ostype") or "Linux") .. " " .. (read_line("/proc/sys/kernel/osrelease") or "")) end -- Like node-exporter.lua: derive the machine type without forking uname(1). local function architecture() local arch = getenv("HOSTTYPE") or getenv("MACHTYPE") if arch and arch ~= "" then return match(arch, "^[^-]+") or arch end arch = match(package.cpath or "", "/lib/([^/]+)/lua") if arch then return match(arch, "^[^-]+") or arch end arch = trim(run("LC_ALL=C uname -m 2>/dev/null") or "") if arch and arch ~= "" then return arch end return "unknown" end local function get_uname() local parts = { read_line("/proc/sys/kernel/ostype") or "Linux", read_line("/proc/sys/kernel/hostname") or "localhost", read_line("/proc/sys/kernel/osrelease") or "", read_line("/proc/sys/kernel/version") or "", architecture(), } return concat(parts, " ") end local function get_boardinfo() local info = {} info.BIOSVendor = read_line("/sys/class/dmi/id/bios_vendor") info.BIOSVersion = read_line("/sys/class/dmi/id/bios_version") info.BIOSDate = read_line("/sys/class/dmi/id/bios_date") info.boardVendor = read_line("/sys/class/dmi/id/board_vendor") or read_line("/sys/class/dmi/id/product_name") info.boardName = read_line("/sys/class/dmi/id/board_name") info.boardVersion = read_line("/sys/class/dmi/id/board_version") local output = run("LC_ALL=C ls -1 /dev/disk/by-id 2>/dev/null") if output then for name in gmatch(output, "[^\n]+") do if sub(name, 1, 1) ~= "." and not find(name, "DVD-ROM", 1, true) then local vendor, model = match(name, "^([^-_]+)-([^_]+)") if vendor and model then info.diskVendor = string.upper(vendor) info.diskModel = model end break end end end return info end -- statvfs binding, if one is installed (LuCI's nixio or luaposix); nil otherwise. local statvfs do local ok, mod = pcall(require, "nixio.fs") if ok and type(mod) == "table" and mod.statvfs then statvfs = mod.statvfs else ok, mod = pcall(require, "posix.sys.statvfs") if ok and type(mod) == "table" and mod.statvfs then statvfs = mod.statvfs end end end local function get_diskinfo() local info = { diskTotal = 0, diskFree = 0, diskUsed = 0, diskPercent = 0 } local total, free if statvfs then local ok, st = pcall(statvfs, ".") if ok and type(st) == "table" then -- nixio uses bare field names, luaposix prefixes them with f_. local bsize = st.frsize or st.f_frsize or st.bsize or st.f_bsize or 0 local blocks, bavail = st.blocks or st.f_blocks, st.bavail or st.f_bavail if blocks and bavail and bsize > 0 then total, free = blocks * bsize, bavail * bsize end end end if not total then -- Fallback: one LC_ALL=C df run, tolerating a device name with digits. local df = "/bin/df" local probe = io.open("/usr/bin/df", "r") if probe then probe:close() df = "/usr/bin/df" end for line in gmatch(run("LC_ALL=C " .. df .. " -kP . 2>/dev/null") or "", "[^\n]+") do local blocks, avail = match(line, "^%S+%s+(%d+)%s+%d+%s+(%d+)%s+%d+%%") if blocks then total, free = tonumber(blocks) * 1024, tonumber(avail) * 1024 break end end end if total and total > 0 then info.diskTotal = round(total / 1073741824, 2) info.diskFree = round((free or 0) / 1073741824, 2) info.diskUsed = round(info.diskTotal - info.diskFree, 2) info.diskPercent = round(info.diskUsed / info.diskTotal * 100, 2) end return info end -- Directory listing binding, if one is installed (LuCI's nixio or luaposix); -- nil otherwise. Modules are enumerated by Lua itself, nothing is forked. local lua_dir do local ok, mod = pcall(require, "posix.dirent") if ok and type(mod) == "table" and type(mod.dir) == "function" then lua_dir = function(path) local listed, entries = pcall(mod.dir, path) if listed and type(entries) == "table" then return entries end end else ok, mod = pcall(require, "nixio.fs") if ok and type(mod) == "table" and type(mod.dir) == "function" then lua_dir = function(path) local listed, iterator = pcall(mod.dir, path) if not listed or type(iterator) ~= "function" then return nil end local entries = {} for entry in iterator do entries[#entries + 1] = entry end return entries end end end end -- The module names reachable through package.path/package.cpath, e.g. -- "bit32, posix". The names are only good enough to be informative: the -- search directories are enumerated and "?/init.lua" is reported as the -- module (or as its namespace) it lives in. local function get_lua_modules() local dirs, modules, seen = {}, {}, {} local function add(name) -- "bit32.so" or "nixio.lua" is a module, a "posix" entry is a -- namespace; dot entries are "." and "..". local module = match(name, "^(.*)%.lua$") or match(name, "^(.*)%.so$") or name if sub(module, 1, 1) ~= "." and not seen[module] then seen[module] = true modules[#modules + 1] = module end end if lua_dir then for _, template in ipairs({ package.path or "", package.cpath or "" }) do for entry in gmatch(template, "[^;]+") do -- ".../5.1/?.lua" and ".../5.1/?/init.lua" name the same -- directory, while a relative entry like "./?.lua" would -- enumerate the cgi directory itself, so only the absolute -- directories are kept. local dir = match(entry, "^(/.*)[/\\][^/\\]*$") if dir and not dirs[dir] then dirs[dir] = true for _, name in ipairs(lua_dir(dir) or {}) do add(name) end end end end end if #modules == 0 then -- No filesystem binding: ask Lua's own module searchers instead. A -- searcher returns a loader (without running it) when it can find the -- module, so the well known libraries are still reported. local searchers = package.searchers or package.loaders or {} for _, name in ipairs({ "posix", "nixio", "bit32", "cjson", "lfs", "socket", "ssl", "zlib", "struct", "lpeg", "md5", "uuid", "sqlite3" }) do for i = 1, #searchers do local ok, loader = pcall(searchers[i], name) if ok and type(loader) == "function" then add(name) break end end end end table.sort(modules) return concat(modules, ", ") end -- /sys/class/net//operstate is "down" for a port without carrier -- (e.g. an unused switch port). Those are hidden from the table; "unknown" -- devices such as lo or tun, and devices without a sysfs entry, are kept. local function netdev_is_down(device) local state = trim(read_line("/sys/class/net/" .. device .. "/operstate")) return state == "down" end -- Returns the device map (for JSON) and the /proc/net/dev order (for the page, -- which is also the order the PHP version printed). local function get_netdev() local info, order = {}, {} local seconds = uptime_seconds() -- /proc/uptime has a 10 ms resolution, which is enough for the "K/s" -- deltas the page computes and does not need a clock syscall. local timestamp = seconds and floor(seconds * 1000) or time() * 1000 local index = 0 each_line("/proc/net/dev", function(line) index = index + 1 if index <= 2 then return end -- "eth0: rx_bytes rx_packets ... (8) ... tx_bytes ..." local device, rx, tx = match(line, "^%s*([^:]+):%s*(%d+)%s+%d+%s+%d+%s+%d+%s+%d+%s+%d+%s+%d+%s+%d+%s+(%d+)") if not device then device = match(line, "^%s*([^:]+):") if not device then return end local counters = match(line, "^%s*[^:]+:%s*(.*)$") or "" rx = match(counters, "^(%d+)") tx = match(counters, "^%d+%s+%d+%s+%d+%s+%d+%s+%d+%s+%d+%s+%d+%s+%d+%s+(%d+)") end if netdev_is_down(device) then return end rx, tx = tonumber(rx) or 0, tonumber(tx) or 0 if not info[device] then order[#order + 1] = device end info[device] = { ts = timestamp, rx = rx, human_rx = human_filesize(rx), tx = tx, human_tx = human_filesize(tx), } end) return info, order end --------------------------------------------------------------------------- -- Routes --------------------------------------------------------------------------- if method == "raw" then local body = read_all(script_path()) or "" send_headers("application/octet-stream", { 'Content-Disposition: attachment; filename="tz.lua"', "Content-Length: " .. #body }) write(body) finish() end if method == "sysinfo" then local body = json_encode({ stat = get_stat(), stime = date("%Y-%m-%d %H:%M:%S"), uptime = get_uptime(), tempinfo = get_tempinfo(), meminfo = get_meminfo(), loadavg = get_loadavg(), sockstat = get_sockstat(), diskinfo = get_diskinfo(), netdev = get_netdev(), }) send_headers("application/json", { "Content-Length: " .. #body }) write(body) finish() end if method == "luainfo" or method == "phpinfo" then local lua_modules = get_lua_modules() local report = { "tz.lua runtime report", format("Lua\t\t: %s (%s)", lua_version, lua_vm), format("Interpreter\t: %s", (arg and arg[-1]) or "lua5.1"), format("Script\t\t: %s", script_path()), format("Mode\t\t: %s", is_cgi and "CGI" or "CLI"), format("Time\t\t: %s", date("%Y-%m-%d %H:%M:%S")), format("Memory\t\t: %.1f KB", collectgarbage("count")), format("Modules\t\t: %s", lua_modules ~= "" and lua_modules or "-"), format("package.path\t: %s", package.path or ""), format("package.cpath\t: %s", package.cpath or ""), "", "Environment:", } for _, key in ipairs({ "GATEWAY_INTERFACE", "REQUEST_METHOD", "QUERY_STRING", "SERVER_NAME", "SERVER_ADDR", "SERVER_PORT", "SERVER_SOFTWARE", "REMOTE_ADDR", "HTTP_HOST", "HTTP_ACCEPT_LANGUAGE", "DOCUMENT_ROOT", "SCRIPT_NAME", "SCRIPT_FILENAME", "LANG", "LC_CTYPE", "TZ", }) do report[#report + 1] = format("%s\t\t: %s", key, getenv(key) or "") end report[#report + 1] = "" report[#report + 1] = "package.loaded:" local modules = {} for name in pairs(package.loaded) do modules[#modules + 1] = name end table.sort(modules) report[#report + 1] = " " .. concat(modules, " ") report[#report + 1] = "" local body = concat(report, "\n") send_headers("text/plain; charset=utf-8", { "Content-Length: " .. #body }) write(body) finish() end --------------------------------------------------------------------------- -- The prober page --------------------------------------------------------------------------- local time_start = clock() local stat = get_stat() local uname = get_uname() local stime = date("%Y-%m-%d %H:%M:%S") local distname = get_distname() local server_addr = get_server_addr() local remote_addr = ip_part(get_remote_addr()) local uptime = get_uptime() local cpuinfo = get_cpuinfo() local tempinfo = get_tempinfo() local meminfo = get_meminfo() or {} local loadavg = get_loadavg() local sockstat = get_sockstat() local boardinfo = get_boardinfo() local diskinfo = get_diskinfo() local netdev, netdev_order = get_netdev() local lua_modules = get_lua_modules() local server_name = getenv("SERVER_NAME") or getenv("HTTP_HOST") or server_addr local hostname = match(uname, "^%S+%s+(%S+)") or server_name local server_software = (getenv("SERVER_SOFTWARE") or "") .. " lua/" .. lua_version local prober_path = gsub(script_path(), "\\", "/") local ctype = find(locale_ctype, "^C$") and "POSIX" or locale_ctype local sockets = (sockstat.sockets and sockstat.sockets.used) or 0 -- No Content-Length here: the page is streamed into the stdio buffer. send_headers("text/html; charset=utf-8") write([[ ]], h(server_name), [[
]], __('Server Information'), [[
]], __('Hostname'), [[ ]], h(hostname), "(", h(server_addr), [[)    ]], __('Client IP'), [[: ]], h(remote_addr), [[
]], __('Uname'), [[ ]], h(uname), [[
]], __('OS'), [[ ]], h(distname), [[ ]], __('Language'), [[ ]], h(ctype), [[
]], __('Server Software'), [[ ]], h(server_software), [[ ]], __('Server Port'), [[ ]], h(getenv("SERVER_PORT")), [[
]], __('Prober Path'), [[ ]], h(prober_path), [[
]]) if tempinfo.cpu then write([[ ]]) end if boardinfo.BIOSVendor then write([[ ]]) end if boardinfo.diskModel then write([[ ]]) end write([[
]], __('Server Realtime Data'), [[
]], __('Time'), [[ ]], h(stime), [[ ]], __('Uptime'), [[ ]], h(uptime), [[
]], __('CPU Model'), " [", cpuinfo.num or 0, [[x] ]]) if cpuinfo.model then write(" ", h(cpuinfo.model)) end if cpuinfo.frequency then write(" | ", __('Frequency'), ": ", h(cpuinfo.frequency), " MHz") end if cpuinfo.l2cache then write(" | ", __('L2 Cache'), ":", h(cpuinfo.l2cache)) end if cpuinfo.bogomips then write(" | Bogomips: ", h(cpuinfo.bogomips), " ×", cpuinfo.num or 0) end write([[
]], __('CPU Instruction Set'), [[ ]], h(cpuinfo.flags), [[
]], __('CPU Temperature'), [[ ]], h(tempinfo.cpu), [[ ]], __('GPU Temperature'), [[ ]], h(tempinfo.gpu), [[
]], __('BIOS Version'), [[ ]], h((boardinfo.BIOSVendor or "") .. " " .. (boardinfo.BIOSDate or "")), [[ ]], __('Board Vendor'), [[ ]], h(concat({ boardinfo.boardVendor or "", boardinfo.boardName or "", boardinfo.boardVersion or "" }, " ")), [[
]], __('HardDisk Model'), [[ ]], h(boardinfo.diskModel .. " " .. (boardinfo.diskVendor or "")), [[
]], __('CPU Usage'), [[ 0.0 user, 0.0 sys, 0.0 nice, 99.9 idle, 0.0 iowait, 0.0 irq, 0.0 softirq, 0.0 steal
]], __('Memory Usage'), [[ ]], __('Physical Memory'), [[ ]], h(meminfo.memTotal), [[ , ]], __('Used'), [[ ]], h(meminfo.memUsed), [[ , ]], __('Cached'), [[ ]], h(meminfo.memBuffers), [[ / ]], h(meminfo.memCached), [[ , ]], __('Free'), [[ ]], h(meminfo.memFree), [[ , ]], __('Percent'), [[ ]], h(meminfo.memUsedPercent), [[%
]]) if leading_number(meminfo.swapTotal) > 0 then write([[ SWAP:]], h(meminfo.swapTotal), [[ , ]], __('Used'), [[ ]], h(meminfo.swapUsed), [[ , ]], __('Free'), [[ ]], h(meminfo.swapFree), [[ , ]], __('Percent'), [[ ]], h(meminfo.swapPercent), [[%
]]) end write([[
]], __('Disk Usage'), [[ ]], __('Total Space'), " ", h(diskinfo.diskTotal), [[ G,]], __('Used'), [[ ]], h(diskinfo.diskUsed), [[ G,]], __('Free'), [[ ]], h(diskinfo.diskFree), [[ G,]], __('Percent'), [[ ]], h(diskinfo.diskPercent), [[%
]], __('Loadavg'), [[ ]], h(loadavg), [[
]], __('Sockets'), [[ ]], h(sockets), [[
]]) for i = 1, #netdev_order do local device = netdev_order[i] local entry = netdev[device] write([[ ]]) end write([[
]], __('Network Usage'), [[
]], h(device), [[ : ]], __('Rx'), [[: ]], h(entry.human_rx), [[ ]], __('Realtime'), [[: 0B/s ]], __('Tx'), [[: ]], h(entry.human_tx), [[ ]], __('Realtime'), [[: 0B/s
]], __('Lua Information'), [[
]], __('Version'), [[]], h(lua_version), [[ ]], __('Virtual Machine'), [[]], h(lua_vm), [[
]], __('Interpreter'), [[]], h((arg and arg[-1]) or "lua5.1"), [[ ]], __('Environment'), [[]], is_cgi and "CGI" or "CLI", [[
]], __('Modules'), [[ ]], h(lua_modules ~= "" and lua_modules or "-"), [[
Lua ]], __('Prober'), [[ (]], __('Turbo Version'), [[) v1.0 Processed in ]], format("%0.1f", (clock() - time_start) * 1000), " ms, ", -- collectgarbage("count") returns a float with a fractional part, so -- "%d" would raise "number has no integer representation" on Lua 5.3+. format("%.0f", collectgarbage("count")), [[ KB memory usage. ]], __('Back to top'), [[
]]) finish()