local ConfirmBox = require ( " ui/widget/confirmbox " )
local DataStorage = require ( " datastorage " )
local Device = require ( " device " )
local InfoMessage = require ( " ui/widget/infomessage " )
local MultiConfirmBox = require ( " ui/widget/multiconfirmbox " )
local NetworkMgr = require ( " ui/network/manager " )
local UIManager = require ( " ui/uimanager " )
local Version = require ( " version " )
local lfs = require ( " libs/libkoreader-lfs " )
local logger = require ( " logger " )
local _ = require ( " gettext " )
local T = require ( " ffi/util " ) . template
local ota_dir = DataStorage : getDataDir ( ) .. " /ota/ "
local OTAManager = {
ota_servers = {
" http://ota.koreader.rocks:80/ " ,
" http://vislab.bjmu.edu.cn:80/apps/koreader/ota/ " ,
--[[
-- NOTE: Because we can't have nice things,
-- the HTTP frontend of these OpenStack storage containers doesn't actually properly support
-- HTTP/1.1 Range requests when multiple byte ranges are requested: they return bogus data when doing so,
-- which confuses zsync, causing it to retry indefinitely instead of aborting...
-- c.f., https://github.com/koreader/koreader-base/pull/699
" http://koreader-fr.ak-team.com:80/ " ,
" http://koreader-pl.ak-team.com:80/ " ,
" http://koreader-na.ak-team.com:80/ " ,
--]]
" http://koreader.ak-team.com:80/ " ,
} ,
ota_channels = {
" stable " ,
" nightly " ,
} ,
zsync_template = " koreader-%s-latest-%s.zsync " ,
installed_package = ota_dir .. " koreader.installed.tar " ,
package_indexfile = " ota/package.index " ,
updated_package = ota_dir .. " koreader.updated.tar " ,
can_pretty_print = lfs.attributes ( " ./fbink " , " mode " ) == " file " and true or false ,
}
local ota_channels = {
stable = _ ( " Stable " ) ,
nightly = _ ( " Development " ) ,
}
-- Try to detect WARIO+ Kindle boards (i.MX6 & i.MX7)
function OTAManager : _isKindleWarioOrMore ( )
local cpu_hw = nil
-- Parse cpuinfo line by line, until we find the Hardware description
for line in io.lines ( " /proc/cpuinfo " ) do
if line : find ( " ^Hardware " ) then
cpu_hw = line : match ( " ^Hardware%s*:%s([%g%s]*)$ " )
end
end
-- NOTE: I couldn't dig up a cpuinfo dump from an Oasis 2 to check the CPU part value,
-- but for Wario (Cortex A9), matching that to 0xc09 would work, too.
-- On the other hand, I'm already using the Hardware match in MRPI, so, that sealed the deal ;).
-- If we've got a Hardware string, check if it mentions an i.MX 6 or 7...
if cpu_hw then
if cpu_hw : find ( " i.MX%s?[6-7] " ) then
return true
else
return false
end
else
return false
end
end
-- "x86", "x64", "arm", "arm64", "ppc", "mips" or "mips64".
local arch = jit.arch
function OTAManager : getOTAModel ( )
if Device : isAndroid ( ) then
if arch == " x86 " then
return " android-x86 "
end
return " android "
elseif Device : isCervantes ( ) then
return " cervantes "
elseif Device : isKindle ( ) then
if Device : isTouchDevice ( ) or Device.model == " Kindle4 " then
if self : _isKindleWarioOrMore ( ) then
return " kindlepw2 "
else
return " kindle "
end
else
return " kindle-legacy "
end
elseif Device : isKobo ( ) then
return " kobo "
elseif Device : isPocketBook ( ) then
return " pocketbook "
elseif Device : isSonyPRSTUX ( ) then
return " sony-prstux "
else
return " "
end
end
function OTAManager : getOTAServer ( )
return G_reader_settings : readSetting ( " ota_server " ) or self.ota_servers [ 1 ]
end
function OTAManager : setOTAServer ( server )
logger.dbg ( " Set OTA server: " , server )
G_reader_settings : saveSetting ( " ota_server " , server )
end
function OTAManager : getOTAChannel ( )
return G_reader_settings : readSetting ( " ota_channel " ) or self.ota_channels [ 1 ]
end
function OTAManager : setOTAChannel ( channel )
logger.dbg ( " Set OTA channel: " , channel )
G_reader_settings : saveSetting ( " ota_channel " , channel )
end
function OTAManager : getZsyncFilename ( )
return self.zsync_template : format ( self : getOTAModel ( ) , self : getOTAChannel ( ) )
end
function OTAManager : checkUpdate ( )
local http = require ( " socket.http " )
local ltn12 = require ( " ltn12 " )
local zsync_file = self : getZsyncFilename ( )
local ota_zsync_file = self : getOTAServer ( ) .. zsync_file
local local_zsync_file = ota_dir .. zsync_file
-- download zsync file from OTA server
logger.dbg ( " downloading zsync file " , ota_zsync_file )
local _ , c , _ = http.request {
url = ota_zsync_file ,
sink = ltn12.sink . file ( io.open ( local_zsync_file , " w " ) ) }
if c ~= 200 then
logger.warn ( " cannot find zsync file " , c )
return
end
-- parse OTA package version
local ota_package = nil
local zsync = io.open ( local_zsync_file , " r " )
if zsync then
for line in zsync : lines ( ) do
ota_package = line : match ( " ^Filename:%s*(.-)%s*$ " )
if ota_package then break end
end
zsync : close ( )
end
local local_ok , local_version = pcall ( function ( )
local rev = Version : getCurrentRevision ( )
if rev then return Version : getNormalizedVersion ( rev ) end
end )
local ota_ok , ota_version = pcall ( function ( )
return Version : getNormalizedVersion ( ota_package )
end )
-- return ota package version if package on OTA server has version
-- larger than the local package version
if local_ok and ota_ok and ota_version and local_version and
ota_version ~= local_version then
return ota_version , local_version
elseif ota_version and ota_version == local_version then
return 0
end
end
function OTAManager : fetchAndProcessUpdate ( )
local ota_version , local_version = OTAManager : checkUpdate ( )
if ota_version == 0 then
UIManager : show ( InfoMessage : new {
text = _ ( " KOReader is up to date. " ) ,
} )
elseif ota_version == nil then
local channel = ota_channels [ OTAManager : getOTAChannel ( ) ]
UIManager : show ( InfoMessage : new {
text = T ( _ ( " OTA package is not available on %1 channel. " ) , channel ) ,
} )
elseif ota_version then
UIManager : show ( ConfirmBox : new {
text = T (
_ ( " Do you want to update? \n Installed version: %1 \n Available version: %2 " ) ,
local_version ,
ota_version
) ,
ok_text = _ ( " Update " ) ,
ok_callback = function ( )
UIManager : show ( InfoMessage : new {
text = _ ( " Downloading may take several minutes… " ) ,
timeout = 3 ,
} )
UIManager : scheduleIn ( 1 , function ( )
if OTAManager : zsync ( ) == 0 then
UIManager : show ( InfoMessage : new {
text = _ ( " KOReader will be updated on next restart. " ) ,
} )
-- Make it clear that zsync is done
if self.can_pretty_print then
os.execute ( " ./fbink -q -y -7 -pm ' ' ' ' " )
end
else
-- Make it clear that zsync is done
if self.can_pretty_print then
os.execute ( " ./fbink -q -y -7 -pm ' ' ' ' " )
end
UIManager : show ( MultiConfirmBox : new {
text = _ ( " Failed to update KOReader. \n \n You can: \n Cancel, keeping temporary files. \n Retry the update process with a full download. \n Abort and cleanup all temporary files. " ) ,
choice1_text = _ ( " Retry " ) ,
choice1_callback = function ( )
UIManager : show ( InfoMessage : new {
text = _ ( " Downloading may take several minutes… " ) ,
timeout = 3 ,
} )
-- Clear the installed package, as well as the complete/incomplete update download
os.execute ( " rm " .. self.installed_package )
os.execute ( " rm " .. self.updated_package .. " * " )
-- And then relaunch zsync in full download mode...
UIManager : scheduleIn ( 1 , function ( )
if OTAManager : zsync ( true ) == 0 then
UIManager : show ( InfoMessage : new {
text = _ ( " KOReader will be updated on next restart. " ) ,
} )
-- Make it clear that zsync is done
if self.can_pretty_print then
os.execute ( " ./fbink -q -y -7 -pm ' ' ' ' " )
end
else
-- Make it clear that zsync is done
if self.can_pretty_print then
os.execute ( " ./fbink -q -y -7 -pm ' ' ' ' " )
end
UIManager : show ( ConfirmBox : new {
text = _ ( " Error updating KOReader. Would you like to delete temporary files? " ) ,
ok_callback = function ( )
os.execute ( " rm " .. ota_dir .. " ko* " )
end ,
} )
end
end )
end ,
choice2_text = _ ( " Abort " ) ,
choice2_callback = function ( )
os.execute ( " rm " .. ota_dir .. " ko* " )
end ,
} )
end
end )
end
} )
end
end
function OTAManager : _buildLocalPackage ( )
-- TODO: validate the installed package?
local installed_package = self.installed_package
if lfs.attributes ( installed_package , " mode " ) == " file " then
return 0
end
if lfs.attributes ( self.package_indexfile , " mode " ) ~= " file " then
logger.err ( " Missing ota metadata: " , self.package_indexfile )
return nil
end
if Device : isAndroid ( ) then
return os.execute ( string.format (
" ./tar --no-recursion -cf %s -T %s " ,
self.installed_package , self.package_indexfile ) )
else
-- With visual feedback if supported...
if self.can_pretty_print then
os.execute ( " ./fbink -q -y -7 -pmh 'Preparing local OTA package' " )
-- We need a vague idea of how much space the tarball we're creating will take to compute a proper percentage...
-- Get the size from the latest zsync package, which'll be a closer match than anything else we might come up with.
local zsync_file = self : getZsyncFilename ( )
local local_zsync_file = ota_dir .. zsync_file
local tarball_size = nil
local zsync = io.open ( local_zsync_file , " r " )
if zsync then
for line in zsync : lines ( ) do
tarball_size = line : match ( " ^Length: (%d*)$ " )
if tarball_size then break end
end
zsync : close ( )
end
-- Next, we need to compute the amount of tar blocks that'll take, knowing that tar's default blocksize is 20 * 512 bytes.
-- c.f., https://superuser.com/questions/168749 & http://www.noah.org/wiki/tar_notes
-- Defaults to a sane-ish value as-of now, in case shit happens...
local blocks = 6405
if tarball_size then
blocks = tarball_size / ( 512 * 20 )
end
-- And since we want a percentage, devise the exact value we need for tar to spit out exactly 100 checkpoints ;).
local cpoints = blocks / 100
return os.execute ( string.format (
" ./tar --no-recursion -cf %s -C .. -T %s --checkpoint=%d --checkpoint-action=exec='./fbink -q -y -6 -P $(($TAR_CHECKPOINT/%d))' " ,
self.installed_package , self.package_indexfile , cpoints , cpoints ) )
else
return os.execute ( string.format (
" ./tar --no-recursion -cf %s -C .. -T %s " ,
self.installed_package , self.package_indexfile ) )
end
end
end
function OTAManager : zsync ( full_dl )
if full_dl or self : _buildLocalPackage ( ) == 0 then
local zsync_wrapper = " zsync "
-- With visual feedback if supported...
if self.can_pretty_print then
zsync_wrapper = " spinning_zsync "
end
-- If that's a full-download fallback, drop the input tarball
if full_dl then
return os.execute (
( " ./%s -o '%s' -u '%s' '%s%s' " ) : format (
zsync_wrapper ,
self.updated_package ,
self : getOTAServer ( ) ,
ota_dir ,
self : getZsyncFilename ( ) )
)
else
return os.execute (
( " ./%s -i '%s' -o '%s' -u '%s' '%s%s' " ) : format (
zsync_wrapper ,
self.installed_package ,
self.updated_package ,
self : getOTAServer ( ) ,
ota_dir ,
self : getZsyncFilename ( ) )
)
end
end
end
function OTAManager : genServerList ( )
local servers = { }
for _ , server in ipairs ( self.ota_servers ) do
local server_item = {
text = server ,
checked_func = function ( ) return self : getOTAServer ( ) == server end ,
callback = function ( ) self : setOTAServer ( server ) end
}
table.insert ( servers , server_item )
end
return servers
end
function OTAManager : genChannelList ( )
local channels = { }
for _ , channel in ipairs ( self.ota_channels ) do
local channel_item = {
text = ota_channels [ channel ] ,
checked_func = function ( ) return self : getOTAChannel ( ) == channel end ,
callback = function ( ) self : setOTAChannel ( channel ) end
}
table.insert ( channels , channel_item )
end
return channels
end
function OTAManager : getOTAMenuTable ( )
return {
text = _ ( " OTA update " ) ,
sub_item_table = {
{
text = _ ( " Check for update " ) ,
callback = function ( )
if not NetworkMgr : isOnline ( ) then
NetworkMgr : promptWifiOn ( )
else
OTAManager : fetchAndProcessUpdate ( )
end
end
} ,
{
text = _ ( " Settings " ) ,
sub_item_table = {
{
text = _ ( " OTA server " ) ,
sub_item_table = self : genServerList ( )
} ,
{
text = _ ( " OTA channel " ) ,
sub_item_table = self : genChannelList ( )
} ,
}
} ,
}
}
end
return OTAManager