#!/opt/lnxall_app/bin/lua

-- Created by jiaqiang.ye@lnxall.com
-- Synchronize System Clock via PTP
-- 2025/11/11

local bit32   = require "bit32"
local posix   = require "posix"
local invoker = require "invoker"
local gfmt    = string.format

local g_busybox      = "/usr/local/bin/busybox"
local g_pid_ntpd     = nil
local g_pid_ptp4l    = nil
local g_fd_ptp4l     = nil
local g_pid_phc2s    = nil

local function systemd_disable(svr)
	invoker.invoke(invoker.NOSTDIO, "systemctl", "disable", svr)
	invoker.invoke(invoker.NOSTDIO, "systemctl", "stop", svr)
end

local function start_ptp4l(netdev)
	if g_fd_ptp4l then
		invoker.closefd(g_fd_ptp4l)
		g_fd_ptp4l = nil
	end
	if g_pid_ptp4l then
		invoker.kill(g_pid_ptp4l, 9)
		invoker.waitpid(g_pid_ptp4l, nil)
		g_pid_ptp4l = nil
	end

	local pid, outfd = invoker.invoke(invoker.OUTPUT + invoker.NOWAIT,
		"ptp4l", "-i", netdev, "-s", "-q", "-m", "-H", "-2")
	if type(pid) ~= "number" then
		io.stderr:write("Error, failed to run ptp4l.\n")
		io.stderr:flush()
		return nil
	end

	-- set to non-block mode
	if type(outfd) == "number" and outfd >= 0 then
		local fdst = posix.fcntl(outfd, posix.F_GETFL, 0)
		if type(fdst) == "number" and fdst >= 0 then
			local newst = bit32.bor(fdst, posix.O_NONBLOCK)
			if newst ~= fdst then
				local r = posix.fcntl(outfd, posix.F_SETFL, newst)
				io.stdout:write(gfmt("Setting fd to NONBLOCK mode: %s\n", outfd, tostring(r)))
				io.stdout:flush()
			end
		end
	end

	g_pid_ptp4l = pid
	g_fd_ptp4l  = outfd
	return true
end

local function read_ptp4l(outfd)
	local output = invoker.readfd(outfd, 8192)
	if type(output) == "string" then
		-- ptp4l[14874.493]: rms   18 max   32 freq -60364 +/-  20 delay  1102 +/-   0
		local delta = string.match(output, "rms%s+(%d+)%s+max")
		if delta then delta = tonumber(delta) end
		if type(delta) == "number" then
			return delta
		end
	end
	return nil
end

local function start_phc2sys(netdev)
	if g_pid_phc2s then
		invoker.kill(g_pid_phc2s, 9)
		invoker.waitpid(g_pid_phc2s, nil)
		g_pid_phc2s = nil
	end

	local pid = invoker.invoke(invoker.NOWAIT + invoker.NOSTDIO,
		"phc2sys", "-q", "-s", netdev, "-m", "-w")
	if type(pid) ~= "number" then
		io.stderr:write("Error, failed to run phc2sys.\n")
		io.stderr:flush()
		return nil
	end
	g_pid_phc2s = pid
	return true
end

local function pid_running(pid)
	if type(pid) ~= "number" or pid <= 0 then
		return false
	end
	local exited, eval = invoker.waitpid(pid, true)
	if exited == nil or exited then
		if type(eval) ~= "number" then eval = 2021 end
		io.stderr:write(gfmt("Warning, PID %d terminated with %d\n", pid, eval))
		io.stderr:flush()
		return false
	end
	return true
end

local function ptp_loop(ptpdev)
	io.stdout:write("Entering PTP loop...\n")
	io.stdout:flush()
	if g_pid_phc2s then
		invoker.kill(g_pid_phc2s, 9)
		invoker.waitpid(g_pid_phc2s, nil)
		g_pid_phc2s = nil
	end

	start_ptp4l(ptpdev)
	if not g_fd_ptp4l then
		invoker.waitsec(10)
		return false
	end

	local maxd = nil
	local run, itercnt = true, 0
	while run do
		invoker.waitsec(5)
		maxd = read_ptp4l(g_fd_ptp4l)
		if maxd and maxd <= 120 then
			if not g_pid_phc2s then
				start_phc2sys(ptpdev)
			end
			itercnt = itercnt + 1
			if bit32.band(itercnt, 0x3f) == 1 then
				invoker.invoke(0, g_busybox, "hwclock", "-w")
			end
		end
		if g_pid_phc2s and not pid_running(g_pid_phc2s) then
			g_pid_phc2s = nil
		end
		run = pid_running(g_pid_ptp4l)
	end
	return false
end

