-- Created by hongbo.he@lnxall.com -- Progress Bar Status Control -- -- 2026-05-09 -- local invoker = require "invoker" local cjson = require "cjson" local PROGRESS_PIPE = '/run/splash/progress' local APPLIST_FILE = '/etc/splash.d/applist.json' local POLL_INTERVAL_MS = 200 local MAX_PROGRESS_BEFORE_READY = 99 local function is_required(v) return v == nil or v == true or v == 'true' or v == 1 or v == '1' end local function normalize_app(app) local name = app and app.name local weight = tonumber(app and app.weight) or 1 if type(name) ~= 'string' or name == '' then return nil end if weight <= 0 then weight = 1 end if not is_required(app.required) then return nil end return { name = name, weight = weight, label = app.label, timeout = tonumber(app.timeout), } end local function load_applist() local content = invoker.readfile(APPLIST_FILE) if not content then return nil, string.format("failed to open %s", APPLIST_FILE) end local ok, config = pcall(cjson.decode, content) if not ok or type(config) ~= 'table' or type(config.applist) ~= 'table' then return nil, string.format("invalid applist json: %s", tostring(config)) end local loaded = {} for _, app in ipairs(config.applist) do local normalized = normalize_app(app) if normalized then loaded[#loaded + 1] = normalized end end if #loaded == 0 then return nil, "empty applist" end return loaded end local function total_weight(apps) local total = 0 for _, app in ipairs(apps) do total = total + app.weight end return total end local function app_is_running(app) local _, output = invoker.invoke(invoker.OUTPUT, '/usr/local/bin/busybox', 'pidof', app, '2>/dev/null') return output ~= nil and output ~= '' end local function emu_is_ready() local _, output = invoker.invoke(invoker.OUTPUT, 'wget', '-q', '-O', '-', '-T', 2, 'http://127.0.0.1:80', '2>/dev/null') return output ~= nil and output ~= '' end local function open_progress_pipe() local fp, err = io.open(PROGRESS_PIPE, 'w+') if not fp then return nil, string.format('failed to open %s: %s', PROGRESS_PIPE, tostring(err)) end return fp end local function sanitize_message(message) if type(message) ~= 'string' or message == '' then return '' end message = message:gsub('[\r\n]', ' ') message = message:gsub('^%s+', ''):gsub('%s+$', '') return message end local function app_label(app) if type(app.label) == 'string' and app.label ~= '' then return app.label end return app.name end local function write_progress(pipe, percent, status, message) local payload = { percent = percent, } if status and message then payload.status = status payload.message = sanitize_message(message) end pipe:write(cjson.encode(payload)) pipe:write('\n') pipe:flush() end local function is_finished(state, app) return state.started[app.name] or state.failed[app.name] end local function first_pending_app(apps, state) for _, app in ipairs(apps) do if not is_finished(state, app) then return app end end return nil end local function progress_from_weight(weight, total) return math.floor(weight * MAX_PROGRESS_BEFORE_READY / total) end local function refresh_percent(state) state.percent = progress_from_weight(state.finished_weight, state.total_weight) end local function mark_started(pipe, state, app) state.started[app.name] = true state.finished_weight = state.finished_weight + app.weight refresh_percent(state) write_progress(pipe, state.percent, 'OK', string.format('%s started', app_label(app))) end local function mark_failed(pipe, state, app) state.failed[app.name] = true state.has_failure = true state.finished_weight = state.finished_weight + app.weight state.waiting_name = nil state.waiting_since = nil refresh_percent(state) write_progress(pipe, state.percent, 'FAILED', string.format('%s failed to start', app_label(app))) end local function scan_started_apps(pipe, apps, state) for _, app in ipairs(apps) do if not is_finished(state, app) and app_is_running(app.name) then mark_started(pipe, state, app) end end end local function track_pending_timeout(pipe, apps, state) local waiting = first_pending_app(apps, state) if not waiting then return end if waiting.name ~= state.waiting_name then state.waiting_name = waiting.name state.waiting_since = os.time() end if not waiting.timeout or waiting.timeout <= 0 then return end if os.difftime(os.time(), state.waiting_since) < waiting.timeout then return end mark_failed(pipe, state, waiting) end local function can_finish(state) return not state.has_failure and state.percent >= MAX_PROGRESS_BEFORE_READY and emu_is_ready() end local function new_state(apps) return { started = {}, failed = {}, finished_weight = 0, total_weight = total_weight(apps), percent = 0, has_failure = false, waiting_name = nil, waiting_since = nil, } end local function main() local apps, err = load_applist() local pipe local state if not apps then io.stderr:write(string.format('Error, %s\n', tostring(err))) io.stderr:flush() return false end pipe, err = open_progress_pipe() if not pipe then io.stderr:write(string.format('Error, %s\n', tostring(err))) io.stderr:flush() return false end state = new_state(apps) while state.percent < 100 do scan_started_apps(pipe, apps, state) track_pending_timeout(pipe, apps, state) write_progress(pipe, state.percent) if can_finish(state) then state.percent = 100 write_progress(pipe, state.percent, 'OK', 'Startup complete') break end invoker.msleep(POLL_INTERVAL_MS) end pipe:close() return true end main() os.exit(0)