#!/opt/lnxall_app/bin/lua

-- created by jiaqiang.ye@lnxall.com
-- Simple command-line utility for EMS2.0 packapp
-- 2024/08/30

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

local gfmt = string.format
-- global variables
local g_packdir = '/mnt/ssd/.packapp'
local g_packapp = '/mnt/ssd/.packapp.tar.gz'
local g_packapp_h, g_ipaddr_h = nil, "127.0.0.1"
local g_ipaddr, g_gwsn, g_cmd, g_4arg = nil, nil, nil, nil

local g_upinfo  = "/tmp/.packapp-info.json"
local g_uplock  = "/tmp/.packapp-uplock.txt"
local g_sshcmd  = { "sshpass", "-p", "lnxall123", "ssh",
	'-oStrictHostKeyChecking=no', '-oUserKnownHostsFile=/dev/null', }

local function help()
	local err = io.stderr
	err:write("Usage: packapp IPADDR GWSN start|stop|query|restart|upgrade|upgraden\n")
	err:write("Examples:\n")
	err:write("    packapp IPADDR GWSN start      # 开始EMS/LC打包\n")
	err:write("    packapp IPADDR GWSN stop       # 中止EMS/LC打包\n")
	err:write("    packapp IPADDR GWSN query      # 查询EMS/LC打包状态\n")
	err:write("    packapp IPADDR GWSN restart    # 重启从机上的EMS/EMU服务\n")
	err:write("    packapp IPADDR GWSN reboot     # 重启从机网关\n")
	err:write("    packapp IPADDR GWSN upgrade path/to/ELF-PACKAGE  # 复制ELF包到从机并在从机上安装运行\n")
	err:write("    packapp IPADDR GWSN upgraden path/to/ELF-PACKAGE # 复制ELF包到从机并在从机上安装运行（带-n运行）\n")
	err:flush()
end

local function load_json(filp)
	local filh = io.open(filp, "rb")
	if not filh then return nil end
	local fild = filh:read("*a")
	filh:close(); filh = nil
	if type(fild) ~= "string" or #fild == 0 then
		return nil
	end

	local okay, jdat = pcall(cjson.decode, fild)
	if not okay then return nil end
	if type(jdat) ~= "table" then return nil end
	return jdat
end

local function validate_args(arg0, arg1, arg2, arg3, arg4)
	g_ipaddr, g_gwsn, g_cmd = arg1, arg2, arg3
	if type(g_ipaddr) ~= "string" then g_ipaddr = 'unknown' end
	if not invoker.isipv4(g_ipaddr) then
		io.stderr:write(gfmt("ERROR: invalid IPv4 address specified: %s\n", g_ipaddr))
		io.stderr:flush()
		if arg1 == nil or arg1 == "-h" or arg1 == "--help" then help() end
		return false
	end

	if type(g_gwsn) ~= "string" then g_gwsn = "" end
	if #g_gwsn == 0 then
		io.stderr:write(gfmt("ERROR: invalid Gateway SN specified: %s\n", g_gwsn))
		io.stderr:flush()
		return false
	end

	if g_cmd ~= "start" and g_cmd ~= "stop" and g_cmd ~= "query" and
		g_cmd ~= "reboot" and g_cmd ~= "restart" and g_cmd ~= "upgrade" and g_cmd ~= "upgraden" then
		if type(g_cmd) ~= "string" then g_cmd = "" end
		io.stderr:write(gfmt("ERROR: invalid command for packapp: %s\n", g_cmd))
		io.stderr:flush()
		return false
	end

	g_4arg = arg4
	if type(arg0) == "string" then arg0 = invoker.realpath(arg0) end
	if type(arg0) == "string" then arg0 = string.match(arg0, "^(.+)/") end
	if type(arg0) ~= "string" or posix.access(arg0) ~= 0 then
		io.stderr:write("ERROR: packapp script not found.\n")
		io.stderr:flush()
		return false
	end
	g_packapp_h = gfmt("%s/packapp.tar.gz", arg0)

	-- initialize environment variables
	posix.setenv("EMS_IP")
	posix.setenv("SSH_CLIENT")
	posix.setenv("SSH_CONNECTION")
	invoker.invoke(invoker.NOSTDIO, "mkdir", "-p",
		"/opt/elect_resources/resources/app/web/dist/exdata")
	return true
end

local function shell_cmd(flag, cmdline)
	if type(flag) ~= "number" then
		flag = invoker.NOSTDIO + invoker.OUTPUT
	end
	local ok, out = invoker.invoke(flag, "/bin/sh", "-c", cmdline)
	return ok, out
end

