luci-mod-admin-full: use incremental background scanning for wireless join
The previous approach of synchroneously scanning while building the result page was suboptimal since it frequently led to connection resets when accessing LuCI via wireless. It also exhibited problems when accessed via SSL on recent Firefox versions where the page were only loaded partially. Rework the wireless scanning to gather scan results in a background process and put them into the ubus session data area where they can be readily accessed without causing network interruptions. Subsequently rebuild the wireless join page to use XHR polling to incrementally fetch updated scan results. Signed-off-by: Jo-Philipp Wich <jo@mein.io>
This commit is contained in:
parent
68dae07225
commit
9b4efaefa1
2 changed files with 257 additions and 122 deletions
|
@ -58,6 +58,12 @@ function index()
|
|||
page = entry({"admin", "network", "wireless_reconnect"}, post("wifi_reconnect"), nil)
|
||||
page.leaf = true
|
||||
|
||||
page = entry({"admin", "network", "wireless_scan_trigger"}, post("wifi_scan_trigger"), nil)
|
||||
page.leaf = true
|
||||
|
||||
page = entry({"admin", "network", "wireless_scan_results"}, call("wifi_scan_results"), nil)
|
||||
page.leaf = true
|
||||
|
||||
page = entry({"admin", "network", "wireless"}, arcombine(cbi("admin_network/wifi_overview"), cbi("admin_network/wifi")), _("Wireless"), 15)
|
||||
page.leaf = true
|
||||
page.subindex = true
|
||||
|
@ -309,6 +315,78 @@ function wifi_assoclist()
|
|||
luci.http.write_json(s.wifi_assoclist())
|
||||
end
|
||||
|
||||
|
||||
local function _wifi_get_scan_results(cache_key)
|
||||
local results = luci.util.ubus("session", "get", {
|
||||
ubus_rpc_session = luci.model.uci:get_session_id(),
|
||||
keys = { cache_key }
|
||||
})
|
||||
|
||||
if type(results) == "table" and
|
||||
type(results.values) == "table" and
|
||||
type(results.values[cache_key]) == "table"
|
||||
then
|
||||
return results.values[cache_key]
|
||||
end
|
||||
|
||||
return { }
|
||||
end
|
||||
|
||||
function wifi_scan_trigger(radio, update)
|
||||
local iw = radio and luci.sys.wifi.getiwinfo(radio)
|
||||
|
||||
if not iw then
|
||||
luci.http.status(404, "No such radio device")
|
||||
return
|
||||
end
|
||||
|
||||
luci.http.status(200, "Scan scheduled")
|
||||
|
||||
if nixio.fork() == 0 then
|
||||
io.stderr:close()
|
||||
io.stdout:close()
|
||||
|
||||
local _, bss
|
||||
local data, bssids = { }, { }
|
||||
local cache_key = "scan_%s" % radio
|
||||
|
||||
luci.util.ubus("session", "set", {
|
||||
ubus_rpc_session = luci.model.uci:get_session_id(),
|
||||
values = { [cache_key] = nil }
|
||||
})
|
||||
|
||||
for _, bss in ipairs(iw.scanlist or { }) do
|
||||
data[_] = bss
|
||||
bssids[bss.bssid] = bss
|
||||
end
|
||||
|
||||
if update then
|
||||
for _, bss in ipairs(_wifi_get_scan_results(cache_key)) do
|
||||
if not bssids[bss.bssid] then
|
||||
bss.stale = true
|
||||
data[#data + 1] = bss
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
luci.util.ubus("session", "set", {
|
||||
ubus_rpc_session = luci.model.uci:get_session_id(),
|
||||
values = { [cache_key] = data }
|
||||
})
|
||||
end
|
||||
end
|
||||
|
||||
function wifi_scan_results(radio)
|
||||
local results = radio and _wifi_get_scan_results("scan_%s" % radio)
|
||||
|
||||
if results and #results > 0 then
|
||||
luci.http.prepare_content("application/json")
|
||||
luci.http.write_json(results)
|
||||
else
|
||||
luci.http.status(404, "No wireless scan results")
|
||||
end
|
||||
end
|
||||
|
||||
function lease_status()
|
||||
local s = require "luci.tools.status"
|
||||
|
||||
|
|
|
@ -8,56 +8,6 @@
|
|||
local sys = require "luci.sys"
|
||||
local utl = require "luci.util"
|
||||
|
||||
function guess_wifi_signal(info)
|
||||
local scale = (100 / (info.quality_max or 100) * (info.quality or 0))
|
||||
local icon
|
||||
|
||||
if not info.bssid or info.bssid == "00:00:00:00:00:00" then
|
||||
icon = resource .. "/icons/signal-none.png"
|
||||
elseif scale < 15 then
|
||||
icon = resource .. "/icons/signal-0.png"
|
||||
elseif scale < 35 then
|
||||
icon = resource .. "/icons/signal-0-25.png"
|
||||
elseif scale < 55 then
|
||||
icon = resource .. "/icons/signal-25-50.png"
|
||||
elseif scale < 75 then
|
||||
icon = resource .. "/icons/signal-50-75.png"
|
||||
else
|
||||
icon = resource .. "/icons/signal-75-100.png"
|
||||
end
|
||||
|
||||
return icon
|
||||
end
|
||||
|
||||
function percent_wifi_signal(info)
|
||||
local qc = info.quality or 0
|
||||
local qm = info.quality_max or 0
|
||||
|
||||
if info.bssid and qc > 0 and qm > 0 then
|
||||
return math.floor((100 / qm) * qc)
|
||||
else
|
||||
return 0
|
||||
end
|
||||
end
|
||||
|
||||
function format_wifi_encryption(info)
|
||||
if info.wep == true then
|
||||
return "WEP"
|
||||
elseif info.wpa > 0 then
|
||||
return translatef("<abbr title='Pairwise: %s / Group: %s'>%s - %s</abbr>",
|
||||
table.concat(info.pair_ciphers, ", "),
|
||||
table.concat(info.group_ciphers, ", "),
|
||||
(info.wpa == 3) and translate("mixed WPA/WPA2")
|
||||
or (info.wpa == 2 and "WPA2" or "WPA"),
|
||||
table.concat(info.auth_suites, ", ")
|
||||
)
|
||||
elseif info.enabled then
|
||||
return "<em>%s</em>" % translate("unknown")
|
||||
else
|
||||
return "<em>%s</em>" % translate("open")
|
||||
end
|
||||
end
|
||||
|
||||
local dev = luci.http.formvalue("device")
|
||||
local iw = luci.sys.wifi.getiwinfo(dev)
|
||||
|
||||
|
@ -65,91 +15,198 @@
|
|||
luci.http.redirect(luci.dispatcher.build_url("admin/network/wireless"))
|
||||
return
|
||||
end
|
||||
|
||||
|
||||
function scanlist(times)
|
||||
local i, k, v
|
||||
local l = { }
|
||||
local s = { }
|
||||
|
||||
for i = 1, times do
|
||||
for k, v in ipairs(iw.scanlist or { }) do
|
||||
if not s[v.bssid] then
|
||||
l[#l+1] = v
|
||||
s[v.bssid] = true
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
return l
|
||||
end
|
||||
-%>
|
||||
|
||||
<%+header%>
|
||||
|
||||
<script type="text/javascript">//<![CDATA[
|
||||
var xhr = new XHR(),
|
||||
poll = null;
|
||||
|
||||
function format_signal(bss) {
|
||||
var qval = bss.quality || 0,
|
||||
qmax = bss.quality_max || 100,
|
||||
scale = 100 / qmax * qval,
|
||||
range = 'none';
|
||||
|
||||
if (!bss.bssid || bss.bssid == '00:00:00:00:00:00')
|
||||
range = 'none';
|
||||
else if (scale < 15)
|
||||
range = '0';
|
||||
else if (scale < 35)
|
||||
range = '0-25';
|
||||
else if (scale < 55)
|
||||
range = '25-50';
|
||||
else if (scale < 75)
|
||||
range = '50-75';
|
||||
else
|
||||
range = '75-100';
|
||||
|
||||
return E('span', {
|
||||
class: 'ifacebadge',
|
||||
title: '<%:Signal%>: %d<%:dB%> / <%:Quality%>: %d/%d'.format(bss.signal, qval, qmax)
|
||||
}, [
|
||||
E('img', { src: '<%=resource%>/icons/signal-%s.png'.format(range) }),
|
||||
' %d%%'.format(scale)
|
||||
]);
|
||||
}
|
||||
|
||||
function format_encryption(bss) {
|
||||
var enc = bss.encryption || { }
|
||||
|
||||
if (enc.wep === true)
|
||||
return 'WEP';
|
||||
else if (enc.wpa > 0)
|
||||
return E('abbr', {
|
||||
title: 'Pairwise: %h / Group: %h'.format(
|
||||
enc.pair_ciphers.join(', '),
|
||||
enc.group_ciphers.join(', '))
|
||||
},
|
||||
'%h - %h'.format(
|
||||
(enc.wpa === 3) ? '<%:mixed WPA/WPA2%>' : (enc.wpa === 2 ? 'WPA2' : 'WPA'),
|
||||
enc.auth_suites.join(', ')));
|
||||
else if (enc.enabled)
|
||||
return '<em><%:unknown%></em>';
|
||||
else
|
||||
return '<em><%:open%></em>';
|
||||
}
|
||||
|
||||
function format_actions(bss) {
|
||||
var enc = bss.encryption || { },
|
||||
input = [
|
||||
E('input', { type: 'submit', class: 'cbi-button cbi-button-action important', value: '<%:Join Network%>' }),
|
||||
E('input', { type: 'hidden', name: 'token', value: '<%=token%>' }),
|
||||
E('input', { type: 'hidden', name: 'device', value: '<%=dev%>' }),
|
||||
E('input', { type: 'hidden', name: 'join', value: bss.ssid }),
|
||||
E('input', { type: 'hidden', name: 'mode', value: bss.mode }),
|
||||
E('input', { type: 'hidden', name: 'bssid', value: bss.bssid }),
|
||||
E('input', { type: 'hidden', name: 'channel', value: bss.channel }),
|
||||
E('input', { type: 'hidden', name: 'clbridge', value: <%=iw.type == "wl" and 1 or 0%> }),
|
||||
E('input', { type: 'hidden', name: 'wep', value: enc.wep ? 1 : 0 })
|
||||
];
|
||||
|
||||
if (enc.wpa) {
|
||||
input.push(E('input', { type: 'hidden', name: 'wpa_version', value: enc.wpa }));
|
||||
|
||||
enc.auth_suites.forEach(function(s) {
|
||||
input.push(E('input', { type: 'hidden', name: 'wpa_suites', value: s }));
|
||||
});
|
||||
|
||||
enc.group_ciphers.forEach(function(s) {
|
||||
input.push(E('input', { type: 'hidden', name: 'wpa_group', value: s }));
|
||||
});
|
||||
|
||||
enc.pair_ciphers.forEach(function(s) {
|
||||
input.push(E('input', { type: 'hidden', name: 'wpa_pairwise', value: s }));
|
||||
});
|
||||
}
|
||||
|
||||
return E('form', {
|
||||
class: 'inline',
|
||||
method: 'post',
|
||||
action: '<%=url("admin/network/wireless_join")%>'
|
||||
}, input);
|
||||
}
|
||||
|
||||
function fade(bss, content) {
|
||||
if (bss.stale)
|
||||
return E('span', { style: 'opacity:0.5' }, content);
|
||||
else
|
||||
return content;
|
||||
}
|
||||
|
||||
function flush() {
|
||||
XHR.stop(poll);
|
||||
XHR.halt();
|
||||
|
||||
scan();
|
||||
}
|
||||
|
||||
function scan() {
|
||||
var tbl = document.getElementById('scan_results');
|
||||
|
||||
cbi_update_table(tbl, [], '<em><img src="<%=resource%>/icons/loading.gif" class="middle" /> <%:Starting wireless scan...%></em>');
|
||||
|
||||
xhr.post('<%=url("admin/network/wireless_scan_trigger", dev)%>', { token: '<%=token%>' },
|
||||
function(s) {
|
||||
if (s.status !== 200) {
|
||||
cbi_update_table(tbl, [], '<em><%:Scan request failed%></em>');
|
||||
return;
|
||||
}
|
||||
|
||||
var count = 0;
|
||||
|
||||
poll = XHR.poll(3, '<%=url("admin/network/wireless_scan_results", dev)%>', null,
|
||||
function(s, results) {
|
||||
if (Array.isArray(results)) {
|
||||
var bss = [];
|
||||
|
||||
results.sort(function(a, b) {
|
||||
var diff = (b.quality - a.quality) || (a.channel - b.channel);
|
||||
|
||||
if (diff)
|
||||
return diff;
|
||||
|
||||
if (a.ssid < b.ssid)
|
||||
return -1;
|
||||
else if (a.ssid > b.ssid)
|
||||
return 1;
|
||||
|
||||
if (a.bssid < b.bssid)
|
||||
return -1;
|
||||
else if (a.bssid > b.bssid)
|
||||
return 1;
|
||||
}).forEach(function(res) {
|
||||
bss.push([
|
||||
fade(res, format_signal(res)),
|
||||
fade(res, res.ssid ? '%h'.format(res.ssid) : E('em', {}, '<%:hidden%>')),
|
||||
fade(res, res.channel),
|
||||
fade(res, res.mode),
|
||||
fade(res, res.bssid),
|
||||
fade(res, format_encryption(res)),
|
||||
format_actions(res)
|
||||
]);
|
||||
});
|
||||
|
||||
cbi_update_table(tbl, bss, '<em><img src="<%=resource%>/icons/loading.gif" class="middle" /> <%:No scan results available yet...%>');
|
||||
}
|
||||
|
||||
if (count++ >= 3) {
|
||||
count = 0;
|
||||
xhr.post('<%=url("admin/network/wireless_scan_trigger", dev, "1")%>',
|
||||
{ token: '<%=token%>' }, function() { });
|
||||
}
|
||||
});
|
||||
|
||||
XHR.run();
|
||||
});
|
||||
}
|
||||
|
||||
document.addEventListener('DOMContentLoaded', scan);
|
||||
|
||||
//]]></script>
|
||||
|
||||
<h2 name="content"><%:Join Network: Wireless Scan%></h2>
|
||||
|
||||
<div class="cbi-map">
|
||||
<div class="cbi-section">
|
||||
<div class="table">
|
||||
<div class="table" id="scan_results">
|
||||
<div class="tr table-titles">
|
||||
<div class="th col-1 center"><%:Signal%></div>
|
||||
<div class="th col-5 left"><%:SSID%></div>
|
||||
<div class="th col-2 center"><%:Channel%></div>
|
||||
<div class="th col-2 left"><%:Mode%></div>
|
||||
<div class="th col-3 left"><%:BSSID%></div>
|
||||
<div class="th col-2 left"><%:Encryption%></div>
|
||||
<div class="th col-1 middle center"><%:Signal%></div>
|
||||
<div class="th col-5 middle left"><%:SSID%></div>
|
||||
<div class="th col-2 middle center"><%:Channel%></div>
|
||||
<div class="th col-2 middle left"><%:Mode%></div>
|
||||
<div class="th col-3 middle left"><%:BSSID%></div>
|
||||
<div class="th col-2 middle left"><%:Encryption%></div>
|
||||
<div class="th cbi-section-actions"> </div>
|
||||
</div>
|
||||
|
||||
<!-- scan list -->
|
||||
<% for i, net in ipairs(scanlist(3)) do net.encryption = net.encryption or { } %>
|
||||
<div class="tr cbi-rowstyle-<%=1 + ((i-1) % 2)%>">
|
||||
<div class="td col-1 center">
|
||||
<abbr title="<%:Signal%>: <%=net.signal%> <%:dB%> / <%:Quality%>: <%=net.quality%>/<%=net.quality_max%>">
|
||||
<img src="<%=guess_wifi_signal(net)%>" /><br />
|
||||
<small><%=percent_wifi_signal(net)%>%</small>
|
||||
</abbr>
|
||||
</div>
|
||||
<div class="td col-5 left" data-title="<%:SSID%>">
|
||||
<strong><%=net.ssid and utl.pcdata(net.ssid) or "<em>%s</em>" % translate("hidden")%></strong>
|
||||
</div>
|
||||
<div class="td col-2 center" data-title="<%:Channel%>">
|
||||
<%=net.channel%>
|
||||
</div>
|
||||
<div class="td col-2 left" data-title="<%:Mode%>">
|
||||
<%=net.mode%>
|
||||
</div>
|
||||
<div class="td col-3 left" data-title="<%:BSSID%>">
|
||||
<%=net.bssid%>
|
||||
</div>
|
||||
<div class="td col-2 left" data-title="<%:Encryption%>">
|
||||
<%=format_wifi_encryption(net.encryption)%>
|
||||
</div>
|
||||
<div class="td cbi-section-actions">
|
||||
<form action="<%=url('admin/network/wireless_join')%>" method="post">
|
||||
<input type="hidden" name="token" value="<%=token%>" />
|
||||
<input type="hidden" name="device" value="<%=utl.pcdata(dev)%>" />
|
||||
<input type="hidden" name="join" value="<%=utl.pcdata(net.ssid)%>" />
|
||||
<input type="hidden" name="mode" value="<%=net.mode%>" />
|
||||
<input type="hidden" name="bssid" value="<%=net.bssid%>" />
|
||||
<input type="hidden" name="channel" value="<%=net.channel%>" />
|
||||
<input type="hidden" name="wep" value="<%=net.encryption.wep and 1 or 0%>" />
|
||||
<% if net.encryption.wpa then %>
|
||||
<input type="hidden" name="wpa_version" value="<%=net.encryption.wpa%>" />
|
||||
<% for _, v in ipairs(net.encryption.auth_suites) do %><input type="hidden" name="wpa_suites" value="<%=v%>" />
|
||||
<% end; for _, v in ipairs(net.encryption.group_ciphers) do %><input type="hidden" name="wpa_group" value="<%=v%>" />
|
||||
<% end; for _, v in ipairs(net.encryption.pair_ciphers) do %><input type="hidden" name="wpa_pairwise" value="<%=v%>" />
|
||||
<% end; end %>
|
||||
|
||||
<input type="hidden" name="clbridge" value="<%=iw.type == "wl" and 1 or 0%>" />
|
||||
|
||||
<input class="cbi-button cbi-button-action important" type="submit" value="<%:Join Network%>" />
|
||||
</form>
|
||||
<div class="tr placeholder">
|
||||
<div class="td">
|
||||
<img src="<%=resource%>/icons/loading.gif" class="middle" />
|
||||
<em><%:Collecting data...%></em>
|
||||
</div>
|
||||
</div>
|
||||
<% end %>
|
||||
<!-- /scan list -->
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -160,7 +217,7 @@
|
|||
<form class="inline" action="<%=url('admin/network/wireless_join')%>" method="post">
|
||||
<input type="hidden" name="token" value="<%=token%>" />
|
||||
<input type="hidden" name="device" value="<%=utl.pcdata(dev)%>" />
|
||||
<input class="cbi-button cbi-button-action" type="submit" value="<%:Repeat scan%>" />
|
||||
<input type="button" class="cbi-button cbi-button-action" value="<%:Repeat scan%>" onclick="flush()" />
|
||||
</form>
|
||||
</div>
|
||||
|
||||
|
|
Loading…
Reference in a new issue