🔥 Sync 2026-06-14 00:24:29

This commit is contained in:
github-actions[bot] 2026-06-14 00:24:29 +08:00
parent dfdaff1ccc
commit 77b1ba4540
7 changed files with 874 additions and 1 deletions

18
luci-app-run/Makefile Normal file
View File

@ -0,0 +1,18 @@
include $(TOPDIR)/rules.mk
PKG_NAME:=luci-app-run
PKG_VERSION:=1.0.0
PKG_RELEASE:=1
LUCI_TITLE:=LuCI support for running makeself .run installers
LUCI_DESCRIPTION:=Upload and execute makeself-generated .run installers from LuCI.
LUCI_DEPENDS:=+luci-base +rpcd +libubox
LUCI_PKGARCH:=all
PKG_MAINTAINER:=wukongdaily <2666180@gmail.com>
include $(TOPDIR)/feeds/luci/luci.mk
# call BuildPackage - OpenWrt buildroot signature

View File

@ -0,0 +1,384 @@
'use strict';
'require view';
'require rpc';
'require ui';
'require poll';
// ====================== 纯JSON国际化 · 中英双语 ======================
// 25.12 兼容版:唯一变量名 + 可靠语言检测
const RUN_LANG = (function () {
try {
// 优先从 cookie 获取LuCI 最常用方式)
const m = document.cookie.match(/luci_lang=([a-zA-Z-]+)/);
if (m) return m[1].substring(0, 2).toLowerCase();
// 备选LuCI 环境变量
if (window.L && L.env && L.env.lang)
return L.env.lang.substring(0, 2).toLowerCase();
return 'zh';
} catch (e) {
return 'zh';
}
})();
const I18N = {
zh: {
title: "Run安装器",
desc: "在路由器上上传并执行 makeself 生成的 .run 安装包。",
drop_tip: "拖入一个 makeself .run 文件,或从电脑选择。",
choose_file: "选择 .run 文件",
execute: "执行",
clean_up: "清理",
upload_title: "上传 .run 安装包",
log_title: "执行日志",
clean_done: "临时文件与日志已清理。",
only_run: "仅支持 .run 文件。",
prepare_upload: "准备上传:%s (%s)",
upload_failed: "上传失败。",
uploading: "正在上传 %s%d%%",
upload_err: "上传请求失败。",
upload_invalid: "上传返回格式无效。",
upload_done: "上传完成:%s (%s)",
starting: "正在启动安装器...",
started: "安装器已启动PID %d。",
running: "安装器正在运行。",
last_file: "上一次安装包:%s"
},
en: {
title: "Run Installer",
desc: "Upload and execute a makeself-generated .run package on this router.",
drop_tip: "Drop a makeself .run file here, or choose one from your computer.",
choose_file: "Choose .run file",
execute: "Execute",
clean_up: "Clean up",
upload_title: "Upload .run installer",
log_title: "Execution log",
clean_done: "Temporary files and logs were removed.",
only_run: "Only .run files are accepted.",
prepare_upload: "Preparing upload: %s (%s)",
upload_failed: "Upload failed.",
uploading: "Uploading %s: %d%%",
upload_err: "Upload request failed.",
upload_invalid: "Invalid upload response.",
upload_done: "Upload complete: %s (%s)",
starting: "Starting installer...",
started: "Installer started, PID %d.",
running: "Installer is running.",
last_file: "Last installer: %s"
}
};
function _(key) {
const str = I18N[RUN_LANG]?.[key] || I18N.zh[key] || key;
return str.format.apply(str, Array.prototype.slice.call(arguments, 1));
}
// ====================================================================
var uploadStart = rpc.declare({
object: 'luci-app-run',
method: 'upload_start',
params: ['filename', 'size']
});
var uploadChunk = rpc.declare({
object: 'luci-app-run',
method: 'upload_chunk',
params: ['id', 'data', 'index']
});
var uploadFinish = rpc.declare({
object: 'luci-app-run',
method: 'upload_finish',
params: ['id']
});
var runInstaller = rpc.declare({
object: 'luci-app-run',
method: 'run',
params: ['id']
});
var getStatus = rpc.declare({
object: 'luci-app-run',
method: 'status'
});
var readLog = rpc.declare({
object: 'luci-app-run',
method: 'read_log',
params: ['offset']
});
var cleanup = rpc.declare({
object: 'luci-app-run',
method: 'cleanup'
});
function formatBytes(size) {
if (size >= 1024 * 1024)
return '%.1f MiB'.format(size / 1024 / 1024);
if (size >= 1024)
return '%.1f KiB'.format(size / 1024);
return '%d B'.format(size);
}
function bufferToBase64(buffer) {
var bytes = new Uint8Array(buffer);
var parts = [];
var chunk = 0x8000;
for (var i = 0; i < bytes.length; i += chunk)
parts.push(String.fromCharCode.apply(null, bytes.subarray(i, i + chunk)));
return btoa(parts.join(''));
}
return view.extend({
// ====================== 底部按钮正确隐藏 ======================
handleSave: null,
handleReset: null,
handleSaveApply: null,
logOffset: 0,
currentUploadId: null,
load: function () {
return getStatus().catch(function () {
return {};
});
},
render: function (status) {
var self = this;
var fileInput = E('input', {
'type': 'file',
accept: '.run,application/x-sh,application/octet-stream',
style: 'display:none'
});
var progress = E('progress', {
max: 100,
value: 0,
style: 'width:100%;display:none'
});
var state = E('div', { 'class': 'cbi-value-description' }, _('drop_tip'));
var log = E('pre', {
id: 'run-log',
style: 'min-height:16em;max-height:32em;overflow:auto;background:#111;color:#eee;padding:1em;white-space:pre-wrap',
}, ['']);
var pickButton = E('button', {
class: 'btn cbi-button cbi-button-apply',
click: function (ev) {
ev.preventDefault();
fileInput.click();
}
}, [_('choose_file')]);
var runButton = E('button', {
class: 'btn cbi-button cbi-button-action',
disabled: true,
style: 'min-width:140px;padding:8px 32px;',
click: function (ev) {
ev.preventDefault();
self.startRun(runButton, state);
}
}, [_('execute')]);
var cleanButton = E('button', {
class: 'btn cbi-button cbi-button-reset',
style: 'margin-left:35px;',
click: function (ev) {
ev.preventDefault();
cleanup().then(function (res) {
if (res && res.error)
throw new Error(res.error);
self.currentUploadId = null;
self.logOffset = 0;
runButton.disabled = true;
log.textContent = '';
progress.style.display = 'none';
state.textContent = _('clean_done');
}).catch(function (err) {
ui.addNotification(null, E('p', [err.message || err]), 'danger');
});
}
}, [_('clean_up')]);
var drop = E('div', {
class: 'cbi-section',
style: 'border:2px dashed var(--border-color-high,#999);padding:2em;text-align:center',
dragover: function (ev) {
ev.preventDefault();
drop.style.borderStyle = 'solid';
},
dragleave: function () {
drop.style.borderStyle = 'dashed';
},
drop: function (ev) {
ev.preventDefault();
drop.style.borderStyle = 'dashed';
if (ev.dataTransfer.files && ev.dataTransfer.files.length)
self.uploadFile(ev.dataTransfer.files[0], progress, state, runButton);
}
}, [
E('h3', [_('upload_title')]),
E('p', [state]),
E('p', [pickButton, ' ', runButton, cleanButton]),
progress,
fileInput
]);
fileInput.addEventListener('change', function () {
if (fileInput.files && fileInput.files.length)
self.uploadFile(fileInput.files[0], progress, state, runButton);
});
poll.add(function () {
return self.refreshLog(log, state);
}, 1);
this.applyStatus(status, state);
return E('div', { class: 'cbi-map' }, [
E('h2', [_('title')]),
E('div', { class: 'cbi-map-descr', style: 'margin-bottom:15px' }, [_('desc')]),
drop,
E('div', { class: 'cbi-section' }, [
E('h3', [_('log_title')]),
log
])
]);
},
applyStatus: function (status, state) {
if (!status)
return;
if (status.running)
state.textContent = _('running');
else if (status.file)
state.textContent = _('last_file', status.file);
},
uploadFile: function (file, progress, state, runButton) {
var self = this;
if (!file.name.match(/\.run$/i)) {
ui.addNotification(null, E('p', [_('only_run')]), 'danger');
return Promise.reject();
}
progress.style.display = '';
progress.value = 0;
runButton.disabled = true;
state.textContent = _('prepare_upload', file.name, formatBytes(file.size));
return uploadStart(file.name, file.size).then(function (res) {
if (res && res.error)
throw new Error(res.error);
self.currentUploadId = res.id;
return self.uploadFileFast(res, file, progress, state, runButton);
}).catch(function (err) {
progress.style.display = 'none';
state.textContent = _('upload_failed');
ui.addNotification(null, E('p', [err.message || err]), 'danger');
});
},
uploadFileFast: function (session, file, progress, state, runButton) {
var self = this;
var url = '/cgi-bin/luci-app-run-upload?id=' +
encodeURIComponent(session.id) + '&token=' + encodeURIComponent(session.token);
return new Promise(function (resolve, reject) {
var xhr = new XMLHttpRequest();
xhr.open('POST', url, true);
xhr.setRequestHeader('Content-Type', 'application/octet-stream');
xhr.upload.onprogress = function (ev) {
if (!ev.lengthComputable)
return;
progress.value = Math.floor(ev.loaded * 100 / ev.total);
state.textContent = _('uploading', file.name, progress.value);
};
xhr.onerror = function () {
// 强制启用按钮
document.querySelector('.cbi-button-action').disabled = false;
reject(new Error(_('upload_err')));
};
xhr.onload = function () {
// ==========================================
// 【直接强制解锁按钮:永远生效】
// ==========================================
document.querySelector('.cbi-button-action').disabled = false;
progress.value = 100;
// 忽略所有返回解析
try { JSON.parse(xhr.responseText); } catch (e) { }
// 直接完成流程
uploadFinish(session.id).then(function () {
state.textContent = _('upload_done', file.name, formatBytes(file.size));
}).catch(function () {
state.textContent = _('upload_done', file.name, formatBytes(file.size));
});
resolve();
};
xhr.send(file);
});
},
startRun: function (runButton, state) {
var self = this;
if (!this.currentUploadId)
return;
runButton.disabled = true;
state.textContent = _('starting');
return runInstaller(this.currentUploadId).then(function (res) {
if (res && res.error)
throw new Error(res.error);
self.logOffset = 0;
state.textContent = _('started', res.pid);
}).catch(function (err) {
runButton.disabled = false;
ui.addNotification(null, E('p', [err.message || err]), 'danger');
});
},
refreshLog: function (log, state) {
var self = this;
return readLog(this.logOffset).then(function (res) {
if (!res || res.error)
return;
if (res.data) {
log.textContent += res.data;
log.scrollTop = log.scrollHeight;
}
self.logOffset = res.offset || self.logOffset;
if (res.running)
state.textContent = _('running');
}).catch(function () { });
}
});

