mirror of
https://github.com/kiddin9/op-packages.git
synced 2026-08-02 23:49:34 +08:00
341 lines
11 KiB
JavaScript
341 lines
11 KiB
JavaScript
// SPDX-License-Identifier: Apache-2.0
|
||
|
||
'use strict';
|
||
'require baseclass';
|
||
'require fs';
|
||
'require poll';
|
||
'require view.daede.backend as backend';
|
||
|
||
function notifyAction() {}
|
||
|
||
function execChecked(command, args) {
|
||
return fs.exec(command, args || []).then(function(res) {
|
||
if (res && res.code !== 0)
|
||
throw new Error((res.stderr || res.stdout || ('exit ' + res.code)).trim());
|
||
return res;
|
||
});
|
||
}
|
||
|
||
function execInit(be, action) {
|
||
return fs.exec(be.initd, [action]).then(function(res) {
|
||
notifyAction(action, res);
|
||
}).catch(function() {});
|
||
}
|
||
|
||
function rejectIfOtherRunning(be, running) {
|
||
const other = be.name === 'dae' ? 'daed' : 'dae';
|
||
if (!running || !running[other])
|
||
return Promise.resolve();
|
||
|
||
return Promise.reject(new Error(_('%s is already running. Stop %s before starting %s because both backends share the same eBPF/cgroup attachment.').format(other, other, be.name)));
|
||
}
|
||
|
||
function toggleService(be, turnOn) {
|
||
const enabled = turnOn ? '1' : '0';
|
||
const action = turnOn ? 'start' : 'stop';
|
||
|
||
// Only guard on start: two backends share the eBPF/cgroup attachment, so
|
||
// starting one while the other runs is unsafe. Stopping is always allowed.
|
||
const guard = turnOn
|
||
? backend.detectRunning().then(function(running) { return rejectIfOtherRunning(be, running); })
|
||
: Promise.resolve();
|
||
|
||
return guard
|
||
.then(function() { return execChecked('/sbin/uci', ['set', be.uci + '.config.enabled=' + enabled]); })
|
||
.then(function() { return execChecked('/sbin/uci', ['commit', be.uci]); })
|
||
.then(function() {
|
||
if (turnOn)
|
||
return execChecked(be.initd, ['enable']);
|
||
return execChecked(be.initd, ['disable']);
|
||
})
|
||
.then(function() {
|
||
if (turnOn && be.useNetns)
|
||
return fs.exec('/sbin/ip', ['netns', 'del', 'daens']).catch(function() {});
|
||
})
|
||
.then(function() { return execChecked(be.initd, [action]); });
|
||
}
|
||
|
||
function waitForService(be, turnOn, attempts) {
|
||
return backend.serviceStatus(be.name).then(function(state) {
|
||
if (!!state.running === turnOn)
|
||
return state;
|
||
if (attempts <= 0)
|
||
throw new Error(turnOn ? _('Service did not start in time.') : _('Service did not stop in time.'));
|
||
return new Promise(function(resolve) { setTimeout(resolve, 250); })
|
||
.then(function() { return waitForService(be, turnOn, attempts - 1); });
|
||
});
|
||
}
|
||
|
||
function renderBackendSwitcher(ctx, onSwitched, initialHint) {
|
||
if (!ctx.installed.dae && !ctx.installed.daed)
|
||
return null;
|
||
|
||
const wrap = E('div', { 'class': 'dd-card dd-backend-card' }, [
|
||
E('h4', { 'class': 'dd-card-title' }, _('Backend')),
|
||
E('div', { 'class': 'dd-backend-row' }, [
|
||
E('span', { 'class': 'dd-backend-label' }, _('Active backend')),
|
||
E('div', { 'class': 'dd-backend-segment' })
|
||
])
|
||
]);
|
||
const segment = wrap.querySelector('.dd-backend-segment');
|
||
const hint = E('div', { 'class': 'dd-backend-help' }, initialHint || '');
|
||
let busy = false;
|
||
|
||
const showHint = function(msg) {
|
||
hint.textContent = msg;
|
||
};
|
||
|
||
const stopIfRunning = function(running) {
|
||
const stops = [];
|
||
|
||
['dae', 'daed'].forEach(function(name) {
|
||
if (running && running[name])
|
||
stops.push(execChecked(backend.BACKENDS[name].initd, ['stop'])
|
||
.then(function() { return waitForService(backend.BACKENDS[name], false, 40); }));
|
||
});
|
||
|
||
if (stops.length)
|
||
showHint(_('Stopping current backend…'));
|
||
|
||
return Promise.all(stops);
|
||
};
|
||
|
||
['daed', 'dae'].forEach(function(name) {
|
||
const active = ctx.name === name;
|
||
const installed = !!ctx.installed[name];
|
||
const btn = E('button', {
|
||
'class': 'dd-backend-btn' + (active ? ' is-active' : ''),
|
||
'type': 'button',
|
||
'disabled': null,
|
||
'title': installed ? _('Switch to %s').format(name) : _('%s is not installed').format(name)
|
||
}, [
|
||
E('span', {}, name),
|
||
E('span', { 'class': 'dd-backend-state' }, active ? _('Active') : (installed ? '' : _('Not installed')))
|
||
]);
|
||
btn.addEventListener('click', function(ev) {
|
||
ev.preventDefault();
|
||
if (busy || active)
|
||
return;
|
||
if (!installed) {
|
||
showHint(_('%s is not installed. Install it first, then switch backend.').format(name));
|
||
return;
|
||
}
|
||
|
||
busy = true;
|
||
Array.prototype.forEach.call(segment.querySelectorAll('button'), function(b) { b.disabled = true; });
|
||
showHint(_('Switching to %s…').format(name));
|
||
|
||
backend.detectRunning()
|
||
.then(stopIfRunning)
|
||
.then(function() { return backend.setActiveBackend(name); })
|
||
.then(function() {
|
||
const message = _('Switched to %s. Click start when you want it to run.').format(name);
|
||
showHint(message);
|
||
return onSwitched ? onSwitched(name, message) : null;
|
||
})
|
||
.catch(function(e) {
|
||
busy = false;
|
||
Array.prototype.forEach.call(segment.querySelectorAll('button'), function(b) { b.disabled = false; });
|
||
showHint(_('Switch failed.'));
|
||
});
|
||
});
|
||
segment.appendChild(btn);
|
||
});
|
||
|
||
wrap.appendChild(hint);
|
||
return wrap;
|
||
}
|
||
|
||
function renderStatusCard(ctx, listenAddr) {
|
||
const be = ctx.backend;
|
||
let busy = false;
|
||
let lastError = '';
|
||
let refreshGeneration = 0;
|
||
const body = E('div', { 'id': 'dd-status-body' }, E('em', {}, _('Collecting data…')));
|
||
const card = E('div', { 'class': 'dd-card dd-status-card' }, [
|
||
E('h4', { 'class': 'dd-card-title' }, _('Service Status')),
|
||
body
|
||
]);
|
||
|
||
const render = function(state) {
|
||
while (body.firstChild) body.removeChild(body.firstChild);
|
||
|
||
const badge = state.running
|
||
? E('span', { 'class': 'dd-badge dd-badge-run' }, [ E('span', { 'class': 'dd-badge-dot' }), _('RUNNING') ])
|
||
: E('span', { 'class': 'dd-badge dd-badge-stop' }, [ E('span', { 'class': 'dd-badge-dot' }), _('STOPPED') ]);
|
||
|
||
const meta = [
|
||
E('span', { 'class': 'dd-meta' }, [ E('span', { 'class': 'dd-meta-label' }, _('Backend')), be.name ])
|
||
];
|
||
|
||
if (state.running && state.pid)
|
||
meta.push(E('span', { 'class': 'dd-meta' }, [ E('span', { 'class': 'dd-meta-label' }, 'PID'), state.pid ]));
|
||
|
||
const swErr = E('span', { 'class': 'dd-meta dd-err', 'style': lastError ? '' : 'display:none' }, lastError);
|
||
const sw = E('button', { 'class': 'dd-switch' + (state.running ? ' is-on' : ''), 'type': 'button', 'aria-label': _('Toggle service') }, [
|
||
E('span', { 'class': 'dd-switch-knob' })
|
||
]);
|
||
sw.addEventListener('click', function(ev) {
|
||
ev.preventDefault();
|
||
if (busy) return;
|
||
refreshGeneration++;
|
||
busy = true;
|
||
sw.disabled = true;
|
||
lastError = '';
|
||
swErr.style.display = 'none';
|
||
const turnOn = !state.running;
|
||
/* instant optimistic feedback — the start/stop chain (esp. dae's eBPF
|
||
load) takes a few seconds; flip the switch and show a pending label
|
||
right away instead of looking frozen */
|
||
sw.classList.toggle('is-on', turnOn);
|
||
const lbl = sw.parentNode && sw.parentNode.querySelector('.dd-switch-label');
|
||
if (lbl) lbl.textContent = '…';
|
||
toggleService(be, turnOn)
|
||
.then(function() { return waitForService(be, turnOn, 40); })
|
||
.then(function() {
|
||
busy = false;
|
||
return refresh(true);
|
||
})
|
||
.catch(function(e) {
|
||
busy = false;
|
||
lastError = _('Toggle failed: %s').format(e.message || e);
|
||
return refresh(true);
|
||
});
|
||
});
|
||
|
||
/* line 1: badge + toggle (always aligned, never wraps); line 2: meta */
|
||
const row = E('div', { 'class': 'dd-status-row' }, [
|
||
badge,
|
||
E('span', { 'class': 'dd-grow' }),
|
||
swErr,
|
||
E('span', { 'class': 'dd-switch-wrap' }, [
|
||
E('span', { 'class': 'dd-switch-label' }, state.running ? 'ON' : 'OFF'),
|
||
sw
|
||
])
|
||
]);
|
||
body.appendChild(row);
|
||
if (meta.length)
|
||
body.appendChild(E('div', { 'class': 'dd-status-meta' }, meta));
|
||
|
||
const actions = [];
|
||
if (be.hasWebUI && state.running) {
|
||
const port = (listenAddr || be.defaultListen).split(':').slice(-1)[0];
|
||
actions.push(E('a', {
|
||
'class': 'cbi-button cbi-button-action',
|
||
'href': 'http://%s:%s'.format(window.location.hostname, port),
|
||
'target': '_blank',
|
||
'rel': 'noreferrer noopener'
|
||
}, _('Open WebUI')));
|
||
}
|
||
if (state.running) {
|
||
const restart = E('button', { 'class': 'cbi-button cbi-button-positive' }, _('Restart'));
|
||
restart.addEventListener('click', function(ev) {
|
||
ev.preventDefault();
|
||
restart.disabled = true;
|
||
execInit(be, 'restart').finally(function() { restart.disabled = false; });
|
||
});
|
||
actions.push(restart);
|
||
}
|
||
if (state.running && be.name === 'dae') {
|
||
const hot = E('button', { 'class': 'cbi-button cbi-button-action' }, _('Hot Reload'));
|
||
hot.addEventListener('click', function(ev) {
|
||
ev.preventDefault();
|
||
hot.disabled = true;
|
||
execInit(be, 'hot_reload').finally(function() { hot.disabled = false; });
|
||
});
|
||
actions.push(hot);
|
||
}
|
||
if (state.running && be.name === 'dae') {
|
||
const ckBtn = E('button', { 'class': 'cbi-button cbi-button-action' }, _('Test YouTube'));
|
||
const ckRes = E('span', { 'class': 'dd-meta', style: 'margin-left:8px;display:none' }, '');
|
||
ckBtn.addEventListener('click', function(ev) {
|
||
ev.preventDefault();
|
||
ckBtn.disabled = true;
|
||
ckRes.style.display = 'inline';
|
||
ckRes.classList.remove('dd-ok', 'dd-err');
|
||
ckRes.textContent = _('Testing…');
|
||
/* actually probe YouTube through dae's transparent proxy */
|
||
fs.exec('/usr/share/luci-app-daede/proxy-check.sh', []).then(function(r) {
|
||
const out = (r && r.stdout) || '';
|
||
const ok = /\bok=1\b/.test(out);
|
||
const code = (out.match(/code=(\S+)/) || [])[1] || '000';
|
||
const ms = parseInt((out.match(/ms=(\d+)/) || [])[1] || '0');
|
||
if (ok) {
|
||
ckRes.classList.add('dd-ok');
|
||
ckRes.textContent = _('YouTube reachable · %d ms').format(ms);
|
||
} else {
|
||
ckRes.classList.add('dd-err');
|
||
ckRes.textContent = (code === '000')
|
||
? _('YouTube unreachable (timeout)')
|
||
: _('YouTube unreachable (HTTP %s)').format(code);
|
||
}
|
||
}).catch(function() {
|
||
ckRes.classList.add('dd-err');
|
||
ckRes.textContent = _('YouTube unreachable (timeout)');
|
||
}).finally(function() {
|
||
ckBtn.disabled = false;
|
||
setTimeout(function() { ckRes.style.display = 'none'; }, 8000);
|
||
});
|
||
});
|
||
actions.push(ckBtn);
|
||
actions.push(ckRes);
|
||
}
|
||
if (actions.length)
|
||
body.appendChild(E('div', { 'class': 'dd-actions' }, actions));
|
||
};
|
||
|
||
const refresh = function(force) {
|
||
if (busy && !force)
|
||
return Promise.resolve();
|
||
const generation = refreshGeneration;
|
||
return backend.serviceStatus(be.name).then(function(state) {
|
||
if (generation !== refreshGeneration || (busy && !force)) return;
|
||
render(state);
|
||
});
|
||
};
|
||
|
||
poll.add(refresh);
|
||
refresh();
|
||
card._ddCleanup = function() { poll.remove(refresh); };
|
||
return card;
|
||
}
|
||
|
||
function wrapSettingsCard(title, descr, mapPromise, advTitle, advFields) {
|
||
return mapPromise.then(function(node) {
|
||
const advBody = E('div', { 'class': 'dd-adv-body' });
|
||
const advWrap = E('div', { 'class': 'dd-adv dd-closed' }, [
|
||
E('div', { 'class': 'dd-adv-bar' }, [
|
||
E('span', {}, advTitle),
|
||
E('span', { 'class': 'dd-adv-chevron' }, '›')
|
||
]),
|
||
advBody
|
||
]);
|
||
advWrap.firstChild.addEventListener('click', function() {
|
||
advWrap.classList.toggle('dd-closed');
|
||
});
|
||
|
||
(advFields || []).forEach(function(name) {
|
||
const row = node.querySelector('[data-name="' + name + '"]');
|
||
if (row) advBody.appendChild(row);
|
||
});
|
||
|
||
const host = node.querySelector('.cbi-section') || node;
|
||
if (advBody.firstChild) host.appendChild(advWrap);
|
||
|
||
const children = [ E('h4', { 'class': 'dd-card-title' }, title) ];
|
||
if (descr) children.push(E('div', { 'class': 'dd-settings-descr' }, descr));
|
||
children.push(node);
|
||
return E('div', { 'class': 'dd-card dd-settings-card' }, children);
|
||
});
|
||
}
|
||
|
||
return baseclass.extend({
|
||
|
||
renderStatusCard: renderStatusCard,
|
||
|
||
renderBackendSwitcher: renderBackendSwitcher,
|
||
|
||
wrapSettingsCard: wrapSettingsCard
|
||
|
||
});
|