From 903d80abd4e7d432aa51cd8fc76dbcf1d2d0b3ef Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Thu, 14 May 2026 20:40:35 +0800 Subject: [PATCH] =?UTF-8?q?=F0=9F=9A=80=20Sync=202026-05-14=2020:40:35?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- 3proxy/Makefile | 2 +- 3proxy/files/3proxy.config | 2 +- 3proxy/files/3proxy.init | 20 +- filebrowser/Makefile | 2 +- hickory-dns/Makefile | 2 +- hickory-dns/files/usr/bin/ipv6-neigh-wrapper | 2 +- ipv6-neigh/Cargo.lock | 186 +-------- ipv6-neigh/Cargo.toml | 21 +- ipv6-neigh/Makefile | 2 +- ipv6-neigh/src/db.rs | 2 +- ipv6-neigh/src/main.rs | 373 +++++++++++++++--- ipv6-neigh/src/op.rs | 2 +- luci-app-amlogic/Makefile | 4 +- .../resources/view/amlogic/armcpu.js | 4 +- .../resources/view/amlogic/backup.js | 52 ++- .../resources/view/amlogic/backuplist.js | 21 +- .../resources/view/amlogic/check.js | 73 ++-- .../resources/view/amlogic/config.js | 34 +- .../resources/view/amlogic/info.js | 30 +- .../resources/view/amlogic/install.js | 31 +- .../luci-static/resources/view/amlogic/log.js | 13 +- .../resources/view/amlogic/poweroff.js | 23 +- .../resources/view/amlogic/shared.js | 15 +- .../resources/view/amlogic/upload.js | 91 ++--- .../share/amlogic/amlogic_check_firmware.sh | 16 +- .../usr/share/amlogic/amlogic_check_kernel.sh | 16 +- .../usr/share/amlogic/amlogic_check_plugin.sh | 16 +- .../root/usr/share/rpcd/ucode/luci.amlogic | 76 ++-- luci-app-passwall/Makefile | 2 +- .../luasrc/view/passwall/global/footer.htm | 112 +++--- .../htdocs/luci-static/merona/cascade.css | 3 + .../htdocs/luci-static/teleofis/cascade.css | 4 + .../htdocs/luci-static/teleofis/checkbox.css | 65 +++ .../luasrc/view/themes/teleofis/header.htm | 1 + qBittorrent-Enhanced-Edition/Makefile | 3 +- 35 files changed, 710 insertions(+), 611 deletions(-) create mode 100644 luci-theme-teleofis/htdocs/luci-static/teleofis/checkbox.css diff --git a/3proxy/Makefile b/3proxy/Makefile index 4aef1708..74142682 100644 --- a/3proxy/Makefile +++ b/3proxy/Makefile @@ -6,7 +6,7 @@ include $(TOPDIR)/rules.mk PKG_NAME:=3proxy PKG_VERSION:=0.9.5 -PKG_RELEASE:=1 +PKG_RELEASE:=2 PKG_SOURCE:=$(PKG_NAME)-$(PKG_VERSION).tar.gz PKG_SOURCE_URL:=https://codeload.github.com/3proxy/3proxy/tar.gz/$(PKG_VERSION)? diff --git a/3proxy/files/3proxy.config b/3proxy/files/3proxy.config index b745a325..237be733 100644 --- a/3proxy/files/3proxy.config +++ b/3proxy/files/3proxy.config @@ -1,5 +1,5 @@ -config 3proxy 'config' +config 3proxy option enabled '0' option conffile '/etc/3proxy/3proxy.cfg' diff --git a/3proxy/files/3proxy.init b/3proxy/files/3proxy.init index d77d7fa9..1c0dc0b3 100755 --- a/3proxy/files/3proxy.init +++ b/3proxy/files/3proxy.init @@ -6,17 +6,21 @@ USE_PROCD=1 CONF="3proxy" PROG="/usr/bin/3proxy" -start_service() { - config_load "$CONF" +start_instance() { + local cfg="$1" local enabled conffile - config_get_bool enabled "config" "enabled" "0" + config_get_bool enabled "$cfg" "enabled" "0" [ "$enabled" -eq "1" ] || return 0 - config_get conffile "config" "conffile" "/etc/3proxy/3proxy.cfg" + config_get conffile "$cfg" "conffile" + if [ -z "$conffile" ] || [ ! -e "$conffile" ]; then + logger -p daemon.err -t "$NAME" "config for $cfg does not exists." + return 1 + fi - procd_open_instance + procd_open_instance "$cfg" procd_set_param command "$PROG" "$conffile" procd_set_param stdout "1" @@ -28,6 +32,12 @@ start_service() { procd_close_instance } +start_service() { + config_load "$CONF" + + config_foreach start_instance 3proxy +} + service_triggers() { procd_add_reload_trigger "$CONF" } diff --git a/filebrowser/Makefile b/filebrowser/Makefile index 3d2856c5..be4fc9d3 100644 --- a/filebrowser/Makefile +++ b/filebrowser/Makefile @@ -7,7 +7,7 @@ include $(TOPDIR)/rules.mk PKG_NAME:=filebrowser -PKG_VERSION:=1.3.1-stable +PKG_VERSION:=1.3.2-stable PKG_RELEASE=1 ifeq ($(ARCH),aarch64) diff --git a/hickory-dns/Makefile b/hickory-dns/Makefile index 3f94efb5..a337e7e1 100644 --- a/hickory-dns/Makefile +++ b/hickory-dns/Makefile @@ -2,7 +2,7 @@ include $(TOPDIR)/rules.mk PKG_NAME:=hickory-dns PKG_VERSION:=0.26.1 -PKG_RELEASE:=26 +PKG_RELEASE:=27 PKG_SOURCE:=$(PKG_NAME)-$(PKG_VERSION).tar.gz PKG_SOURCE_URL:=https://codeload.github.com/hickory-dns/hickory-dns/tar.gz/v$(PKG_VERSION)? diff --git a/hickory-dns/files/usr/bin/ipv6-neigh-wrapper b/hickory-dns/files/usr/bin/ipv6-neigh-wrapper index 6acb91f8..eb805065 100644 --- a/hickory-dns/files/usr/bin/ipv6-neigh-wrapper +++ b/hickory-dns/files/usr/bin/ipv6-neigh-wrapper @@ -11,7 +11,7 @@ i=0 while [ "$i" -lt "$RETRY" ]; do if ss -lnt | grep -q ':53'; then logger -t ipv6-neigh-wrapper "DNS port 53 ready, starting ipv6-neigh" - exec "$NEIGH_PROG" --private-subnet-v4 -d "[::1]:53" -z lan --router-alias router + exec "$NEIGH_PROG" --private-subnet-v4 -d "[::1]:53" -z lan --router-alias router --keepalive-gua fi i=$((i + 1)) sleep 1 diff --git a/ipv6-neigh/Cargo.lock b/ipv6-neigh/Cargo.lock index 15d3b09a..9639aed5 100644 --- a/ipv6-neigh/Cargo.lock +++ b/ipv6-neigh/Cargo.lock @@ -73,12 +73,6 @@ version = "2.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c4512299f36f043ab09a583e57bceb5a5aab7a73db1805848e8fef3c9e8c78b3" -[[package]] -name = "byteorder" -version = "1.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" - [[package]] name = "bytes" version = "1.11.1" @@ -163,16 +157,6 @@ version = "1.0.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1d07550c9036bf2ae0c684c4297d503f838287c83c53686d05370d0e139ae570" -[[package]] -name = "combine" -version = "4.6.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ba5a308b75df32fe02788e748662718f03fde005016435c444eea572398219fd" -dependencies = [ - "bytes", - "memchr", -] - [[package]] name = "cpufeatures" version = "0.3.0" @@ -410,14 +394,13 @@ checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" [[package]] name = "hickory-proto" -version = "0.26.0-beta.3" -source = "git+https://github.com/hickory-dns/hickory-dns.git?rev=34a4cca1a707b642052b427938b7d1e8aaa71e3a#34a4cca1a707b642052b427938b7d1e8aaa71e3a" +version = "0.27.0-alpha.1" +source = "git+https://github.com/hickory-dns/hickory-dns.git?rev=c5e29b9da8334a14961a6326ad04e25397205430#c5e29b9da8334a14961a6326ad04e25397205430" dependencies = [ "bitflags", "data-encoding", "idna", "ipnet", - "jni", "once_cell", "rand", "ring", @@ -558,14 +541,13 @@ dependencies = [ "env_logger", "futures", "hickory-proto", - "libc", "log", "netlink-packet-core", "netlink-packet-route", "netlink-sys", "rtnetlink", "serde_json", - "socket2 0.5.10", + "socket2", "tokio", "ubus", ] @@ -612,55 +594,6 @@ dependencies = [ "syn", ] -[[package]] -name = "jni" -version = "0.22.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5efd9a482cf3a427f00d6b35f14332adc7902ce91efb778580e180ff90fa3498" -dependencies = [ - "cfg-if", - "combine", - "jni-macros", - "jni-sys", - "log", - "simd_cesu8", - "thiserror 2.0.18", - "walkdir", - "windows-link", -] - -[[package]] -name = "jni-macros" -version = "0.22.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a00109accc170f0bdb141fed3e393c565b6f5e072365c3bd58f5b062591560a3" -dependencies = [ - "proc-macro2", - "quote", - "rustc_version", - "simd_cesu8", - "syn", -] - -[[package]] -name = "jni-sys" -version = "0.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c6377a88cb3910bee9b0fa88d4f42e1d2da8e79915598f65fb0c7ee14c878af2" -dependencies = [ - "jni-sys-macros", -] - -[[package]] -name = "jni-sys-macros" -version = "0.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "38c0b942f458fe50cdac086d2f946512305e5631e720728f2a61aabcd47a6264" -dependencies = [ - "quote", - "syn", -] - [[package]] name = "leb128fmt" version = "0.1.0" @@ -713,47 +646,30 @@ dependencies = [ [[package]] name = "netlink-packet-core" -version = "0.7.0" +version = "0.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "72724faf704479d67b388da142b186f916188505e7e0b26719019c525882eda4" +checksum = "3463cbb78394cb0141e2c926b93fc2197e473394b761986eca3b9da2c63ae0f4" dependencies = [ - "anyhow", - "byteorder", - "netlink-packet-utils", + "paste", ] [[package]] name = "netlink-packet-route" -version = "0.21.0" +version = "0.30.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "483325d4bfef65699214858f097d504eb812c38ce7077d165f301ec406c3066e" +checksum = "be8919612f6028ab4eacbbfe1234a9a43e3722c6e0915e7ff519066991905092" dependencies = [ - "anyhow", "bitflags", - "byteorder", "libc", "log", "netlink-packet-core", - "netlink-packet-utils", -] - -[[package]] -name = "netlink-packet-utils" -version = "0.5.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0ede8a08c71ad5a95cdd0e4e52facd37190977039a4704eb82a283f713747d34" -dependencies = [ - "anyhow", - "byteorder", - "paste", - "thiserror 1.0.69", ] [[package]] name = "netlink-proto" -version = "0.11.5" +version = "0.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "72452e012c2f8d612410d89eea01e2d9b56205274abb35d53f60200b2ec41d60" +checksum = "b65d130ee111430e47eed7896ea43ca693c387f097dd97376bffafbf25812128" dependencies = [ "bytes", "futures", @@ -778,9 +694,9 @@ dependencies = [ [[package]] name = "nix" -version = "0.29.0" +version = "0.30.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "71e2746dc3a24dd78b3cfcb7be93368c6de9963d30f43a6a73998a9cf4b17b46" +checksum = "74523f3a35e05aba87a1d978330aef40f67b0304ac79c1c00b294c9830543db6" dependencies = [ "bitflags", "cfg-if", @@ -986,14 +902,15 @@ dependencies = [ [[package]] name = "rtnetlink" -version = "0.14.1" -source = "git+https://github.com/rust-netlink/rtnetlink.git?rev=515471f#515471fa0d759316ab249b7c265f3073b3dc1ffb" +version = "0.21.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc19f84f710fa2f337617f9bc0400260a94224bde7bae28fd8879f3771ca5784" dependencies = [ - "futures", + "futures-channel", + "futures-util", "log", "netlink-packet-core", "netlink-packet-route", - "netlink-packet-utils", "netlink-proto", "netlink-sys", "nix", @@ -1001,15 +918,6 @@ dependencies = [ "tokio", ] -[[package]] -name = "rustc_version" -version = "0.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cfcb3a22ef46e85b45de6ee7e79d063319ebb6594faafcf1c225ea92ab6e9b92" -dependencies = [ - "semver", -] - [[package]] name = "rustls-pki-types" version = "1.14.1" @@ -1019,15 +927,6 @@ dependencies = [ "zeroize", ] -[[package]] -name = "same-file" -version = "1.0.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" -dependencies = [ - "winapi-util", -] - [[package]] name = "scopeguard" version = "1.2.0" @@ -1099,22 +998,6 @@ dependencies = [ "libc", ] -[[package]] -name = "simd_cesu8" -version = "1.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "94f90157bb87cddf702797c5dadfa0be7d266cdf49e22da2fcaa32eff75b2c33" -dependencies = [ - "rustc_version", - "simdutf8", -] - -[[package]] -name = "simdutf8" -version = "0.1.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e3a9fe34e3e7a50316060351f37187a3f546bce95496156754b601a5fa71b76e" - [[package]] name = "slab" version = "0.4.12" @@ -1127,16 +1010,6 @@ version = "1.15.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" -[[package]] -name = "socket2" -version = "0.5.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e22376abed350d73dd1cd119b57ffccad95b4e585a7cda43e286245ce23c0678" -dependencies = [ - "libc", - "windows-sys 0.52.0", -] - [[package]] name = "socket2" version = "0.6.3" @@ -1283,7 +1156,7 @@ dependencies = [ "parking_lot", "pin-project-lite", "signal-hook-registry", - "socket2 0.6.3", + "socket2", "tokio-macros", "windows-sys 0.61.2", ] @@ -1372,16 +1245,6 @@ version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" -[[package]] -name = "walkdir" -version = "2.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b" -dependencies = [ - "same-file", - "winapi-util", -] - [[package]] name = "wasi" version = "0.11.1+wasi-snapshot-preview1" @@ -1440,15 +1303,6 @@ dependencies = [ "semver", ] -[[package]] -name = "winapi-util" -version = "0.1.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" -dependencies = [ - "windows-sys 0.61.2", -] - [[package]] name = "windows-link" version = "0.2.1" @@ -1662,9 +1516,9 @@ dependencies = [ [[package]] name = "zerofrom" -version = "0.1.7" +version = "0.1.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "69faa1f2a1ea75661980b013019ed6687ed0e83d069bc1114e2cc74c6c04c4df" +checksum = "0ec05a11813ea801ff6d75110ad09cd0824ddba17dfe17128ea0d5f68e6c5272" dependencies = [ "zerofrom-derive", ] diff --git a/ipv6-neigh/Cargo.toml b/ipv6-neigh/Cargo.toml index e9f5bcd4..b30f3b81 100644 --- a/ipv6-neigh/Cargo.toml +++ b/ipv6-neigh/Cargo.toml @@ -6,20 +6,19 @@ edition = "2024" [workspace] [dependencies] -rtnetlink = { git = "https://github.com/rust-netlink/rtnetlink.git", rev = "515471f" } +rtnetlink = { version = "0.21.0" } netlink-sys = { version = "0.8" } -tokio = { version = "1.0.1", features = ["full"]} -futures = "0.3.11" -netlink-packet-route = {version = "0.21" } -netlink-packet-core= {version ="0.7"} -clap = { version = "4.0", default-features = false, features = ["cargo", "derive", "help", "std", "suggestions"] } -hickory-proto = { git = "https://github.com/hickory-dns/hickory-dns.git", rev = "34a4cca1a707b642052b427938b7d1e8aaa71e3a", features = ["dnssec-ring"] } -ubus = "0.1.6" -serde_json = "1.0.108" +tokio = { version = "1.52.3", features = ["full"]} +futures = "0.3.32" +netlink-packet-route = {version = "0.30" } +netlink-packet-core= {version ="0.8"} +clap = { version = "4.6", default-features = false, features = ["cargo", "derive", "help", "std", "suggestions"] } +hickory-proto = { git = "https://github.com/hickory-dns/hickory-dns.git", rev = "c5e29b9da8334a14961a6326ad04e25397205430", features = ["dnssec-ring"] } +ubus = "0.1.7" +serde_json = "1.0.149" log = "0.4" env_logger = "0.11" -socket2 = "0.5" -libc = "0.2" +socket2 = { version = "0.6", features = ["all"] } [profile.release] opt-level = 3 diff --git a/ipv6-neigh/Makefile b/ipv6-neigh/Makefile index 5739d29c..6a9e9893 100644 --- a/ipv6-neigh/Makefile +++ b/ipv6-neigh/Makefile @@ -2,7 +2,7 @@ include $(TOPDIR)/rules.mk PKG_NAME:=ipv6-neigh PKG_VERSION:=0.1.0 -PKG_RELEASE:=9 +PKG_RELEASE:=10 PKG_BUILD_DEPENDS:=rust/host PKG_BUILD_PARALLEL:=1 diff --git a/ipv6-neigh/src/db.rs b/ipv6-neigh/src/db.rs index 6ceebbc8..b824549a 100644 --- a/ipv6-neigh/src/db.rs +++ b/ipv6-neigh/src/db.rs @@ -148,7 +148,7 @@ impl DnsUpdater { pub async fn axfr_records(&self) -> Result, Box> { const AXFR_TIMEOUT: Duration = Duration::from_secs(10); - let query = Query::query(self.zone.clone(), RecordType::AXFR); + let query = Query::new(self.zone.clone(), RecordType::AXFR); let mut msg = Message::query(); msg.add_query(query); diff --git a/ipv6-neigh/src/main.rs b/ipv6-neigh/src/main.rs index 7893fad5..66a289f0 100644 --- a/ipv6-neigh/src/main.rs +++ b/ipv6-neigh/src/main.rs @@ -13,7 +13,7 @@ use netlink_packet_route::neighbour::{NeighbourAddress, NeighbourAttribute, Neig use netlink_packet_route::route::RouteType; use rtnetlink::{Error, Handle, new_connection}; use socket2::{Domain, Protocol, SockAddr, Socket, Type}; -use std::os::unix::io::AsRawFd; +use std::num::NonZeroU32; use tokio::time::{self, Duration}; use clap::Parser; @@ -67,6 +67,18 @@ struct Cli { /// Additional DNS name to register router addresses under (e.g. "router" → "router.lan") #[clap(long)] router_alias: Option, + /// Periodically probe the newest GUA per host to maintain NUD/Wi-Fi reachability (without publishing to DNS) + #[clap(long)] + keepalive_gua: bool, + /// Probe interval in seconds for GUA keepalive (only used with --keepalive-gua) + #[clap(long, default_value = "120")] + keepalive_gua_interval: u64, + /// Maximum number of GUA addresses to keepalive-probe per host + #[clap(long, default_value = "1")] + keepalive_gua_per_host: usize, + /// Maximum number of ULA AAAA records to publish per host (oldest pruned first) + #[clap(long, default_value = "2")] + max_ula_per_host: usize, } #[derive(Debug)] @@ -91,12 +103,28 @@ struct RegisteredEntry { ifindex: u32, } +/// A GUA address tracked for keepalive probing only (not published to DNS). +struct GuaKeepaliveEntry { + hostname: String, + addr: Ipv6Addr, + ifindex: u32, + /// When this GUA was first observed — used to select the "newest" address per host. + first_seen: Instant, + /// Last time this address was confirmed REACHABLE by the kernel. + last_confirmed: Instant, +} + fn if_ipv6_in_private_subnet(ip: &Ipv6Addr) -> bool { // Check if address is ULA (fc00::/7) // Note: link-local is always filtered earlier by is_link_local_ipv6() (ip.segments()[0] & 0xfe00) == 0xfc00 } +/// Returns true if `addr` is a Global Unicast Address (2000::/3). +fn is_gua_ipv6(addr: &Ipv6Addr) -> bool { + (addr.segments()[0] & 0xe000) == 0x2000 +} + fn if_ipv4_in_private_subnet(ip: &Ipv4Addr) -> bool { // Check for private network ranges let octets = ip.octets(); @@ -142,11 +170,18 @@ fn ipv6_passes_active_prefix(addr: Ipv6Addr, active_prefixes: &[LanPrefix]) -> b active_prefixes.is_empty() || active_prefixes.iter().any(|p| ipv6_in_prefix(addr, p)) } -async fn process_new_neigh(neigh: &Neigh, updater: &db::DnsUpdater, leases: &HashMap) -> bool { +async fn process_new_neigh(neigh: &Neigh, updater: &db::DnsUpdater, leases: &HashMap, private_subnet_v6: bool) -> bool { let Some(hostname) = leases.get(&neigh.mac) else { debug!("no lease for mac {}, skipping DNS update", neigh.mac); return false; }; + // Guard: never publish GUA to DNS when private_subnet_v6 is set. + if let NeighbourAddress::Inet6(addr) = &neigh.inet { + if private_subnet_v6 && is_gua_ipv6(addr) { + debug!("skipping GUA DNS publish for {} (private_subnet_v6)", hostname); + return false; + } + } let result = match &neigh.inet { NeighbourAddress::Inet6(addr) => updater.upsert_aaaa(hostname, *addr, DEFAULT_TTL).await, NeighbourAddress::Inet(addr) => updater.upsert_a(hostname, *addr, DEFAULT_TTL).await, @@ -196,6 +231,51 @@ fn is_failed_state(state: NeighbourState) -> bool { matches!(state, NeighbourState::Failed) } +/// Enforce the per-host ULA AAAA limit. If `mac` already has `max` or more ULA AAAA +/// records in `registered`, remove the oldest (by `last_confirmed`) and delete from DNS. +/// Returns the number of existing ULA AAAA records for this MAC (after pruning). +async fn prune_ula_for_mac( + mac: &str, + max: usize, + registered: &mut HashMap<(String, String), RegisteredEntry>, + updater: &db::DnsUpdater, +) -> usize { + // Collect existing ULA AAAA keys for this MAC. + let mut ula_keys: Vec<((String, String), Instant)> = registered + .iter() + .filter(|((m, ip_str), _)| { + m == mac && ip_str.parse::().map_or(false, |a| if_ipv6_in_private_subnet(&a)) + }) + .map(|(k, e)| (k.clone(), e.last_confirmed)) + .collect(); + + if ula_keys.len() < max { + return ula_keys.len(); + } + + // Sort oldest first (by last_confirmed asc). + ula_keys.sort_by_key(|(_, ts)| *ts); + + // Remove excess (keep the newest `max - 1` so there's room for the new one). + let to_remove = ula_keys.len() - (max.saturating_sub(1)); + for (key, _) in ula_keys.into_iter().take(to_remove) { + if let Some(entry) = registered.remove(&key) { + let addr: Ipv6Addr = key.1.parse().unwrap(); + let inet = NeighbourAddress::Inet6(addr); + do_delete_dns(&entry.hostname, &inet, updater).await; + info!("ULA pruning: removed oldest {} -> {} for mac {}", entry.hostname, key.1, mac); + } + } + + // Return count after pruning. + registered + .iter() + .filter(|((m, ip_str), _)| { + m == mac && ip_str.parse::().map_or(false, |a| if_ipv6_in_private_subnet(&a)) + }) + .count() +} + #[tokio::main] async fn main() -> Result<(), ()> { let args = Cli::parse(); @@ -214,6 +294,10 @@ async fn main() -> Result<(), ()> { let private_subnet_v4 = args.private_subnet_v4; let private_subnet_v6 = !args.publish_gua; + let keepalive_gua = args.keepalive_gua; + let keepalive_gua_interval = args.keepalive_gua_interval; + let keepalive_gua_per_host = args.keepalive_gua_per_host; + let max_ula_per_host = args.max_ula_per_host; let zone = Name::from_ascii(&args.zone).expect("invalid zone name"); let key_data = std::fs::read(&args.key_file) @@ -257,9 +341,16 @@ async fn main() -> Result<(), ()> { // State cache: tracks (mac, ip_string) -> registered entry (hostname, last confirmed time, ifindex) let mut registered: HashMap<(String, String), RegisteredEntry> = HashMap::new(); + // GUA keepalive: tracks GUA addresses per MAC for periodic probing (not published to DNS). + // Key = MAC address, value = list of GUA entries for that host. + let mut gua_keepalive: HashMap> = HashMap::new(); + // Get active LAN prefixes to filter out neighbours from expired/old prefixes. // If prefix detection fails we log a warning but continue without filtering. - let active_prefixes = get_active_lan_prefixes(handle.clone(), &args.router_iface, private_subnet_v6).await; + // When keepalive_gua is enabled, also include GUA prefixes so we can filter + // keepalive targets against active prefixes (pass private_subnet_v6=false). + let prefix_filter_v6 = if keepalive_gua { false } else { private_subnet_v6 }; + let active_prefixes = get_active_lan_prefixes(handle.clone(), &args.router_iface, prefix_filter_v6).await; if active_prefixes.is_empty() { warn!("no active IPv6 prefixes found on {}, neighbour prefix-filtering disabled", args.router_iface); } else { @@ -275,12 +366,13 @@ async fn main() -> Result<(), ()> { // Dump existing neighbours: // - Skip entries whose IP is not within any active LAN prefix (old-prefix residuals). + // - Route GUA addresses to keepalive map (if enabled) instead of DNS. // - Register REACHABLE entries immediately. // - Send a probe to STALE/DELAY/PROBE entries; the event loop will register them // once the kernel confirms they are REACHABLE. // - Ignore FAILED and other states. debug!("dumping neighbours"); - if let Ok(neighbours) = dump_neighbours(handle.clone(), private_subnet_v4, private_subnet_v6).await { + if let Ok(neighbours) = dump_neighbours(handle.clone(), private_subnet_v4).await { for neigh in &neighbours { debug!("{:?}", neigh); if should_skip_neigh(neigh) { @@ -293,12 +385,54 @@ async fn main() -> Result<(), ()> { continue; } } + + // Route GUA addresses to keepalive map instead of DNS. + if let NeighbourAddress::Inet6(addr) = &neigh.inet { + if is_gua_ipv6(addr) { + if keepalive_gua { + if neigh.state == NeighbourState::Reachable { + if let Some(hostname) = leases.get(&neigh.mac) { + let entries = gua_keepalive.entry(neigh.mac.clone()).or_default(); + if !entries.iter().any(|e| e.addr == *addr) { + let now = Instant::now(); + entries.push(GuaKeepaliveEntry { + hostname: hostname.clone(), + addr: *addr, + ifindex: neigh.ifindex, + first_seen: now, + last_confirmed: now, + }); + debug!("init dump: GUA keepalive tracked {} -> {}", hostname, addr); + } + } + } else if matches!(neigh.state, NeighbourState::Stale | NeighbourState::Delay | NeighbourState::Probe) { + // Probe stale GUA so the kernel NUD state machine can confirm + // reachability; the event loop will add it to gua_keepalive on REACHABLE. + match send_icmpv6_echo(*addr, neigh.ifindex) { + Ok(()) => debug!("init dump: probing stale GUA {}", addr), + Err(e) => debug!("init GUA probe failed for {}: {}", addr, e), + } + } + } + // GUA addresses are never published to DNS (unless --publish-gua) + if private_subnet_v6 { + continue; + } + } + } + let key = (neigh.mac.clone(), inet_to_string(&neigh.inet)); match neigh.state { NeighbourState::Reachable => { // Confirmed online — register in DNS immediately. if !registered.contains_key(&key) { if let Some(hostname) = leases.get(&neigh.mac) { + // Enforce per-host ULA limit before adding a new one. + if let NeighbourAddress::Inet6(addr) = &neigh.inet { + if if_ipv6_in_private_subnet(addr) { + prune_ula_for_mac(&neigh.mac, max_ula_per_host, &mut registered, &updater).await; + } + } let result = match &neigh.inet { NeighbourAddress::Inet6(addr) => updater.upsert_aaaa(hostname, *addr, DEFAULT_TTL).await, NeighbourAddress::Inet(addr) => updater.upsert_a(hostname, *addr, DEFAULT_TTL).await, @@ -369,6 +503,14 @@ async fn main() -> Result<(), ()> { }; probe_timer.tick().await; // consume the immediate first tick + // GUA keepalive timer (separate cadence from DNS probe timer) + let mut gua_keepalive_timer = if keepalive_gua && keepalive_gua_interval > 0 { + time::interval(Duration::from_secs(keepalive_gua_interval)) + } else { + time::interval(Duration::from_secs(u64::MAX / 2)) + }; + gua_keepalive_timer.tick().await; // consume the immediate first tick + let mut lease_refresh_timer = time::interval(Duration::from_secs(60)); lease_refresh_timer.tick().await; // consume the immediate first tick @@ -383,24 +525,70 @@ async fn main() -> Result<(), ()> { if let NetlinkPayload::InnerMessage(msg) = payload { match msg { RouteNetlinkMessage::NewNeighbour(new_neigh) => { - let Some(neigh) = parse_neighbour_message(new_neigh, private_subnet_v4, private_subnet_v6) else { + let Some(neigh) = parse_neighbour_message(new_neigh, private_subnet_v4) else { continue; }; if should_skip_neigh(&neigh) { continue; } - let key = (neigh.mac.clone(), inet_to_string(&neigh.inet)); // Drop IPv6 events for addresses not in any active LAN prefix. if let NeighbourAddress::Inet6(addr) = &neigh.inet { if !ipv6_passes_active_prefix(*addr, &active_prefixes) { debug!("event: skipping {} — not in any active LAN prefix", addr); + let key = (neigh.mac.clone(), inet_to_string(&neigh.inet)); if let Some(entry) = registered.remove(&key) { do_delete_dns(&entry.hostname, &neigh.inet, &updater).await; } + // Also remove from GUA keepalive if present + if let Some(entries) = gua_keepalive.get_mut(&neigh.mac) { + entries.retain(|e| e.addr != *addr); + } continue; } } + // Route GUA addresses to keepalive map instead of DNS. + if let NeighbourAddress::Inet6(addr) = &neigh.inet { + if is_gua_ipv6(addr) && private_subnet_v6 { + // GUA not published to DNS — handle keepalive tracking only + if keepalive_gua { + if is_failed_state(neigh.state) { + if let Some(entries) = gua_keepalive.get_mut(&neigh.mac) { + entries.retain(|e| e.addr != *addr); + debug!("GUA keepalive: removed failed {} for mac {}", addr, neigh.mac); + } + } else if neigh.state == NeighbourState::Reachable { + if let Some(hostname) = leases.get(&neigh.mac) { + let entries = gua_keepalive.entry(neigh.mac.clone()).or_default(); + if let Some(e) = entries.iter_mut().find(|e| e.addr == *addr) { + e.last_confirmed = Instant::now(); + e.ifindex = neigh.ifindex; + } else { + let now = Instant::now(); + entries.push(GuaKeepaliveEntry { + hostname: hostname.clone(), + addr: *addr, + ifindex: neigh.ifindex, + first_seen: now, + last_confirmed: now, + }); + debug!("GUA keepalive: tracking {} -> {}", hostname, addr); + } + } + } else if matches!(neigh.state, NeighbourState::Stale | NeighbourState::Delay | NeighbourState::Probe) { + // Update ifindex only + if let Some(entries) = gua_keepalive.get_mut(&neigh.mac) { + if let Some(e) = entries.iter_mut().find(|e| e.addr == *addr) { + e.ifindex = neigh.ifindex; + } + } + } + } + continue; + } + } + + let key = (neigh.mac.clone(), inet_to_string(&neigh.inet)); if is_failed_state(neigh.state) { // Neighbour confirmed unreachable — remove DNS record if let Some(entry) = registered.remove(&key) { @@ -419,7 +607,13 @@ async fn main() -> Result<(), ()> { // New reachable neighbour — register in DNS debug!("New neighbour: {:?}", neigh); if let Some(hostname) = leases.get(&neigh.mac) { - if process_new_neigh(&neigh, &updater, &leases).await { + // Enforce per-host ULA limit before adding. + if let NeighbourAddress::Inet6(addr) = &neigh.inet { + if if_ipv6_in_private_subnet(addr) { + prune_ula_for_mac(&neigh.mac, max_ula_per_host, &mut registered, &updater).await; + } + } + if process_new_neigh(&neigh, &updater, &leases, private_subnet_v6).await { registered.insert(key, RegisteredEntry { hostname: hostname.clone(), last_confirmed: Instant::now(), @@ -441,12 +635,24 @@ async fn main() -> Result<(), ()> { } } RouteNetlinkMessage::DelNeighbour(del_neigh) => { - let Some(neigh) = parse_neighbour_message(del_neigh, private_subnet_v4, private_subnet_v6) else { + let Some(neigh) = parse_neighbour_message(del_neigh, private_subnet_v4) else { continue; }; if should_skip_neigh(&neigh) { continue; } + // Remove from GUA keepalive if applicable + if let NeighbourAddress::Inet6(addr) = &neigh.inet { + if is_gua_ipv6(addr) && keepalive_gua { + if let Some(entries) = gua_keepalive.get_mut(&neigh.mac) { + entries.retain(|e| e.addr != *addr); + debug!("GUA keepalive: removed deleted {} for mac {}", addr, neigh.mac); + } + if private_subnet_v6 { + continue; + } + } + } let key = (neigh.mac.clone(), inet_to_string(&neigh.inet)); let stored_hostname = registered.remove(&key).map(|e| e.hostname); debug!("Del neighbour: {:?}", neigh); @@ -477,10 +683,21 @@ async fn main() -> Result<(), ()> { ) .await; } + _ = gua_keepalive_timer.tick() => { + if !keepalive_gua || keepalive_gua_interval == 0 { + continue; + } + probe_gua_keepalive(&gua_keepalive, keepalive_gua_per_host); + prune_gua_keepalive(&mut gua_keepalive, keepalive_gua_interval, keepalive_gua_per_host); + } _ = lease_refresh_timer.tick() => { - if let Ok(new_leases) = op::get_lease() { - leases = new_leases; - debug!("refreshed {} DHCP leases", leases.len()); + match tokio::task::spawn_blocking(|| op::get_lease().map_err(|e| e.to_string())).await { + Ok(Ok(new_leases)) => { + leases = new_leases; + debug!("refreshed {} DHCP leases", leases.len()); + } + Ok(Err(e)) => warn!("failed to refresh DHCP leases from ubus: {}", e), + Err(e) => warn!("DHCP lease refresh task panicked: {}", e), } } } @@ -640,26 +857,83 @@ async fn probe_registered_neighbours(registered: &HashMap<(String, String), Regi } } +/// Send ICMPv6 Echo Request to the newest GUA addresses per host for NUD keepalive. +/// Only the top `per_host` addresses (by most-recently-seen) are probed per MAC. +/// This does NOT publish any records to DNS. +fn probe_gua_keepalive( + gua_keepalive: &HashMap>, + per_host: usize, +) { + let now = Instant::now(); + for (mac, entries) in gua_keepalive.iter() { + // Sort indices by first_seen descending to pick the newest GUAs. + let mut indices: Vec = (0..entries.len()).collect(); + indices.sort_by(|a, b| entries[*b].first_seen.cmp(&entries[*a].first_seen)); + + for &idx in indices.iter().take(per_host) { + let e = &entries[idx]; + // Skip entries confirmed recently (within 30s) + if now.duration_since(e.last_confirmed) < Duration::from_secs(30) { + continue; + } + match send_icmpv6_echo(e.addr, e.ifindex) { + Ok(()) => debug!("GUA keepalive probe: {} ({}) -> {}", e.hostname, mac, e.addr), + Err(err) => debug!("GUA keepalive probe failed for {}: {}", e.addr, err), + } + } + } +} + +/// Remove GUA keepalive entries that haven't been confirmed REACHABLE within 3× the +/// keepalive interval, and drop excess entries beyond `per_host` (oldest first). +fn prune_gua_keepalive( + gua_keepalive: &mut HashMap>, + keepalive_interval: u64, + per_host: usize, +) { + let timeout = Duration::from_secs(keepalive_interval.saturating_mul(3)); + let now = Instant::now(); + gua_keepalive.retain(|_, entries| { + // Remove timed-out entries first. + entries.retain(|e| now.duration_since(e.last_confirmed) < timeout); + // Then keep only the newest `per_host` entries (by first_seen desc). + if entries.len() > per_host { + entries.sort_by(|a, b| b.first_seen.cmp(&a.first_seen)); + entries.truncate(per_host); + } + !entries.is_empty() + }); +} + +fn set_ipv6_unicast_if(socket: &Socket, ifindex: u32) -> std::io::Result<()> { + socket.bind_device_by_index_v6(NonZeroU32::new(ifindex)) +} + +fn set_ip_unicast_if(socket: &Socket, ifindex: u32) -> std::io::Result<()> { + socket.bind_device_by_index_v4(NonZeroU32::new(ifindex)) +} + +fn compute_icmpv4_checksum(packet: &mut [u8]) { + packet[2] = 0; + packet[3] = 0; + let mut sum = 0u32; + for chunk in packet.chunks(2) { + let word = u16::from_be_bytes([chunk[0], *chunk.get(1).unwrap_or(&0)]); + sum = sum.wrapping_add(word as u32); + } + while (sum >> 16) > 0 { + sum = (sum & 0xffff) + (sum >> 16); + } + let checksum = !(sum as u16); + packet[2..4].copy_from_slice(&checksum.to_be_bytes()); +} + fn send_icmpv6_echo(addr: Ipv6Addr, ifindex: u32) -> std::io::Result<()> { let socket = Socket::new(Domain::IPV6, Type::DGRAM, Some(Protocol::ICMPV6))?; socket.set_nonblocking(true)?; // Bind outgoing packet to the specific interface via IPV6_UNICAST_IF so the // kernel NUD state machine updates the correct neighbour entry. - if ifindex != 0 { - let ret = unsafe { - let idx = ifindex as libc::c_int; - libc::setsockopt( - socket.as_raw_fd(), - libc::IPPROTO_IPV6, - libc::IPV6_UNICAST_IF, - &idx as *const libc::c_int as *const libc::c_void, - std::mem::size_of::() as libc::socklen_t, - ) - }; - if ret != 0 { - return Err(std::io::Error::last_os_error()); - } - } + set_ipv6_unicast_if(&socket, ifindex)?; // ICMPv6 Echo Request: type=128, code=0, checksum=0 (kernel computes), id=0, seq=1 let packet: [u8; 8] = [128, 0, 0, 0, 0, 0, 0, 1]; let dest = SockAddr::from(std::net::SocketAddrV6::new(addr, 0, 0, 0)); @@ -671,24 +945,10 @@ fn send_icmpv4_echo(addr: Ipv4Addr, ifindex: u32) -> std::io::Result<()> { let socket = Socket::new(Domain::IPV4, Type::DGRAM, Some(Protocol::ICMPV4))?; socket.set_nonblocking(true)?; // Bind outgoing packet to the specific interface via IP_UNICAST_IF. - if ifindex != 0 { - let ret = unsafe { - let idx = ifindex as libc::c_int; - libc::setsockopt( - socket.as_raw_fd(), - libc::IPPROTO_IP, - libc::IP_UNICAST_IF, - &idx as *const libc::c_int as *const libc::c_void, - std::mem::size_of::() as libc::socklen_t, - ) - }; - if ret != 0 { - return Err(std::io::Error::last_os_error()); - } - } - // ICMPv4 Echo Request: type=8, code=0, checksum (simple for 8 bytes), id=0, seq=1 - // Checksum for [08,00,00,00,00,00,00,01]: ~(0x0800 + 0x0001) = 0xf7fe - let packet: [u8; 8] = [8, 0, 0xf7, 0xfe, 0, 0, 0, 1]; + set_ip_unicast_if(&socket, ifindex)?; + // ICMPv4 Echo Request: type=8, code=0, checksum (computed), id=0, seq=1 + let mut packet: [u8; 8] = [8, 0, 0, 0, 0, 0, 0, 1]; + compute_icmpv4_checksum(&mut packet); let dest = SockAddr::from(std::net::SocketAddrV4::new(addr, 0)); socket.send_to(&packet, &dest)?; Ok(()) @@ -715,7 +975,7 @@ fn is_link_local_ipv6(addr: &NeighbourAddress) -> bool { matches!(addr, NeighbourAddress::Inet6(ip) if (ip.segments()[0] & 0xffc0) == 0xfe80) } -fn parse_neighbour_message(neigh: NeighbourMessage, private_subnet_v4: bool, private_subnet_v6: bool) -> Option { +fn parse_neighbour_message(neigh: NeighbourMessage, private_subnet_v4: bool) -> Option { let state = neigh.header.state; // Filter out static and incomplete entries if matches!(state, NeighbourState::Permanent | NeighbourState::Noarp) { @@ -729,23 +989,16 @@ fn parse_neighbour_message(neigh: NeighbourMessage, private_subnet_v4: bool, pri if is_link_local_ipv6(&addr) { return None; } - match addr { - NeighbourAddress::Inet(addr) => { - if private_subnet_v4 && !if_ipv4_in_private_subnet(&addr) { - return None; - } + // IPv4 private-subnet filter stays here (no keepalive concept for IPv4 GUA) + if let NeighbourAddress::Inet(ipv4) = &addr { + if private_subnet_v4 && !if_ipv4_in_private_subnet(ipv4) { + return None; } - NeighbourAddress::Inet6(addr) => { - if private_subnet_v6 && !if_ipv6_in_private_subnet(&addr) { - return None; - } - } - _ => {} - }; + } let kind = neigh.header.kind; let ifindex = neigh.header.ifindex; let mac = neigh.attributes.iter().find_map(|attr| match attr { - NeighbourAttribute::LinkLocalAddress(mac) => Some(mac.to_owned()), + NeighbourAttribute::LinkLayerAddress(mac) => Some(mac.to_owned()), _ => None, })?; let mac_str = format_mac(mac); @@ -762,11 +1015,11 @@ fn parse_neighbour_message(neigh: NeighbourMessage, private_subnet_v4: bool, pri }) } -async fn dump_neighbours(handle: Handle, private_subnet_v4: bool, private_subnet_v6: bool) -> Result, Error> { +async fn dump_neighbours(handle: Handle, private_subnet_v4: bool) -> Result, Error> { let mut neighbours = handle.neighbours().get().execute(); let mut vec: Vec = Vec::new(); while let Some(route) = neighbours.try_next().await? { - if let Some(neigh) = parse_neighbour_message(route, private_subnet_v4, private_subnet_v6) { + if let Some(neigh) = parse_neighbour_message(route, private_subnet_v4) { if !should_skip_neigh(&neigh) { vec.push(neigh); } diff --git a/ipv6-neigh/src/op.rs b/ipv6-neigh/src/op.rs index 441f6be1..ce9c2d20 100644 --- a/ipv6-neigh/src/op.rs +++ b/ipv6-neigh/src/op.rs @@ -16,7 +16,7 @@ pub fn call_ubus(obj_path: &str, method: &str) -> Result String { hex.as_bytes() .chunks(2) - .map(|chunk| std::str::from_utf8(chunk).unwrap()) + .filter_map(|chunk| std::str::from_utf8(chunk).ok()) .collect::>() .join(":") } diff --git a/luci-app-amlogic/Makefile b/luci-app-amlogic/Makefile index 8bad3bd2..f0ce95f9 100644 --- a/luci-app-amlogic/Makefile +++ b/luci-app-amlogic/Makefile @@ -16,8 +16,8 @@ include $(TOPDIR)/rules.mk PKG_NAME:=luci-app-amlogic -PKG_VERSION:=3.1.312 -PKG_RELEASE:=10 +PKG_VERSION:=3.1.313 +PKG_RELEASE:=11 PKG_LICENSE:=GPL-2.0 License PKG_MAINTAINER:=ophub diff --git a/luci-app-amlogic/htdocs/luci-static/resources/view/amlogic/armcpu.js b/luci-app-amlogic/htdocs/luci-static/resources/view/amlogic/armcpu.js index 4b962af7..4a35ce2a 100644 --- a/luci-app-amlogic/htdocs/luci-static/resources/view/amlogic/armcpu.js +++ b/luci-app-amlogic/htdocs/luci-static/resources/view/amlogic/armcpu.js @@ -27,7 +27,9 @@ return view.extend({ ]); }, - render: function (data) { + // Render a tabbed form section per CPU policy (cluster), with options to + // set the governor and min/max frequency. The form is bound to the amlogic.armcpu UCI section. + render: function (data) { const policies = data[0] || []; // Auto-create the armcpu section when missing, so that on a fresh diff --git a/luci-app-amlogic/htdocs/luci-static/resources/view/amlogic/backup.js b/luci-app-amlogic/htdocs/luci-static/resources/view/amlogic/backup.js index b3e29509..62df5ed8 100644 --- a/luci-app-amlogic/htdocs/luci-static/resources/view/amlogic/backup.js +++ b/luci-app-amlogic/htdocs/luci-static/resources/view/amlogic/backup.js @@ -1,13 +1,10 @@ // SPDX-License-Identifier: GPL-2.0 // Backup Firmware Config + Snapshot management + (optional) KVM switch // -// This page exposes three groups of capabilities: -// 1. Pack the current config into openwrt_config.tar.gz and let the user -// download it; -// 2. List / create / restore / delete etc snapshots (etc-000 / etc-001 are -// the read-only initial / update snapshots and cannot be deleted); -// 3. If the backend reports has_kvm, render the KVM dual-system switch -// button. +// Purpose: pack current /etc config into openwrt_config.tar.gz and download it; +// manage system snapshots (list / create / restore / delete); optionally show +// KVM dual-partition switch when the platform reports has_kvm. +// Backend RPC: /usr/share/rpcd/ucode/luci.amlogic (backup_create, snapshot_*, platform_info, kvm_switch). 'use strict'; 'require view'; @@ -36,17 +33,19 @@ const callPlatform = rpc.declare({ object: 'luci.amlogic', method: 'platform_i // Switch active KVM partition. const callKvmSwitch = rpc.declare({ object: 'luci.amlogic', method: 'kvm_switch' }); +// The view object for the Backup & Snapshot page. return view.extend({ handleSave: null, handleSaveApply: null, handleReset: null, + // Load in parallel: platform info + current snapshot list. load: function () { - // Load in parallel: platform info + current snapshot list. amlogicShared.ensureCss(); return Promise.all([callPlatform(), callSnapList()]); }, + // Render the page: backup config section + snapshot management section + (optional) KVM switch section. render: function (data) { const platform = data[0] || {}; const snapNames = data[1] || []; @@ -61,8 +60,7 @@ return view.extend({ } }); - // Download status hint: uses the warning color; replaced by the - // success message after a successful generation. + // Download status hint; updated to success color after generation completes. const downloadStatus = E('span', { class: 'amlogic-status-err' }); const downloadBtn = E('input', { type: 'button', class: 'cbi-button cbi-button-save', @@ -80,10 +78,7 @@ return view.extend({ // Switch to success color before streaming the blob downloadStatus.className = 'amlogic-status-ok'; downloadStatus.textContent = _('The file Will download automatically.') + ' ' + r.path; - // Step 2: fetch the file as a Blob via cgi-download (bypasses - // ubus message-size limits) and trigger a browser download. - // We use fs.read_direct which internally POST to cgi-download - // with the correct sessionid. + // Fetch the file blob via fs.read_direct and trigger browser download. return fs.read_direct(r.path, 'blob').then(function (blob) { const url = URL.createObjectURL(blob); const a = document.createElement('a'); @@ -104,7 +99,8 @@ return view.extend({ }) }); - const restoreBtn = E('input', { + // Navigate to the upload page where the user can upload a backup file to restore. + const restoreBtn = E('input', { type: 'button', class: 'cbi-button cbi-button-save', value: _('Upload Backup'), click: function () { @@ -112,16 +108,11 @@ return view.extend({ } }); - // Snapshots - // Snapshot grid: uses amlogic-snap-list / amlogic-snap-item / - // amlogic-snap-line classes whose styles adapt to the current theme - // (see amlogic.css). + // Snapshot grid; styles adapt to the current theme via amlogic-snap-* classes. const snapDiv = E('div', { class: 'amlogic-snap-list' }); - // Rebuild the snapshot cards from a list of names: - // - etc-000 / etc-001 are read-only and do not get a Delete button; - // - other snapshots get both Restore and Delete buttons. - // The backend takes the short name (without the "etc-" prefix). + // Render snapshot cards; etc-000/001 are read-only (no Delete button). + // Backend expects the short name without the "etc-" prefix. function renderSnapshots(names) { snapDiv.innerHTML = ''; if (!names || !names.length) { @@ -143,8 +134,7 @@ return view.extend({ if (!confirm(_('You selected a snapshot:') + ' [ ' + n + ' ] , ' + _('Confirm recovery and restart OpenWrt?'))) return; - // Capture the button now: ev.currentTarget is null once the - // promise chain resumes asynchronously. + // Capture btn before entering async chain; ev.currentTarget becomes null after await. const btn = ev.currentTarget; btn.disabled = true; btn.value = _('Restoring...'); @@ -176,8 +166,7 @@ return view.extend({ if (!confirm(_('You selected a snapshot:') + ' [ ' + n + ' ] , ' + _('Confirm delete?'))) return; - // Capture the button: ev.currentTarget is null in the - // async then() that runs after the RPC resolves. + // Capture btn before entering async chain; ev.currentTarget becomes null after await. const btn = ev.currentTarget; btn.disabled = true; btn.value = _('Deleting...'); @@ -198,7 +187,8 @@ return view.extend({ } renderSnapshots(snapNames); - const createSnapBtn = E('input', { + // Create snapshot button above the grid. + const createSnapBtn = E('input', { type: 'button', class: 'cbi-button cbi-button-save', value: _('Create Snapshot'), click: ui.createHandlerFn(view, function (ev) { @@ -212,7 +202,8 @@ return view.extend({ }) }); - const sections = [ + // If the platform supports KVM dual partitions, show a button to trigger partition switch. + const sections = [ E('h2', _('Backup Firmware Config')), E('p', _('Backup OpenWrt config (openwrt_config.tar.gz). Use this file to restore the config in [Manually Upload Update].')), E('div', { class: 'cbi-section' }, [ @@ -239,7 +230,8 @@ return view.extend({ ]) ]; - if (platform.has_kvm) { + // If the platform supports KVM dual partitions, show a button to trigger partition switch. + if (platform.has_kvm) { const kvmBtn = E('input', { type: 'button', class: 'cbi-button cbi-button-save', value: _('Switch System'), diff --git a/luci-app-amlogic/htdocs/luci-static/resources/view/amlogic/backuplist.js b/luci-app-amlogic/htdocs/luci-static/resources/view/amlogic/backuplist.js index bdfdf138..5d3f0e2a 100644 --- a/luci-app-amlogic/htdocs/luci-static/resources/view/amlogic/backuplist.js +++ b/luci-app-amlogic/htdocs/luci-static/resources/view/amlogic/backuplist.js @@ -1,10 +1,10 @@ // SPDX-License-Identifier: GPL-2.0 // Backup file list editor // -// Edits /etc/amlogic_backup_list.conf, which controls which files / directories -// the openwrt-backup script archives. On load, if the file does not exist or -// is empty, the backend prime_backup_list extracts the default BACKUP_LIST -// from /usr/sbin/openwrt-backup as a starting template. +// Purpose: edit /etc/amlogic_backup_list.conf which controls which files/dirs +// the openwrt-backup script archives. If the file is missing or empty, the +// backend seeds it from the default BACKUP_LIST in /usr/sbin/openwrt-backup. +// Backend RPC: /usr/share/rpcd/ucode/luci.amlogic (save_backup_list, prime_backup_list). 'use strict'; 'require view'; @@ -15,24 +15,27 @@ // Persist the user-edited content back to /etc/amlogic_backup_list.conf. const callSave = rpc.declare({ object: 'luci.amlogic', method: 'save_backup_list', params: ['content'] }); -// Pre-fill: if the list file is missing or empty, seed it from the defaults -// embedded in the openwrt-backup script (idempotent: existing content stays). +// Seed the list file from defaults embedded in openwrt-backup if missing or empty. const callPrime = rpc.declare({ object: 'luci.amlogic', method: 'prime_backup_list' }); +// This page uses its own Save button; hide LuCI's default Save/Apply/Reset. return view.extend({ // This page uses its own Save button; hide LuCI's default Save/Apply/Reset. handleSave: null, handleSaveApply: null, handleReset: null, - load: function () { + // Load the existing backup list content to pre-fill the editor. If the file is + load: function () { // Run prime first (idempotent), then read the file content. return callPrime().then(function () { return fs.read('/etc/amlogic_backup_list.conf').catch(function () { return ''; }); }); }, - render: function (text) { + // Render a full-width textarea pre-filled with the existing content, and a Save button + // that persists the changes via RPC. Also include a Back button to return to the main Backup page. + render: function (text) { // Full-width multi-line editor pre-filled with the existing list. const ta = E('textarea', { rows: '30', @@ -50,7 +53,7 @@ return view.extend({ return callSave(ta.value).then(function (r) { if (r && (r.ok || r.code == 0)) { status.textContent = _('Successfully saved.'); - // Wait 0.7s before navigating back so the success text is visible. + // Navigate back after a short delay so the success message is visible. setTimeout(function () { location.href = L.url('admin/system/amlogic/backup'); }, 700); diff --git a/luci-app-amlogic/htdocs/luci-static/resources/view/amlogic/check.js b/luci-app-amlogic/htdocs/luci-static/resources/view/amlogic/check.js index 8c70772e..8243b4c5 100644 --- a/luci-app-amlogic/htdocs/luci-static/resources/view/amlogic/check.js +++ b/luci-app-amlogic/htdocs/luci-static/resources/view/amlogic/check.js @@ -1,9 +1,11 @@ // SPDX-License-Identifier: GPL-2.0 // Online Download Update + Rescue Kernel // -// Flow matches the Lua version: check_* RPCs write progress logs; the final -// log line is a raw HTML button string that we parse and render as a -// real DOM button so the user can explicitly click Download → then Update. +// Purpose: check / download / install plugin, kernel, and firmware updates online. +// Log tail is polled at 1 Hz; the last log line drives a state machine +// (idle → checking → button → installing → done). Terminal log lines contain +// raw HTML button strings which are parsed and rendered as DOM buttons. +// Backend RPC: /usr/share/rpcd/ucode/luci.amlogic (state, check_*, start_*, read_log_tail). 'use strict'; 'require view'; @@ -13,8 +15,10 @@ 'require poll'; 'require view.amlogic.shared as amlogicShared'; -// ── RPCs ──────────────────────────────────────────────────────────────────── +// ── RPCs ───────────────────────────────────────────────────────────────────── +// Query system state (firmware / plugin / kernel versions). const callState = rpc.declare({ object: 'luci.amlogic', method: 'state' }); +// Run plugin / kernel / firmware version check and write progress to the log. const callCheckPlugin = rpc.declare({ object: 'luci.amlogic', method: 'check_plugin', params: ['options'] }); const callCheckKernel = rpc.declare({ object: 'luci.amlogic', method: 'check_kernel', params: ['options'] }); const callCheckFirmware = rpc.declare({ object: 'luci.amlogic', method: 'check_firmware', params: ['options'] }); @@ -22,14 +26,12 @@ const callStartPlugin = rpc.declare({ object: 'luci.amlogic', method: 'start_p const callStartKernel = rpc.declare({ object: 'luci.amlogic', method: 'start_kernel' }); const callStartUpdate = rpc.declare({ object: 'luci.amlogic', method: 'start_update', params: ['amlogic_update_sel'] }); const callStartRescue = rpc.declare({ object: 'luci.amlogic', method: 'start_rescue' }); +// Read the last line (up to 4096 bytes) of the named log file. const callLogTail = rpc.declare({ object: 'luci.amlogic', method: 'read_log_tail', params: ['name'], expect: { line: '' } }); // ── Log-line HTML button parser ────────────────────────────────────────────── -// Returns null when the line is plain text, or an object when it is a button: -// { type: 'firmware-download'|'kernel-download'|'firmware-update'| -// 'kernel-update'|'plugin-update', -// param: string|null, // extracted onclick argument (if any) -// label: string } // text after the closing /> +// Parse an button string from the log; return null for plain-text lines. +// Returned object: { type, param, label } where type identifies the action. function parseButtonLine(line) { if (!line || line.indexOf('= 0; i--) { @@ -182,8 +179,7 @@ function handlePoll(action) { }); } -// Render the sub-button extracted from the log line. The onClick triggers the -// appropriate second-phase RPC and transitions to 'installing'. +// Render the sub-button parsed from the log line and wire up the second-phase RPC. function renderParsedButton(action) { var parsed = action._parsed; var el = action.statusEl; @@ -194,8 +190,7 @@ function renderParsedButton(action) { showSubButton(el, _('Download'), parsed.label, function (ev) { ev.currentTarget.disabled = true; ev.currentTarget.value = _('Downloading...'); - action.state = 'checking'; // re-enter checking loop, wait for Update button - // parsed.param is e.g. "download_3.1.290" — pass directly to the script + action.state = 'checking'; callCheckPlugin(parsed.param).then(function () {}); }); break; @@ -204,10 +199,8 @@ function renderParsedButton(action) { showSubButton(el, _('Download'), parsed.label, function (ev) { ev.currentTarget.disabled = true; ev.currentTarget.value = _('Downloading...'); - action.state = 'checking'; // will re-enter checking loop - callCheckFirmware(parsed.param).then(function () { - // After download, keep polling; log will emit Update button - }); + action.state = 'checking'; + callCheckFirmware(parsed.param).then(function () {}); }); break; @@ -224,10 +217,7 @@ function renderParsedButton(action) { showSubButton(el, _('Update'), parsed.label, function (ev) { ev.currentTarget.disabled = true; ev.currentTarget.value = _('Updating...'); - // Transition to 'installing': poll will show log lines and detect - // the terminal result. callStartUpdate fires the background script - // and returns {code:0} immediately — do NOT use its return value - // to judge success/failure. + // Transition to 'installing'; terminal result comes via log polling. action.state = 'installing'; dom.content(el, E('span', { class: 'amlogic-status-info' }, _('Starting update...'))); callStartUpdate(parsed.param).catch(function () { @@ -307,7 +297,9 @@ return view.extend({ btn.value = idleLabel; } - var btnPlugin = E('input', { + // Each button triggers the first-phase RPC to start the check/download/install process, + // then the poll handler picks up log lines to advance the state machine and render sub-buttons as needed. + var btnPlugin = E('input', { type: 'button', class: 'cbi-button cbi-button-reload', value: _('Only update Amlogic Service'), click: ui.createHandlerFn(this, function (ev) { @@ -332,7 +324,8 @@ return view.extend({ }) }); - var btnKernel = E('input', { + // The kernel and firmware buttons have similar logic, just different RPCs and labels. + var btnKernel = E('input', { type: 'button', class: 'cbi-button cbi-button-reload', value: _('Update system kernel only'), click: ui.createHandlerFn(this, function (ev) { @@ -356,7 +349,8 @@ return view.extend({ }) }); - var btnFirmware = E('input', { + // Firmware update may include both kernel and plugin updates, so it has a more generic label. + var btnFirmware = E('input', { type: 'button', class: 'cbi-button cbi-button-reload', value: _('Complete system update'), click: ui.createHandlerFn(this, function (ev) { @@ -380,7 +374,8 @@ return view.extend({ }) }); - var btnRescue = E('input', { + // Rescue button: triggers the rescue RPC which starts mutual recovery; the log will show progress and terminal status. + var btnRescue = E('input', { type: 'button', class: 'cbi-button cbi-button-reload', value: _('Rescue the original system kernel'), click: ui.createHandlerFn(this, function (ev) { @@ -390,16 +385,10 @@ return view.extend({ if (actRescue.state !== 'idle') return; btn.disabled = true; btn.value = _('Rescuing...'); - // Set installing state BEFORE the call so the poller picks up - // log lines. The script runs in the background (shbg); the RPC - // returns {code:0} immediately and we do NOT use it to judge - // success/failure — the poll loop reads LOG_RESCUE and detects - // the terminal line. + // Set installing state before the call so the poller picks up log lines. actRescue.state = 'installing'; showInfo(actRescue.statusEl, _('Starting rescue...')); return callStartRescue().then(function () { - // RPC triggered OK; result comes via log polling. - // Re-enable the button so the user can retry if needed. btn.disabled = false; btn.value = _('Rescue the original system kernel'); }).catch(function () { diff --git a/luci-app-amlogic/htdocs/luci-static/resources/view/amlogic/config.js b/luci-app-amlogic/htdocs/luci-static/resources/view/amlogic/config.js index 3c02c4cf..4ce34918 100644 --- a/luci-app-amlogic/htdocs/luci-static/resources/view/amlogic/config.js +++ b/luci-app-amlogic/htdocs/luci-static/resources/view/amlogic/config.js @@ -1,14 +1,10 @@ // SPDX-License-Identifier: GPL-2.0 // Plugin Settings // -// Uses form.Map to maintain the `config` section in /etc/config/amlogic, -// covering: -// - OpenWrt firmware GitHub repo, tag keyword, file suffix -// - Kernel download repo / tag / version branch -// - Keep config, auto-write bootloader, shared partition fs type -// This page uses the default form.Map Save / Apply buttons; saving only -// writes UCI, while other pages (Online Update / Manual Upload) read these -// values to drive the actual operations. +// Purpose: configure the `config` section in /etc/config/amlogic via form.Map, +// covering firmware/kernel GitHub repos, tags, version branch, plugin branch, +// keep-config flag, bootloader write flag, and shared partition fs type. +// Backend RPC: /usr/share/rpcd/ucode/luci.amlogic (platform_info, state). 'use strict'; 'require view'; @@ -18,12 +14,13 @@ // Get platform info; only used to display the device PLATFORM tag. const callPlatform = rpc.declare({ object: 'luci.amlogic', method: 'platform_info' }); -// Get runtime state; only kernel_release is used here, to derive default -// values for kernel tags / version branch. +// Query runtime state; kernel_release is used to derive default kernel tag and branch. const callState = rpc.declare({ object: 'luci.amlogic', method: 'state' }); +// This page uses its own Save button; hide LuCI's default Save/Apply/Reset. return view.extend({ - load: function () { + // Load platform info + state in parallel, along with the UCI config (we will read/write it). + load: function () { return Promise.all([ callPlatform(), callState(), @@ -31,7 +28,8 @@ return view.extend({ ]); }, - render: function (data) { + // Render form options bound to the amlogic.config NamedSection, and pre-fill some defaults based on platform/state. + render: function (data) { const platform = data[0] || {}; const state = data[1] || {}; @@ -40,7 +38,8 @@ return view.extend({ uci.add('amlogic', 'amlogic', 'config'); } - const m = new form.Map('amlogic', _('Plugin Settings'), + // Build a form.Map bound to the amlogic.config NamedSection, with options for various plugin settings. + const m = new form.Map('amlogic', _('Plugin Settings'), _('You can customize the github.com download repository of OpenWrt files and kernels in [Online Download Update].') + '
' + _('Tip: The same files as the current OpenWrt system\'s BOARD (such as rock5b) and kernel (such as 5.10) will be downloaded.')); const o = m.section(form.NamedSection, 'config', 'amlogic'); @@ -82,11 +81,8 @@ return view.extend({ kpath.default = 'https://github.com/breakingbadboy/OpenWrt'; kpath.rmempty = false; - // 6. Kernel tags - the list depends on currently saved kpath value. - // The breakingbadboy/OpenWrt repo only ships rk3588/rk35xx/stable; - // ophub/kernel additionally ships flippy/h6/beta. The default value is - // auto-derived from -rk3588 / -rk35xx / -h6 keywords in `uname` so the - // user does not have to pick the wrong tag. + // 6. Kernel tags; available tags depend on the selected kernel repo. + // Default is auto-derived from kernel_release suffixes (-rk3588/-rk35xx/-h6). const currentKpath = uci.get('amlogic', 'config', 'amlogic_kernel_path') || 'https://github.com/breakingbadboy/OpenWrt'; const knownTags = { @@ -99,7 +95,7 @@ return view.extend({ knownTags.kernel_h6 = 'kernel_h6 [Allwinner H6 Kernel]'; knownTags.kernel_beta = 'kernel_beta [Beta Kernel]'; } - // Determine default tag from existing config or uname + // Determine default tag from saved config or kernel_release uname string. let kernelTagDefault = uci.get('amlogic', 'config', 'amlogic_kernel_tags') || ''; if (!kernelTagDefault) { const u = state.kernel_release || ''; diff --git a/luci-app-amlogic/htdocs/luci-static/resources/view/amlogic/info.js b/luci-app-amlogic/htdocs/luci-static/resources/view/amlogic/info.js index ce9f8d27..cec86c7c 100644 --- a/luci-app-amlogic/htdocs/luci-static/resources/view/amlogic/info.js +++ b/luci-app-amlogic/htdocs/luci-static/resources/view/amlogic/info.js @@ -1,18 +1,16 @@ // SPDX-License-Identifier: GPL-2.0 // Plugin landing / info page // -// Renders the plugin LOGO, three quick-link badges (packit project, author -// homepage, plugin repository), the feature summary and the list of -// supported boxes. The page does not read or write any UCI config; it only -// calls the author RPC to obtain the author's repository URL. +// Purpose: render the plugin logo, badge links, feature summary, and supported-box list. +// Calls sync_menu on load to update sidebar menu flags from runtime platform detection. +// Backend RPC: /usr/share/rpcd/ucode/luci.amlogic (author, sync_menu, state). 'use strict'; 'require view'; 'require rpc'; 'require view.amlogic.shared as amlogicShared'; -// Extract OPENWRT_AUTHOR from /usr/sbin/openwrt-update-amlogic and return the -// author's GitHub repo URL, used when the user clicks author.svg. +// Extract OPENWRT_AUTHOR from /usr/sbin/openwrt-update-amlogic and return the author's GitHub repo URL. const callAuthor = rpc.declare({ object: 'luci.amlogic', method: 'author', @@ -20,7 +18,6 @@ const callAuthor = rpc.declare({ }); // Sync menu_install / menu_armcpu UCI flags from runtime platform detection. -// Called on every info page load so the sidebar menu is correct from first visit. const callSyncMenu = rpc.declare({ object: 'luci.amlogic', method: 'sync_menu' }); // Get runtime state to determine plugin_branch for the language badge. @@ -53,20 +50,18 @@ function openExternal(url) { if (!w) window.location.href = url; } +// This is a read-only info page, so we disable the top Save / Apply / Reset buttons by setting the handlers to null. return view.extend({ // Read-only info page: disable the top Save / Apply / Reset buttons. handleSave: null, handleSaveApply: null, handleReset: null, - load: function () { - // Call sync_menu and wait for it. sync_menu writes menu_install / - // menu_armcpu into UCI and clears the LuCI index cache on the server. - // On first boot from USB the install menu may be missing from the - // browser-side navigation (built before sync_menu ran). After the RPC - // returns we check if the sidebar already has an install link; if not, - // reload once so the browser re-fetches the updated menu tree. - // A URL hash flag (#menu-synced) prevents an infinite reload loop. + // On load, call sync_menu to update sidebar menu flags from runtime platform detection. If the install menu becomes + // visible but is not in the current nav, reload once to pick up the new menu (guarded by #menu-synced hash). + load: function () { + // Call sync_menu; if the install menu is now visible on the server but + // absent in the browser nav, reload once (guarded by #menu-synced hash). const alreadyReloaded = window.location.hash === '#menu-synced'; const syncMenuPromise = callSyncMenu().then(function (res) { if (alreadyReloaded) return; @@ -74,7 +69,7 @@ return view.extend({ // Check if the sidebar navigation already contains the install link. const installLink = document.querySelector('a[href*="amlogic/install"]'); if (!installLink) { - // Server has updated the index cache; reload to pick up new nav. + // Server-side index cache updated; reload to pick up new nav. window.location.replace(window.location.pathname + '#menu-synced'); window.location.reload(); } @@ -83,7 +78,8 @@ return view.extend({ return Promise.all([callAuthor(), syncMenuPromise]).then(function (r) { return r[0]; }); }, - render: function (authorUrl) { + // Render the plugin info: logo, badge links, feature summary, and supported-box list. + render: function (authorUrl) { // Inject the theme stylesheet on first entry so amlogic-* classes work. amlogicShared.ensureCss(); const res = L.resource('amlogic'); diff --git a/luci-app-amlogic/htdocs/luci-static/resources/view/amlogic/install.js b/luci-app-amlogic/htdocs/luci-static/resources/view/amlogic/install.js index 2d9ca9ed..78754ca2 100644 --- a/luci-app-amlogic/htdocs/luci-static/resources/view/amlogic/install.js +++ b/luci-app-amlogic/htdocs/luci-static/resources/view/amlogic/install.js @@ -1,12 +1,9 @@ // SPDX-License-Identifier: GPL-2.0 // Install OpenWrt to EMMC // -// The dropdown lists common devices from the backend model database, plus an -// "Enter the dtb file name" option. When the user picks the manual option and -// fills in dtb / soc / uboot_overload, the frontend assembles the string -// "id@dtb:soc:uboot" and passes it to the start_install RPC, which invokes -// openwrt-install-amlogic to perform the actual installation. The install log -// tail is polled while the work runs. +// Purpose: let the user select a device model (or enter dtb/soc/uboot manually), +// then invoke start_install and poll the log tail until success or failure. +// Backend RPC: /usr/share/rpcd/ucode/luci.amlogic (model_database, start_install, read_log_tail). 'use strict'; 'require view'; @@ -16,18 +13,17 @@ 'require poll'; 'require view.amlogic.shared as amlogicShared'; -// Read the model database (parsed by the backend from -// /usr/share/amlogic/model_database.txt). +// Read the model database (parsed by the backend from /usr/share/amlogic/model_database.txt). const callDB = rpc.declare({ object: 'luci.amlogic', method: 'model_database', expect: { entries: [] } }); -// Start install; the parameter is the composed string "id@dtb:soc:uboot", -// kept compatible with the original Lua plugin. +// Start install; parameter is the composed string "id@dtb:soc:uboot". const callStartInst = rpc.declare({ object: 'luci.amlogic', method: 'start_install', params: ['amlogic_install_sel'] }); // Read the last line of the named log (here we use the 'install' log). const callLogTail = rpc.declare({ object: 'luci.amlogic', method: 'read_log_tail', params: ['name'], expect: { line: '' } }); +// This page uses its own Install button; hide LuCI's default Save/Apply/Reset. return view.extend({ handleSave: null, handleSaveApply: null, @@ -37,10 +33,10 @@ return view.extend({ return callDB(); }, - render: function (entries) { + // Render the plugin info: logo, badge links, feature summary, and supported-box list. + render: function (entries) { amlogicShared.ensureCss(); - // Model dropdown: 0 = placeholder, 99 = manual entry, anything in between - // is a model from the database. + // Model dropdown: 0 = placeholder, 99 = manual entry, others from database. const sel = E('select', { style: 'width:auto', name: 'amlogic_soc', id: 'amlogic_soc' }); sel.appendChild(E('option', { value: '0' }, _('Select List'))); (entries || []).forEach(function (e) { @@ -89,12 +85,10 @@ return view.extend({ click: ui.createHandlerFn(this, function (ev) { if (installing) return; const text = sel.options[sel.selectedIndex].text; - // Confirm twice: install only proceeds after the user clicks - // "Start install?". + // Confirm dialog; install proceeds only after user confirms. if (!confirm(_('You have chosen:') + ' ' + text + ', ' + _('Start install?'))) return; - // When manual entries are blank, dtb falls back to auto_dtb so the - // backend can autodetect. + // dtb falls back to auto_dtb when the manual field is blank. const dtbVal = dtbInput.value || 'auto_dtb'; const socVal = socInput.value || ''; const ubVal = ubootInput.value || ''; @@ -114,8 +108,7 @@ return view.extend({ }) }); - // Poll the install log tail once per second to surface backend progress - // and detect terminal success/failure keywords. + // Poll the install log once per second; detect success/failure keywords. poll.add(function () { return callLogTail('install').then(function (line) { if (!line || line === '\n') { diff --git a/luci-app-amlogic/htdocs/luci-static/resources/view/amlogic/log.js b/luci-app-amlogic/htdocs/luci-static/resources/view/amlogic/log.js index ec2170fa..0a3a5be8 100644 --- a/luci-app-amlogic/htdocs/luci-static/resources/view/amlogic/log.js +++ b/luci-app-amlogic/htdocs/luci-static/resources/view/amlogic/log.js @@ -1,10 +1,10 @@ // SPDX-License-Identifier: GPL-2.0 // Service log viewer // -// Reads the full content of the amlogic service log (name=main) into a -// read-only textarea and refreshes it every 2 seconds. Provides controls to -// stop / restart polling, clear the log, and download the current contents -// as a Blob in the browser. +// Purpose: read the full amlogic service log into a read-only textarea and +// refresh it every 2 seconds; provide controls to pause/resume polling, clear +// the log, and download the current contents as a plain-text file. +// Backend RPC: /usr/share/rpcd/ucode/luci.amlogic (read_log_full, del_log). 'use strict'; 'require view'; @@ -19,6 +19,8 @@ const callLogFull = rpc.declare({ object: 'luci.amlogic', method: 'read_log_full // Clear the service main log. const callDelLog = rpc.declare({ object: 'luci.amlogic', method: 'del_log' }); +// This page uses its own Start/Stop Refresh buttons and does not need the default Save/Apply/Reset buttons, +// so we disable them by setting the handlers to null. return view.extend({ handleSave: null, handleSaveApply: null, @@ -66,8 +68,7 @@ return view.extend({ type: 'button', class: 'cbi-button cbi-button-save', value: _('Download Log'), click: function () { - // Wrap the textarea content in a Blob and trigger a browser - // download via a temporary ; avoids a second server fetch. + // Wrap textarea content in a Blob and trigger a browser download via a temporary . const blob = new Blob([ta.value || ''], { type: 'text/plain' }); const url = URL.createObjectURL(blob); const a = document.createElement('a'); diff --git a/luci-app-amlogic/htdocs/luci-static/resources/view/amlogic/poweroff.js b/luci-app-amlogic/htdocs/luci-static/resources/view/amlogic/poweroff.js index a195089b..c7590076 100644 --- a/luci-app-amlogic/htdocs/luci-static/resources/view/amlogic/poweroff.js +++ b/luci-app-amlogic/htdocs/luci-static/resources/view/amlogic/poweroff.js @@ -1,10 +1,9 @@ // SPDX-License-Identifier: GPL-2.0 // Safe Power Off page // -// On click, calls the backend poweroff RPC (which finally executes the system -// `poweroff` command) and runs a 5-second on-page countdown so the user knows -// the device is shutting down. When the countdown ends, the status switches -// to "Powered off" and a power-off icon is shown. +// Purpose: call the backend poweroff RPC then run a 5-second on-page countdown; +// switch status to "Powered off" and show a power-off icon when done. +// Backend RPC: /usr/share/rpcd/ucode/luci.amlogic (poweroff). 'use strict'; 'require view'; @@ -15,15 +14,16 @@ // Trigger the backend poweroff RPC. const callPoweroff = rpc.declare({ object: 'luci.amlogic', method: 'poweroff' }); +// This page uses its own PowerOff button and does not need the default Save/Apply/Reset buttons, +// so we disable them by setting the handlers to null. return view.extend({ - handleSave: null, - handleSaveApply: null, - handleReset: null, + handleSave: null, + handleSaveApply: null, + handleReset: null, render: function () { amlogicShared.ensureCss(); - // Status text: uses the warning color amlogic-status-err which adapts - // to dark mode automatically. + // Status text; amlogic-status-err color adapts to dark mode automatically. const status = E('span', { class: 'amlogic-status-err', style: 'margin-left:1em' }); // Power-off icon, hidden by default; shown only after the countdown ends. const icon = E('img', { @@ -32,7 +32,8 @@ return view.extend({ style: 'width:32px; height:32px; max-width:32px; display:none; margin-left:1em; vertical-align:middle' }); - const btn = E('input', { + // PowerOff button click handler: confirm twice, then call RPC and start countdown on success. + const btn = E('input', { type: 'button', class: 'cbi-button cbi-button-remove', value: _('Perform PowerOff'), click: ui.createHandlerFn(this, function (ev) { @@ -41,7 +42,7 @@ return view.extend({ return; ev.currentTarget.disabled = true; return callPoweroff().then(function () { - // Backend has fired poweroff; show a 5-second countdown to the user. + // Show a 5-second countdown; replace with "Powered off" and icon when done. status.textContent = _('Powering off, please wait...'); let n = 5; const t = setInterval(function () { diff --git a/luci-app-amlogic/htdocs/luci-static/resources/view/amlogic/shared.js b/luci-app-amlogic/htdocs/luci-static/resources/view/amlogic/shared.js index b204488a..b9c011d7 100644 --- a/luci-app-amlogic/htdocs/luci-static/resources/view/amlogic/shared.js +++ b/luci-app-amlogic/htdocs/luci-static/resources/view/amlogic/shared.js @@ -1,20 +1,15 @@ // SPDX-License-Identifier: GPL-2.0 -// // Shared helper module for all luci-app-amlogic JS views. -// Purpose: on first page load, inject the theme-aware stylesheet amlogic.css -// into (which contains .amlogic-status-* status colors, .amlogic-snap-* -// snapshot cards, and prefers-color-scheme dark mode overrides) so every view -// can rely on the same set of classes instead of duplicating inline styles. +// +// Purpose: inject the theme-aware stylesheet amlogic.css into on first +// page load so every view can use .amlogic-status-* / .amlogic-snap-* classes +// and prefers-color-scheme dark mode overrides without duplicating inline styles. 'use strict'; 'require baseclass'; return baseclass.extend({ - // Inject the shared stylesheet (idempotent). - // We tag the element with a fixed id 'amlogic-shared-css' so that - // subsequent calls return immediately and avoid appending it twice. The - // stylesheet path is resolved via L.resource() to - // /luci-static/resources/view/amlogic/amlogic.css. + // Inject amlogic.css once; guarded by element id to prevent duplicates. ensureCss: function () { if (document.getElementById('amlogic-shared-css')) return; const link = document.createElement('link'); diff --git a/luci-app-amlogic/htdocs/luci-static/resources/view/amlogic/upload.js b/luci-app-amlogic/htdocs/luci-static/resources/view/amlogic/upload.js index 4bad71e5..37be15fd 100644 --- a/luci-app-amlogic/htdocs/luci-static/resources/view/amlogic/upload.js +++ b/luci-app-amlogic/htdocs/luci-static/resources/view/amlogic/upload.js @@ -1,17 +1,11 @@ // SPDX-License-Identifier: GPL-2.0 // Manually Upload Update // -// Features: -// 1. Use ui.uploadFile() to open the browser-native file picker and upload -// to the path returned by the backend (default /tmp/upload/); -// 2. Show a table of files in the upload directory (name / mtime / mode / -// size) with Remove, Install (ipk) and Restore (config backup) actions; -// 3. Dynamically reveal "Update firmware" / "Replace kernel" buttons based -// on which file types are currently present in the upload dir; all -// buttons share doButton() to keep disabled / text / status transitions -// consistent; -// 4. Concurrently poll the firmware/kernel log tails and display the -// current firmware version. +// Purpose: upload local files (firmware / kernel / ipk / config backup) to the server, +// list them, and trigger install / restore / update actions. Chunked upload uses +// a custom ubus RPC instead of cgi-upload to avoid session ACL issues. +// Backend RPC: /usr/share/rpcd/ucode/luci.amlogic (upload_path, list_uploads, upload_chunk, +// delete_upload, install_upload, start_update, start_kernel, read_log_tail, state). 'use strict'; 'require view'; @@ -22,43 +16,40 @@ 'require fs'; 'require view.amlogic.shared as amlogicShared'; -// Query the upload directory the backend allows (default /tmp/upload/, the -// backend ensures it exists). -const callUploadPath = rpc.declare({ object: 'luci.amlogic', method: 'upload_path', - expect: { path: '/tmp/upload/' } }); +// Query the upload directory the backend allows (default /tmp/upload/, the backend ensures it exists). +const callUploadPath = rpc.declare({ object: 'luci.amlogic', method: 'upload_path', expect: { path: '/tmp/upload/' } }); // List files in the upload dir plus flags about installable firmware/kernel/cfg. const callList = rpc.declare({ object: 'luci.amlogic', method: 'list_uploads' }); // Delete a named file in the upload dir. -const callDelete = rpc.declare({ object: 'luci.amlogic', method: 'delete_upload', - params: ['name'] }); -// Install one uploaded file (ipk) or restore a config backup; the backend -// dispatches by suffix. -const callInstall = rpc.declare({ object: 'luci.amlogic', method: 'install_upload', - params: ['name'] }); +const callDelete = rpc.declare({ object: 'luci.amlogic', method: 'delete_upload', params: ['name'] }); +// Install one uploaded file (ipk) or restore a config backup; the backend dispatches by suffix. +const callInstall = rpc.declare({ object: 'luci.amlogic', method: 'install_upload', params: ['name'] }); // Chunked upload via our own RPC (sidesteps cgi-upload's session ACL flow). -const callUploadChunk = rpc.declare({ object: 'luci.amlogic', method: 'upload_chunk', - params: ['name', 'data', 'append'] }); +const callUploadChunk = rpc.declare({ object: 'luci.amlogic', method: 'upload_chunk', params: ['name', 'data', 'append'] }); // Run the full firmware update flow with parameters auto@updated@/tmp. -const callStartUpd = rpc.declare({ object: 'luci.amlogic', method: 'start_update', - params: ['amlogic_update_sel'] }); +const callStartUpd = rpc.declare({ object: 'luci.amlogic', method: 'start_update', params: ['amlogic_update_sel'] }); // Run the kernel-only replace flow. const callStartKnl = rpc.declare({ object: 'luci.amlogic', method: 'start_kernel' }); // Tail a named log to surface progress during install/update. -const callLogTail = rpc.declare({ object: 'luci.amlogic', method: 'read_log_tail', - params: ['name'], expect: { line: '' } }); +const callLogTail = rpc.declare({ object: 'luci.amlogic', method: 'read_log_tail', params: ['name'], expect: { line: '' } }); // Read system state (this page only uses current_firmware_version). const callState = rpc.declare({ object: 'luci.amlogic', method: 'state' }); +// This page uses its own Upload/Install buttons and does not need the default Save/Apply/Reset buttons, +// so we disable them by setting the handlers to null. return view.extend({ handleSave: null, handleSaveApply: null, handleReset: null, + // On load, ensure the CSS is injected and load the upload path from the backend. load: function () { amlogicShared.ensureCss(); return callUploadPath(); }, + // Render the upload page: file input + upload button, table of existing uploads with Remove + Install/Restore buttons, + // and firmware/kernel update buttons if applicable. Also start polling the install/update log tails. render: function (path) { const view = this; @@ -87,11 +78,9 @@ return view.extend({ const fwVer = E('span', _('Collecting data...')); function doButton(btn, busyText, fail, fn) { - // Shared button state machine: disabled -> RPC -> updating/fail label. - // On success (code==0) the button stays in busyText state and - // remains disabled — the device is about to reboot, so there is - // nothing to restore. Only on failure do we surface an error and - // re-enable the button so the user can retry. + // Shared button state machine: disabled → RPC → busyText or fail label. + // On success (code==0) the button stays in busyText state (device rebooting); + // on failure, re-enable so the user can retry. btn.disabled = true; btn.value = busyText; return Promise.resolve(fn()).then(function (r) { if (!r || r.code !== 0) { @@ -106,9 +95,8 @@ return view.extend({ } function refreshList() { - // Refresh the file table and the hint text; the firmware / kernel - // buttons are shown or hidden based on the has_firmware / has_kernel - // flags reported by the backend. + // Refresh the file table; show/hide firmware/kernel update buttons + // based on has_firmware / has_kernel flags from the backend. return callList().then(function (info) { dom.content(tableContainer, buildTable(info.items || [], info.path)); let parts = []; @@ -124,10 +112,9 @@ return view.extend({ }); } + // Helper to build the file table with Remove + Install/Restore buttons per row. function buildTable(items, dir) { - // We expose three actions per row: remove any file, install ipk, and - // restore a config backup. Other types (firmware, kernel archive) - // are listed but have no individual install entry. + // File table: expose Remove + Install (ipk) / Restore (config backup) per row. const tbl = E('table', { class: 'table cbi-section-table' }, [ E('tr', { class: 'tr cbi-section-table-titles' }, [ E('th', { class: 'th' }, _('File name')), @@ -200,27 +187,13 @@ return view.extend({ return tbl; } - // File upload widget. We deliberately avoid /cgi-bin/cgi-upload here - // because it requires the LuCI session to carry a `file` ACL entry - // for the chosen target path, which depends on rpcd ACL bookkeeping - // being live for this session — historically a fragile path. - // Instead we slice the picked file into ~256 KB chunks, base64-encode - // each chunk and POST them through our own ubus method - // `luci.amlogic.upload_chunk`. The ucode handler runs as root and - // owns the upload directory, so no separate file-write ACL is - // involved. Permission is governed solely by the existing - // `upload_chunk` entry in the rpcd ACL `write.ubus` block. - // Chunk size for ubus payload. ubus has a per-request size limit - // (~64 KB) — at 256 KB raw → ~344 KB base64 the request was silently - // dropped and luci returned "No related RPC reply". 32 KB raw → - // ~44 KB base64 leaves comfortable headroom. + // Chunked file upload via luci.amlogic.upload_chunk RPC (avoids cgi-upload session ACL). + // 32 KB raw per chunk → ~44 KB base64, safely under the ubus per-request size limit. const CHUNK_SIZE = 32 * 1024; const uploadStatus = E('span'); const uploadFileName = E('span', { style: 'margin-right:1em' }); - // Encode a Uint8Array as base64. btoa() expects a binary string, so - // we walk through the bytes in 32k slices to avoid argument-list - // limits in browsers. + // Encode a Uint8Array as base64 in 32 KB slices to avoid call-stack limits. function bytesToB64(buf) { let s = ''; for (let i = 0; i < buf.length; i += 0x8000) @@ -228,6 +201,7 @@ return view.extend({ return btoa(s); } + // Upload a file in chunks; on success, refresh the file list to show the new upload. function uploadFile(file, target, basename) { const total = file.size; let offset = 0; @@ -256,6 +230,7 @@ return view.extend({ return step(); } + // Hidden file input to trigger the file picker dialog; on file selection, start the chunked upload. const hiddenFileInput = E('input', { type: 'file', style: 'display:none', change: ui.createHandlerFn(this, function (ev) { @@ -279,8 +254,7 @@ return view.extend({ click: function () { hiddenFileInput.click(); } }); - // Polling: log tails + version - // Pull the last line of both firmware and kernel logs once per second. + // Polling: pull firmware and kernel log tails once per second. poll.add(function () { return Promise.all([ callLogTail('firmware').then(function (l) { @@ -315,8 +289,7 @@ return view.extend({ E('div', { class: 'cbi-section' }, [ E('p', { style: 'text-align:center' }, _('After uploading firmware (.img/.img.gz/.img.xz/.7z suffix) or kernel files (3 kernel files), the update button will be displayed.')), - // Single centered cell: when fwBtn / knBtn are hidden the row - // would otherwise show an empty left column. + // Single centered cell: when fwBtn / knBtn are hidden the row would otherwise show an empty left column. E('div', { style: 'text-align:center' }, [fwBtn, knBtn, ' ', fwVer, ' ', fwLog, knLog]) ]) diff --git a/luci-app-amlogic/root/usr/share/amlogic/amlogic_check_firmware.sh b/luci-app-amlogic/root/usr/share/amlogic/amlogic_check_firmware.sh index 1f613023..732b7a90 100755 --- a/luci-app-amlogic/root/usr/share/amlogic/amlogic_check_firmware.sh +++ b/luci-app-amlogic/root/usr/share/amlogic/amlogic_check_firmware.sh @@ -1,16 +1,10 @@ #!/bin/bash -#================================================================== -# This file is licensed under the terms of the GNU General Public -# License version 2. This program is licensed "as is" without any -# warranty of any kind, whether express or implied. +# SPDX-License-Identifier: GPL-2.0 +# amlogic_check_firmware.sh — check and download OpenWrt firmware updates. # -# This file is a part of the luci-app-amlogic plugin -# https://github.com/ophub/luci-app-amlogic -# -# Description: Check and update OpenWrt firmware -# Copyright (C) 2021- https://github.com/unifreq/openwrt_packit -# Copyright (C) 2021- https://github.com/ophub/luci-app-amlogic -#================================================================== +# Purpose: query the configured GitHub repository for the latest firmware +# release, compare with the local version, and download to /tmp/amlogic +# if a newer version is available. # Set a fixed value check_option="${1}" diff --git a/luci-app-amlogic/root/usr/share/amlogic/amlogic_check_kernel.sh b/luci-app-amlogic/root/usr/share/amlogic/amlogic_check_kernel.sh index 6cd8a0f8..a8c02261 100755 --- a/luci-app-amlogic/root/usr/share/amlogic/amlogic_check_kernel.sh +++ b/luci-app-amlogic/root/usr/share/amlogic/amlogic_check_kernel.sh @@ -1,16 +1,10 @@ #!/bin/bash -#================================================================== -# This file is licensed under the terms of the GNU General Public -# License version 2. This program is licensed "as is" without any -# warranty of any kind, whether express or implied. +# SPDX-License-Identifier: GPL-2.0 +# amlogic_check_kernel.sh — check and download OpenWrt kernel updates. # -# This file is a part of the luci-app-amlogic plugin -# https://github.com/ophub/luci-app-amlogic -# -# Description: Check and update OpenWrt Kernel -# Copyright (C) 2021- https://github.com/unifreq/openwrt_packit -# Copyright (C) 2021- https://github.com/ophub/luci-app-amlogic -#================================================================== +# Purpose: query the configured GitHub repository for the latest kernel +# packages, compare with the running kernel version, and download to +# /tmp/amlogic if a newer version is available. # Set a fixed value check_option="${1}" diff --git a/luci-app-amlogic/root/usr/share/amlogic/amlogic_check_plugin.sh b/luci-app-amlogic/root/usr/share/amlogic/amlogic_check_plugin.sh index 6edd6996..6ed52bf3 100755 --- a/luci-app-amlogic/root/usr/share/amlogic/amlogic_check_plugin.sh +++ b/luci-app-amlogic/root/usr/share/amlogic/amlogic_check_plugin.sh @@ -1,16 +1,10 @@ #!/bin/bash -#================================================================== -# This file is licensed under the terms of the GNU General Public -# License version 2. This program is licensed "as is" without any -# warranty of any kind, whether express or implied. +# SPDX-License-Identifier: GPL-2.0 +# amlogic_check_plugin.sh — check and download luci-app-amlogic plugin updates. # -# This file is a part of the luci-app-amlogic plugin -# https://github.com/ophub/luci-app-amlogic -# -# Description: Check and update luci-app-amlogic plugin -# Copyright (C) 2021- https://github.com/unifreq/openwrt_packit -# Copyright (C) 2021- https://github.com/ophub/luci-app-amlogic -#================================================================== +# Purpose: query the configured GitHub repository for the latest plugin +# release (JS or Lua branch), compare with the installed version, and +# download the IPK/APK package to /tmp/amlogic if a newer version is available. # Set a fixed value check_option="${1}" diff --git a/luci-app-amlogic/root/usr/share/rpcd/ucode/luci.amlogic b/luci-app-amlogic/root/usr/share/rpcd/ucode/luci.amlogic index d5b177b6..0e373aac 100644 --- a/luci-app-amlogic/root/usr/share/rpcd/ucode/luci.amlogic +++ b/luci-app-amlogic/root/usr/share/rpcd/ucode/luci.amlogic @@ -1,8 +1,9 @@ // SPDX-License-Identifier: GPL-2.0 +// luci.amlogic — ucode RPC bridge for luci-app-amlogic (JS branch). // -// luci.amlogic - ucode RPC bridge for the JS-based luci-app-amlogic. -// -// Helpers +// Purpose: expose all plugin operations (install, update, backup, snapshot, +// CPU policy, log read, upload) as ubus methods consumed by the JS views. +// Each method is registered in the `methods` table at the bottom of this file. 'use strict'; import { popen, readfile, writefile, mkstemp, stat, glob, error as fserror, @@ -44,19 +45,14 @@ function sh(cmd) { // Run a shell command, return exit code (0 on success) function shcall(cmd) { - // `sh -c "...; echo $?"` — last line is exit code. const out = sh('(' + cmd + ') >/dev/null 2>&1; echo $?'); const lines = split(out, '\n'); const last = lines[length(lines) - 1] ?? '1'; return int(last); } -// Run a shell command in the background (fire-and-forget). -// Returns immediately so rpcd is not blocked, allowing concurrent -// read_log_tail / read_log_full polls to succeed. +// Run cmd in the background; close stdin so shbg does not block rpcd. function shbg(cmd) { - // Detach from rpcd: close stdin so interactive prompts don't block, - // but let the command manage its own stdout/stderr (e.g. redirect to log). const wrapper = '(' + cmd + ') /removable to distinguish them. -// mmcblk* and nvme* are always internal storage on these platforms. +// Detect whether the root fs is on internal storage (eMMC/NVMe/fixed disk). +// Returns true if already installed; sd* removable flag distinguishes USB vs SATA. function root_on_internal_storage() { const root_pt = trim(sh("df / | tail -n1 | awk '{print $1}' | awk -F '/' '{print $3}'")); if (!root_pt) return false; @@ -108,17 +98,15 @@ function root_on_internal_storage() { return true; } if (match(root_pt, /^[hsv]d[a-z][0-9]+$/)) { - // Strip trailing partition digit(s) to get base device (e.g. sda2 → sda) + // Strip partition digit(s) to get base device (e.g. sda2 → sda); check removable flag. const base_dev = replace(root_pt, /[0-9]+$/, ''); const removable = trim(sh('cat /sys/block/' + shq(base_dev) + '/removable 2>/dev/null')); - // removable=0 → internal SATA/fixed disk; removable=1 → USB/removable - return (removable == '0'); + return (removable == '0'); // 0 = internal SATA; 1 = USB/removable } return false; } -// Pick the helper script names for the current platform (as the original -// controller did). +// Return platform-specific helper script names for install/update/kernel operations. function platform_scripts() { const p = platform(); if (index(p, 'rockchip') >= 0) @@ -192,8 +180,7 @@ function read_log_tail(name) { function read_log_full(name) { const p = _safe_log_path(name ?? 'main'); if (!p) return ''; - // Use popen+cat for consistency with read_log_tail; readfile() may be - // stale inside the long-lived rpcd ucode process. + // Use popen+cat to bypass rpcd process-level file cache. const pipe = popen('cat ' + p + ' 2>/dev/null', 'r'); if (!pipe) return ''; const t = pipe.read('all') ?? ''; @@ -268,9 +255,7 @@ function m_platform_info() { const can_install = (index(p, 'amlogic') >= 0 || index(p, 'allwinner') >= 0 || index(m, 'yes') >= 0); // can_armcpu: all platforms except qemu support CPU frequency settings const can_armcpu = (index(p, 'qemu') < 0); - // is_installed: root fs is on an internal storage device (eMMC/NVMe/fixed disk). - // sd* devices may be USB (removable=1) or internal SATA (removable=0); check - // /sys/block//removable to distinguish. When true the Install menu is hidden. + // is_installed: root fs on internal storage means already installed; hide Install menu. const is_installed = root_on_internal_storage(); return { platform: p, @@ -312,16 +297,13 @@ function m_snapshot_list() { return { names }; } -// Mirrors the Lua "Edit List" button's side-effect: if the backup-list file is -// missing or empty, pre-populate it from the BACKUP_LIST shell variable inside -// /usr/sbin/openwrt-backup. +// Seed /etc/amlogic_backup_list.conf from the default BACKUP_LIST in openwrt-backup if absent. function m_prime_backup_list() { shcall("[ -s " + BACKUP_LIST + " ] || sed -n \"/BACKUP_LIST='/,/.*'$/p\" /usr/sbin/openwrt-backup | sed -e \"s/BACKUP_LIST=\\(.*\\)/\\1/; s/'//g; s/\\\\\\\\//g; s/ //g\" > " + BACKUP_LIST + " 2>/dev/null"); return { code: 0 }; } -// Create a read-only snapshot of /etc into /.snapshots/etc-. -// Mirrors the `Create Snapshot` button in amlogic_backup.lua. +// Create a read-only btrfs snapshot of /etc into /.snapshots/. function m_snapshot_create() { shcall("btrfs subvolume snapshot -r /etc /.snapshots/etc-$(date +%m.%d.%H%M%S) && sync"); return { code: 0 }; @@ -368,9 +350,7 @@ function m_check_kernel(req) { shbg('/usr/share/amlogic/amlogic_check_kernel.sh -check'); return { code: 0 }; } - // download option: pass the full param string directly to the script - // (it may contain underscores, dots, and digits — safe to pass verbatim - // since shbg wraps the whole command in single-quoted sh -c) + // download option: pass the full param string verbatim to the script. shbg('/usr/share/amlogic/amlogic_check_kernel.sh -download ' + shq(opt)); return { code: 0 }; } @@ -396,9 +376,7 @@ function m_start_plugin() { const script_path = LOG_DIR + '/amlogic_install_plugin.sh'; writefile(LOG_RUNNING, '1@Plugin update in progress, try again later!\n'); writefile(LOG_PLUGIN, ''); - // Write install script to a file so shbg (which redirects stdin from - // /dev/null) can execute it — heredoc via shbg does not work because - // shbg closes stdin before sh can read the here-document. + // Write install script to a temp file; shbg closes stdin so heredoc cannot be used. const script = '#!/bin/sh\n' + 'LOG=' + shq(LOG_PLUGIN) + '\n' + 'LOG_RUNNING=' + shq(LOG_RUNNING) + '\n' @@ -643,22 +621,16 @@ function m_list_uploads() { has_config: cfg }; } +// Strip directory prefix; reject empty, dot/dotdot, or paths still containing separators. function _safe_basename(s) { const b = replace(s ?? '', /^.*[\\\/]/, ''); - // Permissive basename check: only reject empty, dot/dotdot and anything - // still containing path separators after the strip. Strict allowlists - // rejected legitimate filenames (e.g. some i18n packages) on certain - // runtimes where the regex character class behaved unexpectedly. if (b == '' || b == '.' || b == '..') return null; if (index(b, '/') >= 0 || index(b, '\\') >= 0) return null; return b; } -// Chunked upload helper. The browser-side view splits the file into base64 -// chunks and calls this method N times. The first call (append=false) -// truncates / creates the file; later calls append. Implemented through our -// own ubus method so it bypasses the cgi-upload ACL flow entirely (the -// ucode runs as root and owns the upload dir). +// Receive one base64-encoded chunk and write (or append) it to the upload file. +// First call (append=false) truncates/creates; subsequent calls append. function m_upload_chunk(req) { const dir = compute_upload_path(); mkdir(dir); @@ -739,9 +711,7 @@ function m_reload_cpu() { return { code: 0 }; } -// Sync menu visibility UCI flags from runtime platform detection. -// Called from info.js on every page load so the sidebar reflects the -// current hardware state without requiring the user to visit Plugin Settings. +// Sync menu_install and menu_armcpu UCI flags based on runtime platform detection. function m_sync_menu() { const p = platform(); const m = show_install_menu(); @@ -757,9 +727,7 @@ function m_sync_menu() { return { code: 0, show_install, show_armcpu }; } -// Enumerate cpufreq policies + their available freqs/governors so the armcpu -// view can render a tab per CPU policy (mirrors the Lua dynamic taboption -// loop in amlogic_armcpu.lua). +// Enumerate cpufreq policies and their available frequencies and governors. function m_cpu_policies() { const policies_str = sh("ls /sys/devices/system/cpu/cpufreq 2>/dev/null | grep -E 'policy[0-9]{1,3}' | xargs"); let names = split(policies_str, ' '); diff --git a/luci-app-passwall/Makefile b/luci-app-passwall/Makefile index 4fc50b71..426296a8 100644 --- a/luci-app-passwall/Makefile +++ b/luci-app-passwall/Makefile @@ -8,7 +8,7 @@ include $(TOPDIR)/rules.mk PKG_NAME:=luci-app-passwall PKG_VERSION:=26.5.11 -PKG_RELEASE:=121 +PKG_RELEASE:=122 PKG_PO_VERSION:=$(PKG_VERSION) PKG_CONFIG_DEPENDS:= \ diff --git a/luci-app-passwall/luasrc/view/passwall/global/footer.htm b/luci-app-passwall/luasrc/view/passwall/global/footer.htm index 7aee20c2..7c7b1b4d 100644 --- a/luci-app-passwall/luasrc/view/passwall/global/footer.htm +++ b/luci-app-passwall/luasrc/view/passwall/global/footer.htm @@ -35,22 +35,25 @@ local appname = api.appname