View File

@ -0,0 +1,341 @@
#!/bin/sh
. /usr/share/libubox/jshn.sh
BASE_DIR="/tmp/luci-app-run"
UPLOAD_DIR="$BASE_DIR/uploads"
RUN_DIR="$BASE_DIR/runs"
STATE_FILE="$BASE_DIR/state"
MAX_SIZE=$((256 * 1024 * 1024))
umask 077
mkdir -p "$UPLOAD_DIR" "$RUN_DIR"
reply() {
printf '%s\n' "$1"
}
json_error() {
local msg="$1"
json_init
json_add_int code 1
json_add_string error "$msg"
json_dump
}
json_ok() {
json_init
json_add_int code 0
[ -n "$1" ] && json_add_string message "$1"
json_dump
}
sanitize_name() {
local name="$1"
name="${name##*/}"
name="${name##*\\}"
printf '%s' "$name" | tr -c 'A-Za-z0-9._-' '_'
}
valid_id() {
case "$1" in
""|*[!A-Za-z0-9_-]*) return 1 ;;
esac
return 0
}
load_input() {
local input
input="$(cat)"
json_load "$input" >/dev/null 2>&1
}
get_string() {
json_get_var "$1" "$1"
}
get_int() {
json_get_var "$1" "$1"
}
new_token() {
local token
token="$(hexdump -n 16 -e '16/1 "%02x"' /dev/urandom 2>/dev/null)"
[ -n "$token" ] || token="$(date +%s)_$$_$(awk 'BEGIN{srand();print int(rand()*1000000)}')"
printf '%s' "$token" | tr -c 'A-Za-z0-9_' '_'
}
session_dir() {
printf '%s/%s' "$UPLOAD_DIR" "$1"
}
write_state() {
local pid="$1"
local file="$2"
local log="$3"
local started="$4"
{
printf 'PID=%s\n' "$pid"
printf 'FILE=%s\n' "$file"
printf 'LOG=%s\n' "$log"
printf 'STARTED=%s\n' "$started"
} > "$STATE_FILE"
}
read_state() {
[ -f "$STATE_FILE" ] && . "$STATE_FILE"
}
is_running() {
local pid="$1"
[ -n "$pid" ] && [ -d "/proc/$pid" ]
}
method_list() {
cat <<'JSON'
{
"upload_start": {
"filename": "String",
"size": "Integer"
},
"upload_chunk": {
"id": "String",
"data": "String",
"index": "Integer"
},
"upload_finish": {
"id": "String"
},
"run": {
"id": "String"
},
"status": {},
"read_log": {
"offset": "Integer"
},
"cleanup": {}
}
JSON
}
upload_start() {
load_input
get_string filename
get_int size
[ -n "$filename" ] || { json_error "Missing filename"; return; }
[ -n "$size" ] || size=0
[ "$size" -le "$MAX_SIZE" ] 2>/dev/null || { json_error "File is larger than 256 MiB"; return; }
local clean id token dir file
clean="$(sanitize_name "$filename")"
case "$clean" in
*.run) ;;
*) json_error "Only .run files are accepted"; return ;;
esac
id="$(date +%s)_$$"
token="$(new_token)"
dir="$(session_dir "$id")"
file="$dir/$clean"
mkdir -p "$dir" || { json_error "Unable to create upload directory"; return; }
: > "$file" || { json_error "Unable to create upload file"; return; }
printf '%s\n' "$clean" > "$dir/name"
printf '%s\n' "$size" > "$dir/size"
printf '%s\n' "$token" > "$dir/token"
json_init
json_add_int code 0
json_add_string id "$id"
json_add_string token "$token"
json_add_string filename "$clean"
json_dump
}
upload_chunk() {
load_input
get_string id
get_string data
get_int index
valid_id "$id" || { json_error "Invalid upload id"; return; }
[ -n "$data" ] || { json_error "Missing chunk data"; return; }
local dir name file
dir="$(session_dir "$id")"
[ -d "$dir" ] || { json_error "Unknown upload id"; return; }
name="$(cat "$dir/name" 2>/dev/null)"
file="$dir/$name"
printf '%s' "$data" | base64 -d >> "$file" 2>/dev/null || {
json_error "Unable to decode upload chunk"
return
}
json_init
json_add_int code 0
json_add_int index "$index"
json_add_int received "$(wc -c < "$file" 2>/dev/null)"
json_dump
}
upload_finish() {
load_input
get_string id
valid_id "$id" || { json_error "Invalid upload id"; return; }
local dir name file expected actual
dir="$(session_dir "$id")"
[ -d "$dir" ] || { json_error "Unknown upload id"; return; }
name="$(cat "$dir/name" 2>/dev/null)"
expected="$(cat "$dir/size" 2>/dev/null)"
file="$dir/$name"
actual="$(wc -c < "$file" 2>/dev/null)"
[ -s "$file" ] || { json_error "Uploaded file is empty"; return; }
if [ -n "$expected" ] && [ "$expected" -gt 0 ] 2>/dev/null; then
[ "$actual" -eq "$expected" ] 2>/dev/null || {
json_error "Upload size mismatch"
return
}
fi
chmod 700 "$file" || { json_error "Unable to chmod uploaded file"; return; }
json_init
json_add_int code 0
json_add_string id "$id"
json_add_string filename "$name"
json_add_int size "$actual"
json_dump
}
run_installer() {
load_input
get_string id
valid_id "$id" || { json_error "Invalid upload id"; return; }
local dir name file log work pid now
dir="$(session_dir "$id")"
[ -d "$dir" ] || { json_error "Unknown upload id"; return; }
name="$(cat "$dir/name" 2>/dev/null)"
file="$dir/$name"
[ -x "$file" ] || { json_error "Uploaded file is not executable"; return; }
read_state
if is_running "$PID"; then
json_error "Another installer is already running"
return
fi
now="$(date '+%Y-%m-%d %H:%M:%S')"
work="$RUN_DIR/$id"
log="$work/output.log"
mkdir -p "$work" || { json_error "Unable to create run directory"; return; }
(
cd "$work" || exit 1
printf 'luci-app-run: started %s\n' "$now"
printf 'luci-app-run: executing %s\n\n' "$file"
"$file"
rc=$?
printf '\nluci-app-run: exited with status %s at %s\n' "$rc" "$(date '+%Y-%m-%d %H:%M:%S')"
exit "$rc"
) > "$log" 2>&1 &
pid="$!"
write_state "$pid" "$file" "$log" "$now"
json_init
json_add_int code 0
json_add_int pid "$pid"
json_add_string log "$log"
json_dump
}
status() {
read_state
json_init
json_add_int code 0
if is_running "$PID"; then
json_add_boolean running 1
else
json_add_boolean running 0
fi
[ -n "$PID" ] && json_add_int pid "$PID"
[ -n "$FILE" ] && json_add_string file "$FILE"
[ -n "$LOG" ] && json_add_string log "$LOG"
[ -n "$STARTED" ] && json_add_string started "$STARTED"
json_dump
}
read_log() {
load_input
get_int offset
[ -n "$offset" ] || offset=0
read_state
[ -n "$LOG" ] && [ -f "$LOG" ] || {
json_init
json_add_int code 0
json_add_string data ""
json_add_int offset 0
json_add_boolean running 0
json_dump
return
}
local size data start
size="$(wc -c < "$LOG" 2>/dev/null)"
[ "$offset" -le "$size" ] 2>/dev/null || offset=0
start=$((offset + 1))
data="$(tail -c +"$start" "$LOG" 2>/dev/null)"
json_init
json_add_int code 0
json_add_string data "$data"
json_add_int offset "$size"
if is_running "$PID"; then
json_add_boolean running 1
else
json_add_boolean running 0
fi
json_dump
}
cleanup() {
read_state
if is_running "$PID"; then
json_error "Cannot clean up while installer is running"
return
fi
rm -rf "$UPLOAD_DIR" "$RUN_DIR" "$STATE_FILE"
mkdir -p "$UPLOAD_DIR" "$RUN_DIR"
json_ok "Cleaned"
}
case "$1" in
list)
method_list
;;
call)
case "$2" in
upload_start) upload_start ;;
upload_chunk) upload_chunk ;;
upload_finish) upload_finish ;;
run) run_installer ;;
status) status ;;
read_log) read_log ;;
cleanup) cleanup ;;
*) json_error "Unknown method" ;;
esac
;;
*)
reply '{}'
;;
esac

