#!/opt/lnxall_app/bin/lua

-- Created by jiaqiang.ye@lnxall.com
-- Dump Application running time
-- 2023/03/27

local posix = require "posix"
local invoker = require "invoker"

-- kernel configuration, CONFIG_HZ:
local g_HZ = nil

local function dump_appinfo(pid)
	local path = invoker.readlink(string.format('/proc/%d/exe', pid))
	if type(path) ~= "string" or string.len(path) == 0 then
		return false
	end

	local appn = string.match(path, "/([^/]+)$")
	if not appn then
		io.stderr:write(string.format("Error, failed to determine name: '%s'\n", path))
		io.stderr:flush()
		return false
	end

	local appst = invoker.readfile(string.format('/proc/%d/stat', pid), invoker.TRIMEND)
	if type(appst) ~= "string" or string.len(appst) == 0 then
		return false
	end

	local s0, s1 = string.find(appst, "%(.+%)")
	if not s0 then
		io.stderr:write(string.format("Error, failed to determine AppName: '%s'\n", appn))
		io.stderr:flush()
		return false
	end
	-- get invoke name:
	local appm = (s1 - s0) > 0x2 and appst:sub(s0 + 1, s1 - 1) or appn

	s1 = s1 + 0x2
	if s1 >= string.len(appst) then
		io.stderr:write(string.format("Error, invalid '/proc/xxx/stat' for '%s'\n", appn))
		io.stderr:flush()
		return false
	end

	local runtime = nil
	local left, count = appst:sub(s1), 0
	for field in string.gmatch(left, "([^%s]+)") do
		count = count + 0x1
		if count >= 20 then
			runtime = tonumber(field)
			break
		end
	end

	if not runtime then
		io.stderr:write(string.format("Error, cannot determine runtime for '%s'\n", appn))
		io.stderr:flush()
		return false
	end

	local uptim = invoker.uptime()
	local duration = math.floor(runtime / g_HZ) -- seconds
	if uptim < duration then
		io.stderr:write(string.format("Error, invalid uptime and duration for '%s': %d, %d\n",
			appn, uptim, duration))
		io.stderr:flush()
		return false
	end

	duration = uptim - duration
	local days, dur = math.floor(duration / 86400), duration
	duration = duration % 86400
	local hour = math.floor(duration / 3600)
	local mins = math.floor((duration % 3600) / 60)
	return { ["appn"] = appn, ["appm"] = appm, ["pid"] = pid,
		["duration"] = dur, ["days"] = days, ["hours"] = hour, ["mins"] = mins }
end

local function warn_about_hz()
	g_HZ = 100
	return true
end

local function fetch_config_hz()
	local info = "==== NOTE THAT CONFIG_HZ=%d\n"
	local cfghz = posix.getenv("CONFIG_HZ")
	if type(cfghz) == "string" and string.len(cfghz) > 0 then
		local hz = tonumber(cfghz)
		if type(hz) == "number" and hz > 0 and hz == math.floor(hz) then
			g_HZ = hz
			io.stdout:write(string.format(info, hz))
			io.stdout:flush()
			return true
		end
	end

	return warn_about_hz()
end

local function padstr(ostr, lstr, right)
	local olen = string.len(ostr)
	if olen > lstr then return ostr:sub(1, lstr) end
	if olen == lstr then return ostr end
	if right then
		return string.format("%s%s", string.rep(" ", lstr - olen), ostr)
	end
	return string.format("%s%s", ostr, string.rep(" ", lstr - olen))
end

local function dump_allapp()
	local apps = posix.glob("/proc/*/exe")
	if type(apps) ~= "table" then
		io.stderr:write("Error, no application found.\n")
		io.stderr:flush()
		return false
	end

	local appInfo, appnum, namelen = {}, 0, 1
	for _, what in ipairs(apps) do
		local pid = string.match(what, "/proc/(%d+)/exe")
		if pid then pid = tonumber(pid) end
		if type(pid) == "number" then
			local appi = dump_appinfo(pid)
			if appi then
				local nlen = string.len(appi["appn"])
				if nlen > namelen then namelen = nlen end
				appnum = appnum + 0x1
				appInfo[appnum] = appi
			end
		end
	end

	namelen = namelen + 0x1
	if appnum > 1 then
		table.sort(appInfo, function (ap, bp)
			return ap.duration > bp.duration
		end)
	end
	for _, what in ipairs(appInfo) do
		io.stdout:write(string.format(
			"%s => %s, PID: %d, duration: %d seconds, (%d days, %d hours, %d mins)\n",
			padstr(what.appn, namelen, true), padstr(what.appm, 15),
			what.pid, what.duration, what.days, what.hours, what.mins))
		io.stdout:flush()
	end

	return true
end

if not fetch_config_hz() then os.exit(1) end
dump_allapp(); os.exit(0)