local function scp_cmd(flag, cmdlist)
	if #cmdlist ~= 0x2 then
		io.stdout:write("ERROR: invalid number of argument for scp.\n")
		io.stdout:flush()
		return nil
	end

	local list, nl = {}, 1
	while type(g_sshcmd[nl]) == "string" do
		if g_sshcmd[nl] == "ssh" then
			list[nl] = "scp"
		else
			list[nl] = g_sshcmd[nl]
		end
		nl = nl + 1
	end

	list[nl] = "-r"; nl = nl + 1
	list[nl] = cmdlist[1]; nl = nl + 1
	list[nl] = gfmt("root@%s:%s", g_ipaddr, cmdlist[2])
	if type(flag) ~= "number" then
		flag = invoker.NOSTDIO + invoker.OUTPUT
	end

	local ok, out = invoker.invoke(flag, list)
	return ok, out
end

local function ssh_cmd(flag, cmdlist)
	local list, nl = {}, 1
	while type(g_sshcmd[nl]) == "string" do
		list[nl] = g_sshcmd[nl]
		nl = nl + 1
	end

	list[nl] = "root@" .. g_ipaddr
	nl = nl + 1
	for _, cmd in ipairs(cmdlist) do
		list[nl] = cmd
		nl = nl + 1
	end

	if type(flag) ~= "number" then
		flag = invoker.NOSTDIO + invoker.OUTPUT
	end

	local ok, out = invoker.invoke(flag, list)
	return ok, out
end

-- check whether we can upgrade given GWSN
local function check_upgrade()
	local lfd = invoker.waitlock(g_uplock)
	if not lfd then
		io.stderr:write("ERROR: failed to acquire file lock.\n")
		io.stderr:flush()
		lfd = -1
	end

	local jdat = load_json(g_upinfo)
	if type(jdat) == "table" then
		jdat = jdat["upgrade"]
	end
	if type(jdat) ~= "table" then
		posix.close(lfd)
		return true
	end
	local lastup = jdat[g_gwsn]
	if not lastup then
		posix.close(lfd)
		return true
	end

	if (lastup + 30) >= invoker.uptime() then
		posix.close(lfd)
		return false
	end
	posix.close(lfd)
	return true
end

local function write_upgrade_time(when)
	local lfd = invoker.waitlock(g_uplock)
	if not lfd then
		io.stderr:write("ERROR: failed to acquire file lock.\n")
		io.stderr:flush()
		lfd = -1
	end

	local oldi = load_json(g_upinfo)
	if not oldi then oldi = {} end
	local upin = oldi["upgrade"]
	if type(upin) ~= "table" then upin = {} end

	if not when then when = invoker.uptime() end
	upin[g_gwsn] = when
	oldi["upgrade"] = upin
	local uph = io.open(g_upinfo, "wb")
	if uph then
		uph:write(cjson.encode(oldi))
		uph:close(); uph = nil
	end
	posix.close(lfd)
	return true
end

