update 2024-12-25 23:13:18

This commit is contained in:
actions-user 2024-12-25 23:13:18 +08:00
parent 802fb67ddc
commit 569f523ce6
9 changed files with 741 additions and 0 deletions

13
luci-app-ota/Makefile Executable file
View File

@ -0,0 +1,13 @@
include $(TOPDIR)/rules.mk
LUCI_TITLE:=LuCI for OTA upgrade
LUCI_PKGARCH:=all
LUCI_DEPENDS:=+fw_download_tool +jsonfilter
PKG_VERSION:=1.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,175 @@
--[[
LuCI - Lua Configuration Interface
Copyright 2021 jjm2473
]]--
require "luci.util"
module("luci.controller.admin.ota",package.seeall)
function index()
if nixio.fs.access("/rom/bin/ota") then
entry({"admin", "system", "ota"}, call("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
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 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 "10.0.0.1" or nil
})
fork_exec("sleep 1; killall dropbear uhttpd nginx; sleep 1; sync; /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 = "Already the latest firmware"
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,218 @@
<%#
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>
</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 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%>";
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 (/^[0-9]/.test(d.msg)) {
if (!refresh_download_progress_paused)
cancel.disabled = undefined;
download_progress.innerText = "<%:Downloading%>: "+d.msg;
} 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%>

File diff suppressed because one or more lines are too long

1
luci-app-ota/po/zh-cn Symbolic link
View File

@ -0,0 +1 @@
zh_Hans

View File

@ -0,0 +1,53 @@
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 "镜像文件格式不支持或损坏。也许稍候再试试"

156
luci-app-ota/root/bin/ota Executable file
View File

@ -0,0 +1,156 @@
#!/bin/sh
. /etc/os-release
. /lib/functions/uci-defaults.sh
API=$(uci -q get ota.config.api_url)
WRLOCK=/var/lock/ota_background.lock
# armsr/armv8
[ "$OPENWRT_BOARD" = "armsr/armv8" ] && alias board_name="echo armsr,armv8"
# x86_64
[ $(uname -m) = "x86_64" ] && alias board_name="echo x86_64"
action=${1}
shift
sha256() {
sha256sum $1 | cut -d' ' -f1
}
download() {
read_json
if [ -f /tmp/firmware.img ]; then
echo "Checking existed firmware.img..." >> /tmp/firmware.img.progress
if [ "`sha256 /tmp/firmware.img`" = "$FW_SHA256SUM" ]; then
return 0;
else
echo "Check failed, redownload" >> /tmp/firmware.img.progress
rm -f /tmp/firmware.img
fi
fi
touch /tmp/firmware.img.progress
fw_download_tool "$FW_URL" -o /tmp/firmware.img.part -k -L -4 > /tmp/firmware.img.progress 2>&1 &
echo "$! $PPID" > /var/run/ota/download.pid
while true; do
progress=$(grep -c "100.00%" /tmp/firmware.img.progress)
if [ "$progress" -ge "1" ]; then
echo "Checking new firmware.img.part..." > /tmp/firmware.img.progress
break
fi
sleep 1
done
if [ "`sha256 /tmp/firmware.img.part`" = "$FW_SHA256SUM" ]; then
mv /tmp/firmware.img.part /tmp/firmware.img && echo $FW_SHA256SUM > /tmp/firmware.img.sha256sum
rm -f /tmp/firmware.img.progress
return 0
else
echo "Checksum failed!" >>/tmp/firmware.img.progress
sleep 1
rm -rf /tmp/firmware.img.part
return 1
fi
}
lock_download() {
local lock="$WRLOCK"
exec 200>$lock
flock -n 200 || return
download
flock -u 200
}
# 0: found newer fw, 1: already newest fw, *: err
do_check() {
url_check=$(curl -I -o /dev/null -s -w %{http_code} "$API")
[ $url_check -ne 200 ] && exit 255
curl -s "$API" > /var/run/ota/fw.json
read_json
NEW_VERSION=$(basename $FW_URL | awk -F- '{if (NF >= 3 && $3 ~ /^rc/) {print $2 "-" $3} else {print $2}}')
if [ "$BUILD_DATE" -lt "$FW_BUILD_DATE" ]; then
echo "<h3>Model:&nbsp;&nbsp;<font color=\"green\">$(board_name)</font><br/>Current Version:&nbsp;&nbsp;<font color=\"green\">$PRETTY_NAME</font><br/>Build Date:&nbsp;&nbsp;<font color=\"Orange\">$(date "+%Y-%m-%d %H:%M:%S" -d "@$BUILD_DATE")</font></h3>"
echo "<h3>New Version:&nbsp;&nbsp;<font color=\"green\">OpenWrt $NEW_VERSION</font><br/>Build Date:&nbsp;&nbsp;<font color=\"green\">$(date "+%Y-%m-%d %H:%M:%S" -d "@$FW_BUILD_DATE")</font></h3>"
return 0
elif [ "$BUILD_DATE" -ge "$FW_BUILD_DATE" ]; then
return 1
else
return 255
fi
}
# async download
do_download(){
[ ! -f "/var/run/ota/fw.json" ] && { echo "do check first" >&2 ; return 254; }
lock_download &
return 0
}
# 0: done, 1: downloading, 2: failed, *: err
do_progress() {
read_json
[ -f /tmp/firmware.img.sha256sum ] && [ "`cat /tmp/firmware.img.sha256sum`" = "$FW_SHA256SUM" ] && 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; }
tail -1 /tmp/firmware.img.progress | grep -Fq 'Checksum failed!' && { echo "Checksum failed!"; return 254; }
grep -v '\r' /tmp/firmware.img.progress >&2
return 1
}
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 -9 $pid >/dev/null 2>&1; do
if ! sleep 1; then
break
fi
done
rm -rf /tmp/firmware.img* /var/lock/ota_background.lock /var/lock/ota_api.lock /var/run/ota/download.pid
echo "" >> /tmp/firmware.img.progress
echo "Canceled!" >> /tmp/firmware.img.progress
fi
fi
return 0
}
read_json(){
FW_BUILD_DATE=$(jsonfilter -i /var/run/ota/fw.json -e "@['$(board_name)'][0]['build_date']")
FW_SHA256SUM=$(jsonfilter -i /var/run/ota/fw.json -e "@['$(board_name)'][0]['sha256sum']")
FW_URL=$(jsonfilter -i /var/run/ota/fw.json -e "@['$(board_name)'][0]['url']")
}
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

View File

@ -0,0 +1,3 @@
config ota 'config'
option api_url ''

View File

@ -0,0 +1,11 @@
{
"luci-app-ota": {
"description": "Grant UCI access for luci-app-ota",
"read": {
"uci": [ "ota" ]
},
"write": {
"uci": [ "ota" ]
}
}
}