2013-10-17 21:45:23 +02:00
#!/usr/bin/env lua
2013-11-04 22:34:09 +01:00
--- This script provides an interface to upgrade or downgrade the Doodle3D wifibox.
-- It can both be used as a standalone command-line tool and as a Lua library.
2013-10-17 21:45:23 +02:00
-- TODO/NOTES:
2013-10-18 16:02:22 +02:00
-- add to status: validImage: none|<version> (can use checkValidImage for this)
-- any more TODO's across this file?
-- max 1 image tegelijk (moet api doen), en rekening houden met printbuffer (printen blokkeren?)
-- MAYBE/LATER:
2013-10-18 23:27:58 +02:00
-- add API calls to retrieve a list of all versions with their info (i.e., the result of getAvailableVersions)
2013-11-04 22:34:09 +01:00
-- wget: add provision (in verbose mode?) to use '-v' instead of '-q' and disable output redirection
2013-10-18 16:02:22 +02:00
-- document index file format (Version first, then in any order: Files: sysup; factory, FileSize: sysup; factory, MD5: sysup; factory, ChangelogStart:, ..., ChangelogEnd:)
-- copy improved fileSize back to utils (add unit tests!)
-- create new utils usable by updater as well as api? (remove dependencies on uci and logger etc)
2013-10-18 23:27:58 +02:00
-- note: take care not to print any text in module functions, as this breaks http responses
2013-10-21 13:42:58 +02:00
-- change representation of sysupgrade/factory info in versionInfo? (and also in image index?) <- create api call to get all info on all versions?
2013-10-17 21:45:23 +02:00
local M = { }
2013-11-04 22:34:09 +01:00
--- Possible states the updater can be in, they are stored in @{STATE_FILE}.
-- @table STATE
M.STATE = {
NONE = 1 , -- @{STATE_FILE} does not exist
DOWNLOADING = 2 , -- downloading is started but not finished yet
DOWNLOAD_FAILED = 3 , -- download failed (often occurs when the wifibox is not connected to internet)
IMAGE_READY = 4 , -- download succeeded and the image is valid
INSTALLING = 5 , -- image is being installed (this state will probably never be returned since the box is flashing/rebooting)
INSTALLED = 6 , -- image has been installed successfully (this state will never be returned since the box will reboot)
INSTALL_FAILED = 7 -- installation failed
}
-- Names for the states in @{STATE}, these are returned through the REST API.
2013-10-18 16:02:22 +02:00
M.STATE_NAMES = {
2013-10-18 21:46:41 +02:00
[ M.STATE . NONE ] = ' none ' , [ M.STATE . DOWNLOADING ] = ' downloading ' , [ M.STATE . DOWNLOAD_FAILED ] = ' download_failed ' , [ M.STATE . IMAGE_READY ] = ' image_ready ' ,
2013-10-18 16:02:22 +02:00
[ M.STATE . INSTALLING ] = ' installing ' , [ M.STATE . INSTALLED ] = ' installed ' , [ M.STATE . INSTALL_FAILED ] = ' install_failed '
}
2013-11-04 22:34:09 +01:00
--- The base URL to use for finding update files.
-- This URL will usually contain both an OpenWRT feed directory and an `images`-directory.
-- This script uses only the latter, and expects to find the file @{IMAGE_INDEX_FILE} there.
2013-10-17 21:45:23 +02:00
M.DEFAULT_BASE_URL = ' http://doodle3d.com/updates '
2013-10-18 16:02:22 +02:00
--M.DEFAULT_BASE_URL = 'http://localhost/~USERNAME/wifibox/updates'
2013-11-04 22:34:09 +01:00
--- The index file containing metadata on update images.
2013-10-17 21:45:23 +02:00
M.IMAGE_INDEX_FILE = ' wifibox-image.index '
2013-11-04 22:34:09 +01:00
--- Path to the updater cache.
2013-10-17 21:45:23 +02:00
M.CACHE_PATH = ' /tmp/d3d-updater '
2013-11-04 22:34:09 +01:00
--- Name of the file to store current state in, this file resides in @{CACHE_PATH}.
2013-10-18 16:02:22 +02:00
M.STATE_FILE = ' update-state '
2013-11-04 22:34:09 +01:00
2013-10-17 21:45:23 +02:00
M.WGET_OPTIONS = " -q -t 1 -T 30 "
--M.WGET_OPTIONS = "-v -t 1 -T 30"
2013-10-21 12:36:54 +02:00
local verbosity = 0 -- set by parseCommandlineArguments()
2013-10-18 16:02:22 +02:00
local log = nil -- wifibox API can use M.setLogger to enable this module to use its logger
2013-10-21 12:36:54 +02:00
local useCache = true -- default, can be overwritten using M.setUseCache()
local baseUrl = M.DEFAULT_BASE_URL -- default, can be overwritten by M.setBaseUrl()
2013-10-17 21:45:23 +02:00
---------------------
-- LOCAL FUNCTIONS --
---------------------
2013-11-04 22:34:09 +01:00
--- Log a message with the given level, if logging is enabled for that level.
-- Messages will be written to [stdout](http://www.cplusplus.com/reference/cstdio/stdout/), or logged using the logger set with @{setLogger}.
-- @number lvl Level to log to, use 1 for important messages, 0 for regular messages and -1 for less important messages.
-- @string msg The message to log.
2013-10-18 21:46:41 +02:00
local function P ( lvl , msg )
if log then
if lvl == - 1 then log : debug ( msg )
elseif lvl == 0 or lvl == 1 then log : info ( msg )
end
else
if ( - lvl <= verbosity ) then print ( msg ) end
end
end
2013-11-04 22:34:09 +01:00
--- Log a debug message, this function wraps @{P}.
-- The message will be logged with level -1 and be prefixed with '(DBG)'.
-- @string msg The message to log.
2013-10-18 21:46:41 +02:00
local function D ( msg ) P ( - 1 , ( log and msg or " (DBG) " .. msg ) ) end
2013-11-04 22:34:09 +01:00
--- Log an error.
-- Messages will be written to [stderr](http://www.cplusplus.com/reference/cstdio/stderr/), or logged using the logger set with @{setLogger}.
-- @string msg The message to log.
2013-10-18 21:46:41 +02:00
local function E ( msg )
if log then log : error ( msg )
else io.stderr : write ( msg .. ' \n ' )
end
end
2013-11-04 22:34:09 +01:00
--- Splits the return status from `os.execute`, which consists of two bytes.
--
-- `os.execute` internally calls [system](http://linux.die.net/man/3/system),
-- which usually returns the command exit status as high byte (see [WEXITSTATUS](http://linux.die.net/man/2/wait)).
-- Furthermore, see [shifting bits in Lua](http://stackoverflow.com/questions/16158436/how-to-shift-and-mask-bits-from-integer-in-lua).
-- @number exitStatus The combined exit status.
-- @treturn number The command exit status.
-- @treturn number The `os.execute`/[system](http://linux.die.net/man/3/system) return status.
2013-10-21 12:36:54 +02:00
local function splitExitStatus ( exitStatus )
2013-10-21 13:42:58 +02:00
if exitStatus == - 1 then return - 1 , - 1 end
2013-10-21 12:36:54 +02:00
local cmdStatus = math.floor ( exitStatus / 256 )
local systemStatus = exitStatus - cmdStatus * 256
return cmdStatus , systemStatus
end
2013-11-04 22:34:09 +01:00
--- Returns a human-readable message for a [wget exit status](http://www.gnu.org/software/wget/manual/wget.html#Exit-Status).
-- @number exitStatus An exit status from wget.
-- @treturn string|number Either the status followed by a description, or a message indicating the call was interrupted, or just the status if it was not recognized.
2013-10-21 12:36:54 +02:00
local function wgetStatusToString ( exitStatus )
local wgetStatus , systemStatus = splitExitStatus ( exitStatus )
if systemStatus ~= 0 then
return " interrupted: " .. systemStatus
end
2013-10-18 23:27:58 +02:00
-- adapted from man(1) wget on OSX
local statusTexts = {
[ ' 0 ' ] = ' Ok ' ,
[ ' 1 ' ] = ' Generic error ' ,
[ ' 2 ' ] = ' Parse error ' , -- for instance, when parsing command-line options, the .wgetrc or .netrc...
[ ' 3 ' ] = ' File I/O error ' ,
[ ' 4 ' ] = ' Network failure ' ,
[ ' 5 ' ] = ' SSL verification failure ' ,
[ ' 6 ' ] = ' Username/password authentication failure ' ,
[ ' 7 ' ] = ' Protocol error ' ,
[ ' 8 ' ] = ' Server issued an error response '
}
2013-10-21 12:36:54 +02:00
local result = statusTexts [ tostring ( wgetStatus ) ]
2013-10-18 23:27:58 +02:00
if result then return exitStatus .. " : " .. result
else return exitStatus
end
end
2013-10-18 21:46:41 +02:00
2013-11-04 22:34:09 +01:00
--- Creates the updater cache directory.
-- @return bool|nil True, or nil on error.
-- @return ?string A message in case of error.
2013-10-18 21:46:41 +02:00
local function createCacheDirectory ( )
if os.execute ( ' mkdir -p ' .. M.CACHE_PATH ) ~= 0 then
return nil , " Error: could not create cache directory ' " .. M.CACHE_PATH .. " ' "
end
return true
end
2013-10-18 16:02:22 +02:00
2013-11-04 22:34:09 +01:00
--- Retrieves the current updater state code and message from @{STATE_FILE}.
-- @treturn STATE The current state code (@{STATE}.NONE if no state has been set).
-- @treturn string The current state message (empty string if no state has been set).
2013-10-18 16:02:22 +02:00
local function getState ( )
local file , msg = io.open ( M.CACHE_PATH .. ' / ' .. M.STATE_FILE , ' r ' )
if not file then return M.STATE . NONE , " " end
local state = file : read ( ' *a ' )
file : close ( )
local code , msg = string.match ( state , ' ([^|]+)|+(.*) ' )
return code , msg
end
2013-11-04 22:34:09 +01:00
--- Trims whitespace from both ends of a string.
-- See [this Lua snippet](http://snippets.luacode.org/?p=snippets/trim_whitespace_from_string_76).
-- @string s The text to trim.
-- @treturn string s, with whitespace trimmed.
2013-10-17 21:45:23 +02:00
local function trim ( s )
if type ( s ) ~= ' string ' then return s end
return ( s : find ( ' ^%s*$ ' ) and ' ' or s : match ( ' ^%s*(.*%S) ' ) )
end
2013-11-04 22:34:09 +01:00
--- Read the contents of a file.
--
-- TODO: this file has been copied from @{util.utils}.lua and should be merged again.
-- @string filePath The file to read.
-- @bool trimResult Whether or not to trim the read data.
-- @treturn bool|nil True, or nil on error.
-- @treturn ?string A descriptive message on error.
-- @treturn ?number TODO: find out why this value is returned.
2013-10-17 21:45:23 +02:00
local function readFile ( filePath , trimResult )
local f , msg , nr = io.open ( filePath , ' r ' )
if not f then return nil , msg , nr end
local res = f : read ( ' *all ' )
f : close ( )
if trimResult then
res = trim ( res )
end
return res
end
2013-11-04 22:34:09 +01:00
--- Reports whether or not a file exists.
--
-- TODO: this file has been copied from @{util.utils}.lua and should be merged again.
-- @string file The file to report about.
-- @treturn bool True if the file exists, false otherwise.
2013-10-17 21:45:23 +02:00
local function exists ( file )
if not file or type ( file ) ~= ' string ' or file : len ( ) == 0 then
return nil , " file must be a non-empty string "
end
local r = io.open ( file , ' r ' ) -- ignore returned message
if r then r : close ( ) end
return r ~= nil
end
2013-11-04 22:34:09 +01:00
--- Reports the size of a file or file handle.
--
-- TODO: this file has been copied from @{util.utils}.lua and should be merged again.
-- @param file A file path or open file handle.
-- @treturn number Size of the file.
2013-10-17 21:45:23 +02:00
local function fileSize ( file )
2013-10-18 16:02:22 +02:00
local size = nil
if type ( file ) == ' file ' then
local current = file : seek ( )
size = file : seek ( ' end ' )
file : seek ( ' set ' , current )
elseif type ( file ) == ' string ' then
local f = io.open ( file )
if f then
size = f : seek ( ' end ' )
f : close ( )
end
end
2013-10-17 21:45:23 +02:00
return size
end
2013-11-04 22:34:09 +01:00
--- Runs an arbitrary shell command.
-- @string command The command to run.
-- @bool dryRun Only log a message if true, otherwise run the command and log a message.
-- @treturn number Exit status of of command or -1 if dryRun is true.
2013-10-21 12:36:54 +02:00
local function runCommand ( command , dryRun )
D ( " about to run: ' " .. command .. " ' " )
2013-10-21 13:42:58 +02:00
return ( not dryRun ) and os.execute ( command ) or - 1
2013-10-21 12:36:54 +02:00
end
2013-11-04 22:34:09 +01:00
--- Removes a file.
-- @string filePath The file to remove.
2013-10-21 12:36:54 +02:00
local function removeFile ( filePath )
return runCommand ( ' rm ' .. filePath )
end
2013-10-17 21:45:23 +02:00
2013-11-04 22:34:09 +01:00
--- Downloads a file and stores it locally.
-- @string url The full URL to download.
-- @string saveDir The path at which to save the downloaded file.
-- @string[opt] filename File name to save as, note that leaving this out has issues with files not being overwritten but suffixed with '.1', '.2',etc instead.
-- @treturn number|nil Exit status of wget command or nil on error.
-- @treturn ?string Descriptive message if saveDir is nil or empty.
2013-10-17 21:45:23 +02:00
local function downloadFile ( url , saveDir , filename )
if not saveDir or saveDir : len ( ) == 0 then return nil , " saveDir must be non-empty " end
local outArg = ( filename : len ( ) > 0 ) and ( ' -O ' .. filename ) or ' '
if filename : len ( ) > 0 then
2013-10-18 16:02:22 +02:00
return runCommand ( ' wget ' .. M.WGET_OPTIONS .. ' -O ' .. saveDir .. ' / ' .. filename .. ' ' .. url .. ' 2> /dev/null ' )
2013-10-17 21:45:23 +02:00
else
2013-10-18 16:02:22 +02:00
return runCommand ( ' wget ' .. M.WGET_OPTIONS .. ' -P ' .. saveDir .. ' ' .. url .. ' 2> /dev/null ' )
end
2013-10-17 21:45:23 +02:00
end
2013-11-04 22:34:09 +01:00
--- Parses command-line arguments and returns a table containing information distilled from them.
-- @tab arglist A table in the same form as the [arg table](http://www.lua.org/pil/1.4.html) created by Lua.
-- @treturn tabla|nil A table containing information on what to do, or nil if invalid arguments were specified.
-- @treturn ?string Descriptive message on error.
2013-10-17 21:45:23 +02:00
local function parseCommandlineArguments ( arglist )
local result = { verbosity = 0 , baseUrl = M.DEFAULT_BASE_URL , action = nil }
local nextIsVersion , nextIsUrl = false , false
for index , argument in ipairs ( arglist ) do
if nextIsVersion then
result.version = argument ; nextIsVersion = false
elseif nextIsUrl then
result.baseUrl = argument ; nextIsUrl = false
else
if argument == ' -h ' then result.action = ' showHelp '
elseif argument == ' -q ' then result.verbosity = - 1
elseif argument == ' -V ' then result.verbosity = 1
elseif argument == ' -c ' then result.useCache = true
elseif argument == ' -C ' then result.useCache = false
elseif argument == ' -u ' then nextIsUrl = true
elseif argument == ' -v ' then result.action = ' showCurrentVersion '
2013-10-18 16:02:22 +02:00
elseif argument == ' -s ' then result.action = ' showStatus '
2013-10-17 21:45:23 +02:00
elseif argument == ' -l ' then result.action = ' showAvailableVersions '
elseif argument == ' -i ' then result.action = ' showVersionInfo ' ; nextIsVersion = true
elseif argument == ' -d ' then result.action = ' imageDownload ' ; nextIsVersion = true
elseif argument == ' -f ' then result.action = ' imageInstall ' ; nextIsVersion = true
2013-10-18 16:02:22 +02:00
elseif argument == ' -r ' then result.action = ' clear '
else return nil , " unrecognized argument ' " .. argument .. " ' "
2013-10-17 21:45:23 +02:00
end
end
end
2013-10-18 16:02:22 +02:00
if result.version then
2013-10-17 21:45:23 +02:00
result.version = M.parseVersion ( result.version )
if not result.version then
return nil , " error parsing specified version "
end
end
2013-10-18 16:02:22 +02:00
if nextIsVersion then return nil , " missing required version argument " end
if nextIsUrl then return nil , " missing required URL argument " end
2013-10-17 21:45:23 +02:00
return result
end
----------------------
-- MODULE FUNCTIONS --
----------------------
2013-11-04 22:34:09 +01:00
--- Enables use of the given @{util.logger} object, otherwise `stdout`/`stderr` will be used.
-- @tparam util.logger logger The logger to log future messages to.
2013-10-18 16:02:22 +02:00
function M . setLogger ( logger )
log = logger
end
2013-11-04 22:34:09 +01:00
--- Controls whether or not to use pre-existing files over (re-)downloading them.
--
-- Note that the mechanism is currently naive, (e.g., there are no mechanisms like maximum cache age).
-- @bool use If true, try not to download anything unless necessary.
2013-10-21 12:36:54 +02:00
function M . setUseCache ( use )
useCache = use
end
2013-11-04 22:34:09 +01:00
--- Sets the base URL to use for finding update images, defaults to @{DEFAULT_BASE_URL}.
-- @string url The new base URL to use.
2013-10-21 12:36:54 +02:00
function M . setBaseUrl ( url )
baseUrl = url
end
2013-11-04 22:34:09 +01:00
--- Returns a table with information about current update status of the wifibox.
--
-- The result table will contain at least the current version, current state code and text.
-- If the box has internet access, it will also include the newest version available.
-- If an image is currently being downloaded, progress information will also be included.
--
-- @treturn bool True if status has been determined fully, false if not.
-- @treturn table The result table.
-- @treturn ?string Descriptive message in case the result table is not complete.
2013-10-21 12:36:54 +02:00
function M . getStatus ( )
2013-10-18 21:46:41 +02:00
if not baseUrl then baseUrl = M.DEFAULT_BASE_URL end
2013-10-21 13:42:58 +02:00
local unknownVersion = { major = 0 , minor = 0 , patch = 0 }
2013-10-18 16:02:22 +02:00
local result = { }
2013-10-22 03:30:23 +02:00
result.currentVersion = M.getCurrentVersion ( )
result.stateCode , result.stateText = getState ( )
result.stateCode = tonumber ( result.stateCode )
2013-11-04 22:34:09 +01:00
2013-10-21 12:36:54 +02:00
local verTable , msg = M.getAvailableVersions ( )
2013-10-21 13:42:58 +02:00
if not verTable then
D ( " could not obtain available versions ( " .. msg .. " ) " )
-- TODO: set an error state in result to signify we probably do not have internet access?
2013-10-22 03:30:23 +02:00
return false , result , msg
2013-10-21 13:42:58 +02:00
end
2013-10-18 21:46:41 +02:00
2013-10-21 13:42:58 +02:00
local newest = verTable and verTable [ # verTable ]
result.newestVersion = newest and newest.version or unknownVersion
2013-11-04 22:34:09 +01:00
2013-10-18 16:02:22 +02:00
if result.stateCode == M.STATE . DOWNLOADING then
result.progress = fileSize ( M.CACHE_PATH .. ' / ' .. newest.sysupgradeFilename )
2013-10-18 21:46:41 +02:00
if not result.progress then result.progress = 0 end -- in case the file does not exist yet (which yields nil)
2013-10-18 16:02:22 +02:00
result.imageSize = newest.sysupgradeFileSize
end
2013-10-22 03:30:23 +02:00
return true , result
2013-10-18 16:02:22 +02:00
end
2013-10-18 21:46:41 +02:00
-- Turns a plain-text version into a table.
-- tables as argument are ignored so you can safely pass in an already parsed
-- version and expect it back unmodified.
2013-10-17 21:45:23 +02:00
function M . parseVersion ( versionText )
2013-10-18 21:46:41 +02:00
if type ( versionText ) == ' table ' then return versionText end
2013-10-17 21:45:23 +02:00
if not versionText or versionText : len ( ) == 0 then return nil end
2013-10-18 21:46:41 +02:00
2013-10-17 21:45:23 +02:00
local major , minor , patch = versionText : match ( " ^%s*(%d+)%.(%d+)%.(%d+)%s*$ " )
if not major or not minor or not patch then return nil end
2013-10-18 21:46:41 +02:00
2013-10-17 21:45:23 +02:00
return { [ ' major ' ] = major , [ ' minor ' ] = minor , [ ' patch ' ] = patch }
end
2013-10-18 21:46:41 +02:00
-- Formats a version as returned by parseVersion().
-- Strings are returned unmodified, so an 'already formatted' version can be
-- passed in safely and expected back unmodified.
function M . formatVersion ( version )
if type ( version ) == ' string ' then return version end
return version.major .. " . " .. version.minor .. " . " .. version.patch
end
2013-10-17 21:45:23 +02:00
-- expects two tables as created by M.parseVersion()
function M . compareVersions ( versionA , versionB )
if type ( versionA ) ~= ' table ' or type ( versionB ) ~= ' table ' then return nil end
local diff = versionA.major - versionB.major
if diff == 0 then diff = versionA.minor - versionB.minor end
if diff == 0 then diff = versionA.patch - versionB.patch end
return diff > 0 and 1 or ( diff < 0 and - 1 or 0 )
end
2013-10-21 12:36:54 +02:00
-- verTable is optional, getAvailableVersions will be used to obtain it if nil
function M . findVersion ( version , verTable )
local msg = nil
2013-10-21 13:42:58 +02:00
version = M.parseVersion ( version )
2013-10-21 12:36:54 +02:00
if not verTable then verTable , msg = M.getAvailableVersions ( ) end
if not verTable then return nil , msg end
2013-10-17 21:45:23 +02:00
for _ , ent in pairs ( verTable ) do
if M.compareVersions ( ent.version , version ) == 0 then return ent end
end
2013-10-21 12:36:54 +02:00
return nil , " no such version "
2013-10-17 21:45:23 +02:00
end
-- version may be a table or a string, devtype and isFactory are optional
2013-10-18 21:46:41 +02:00
function M . constructImageFilename ( version , devType , isFactory )
2013-10-17 21:45:23 +02:00
local sf = isFactory and ' factory ' or ' sysupgrade '
2013-10-18 21:46:41 +02:00
local v = M.formatVersion ( version )
2013-10-17 21:45:23 +02:00
local dt = devType and devType or ' tl-mr3020 '
return ' doodle3d-wifibox- ' .. M.formatVersion ( v ) .. ' - ' .. dt .. ' - ' .. sf .. ' .bin '
end
2013-10-21 12:36:54 +02:00
--TODO: move up to locals
local function md5sum ( filepath )
-- TODO [osx: md5 -q <file>], [linux: ?]
end
function M . checkValidImage ( versionEntry , devType , isFactory )
local filename = M.constructImageFilename ( versionEntry.version , devType , isFactory )
--return versionEntry.md5 == md5sum(M.CACHE_PATH .. '/' .. filename)
local size = fileSize ( M.CACHE_PATH .. ' / ' .. filename )
versionEntry.isValid = versionEntry.sysupgradeFileSize == size
return versionEntry.isValid
end
2013-10-17 21:45:23 +02:00
-- returns a plain text version
function M . getCurrentVersionText ( )
local res , msg , nr = readFile ( ' /etc/wifibox-version ' , true )
if res then return res else return nil , msg , nr end
end
-- returns a table with major, minor and patch as keys
function M . getCurrentVersion ( )
2013-10-18 16:02:22 +02:00
local vt , msg = M.getCurrentVersionText ( )
2013-10-17 21:45:23 +02:00
return vt and M.parseVersion ( vt ) or nil , msg
end
2013-10-21 12:36:54 +02:00
-- returns an indexed (and sorted) table containing version tables
function M . getAvailableVersions ( )
2013-10-18 21:46:41 +02:00
if not baseUrl then baseUrl = M.DEFAULT_BASE_URL end
2013-10-17 21:45:23 +02:00
local indexFilename = M.CACHE_PATH .. ' / ' .. M.IMAGE_INDEX_FILE
2013-10-18 21:46:41 +02:00
local ccRv , ccMsg = createCacheDirectory ( )
if not ccRv then return nil , ccMsg end
2013-10-17 21:45:23 +02:00
if not useCache or not exists ( indexFilename ) then
local rv = downloadFile ( baseUrl .. ' /images/ ' .. M.IMAGE_INDEX_FILE , M.CACHE_PATH , M.IMAGE_INDEX_FILE )
2013-10-18 23:27:58 +02:00
if rv ~= 0 then return nil , " could not download image index file ( " .. wgetStatusToString ( rv ) .. " ) " end
2013-10-17 21:45:23 +02:00
end
local status , idxLines = pcall ( io.lines , indexFilename )
if not status then return nil , " could not open image index file ' " .. indexFilename .. " ' " end --do not include io.lines error message
local result , entry = { } , nil
local lineno , changelogMode = 1 , false
for line in idxLines do
local k , v = line : match ( ' ^(.-):(.*)$ ' )
k , v = trim ( k ) , trim ( v )
2013-10-18 23:27:58 +02:00
if not log then D ( " # " .. lineno .. " : considering ' " .. line .. " ' ( " .. ( k or ' <nil> ' ) .. " / " .. ( v or ' <nil> ' ) .. " ) " ) end
2013-10-17 21:45:23 +02:00
if not changelogMode and ( not k or not v ) then return nil , " incorrectly formatted line in index file (line " .. lineno .. " ) " end
if k == ' ChangelogEnd ' then
changelogMode = false
elseif changelogMode then
entry.changelog = entry.changelog .. line .. ' \n '
else
if k == ' Version ' then
if entry ~= nil then table.insert ( result , entry ) end
local pv = M.parseVersion ( v )
if not pv then return nil , " incorrect version text in index file (line " .. lineno .. " ) " end
entry = { version = pv }
elseif k == ' ChangelogStart ' then
changelogMode = true
entry.changelog = " "
elseif k == ' Files ' then
local sName , fName = v : match ( ' ^(.-);(.*)$ ' )
sName , fName = trim ( sName ) , trim ( fName )
if sName then entry.sysupgradeFilename = sName end
if fName then entry.factoryFilename = fName end
elseif k == ' FileSize ' then
local sSize , fSize = v : match ( ' ^(.-);(.*)$ ' )
sSize , fSize = trim ( sSize ) , trim ( fSize )
2013-10-18 16:02:22 +02:00
if sSize then entry.sysupgradeFileSize = tonumber ( sSize ) end
if fSize then entry.factoryFileSize = tonumber ( fSize ) end
2013-10-17 21:45:23 +02:00
elseif k == ' MD5 ' then
local sSum , fSum = v : match ( ' ^(.-);(.*)$ ' )
sSum , fSum = trim ( sSum ) , trim ( fSum )
if sSum then entry.sysupgradeMD5 = sSum end
if fSum then entry.factoryMD5 = fSum end
else
P ( - 1 , " ignoring unrecognized field in index file ' " .. k .. " ' (line " .. lineno .. " ) " )
end
end
lineno = lineno + 1
end
if entry ~= nil then table.insert ( result , entry ) end
table.sort ( result , function ( a , b )
return M.compareVersions ( a.version , b.version ) < 0
end )
return result
end
2013-10-21 12:36:54 +02:00
-- devtype and isFactory are optional
2013-10-18 21:46:41 +02:00
-- returns true or nil+msg or nil + return value from wget
2013-10-21 12:36:54 +02:00
function M . downloadImageFile ( versionEntry , devType , isFactory )
2013-10-18 21:46:41 +02:00
if not baseUrl then baseUrl = M.DEFAULT_BASE_URL end
2013-10-21 12:36:54 +02:00
local filename = M.constructImageFilename ( versionEntry.version , devType , isFactory )
local doDownload = not useCache
2013-10-18 16:02:22 +02:00
2013-10-18 21:46:41 +02:00
local ccRv , ccMsg = createCacheDirectory ( )
if not ccRv then return nil , ccMsg end
2013-10-21 12:36:54 +02:00
if versionEntry.isValid == false then
doDownload = true
elseif versionEntry.isValid == nil then
M.checkValidImage ( versionEntry , devType , isFactory )
if versionEntry.isValid == false then doDownload = true end
end
2013-10-18 16:02:22 +02:00
local rv = 0
if doDownload then
2013-10-22 03:30:23 +02:00
M.setState ( M.STATE . DOWNLOADING , " Downloading image ( " .. filename .. " ) " )
2013-10-18 21:46:41 +02:00
rv = downloadFile ( baseUrl .. ' /images/ ' .. filename , M.CACHE_PATH , filename )
2013-10-18 16:02:22 +02:00
end
2013-10-18 21:46:41 +02:00
if rv == 0 then
2013-10-21 12:36:54 +02:00
if M.checkValidImage ( versionEntry , devType , isFactory ) then
2013-10-22 03:30:23 +02:00
M.setState ( M.STATE . IMAGE_READY , " Image downloaded, ready to install (image name: " .. filename .. " ) " )
2013-10-21 12:36:54 +02:00
return true
else
removeFile ( M.CACHE_PATH .. ' / ' .. filename )
local ws = " Image download failed (invalid image) "
2013-10-22 03:30:23 +02:00
M.setState ( M.STATE . DOWNLOAD_FAILED , ws )
2013-10-21 12:36:54 +02:00
return nil , ws
end
2013-10-18 21:46:41 +02:00
else
2013-10-18 23:27:58 +02:00
local ws = wgetStatusToString ( rv )
2013-10-21 12:36:54 +02:00
removeFile ( M.CACHE_PATH .. ' / ' .. filename )
2013-10-22 03:30:23 +02:00
M.setState ( M.STATE . DOWNLOAD_FAILED , " Image download failed (wget error: " .. ws .. " ) " )
2013-10-18 23:27:58 +02:00
return nil , ws
2013-10-18 21:46:41 +02:00
end
2013-10-17 21:45:23 +02:00
end
2013-10-21 12:36:54 +02:00
-- this function will not return if everything goes to plan
2013-10-18 21:46:41 +02:00
-- noRetain, devType and isFactory are optional
-- returns true or nil + wget return value
2013-10-21 12:36:54 +02:00
function M . flashImageVersion ( versionEntry , noRetain , devType , isFactory )
2013-10-22 03:30:23 +02:00
log : info ( " flashImageVersion " )
2013-10-21 12:36:54 +02:00
local imgName = M.constructImageFilename ( versionEntry.version , devType , isFactory )
2013-10-17 21:45:23 +02:00
local cmd = noRetain and ' sysupgrade -n ' or ' sysupgrade '
cmd = cmd .. M.CACHE_PATH .. ' / ' .. imgName
2013-10-18 21:46:41 +02:00
local ccRv , ccMsg = createCacheDirectory ( )
if not ccRv then return nil , ccMsg end
2013-10-21 12:36:54 +02:00
if not M.checkValidImage ( versionEntry ) then
return nil , " no valid image for requested version present "
end
2013-10-22 03:30:23 +02:00
M.setState ( M.STATE . INSTALLING , " Installing new image ( " .. imgName .. " ) " ) -- yes this is rather pointless
2013-10-21 12:36:54 +02:00
local rv = runCommand ( cmd ) -- if everything goes to plan, this will not return
2013-10-18 16:02:22 +02:00
2013-10-21 12:36:54 +02:00
if rv == 0 then
2013-10-22 03:30:23 +02:00
M.setState ( M.STATE . INSTALLED , " Image installed " )
2013-10-21 12:36:54 +02:00
else
-- NOTE: if cmdrv == 127, this means the command was not found
local cmdrv , sysrv = splitExitStatus ( rv )
2013-10-22 03:30:23 +02:00
M.setState ( M.STATE . INSTALL_FAILED , " Image installation failed (sysupgrade returned " .. cmdrv .. " , execution status: " .. sysrv .. " ) " )
2013-10-18 16:02:22 +02:00
end
2013-10-18 21:46:41 +02:00
return ( rv == 0 ) and true or nil , rv
2013-10-17 21:45:23 +02:00
end
2013-10-18 21:46:41 +02:00
--returns true on success, or nil+msg otherwise
2013-10-18 16:02:22 +02:00
function M . clear ( )
2013-10-18 21:46:41 +02:00
local ccRv , ccMsg = createCacheDirectory ( )
if not ccRv then return nil , ccMsg end
2013-10-18 23:27:58 +02:00
D ( " Removing " .. M.CACHE_PATH .. " /doodle3d-wifibox-*.bin " )
2013-10-22 03:30:23 +02:00
M.setState ( M.STATE . NONE , " " )
2013-10-18 21:46:41 +02:00
local rv = os.execute ( ' rm -f ' .. M.CACHE_PATH .. ' /doodle3d-wifibox-*.bin ' )
return ( rv == 0 ) and true or nil , " could not remove image files "
2013-10-18 16:02:22 +02:00
end
2013-10-22 03:30:23 +02:00
-- NOTE: make sure the cache directory exists before calling this function or it will fail.
-- NOTE: this function _can_ fail but we don't expect this to happen so the return value is ignored for now
function M . setState ( code , msg )
local s = code .. ' | ' .. msg
D ( " set update state: " .. M.STATE_NAMES [ code ] .. " (' " .. s .. " ') " )
local file , msg = io.open ( M.CACHE_PATH .. ' / ' .. M.STATE_FILE , ' w ' )
2013-10-18 16:02:22 +02:00
2013-10-22 03:30:23 +02:00
if not file then
E ( " error: could not open state file for writing ( " .. msg .. " ) " )
return false
end
file : write ( s )
file : close ( )
return true
end
2013-10-18 16:02:22 +02:00
2013-10-17 21:45:23 +02:00
----------
-- MAIN --
----------
2013-11-04 22:34:09 +01:00
--- The main function which will be called in standalone mode.
-- At the end of the file, this function will be invoked only if `arg` is defined,
-- so this file can also be used as a library.
-- Command-line arguments are expected to be present in the global `arg` variable.
2013-10-17 21:45:23 +02:00
local function main ( )
local argTable , msg = parseCommandlineArguments ( arg )
if not argTable then
E ( " error interpreting command-line arguments, try '-h' for help ( " .. msg .. " ) " )
os.exit ( 1 )
end
2013-10-18 21:46:41 +02:00
verbosity = argTable.verbosity
2013-10-17 21:45:23 +02:00
if argTable.useCache ~= nil then useCache = argTable.useCache end
P ( 0 , " Doodle3D Wifibox firmware updater " )
2013-10-18 21:46:41 +02:00
local cacheCreated , msg = createCacheDirectory ( )
if not cacheCreated then
E ( msg )
2013-10-17 21:45:23 +02:00
os.exit ( 1 )
end
if argTable.action == ' showHelp ' then
2013-10-18 16:02:22 +02:00
P ( 1 , " \t -h \t \t Show this help message " )
P ( 1 , " \t -q \t \t quiet mode " )
P ( 1 , " \t -V \t \t verbose mode " )
P ( 1 , " \t -c \t \t Use cache as much as possible " )
P ( 1 , " \t -C \t \t Do not use the cache " )
P ( 1 , " \t -u <base_url> \t Use specified base URL (default: " .. M.DEFAULT_BASE_URL .. " ) " )
P ( 1 , " \t -v \t \t Show current image version " )
P ( 1 , " \t -s \t \t Show current update status " )
P ( 1 , " \t -l \t \t Show list of available image versions (and which one has been downloaded, if any) " )
P ( 1 , " \t -i <version> \t Show information (changelog) about the requested image version " )
P ( 1 , " \t -d <version> \t Download requested image version " )
P ( 1 , " \t -f <version> \t Flash to requested image version (by means of sysupgrade) " )
P ( 1 , " \t -r \t \t Clear downloaded images and reset state " )
2013-10-17 21:45:23 +02:00
os.exit ( 10 )
elseif argTable.action == ' showCurrentVersion ' then
local vText , msg , nr = M.getCurrentVersionText ( )
if not vText then E ( " error reading firmware version ( " .. nr .. " : " .. msg .. " ) " ) ; os.exit ( 1 ) end
local v = M.parseVersion ( vText )
if not v then E ( " error parsing version ' " .. vText .. " ' " ) ; os.exit ( 2 ) end
P ( 1 , " version: " .. M.formatVersion ( v ) )
2013-10-18 16:02:22 +02:00
elseif argTable.action == ' showStatus ' then
2013-10-21 12:36:54 +02:00
local status = M.getStatus ( )
2013-10-18 16:02:22 +02:00
P ( 0 , " Current update status: " )
P ( 1 , " currentVersion: \t " .. ( M.formatVersion ( status.currentVersion ) or ' ? ' ) )
P ( 1 , " newestVersion: \t " .. ( M.formatVersion ( status.newestVersion ) or ' ? ' ) )
if status.stateText and status.stateText : len ( ) > 0 then
P ( 1 , " state: \t \t " .. M.STATE_NAMES [ status.stateCode ] .. " ( " .. status.stateText .. " ) " )
else
P ( 1 , " state: \t \t " .. M.STATE_NAMES [ status.stateCode ] )
end
if status.stateCode == M.STATE . DOWNLOADING then
local percent = ( status.imageSize > 0 ) and ( math.ceil ( status.progress / status.imageSize * 1000 ) / 10 ) or 0
P ( 1 , " download progress: \t " .. status.progress .. " / " .. status.imageSize .. " ( " .. percent .. " %) " )
end
2013-10-17 21:45:23 +02:00
elseif argTable.action == ' showAvailableVersions ' then
2013-10-21 12:36:54 +02:00
local verTable , msg = M.getAvailableVersions ( )
2013-10-17 21:45:23 +02:00
if not verTable then
E ( " error collecting version information ( " .. msg .. " ) " )
os.exit ( 2 )
end
P ( 0 , " Available versions: " )
for _ , ent in ipairs ( verTable ) do P ( 1 , M.formatVersion ( ent.version ) ) end
elseif argTable.action == ' showVersionInfo ' then
2013-10-21 12:36:54 +02:00
local vEnt , msg = M.findVersion ( argTable.version )
2013-10-17 21:45:23 +02:00
if vEnt then
P ( 0 , " Information on version: " )
2013-10-18 16:02:22 +02:00
P ( 1 , " version: \t \t " .. M.formatVersion ( vEnt.version ) )
P ( 1 , " sysupgradeFilename: \t " .. ( vEnt.sysupgradeFilename or ' - ' ) )
P ( 1 , " sysupgradeFileSize: \t " .. ( vEnt.sysupgradeFileSize or ' - ' ) )
P ( 1 , " sysupgradeMD5: \t " .. ( vEnt.sysupgradeMD5 or ' - ' ) )
P ( 1 , " factoryFilename: \t " .. ( vEnt.factoryFilename or ' - ' ) )
P ( 1 , " factoryFileSize: \t " .. ( vEnt.factoryFileSize or ' - ' ) )
P ( 1 , " factoryMD5: \t \t " .. ( vEnt.factoryMD5 or ' - ' ) )
if vEnt.changelog then
P ( 1 , " \n --- Changelog --- \n " .. vEnt.changelog .. ' --- ' )
else
P ( 1 , " changelog: \t \t - " )
end
2013-10-21 12:36:54 +02:00
elseif vEnt == false then
P ( 1 , " no such version " )
os.exit ( 4 )
elseif vEnt == nil then
E ( " error searching version index ( " .. msg .. " ) " )
os.exit ( 2 )
2013-10-17 21:45:23 +02:00
end
elseif argTable.action == ' imageDownload ' then
2013-10-21 12:36:54 +02:00
local vEnt , msg = M.findVersion ( argTable.version )
if vEnt == false then
P ( 1 , " no such version " )
os.exit ( 4 )
elseif vEnt == nil then
E ( " error searching version index ( " .. msg .. " ) " )
os.exit ( 2 )
end
local rv , msg = M.downloadImageFile ( vEnt )
2013-10-18 21:46:41 +02:00
if not rv then E ( " could not download file ( " .. msg .. " ) " )
2013-10-17 21:45:23 +02:00
else P ( 1 , " success " )
end
2013-10-21 12:36:54 +02:00
2013-10-18 16:02:22 +02:00
elseif argTable.action == ' clear ' then
2013-10-18 21:46:41 +02:00
local rv , msg = M.clear ( )
if not rv then P ( 1 , " error ( " .. msg .. " ) " )
else P ( 1 , " success " )
2013-10-18 16:02:22 +02:00
end
2013-10-18 21:46:41 +02:00
2013-10-17 21:45:23 +02:00
elseif argTable.action == ' imageInstall ' then
2013-10-21 12:36:54 +02:00
local vEnt , msg = nil , nil
vEnt , msg = M.findVersion ( argTable.version )
if vEnt == false then
P ( 1 , " no such version " )
os.exit ( 4 )
elseif vEnt == nil then
E ( " error searching version index ( " .. msg .. " ) " )
os.exit ( 2 )
end
local rv
rv , msg = M.flashImageVersion ( vEnt )
E ( " error: failed to flash image to device ( " .. msg .. " ) " )
2013-10-17 21:45:23 +02:00
os.exit ( 3 )
2013-10-18 21:46:41 +02:00
2013-10-18 16:02:22 +02:00
else
P ( 0 , " usage: d3d-updater [-hqVcCvslr] [-u base_url] [-i version] [-d version] [-f version] " )
2013-10-17 21:45:23 +02:00
end
os.exit ( 0 )
end
2013-11-04 22:34:09 +01:00
--- Only execute the main function if an arg table is present, this enables usage both as module and as standalone script.
2013-10-17 21:45:23 +02:00
if arg ~= nil then main ( ) end
return M