local function do_sysupgrade(hasn)
	if type(g_4arg) ~= "string" or posix.access(g_4arg) ~= 0 then
		io.stderr:write("ERROR: upgrade ELF file does not exist.\n")
		io.stderr:flush()
		return false
	end

	if not check_upgrade() then
		io.stderr:write(gfmt("ERROR: cannot upgrade within 30 seconds: %s\n", g_gwsn))
		io.stderr:flush()
		return false
	end

	io.stdout:write(gfmt("INFO: copying ELF package for %s...\n", g_gwsn))
	io.stdout:flush()
	if g_ipaddr == g_ipaddr_h then
		if shell_cmd(nil, gfmt("cp -v '%s' /mnt/ssd/.LNXALL_APP.elf", g_4arg)) ~= 0 then
			io.stderr:write(gfmt("ERROR, failed to copy %s\n", g_4arg))
			io.stderr:flush()
			return false
		end
	else -- target NOT EMS device
		-- copy ELF package via `scp
		if scp_cmd(invoker.NOSTDIO, { [1] = g_4arg, [2] = "/mnt/ssd/.LNXALL_APP.elf" }) ~= 0 then
			io.stderr:write(gfmt("ERROR: failed to copy %s to %s\n", g_4arg, g_gwsn))
			io.stderr:flush()
			return false
		end
	end

	local okay = nil
	write_upgrade_time()
	local cmd = gfmt("%s/%s %s %s",
		g_packdir, "cmd_upgrade.sh", g_gwsn, hasn and "-n" or "")
	if g_ipaddr == g_ipaddr_h then
		okay = shell_cmd(0, cmd)
	else
		okay = ssh_cmd(0, { [1] = cmd })
	end
	if okay ~= 0 then okay = 3 end
	os.exit(okay)
end

local function check_packapp()
	if not invoker.tcpcheck(g_ipaddr, 22, 2500) then
		io.stderr:write(gfmt("ERROR: gateway %s not online: %s\n", g_ipaddr, g_gwsn))
		io.stderr:flush()
		os.exit(2)
	end

	local md50, md51 = nil, nil
	local okay, output = invoker.invoke(invoker.NOSTDIO + invoker.OUTPUT,
		"md5sum", g_packapp_h)
	if okay == 0 and type(output) == "string" then
		md50 = string.match(output, "^([%d%x]+)")
	end
	if type(md50) ~= "string" or #md50 ~= 32 then
		io.stderr:write(gfmt("ERROR: cannot get md5sum for %s\n", g_packapp_h))
		io.stderr:flush()
		os.exit(3)
	end

	local tried = false
	if g_ipaddr == g_ipaddr_h then
		while true do
			okay, output = shell_cmd(nil, gfmt("[ -d %s ] && md5sum %s", g_packdir, g_packapp))
			if okay ~= 0 then output = "" end
			md51 = string.match(output, "^([%d%x]+)")
			if (md51 == md50 or tried) and posix.access(g_packdir) == 0 then
				break
			end
			tried = true
			invoker.invoke(invoker.NOSTDIO, "/bin/sh", "-c", gfmt("rm -rf %s %s", g_packdir, g_packapp))
			invoker.invoke(invoker.NOSTDIO, "cp", "-f", g_packapp_h, g_packapp)
			invoker.invoke(invoker.NOSTDIO, "/bin/sh", "-c", string.format("cd /mnt/ssd && tar -zxf %s", g_packapp))
		end

		if md51 ~= md50 then
			io.stderr:write(gfmt("ERROR: cannot copy packapp to %s, %s\n", g_gwsn, g_ipaddr))
			io.stderr:flush()
			os.exit(4)
		end
		return true
	end

	while true do
		okay, output = ssh_cmd(nil, { [1] = gfmt("md5sum %s", g_packapp) })
		if okay ~= 0 then output = "" end
		md51 = string.match(output, "^([%d%x]+)")
		if md51 == md50 or tried then
			break
		end
		tried = true
		ssh_cmd(nil, { [1] = gfmt("rm -rf %s %s", g_packapp, g_packdir ) })
		if scp_cmd(invoker.NOSTDIO, { [1] = g_packapp_h, [2] = g_packapp }) == 0 then
			ssh_cmd(nil, { [1] = gfmt("cd /mnt/ssd && tar -zxf %s", g_packapp) })
		end
	end

	if md51 ~= md50 then
		io.stderr:write(gfmt("ERROR: cannot copy packapp to %s, %s\n", g_gwsn, g_ipaddr))
		io.stderr:flush()
		os.exit(5)
	end
	return true
end

local function mainfunc_op()
	local cmd = nil
	if g_ipaddr == g_ipaddr_h then posix.setenv("EMS_IP", g_ipaddr_h) end
	if g_cmd == "start" then
		cmd = gfmt("%s/cmd_start.sh %s", g_packdir, g_gwsn)
	elseif g_cmd == "stop" then
		cmd = gfmt("%s/cmd_stop.sh %s", g_packdir, g_gwsn)
	elseif g_cmd == "query" then
		cmd = gfmt("%s/cmd_query.sh %s", g_packdir, g_gwsn)
	elseif g_cmd == "reboot" then
		cmd = gfmt("%s/cmd_reboot.sh %s", g_packdir, g_gwsn)
	elseif g_cmd == "restart" then
		cmd = gfmt("%s/cmd_restart.sh %s", g_packdir, g_gwsn)
	elseif g_cmd == "upgrade" then
		return do_sysupgrade(false)
	elseif g_cmd == "upgraden" then
		return do_sysupgrade(true)
	else
		io.stderr:write(gfmt("ERROR: invalid command: %s\n", g_cmd))
		io.stderr:flush()
		os.exit(5)
	end

	local okay, output = nil, nil
	if g_ipaddr == g_ipaddr_h then
		okay, output = shell_cmd(nil, cmd)
	else
		okay, output = ssh_cmd(nil, { [1] = cmd })
	end
	if type(okay) ~= "number" or okay ~= 0 then
		if type(okay) ~= "number" then okay = 90 end
		if type(output) == "string" then
			io.stdout:write(output)
		else
			io.stdout:write(gfmt("ERROR: command '%s' has failed with: %d\n", g_cmd, okay))
		end
		io.stdout:flush()
		os.exit(okay)
	end

	if type(output) ~= "string" or #output == 0 then
		output = gfmt("INFO: command %s executed successfully, without output.", g_cmd)
	end
	io.stdout:write(output)
	io.stdout:flush()
	os.exit(0)
end

if not validate_args(arg[0], arg[1], arg[2], arg[3], arg[4]) then
	os.exit(1)
end
check_packapp()
mainfunc_op()
os.exit(2)
