luci-app-wol: Enables persistent configuration of hosts to wake up

Signed-off-by: Martin Devolder <martin.devolder2@gmail.com>
(cherry picked from commit fa74b609e9)
This commit is contained in:
mdevolde
2025-12-12 16:35:15 +01:00
committed by Paul Donald
parent 71d1d0cafe
commit 946f77ac26
4 changed files with 229 additions and 103 deletions

View File

@@ -1,8 +1,6 @@
'use strict';
'require view';
'require dom';
'require uci';
'require fs';
'require ui';
'require rpc';
'require form';
@@ -11,158 +9,283 @@
const ETHERWAKE_BIN = '/usr/bin/etherwake';
const WAKEONLAN_BIN = '/usr/bin/wakeonlan';
const PACKAGES_URL = 'admin/system/package-manager';
return view.extend({
formdata: { wol: {} },
outputText: '',
callStat: rpc.declare({
object: 'luci.wol',
method: 'stat',
params: [ ],
expect: { }
params: [],
expect: {}
}),
callExec: rpc.declare({
object: 'luci.wol',
method: 'exec',
params: [ 'name', 'args' ],
expect: { }
params: ['name', 'args'],
expect: {}
}),
callHostHints: rpc.declare({
object: 'luci-rpc',
method: 'getHostHints',
expect: { '': {} }
expect: {
'': {}
}
}),
load: function() {
option_install_etherwake() {
window.open(L.url(PACKAGES_URL) +
'?query=etherwake', '_blank', 'noopener');
},
option_install_wakeonlan() {
window.open(L.url(PACKAGES_URL) +
'?query=wakeonlan', '_blank', 'noopener');
},
load() {
return Promise.all([
L.resolveDefault(this.callStat()),
this.callHostHints(),
uci.load('etherwake')
uci.load('luci-wol')
]);
},
render([stat, hosts]) {
var has_ewk = stat && stat.etherwake,
has_wol = stat && stat.wakeonlan,
m, s, o;
const has_ewk = stat && stat.etherwake,
has_wol = stat && stat.wakeonlan;
let m, s, o;
this.formdata.has_ewk = has_ewk;
this.formdata.has_wol = has_wol;
// Check if at least one Wake on LAN utility is available, else show install buttons
if (!has_ewk && !has_wol) {
m = new form.Map('luci-wol', _('Wake on LAN'),
_('Wake on LAN is a mechanism to boot computers remotely in the local network.'));
m = new form.JSONMap(this.formdata, _('Wake on LAN'),
s = m.section(form.NamedSection, 'packages', 'packages',
_('Required Packages'),
_('At least one Wake on LAN utility is needed. Please install one of the following packages (some extra permissions may be required):'));
s.render = L.bind(function(view) {
return form.NamedSection.prototype.render.apply(this, arguments)
.then(L.bind(function(node) {
node.appendChild(E('div', {
'class': 'control-group'
}, [
E('button', {
'class': 'btn cbi-button-action',
'click': ui.createHandlerFn(view, 'option_install_etherwake', this.map),
'title': _('Install etherwake package')
}, [_('Install etherwake')]),
' ',
E('button', {
'class': 'btn cbi-button-action',
'click': ui.createHandlerFn(view, 'option_install_wakeonlan', this.map),
'title': _('Install wakeonlan package')
}, [_('Install wakeonlan')])
]));
return node;
}, this));
}, s, this);
return m.render();
}
m = new form.Map('luci-wol', _('Wake on LAN'),
_('Wake on LAN is a mechanism to boot computers remotely in the local network.'));
s = m.section(form.NamedSection, 'wol');
// Default settings section (used executable)
s = m.section(form.NamedSection, 'defaults', 'wol', _('Default Settings'));
if (has_ewk && has_wol) {
o = s.option(form.ListValue, 'executable', _('WoL program'),
_('Sometimes only one of the two tools works. If one fails, try the other one'));
o = s.option(form.ListValue, 'executable', _('Default WoL program'),
_('Choose the default Wake on LAN utility'));
o.value(ETHERWAKE_BIN, 'Etherwake');
o.value(WAKEONLAN_BIN, 'Wakeonlan');
o.default = ETHERWAKE_BIN;
o.onchange = function(ev, section_id, value) {
return m.save(null, true);
};
} else {
// If only one binary is available, show info message with install button for the other
o = s.option(form.DummyValue, '_info');
o.rawhtml = true;
o.default = E('div', {}, [
E('p', {}, [
_('Binary used') + ': ',
E('strong', {}, has_ewk ? 'Etherwake' : 'Wakeonlan')
]),
E('p', {
'style': 'margin-top: 10px'
},
_('You can also install the alternative Wake on LAN utility (some extra permissions may be required):')),
E('div', {
'class': 'control-group'
}, [
E('button', {
'class': 'btn cbi-button-action',
'click': ui.createHandlerFn(this, has_ewk ? 'option_install_wakeonlan' : 'option_install_etherwake'),
'title': _('Install the alternative Wake on LAN package')
}, [_('Install %s').format(has_ewk ? 'wakeonlan' : 'etherwake')])
])
]);
}
if (has_ewk) {
o = s.option(widgets.DeviceSelect, 'iface', _('Network interface to use'),
_('Specifies the interface the WoL packet is sent on'));
// Targets section with GridSection
s = m.section(form.GridSection, 'target', _('Wake on LAN Targets'), _('Configure hosts that can be woken up. Click the Wake button to send a magic packet.') + '<br>' + _('Note: wakeonlan binary does not support interface, broadcast, and password options (etherwake only).') + ' ' + _('These options will be ignored if wakeonlan is used.'));
o.default = uci.get('etherwake', 'setup', 'interface');
o.rmempty = false;
o.noaliases = true;
o.noinactive = true;
uci.sections('etherwake', 'target', function(section) {
if (section.mac && section.name) {
// Create a host entry if it doesn't exist
if (!hosts[section.mac]) {
hosts[section.mac] = { name: section.name };
}
}
});
if (has_wol)
o.depends('executable', ETHERWAKE_BIN);
}
o = s.option(form.Value, 'mac', _('Host to wake up'),
_('Choose the host to wake up or enter a custom MAC address to use'));
s.addremove = true;
s.anonymous = true;
s.sortable = true;
s.nodescriptions = true;
// Name column
o = s.option(form.Value, 'name', _('Name'), _('Mandatory'));
o.rmempty = false;
o.datatype = 'string';
L.sortedKeys(hosts).forEach(function(mac) {
o.value(mac, E([], [ mac, ' (', E('strong', [
hosts[mac].name ||
// MAC address column
o = s.option(form.Value, 'mac', _('MAC Address'), _('Mandatory'));
o.rmempty = false;
o.datatype = 'macaddr';
L.sortedKeys(hosts).forEach(function(mac) { // Add host hints, need 'getHostHints' acl (luci-rpc)
const hint = hosts[mac].name ||
L.toArray(hosts[mac].ipaddrs || hosts[mac].ipv4)[0] ||
L.toArray(hosts[mac].ip6addrs || hosts[mac].ipv6)[0] ||
'?'
]), ')' ]));
L.toArray(hosts[mac].ip6addrs || hosts[mac].ipv6)[0];
o.value(mac, hint ? '%s (%s)'.format(mac, hint) : mac);
});
// Interface column (only for etherwake)
if (has_ewk) {
o = s.option(form.Flag, 'broadcast', _('Send to broadcast address'));
if (has_wol)
o.depends('executable', ETHERWAKE_BIN);
o = s.option(widgets.DeviceSelect, 'iface', _('Interface'), _('Etherwake only')); // Network device selector widget, needs 'getNetworkDevices' acl (luci-rpc)
o.noaliases = true;
o.noinactive = true;
}
// Broadcast flag (only for etherwake)
if (has_ewk) {
o = s.option(form.Flag, 'broadcast', _('Broadcast'), _('Etherwake only'));
o.default = o.disabled;
}
// Password field (only for etherwake)
if (has_ewk) {
o = s.option(form.Value, 'password', _('Password'), _('Etherwake only'));
o.datatype = 'string';
o.placeholder = '00:22:44:66:88:aa or 192.168.1.1';
o.datatype = 'or(macaddr,ip4addr("nomask"))'; // Accept MAC or IPv4 address format
}
// When editing, set modal title to include target name
s.modaltitle = L.bind(function(section_id) {
var name = uci.get('luci-wol', section_id, 'name');
return _('Edit target') + (name ? ': ' + name : '');
}, this);
// Keep reference to GridSection for button handlers
const gridSection = s;
// Take default row actions and add "Wake" button
s.renderRowActions = L.bind(function(section_id) {
const defaultButtons = form.GridSection.prototype.renderRowActions.call(gridSection, section_id, _('Edit'));
const wakeButton = E('button', {
'class': 'cbi-button cbi-button-action',
'click': ui.createHandlerFn(this, function() {
return this.handleWakeup(section_id, has_ewk, has_wol);
})
}, _('Wake'));
const buttonContainer = defaultButtons.querySelector('div');
if (buttonContainer) {
buttonContainer.insertBefore(wakeButton, buttonContainer.firstChild);
}
return defaultButtons;
}, this);
// Output section, for wake results
s = m.section(form.NamedSection, 'output', 'wol', _('Output'));
s.anonymous = true;
s.render = L.bind(function() {
return E('div', {
'class': 'cbi-section'
}, [
E('h3', {}, _('Output')),
E('textarea', {
'readonly': true,
'rows': 10,
'style': 'width: 100%; font-family: monospace;',
'id': 'wol-output-text'
}, this.outputText)
]);
}, this);
return m.render();
},
handleWakeup: function(ev) {
var map = document.querySelector('#maincontent .cbi-map'),
data = this.formdata,
self = this;
handleWakeup(section_id, has_ewk, has_wol) {
const self = this;
const name = uci.get('luci-wol', section_id, 'name');
const mac = uci.get('luci-wol', section_id, 'mac');
return dom.callClassMethod(map, 'save').then(function() {
if (!data.wol.mac)
return alert(_('No target host specified!'));
// Determine which binary to use and verify availability
const defaultBin = uci.get('luci-wol', 'defaults', 'executable');
let bin = defaultBin || (has_ewk ? ETHERWAKE_BIN : WAKEONLAN_BIN);
var bin = data.wol.executable || (data.has_ewk ? ETHERWAKE_BIN : WAKEONLAN_BIN),
args = [];
if (bin == ETHERWAKE_BIN && !has_ewk)
bin = WAKEONLAN_BIN;
else if (bin == WAKEONLAN_BIN && !has_wol)
bin = ETHERWAKE_BIN;
if (bin == ETHERWAKE_BIN) {
args.push('-D', '-i', data.wol.iface);
// Build argument list based on selected binary
const args = [];
if (data.wol.broadcast == '1')
args.push('-b');
if (bin == ETHERWAKE_BIN) {
args.push('-D');
const iface = uci.get('luci-wol', section_id, 'iface');
if (iface)
args.push('-i', iface);
args.push(data.wol.mac);
}
else {
args.push(data.wol.mac);
}
const broadcast = uci.get('luci-wol', section_id, 'broadcast');
if (broadcast == '1')
args.push('-b');
ui.showModal(_('Waking host'), [
E('p', { 'class': 'spinning' }, [ _('Starting WoL utility…') ])
]);
return self.callExec(bin, args).then(function(res) {
ui.showModal(_('Waking host'), [
res.stdout ? E('p', [ res.stdout ]) : '',
res.stderr ? E('pre', [ res.stderr ]) : '',
E('div', { 'class': 'right' }, [
E('button', {
'class': 'cbi-button cbi-button-primary',
'click': ui.hideModal
}, [ _('Dismiss') ])
])
]);
}).catch(function(err) {
ui.hideModal();
ui.addNotification(null, [
E('p', [ _('Waking host failed: '), err ])
]);
});
const password = uci.get('luci-wol', section_id, 'password');
if (password)
args.push('-p', password);
args.push(mac);
} else {
args.push(mac);
}
// Execute the wake command and handle output
this.appendOutput(`Sending wakeup to ${name} (${mac})...\n`);
return this.callExec(bin, args).then(function(res) {
if (res.stdout)
self.appendOutput(res.stdout + '\n');
if (res.stderr)
self.appendOutput('Error: ' + res.stderr + '\n');
if (!res.stdout && !res.stderr)
self.appendOutput('Command completed with code ' + (res.code || 0) + '\n');
self.appendOutput('\n');
}).catch(function(err) {
self.appendOutput('Error: ' + err + '\n\n');
});
},
addFooter: function() {
return E('div', { 'class': 'cbi-page-actions' }, [
E('button', {
'class': 'cbi-button cbi-button-save',
'click': L.ui.createHandlerFn(this, 'handleWakeup')
}, [ _('Wake up host') ])
]);
appendOutput(text) {
// Append text to the output textarea and scroll to bottom
this.outputText += text;
const textarea = document.getElementById('wol-output-text');
if (textarea) {
textarea.value = this.outputText;
textarea.scrollTop = textarea.scrollHeight;
}
}
});

View File

@@ -0,0 +1 @@
config wol 'defaults'

View File

@@ -6,12 +6,14 @@
"luci.wol": [ "stat" ],
"luci-rpc": [ "getHostHints", "getNetworkDevices" ]
},
"uci": [ "etherwake" ]
"uci": [ "luci-wol" ]
},
"write": {
"ubus": {
"luci.wol": [ "exec" ]
}
"luci.wol": [ "exec" ],
"uci": [ "add", "set", "delete", "order" ]
},
"uci": [ "luci-wol" ]
}
}
}

View File

@@ -43,7 +43,7 @@ const methods = {
result.stdout = fd.read('all');
result.stderr = '';
result.code = 0;
result.code = fd.close();
} else {
result.stdout = '';
result.stderr = 'disallowed';