From b0b6317b87dac1f1c5eaa280c88828bac7c25463 Mon Sep 17 00:00:00 2001 From: Ben Date: Sat, 2 Sep 2023 17:27:32 +0000 Subject: [PATCH] first commit --- Makefile | 18 ++ README.md | 0 htdocs/luci-static/resources/tasks/tasks.css | 112 +++++++++ htdocs/luci-static/resources/tasks/tasks.js | 232 +++++++++++++++++++ luasrc/controller/tasks-lib.lua | 92 ++++++++ luasrc/model/tasks.lua | 100 ++++++++ luasrc/view/tasks/docker.htm | 56 +++++ luasrc/view/tasks/embed.htm | 34 +++ src/Makefile | 15 ++ src/dummy/package.mk | 2 + src/po/zh-cn/lib-tasks.po | 41 ++++ 11 files changed, 702 insertions(+) create mode 100644 Makefile create mode 100644 README.md create mode 100644 htdocs/luci-static/resources/tasks/tasks.css create mode 100644 htdocs/luci-static/resources/tasks/tasks.js create mode 100644 luasrc/controller/tasks-lib.lua create mode 100644 luasrc/model/tasks.lua create mode 100644 luasrc/view/tasks/docker.htm create mode 100644 luasrc/view/tasks/embed.htm create mode 100644 src/Makefile create mode 100644 src/dummy/package.mk create mode 100644 src/po/zh-cn/lib-tasks.po diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..843fcd9 --- /dev/null +++ b/Makefile @@ -0,0 +1,18 @@ +# +# Copyright (C) 2022 jjm2473 +# +# This is free software, licensed under the MIT License. +# + +include $(TOPDIR)/rules.mk + +LUCI_TITLE:=Task library +LUCI_DEPENDS:=+luci-lib-xterm +taskd +LUCI_EXTRA_DEPENDS:=taskd (>=1.0.3-1) +LUCI_PKGARCH:=all + +PKG_MAINTAINER:=jjm2473 + +include $(TOPDIR)/feeds/luci/luci.mk + +# call BuildPackage - OpenWrt buildroot signature diff --git a/README.md b/README.md new file mode 100644 index 0000000..e69de29 diff --git a/htdocs/luci-static/resources/tasks/tasks.css b/htdocs/luci-static/resources/tasks/tasks.css new file mode 100644 index 0000000..cf9cf05 --- /dev/null +++ b/htdocs/luci-static/resources/tasks/tasks.css @@ -0,0 +1,112 @@ +[hidden] { + display: none !important; +} + +#tasks_detail_container { + position: fixed; + z-index: 1000; + top: 0; + left: 0; + width: 100%; + height: 100%; + overflow: hidden; + background-color: #0008; +} +#tasks_dialog { + position: absolute; + width: 770px; + max-width: 100%; + max-height: 100%; + left: 50%; + top: 50%; + transform: translate(-50%, -50%); + background-color: #000; + + border-radius: 10px; + box-shadow: 2px 2px 6px #000a; + padding: 20px; + + color: white; +} +.dialog-title-bar { + margin-top: -10px; + margin-right: -10px; + margin-bottom: 5px; + display: flex; + flex-direction: row; + justify-content: space-between; + flex-wrap: nowrap; + align-items: center; +} +.dialog-title-bar .dialog-title { + word-break: break-all; + text-overflow: ellipsis; + white-space: nowrap; + overflow: hidden; +} +.dialog-content { + max-height: 500px; + overflow-y: scroll; + margin-right: -10px; +} +.dialog-icons { + align-self: center; + display: flex; + justify-content: flex-end; +} +.dialog-icon { + display: flex; + align-items: center; + flex-wrap: nowrap; + + width: 20px; + height: 20px; + background-color: white; + color: black; + border-radius: 50%; + font-size: 10px; + font-weight: bold; + user-select: none; + margin-left: 10px; + line-height: 1; + font-family: sans-serif; + justify-content: center; + cursor: pointer; +} +.dialog-icon.dialog-icon-min { + background-color: darkorange; +} +.dialog-icon.dialog-icon-close { + background-color: #ff5f56; +} +.dialog-icons:hover .dialog-icon.dialog-icon-min:before { + content: "_"; +} +.dialog-icons:hover .dialog-icon.dialog-icon-close:before { + content: "X"; +} + +.tasks_stopped .dialog-icon.dialog-icon-close { + background-color: #27c840; +} +.tasks_stopped #tasks_dialog, .tasks_unknown #tasks_dialog { + padding: 19px; + border: 1px #27c840 solid; + + animation: border-blink 1s; + animation-iteration-count: infinite; +} + +.tasks_failed #tasks_dialog { + border-color: #ff0000; +} + +.tasks_failed .dialog-icon.dialog-icon-close { + background-color: #ff0000; +} + +.tasks_unknown #tasks_dialog { + border-color: darkorange; +} + +@keyframes border-blink { 50% { border-color:#fff ; } } diff --git a/htdocs/luci-static/resources/tasks/tasks.js b/htdocs/luci-static/resources/tasks/tasks.js new file mode 100644 index 0000000..648652f --- /dev/null +++ b/htdocs/luci-static/resources/tasks/tasks.js @@ -0,0 +1,232 @@ + +(function(){ + const taskd={}; + const $gettext = function(str) { + return taskd.i18n[str] || str; + }; + const retryPromise = function(fn) { + return new Promise((resolve, reject) => { + const retry = function() { + fn(resolve, reject, retry); + }; + retry(); + }); + }; + const retry403XHR = function(url, method, responseType) { + return retryPromise((resolve, reject, retry) => { + var oReq = new XMLHttpRequest(); + oReq.onerror = reject; + oReq.open(method || 'GET', url, true); + if (responseType) { + oReq.responseType = responseType; + } + oReq.onload = function (oEvent) { + if (oReq.status == 403) { + alert($gettext("Lost login status")); + location.href = location.href; + } else if (oReq.status == 404) { + reject(oEvent); + } else { + resolve(oReq); + } + }; + if (method=='POST') { + oReq.setRequestHeader("Content-type", "application/x-www-form-urlencoded"); + } + oReq.send(method=='POST'?("token="+taskd.csrfToken):null); + }); + }; + const request = function(url, method) { + return retry403XHR(url, method).then(oReq => oReq.responseText); + }; + const getBin = function(url) { + return retry403XHR(url, null, "arraybuffer").then(oReq => {return {status: oReq.status, buffer: new Uint8Array(oReq.response)}}); + }; + const getTaskDetail = function(task_id) { + return request("/cgi-bin/luci/admin/system/tasks/status?task_id="+task_id).then(data=>JSON.parse(data)); + }; + const create_dialog = function(cfg) { + const container = document.createElement('div'); + container.id = "tasks_detail_container"; + container.innerHTML = taskd.dialog_template; + + document.body.appendChild(container); + const title_view = container.querySelector(".dialog-title-bar .dialog-title"); + title_view.innerText = cfg.title; + + const term = new Terminal({convertEol: cfg.convertEol||false}); + if (cfg.nohide) { + container.querySelector(".dialog-icon-min").hidden = true; + } else { + container.querySelector(".dialog-icon-min").onclick = function(){ + container.hidden=true; + term.dispose(); + document.body.removeChild(container); + cfg.onhide && cfg.onhide(); + return false; + }; + } + term.open(document.getElementById("tasks_xterm_log")); + + return {term,container}; + }; + const show_log_txt = function(title, content, onclose) { + const dialog = create_dialog({title, convertEol:true, onhine:onclose}); + const container = dialog.container; + const term = dialog.term; + container.querySelector(".dialog-icon-close").hidden = true; + term.write(content); + }; + const show_log = function(task_id, nohide) { + let showing = true; + let running = true; + const dialog = create_dialog({title:task_id, nohide, onhide:function(){showing=false;}}); + const container = dialog.container; + const term = dialog.term; + + const title_view = container.querySelector(".dialog-title-bar .dialog-title"); + container.querySelector(".dialog-icon-close").onclick = function(){ + if (!running || confirm($gettext("Stop running task?"))) { + running=false; + showing=false; + del_task(task_id).then(()=>{ + location.href = location.href; + }); + } + return false; + }; + const checkTask = function() { + return getTaskDetail(task_id).then(data=>{ + if (!running) { + return false; + } + running = data.running; + let title = task_id; + if (!data.running && data.stop) { + title += " (" + (data.exit_code?$gettext("Failed at:"):$gettext("Finished at:")) + " " + new Date(data.stop * 1000).toLocaleString() + ")"; + } + title += " > " + (data.command || ''); + title_view.title = title; + title_view.innerText = title; + if (!data.running) { + container.classList.add('tasks_stopped'); + if (data.exit_code) { + container.classList.add('tasks_failed'); + } + } + // last pull + return showing; + }); + }; + let logoffset = 0; + const pulllog = function(check) { + let starter = Promise.resolve(showing); + if (check) { + starter = checkTask(); + } + starter.then(again => { + if (again) + return getBin("/cgi-bin/luci/admin/system/tasks/log?task_id="+task_id+"&offset="+logoffset); + else + return {status: 204}; + }).then(function(res){ + if (!showing) { + return false; + } + switch(res.status){ + case 205: + term.reset(); + logoffset = 0; + return running; + break; + case 204: + return running && checkTask(); + break; + case 200: + logoffset += res.buffer.byteLength; + term.write(res.buffer); + return running; + break; + } + }).then(again => { + if (again) { + setTimeout(pulllog, 0); + } + }).catch(err => { + if (showing) { + if (err.target.status == 0) { + title_view.innerText = task_id + ' (' + $gettext("Fetch log failed, retrying...") + ')'; + setTimeout(()=>pulllog(true), 1000); + } else if (err.target.status == 403 || err.target.status == 404) { + title_view.innerText = task_id + ' (' + $gettext(err.target.status == 403?"Lost login status":"Task does not exist or has been deleted") + ')'; + container.querySelector(".dialog-icon-close").hidden = true; + container.classList.add('tasks_unknown'); + } else { + console.error(err); + } + } + }); + }; + pulllog(true); + }; + const del_task = function(task_id) { + return request("/cgi-bin/luci/admin/system/tasks/stop?task_id="+task_id, "POST"); + }; + taskd.show_log = show_log; + taskd.remove = del_task; + taskd.show_log_txt = show_log_txt; + window.taskd=taskd; +})(); + +(function(){ + // compat + if (typeof(window.findParent) !== 'function') { + const elem = function(e) { + return (e != null && typeof(e) == 'object' && 'nodeType' in e); + }; + const matches = function(node, selector) { + var m = elem(node) ? node.matches || node.msMatchesSelector : null; + return m ? m.call(node, selector) : false; + }; + window.findParent = function (node, selector) { + if (elem(node) && node.closest) + return node.closest(selector); + + while (elem(node)) + if (matches(node, selector)) + return node; + else + node = node.parentNode; + + return null; + }; + } + if (typeof(window.cbi_submit) !== 'function') { + const makeHidden = function(name) { + const input = document.createElement('input'); + input.type = 'hidden'; + input.name = name; + return input; + }; + window.cbi_submit = function(elem, name, value, action) { + var form = elem.form || findParent(elem, 'form'); + + if (!form) + return false; + + if (action) + form.action = action; + + if (name) { + var hidden = form.querySelector('input[type="hidden"][name="%s"]'.format(name)) || + makeHidden(name); + + hidden.value = value || '1'; + form.appendChild(hidden); + } + + form.submit(); + return true; + }; + } +})(); \ No newline at end of file diff --git a/luasrc/controller/tasks-lib.lua b/luasrc/controller/tasks-lib.lua new file mode 100644 index 0000000..1aaa6eb --- /dev/null +++ b/luasrc/controller/tasks-lib.lua @@ -0,0 +1,92 @@ + +module("luci.controller.tasks-lib", package.seeall) + + +function index() + entry({"admin", "system", "tasks", "status"}, call("tasks_status")).dependent=false + entry({"admin", "system", "tasks", "log"}, call("tasks_log")).dependent=false + entry({"admin", "system", "tasks", "stop"}, post("tasks_stop")).dependent=false +end + +local util = require "luci.util" +local jsonc = require "luci.jsonc" +local ltn12 = require "luci.ltn12" + +local taskd = require "luci.model.tasks" + +function tasks_status() + local data = taskd.status(luci.http.formvalue("task_id")) + luci.http.prepare_content("application/json") + luci.http.write_json(data) +end + +function tasks_log() + local wait = 107 + local task_id = luci.http.formvalue("task_id") + local offset = luci.http.formvalue("offset") + offset = offset and tonumber(offset) or 0 + local logpath = "/var/log/tasks/"..task_id..".log" + local i + local logfd = io.open(logpath, "rb") + if logfd == nil then + luci.http.status(404) + luci.http.write("log not found") + return + end + + local size = logfd:seek("end") + + if size < offset then + luci.http.status(205, "Reset Content") + luci.http.write("reset offset") + return + end + + i = 0 + while (i < wait) + do + if size > offset then + break + end + nixio.nanosleep(0, 10000000) -- sleep 10ms + size = logfd:seek("end") + i = i+1 + end + if i == wait then + logfd:close() + luci.http.status(204) + luci.http.prepare_content("application/octet-stream") + return + end + logfd:seek("set", offset) + + local write_log = function() + local buffer = logfd:read(4096) + if buffer and #buffer > 0 then + return buffer + else + logfd:close() + return nil + end + end + + luci.http.prepare_content("application/octet-stream") + + if logfd then + ltn12.pump.all(write_log, luci.http.write) + end +end + +function tasks_stop() + local sys = require("luci.sys") + local task_id = luci.http.formvalue("task_id") or "" + if task_id == "" then + luci.http.status(400) + luci.http.write("task_id is empty") + return + end + if sys.call("/etc/init.d/tasks task_del "..task_id.." >/dev/null 2>&1") ~= 0 then + nixio.nanosleep(2, 10000000) + end + luci.http.status(204) +end diff --git a/luasrc/model/tasks.lua b/luasrc/model/tasks.lua new file mode 100644 index 0000000..07aff98 --- /dev/null +++ b/luasrc/model/tasks.lua @@ -0,0 +1,100 @@ +local util = require "luci.util" +local jsonc = require "luci.jsonc" + +local taskd = {} + +local function output(data) + local ret={} + ret.running=data.running + if not data.running then + ret.exit_code=data.exit_code + if nil == ret.exit_code then + if data["data"] and data["data"]["exit_code"] and data["data"]["exit_code"] ~= "" then + ret.exit_code=tonumber(data["data"]["exit_code"]) + else + ret.exit_code=143 + end + end + end + ret.command=data["command"] and data["command"][4] or '#' + if data["data"] then + ret.start=tonumber(data["data"]["start"]) + if not data.running and data["data"]["stop"] then + ret.stop=tonumber(data["data"]["stop"]) + end + end + return ret +end + +taskd.status = function (task_id) + task_id = task_id or "" + local data = util.trim(util.exec("/etc/init.d/tasks task_status "..task_id.." 2>/dev/null")) or "" + if data ~= "" then + data = jsonc.parse(data) + else + if task_id == "" then + data = {} + else + data = {running=false, exit_code=404} + end + end + if task_id ~= "" then + return output(data) + end + local ary={} + for k, v in pairs(data) do + ary[k] = output(v) + end + return ary +end + +taskd.docker_map = function(config, task_id, script_path, title, desc) + require("luci.cbi") + require("luci.http") + require("luci.sys") + local translate = require("luci.i18n").translate + local m + m = luci.cbi.Map(config, title, desc) + m.template = "tasks/docker" + -- hide default buttons + m.pageaction = false + -- we want hook 'on_after_apply' works, 'apply_on_parse' can be true (rollback) or false (no rollback), + -- but 'apply_on_parse' must be true for luci 17.01 and below + m.apply_on_parse = true + m.script_path = script_path + m.task_id = task_id + m.auto_show_task = true + m.on_before_apply = function(self) + if self.uci.rollback then + -- luci 18.06+ has 'rollback' function + -- rollback dialog will show because 'apply_on_parse' is true, + -- hide rollback dialog by hook 'apply' function + local apply = self.uci.apply + self.uci.apply = function(uci, rollback) + apply(uci, false) + end + end + end + m.on_after_apply = function(self) + local cmd + local action = luci.http.formvalue("cbi.apply") or "null" + if "upgrade" == action or "install" == action + or "start" == action or "stop" == action or "restart" == action or "rm" == action then + cmd = string.format("\"%s\" %s", script_path, action) + end + if cmd then + if luci.sys.call("/etc/init.d/tasks task_add " .. task_id .. " " .. luci.util.shellquote(cmd) .. " >/dev/null 2>&1") ~= 0 then + self.task_start_failed = true + self.message = translate("Config saved, but apply failed") + end + else + self.message = translate("Unknown command: ") .. action + end + if self.message then + self.auto_show_task = false + end + end + return m +end + +return taskd diff --git a/luasrc/view/tasks/docker.htm b/luasrc/view/tasks/docker.htm new file mode 100644 index 0000000..66ce0e6 --- /dev/null +++ b/luasrc/view/tasks/docker.htm @@ -0,0 +1,56 @@ + +<% if self.task_start_failed then %> +
<%:Another task running, try again later.%> <%:Click here to check running task%>
+<% end %> + +<%+cbi/map%> +<% +local task_running = false +local taskd = require "luci.model.tasks" +local status = taskd.status(self.task_id) +task_running = status.running +-%> +
+<% +if not task_running then +%> + <% + local util = require "luci.util" + local container_status = util.trim(util.exec(self.script_path.." status")) + local container_install = (string.len(container_status) > 0) + local container_running = container_status == "running" + if container_install then + -%> + + <% + if container_running then + -%> + + + + <% else %> + + + + <% end + else %> + + <% end +else + %> + +<% +end +%> +
+ +<%+tasks/embed%> +<% +if self.auto_show_task and task_running then +-%> + +<% +end +%> diff --git a/luasrc/view/tasks/embed.htm b/luasrc/view/tasks/embed.htm new file mode 100644 index 0000000..2f3dca0 --- /dev/null +++ b/luasrc/view/tasks/embed.htm @@ -0,0 +1,34 @@ +<%+xterm/embed%> + + +<% + local i18n = {} + local function tr(str) + i18n[str]=translate(str) + end + tr("Stop running task?") + tr("Stopped at:") + tr("Finished at:") + tr("Failed at:") + tr("Lost login status") + tr("Fetch log failed, retrying...") + tr("Task does not exist or has been deleted") +-%> + diff --git a/src/Makefile b/src/Makefile new file mode 100644 index 0000000..084ef06 --- /dev/null +++ b/src/Makefile @@ -0,0 +1,15 @@ +clean: +compile: + +include $(TOPDIR)/rules.mk + +LUCI_NAME:=luci-lib-dummy +INCLUDE_DIR:=./dummy + +include $(TOPDIR)/feeds/luci/luci.mk + +install: + mkdir -p "$(DESTDIR)$(LUCI_LIBRARYDIR)/i18n" + $(foreach lang,$(LUCI_LANGUAGES),$(foreach po,$(wildcard ${CURDIR}/po/$(lang)/*.po), \ + po2lmo $(po) \ + $(DESTDIR)$(LUCI_LIBRARYDIR)/i18n/$(basename $(notdir $(po))).$(lang).lmo;)) diff --git a/src/dummy/package.mk b/src/dummy/package.mk new file mode 100644 index 0000000..da8dd1f --- /dev/null +++ b/src/dummy/package.mk @@ -0,0 +1,2 @@ +define BuildPackage +endef \ No newline at end of file diff --git a/src/po/zh-cn/lib-tasks.po b/src/po/zh-cn/lib-tasks.po new file mode 100644 index 0000000..b1aafa7 --- /dev/null +++ b/src/po/zh-cn/lib-tasks.po @@ -0,0 +1,41 @@ +msgid "" +msgstr "Content-Type: text/plain; charset=UTF-8" + +msgid "Stop running task?" +msgstr "删除运行中的任务?" + +msgid "Finished at:" +msgstr "完成于:" + +msgid "Failed at:" +msgstr "失败于:" + +msgid "Lost login status" +msgstr "丢失登陆状态" + +msgid "Fetch log failed, retrying..." +msgstr "拉取日志失败,正在重试..." + +msgid "Task does not exist or has been deleted" +msgstr "任务不存在或已删除" + +msgid "Stop and Remove" +msgstr "停止并删除" + +msgid "Hide" +msgstr "隐藏" + +msgid "Config saved, but apply failed" +msgstr "配置已保存,但应用失败" + +msgid "Unknown command: " +msgstr "未知命令:" + +msgid "Another task running, try again later." +msgstr "已有后台任务运行中,请稍后重试。" + +msgid "Click here to check running task" +msgstr "点此查看运行中的任务" + +msgid "Task Running" +msgstr "任务执行中"