mirror of
https://github.com/kiddin9/op-packages.git
synced 2026-11-15 04:53:00 +08:00
⛅ Sync 2026-10-11 13:16:00
This commit is contained in:
1 parent
705dadd362
commit
e6d1b4e2d7
105 files changed
+26511
-50
No files matched your search
@@ -0,0 +1,216 @@
|
||||
// SPDX-License-Identifier: MIT
|
||||
'use strict';
|
||||
'require baseclass';
|
||||
'require rpc';
|
||||
'require uci';
|
||||
'require fs';
|
||||
'require device-manager.model as model';
|
||||
|
||||
const CONFIG = 'device_manager';
|
||||
const callHints = rpc.declare({ object: 'luci-rpc', method: 'getHostHints', reject: true });
|
||||
const callLeases = rpc.declare({ object: 'luci-rpc', method: 'getDHCPLeases', reject: true });
|
||||
const callNeighbors = rpc.declare({ object: 'luci.device-manager', method: 'get_online_status', reject: true });
|
||||
const callScan = rpc.declare({ object: 'luci.device-manager', method: 'scan_devices', reject: true });
|
||||
const callPing = rpc.declare({ object: 'luci.device-manager', method: 'ping_device', params: [ 'mac' ], reject: true });
|
||||
const callWireless = rpc.declare({ object: 'luci-rpc', method: 'getWirelessDevices', reject: true });
|
||||
const callAssoc = rpc.declare({ object: 'iwinfo', method: 'assoclist', params: [ 'device' ], reject: true });
|
||||
const callCommit = rpc.declare({ object: 'uci', method: 'commit', params: [ 'config' ], reject: true });
|
||||
const callRevert = rpc.declare({ object: 'uci', method: 'revert', params: [ 'config' ], reject: true });
|
||||
|
||||
function requireObject(value) {
|
||||
if (!value || typeof value !== 'object' || Array.isArray(value)) throw new Error(_('Invalid discovery response'));
|
||||
return value;
|
||||
}
|
||||
|
||||
function readSource(name, read) {
|
||||
return Promise.resolve().then(read).then(value => ({ name: name, ok: true, value: value }),
|
||||
error => ({ name: name, ok: false, error: error.message || String(error) }));
|
||||
}
|
||||
|
||||
function wifiStations() {
|
||||
return callWireless().then(requireObject).then(radios => {
|
||||
const interfaces = new Set();
|
||||
for (const radio of Object.values(radios)) {
|
||||
for (const iface of radio.interfaces || []) {
|
||||
const mode = (iface.config || {}).mode;
|
||||
if (mode && mode !== 'ap' && mode !== 'ap-wds') continue;
|
||||
if (iface.ifname) interfaces.add(iface.ifname);
|
||||
for (const vlan of iface.vlans || []) if (vlan.ifname) interfaces.add(vlan.ifname);
|
||||
}
|
||||
}
|
||||
return Promise.all(Array.from(interfaces).map(device => readSource(device, () => callAssoc(device).then(reply => {
|
||||
if (!Array.isArray(requireObject(reply).results)) throw new Error(_('Invalid discovery response'));
|
||||
return reply.results.map(station => ({ mac: station.mac, dev: device }));
|
||||
})))).then(results => ({
|
||||
stations: results.filter(result => result.ok).flatMap(result => result.value),
|
||||
complete: results.every(result => result.ok),
|
||||
errors: results.filter(result => !result.ok).map(result => result.name + ': ' + result.error)
|
||||
}));
|
||||
});
|
||||
}
|
||||
|
||||
function reloadConfig() {
|
||||
uci.unload(CONFIG);
|
||||
return uci.load(CONFIG);
|
||||
}
|
||||
|
||||
function validateText(value, limit) {
|
||||
const text = model.sanitizeInput(value);
|
||||
if (text.length > limit) throw new Error(_('Input is too long'));
|
||||
return text;
|
||||
}
|
||||
|
||||
function validateGroupName(name, exceptId) {
|
||||
const text = validateText(name, 32);
|
||||
if (!text) throw new Error(_('Enter a valid group name'));
|
||||
if (uci.sections(CONFIG, 'group').some(group => group['.name'] !== exceptId && model.sanitizeInput(group.name) === text))
|
||||
throw new Error(_('A group with this name already exists'));
|
||||
return text;
|
||||
}
|
||||
|
||||
function requireGroup(id) {
|
||||
if (!uci.sections(CONFIG, 'group').some(group => group['.name'] === id)) throw new Error(_('This group no longer exists'));
|
||||
}
|
||||
|
||||
return baseclass.extend({
|
||||
scanDevices: function() {
|
||||
if (L.hasViewPermission() !== true) return Promise.reject(new Error(_('Read-only access')));
|
||||
return this.queue(() => callScan().then(reply => {
|
||||
if (requireObject(reply).ok !== true)
|
||||
throw new Error(_('LAN scanning is unavailable. Check the selected interfaces and probe dependencies.'));
|
||||
}));
|
||||
},
|
||||
pingDevice: function(mac) {
|
||||
const normalized = model.normalizeMac(mac);
|
||||
if (!normalized) return Promise.reject(new Error(_('Enter a valid MAC address')));
|
||||
return callPing(normalized).then(reply => {
|
||||
const errors = {
|
||||
'No known IP address for this device': _('No known IP address for this device'),
|
||||
'Another device is being probed. Please retry.': _('Another device is being probed. Please retry.'),
|
||||
'Ping is unavailable': _('Ping is unavailable'),
|
||||
'Enter a valid MAC address': _('Enter a valid MAC address'),
|
||||
'Unable to read the kernel neighbor table': _('Unable to read the kernel neighbor table')
|
||||
};
|
||||
if (requireObject(reply).ok !== true) {
|
||||
const error = new Error(errors[reply.error] || _('Ping could not be completed'));
|
||||
error.output = typeof reply.output === 'string' ? reply.output : '';
|
||||
throw error;
|
||||
}
|
||||
if (typeof reply.reachable !== 'boolean' || typeof reply.ip !== 'string' || typeof reply.output !== 'string')
|
||||
throw new Error(_('Invalid discovery response'));
|
||||
return reply;
|
||||
});
|
||||
},
|
||||
// Reads and writes share the queue so a refresh cannot discard an in-flight edit.
|
||||
queue: function(task) {
|
||||
const result = (this.pending || Promise.resolve()).then(task);
|
||||
this.pending = result.catch(() => {});
|
||||
return result;
|
||||
},
|
||||
load: function() {
|
||||
return this.queue(() => this.recoverPending().then(() => {
|
||||
const config = reloadConfig().then(() => ({
|
||||
groups: uci.sections(CONFIG, 'group'), devices: uci.sections(CONFIG, 'device')
|
||||
}));
|
||||
const sources = Promise.all([
|
||||
readSource('hints', () => callHints().then(requireObject)),
|
||||
readSource('leases', () => callLeases().then(requireObject)),
|
||||
readSource('neighbors', () => callNeighbors().then(reply => {
|
||||
if (requireObject(reply).ok !== true || !Array.isArray(reply.neighbors)) throw new Error(reply.error || _('Invalid discovery response'));
|
||||
return reply;
|
||||
})),
|
||||
readSource('wifi', wifiStations),
|
||||
readSource('arp', () => fs.read('/proc/net/arp').then(model.parseArp))
|
||||
]);
|
||||
return Promise.all([ config, sources ]).then(results => {
|
||||
const data = Object.assign({ errors: [], discoveryComplete: true }, results[0]);
|
||||
for (const result of results[1]) {
|
||||
if (!result.ok) {
|
||||
data.errors.push(result.name + ': ' + result.error);
|
||||
data.discoveryComplete = false;
|
||||
continue;
|
||||
}
|
||||
if (result.name === 'wifi') {
|
||||
data.wifi = result.value.stations;
|
||||
data.discoveryComplete = data.discoveryComplete && result.value.complete;
|
||||
data.errors.push(...result.value.errors);
|
||||
} else data[result.name] = result.value;
|
||||
}
|
||||
return data;
|
||||
});
|
||||
}));
|
||||
},
|
||||
recoverPending: function() {
|
||||
if (!this.rollbackPending) return Promise.resolve();
|
||||
return callRevert(CONFIG).then(() => { this.rollbackPending = false; });
|
||||
},
|
||||
mutate: function(change) {
|
||||
return this.queue(() => {
|
||||
if (L.hasViewPermission() !== true) throw new Error(_('Read-only access'));
|
||||
let staged = false;
|
||||
return this.recoverPending().then(reloadConfig).then(change).then(() => {
|
||||
staged = true;
|
||||
return uci.save();
|
||||
}).then(() => callCommit(CONFIG)).catch(error => {
|
||||
if (!staged) throw error;
|
||||
this.rollbackPending = true;
|
||||
return this.recoverPending().then(() => { throw error; }, rollbackError => {
|
||||
throw new Error(_('%s; failed to discard pending changes: %s').format(error.message, rollbackError.message));
|
||||
});
|
||||
}).finally(() => uci.unload(CONFIG));
|
||||
});
|
||||
},
|
||||
saveDevice: function(mac, name, remark, group, type) {
|
||||
return this.mutate(() => {
|
||||
const normalized = model.normalizeMac(mac);
|
||||
if (!normalized) throw new Error(_('Enter a valid MAC address'));
|
||||
const deviceName = validateText(name, 64), deviceRemark = validateText(remark, 256);
|
||||
if (group && group !== 'ungrouped') requireGroup(group);
|
||||
const matches = uci.sections(CONFIG, 'device').filter(section => model.sectionMac(section) === normalized);
|
||||
const sid = matches.length ? matches[matches.length - 1]['.name'] : model.getSectionId(normalized);
|
||||
if (!matches.length) {
|
||||
if (uci.get(CONFIG, sid)) throw new Error(_('A conflicting configuration record exists'));
|
||||
uci.add(CONFIG, 'device', sid);
|
||||
}
|
||||
for (const section of matches) if (section['.name'] !== sid) uci.remove(CONFIG, section['.name']);
|
||||
uci.set(CONFIG, sid, 'mac', normalized);
|
||||
for (const [ key, value ] of Object.entries({ name: deviceName, remark: deviceRemark,
|
||||
group: group && group !== 'ungrouped' ? group : '',
|
||||
type: type && type !== 'auto' ? type : '' })) {
|
||||
if (value) uci.set(CONFIG, sid, key, value);
|
||||
else uci.unset(CONFIG, sid, key);
|
||||
}
|
||||
});
|
||||
},
|
||||
deleteDevice: function(device) {
|
||||
return this.mutate(() => {
|
||||
for (const section of uci.sections(CONFIG, 'device')) {
|
||||
if (model.sectionMac(section) === device.mac) uci.remove(CONFIG, section['.name']);
|
||||
}
|
||||
});
|
||||
},
|
||||
addGroup: function(name) {
|
||||
return this.mutate(() => {
|
||||
const text = validateGroupName(name);
|
||||
let id;
|
||||
do { id = 'grp_' + Date.now().toString(36) + '_' + Math.random().toString(36).slice(2, 8); } while (uci.get(CONFIG, id));
|
||||
uci.add(CONFIG, 'group', id);
|
||||
uci.set(CONFIG, id, 'name', text);
|
||||
});
|
||||
},
|
||||
renameGroup: function(id, name) {
|
||||
return this.mutate(() => {
|
||||
requireGroup(id);
|
||||
uci.set(CONFIG, id, 'name', validateGroupName(name, id));
|
||||
});
|
||||
},
|
||||
deleteGroup: function(id) {
|
||||
return this.mutate(() => {
|
||||
requireGroup(id);
|
||||
uci.remove(CONFIG, id);
|
||||
for (const section of uci.sections(CONFIG, 'device')) {
|
||||
if (section.group === id) uci.unset(CONFIG, section['.name'], 'group');
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
Reference in new issue
Block a user