luci: updater: Add new func tools.execAndRead

This commit is contained in:
remittor
2026-01-10 20:51:11 +03:00
parent 17afabe150
commit ab50c2099b
3 changed files with 170 additions and 126 deletions

View File

@@ -6,6 +6,7 @@ return baseclass.extend({
appName : 'zapret2', appName : 'zapret2',
AppName : 'Zapret2', AppName : 'Zapret2',
execPath : '/etc/init.d/zapret2', execPath : '/etc/init.d/zapret2',
appDir : '/opt/zapret2',
syncCfgPath : '/opt/zapret2/sync_config.sh', syncCfgPath : '/opt/zapret2/sync_config.sh',
defCfgPath : '/opt/zapret2/def-cfg.sh', defCfgPath : '/opt/zapret2/def-cfg.sh',
defaultCfgPath : '/opt/zapret2/restore-def-cfg.sh', defaultCfgPath : '/opt/zapret2/restore-def-cfg.sh',

View File

@@ -562,4 +562,93 @@ return baseclass.extend({
}, },
}), }),
execAndRead: async function({ cmd = [ ], log = '', logArea = null, callback = null, cbarg = null, hiderow = [ ], rpc_timeout = 5, rpc_root = false } = {})
{
function appendLog(msg, end = '\n')
{
logArea.value += msg + end;
logArea.scrollTop = logArea.scrollHeight;
}
function fixLogEnd()
{
if (logArea.value && logArea.value.slice(-1) != '\n') {
appendLog('');
}
}
let hide_rows = Array.isArray(hiderow) ? hiderow : [ hiderow ];
let rpc_opt = { "timeout": rpc_timeout*1000 };
if (rpc_root) {
rpc_opt.uid = 0; // run under root
}
const logFile = log; // file for reading: '/tmp/zapret_pkg_install.log'
const rcFile = logFile + '.rc';
try {
await fs.exec('/bin/busybox', [ 'rm', '-f', logFile + '*' ], null, rpc_opt);
appendLog('Output file cleared!');
} catch (e) {
return callback(cbarg, 500, 'ERROR: Failed to clear output file');
}
try {
let opt_list = [ logFile ];
opt_list.push(...cmd);
let res = await fs.exec(this.appDir+'/script-exec.sh', opt_list, null, rpc_opt);
if (res.code != 0) {
return callback(cbarg, 525, 'ERROR: cannot run "' + cmd[0] + '" script! (error = ' + res.code + ')');
}
appendLog('Process started...');
} catch (e) {
return callback(cbarg, 520, 'ERROR: Failed on execute process: ' + e.message);
}
let lastLen = 0;
let retCode = -1;
let timerBusy = false;
let timer = setInterval(async () => {
if (timerBusy)
return; // skip iteration
timerBusy = true;
try {
let res = await fs.exec('/bin/cat', [ logFile ], null, rpc_opt);
if (res.stdout && res.stdout.length > lastLen) {
let log = res.stdout.slice(lastLen);
hide_rows.forEach(re => {
log = log.replace(re, '');
});
appendLog(log, '');
lastLen = res.stdout.length;
}
if (retCode < 0) {
let rc = await fs.exec('/bin/cat', [ rcFile ], null, rpc_opt);
if (rc.code != 0) {
clearInterval(timer);
fixLogEnd();
return callback(cbarg, 545, 'ERROR: cannot read file "' + rcFile + '"');
}
if (rc.stdout) {
retCode = parseInt(rc.stdout.trim(), 10);
}
}
if (retCode >= 0) {
clearInterval(timer);
fixLogEnd();
if (retCode == 0 && res.stdout) {
return callback(cbarg, 0, res.stdout);
}
return callback(cbarg, retCode, 'ERROR: Process failed with error ' + retCode);
}
} catch (e) {
if (e.message?.includes('RPC call to file/exec failed with error -32000: Object not found')) {
console.warn('WARN: execAndRead: ' + e.message);
return; // goto next timer iteration
}
clearInterval(timer);
fixLogEnd();
let errtxt = 'ERROR: execAndRead: ' + e.message;
errtxt += 'ERROR: execAndRead: ' + e.stack?.trim().split('\n')[0];
return callback(cbarg, 540, errtxt);
} finally {
timerBusy = false;
}
}, 500);
},
}); });

View File

