diff --git a/frontend/apps/cloudstorage/cloudstorage.lua b/frontend/apps/cloudstorage/cloudstorage.lua index 149ba05af..fafaa2d69 100644 --- a/frontend/apps/cloudstorage/cloudstorage.lua +++ b/frontend/apps/cloudstorage/cloudstorage.lua @@ -149,20 +149,17 @@ function CloudStorage:openCloudServer(url) local tbl local NetworkMgr = require("ui/network/manager") if self.type == "dropbox" then - if not NetworkMgr:isOnline() then - NetworkMgr:promptWifiOn() + if NetworkMgr:willRerunWhenOnline(function() self:openCloudServer(url) end) then return end tbl = DropBox:run(url, self.password, self.choose_folder_mode) elseif self.type == "ftp" then - if not NetworkMgr:isConnected() then - NetworkMgr:promptWifiOn() + if NetworkMgr:willRerunWhenConnected(function() self:openCloudServer(url) end) then return end tbl = Ftp:run(self.address, self.username, self.password, url) elseif self.type == "webdav" then - if not NetworkMgr:isConnected() then - NetworkMgr:promptWifiOn() + if NetworkMgr:willRerunWhenConnected(function() self:openCloudServer(url) end) then return end tbl = WebDav:run(self.address, self.username, self.password, url) diff --git a/frontend/apps/reader/modules/readerdictionary.lua b/frontend/apps/reader/modules/readerdictionary.lua index 4e2c18326..5d49dc5a6 100644 --- a/frontend/apps/reader/modules/readerdictionary.lua +++ b/frontend/apps/reader/modules/readerdictionary.lua @@ -836,11 +836,10 @@ function ReaderDictionary:showDownload(downloadable_dicts) for dummy, dict in ipairs(downloadable_dicts) do table.insert(kv_pairs, {dict.name, "", callback = function() - if not NetworkMgr:isOnline() then - NetworkMgr:promptWifiOn() - return + local connect_callback = function() + self:downloadDictionaryPrep(dict) end - self:downloadDictionaryPrep(dict) + NetworkMgr:runWhenOnline(connect_callback) end}) local lang if dict.lang_in == dict.lang_out then diff --git a/frontend/apps/reader/modules/readerfooter.lua b/frontend/apps/reader/modules/readerfooter.lua index 7c1887dfb..588a889f7 100644 --- a/frontend/apps/reader/modules/readerfooter.lua +++ b/frontend/apps/reader/modules/readerfooter.lua @@ -57,7 +57,7 @@ local symbol_prefix = { frontlight = C_("FooterLetterPrefix", "L:"), -- @translators This is the footer letter prefix for memory usage. mem_usage = C_("FooterLetterPrefix", "M:"), - -- @translators This is the footer letter prefix for wifi status. + -- @translators This is the footer letter prefix for Wi-Fi status. wifi_status = C_("FooterLetterPrefix", "W:"), }, icons = { @@ -1835,7 +1835,7 @@ function ReaderFooter:applyFooterMode(mode) -- 7 for from statistics chapter time to read -- 8 for front light level -- 9 for memory usage - -- 10 for wifi status + -- 10 for Wi-Fi status -- 11 for book title -- 12 for current chapter diff --git a/frontend/apps/reader/modules/readerwikipedia.lua b/frontend/apps/reader/modules/readerwikipedia.lua index 1e9265e59..e7ddda091 100644 --- a/frontend/apps/reader/modules/readerwikipedia.lua +++ b/frontend/apps/reader/modules/readerwikipedia.lua @@ -384,10 +384,11 @@ function ReaderWikipedia:onLookupWikipedia(word, box, get_fullpage, forced_lang) end function ReaderWikipedia:lookupWikipedia(word, box, get_fullpage, forced_lang) - if not NetworkMgr:isOnline() then - NetworkMgr:promptWifiOn() + if NetworkMgr:willRerunWhenOnline(function() self:lookupWikipedia(word, box, get_fullpage, forced_lang) end) then + -- Not online yet, nothing more to do here, NetworkMgr will forward the callback and run it once connected! return end + -- word is the text to query. If get_fullpage is true, it is the -- exact wikipedia page title we want the full page of. self:initLanguages(word) @@ -525,11 +526,10 @@ function ReaderWikipedia:onSaveSettings() end function ReaderWikipedia:onShowWikipediaLookup() - if NetworkMgr:isOnline() then + local connect_callback = function() self:lookupInput() - else - NetworkMgr:promptWifiOn() end + NetworkMgr:runWhenOnline(connect_callback) return true end diff --git a/frontend/device/cervantes/device.lua b/frontend/device/cervantes/device.lua index 96e5a4eee..24d3ffb0f 100644 --- a/frontend/device/cervantes/device.lua +++ b/frontend/device/cervantes/device.lua @@ -17,7 +17,7 @@ local function isConnected() -- read carrier state from sysfs (for eth0) local file = io.open("/sys/class/net/eth0/carrier", "rb") - -- file exists while wifi module is loaded. + -- file exists while Wi-Fi module is loaded. if not file then return 0 end -- 0 means not connected, 1 connected @@ -224,7 +224,7 @@ end -- wireless function Cervantes:initNetworkManager(NetworkMgr) function NetworkMgr:turnOffWifi(complete_callback) - logger.info("Cervantes: disabling WiFi") + logger.info("Cervantes: disabling Wi-Fi") self:releaseIP() os.execute("./disable-wifi.sh") if complete_callback then @@ -232,10 +232,13 @@ function Cervantes:initNetworkManager(NetworkMgr) end end function NetworkMgr:turnOnWifi(complete_callback) - logger.info("Cervantes: enabling WiFi") + logger.info("Cervantes: enabling Wi-Fi") os.execute("./enable-wifi.sh") self:reconnectOrShowNetworkMenu(complete_callback) end + function NetworkMgr:getNetworkInterfaceName() + return "eth0" + end NetworkMgr:setWirelessBackend("wpa_supplicant", {ctrl_interface = "/var/run/wpa_supplicant/eth0"}) function NetworkMgr:obtainIP() os.execute("./obtain-ip.sh") diff --git a/frontend/device/generic/device.lua b/frontend/device/generic/device.lua index 3e6f5476a..8181f1b6a 100644 --- a/frontend/device/generic/device.lua +++ b/frontend/device/generic/device.lua @@ -282,12 +282,12 @@ function Device:onPowerEvent(ev) self.screen_saver_mode = true UIManager:scheduleIn(0.1, function() local network_manager = require("ui/network/manager") - -- NOTE: wifi_was_on does not necessarily mean that WiFi is *currently* on! It means *we* enabled it. + -- NOTE: wifi_was_on does not necessarily mean that Wi-Fi is *currently* on! It means *we* enabled it. -- This is critical on Kobos (c.f., #3936), where it might still be on from KSM or Nickel, -- without us being aware of it (i.e., wifi_was_on still unset or false), - -- because suspend will at best fail, and at worst deadlock the system if WiFi is on, + -- because suspend will at best fail, and at worst deadlock the system if Wi-Fi is on, -- regardless of who enabled it! - if network_manager.wifi_was_on or network_manager:isWifiOn() then + if network_manager:isWifiOn() then network_manager:releaseIP() network_manager:turnOffWifi() end diff --git a/frontend/device/kindle/device.lua b/frontend/device/kindle/device.lua index 4f016583a..3eb18577e 100644 --- a/frontend/device/kindle/device.lua +++ b/frontend/device/kindle/device.lua @@ -12,7 +12,7 @@ local function kindleEnableWifi(toggle) end if lipc_handle then -- Be extremely thorough... c.f., #6019 - -- NOTE: I *assume* this'll also ensure we prefer WiFi over 3G/4G, which is a plus in my book... + -- NOTE: I *assume* this'll also ensure we prefer Wi-Fi over 3G/4G, which is a plus in my book... if toggle == 1 then lipc_handle:set_int_property("com.lab126.cmd", "wirelessEnable", 1) lipc_handle:set_int_property("com.lab126.wifid", "enable", 1) @@ -103,20 +103,19 @@ function Kindle:initNetworkManager(NetworkMgr) function NetworkMgr:turnOnWifi(complete_callback) kindleEnableWifi(1) -- NOTE: As we defer the actual work to lipc, - -- we have no guarantee the WiFi state will have changed by the time kindleEnableWifi returns, - -- so, delay the callback a bit... + -- we have no guarantee the Wi-Fi state will have changed by the time kindleEnableWifi returns, + -- so, delay the callback until we at least can ensure isConnect is true. if complete_callback then - local UIManager = require("ui/uimanager") - UIManager:scheduleIn(1, complete_callback) + NetworkMgr:scheduleConnectivityCheck(complete_callback) end end function NetworkMgr:turnOffWifi(complete_callback) kindleEnableWifi(0) - -- NOTE: Same here... + -- NOTE: Same here, except disconnect is simpler, so a dumb delay will do... if complete_callback then local UIManager = require("ui/uimanager") - UIManager:scheduleIn(1, complete_callback) + UIManager:scheduleIn(2, complete_callback) end end @@ -374,7 +373,7 @@ local KindleOasis = Kindle:new{ hasGSensor = yes, display_dpi = 300, --[[ - -- NOTE: Points to event3 on WiFi devices, event4 on 3G devices... + -- NOTE: Points to event3 on Wi-Fi devices, event4 on 3G devices... -- 3G devices apparently have an extra SX9500 Proximity/Capacitive controller for mysterious purposes... -- This evidently screws with the ordering, so, use the udev by-path path instead to avoid hackier workarounds. -- cf. #2181 @@ -405,7 +404,7 @@ local KindlePaperWhite4 = Kindle:new{ display_dpi = 300, -- NOTE: LTE devices once again have a mysterious extra SX9310 proximity sensor... -- Except this time, we can't rely on by-path, because there's no entry for the TS :/. - -- Should be event2 on WiFi, event3 on LTE, we'll fix it in init. + -- Should be event2 on Wi-Fi, event3 on LTE, we'll fix it in init. touch_dev = "/dev/input/event2", } diff --git a/frontend/device/kobo/device.lua b/frontend/device/kobo/device.lua index 2f2ba0f8a..fe8107d2c 100644 --- a/frontend/device/kobo/device.lua +++ b/frontend/device/kobo/device.lua @@ -10,11 +10,11 @@ local function yes() return true end local function no() return false end local function koboEnableWifi(toggle) - if toggle == 1 then - logger.info("Kobo WiFi: enabling WiFi") + if toggle == true then + logger.info("Kobo Wi-Fi: enabling Wi-Fi") os.execute("./enable-wifi.sh") else - logger.info("Kobo WiFi: disabling WiFi") + logger.info("Kobo Wi-Fi: disabling Wi-Fi") os.execute("./disable-wifi.sh") end end @@ -414,14 +414,15 @@ end function Kobo:initNetworkManager(NetworkMgr) function NetworkMgr:turnOffWifi(complete_callback) - koboEnableWifi(0) + self:releaseIP() + koboEnableWifi(false) if complete_callback then complete_callback() end end function NetworkMgr:turnOnWifi(complete_callback) - koboEnableWifi(1) + koboEnableWifi(true) self:showNetworkMenu(complete_callback) end @@ -429,6 +430,9 @@ function Kobo:initNetworkManager(NetworkMgr) if not net_if then net_if = "eth0" end + function NetworkMgr:getNetworkInterfaceName() + return net_if + end NetworkMgr:setWirelessBackend( "wpa_supplicant", {ctrl_interface = "/var/run/wpa_supplicant/" .. net_if}) @@ -444,14 +448,14 @@ function Kobo:initNetworkManager(NetworkMgr) os.execute("./restore-wifi-async.sh") end - -- NOTE: Cheap-ass way of checking if WiFi seems to be enabled... + -- NOTE: Cheap-ass way of checking if Wi-Fi seems to be enabled... -- Since the crux of the issues lies in race-y module unloading, this is perfectly fine for our usage. function NetworkMgr:isWifiOn() local fd = io.open("/proc/modules", "r") if fd then local lsmod = fd:read("*all") fd:close() - -- lsmod is usually empty, unless WiFi or USB is enabled + -- lsmod is usually empty, unless Wi-Fi or USB is enabled -- We could alternatively check if lfs.attributes("/proc/sys/net/ipv4/conf/" .. os.getenv("INTERFACE"), "mode") == "directory" -- c.f., also what Cervantes does via /sys/class/net/eth0/carrier to check if the interface is up. -- That said, since we only care about whether *modules* are loaded, this does the job nicely. diff --git a/frontend/device/sony-prstux/device.lua b/frontend/device/sony-prstux/device.lua index 70dfb6698..6a1626994 100644 --- a/frontend/device/sony-prstux/device.lua +++ b/frontend/device/sony-prstux/device.lua @@ -158,6 +158,10 @@ function SonyPRSTUX:initNetworkManager(NetworkMgr) self:showNetworkMenu(complete_callback) end + function NetworkMgr:getNetworkInterfaceName() + return "wlan0" + end + NetworkMgr:setWirelessBackend("wpa_supplicant", {ctrl_interface = "/var/run/wpa_supplicant/wlan0"}) function NetworkMgr:obtainIP() diff --git a/frontend/ui/elements/filemanager_menu_order.lua b/frontend/ui/elements/filemanager_menu_order.lua index 2eae2d4e9..7e4017e17 100644 --- a/frontend/ui/elements/filemanager_menu_order.lua +++ b/frontend/ui/elements/filemanager_menu_order.lua @@ -62,9 +62,11 @@ local order = { network = { "network_wifi", "network_proxy", + "network_powersave", "network_restore", "network_info", "network_before_wifi_action", + "network_after_wifi_action", "network_dismiss_scan", "----------------------------", "ssh", diff --git a/frontend/ui/elements/reader_menu_order.lua b/frontend/ui/elements/reader_menu_order.lua index d2e46fca9..f2f9736c3 100644 --- a/frontend/ui/elements/reader_menu_order.lua +++ b/frontend/ui/elements/reader_menu_order.lua @@ -83,9 +83,11 @@ local order = { network = { "network_wifi", "network_proxy", + "network_powersave", "network_restore", "network_info", "network_before_wifi_action", + "network_after_wifi_action", "network_dismiss_scan", "----------------------------", "ssh", diff --git a/frontend/ui/network/manager.lua b/frontend/ui/network/manager.lua index dc32d2d2f..271c5302c 100644 --- a/frontend/ui/network/manager.lua +++ b/frontend/ui/network/manager.lua @@ -2,6 +2,7 @@ local BD = require("ui/bidi") local ConfirmBox = require("ui/widget/confirmbox") local DataStorage = require("datastorage") local Device = require("device") +local Event = require("ui/event") local InfoMessage = require("ui/widget/infomessage") local LuaSettings = require("luasettings") local UIManager = require("ui/uimanager") @@ -16,38 +17,80 @@ function NetworkMgr:readNWSettings() self.nw_settings = LuaSettings:open(DataStorage:getSettingsDir().."/network.lua") end --- Used after restoreWifiAsync() to make sure we eventually send a NetworkConnected event, as a few things rely on it (KOSync, c.f. #5109). -function NetworkMgr:connectivityCheck(iter) - -- Give up after a while... - if iter > 6 then +-- Used after restoreWifiAsync() and the turn_on beforeWifiAction to make sure we eventually send a NetworkConnected event, +-- as quite a few things rely on it (KOSync, c.f. #5109; the network activity check, c.f., #6424). +function NetworkMgr:connectivityCheck(iter, callback, widget) + -- Give up after a while (restoreWifiAsync can take over 45s, so, try to cover that)... + if iter > 25 then + logger.info("Failed to restore Wi-Fi (after", iter, "iterations)!") + self.wifi_was_on = false + G_reader_settings:saveSetting("wifi_was_on", false) + -- If we abort, murder Wi-Fi and the async script first... + if Device:hasWifiManager() and not Device:isEmulator() then + os.execute("pkill -TERM restore-wifi-async.sh 2>/dev/null") + end + NetworkMgr:turnOffWifi() + + -- Handle the UI warning if it's from a beforeWifiAction... + if widget then + UIManager:close(widget) + UIManager:show(InfoMessage:new{ text = _("Error connecting to the network") }) + end return end if NetworkMgr:isWifiOn() and NetworkMgr:isConnected() then - local Event = require("ui/event") + self.wifi_was_on = true + G_reader_settings:saveSetting("wifi_was_on", true) UIManager:broadcastEvent(Event:new("NetworkConnected")) - logger.info("WiFi successfully restored!") + logger.info("Wi-Fi successfully restored (after", iter, "iterations)!") + + -- Handle the UI & callback if it's from a beforeWifiAction... + if widget then + UIManager:close(widget) + end + if callback then + callback() + else + -- If this trickled down from a turn_onbeforeWifiAction and there is no callback, + -- mention that the action needs to be retried manually. + if widget then + UIManager:show(InfoMessage:new{ + text = _("You can now retry the action that required network access"), + timeout = 3, + }) + end + end else - UIManager:scheduleIn(2, function() NetworkMgr:connectivityCheck(iter + 1) end) + UIManager:scheduleIn(2, function() NetworkMgr:connectivityCheck(iter + 1, callback, widget) end) end end -function NetworkMgr:scheduleConnectivityCheck() - UIManager:scheduleIn(2, function() NetworkMgr:connectivityCheck(1) end) +function NetworkMgr:scheduleConnectivityCheck(callback, widget) + UIManager:scheduleIn(2, function() NetworkMgr:connectivityCheck(1, callback, widget) end) end function NetworkMgr:init() - -- On Kobo, kill WiFi if NetworkMgr:isWifiOn() and NOT NetworkMgr:isConnected() - -- (i.e., if the launcher left the WiFi in an inconsistent state: modules loaded, but no route to gateway). + -- On Kobo, kill Wi-Fi if NetworkMgr:isWifiOn() and NOT NetworkMgr:isConnected() + -- (i.e., if the launcher left the Wi-Fi in an inconsistent state: modules loaded, but no route to gateway). if Device:isKobo() and self:isWifiOn() and not self:isConnected() then - logger.info("Kobo WiFi: Left in an inconsistent state by launcher!") + logger.info("Kobo Wi-Fi: Left in an inconsistent state by launcher!") self:turnOffWifi() end self.wifi_was_on = G_reader_settings:isTrue("wifi_was_on") if self.wifi_was_on and G_reader_settings:isTrue("auto_restore_wifi") then - self:restoreWifiAsync() + -- Don't bother if WiFi is already up... + if not (self:isWifiOn() and self:isConnected()) then + self:restoreWifiAsync() + end self:scheduleConnectivityCheck() + else + -- Trigger an initial NetworkConnected event if WiFi was already up when we were launched + if NetworkMgr:isWifiOn() and NetworkMgr:isConnected() then + -- NOTE: This needs to be delayed because NetworkListener is initialized slightly later by the FM/Reader app... + UIManager:scheduleIn(2, function() UIManager:broadcastEvent(Event:new("NetworkConnected")) end) + end end end @@ -57,6 +100,7 @@ end function NetworkMgr:turnOnWifi() end function NetworkMgr:turnOffWifi() end function NetworkMgr:isWifiOn() end +function NetworkMgr:getNetworkInterfaceName() end function NetworkMgr:getNetworkList() end function NetworkMgr:getCurrentNetwork() end function NetworkMgr:authenticateNetwork() end @@ -92,32 +136,69 @@ function NetworkMgr:promptWifiOff(complete_callback) end function NetworkMgr:turnOnWifiAndWaitForConnection(callback) - NetworkMgr:turnOnWifi() - local timeout = 30 - local retry_count = 0 - local info = InfoMessage:new{ text = T(_("Enabling Wi-Fi. Waiting for Internet connection…\nTimeout %1 seconds."), timeout)} + local info = InfoMessage:new{ text = _("Connecting to Wi-Fi…") } UIManager:show(info) UIManager:forceRePaint() - while not NetworkMgr:isOnline() and retry_count < timeout do - ffiutil.sleep(1) - retry_count = retry_count + 1 - end - UIManager:close(info) - if retry_count == timeout then - UIManager:show(InfoMessage:new{ text = _("Error connecting to the network") }) - return + + -- Don't bother if WiFi is already up... + if not (self:isWifiOn() and self:isConnected()) then + self:turnOnWifi() end - if callback then callback() end + + -- This will handle sending the proper Event, manage wifi_was_on, as well as tearing down Wi-Fi in case of failures, + -- (i.e., much like getWifiToggleMenuTable). + self:scheduleConnectivityCheck(callback, info) +end + +--- This quirky internal flag is used for the rare beforeWifiAction -> afterWifiAction brackets. +function NetworkMgr:clearBeforeActionFlag() + self._before_action_tripped = nil +end + +function NetworkMgr:setBeforeActionFlag() + self._before_action_tripped = true +end + +function NetworkMgr:getBeforeActionFlag() + return self._before_action_tripped end +--- @note: The callback will only run *after* a *succesful* network connection. +--- The only guarantee it provides is isConnected (i.e., an IP & a local gateway), +--- *NOT* isOnline (i.e., WAN), se be careful with recursive callbacks! function NetworkMgr:beforeWifiAction(callback) + -- Remember that we ran, for afterWifiAction... + self:setBeforeActionFlag() + local wifi_enable_action = G_reader_settings:readSetting("wifi_enable_action") if wifi_enable_action == "turn_on" then NetworkMgr:turnOnWifiAndWaitForConnection(callback) else NetworkMgr:promptWifiOn(callback) end - end +end + +-- NOTE: This is actually used very sparingly (newsdownloader/send2ebook), +-- because bracketing a single action in a connect/disconnect session doesn't necessarily make much sense... +function NetworkMgr:afterWifiAction(callback) + -- Don't do anything if beforeWifiAction never actually ran... + if not self:getBeforeActionFlag() then + return + end + self:clearBeforeActionFlag() + + local wifi_disable_action = G_reader_settings:readSetting("wifi_disable_action") + if wifi_disable_action == "leave_on" then + -- NOP :) + if callback then + callback() + end + elseif wifi_disable_action == "turn_off" then + NetworkMgr:turnOffWifi(callback) + else + NetworkMgr:promptWifiOff(callback) + end +end function NetworkMgr:isConnected() if Device:isAndroid() or Device:isCervantes() or Device:isPocketBook() or Device:isEmulator() then @@ -174,6 +255,68 @@ function NetworkMgr:setHTTPProxy(proxy) end end +-- Helper functions to hide the quirks of using beforeWifiAction properly ;). + +-- Run callback *now* if you're currently online (ie., isOnline), +-- or attempt to go online and run it *ASAP* without any more user interaction. +-- NOTE: If you're currently connected but without Internet access (i.e., isConnected and not isOnline), +-- it will just attempt to re-connect, *without* running the callback. +-- c.f., ReaderWikipedia:onShowWikipediaLookup @ frontend/apps/reader/modules/readerwikipedia.lua +function NetworkMgr:runWhenOnline(callback) + if self:isOnline() then + callback() + else + --- @note: Avoid infinite recursion, beforeWifiAction only guarantees isConnected, not isOnline. + if not self:isConnected() then + self:beforeWifiAction(callback) + else + self:beforeWifiAction() + end + end +end + +-- This one is for callbacks that only require isConnected, and since that's guaranteed by beforeWifiAction, +-- you also have a guarantee that the callback *will* run. +function NetworkMgr:runWhenConnected(callback) + if self:isConnected() then + callback() + else + self:beforeWifiAction(callback) + end +end + +-- Mild variants that are used for recursive calls at the beginning of a complex function call. +-- Returns true when not yet online, in which case you should *abort* (i.e., return) the initial call, +-- and otherwise, go-on as planned. +-- NOTE: If you're currently connected but without Internet access (i.e., isConnected and not isOnline), +-- it will just attempt to re-connect, *without* running the callback. +-- c.f., ReaderWikipedia:lookupWikipedia @ frontend/apps/reader/modules/readerwikipedia.lua +function NetworkMgr:willRerunWhenOnline(callback) + if not self:isOnline() then + --- @note: Avoid infinite recursion, beforeWifiAction only guarantees isConnected, not isOnline. + if not self:isConnected() then + self:beforeWifiAction(callback) + else + self:beforeWifiAction() + end + return true + end + + return false +end + +-- This one is for callbacks that only require isConnected, and since that's guaranteed by beforeWifiAction, +-- you also have a guarantee that the callback *will* run. +function NetworkMgr:willRerunWhenConnected(callback) + if not self:isConnected() then + self:beforeWifiAction(callback) + return true + end + + return false +end + + function NetworkMgr:getWifiMenuTable() if Device:isAndroid() then return { @@ -196,7 +339,6 @@ function NetworkMgr:getWifiToggleMenuTable() local complete_callback = function() -- notify touch menu to update item check state touchmenu_instance:updateItems() - local Event = require("ui/event") -- if wifi was on, this callback will only be executed when the network has been -- disconnected. if wifi_status then @@ -208,7 +350,7 @@ function NetworkMgr:getWifiToggleMenuTable() if NetworkMgr:isWifiOn() and NetworkMgr:isConnected() then UIManager:broadcastEvent(Event:new("NetworkConnected")) elseif NetworkMgr:isWifiOn() and not NetworkMgr:isConnected() then - -- Don't leave WiFi in an inconsistent state if the connection failed. + -- Don't leave Wi-Fi in an inconsistent state if the connection failed. self.wifi_was_on = false G_reader_settings:saveSetting("wifi_was_on", false) -- NOTE: We're limiting this to only a few platforms, as it might be actually harmful on some devices. @@ -219,8 +361,8 @@ function NetworkMgr:getWifiToggleMenuTable() -- Kobo: Yes, please. -- Cervantes: Loads/unloads module, probably could use it like Kobo. -- Kindle: Probably could use it, if only because leaving Wireless on is generally a terrible idea on Kindle, - -- except that we defer to lipc, which makes WiFi handling asynchronous, and the callback is simply delayed by 1s, - -- so we can't be sure the system will actually have finished bringing WiFi up by then... + -- except that we defer to lipc, which makes Wi-Fi handling asynchronous, and the callback is simply delayed by 1s, + -- so we can't be sure the system will actually have finished bringing Wi-Fi up by then... NetworkMgr:turnOffWifi() touchmenu_instance:updateItems() end @@ -276,9 +418,26 @@ function NetworkMgr:getProxyMenuTable() } end +function NetworkMgr:getPowersaveMenuTable() + return { + text = _("Kill Wi-Fi connection when inactive"), + help_text = _([[This will automatically turn Wi-Fi off after a generous period of network inactivity, without disrupting workflows that require a network connection, so you can just keep reading without worrying about battery drain.]]), + checked_func = function() return G_reader_settings:isTrue("auto_disable_wifi") end, + enabled_func = function() return Device:hasWifiManager() and not Device:isEmulator() end, + callback = function() + G_reader_settings:flipNilOrFalse("auto_disable_wifi") + -- NOTE: Well, not exactly, but the activity check wouldn't be (un)scheduled until the next Network(Dis)Connected event... + UIManager:show(InfoMessage:new{ + text = _("This will take effect on next restart."), + }) + end, + } +end + function NetworkMgr:getRestoreMenuTable() return { - text = _("Automatically restore Wi-Fi connection after resume"), + text = _("Restore Wi-Fi connection on resume"), + help_text = _([[This will attempt to automatically and silently re-connect to Wi-Fi on startup or on resume if Wi-Fi used to be enabled the last time you used KOReader.]]), checked_func = function() return G_reader_settings:isTrue("auto_restore_wifi") end, enabled_func = function() return Device:hasWifiManager() and not Device:isEmulator() end, callback = function() G_reader_settings:flipNilOrFalse("auto_restore_wifi") end, @@ -306,34 +465,67 @@ function NetworkMgr:getInfoMenuTable() end function NetworkMgr:getBeforeWifiActionMenuTable() - local wifi_enable_action_setting = G_reader_settings:readSetting("wifi_enable_action") or "prompt" - local wifi_enable_actions = { - turn_on = {_("turn on"), _("Turn on (experimental)")}, - prompt = {_("prompt"), _("Prompt")}, - } - local action_table = function(wifi_enable_action) - return { - text = wifi_enable_actions[wifi_enable_action][2], - checked_func = function() - return wifi_enable_action_setting == wifi_enable_action - end, - callback = function() - wifi_enable_action_setting = wifi_enable_action - G_reader_settings:saveSetting("wifi_enable_action", wifi_enable_action) - end, - } - end - return { - text_func = function() - return T(_("Action when Wi-Fi is off: %1"), - wifi_enable_actions[wifi_enable_action_setting][1] - ) - end, - sub_item_table = { - action_table("turn_on"), - action_table("prompt"), - } - } + local wifi_enable_action_setting = G_reader_settings:readSetting("wifi_enable_action") or "prompt" + local wifi_enable_actions = { + turn_on = {_("turn on"), _("Turn on")}, + prompt = {_("prompt"), _("Prompt")}, + } + local action_table = function(wifi_enable_action) + return { + text = wifi_enable_actions[wifi_enable_action][2], + checked_func = function() + return wifi_enable_action_setting == wifi_enable_action + end, + callback = function() + wifi_enable_action_setting = wifi_enable_action + G_reader_settings:saveSetting("wifi_enable_action", wifi_enable_action) + end, + } + end + return { + text_func = function() + return T(_("Action when Wi-Fi is off: %1"), + wifi_enable_actions[wifi_enable_action_setting][1] + ) + end, + sub_item_table = { + action_table("turn_on"), + action_table("prompt"), + } + } +end + +function NetworkMgr:getAfterWifiActionMenuTable() + local wifi_disable_action_setting = G_reader_settings:readSetting("wifi_disable_action") or "prompt" + local wifi_disable_actions = { + leave_on = {_("leave on"), _("Leave on")}, + turn_off = {_("turn off"), _("Turn off")}, + prompt = {_("prompt"), _("Prompt")}, + } + local action_table = function(wifi_disable_action) + return { + text = wifi_disable_actions[wifi_disable_action][2], + checked_func = function() + return wifi_disable_action_setting == wifi_disable_action + end, + callback = function() + wifi_disable_action_setting = wifi_disable_action + G_reader_settings:saveSetting("wifi_disable_action", wifi_disable_action) + end, + } + end + return { + text_func = function() + return T(_("Action when done with Wi-Fi: %1"), + wifi_disable_actions[wifi_disable_action_setting][1] + ) + end, + sub_item_table = { + action_table("leave_on"), + action_table("turn_off"), + action_table("prompt"), + } + } end function NetworkMgr:getDismissScanMenuTable() @@ -354,9 +546,11 @@ function NetworkMgr:getMenuTable(common_settings) common_settings.network_info = self:getInfoMenuTable() if Device:hasWifiManager() then + common_settings.network_powersave = self:getPowersaveMenuTable() common_settings.network_restore = self:getRestoreMenuTable() common_settings.network_dismiss_scan = self:getDismissScanMenuTable() common_settings.network_before_wifi_action = self:getBeforeWifiActionMenuTable() + common_settings.network_after_wifi_action = self:getAfterWifiActionMenuTable() end end @@ -458,6 +652,7 @@ if NETWORK_PROXY then NetworkMgr:setHTTPProxy(NETWORK_PROXY) end + Device:initNetworkManager(NetworkMgr) NetworkMgr:init() diff --git a/frontend/ui/network/networklistener.lua b/frontend/ui/network/networklistener.lua index 0aa6869f9..2f86214d6 100644 --- a/frontend/ui/network/networklistener.lua +++ b/frontend/ui/network/networklistener.lua @@ -1,8 +1,11 @@ local BD = require("ui/bidi") +local Device = require("device") +local Event = require("ui/event") local InfoMessage = require("ui/widget/infomessage") local InputContainer = require("ui/widget/container/inputcontainer") local NetworkMgr = require("ui/network/manager") local UIManager = require("ui/uimanager") +local logger = require("logger") local _ = require("gettext") local T = require("ffi/util").template @@ -16,10 +19,17 @@ function NetworkListener:onToggleWifi() }) -- NB Normal widgets should use NetworkMgr:promptWifiOn() - -- This is specifically the toggle wifi action, so consent is implied. - NetworkMgr:turnOnWifi() + -- (or, better yet, the NetworkMgr:beforeWifiAction wrappers: NetworkMgr:runWhenOnline() & co.) + -- This is specifically the toggle Wi-Fi action, so consent is implied. + local complete_callback = function() + UIManager:broadcastEvent(Event:new("NetworkConnected")) + end + NetworkMgr:turnOnWifi(complete_callback) else - NetworkMgr:turnOffWifi() + local complete_callback = function() + UIManager:broadcastEvent(Event:new("NetworkDisconnected")) + end + NetworkMgr:turnOffWifi(complete_callback) UIManager:show(InfoMessage:new{ text = _("Wi-Fi off."), @@ -29,8 +39,11 @@ function NetworkListener:onToggleWifi() end function NetworkListener:onInfoWifiOff() - -- can't hurt - NetworkMgr:turnOffWifi() + -- That's the end goal + local complete_callback = function() + UIManager:broadcastEvent(Event:new("NetworkDisconnected")) + end + NetworkMgr:turnOffWifi(complete_callback) UIManager:show(InfoMessage:new{ text = _("Wi-Fi off."), @@ -46,8 +59,12 @@ function NetworkListener:onInfoWifiOn() }) -- NB Normal widgets should use NetworkMgr:promptWifiOn() + -- (or, better yet, the NetworkMgr:beforeWifiAction wrappers: NetworkMgr:runWhenOnline() & co.) -- This is specifically the toggle Wi-Fi action, so consent is implied. - NetworkMgr:turnOnWifi() + local complete_callback = function() + UIManager:broadcastEvent(Event:new("NetworkConnected")) + end + NetworkMgr:turnOnWifi(complete_callback) else local info_text local current_network = NetworkMgr:getCurrentNetwork() @@ -64,4 +81,136 @@ function NetworkListener:onInfoWifiOn() end end +-- Everything below is to handle auto_disable_wifi ;). +local default_network_timeout_seconds = 5*60 +local max_network_timeout_seconds = 30*60 +-- This should be more than enough to catch actual activity vs. noise spread over 5 minutes. +local network_activity_noise_margin = 12 -- unscaled_size_check: ignore + +-- Read the statistics/tx_packets sysfs entry for the current network interface. +-- It *should* be the least noisy entry on an idle network... +-- The fact that auto_disable_wifi is only available on (Device:hasWifiManager() and not Device:isEmulator()) +-- allows us to get away with a Linux-only solution. +function NetworkListener:_getTxPackets() + -- read tx_packets stats from sysfs (for the right network if) + local file = io.open("/sys/class/net/" .. NetworkMgr:getNetworkInterfaceName() .. "/statistics/tx_packets", "rb") + + -- file exists only when Wi-Fi module is loaded. + if not file then return nil end + + local out = file:read("*all") + file:close() + + -- strip NaN from file read (i.e.,: line endings, error messages) + local tx_packets + if type(out) ~= "number" then + tx_packets = tonumber(out) + else + tx_packets = out + end + + -- finally return it + if type(tx_packets) == "number" then + return tx_packets + else + return nil + end +end + +function NetworkListener:_unscheduleActivityCheck() + logger.dbg("NetworkListener: unschedule network activity check") + if self._activity_check_scheduled then + UIManager:unschedule(self._scheduleActivityCheck) + self._activity_check_scheduled = nil + logger.dbg("NetworkListener: network activity check unscheduled") + end + + -- We also need to reset the stats, otherwise we'll be comparing apples vs. oranges... (i.e., two different network sessions) + if self._last_tx_packets then + self._last_tx_packets = nil + end + if self._activity_check_delay then + self._activity_check_delay = nil + end +end + +function NetworkListener:_scheduleActivityCheck() + logger.dbg("NetworkListener: network activity check") + local keep_checking = true + + local tx_packets = NetworkListener:_getTxPackets() + if self._last_tx_packets then + -- Compute noise margin based on the current delay + local delay = self._activity_check_delay or default_network_timeout_seconds + local noise = delay / default_network_timeout_seconds * network_activity_noise_margin + -- If there was no meaningful activity (+/- a couple packets), kill the Wi-Fi + if math.max(0, tx_packets - noise) <= self._last_tx_packets then + logger.dbg("NetworkListener: No meaningful network activity ( then:", self._last_tx_packets, "vs. now:", tx_packets, "), disabling Wi-Fi") + keep_checking = false + local complete_callback = function() + UIManager:broadcastEvent(Event:new("NetworkDisconnected")) + end + NetworkMgr:turnOffWifi(complete_callback) + -- NOTE: We leave wifi_was_on as-is on purpose, we wouldn't want to break auto_restore_wifi workflows on the next start... + end + end + + -- If we've just killed Wi-Fi, onNetworkDisconnected will take care of unscheduling us + if keep_checking then + -- Update tracker for next iter + self._last_tx_packets = tx_packets + + -- If it's already been scheduled, increase the delay until we hit the ceiling + if self._activity_check_delay then + self._activity_check_delay = self._activity_check_delay + default_network_timeout_seconds + + if self._activity_check_delay > max_network_timeout_seconds then + self._activity_check_delay = max_network_timeout_seconds + end + else + self._activity_check_delay = default_network_timeout_seconds + end + + UIManager:scheduleIn(self._activity_check_delay, self._scheduleActivityCheck, self) + self._activity_check_scheduled = true + logger.dbg("NetworkListener: network activity check scheduled in", self._activity_check_delay, "seconds") + end +end + +function NetworkListener:onNetworkConnected() + if not (Device:hasWifiManager() and not Device:isEmulator()) then + return + end + + if not G_reader_settings:isTrue("auto_disable_wifi") then + return + end + + -- If the activity check has already been scheduled for some reason, unschedule it first. + NetworkListener:_unscheduleActivityCheck() + + NetworkListener:_scheduleActivityCheck() +end + +function NetworkListener:onNetworkDisconnected() + if not (Device:hasWifiManager() and not Device:isEmulator()) then + return + end + + if not G_reader_settings:isTrue("auto_disable_wifi") then + return + end + + NetworkListener:_unscheduleActivityCheck() + + -- Reset NetworkMgr's beforeWifiAction marker + NetworkMgr:clearBeforeActionFlag() +end + +-- Also unschedule on suspend (and we happen to also kill Wi-Fi to do so, so resetting the stats is also relevant here) +function NetworkListener:onSuspend() + self:onNetworkDisconnected() +end + + return NetworkListener diff --git a/frontend/ui/otamanager.lua b/frontend/ui/otamanager.lua index 44e33b16c..ca3ea9025 100644 --- a/frontend/ui/otamanager.lua +++ b/frontend/ui/otamanager.lua @@ -455,21 +455,19 @@ function OTAManager:getOTAMenuTable() return { text = _("Update"), hold_callback = function() - if not NetworkMgr:isOnline() then - NetworkMgr:promptWifiOn() - else + local connect_callback = function() OTAManager:fetchAndProcessUpdate() end + NetworkMgr:runWhenOnline(connect_callback) end, sub_item_table = { { text = _("Check for update"), callback = function() - if not NetworkMgr:isOnline() then - NetworkMgr:promptWifiOn() - else + local connect_callback = function() OTAManager:fetchAndProcessUpdate() end + NetworkMgr:runWhenOnline(connect_callback) end }, { diff --git a/frontend/ui/translator.lua b/frontend/ui/translator.lua index 28704628b..a8d183660 100644 --- a/frontend/ui/translator.lua +++ b/frontend/ui/translator.lua @@ -416,10 +416,10 @@ Show translated text in TextViewer, with alternate translations --]] function Translator:showTranslation(text, target_lang, source_lang) local NetworkMgr = require("ui/network/manager") - if not NetworkMgr:isOnline() then - NetworkMgr:promptWifiOn() + if NetworkMgr:willRerunWhenOnline(function() self:showTranslation(text, target_lang, source_lang) end) then return end + -- Wrap next function with Trapper to be able to interrupt -- translation service query. local Trapper = require("ui/trapper") diff --git a/frontend/ui/widget/networksetting.lua b/frontend/ui/widget/networksetting.lua index 3d17297ee..62b13a541 100644 --- a/frontend/ui/widget/networksetting.lua +++ b/frontend/ui/widget/networksetting.lua @@ -22,9 +22,12 @@ Example: UIManager:show(require("ui/widget/networksetting"):new{ network_list = network_list, connect_callback = function() - -- connect_callback will be called when an connect/disconnect - -- attempt has been made. you can update UI widgets in the - -- callback. + -- connect_callback will be called when a *connect* (NOT disconnect) + -- attempt has been successful. + -- You can update UI widgets in the callback. + end, + disconnect_callback = function() + -- This one will fire unconditionally after a disconnect attempt. end, }) @@ -224,7 +227,8 @@ function NetworkItem:connect() text = err_msg end - if self.setting_ui.connect_callback then + -- Do what it says on the tin, and only trigger the connect_callback on a *successful* connect. + if success and self.setting_ui.connect_callback then self.setting_ui.connect_callback() end @@ -244,8 +248,8 @@ function NetworkItem:disconnect() self.info.connected = nil self:refresh() self.setting_ui:setConnectedItem(nil) - if self.setting_ui.connect_callback then - self.setting_ui.connect_callback() + if self.setting_ui.disconnect_callback then + self.setting_ui.disconnect_callback() end end @@ -378,6 +382,7 @@ local NetworkSetting = InputContainer:new{ -- } network_list = nil, connect_callback = nil, + disconnect_callback = nil, } function NetworkSetting:init() diff --git a/frontend/ui/widget/opdsbrowser.lua b/frontend/ui/widget/opdsbrowser.lua index 90526ec46..af176587f 100644 --- a/frontend/ui/widget/opdsbrowser.lua +++ b/frontend/ui/widget/opdsbrowser.lua @@ -377,10 +377,7 @@ end function OPDSBrowser:getCatalog(item_url, username, password) local ok, catalog = pcall(self.parseFeed, self, item_url, username, password) - if not ok and catalog and not NetworkMgr:isOnline() then - NetworkMgr:promptWifiOn() - return - elseif not ok and catalog then + if not ok and catalog then logger.info("cannot get catalog info from", item_url, catalog) UIManager:show(InfoMessage:new{ text = T(_("Cannot get catalog info from %1"), (BD.url(item_url) or "")), @@ -724,11 +721,17 @@ function OPDSBrowser:onMenuSelect(item) self:showDownloads(item) -- navigation else + local connect_callback if item.searchable then - self:browseSearchable(item.url, item.username, item.password) + connect_callback = function() + self:browseSearchable(item.url, item.username, item.password) + end else - self:browse(item.url, item.username, item.password) + connect_callback = function() + self:browse(item.url, item.username, item.password) + end end + NetworkMgr:runWhenConnected(connect_callback) end return true end diff --git a/platform/cervantes/restore-wifi-async.sh b/platform/cervantes/restore-wifi-async.sh index e3fa99d67..42306beeb 100755 --- a/platform/cervantes/restore-wifi-async.sh +++ b/platform/cervantes/restore-wifi-async.sh @@ -21,11 +21,11 @@ EOF } RestoreWifi() { - echo "[$(date)] restore-wifi-async.sh: Restarting WiFi" + echo "[$(date)] restore-wifi-async.sh: Restarting Wi-Fi" ./enable-wifi.sh RunWpaCli ./obtain-ip.sh - echo "[$(date)] restore-wifi-async.sh: Restarted WiFi" + echo "[$(date)] restore-wifi-async.sh: Restarted Wi-Fi" } RestoreWifi & diff --git a/platform/kobo/disable-wifi.sh b/platform/kobo/disable-wifi.sh index a8737f1cd..fc171a4d1 100755 --- a/platform/kobo/disable-wifi.sh +++ b/platform/kobo/disable-wifi.sh @@ -1,8 +1,38 @@ #!/bin/sh # Disable wifi, and remove all modules. +# NOTE: Save our resolv.conf to avoid ending up with an empty one, in case the DHCP client wipes it on release (#6424). +cp -a "/etc/resolv.conf" "/tmp/resolv.ko" +old_hash="$(md5sum "/etc/resolv.conf" | cut -f1 -d' ')" -killall udhcpc default.script wpa_supplicant 2>/dev/null +if [ -x "/sbin/dhcpcd" ]; then + env -u LD_LIBRARY_PATH dhcpcd -d -k "${INTERFACE}" + killall -q -TERM udhcpc default.script +else + killall -q -TERM udhcpc default.script dhcpcd +fi + +# NOTE: dhcpcd -k waits for the signalled process to die, but busybox's killall doesn't have a -w, --wait flag, +# so we have to wait for udhcpc to die ourselves... +kill_timeout=0 +while pkill -0 udhcpc; do + # Stop waiting after 5s + if [ ${kill_timeout} -ge 20 ]; then + break + fi + usleep 250000 + kill_timeout=$((kill_timeout + 1)) +done + +new_hash="$(md5sum "/etc/resolv.conf" | cut -f1 -d' ')" +# Restore our network-specific resolv.conf if the DHCP client wiped it when releasing the lease... +if [ "${new_hash}" != "${old_hash}" ]; then + mv -f "/tmp/resolv.ko" "/etc/resolv.conf" +else + rm -f "/tmp/resolv.ko" +fi + +wpa_cli terminate [ "${WIFI_MODULE}" != "8189fs" ] && [ "${WIFI_MODULE}" != "8192es" ] && wlarm_le -i "${INTERFACE}" down ifconfig "${INTERFACE}" down diff --git a/platform/kobo/enable-wifi.sh b/platform/kobo/enable-wifi.sh index f95c6d019..98dd0ae9b 100755 --- a/platform/kobo/enable-wifi.sh +++ b/platform/kobo/enable-wifi.sh @@ -1,7 +1,6 @@ #!/bin/sh # Load wifi modules and enable wifi. - lsmod | grep -q sdio_wifi_pwr || insmod "/drivers/${PLATFORM}/wifi/sdio_wifi_pwr.ko" # Moar sleep! usleep 250000 @@ -13,6 +12,6 @@ sleep 1 ifconfig "${INTERFACE}" up [ "${WIFI_MODULE}" != "8189fs" ] && [ "${WIFI_MODULE}" != "8192es" ] && wlarm_le -i "${INTERFACE}" up -pidof wpa_supplicant >/dev/null || +pkill -0 wpa_supplicant || env -u LD_LIBRARY_PATH \ - wpa_supplicant -D wext -s -i "${INTERFACE}" -O /var/run/wpa_supplicant -c /etc/wpa_supplicant/wpa_supplicant.conf -B + wpa_supplicant -D wext -s -i "${INTERFACE}" -c /etc/wpa_supplicant/wpa_supplicant.conf -O /var/run/wpa_supplicant -B diff --git a/platform/kobo/koreader.sh b/platform/kobo/koreader.sh index dea953577..b83f0d6b4 100755 --- a/platform/kobo/koreader.sh +++ b/platform/kobo/koreader.sh @@ -110,7 +110,11 @@ if [ "${VIA_NICKEL}" = "true" ]; then sync # And we can now stop the full Kobo software stack # NOTE: We don't need to kill KFMon, it's smart enough not to allow running anything else while we're up - killall -TERM nickel hindenburg sickel fickel adobehost fmon 2>/dev/null + # NOTE: We kill Nickel's master dhcpcd daemon on purpose, + # as we want to be able to use our own per-if processes w/ custom args later on. + # A SIGTERM does not break anything, it'll just prevent automatic lease renewal until the time + # KOReader actually sets the if up itself (i.e., it'll do)... + killall -q -TERM nickel hindenburg sickel fickel adobehost dhcpcd-dbus dhcpcd fmon fi # fallback for old fmon, KFMon and advboot users (-> if no args were passed to the script, start the FM) @@ -131,7 +135,7 @@ if [ -z "${PRODUCT}" ]; then export PRODUCT fi -# PLATFORM is used in koreader for the path to the WiFi drivers (as well as when restarting nickel) +# PLATFORM is used in koreader for the path to the Wi-Fi drivers (as well as when restarting nickel) if [ -z "${PLATFORM}" ]; then # shellcheck disable=SC2046 export $(grep -s -e '^PLATFORM=' "/proc/$(pidof -s udevd)/environ") @@ -209,6 +213,16 @@ ko_do_fbdepth() { fi } +# Ensure we start with a valid nameserver in resolv.conf, otherwise we're stuck with broken name resolution (#6421, #6424). +# Fun fact: this wouldn't be necessary if Kobo were using a non-prehistoric glibc... (it was fixed in glibc 2.26). +ko_do_dns() { + # If there aren't any servers listed, append CloudFlare's + if not grep -q '^nameserver' "/etc/resolv.conf"; then + echo "# Added by KOReader because your setup is broken" >>"/etc/resolv.conf" + echo "nameserver 1.1.1.1" >>"/etc/resolv.conf" + fi +} + # Remount the SD card RW if it's inserted and currently RO if awk '$4~/(^|,)ro($|,)/' /proc/mounts | grep ' /mnt/sd '; then mount -o remount,rw /mnt/sd @@ -232,6 +246,8 @@ while [ ${RETURN_VALUE} -ne 0 ]; do ko_update_check # Do or double-check the fb depth switch, or restore original bitdepth if requested ko_do_fbdepth + # Make sure we have a sane resolv.conf + ko_do_dns fi ./reader.lua "${args}" >>crash.log 2>&1 @@ -273,11 +289,11 @@ while [ ${RETURN_VALUE} -ne 0 ]; do fi # U+1F4A3, the hard way, because we can't use \u or \U escape sequences... # shellcheck disable=SC2039 - ./fbink -q -b -O -m -t regular=./fonts/freefont/FreeSerif.ttf,px=${bombHeight},top=${bombMargin} $'\xf0\x9f\x92\xa3' + ./fbink -q -b -O -m -t regular=./fonts/freefont/FreeSerif.ttf,px=${bombHeight},top=${bombMargin} -- $'\xf0\x9f\x92\xa3' # And then print the tail end of the log on the bottom of the screen... crashLog="$(tail -n 25 crash.log | sed -e 's/\t/ /g')" # The idea for the margins being to leave enough room for an fbink -Z bar, small horizontal margins, and a font size based on what 6pt looked like @ 265dpi - ./fbink -q -b -O -t regular=./fonts/droid/DroidSansMono.ttf,top=$((viewHeight / 2 + FONTH * 2 + FONTH / 2)),left=$((viewWidth / 60)),right=$((viewWidth / 60)),px=$((viewHeight / 64)) "${crashLog}" + ./fbink -q -b -O -t regular=./fonts/droid/DroidSansMono.ttf,top=$((viewHeight / 2 + FONTH * 2 + FONTH / 2)),left=$((viewWidth / 60)),right=$((viewWidth / 60)),px=$((viewHeight / 64)) -- "${crashLog}" # So far, we hadn't triggered an actual screen refresh, do that now, to make sure everything is bundled in a single flashing refresh. ./fbink -q -f -s # Cue a lemming's faceplant sound effect! diff --git a/platform/kobo/nickel.sh b/platform/kobo/nickel.sh index 71b12dc66..76562dbc3 100755 --- a/platform/kobo/nickel.sh +++ b/platform/kobo/nickel.sh @@ -21,10 +21,38 @@ unset KO_NO_CBB /etc/init.d/on-animator.sh ) & -# Make sure we kill the WiFi first, because nickel apparently doesn't like it if it's up... (cf. #1520) +# Make sure we kill the Wi-Fi first, because nickel apparently doesn't like it if it's up... (cf. #1520) # NOTE: That check is possibly wrong on PLATFORM == freescale (because I don't know if the sdio_wifi_pwr module exists there), but we don't terribly care about that. if lsmod | grep -q sdio_wifi_pwr; then - killall restore-wifi-async.sh enable-wifi.sh obtain-ip.sh udhcpc default.script wpa_supplicant 2>/dev/null + killall -q -TERM restore-wifi-async.sh enable-wifi.sh obtain-ip.sh + cp -a "/etc/resolv.conf" "/tmp/resolv.ko" + old_hash="$(md5sum "/etc/resolv.conf" | cut -f1 -d' ')" + if [ -x "/sbin/dhcpcd" ]; then + env -u LD_LIBRARY_PATH dhcpcd -d -k "${INTERFACE}" + killall -q -TERM udhcpc default.script + else + killall -q -TERM udhcpc default.script dhcpcd + fi + # NOTE: dhcpcd -k waits for the signalled process to die, but busybox's killall doesn't have a -w, --wait flag, + # so we have to wait for udhcpc to die ourselves... + kill_timeout=0 + while pkill -0 udhcpc; do + # Stop waiting after 5s + if [ ${kill_timeout} -ge 20 ]; then + break + fi + usleep 250000 + kill_timeout=$((kill_timeout + 1)) + done + + new_hash="$(md5sum "/etc/resolv.conf" | cut -f1 -d' ')" + # Restore our network-specific resolv.conf if the DHCP client wiped it when releasing the lease... + if [ "${new_hash}" != "${old_hash}" ]; then + mv -f "/tmp/resolv.ko" "/etc/resolv.conf" + else + rm -f "/tmp/resolv.ko" + fi + wpa_cli terminate [ "${WIFI_MODULE}" != "8189fs" ] && [ "${WIFI_MODULE}" != "8192es" ] && wlarm_le -i "${INTERFACE}" down ifconfig "${INTERFACE}" down # NOTE: Kobo's busybox build is weird. rmmod appears to be modprobe in disguise, defaulting to the -r flag... diff --git a/platform/kobo/obtain-ip.sh b/platform/kobo/obtain-ip.sh index 0bd2fe731..fd4dca469 100755 --- a/platform/kobo/obtain-ip.sh +++ b/platform/kobo/obtain-ip.sh @@ -2,5 +2,10 @@ ./release-ip.sh -# Use udhcpc to obtain IP. -env -u LD_LIBRARY_PATH udhcpc -S -i "${INTERFACE}" -s /etc/udhcpc.d/default.script -t15 -T10 -A3 -b -q +# NOTE: Prefer dhcpcd over udhcpc if available. That's what Nickel uses, +# and udhcpc appears to trip some insanely wonky corner cases on current FW (#6421) +if [ -x "/sbin/dhcpcd" ]; then + env -u LD_LIBRARY_PATH dhcpcd -d -t 30 -w "${INTERFACE}" +else + env -u LD_LIBRARY_PATH udhcpc -S -i "${INTERFACE}" -s /etc/udhcpc.d/default.script -b -q +fi diff --git a/platform/kobo/release-ip.sh b/platform/kobo/release-ip.sh index 4dcc083d0..2746c3d29 100755 --- a/platform/kobo/release-ip.sh +++ b/platform/kobo/release-ip.sh @@ -1,8 +1,34 @@ #!/bin/sh -# PATH export is only needed if you run this script manually from a shell -export PATH="${PATH}:/sbin" - # Release IP and shutdown udhcpc. -pkill -9 -f '/bin/sh /etc/udhcpc.d/default.script' -ifconfig "${INTERFACE}" 0.0.0.0 +# NOTE: Save our resolv.conf to avoid ending up with an empty one, in case the DHCP client wipes it on release (#6424). +cp -a "/etc/resolv.conf" "/tmp/resolv.ko" +old_hash="$(md5sum "/etc/resolv.conf" | cut -f1 -d' ')" + +if [ -x "/sbin/dhcpcd" ]; then + env -u LD_LIBRARY_PATH dhcpcd -d -k "${INTERFACE}" + killall -q -TERM udhcpc default.script +else + killall -q -TERM udhcpc default.script dhcpcd + ifconfig "${INTERFACE}" 0.0.0.0 +fi + +# NOTE: dhcpcd -k waits for the signalled process to die, but busybox's killall doesn't have a -w, --wait flag, +# so we have to wait for udhcpc to die ourselves... +kill_timeout=0 +while pkill -0 udhcpc; do + # Stop waiting after 5s + if [ ${kill_timeout} -ge 20 ]; then + break + fi + usleep 250000 + kill_timeout=$((kill_timeout + 1)) +done + +new_hash="$(md5sum "/etc/resolv.conf" | cut -f1 -d' ')" +# Restore our network-specific resolv.conf if the DHCP client wiped it when releasing the lease... +if [ "${new_hash}" != "${old_hash}" ]; then + mv -f "/tmp/resolv.ko" "/etc/resolv.conf" +else + rm -f "/tmp/resolv.ko" +fi diff --git a/platform/kobo/restore-wifi-async.sh b/platform/kobo/restore-wifi-async.sh index b0ac817e0..4090df805 100755 --- a/platform/kobo/restore-wifi-async.sh +++ b/platform/kobo/restore-wifi-async.sh @@ -1,7 +1,7 @@ #!/bin/sh RestoreWifi() { - echo "[$(date)] restore-wifi-async.sh: Restarting WiFi" + echo "[$(date)] restore-wifi-async.sh: Restarting Wi-Fi" ./enable-wifi.sh @@ -9,8 +9,8 @@ RestoreWifi() { # Pilfered from https://github.com/shermp/Kobo-UNCaGED/pull/21 ;) wpac_timeout=0 while ! wpa_cli status | grep -q "wpa_state=COMPLETED"; do - # If wpa_supplicant hasn't connected within 10 seconds, assume it never will, and tear down WiFi - if [ ${wpac_timeout} -ge 40 ]; then + # If wpa_supplicant hasn't connected within 15 seconds, assume it never will, and tear down Wi-Fi + if [ ${wpac_timeout} -ge 60 ]; then echo "[$(date)] restore-wifi-async.sh: Failed to connect to preferred AP!" ./disable-wifi.sh return 1 @@ -21,7 +21,7 @@ RestoreWifi() { ./obtain-ip.sh - echo "[$(date)] restore-wifi-async.sh: Restarted WiFi" + echo "[$(date)] restore-wifi-async.sh: Restarted Wi-Fi" } RestoreWifi & diff --git a/platform/remarkable/README_remarkable.txt b/platform/remarkable/README_remarkable.txt index d4cff6245..b0e202ff8 100644 --- a/platform/remarkable/README_remarkable.txt +++ b/platform/remarkable/README_remarkable.txt @@ -1,6 +1,6 @@ # General -When connected to WiFi you can find the IP address and root password for your +When connected to Wi-Fi you can find the IP address and root password for your reMarkable in Settings -> About, then scroll down the GPLv3 compliance on the right (finger drag scroll, not the page forward/back buttons). This should also work for the USB network you get if you connect the reMarkable to your computer diff --git a/platform/remarkable/koreader.sh b/platform/remarkable/koreader.sh index fcb79746c..7583a014d 100755 --- a/platform/remarkable/koreader.sh +++ b/platform/remarkable/koreader.sh @@ -178,11 +178,11 @@ while [ ${RETURN_VALUE} -ne 0 ]; do fi # U+1F4A3, the hard way, because we can't use \u or \U escape sequences... # shellcheck disable=SC2039 - ./fbink -q -b -O -m -t regular=./fonts/freefont/FreeSerif.ttf,px=${bombHeight},top=${bombMargin} $'\xf0\x9f\x92\xa3' + ./fbink -q -b -O -m -t regular=./fonts/freefont/FreeSerif.ttf,px=${bombHeight},top=${bombMargin} -- $'\xf0\x9f\x92\xa3' # And then print the tail end of the log on the bottom of the screen... crashLog="$(tail -n 25 crash.log | sed -e 's/\t/ /g')" # The idea for the margins being to leave enough room for an fbink -Z bar, small horizontal margins, and a font size based on what 6pt looked like @ 265dpi - ./fbink -q -b -O -t regular=./fonts/droid/DroidSansMono.ttf,top=$((viewHeight / 2 + FONTH * 2 + FONTH / 2)),left=$((viewWidth / 60)),right=$((viewWidth / 60)),px=$((viewHeight / 64)) "${crashLog}" + ./fbink -q -b -O -t regular=./fonts/droid/DroidSansMono.ttf,top=$((viewHeight / 2 + FONTH * 2 + FONTH / 2)),left=$((viewWidth / 60)),right=$((viewWidth / 60)),px=$((viewHeight / 64)) -- "${crashLog}" # So far, we hadn't triggered an actual screen refresh, do that now, to make sure everything is bundled in a single flashing refresh. ./fbink -q -f -s # Cue a lemming's faceplant sound effect! diff --git a/platform/sony-prstux/set-wifi.sh b/platform/sony-prstux/set-wifi.sh index de0338425..dbd001682 100755 --- a/platform/sony-prstux/set-wifi.sh +++ b/platform/sony-prstux/set-wifi.sh @@ -5,9 +5,9 @@ if [ "$1" = "on" ]; then wmiconfig -i wlan0 --wlan enable wmiconfig -i wlan0 --setreassocmode 0 wmiconfig -i wlan0 --power maxperf - echo "WiFi Enabled" + echo "Wi-Fi Enabled" else wmiconfig -i wlan0 --abortscan wmiconfig -i wlan0 --wlan disable - echo "Wifi Disabled" + echo "Wi-Fi Disabled" fi diff --git a/platform/sony-prstux/suspend.sh b/platform/sony-prstux/suspend.sh index f0eef68d6..bf24f0c88 100755 --- a/platform/sony-prstux/suspend.sh +++ b/platform/sony-prstux/suspend.sh @@ -2,7 +2,7 @@ set -x -# disable WiFi +# disable Wi-Fi ./set-wifi.sh off # enter sleep, disabling all devices except CPU diff --git a/plugins/calibre.koplugin/wireless.lua b/plugins/calibre.koplugin/wireless.lua index 281b6299c..debc57f64 100644 --- a/plugins/calibre.koplugin/wireless.lua +++ b/plugins/calibre.koplugin/wireless.lua @@ -203,6 +203,10 @@ Do you want to continue? ]]), driver), end function CalibreWireless:connect() + if NetworkMgr:willRerunWhenConnected(function() self:connect() end) then + return + end + self.connect_message = false local host, port if G_reader_settings:hasNot("calibre_wireless_url") then @@ -231,8 +235,6 @@ function CalibreWireless:connect() else self:setInboxDir(host, port) end - elseif not NetworkMgr:isConnected() then - NetworkMgr:promptWifiOn() else logger.info("cannot connect to calibre server") UIManager:show(InfoMessage:new{ diff --git a/plugins/evernote.koplugin/main.lua b/plugins/evernote.koplugin/main.lua index c1a861444..f23bd0c36 100644 --- a/plugins/evernote.koplugin/main.lua +++ b/plugins/evernote.koplugin/main.lua @@ -347,10 +347,10 @@ For more information, please visit https://github.com/koreader/koreader/wiki/Eve end function EvernoteExporter:login() - if not NetworkMgr:isOnline() then - NetworkMgr:promptWifiOn() + if NetworkMgr:willRerunWhenOnline(function() self:login() end) then return end + self.login_dialog = LoginDialog:new{ title = self.login_title, username = self.evernote_username or "", @@ -410,7 +410,7 @@ function EvernoteExporter:doLogin(username, password) } self.evernote_username = username local ok, token = pcall(oauth.getToken, oauth) - -- prompt users to turn on Wifi if network is unreachable + -- prompt users to turn on Wi-Fi if network is unreachable if not ok and token then UIManager:show(InfoMessage:new{ text = _("An error occurred while logging in:") .. "\n" .. token, @@ -425,7 +425,8 @@ function EvernoteExporter:doLogin(username, password) local guid ok, guid = pcall(self.getExportNotebook, self, client) if not ok and guid and guid:find("Transport not open") then - NetworkMgr:promptWifiOn() + --- @note: No recursive callback because it feels fishy here... + NetworkMgr:beforeWifiAction() return elseif not ok and guid then UIManager:show(InfoMessage:new{ @@ -589,7 +590,7 @@ function EvernoteExporter:exportClippings(clippings) end -- check if booknotes are exported in this notebook -- so that booknotes will still be exported after switching user account - --Don't respect exported_stamp on txt export since it isn't possible to delete(update) prior clippings. + -- Don't respect exported_stamp on txt export since it isn't possible to delete(update) prior clippings. if booknotes.exported[exported_stamp] ~= true or self.txt_export or self.json_export then local ok, err if self.html_export then @@ -603,9 +604,10 @@ function EvernoteExporter:exportClippings(clippings) else ok, err = pcall(self.exportBooknotesToEvernote, self, client, title, booknotes) end - -- error reporting + -- Error reporting if not ok and err and err:find("Transport not open") then - NetworkMgr:promptWifiOn() + --- @note: No recursive callback because it feels fishy here... + NetworkMgr:beforeWifiAction() return elseif not ok and err then logger.dbg("Error while exporting book", title, err) diff --git a/plugins/goodreads.koplugin/main.lua b/plugins/goodreads.koplugin/main.lua index 225d86659..ff1b6f300 100644 --- a/plugins/goodreads.koplugin/main.lua +++ b/plugins/goodreads.koplugin/main.lua @@ -170,16 +170,16 @@ end -- search_type = author - serch book by author -- search_type = title - search book by title function Goodreads:search(search_type) + if NetworkMgr:willRerunWhenOnline(function() self:search(search_type) end) then + return + end + local title_header local hint local search_input local text_input local info local result - if not NetworkMgr:isOnline() then - NetworkMgr:promptWifiOn() - return - end if search_type == "all" then title_header = _("Search all books in Goodreads") hint = _("Title, author or ISBN") diff --git a/plugins/kosync.koplugin/main.lua b/plugins/kosync.koplugin/main.lua index a4e354e87..13d3bad8f 100644 --- a/plugins/kosync.koplugin/main.lua +++ b/plugins/kosync.koplugin/main.lua @@ -295,10 +295,10 @@ function KOSync:setWhisperBackward(strategy) end function KOSync:login() - if not NetworkMgr:isOnline() then - NetworkMgr:promptWifiOn() + if NetworkMgr:willRerunWhenOnline(function() self:login() end) then return end + self.login_dialog = LoginDialog:new{ title = self.title, username = self.kosync_username or "", diff --git a/plugins/newsdownloader.koplugin/main.lua b/plugins/newsdownloader.koplugin/main.lua index a5d3ec472..ae4ef57d7 100644 --- a/plugins/newsdownloader.koplugin/main.lua +++ b/plugins/newsdownloader.koplugin/main.lua @@ -22,7 +22,6 @@ local NewsDownloader = WidgetContainer:new{ } local initialized = false -local wifi_enabled_before_action = true local feed_config_file_name = "feed_config.lua" local news_downloader_config_file = "news_downloader_settings.lua" local news_downloader_settings @@ -59,13 +58,6 @@ local function getFeedLink(possible_link) end end ---- @todo Implement as NetworkMgr:afterWifiAction with configuration options. -function NewsDownloader:afterWifiAction() - if not wifi_enabled_before_action then - NetworkMgr:promptWifiOff() - end -end - function NewsDownloader:init() self.ui.menu:registerToMainMenu(self) end @@ -79,12 +71,7 @@ function NewsDownloader:addToMainMenu(menu_items) text = _("Download news"), keep_menu_open = true, callback = function() - if not NetworkMgr:isOnline() then - wifi_enabled_before_action = false - NetworkMgr:beforeWifiAction(self.loadConfigAndProcessFeedsWithUI) - else - self:loadConfigAndProcessFeedsWithUI() - end + NetworkMgr:runWhenOnline(function() self:loadConfigAndProcessFeedsWithUI() end) end, }, { @@ -222,7 +209,7 @@ function NewsDownloader:loadConfigAndProcessFeeds() end UI:info(T(_("Downloading news finished. Could not process some feeds. Unsupported format in: %1"), unsupported_urls)) end - NewsDownloader:afterWifiAction() + NetworkMgr:afterWifiAction() end function NewsDownloader:loadConfigAndProcessFeedsWithUI() diff --git a/plugins/send2ebook.koplugin/main.lua b/plugins/send2ebook.koplugin/main.lua index 60f97fd79..771b6e49f 100644 --- a/plugins/send2ebook.koplugin/main.lua +++ b/plugins/send2ebook.koplugin/main.lua @@ -21,7 +21,6 @@ local Send2Ebook = WidgetContainer:new{ } local initialized = false -local wifi_enabled_before_action = true local send2ebook_config_file = "send2ebook_settings.lua" local config_key_custom_dl_dir = "custom_dl_dir"; local default_download_dir_name = "send2ebook" @@ -45,13 +44,6 @@ function Send2Ebook:downloadFileAndRemove(connection_url, remote_path, local_dow end end ---- @todo Implement as NetworkMgr:afterWifiAction with configuration options. -function Send2Ebook:afterWifiAction() - if not wifi_enabled_before_action then - NetworkMgr:promptWifiOff() - end -end - function Send2Ebook:init() self.ui.menu:registerToMainMenu(self) end @@ -65,12 +57,10 @@ function Send2Ebook:addToMainMenu(menu_items) text = _("Download and remove from server"), keep_menu_open = true, callback = function() - if not NetworkMgr:isOnline() then - wifi_enabled_before_action = false - NetworkMgr:beforeWifiAction(self.process) - else - self:process() - end + local connect_callback = function() + self:process() + end + NetworkMgr:runWhenOnline(connect_callback) end, }, { @@ -171,7 +161,7 @@ function Send2Ebook:process() end end UIManager:show(info) - Send2Ebook:afterWifiAction() + NetworkMgr:afterWifiAction() end function Send2Ebook:removeReadActicles() diff --git a/plugins/timesync.koplugin/main.lua b/plugins/timesync.koplugin/main.lua index ef675e592..68325b96e 100644 --- a/plugins/timesync.koplugin/main.lua +++ b/plugins/timesync.koplugin/main.lua @@ -34,7 +34,7 @@ local function currentTime() return _("Time synchronized.") end -local function execute() +local function syncNTP() local info = InfoMessage:new{ text = _("Synchronizing time. This may take several seconds.") } @@ -58,11 +58,7 @@ local menuItem = { text = _("Synchronize time"), keep_menu_open = true, callback = function() - if NetworkMgr:isOnline() then - execute() - else - NetworkMgr:promptWifiOn() - end + NetworkMgr:runWhenOnline(function() syncNTP() end) end } diff --git a/plugins/wallabag.koplugin/main.lua b/plugins/wallabag.koplugin/main.lua index 56b9e7ea1..fe23c3244 100644 --- a/plugins/wallabag.koplugin/main.lua +++ b/plugins/wallabag.koplugin/main.lua @@ -112,15 +112,14 @@ function Wallabag:addToMainMenu(menu_items) { text = _("Delete finished articles remotely"), callback = function() - if not NetworkMgr:isOnline() then - NetworkMgr:promptWifiOn() - return + local connect_callback = function() + local num_deleted = self:processLocalFiles("manual") + UIManager:show(InfoMessage:new{ + text = T(_("Articles processed.\nDeleted: %1"), num_deleted) + }) + self:refreshCurrentDirIfNeeded() end - local num_deleted = self:processLocalFiles("manual") - UIManager:show(InfoMessage:new{ - text = T(_("Articles processed.\nDeleted: %1"), num_deleted) - }) - self:refreshCurrentDirIfNeeded() + NetworkMgr:runWhenOnline(connect_callback) end, enabled_func = function() return self.is_delete_finished or self.is_delete_read @@ -1078,12 +1077,11 @@ function Wallabag:onAddWallabagArticle(article_url) end function Wallabag:onSynchronizeWallabag() - if not NetworkMgr:isOnline() then - NetworkMgr:promptWifiOn() - return + local connect_callback = function() + self:synchronize() + self:refreshCurrentDirIfNeeded() end - self:synchronize() - self:refreshCurrentDirIfNeeded() + NetworkMgr:runWhenOnline(connect_callback) -- stop propagation return true diff --git a/plugins/zsync.koplugin/main.lua b/plugins/zsync.koplugin/main.lua index fe8cc5d9e..d077b9560 100644 --- a/plugins/zsync.koplugin/main.lua +++ b/plugins/zsync.koplugin/main.lua @@ -41,15 +41,14 @@ function ZSync:addToMainMenu(menu_items) end, callback = function() local NetworkMgr = require("ui/network/manager") - if not NetworkMgr:isOnline() then - NetworkMgr:promptWifiOn() - return - end - if not self.filemq_server then - self:publish() - else - self:unpublish() + local connect_callback = function() + if not self.filemq_server then + self:publish() + else + self:unpublish() + end end + NetworkMgr:runWhenOnline(connect_callback) end }, { @@ -63,15 +62,14 @@ function ZSync:addToMainMenu(menu_items) end, callback = function() local NetworkMgr = require("ui/network/manager") - if not NetworkMgr:isOnline() then - NetworkMgr:promptWifiOn() - return - end - if not self.filemq_client then - self:subscribe() - else - self:unsubscribe() + local connect_callback = function() + if not self.filemq_client then + self:subscribe() + else + self:unsubscribe() + end end + NetworkMgr:runWhenOnline(connect_callback) end } } diff --git a/spec/unit/networksetting_spec.lua b/spec/unit/networksetting_spec.lua index 6f1f86c1f..acd7642e9 100644 --- a/spec/unit/networksetting_spec.lua +++ b/spec/unit/networksetting_spec.lua @@ -12,7 +12,7 @@ describe("NetworkSetting module", function() assert.is.falsy(ns.connected_item) end) - it("should call connect_callback after disconnect", function() + it("should NOT call connect_callback after disconnect", function() stub(NetworkMgr, "disconnectNetwork") stub(NetworkMgr, "releaseIP") @@ -33,6 +33,33 @@ describe("NetworkSetting module", function() connect_callback = function() called = true end } ns.connected_item:disconnect() + assert.falsy(called) + + NetworkMgr.disconnectNetwork:revert() + NetworkMgr.releaseIP:revert() + end) + + it("should call disconnect_callback after disconnect", function() + stub(NetworkMgr, "disconnectNetwork") + stub(NetworkMgr, "releaseIP") + + UIManager:quit() + local called = false + local network_list = { + { + ssid = "foo", + signal_level = -58, + flags = "[WPA2-PSK-CCMP][ESS]", + signal_quality = 84, + password = "123abc", + connected = true, + }, + } + local ns = NetworkSetting:new{ + network_list = network_list, + disconnect_callback = function() called = true end + } + ns.connected_item:disconnect() assert.truthy(called) NetworkMgr.disconnectNetwork:revert()