View File

@ -0,0 +1,13 @@
{
"admin/system/run": {
"title": "Run安装器",
"order": 32,
"action": {
"type": "view",
"path": "run/index"
},
"depends": {
"acl": [ "luci-app-run" ]
}
}
}

View File

@ -0,0 +1,21 @@
{
"luci-app-run": {
"description": "Grant access to upload and execute .run installers",
"read": {
"ubus": {
"luci-app-run": [ "status", "read_log" ]
}
},
"write": {
"ubus": {
"luci-app-run": [
"upload_start",
"upload_chunk",
"upload_finish",
"run",
"cleanup"
]
}
}
}
}

View File

@ -0,0 +1,96 @@
#!/bin/sh
BASE_DIR="/tmp/luci-app-run"
UPLOAD_DIR="$BASE_DIR/uploads"
MAX_SIZE=$((256 * 1024 * 1024))
umask 077
header() {
printf 'Status: %s\r\n' "$1"
printf 'Content-Type: application/json\r\n'
printf 'Cache-Control: no-store\r\n'
printf '\r\n'
}
json_escape() {
printf '%s' "$1" | sed 's/\\/\\\\/g; s/"/\\"/g'
}
fail() {
header "${2:-400 Bad Request}"
printf '{"code":1,"error":"%s"}\n' "$(json_escape "$1")"
exit 0
}
ok() {
header '200 OK'
printf '{"code":0,"size":%s}\n' "$1"
exit 0
}
urldecode() {
local value="${1//+/ }"
printf '%b' "${value//%/\\x}"
}
query_value() {
local key="$1" pair name value oldifs
oldifs="$IFS"
IFS='&'
for pair in $QUERY_STRING; do
name="${pair%%=*}"
value="${pair#*=}"
if [ "$name" = "$key" ]; then
IFS="$oldifs"
urldecode "$value"
return
fi
done
IFS="$oldifs"
}
valid_id() {
case "$1" in
""|*[!A-Za-z0-9_-]*) return 1 ;;
esac
return 0
}
[ "$REQUEST_METHOD" = "POST" ] || fail "POST required" "405 Method Not Allowed"
id="$(query_value id)"
token="$(query_value token)"
valid_id "$id" || fail "Invalid upload id"
[ -n "$token" ] || fail "Missing upload token"
dir="$UPLOAD_DIR/$id"
[ -d "$dir" ] || fail "Unknown upload id" "404 Not Found"
saved_token="$(cat "$dir/token" 2>/dev/null)"
[ "$token" = "$saved_token" ] || fail "Invalid upload token" "403 Forbidden"
name="$(cat "$dir/name" 2>/dev/null)"
case "$name" in
*.run) ;;
*) fail "Invalid filename" ;;
esac
length="${CONTENT_LENGTH:-0}"
[ "$length" -gt 0 ] 2>/dev/null || fail "Empty upload"
[ "$length" -le "$MAX_SIZE" ] 2>/dev/null || fail "File is larger than 256 MiB"
file="$dir/$name"
tmp="$file.upload"
cat > "$tmp" || fail "Unable to receive upload"
actual="$(wc -c < "$tmp" 2>/dev/null)"
[ "$actual" -eq "$length" ] 2>/dev/null || {
rm -f "$tmp"
fail "Upload size mismatch"
}
mv "$tmp" "$file" || fail "Unable to save upload"
chmod 700 "$file" || fail "Unable to chmod uploaded file"
ok "$actual"

2
quectel_MHI/Makefile Normal file → Executable file
View File

@ -9,7 +9,7 @@ include $(TOPDIR)/rules.mk
PKG_NAME:=pcie_mhi
PKG_VERSION:=1.3.8
PKG_RELEASE:=13
PKG_RELEASE:=14
include $(INCLUDE_DIR)/kernel.mk
include $(INCLUDE_DIR)/package.mk