mirror of
https://github.com/kiddin9/op-packages.git
synced 2026-10-26 21:22:51 +08:00
✨ Sync 2026-05-11 21:02:00
This commit is contained in:
1 parent
3d2fefa43a
commit
ce784c582e
60 files changed
+3238
-2526
No files matched your search
@@ -2,7 +2,7 @@ include $(TOPDIR)/rules.mk
|
||||
|
||||
PKG_NAME:=hickory-dns
|
||||
PKG_VERSION:=0.26.1
|
||||
PKG_RELEASE:=23
|
||||
PKG_RELEASE:=24
|
||||
|
||||
PKG_SOURCE:=$(PKG_NAME)-$(PKG_VERSION).tar.gz
|
||||
PKG_SOURCE_URL:=https://codeload.github.com/hickory-dns/hickory-dns/tar.gz/v$(PKG_VERSION)?
|
||||
|
||||
@@ -35,7 +35,7 @@ start_service() {
|
||||
sleep 1
|
||||
done
|
||||
procd_open_instance ipv6-neigh
|
||||
procd_set_param command $NEIGH_PROG --private-subnet-v4 -d "[::1]:53" -z lan
|
||||
procd_set_param command $NEIGH_PROG --private-subnet-v4 -d "[::1]:53" -z lan --router-alias router
|
||||
procd_set_param stdout 1
|
||||
procd_set_param stderr 1
|
||||
procd_set_param respawn "${respawn_threshold:-3600}" "${respawn_timeout:-5}" "${respawn_retry:-5}"
|
||||
|
||||
Generated
+7
-7
@@ -87,9 +87,9 @@ checksum = "1e748733b7cbc798e1434b6ac524f0c1ff2ab456fe201501e6497c8417a4fc33"
|
||||
|
||||
[[package]]
|
||||
name = "cc"
|
||||
version = "1.2.61"
|
||||
version = "1.2.62"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d16d90359e986641506914ba71350897565610e87ce0ad9e6f28569db3dd5c6d"
|
||||
checksum = "a1dce859f0832a7d088c4f1119888ab94ef4b5d6795d1ce05afb7fe159d79f98"
|
||||
dependencies = [
|
||||
"find-msvc-tools",
|
||||
"shlex",
|
||||
@@ -398,9 +398,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "hashbrown"
|
||||
version = "0.17.0"
|
||||
version = "0.17.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4f467dd6dccf739c208452f8014c75c18bb8301b050ad1cfb27153803edb0f51"
|
||||
checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a"
|
||||
|
||||
[[package]]
|
||||
name = "heck"
|
||||
@@ -545,7 +545,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d466e9454f08e4a911e14806c24e16fba1b4c121d1ea474396f396069cf949d9"
|
||||
dependencies = [
|
||||
"equivalent",
|
||||
"hashbrown 0.17.0",
|
||||
"hashbrown 0.17.1",
|
||||
"serde",
|
||||
"serde_core",
|
||||
]
|
||||
@@ -1273,9 +1273,9 @@ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20"
|
||||
|
||||
[[package]]
|
||||
name = "tokio"
|
||||
version = "1.52.2"
|
||||
version = "1.52.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "110a78583f19d5cdb2c5ccf321d1290344e71313c6c37d43520d386027d18386"
|
||||
checksum = "8fc7f01b389ac15039e4dc9531aa973a135d7a4135281b12d7c1bc79fd57fffe"
|
||||
dependencies = [
|
||||
"bytes",
|
||||
"libc",
|
||||
|
||||
+1
-1
@@ -2,7 +2,7 @@ include $(TOPDIR)/rules.mk
|
||||
|
||||
PKG_NAME:=ipv6-neigh
|
||||
PKG_VERSION:=0.1.0
|
||||
PKG_RELEASE:=8
|
||||
PKG_RELEASE:=9
|
||||
|
||||
PKG_BUILD_DEPENDS:=rust/host
|
||||
PKG_BUILD_PARALLEL:=1
|
||||
|
||||
+148
-67
@@ -40,9 +40,9 @@ struct Cli {
|
||||
/// Restrict IPv4 neighbours to private subnets only (10/8, 172.16/12, 192.168/16, 127/8)
|
||||
#[clap(long)]
|
||||
private_subnet_v4: bool,
|
||||
/// Restrict IPv6 neighbours to ULA (fc00::/7) only; without this flag GUA is also included
|
||||
/// Also publish GUA (Global Unicast Address) AAAA records; by default only ULA (fc00::/7) is published
|
||||
#[clap(long)]
|
||||
private_subnet_v6: bool,
|
||||
publish_gua: bool,
|
||||
/// hickory-dns server address for DNS updates (e.g. "[::1]:5335")
|
||||
#[clap(short, long, default_value = "[::1]:5335")]
|
||||
dns_server: StdSocketAddr,
|
||||
@@ -64,6 +64,9 @@ struct Cli {
|
||||
/// Network interface to read the router's own addresses from
|
||||
#[clap(long, default_value = "br-lan")]
|
||||
router_iface: String,
|
||||
/// Additional DNS name to register router addresses under (e.g. "router" → "router.lan")
|
||||
#[clap(long)]
|
||||
router_alias: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
@@ -81,6 +84,13 @@ struct LanPrefix {
|
||||
prefix_len: u8,
|
||||
}
|
||||
|
||||
/// State tracked for each (mac, ip) pair that has been successfully registered in DNS.
|
||||
struct RegisteredEntry {
|
||||
hostname: String,
|
||||
last_confirmed: Instant,
|
||||
ifindex: u32,
|
||||
}
|
||||
|
||||
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()
|
||||
@@ -126,15 +136,21 @@ fn ipv6_in_prefix(addr: Ipv6Addr, prefix: &LanPrefix) -> bool {
|
||||
(u128::from(addr) & mask) == (u128::from(prefix.addr) & mask)
|
||||
}
|
||||
|
||||
/// Returns true if `addr` is within any active LAN prefix, or if the prefix list is empty
|
||||
/// (in which case prefix-based filtering is disabled).
|
||||
fn ipv6_passes_active_prefix(addr: Ipv6Addr, active_prefixes: &[LanPrefix]) -> bool {
|
||||
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<String, String>) -> bool {
|
||||
let Some(hostname) = leases.get(&neigh.mac) else {
|
||||
debug!("no lease for mac {}, skipping DNS update", neigh.mac);
|
||||
return true;
|
||||
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,
|
||||
_ => return true,
|
||||
_ => return false,
|
||||
};
|
||||
match result {
|
||||
Ok(()) => {
|
||||
@@ -148,18 +164,16 @@ async fn process_new_neigh(neigh: &Neigh, updater: &db::DnsUpdater, leases: &Has
|
||||
}
|
||||
}
|
||||
|
||||
async fn process_del_neigh(neigh: &Neigh, updater: &db::DnsUpdater, leases: &HashMap<String, String>) -> bool {
|
||||
let Some(hostname) = leases.get(&neigh.mac) else {
|
||||
return true;
|
||||
};
|
||||
let result = match &neigh.inet {
|
||||
/// Delete a specific DNS record directly by hostname and address.
|
||||
async fn do_delete_dns(hostname: &str, inet: &NeighbourAddress, updater: &db::DnsUpdater) -> bool {
|
||||
let result = match inet {
|
||||
NeighbourAddress::Inet6(addr) => updater.delete_aaaa(hostname, *addr).await,
|
||||
NeighbourAddress::Inet(addr) => updater.delete_a(hostname, *addr).await,
|
||||
_ => return true,
|
||||
};
|
||||
match result {
|
||||
Ok(()) => {
|
||||
info!("DNS update: removed {} -> {:?}", hostname, neigh.inet);
|
||||
info!("DNS update: removed {} -> {:?}", hostname, inet);
|
||||
true
|
||||
}
|
||||
Err(e) => {
|
||||
@@ -199,7 +213,7 @@ async fn main() -> Result<(), ()> {
|
||||
.init();
|
||||
|
||||
let private_subnet_v4 = args.private_subnet_v4;
|
||||
let private_subnet_v6 = args.private_subnet_v6;
|
||||
let private_subnet_v6 = !args.publish_gua;
|
||||
let zone = Name::from_ascii(&args.zone).expect("invalid zone name");
|
||||
|
||||
let key_data = std::fs::read(&args.key_file)
|
||||
@@ -224,10 +238,12 @@ async fn main() -> Result<(), ()> {
|
||||
if router_hostname.is_empty() {
|
||||
warn!("could not read router hostname from /proc/sys/kernel/hostname");
|
||||
} else {
|
||||
let router_alias = args.router_alias.as_deref().filter(|a| *a != router_hostname);
|
||||
register_router_addresses(
|
||||
handle.clone(),
|
||||
&args.router_iface,
|
||||
&router_hostname,
|
||||
router_alias,
|
||||
&updater,
|
||||
private_subnet_v6,
|
||||
)
|
||||
@@ -238,8 +254,8 @@ async fn main() -> Result<(), ()> {
|
||||
let mut leases = op::get_lease().unwrap_or_default();
|
||||
info!("loaded {} DHCP leases", leases.len());
|
||||
|
||||
// State cache: tracks (mac, ip_string) -> (last confirmed time, ifindex)
|
||||
let mut registered: HashMap<(String, String), (Instant, u32)> = HashMap::new();
|
||||
// State cache: tracks (mac, ip_string) -> registered entry (hostname, last confirmed time, ifindex)
|
||||
let mut registered: HashMap<(String, String), RegisteredEntry> = 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.
|
||||
@@ -282,8 +298,25 @@ async fn main() -> Result<(), ()> {
|
||||
NeighbourState::Reachable => {
|
||||
// Confirmed online — register in DNS immediately.
|
||||
if !registered.contains_key(&key) {
|
||||
if process_new_neigh(neigh, &updater, &leases).await {
|
||||
registered.insert(key, (Instant::now(), neigh.ifindex));
|
||||
if let Some(hostname) = leases.get(&neigh.mac) {
|
||||
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,
|
||||
_ => Ok(()),
|
||||
};
|
||||
match result {
|
||||
Ok(()) => {
|
||||
info!("DNS update: added {} -> {:?}", hostname, neigh.inet);
|
||||
registered.insert(key, RegisteredEntry {
|
||||
hostname: hostname.clone(),
|
||||
last_confirmed: Instant::now(),
|
||||
ifindex: neigh.ifindex,
|
||||
});
|
||||
}
|
||||
Err(e) => error!("DNS update failed for {}: {}", hostname, e),
|
||||
}
|
||||
} else {
|
||||
debug!("no lease for mac {}, skipping DNS update", neigh.mac);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -336,7 +369,8 @@ async fn main() -> Result<(), ()> {
|
||||
};
|
||||
probe_timer.tick().await; // consume the immediate first tick
|
||||
|
||||
let mut event_count: u64 = 0;
|
||||
let mut lease_refresh_timer = time::interval(Duration::from_secs(60));
|
||||
lease_refresh_timer.tick().await; // consume the immediate first tick
|
||||
|
||||
loop {
|
||||
tokio::select! {
|
||||
@@ -345,15 +379,6 @@ async fn main() -> Result<(), ()> {
|
||||
break;
|
||||
};
|
||||
|
||||
// Refresh leases periodically (every 50 events)
|
||||
event_count += 1;
|
||||
if event_count % 50 == 0 {
|
||||
if let Ok(new_leases) = op::get_lease() {
|
||||
leases = new_leases;
|
||||
debug!("refreshed {} DHCP leases", leases.len());
|
||||
}
|
||||
}
|
||||
|
||||
let payload = message.payload;
|
||||
if let NetlinkPayload::InnerMessage(msg) = payload {
|
||||
match msg {
|
||||
@@ -365,32 +390,53 @@ async fn main() -> Result<(), ()> {
|
||||
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);
|
||||
if let Some(entry) = registered.remove(&key) {
|
||||
do_delete_dns(&entry.hostname, &neigh.inet, &updater).await;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
if is_failed_state(neigh.state) {
|
||||
// Neighbour confirmed unreachable — remove DNS record
|
||||
if registered.remove(&key).is_some() {
|
||||
if let Some(entry) = registered.remove(&key) {
|
||||
debug!("Neighbour failed: {:?}", neigh);
|
||||
if !process_del_neigh(&neigh, &updater, &leases).await {
|
||||
if !do_delete_dns(&entry.hostname, &neigh.inet, &updater).await {
|
||||
// DNS delete failed, put back so we retry
|
||||
registered.insert(key, (Instant::now(), neigh.ifindex));
|
||||
registered.insert(key, entry);
|
||||
}
|
||||
}
|
||||
} else if neigh.state == NeighbourState::Reachable {
|
||||
if registered.contains_key(&key) {
|
||||
// Already registered, just update timestamp
|
||||
registered.insert(key, (Instant::now(), neigh.ifindex));
|
||||
if let Some(entry) = registered.get_mut(&key) {
|
||||
// Already registered, just update timestamp and ifindex
|
||||
entry.last_confirmed = Instant::now();
|
||||
entry.ifindex = neigh.ifindex;
|
||||
} else {
|
||||
// New reachable neighbour — register in DNS
|
||||
debug!("New neighbour: {:?}", neigh);
|
||||
if process_new_neigh(&neigh, &updater, &leases).await {
|
||||
registered.insert(key, (Instant::now(), neigh.ifindex));
|
||||
if let Some(hostname) = leases.get(&neigh.mac) {
|
||||
if process_new_neigh(&neigh, &updater, &leases).await {
|
||||
registered.insert(key, RegisteredEntry {
|
||||
hostname: hostname.clone(),
|
||||
last_confirmed: Instant::now(),
|
||||
ifindex: neigh.ifindex,
|
||||
});
|
||||
}
|
||||
} else {
|
||||
debug!("no lease for mac {}, skipping DNS update", neigh.mac);
|
||||
}
|
||||
}
|
||||
} else if matches!(neigh.state, NeighbourState::Stale | NeighbourState::Delay | NeighbourState::Probe) {
|
||||
// Uncertain state: keep existing registrations alive but do
|
||||
// not create new DNS records without REACHABLE confirmation.
|
||||
if registered.contains_key(&key) {
|
||||
registered.insert(key, (Instant::now(), neigh.ifindex));
|
||||
// Uncertain state: update ifindex only so the next probe uses
|
||||
// the right interface, but do NOT refresh last_confirmed —
|
||||
// STALE/DELAY/PROBE is not confirmed reachable and refreshing
|
||||
// the timestamp would suppress the periodic probe.
|
||||
if let Some(entry) = registered.get_mut(&key) {
|
||||
entry.ifindex = neigh.ifindex;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -402,11 +448,15 @@ async fn main() -> Result<(), ()> {
|
||||
continue;
|
||||
}
|
||||
let key = (neigh.mac.clone(), inet_to_string(&neigh.inet));
|
||||
registered.remove(&key);
|
||||
let stored_hostname = registered.remove(&key).map(|e| e.hostname);
|
||||
debug!("Del neighbour: {:?}", neigh);
|
||||
// Always attempt DNS deletion even if not in registered map,
|
||||
// to clean up records that survived a program restart.
|
||||
process_del_neigh(&neigh, &updater, &leases).await;
|
||||
// Use stored hostname if available, fall back to leases for records
|
||||
// that survived a program restart (not in registered).
|
||||
let hostname_opt = stored_hostname
|
||||
.or_else(|| leases.get(&neigh.mac).cloned());
|
||||
if let Some(ref hostname) = hostname_opt {
|
||||
do_delete_dns(hostname, &neigh.inet, &updater).await;
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
@@ -427,6 +477,12 @@ async fn main() -> Result<(), ()> {
|
||||
)
|
||||
.await;
|
||||
}
|
||||
_ = lease_refresh_timer.tick() => {
|
||||
if let Ok(new_leases) = op::get_lease() {
|
||||
leases = new_leases;
|
||||
debug!("refreshed {} DHCP leases", leases.len());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
@@ -445,7 +501,7 @@ async fn main() -> Result<(), ()> {
|
||||
/// via DNS UPDATE so the zone stays consistent.
|
||||
async fn reconcile_dns(
|
||||
updater: &db::DnsUpdater,
|
||||
registered: &mut HashMap<(String, String), (Instant, u32)>,
|
||||
registered: &mut HashMap<(String, String), RegisteredEntry>,
|
||||
leases: &HashMap<String, String>,
|
||||
active_prefixes: &[LanPrefix],
|
||||
private_subnet_v4: bool,
|
||||
@@ -504,14 +560,25 @@ async fn reconcile_dns(
|
||||
registered.keys().map(|(_, ip)| ip.as_str()).collect();
|
||||
|
||||
// --- DNS orphans (in DNS but not in registered) ---
|
||||
// Probe with ifindex=0 so the kernel routes via the default LAN route.
|
||||
// If the host is alive the resulting REACHABLE event will re-populate `registered`.
|
||||
// If gone, the record's TTL (60 s) will expire and it won't be renewed.
|
||||
// Delete the orphan immediately — DNS TTL only controls resolver caches, not
|
||||
// authoritative record lifetime, so records do NOT auto-expire.
|
||||
// After deletion we probe the address: if the device is still alive the kernel
|
||||
// will emit a REACHABLE NewNeighbour event which re-registers it in DNS.
|
||||
for (ip_str, hostname) in &dns_ips {
|
||||
if registered_ips.contains(ip_str.as_str()) {
|
||||
continue;
|
||||
}
|
||||
info!("reconcile: DNS orphan {} -> {}, probing", hostname, ip_str);
|
||||
info!("reconcile: DNS orphan {} -> {}, deleting and probing", hostname, ip_str);
|
||||
let del_result = match ip_str.parse::<IpAddr>() {
|
||||
Ok(IpAddr::V6(addr)) => updater.delete_aaaa(hostname, addr).await,
|
||||
Ok(IpAddr::V4(addr)) => updater.delete_a(hostname, addr).await,
|
||||
_ => continue,
|
||||
};
|
||||
match del_result {
|
||||
Ok(()) => info!("reconcile: deleted orphan DNS {} -> {}", hostname, ip_str),
|
||||
Err(e) => warn!("reconcile: failed to delete orphan {} {}: {}", hostname, ip_str, e),
|
||||
}
|
||||
// Probe so alive devices trigger a REACHABLE event and re-register.
|
||||
match ip_str.parse::<IpAddr>() {
|
||||
Ok(IpAddr::V6(addr)) => {
|
||||
if let Err(e) = send_icmpv6_echo(addr, 0) {
|
||||
@@ -519,7 +586,9 @@ async fn reconcile_dns(
|
||||
}
|
||||
}
|
||||
Ok(IpAddr::V4(addr)) => {
|
||||
let _ = send_icmpv4_echo(addr, 0);
|
||||
if let Err(e) = send_icmpv4_echo(addr, 0) {
|
||||
debug!("reconcile: probe failed for {}: {}", addr, e);
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
@@ -527,11 +596,12 @@ async fn reconcile_dns(
|
||||
|
||||
// --- Registered orphans (in registered but not in DNS) ---
|
||||
// hickory-dns may have restarted and lost its journal; re-push the record.
|
||||
for ((mac, ip_str), (last_confirmed, _)) in registered.iter_mut() {
|
||||
// Use the hostname stored in the entry — independent of the current lease table.
|
||||
for ((_, ip_str), entry) in registered.iter_mut() {
|
||||
if dns_ips.contains_key(ip_str.as_str()) {
|
||||
continue;
|
||||
}
|
||||
let Some(hostname) = leases.get(mac) else { continue };
|
||||
let hostname = &entry.hostname;
|
||||
debug!("reconcile: registered orphan {} -> {}, re-pushing", hostname, ip_str);
|
||||
let result = match ip_str.parse::<IpAddr>() {
|
||||
Ok(IpAddr::V6(addr)) => updater.upsert_aaaa(hostname, addr, DEFAULT_TTL).await,
|
||||
@@ -541,7 +611,7 @@ async fn reconcile_dns(
|
||||
match result {
|
||||
Ok(()) => {
|
||||
info!("reconcile: re-pushed {} -> {}", hostname, ip_str);
|
||||
*last_confirmed = Instant::now();
|
||||
entry.last_confirmed = Instant::now();
|
||||
}
|
||||
Err(e) => warn!("reconcile: failed to re-push {} {}: {}", hostname, ip_str, e),
|
||||
}
|
||||
@@ -551,19 +621,19 @@ async fn reconcile_dns(
|
||||
/// Send ICMPv6 Echo Request / ICMPv4 Echo Request to all registered neighbours.
|
||||
/// This forces the kernel NUD state machine to verify reachability, generating
|
||||
/// NewNeighbour events with the resulting state (Reachable or Failed).
|
||||
async fn probe_registered_neighbours(registered: &HashMap<(String, String), (Instant, u32)>) {
|
||||
async fn probe_registered_neighbours(registered: &HashMap<(String, String), RegisteredEntry>) {
|
||||
let now = Instant::now();
|
||||
for ((_, ip_str), (last_confirmed, ifindex)) in registered.iter() {
|
||||
for ((_, ip_str), entry) in registered.iter() {
|
||||
// Only probe entries not confirmed recently (older than 30s)
|
||||
if now.duration_since(*last_confirmed) < Duration::from_secs(30) {
|
||||
if now.duration_since(entry.last_confirmed) < Duration::from_secs(30) {
|
||||
continue;
|
||||
}
|
||||
if let Ok(addr) = ip_str.parse::<Ipv6Addr>() {
|
||||
if let Err(e) = send_icmpv6_echo(addr, *ifindex) {
|
||||
if let Err(e) = send_icmpv6_echo(addr, entry.ifindex) {
|
||||
debug!("probe failed for {}: {}", ip_str, e);
|
||||
}
|
||||
} else if let Ok(addr) = ip_str.parse::<Ipv4Addr>() {
|
||||
if let Err(e) = send_icmpv4_echo(addr, *ifindex) {
|
||||
if let Err(e) = send_icmpv4_echo(addr, entry.ifindex) {
|
||||
debug!("probe failed for {}: {}", ip_str, e);
|
||||
}
|
||||
}
|
||||
@@ -576,7 +646,7 @@ fn send_icmpv6_echo(addr: Ipv6Addr, ifindex: u32) -> std::io::Result<()> {
|
||||
// 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 {
|
||||
unsafe {
|
||||
let ret = unsafe {
|
||||
let idx = ifindex as libc::c_int;
|
||||
libc::setsockopt(
|
||||
socket.as_raw_fd(),
|
||||
@@ -584,13 +654,16 @@ fn send_icmpv6_echo(addr: Ipv6Addr, ifindex: u32) -> std::io::Result<()> {
|
||||
libc::IPV6_UNICAST_IF,
|
||||
&idx as *const libc::c_int as *const libc::c_void,
|
||||
std::mem::size_of::<libc::c_int>() as libc::socklen_t,
|
||||
);
|
||||
)
|
||||
};
|
||||
if ret != 0 {
|
||||
return Err(std::io::Error::last_os_error());
|
||||
}
|
||||
}
|
||||
// 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));
|
||||
let _ = socket.send_to(&packet, &dest);
|
||||
socket.send_to(&packet, &dest)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -599,7 +672,7 @@ fn send_icmpv4_echo(addr: Ipv4Addr, ifindex: u32) -> std::io::Result<()> {
|
||||
socket.set_nonblocking(true)?;
|
||||
// Bind outgoing packet to the specific interface via IP_UNICAST_IF.
|
||||
if ifindex != 0 {
|
||||
unsafe {
|
||||
let ret = unsafe {
|
||||
let idx = ifindex as libc::c_int;
|
||||
libc::setsockopt(
|
||||
socket.as_raw_fd(),
|
||||
@@ -607,14 +680,17 @@ fn send_icmpv4_echo(addr: Ipv4Addr, ifindex: u32) -> std::io::Result<()> {
|
||||
libc::IP_UNICAST_IF,
|
||||
&idx as *const libc::c_int as *const libc::c_void,
|
||||
std::mem::size_of::<libc::c_int>() 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];
|
||||
let dest = SockAddr::from(std::net::SocketAddrV4::new(addr, 0));
|
||||
let _ = socket.send_to(&packet, &dest);
|
||||
socket.send_to(&packet, &dest)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -754,6 +830,7 @@ async fn register_router_addresses(
|
||||
handle: Handle,
|
||||
iface: &str,
|
||||
hostname: &str,
|
||||
extra_alias: Option<&str>,
|
||||
updater: &db::DnsUpdater,
|
||||
private_subnet_v6: bool,
|
||||
) {
|
||||
@@ -791,15 +868,19 @@ async fn register_router_addresses(
|
||||
if private_subnet_v6 && !is_ula {
|
||||
continue;
|
||||
}
|
||||
match updater.upsert_aaaa(hostname, *addr, DEFAULT_TTL).await {
|
||||
Ok(()) => info!("registered router {} AAAA {}", hostname, addr),
|
||||
Err(e) => warn!("failed to register router AAAA {} for {}: {}", addr, hostname, e),
|
||||
for name in std::iter::once(hostname).chain(extra_alias) {
|
||||
match updater.upsert_aaaa(name, *addr, DEFAULT_TTL).await {
|
||||
Ok(()) => info!("registered router {} AAAA {}", name, addr),
|
||||
Err(e) => warn!("failed to register router AAAA {} for {}: {}", addr, name, e),
|
||||
}
|
||||
}
|
||||
}
|
||||
IpAddr::V4(addr) => {
|
||||
match updater.upsert_a(hostname, *addr, DEFAULT_TTL).await {
|
||||
Ok(()) => info!("registered router {} A {}", hostname, addr),
|
||||
Err(e) => warn!("failed to register router A {} for {}: {}", addr, hostname, e),
|
||||
for name in std::iter::once(hostname).chain(extra_alias) {
|
||||
match updater.upsert_a(name, *addr, DEFAULT_TTL).await {
|
||||
Ok(()) => info!("registered router {} A {}", name, addr),
|
||||
Err(e) => warn!("failed to register router A {} for {}: {}", addr, name, e),
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,8 +16,8 @@
|
||||
include $(TOPDIR)/rules.mk
|
||||
|
||||
PKG_NAME:=luci-app-amlogic
|
||||
PKG_VERSION:=3.1.295
|
||||
PKG_RELEASE:=7
|
||||
PKG_VERSION:=3.1.303
|
||||
PKG_RELEASE:=8
|
||||
|
||||
PKG_LICENSE:=GPL-2.0 License
|
||||
PKG_MAINTAINER:=ophub <https://github.com/ophub/luci-app-amlogic>
|
||||
@@ -25,7 +25,8 @@ PKG_MAINTAINER:=ophub <https://github.com/ophub/luci-app-amlogic>
|
||||
LUCI_TITLE:=LuCI support for custom TV boxes
|
||||
LUCI_PKGARCH:=all
|
||||
LUCI_DEPENDS:= \
|
||||
@(aarch64||arm) +bash +blkid +block-mount +curl +dosfstools +e2fsprogs \
|
||||
@(aarch64||arm) +luci-base +rpcd +rpcd-mod-ucode \
|
||||
+bash +blkid +block-mount +curl +dosfstools +e2fsprogs \
|
||||
+fdisk +jq +losetup +lsblk +parted +perl +pv +uuidgen
|
||||
|
||||
define Package/$(PKG_NAME)/conffiles
|
||||
|
||||
@@ -0,0 +1,84 @@
|
||||
/* SPDX-License-Identifier: GPL-2.0
|
||||
*
|
||||
* Theme-aware styles for luci-app-amlogic JS views.
|
||||
*/
|
||||
|
||||
/* --- Status text ------------------------------------ */
|
||||
.amlogic-status-ok { color: #1a7f37; }
|
||||
.amlogic-status-err { color: #c0392b; }
|
||||
.amlogic-status-info { color: #1f6feb; }
|
||||
|
||||
/* --- Spacious row table -----------------------------------------------
|
||||
* Used by views that lay out a single button + status text per row
|
||||
* (check / backup / upload). border-collapse:collapse avoids the visible
|
||||
* blank band that border-spacing would otherwise add around the table
|
||||
* edges; row breathing room comes purely from cell padding.
|
||||
* ---------------------------------------------------------------------- */
|
||||
.amlogic-row-table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
}
|
||||
.amlogic-row-table > tbody > tr > td,
|
||||
.amlogic-row-table > tr > td {
|
||||
padding: 10px 12px;
|
||||
vertical-align: middle;
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
/* --- Snapshot grid (cards) --------------------------------------------- */
|
||||
.amlogic-snap-list {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
justify-content: flex-start;
|
||||
width: 100%;
|
||||
background: transparent;
|
||||
}
|
||||
.amlogic-snap-list:after,
|
||||
.amlogic-snap-list .amlogic-snap-item:not(:nth-child(3n)) {
|
||||
content: "";
|
||||
}
|
||||
.amlogic-snap-item {
|
||||
box-sizing: border-box;
|
||||
width: 150px;
|
||||
height: 130px;
|
||||
margin: 8px;
|
||||
padding: 4px;
|
||||
text-align: center;
|
||||
font-size: 14px;
|
||||
color: inherit;
|
||||
background: transparent;
|
||||
border: 1px solid rgba(127, 127, 127, 0.35);
|
||||
border-radius: 4px;
|
||||
transition: background-color 0.15s ease-in-out;
|
||||
}
|
||||
.amlogic-snap-item:hover {
|
||||
background: rgba(127, 127, 127, 0.15);
|
||||
}
|
||||
.amlogic-snap-line {
|
||||
line-height: 40px;
|
||||
}
|
||||
|
||||
/* --- Info-page logo / author tiles ------------------------------------- */
|
||||
.amlogic-info-logo {
|
||||
max-width: 100%;
|
||||
height: auto;
|
||||
}
|
||||
|
||||
/* --- Dark mode overrides (system / browser preference) -----------------
|
||||
* Reads prefers-color-scheme from the system / browser. When the user has
|
||||
* picked a dark theme, switch to brighter colors to keep readable contrast.
|
||||
* LuCI itself only adjusts the background, so this stylesheet fills in the
|
||||
* status text and snapshot border colors that the upstream theme leaves
|
||||
* untouched.
|
||||
* ---------------------------------------------------------------------- */
|
||||
@media (prefers-color-scheme: dark) {
|
||||
.amlogic-status-ok { color: #4ade80; }
|
||||
.amlogic-status-err { color: #f87171; }
|
||||
.amlogic-status-info { color: #60a5fa; }
|
||||
.amlogic-snap-item {
|
||||
border-color: rgba(200, 200, 200, 0.25);
|
||||
}
|
||||
.amlogic-snap-item:hover {
|
||||
background: rgba(255, 255, 255, 0.08);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,92 @@
|
||||
// SPDX-License-Identifier: GPL-2.0
|
||||
// CPU Freq Settings page
|
||||
//
|
||||
// Purpose: configure scaling governor and min/max frequency per CPU policy
|
||||
// (cluster). After save, the backend script fixcpufreq.pl is invoked so the
|
||||
// new values take effect immediately. The backing RPC is exposed by
|
||||
// /usr/share/rpcd/ucode/luci.amlogic.
|
||||
|
||||
'use strict';
|
||||
'require view';
|
||||
'require form';
|
||||
'require rpc';
|
||||
'require uci';
|
||||
|
||||
// Query CPU cluster info (each policy: core type, available governors, freqs).
|
||||
const callPolicies = rpc.declare({ object: 'luci.amlogic', method: 'cpu_policies',
|
||||
expect: { policies: [] } });
|
||||
// Tell backend to apply the new governor / frequency limits right after save.
|
||||
const callReloadCpu = rpc.declare({ object: 'luci.amlogic', method: 'reload_cpu' });
|
||||
|
||||
return view.extend({
|
||||
// Load in parallel: CPU policy info + UCI config (we will read/write it).
|
||||
load: function () {
|
||||
return Promise.all([
|
||||
callPolicies(),
|
||||
uci.load('amlogic')
|
||||
]);
|
||||
},
|
||||
|
||||
render: function (data) {
|
||||
const policies = data[0] || [];
|
||||
|
||||
// Auto-create the armcpu section when missing, so that on a fresh
|
||||
// install/upgrade the user can save without first creating the section
|
||||
// manually.
|
||||
if (!uci.get('amlogic', 'armcpu')) {
|
||||
uci.add('amlogic', 'settings', 'armcpu');
|
||||
}
|
||||
|
||||
// Build CBI Map bound to the amlogic.armcpu NamedSection.
|
||||
const m = new form.Map('amlogic', _('CPU Freq Settings'),
|
||||
_('Set CPU Scaling Governor to Max Performance or Balance Mode'));
|
||||
const s = m.section(form.NamedSection, 'armcpu', 'settings');
|
||||
s.anonymous = true;
|
||||
|
||||
// Render an independent tab + form options per CPU cluster (big/little).
|
||||
policies.forEach(function (p) {
|
||||
const tabId = 'tab' + p.id;
|
||||
s.tab(tabId, p.name);
|
||||
|
||||
// Microarchitecture (read-only display, inline so it aligns with the label).
|
||||
const ct = s.taboption(tabId, form.DummyValue, 'core_type' + p.id,
|
||||
_('Microarchitectures:'));
|
||||
ct.cfgvalue = function () { return p.core_type; };
|
||||
ct.renderWidget = function (section_id) {
|
||||
return E('span', { style: 'line-height:2em; display:inline-block; vertical-align:middle' },
|
||||
this.cfgvalue(section_id) || '');
|
||||
};
|
||||
|
||||
// CPU scaling governor: list of governors exposed by the kernel
|
||||
// (performance / schedutil / etc).
|
||||
const gov = s.taboption(tabId, form.ListValue, 'governor' + p.id,
|
||||
_('CPU Scaling Governor:'));
|
||||
(p.governors || []).forEach(function (e) { gov.value(e, e.toUpperCase()); });
|
||||
gov.default = 'schedutil';
|
||||
gov.rmempty = false;
|
||||
|
||||
// Minimum running frequency (kHz).
|
||||
const minF = s.taboption(tabId, form.ListValue, 'minfreq' + p.id, _('Min Freq:'));
|
||||
(p.freqs || []).forEach(function (e) { minF.value(e); });
|
||||
minF.default = '500000';
|
||||
minF.rmempty = false;
|
||||
|
||||
// Maximum running frequency (kHz).
|
||||
const maxF = s.taboption(tabId, form.ListValue, 'maxfreq' + p.id, _('Max Freq:'));
|
||||
(p.freqs || []).forEach(function (e) { maxF.value(e); });
|
||||
maxF.default = '1512000';
|
||||
maxF.rmempty = false;
|
||||
});
|
||||
|
||||
// Wrap the default saveApply: after a successful UCI commit, trigger
|
||||
// reload_cpu so the new governor and frequency limits take effect
|
||||
// without the user manually restarting the service.
|
||||
const origHandle = m.handleSaveApply;
|
||||
m.handleSaveApply = function () {
|
||||
return origHandle.apply(this, arguments)
|
||||
.then(function (r) { callReloadCpu(); return r; });
|
||||
};
|
||||
|
||||
return m.render();
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,269 @@
|
||||
// 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.
|
||||
|
||||
'use strict';
|
||||
'require view';
|
||||
'require rpc';
|
||||
'require ui';
|
||||
'require fs';
|
||||
'require view.amlogic.shared as amlogicShared';
|
||||
|
||||
// Generate one openwrt_config.tar.gz on the server and return its path so the
|
||||
// frontend can stream it back via fs.read_direct.
|
||||
const callBackup = rpc.declare({ object: 'luci.amlogic', method: 'backup_create' });
|
||||
// List existing snapshot names (e.g. etc-000 / etc-001 / etc-202401...).
|
||||
const callSnapList = rpc.declare({ object: 'luci.amlogic', method: 'snapshot_list',
|
||||
expect: { names: [] } });
|
||||
// Create a new snapshot (the backend chooses its name).
|
||||
const callSnapCreate = rpc.declare({ object: 'luci.amlogic', method: 'snapshot_create' });
|
||||
// Delete a snapshot by short name (without the etc- prefix).
|
||||
const callSnapDel = rpc.declare({ object: 'luci.amlogic', method: 'snapshot_delete',
|
||||
params: ['name'] });
|
||||
// Restore the given snapshot and reboot.
|
||||
const callSnapRest = rpc.declare({ object: 'luci.amlogic', method: 'snapshot_restore',
|
||||
params: ['name'] });
|
||||
// Read platform info (this page only consumes has_kvm to decide whether to
|
||||
// show the KVM switch row).
|
||||
const callPlatform = rpc.declare({ object: 'luci.amlogic', method: 'platform_info' });
|
||||
// Switch active KVM partition.
|
||||
const callKvmSwitch = rpc.declare({ object: 'luci.amlogic', method: 'kvm_switch' });
|
||||
|
||||
return view.extend({
|
||||
handleSave: null,
|
||||
handleSaveApply: null,
|
||||
handleReset: null,
|
||||
|
||||
load: function () {
|
||||
// Load in parallel: platform info + current snapshot list.
|
||||
amlogicShared.ensureCss();
|
||||
return Promise.all([callPlatform(), callSnapList()]);
|
||||
},
|
||||
|
||||
render: function (data) {
|
||||
const platform = data[0] || {};
|
||||
const snapNames = data[1] || [];
|
||||
const view = this;
|
||||
|
||||
const editListBtn = E('input', {
|
||||
type: 'button', class: 'cbi-button cbi-button-save',
|
||||
value: _('Open List'),
|
||||
// Navigate to the backuplist page to edit the backup file list.
|
||||
click: function () {
|
||||
location.href = L.url('admin/system/amlogic/backuplist');
|
||||
}
|
||||
});
|
||||
|
||||
// Download status hint: uses the warning color; replaced by the
|
||||
// success message after a successful generation.
|
||||
const downloadStatus = E('span', { class: 'amlogic-status-err' });
|
||||
const downloadBtn = E('input', {
|
||||
type: 'button', class: 'cbi-button cbi-button-save',
|
||||
value: _('Download Backup'),
|
||||
click: ui.createHandlerFn(view, function (ev) {
|
||||
const btn = ev.currentTarget;
|
||||
btn.disabled = true;
|
||||
btn.value = _('Generating...');
|
||||
// Step 1: ask the server to pack /etc into openwrt_config.tar.gz.
|
||||
return callBackup().then(function (r) {
|
||||
if (!r || !r.ok) {
|
||||
downloadStatus.textContent = _("Couldn't open file:") + ' ' + (r ? r.path : '');
|
||||
return;
|
||||
}
|
||||
// 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.
|
||||
return fs.read_direct(r.path, 'blob').then(function (blob) {
|
||||
const url = URL.createObjectURL(blob);
|
||||
const a = document.createElement('a');
|
||||
a.href = url;
|
||||
a.download = 'openwrt_config.tar.gz';
|
||||
document.body.appendChild(a);
|
||||
a.click();
|
||||
document.body.removeChild(a);
|
||||
setTimeout(function () { URL.revokeObjectURL(url); }, 10000);
|
||||
});
|
||||
}).catch(function (e) {
|
||||
downloadStatus.textContent = _('Create upload file error.') +
|
||||
' ' + (e && e.message ? e.message : '');
|
||||
}).finally(function () {
|
||||
btn.disabled = false;
|
||||
btn.value = _('Download Backup');
|
||||
});
|
||||
})
|
||||
});
|
||||
|
||||
const restoreBtn = E('input', {
|
||||
type: 'button', class: 'cbi-button cbi-button-save',
|
||||
value: _('Upload Backup'),
|
||||
click: function () {
|
||||
location.href = L.url('admin/system/amlogic/upload');
|
||||
}
|
||||
});
|
||||
|
||||
// Snapshots
|
||||
// Snapshot grid: uses amlogic-snap-list / amlogic-snap-item /
|
||||
// amlogic-snap-line classes whose styles adapt to the current theme
|
||||
// (see amlogic.css).
|
||||
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).
|
||||
function renderSnapshots(names) {
|
||||
snapDiv.innerHTML = '';
|
||||
if (!names || !names.length) {
|
||||
snapDiv.appendChild(E('span', { class: 'amlogic-status-err' },
|
||||
_('Currently OpenWrt does not support the snapshot function.') +
|
||||
_('Please use this plugin to reinstall or upgrade OpenWrt to enable the snapshot function.')));
|
||||
return;
|
||||
}
|
||||
names.forEach(function (n) {
|
||||
let title;
|
||||
if (n === 'etc-000') title = _('Initialize Snapshot');
|
||||
else if (n === 'etc-001') title = _('Update Snapshot');
|
||||
else title = n;
|
||||
const restoreSnapBtn = E('input', {
|
||||
type: 'button', class: 'cbi-button cbi-button-apply',
|
||||
value: _('Restore Snap'),
|
||||
click: ui.createHandlerFn(view, function (ev) {
|
||||
const short = n.replace(/^etc-/, '');
|
||||
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.
|
||||
const btn = ev.currentTarget;
|
||||
btn.disabled = true;
|
||||
btn.value = _('Restoring...');
|
||||
return callSnapRest(short).then(function (r) {
|
||||
if (r && r.code == 0) {
|
||||
btn.value = _('Successfully Restored');
|
||||
ui.addNotification(null,
|
||||
E('p', { class: 'amlogic-status-err' },
|
||||
_('Snapshot restored. The system will reboot now.')));
|
||||
} else {
|
||||
btn.value = _('Restore Failed');
|
||||
}
|
||||
}).catch(function () { btn.value = _('Restore Failed'); });
|
||||
})
|
||||
});
|
||||
const item = E('div', {
|
||||
class: 'amlogic-snap-item',
|
||||
id: 'snapshots_div_' + n
|
||||
}, [
|
||||
E('div', { class: 'amlogic-snap-line' }, title),
|
||||
E('div', { class: 'amlogic-snap-line' }, restoreSnapBtn)
|
||||
]);
|
||||
if (n !== 'etc-000' && n !== 'etc-001') {
|
||||
const delBtn = E('input', {
|
||||
type: 'button', class: 'cbi-button cbi-button-remove',
|
||||
value: _('Delete Snap'),
|
||||
click: ui.createHandlerFn(view, function (ev) {
|
||||
const short = n.replace(/^etc-/, '');
|
||||
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.
|
||||
const btn = ev.currentTarget;
|
||||
btn.disabled = true;
|
||||
btn.value = _('Deleting...');
|
||||
return callSnapDel(short).then(function (r) {
|
||||
if (r && r.code == 0) {
|
||||
btn.value = _('Successfully Deleted');
|
||||
item.style.display = 'none';
|
||||
} else {
|
||||
btn.value = _('Delete Failed');
|
||||
}
|
||||
}).catch(function () { btn.value = _('Delete Failed'); });
|
||||
})
|
||||
});
|
||||
item.appendChild(E('div', { class: 'amlogic-snap-line' }, delBtn));
|
||||
}
|
||||
snapDiv.appendChild(item);
|
||||
});
|
||||
}
|
||||
renderSnapshots(snapNames);
|
||||
|
||||
const createSnapBtn = E('input', {
|
||||
type: 'button', class: 'cbi-button cbi-button-save',
|
||||
value: _('Create Snapshot'),
|
||||
click: ui.createHandlerFn(view, function (ev) {
|
||||
const btn = ev.currentTarget;
|
||||
btn.disabled = true;
|
||||
return callSnapCreate().then(function () {
|
||||
return callSnapList();
|
||||
}).then(function (r) {
|
||||
renderSnapshots(Array.isArray(r) ? r : (r && r.names) || []);
|
||||
}).finally(function () { btn.disabled = false; });
|
||||
})
|
||||
});
|
||||
|
||||
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' }, [
|
||||
E('table', { class: 'amlogic-row-table' }, [
|
||||
E('tr', [
|
||||
E('td', { width: '20%', align: 'right' }, _('Edit List:')),
|
||||
E('td', { width: '80%', align: 'left' }, editListBtn)
|
||||
]),
|
||||
E('tr', [
|
||||
E('td', { width: '20%', align: 'right' }, _('Backup Config:')),
|
||||
E('td', { width: '80%', align: 'left' }, [downloadBtn, ' ', downloadStatus])
|
||||
]),
|
||||
E('tr', [
|
||||
E('td', { width: '20%', align: 'right' }, _('Restore Backup:')),
|
||||
E('td', { width: '80%', align: 'left' }, restoreBtn)
|
||||
])
|
||||
])
|
||||
]),
|
||||
E('h2', _('Snapshot Management')),
|
||||
E('p', _('Create a snapshot of the current system configuration, or restore to a snapshot.')),
|
||||
E('div', { class: 'cbi-section' }, [
|
||||
E('div', { style: 'margin-bottom:10px' }, createSnapBtn),
|
||||
snapDiv
|
||||
])
|
||||
];
|
||||
|
||||
if (platform.has_kvm) {
|
||||
const kvmBtn = E('input', {
|
||||
type: 'button', class: 'cbi-button cbi-button-save',
|
||||
value: _('Switch System'),
|
||||
click: ui.createHandlerFn(view, function (ev) {
|
||||
if (!confirm(_('Are you sure you want to switch systems?'))) return;
|
||||
ev.currentTarget.disabled = true;
|
||||
ui.showModal(_('System is switching...'), [
|
||||
E('p', { class: 'spinning' }, _('Waiting for system switching...'))
|
||||
]);
|
||||
return callKvmSwitch().then(function () {
|
||||
if (ui.awaitReconnect) {
|
||||
ui.awaitReconnect();
|
||||
} else {
|
||||
// Fallback for older LuCI builds without awaitReconnect
|
||||
setTimeout(function () { location.reload(); }, 5000);
|
||||
}
|
||||
});
|
||||
})
|
||||
});
|
||||
sections.push(E('h2', _('KVM dual system switching')));
|
||||
sections.push(E('p', _('You can freely switch between KVM dual partitions, using OpenWrt systems in different partitions.')));
|
||||
sections.push(E('div', { class: 'cbi-section' }, [kvmBtn]));
|
||||
}
|
||||
|
||||
return E('div', {}, sections);
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,79 @@
|
||||
// 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.
|
||||
|
||||
'use strict';
|
||||
'require view';
|
||||
'require rpc';
|
||||
'require ui';
|
||||
'require fs';
|
||||
|
||||
// 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).
|
||||
const callPrime = rpc.declare({ object: 'luci.amlogic', method: 'prime_backup_list' });
|
||||
|
||||
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 () {
|
||||
// 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) {
|
||||
// Full-width multi-line editor pre-filled with the existing list.
|
||||
const ta = E('textarea', {
|
||||
rows: '30',
|
||||
style: 'width:100%; font-family:monospace'
|
||||
}, text || '');
|
||||
|
||||
// Status hint area (saved successfully / failed).
|
||||
const status = E('span', { style: 'margin-left:1em' });
|
||||
const saveBtn = E('input', {
|
||||
type: 'button', class: 'cbi-button cbi-button-save',
|
||||
value: _('Save'),
|
||||
click: ui.createHandlerFn(this, function (ev) {
|
||||
const btn = ev.currentTarget;
|
||||
btn.disabled = true;
|
||||
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.
|
||||
setTimeout(function () {
|
||||
location.href = L.url('admin/system/amlogic/backup');
|
||||
}, 700);
|
||||
} else {
|
||||
status.textContent = _('Save Failed');
|
||||
}
|
||||
}).catch(function (e) {
|
||||
status.textContent = _('Save Failed') + ': ' + (e && e.message ? e.message : String(e));
|
||||
}).finally(function () { btn.disabled = false; });
|
||||
})
|
||||
});
|
||||
// Return to the Backup main page (without saving).
|
||||
const backBtn = E('input', {
|
||||
type: 'button', class: 'cbi-button cbi-button-reset',
|
||||
value: _('Back'),
|
||||
click: function () { location.href = L.url('admin/system/amlogic/backup'); }
|
||||
});
|
||||
|
||||
return E('div', {}, [
|
||||
E('h2', _('Edit list of backup files')),
|
||||
E('p', _('In this list, you can manually add the file or directory list of files that need to be backed up. Please confirm that the format and content of the file are correct.')),
|
||||
E('div', { class: 'cbi-section' }, [ta]),
|
||||
E('div', { class: 'cbi-page-actions' }, [saveBtn, ' ', backBtn, status])
|
||||
]);
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,487 @@
|
||||
// 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 <input> button string that we parse and render as a
|
||||
// real DOM button so the user can explicitly click Download → then Update.
|
||||
|
||||
'use strict';
|
||||
'require view';
|
||||
'require rpc';
|
||||
'require ui';
|
||||
'require dom';
|
||||
'require poll';
|
||||
'require view.amlogic.shared as amlogicShared';
|
||||
|
||||
// ── RPCs ────────────────────────────────────────────────────────────────────
|
||||
const callState = rpc.declare({ object: 'luci.amlogic', method: 'state' });
|
||||
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'] });
|
||||
const callStartPlugin = rpc.declare({ object: 'luci.amlogic', method: 'start_plugin' });
|
||||
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' });
|
||||
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 />
|
||||
function parseButtonLine(line) {
|
||||
if (!line || line.indexOf('<input') === -1) return null;
|
||||
|
||||
// Extract value="..."
|
||||
var valueMatch = line.match(/value="([^"]*)"/);
|
||||
var onclickMatch = line.match(/onclick="([^"]*)"/);
|
||||
if (!valueMatch || !onclickMatch) return null;
|
||||
|
||||
var value = valueMatch[1];
|
||||
var onclick = onclickMatch[1];
|
||||
|
||||
// Text after the self-closing tag
|
||||
var labelMatch = line.match(/\/>(.*)$/);
|
||||
var label = labelMatch ? labelMatch[1].trim() : '';
|
||||
|
||||
// Plugin download: b_check_plugin(this, 'download_VERSION')
|
||||
if (value === 'Download' && onclick.indexOf('b_check_plugin') !== -1) {
|
||||
var m = onclick.match(/b_check_plugin\s*\(\s*this\s*,\s*'([^']*)'\s*\)/);
|
||||
return { type: 'plugin-download', param: m ? m[1] : null, label: label };
|
||||
}
|
||||
|
||||
// Firmware download: b_check_firmware(this, 'PARAM')
|
||||
if (value === 'Download' && onclick.indexOf('b_check_firmware') !== -1) {
|
||||
var m = onclick.match(/b_check_firmware\s*\(\s*this\s*,\s*'([^']*)'\s*\)/);
|
||||
return { type: 'firmware-download', param: m ? m[1] : null, label: label };
|
||||
}
|
||||
|
||||
// Kernel download: b_check_kernel(this, 'PARAM')
|
||||
if (value === 'Download' && onclick.indexOf('b_check_kernel') !== -1) {
|
||||
var m = onclick.match(/b_check_kernel\s*\(\s*this\s*,\s*'([^']*)'\s*\)/);
|
||||
return { type: 'kernel-download', param: m ? m[1] : null, label: label };
|
||||
}
|
||||
|
||||
// Firmware install: amlogic_update(this, 'PARAM')
|
||||
if (value === 'Update' && onclick.indexOf('amlogic_update') !== -1) {
|
||||
var m = onclick.match(/amlogic_update\s*\(\s*this\s*,\s*'([^']*)'\s*\)/);
|
||||
return { type: 'firmware-update', param: m ? m[1] : null, label: label };
|
||||
}
|
||||
|
||||
// Kernel install: amlogic_kernel(this)
|
||||
if (value === 'Update' && onclick.indexOf('amlogic_kernel') !== -1) {
|
||||
return { type: 'kernel-update', param: null, label: label };
|
||||
}
|
||||
|
||||
// Plugin install: amlogic_plugin(this)
|
||||
if (value === 'Update' && onclick.indexOf('amlogic_plugin') !== -1) {
|
||||
return { type: 'plugin-update', param: null, label: label };
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
// ── Per-action state machine ─────────────────────────────────────────────────
|
||||
// States: 'idle' | 'checking' | 'button' | 'installing' | 'done'
|
||||
//
|
||||
// statusEl – the span shown in the right column
|
||||
// Each machine is a plain object; we keep one per action.
|
||||
|
||||
function makeAction(name, statusEl) {
|
||||
return { name: name, statusEl: statusEl, state: 'idle', subBtn: null };
|
||||
}
|
||||
|
||||
// Update the status span with a text string (blue info style).
|
||||
function showInfo(el, text) {
|
||||
dom.content(el, E('span', { class: 'amlogic-status-info' }, text));
|
||||
}
|
||||
|
||||
// Update the status span with a success string (green).
|
||||
function showOk(el, text) {
|
||||
dom.content(el, E('span', { class: 'amlogic-status-ok' }, text));
|
||||
}
|
||||
|
||||
// Update the status span with an error string (red).
|
||||
function showErr(el, text) {
|
||||
dom.content(el, E('span', { class: 'amlogic-status-err' }, text));
|
||||
}
|
||||
|
||||
// Replace statusEl contents with a real DOM button + optional label text.
|
||||
// onClick is called when the user presses it.
|
||||
function showSubButton(el, btnLabel, labelText, onClick) {
|
||||
var btn = E('input', {
|
||||
type: 'button',
|
||||
class: 'cbi-button cbi-button-reload',
|
||||
value: btnLabel,
|
||||
click: onClick
|
||||
});
|
||||
var nodes = [btn];
|
||||
if (labelText) nodes.push(E('span', { class: 'amlogic-status-info' }, ' ' + labelText));
|
||||
dom.content(el, nodes);
|
||||
return btn;
|
||||
}
|
||||
|
||||
// ── Poll handler (called at 1 Hz) ────────────────────────────────────────────
|
||||
function handlePoll(action) {
|
||||
if (action.state === 'done') return;
|
||||
|
||||
return callLogTail(action.name).then(function (raw) {
|
||||
// read_log_tail returns up to 4096 bytes (multi-line). Only inspect
|
||||
// the last non-empty line so that intermediate opkg/apk output
|
||||
// (e.g. "Collected errors:", dependency warnings) cannot trigger a
|
||||
// false-positive 'Failed'/'error' match before the final result is
|
||||
// written. The terminal keywords ("Successful Update", "FAILED",
|
||||
// etc.) are always written as the very last log line.
|
||||
var lines = (raw || '').split('\n');
|
||||
var line = '';
|
||||
for (var i = lines.length - 1; i >= 0; i--) {
|
||||
var l = lines[i].trim();
|
||||
if (l) { line = l; break; }
|
||||
}
|
||||
|
||||
if (action.state === 'idle') {
|
||||
return;
|
||||
}
|
||||
|
||||
if (action.state === 'button') {
|
||||
// Waiting for user to click sub-button; don't re-render on every poll.
|
||||
return;
|
||||
}
|
||||
|
||||
if (action.state === 'installing') {
|
||||
// Show progress lines; detect terminal keywords.
|
||||
if (!line) return;
|
||||
// Skip if line is the same HTML button (stale log from check phase).
|
||||
if (line.indexOf('<input') !== -1) return;
|
||||
if (line.indexOf('Successfully updated') !== -1 ||
|
||||
line.indexOf('Successful Update') !== -1 ||
|
||||
line.indexOf('Successful') !== -1) {
|
||||
showOk(action.statusEl, line);
|
||||
action.state = 'done';
|
||||
} else if (line.indexOf('Failed') !== -1 || line.indexOf('failed') !== -1 ||
|
||||
line.indexOf('Error') !== -1 || line.indexOf('error') !== -1) {
|
||||
showErr(action.statusEl, line);
|
||||
action.state = 'done';
|
||||
} else {
|
||||
// Show current progress line (may change rapidly).
|
||||
showInfo(action.statusEl, line);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// state === 'checking': look for HTML button line or plain progress text.
|
||||
var parsed = parseButtonLine(line);
|
||||
if (parsed) {
|
||||
action.state = 'button';
|
||||
action._parsed = parsed;
|
||||
renderParsedButton(action);
|
||||
} else if (line) {
|
||||
showInfo(action.statusEl, line);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Render the sub-button extracted from the log line. The onClick triggers the
|
||||
// appropriate second-phase RPC and transitions to 'installing'.
|
||||
function renderParsedButton(action) {
|
||||
var parsed = action._parsed;
|
||||
var el = action.statusEl;
|
||||
|
||||
switch (parsed.type) {
|
||||
|
||||
case 'plugin-download':
|
||||
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
|
||||
callCheckPlugin(parsed.param).then(function () {});
|
||||
});
|
||||
break;
|
||||
|
||||
case 'firmware-download':
|
||||
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
|
||||
});
|
||||
});
|
||||
break;
|
||||
|
||||
case 'kernel-download':
|
||||
showSubButton(el, _('Download'), parsed.label, function (ev) {
|
||||
ev.currentTarget.disabled = true;
|
||||
ev.currentTarget.value = _('Downloading...');
|
||||
action.state = 'checking';
|
||||
callCheckKernel(parsed.param).then(function () {});
|
||||
});
|
||||
break;
|
||||
|
||||
case 'firmware-update':
|
||||
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.
|
||||
action.state = 'installing';
|
||||
dom.content(el, E('span', { class: 'amlogic-status-info' }, _('Starting update...')));
|
||||
callStartUpdate(parsed.param).catch(function () {
|
||||
// Only show error if the trigger call itself fails (rare).
|
||||
showErr(el, _('Failed to start update'));
|
||||
action.state = 'done';
|
||||
});
|
||||
});
|
||||
break;
|
||||
|
||||
case 'kernel-update':
|
||||
showSubButton(el, _('Update'), parsed.label, function (ev) {
|
||||
ev.currentTarget.disabled = true;
|
||||
ev.currentTarget.value = _('Updating...');
|
||||
action.state = 'installing';
|
||||
dom.content(el, E('span', { class: 'amlogic-status-info' }, _('Starting kernel update...')));
|
||||
callStartKernel().catch(function () {
|
||||
showErr(el, _('Failed to start kernel update'));
|
||||
action.state = 'done';
|
||||
});
|
||||
});
|
||||
break;
|
||||
|
||||
case 'plugin-update':
|
||||
showSubButton(el, _('Update'), parsed.label, function (ev) {
|
||||
ev.currentTarget.disabled = true;
|
||||
ev.currentTarget.value = _('Updating...');
|
||||
action.state = 'installing';
|
||||
dom.content(el, E('span', { class: 'amlogic-status-info' }, _('Starting plugin update...')));
|
||||
callStartPlugin().catch(function () {
|
||||
showErr(el, _('Failed to start plugin update'));
|
||||
action.state = 'done';
|
||||
});
|
||||
});
|
||||
break;
|
||||
|
||||
default:
|
||||
showInfo(el, parsed.label || '');
|
||||
}
|
||||
}
|
||||
|
||||
// ── Main view ────────────────────────────────────────────────────────────────
|
||||
return view.extend({
|
||||
handleSave: null,
|
||||
handleSaveApply: null,
|
||||
handleReset: null,
|
||||
|
||||
render: function () {
|
||||
amlogicShared.ensureCss();
|
||||
|
||||
// Version display spans (left persistent, filled by state RPC)
|
||||
var verFirmware = E('span', _('Collecting data...'));
|
||||
var verPlugin = E('span', _('Collecting data...'));
|
||||
var verKernel = E('span', _('Collecting data...'));
|
||||
var verRescue = E('span', _('Collecting data...'));
|
||||
var branchSpan = E('span');
|
||||
|
||||
// Per-action status spans (right side, updated by poll)
|
||||
var statusFw = E('span');
|
||||
var statusPl = E('span');
|
||||
var statusKn = E('span');
|
||||
var statusRs = E('span');
|
||||
|
||||
// State machines
|
||||
var actPlugin = makeAction('plugin', statusPl);
|
||||
var actKernel = makeAction('kernel', statusKn);
|
||||
var actFirmware = makeAction('firmware', statusFw);
|
||||
var actRescue = makeAction('rescue', statusRs);
|
||||
|
||||
// ── Action buttons (left column) ──────────────────────────────────
|
||||
|
||||
// Helper: reset an action back to idle so it can be re-run after 'done'.
|
||||
function resetAction(action, btn, idleLabel) {
|
||||
action.state = 'idle';
|
||||
action.subBtn = null;
|
||||
dom.content(action.statusEl, '');
|
||||
btn.disabled = false;
|
||||
btn.value = idleLabel;
|
||||
}
|
||||
|
||||
var btnPlugin = E('input', {
|
||||
type: 'button', class: 'cbi-button cbi-button-reload',
|
||||
value: _('Only update Amlogic Service'),
|
||||
click: ui.createHandlerFn(this, function (ev) {
|
||||
var btn = ev.currentTarget;
|
||||
// Allow re-run after completion by resetting 'done' → 'idle'
|
||||
if (actPlugin.state === 'done')
|
||||
resetAction(actPlugin, btn, _('Only update Amlogic Service'));
|
||||
if (actPlugin.state !== 'idle') return;
|
||||
btn.disabled = true;
|
||||
btn.value = _('Checking...');
|
||||
actPlugin.state = 'checking';
|
||||
dom.content(actPlugin.statusEl, '');
|
||||
return callCheckPlugin('check').then(function () {
|
||||
btn.disabled = false;
|
||||
btn.value = _('Only update Amlogic Service');
|
||||
}).catch(function () {
|
||||
btn.disabled = false;
|
||||
btn.value = _('Only update Amlogic Service');
|
||||
showErr(actPlugin.statusEl, _('Check Failed'));
|
||||
actPlugin.state = 'idle';
|
||||
});
|
||||
})
|
||||
});
|
||||
|
||||
var btnKernel = E('input', {
|
||||
type: 'button', class: 'cbi-button cbi-button-reload',
|
||||
value: _('Update system kernel only'),
|
||||
click: ui.createHandlerFn(this, function (ev) {
|
||||
var btn = ev.currentTarget;
|
||||
if (actKernel.state === 'done')
|
||||
resetAction(actKernel, btn, _('Update system kernel only'));
|
||||
if (actKernel.state !== 'idle') return;
|
||||
btn.disabled = true;
|
||||
btn.value = _('Checking...');
|
||||
actKernel.state = 'checking';
|
||||
dom.content(actKernel.statusEl, '');
|
||||
return callCheckKernel('check').then(function () {
|
||||
btn.disabled = false;
|
||||
btn.value = _('Update system kernel only');
|
||||
}).catch(function () {
|
||||
btn.disabled = false;
|
||||
btn.value = _('Update system kernel only');
|
||||
showErr(actKernel.statusEl, _('Check Failed'));
|
||||
actKernel.state = 'idle';
|
||||
});
|
||||
})
|
||||
});
|
||||
|
||||
var btnFirmware = E('input', {
|
||||
type: 'button', class: 'cbi-button cbi-button-reload',
|
||||
value: _('Complete system update'),
|
||||
click: ui.createHandlerFn(this, function (ev) {
|
||||
var btn = ev.currentTarget;
|
||||
if (actFirmware.state === 'done')
|
||||
resetAction(actFirmware, btn, _('Complete system update'));
|
||||
if (actFirmware.state !== 'idle') return;
|
||||
btn.disabled = true;
|
||||
btn.value = _('Checking...');
|
||||
actFirmware.state = 'checking';
|
||||
dom.content(actFirmware.statusEl, '');
|
||||
return callCheckFirmware('check').then(function () {
|
||||
btn.disabled = false;
|
||||
btn.value = _('Complete system update');
|
||||
}).catch(function () {
|
||||
btn.disabled = false;
|
||||
btn.value = _('Complete system update');
|
||||
showErr(actFirmware.statusEl, _('Check Failed'));
|
||||
actFirmware.state = 'idle';
|
||||
});
|
||||
})
|
||||
});
|
||||
|
||||
var btnRescue = E('input', {
|
||||
type: 'button', class: 'cbi-button cbi-button-reload',
|
||||
value: _('Rescue the original system kernel'),
|
||||
click: ui.createHandlerFn(this, function (ev) {
|
||||
var btn = ev.currentTarget;
|
||||
if (actRescue.state === 'done')
|
||||
resetAction(actRescue, btn, _('Rescue the original system kernel'));
|
||||
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.
|
||||
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 () {
|
||||
btn.disabled = false;
|
||||
btn.value = _('Rescue the original system kernel');
|
||||
showErr(actRescue.statusEl, _('Failed to start rescue'));
|
||||
actRescue.state = 'idle';
|
||||
});
|
||||
})
|
||||
});
|
||||
|
||||
// ── Layout ────────────────────────────────────────────────────────
|
||||
|
||||
var body = E('div', {}, [
|
||||
E('h2', _('Check Update')),
|
||||
E('p', _('Provide OpenWrt Firmware, Kernel and Plugin online check, download and update service.')),
|
||||
E('div', { class: 'cbi-section' }, [
|
||||
E('p', { style: 'text-align:center' }, [
|
||||
_('Update plugins first, then update the kernel or firmware. More options can be configured in [Plugin Settings].'),
|
||||
' ', branchSpan
|
||||
]),
|
||||
E('table', { class: 'amlogic-row-table' }, [
|
||||
E('tr', [
|
||||
E('td', { width: '35%', align: 'right' }, btnPlugin),
|
||||
E('td', { width: '65%', align: 'left' }, [verPlugin, ' ', statusPl])
|
||||
]),
|
||||
E('tr', [
|
||||
E('td', { width: '35%', align: 'right' }, btnKernel),
|
||||
E('td', { width: '65%', align: 'left' }, [verKernel, ' ', statusKn])
|
||||
]),
|
||||
E('tr', [
|
||||
E('td', { width: '35%', align: 'right' }, btnFirmware),
|
||||
E('td', { width: '65%', align: 'left' }, [verFirmware, ' ', statusFw])
|
||||
])
|
||||
])
|
||||
]),
|
||||
E('h2', _('Rescue Kernel')),
|
||||
E('p', _('When a kernel update fails and causes the OpenWrt system to be unbootable, the kernel can be restored by mutual recovery from eMMC/NVMe/sdX.')),
|
||||
E('div', { class: 'cbi-section' }, [
|
||||
E('table', { class: 'amlogic-row-table' }, [
|
||||
E('tr', [
|
||||
E('td', { width: '35%', align: 'right' }, btnRescue),
|
||||
E('td', { width: '65%', align: 'left' }, [verRescue, ' ', statusRs])
|
||||
])
|
||||
])
|
||||
])
|
||||
]);
|
||||
|
||||
// ── Populate version spans ────────────────────────────────────────
|
||||
|
||||
callState().then(function (s) {
|
||||
dom.content(verFirmware, s && s.current_firmware_version
|
||||
? E('span', { class: 'amlogic-status-ok' }, _('Current Version') + ' [ ' + s.current_firmware_version + ' ] ')
|
||||
: E('span', { class: 'amlogic-status-err' }, _('Invalid value.')));
|
||||
dom.content(verPlugin, s && s.current_plugin_version
|
||||
? E('span', { class: 'amlogic-status-ok' }, _('Current Version') + ' [ ' + s.current_plugin_version + ' ] ')
|
||||
: E('span', { class: 'amlogic-status-err' }, _('Invalid value.')));
|
||||
dom.content(verKernel, s && s.current_kernel_version
|
||||
? E('span', { class: 'amlogic-status-ok' }, _('Current Version') + ' [ ' + s.current_kernel_version + ' ] ')
|
||||
: E('span', { class: 'amlogic-status-err' }, _('Invalid value.')));
|
||||
dom.content(branchSpan, s && s.current_kernel_branch
|
||||
? ' [ ' + s.current_kernel_branch + '.y ] '
|
||||
: ' [ ' + _('Invalid value.') + ' ]');
|
||||
|
||||
// Rescue row: show same kernel version directly via the verRescue span
|
||||
dom.content(verRescue, s && s.current_kernel_version
|
||||
? E('span', { class: 'amlogic-status-ok' }, _('Current Version') + ' [ ' + s.current_kernel_version + ' ] ')
|
||||
: E('span', { class: 'amlogic-status-err' }, _('Invalid value.')));
|
||||
});
|
||||
|
||||
// ── 1 Hz poll ─────────────────────────────────────────────────────
|
||||
|
||||
poll.add(function () {
|
||||
return Promise.all([
|
||||
handlePoll(actPlugin),
|
||||
handlePoll(actKernel),
|
||||
handlePoll(actFirmware),
|
||||
handlePoll(actRescue)
|
||||
]);
|
||||
}, 1);
|
||||
|
||||
return body;
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,163 @@
|
||||
// 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.
|
||||
|
||||
'use strict';
|
||||
'require view';
|
||||
'require form';
|
||||
'require rpc';
|
||||
'require uci';
|
||||
|
||||
// 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.
|
||||
const callState = rpc.declare({ object: 'luci.amlogic', method: 'state' });
|
||||
|
||||
return view.extend({
|
||||
load: function () {
|
||||
return Promise.all([
|
||||
callPlatform(),
|
||||
callState(),
|
||||
uci.load('amlogic')
|
||||
]);
|
||||
},
|
||||
|
||||
render: function (data) {
|
||||
const platform = data[0] || {};
|
||||
const state = data[1] || {};
|
||||
|
||||
// Auto-create the section if missing
|
||||
if (!uci.get('amlogic', 'config')) {
|
||||
uci.add('amlogic', 'amlogic', 'config');
|
||||
}
|
||||
|
||||
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].') +
|
||||
'<br />' + _('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');
|
||||
o.anonymous = true;
|
||||
|
||||
// 1. Display device platform
|
||||
const dev = o.option(form.DummyValue, 'mydevice', _('Current Device:'),
|
||||
_('Display the PLATFORM classification of the device.'));
|
||||
dev.cfgvalue = function () { return 'PLATFORM: ' + (platform.platform || 'Unknown'); };
|
||||
|
||||
// 2. OpenWrt download repository
|
||||
const repo = o.option(form.Value, 'amlogic_firmware_repo',
|
||||
_('OpenWrt download repository:'),
|
||||
_('Set the OpenWrt files download repository on github.com in [Online Download Update].'));
|
||||
repo.default = 'https://github.com/breakingbadboy/OpenWrt';
|
||||
repo.rmempty = false;
|
||||
|
||||
// 3. Tags keyword
|
||||
const tag = o.option(form.Value, 'amlogic_firmware_tag',
|
||||
_('OpenWrt download tags keyword:'),
|
||||
_('Set the OpenWrt files download tags keyword for github.com in [Online Download Update].'));
|
||||
tag.default = 'ARMv8';
|
||||
tag.rmempty = false;
|
||||
|
||||
// 4. File suffix
|
||||
const suffix = o.option(form.ListValue, 'amlogic_firmware_suffix',
|
||||
_('OpenWrt files suffix:'),
|
||||
_('Set the OpenWrt files download suffix for github.com in [Online Download Update].'));
|
||||
['.7z', '.zip', '.img.gz', '.img.xz'].forEach(function (s) { suffix.value(s, _(s)); });
|
||||
suffix.default = '.img.gz';
|
||||
suffix.rmempty = false;
|
||||
|
||||
// 5. Kernel download repository
|
||||
const kpath = o.option(form.ListValue, 'amlogic_kernel_path',
|
||||
_('Kernel download repository:'),
|
||||
_('Set the kernel files download repository on github.com in [Online Download Update].'));
|
||||
kpath.value('https://github.com/breakingbadboy/OpenWrt');
|
||||
kpath.value('https://github.com/ophub/kernel');
|
||||
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.
|
||||
const currentKpath = uci.get('amlogic', 'config', 'amlogic_kernel_path') ||
|
||||
'https://github.com/breakingbadboy/OpenWrt';
|
||||
const knownTags = {
|
||||
kernel_rk3588: 'kernel_rk3588 [Rockchip RK3588 Kernel]',
|
||||
kernel_rk35xx: 'kernel_rk35xx [Rockchip RK35xx Kernel]',
|
||||
kernel_stable: 'kernel_stable [Mainline Stable Kernel]'
|
||||
};
|
||||
if (currentKpath.indexOf('ophub/kernel') >= 0) {
|
||||
knownTags.kernel_flippy = 'kernel_flippy [Mainline Stable Kernel by Flippy]';
|
||||
knownTags.kernel_h6 = 'kernel_h6 [Allwinner H6 Kernel]';
|
||||
knownTags.kernel_beta = 'kernel_beta [Beta Kernel]';
|
||||
}
|
||||
// Determine default tag from existing config or uname
|
||||
let kernelTagDefault = uci.get('amlogic', 'config', 'amlogic_kernel_tags') || '';
|
||||
if (!kernelTagDefault) {
|
||||
const u = state.kernel_release || '';
|
||||
if (u.indexOf('-rk3588') >= 0) kernelTagDefault = 'kernel_rk3588';
|
||||
else if (u.indexOf('-rk35xx') >= 0) kernelTagDefault = 'kernel_rk35xx';
|
||||
else if (u.indexOf('-h6') >= 0 || u.indexOf('-zicai') >= 0) kernelTagDefault = 'kernel_h6';
|
||||
else kernelTagDefault = 'kernel_stable';
|
||||
}
|
||||
const ktags = o.option(form.ListValue, 'amlogic_kernel_tags',
|
||||
_('Kernel download tags:'),
|
||||
_('Set the kernel files download tags on github.com in [Online Download Update].'));
|
||||
Object.keys(knownTags).forEach(function (k) { ktags.value(k, _(knownTags[k])); });
|
||||
if (!knownTags[kernelTagDefault]) ktags.value(kernelTagDefault, kernelTagDefault);
|
||||
ktags.default = kernelTagDefault;
|
||||
ktags.rmempty = false;
|
||||
|
||||
// 7. Kernel branch
|
||||
const kbranch = o.option(form.ListValue, 'amlogic_kernel_branch',
|
||||
_('Set version branch:'),
|
||||
_('Set the version branch of the OpenWrt files and kernel selected in [Online Download Update].'));
|
||||
['5.4', '5.10', '5.15', '6.1', '6.6', '6.12', '6.18'].forEach(function (b) {
|
||||
kbranch.value(b, _(b));
|
||||
});
|
||||
const m2 = (state.kernel_release || '').match(/^(\d+\.\d+)/);
|
||||
kbranch.default = m2 ? m2[1] : '5.10';
|
||||
kbranch.rmempty = false;
|
||||
|
||||
// 8. Plugin branch
|
||||
const pbranch = o.option(form.ListValue, 'amlogic_plugin_branch',
|
||||
_('Set plugin branch:'),
|
||||
_('Set the branch of the luci-app-amlogic plugin used in [Only update Amlogic Service]. Default (empty) uses the main (JavaScript) branch.'));
|
||||
pbranch.value('', _('main [JavaScript version]'));
|
||||
pbranch.value('lua', _('lua [Lua version]'));
|
||||
pbranch.default = '';
|
||||
pbranch.rmempty = true;
|
||||
|
||||
// 9. Keep config
|
||||
const fcfg = o.option(form.Flag, 'amlogic_firmware_config',
|
||||
_('Keep config update:'),
|
||||
_('Set whether to keep the current config during [Online Download Update] and [Manually Upload Update].'));
|
||||
fcfg.default = '1';
|
||||
fcfg.rmempty = false;
|
||||
|
||||
// 9. Write bootloader
|
||||
const wb = o.option(form.Flag, 'amlogic_write_bootloader',
|
||||
_('Auto write bootloader:'),
|
||||
_('[Recommended choice] Set whether to auto write bootloader during install and update OpenWrt.'));
|
||||
wb.default = '0';
|
||||
wb.rmempty = false;
|
||||
|
||||
// 10. Shared partition fs type
|
||||
const fst = o.option(form.ListValue, 'amlogic_shared_fstype',
|
||||
_('Set the file system type:'),
|
||||
_('[Default ext4] Set the file system type of the shared partition (/mnt/mmcblk*p4) when install OpenWrt.'));
|
||||
['ext4', 'f2fs', 'btrfs', 'xfs'].forEach(function (s) { fst.value(s, _(s)); });
|
||||
fst.default = 'ext4';
|
||||
fst.rmempty = false;
|
||||
|
||||
return m.render();
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,126 @@
|
||||
// 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.
|
||||
|
||||
'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.
|
||||
const callAuthor = rpc.declare({
|
||||
object: 'luci.amlogic',
|
||||
method: 'author',
|
||||
expect: { openwrt_author: 'Invalid value.' }
|
||||
});
|
||||
|
||||
// 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' });
|
||||
|
||||
// Static list of supported boxes (display only, not used in any logic).
|
||||
const SUPPORTED_BOXES = [
|
||||
_('Amlogic s922x --- [ Beelink, Beelink-Pro, Ugoos-AM6-Plus, ODROID-N2, Khadas-VIM3, Ali-CT2000 ]'),
|
||||
_('Amlogic s905x3 -- [ X96-Max+, HK1-Box, H96-Max-X3, Ugoos-X3, TX3, X96-Air, A95XF3-Air ]'),
|
||||
_('Amlogic s905x2 -- [ X96Max-4G, X96Max-2G, MECOOL-KM3-4G, Tanix-Tx5-Max, A95X-F2 ]'),
|
||||
_('Amlogic s912 ---- [ H96-Pro-Plus, Octopus-Planet, A1, A2, Z6-Plus, TX92, X92, TX8-MAX, TX9-Pro ]'),
|
||||
_('Amlogic s905x --- [ HG680P, B860H, TBee, T95, TX9, XiaoMI-3S, X96 ]'),
|
||||
_('Amlogic s905w --- [ X96-Mini, TX3-Mini, W95, X96W/FunTV, MXQ-Pro-4K ]'),
|
||||
_('Amlogic s905d --- [ Phicomm-N1, MECOOL-KI-Pro, SML-5442TW ]'),
|
||||
_('Amlogic s905l --- [ UNT402A, M201-S ]'),
|
||||
_('Amlogic s905l2 -- [ MGV2000, MGV3000, Wojia-TV-IPBS9505, M301A, E900v21E ]'),
|
||||
_('Amlogic s905l3 -- [ CM211-1, CM311-1, HG680-LC, M401A, UNT400G1, UNT402A, ZXV10-BV310 ]'),
|
||||
_('Amlogic s905l3a - [ E900V22C/D, CM311-1a-YST, M401A, M411A, UNT403A, UNT413A, IP112H ]'),
|
||||
_('Amlogic s905l3b - [ CM211-1, CM311-1, E900V22D, E900V21E, E900V22E, M302A/M304A ]'),
|
||||
_('Amlogic s905 ---- [ Beelink-Mini-MX-2G, Sunvell-T95M, MXQ-Pro+4K, SumaVision-Q5 ]'),
|
||||
_('Allwinner H6 ---- [ V-Plus Cloud ]'),
|
||||
_('Rockchip -------- [ BeikeYun, L1-Pro, FastRhino R66S/R68S, Radxa 5B/E25 ]'),
|
||||
_('Used in KVM ----- [ Can be used in KVM virtual machine of Armbian system. ]')
|
||||
];
|
||||
|
||||
// Open an external link in a new tab; fall back to the current window if the
|
||||
// browser blocks the popup.
|
||||
function openExternal(url) {
|
||||
const w = window.open(url, '_blank', 'noopener,noreferrer');
|
||||
if (!w) window.location.href = url;
|
||||
}
|
||||
|
||||
return view.extend({
|
||||
// Read-only info page: disable the top Save / Apply / Reset buttons.
|
||||
handleSave: null,
|
||||
handleSaveApply: null,
|
||||
handleReset: null,
|
||||
|
||||
load: function () {
|
||||
// Fire sync_menu in background (don't block render on it).
|
||||
callSyncMenu().catch(function () {});
|
||||
return callAuthor();
|
||||
},
|
||||
|
||||
render: function (authorUrl) {
|
||||
// Inject the theme stylesheet on first entry so amlogic-* classes work.
|
||||
amlogicShared.ensureCss();
|
||||
const res = L.resource('amlogic');
|
||||
|
||||
// Flatten the supported-boxes list to [text, <br>, text, <br>, ...].
|
||||
const boxRows = [];
|
||||
for (let i = 0; i < SUPPORTED_BOXES.length; i++)
|
||||
boxRows.push(SUPPORTED_BOXES[i], E('br'));
|
||||
|
||||
return E('div', { class: 'cbi-section' }, [
|
||||
E('h2', { style: 'border-bottom:none' }, _('Amlogic Service')),
|
||||
E('p', { style: 'border:none; margin-bottom:0' }, _('Supports management of Amlogic s9xxx, Allwinner (V-Plus Cloud), and Rockchip (BeikeYun, Chainedbox L1 Pro) boxes.')),
|
||||
E('table', { class: 'amlogic-row-table' }, [
|
||||
E('tr', [
|
||||
E('td', { colspan: '2', style: 'text-align:center' }, [
|
||||
E('img', { src: res + '/logo.png', alt: 'Logo', width: '135' })
|
||||
])
|
||||
]),
|
||||
E('tr', [
|
||||
E('td', { colspan: '2', style: 'text-align:center' }, [
|
||||
E('img', {
|
||||
src: res + '/packit.svg', alt: 'Packit',
|
||||
width: '168', height: '20',
|
||||
style: 'cursor:pointer; margin:0 5px',
|
||||
click: function () { openExternal('https://github.com/unifreq/openwrt_packit'); }
|
||||
}),
|
||||
E('img', {
|
||||
src: res + '/author.svg', alt: 'Author',
|
||||
width: '168', height: '20',
|
||||
style: 'cursor:pointer; margin:0 5px',
|
||||
click: function () {
|
||||
callAuthor().then(function (repo) {
|
||||
let url = String(repo || '').trim();
|
||||
if (!url) return;
|
||||
if (url.indexOf('https') === -1)
|
||||
url = 'https://github.com/' + url;
|
||||
openExternal(url);
|
||||
});
|
||||
}
|
||||
}),
|
||||
E('img', {
|
||||
src: res + '/plugin.svg', alt: 'luci-app-amlogic',
|
||||
width: '160', height: '20',
|
||||
style: 'cursor:pointer; margin:0 5px',
|
||||
click: function () { openExternal('https://github.com/ophub/luci-app-amlogic'); }
|
||||
})
|
||||
])
|
||||
]),
|
||||
E('tr', [
|
||||
E('td', { width: '20%', align: 'right' }, _('Supported functions:')),
|
||||
E('td', { width: '80%', align: 'left' },
|
||||
_('Provide services such as install to EMMC, Update Firmware and Kernel, Backup and Recovery Config, Snapshot management, etc.'))
|
||||
]),
|
||||
E('tr', [
|
||||
E('td', { width: '20%', align: 'right', style: 'vertical-align:top; padding-top:4px' }, _('Supported Boxes:')),
|
||||
E('td', { width: '80%', align: 'left', style: 'line-height:1.8' }, boxRows)
|
||||
])
|
||||
])
|
||||
]);
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,163 @@
|
||||
// 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.
|
||||
|
||||
'use strict';
|
||||
'require view';
|
||||
'require rpc';
|
||||
'require ui';
|
||||
'require dom';
|
||||
'require poll';
|
||||
'require view.amlogic.shared as amlogicShared';
|
||||
|
||||
// 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.
|
||||
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: '' } });
|
||||
|
||||
return view.extend({
|
||||
handleSave: null,
|
||||
handleSaveApply: null,
|
||||
handleReset: null,
|
||||
|
||||
load: function () {
|
||||
return callDB();
|
||||
},
|
||||
|
||||
render: function (entries) {
|
||||
amlogicShared.ensureCss();
|
||||
// Model dropdown: 0 = placeholder, 99 = manual entry, anything in between
|
||||
// is a model from the 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) {
|
||||
sel.appendChild(E('option', { value: e.id },
|
||||
'[ ' + e.id + ' ] ' + e.name));
|
||||
});
|
||||
sel.appendChild(E('option', { value: '99' }, _('Enter the dtb file name')));
|
||||
|
||||
// The three manual fields are only required when the user picks 99.
|
||||
const dtbInput = E('input', { type: 'text', class: 'cbi-input-text',
|
||||
style: 'width:235px', id: 'amlogic_dtb' });
|
||||
const socInput = E('input', { type: 'text', class: 'cbi-input-text',
|
||||
style: 'width:235px', id: 'amlogic_socname' });
|
||||
const ubootInput = E('input', { type: 'text', class: 'cbi-input-text',
|
||||
style: 'width:235px', id: 'amlogic_uboot' });
|
||||
|
||||
const trDtb = E('tr', { style: 'display:none' }, [
|
||||
E('td', { width: '30%', align: 'right' }, _('Enter the dtb file name:')),
|
||||
E('td', { width: '70%', align: 'left' }, dtbInput)
|
||||
]);
|
||||
const trSoc = E('tr', { style: 'display:none' }, [
|
||||
E('td', { width: '30%', align: 'right' }, _('Enter the soc name:')),
|
||||
E('td', { width: '70%', align: 'left' }, socInput)
|
||||
]);
|
||||
const trUboot = E('tr', { style: 'display:none' }, [
|
||||
E('td', { width: '30%', align: 'right' }, _('Enter the uboot_overload name:')),
|
||||
E('td', { width: '70%', align: 'left' }, ubootInput)
|
||||
]);
|
||||
|
||||
sel.addEventListener('change', function () {
|
||||
// On selection change, dynamically show/hide the three manual rows.
|
||||
const show = (sel.value === '99') ? '' : 'none';
|
||||
trDtb.style.display = show;
|
||||
trSoc.style.display = show;
|
||||
trUboot.style.display = show;
|
||||
});
|
||||
|
||||
// Status / log line shown next to the install button while it runs.
|
||||
// 'installing' tracks whether the background install is in progress.
|
||||
const logSpan = E('span');
|
||||
var installing = false;
|
||||
var installDone = false;
|
||||
const btn = E('input', {
|
||||
type: 'button', class: 'cbi-button cbi-button-reload',
|
||||
value: _('Install'),
|
||||
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?".
|
||||
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.
|
||||
const dtbVal = dtbInput.value || 'auto_dtb';
|
||||
const socVal = socInput.value || '';
|
||||
const ubVal = ubootInput.value || '';
|
||||
// Compose the "id@dtb:soc:uboot" string the backend expects.
|
||||
const composed = sel.value + '@' + dtbVal + ':' + socVal + ':' + ubVal;
|
||||
const target = ev.currentTarget;
|
||||
target.disabled = true;
|
||||
target.value = _('Installing...');
|
||||
installing = true;
|
||||
installDone = false;
|
||||
// Fire the background install — result comes via log polling.
|
||||
return callStartInst(composed).catch(function () {
|
||||
target.value = _('Install Failed');
|
||||
target.disabled = false;
|
||||
installing = false;
|
||||
});
|
||||
})
|
||||
});
|
||||
|
||||
// Poll the install log tail once per second to surface backend progress
|
||||
// and detect terminal success/failure keywords.
|
||||
poll.add(function () {
|
||||
return callLogTail('install').then(function (line) {
|
||||
if (!line || line === '\n') {
|
||||
dom.content(logSpan, '');
|
||||
return;
|
||||
}
|
||||
dom.content(logSpan, E('span', { class: 'amlogic-status-info' }, ' ' + line));
|
||||
if (installing && !installDone) {
|
||||
if (line.indexOf('Successfully') !== -1 || line.indexOf('successful') !== -1) {
|
||||
installDone = true;
|
||||
installing = false;
|
||||
btn.value = _('Successful Install');
|
||||
btn.disabled = false;
|
||||
dom.content(logSpan, E('span', { class: 'amlogic-status-ok' }, ' ' + line));
|
||||
} else if (line.indexOf('Failed') !== -1 || line.indexOf('failed') !== -1 ||
|
||||
line.indexOf('Error') !== -1 || line.indexOf('error') !== -1) {
|
||||
installDone = true;
|
||||
installing = false;
|
||||
btn.value = _('Install Failed');
|
||||
btn.disabled = false;
|
||||
dom.content(logSpan, E('span', { class: 'amlogic-status-err' }, ' ' + line));
|
||||
}
|
||||
}
|
||||
});
|
||||
}, 1);
|
||||
|
||||
return E('div', {}, [
|
||||
E('h2', _('Install OpenWrt')),
|
||||
E('p', _('Install OpenWrt to EMMC, Please select the device model, Or enter the dtb file name.')),
|
||||
E('div', { class: 'cbi-section' }, [
|
||||
E('table', { class: 'amlogic-row-table' }, [
|
||||
E('tr', [
|
||||
E('td', { width: '35%', align: 'right' }, _('Select the device model:')),
|
||||
E('td', { width: '65%', align: 'left' }, sel)
|
||||
]),
|
||||
trDtb, trSoc, trUboot,
|
||||
E('tr', [
|
||||
E('td', { width: '30%', align: 'right' }, _('Install OpenWrt:')),
|
||||
E('td', { width: '70%', align: 'left' }, [btn, ' ', logSpan])
|
||||
])
|
||||
])
|
||||
])
|
||||
]);
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,96 @@
|
||||
// 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.
|
||||
|
||||
'use strict';
|
||||
'require view';
|
||||
'require rpc';
|
||||
'require ui';
|
||||
'require poll';
|
||||
'require view.amlogic.shared as amlogicShared';
|
||||
|
||||
// Read the full log content (entire text).
|
||||
const callLogFull = rpc.declare({ object: 'luci.amlogic', method: 'read_log_full',
|
||||
params: ['name'], expect: { content: '' } });
|
||||
// Clear the service main log.
|
||||
const callDelLog = rpc.declare({ object: 'luci.amlogic', method: 'del_log' });
|
||||
|
||||
return view.extend({
|
||||
handleSave: null,
|
||||
handleSaveApply: null,
|
||||
handleReset: null,
|
||||
|
||||
render: function () {
|
||||
amlogicShared.ensureCss();
|
||||
// Read-only, no-wrap, monospace log display area.
|
||||
const ta = E('textarea', {
|
||||
rows: '20', readonly: 'readonly', wrap: 'off',
|
||||
style: 'width:100%; font-family:monospace; white-space:pre'
|
||||
});
|
||||
// Status hint: Running / Stopped.
|
||||
const status = E('span', { class: 'amlogic-status-ok', style: 'margin-left:1em' });
|
||||
// Polling is enabled by default when the page opens.
|
||||
let polling = true;
|
||||
|
||||
// Refresh the full log and scroll to the bottom.
|
||||
function refresh() {
|
||||
return callLogFull('main').then(function (txt) {
|
||||
ta.value = txt || '';
|
||||
ta.scrollTop = ta.scrollHeight;
|
||||
});
|
||||
}
|
||||
const view = this;
|
||||
|
||||
const stopBtn = E('input', {
|
||||
type: 'button', class: 'cbi-button cbi-button-reset',
|
||||
value: _('Stop Refresh'),
|
||||
click: function () { polling = false; status.textContent = _('Stopped'); }
|
||||
});
|
||||
const startBtn = E('input', {
|
||||
type: 'button', class: 'cbi-button cbi-button-apply',
|
||||
value: _('Start Refresh'),
|
||||
click: function () { polling = true; status.textContent = _('Running'); refresh(); }
|
||||
});
|
||||
const cleanBtn = E('input', {
|
||||
type: 'button', class: 'cbi-button cbi-button-remove',
|
||||
value: _('Clean Log'),
|
||||
click: ui.createHandlerFn(view, function () {
|
||||
return callDelLog().then(refresh);
|
||||
})
|
||||
});
|
||||
const dlBtn = E('input', {
|
||||
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 <a>; avoids a second server fetch.
|
||||
const blob = new Blob([ta.value || ''], { type: 'text/plain' });
|
||||
const url = URL.createObjectURL(blob);
|
||||
const a = document.createElement('a');
|
||||
a.href = url;
|
||||
a.download = 'amlogic_service.log';
|
||||
document.body.appendChild(a);
|
||||
a.click();
|
||||
document.body.removeChild(a);
|
||||
URL.revokeObjectURL(url);
|
||||
}
|
||||
});
|
||||
|
||||
// Polling: only refresh while polling is true; runs once every 2s.
|
||||
poll.add(function () { if (polling) return refresh(); }, 2);
|
||||
refresh();
|
||||
status.textContent = _('Running');
|
||||
|
||||
return E('div', {}, [
|
||||
E('h2', _('Service Log')),
|
||||
E('p', _('Display the log of plug-in service operation.')),
|
||||
E('div', { class: 'cbi-section' }, [ta]),
|
||||
E('div', { class: 'cbi-page-actions' },
|
||||
[stopBtn, ' ', startBtn, ' ', cleanBtn, ' ', dlBtn, status])
|
||||
]);
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,66 @@
|
||||
// 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.
|
||||
|
||||
'use strict';
|
||||
'require view';
|
||||
'require rpc';
|
||||
'require ui';
|
||||
'require view.amlogic.shared as amlogicShared';
|
||||
|
||||
// Trigger the backend poweroff RPC.
|
||||
const callPoweroff = rpc.declare({ object: 'luci.amlogic', method: 'poweroff' });
|
||||
|
||||
return view.extend({
|
||||
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.
|
||||
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', {
|
||||
src: L.resource('amlogic/poweroff.png'),
|
||||
width: '32', height: '32',
|
||||
style: 'width:32px; height:32px; max-width:32px; display:none; margin-left:1em; vertical-align:middle'
|
||||
});
|
||||
|
||||
const btn = E('input', {
|
||||
type: 'button', class: 'cbi-button cbi-button-remove',
|
||||
value: _('Perform PowerOff'),
|
||||
click: ui.createHandlerFn(this, function (ev) {
|
||||
// Confirm twice to avoid accidental clicks.
|
||||
if (!confirm(_('Are you sure you want to power off the device?')))
|
||||
return;
|
||||
ev.currentTarget.disabled = true;
|
||||
return callPoweroff().then(function () {
|
||||
// Backend has fired poweroff; show a 5-second countdown to the user.
|
||||
status.textContent = _('Powering off, please wait...');
|
||||
let n = 5;
|
||||
const t = setInterval(function () {
|
||||
if (--n <= 0) {
|
||||
clearInterval(t);
|
||||
status.textContent = _('Powered off.');
|
||||
icon.style.display = 'inline-block';
|
||||
} else {
|
||||
status.textContent = _('Powering off, please wait...') + ' ' + n;
|
||||
}
|
||||
}, 1000);
|
||||
});
|
||||
})
|
||||
});
|
||||
|
||||
return E('div', {}, [
|
||||
E('h2', _('Power Off')),
|
||||
E('p', _('Click the button below to safely power off the device.')),
|
||||
E('div', { class: 'cbi-section' }, [btn, status, icon])
|
||||
]);
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,27 @@
|
||||
// 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 <head> (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.
|
||||
|
||||
'use strict';
|
||||
'require baseclass';
|
||||
|
||||
return baseclass.extend({
|
||||
// Inject the shared stylesheet (idempotent).
|
||||
// We tag the <link> 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.
|
||||
ensureCss: function () {
|
||||
if (document.getElementById('amlogic-shared-css')) return;
|
||||
const link = document.createElement('link');
|
||||
link.id = 'amlogic-shared-css';
|
||||
link.rel = 'stylesheet';
|
||||
link.type = 'text/css';
|
||||
link.href = L.resource('view/amlogic/amlogic.css');
|
||||
document.head.appendChild(link);
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,315 @@
|
||||
// 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.
|
||||
|
||||
'use strict';
|
||||
'require view';
|
||||
'require rpc';
|
||||
'require ui';
|
||||
'require dom';
|
||||
'require poll';
|
||||
'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/' } });
|
||||
// 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'] });
|
||||
// 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'] });
|
||||
// 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'] });
|
||||
// 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: '' } });
|
||||
// Read system state (this page only uses current_firmware_version).
|
||||
const callState = rpc.declare({ object: 'luci.amlogic', method: 'state' });
|
||||
|
||||
return view.extend({
|
||||
handleSave: null,
|
||||
handleSaveApply: null,
|
||||
handleReset: null,
|
||||
|
||||
load: function () {
|
||||
amlogicShared.ensureCss();
|
||||
return callUploadPath();
|
||||
},
|
||||
|
||||
render: function (path) {
|
||||
const view = this;
|
||||
|
||||
const tableContainer = E('div');
|
||||
const descSpan = E('span', { class: 'amlogic-status-ok', style: 'font-weight:bold' });
|
||||
const fwBtn = E('input', {
|
||||
type: 'button', class: 'cbi-button cbi-button-reload',
|
||||
value: _('Update OpenWrt firmware'), style: 'display:none',
|
||||
click: ui.createHandlerFn(this, function (ev) {
|
||||
return doButton(ev.currentTarget,
|
||||
_('Updating...'), _('Successful Update'), _('Update Failed'),
|
||||
function () { return callStartUpd('auto@updated@/tmp'); });
|
||||
})
|
||||
});
|
||||
const knBtn = E('input', {
|
||||
type: 'button', class: 'cbi-button cbi-button-reload',
|
||||
value: _('Replace OpenWrt Kernel'), style: 'display:none; margin-left:5px',
|
||||
click: ui.createHandlerFn(this, function (ev) {
|
||||
return doButton(ev.currentTarget,
|
||||
_('Updating...'), _('Successful Update'), _('Update Failed'),
|
||||
function () { return callStartKnl(); });
|
||||
})
|
||||
});
|
||||
const fwLog = E('span');
|
||||
const knLog = E('span');
|
||||
const fwVer = E('span', _('Collecting data...'));
|
||||
|
||||
function doButton(btn, busyText, ok, fail, fn) {
|
||||
// Shared button state machine: disabled -> RPC -> success/fail label.
|
||||
btn.disabled = true; btn.value = busyText;
|
||||
return Promise.resolve(fn()).then(function (r) {
|
||||
btn.value = (r && r.code == 0) ? ok : fail;
|
||||
}).catch(function () { btn.value = fail; })
|
||||
.finally(function () { btn.disabled = false; });
|
||||
}
|
||||
|
||||
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.
|
||||
return callList().then(function (info) {
|
||||
dom.content(tableContainer, buildTable(info.items || [], info.path));
|
||||
let parts = [];
|
||||
if (info.has_config)
|
||||
parts.push(_('There are config file in the upload directory, and you can restore the config. '));
|
||||
if (info.has_kernel)
|
||||
parts.push(_('There are kernel files in the upload directory, and you can replace the kernel.'));
|
||||
if (info.has_firmware)
|
||||
parts.push(_('There are openwrt firmware file in the upload directory, and you can update the openwrt.'));
|
||||
descSpan.textContent = parts.length ? ' Tip: ' + parts.join('') : '';
|
||||
fwBtn.style.display = info.has_firmware ? '' : 'none';
|
||||
knBtn.style.display = info.has_kernel ? '' : 'none';
|
||||
});
|
||||
}
|
||||
|
||||
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.
|
||||
const tbl = E('table', { class: 'table cbi-section-table' }, [
|
||||
E('tr', { class: 'tr cbi-section-table-titles' }, [
|
||||
E('th', { class: 'th' }, _('File name')),
|
||||
E('th', { class: 'th' }, _('Modify time')),
|
||||
E('th', { class: 'th' }, _('Attributes')),
|
||||
E('th', { class: 'th' }, _('Size')),
|
||||
E('th', { class: 'th' }, _('Remove')),
|
||||
E('th', { class: 'th' }, _('Install'))
|
||||
])
|
||||
]);
|
||||
items.forEach(function (it) {
|
||||
const removeBtn = E('input', {
|
||||
type: 'button', class: 'cbi-button cbi-button-remove',
|
||||
value: _('Remove'),
|
||||
click: ui.createHandlerFn(view, function (ev) {
|
||||
return callDelete(it.name).then(refreshList);
|
||||
})
|
||||
});
|
||||
let installCell;
|
||||
if (it.ipk) {
|
||||
// ipk: "Install" calls install_upload to install one package.
|
||||
installCell = E('input', {
|
||||
type: 'button', class: 'cbi-button cbi-button-apply',
|
||||
value: _('Install'),
|
||||
click: ui.createHandlerFn(view, function (ev) {
|
||||
ev.currentTarget.disabled = true;
|
||||
return callInstall(it.name).then(function (r) {
|
||||
if (r && r.code !== 0 && r.output)
|
||||
ui.addNotification(null,
|
||||
E('p', { class: 'amlogic-status-err' }, [
|
||||
r.output, E('br'),
|
||||
E('b', _('Please refresh the page to see the changes.'))
|
||||
]));
|
||||
else if (r && r.code === 0)
|
||||
ui.addNotification(null,
|
||||
E('p', { class: 'amlogic-status-ok' },
|
||||
_('Package installed successfully. Please refresh the page to see the changes.')));
|
||||
return refreshList();
|
||||
});
|
||||
})
|
||||
});
|
||||
} else if (it.cfg) {
|
||||
// Config backup (openwrt-config.tar.gz): "Restore" makes the
|
||||
// backend roll the config back and reboot.
|
||||
installCell = E('input', {
|
||||
type: 'button', class: 'cbi-button cbi-button-apply',
|
||||
value: _('Restore'),
|
||||
click: ui.createHandlerFn(view, function (ev) {
|
||||
ev.currentTarget.disabled = true;
|
||||
ui.addNotification(null,
|
||||
E('p', { class: 'amlogic-status-ok' },
|
||||
_('Tip: The config is being restored, and it will automatically restart after completion.')));
|
||||
return callInstall(it.name);
|
||||
})
|
||||
});
|
||||
} else {
|
||||
installCell = E('span', '-');
|
||||
}
|
||||
tbl.appendChild(E('tr', { class: 'tr cbi-rowstyle-1' }, [
|
||||
E('td', { class: 'td' }, it.name),
|
||||
E('td', { class: 'td' }, it.mtime),
|
||||
E('td', { class: 'td' }, it.modestr),
|
||||
E('td', { class: 'td' }, it.size),
|
||||
E('td', { class: 'td' }, removeBtn),
|
||||
E('td', { class: 'td' }, installCell)
|
||||
]));
|
||||
});
|
||||
if (!items.length)
|
||||
tbl.appendChild(E('tr', [E('td', { class: 'td', colspan: '6' }, _('No specify upload file.'))]));
|
||||
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.
|
||||
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.
|
||||
function bytesToB64(buf) {
|
||||
let s = '';
|
||||
for (let i = 0; i < buf.length; i += 0x8000)
|
||||
s += String.fromCharCode.apply(null, buf.subarray(i, i + 0x8000));
|
||||
return btoa(s);
|
||||
}
|
||||
|
||||
function uploadFile(file, target, basename) {
|
||||
const total = file.size;
|
||||
let offset = 0;
|
||||
let first = true;
|
||||
function step() {
|
||||
if (offset >= total) {
|
||||
uploadStatus.textContent = _('File saved to') + ' ' + target;
|
||||
return refreshList();
|
||||
}
|
||||
const slice = file.slice(offset, offset + CHUNK_SIZE);
|
||||
return slice.arrayBuffer().then(function (buf) {
|
||||
const b64 = bytesToB64(new Uint8Array(buf));
|
||||
const append = !first;
|
||||
first = false;
|
||||
return callUploadChunk(basename, b64, append).then(function (r) {
|
||||
if (!r || !r.ok)
|
||||
throw new Error((r && r.error) || _('upload chunk failed'));
|
||||
offset += CHUNK_SIZE;
|
||||
const pct = Math.min(100, (offset / total) * 100);
|
||||
uploadStatus.textContent = _('Uploading file…') +
|
||||
' ' + pct.toFixed(1) + '%';
|
||||
return step();
|
||||
});
|
||||
});
|
||||
}
|
||||
return step();
|
||||
}
|
||||
|
||||
const hiddenFileInput = E('input', {
|
||||
type: 'file', style: 'display:none',
|
||||
change: ui.createHandlerFn(this, function (ev) {
|
||||
const file = ev.target.files && ev.target.files[0];
|
||||
if (!file) return;
|
||||
const basename = file.name.replace(/^.*[\\\/]/, '');
|
||||
const target = path.replace(/\/+$/, '') + '/' + basename;
|
||||
uploadFileName.textContent = basename;
|
||||
uploadStatus.textContent = _('Uploading file…') + ' 0%';
|
||||
return uploadFile(file, target, basename).catch(function (e) {
|
||||
uploadStatus.textContent = _('Create upload file error.') +
|
||||
' ' + (e && e.message ? e.message : e);
|
||||
}).finally(function () {
|
||||
hiddenFileInput.value = '';
|
||||
});
|
||||
})
|
||||
});
|
||||
const uploadBtn = E('input', {
|
||||
type: 'button', class: 'cbi-button cbi-button-apply',
|
||||
value: _('Upload file...'),
|
||||
click: function () { hiddenFileInput.click(); }
|
||||
});
|
||||
|
||||
// Polling: log tails + version
|
||||
// Pull the last line of both firmware and kernel logs once per second.
|
||||
poll.add(function () {
|
||||
return Promise.all([
|
||||
callLogTail('firmware').then(function (l) {
|
||||
dom.content(fwLog, l && l !== '\n'
|
||||
? E('span', { class: 'amlogic-status-info' }, ' ' + l) : '');
|
||||
}),
|
||||
callLogTail('kernel').then(function (l) {
|
||||
dom.content(knLog, l && l !== '\n'
|
||||
? E('span', { class: 'amlogic-status-info' }, ' ' + l) : '');
|
||||
})
|
||||
]);
|
||||
}, 1);
|
||||
callState().then(function (s) {
|
||||
dom.content(fwVer, s.current_firmware_version
|
||||
? E('span', { class: 'amlogic-status-ok' }, _('Current Version') + ' [ ' + s.current_firmware_version + ' ] ')
|
||||
: E('span', { class: 'amlogic-status-err' }, _('Invalid value.')));
|
||||
});
|
||||
refreshList();
|
||||
|
||||
return E('div', {}, [
|
||||
E('h2', _('Upload')),
|
||||
E('p', _('Update plugins first, then update the kernel or firmware.')),
|
||||
E('p', _('After uploading [Firmware], [Kernel], [IPK] or [Backup Config], the operation buttons will be displayed.')),
|
||||
E('div', { class: 'cbi-section' }, [
|
||||
E('label', { style: 'display:inline-block;width:150px' },
|
||||
_('Choose local file:')),
|
||||
uploadBtn, ' ', uploadFileName, hiddenFileInput,
|
||||
E('br'), uploadStatus
|
||||
]),
|
||||
E('h3', _('Upload file list')),
|
||||
E('div', { class: 'cbi-section' }, [descSpan, tableContainer]),
|
||||
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.
|
||||
E('div', { style: 'text-align:center' },
|
||||
[fwBtn, knBtn, ' ', fwVer, ' ', fwLog, knLog])
|
||||
])
|
||||
]);
|
||||
}
|
||||
});
|
||||
@@ -1,568 +0,0 @@
|
||||
module("luci.controller.amlogic", package.seeall)
|
||||
|
||||
local sys = require "luci.sys"
|
||||
local PKG_NAME = "luci-app-amlogic"
|
||||
|
||||
function index()
|
||||
if not nixio.fs.access("/etc/config/amlogic") then
|
||||
return
|
||||
end
|
||||
|
||||
local page = entry({ "admin", "system", "amlogic" }, alias("admin", "system", "amlogic", "info"), _("Amlogic Service"), 88)
|
||||
page.dependent = true
|
||||
page.acl_depends = { PKG_NAME }
|
||||
|
||||
local platfrom = luci.sys.exec("cat /etc/flippy-openwrt-release 2>/dev/null | grep PLATFORM | awk -F'=' '{print $2}' | grep -oE '(amlogic|rockchip|allwinner|qemu)' | xargs") or "Unknown"
|
||||
local install_menu = luci.sys.exec("cat /etc/flippy-openwrt-release 2>/dev/null | grep SHOW_INSTALL_MENU | awk -F'=' '{print $2}' | grep -oE '(yes|no)' | xargs") or "Unknown"
|
||||
|
||||
entry({ "admin", "system", "amlogic", "info" }, form("amlogic/amlogic_info"), _("Amlogic Service"), 1).leaf = true
|
||||
if (string.find(platfrom, "amlogic")) ~= nil or (string.find(install_menu, "yes")) ~= nil then
|
||||
entry({ "admin", "system", "amlogic", "install" }, form("amlogic/amlogic_install"), _("Install OpenWrt"), 2).leaf = true
|
||||
end
|
||||
entry({ "admin", "system", "amlogic", "upload" }, form("amlogic/amlogic_upload"), _("Manually Upload Update"), 3).leaf = true
|
||||
entry({ "admin", "system", "amlogic", "check" }, form("amlogic/amlogic_check"), _("Online Download Update"), 4).leaf = true
|
||||
entry({ "admin", "system", "amlogic", "backup" }, form("amlogic/amlogic_backup"), _("Backup Firmware Config"), 5).leaf = true
|
||||
entry({ "admin", "system", "amlogic", "backuplist" }, form("amlogic/amlogic_backuplist")).leaf = true
|
||||
if (string.find(platfrom, "qemu")) == nil then
|
||||
entry({ "admin", "system", "amlogic", "armcpu" }, cbi("amlogic/amlogic_armcpu"), _("CPU Settings"), 6).leaf = true
|
||||
end
|
||||
entry({ "admin", "system", "amlogic", "config" }, cbi("amlogic/amlogic_config"), _("Plugin Settings"), 7).leaf = true
|
||||
entry({ "admin", "system", "amlogic", "log" }, form("amlogic/amlogic_log"), _("Server Logs"), 8).leaf = true
|
||||
entry({ "admin", "system", "amlogic", "poweroff" }, form("amlogic/amlogic_poweroff"), _("PowerOff"), 9).leaf = true
|
||||
entry({ "admin", "system", "amlogic", "check_firmware" }, call("action_check_firmware"))
|
||||
entry({ "admin", "system", "amlogic", "check_plugin" }, call("action_check_plugin"))
|
||||
entry({ "admin", "system", "amlogic", "check_plugin_download" }, call("action_check_plugin_download")).leaf = true
|
||||
entry({ "admin", "system", "amlogic", "check_kernel" }, call("action_check_kernel"))
|
||||
entry({ "admin", "system", "amlogic", "refresh_log" }, call("action_refresh_log"))
|
||||
entry({ "admin", "system", "amlogic", "del_log" }, call("action_del_log"))
|
||||
entry({ "admin", "system", "amlogic", "start_model_database" }, call("action_check_model_database")).leaf = true
|
||||
entry({ "admin", "system", "amlogic", "start_check_install" }, call("action_start_check_install")).leaf = true
|
||||
entry({ "admin", "system", "amlogic", "start_check_firmware" }, call("action_start_check_firmware")).leaf = true
|
||||
entry({ "admin", "system", "amlogic", "start_check_plugin" }, call("action_start_check_plugin")).leaf = true
|
||||
entry({ "admin", "system", "amlogic", "start_check_kernel" }, call("action_start_check_kernel")).leaf = true
|
||||
entry({ "admin", "system", "amlogic", "start_check_rescue" }, call("action_start_check_rescue")).leaf = true
|
||||
entry({ "admin", "system", "amlogic", "start_check_upfiles" }, call("action_start_check_upfiles")).leaf = true
|
||||
entry({ "admin", "system", "amlogic", "start_amlogic_install" }, call("action_start_amlogic_install")).leaf = true
|
||||
entry({ "admin", "system", "amlogic", "start_amlogic_update" }, call("action_start_amlogic_update")).leaf = true
|
||||
entry({ "admin", "system", "amlogic", "start_amlogic_kernel" }, call("action_start_amlogic_kernel")).leaf = true
|
||||
entry({ "admin", "system", "amlogic", "start_amlogic_plugin" }, call("action_start_amlogic_plugin")).leaf = true
|
||||
entry({ "admin", "system", "amlogic", "start_amlogic_rescue" }, call("action_start_amlogic_rescue")).leaf = true
|
||||
entry({ "admin", "system", "amlogic", "start_snapshot_delete" }, call("action_start_snapshot_delete")).leaf = true
|
||||
entry({ "admin", "system", "amlogic", "start_snapshot_restore" }, call("action_start_snapshot_restore")).leaf = true
|
||||
entry({ "admin", "system", "amlogic", "start_snapshot_list" }, call("action_check_snapshot")).leaf = true
|
||||
entry({ "admin", "system", "amlogic", "start_openwrt_author" }, call("action_openwrt_author")).leaf = true
|
||||
entry({ "admin", "system", "amlogic", "state" }, call("action_state")).leaf = true
|
||||
entry({ "admin", "system", "amlogic", "start_poweroff" }, call("action_poweroff")).leaf = true
|
||||
entry({ "admin", "system", "amlogic", "start_switch" }, call("action_switch")).leaf = true
|
||||
end
|
||||
|
||||
--Remove the spaces in the string
|
||||
function trim(str)
|
||||
--return (string.gsub(str, "^%s*(.-)%s*$", "%1"))
|
||||
return (string.gsub(str, "%s+", ""))
|
||||
end
|
||||
|
||||
--Create a temporary folder
|
||||
local tmp_upload_dir = luci.sys.exec("[ -d /tmp/upload ] || mkdir -p /tmp/upload >/dev/null")
|
||||
local tmp_amlogic_dir = luci.sys.exec("[ -d /tmp/amlogic ] || mkdir -p /tmp/amlogic >/dev/null")
|
||||
|
||||
--Whether to automatically restore the firmware config
|
||||
local amlogic_firmware_config = luci.sys.exec("uci get amlogic.config.amlogic_firmware_config 2>/dev/null") or "1"
|
||||
if tonumber(amlogic_firmware_config) == 0 then
|
||||
update_restore_config = "no-restore"
|
||||
else
|
||||
update_restore_config = "restore"
|
||||
end
|
||||
|
||||
--Whether to automatically write to the bootLoader
|
||||
local amlogic_write_bootloader = luci.sys.exec("uci get amlogic.config.amlogic_write_bootloader 2>/dev/null") or "1"
|
||||
if tonumber(amlogic_write_bootloader) == 0 then
|
||||
auto_write_bootloader = "no"
|
||||
else
|
||||
auto_write_bootloader = "yes"
|
||||
end
|
||||
|
||||
--Set the file system type of the shared partition
|
||||
local amlogic_shared_fstype = luci.sys.exec("uci get amlogic.config.amlogic_shared_fstype 2>/dev/null") or ""
|
||||
if trim(amlogic_shared_fstype) == "" then
|
||||
auto_shared_fstype = "ext4"
|
||||
else
|
||||
auto_shared_fstype = trim(amlogic_shared_fstype)
|
||||
end
|
||||
|
||||
--Device identification
|
||||
device_platfrom = luci.sys.exec("cat /etc/flippy-openwrt-release 2>/dev/null | grep PLATFORM | awk -F'=' '{print $2}' | grep -oE '(amlogic|rockchip|allwinner|qemu)' | xargs") or "Unknown"
|
||||
if (string.find(device_platfrom, "rockchip")) ~= nil then
|
||||
device_install_script = ""
|
||||
device_update_script = "openwrt-update-rockchip"
|
||||
device_kernel_script = "openwrt-kernel"
|
||||
elseif (string.find(device_platfrom, "allwinner")) ~= nil then
|
||||
device_install_script = "openwrt-install-allwinner"
|
||||
device_update_script = "openwrt-update-allwinner"
|
||||
device_kernel_script = "openwrt-kernel"
|
||||
elseif (string.find(device_platfrom, "qemu")) ~= nil then
|
||||
device_install_script = ""
|
||||
device_update_script = "openwrt-update-kvm"
|
||||
device_kernel_script = "openwrt-kernel"
|
||||
else
|
||||
device_install_script = "openwrt-install-amlogic"
|
||||
device_update_script = "openwrt-update-amlogic"
|
||||
device_kernel_script = "openwrt-kernel"
|
||||
end
|
||||
|
||||
--General array functions
|
||||
function string.split(input, delimiter)
|
||||
input = tostring(input)
|
||||
delimiter = tostring(delimiter)
|
||||
if (delimiter == '') then return false end
|
||||
local pos, arr = 0, {}
|
||||
-- for each divider found
|
||||
for st, sp in function() return string.find(input, delimiter, pos, true) end do
|
||||
table.insert(arr, string.sub(input, pos, st - 1))
|
||||
pos = sp + 1
|
||||
end
|
||||
table.insert(arr, string.sub(input, pos))
|
||||
return arr
|
||||
end
|
||||
|
||||
--Refresh the log
|
||||
function action_refresh_log()
|
||||
local logfile = "/tmp/amlogic/amlogic.log"
|
||||
if not nixio.fs.access(logfile) then
|
||||
luci.sys.exec("uname -a > /tmp/amlogic/amlogic.log && sync")
|
||||
luci.sys.exec("echo '' > /tmp/amlogic/amlogic_check_install.log && sync >/dev/null 2>&1")
|
||||
luci.sys.exec("echo '' > /tmp/amlogic/amlogic_check_upfiles.log && sync >/dev/null 2>&1")
|
||||
luci.sys.exec("echo '' > /tmp/amlogic/amlogic_check_plugin.log && sync >/dev/null 2>&1")
|
||||
luci.sys.exec("echo '' > /tmp/amlogic/amlogic_check_kernel.log && sync >/dev/null 2>&1")
|
||||
luci.sys.exec("echo '' > /tmp/amlogic/amlogic_check_firmware.log && sync >/dev/null 2>&1")
|
||||
luci.sys.exec("echo '' > /tmp/amlogic/amlogic_check_rescue.log && sync >/dev/null 2>&1")
|
||||
luci.sys.exec("echo '' > /tmp/amlogic/amlogic_running_script.log && sync >/dev/null 2>&1")
|
||||
end
|
||||
luci.http.prepare_content("text/plain; charset=utf-8")
|
||||
local f = io.open(logfile, "r+")
|
||||
f:seek("set")
|
||||
local a = f:read(2048000) or ""
|
||||
f:close()
|
||||
luci.http.write(a)
|
||||
end
|
||||
|
||||
--Delete the logs
|
||||
function action_del_log()
|
||||
luci.sys.exec(": > /tmp/amlogic/amlogic.log")
|
||||
luci.sys.exec(": > /tmp/amlogic/amlogic_check_install.log")
|
||||
luci.sys.exec(": > /tmp/amlogic/amlogic_check_upfiles.log")
|
||||
luci.sys.exec(": > /tmp/amlogic/amlogic_check_plugin.log")
|
||||
luci.sys.exec(": > /tmp/amlogic/amlogic_check_kernel.log")
|
||||
luci.sys.exec(": > /tmp/amlogic/amlogic_check_firmware.log")
|
||||
luci.sys.exec(": > /tmp/amlogic/amlogic_check_rescue.log")
|
||||
luci.sys.exec(": > /tmp/amlogic/amlogic_running_script.log")
|
||||
end
|
||||
|
||||
|
||||
--Upgrade luci-app-amlogic plugin
|
||||
function start_amlogic_plugin()
|
||||
local log_file = "/tmp/amlogic/amlogic_check_plugin.log"
|
||||
local running_lock = "/tmp/amlogic/amlogic_running_script.log"
|
||||
local config_file = "/etc/config/amlogic"
|
||||
local config_bak = "/etc/config/amlogic_bak"
|
||||
|
||||
-- 1. Create a running lock and clear previous log
|
||||
luci.sys.call("echo '1@Plugin update in progress, try again later!' > " .. running_lock .. " && > " .. log_file)
|
||||
|
||||
-- 2. Backup config file
|
||||
luci.sys.call(string.format("[ -f %s ] && cp -vf %s %s >> %s 2>&1", config_file, config_file, config_bak, log_file))
|
||||
|
||||
-- 3. Detect and install
|
||||
local install_status = 1
|
||||
if luci.sys.call("command -v opkg >/dev/null") == 0 then
|
||||
luci.sys.call("echo 'System uses opkg. Attempting to install .ipk package...' >> " .. log_file)
|
||||
local install_cmd = string.format("opkg --force-reinstall install /tmp/amlogic/*.ipk >> %s 2>&1", log_file)
|
||||
install_status = luci.sys.call(install_cmd)
|
||||
elseif luci.sys.call("command -v apk >/dev/null") == 0 then
|
||||
luci.sys.call("echo 'System uses apk. Attempting to install .apk package...' >> " .. log_file)
|
||||
local install_cmd = string.format("apk add --force-overwrite --allow-untrusted /tmp/amlogic/*.apk >> %s 2>&1", log_file)
|
||||
install_status = luci.sys.call(install_cmd)
|
||||
else
|
||||
luci.sys.call("echo 'Error: Neither opkg nor apk found. Aborting.' >> " .. log_file)
|
||||
end
|
||||
|
||||
-- 4. Check result and finalize
|
||||
if install_status == 0 then
|
||||
-- SUCCESS
|
||||
luci.sys.call("echo 'Installation successful. Finalizing...' >> " .. log_file)
|
||||
luci.sys.call(string.format("[ -f %s ] && cp -vf %s %s >> %s 2>&1", config_bak, config_bak, config_file, log_file))
|
||||
luci.sys.call("rm -rf /tmp/luci-indexcache /tmp/luci-modulecache/* " .. config_bak)
|
||||
luci.sys.call("echo '' > " .. running_lock)
|
||||
luci.sys.call("echo 'Successful Update' > " .. log_file)
|
||||
return 0
|
||||
else
|
||||
-- FAILURE
|
||||
luci.sys.call("echo '--- INSTALLATION FAILED! ---' >> " .. log_file)
|
||||
luci.sys.call("echo 'Restoring configuration and aborting.' >> " .. log_file)
|
||||
luci.sys.call(string.format("[ -f %s ] && cp -vf %s %s >> %s 2>&1", config_bak, config_bak, config_file, log_file))
|
||||
luci.sys.call("rm -f " .. config_bak)
|
||||
luci.sys.call("echo '' > " .. running_lock)
|
||||
return install_status
|
||||
end
|
||||
end
|
||||
|
||||
--Upgrade the kernel
|
||||
function start_amlogic_kernel()
|
||||
luci.sys.call("echo '2@Kernel update in progress, try again later!' > /tmp/amlogic/amlogic_running_script.log && sync >/dev/null 2>&1")
|
||||
luci.sys.call("chmod +x /usr/sbin/" .. device_kernel_script .. " >/dev/null 2>&1")
|
||||
local state = luci.sys.call("/usr/sbin/" .. device_kernel_script .. " " .. auto_write_bootloader .. " > /tmp/amlogic/amlogic_check_kernel.log && sync >/dev/null 2>&1")
|
||||
return state
|
||||
end
|
||||
|
||||
--Upgrade amlogic openwrt firmware
|
||||
function start_amlogic_update()
|
||||
luci.sys.call("echo '3@OpenWrt update in progress, try again later!' > /tmp/amlogic/amlogic_running_script.log && sync >/dev/null 2>&1")
|
||||
luci.sys.call("chmod +x /usr/sbin/" .. device_update_script .. " >/dev/null 2>&1")
|
||||
local amlogic_update_sel = luci.http.formvalue("amlogic_update_sel")
|
||||
local res = string.split(amlogic_update_sel, "@")
|
||||
local update_firmware_name = res[1] or "auto"
|
||||
local update_firmware_updated = res[2] or "updated"
|
||||
local update_write_path = res[3] or "/tmp"
|
||||
luci.sys.call("echo " .. update_firmware_updated .. " > " .. update_write_path .. "/.luci-app-amlogic/op_release_code 2>/dev/null && sync")
|
||||
local state = luci.sys.call("/usr/sbin/" .. device_update_script .. " " .. update_firmware_name .. " " .. auto_write_bootloader .. " " .. update_restore_config .. " > /tmp/amlogic/amlogic_check_firmware.log && sync 2>/dev/null")
|
||||
return state
|
||||
end
|
||||
|
||||
--Read rescue kernel log
|
||||
local function start_amlogic_rescue()
|
||||
luci.sys.call("echo '4@Kernel rescue in progress, try again later!' > /tmp/amlogic/amlogic_running_script.log && sync >/dev/null 2>&1")
|
||||
luci.sys.call("chmod +x /usr/sbin/" .. device_kernel_script .. " >/dev/null 2>&1")
|
||||
local state = luci.sys.call("/usr/sbin/" .. device_kernel_script .. " -s > /tmp/amlogic/amlogic_check_rescue.log && sync >/dev/null 2>&1")
|
||||
luci.sys.call("echo '' > /tmp/amlogic/amlogic_running_script.log && sync >/dev/null 2>&1")
|
||||
return state
|
||||
end
|
||||
|
||||
--Install amlogic openwrt firmware
|
||||
function start_amlogic_install()
|
||||
luci.sys.exec("chmod +x /usr/sbin/" .. device_install_script .. " >/dev/null 2>&1")
|
||||
local amlogic_install_sel = luci.http.formvalue("amlogic_install_sel")
|
||||
local res = string.split(amlogic_install_sel, "@")
|
||||
soc_id = res[1] or "99"
|
||||
if tonumber(res[1]) == 99 then
|
||||
dtb_filename = res[2] or "auto_dtb"
|
||||
else
|
||||
dtb_filename = "auto_dtb"
|
||||
end
|
||||
local state = luci.sys.call("/usr/sbin/" .. device_install_script .. " " .. auto_write_bootloader .. " " .. soc_id .. " " .. dtb_filename .. " " .. auto_shared_fstype .. " > /tmp/amlogic/amlogic_check_install.log && sync 2>/dev/null")
|
||||
return state
|
||||
end
|
||||
|
||||
--Delets the snapshot
|
||||
function start_snapshot_delete()
|
||||
local snapshot_delete_sel = luci.http.formvalue("snapshot_delete_sel")
|
||||
local state = luci.sys.exec("btrfs subvolume delete -c /.snapshots/" .. snapshot_delete_sel .. " 2>/dev/null && sync")
|
||||
return state
|
||||
end
|
||||
|
||||
--Restore to this snapshot
|
||||
function start_snapshot_restore()
|
||||
local snapshot_restore_sel = luci.http.formvalue("snapshot_restore_sel")
|
||||
local state = luci.sys.exec("btrfs subvolume snapshot /.snapshots/etc-" .. snapshot_restore_sel .. " /etc 2>/dev/null && sync")
|
||||
local state_nowreboot = luci.sys.exec("echo 'b' > /proc/sysrq-trigger 2>/dev/null")
|
||||
return state
|
||||
end
|
||||
|
||||
--Check the plugin (phase 1: query latest version, output Download button to log)
|
||||
function action_check_plugin()
|
||||
luci.sys.exec("chmod +x /usr/share/amlogic/amlogic_check_plugin.sh >/dev/null 2>&1")
|
||||
return luci.sys.call("/usr/share/amlogic/amlogic_check_plugin.sh -c >/dev/null 2>&1")
|
||||
end
|
||||
|
||||
--Check the plugin (phase 2: download the given version, output Update button to log)
|
||||
function action_check_plugin_download()
|
||||
luci.sys.exec("chmod +x /usr/share/amlogic/amlogic_check_plugin.sh >/dev/null 2>&1")
|
||||
local plugin_options = luci.http.formvalue("plugin_options") or ""
|
||||
luci.http.prepare_content("application/json")
|
||||
luci.http.write_json({
|
||||
check_plugin_status = luci.sys.call("/usr/share/amlogic/amlogic_check_plugin.sh -d " .. plugin_options .. " >/dev/null 2>&1");
|
||||
})
|
||||
end
|
||||
|
||||
--Check and download the kernel
|
||||
function check_kernel()
|
||||
luci.sys.exec("chmod +x /usr/share/amlogic/amlogic_check_kernel.sh >/dev/null 2>&1")
|
||||
local kernel_options = luci.http.formvalue("kernel_options")
|
||||
if kernel_options == "check" then
|
||||
local state = luci.sys.call("/usr/share/amlogic/amlogic_check_kernel.sh -check >/dev/null 2>&1")
|
||||
else
|
||||
local state = luci.sys.call("/usr/share/amlogic/amlogic_check_kernel.sh -download " .. kernel_options .. " >/dev/null 2>&1")
|
||||
end
|
||||
return state
|
||||
end
|
||||
|
||||
--Check the kernel
|
||||
function action_check_kernel()
|
||||
luci.http.prepare_content("application/json")
|
||||
luci.http.write_json({
|
||||
check_kernel_status = check_kernel();
|
||||
})
|
||||
end
|
||||
|
||||
--Check the firmware
|
||||
function check_firmware()
|
||||
luci.sys.exec("chmod +x /usr/share/amlogic/amlogic_check_firmware.sh >/dev/null 2>&1")
|
||||
local firmware_options = luci.http.formvalue("firmware_options")
|
||||
if firmware_options == "check" then
|
||||
local state = luci.sys.call("/usr/share/amlogic/amlogic_check_firmware.sh -check >/dev/null 2>&1")
|
||||
else
|
||||
local state = luci.sys.call("/usr/share/amlogic/amlogic_check_firmware.sh -download " .. firmware_options .. " >/dev/null 2>&1")
|
||||
end
|
||||
return state
|
||||
end
|
||||
|
||||
--Check the openwrt firmware version online
|
||||
function action_check_firmware()
|
||||
luci.http.prepare_content("application/json")
|
||||
luci.http.write_json({
|
||||
check_firmware_status = check_firmware();
|
||||
})
|
||||
end
|
||||
|
||||
--Read upload files log
|
||||
local function start_check_upfiles()
|
||||
return luci.sys.exec("sed -n '$p' /tmp/amlogic/amlogic_check_upfiles.log 2>/dev/null")
|
||||
end
|
||||
|
||||
--Read plug-in check log
|
||||
local function start_check_plugin()
|
||||
return luci.sys.exec("sed -n '$p' /tmp/amlogic/amlogic_check_plugin.log 2>/dev/null")
|
||||
end
|
||||
|
||||
--Read kernel check log
|
||||
local function start_check_kernel()
|
||||
return luci.sys.exec("sed -n '$p' /tmp/amlogic/amlogic_check_kernel.log 2>/dev/null")
|
||||
end
|
||||
|
||||
--Read openwrt firmware check log
|
||||
local function start_check_firmware()
|
||||
return luci.sys.exec("sed -n '$p' /tmp/amlogic/amlogic_check_firmware.log 2>/dev/null")
|
||||
end
|
||||
|
||||
--Read rescue kernel log
|
||||
local function start_check_rescue()
|
||||
return luci.sys.exec("sed -n '$p' /tmp/amlogic/amlogic_check_rescue.log 2>/dev/null")
|
||||
end
|
||||
|
||||
--Read openwrt install log
|
||||
local function start_check_install()
|
||||
return luci.sys.exec("sed -n '$p' /tmp/amlogic/amlogic_check_install.log 2>/dev/null")
|
||||
end
|
||||
|
||||
--Return online check plugin result
|
||||
function action_start_check_plugin()
|
||||
luci.http.prepare_content("application/json")
|
||||
luci.http.write_json({
|
||||
start_check_plugin = start_check_plugin();
|
||||
})
|
||||
end
|
||||
|
||||
--Return online check kernel result
|
||||
function action_start_check_kernel()
|
||||
luci.http.prepare_content("application/json")
|
||||
luci.http.write_json({
|
||||
start_check_kernel = start_check_kernel();
|
||||
})
|
||||
end
|
||||
|
||||
--Return online check openwrt firmware result
|
||||
function action_start_check_firmware()
|
||||
luci.http.prepare_content("application/json")
|
||||
luci.http.write_json({
|
||||
start_check_firmware = start_check_firmware();
|
||||
})
|
||||
end
|
||||
|
||||
--Return online check rescue kernel result
|
||||
function action_start_check_rescue()
|
||||
luci.http.prepare_content("application/json")
|
||||
luci.http.write_json({
|
||||
start_check_rescue = start_check_rescue();
|
||||
})
|
||||
end
|
||||
|
||||
--Return online install openwrt result
|
||||
function action_start_check_install()
|
||||
luci.http.prepare_content("application/json")
|
||||
luci.http.write_json({
|
||||
start_check_install = start_check_install();
|
||||
})
|
||||
end
|
||||
|
||||
--Return online install openwrt result for amlogic
|
||||
function action_start_amlogic_install()
|
||||
luci.http.prepare_content("application/json")
|
||||
luci.http.write_json({
|
||||
rule_install_status = start_amlogic_install();
|
||||
})
|
||||
end
|
||||
|
||||
--Return snapshot delete result
|
||||
function action_start_snapshot_delete()
|
||||
luci.http.prepare_content("application/json")
|
||||
luci.http.write_json({
|
||||
rule_delete_status = start_snapshot_delete();
|
||||
})
|
||||
end
|
||||
|
||||
--Return snapshot restore result
|
||||
function action_start_snapshot_restore()
|
||||
luci.http.prepare_content("application/json")
|
||||
luci.http.write_json({
|
||||
rule_restore_status = start_snapshot_restore();
|
||||
})
|
||||
end
|
||||
|
||||
--Return openwrt update result for amlogic
|
||||
function action_start_amlogic_update()
|
||||
luci.http.prepare_content("application/json")
|
||||
luci.http.write_json({
|
||||
rule_update_status = start_amlogic_update();
|
||||
})
|
||||
end
|
||||
|
||||
--Return kernel update result
|
||||
function action_start_amlogic_kernel()
|
||||
luci.http.prepare_content("application/json")
|
||||
luci.http.write_json({
|
||||
rule_kernel_status = start_amlogic_kernel();
|
||||
})
|
||||
end
|
||||
|
||||
--Return plugin update result
|
||||
function action_start_amlogic_plugin()
|
||||
luci.http.prepare_content("application/json")
|
||||
luci.http.write_json({
|
||||
rule_plugin_status = start_amlogic_plugin();
|
||||
})
|
||||
end
|
||||
|
||||
--Return rescue kernel result
|
||||
function action_start_amlogic_rescue()
|
||||
luci.http.prepare_content("application/json")
|
||||
luci.http.write_json({
|
||||
start_amlogic_rescue = start_amlogic_rescue();
|
||||
})
|
||||
end
|
||||
|
||||
--Return files upload result
|
||||
function action_start_check_upfiles()
|
||||
luci.http.prepare_content("application/json")
|
||||
luci.http.write_json({
|
||||
start_check_upfiles = start_check_upfiles();
|
||||
})
|
||||
end
|
||||
|
||||
--Return the current openwrt firmware version
|
||||
local function current_firmware_version()
|
||||
return luci.sys.exec("uname -r 2>/dev/null | grep -oE '^[1-9].[0-9]{1,3}.[0-9]+'") or "Invalid value."
|
||||
end
|
||||
|
||||
--Return the current plugin version
|
||||
local function current_plugin_version()
|
||||
local version_str = ""
|
||||
|
||||
-- Check if opkg command exists
|
||||
if sys.call("command -v opkg >/dev/null") == 0 then
|
||||
local opkg_cmd = string.format("opkg list-installed | grep '^%s' | awk '{print $3}' | cut -d'-' -f1", PKG_NAME)
|
||||
version_str = trim(sys.exec(opkg_cmd))
|
||||
end
|
||||
|
||||
-- If opkg failed or not found, check if apk command exists
|
||||
if version_str == "" and sys.call("command -v apk >/dev/null") == 0 then
|
||||
local apk_cmd = string.format("apk list --installed 2>/dev/null | grep '^%s' | awk '{print $1}' | cut -d'-' -f4", PKG_NAME)
|
||||
version_str = trim(sys.exec(apk_cmd))
|
||||
end
|
||||
|
||||
if version_str ~= "" then
|
||||
return version_str
|
||||
else
|
||||
return "Invalid value."
|
||||
end
|
||||
end
|
||||
|
||||
--Return the current kernel version
|
||||
local function current_kernel_version()
|
||||
return luci.sys.exec("uname -r 2>/dev/null | grep -oE '^[1-9].[0-9]{1,3}.[0-9]+'") or "Invalid value."
|
||||
end
|
||||
|
||||
--Return the current kernel branch
|
||||
local function current_kernel_branch()
|
||||
local default_kernel_branch = luci.sys.exec("uname -r 2>/dev/null | grep -oE '^[1-9].[0-9]{1,3}'")
|
||||
local amlogic_kernel_branch = luci.sys.exec("uci get amlogic.config.amlogic_kernel_branch 2>/dev/null | grep -oE '^[1-9].[0-9]{1,3}'") or ""
|
||||
if trim(amlogic_kernel_branch) == "" then
|
||||
return default_kernel_branch
|
||||
else
|
||||
return amlogic_kernel_branch
|
||||
end
|
||||
end
|
||||
|
||||
--Return current version information
|
||||
function action_state()
|
||||
luci.http.prepare_content("application/json")
|
||||
luci.http.write_json({
|
||||
current_firmware_version = current_firmware_version(),
|
||||
current_plugin_version = current_plugin_version(),
|
||||
current_kernel_version = current_kernel_version(),
|
||||
current_kernel_branch = current_kernel_branch();
|
||||
})
|
||||
end
|
||||
|
||||
--Read external model database
|
||||
local function my_model_database()
|
||||
if (string.find(device_platfrom, "allwinner")) ~= nil then
|
||||
return luci.sys.exec("cat /etc/model_database.txt 2>/dev/null | grep -E '^w[0-9]{1,9}.*:' | awk -F ':' '{print $1,$2}' OFS='###' ORS='@@@' | tr ' ' '~' 2>&1")
|
||||
else
|
||||
return luci.sys.exec("cat /etc/model_database.txt 2>/dev/null | grep -E '^[0-9]{1,9}.*:' | awk -F ':' '{print $1,$2}' OFS='###' ORS='@@@' | tr ' ' '~' 2>&1")
|
||||
end
|
||||
end
|
||||
|
||||
--Return external model database
|
||||
function action_check_model_database()
|
||||
luci.http.prepare_content("application/json")
|
||||
luci.http.write_json({
|
||||
my_model_database = my_model_database();
|
||||
})
|
||||
end
|
||||
|
||||
--Return the current snapshot list
|
||||
local function current_snapshot()
|
||||
return luci.sys.exec("btrfs subvolume list -rt / | awk '{print $4}' | grep .snapshots") or "Invalid value."
|
||||
end
|
||||
|
||||
--Return current snapshot information
|
||||
function action_check_snapshot()
|
||||
luci.http.prepare_content("application/json")
|
||||
luci.http.write_json({
|
||||
current_snapshot = current_snapshot();
|
||||
})
|
||||
end
|
||||
|
||||
--Return the openwrt compile author
|
||||
local function openwrt_author()
|
||||
return luci.sys.exec("uci get amlogic.config.amlogic_firmware_repo 2>/dev/null") or "Invalid value."
|
||||
end
|
||||
|
||||
--Return current snapshot information
|
||||
function action_openwrt_author()
|
||||
luci.http.prepare_content("application/json")
|
||||
luci.http.write_json({
|
||||
openwrt_author = openwrt_author();
|
||||
})
|
||||
end
|
||||
|
||||
--Shut down the router
|
||||
function action_poweroff()
|
||||
luci.sys.exec("/sbin/poweroff")
|
||||
end
|
||||
|
||||
--Switching dual partition
|
||||
function action_switch()
|
||||
luci.sys.exec("[ -f /boot/efi/EFI/BOOT/grub.cfg.prev ] && (cd /boot/efi/EFI/BOOT/ && mv -f grub.cfg grub.cfg.bak && mv -f grub.cfg.prev grub.cfg && mv -f grub.cfg.bak grub.cfg.prev)")
|
||||
luci.sys.exec("sync && reboot")
|
||||
end
|
||||
|
||||
@@ -1,98 +0,0 @@
|
||||
--Copyright: https://github.com/coolsnowwolf/luci/tree/master/applications/luci-app-cpufreq
|
||||
--Planner: https://github.com/unifreq/openwrt_packit
|
||||
--Extended support: https://github.com/ophub/luci-app-amlogic
|
||||
--Function: Support multi-core
|
||||
|
||||
local mp
|
||||
|
||||
--Remove the spaces in the string
|
||||
function trim(str)
|
||||
--return (string.gsub(str, "^%s*(.-)%s*$", "%1"))
|
||||
return (string.gsub(str, "%s+", ""))
|
||||
end
|
||||
|
||||
--split
|
||||
function string.split(e, t)
|
||||
e = tostring(e)
|
||||
t = tostring(t)
|
||||
if (t == '') then return false end
|
||||
local a, o = 0, {}
|
||||
for i, t in function() return string.find(e, t, a, true) end do
|
||||
table.insert(o, string.sub(e, a, i - 1))
|
||||
a = t + 1
|
||||
end
|
||||
table.insert(o, string.sub(e, a))
|
||||
return o
|
||||
end
|
||||
|
||||
--Auto-complete node
|
||||
local check_config_settings = luci.sys.exec("uci get amlogic.@settings[0].governor0 2>/dev/null") or ""
|
||||
if (trim(check_config_settings) == "") then
|
||||
luci.sys.exec("uci delete amlogic.@settings[0] 2>/dev/null")
|
||||
luci.sys.exec("uci set amlogic.armcpu='settings' 2>/dev/null")
|
||||
luci.sys.exec("uci commit amlogic 2>/dev/null")
|
||||
end
|
||||
|
||||
mp = Map("amlogic")
|
||||
mp.title = translate("CPU Freq Settings")
|
||||
mp.description = translate("Set CPU Scaling Governor to Max Performance or Balance Mode")
|
||||
|
||||
s = mp:section(NamedSection, "armcpu", "settings")
|
||||
s.anonymouse = true
|
||||
|
||||
local cpu_policys = luci.sys.exec("ls /sys/devices/system/cpu/cpufreq | grep -E 'policy[0-9]{1,3}' | xargs") or "policy0"
|
||||
policy_array = string.split(cpu_policys, " ")
|
||||
|
||||
for tt, policy_name in ipairs(policy_array) do
|
||||
|
||||
--Dynamic tab, automatically changes according to the number of cores, begin ------
|
||||
policy_name = tostring(trim(policy_name))
|
||||
policy_id = tostring(trim(string.gsub(policy_name, "policy", "")))
|
||||
|
||||
tab_name = policy_name
|
||||
tab_id = tostring(trim("tab" .. policy_id))
|
||||
|
||||
cpu_freqs = nixio.fs.readfile(trim("/sys/devices/system/cpu/cpufreq/" .. policy_name .. "/scaling_available_frequencies")) or "100000"
|
||||
cpu_freqs = string.sub(cpu_freqs, 1, -3)
|
||||
cpu_governors = nixio.fs.readfile(trim("/sys/devices/system/cpu/cpufreq/" .. policy_name .. "/scaling_available_governors")) or "performance"
|
||||
cpu_governors = string.sub(cpu_governors, 1, -3)
|
||||
freq_array = string.split(cpu_freqs, " ")
|
||||
governor_array = string.split(cpu_governors, " ")
|
||||
|
||||
s:tab(tab_id, tab_name)
|
||||
|
||||
tab_core_type = s:taboption(tab_id, DummyValue, trim("core_type" .. policy_id), translate("Microarchitectures:"))
|
||||
tab_core_type.default = luci.sys.exec("cat /sys/devices/system/cpu/cpu" .. policy_id .. "/uevent | grep -E '^OF_COMPATIBLE_0.*' | tr -d 'OF_COMPATIBLE_0=' | xargs") or "Unknown"
|
||||
tab_core_type.rmempty = false
|
||||
|
||||
governor = s:taboption(tab_id, ListValue, trim("governor" .. policy_id), translate("CPU Scaling Governor:"))
|
||||
for t, e in ipairs(governor_array) do
|
||||
if e ~= "" then governor:value(e, translate(e, string.upper(e))) end
|
||||
end
|
||||
governor.default = "schedutil"
|
||||
governor.rmempty = false
|
||||
|
||||
minfreq = s:taboption(tab_id, ListValue, trim("minfreq" .. policy_id), translate("Min Freq:"))
|
||||
for t, e in ipairs(freq_array) do
|
||||
if e ~= "" then minfreq:value(e) end
|
||||
end
|
||||
minfreq.default = "500000"
|
||||
minfreq.rmempty = false
|
||||
|
||||
maxfreq = s:taboption(tab_id, ListValue, trim("maxfreq" .. policy_id), translate("Max Freq:"))
|
||||
for t, e in ipairs(freq_array) do
|
||||
if e ~= "" then maxfreq:value(e) end
|
||||
end
|
||||
maxfreq.default = "1512000"
|
||||
maxfreq.rmempty = false
|
||||
|
||||
--Dynamic tab, automatically changes according to the number of cores, end ------
|
||||
|
||||
end
|
||||
|
||||
-- Apply the settings to the system
|
||||
function mp.on_after_commit(self)
|
||||
luci.sys.exec("(sleep 2 && /etc/init.d/amlogic start) >/dev/null 2>&1 &")
|
||||
end
|
||||
|
||||
return mp
|
||||
@@ -1,151 +0,0 @@
|
||||
--Copyright: https://github.com/coolsnowwolf/luci/tree/master/applications/luci-app-filetransfer
|
||||
--Extended support: https://github.com/ophub/luci-app-amlogic
|
||||
--Function: Download files
|
||||
|
||||
local io = require "io"
|
||||
local os = require "os"
|
||||
local fs = require "nixio.fs"
|
||||
local b, c, x
|
||||
|
||||
-- Checks wheather the given path exists and points to a directory.
|
||||
function isdirectory(dirname)
|
||||
return fs.stat(dirname, "type") == "dir"
|
||||
end
|
||||
|
||||
-- Check if a file or directory exists
|
||||
function file_exists(path)
|
||||
local file = io.open(path, "rb")
|
||||
if file then file:close() end
|
||||
return file ~= nil
|
||||
end
|
||||
|
||||
--SimpleForm for Backup Config
|
||||
b = SimpleForm("backup", nil)
|
||||
b.title = translate("Backup Firmware Config")
|
||||
b.description = translate("Backup OpenWrt config (openwrt_config.tar.gz). Use this file to restore the config in [Manually Upload Update].")
|
||||
b.reset = false
|
||||
b.submit = false
|
||||
|
||||
s = b:section(SimpleSection, "", "")
|
||||
|
||||
-- Button for customize backup list
|
||||
my = s:option(Button, "customize", translate("Edit List:"))
|
||||
my.template = "amlogic/other_button"
|
||||
|
||||
my.render = function(self, section, scope)
|
||||
self.section = true
|
||||
scope.display = ""
|
||||
self.inputtitle = translate("Open List")
|
||||
self.inputstyle = "save"
|
||||
Button.render(self, section, scope)
|
||||
end
|
||||
|
||||
my.write = function(self, section, scope)
|
||||
local handle = io.popen("[[ -s /etc/amlogic_backup_list.conf ]] || sed -n \"/BACKUP_LIST='/,/.*'$/p\" /usr/sbin/openwrt-backup | sed -e \"s/BACKUP_LIST=\\(.*\\)/\\1/; s/'//g; s/\\\\\\\\//g; s/ //g\" > /etc/amlogic_backup_list.conf 2>/dev/null")
|
||||
handle:close()
|
||||
luci.http.redirect(luci.dispatcher.build_url("admin", "system", "amlogic", "backuplist"))
|
||||
end
|
||||
|
||||
-- Button for download backup config
|
||||
o = s:option(Button, "download", translate("Backup Config:"))
|
||||
o.template = "amlogic/other_button"
|
||||
|
||||
um = s:option(DummyValue, "", nil)
|
||||
um.template = "amlogic/other_dvalue"
|
||||
|
||||
o.render = function(self, section, scope)
|
||||
self.section = true
|
||||
scope.display = ""
|
||||
self.inputtitle = translate("Download Backup")
|
||||
self.inputstyle = "save"
|
||||
Button.render(self, section, scope)
|
||||
end
|
||||
|
||||
o.write = function(self, section, scope)
|
||||
local x = luci.sys.exec("chmod +x /usr/sbin/openwrt-backup 2>/dev/null")
|
||||
local r = luci.sys.exec("/usr/sbin/openwrt-backup -b > /tmp/amlogic/amlogic.log && sync 2>/dev/null")
|
||||
|
||||
local sPath, sFile, fd, block
|
||||
sPath = "/.reserved/openwrt_config.tar.gz"
|
||||
sFile = fs.basename(sPath)
|
||||
if isdirectory(sPath) then
|
||||
fd = io.popen('tar -C "%s" -cz .' % { sPath }, "r")
|
||||
sFile = sFile .. ".tar.gz"
|
||||
else
|
||||
fd = nixio.open(sPath, "r")
|
||||
end
|
||||
if not fd then
|
||||
um.value = translate("Couldn't open file:") .. sPath
|
||||
return
|
||||
else
|
||||
um.value = translate("The file Will download automatically.") .. sPath
|
||||
end
|
||||
|
||||
luci.http.header('Content-Disposition', 'attachment; filename="%s"' % { sFile })
|
||||
luci.http.prepare_content("application/octet-stream")
|
||||
while true do
|
||||
block = fd:read(nixio.const.buffersize)
|
||||
if (not block) or (#block == 0) then
|
||||
break
|
||||
else
|
||||
luci.http.write(block)
|
||||
end
|
||||
end
|
||||
fd:close()
|
||||
luci.http.close()
|
||||
end
|
||||
|
||||
-- Button for restore backup list
|
||||
r = s:option(Button, "restore", translate("Restore Backup:"))
|
||||
r.template = "amlogic/other_button"
|
||||
|
||||
r.render = function(self, section, scope)
|
||||
self.section = true
|
||||
scope.display = ""
|
||||
self.inputtitle = translate("Upload Backup")
|
||||
self.inputstyle = "save"
|
||||
Button.render(self, section, scope)
|
||||
end
|
||||
|
||||
r.write = function(self, section, scope)
|
||||
luci.http.redirect(luci.dispatcher.build_url("admin", "system", "amlogic", "upload"))
|
||||
end
|
||||
|
||||
-- SimpleForm for Create Snapshot
|
||||
c = SimpleForm("snapshot", nil)
|
||||
c.title = translate("Snapshot Management")
|
||||
c.description = translate("Create a snapshot of the current system configuration, or restore to a snapshot.")
|
||||
c.reset = false
|
||||
c.submit = false
|
||||
|
||||
d = c:section(SimpleSection, "", nil)
|
||||
|
||||
w = d:option(Button, "create_snapshot", "")
|
||||
w.template = "amlogic/other_button"
|
||||
w.render = function(self, section, scope)
|
||||
self.section = true
|
||||
scope.display = ""
|
||||
self.inputtitle = translate("Create Snapshot")
|
||||
self.inputstyle = "save"
|
||||
Button.render(self, section, scope)
|
||||
end
|
||||
|
||||
w.write = function(self, section, scope)
|
||||
local x = luci.sys.exec("btrfs subvolume snapshot -r /etc /.snapshots/etc-" .. os.date("%m.%d.%H%M%S") .. " 2>/dev/null && sync")
|
||||
luci.http.redirect(luci.dispatcher.build_url("admin", "system", "amlogic", "backup"))
|
||||
end
|
||||
w = d:option(TextValue, "snapshot_list", nil)
|
||||
w.template = "amlogic/other_snapshot"
|
||||
|
||||
--KVM virtual machine switching dual partition
|
||||
if file_exists("/boot/efi/EFI/BOOT/grub.cfg.prev") then
|
||||
x = SimpleForm("kvm", nil)
|
||||
x.title = translate("KVM dual system switching")
|
||||
x.description = translate("You can freely switch between KVM dual partitions, using OpenWrt systems in different partitions.")
|
||||
x.reset = false
|
||||
x.submit = false
|
||||
|
||||
x:section(SimpleSection).template = "amlogic/other_kvm"
|
||||
end
|
||||
|
||||
return b, c, x
|
||||
@@ -1,51 +0,0 @@
|
||||
local fs = require "nixio.fs"
|
||||
local backup_list_conf = "/etc/amlogic_backup_list.conf"
|
||||
|
||||
-- Delete all spaces and tabs at the beginning of the line, and the unified line break is \n
|
||||
function remove_spaces(value)
|
||||
local lines = {}
|
||||
for line in value:gmatch("[^\r\n]+") do
|
||||
line = line:gsub("^%s*", "")
|
||||
if line ~= "" then
|
||||
table.insert(lines, line)
|
||||
end
|
||||
end
|
||||
value = table.concat(lines, "\n")
|
||||
value = value:gsub("[\r\n]+", "\n")
|
||||
return value
|
||||
end
|
||||
|
||||
-- Remove backslash at the end of each line
|
||||
function remove_backslash_at_end(value)
|
||||
local lines = {}
|
||||
for line in value:gmatch("[^\r\n]+") do
|
||||
line = line:gsub("%s*\\%s*$", "")
|
||||
table.insert(lines, line)
|
||||
end
|
||||
return table.concat(lines, "\n")
|
||||
end
|
||||
|
||||
local f = SimpleForm("customize",
|
||||
translate("Backup Configuration - Custom List"),
|
||||
translate("Write one configuration item per line, and directories should end with a /."))
|
||||
|
||||
local o = f:field(Value, "_custom")
|
||||
|
||||
o.template = "cbi/tvalue"
|
||||
o.rows = 30
|
||||
|
||||
function o.cfgvalue(self, section)
|
||||
local readconf = fs.readfile(backup_list_conf)
|
||||
local value = remove_spaces(readconf)
|
||||
local value = remove_backslash_at_end(value)
|
||||
return value
|
||||
end
|
||||
|
||||
function o.write(self, section, value)
|
||||
local value = remove_spaces(value)
|
||||
local value = remove_backslash_at_end(value)
|
||||
fs.writefile(backup_list_conf, value)
|
||||
luci.http.redirect(luci.dispatcher.build_url("admin", "system", "amlogic", "backup"))
|
||||
end
|
||||
|
||||
return f
|
||||
@@ -1,22 +0,0 @@
|
||||
local b, c
|
||||
|
||||
--SimpleForm for Check
|
||||
b = SimpleForm("amlogic", nil)
|
||||
b.title = translate("Check Update")
|
||||
b.description = translate("Provide OpenWrt Firmware, Kernel and Plugin online check, download and update service.")
|
||||
b.reset = false
|
||||
b.submit = false
|
||||
|
||||
b:section(SimpleSection).template = "amlogic/other_check"
|
||||
|
||||
|
||||
--SimpleForm for Rescue Kernel
|
||||
c = SimpleForm("rescue", nil)
|
||||
c.title = translate("Rescue Kernel")
|
||||
c.description = translate("When a kernel update fails and causes the OpenWrt system to be unbootable, the kernel can be restored by mutual recovery from eMMC/NVMe/sdX.")
|
||||
c.reset = false
|
||||
c.submit = false
|
||||
|
||||
c:section(SimpleSection).template = "amlogic/other_rescue"
|
||||
|
||||
return b, c
|
||||
@@ -1,164 +0,0 @@
|
||||
--Remove the spaces in the string
|
||||
function trim(str)
|
||||
--return (string.gsub(str, "^%s*(.-)%s*$", "%1"))
|
||||
return (string.gsub(str, "%s+", ""))
|
||||
end
|
||||
|
||||
--Auto-complete node
|
||||
local check_config_amlogic = luci.sys.exec("uci get amlogic.@amlogic[0].amlogic_firmware_repo 2>/dev/null") or ""
|
||||
if (trim(check_config_amlogic) == "") then
|
||||
luci.sys.exec("uci delete amlogic.@amlogic[0] 2>/dev/null")
|
||||
luci.sys.exec("uci set amlogic.config='amlogic' 2>/dev/null")
|
||||
luci.sys.exec("uci commit amlogic 2>/dev/null")
|
||||
end
|
||||
|
||||
b = Map("amlogic")
|
||||
b.title = translate("Plugin Settings")
|
||||
local des_content = translate("You can customize the github.com download repository of OpenWrt files and kernels in [Online Download Update].")
|
||||
local des_content = des_content .. "<br />" .. translate("Tip: The same files as the current OpenWrt system's BOARD (such as rock5b) and kernel (such as 5.10) will be downloaded.")
|
||||
b.description = des_content
|
||||
|
||||
o = b:section(NamedSection, "config", "amlogic")
|
||||
o.anonymouse = true
|
||||
|
||||
--1.Set OpenWrt Firmware Repository
|
||||
mydevice = o:option(DummyValue, "mydevice", translate("Current Device:"))
|
||||
mydevice.description = translate("Display the PLATFORM classification of the device.")
|
||||
mydevice_platfrom = trim(luci.sys.exec("cat /etc/flippy-openwrt-release 2>/dev/null | grep PLATFORM | awk -F'=' '{print $2}' | grep -oE '(amlogic|rockchip|allwinner|qemu)'")) or "Unknown"
|
||||
mydevice.default = "PLATFORM: " .. mydevice_platfrom
|
||||
mydevice.rmempty = false
|
||||
|
||||
--2.Set OpenWrt Firmware Repository
|
||||
firmware_repo = o:option(Value, "amlogic_firmware_repo", translate("OpenWrt download repository:"))
|
||||
firmware_repo.description = translate("Set the OpenWrt files download repository on github.com in [Online Download Update].")
|
||||
firmware_repo.default = "https://github.com/breakingbadboy/OpenWrt"
|
||||
firmware_repo.rmempty = false
|
||||
|
||||
--3.Set OpenWrt Releases's Tag Keywords
|
||||
firmware_tag = o:option(Value, "amlogic_firmware_tag", translate("OpenWrt download tags keyword:"))
|
||||
firmware_tag.description = translate("Set the OpenWrt files download tags keyword for github.com in [Online Download Update].")
|
||||
firmware_tag.default = "ARMv8"
|
||||
firmware_tag.rmempty = false
|
||||
|
||||
--4.Set OpenWrt Firmware Suffix
|
||||
firmware_suffix = o:option(Value, "amlogic_firmware_suffix", translate("OpenWrt files suffix:"))
|
||||
firmware_suffix.description = translate("Set the OpenWrt files download suffix for github.com in [Online Download Update].")
|
||||
firmware_suffix:value(".7z", translate(".7z"))
|
||||
firmware_suffix:value(".zip", translate(".zip"))
|
||||
firmware_suffix:value(".img.gz", translate(".img.gz"))
|
||||
firmware_suffix:value(".img.xz", translate(".img.xz"))
|
||||
firmware_suffix.default = ".img.gz"
|
||||
firmware_suffix.rmempty = false
|
||||
|
||||
--5.Set OpenWrt Kernel DownLoad Path
|
||||
kernel_path = o:option(Value, "amlogic_kernel_path", translate("Kernel download repository:"))
|
||||
kernel_path.description = translate("Set the kernel files download repository on github.com in [Online Download Update].")
|
||||
kernel_path:value("https://github.com/breakingbadboy/OpenWrt")
|
||||
kernel_path:value("https://github.com/ophub/kernel")
|
||||
kernel_path.default = "https://github.com/breakingbadboy/OpenWrt"
|
||||
kernel_path.rmempty = false
|
||||
|
||||
--6. Set OpenWrt Kernel Tags
|
||||
-- Read the currently SAVED value of the kernel path.
|
||||
local current_kernel_path = trim(luci.sys.exec("uci get amlogic.@amlogic[0].amlogic_kernel_path 2>/dev/null") or "")
|
||||
-- If it's not set yet, use its default value for the logic below.
|
||||
if current_kernel_path == "" then
|
||||
current_kernel_path = kernel_path.default
|
||||
end
|
||||
-- Define the tag lists.
|
||||
-- The base list, available for all repositories.
|
||||
local known_tags = {
|
||||
kernel_rk3588 = "kernel_rk3588 [Rockchip RK3588 Kernel]",
|
||||
kernel_rk35xx = "kernel_rk35xx [Rockchip RK35xx Kernel]",
|
||||
kernel_stable = "kernel_stable [Mainline Stable Kernel]",
|
||||
}
|
||||
-- Additional tags only available for the 'ophub/kernel' repository.
|
||||
local ophub_extra_tags = {
|
||||
kernel_flippy = "kernel_flippy [Mainline Stable Kernel by Flippy]",
|
||||
kernel_h6 = "kernel_h6 [Allwinner H6 Kernel]",
|
||||
kernel_beta = "kernel_beta [Beta Kernel]",
|
||||
}
|
||||
-- Conditionally add the extra tags to the list.
|
||||
if (string.find(current_kernel_path, "ophub/kernel")) then
|
||||
for value, display_name in pairs(ophub_extra_tags) do
|
||||
known_tags[value] = display_name
|
||||
end
|
||||
end
|
||||
-- Determine the default kernel tag based on existing config or system info.
|
||||
local kernel_tagsname
|
||||
local existing_tag = trim(luci.sys.exec("uci get amlogic.@amlogic[0].amlogic_kernel_tags 2>/dev/null") or "")
|
||||
if existing_tag ~= "" then
|
||||
kernel_tagsname = existing_tag
|
||||
else
|
||||
local kernel_release_info = trim(luci.sys.exec("uname -r 2>/dev/null")) or ""
|
||||
|
||||
if (string.find(kernel_release_info, "-rk3588")) then
|
||||
kernel_tagsname = "kernel_rk3588"
|
||||
elseif (string.find(kernel_release_info, "-rk35xx")) then
|
||||
kernel_tagsname = "kernel_rk35xx"
|
||||
elseif (string.find(kernel_release_info, "-h6") or string.find(kernel_release_info, "-zicai")) then
|
||||
kernel_tagsname = "kernel_h6"
|
||||
else
|
||||
kernel_tagsname = "kernel_stable"
|
||||
end
|
||||
end
|
||||
-- Create the kernel tags option.
|
||||
kernel_tags = o:option(Value, "amlogic_kernel_tags", translate("Kernel download tags:"))
|
||||
kernel_tags.description = translate("Set the kernel files download tags on github.com in [Online Download Update].")
|
||||
-- Populate the dropdown with the known tags.
|
||||
for value, display_name in pairs(known_tags) do
|
||||
kernel_tags:value(value, translate(display_name))
|
||||
end
|
||||
-- Ensure the default tag is included in the options.
|
||||
if not known_tags[kernel_tagsname] then
|
||||
kernel_tags:value(kernel_tagsname, kernel_tagsname)
|
||||
end
|
||||
-- Set the default and other properties.
|
||||
kernel_tags.default = kernel_tagsname
|
||||
kernel_tags.rmempty = false
|
||||
|
||||
--7.Set kernel version branch
|
||||
kernel_branch = o:option(Value, "amlogic_kernel_branch", translate("Set version branch:"))
|
||||
kernel_branch.description = translate("Set the version branch of the OpenWrt files and kernel selected in [Online Download Update].")
|
||||
kernel_branch:value("5.4", translate("5.4"))
|
||||
kernel_branch:value("5.10", translate("5.10"))
|
||||
kernel_branch:value("5.15", translate("5.15"))
|
||||
kernel_branch:value("6.1", translate("6.1"))
|
||||
kernel_branch:value("6.6", translate("6.6"))
|
||||
kernel_branch:value("6.12", translate("6.12"))
|
||||
kernel_branch:value("6.18", translate("6.18"))
|
||||
local default_kernel_branch = luci.sys.exec("uname -r | grep -oE '^[1-9].[0-9]{1,3}'")
|
||||
kernel_branch.default = trim(default_kernel_branch)
|
||||
kernel_branch.rmempty = false
|
||||
|
||||
--8.Set plugin branch
|
||||
plugin_branch = o:option(Value, "amlogic_plugin_branch", translate("Set plugin branch:"))
|
||||
plugin_branch.description = translate("Set the branch of the luci-app-amlogic plugin used in [Only update Amlogic Service]. Default (empty) uses the main (Lua) branch.")
|
||||
plugin_branch:value("", translate("main [Lua version]"))
|
||||
plugin_branch:value("js", translate("js [JavaScript version]"))
|
||||
plugin_branch.default = ""
|
||||
plugin_branch.rmempty = true
|
||||
|
||||
--9.Restore configuration
|
||||
firmware_config = o:option(Flag, "amlogic_firmware_config", translate("Keep config update:"))
|
||||
firmware_config.description = translate("Set whether to keep the current config during [Online Download Update] and [Manually Upload Update].")
|
||||
firmware_config.default = "1"
|
||||
firmware_config.rmempty = false
|
||||
|
||||
--9.Write bootloader
|
||||
write_bootloader = o:option(Flag, "amlogic_write_bootloader", translate("Auto write bootloader:"))
|
||||
write_bootloader.description = translate("[Recommended choice] Set whether to auto write bootloader during install and update OpenWrt.")
|
||||
write_bootloader.default = "0"
|
||||
write_bootloader.rmempty = false
|
||||
|
||||
--10.Set the file system type of the shared partition
|
||||
shared_fstype = o:option(ListValue, "amlogic_shared_fstype", translate("Set the file system type:"))
|
||||
shared_fstype.description = translate("[Default ext4] Set the file system type of the shared partition (/mnt/mmcblk*p4) when install OpenWrt.")
|
||||
shared_fstype:value("ext4", translate("ext4"))
|
||||
shared_fstype:value("f2fs", translate("f2fs"))
|
||||
shared_fstype:value("btrfs", translate("btrfs"))
|
||||
shared_fstype:value("xfs", translate("xfs"))
|
||||
shared_fstype.default = "ext4"
|
||||
shared_fstype.rmempty = false
|
||||
|
||||
return b
|
||||
@@ -1,12 +0,0 @@
|
||||
local b
|
||||
|
||||
--SimpleForm for Info
|
||||
b = SimpleForm("amlogic", nil)
|
||||
b.title = translate("Amlogic Service")
|
||||
b.description = translate("Supports management of Amlogic s9xxx, Allwinner (V-Plus Cloud), and Rockchip (BeikeYun, Chainedbox L1 Pro) boxes.")
|
||||
b.reset = false
|
||||
b.submit = false
|
||||
|
||||
b:section(SimpleSection).template = "amlogic/other_info"
|
||||
|
||||
return b
|
||||
@@ -1,12 +0,0 @@
|
||||
local b
|
||||
|
||||
--SimpleForm for Install OpenWrt to Amlogic EMMC
|
||||
b = SimpleForm("amlogic_install", nil)
|
||||
b.title = translate("Install OpenWrt")
|
||||
b.description = translate("Install OpenWrt to EMMC, Please select the device model, Or enter the dtb file name.")
|
||||
b.reset = false
|
||||
b.submit = false
|
||||
|
||||
b:section(SimpleSection).template = "amlogic/other_install"
|
||||
|
||||
return b
|
||||
@@ -1,15 +0,0 @@
|
||||
local b
|
||||
|
||||
--SimpleForm for Server Logs
|
||||
b = SimpleForm("amlogic_log", nil)
|
||||
b.title = translate("Server Logs")
|
||||
b.description = translate("Display the execution log of the current operation.")
|
||||
b.reset = false
|
||||
b.submit = false
|
||||
|
||||
s = b:section(SimpleSection, "", nil)
|
||||
|
||||
o = s:option(TextValue, "")
|
||||
o.template = "amlogic/other_log"
|
||||
|
||||
return b
|
||||
@@ -1,12 +0,0 @@
|
||||
local b
|
||||
|
||||
--SimpleForm for PowerOff
|
||||
b = SimpleForm("poweroff", nil)
|
||||
b.title = translate("PowerOff")
|
||||
b.description = translate("Shut down your router device.")
|
||||
b.reset = false
|
||||
b.submit = false
|
||||
|
||||
b:section(SimpleSection).template = "amlogic/other_poweroff"
|
||||
|
||||
return b
|
||||
@@ -1,300 +0,0 @@
|
||||
--Copyright: https://github.com/coolsnowwolf/luci/tree/master/applications/luci-app-filetransfer
|
||||
--Extended support: https://github.com/ophub/luci-app-amlogic
|
||||
--Function: Upload files
|
||||
|
||||
local os = require "os"
|
||||
local fs = require "nixio.fs"
|
||||
local nutil = require "nixio.util"
|
||||
local type = type
|
||||
local b, form
|
||||
|
||||
--Remove the spaces in the string
|
||||
function trim(str)
|
||||
--return (string.gsub(str, "^%s*(.-)%s*$", "%1"))
|
||||
return (string.gsub(str, "%s+", ""))
|
||||
end
|
||||
|
||||
-- Evaluate given shell glob pattern and return a table containing all matching
|
||||
function glob(...)
|
||||
local iter, code, msg = fs.glob(...)
|
||||
if iter then
|
||||
return nutil.consume(iter)
|
||||
else
|
||||
return nil, code, msg
|
||||
end
|
||||
end
|
||||
|
||||
-- Checks wheather the given path exists and points to a regular file.
|
||||
function isfile(filename)
|
||||
return fs.stat(filename, "type") == "reg"
|
||||
end
|
||||
|
||||
-- Get the last modification time of given file path in Unix epoch format.
|
||||
function mtime(path)
|
||||
return fs.stat(path, "mtime")
|
||||
end
|
||||
|
||||
local stat_tr = {
|
||||
reg = "regular",
|
||||
dir = "directory",
|
||||
lnk = "link",
|
||||
chr = "character device",
|
||||
blk = "block device",
|
||||
fifo = "fifo",
|
||||
sock = "socket"
|
||||
}
|
||||
-- Get information about given file or directory.
|
||||
function stat(path, key)
|
||||
local data, code, msg = fs.stat(path)
|
||||
if data then
|
||||
data.mode = data.modestr
|
||||
data.type = stat_tr[data.type] or "?"
|
||||
end
|
||||
return key and data and data[key] or data, code, msg
|
||||
end
|
||||
|
||||
--Set default upload path
|
||||
local ROOT_PTNAME = trim(luci.sys.exec("df / | tail -n1 | awk '{print $1}' | awk -F '/' '{print $3}'"))
|
||||
if ROOT_PTNAME then
|
||||
if (string.find(ROOT_PTNAME, "mmcblk[0-4]p[1-4]")) then
|
||||
local EMMC_NAME = trim(luci.sys.exec("echo " .. ROOT_PTNAME .. " | awk '{print substr($1, 1, length($1)-2)}'"))
|
||||
upload_path = trim("/mnt/" .. EMMC_NAME .. "p4/")
|
||||
elseif (string.find(ROOT_PTNAME, "[hsv]d[a-z]")) then
|
||||
local EMMC_NAME = trim(luci.sys.exec("echo " .. ROOT_PTNAME .. " | awk '{print substr($1, 1, length($1)-1)}'"))
|
||||
upload_path = trim("/mnt/" .. EMMC_NAME .. "4/")
|
||||
else
|
||||
upload_path = "/tmp/upload/"
|
||||
end
|
||||
else
|
||||
upload_path = "/tmp/upload/"
|
||||
end
|
||||
|
||||
--Clear the version check log
|
||||
luci.sys.exec("echo '' > /tmp/amlogic/amlogic_check_plugin.log && sync >/dev/null 2>&1")
|
||||
luci.sys.exec("echo '' > /tmp/amlogic/amlogic_check_kernel.log && sync >/dev/null 2>&1")
|
||||
|
||||
--SimpleForm for Update OpenWrt firmware/kernel
|
||||
b = SimpleForm("upload", nil)
|
||||
b.title = translate("Upload")
|
||||
local des_content = translate("Update plugins first, then update the kernel or firmware.")
|
||||
local des_content = des_content .. "<br />" .. translate("After uploading [Firmware], [Kernel], [IPK] or [Backup Config], the operation buttons will be displayed.")
|
||||
b.description = des_content
|
||||
b.reset = false
|
||||
b.submit = false
|
||||
|
||||
s = b:section(SimpleSection, "", "")
|
||||
|
||||
o = s:option(FileUpload, "")
|
||||
o.template = "amlogic/other_upload"
|
||||
|
||||
um = s:option(DummyValue, "", nil)
|
||||
um.template = "amlogic/other_dvalue"
|
||||
|
||||
local dir, fd
|
||||
dir = upload_path
|
||||
fs.mkdir(dir)
|
||||
luci.http.setfilehandler(
|
||||
function(meta, chunk, eof)
|
||||
if not fd then
|
||||
if not meta then return end
|
||||
if meta and chunk then fd = nixio.open(dir .. meta.file, "w") end
|
||||
if not fd then
|
||||
um.value = translate("Create upload file error.") .. " Error Info: " .. trim(upload_path .. meta.file)
|
||||
return
|
||||
end
|
||||
end
|
||||
if chunk and fd then
|
||||
fd:write(chunk)
|
||||
end
|
||||
if eof and fd then
|
||||
fd:close()
|
||||
fd = nil
|
||||
um.value = translate("File saved to") .. trim(upload_path .. meta.file)
|
||||
end
|
||||
end
|
||||
)
|
||||
|
||||
if luci.http.formvalue("upload") then
|
||||
local f = luci.http.formvalue("ulfile")
|
||||
if #f <= 0 then
|
||||
um.value = translate("No specify upload file.")
|
||||
end
|
||||
end
|
||||
|
||||
local function getSizeStr(size)
|
||||
local i = 0
|
||||
local byteUnits = { ' kB', ' MB', ' GB', ' TB' }
|
||||
repeat
|
||||
size = size / 1024
|
||||
i = i + 1
|
||||
until (size <= 1024)
|
||||
return string.format("%.1f", size) .. byteUnits[i]
|
||||
end
|
||||
|
||||
local inits, attr = {}
|
||||
for i, f in ipairs(glob(trim(upload_path .. "*"))) do
|
||||
attr = stat(f)
|
||||
itisfile = isfile(f)
|
||||
if attr and itisfile then
|
||||
inits[i] = {}
|
||||
inits[i].name = fs.basename(f)
|
||||
inits[i].mtime = os.date("%Y-%m-%d %H:%M:%S", attr.mtime)
|
||||
inits[i].modestr = attr.modestr
|
||||
inits[i].size = getSizeStr(attr.size)
|
||||
inits[i].remove = 0
|
||||
inits[i].ipk = false
|
||||
|
||||
--Check whether the openwrt firmware file
|
||||
-- openwrt_s905d_v5.10.16_2021.05.31.1958.img.gz
|
||||
if (string.lower(string.sub(fs.basename(f), -7, -1)) == ".img.gz") then
|
||||
openwrt_firmware_file = true
|
||||
end
|
||||
-- openwrt_s905d_n1_R21.7.15_k5.4.134-flippy-62+o.img.xz
|
||||
if (string.lower(string.sub(fs.basename(f), -7, -1)) == ".img.xz") then
|
||||
openwrt_firmware_file = true
|
||||
end
|
||||
-- openwrt_s905d_n1_R21.7.15_k5.13.2-flippy-62+.7z
|
||||
if (string.lower(string.sub(fs.basename(f), -3, -1)) == ".7z") then
|
||||
openwrt_firmware_file = true
|
||||
end
|
||||
-- openwrt_s905d_n1_R21.7.15_k5.13.2-flippy-62+.img
|
||||
if (string.lower(string.sub(fs.basename(f), -4, -1)) == ".img") then
|
||||
openwrt_firmware_file = true
|
||||
end
|
||||
|
||||
--Check whether the three kernel files
|
||||
-- boot-5.10.16-flippy-53+.tar.gz
|
||||
if (string.lower(string.sub(fs.basename(f), 1, 5)) == "boot-") then
|
||||
boot_file = true
|
||||
end
|
||||
-- dtb-amlogic-5.10.16-flippy-53+.tar.gz
|
||||
if (string.lower(string.sub(fs.basename(f), 1, 4)) == "dtb-") then
|
||||
dtb_file = true
|
||||
end
|
||||
-- modules-5.10.16-flippy-53+.tar.gz
|
||||
if (string.lower(string.sub(fs.basename(f), 1, 8)) == "modules-") then
|
||||
modules_file = true
|
||||
end
|
||||
|
||||
--Check whether the backup file
|
||||
-- openwrt_config.tar.gz
|
||||
if (string.lower(string.sub(fs.basename(f), 1, -1)) == "openwrt_config.tar.gz") then
|
||||
backup_config_file = true
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
--SimpleForm for Upload file list
|
||||
form = SimpleForm("filelist", translate("Upload file list"), nil)
|
||||
form.reset = false
|
||||
form.submit = false
|
||||
|
||||
description_info = ""
|
||||
luci.sys.exec("echo '' > /tmp/amlogic/amlogic_check_upfiles.log && sync >/dev/null 2>&1")
|
||||
|
||||
if backup_config_file then
|
||||
description_info = description_info .. translate("There are config file in the upload directory, and you can restore the config. ")
|
||||
end
|
||||
|
||||
if boot_file and dtb_file and modules_file then
|
||||
description_info = description_info .. translate("There are kernel files in the upload directory, and you can replace the kernel.")
|
||||
luci.sys.exec("echo 'kernel' > /tmp/amlogic/amlogic_check_upfiles.log && sync >/dev/null 2>&1")
|
||||
end
|
||||
|
||||
if openwrt_firmware_file then
|
||||
description_info = description_info .. translate("There are openwrt firmware file in the upload directory, and you can update the openwrt.")
|
||||
luci.sys.exec("echo 'firmware' > /tmp/amlogic/amlogic_check_upfiles.log && sync >/dev/null 2>&1")
|
||||
end
|
||||
|
||||
if description_info ~= "" then
|
||||
form.description = ' <span style="color: green"><b> Tip: ' .. description_info .. ' </b></span> '
|
||||
end
|
||||
|
||||
tb = form:section(Table, inits)
|
||||
nm = tb:option(DummyValue, "name", translate("File name"))
|
||||
mt = tb:option(DummyValue, "mtime", translate("Modify time"))
|
||||
ms = tb:option(DummyValue, "modestr", translate("Attributes"))
|
||||
sz = tb:option(DummyValue, "size", translate("Size"))
|
||||
btnrm = tb:option(Button, "remove", translate("Remove"))
|
||||
btnrm.render = function(self, section, scope)
|
||||
self.inputstyle = "remove"
|
||||
Button.render(self, section, scope)
|
||||
end
|
||||
btnrm.write = function(self, section)
|
||||
local v = fs.unlink(trim(upload_path .. fs.basename(inits[section].name)))
|
||||
if v then table.remove(inits, section) end
|
||||
return v
|
||||
end
|
||||
|
||||
function IsConfigFile(name)
|
||||
name = name or ""
|
||||
local config_file = string.lower(string.sub(name, 1, -1))
|
||||
return config_file == "openwrt_config.tar.gz"
|
||||
end
|
||||
|
||||
-- Check if the file is a known package type (.ipk or .apk)
|
||||
function IsPackageFile(name)
|
||||
name = name or ""
|
||||
local lname = string.lower(name)
|
||||
return string.sub(lname, -4) == ".ipk" or string.sub(lname, -4) == ".apk"
|
||||
end
|
||||
|
||||
--Add Button for *.ipk
|
||||
btnis = tb:option(Button, "ipk", translate("Install"))
|
||||
btnis.template = "amlogic/other_button"
|
||||
btnis.render = function(self, section, scope)
|
||||
if not inits[section] then return false end
|
||||
if IsPackageFile(inits[section].name) then
|
||||
scope.display = ""
|
||||
self.inputtitle = translate("Install")
|
||||
elseif IsConfigFile(inits[section].name) then
|
||||
scope.display = ""
|
||||
self.inputtitle = translate("Restore")
|
||||
else
|
||||
scope.display = "none"
|
||||
end
|
||||
|
||||
self.inputstyle = "apply"
|
||||
Button.render(self, section, scope)
|
||||
end
|
||||
btnis.write = function(self, section)
|
||||
local file_to_install = inits[section].name
|
||||
local full_path = upload_path .. file_to_install
|
||||
|
||||
if IsPackageFile(file_to_install) then
|
||||
local r = ""
|
||||
local install_cmd = ""
|
||||
|
||||
-- Check for opkg first
|
||||
if luci.sys.call("command -v opkg >/dev/null") == 0 then
|
||||
install_cmd = string.format('opkg --force-reinstall install %s', full_path)
|
||||
r = luci.sys.exec(install_cmd)
|
||||
-- If opkg is not found, check for apk
|
||||
elseif luci.sys.call("command -v apk >/dev/null") == 0 then
|
||||
-- NOTE: --allow-untrusted is required for local packages
|
||||
install_cmd = string.format('apk add --force-overwrite --allow-untrusted %s', full_path)
|
||||
r = luci.sys.exec(install_cmd)
|
||||
-- If neither is found
|
||||
else
|
||||
r = "Error: Neither 'opkg' nor 'apk' package manager found on the system."
|
||||
end
|
||||
|
||||
-- Clean LuCI cache after installation
|
||||
luci.sys.exec("rm -rf /tmp/luci-indexcache /tmp/luci-modulecache/* >/dev/null 2>&1")
|
||||
|
||||
-- Display the result message
|
||||
-- We add a message to prompt the user to refresh the page to see changes.
|
||||
local result_msg = r .. "<br/><b>" .. translate("Please refresh the page to see the changes.") .. "</b>"
|
||||
form.description = string.format('<span style="color: red">%s</span>', result_msg)
|
||||
|
||||
elseif IsConfigFile(inits[section].name) then
|
||||
form.description = ' <span style="color: green"><b> ' .. translate("Tip: The config is being restored, and it will automatically restart after completion.") .. ' </b></span> '
|
||||
luci.sys.exec("chmod +x /usr/sbin/openwrt-backup 2>/dev/null")
|
||||
luci.sys.exec("/usr/sbin/openwrt-backup -r > /tmp/amlogic/amlogic.log && sync 2>/dev/null")
|
||||
end
|
||||
end
|
||||
|
||||
--SimpleForm for Check upload files
|
||||
form:section(SimpleSection).template = "amlogic/other_upfiles"
|
||||
|
||||
return b, form
|
||||
@@ -1,7 +0,0 @@
|
||||
<%+cbi/valueheader%>
|
||||
<% if self:cfgvalue(section) ~= false then %>
|
||||
<input class="cbi-button cbi-button-<%=self.inputstyle or "button" %>" style="display: <%= display %>" type="submit" <%= attr("name", cbid) .. attr("id", cbid) .. attr("value", self.inputtitle or self.title)%> />
|
||||
<% else %>
|
||||
-
|
||||
<% end %>
|
||||
<%+cbi/valuefooter%>
|
||||
@@ -1,240 +0,0 @@
|
||||
<style>
|
||||
.NewsTdHeight{ line-height: 20px; }
|
||||
</style>
|
||||
<fieldset class="cbi-section">
|
||||
<table width="100%" class="NewsTdHeight">
|
||||
<tr><td width="100%" colspan="2">
|
||||
<p align="center">
|
||||
<%:Update plugins first, then update the kernel or firmware. More options can be configured in [Plugin Settings].%> <span id="_openwrt_mainline_version"></span>
|
||||
</p>
|
||||
</td></tr>
|
||||
<tr><td width="35%" align="right"><input type="button" class="cbi-button cbi-button-reload" value="<%:Only update Amlogic Service%>" onclick="return b_check_plugin(this)"/></td><td width="65%" align="left"><span id="_current_plugin_version"><%:Collecting data...%></span> <span id="_check_plugin"></span></td></tr>
|
||||
<tr><td width="35%" align="right"><input type="button" class="cbi-button cbi-button-reload" value="<%:Update system kernel only%>" onclick="return b_check_kernel(this, 'check')"/></td><td width="65%" align="left"><span id="_current_kernel_version"><%:Collecting data...%></span> <span id="_check_kernel"></span></td></tr>
|
||||
<tr><td width="35%" align="right"><input type="button" class="cbi-button cbi-button-reload" value="<%:Complete system update%>" onclick="return b_check_firmware(this, 'check')"/></td><td width="65%" align="left"><span id="_current_firmware_version"><%:Collecting data...%></span> <span id="_check_firmware"></span></td></tr>
|
||||
</table>
|
||||
</fieldset>
|
||||
<script type="text/javascript">//<![CDATA[
|
||||
|
||||
function amlogic_update(btn,amlogic_update_sel)
|
||||
{
|
||||
btn.disabled = true;
|
||||
btn.value = '<%:Updating...%> ';
|
||||
|
||||
XHR.get('<%=luci.dispatcher.build_url("admin", "system", "amlogic", "start_amlogic_update")%>',
|
||||
{
|
||||
amlogic_update_sel: amlogic_update_sel
|
||||
},
|
||||
function(x,status)
|
||||
{
|
||||
if ( x && x.status == 200 ) {
|
||||
if(status.rule_update_status!="0")
|
||||
{
|
||||
btn.value = '<%:Update Failed%>';
|
||||
}
|
||||
else
|
||||
{
|
||||
btn.value = '<%:Successful Update%>';
|
||||
}
|
||||
}
|
||||
else {
|
||||
btn.value = '<%:Update%>';
|
||||
}
|
||||
}
|
||||
);
|
||||
btn.disabled = false;
|
||||
return false;
|
||||
}
|
||||
|
||||
function amlogic_kernel(btn)
|
||||
{
|
||||
btn.disabled = true;
|
||||
btn.value = '<%:Updating...%> ';
|
||||
|
||||
XHR.get('<%=luci.dispatcher.build_url("admin", "system", "amlogic", "start_amlogic_kernel")%>', null, function(x, status) {
|
||||
if ( x && x.status == 200 ) {
|
||||
if(status.rule_kernel_status!="0")
|
||||
{
|
||||
btn.value = '<%:Update Failed%>';
|
||||
}
|
||||
else
|
||||
{
|
||||
btn.value = '<%:Successful Update%>';
|
||||
}
|
||||
}
|
||||
else {
|
||||
btn.value = '<%:Update%>';
|
||||
}
|
||||
}
|
||||
);
|
||||
btn.disabled = false;
|
||||
return false;
|
||||
}
|
||||
|
||||
function amlogic_plugin(btn)
|
||||
{
|
||||
btn.disabled = true;
|
||||
btn.value = '<%:Updating...%> ';
|
||||
|
||||
XHR.get('<%=luci.dispatcher.build_url("admin", "system", "amlogic", "start_amlogic_plugin")%>', null, function(x, status) {
|
||||
if ( x && x.status == 200 ) {
|
||||
if(status.rule_plugin_status!="0")
|
||||
{
|
||||
btn.value = '<%:Update Failed%>';
|
||||
}
|
||||
else
|
||||
{
|
||||
btn.value = '<%:Successful Update%>';
|
||||
}
|
||||
}
|
||||
else {
|
||||
btn.value = '<%:Update%>';
|
||||
}
|
||||
}
|
||||
);
|
||||
btn.disabled = false;
|
||||
return false;
|
||||
}
|
||||
|
||||
function b_check_firmware(btn,firmware_options)
|
||||
{
|
||||
btn.disabled = true;
|
||||
btn.value = '<%:Checking...%>';
|
||||
XHR.get('<%=luci.dispatcher.build_url("admin", "system", "amlogic", "check_firmware")%>',
|
||||
{
|
||||
firmware_options: firmware_options
|
||||
},
|
||||
function(x,status)
|
||||
{
|
||||
if ( x && x.status == 200 ) {
|
||||
if(status.check_firmware_status!="0")
|
||||
{
|
||||
btn.value = '<%:Complete system update%>';
|
||||
}
|
||||
else
|
||||
{
|
||||
btn.value = '<%:Complete system update%>';
|
||||
}
|
||||
}
|
||||
else {
|
||||
btn.value = '<%:Complete system update%>';
|
||||
}
|
||||
}
|
||||
);
|
||||
btn.disabled = false;
|
||||
return false;
|
||||
}
|
||||
|
||||
// b_check_plugin handles both phases:
|
||||
// Phase 1 (check): called with no second arg — queries latest version, log shows Download button
|
||||
// Phase 2 (download): called with 'download_VERSION' — downloads the package, log shows Update button
|
||||
function b_check_plugin(btn, plugin_options)
|
||||
{
|
||||
btn.disabled = true;
|
||||
if (plugin_options && plugin_options.indexOf('download_') === 0) {
|
||||
// Phase 2: download the specified version
|
||||
btn.value = '<%:Downloading...%>';
|
||||
XHR.get('<%=luci.dispatcher.build_url("admin", "system", "amlogic", "check_plugin_download")%>',
|
||||
{ plugin_options: plugin_options },
|
||||
function(x, status) {
|
||||
btn.disabled = false;
|
||||
btn.value = '<%:Only update Amlogic Service%>';
|
||||
}
|
||||
);
|
||||
} else {
|
||||
// Phase 1: check for new version
|
||||
btn.value = '<%:Checking...%>';
|
||||
XHR.get('<%=luci.dispatcher.build_url("admin", "system", "amlogic", "check_plugin")%>', null,
|
||||
function(x, status) {
|
||||
btn.disabled = false;
|
||||
btn.value = '<%:Only update Amlogic Service%>';
|
||||
}
|
||||
);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
// Called from the log-injected Download button (two-phase new script)
|
||||
function b_check_plugin_download(btn, plugin_options)
|
||||
{
|
||||
return b_check_plugin(btn, plugin_options);
|
||||
}
|
||||
|
||||
function b_check_kernel(btn,kernel_options)
|
||||
{
|
||||
btn.disabled = true;
|
||||
btn.value = '<%:Checking...%>';
|
||||
XHR.get('<%=luci.dispatcher.build_url("admin", "system", "amlogic", "check_kernel")%>',
|
||||
{
|
||||
kernel_options: kernel_options
|
||||
},
|
||||
function(x,status)
|
||||
{
|
||||
if ( x && x.status == 200 ) {
|
||||
if(status.check_kernel_status!="0")
|
||||
{
|
||||
btn.value = '<%:Update system kernel only%>';
|
||||
}
|
||||
else
|
||||
{
|
||||
btn.value = '<%:Update system kernel only%>';
|
||||
}
|
||||
}
|
||||
else {
|
||||
btn.value = '<%:Update system kernel only%>';
|
||||
}
|
||||
}
|
||||
);
|
||||
btn.disabled = false;
|
||||
return false;
|
||||
}
|
||||
|
||||
var _check_firmware = document.getElementById('_check_firmware');
|
||||
XHR.poll(1, '<%=luci.dispatcher.build_url("admin", "system", "amlogic", "start_check_firmware")%>', status.start_check_firmware, function(x, status) {
|
||||
if ( x && x.status == 200 ) {
|
||||
if ( status.start_check_firmware != "\n" && status.start_check_firmware != "" ) {
|
||||
_check_firmware.innerHTML = '<font color="blue"> '+status.start_check_firmware+'</font>';
|
||||
}
|
||||
if ( status.start_check_firmware == "\n" || status.start_check_firmware == "" ) {
|
||||
_check_firmware.innerHTML = '';
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
var _check_plugin = document.getElementById('_check_plugin');
|
||||
XHR.poll(1, '<%=luci.dispatcher.build_url("admin", "system", "amlogic", "start_check_plugin")%>', status.start_check_plugin, function(x, status) {
|
||||
if ( x && x.status == 200 ) {
|
||||
if ( status.start_check_plugin != "\n" && status.start_check_plugin != "" ) {
|
||||
_check_plugin.innerHTML = '<font color="blue"> '+status.start_check_plugin+'</font>';
|
||||
}
|
||||
if ( status.start_check_plugin == "\n" || status.start_check_plugin == "" ) {
|
||||
_check_plugin.innerHTML = '';
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
var _check_kernel = document.getElementById('_check_kernel');
|
||||
XHR.poll(1, '<%=luci.dispatcher.build_url("admin", "system", "amlogic", "start_check_kernel")%>', status.start_check_kernel, function(x, status) {
|
||||
if ( x && x.status == 200 ) {
|
||||
if ( status.start_check_kernel != "\n" && status.start_check_kernel != "" ) {
|
||||
_check_kernel.innerHTML = '<font color="blue"> '+status.start_check_kernel+'</font>';
|
||||
}
|
||||
if ( status.start_check_kernel == "\n" || status.start_check_kernel == "" ) {
|
||||
_check_kernel.innerHTML = '';
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
var _current_firmware_version = document.getElementById('_current_firmware_version');
|
||||
var _current_plugin_version = document.getElementById('_current_plugin_version');
|
||||
var _current_kernel_version = document.getElementById('_current_kernel_version');
|
||||
var _openwrt_mainline_version = document.getElementById('_openwrt_mainline_version');
|
||||
XHR.get('<%=luci.dispatcher.build_url("admin", "system", "amlogic", "state")%>', null, function(x, status) {
|
||||
if ( x && x.status == 200 ) {
|
||||
_current_firmware_version.innerHTML = status.current_firmware_version ? "<font color=green><%:Current Version%> [ "+status.current_firmware_version+" ] </font>" : "<font color=red>"+"<%:Invalid value.%>"+"</font>";
|
||||
_current_plugin_version.innerHTML = status.current_plugin_version ? "<font color=green><%:Current Version%> [ "+status.current_plugin_version+" ] </font>" : "<font color=red>"+"<%:Invalid value.%>"+"</font>";
|
||||
_current_kernel_version.innerHTML = status.current_kernel_version ? "<font color=green><%:Current Version%> [ "+status.current_kernel_version+" ] </font>" : "<font color=red>"+"<%:Invalid value.%>"+"</font>";
|
||||
_openwrt_mainline_version.innerHTML = status.current_kernel_branch ? " [ "+status.current_kernel_branch+".y ] " : "[ "+"<%:Invalid value.%>"+" ]";
|
||||
}
|
||||
});
|
||||
|
||||
//]]></script>
|
||||
@@ -1,8 +0,0 @@
|
||||
<%+cbi/valueheader%>
|
||||
<span style="color: red">
|
||||
<%
|
||||
local val = self:cfgvalue(section) or self.default or ""
|
||||
write(pcdata(val))
|
||||
%>
|
||||
</span>
|
||||
<%+cbi/valuefooter%>
|
||||
@@ -1,70 +0,0 @@
|
||||
<style>
|
||||
.NewsTdHeight{ line-height:20px; }
|
||||
</style>
|
||||
<fieldset class="cbi-section">
|
||||
<table width="100%" class="NewsTdHeight">
|
||||
<tr><td colspan="2" width="100%" align="center">
|
||||
<img id="logo_amlogic" src="<%=resource%>/amlogic/logo.png" alt="Logo" width="135" />
|
||||
</td></tr>
|
||||
<tr><td colspan="2" width="100%" align="center">
|
||||
<img id="Packit" src="<%=resource%>/amlogic/packit.svg" alt="Packit" width="168px" height="20px" onclick="return homepage('packit')" />
|
||||
|
||||
<img id="Author" src="<%=resource%>/amlogic/author.svg" alt="Author" width="168px" height="20px" onclick="return author_homepage('author')" />
|
||||
|
||||
<img id="Plugin" src="<%=resource%>/amlogic/plugin.svg" alt="luci-app-amlogic" width="160px" height="20px" onclick="return homepage('plugin')" />
|
||||
</td></tr>
|
||||
<tr><td width="20%" align="right"><%:Supported functions:%></td><td width="80%" align="left" id="_clash">
|
||||
<%:Provide services such as install to EMMC, Update Firmware and Kernel, Backup and Recovery Config, Snapshot management, etc.%>
|
||||
</td></tr>
|
||||
<tr><td width="20%" align="right"><%:Supported Boxes:%></td><td width="80%" align="left" id="_clash">
|
||||
<%:Amlogic s922x --- [ Beelink, Beelink-Pro, Ugoos-AM6-Plus, ODROID-N2, Khadas-VIM3, Ali-CT2000 ]%><br>
|
||||
<%:Amlogic s905x3 -- [ X96-Max+, HK1-Box, H96-Max-X3, Ugoos-X3, TX3, X96-Air, A95XF3-Air ]%><br>
|
||||
<%:Amlogic s905x2 -- [ X96Max-4G, X96Max-2G, MECOOL-KM3-4G, Tanix-Tx5-Max, A95X-F2 ]%><br>
|
||||
<%:Amlogic s912 ---- [ H96-Pro-Plus, Octopus-Planet, A1, A2, Z6-Plus, TX92, X92, TX8-MAX, TX9-Pro ]%><br>
|
||||
<%:Amlogic s905x --- [ HG680P, B860H, TBee, T95, TX9, XiaoMI-3S, X96 ]%><br>
|
||||
<%:Amlogic s905w --- [ X96-Mini, TX3-Mini, W95, X96W/FunTV, MXQ-Pro-4K ]%><br>
|
||||
<%:Amlogic s905d --- [ Phicomm-N1, MECOOL-KI-Pro, SML-5442TW ]%><br>
|
||||
<%:Amlogic s905l --- [ UNT402A, M201-S ]%><br>
|
||||
<%:Amlogic s905l2 -- [ MGV2000, MGV3000, Wojia-TV-IPBS9505, M301A, E900v21E ]%><br>
|
||||
<%:Amlogic s905l3 -- [ CM211-1, CM311-1, HG680-LC, M401A, UNT400G1, UNT402A, ZXV10-BV310 ]%><br>
|
||||
<%:Amlogic s905l3a - [ E900V22C/D, CM311-1a-YST, M401A, M411A, UNT403A, UNT413A, IP112H ]%><br>
|
||||
<%:Amlogic s905l3b - [ CM211-1, CM311-1, E900V22D, E900V21E, E900V22E, M302A/M304A ]%><br>
|
||||
<%:Amlogic s905 ---- [ Beelink-Mini-MX-2G, Sunvell-T95M, MXQ-Pro+4K, SumaVision-Q5 ]%><br>
|
||||
<%:Allwinner H6 ---- [ V-Plus Cloud ]%><br>
|
||||
<%:Rockchip -------- [ BeikeYun, L1-Pro, FastRhino R66S/R68S, Radxa 5B/E25 ]%><br>
|
||||
<%:Used in KVM ----- [ Can be used in KVM virtual machine of Armbian system. ]%>
|
||||
</td></tr>
|
||||
</table>
|
||||
</fieldset>
|
||||
<script type="text/javascript">//<![CDATA[
|
||||
|
||||
function winOpen(url)
|
||||
{
|
||||
var winOpen = window.open(url);
|
||||
if( winOpen == null || typeof(winOpen) == 'undefined' ){ window.location.href=url; }
|
||||
}
|
||||
|
||||
function homepage(website)
|
||||
{
|
||||
var url = "";
|
||||
if( website == 'packit' ) { url = 'https://github.com/unifreq/openwrt_packit'; }
|
||||
if( website == 'plugin' ) { url = 'https://github.com/ophub/luci-app-amlogic'; }
|
||||
if ( url != "" ) { winOpen(url); }
|
||||
}
|
||||
|
||||
function author_homepage(website)
|
||||
{
|
||||
var openwrt_author_repo = "";
|
||||
XHR.get('<%=luci.dispatcher.build_url("admin", "system", "amlogic", "start_openwrt_author")%>', null, function(x, status) {
|
||||
if ( x && x.status == 200 ) {
|
||||
let openwrt_author_repo = status.openwrt_author;
|
||||
let trimstr_au = openwrt_author_repo.replace(/(^\s*)|(\s*$)/g, "");
|
||||
if (trimstr_au.indexOf("https") == -1) {
|
||||
trimstr_au = 'https://github.com/' + trimstr_au;
|
||||
}
|
||||
if ( trimstr_au != "" ) { winOpen(trimstr_au); }
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
//]]></script>
|
||||
@@ -1,128 +0,0 @@
|
||||
<style>
|
||||
.NewsTdHeight{ line-height:20px; }
|
||||
</style>
|
||||
<fieldset class="cbi-section">
|
||||
<table width="100%" class="NewsTdHeight">
|
||||
<tr><td width="35%" align="right"><%:Select the device model:%></td>
|
||||
<td width="65%" align="left">
|
||||
<select name="amlogic_soc" id="amlogic_soc" style="width:auto" onchange="sel_dtb_input(this.options[this.selectedIndex].value)">
|
||||
<option value="0"><%:Select List%></option>
|
||||
<option value="99"><%:Enter the dtb file name%></option>
|
||||
</select>
|
||||
</td>
|
||||
</tr>
|
||||
<tr id="tr_sel_dtb" style="display:none;"><td width="30%" align="right"><%:Enter the dtb file name:%></td><td width="70%" align="left"><input class="cbi-input-file" style="width: 235px" type="text" id="amlogic_dtb" name="amlogic_dtb" /></td></tr>
|
||||
<tr id="tr_sel_soc" style="display:none;"><td width="30%" align="right"><%:Enter the soc name:%></td><td width="70%" align="left"><input class="cbi-input-file" style="width: 235px" type="text" id="amlogic_socname" name="amlogic_socname" /></td></tr>
|
||||
<tr id="tr_sel_uboot" style="display:none;"><td width="30%" align="right"><%:Enter the uboot_overload name:%></td><td width="70%" align="left"><input class="cbi-input-file" style="width: 235px" type="text" id="amlogic_uboot" name="amlogic_uboot" /></td></tr>
|
||||
<tr>
|
||||
<td width="30%" align="right"><%:Install OpenWrt:%></td>
|
||||
<td width="70%" align="left">
|
||||
<input type="button" class="cbi-button cbi-button-reload" value="<%:Install%>" onclick="return amlogic_install(this, '<%=self:cfgvalue(section)%>')"/> <span id="_check_install"></span>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</fieldset>
|
||||
<script type="text/javascript">//<![CDATA[
|
||||
|
||||
// Show custom dtb input box
|
||||
function sel_dtb_input(soc_value){
|
||||
if ( soc_value == "99" )
|
||||
{
|
||||
document.getElementById('tr_sel_dtb').style = '';
|
||||
document.getElementById('tr_sel_soc').style = '';
|
||||
document.getElementById('tr_sel_uboot').style = '';
|
||||
}
|
||||
else
|
||||
{
|
||||
document.getElementById('tr_sel_dtb').style = 'display:none;';
|
||||
document.getElementById('tr_sel_soc').style = 'display:none;';
|
||||
document.getElementById('tr_sel_uboot').style = 'display:none;';
|
||||
}
|
||||
}
|
||||
|
||||
// Show the status of the install button
|
||||
function amlogic_install(btn,amlogic_install_sel)
|
||||
{
|
||||
var amlogic_soc_id = document.getElementById('amlogic_soc');
|
||||
var amlogic_soc_index = amlogic_soc_id.selectedIndex;
|
||||
var amlogic_soc_value = amlogic_soc_id.options[amlogic_soc_index].value;
|
||||
var amlogic_soc_text = amlogic_soc_id.options[amlogic_soc_index].text;
|
||||
|
||||
var amlogic_dtb_id = document.getElementById('amlogic_dtb');
|
||||
var amlogic_dtb_value = amlogic_dtb_id.value;
|
||||
|
||||
var amlogic_soc_name = document.getElementById('amlogic_socname');
|
||||
var amlogic_socname_value = amlogic_socname.value;
|
||||
|
||||
var amlogic_ubootname = document.getElementById('amlogic_uboot');
|
||||
var amlogic_ubootname_value = amlogic_ubootname.value;
|
||||
|
||||
var amlogic_confirm_info = amlogic_soc_text;
|
||||
if (confirm('<%:You have chosen:%>' + amlogic_confirm_info + ', <%:Start install?%>') != true) { return false; }
|
||||
|
||||
btn.disabled = true;
|
||||
btn.value = '<%:Installing...%> ';
|
||||
|
||||
XHR.get('<%=luci.dispatcher.build_url("admin", "system", "amlogic", "start_amlogic_install")%>',
|
||||
{
|
||||
amlogic_install_sel: amlogic_soc_value + '@' + amlogic_dtb_value + ':' + amlogic_socname_value + ':' + amlogic_ubootname_value
|
||||
},
|
||||
function(x,status)
|
||||
{
|
||||
if ( x && x.status == 200 ) {
|
||||
if(status.rule_install_status!="0")
|
||||
{
|
||||
btn.value = '<%:Install Failed%>';
|
||||
}
|
||||
else
|
||||
{
|
||||
btn.value = '<%:Successful Install%>';
|
||||
}
|
||||
}
|
||||
else {
|
||||
btn.value = '<%:Install%>';
|
||||
}
|
||||
}
|
||||
);
|
||||
btn.disabled = false;
|
||||
return false;
|
||||
}
|
||||
|
||||
// Read openwrt install log
|
||||
var start_check_install = document.getElementById('_check_install');
|
||||
XHR.poll(1, '<%=luci.dispatcher.build_url("admin", "system", "amlogic", "start_check_install")%>', status.start_check_install, function(x, status) {
|
||||
if ( x && x.status == 200 ) {
|
||||
if ( status.start_check_install != "\n" && status.start_check_install != "" ) {
|
||||
start_check_install.innerHTML = '<font color="blue"> '+status.start_check_install+'</font>';
|
||||
}
|
||||
if ( status.start_check_install == "\n" || status.start_check_install == "" ) {
|
||||
start_check_install.innerHTML = '';
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Show external modal database: /etc/model_database.txt
|
||||
var mymodel_arrlen = 0;
|
||||
var _option_codes = '<option value="0"><%:Select List%></option>';
|
||||
var _check_my_amlogic_soc = document.getElementById('amlogic_soc');
|
||||
XHR.get('<%=luci.dispatcher.build_url("admin", "system", "amlogic", "start_model_database")%>', null, function(x, status) {
|
||||
if ( x && x.status == 200 ) {
|
||||
if ( status.my_model_database != "" ) {
|
||||
let userModelList = status.my_model_database;
|
||||
var user_arry = userModelList.split('@@@');
|
||||
var mymodel_arrlen = user_arry.length - 1;
|
||||
for ( j = 0; j < mymodel_arrlen; j++ ) {
|
||||
my_box_mode = user_arry[j].split('###');
|
||||
my_box_mode_id = my_box_mode[0];
|
||||
my_box_mode_name = my_box_mode[1];
|
||||
if ( my_box_mode_id != "" && my_box_mode_name != "" ) {
|
||||
_option_codes = _option_codes + '<option value="' + my_box_mode_id + '">[ ' + my_box_mode_id + ' ] ' + my_box_mode_name.replace(/~/g, " ") + '</option>';
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
_option_codes = _option_codes + '<option value="99"><%:Enter the dtb file name%></option>';
|
||||
_check_my_amlogic_soc.innerHTML = _option_codes;
|
||||
});
|
||||
|
||||
//]]></script>
|
||||
@@ -1,48 +0,0 @@
|
||||
<style>
|
||||
.NewsTdHeight{ line-height:32px; }
|
||||
.imgLeft{ float:left; margin-right:10px; vertical-align:middle; }
|
||||
.contentRight{ align-items: center; }
|
||||
</style>
|
||||
<fieldset class="cbi-section">
|
||||
<table width="100%" class="NewsTdHeight">
|
||||
<tr><td width="100%" align="left">
|
||||
<input class="cbi-button cbi-button-save" type="button" value="<%:Switch System%>" onclick="switchopenwrt(this)" />
|
||||
<p style="display:none">
|
||||
<img id="img_loading" style="display:block" src="<%=resource%>/amlogic/loading.gif" alt="<%:Loading%>" class="imgLeft" />
|
||||
<img id="img_switch" style="display:none" src="<%=resource%>/amlogic/switch.png" alt="<%:PowerOff%>" class="imgLeft" />
|
||||
<span id="msg_switch" class="contentRight"><%:System is switching...%></span>
|
||||
</p>
|
||||
</td></tr>
|
||||
</table>
|
||||
</fieldset>
|
||||
|
||||
<script type="text/javascript">//<![CDATA[
|
||||
|
||||
function status_check() {
|
||||
var time = 5;
|
||||
var img_loading = document.getElementById("img_loading");
|
||||
var img_switch = document.getElementById("img_switch");
|
||||
var msg = document.getElementById("msg_switch");
|
||||
var set = setInterval(function() {
|
||||
time--;
|
||||
msg.innerHTML = "<%:Waiting for system switching...%>";
|
||||
if(time === 0) {
|
||||
img_loading.style.display = 'none';
|
||||
img_switch.style.display = 'block';
|
||||
msg.innerHTML = "<%:System switchover succeeded, restarting...%>";
|
||||
clearInterval(set);
|
||||
}
|
||||
}, 1000);
|
||||
}
|
||||
|
||||
function switchopenwrt(btn) {
|
||||
if (confirm('<%:Are you sure you want to switch systems?%>') != true) { return false; }
|
||||
|
||||
btn.style.display = 'none';
|
||||
document.getElementById('msg_switch').parentNode.style.display = 'block';
|
||||
|
||||
(new XHR()).post('<%=luci.dispatcher.build_url("admin", "system", "amlogic", "start_switch")%>', { token: '<%=token%>' }, status_check);
|
||||
}
|
||||
|
||||
|
||||
//]]></script>
|
||||
@@ -1,117 +0,0 @@
|
||||
<%+cbi/valueheader%>
|
||||
<textarea id="amlogic.ophub.clog" class="cbi-input-textarea" style="width: 100%;display:inline" data-update="change" rows="8" cols="60" readonly="readonly" > </textarea>
|
||||
<fieldset class="cbi-section">
|
||||
<table width="100%">
|
||||
<tr>
|
||||
<td width="25%" align="center"><input type="button" class="cbi-button cbi-button-apply" id="stop_refresh_button" value="<%:Stop Refresh Log%>" onclick=" return stop_refresh() "/></td>
|
||||
<td width="25%" align="center"><input type="button" class="cbi-button cbi-button-apply" id="start_refresh_button" value="<%:Start Refresh Log%>" onclick=" return start_refresh() "/></td>
|
||||
<td width="25%" align="center"><input type="button" class="cbi-button cbi-button-apply" id="del_log_button" value="<%:Clean Log%>" style=" display:inline;" onclick=" return del_log() " /></td>
|
||||
<td width="25%" align="center"><input type="button" class="cbi-button cbi-button-apply" id="down_log_button" value="<%:Download Log%>" style=" display:inline;" onclick=" return download_log() " /></td>
|
||||
</tr>
|
||||
</table>
|
||||
</fieldset>
|
||||
|
||||
<script type="text/javascript">//<![CDATA[
|
||||
var r
|
||||
|
||||
function stop_refresh() {
|
||||
clearTimeout(r);
|
||||
return
|
||||
}
|
||||
|
||||
function start_refresh() {
|
||||
clearTimeout(r);
|
||||
r=setTimeout("poll_log()",1000*2);
|
||||
return
|
||||
}
|
||||
|
||||
function createAndDownloadFile(fileName, content) {
|
||||
var aTag = document.createElement('a');
|
||||
var blob = new Blob([content]);
|
||||
aTag.download = fileName;
|
||||
aTag.href = URL.createObjectURL(blob);
|
||||
aTag.click();
|
||||
URL.revokeObjectURL(blob);
|
||||
}
|
||||
|
||||
function download_log(){
|
||||
var lv = document.getElementById('amlogic.ophub.clog');
|
||||
var dt = new Date();
|
||||
var timestamp = dt.getFullYear()+"-"+(dt.getMonth()+1)+"-"+dt.getDate()+"-"+dt.getHours()+"-"+dt.getMinutes()+"-"+dt.getSeconds();
|
||||
createAndDownloadFile("Amlogic-"+timestamp+".log",lv.innerHTML)
|
||||
return
|
||||
}
|
||||
|
||||
function del_log() {
|
||||
XHR.get('<%=luci.dispatcher.build_url("admin", "system", "amlogic", "del_log")%>',null,function(x, data){
|
||||
var lv = document.getElementById('amlogic.ophub.clog');
|
||||
lv.innerHTML="";
|
||||
}
|
||||
);
|
||||
return
|
||||
}
|
||||
|
||||
function p(s) {
|
||||
return s < 10 ? '0' + s: s;
|
||||
}
|
||||
|
||||
function line_tolocal(str){
|
||||
var strt=new Array();
|
||||
str.trim().split('\n').forEach(function(v, i) {
|
||||
var dt = new Date(v.substring(6,26));
|
||||
if (dt != "Invalid Date"){
|
||||
strt[i]=dt.getFullYear()+"-"+p(dt.getMonth()+1)+"-"+p(dt.getDate())+" "+p(dt.getHours())+":"+p(dt.getMinutes())+":"+p(dt.getSeconds())+v.substring(27);
|
||||
}else{
|
||||
strt[i]=v;}})
|
||||
var old_log_line = sessionStorage.log_line;
|
||||
if ( old_log_line != null && strt.length != null ) {
|
||||
if (old_log_line - strt.length < 0) {
|
||||
sessionStorage.log_line = strt.length;
|
||||
return strt.slice('-'+(strt.length-old_log_line))
|
||||
}
|
||||
else if (old_log_line == strt.length) {
|
||||
sessionStorage.log_line = strt.length;
|
||||
return
|
||||
}
|
||||
else if (old_log_line - strt.length > 0) {
|
||||
sessionStorage.log_line = strt.length;
|
||||
var lv = document.getElementById('amlogic.ophub.clog');
|
||||
lv.innerHTML = "";
|
||||
return strt
|
||||
}
|
||||
}
|
||||
else if ( strt.length != null ) {
|
||||
sessionStorage.log_line = strt.length;
|
||||
return strt
|
||||
}
|
||||
else {
|
||||
sessionStorage.log_line = "0";
|
||||
return strt
|
||||
}
|
||||
}
|
||||
|
||||
function poll_log(){
|
||||
XHR.get('<%=luci.dispatcher.build_url("admin", "system", "amlogic", "refresh_log")%>', null,
|
||||
function(x, data) {
|
||||
if ( x && x.status == 200 ) {
|
||||
var lv = document.getElementById('amlogic.ophub.clog');
|
||||
if (x.responseText && lv) {
|
||||
var lines=line_tolocal(x.responseText);
|
||||
if (lines != null) {
|
||||
lv.innerHTML = lines.reverse().join('\n')+'\n'+lv.innerHTML;
|
||||
//lv.innerHTML = x.responseText.split('\n').reverse().join('\n')+lv.innerHTML;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
);
|
||||
r=setTimeout("poll_log()",1000*2);
|
||||
}
|
||||
|
||||
sessionStorage.removeItem("log_line");
|
||||
poll_log();
|
||||
|
||||
//]]>
|
||||
</script>
|
||||
|
||||
<%+cbi/valuefooter%>
|
||||
@@ -1,48 +0,0 @@
|
||||
<style>
|
||||
.NewsTdHeight{ line-height:32px; }
|
||||
.imgLeft{ float:left; margin-right:10px; vertical-align:middle; }
|
||||
.contentRight{ align-items: center; }
|
||||
</style>
|
||||
<fieldset class="cbi-section">
|
||||
<table width="100%" class="NewsTdHeight">
|
||||
<tr><td width="100%" align="left">
|
||||
<input class="cbi-button cbi-button-remove" type="button" value="<%:Perform PowerOff%>" onclick="poweroff(this)" />
|
||||
<p style="display:none">
|
||||
<img id="img_loading" style="display:block" src="<%=resource%>/amlogic/loading.gif" alt="<%:Loading%>" class="imgLeft" />
|
||||
<img id="img_poweroff" style="display:none" src="<%=resource%>/amlogic/poweroff.png" alt="<%:PowerOff%>" class="imgLeft" />
|
||||
<span id="msg_poweroff" class="contentRight"><%:Device is shutting down...%></span>
|
||||
</p>
|
||||
</td></tr>
|
||||
</table>
|
||||
</fieldset>
|
||||
|
||||
<script type="text/javascript">//<![CDATA[
|
||||
|
||||
function status_check() {
|
||||
var time = 5;
|
||||
var img_loading = document.getElementById("img_loading");
|
||||
var img_poweroff = document.getElementById("img_poweroff");
|
||||
var msg = document.getElementById("msg_poweroff");
|
||||
var set = setInterval(function() {
|
||||
time--;
|
||||
msg.innerHTML = "<%:Waiting for the device to shut down...%>";
|
||||
if(time === 0) {
|
||||
img_loading.style.display = 'none';
|
||||
img_poweroff.style.display = 'block';
|
||||
msg.innerHTML = "<%:The device has been turned off%>";
|
||||
clearInterval(set);
|
||||
}
|
||||
}, 1000);
|
||||
}
|
||||
|
||||
function poweroff(btn) {
|
||||
if (confirm('<%:Are you sure you want to shut down?%>') != true) { return false; }
|
||||
|
||||
btn.style.display = 'none';
|
||||
document.getElementById('msg_poweroff').parentNode.style.display = 'block';
|
||||
|
||||
(new XHR()).post('<%=luci.dispatcher.build_url("admin", "system", "amlogic", "start_poweroff")%>', { token: '<%=token%>' }, status_check);
|
||||
}
|
||||
|
||||
|
||||
//]]></script>
|
||||
@@ -1,46 +0,0 @@
|
||||
<style>
|
||||
.NewsTdHeight{ line-height: 20px; }
|
||||
</style>
|
||||
<fieldset class="cbi-section">
|
||||
<table width="100%" class="NewsTdHeight">
|
||||
<tr><td width="35%" align="right"><input type="button" class="cbi-button cbi-button-reload" value="<%:Rescue the original system kernel%>" onclick="return b_rescue_kernel(this)"/></td><td width="65%" align="left"><span id="_current_rescue_version"><%:Collecting data...%></span> <span id="_check_rescue"></span></td></tr>
|
||||
</table>
|
||||
</fieldset>
|
||||
<script type="text/javascript">//<![CDATA[
|
||||
function b_rescue_kernel(btn)
|
||||
{
|
||||
btn.disabled = true;
|
||||
btn.value = '<%:Rescuing...%>';
|
||||
XHR.get('<%=luci.dispatcher.build_url("admin", "system", "amlogic", "start_amlogic_rescue")%>', null, function(x, status) {
|
||||
if ( x && x.status == 200 ) {
|
||||
btn.disabled = false;
|
||||
btn.value = '<%:Rescue the original system kernel%>';
|
||||
}
|
||||
else {
|
||||
btn.disabled = false;
|
||||
btn.value = '<%:Rescue the original system kernel%>';
|
||||
}
|
||||
return false;
|
||||
});
|
||||
}
|
||||
|
||||
var _check_rescue = document.getElementById('_check_rescue');
|
||||
XHR.poll(1, '<%=luci.dispatcher.build_url("admin", "system", "amlogic", "start_check_rescue")%>', status.start_check_rescue, function(x, status) {
|
||||
if ( x && x.status == 200 ) {
|
||||
if ( status.start_check_rescue != "\n" && status.start_check_rescue != "" ) {
|
||||
_check_rescue.innerHTML = '<font color="blue"> '+status.start_check_rescue+'</font>';
|
||||
}
|
||||
if ( status.start_check_rescue == "\n" || status.start_check_rescue == "" ) {
|
||||
_check_rescue.innerHTML = '';
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
var _current_rescue_version = document.getElementById('_current_rescue_version');
|
||||
XHR.get('<%=luci.dispatcher.build_url("admin", "system", "amlogic", "state")%>', null, function(x, status) {
|
||||
if ( x && x.status == 200 ) {
|
||||
_current_rescue_version.innerHTML = status.current_kernel_version ? "<font color=green><%:Current Version%> [ "+status.current_kernel_version+" ] </font>" : "<font color=red>"+"<%:Invalid value.%>"+"</font>";
|
||||
}
|
||||
});
|
||||
|
||||
//]]></script>
|
||||
@@ -1,152 +0,0 @@
|
||||
<style>
|
||||
.NewsTdHeight{ line-height:20px; }
|
||||
.SnapshotsTitHeight{ line-height:40px; text-align: center; }
|
||||
.list{
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
justify-content: space-between;
|
||||
text-align: center;
|
||||
position: relative;
|
||||
width: 100%;
|
||||
background-color: #FFF;
|
||||
}
|
||||
.item{
|
||||
border: 1px solid #CACACA;
|
||||
color: #888;
|
||||
font-size: 14px;
|
||||
width: 150px;
|
||||
height: 130px;
|
||||
margin:8px;
|
||||
text-align: center;
|
||||
align-items:center;
|
||||
}
|
||||
.item:hover{background:#e5f3f6;}
|
||||
.item:not(:nth-child(3n)){
|
||||
content: "";
|
||||
width: 150px;
|
||||
}
|
||||
.list:after {
|
||||
content: "";
|
||||
width: 150px;
|
||||
}
|
||||
</style>
|
||||
|
||||
<div class="list" id="_check_snapshot">
|
||||
<%:Collecting data...%>
|
||||
</div>
|
||||
|
||||
<script type="text/javascript">//<![CDATA[
|
||||
|
||||
// Delete snapshot function
|
||||
function delete_snapshot(btn,snapshot_delete_sel)
|
||||
{
|
||||
|
||||
if (confirm('<%:You selected a snapshot:%> [ ' + snapshot_delete_sel + ' ] , <%:Confirm delete?%>') != true) { return false; }
|
||||
|
||||
btn.disabled = true;
|
||||
btn.value = '<%:Deleting...%> ';
|
||||
|
||||
XHR.get('<%=luci.dispatcher.build_url("admin", "system", "amlogic", "start_snapshot_delete")%>',
|
||||
{
|
||||
snapshot_delete_sel: snapshot_delete_sel
|
||||
},
|
||||
function(x,status)
|
||||
{
|
||||
if ( x && x.status == 200 ) {
|
||||
if(status.rule_delete_status!="0")
|
||||
{
|
||||
btn.value = '<%:Delete Failed%>';
|
||||
}
|
||||
else
|
||||
{
|
||||
btn.value = '<%:Successfully Deleted%>';
|
||||
}
|
||||
}
|
||||
else {
|
||||
btn.value = '<%:Delete Snapshot%>';
|
||||
}
|
||||
}
|
||||
);
|
||||
var sdiv = 'snapshots_div_' + snapshot_delete_sel;
|
||||
document.getElementById(sdiv).style.display = 'none';
|
||||
btn.disabled = false;
|
||||
return false;
|
||||
|
||||
}
|
||||
|
||||
// Restore snapshot function
|
||||
function restore_snapshot(btn,snapshot_restore_sel)
|
||||
{
|
||||
|
||||
if (confirm('<%:You selected a snapshot:%> [ ' + snapshot_restore_sel + ' ] , <%:Confirm recovery and restart OpenWrt?%>') != true) { return false; }
|
||||
|
||||
btn.disabled = true;
|
||||
btn.value = '<%:Restoring...%> ';
|
||||
|
||||
XHR.get('<%=luci.dispatcher.build_url("admin", "system", "amlogic", "start_snapshot_restore")%>',
|
||||
{
|
||||
snapshot_restore_sel: snapshot_restore_sel
|
||||
},
|
||||
function(x,status)
|
||||
{
|
||||
if ( x && x.status == 200 ) {
|
||||
if(status.rule_restore_status!="0")
|
||||
{
|
||||
btn.value = '<%:Restore Failed%>';
|
||||
}
|
||||
else
|
||||
{
|
||||
btn.value = '<%:Successfully Restored%>';
|
||||
}
|
||||
}
|
||||
else {
|
||||
btn.value = '<%:Restore Snapshot%>';
|
||||
}
|
||||
}
|
||||
);
|
||||
btn.disabled = false;
|
||||
return false;
|
||||
}
|
||||
|
||||
// Show current snapshot list
|
||||
mycars_arrlen = 0;
|
||||
_no_snapshots = "<%:Currently OpenWrt does not support the snapshot function.%>";
|
||||
_no_snapshots = _no_snapshots + "<%:Please use this plugin to reinstall or upgrade OpenWrt to enable the snapshot function.%>";
|
||||
var _check_snapshot = document.getElementById('_check_snapshot');
|
||||
XHR.get('<%=luci.dispatcher.build_url("admin", "system", "amlogic", "start_snapshot_list")%>', null, function(x, status) {
|
||||
if ( x && x.status == 200 ) {
|
||||
|
||||
let userArer = status.current_snapshot;
|
||||
let user_arer_clear = userArer.replace(/\.snapshots\//g, "");
|
||||
let trimstr = user_arer_clear.replace(/(^\s*)|(\s*$)/g, "");
|
||||
let user_arer = trimstr.replace(/\s+/g, ",");
|
||||
var mycars = user_arer.split(',');
|
||||
var mycars_arrlen = mycars.length;
|
||||
|
||||
_div_codes = ""
|
||||
_snapshots_tit = ""
|
||||
for ( j = 0; j < mycars_arrlen; j++ ) {
|
||||
|
||||
if ( mycars[j] == "etc-000" ) {
|
||||
_snapshots_tit = "<%:Initialize Snapshot%>";
|
||||
} else if ( mycars[j] == "etc-001" ) {
|
||||
_snapshots_tit = "<%:Update Snapshot%>";
|
||||
} else {
|
||||
_snapshots_tit = mycars[j];
|
||||
}
|
||||
|
||||
_div_codes = _div_codes + '<div class="item" id="snapshots_div_' + mycars[j] + '">';
|
||||
_div_codes = _div_codes + '<div class="SnapshotsTitHeight">' + _snapshots_tit + '</div>'
|
||||
_div_codes = _div_codes + '<div class="SnapshotsTitHeight"><input type="button" class="cbi-button cbi-button-apply" value="<%:Restore Snap%>" onclick="return restore_snapshot(this, \'' + mycars[j] + '\')"/></div>'
|
||||
if ( mycars[j] != "etc-000" && mycars[j] != "etc-001" ) { _div_codes = _div_codes + '<div class="SnapshotsTitHeight"><input type="button" class="cbi-button cbi-button-remove" value="<%:Delete Snap%>" onclick="return delete_snapshot(this, \'' + mycars[j] + '\')"/></div>' }
|
||||
_div_codes = _div_codes + '</div>';
|
||||
|
||||
}
|
||||
_check_snapshot.innerHTML = _div_codes ? _div_codes : "<font color=red>"+"<%:Invalid value.%>"+"</font>";
|
||||
} else {
|
||||
_check_snapshot.innerHTML = "<font color=red>"+ _no_snapshots +"</font>";
|
||||
}
|
||||
});
|
||||
|
||||
//]]></script>
|
||||
|
||||
@@ -1,130 +0,0 @@
|
||||
<style>
|
||||
.NewsTdHeight{ line-height:20px; }
|
||||
</style>
|
||||
|
||||
<fieldset class="cbi-section">
|
||||
<table width="100%" class="NewsTdHeight">
|
||||
<tr><td width="100%" colspan="2">
|
||||
<p align="center">
|
||||
<%:After uploading firmware (.img/.img.gz/.img.xz/.7z suffix) or kernel files (3 kernel files), the update button will be displayed.%>
|
||||
</p>
|
||||
</td></tr>
|
||||
<tr><td width="30%" align="right">
|
||||
<input id="_have_firmware" type="button" class="cbi-button cbi-button-reload" value="<%:Update OpenWrt firmware%>" onclick="return amlogic_update(this, 'auto@updated@/tmp')"/>
|
||||
<input id="_have_kernel" type="button" class="cbi-button cbi-button-reload" value="<%:Replace OpenWrt Kernel%>" onclick="return amlogic_kernel(this)"/>
|
||||
</td><td width="70%" align="left">
|
||||
<span id="_current_firmware_version"><%:Collecting data...%></span>
|
||||
<span id="_check_log_firmware"></span><span id="_check_log_kernel"></span>
|
||||
</td></tr>
|
||||
</table>
|
||||
</fieldset>
|
||||
|
||||
<script type="text/javascript">//<![CDATA[
|
||||
|
||||
function amlogic_update(btn,amlogic_update_sel)
|
||||
{
|
||||
btn.disabled = true;
|
||||
btn.value = '<%:Updating...%> ';
|
||||
|
||||
XHR.get('<%=luci.dispatcher.build_url("admin", "system", "amlogic", "start_amlogic_update")%>',
|
||||
{
|
||||
amlogic_update_sel: amlogic_update_sel
|
||||
},
|
||||
function(x,status)
|
||||
{
|
||||
if ( x && x.status == 200 ) {
|
||||
if(status.rule_update_status!="0")
|
||||
{
|
||||
btn.value = '<%:Update Failed%>';
|
||||
}
|
||||
else
|
||||
{
|
||||
btn.value = '<%:Successful Update%>';
|
||||
}
|
||||
}
|
||||
else {
|
||||
btn.value = '<%:Update OpenWrt firmware%>';
|
||||
}
|
||||
}
|
||||
);
|
||||
btn.disabled = false;
|
||||
return false;
|
||||
}
|
||||
|
||||
function amlogic_kernel(btn)
|
||||
{
|
||||
btn.disabled = true;
|
||||
btn.value = '<%:Updating...%> ';
|
||||
|
||||
XHR.get('<%=luci.dispatcher.build_url("admin", "system", "amlogic", "start_amlogic_kernel")%>', null, function(x, status) {
|
||||
if ( x && x.status == 200 ) {
|
||||
if(status.rule_kernel_status!="0")
|
||||
{
|
||||
btn.value = '<%:Update Failed%>';
|
||||
}
|
||||
else
|
||||
{
|
||||
btn.value = '<%:Successful Update%>';
|
||||
}
|
||||
}
|
||||
else {
|
||||
btn.value = '<%:Replace OpenWrt Kernel%>';
|
||||
}
|
||||
}
|
||||
);
|
||||
btn.disabled = false;
|
||||
return false;
|
||||
}
|
||||
|
||||
var _have_firmware = document.getElementById('_have_firmware');
|
||||
var _have_kernel = document.getElementById('_have_kernel');
|
||||
XHR.poll(1, '<%=luci.dispatcher.build_url("admin", "system", "amlogic", "start_check_upfiles")%>', status.start_check_upfiles, function(x, status) {
|
||||
if ( x && x.status == 200 ) {
|
||||
if ( status.start_check_upfiles == "firmware\n" || status.start_check_upfiles == "firmware" ) {
|
||||
_have_firmware.style.display = 'block';
|
||||
} else {
|
||||
_have_firmware.style.display = 'none';
|
||||
}
|
||||
|
||||
if ( status.start_check_upfiles == "kernel\n" || status.start_check_upfiles == "kernel" ) {
|
||||
_have_kernel.style.display = 'block';
|
||||
} else {
|
||||
_have_kernel.style.display = 'none';
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
var _check_log_firmware = document.getElementById('_check_log_firmware');
|
||||
XHR.poll(1, '<%=luci.dispatcher.build_url("admin", "system", "amlogic", "start_check_firmware")%>', status.start_check_firmware, function(x, status) {
|
||||
if ( x && x.status == 200 ) {
|
||||
if ( status.start_check_firmware != "\n" && status.start_check_firmware != "" ) {
|
||||
_check_log_firmware.innerHTML = '<font color="blue"> '+status.start_check_firmware+'</font>';
|
||||
}
|
||||
if ( status.start_check_firmware == "\n" || status.start_check_firmware == "" ) {
|
||||
_check_log_firmware.innerHTML = '';
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
var _check_log_kernel = document.getElementById('_check_log_kernel');
|
||||
XHR.poll(1, '<%=luci.dispatcher.build_url("admin", "system", "amlogic", "start_check_kernel")%>', status.start_check_kernel, function(x, status) {
|
||||
if ( x && x.status == 200 ) {
|
||||
if ( status.start_check_kernel != "\n" && status.start_check_kernel != "" ) {
|
||||
_check_log_kernel.innerHTML = '<font color="blue"> '+status.start_check_kernel+'</font>';
|
||||
}
|
||||
if ( status.start_check_kernel == "\n" || status.start_check_kernel == "" ) {
|
||||
_check_log_kernel.innerHTML = '';
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
var _current_firmware_version = document.getElementById('_current_firmware_version');
|
||||
//var _current_kernel_version = document.getElementById('_current_kernel_version');
|
||||
XHR.get('<%=luci.dispatcher.build_url("admin", "system", "amlogic", "state")%>', null, function(x, status) {
|
||||
if ( x && x.status == 200 ) {
|
||||
_current_firmware_version.innerHTML = status.current_firmware_version ? "<font color=green><%:Current Version%> [ "+status.current_firmware_version+" ] </font>" : "<font color=red>"+"<%:Invalid value.%>"+"</font>";
|
||||
//_current_kernel_version.innerHTML = status.current_kernel_version ? "<font color=green><%:Current Version%> [ "+status.current_kernel_version+" ] </font>" : "<font color=red>"+"<%:Invalid value.%>"+"</font>";
|
||||
}
|
||||
});
|
||||
|
||||
//]]></script>
|
||||
@@ -1,5 +0,0 @@
|
||||
<%+cbi/valueheader%>
|
||||
<label class="cbi-value" style="display:inline-block; width: 150px" for="ulfile"><%:Choose local file:%></label>
|
||||
<input class="cbi-input-file" style="width: 400px" type="file" id="ulfile" name="ulfile" />
|
||||
<input type="submit" class="cbi-button cbi-input-apply" name="upload" value="<%:Upload%>" />
|
||||
<%+cbi/valuefooter%>
|
||||
@@ -425,14 +425,14 @@ msgstr "Укажите ветку версии для файлов OpenWrt и я
|
||||
msgid "Set plugin branch:"
|
||||
msgstr "Ветка плагина:"
|
||||
|
||||
msgid "Set the branch of the luci-app-amlogic plugin used in [Only update Amlogic Service]. Default (empty) uses the main (Lua) branch."
|
||||
msgstr "Укажите ветку плагина luci-app-amlogic для раздела [Только обновить сервис Amlogic]. По умолчанию (пусто) используется ветка main (Lua)."
|
||||
msgid "Set the branch of the luci-app-amlogic plugin used in [Only update Amlogic Service]. Default (empty) uses the main (JavaScript) branch."
|
||||
msgstr "Укажите ветку плагина luci-app-amlogic для раздела [Только обновить сервис Amlogic]. По умолчанию (пусто) используется ветка main (JavaScript)."
|
||||
|
||||
msgid "main [Lua version]"
|
||||
msgstr "main [Lua версия]"
|
||||
msgid "main [JavaScript version]"
|
||||
msgstr "main [JavaScript версия]"
|
||||
|
||||
msgid "js [JavaScript version]"
|
||||
msgstr "js [JavaScript версия]"
|
||||
msgid "lua [Lua version]"
|
||||
msgstr "lua [Lua версия]"
|
||||
|
||||
msgid "Keep config update:"
|
||||
msgstr "Сохранять конфигурацию при обновлении:"
|
||||
@@ -598,3 +598,60 @@ msgstr "Ожидание завершения выключения..."
|
||||
|
||||
msgid "The device has been turned off"
|
||||
msgstr "Устройство выключено"
|
||||
|
||||
msgid "Are you sure you want to power off the device?"
|
||||
msgstr "Вы уверены, что хотите выключить устройство?"
|
||||
|
||||
msgid "Back"
|
||||
msgstr "Назад"
|
||||
|
||||
msgid "Click the button below to safely power off the device."
|
||||
msgstr "Нажмите кнопку ниже, чтобы безопасно выключить устройство."
|
||||
|
||||
msgid "Display the log of plug-in service operation."
|
||||
msgstr "Показать журнал работы службы плагина."
|
||||
|
||||
msgid "Edit list of backup files"
|
||||
msgstr "Изменить список файлов резервной копии"
|
||||
|
||||
msgid "Enter the dtb file name:"
|
||||
msgstr "Введите имя файла dtb:"
|
||||
|
||||
msgid "In this list, you can manually add the file or directory list of files that need to be backed up. Please confirm that the format and content of the file are correct."
|
||||
msgstr "В этом списке можно вручную добавить файлы или каталоги, которые нужно резервировать. Пожалуйста, проверьте корректность формата и содержимого."
|
||||
|
||||
msgid "Please refresh the page to see the changes."
|
||||
msgstr "Пожалуйста, обновите страницу, чтобы увидеть изменения."
|
||||
|
||||
msgid "Power Off"
|
||||
msgstr "Выключить"
|
||||
|
||||
msgid "Powered off."
|
||||
msgstr "Выключено."
|
||||
|
||||
msgid "Powering off, please wait..."
|
||||
msgstr "Выключение, пожалуйста, подождите..."
|
||||
|
||||
msgid "Running"
|
||||
msgstr "Работает"
|
||||
|
||||
msgid "Save Failed"
|
||||
msgstr "Ошибка сохранения"
|
||||
|
||||
msgid "Service Log"
|
||||
msgstr "Журнал службы"
|
||||
|
||||
msgid "Start Refresh"
|
||||
msgstr "Начать обновление"
|
||||
|
||||
msgid "Stop Refresh"
|
||||
msgstr "Остановить обновление"
|
||||
|
||||
msgid "Stopped"
|
||||
msgstr "Остановлено"
|
||||
|
||||
msgid "Successfully saved."
|
||||
msgstr "Сохранено успешно."
|
||||
|
||||
msgid "Upload file..."
|
||||
msgstr "Загрузить файл..."
|
||||
Symlink
+1
@@ -0,0 +1 @@
|
||||
zh_Hans
|
||||
@@ -1 +0,0 @@
|
||||
zh-cn
|
||||
@@ -425,14 +425,14 @@ msgstr "设置 [在线下载更新] 时 OpenWrt 文件与内核所选用的版
|
||||
msgid "Set plugin branch:"
|
||||
msgstr "设置插件分支:"
|
||||
|
||||
msgid "Set the branch of the luci-app-amlogic plugin used in [Only update Amlogic Service]. Default (empty) uses the main (Lua) branch."
|
||||
msgstr "设置 [仅更新晶晨宝盒] 时使用的插件分支。默认(空)使用 main (Lua) 分支。"
|
||||
msgid "Set the branch of the luci-app-amlogic plugin used in [Only update Amlogic Service]. Default (empty) uses the main (JavaScript) branch."
|
||||
msgstr "设置 [仅更新晶晨宝盒] 时使用的插件分支。默认(空)使用 main (JavaScript) 分支。"
|
||||
|
||||
msgid "main [Lua version]"
|
||||
msgstr "main [Lua 版本]"
|
||||
msgid "main [JavaScript version]"
|
||||
msgstr "main [JavaScript 版本]"
|
||||
|
||||
msgid "js [JavaScript version]"
|
||||
msgstr "js [JavaScript 版本]"
|
||||
msgid "lua [Lua version]"
|
||||
msgstr "lua [Lua 版本]"
|
||||
|
||||
msgid "Keep config update:"
|
||||
msgstr "保留配置更新:"
|
||||
@@ -509,9 +509,6 @@ msgstr "首先更新插件,再更新内核或固件。更多选项可以在插
|
||||
msgid "Collecting data..."
|
||||
msgstr "正在收集数据…"
|
||||
|
||||
msgid "Check All Components Update"
|
||||
msgstr "插件和内核检查更新"
|
||||
|
||||
msgid "Server Logs"
|
||||
msgstr "操作日志"
|
||||
|
||||
@@ -603,3 +600,60 @@ msgid "The device has been turned off"
|
||||
msgstr "设备已经关机"
|
||||
|
||||
|
||||
|
||||
msgid "Are you sure you want to power off the device?"
|
||||
msgstr "确定要关闭设备吗?"
|
||||
|
||||
msgid "Back"
|
||||
msgstr "返回"
|
||||
|
||||
msgid "Click the button below to safely power off the device."
|
||||
msgstr "点击下方的按钮可以安全地关闭设备。"
|
||||
|
||||
msgid "Display the log of plug-in service operation."
|
||||
msgstr "显示插件服务运行日志。"
|
||||
|
||||
msgid "Edit list of backup files"
|
||||
msgstr "编辑备份文件列表"
|
||||
|
||||
msgid "Enter the dtb file name:"
|
||||
msgstr "输入 dtb 文件名:"
|
||||
|
||||
msgid "In this list, you can manually add the file or directory list of files that need to be backed up. Please confirm that the format and content of the file are correct."
|
||||
msgstr "在此列表中,您可以手动添加需要备份的文件或目录。请确认文件的格式和内容正确。"
|
||||
|
||||
msgid "Please refresh the page to see the changes."
|
||||
msgstr "请刷新页面以查看更改。"
|
||||
|
||||
msgid "Power Off"
|
||||
msgstr "关闭设备"
|
||||
|
||||
msgid "Powered off."
|
||||
msgstr "已关机。"
|
||||
|
||||
msgid "Powering off, please wait..."
|
||||
msgstr "正在关机,请稍候……"
|
||||
|
||||
msgid "Running"
|
||||
msgstr "运行中"
|
||||
|
||||
msgid "Save Failed"
|
||||
msgstr "保存失败"
|
||||
|
||||
msgid "Service Log"
|
||||
msgstr "服务日志"
|
||||
|
||||
msgid "Start Refresh"
|
||||
msgstr "开始刷新"
|
||||
|
||||
msgid "Stop Refresh"
|
||||
msgstr "停止刷新"
|
||||
|
||||
msgid "Stopped"
|
||||
msgstr "已停止"
|
||||
|
||||
msgid "Successfully saved."
|
||||
msgstr "保存成功。"
|
||||
|
||||
msgid "Upload file..."
|
||||
msgstr "上传文件……"
|
||||
@@ -7,7 +7,7 @@ config amlogic 'config'
|
||||
option amlogic_kernel_path 'https://github.com/ophub/kernel'
|
||||
option amlogic_kernel_tags ''
|
||||
option amlogic_kernel_branch ''
|
||||
option amlogic_plugin_branch ''
|
||||
option amlogic_plugin_branch 'main'
|
||||
option amlogic_firmware_config '1'
|
||||
option amlogic_write_bootloader '0'
|
||||
option amlogic_shared_fstype 'ext4'
|
||||
|
||||
@@ -7,14 +7,36 @@
|
||||
}
|
||||
EEOF
|
||||
}
|
||||
# luci-app-amlogic-js: post-install configuration tweaks.
|
||||
|
||||
sed -i 's/cbi.submit\"] = true/cbi.submit\"] = \"1\"/g' /usr/lib/lua/luci/dispatcher.lua
|
||||
# Detect running platform from /etc/flippy-openwrt-release. The original Lua
|
||||
# controller hid the "Install OpenWrt" menu unless platform=="amlogic" or
|
||||
# SHOW_INSTALL_MENU=="yes", and hid "CPU Settings" on the qemu platform.
|
||||
# Since menu.d JSON cannot run shell logic, we mirror those checks once here
|
||||
# and store the result as uci flags that the menu entries depend on.
|
||||
|
||||
PLATFORM=$(grep -E '^PLATFORM=' /etc/flippy-openwrt-release 2>/dev/null | \
|
||||
awk -F= '{print $2}' | tr -d '"' | xargs)
|
||||
SHOW_INSTALL=$(grep -E '^SHOW_INSTALL_MENU=' /etc/flippy-openwrt-release 2>/dev/null | \
|
||||
awk -F= '{print $2}' | tr -d '"' | xargs)
|
||||
|
||||
case "$PLATFORM" in
|
||||
*amlogic*) MENU_INSTALL=yes ;;
|
||||
*) [ "$SHOW_INSTALL" = "yes" ] && MENU_INSTALL=yes || MENU_INSTALL=no ;;
|
||||
esac
|
||||
case "$PLATFORM" in
|
||||
*qemu*) MENU_ARMCPU=no ;;
|
||||
*) MENU_ARMCPU=yes ;;
|
||||
esac
|
||||
|
||||
uci -q batch <<-EOF >/dev/null
|
||||
delete ucitrack.@amlogic[-1]
|
||||
add ucitrack amlogic
|
||||
set ucitrack.@amlogic[-1].init=amlogic
|
||||
commit ucitrack
|
||||
set amlogic.config.menu_install=${MENU_INSTALL}
|
||||
set amlogic.config.menu_armcpu=${MENU_ARMCPU}
|
||||
commit amlogic
|
||||
EOF
|
||||
|
||||
rm -rf /tmp/luci-*
|
||||
|
||||
@@ -64,8 +64,8 @@ fi
|
||||
tolog "PLATFORM: [ ${PLATFORM} ]"
|
||||
sleep 2
|
||||
|
||||
# Read plugin branch from UCI config; default to "" (main-lua) when missing or empty
|
||||
# When amlogic_plugin_branch is missing, add it with empty value (main-lua default)
|
||||
# Read plugin branch from UCI config; default to "" (main-js) when missing or empty
|
||||
# When amlogic_plugin_branch is missing, add it with empty value (main-js default)
|
||||
if [[ -f "${AMLOGIC_CONFIG_FILE}" ]]; then
|
||||
plugin_branch="$(uci get amlogic.config.amlogic_plugin_branch 2>/dev/null | xargs)"
|
||||
if ! grep -q "amlogic_plugin_branch" "${AMLOGIC_CONFIG_FILE}" 2>/dev/null; then
|
||||
@@ -76,17 +76,29 @@ if [[ -f "${AMLOGIC_CONFIG_FILE}" ]]; then
|
||||
else
|
||||
plugin_branch=""
|
||||
fi
|
||||
tolog "Plugin branch: [ ${plugin_branch:-main-lua} ]"
|
||||
tolog "Plugin branch: [ ${plugin_branch:-main-js} ]"
|
||||
sleep 1
|
||||
get_plugin_info() {
|
||||
package_manager=""
|
||||
current_plugin_v=""
|
||||
current_plugin_release=""
|
||||
if command -v opkg >/dev/null 2>&1; then
|
||||
package_manager="ipk"
|
||||
current_plugin_v="$(opkg list-installed | grep '^luci-app-amlogic ' | awk '{print $3}' | cut -d'-' -f1)"
|
||||
# Full version string e.g. "3.1.295-1" or "3.1.295-2"
|
||||
local full_v
|
||||
full_v="$(opkg list-installed | grep '^luci-app-amlogic ' | awk '{print $3}')"
|
||||
current_plugin_v="$(echo "${full_v}" | cut -d'-' -f1)"
|
||||
# Strip optional "r" prefix from release (e.g. "r2" -> "2")
|
||||
current_plugin_release="$(echo "${full_v}" | cut -d'-' -f2 | sed 's/^r//')"
|
||||
elif command -v apk >/dev/null 2>&1; then
|
||||
package_manager="apk"
|
||||
current_plugin_v="$(apk list --installed | grep '^luci-app-amlogic-' | awk '{print $1}' | cut -d'-' -f4)"
|
||||
# Package name e.g. "luci-app-amlogic-3.1.295-r2"
|
||||
# Fields: luci(1) app(2) amlogic(3) 3.1.295(4) r2(5)
|
||||
local pkg_name
|
||||
pkg_name="$(apk list --installed | grep '^luci-app-amlogic-' | awk '{print $1}')"
|
||||
current_plugin_v="$(echo "${pkg_name}" | cut -d'-' -f4)"
|
||||
# Extract release number: "r2" -> "2"
|
||||
current_plugin_release="$(echo "${pkg_name}" | cut -d'-' -f5 | sed 's/^r//')"
|
||||
fi
|
||||
}
|
||||
|
||||
@@ -97,16 +109,16 @@ check_plugin() {
|
||||
if [[ -z "${package_manager}" || -z "${current_plugin_v}" ]]; then
|
||||
tolog "01.01 Plugin 'luci-app-amlogic' not found or package manager unknown." "1"
|
||||
else
|
||||
tolog "01.01 Using [${package_manager}]. Current version: ${current_plugin_v}"
|
||||
tolog "01.01 Using [${package_manager}]. Current version: ${current_plugin_v}, Release: ${current_plugin_release:-unknown}"
|
||||
fi
|
||||
sleep 2
|
||||
|
||||
tolog "02. Start querying plugin version..."
|
||||
if [[ "${plugin_branch}" == "js" ]]; then
|
||||
if [[ "${plugin_branch}" == "lua" ]]; then
|
||||
latest_version="$(
|
||||
curl -fsSL -m 10 \
|
||||
https://github.com/ophub/luci-app-amlogic/releases |
|
||||
grep -oE 'expanded_assets/[0-9]+\.[0-9]+\.[0-9]+-js' | sed 's|expanded_assets/||g' |
|
||||
grep -oE 'expanded_assets/[0-9]+\.[0-9]+\.[0-9]+-lua' | sed 's|expanded_assets/||g' |
|
||||
sort -urV | head -n 1
|
||||
)"
|
||||
else
|
||||
@@ -114,7 +126,7 @@ check_plugin() {
|
||||
curl -fsSL -m 10 \
|
||||
https://github.com/ophub/luci-app-amlogic/releases |
|
||||
grep -oE 'expanded_assets/[0-9]+\.[0-9]+\.[0-9]+' | sed 's|expanded_assets/||g' |
|
||||
grep -v '\-js' |
|
||||
grep -v '\-lua' |
|
||||
sort -urV | head -n 1
|
||||
)"
|
||||
fi
|
||||
@@ -125,7 +137,22 @@ check_plugin() {
|
||||
tolog "02.01 Current version: ${current_plugin_v}, Latest version: ${latest_version}"
|
||||
sleep 2
|
||||
|
||||
if [[ "${current_plugin_v}" == "${latest_version}" ]]; then
|
||||
# Strip variant suffix (e.g. "-lua") from latest_version to get the numeric part.
|
||||
latest_version_base="${latest_version%%-*}"
|
||||
|
||||
# Determine target PKG_RELEASE for the selected branch:
|
||||
# js branch (default) -> release 2
|
||||
# lua branch -> release 1
|
||||
if [[ "${plugin_branch}" == "lua" ]]; then
|
||||
target_release="1"
|
||||
else
|
||||
target_release="2"
|
||||
fi
|
||||
|
||||
# Only report "already latest" when BOTH the version number AND the installed
|
||||
# branch (PKG_RELEASE) match the selected branch. If the user switched branches
|
||||
# (same version number but different release), we still offer an update.
|
||||
if [[ "${current_plugin_v}" == "${latest_version_base}" && "${current_plugin_release}" == "${target_release}" ]]; then
|
||||
tolog "02.02 Already the latest version, no need to update." "1"
|
||||
else
|
||||
tolog '<input type="button" class="cbi-button cbi-button-reload" value="Download" onclick="return b_check_plugin(this, '"'download_${latest_version}'"')"/> Latest version: '${latest_version}'' "1"
|
||||
|
||||
@@ -0,0 +1,77 @@
|
||||
{
|
||||
"admin/system/amlogic": {
|
||||
"title": "Amlogic Service",
|
||||
"order": 88,
|
||||
"action": {
|
||||
"type": "firstchild"
|
||||
},
|
||||
"depends": {
|
||||
"acl": [ "luci-app-amlogic" ],
|
||||
"fs": { "/etc/config/amlogic": "file" }
|
||||
}
|
||||
},
|
||||
|
||||
"admin/system/amlogic/info": {
|
||||
"title": "Amlogic Service",
|
||||
"order": 1,
|
||||
"action": { "type": "view", "path": "amlogic/info" }
|
||||
},
|
||||
|
||||
"admin/system/amlogic/install": {
|
||||
"title": "Install OpenWrt",
|
||||
"order": 2,
|
||||
"action": { "type": "view", "path": "amlogic/install" },
|
||||
"depends": {
|
||||
"uci": { "amlogic": { "config": { "menu_install": "yes" } } }
|
||||
}
|
||||
},
|
||||
|
||||
"admin/system/amlogic/upload": {
|
||||
"title": "Manually Upload Update",
|
||||
"order": 3,
|
||||
"action": { "type": "view", "path": "amlogic/upload" }
|
||||
},
|
||||
|
||||
"admin/system/amlogic/check": {
|
||||
"title": "Online Download Update",
|
||||
"order": 4,
|
||||
"action": { "type": "view", "path": "amlogic/check" }
|
||||
},
|
||||
|
||||
"admin/system/amlogic/backup": {
|
||||
"title": "Backup Firmware Config",
|
||||
"order": 5,
|
||||
"action": { "type": "view", "path": "amlogic/backup" }
|
||||
},
|
||||
|
||||
"admin/system/amlogic/backuplist": {
|
||||
"action": { "type": "view", "path": "amlogic/backuplist" }
|
||||
},
|
||||
|
||||
"admin/system/amlogic/armcpu": {
|
||||
"title": "CPU Settings",
|
||||
"order": 6,
|
||||
"action": { "type": "view", "path": "amlogic/armcpu" },
|
||||
"depends": {
|
||||
"uci": { "amlogic": { "config": { "menu_armcpu": "yes" } } }
|
||||
}
|
||||
},
|
||||
|
||||
"admin/system/amlogic/config": {
|
||||
"title": "Plugin Settings",
|
||||
"order": 7,
|
||||
"action": { "type": "view", "path": "amlogic/config" }
|
||||
},
|
||||
|
||||
"admin/system/amlogic/log": {
|
||||
"title": "Server Logs",
|
||||
"order": 8,
|
||||
"action": { "type": "view", "path": "amlogic/log" }
|
||||
},
|
||||
|
||||
"admin/system/amlogic/poweroff": {
|
||||
"title": "PowerOff",
|
||||
"order": 9,
|
||||
"action": { "type": "view", "path": "amlogic/poweroff" }
|
||||
}
|
||||
}
|
||||
@@ -1,11 +1,75 @@
|
||||
{
|
||||
"luci-app-amlogic": {
|
||||
"description": "Grant UCI access for luci-app-amlogic",
|
||||
"description": "Grant UCI/file/RPC access for the Amlogic Service plugin",
|
||||
"read": {
|
||||
"uci": [ "amlogic" ]
|
||||
"ubus": {
|
||||
"luci.amlogic": [
|
||||
"state",
|
||||
"author",
|
||||
"model_database",
|
||||
"platform_info",
|
||||
"snapshot_list",
|
||||
"check_upfiles",
|
||||
"upload_path",
|
||||
"list_uploads",
|
||||
"read_log_tail",
|
||||
"read_log_full",
|
||||
"cpu_policies"
|
||||
]
|
||||
},
|
||||
"uci": [ "amlogic" ],
|
||||
"file": {
|
||||
"/tmp/amlogic/amlogic.log": [ "read" ],
|
||||
"/tmp/amlogic/amlogic_check_install.log": [ "read" ],
|
||||
"/tmp/amlogic/amlogic_check_upfiles.log": [ "read" ],
|
||||
"/tmp/amlogic/amlogic_check_plugin.log": [ "read" ],
|
||||
"/tmp/amlogic/amlogic_check_kernel.log": [ "read" ],
|
||||
"/tmp/amlogic/amlogic_check_firmware.log": [ "read" ],
|
||||
"/tmp/amlogic/amlogic_check_rescue.log": [ "read" ],
|
||||
"/tmp/amlogic/amlogic_running_script.log": [ "read" ],
|
||||
"/etc/amlogic_backup_list.conf": [ "read" ],
|
||||
"/etc/flippy-openwrt-release": [ "read" ],
|
||||
"/etc/model_database.txt": [ "read" ],
|
||||
"/.reserved/openwrt_config.tar.gz": [ "read" ]
|
||||
}
|
||||
},
|
||||
"write": {
|
||||
"uci": [ "amlogic" ]
|
||||
"ubus": {
|
||||
"luci.amlogic": [
|
||||
"snapshot_create",
|
||||
"snapshot_delete",
|
||||
"snapshot_restore",
|
||||
"check_plugin",
|
||||
"check_kernel",
|
||||
"check_firmware",
|
||||
"start_plugin",
|
||||
"start_kernel",
|
||||
"start_update",
|
||||
"start_install",
|
||||
"start_rescue",
|
||||
"del_log",
|
||||
"poweroff",
|
||||
"kvm_switch",
|
||||
"delete_upload",
|
||||
"install_upload",
|
||||
"upload_chunk",
|
||||
"backup_create",
|
||||
"save_backup_list",
|
||||
"prime_backup_list",
|
||||
"reload_cpu",
|
||||
"sync_menu"
|
||||
]
|
||||
},
|
||||
"uci": [ "amlogic" ],
|
||||
"file": {
|
||||
"/etc/amlogic_backup_list.conf": [ "write" ],
|
||||
"/tmp/upload/*": [ "write" ],
|
||||
"/tmp/amlogic/*": [ "write" ],
|
||||
"/mnt/mmcblk*p4/*": [ "write" ],
|
||||
"/mnt/sd*4/*": [ "write" ],
|
||||
"/mnt/hd*4/*": [ "write" ],
|
||||
"/mnt/vd*4/*": [ "write" ]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,759 @@
|
||||
// SPDX-License-Identifier: GPL-2.0
|
||||
//
|
||||
// luci.amlogic - ucode RPC bridge for the JS-based luci-app-amlogic.
|
||||
//
|
||||
// Helpers
|
||||
'use strict';
|
||||
|
||||
import { popen, readfile, writefile, mkstemp, stat, glob, error as fserror,
|
||||
unlink, mkdir, access, open as fsopen } from 'fs';
|
||||
|
||||
const LOG_DIR = '/tmp/amlogic';
|
||||
const LOG_FILE = LOG_DIR + '/amlogic.log';
|
||||
const LOG_RUNNING = LOG_DIR + '/amlogic_running_script.log';
|
||||
const LOG_INSTALL = LOG_DIR + '/amlogic_check_install.log';
|
||||
const LOG_UPFILES = LOG_DIR + '/amlogic_check_upfiles.log';
|
||||
const LOG_PLUGIN = LOG_DIR + '/amlogic_check_plugin.log';
|
||||
const LOG_KERNEL = LOG_DIR + '/amlogic_check_kernel.log';
|
||||
const LOG_FIRMWARE = LOG_DIR + '/amlogic_check_firmware.log';
|
||||
const LOG_RESCUE = LOG_DIR + '/amlogic_check_rescue.log';
|
||||
const UPLOAD_TMP = '/tmp/upload';
|
||||
const PKG_NAME = 'luci-app-amlogic';
|
||||
const BACKUP_LIST = '/etc/amlogic_backup_list.conf';
|
||||
|
||||
// Map of safe log names -> file paths (for read_log_tail/read_log_full)
|
||||
const LOG_MAP = {
|
||||
'main': LOG_FILE,
|
||||
'install': LOG_INSTALL,
|
||||
'upfiles': LOG_UPFILES,
|
||||
'plugin': LOG_PLUGIN,
|
||||
'kernel': LOG_KERNEL,
|
||||
'firmware': LOG_FIRMWARE,
|
||||
'rescue': LOG_RESCUE,
|
||||
'running': LOG_RUNNING
|
||||
};
|
||||
|
||||
// Run a shell command, return trimmed stdout
|
||||
function sh(cmd) {
|
||||
const p = popen(cmd, 'r');
|
||||
if (!p) return '';
|
||||
const out = p.read('all') ?? '';
|
||||
p.close();
|
||||
return trim(out);
|
||||
}
|
||||
|
||||
// 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.
|
||||
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 + ') </dev/null &';
|
||||
const p = popen(wrapper, 'r');
|
||||
if (p) p.close();
|
||||
}
|
||||
|
||||
// Shell-quote a string for embedding into a /bin/sh command line
|
||||
function shq(s) {
|
||||
return "'" + replace(s ?? '', "'", "'\\''") + "'";
|
||||
}
|
||||
|
||||
// Ensure the runtime tmp dirs exist (mirrors the luci.sys.exec calls in the
|
||||
// original controller's index() function).
|
||||
function ensure_dirs() {
|
||||
mkdir(LOG_DIR);
|
||||
mkdir(UPLOAD_TMP);
|
||||
}
|
||||
|
||||
// Read flippy-openwrt-release platform identifier
|
||||
function flippy_var(name) {
|
||||
return sh("cat /etc/flippy-openwrt-release 2>/dev/null | grep " + shq(name) +
|
||||
" | awk -F= '{print $2}' | tr -d '\"' | xargs");
|
||||
}
|
||||
|
||||
function platform() {
|
||||
const v = flippy_var('PLATFORM');
|
||||
const m = match(v, /(amlogic|rockchip|allwinner|qemu)/);
|
||||
return m ? m[1] : 'Unknown';
|
||||
}
|
||||
|
||||
function show_install_menu() {
|
||||
const v = flippy_var('SHOW_INSTALL_MENU');
|
||||
const m = match(v, /(yes|no)/);
|
||||
return m ? m[1] : 'Unknown';
|
||||
}
|
||||
|
||||
// Pick the helper script names for the current platform (as the original
|
||||
// controller did).
|
||||
function platform_scripts() {
|
||||
const p = platform();
|
||||
if (index(p, 'rockchip') >= 0)
|
||||
return { install: '', update: 'openwrt-update-rockchip', kernel: 'openwrt-kernel' };
|
||||
if (index(p, 'allwinner') >= 0)
|
||||
return { install: 'openwrt-install-allwinner', update: 'openwrt-update-allwinner', kernel: 'openwrt-kernel' };
|
||||
if (index(p, 'qemu') >= 0)
|
||||
return { install: '', update: 'openwrt-update-kvm', kernel: 'openwrt-kernel' };
|
||||
return { install: 'openwrt-install-amlogic', update: 'openwrt-update-amlogic', kernel: 'openwrt-kernel' };
|
||||
}
|
||||
|
||||
// Read an option from the [config] section of /etc/config/amlogic
|
||||
function uci_cfg(opt) {
|
||||
return sh('uci -q get amlogic.config.' + opt);
|
||||
}
|
||||
|
||||
// --- Version helpers (identical logic to controller's *_version() funcs) ---
|
||||
function current_firmware_version() {
|
||||
const v = sh("uname -r 2>/dev/null | grep -oE '^[1-9]\\.[0-9]{1,3}\\.[0-9]+'");
|
||||
return v != '' ? v : 'Invalid value.';
|
||||
}
|
||||
function current_kernel_version() { return current_firmware_version(); }
|
||||
function current_kernel_branch() {
|
||||
const def = sh("uname -r 2>/dev/null | grep -oE '^[1-9]\\.[0-9]{1,3}'");
|
||||
const cfg = sh("uci -q get amlogic.config.amlogic_kernel_branch | grep -oE '^[1-9]\\.[0-9]{1,3}'");
|
||||
return cfg != '' ? cfg : def;
|
||||
}
|
||||
function current_plugin_version() {
|
||||
let v = '';
|
||||
if (shcall('command -v opkg') == 0)
|
||||
v = sh("opkg list-installed | grep '^" + PKG_NAME + "' | awk '{print $3}' | cut -d'-' -f1");
|
||||
if (v == '' && shcall('command -v apk') == 0)
|
||||
v = sh("apk list --installed 2>/dev/null | grep '^" + PKG_NAME + "' | awk '{print $1}' | cut -d'-' -f4");
|
||||
return v != '' ? v : 'Invalid value.';
|
||||
}
|
||||
|
||||
// --- Config-driven flags used when invoking helper scripts ---
|
||||
function update_restore_config() {
|
||||
const v = uci_cfg('amlogic_firmware_config');
|
||||
return (v == '' || int(v) != 0) ? 'restore' : 'no-restore';
|
||||
}
|
||||
function auto_write_bootloader() {
|
||||
const v = uci_cfg('amlogic_write_bootloader');
|
||||
return (v == '' || int(v) != 0) ? 'yes' : 'no';
|
||||
}
|
||||
function auto_shared_fstype() {
|
||||
const v = trim(uci_cfg('amlogic_shared_fstype'));
|
||||
return v != '' ? v : 'ext4';
|
||||
}
|
||||
|
||||
// Read tail / full content of a known log file
|
||||
function _safe_log_path(name) {
|
||||
return LOG_MAP[name] ?? null;
|
||||
}
|
||||
function read_log_tail(name) {
|
||||
const p = _safe_log_path(name);
|
||||
if (!p) return '';
|
||||
const pipe = popen('cat ' + p + ' 2>/dev/null | tail -c 4096', 'r');
|
||||
if (!pipe) return '';
|
||||
const content = pipe.read('all') ?? '';
|
||||
pipe.close();
|
||||
if (!content) return '';
|
||||
// Find the last non-empty line
|
||||
const lines = split(content, '\n');
|
||||
for (let i = length(lines) - 1; i >= 0; i--) {
|
||||
const line = trim(lines[i]);
|
||||
if (line !== '') return line;
|
||||
}
|
||||
return '';
|
||||
}
|
||||
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.
|
||||
const pipe = popen('cat ' + p + ' 2>/dev/null', 'r');
|
||||
if (!pipe) return '';
|
||||
const t = pipe.read('all') ?? '';
|
||||
pipe.close();
|
||||
return t;
|
||||
}
|
||||
|
||||
// Lazily seed the main log on first read (matches the Lua action_refresh_log
|
||||
// behaviour: when the log is missing, populate it with `uname -a`).
|
||||
function ensure_main_log() {
|
||||
ensure_dirs();
|
||||
if (!access(LOG_FILE)) {
|
||||
shcall("uname -a > " + LOG_FILE);
|
||||
let _logs1 = [LOG_INSTALL, LOG_UPFILES, LOG_PLUGIN, LOG_KERNEL,
|
||||
LOG_FIRMWARE, LOG_RESCUE, LOG_RUNNING];
|
||||
for (let i = 0; i < length(_logs1); i++)
|
||||
writefile(_logs1[i], '');
|
||||
}
|
||||
}
|
||||
|
||||
// Helper: clear (truncate) a file
|
||||
function clear(path) { writefile(path, ''); }
|
||||
|
||||
// Cast/sanitise an `amlogic_install_sel` token: form is
|
||||
// "<soc_id>@<dtb>:<socname>:<ubootname>" (from old install view)
|
||||
// The Lua original passed res[2] verbatim (colon-string) to the install
|
||||
// script as the dtb_filename argument when soc_id == 99.
|
||||
function parse_install_sel(s) {
|
||||
const parts = split(s ?? '', '@');
|
||||
const soc_id = (parts[0] ?? '99');
|
||||
let dtb = 'auto_dtb';
|
||||
if (int(parts[0]) == 99 && length(parts) > 1 && parts[1] != '')
|
||||
dtb = parts[1];
|
||||
return { soc_id, dtb };
|
||||
}
|
||||
|
||||
// Helper: parse `amlogic_update_sel` form
|
||||
// "<firmware_name>@<updated>@<write_path>"
|
||||
function parse_update_sel(s) {
|
||||
const a = split(s ?? '', '@');
|
||||
return {
|
||||
name: a[0] ?? 'auto',
|
||||
updated: a[1] ?? 'updated',
|
||||
path: a[2] ?? '/tmp'
|
||||
};
|
||||
}
|
||||
|
||||
// === ubus method bodies ===
|
||||
|
||||
function m_state() {
|
||||
return {
|
||||
current_firmware_version: current_firmware_version(),
|
||||
current_plugin_version: current_plugin_version(),
|
||||
current_kernel_version: current_kernel_version(),
|
||||
current_kernel_branch: current_kernel_branch(),
|
||||
kernel_release: sh('uname -r')
|
||||
};
|
||||
}
|
||||
|
||||
function m_author() {
|
||||
const r = uci_cfg('amlogic_firmware_repo');
|
||||
return { openwrt_author: r != '' ? r : 'Invalid value.' };
|
||||
}
|
||||
|
||||
function m_platform_info() {
|
||||
const p = platform();
|
||||
const m = show_install_menu();
|
||||
const has_kvm = access('/boot/efi/EFI/BOOT/grub.cfg.prev') ? true : false;
|
||||
// can_install: amlogic or allwinner have an install script; also forced by SHOW_INSTALL_MENU=yes
|
||||
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 (mmcblk/sd/hd/vd),
|
||||
// meaning OpenWrt has already been installed to eMMC/NVMe/disk. When true,
|
||||
// the Install OpenWrt menu should be hidden.
|
||||
const root_pt = sh("df / | tail -n1 | awk '{print $1}' | awk -F '/' '{print $3}'");
|
||||
const is_installed = (root_pt != '' &&
|
||||
(match(root_pt, /^mmcblk[0-9]+p[1-4]$/) ||
|
||||
match(root_pt, /^[hsv]d[a-z][0-9]+$/) ||
|
||||
match(root_pt, /^nvme[0-9]+n[0-9]+p[0-9]+$/)));
|
||||
return {
|
||||
platform: p,
|
||||
install_menu: m,
|
||||
can_install,
|
||||
can_armcpu,
|
||||
has_kvm,
|
||||
is_installed: is_installed ? true : false
|
||||
};
|
||||
}
|
||||
|
||||
function m_model_database() {
|
||||
const p = platform();
|
||||
let pat;
|
||||
if (index(p, 'allwinner') >= 0)
|
||||
pat = "^w[0-9]{1,9}.*:";
|
||||
else
|
||||
pat = "^[0-9]{1,9}.*:";
|
||||
const out = sh("cat /etc/model_database.txt 2>/dev/null | grep -E " + shq(pat) +
|
||||
" | awk -F ':' '{print $1\"###\"$2}' | tr ' ' '~'");
|
||||
const entries = [];
|
||||
for (let line in split(out, '\n')) {
|
||||
const t = trim(line);
|
||||
if (t == '') continue;
|
||||
const kv = split(t, '###');
|
||||
if (length(kv) >= 2)
|
||||
push(entries, { id: trim(kv[0]), name: replace(trim(kv[1]), '~', ' ') });
|
||||
}
|
||||
return { entries };
|
||||
}
|
||||
|
||||
function m_snapshot_list() {
|
||||
const out = sh("btrfs subvolume list -rt / 2>/dev/null | awk '{print $4}' | grep .snapshots");
|
||||
const names = [];
|
||||
for (let l in split(out, '\n')) {
|
||||
const n = replace(trim(l), '.snapshots/', '');
|
||||
if (n != '') push(names, n);
|
||||
}
|
||||
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.
|
||||
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-<MM.DD.HHMMSS>.
|
||||
// Mirrors the `Create Snapshot` button in amlogic_backup.lua.
|
||||
function m_snapshot_create() {
|
||||
shcall("btrfs subvolume snapshot -r /etc /.snapshots/etc-$(date +%m.%d.%H%M%S) && sync");
|
||||
return { code: 0 };
|
||||
}
|
||||
|
||||
function m_snapshot_delete(req) {
|
||||
const n = req.args?.name ?? '';
|
||||
if (!match(n, /^[A-Za-z0-9._-]+$/))
|
||||
return { code: 1, error: 'invalid name' };
|
||||
const rc = shcall("btrfs subvolume delete -c /.snapshots/etc-" + n + " && sync");
|
||||
return { code: rc };
|
||||
}
|
||||
|
||||
function m_snapshot_restore(req) {
|
||||
const n = req.args?.name ?? '';
|
||||
if (!match(n, /^[A-Za-z0-9._-]+$/))
|
||||
return { error: 'invalid name' };
|
||||
shcall("btrfs subvolume snapshot /.snapshots/etc-" + n + " /etc && sync");
|
||||
// trigger a reboot like the Lua version (echo b > /proc/sysrq-trigger)
|
||||
shcall("(sleep 1 && echo b > /proc/sysrq-trigger) &");
|
||||
return { code: 0 };
|
||||
}
|
||||
|
||||
function m_check_plugin(req) {
|
||||
ensure_dirs();
|
||||
const opt = req.args?.options ?? 'check';
|
||||
writefile(LOG_PLUGIN, '');
|
||||
shcall('chmod +x /usr/share/amlogic/amlogic_check_plugin.sh');
|
||||
if (opt === 'check') {
|
||||
shbg('/usr/share/amlogic/amlogic_check_plugin.sh -check');
|
||||
return { code: 0 };
|
||||
}
|
||||
// download option: e.g. "download_3.1.290" — pass verbatim to the script
|
||||
shbg('/usr/share/amlogic/amlogic_check_plugin.sh -download ' + shq(opt));
|
||||
return { code: 0 };
|
||||
}
|
||||
|
||||
function m_check_kernel(req) {
|
||||
ensure_dirs();
|
||||
const opt = req.args?.options ?? 'check';
|
||||
writefile(LOG_KERNEL, '');
|
||||
shcall('chmod +x /usr/share/amlogic/amlogic_check_kernel.sh');
|
||||
if (opt == 'check') {
|
||||
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)
|
||||
shbg('/usr/share/amlogic/amlogic_check_kernel.sh -download ' + shq(opt));
|
||||
return { code: 0 };
|
||||
}
|
||||
|
||||
function m_check_firmware(req) {
|
||||
ensure_dirs();
|
||||
const opt = req.args?.options ?? 'check';
|
||||
writefile(LOG_FIRMWARE, '');
|
||||
shcall('chmod +x /usr/share/amlogic/amlogic_check_firmware.sh');
|
||||
if (opt == 'check') {
|
||||
shbg('/usr/share/amlogic/amlogic_check_firmware.sh -check');
|
||||
return { code: 0 };
|
||||
}
|
||||
// download option: may contain @, /, dots, etc. — pass via shq
|
||||
shbg('/usr/share/amlogic/amlogic_check_firmware.sh -download ' + shq(opt));
|
||||
return { code: 0 };
|
||||
}
|
||||
|
||||
function m_start_plugin() {
|
||||
ensure_dirs();
|
||||
const cfg = '/etc/config/amlogic';
|
||||
const cfg_bak = '/etc/config/amlogic_bak';
|
||||
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.
|
||||
const script = '#!/bin/sh\n'
|
||||
+ 'LOG=' + shq(LOG_PLUGIN) + '\n'
|
||||
+ 'LOG_RUNNING=' + shq(LOG_RUNNING) + '\n'
|
||||
+ 'cfg=' + shq(cfg) + '\n'
|
||||
+ 'cfg_bak=' + shq(cfg_bak) + '\n'
|
||||
+ '[ -f "$cfg" ] && cp -f "$cfg" "$cfg_bak"\n'
|
||||
+ 'if command -v opkg >/dev/null 2>&1; then\n'
|
||||
+ ' echo "System uses opkg." >> "$LOG"\n'
|
||||
+ ' opkg --force-reinstall --force-downgrade install /tmp/amlogic/*.ipk >> "$LOG" 2>&1\n'
|
||||
+ ' rc=$?\n'
|
||||
+ 'elif command -v apk >/dev/null 2>&1; then\n'
|
||||
+ ' echo "System uses apk." >> "$LOG"\n'
|
||||
+ ' apk add --force-overwrite --allow-untrusted /tmp/amlogic/*.apk >> "$LOG" 2>&1\n'
|
||||
+ ' rc=$?\n'
|
||||
+ 'else\n'
|
||||
+ ' echo "Error: No package manager." >> "$LOG"\n'
|
||||
+ ' rc=1\n'
|
||||
+ 'fi\n'
|
||||
+ 'if [ "$rc" = "0" ]; then\n'
|
||||
+ ' [ -f "$cfg_bak" ] && cp -f "$cfg_bak" "$cfg"\n'
|
||||
+ ' rm -f /etc/config/amlogic.apk-new /etc/config/amlogic.ipk-old\n'
|
||||
+ ' rm -rf /tmp/luci-indexcache /tmp/luci-modulecache/* "$cfg_bak"\n'
|
||||
+ ' echo "" > "$LOG_RUNNING"\n'
|
||||
+ ' echo "Successful Update" > "$LOG"\n'
|
||||
+ ' # Cross-branch cleanup: run in background after a delay so the browser\n'
|
||||
+ ' # can poll "Successful Update" before any files are removed.\n'
|
||||
+ ' (sleep 3\n'
|
||||
+ ' new_release=""\n'
|
||||
+ ' if command -v opkg >/dev/null 2>&1; then\n'
|
||||
+ ' _raw="$(opkg list-installed | grep \'^luci-app-amlogic \' | awk \'{print $3}\' | cut -d\'-\' -f2)"\n'
|
||||
+ ' new_release="${_raw#r}"\n'
|
||||
+ ' elif command -v apk >/dev/null 2>&1; then\n'
|
||||
+ ' new_release="$(apk list --installed 2>/dev/null | grep \'^luci-app-amlogic-\' | awk \'{print $1}\' | cut -d\'-\' -f5 | sed \'s/^r//\')"\n'
|
||||
+ ' fi\n'
|
||||
+ ' if [ "${new_release}" = "2" ]; then\n'
|
||||
+ ' rm -f /usr/lib/lua/luci/controller/amlogic.lua\n'
|
||||
+ ' rm -rf /usr/lib/lua/luci/model/cbi/amlogic\n'
|
||||
+ ' rm -rf /usr/lib/lua/luci/view/amlogic\n'
|
||||
+ ' elif [ "${new_release}" = "1" ]; then\n'
|
||||
+ ' rm -f /usr/share/rpcd/ucode/luci.amlogic\n'
|
||||
+ ' rm -f /www/luci-static/resources/view/amlogic/*.js\n'
|
||||
+ ' rm -f /usr/share/luci/menu.d/luci-app-amlogic.json\n'
|
||||
+ ' fi) &\n'
|
||||
+ 'else\n'
|
||||
+ ' echo "--- INSTALLATION FAILED! ---" >> "$LOG"\n'
|
||||
+ ' [ -f "$cfg_bak" ] && cp -f "$cfg_bak" "$cfg"\n'
|
||||
+ ' rm -f "$cfg_bak"\n'
|
||||
+ ' echo "" > "$LOG_RUNNING"\n'
|
||||
+ 'fi\n';
|
||||
writefile(script_path, script);
|
||||
shbg('sh ' + shq(script_path));
|
||||
return { code: 0 };
|
||||
}
|
||||
|
||||
function m_start_kernel() {
|
||||
ensure_dirs();
|
||||
const sc = platform_scripts();
|
||||
writefile(LOG_RUNNING, '2@Kernel update in progress, try again later!\n');
|
||||
shcall('chmod +x /usr/sbin/' + sc.kernel);
|
||||
shbg('/usr/sbin/' + sc.kernel + ' ' + auto_write_bootloader() + ' > ' + LOG_KERNEL + ' 2>&1 && sync && echo "" > ' + LOG_RUNNING);
|
||||
return { code: 0 };
|
||||
}
|
||||
|
||||
function m_start_update(req) {
|
||||
ensure_dirs();
|
||||
const sc = platform_scripts();
|
||||
writefile(LOG_RUNNING, '3@OpenWrt update in progress, try again later!\n');
|
||||
shcall('chmod +x /usr/sbin/' + sc.update);
|
||||
const u = parse_update_sel(req.args?.amlogic_update_sel);
|
||||
shcall('mkdir -p ' + shq(u.path + '/.luci-app-amlogic'));
|
||||
shcall('echo ' + shq(u.updated) + ' > ' + shq(u.path + '/.luci-app-amlogic/op_release_code') + ' && sync');
|
||||
shbg('/usr/sbin/' + sc.update + ' ' + shq(u.name) + ' ' +
|
||||
auto_write_bootloader() + ' ' + update_restore_config() +
|
||||
' > ' + LOG_FIRMWARE + ' 2>&1 && sync && echo "" > ' + LOG_RUNNING);
|
||||
return { code: 0 };
|
||||
}
|
||||
|
||||
function m_start_install(req) {
|
||||
ensure_dirs();
|
||||
const sc = platform_scripts();
|
||||
if (sc.install == '') return { error: 'install not supported on platform' };
|
||||
shcall('chmod +x /usr/sbin/' + sc.install);
|
||||
const i = parse_install_sel(req.args?.amlogic_install_sel);
|
||||
if (!match(i.soc_id, /^[0-9]+$/) || !match(i.dtb, /^[A-Za-z0-9._:-]+$/))
|
||||
return { error: 'invalid install sel' };
|
||||
writefile(LOG_INSTALL, '');
|
||||
shbg('/usr/sbin/' + sc.install + ' ' + auto_write_bootloader() + ' ' +
|
||||
i.soc_id + ' ' + i.dtb + ' ' + auto_shared_fstype() +
|
||||
' > ' + LOG_INSTALL + ' 2>&1 && sync');
|
||||
return { code: 0 };
|
||||
}
|
||||
|
||||
function m_start_rescue() {
|
||||
ensure_dirs();
|
||||
const sc = platform_scripts();
|
||||
writefile(LOG_RUNNING, '4@Kernel rescue in progress, try again later!\n');
|
||||
shcall('chmod +x /usr/sbin/' + sc.kernel);
|
||||
shbg('/usr/sbin/' + sc.kernel + ' -s > ' + LOG_RESCUE + ' 2>&1 && sync && echo "" > ' + LOG_RUNNING);
|
||||
return { code: 0 };
|
||||
}
|
||||
|
||||
function m_read_log_tail(req) {
|
||||
return { line: read_log_tail(req.args?.name ?? '') };
|
||||
}
|
||||
|
||||
function m_read_log_full(req) {
|
||||
ensure_main_log();
|
||||
return { content: read_log_full(req.args?.name ?? 'main') };
|
||||
}
|
||||
|
||||
function m_del_log() {
|
||||
let _logs2 = [LOG_FILE, LOG_INSTALL, LOG_UPFILES, LOG_PLUGIN, LOG_KERNEL,
|
||||
LOG_FIRMWARE, LOG_RESCUE, LOG_RUNNING];
|
||||
for (let i = 0; i < length(_logs2); i++)
|
||||
writefile(_logs2[i], '');
|
||||
return { code: 0 };
|
||||
}
|
||||
|
||||
function m_poweroff() {
|
||||
shcall('(sleep 1 && /sbin/poweroff) &');
|
||||
return { code: 0 };
|
||||
}
|
||||
|
||||
function m_kvm_switch() {
|
||||
shcall('[ -f /boot/efi/EFI/BOOT/grub.cfg.prev ] && (cd /boot/efi/EFI/BOOT/ && ' +
|
||||
'mv -f grub.cfg grub.cfg.bak && mv -f grub.cfg.prev grub.cfg && ' +
|
||||
'mv -f grub.cfg.bak grub.cfg.prev)');
|
||||
shcall('(sync && sleep 1 && reboot) &');
|
||||
return { code: 0 };
|
||||
}
|
||||
|
||||
function m_check_upfiles() {
|
||||
const t = readfile(LOG_UPFILES) ?? '';
|
||||
return { value: trim(t) };
|
||||
}
|
||||
|
||||
// Pick upload directory using exactly the same heuristic as the Lua version
|
||||
function compute_upload_path() {
|
||||
const root_pt = sh("df / | tail -n1 | awk '{print $1}' | awk -F '/' '{print $3}'");
|
||||
if (root_pt != '' && match(root_pt, /^mmcblk[0-4]p[1-4]$/)) {
|
||||
const emmc = sh('echo ' + shq(root_pt) + " | awk '{print substr($1,1,length($1)-2)}'");
|
||||
return '/mnt/' + emmc + 'p4/';
|
||||
}
|
||||
if (root_pt != '' && match(root_pt, /^[hsv]d[a-z][0-9]+$/)) {
|
||||
const emmc = sh('echo ' + shq(root_pt) + " | awk '{print substr($1,1,length($1)-1)}'");
|
||||
return '/mnt/' + emmc + '4/';
|
||||
}
|
||||
return '/tmp/upload/';
|
||||
}
|
||||
|
||||
function m_upload_path() {
|
||||
const p = compute_upload_path();
|
||||
mkdir(p);
|
||||
return { path: p };
|
||||
}
|
||||
|
||||
function _human_size(n) {
|
||||
const units = [' kB', ' MB', ' GB', ' TB'];
|
||||
let i = -1;
|
||||
let s = +n;
|
||||
while (s > 1024 && i < length(units) - 1) {
|
||||
s = s / 1024;
|
||||
i++;
|
||||
}
|
||||
if (i < 0) { s = n / 1024; i = 0; }
|
||||
return sprintf('%.1f', s) + units[i];
|
||||
}
|
||||
|
||||
function m_list_uploads() {
|
||||
const dir = compute_upload_path();
|
||||
mkdir(dir);
|
||||
const items = [];
|
||||
let firmware = false, boot = false, dtb = false, modules = false, cfg = false;
|
||||
const files = glob(dir + '*') ?? [];
|
||||
for (let f in files) {
|
||||
const st = stat(f);
|
||||
if (!st || st.type != 'file') continue;
|
||||
const base = replace(f, /^.*\//, '');
|
||||
const lcname = lc(base);
|
||||
if (match(lcname, /\.img\.gz$/) || match(lcname, /\.img\.xz$/) ||
|
||||
match(lcname, /\.7z$/) || match(lcname, /\.img$/))
|
||||
firmware = true;
|
||||
if (match(lcname, /^boot-/)) boot = true;
|
||||
if (match(lcname, /^dtb-/)) dtb = true;
|
||||
if (match(lcname, /^modules-/)) modules = true;
|
||||
if (lcname == 'openwrt_config.tar.gz') cfg = true;
|
||||
push(items, {
|
||||
name: base,
|
||||
mtime: sprintf('%s', sh('date -d @' + st.mtime + ' +"%Y-%m-%d %H:%M:%S"')),
|
||||
modestr: st.modestr ?? '',
|
||||
size: _human_size(st.size),
|
||||
ipk: match(lcname, /\.(ipk|apk)$/) ? true : false,
|
||||
cfg: (lcname == 'openwrt_config.tar.gz')
|
||||
});
|
||||
}
|
||||
// Match the Lua page side-effect: write the upfiles status flag.
|
||||
ensure_dirs();
|
||||
if (firmware) writefile(LOG_UPFILES, 'firmware\n');
|
||||
else if (boot && dtb && modules) writefile(LOG_UPFILES, 'kernel\n');
|
||||
else writefile(LOG_UPFILES, '');
|
||||
return { path: dir, items, has_firmware: firmware,
|
||||
has_kernel: (boot && dtb && modules),
|
||||
has_config: cfg };
|
||||
}
|
||||
|
||||
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).
|
||||
function m_upload_chunk(req) {
|
||||
const dir = compute_upload_path();
|
||||
mkdir(dir);
|
||||
const b = _safe_basename(req.args?.name);
|
||||
if (!b) return { ok: false, error: 'invalid name: ' + (req.args?.name ?? '') };
|
||||
const full = dir + b;
|
||||
const data = b64dec(req.args?.data ?? '');
|
||||
if (data === null) return { ok: false, error: 'invalid data' };
|
||||
const append = !!req.args?.append;
|
||||
const fp = fsopen(full, append ? 'a' : 'w');
|
||||
if (!fp) return { ok: false, error: 'open failed: ' + (fserror() ?? '') };
|
||||
const n = fp.write(data);
|
||||
fp.close();
|
||||
if (n === null) return { ok: false, error: 'write failed: ' + (fserror() ?? '') };
|
||||
return { ok: true, written: n };
|
||||
}
|
||||
|
||||
function m_delete_upload(req) {
|
||||
const dir = compute_upload_path();
|
||||
const b = _safe_basename(req.args?.name);
|
||||
if (!b) return { error: 'invalid name' };
|
||||
return { ok: (unlink(dir + b) ? true : false) };
|
||||
}
|
||||
|
||||
function m_install_upload(req) {
|
||||
const dir = compute_upload_path();
|
||||
const b = _safe_basename(req.args?.name);
|
||||
if (!b) return { error: 'invalid name' };
|
||||
const full = dir + b;
|
||||
const lc_b = lc(b);
|
||||
if (lc_b == 'openwrt_config.tar.gz') {
|
||||
shcall('chmod +x /usr/sbin/openwrt-backup');
|
||||
// Use shbg so the restore+reboot does not block the rpcd thread.
|
||||
shbg('/usr/sbin/openwrt-backup -r > ' + LOG_FILE + ' 2>&1 && sync');
|
||||
return { code: 0, kind: 'config' };
|
||||
}
|
||||
if (match(lc_b, /\.ipk$/) || match(lc_b, /\.apk$/)) {
|
||||
let out = '';
|
||||
if (shcall('command -v opkg') == 0)
|
||||
out = sh('opkg --force-reinstall --force-downgrade install ' + shq(full) + ' 2>&1');
|
||||
else if (shcall('command -v apk') == 0)
|
||||
out = sh('apk add --force-overwrite --allow-untrusted ' + shq(full) + ' 2>&1');
|
||||
else
|
||||
out = "Error: Neither 'opkg' nor 'apk' package manager found on the system.";
|
||||
shcall('rm -rf /tmp/luci-indexcache /tmp/luci-modulecache/*');
|
||||
return { code: 0, kind: 'package', output: out };
|
||||
}
|
||||
return { error: 'unsupported file type' };
|
||||
}
|
||||
|
||||
function m_backup_create() {
|
||||
ensure_dirs();
|
||||
shcall('chmod +x /usr/sbin/openwrt-backup');
|
||||
shcall('/usr/sbin/openwrt-backup -b > ' + LOG_FILE + ' && sync');
|
||||
const path = '/.reserved/openwrt_config.tar.gz';
|
||||
const ok = access(path);
|
||||
return { ok: ok ? true : false, path };
|
||||
}
|
||||
|
||||
function m_save_backup_list(req) {
|
||||
let v = req.args?.content ?? '';
|
||||
// strip leading whitespace per line
|
||||
v = replace(v, /\r/g, '');
|
||||
const lines = split(v, '\n');
|
||||
const out = [];
|
||||
for (let l in lines) {
|
||||
let t = replace(l, /^\s+/, '');
|
||||
// strip trailing backslash continuation
|
||||
t = replace(t, /\s*\\\s*$/, '');
|
||||
if (t != '') push(out, t);
|
||||
}
|
||||
writefile(BACKUP_LIST, join('\n', out) + '\n');
|
||||
return { ok: true };
|
||||
}
|
||||
|
||||
function m_reload_cpu() {
|
||||
shcall('(sleep 1 && /etc/init.d/amlogic start) &');
|
||||
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.
|
||||
function m_sync_menu() {
|
||||
const p = platform();
|
||||
const m = show_install_menu();
|
||||
// Detect whether OpenWrt has already been installed to internal storage
|
||||
const root_pt = sh("df / | tail -n1 | awk '{print $1}' | awk -F '/' '{print $3}'");
|
||||
const is_installed = (root_pt != '' &&
|
||||
(match(root_pt, /^mmcblk[0-9]+p[1-4]$/) ||
|
||||
match(root_pt, /^[hsv]d[a-z][0-9]+$/) ||
|
||||
match(root_pt, /^nvme[0-9]+n[0-9]+p[0-9]+$/)));
|
||||
const can_install = (index(p, 'amlogic') >= 0 || index(p, 'allwinner') >= 0 || index(m, 'yes') >= 0);
|
||||
const show_install = (can_install && !is_installed) ? 'yes' : 'no';
|
||||
const show_armcpu = (index(p, 'qemu') < 0) ? 'yes' : 'no';
|
||||
shcall('uci -q set amlogic.config.menu_install=' + shq(show_install) +
|
||||
' && uci -q set amlogic.config.menu_armcpu=' + shq(show_armcpu) +
|
||||
' && uci commit amlogic');
|
||||
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).
|
||||
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, ' ');
|
||||
if (length(names) == 1 && names[0] == '') names = ['policy0'];
|
||||
const out = [];
|
||||
for (let n in names) {
|
||||
const name = trim(n);
|
||||
if (name == '') continue;
|
||||
const id = replace(name, 'policy', '');
|
||||
const freqs_raw = readfile('/sys/devices/system/cpu/cpufreq/' + name + '/scaling_available_frequencies') ?? '';
|
||||
const govs_raw = readfile('/sys/devices/system/cpu/cpufreq/' + name + '/scaling_available_governors') ?? '';
|
||||
const freqs = []; for (let v in split(trim(freqs_raw), ' ')) if (v != '') push(freqs, v);
|
||||
const govs = []; for (let v in split(trim(govs_raw), ' ')) if (v != '') push(govs, v);
|
||||
const core_type = sh('cat /sys/devices/system/cpu/cpu' + id + "/uevent | grep -E '^OF_COMPATIBLE_0' | sed 's/^OF_COMPATIBLE_0=//' | xargs");
|
||||
push(out, { name, id, freqs, governors: govs, core_type: core_type != '' ? core_type : 'Unknown' });
|
||||
}
|
||||
return { policies: out };
|
||||
}
|
||||
|
||||
const methods = {
|
||||
state: { call: m_state },
|
||||
author: { call: m_author },
|
||||
platform_info: { call: m_platform_info },
|
||||
model_database: { call: m_model_database },
|
||||
snapshot_list: { call: m_snapshot_list },
|
||||
snapshot_create: { call: m_snapshot_create },
|
||||
snapshot_delete: { args: { name: '' }, call: m_snapshot_delete },
|
||||
snapshot_restore:{ args: { name: '' }, call: m_snapshot_restore },
|
||||
check_plugin: { args: { options: '' }, call: m_check_plugin },
|
||||
check_kernel: { args: { options: '' }, call: m_check_kernel },
|
||||
check_firmware: { args: { options: '' }, call: m_check_firmware },
|
||||
start_plugin: { call: m_start_plugin },
|
||||
start_kernel: { call: m_start_kernel },
|
||||
start_update: { args: { amlogic_update_sel: '' }, call: m_start_update },
|
||||
start_install: { args: { amlogic_install_sel: '' }, call: m_start_install },
|
||||
start_rescue: { call: m_start_rescue },
|
||||
read_log_tail: { args: { name: '' }, call: m_read_log_tail },
|
||||
read_log_full: { args: { name: '' }, call: m_read_log_full },
|
||||
del_log: { call: m_del_log },
|
||||
poweroff: { call: m_poweroff },
|
||||
kvm_switch: { call: m_kvm_switch },
|
||||
check_upfiles: { call: m_check_upfiles },
|
||||
upload_path: { call: m_upload_path },
|
||||
upload_chunk: { args: { name: '', data: '', append: false }, call: m_upload_chunk },
|
||||
list_uploads: { call: m_list_uploads },
|
||||
delete_upload: { args: { name: '' }, call: m_delete_upload },
|
||||
install_upload: { args: { name: '' }, call: m_install_upload },
|
||||
backup_create: { call: m_backup_create },
|
||||
save_backup_list:{ args: { content: '' }, call: m_save_backup_list },
|
||||
prime_backup_list:{ call: m_prime_backup_list },
|
||||
reload_cpu: { call: m_reload_cpu },
|
||||
sync_menu: { call: m_sync_menu },
|
||||
cpu_policies: { call: m_cpu_policies }
|
||||
};
|
||||
|
||||
return { 'luci.amlogic': methods };
|
||||
@@ -7,8 +7,8 @@
|
||||
include $(TOPDIR)/rules.mk
|
||||
|
||||
PKG_NAME:=luci-app-passwall
|
||||
PKG_VERSION:=26.5.3
|
||||
PKG_RELEASE:=117
|
||||
PKG_VERSION:=26.5.11
|
||||
PKG_RELEASE:=118
|
||||
PKG_PO_VERSION:=$(PKG_VERSION)
|
||||
|
||||
PKG_CONFIG_DEPENDS:= \
|
||||
|
||||
@@ -54,7 +54,7 @@ function index()
|
||||
entry({"admin", "services", appname, "socks_config"}, cbi(appname .. "/client/socks_config")).leaf = true
|
||||
entry({"admin", "services", appname, "acl"}, cbi(appname .. "/client/acl"), _("Access control"), 98).leaf = true
|
||||
entry({"admin", "services", appname, "acl_config"}, cbi(appname .. "/client/acl_config")).leaf = true
|
||||
entry({"admin", "services", appname, "log"}, form(appname .. "/client/log"), _("Watch Logs"), 999).leaf = true
|
||||
entry({"admin", "services", appname, "log"}, form(appname .. "/client/log"), _("Runtime Logs"), 999).leaf = true
|
||||
|
||||
--[[ Server ]]
|
||||
entry({"admin", "services", appname, "server"}, cbi(appname .. "/server/index"), _("Server-Side"), 99).leaf = true
|
||||
|
||||
@@ -1267,6 +1267,9 @@ function gen_config(var)
|
||||
else
|
||||
ut_nodes = _node.urltest_node
|
||||
end
|
||||
|
||||
api.log(" - 加载 Sing-Box URLTest 节点【" .. (_node.remarks or "") .. "】,子节点数量:" .. #(ut_nodes or {}))
|
||||
|
||||
local valid_nodes = {}
|
||||
for i = 1, #(ut_nodes or {}) do
|
||||
local ut_node_id = ut_nodes[i]
|
||||
|
||||
@@ -1011,6 +1011,9 @@ function gen_config(var)
|
||||
else
|
||||
blc_nodes = _node.balancing_node
|
||||
end
|
||||
|
||||
api.log(" - 加载 Xray 负载均衡 节点【" .. (_node.remarks or "") .. "】,子节点数量:" .. #(blc_nodes or {}))
|
||||
|
||||
local valid_nodes = {}
|
||||
for i = 1, #(blc_nodes or {}) do
|
||||
local blc_node_id = blc_nodes[i]
|
||||
|
||||
@@ -100,8 +100,8 @@ msgstr "规则列表"
|
||||
msgid "Access control"
|
||||
msgstr "访问控制"
|
||||
|
||||
msgid "Watch Logs"
|
||||
msgstr "查看日志"
|
||||
msgid "Runtime Logs"
|
||||
msgstr "运行日志"
|
||||
|
||||
msgid "Node Config"
|
||||
msgstr "节点配置"
|
||||
|
||||
@@ -2,8 +2,8 @@
|
||||
|
||||
include $(TOPDIR)/rules.mk
|
||||
|
||||
PKG_VERSION:=1.1.7-r1
|
||||
PKG_RELEASE:=3
|
||||
PKG_VERSION:=1.1.8-r1
|
||||
PKG_RELEASE:=4
|
||||
|
||||
LUCI_TITLE:=LuCI support for shadowrt
|
||||
LUCI_PKGARCH:=all
|
||||
|
||||
@@ -5,7 +5,8 @@ mkdir -p /var/lock
|
||||
for service in adb-enablemodem appfilter dockerd gpio_switch hd-idle \
|
||||
kmods kmods-unload lcdsimple led lm-sensors luci-fan \
|
||||
mdadm modemmanager odhcpd smartd sysfixtime sysfsutils sysntpd \
|
||||
tuning_net umount usbmode usbmuxd wan_drop zprintk zram
|
||||
tuning_net umount usbmode usbmuxd wan_drop zprintk zram \
|
||||
log
|
||||
do
|
||||
[ -x /etc/init.d/$service ] && /etc/init.d/$service disable
|
||||
done
|
||||
|
||||
@@ -2,7 +2,7 @@ include $(TOPDIR)/rules.mk
|
||||
|
||||
PKG_NAME:=qBittorrent-Enhanced-Edition
|
||||
PKG_VERSION:=5.1.3.10
|
||||
PKG_RELEASE:=5
|
||||
PKG_RELEASE:=6
|
||||
|
||||
PKG_SOURCE:=$(PKG_NAME)-$(PKG_VERSION).tar.gz
|
||||
PKG_SOURCE_URL:=https://codeload.github.com/c0re100/qBittorrent-Enhanced-Edition/tar.gz/release-$(PKG_VERSION)?
|
||||
|
||||
@@ -37,8 +37,9 @@ start_service() {
|
||||
program=
|
||||
|
||||
[BitTorrent]
|
||||
Session\AddTrackersFromURLEnabled=true
|
||||
Session\AdditionalTrackersURL=https://trackerslist.com/all.txt
|
||||
Session\AutoBanUnknownPeer=true
|
||||
Session\AutoUpdateTrackersEnabled=true
|
||||
Session\DefaultSavePath=$download_dir
|
||||
|
||||
[LegalNotice]
|
||||
@@ -48,7 +49,6 @@ start_service() {
|
||||
Cookies=@Invalid()
|
||||
|
||||
[Preferences]
|
||||
Bittorrent\CustomizeTrackersListUrl=https://trackerslist.com/all.txt
|
||||
WebUI\CSRFProtection=false
|
||||
WebUI\Password_PBKDF2="@ByteArray(zH92gnE9xijRN5IjzIPU+A==:+JxpKBWsSyuzpm/9LrhO2uLQSGsBS5giqF0AYRU8COcMXn5AIaSsL2S9hIuB20wkhsMfEoN+77Q9BmAd3ysYxw==)"
|
||||
WebUI\Port=$http_port
|
||||
|
||||
Reference in new issue
Block a user