first commit

main
Ben 1 year ago
commit b0b6317b87

@ -0,0 +1,18 @@
#
# Copyright (C) 2022 jjm2473 <jjm2473@gmail.com>
#
# 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 <jjm2473@gmail.com>
include $(TOPDIR)/feeds/luci/luci.mk
# call BuildPackage - OpenWrt buildroot signature

@ -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 ; } }

@ -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;
};
}
})();

@ -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

@ -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

@ -0,0 +1,56 @@
<% if self.task_start_failed then %>
<div class="alert-message warning"><%:Another task running, try again later.%> <a href="javascript:void(taskd.show_log('<%=self.task_id%>'))"><%:Click here to check running task%></a></div>
<% end %>
<%+cbi/map%>
<%
local task_running = false
local taskd = require "luci.model.tasks"
local status = taskd.status(self.task_id)
task_running = status.running
-%>
<div class="cbi-page-actions control-group">
<%
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
-%>
<input class="btn cbi-button cbi-button-apply" type="button" value="<%:Upgrade%>/<%:Apply%>" onclick="cbi_submit(this, 'cbi.apply', 'upgrade')" />
<%
if container_running then
-%>
<input class="btn cbi-button cbi-button-remove" type="button" value="<%:Stop%>" onclick="cbi_submit(this, 'cbi.apply', 'stop')" />
<input class="btn cbi-button cbi-button-reload" type="button" value="<%:Restart%>" onclick="cbi_submit(this, 'cbi.apply', 'restart')" />
<% else %>
<input class="btn cbi-button cbi-button-apply" type="button" value="<%:Start%>" onclick="cbi_submit(this, 'cbi.apply', 'start')" />
<input class="btn cbi-button cbi-button-remove" type="button" value="<%:Remove%>" onclick="cbi_submit(this, 'cbi.apply', 'rm')" />
<% end
else %>
<input class="btn cbi-button cbi-button-apply" type="button" value="<%:Install%>" onclick="cbi_submit(this, 'cbi.apply', 'install')" />
<% end
else
%>
<input class="btn cbi-button cbi-button-apply" type="button" value="<%:Task Running%>&hellip;" onclick="taskd.show_log('<%=self.task_id%>')" />
<%
end
%>
</div>
<%+tasks/embed%>
<%
if self.auto_show_task and task_running then
-%>
<script>
taskd.show_log("<%=self.task_id%>");
</script>
<%
end
%>

@ -0,0 +1,34 @@
<%+xterm/embed%>
<link rel="stylesheet" href="<%=resource%>/tasks/tasks.css<%# ?v=PKG_VERSION %>">
<script src="<%=resource%>/tasks/tasks.js<%# ?v=PKG_VERSION %>"></script>
<%
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")
-%>
<script>
window.taskd.csrfToken="<%=token%>";
window.taskd.i18n=<% luci.http.write_json(i18n) %>;
window.taskd.dialog_template=`
<div id="tasks_dialog">
<div class="dialog-title-bar">
<span class="dialog-title" id="tasks_id"></span>
<span class="dialog-icons">
<span class="dialog-icon dialog-icon-close" title="<%:Stop and Remove%>"></span>
<span class="dialog-icon dialog-icon-min" title="<%:Hide%>"></span>
</span>
</div>
<div class="dialog-content">
<div id="tasks_xterm_log"></div>
</div>
</div>
`;
</script>

@ -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;))

@ -0,0 +1,2 @@
define BuildPackage
endef

@ -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 "任务执行中"
Loading…
Cancel
Save