local function start_ntpd(isntp)
	if g_pid_ntpd then
		invoker.kill(g_pid_ntpd, 9)
		invoker.waitpid(g_pid_ntpd, nil)
		g_pid_ntpd = nil
	end

	local ifags = invoker.NOWAIT
	local cmds = { g_busybox, "ntpd", "-n", "-N", "-l", }
	if isntp then
		ifags = invoker.NOFORK
		cmds[#cmds + 1] = "-S"
		cmds[#cmds + 1] = "/usr/bin/lnxntpd_sync"

		local env1 = os.getenv("NTP_SERVER1")
		if env1 and #env1 > 0 then
			cmds[#cmds + 1] = "-p"
			cmds[#cmds + 1] = env1
		end

		env1 = os.getenv("NTP_SERVER2")
		if env1 and #env1 > 0 then
			cmds[#cmds + 1] = "-p"
			cmds[#cmds + 1] = env1
		end

		env1 = os.getenv("NTP_SERVER3")
		if env1 and #env1 > 0 then
			cmds[#cmds + 1] = "-p"
			cmds[#cmds + 1] = env1
		end

		env1 = os.getenv("NTP_SERVER4")
		if env1 and #env1 > 0 then
			cmds[#cmds + 1] = "-p"
			cmds[#cmds + 1] = env1
		end
	end

	local pid = invoker.invoke(ifags, cmds)
	if pid then
		g_pid_ntpd = pid
		return true
	end
	io.stderr:write("Error, failed to start ntpd server.\n")
	io.stderr:flush()
	return false
end

local function check_ptp(ndev)
	if type(ndev) ~= "string" or #ndev == 0 then return false end
	if invoker.invoke(invoker.NOSTDIO, "ip", "link", "show", "dev", ndev) ~= 0 then
		return false
	end

	local okay, output = invoker.invoke(invoker.OUTPUT, "ethtool", "-T", ndev)
	if okay ~= 0 or type(output) ~= "string" then
		io.stderr:write(gfmt("Error, ethtool has failed for %s\n", ndev))
		io.stderr:flush()
		return false
	end

	okay = false
	if string.find(output, "HWTSTAMP_TX_OFF", 1, true) or string.find(output, "HWTSTAMP_TX_ON", 1, true) then
		okay = true
	end
	if string.find(output, "Hardware Transmit Timestamp Modes:\n", 1, true) then
		okay = true
	end
	if string.find(output, "Hardware Transmit Timestamp Modes: none", 1, true) then
		okay = false
	end

	if not okay then
		io.stderr:write(gfmt("Error, network device does not support PTP: %s\n", ndev))
		io.stderr:flush()
		return false
	end
	return true
end

local function ntpdrtc_init()
	invoker.setenv("LANG", nil)
	invoker.setenv("PTPJUMPEXIT", "1")
	invoker.setenv("PATH", "/opt/lnxall_app/bin:/usr/local/bin:/usr/bin:/usr/sbin:/bin:/sbin")
	if not invoker.statfile(g_busybox) then
		local b0, b1 = "/bin/busybox", "/usr/bin/busybox"
		if invoker.statfile(b0) then
			g_busybox = b0
		elseif invoker.statfile(b1) then
			g_busybox = b1
		else
            io.stderr:write("Error, no valid busybox executable found.\n")
			io.stderr:flush()
			return false
		end
	end
	systemd_disable("ntp")
	systemd_disable("chronyd")
	systemd_disable("systemd-timesyncd")

	local sysntpd = "/etc/init.d/sysntpd"
	if invoker.statfile(sysntpd) then
		invoker.invoke(invoker.NOSTDIO, sysntpd, "stop")
		invoker.invoke(invoker.NOSTDIO, sysntpd, "disable")
	end
	if invoker.invoke(invoker.NOSTDIO, "timedatectl", "show") == 0 then
		invoker.invoke(invoker.NOSTDIO, "timedatectl", "set-ntp", "false")
	end
	return true
end

local function mainfunc()
	io.stdout:write("Updating system time from RTC device...\n")
	io.stdout:flush()
	local okay, output = invoker.invoke(invoker.OUTPUT, g_busybox, "hwclock", "-r")
	if okay == 0 and type(output) == "string" and #output > 0 then
		invoker.invoke(0, g_busybox, "hwclock", "-s")
	end

    io.stdout:write("Starting busybox based NTP Server...\n")
	io.stdout:flush()
	local ptpdev = os.getenv("PTP_DEVICE")
	if check_ptp(ptpdev) then
		invoker.setname("PTPRTC")
		start_ntpd(false)
		while true do ptp_loop(ptpdev) end
	else
		start_ntpd(true)
	end
end

if not ntpdrtc_init() then
	invoker.waitsec(15)
	os.exit(1)
end
mainfunc()
os.exit(2)
