package: add luci-app-ota

This commit is contained in:
jjm2473 2022-11-11 18:45:38 +08:00
parent 14be089e18
commit 228dc67b0e
8 changed files with 819 additions and 0 deletions

View File

@ -0,0 +1,12 @@
include $(TOPDIR)/rules.mk
LUCI_TITLE:=LuCI for OTA upgrade
LUCI_PKGARCH:=all
PKG_VERSION:=1.2.2
PKG_RELEASE:=1
PKG_MAINTAINER:=jjm2473 <jjm2473@gmail.com>
include $(TOPDIR)/feeds/luci/luci.mk
# call BuildPackage - OpenWrt buildroot signature

View File

@ -0,0 +1,176 @@
--[[
LuCI - Lua Configuration Interface
Copyright 2021 jjm2473
]]--
module("luci.controller.admin.ota",package.seeall)
require "luci.util"
function index()
entry({"admin", "system", "ota"}, post_on({ apply = "1" }, "action_ota"), _("OTA"), 69)
entry({"admin", "system", "ota", "check"}, post("action_check"))
entry({"admin", "system", "ota", "download"}, post("action_download"))
entry({"admin", "system", "ota", "progress"}, call("action_progress"))
entry({"admin", "system", "ota", "cancel"}, post("action_cancel"))
end
local function ota_exec(cmd)
local nixio = require "nixio"
local os = require "os"
local fs = require "nixio.fs"
local rshift = nixio.bit.rshift
local oflags = nixio.open_flags("wronly", "creat")
local lock, code, msg = nixio.open("/var/lock/ota_api.lock", oflags)
if not lock then
return 255, "", "Open stdio lock failed: " .. msg
end
-- Acquire lock
local stat, code, msg = lock:lock("tlock")
if not stat then
lock:close()
return 255, "", "Lock stdio failed: " .. msg
end
local r = os.execute(cmd .. " >/var/log/ota.stdout 2>/var/log/ota.stderr")
local e = fs.readfile("/var/log/ota.stderr")
local o = fs.readfile("/var/log/ota.stdout")
fs.unlink("/var/log/ota.stderr")
fs.unlink("/var/log/ota.stdout")
lock:lock("ulock")
lock:close()
e = e or ""
if r == 256 and e == "" then
e = "os.execute failed, is /var/log full or not existed?"
end
return rshift(r, 8), o or "", e or ""
end
local function image_supported(image)
return (os.execute("sysupgrade -T %q >/dev/null" % image) == 0)
end
function action_ota()
local image_tmp = "/tmp/firmware.img"
local http = require "luci.http"
if http.formvalue("apply") == "1" then
if not luci.dispatcher.test_post_security() then
return
end
if not image_supported(image_tmp) then
luci.template.render("admin_system/ota", {image_invalid = true})
return
end
local keep = (http.formvalue("keep") == "1") and "" or "-n"
luci.template.render("admin_system/ota_flashing", {
title = luci.i18n.translate("Flashing…"),
msg = luci.i18n.translate("The system is flashing now.<br /> DO NOT POWER OFF THE DEVICE!<br /> Wait a few minutes before you try to reconnect. It might be necessary to renew the address of your computer to reach the device again, depending on your settings."),
addr = (#keep > 0) and "192.168.1.1" or nil
})
fork_exec("sleep 1; killall dropbear uhttpd; sleep 1; /sbin/sysupgrade %s %q" %{ keep, image_tmp })
else
luci.template.render("admin_system/ota")
end
end
function action_check()
local r,o,e = ota_exec("ota check")
local ret = {
code = 500,
msg = "Unknown"
}
if r == 0 then
ret.code = 0
ret.msg = o
elseif r == 1 then
ret.code = 1
ret.msg = o
else
ret.code = 500
ret.msg = e
end
luci.http.prepare_content("application/json")
luci.http.write_json(ret)
end
function action_download()
local r,o,e = ota_exec("ota download")
local ret = {
code = 500,
msg = "Unknown"
}
if r == 0 then
ret.code = 0
ret.msg = ""
else
ret.code = 500
ret.msg = e
end
luci.http.prepare_content("application/json")
luci.http.write_json(ret)
end
function action_progress()
local r,o,e = ota_exec("ota progress")
local ret = {
code = 500,
msg = "Unknown"
}
if r == 0 then
ret.code = 0
ret.msg = "done"
elseif r == 1 or r == 2 then
ret.code = r
ret.msg = o
else
ret.code = 500
ret.msg = e
end
luci.http.prepare_content("application/json")
luci.http.write_json(ret)
end
function action_cancel()
local r,o,e = ota_exec("ota cancel")
local ret = {
code = 500,
msg = "Unknown"
}
if r == 0 then
ret.code = 0
ret.msg = "ok"
else
ret.code = 500
ret.msg = e
end
luci.http.prepare_content("application/json")
luci.http.write_json(ret)
end
function fork_exec(command)
local pid = nixio.fork()
if pid > 0 then
return
elseif pid == 0 then
-- change to root dir
nixio.chdir("/")
-- patch stdin, out, err to /dev/null
local null = nixio.open("/dev/null", "w+")
if null then
nixio.dup(null, nixio.stderr)
nixio.dup(null, nixio.stdout)
nixio.dup(null, nixio.stdin)
if null:fileno() > 2 then
null:close()
end
end
-- replace with target command
nixio.exec("/bin/sh", "-c", command)
end
end

View File

@ -0,0 +1,224 @@
<%#
Licensed to the public under the Apache License 2.0.
-%>
<%+header%>
<%
local fs = require "nixio.fs"
-%>
<h2 name="content"><%:OTA%></h2>
<style>
.state-ctl .state {
display: none;
padding: 0 !important;
margin: 0 !important;
border: 0 !important;
}
.state-ctl.state-ctl-unchecked .state.state-unchecked,
.state-ctl.state-ctl-checked .state.state-checked,
.state-ctl.state-ctl-downloading .state.state-downloading,
.state-ctl.state-ctl-downloaded .state.state-downloaded
{
display: inherit;
}
</style>
<fieldset class="cbi-section">
<fieldset class="cbi-section state-ctl state-ctl-unchecked">
<legend><%:Upgrade firmware On the Air%></legend>
<div class="cbi-section-descr"><%:Check and upgrade firmware from the Internet%></div>
<% if image_invalid then %>
<div class="cbi-section-error"><%:The image file does not contain a supported format. Maybe try again later.%></div>
<% end %>
<div class="cbi-section-node">
<div class="state state-unchecked">
<form>
<div class="cbi-value">
<label class="cbi-value-title" id="check_result"></label>
<div class="cbi-value-field">
<input class="cbi-button cbi-button-reload" type="button" name="check" value="<%:Check update%>" />
</div>
</div>
</form>
</div>
<div class="state state-checked">
<form>
<div class="cbi-value">
<label class="cbi-value-title" id="download_result"><%:Found new firmware%></label>
<div class="cbi-value-field">
<input class="cbi-button cbi-button-apply" type="button" name="download" value="<%:Download firmware%>" />
</div>
</div>
</form>
</div>
<div class="state state-downloading">
<form>
<div class="cbi-value">
<label class="cbi-value-title" id="download_progress">0%</label>
<div class="cbi-value-field">
<input class="cbi-button cbi-button-reset" type="button" name="cancel" value="<%:Cancel download%>" />
</div>
</div>
</form>
</div>
<div class="state state-downloaded">
<% if fs.access("/usr/lib/lua/luci/controller/admin/system.lua") then %>
<form method="post" action="<%=url('admin/system/flashops/sysupgrade')%>" enctype="multipart/form-data">
<input type="hidden" name="token" value="<%=token%>" />
<input type="hidden" name="step" value="1" />
<div class="cbi-value">
<label class="cbi-value-title"><%:Firmware downloaded%></label>
<div class="cbi-value-field">
<input type="submit" class="cbi-button cbi-input-apply" value="<%:Flash image...%>" />
</div>
</div>
</form>
<% else %>
<form method="post" action="<%=url('admin/system/ota')%>">
<input type="hidden" name="apply" value="1" />
<input type="hidden" name="token" value="<%=token%>" />
<div class="cbi-value">
<label class="cbi-value-title" for="keep"><%:Keep settings and retain the current configuration%></label>
<div class="cbi-value-field">
<input type="checkbox" name="keep" value="1" id="keep" checked="checked" />
</div>
</div>
<div class="cbi-value cbi-value-last">
<label class="cbi-value-title"><%:Firmware downloaded%></label>
<div class="cbi-value-field">
<input type="submit" class="cbi-button cbi-input-apply" value="<%:Flash image...%>" />
</div>
</div>
</form>
<% end %>
</div>
</div>
<div class="state state-checked state-downloading state-downloaded">
<div class="cbi-section-descr">
<h2><%:The latest firmware%>:</h2>
<div id="upgrade_log"></div>
</div>
</div>
<div class="state state-unchecked">
<div class="cbi-section-descr">
<div id="history_log"></div>
</div>
</div>
</fieldset>
<script>
(function(){
var csrfToken = "<%=token%>";
var check = document.querySelector('input[name="check"]');
var download = document.querySelector('input[name="download"]');
var cancel = document.querySelector('input[name="cancel"]');
var state_ctl = document.querySelector('.state-ctl');
var check_result = document.querySelector('#check_result');
var download_result = document.querySelector('#download_result');
var download_progress = document.querySelector('#download_progress');
var upgrade_log = document.querySelector('#upgrade_log');
var history_log = document.querySelector('#history_log');
var xhr_post = function(url, data, cb) {
data = data || {};
data.token = '<%=token%>';
new XHR().post(url, data, function(x, d){
cb && cb(x, x.status==200?JSON.parse(x.responseText):{code:500,msg:x.responseText});
});
};
var state_switch = function(from, to) {
state_ctl.classList.remove("state-ctl-" + from);
state_ctl.classList.add("state-ctl-" + to);
};
check.onclick = function(){
check.disabled = 'disabled';
check_result.innerText = "<%:Checking...%>";
xhr_post("<%=url('admin/system/ota/check')%>", {}, function(x, d){
check.disabled = undefined;
switch(d.code){
case 0:
check_result.innerText = "";
upgrade_log.innerHTML = d.msg;
state_switch("unchecked", "checked");
break;
case 1:
check_result.innerText = "<%:Already the latest firmware%>";
history_log.innerHTML = '<h2><%:History Log%>:</h2>' + d.msg;
break;
default:
check_result.innerText = "<%:Check failed%>";
alert("<%:Check failed%>:\n"+d.msg);
}
});
};
var refresh_download_progress_paused = false;
var refresh_download_progress_timer = null;
var refresh_download_progress = function() {
if (refresh_download_progress_paused) {
refresh_download_progress_timer = setTimeout(refresh_download_progress, 500);
return;
}
XHR.get("<%=url('admin/system/ota/progress')%>", {}, function(x, d){
refresh_download_progress_timer = null;
switch(d.code){
case 0:
download_progress.innerText = "";
state_switch("downloading", "downloaded");
break;
case 1:
if (d.msg.startsWith('#')) {
if (!refresh_download_progress_paused)
cancel.disabled = undefined;
download_progress.innerText = "<%:Downloading%>: "+d.msg.replaceAll(/[# ]/g, '');
} else {
if (!refresh_download_progress_paused)
cancel.disabled = 'disabled';
download_progress.innerText = d.msg;
}
refresh_download_progress_timer = setTimeout(refresh_download_progress, 500);
break;
case 2:
download_result.innerText = "<%:Download canceled%>";
state_switch("downloading", "checked");
break;
default:
download_result.innerText = "<%:Download failed%>";
state_switch("downloading", "checked");
alert("<%:Download failed%>:"+d.msg);
}
});
};
download.onclick = function(){
download.disabled = 'disabled';
xhr_post("<%=url('admin/system/ota/download')%>", {}, function(x, d){
download.disabled = undefined;
switch(d.code){
case 0:
download_progress.innerText = "<%:Downloading%>: ...";
cancel.disabled = 'disabled';
state_switch("checked", "downloading");
refresh_download_progress_timer = setTimeout(refresh_download_progress, 500);
break;
default:
alert("<%:Download failed%>:"+d.msg);
}
});
};
cancel.onclick = function() {
cancel.disabled = 'disabled';
refresh_download_progress_paused = true;
xhr_post("<%=url('admin/system/ota/cancel')%>", {}, function(x, d){
refresh_download_progress_paused = false;
});
};
})();
</script>
</fieldset>
<%+footer%>

View File

@ -0,0 +1,52 @@
<%#
Copyright 2008 Steven Barth <steven@midlink.org>
Copyright 2008 Jo-Philipp Wich <jow@openwrt.org>
Licensed to the public under the Apache License 2.0.
-%>
<html>
<head>
<title><%=luci.sys.hostname()%> - <%= title or translate("Rebooting...") %></title>
<link rel="stylesheet" type="text/css" media="screen" href="<%=media%>/cascade.css" />
<script type="text/javascript">//<![CDATA[
var interval = window.setInterval(function() {
var img = new Image();
var target = ('https:' == document.location.protocol ? 'https://' : 'http://') + <%=addr and "'%s'" % addr or "window.location.host"%>;
img.onload = function() {
window.clearInterval(interval);
window.location.replace(target);
};
img.src = target + '<%=resource%>/icons/loading.gif?' + Math.random();
}, 5000);
//]]></script>
</head>
<body>
<header>
<div class="fill">
<div class="container">
<p class="brand"><%=luci.sys.hostname() or "?"%></p>
</div>
</div>
</header>
&#160;
<div class="main">
<div id="maincontainer">
<div id="maincontent" class="container">
<h2 name="content" id="applyreboot-container" ><%:System%> - <%= title or translate("Rebooting...") %></h2>
<div class="cbi-section" id="applyreboot-section">
<div>
<%= msg or translate("Changes applied.") %>
</div>
<div>
<img src="<%=resource%>/icons/loading.gif" alt="<%:Loading%>" style="vertical-align:middle" />
<%:Waiting for device...%>
</div>
</div>
</div>
</div>
</div>
</body>
</html>

View File

@ -0,0 +1,56 @@
msgid ""
msgstr "Content-Type: text/plain; charset=UTF-8"
msgid "OTA"
msgstr ""
msgid "Already the latest firmware"
msgstr ""
msgid "Check update"
msgstr ""
msgid "Found new firmware"
msgstr ""
msgid "Download firmware"
msgstr ""
msgid "Cancel download"
msgstr ""
msgid "Firmware downloaded"
msgstr ""
msgid "Flash image..."
msgstr ""
msgid "Checking..."
msgstr ""
msgid "Check failed"
msgstr ""
msgid "Downloading"
msgstr ""
msgid "Download canceled"
msgstr ""
msgid "Download failed"
msgstr ""
msgid "Upgrade firmware On the Air"
msgstr ""
msgid "Check and upgrade firmware from the Internet"
msgstr ""
msgid "The latest firmware"
msgstr ""
msgid "The image file does not contain a supported format. Maybe try again later."
msgstr ""
msgid "History Log"
msgstr ""

View File

@ -0,0 +1,56 @@
msgid ""
msgstr "Content-Type: text/plain; charset=UTF-8"
msgid "OTA"
msgstr ""
msgid "Already the latest firmware"
msgstr ""
msgid "Check update"
msgstr ""
msgid "Found new firmware"
msgstr ""
msgid "Download firmware"
msgstr ""
msgid "Cancel download"
msgstr ""
msgid "Firmware downloaded"
msgstr ""
msgid "Flash image..."
msgstr ""
msgid "Checking..."
msgstr ""
msgid "Check failed"
msgstr ""
msgid "Downloading"
msgstr ""
msgid "Download canceled"
msgstr ""
msgid "Download failed"
msgstr ""
msgid "Upgrade firmware On the Air"
msgstr ""
msgid "Check and upgrade firmware from the Internet"
msgstr ""
msgid "The latest firmware"
msgstr ""
msgid "The image file does not contain a supported format. Maybe try again later."
msgstr ""
msgid "History Log"
msgstr ""

View File

@ -0,0 +1,56 @@
msgid ""
msgstr "Content-Type: text/plain; charset=UTF-8"
msgid "OTA"
msgstr "在线升级"
msgid "Already the latest firmware"
msgstr "已经是最新固件"
msgid "Check update"
msgstr "检查更新"
msgid "Found new firmware"
msgstr "发现新固件"
msgid "Download firmware"
msgstr "下载固件"
msgid "Cancel download"
msgstr "取消下载"
msgid "Firmware downloaded"
msgstr "固件已下载"
msgid "Flash image..."
msgstr "刷写固件..."
msgid "Checking..."
msgstr "检查中..."
msgid "Check failed"
msgstr "检查失败"
msgid "Downloading"
msgstr "下载中"
msgid "Download canceled"
msgstr "下载已取消"
msgid "Download failed"
msgstr "下载失败"
msgid "Upgrade firmware On the Air"
msgstr "在线升级固件"
msgid "Check and upgrade firmware from the Internet"
msgstr "通过网络检查和更新固件"
msgid "The latest firmware"
msgstr "最新固件"
msgid "The image file does not contain a supported format. Maybe try again later."
msgstr "镜像文件格式不支持或损坏。也许稍候再试试"
msgid "History Log"
msgstr "历史更新日志"

View File

@ -0,0 +1,187 @@
#!/bin/sh
# author jjm2473
if [ -f /lib/upgrade/ota.sh ]; then
. /lib/upgrade/ota.sh
export_ota_url
fi
if [ -z "$OTA_URL_BASE" ]; then
echo "Unsupported device." >&2
exit 255
fi
WRLOCK=/var/lock/ota_background.lock
action=${1}
shift
sha256() {
sha256sum $1 | cut -d' ' -f1
}
fcurl() {
curl --fail --show-error "$@"
}
download() {
local img=$1
local sum=$2
touch /tmp/firmware.img.part
echo >/tmp/firmware.img.progress
rm -f /tmp/firmware.img.sha256sum
if [ -f /tmp/firmware.img ]; then
echo "Checking existed firmware.img..." >> /tmp/firmware.img.progress
if [ "`sha256 /tmp/firmware.img`" = "$sum" ]; then
echo $sum > /tmp/firmware.img.sha256sum;
rm -f /tmp/firmware.img.part
rm -f /tmp/firmware.img.progress
return 0;
else
echo "Check failed, redownload" >> /tmp/firmware.img.progress
rm -f /tmp/firmware.img
fi
fi
curl --fail --show-error --connect-timeout 5 --progress-bar -o /tmp/firmware.img.part "$OTA_URL_BASE/$img" 2>/tmp/firmware.img.progress &
echo "$!" > /var/run/ota/download.pid
if wait $!; then
rm -f /var/run/ota/download.pid
echo "Checking new firmware.img.part..." > /tmp/firmware.img.progress
if [ "`sha256 /tmp/firmware.img.part`" = "$sum" ]; then
mv /tmp/firmware.img.part /tmp/firmware.img && echo $sum > /tmp/firmware.img.sha256sum
rm -f /tmp/firmware.img.progress
return 0
else
echo "Checksum failed!" >>/tmp/firmware.img.progress
fi
fi
rm -f /var/run/ota/download.pid
rm -f /tmp/firmware.img.part
return 1
}
lock_download() {
local lock="$WRLOCK"
exec 200>$lock
flock -n 200 || return
download $1 $2
flock -u 200
}
prepare_check() {
local line
local release=`grep 'DISTRIB_RELEASE=' /etc/openwrt_release | sed -E "s/.*'(.+)'.*/\1/"`
local revision=`grep 'DISTRIB_REVISION=' /etc/openwrt_release | sed -E "s/.*'(.+)'.*/\1/" | cut -d'-' -f1`
local current="$release-$revision"
local latest=0
cat /dev/null > /var/run/ota/ota.log
fcurl --connect-timeout 5 -o /var/run/ota/ota.latest "$OTA_URL_BASE/version.latest" || { rm -f /var/run/ota/ota.latest ; return 255; }
if grep -Fq "$current" /var/run/ota/ota.latest; then
latest=1
current=""
fi
fcurl --connect-timeout 5 -o /var/run/ota/ota.index "$OTA_URL_BASE/version.index" || return 255;
head -5 /var/run/ota/ota.index | while read line; do
if [ -z "$line" -o "$line" = "$current" ]; then
break
fi
echo "<h3>`echo $line | sed 's/-/ /'`</h3><pre>" >> /var/run/ota/ota.log
fcurl --connect-timeout 1 "$OTA_URL_BASE/log/$line.log" >> /var/run/ota/ota.log
echo "</pre>" >> /var/run/ota/ota.log
done
[ "`wc -l /var/run/ota/ota.log | cut -d' ' -f1`" -eq 0 ] && return 1
[ "`wc -l /var/run/ota/ota.index | cut -d' ' -f1`" -gt 5 ] && echo "<a href=\"$OTA_URL_BASE/\" onclick=\"void(0)\" target=\"_blank\">More</a>" >> /var/run/ota/ota.log
return $latest
}
# 0: found newer fw, 1: already newest fw, *: err
do_check() {
local ret=0
local lock="$WRLOCK"
exec 200>$lock
flock -n 200 || return 255
prepare_check
ret=$?
flock -u 200 >/dev/null 2>&1
if [ "$ret" -eq 0 ]; then
# output
echo "<pre>"
sed -E 's/.*\((.+)\).*/\1/' /var/run/ota/ota.latest
echo "</pre>"
echo "<br><h2>ChangeLog:</h2>"
fi
[ "$ret" -eq 0 -o "$ret" -eq 1 ] && cat /var/run/ota/ota.log
return $ret
}
# async download
do_download(){
local img=`head -1 /var/run/ota/ota.latest | sed -E 's/.*\((.+)\).*/\1/'`
local sum=`grep 'SHA256: ' /var/run/ota/ota.latest | cut -d' ' -f2`
[ -z "$img" -o -z "$sum" ] && { echo "do check first" >&2 ; return 254; }
lock_download $img $sum &
return 0
}
# 0: done, 1: downloading, 2: failed, *: err
do_progress() {
local sum=`grep 'SHA256: ' /var/run/ota/ota.latest | cut -d' ' -f2`
[ -z "$sum" ] && { echo "do check first" >&2 ; return 254; }
[ -f /tmp/firmware.img.sha256sum ] && [ "`cat /tmp/firmware.img.sha256sum`" = "$sum" ] && return 0
[ -f /tmp/firmware.img.progress ] || { echo "download not in progress" >&2 ; return 254; }
[ -f /tmp/firmware.img.part ] && { cat /tmp/firmware.img.progress | tr '\r' '\n' | tail -n1; return 1; }
tail -1 /tmp/firmware.img.progress | grep -Fq 'Canceled!' && { echo "Canceled"; return 2; }
grep -v '\r' /tmp/firmware.img.progress >&2
return 3
}
do_cancel() {
if [ -f /var/run/ota/download.pid ]; then
local pid=`cat /var/run/ota/download.pid`
if [ -n "$pid" ]; then
kill -TERM $pid;
while kill -0 $pid >/dev/null 2>&1; do
if ! sleep 1; then
break
fi
done
echo "" >> /tmp/firmware.img.progress
echo "Canceled!" >> /tmp/firmware.img.progress
fi
fi
return 0
}
ota_init(){
mkdir -p /var/run/ota >/dev/null 2>&1 || true
}
usage() {
echo "usage: ota sub-command"
echo "where sub-command is one of:"
echo " check Check firmware upgrade"
echo " download Download latest firmware"
echo " progress Download progress"
echo " cancel Cancel download"
}
ota_init || exit 255
case $action in
"check")
do_check
;;
"download")
do_download
;;
"progress")
do_progress
;;
"cancel")
do_cancel
;;
*)
usage
;;
esac