// SPDX-License-Identifier: GPL-2.0 // Online Download Update + Rescue Kernel // // Purpose: check / download / install plugin, kernel, and firmware updates online. // Log tail is polled at 1 Hz; the last log line drives a state machine // (idle → checking → button → installing → done). Terminal log lines contain // raw HTML button strings which are parsed and rendered as DOM buttons. // Backend RPC: /usr/share/rpcd/ucode/luci.amlogic (state, check_*, start_*, read_log_tail). 'use strict'; 'require view'; 'require rpc'; 'require ui'; 'require dom'; 'require poll'; 'require view.amlogic.shared as amlogicShared'; // ── RPCs ───────────────────────────────────────────────────────────────────── // Query system state (firmware / plugin / kernel versions). const callState = rpc.declare({ object: 'luci.amlogic', method: 'state' }); // Run plugin / kernel / firmware version check and write progress to the log. const callCheckPlugin = rpc.declare({ object: 'luci.amlogic', method: 'check_plugin', params: ['options'] }); const callCheckKernel = rpc.declare({ object: 'luci.amlogic', method: 'check_kernel', params: ['options'] }); const callCheckFirmware = rpc.declare({ object: 'luci.amlogic', method: 'check_firmware', params: ['options'] }); 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' }); // Read the last line (up to 4096 bytes) of the named log file. const callLogTail = rpc.declare({ object: 'luci.amlogic', method: 'read_log_tail', params: ['name'], expect: { line: '' } }); // ── Log-line HTML button parser ────────────────────────────────────────────── // Parse an button string from the log; return null for plain-text lines. // Returned object: { type, param, label } where type identifies the action. function parseButtonLine(line) { if (!line || line.indexOf('(.*)$/); 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' // Action object factory: { name, statusEl, state, subBtn } 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) ──────────────────────────────────────────── // Inspect the last non-empty log line and advance the action state machine. function handlePoll(action) { if (action.state === 'done') return; return callLogTail(action.name).then(function (raw) { // Only inspect the last non-empty line to avoid false-positive // 'Failed'/'error' matches from intermediate opkg/apk output. 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('