@@ -19,17 +19,20 @@ const fn_update_pkg_sh = '/opt/'+tools.appName+'/update-pkg.sh';
return baseclass.extend({ return baseclass.extend({
releasesUrlPrefix : 'https://raw.githubusercontent.com/remittor/zapret-openwrt/gh-pages/releases/', releasesUrlPrefix : 'https://raw.githubusercontent.com/remittor/zapret-openwrt/gh-pages/releases/',
appendLog: function(msg, end = '\n') { appendLog: function(msg, end = '\n')
{
this.logArea.value += msg + end; this.logArea.value += msg + end;
this.logArea.scrollTop = this.logArea.scrollHeight; this.logArea.scrollTop = this.logArea.scrollHeight;
}, },
setBtnMode: function(enable) { setBtnMode: function(enable)
{
this.btn_cancel.disabled = enable ? false : true; this.btn_cancel.disabled = enable ? false : true;
this.btn_action.disabled = (enable == 2) ? false : true; this.btn_action.disabled = (enable == 2) ? false : true;
}, },
setStage: function(stage, btn_flag = true) { setStage: function(stage, btn_flag = true)
{
if (stage == 0) { if (stage == 0) {
this.btn_action.textContent = _('Check for updates'); this.btn_action.textContent = _('Check for updates');
this.btn_action.classList.remove('hidden'); this.btn_action.classList.remove('hidden');
@@ -46,146 +49,94 @@ return baseclass.extend({
this.stage = stage; this.stage = stage;
}, },
checkUpdates: function() { checkUpdates: async function()
{
this._action = 'checkUpdates';
this.setStage(0); this.setStage(0);
this.setBtnMode(0); this.setBtnMode(0);
this.pkg_url = null; this.pkg_url = null;
this.appendLog(_('Checking for updates...')); this.appendLog(_('Checking for updates...'));
let opt_list = [ '-c' ]; // check for updates let cmd = [ fn_update_pkg_sh, '-c' ]; // check for updates
if (document.getElementById('cfg_exclude_prereleases').checked == false) { if (document.getElementById('cfg_exclude_prereleases').checked == false) {
opt_list.push('-p'); // include prereleases ZIP-files cmd.push('-p'); // include prereleases ZIP-files
} }
let forced_reinstall = document.getElementById('cfg_forced_reinstall').checked; this.forced_reinstall = document.getElementById('cfg_forced_reinstall').checked;
let rpc_opt = { timeout: 20*1000 } let log = '/tmp/'+tools.appName+'_pkg_check.log';
//rpc_opt.uid = 0; // run under root let callback = this.execAndReadCallback;
let res = fs.exec(fn_update_pkg_sh, opt_list, null, rpc_opt).then(res => { let wnd = this;
let log = res.stdout.trim(); return tools.execAndRead({ cmd: cmd, log: log, logArea: this.logArea, callback: callback, cbarg: wnd });
this.appendLog(log);
let code = log.match(/^RESULT:\s*\(([^)]+)\)\s+.+$/m);
let pkg_url = log.match(/^ZAP_PKG_URL\s*=\s*(.+)$/m);
if (res.code == 0 && code && pkg_url) {
this.pkg_url = pkg_url[1];
code = code[1];
if (code == 'E' && !forced_reinstall) {
this.setStage(999);
return 0;
}
this.setStage(1);
this.setBtnMode(2); // enable all buttons
} else {
if (res.code != 0) {
this.appendLog('ERROR: Check for updates failed with error ' + res.code);
}
this.setStage(999);
}
return res.code;
}).catch(e => {
this.appendLog('ERROR: ' + _('Updates checking failed'));
this.appendLog('ERROR: ' + e);
this.setStage(999);
return 1;
}).finally(() => {
this.appendLog('=========================================================');
});
}, },
installUpdates: async function() { installUpdates: async function()
{
this._action = 'installUpdates';
this.setStage(1); this.setStage(1);
this.setBtnMode(0); this.setBtnMode(0);
if (!this.pkg_url || this.pkg_url.length < 10) { if (!this.pkg_url || this.pkg_url.length < 10) {
this.appendLog('ERROR: pkg_url = null'); this.appendLog('ERROR: pkg_url = null');
this.setStage(999); this.setStage(999);
return 1; return;
} }
this.appendLog(_('Install updates...')); this.appendLog(_('Install updates...'));
let opt_list = [ '-u', this.pkg_url ]; // update packages let cmd = [ fn_update_pkg_sh, '-u', this.pkg_url ]; // update packages
if (document.getElementById('cfg_forced_reinstall').checked == true) { if (document.getElementById('cfg_forced_reinstall').checked == true) {
opt_list.push('-f'); // forced reinstall if same version cmd.push('-f'); // forced reinstall if same version
} }
let rpc_opt = { timeout: 5*1000 } //this._test = 1; cmd.push('-t'); cmd.push('45'); // only for testing
//rpc_opt.uid = 0; // run under root let log = '/tmp/'+tools.appName+'_pkg_install.log';
const logFile = '/tmp/'+tools.appName+'_pkg_install.log'; let hiderow = /^ \* resolve_conffiles.*(?:\r?\n|$)/gm;
const rcFile = logFile + '.rc'; let callback = this.execAndReadCallback;
try { let wnd = this;
await fs.exec('/bin/busybox', [ 'rm', '-f', logFile + '*' ], null, rpc_opt); return tools.execAndRead({ cmd: cmd, log: log, logArea: this.logArea, hiderow: hiderow, callback: callback, cbarg: wnd });
this.appendLog('Install log cleared.');
} catch (e) {
this.appendLog('ERROR: Failed to clear log file');
this.setStage(999);
return 1;
}
try {
let opt = [ logFile, fn_update_pkg_sh ];
//opt.push('-t'); opt.push('0'); // only for testing
opt.push(...opt_list);
let res = await fs.exec('/opt/'+tools.appName+'/script-exec.sh', opt, null, rpc_opt);
if (res.code == 0) {
this.appendLog('Process started...');
} else {
this.appendLog('ERROR: cannot run ' + fn_update_pkg_sh + ' script! (error = ' + res.code + ')');
throw new Error('cannot run script');
}
} catch (e) {
this.appendLog('ERROR: Failed to start process: ' + e.message);
this.setStage(999);
return 1;
}
let lastLen = 0;
let retCode = -1;
let timerBusy = false;
let timer = setInterval(async () => {
if (timerBusy)
return; // skip iteration
timerBusy = true;
try {
let res = await fs.exec('/bin/cat', [ logFile ], null, rpc_opt);
if (res.stdout && res.stdout.length > lastLen) {
let log = res.stdout.slice(lastLen);
log = log.replace(/^ \* resolve_conffiles.*(?:\r?\n|$)/gm, '');
this.appendLog(log, '');
lastLen = res.stdout.length;
}
if (retCode < 0) {
let rc = await fs.exec('/bin/cat', [ rcFile ], null, rpc_opt);
if (rc.code != 0) {
throw new Error('cannot read file "' + rcFile + '"');
}
if (rc.stdout) {
retCode = parseInt(rc.stdout.trim(), 10);
}
}
if (retCode >= 0) {
clearInterval(timer);
this.appendLog('\n' + 'Process finished.');
if (res.stdout) {
let code = res.stdout.match(/^RESULT:\s*\(([^)]+)\)\s+.+$/m);
if (retCode == 0 && code && code[1] == '+') {
this.stage = 999;
this.btn_action.textContent = _('OK');
this.btn_action.disabled = false;
this.btn_cancel.disabled = true;
return 0;
}
}
this.appendLog('ERROR: Install updates failed with error ' + retCode);
this.setStage(999);
}
} catch (e) {
if (e.message?.includes('RPC call to file/exec failed with error -32000: Object not found')) {
console.warn('WARN: installUpdates: ' + e.message);
return; // goto next timer iteration
}
clearInterval(timer);
this.appendLog('ERROR: installUpdates: ' + e.message);
this.appendLog('ERROR: installUpdates: ' + e.stack?.trim().split('\n')[0]);
this.setStage(999);
} finally {
timerBusy = false;
}
}, 500);
}, },
openUpdateDialog: function(pkg_arch) { execAndReadCallback: function(wnd, rc, txt = '')
{
//console.log('execAndReadCallback = ' + rc + '; _action = ' + wnd._action);
if (rc == 0 && txt) {
let code = txt.match(/^RESULT:\s*\(([^)]+)\)\s+.+$/m);
if (wnd._action == 'checkUpdates') {
let pkg_url = txt.match(/^ZAP_PKG_URL\s*=\s*(.+)$/m);
if (code && pkg_url) {
wnd.appendLog('=========================================================');
wnd.pkg_url = pkg_url[1];
code = code[1];
if (code == 'E' && !wnd.forced_reinstall) {
wnd.setStage(999); // install not needed
return;
}
wnd.setStage(1);
wnd.setBtnMode(2); // enable all buttons
return; // install allowed
}
}
if (wnd._action == 'installUpdates') {
if (wnd._test || (code && code[1] == '+')) {
wnd.stage = 999;
wnd.btn_action.textContent = _('OK');
wnd.btn_action.disabled = false;
wnd.btn_cancel.disabled = true;
return;
}
}
}
if (rc >= 500) {
if (txt) {
wnd.appendLog(txt.startsWith('ERROR') ? txt : 'ERROR: ' + txt);
} else {
wnd.appendLog('ERROR: ' + wnd._action + ': Terminated with error code = ' + rc);
}
} else {
wnd.appendLog('ERROR: Process finished with retcode = ' + rc);
}
wnd.setStage(999);
if (wnd._action == 'checkUpdates') {
wnd.appendLog('=========================================================');
}
},
openUpdateDialog: function(pkg_arch)
{
this.stage = 0; this.stage = 0;
this.pkg_arch = pkg_arch; this.pkg_arch = pkg_arch;
this.pkg_url = null; this.pkg_url = null;
@@ -201,8 +152,11 @@ return baseclass.extend({
]); ]);
this.logArea = E('textarea', { this.logArea = E('textarea', {
'id': 'widget.modal_content',
'readonly': true, 'readonly': true,
'style': 'width:100%; height:400px; font-family: monospace;' 'style': 'width:100% !important; font-family: monospace;',
'rows': 20,
'wrap': 'off',
}); });
this.btn_cancel = E('button', { this.btn_cancel = E('button', {