diff options
author | Jo-Philipp Wich <jo@mein.io> | 2018-07-18 14:43:27 +0200 |
---|---|---|
committer | Jo-Philipp Wich <jo@mein.io> | 2018-07-18 14:43:27 +0200 |
commit | 9b4efaefa1b4c94a7d976c8d65169bf056032e09 (patch) | |
tree | b76fde8eeb5fe85df6e0dff1306134ea0ae17043 | |
parent | 68dae07225c44ca43aec7d4826ebc954d0c9ef10 (diff) |
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>
-rw-r--r-- | modules/luci-mod-admin-full/luasrc/controller/admin/network.lua | 78 | ||||
-rw-r--r-- | modules/luci-mod-admin-full/luasrc/view/admin_network/wifi_join.htm | 301 |
2 files changed, 257 insertions, 122 deletions
diff --git a/modules/luci-mod-admin-full/luasrc/controller/admin/network.lua b/modules/luci-mod-admin-full/luasrc/controller/admin/network.lua index 31b941625..c45605a98 100644 --- a/modules/luci-mod-admin-full/luasrc/controller/admin/network.lua +++ b/modules/luci-mod-admin-full/luasrc/controller/admin/network.lua @@ -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" diff --git a/modules/luci-mod-admin-full/luasrc/view/admin_network/wifi_join.htm b/modules/luci-mod-admin-full/luasrc/view/admin_network/wifi_join.htm index 9b93942c8..987123642 100644 --- a/modules/luci-mod-admin-full/luasrc/view/admin_network/wifi_join.htm +++ b/modules/luci-mod-admin-full/luasrc/view/admin_network/wifi_join.htm @@ -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> |