mirror of
https://github.com/Doodle3D/doodle3d-firmware.git
synced 2024-12-21 10:33:48 +01:00
Integrate new release scripts with firmware.
Accept 'y' for an answer to sync question. Add makeshift test file for later reference.
This commit is contained in:
parent
8582f88e06
commit
96065095ab
@ -1,2 +0,0 @@
|
||||
alias d='ls -la --color=auto'
|
||||
alias wopkg='/usr/share/lua/wifibox/opkg.conf'
|
@ -44,10 +44,10 @@ local paths = {}
|
||||
-----------------------
|
||||
|
||||
local function loadUpdateManager()
|
||||
package.path = package.path .. ';' .. pl.path.join(paths.firmware, 'updater-ng') .. '/?.lua'
|
||||
package.path = package.path .. ';' .. pl.path.join(paths.firmware, 'src') .. '/?.lua'
|
||||
local argStash = arg
|
||||
arg = nil
|
||||
um = require('d3d-update-mgr') -- arg must be nil for the update manager to load as module
|
||||
um = require('script.d3d-updater') -- arg must be nil for the update manager to load as module
|
||||
arg = argStash
|
||||
end
|
||||
|
||||
@ -68,9 +68,9 @@ local function getYesNo(question)
|
||||
io.write(question)
|
||||
io.flush()
|
||||
answer = io.stdin:read('*line'):lower()
|
||||
until answer == 'yes' or answer == 'no' or answer == 'n'
|
||||
until answer == 'yes' or answer == 'y' or answer == 'no' or answer == 'n'
|
||||
|
||||
return (answer == 'yes') and true or false
|
||||
return (answer:sub(1, 1) == 'y') and true or false
|
||||
end
|
||||
|
||||
local function detectRootPrivileges()
|
@ -4,8 +4,8 @@
|
||||
# Using the -u option, it can also upload to doodle3d.com/updates (make sure ssh automatically uses the correct username, or change the rsync command below).
|
||||
# Modify WIFIBOX_BASE_DIR to point to your wifibox directory tree.
|
||||
|
||||
WIFIBOX_BASE_DIR=~/Files/_devel/eclipse-workspace/wifibox
|
||||
DEST_DIR=~/Sites/wifibox
|
||||
WIFIBOX_BASE_DIR=~/Files/_devel/eclipse-workspace/doodle3d/doodle3d-firmware
|
||||
DEST_DIR=~/public_html/wifibox
|
||||
UPDATES_DIR=updates
|
||||
BASE_URL=doodle3d.com
|
||||
|
||||
|
@ -10,12 +10,12 @@
|
||||
--- 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.
|
||||
|
||||
-- TODO/NOTES:
|
||||
-- TODO/NOTES: (from old script)
|
||||
-- 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:
|
||||
-- MAYBE/LATER: (from old script)
|
||||
-- add API calls to retrieve a list of all versions with their info (i.e., the result of getAvailableVersions)
|
||||
-- wget: add provision (in verbose mode?) to use '-v' instead of '-q' and disable output redirection
|
||||
-- document index file format (Version first, then in any order: Files: sysup; factory, FileSize: sysup; factory, MD5: sysup; factory, ChangelogStart:, ..., ChangelogEnd:)
|
||||
@ -45,26 +45,30 @@ M.STATE_NAMES = {
|
||||
}
|
||||
|
||||
--- 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.
|
||||
-- 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 files @{IMAGE_STABLE_INDEX_FILE} and @{IMAGE_BETA_INDEX_FILE} there.
|
||||
M.DEFAULT_BASE_URL = 'http://doodle3d.com/updates'
|
||||
--M.DEFAULT_BASE_URL = 'http://localhost/~USERNAME/wifibox/updates'
|
||||
|
||||
--- The index file containing metadata on update images.
|
||||
M.IMAGE_INDEX_FILE = 'wifibox-image.index'
|
||||
--- The index file containing metadata on stable update images.
|
||||
M.IMAGE_STABLE_INDEX_FILE = 'wifibox-image.index'
|
||||
|
||||
--- The index file containing metadata on beta update images.
|
||||
M.IMAGE_BETA_INDEX_FILE = 'wifibox-image.beta.index'
|
||||
|
||||
--- Path to the updater cache.
|
||||
M.CACHE_PATH = '/tmp/d3d-updater'
|
||||
M.DEFAULT_CACHE_PATH = '/tmp/d3d-updater'
|
||||
|
||||
--- Name of the file to store current state in, this file resides in @{CACHE_PATH}.
|
||||
--- Name of the file to store current state in, this file resides in @{cachePath}.
|
||||
M.STATE_FILE = 'update-state'
|
||||
|
||||
M.WGET_OPTIONS = "-q -t 1 -T 30"
|
||||
--M.WGET_OPTIONS = "-v -t 1 -T 30"
|
||||
|
||||
local verbosity = 0 -- set by parseCommandlineArguments()
|
||||
local verbosity = 0 -- set by parseCommandlineArguments() or @{setVerbosity}
|
||||
local log = nil -- wifibox API can use M.setLogger to enable this module to use its logger
|
||||
local useCache = true -- default, can be overwritten using M.setUseCache()
|
||||
local useCache = true -- default, can be overwritten using @{setUseCache}
|
||||
local cachePath = M.DEFAULT_CACHE_PATH -- default, can be change using @{setCachePath}
|
||||
local baseUrl = M.DEFAULT_BASE_URL -- default, can be overwritten by M.setBaseUrl()
|
||||
|
||||
|
||||
@ -101,7 +105,7 @@ local function E(msg)
|
||||
end
|
||||
end
|
||||
|
||||
--- Splits the return status from `os.execute`, which consists of two bytes.
|
||||
--- Splits the return status from `os.execute` (only Lua <= 5.1), 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)).
|
||||
@ -120,11 +124,12 @@ end
|
||||
-- @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.
|
||||
local function wgetStatusToString(exitStatus)
|
||||
local wgetStatus,systemStatus = splitExitStatus(exitStatus)
|
||||
-- local wgetStatus,systemStatus = splitExitStatus(exitStatus)
|
||||
local wgetStatus = exitStatus
|
||||
|
||||
if systemStatus ~= 0 then
|
||||
return "interrupted:" .. systemStatus
|
||||
end
|
||||
-- if systemStatus ~= 0 then
|
||||
-- return "interrupted: " .. systemStatus
|
||||
-- end
|
||||
|
||||
-- adapted from man(1) wget on OSX
|
||||
local statusTexts = {
|
||||
@ -149,8 +154,9 @@ end
|
||||
-- @return bool|nil True, or nil on error.
|
||||
-- @return ?string A message in case of error.
|
||||
local function createCacheDirectory()
|
||||
if os.execute('mkdir -p ' .. M.CACHE_PATH) ~= 0 then
|
||||
return nil,"Error: could not create cache directory '" .. M.CACHE_PATH .. "'"
|
||||
local _,rv = M.compatexecute('mkdir -p ' .. cachePath)
|
||||
if rv ~= 0 then
|
||||
return nil,"Error: could not create cache directory '" .. cachePath .. "'"
|
||||
end
|
||||
return true
|
||||
end
|
||||
@ -159,7 +165,7 @@ end
|
||||
-- @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).
|
||||
local function getState()
|
||||
local file,msg = io.open(M.CACHE_PATH .. '/' .. M.STATE_FILE, 'r')
|
||||
local file,msg = io.open(cachePath .. '/' .. M.STATE_FILE, 'r')
|
||||
if not file then return M.STATE.NONE,"" end
|
||||
|
||||
local state = file:read('*a')
|
||||
@ -179,7 +185,7 @@ end
|
||||
|
||||
--- Read the contents of a file.
|
||||
--
|
||||
-- TODO: this file has been copied from @{util.utils}.lua and should be merged again.
|
||||
-- TODO: this file has been copied from @{util.utils}.lua and should be merged back.
|
||||
-- @string filePath The file to read.
|
||||
-- @bool trimResult Whether or not to trim the read data.
|
||||
-- @treturn bool|nil True, or nil on error.
|
||||
@ -201,7 +207,7 @@ end
|
||||
|
||||
--- Reports whether or not a file exists.
|
||||
--
|
||||
-- TODO: this file has been copied from @{util.utils}.lua and should be merged again.
|
||||
-- TODO: this file has been copied from @{util.utils}.lua and should be merged back.
|
||||
-- @string file The file to report about.
|
||||
-- @treturn bool True if the file exists, false otherwise.
|
||||
local function exists(file)
|
||||
@ -216,7 +222,7 @@ end
|
||||
|
||||
--- Reports the size of a file or file handle.
|
||||
--
|
||||
-- TODO: this file has been copied from @{util.utils}.lua and should be merged again.
|
||||
-- TODO: this file has been copied from @{util.utils}.lua and should be merged back.
|
||||
-- @param file A file path or open file handle.
|
||||
-- @treturn number Size of the file.
|
||||
local function fileSize(file)
|
||||
@ -241,8 +247,9 @@ end
|
||||
-- @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.
|
||||
local function runCommand(command, dryRun)
|
||||
D("about to run: '" .. command .. "'")
|
||||
return (not dryRun) and os.execute(command) or -1
|
||||
--D("about to run: '" .. command .. "'")
|
||||
if dryRun then return -1 end
|
||||
return M.compatexecute(command)
|
||||
end
|
||||
|
||||
--- Removes a file.
|
||||
@ -260,6 +267,7 @@ end
|
||||
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 ''
|
||||
D("Downloading file '" .. url .. "'")
|
||||
if filename:len() > 0 then
|
||||
return runCommand('wget ' .. M.WGET_OPTIONS .. ' -O ' .. saveDir .. '/' .. filename .. ' ' .. url .. ' 2> /dev/null')
|
||||
else
|
||||
@ -328,6 +336,31 @@ end
|
||||
-- MODULE FUNCTIONS --
|
||||
----------------------
|
||||
|
||||
local compatlua51 = _VERSION == 'Lua 5.1'
|
||||
|
||||
--- execute a shell command. Taken from penlight library.
|
||||
-- This is a compatibility function that returns the same for Lua 5.1 and Lua 5.2
|
||||
-- @param cmd a shell command
|
||||
-- @return true if successful
|
||||
-- @return actual return code
|
||||
function M.compatexecute (cmd)
|
||||
local res1,res2,res3 = os.execute(cmd)
|
||||
if compatlua51 then
|
||||
local cmd, sys = splitExitStatus(res1)
|
||||
return (res1 == 0) and true or nil, sys
|
||||
else
|
||||
return res1, res3
|
||||
end
|
||||
end
|
||||
|
||||
--- Set verbosity (log level) that determines which messages do get logged and which do not.
|
||||
-- @tparam number level The level to set, between -1 and 1.
|
||||
function M.setVerbosity(level)
|
||||
if level and level >= -1 and level <= 1 then
|
||||
verbosity = level
|
||||
end
|
||||
end
|
||||
|
||||
--- 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.
|
||||
function M.setLogger(logger)
|
||||
@ -348,6 +381,12 @@ function M.setBaseUrl(url)
|
||||
baseUrl = url
|
||||
end
|
||||
|
||||
--- Sets the filesystem path to use as cache for downloaded index and image files.
|
||||
-- @string path The path to use, use nil to restore default @{DEFAULT_CACHE_PATH}.
|
||||
function M.setCachePath(path)
|
||||
cachePath = path or M.DEFAULT_CACHE_PATH
|
||||
end
|
||||
|
||||
--- 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.
|
||||
@ -357,7 +396,7 @@ end
|
||||
-- @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.
|
||||
function M.getStatus()
|
||||
function M.getStatus(includeBetas)
|
||||
if not baseUrl then baseUrl = M.DEFAULT_BASE_URL end
|
||||
local unknownVersion = { major = 0, minor = 0, patch = 0 }
|
||||
local result = {}
|
||||
@ -366,18 +405,27 @@ function M.getStatus()
|
||||
result.stateCode, result.stateText = getState()
|
||||
result.stateCode = tonumber(result.stateCode)
|
||||
|
||||
local verTable,msg = M.getAvailableVersions()
|
||||
local verTable,msg = M.getAvailableVersions(includeBetas and 'both' or 'stables')
|
||||
if not verTable then
|
||||
D("could not obtain available versions (" .. msg .. ")")
|
||||
D("error: could not obtain available versions (" .. msg .. ")")
|
||||
-- TODO: set an error state in result to signify we probably do not have internet access?
|
||||
return false, result, msg
|
||||
end
|
||||
|
||||
local newest = verTable and verTable[#verTable]
|
||||
result.newestVersion = newest and newest.version or unknownVersion
|
||||
result.newestReleaseTimestamp = newest and newest.timestamp
|
||||
|
||||
-- look up timestamp of current version
|
||||
local cEnt = M.findVersion(result.currentVersion, verTable)
|
||||
if cEnt then
|
||||
result.currentReleaseTimestamp = cEnt.timestamp
|
||||
else
|
||||
D("warning: could not find current wifibox version in release index, beta setting disabled after having beta installed?")
|
||||
end
|
||||
|
||||
if result.stateCode == M.STATE.DOWNLOADING then
|
||||
result.progress = fileSize(M.CACHE_PATH .. '/' .. newest.sysupgradeFilename)
|
||||
result.progress = fileSize(cachePath .. '/' .. newest.sysupgradeFilename)
|
||||
if not result.progress then result.progress = 0 end -- in case the file does not exist yet (which yields nil)
|
||||
result.imageSize = newest.sysupgradeFileSize
|
||||
end
|
||||
@ -387,43 +435,77 @@ end
|
||||
|
||||
--- Turns a plain-text version as returned by @{formatVersion} into a table.
|
||||
-- @tparam string|table versionText The version string to parse, if it is already a table, it is returned as-is.
|
||||
-- @treturn table A parse version.
|
||||
-- @treturn table A parsed version or nil on incorrect argument.
|
||||
function M.parseVersion(versionText)
|
||||
if not versionText then return nil end
|
||||
if type(versionText) == 'table' then return versionText end
|
||||
if not versionText or versionText:len() == 0 then return nil end
|
||||
|
||||
local major,minor,patch = versionText:match("^%s*(%d+)%.(%d+)%.(%d+)%s*$")
|
||||
if not major or not minor or not patch then return nil end
|
||||
local major,minor,patch,suffix = versionText:match("^%s*(%d+)%.(%d+)%.(%d+)(-?%w*)%s*$")
|
||||
if not major or not minor or not patch then return nil end -- suffix not required
|
||||
|
||||
return { ['major'] = major, ['minor'] = minor, ['patch'] = patch }
|
||||
if type(suffix) == 'string' and suffix:len() > 0 then
|
||||
if suffix:sub(1, 1) ~= '-' then return nil end
|
||||
suffix = suffix:sub(2)
|
||||
else
|
||||
suffix = nil
|
||||
end
|
||||
|
||||
return { ['major'] = major, ['minor'] = minor, ['patch'] = patch, ['suffix'] = suffix }
|
||||
end
|
||||
|
||||
--- Formats a version as returned by @{parseVersion}.
|
||||
-- @tparam table|string version The version to format, if it is already a string, that will be returned unmodified.
|
||||
-- @treturn string A formatted version.
|
||||
-- @treturn string A formatted version or nil on incorrect argument.
|
||||
function M.formatVersion(version)
|
||||
if not version then return nil end
|
||||
if type(version) == 'string' then return version end
|
||||
return version.major .. "." .. version.minor .. "." .. version.patch
|
||||
|
||||
local ver = version.major .. "." .. version.minor .. "." .. version.patch
|
||||
if version.suffix then ver = ver .. '-' .. version.suffix end
|
||||
|
||||
return ver
|
||||
end
|
||||
|
||||
--- Compares two versions.
|
||||
--- Compares two versions. Note that the second return value must be used for equality testing.
|
||||
-- If given, the timestamps have higher priority than the versions. Suffixes are ignored.
|
||||
-- @tparam table versionA A version as returned by @{parseVersion}.
|
||||
-- @tparam table versionB A version as returned by @{parseVersion}.
|
||||
-- @treturn number -1 if versionA is smaller than versionB, 0 if versions are equal or 1 if versionA is larger than versionB.
|
||||
function M.compareVersions(versionA, versionB)
|
||||
-- @param timestampA[opt] A timestamp as returned by @{parseDate}.
|
||||
-- @param timestampB[opt] A timestamp as returned by @{parseDate}.
|
||||
-- @treturn number -1 if versionA/timestampA is smaller/older than versionB/timestampB, 0 if versions are equal (or undecided) or 1 if A is larger/newer than B.
|
||||
-- @treturn bool True if versions are really equal (first return value can be 0 if everything but the suffix is equal)
|
||||
function M.compareVersions(versionA, versionB, timestampA, timestampB)
|
||||
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)
|
||||
|
||||
local diff = 0
|
||||
if timestampA and timestampB then diff = timestampA - timestampB end
|
||||
if diff == 0 then
|
||||
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
|
||||
end
|
||||
|
||||
local result = diff > 0 and 1 or (diff < 0 and -1 or 0)
|
||||
local reallyEqual = (diff == 0) and (versionA.suffix == versionB.suffix)
|
||||
|
||||
return result, (reallyEqual and true or false)
|
||||
end
|
||||
|
||||
--- Checks if versions are exactly equal.
|
||||
-- It returns the second return value of @{compareVersions} and accepts the same arguments.
|
||||
-- @treturn bool True if versions are equal, false otherwise.
|
||||
function M.versionsEqual(versionA, versionB, timestampA, timestampB)
|
||||
return select(2, M.compareVersions(versionA, versionB, timestampA, timestampB))
|
||||
end
|
||||
|
||||
--- Returns information on a version if it can be found in a collection of versions as returned by @{getAvailableVersions}.
|
||||
-- @tparam table version The version to look for.
|
||||
-- @tparam table[opt] verTable A table containing a collection of versions, if not passed in, it will be obtained using @{getAvailableVersions}.
|
||||
-- @param timestamp[opt] Specific timestamp to look for.
|
||||
-- @treturn table|nil Version information table found in the collection, or nil on error or if not found.
|
||||
-- @treturn string Descriptive message in case of error or if the version could not be found.
|
||||
function M.findVersion(version, verTable)
|
||||
function M.findVersion(version, verTable, timestamp)
|
||||
local msg = nil
|
||||
version = M.parseVersion(version)
|
||||
if not verTable then verTable,msg = M.getAvailableVersions() end
|
||||
@ -431,11 +513,28 @@ function M.findVersion(version, verTable)
|
||||
if not verTable then return nil,msg end
|
||||
|
||||
for _,ent in pairs(verTable) do
|
||||
if M.compareVersions(ent.version, version) == 0 then return ent end
|
||||
if M.versionsEqual(ent.version, version, ent.timestamp, timestamp) == true then return ent end
|
||||
end
|
||||
return nil,"no such version"
|
||||
end
|
||||
|
||||
--- Turns a date of the format 'yyyymmdd' into a timestamp as returned by os.time.
|
||||
-- @tparam string dateText The date to parse.
|
||||
-- @return A timestamp or nil if the argument does not have correct format.
|
||||
function M.parseDate(dateText)
|
||||
if type(dateText) ~= 'string' or dateText:len() ~= 8 or dateText:find('[^%d]') ~= nil then return nil end
|
||||
|
||||
return os.time({ year = dateText:sub(1, 4), month = dateText:sub(5, 6), day = dateText:sub(7,8) })
|
||||
end
|
||||
|
||||
--- Formats a timestamp as returned by os.time to a date of the form 'yyyymmdd'.
|
||||
-- @param timestamp The timestamp to format.
|
||||
-- @return A formatted date or nil if the argument is nil.
|
||||
function M.formatDate(timestamp)
|
||||
if not timestamp then return nil end
|
||||
return os.date('%Y%m%d', timestamp)
|
||||
end
|
||||
|
||||
--- Creates an image file name based on given properties.
|
||||
-- The generated name has the following form: `doodle3d-wifibox-<version>-<deviceType>-<'factory'|'sysupgrade'>.bin`.
|
||||
-- @tparam table|string version The version of the image.
|
||||
@ -449,7 +548,7 @@ function M.constructImageFilename(version, devType, isFactory)
|
||||
return 'doodle3d-wifibox-' .. M.formatVersion(v) .. '-' .. dt .. '-' .. sf .. '.bin'
|
||||
end
|
||||
|
||||
--- Checks whether a valid image file is present in @{CACHE_PATH} for the given image properties.
|
||||
--- Checks whether a valid image file is present in @{cachePath} for the given image properties.
|
||||
-- The versionEntry table will be augmented with an `isValid` key.
|
||||
--
|
||||
-- NOTE: currently, this function only checks the image exists and has the correct size.
|
||||
@ -461,8 +560,8 @@ end
|
||||
-- @treturn bool True if a valid image is present, false otherwise.
|
||||
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)
|
||||
--return versionEntry.md5 == md5sum(cachePath .. '/' .. filename)
|
||||
local size = fileSize(cachePath .. '/' .. filename)
|
||||
versionEntry.isValid = versionEntry.sysupgradeFileSize == size
|
||||
return versionEntry.isValid
|
||||
end
|
||||
@ -482,18 +581,14 @@ function M.getCurrentVersion()
|
||||
end
|
||||
|
||||
--- Returns an indexed and sorted table containing version information tables.
|
||||
-- The information is obtained from the either cached or downloaded image index (@{IMAGE_INDEX_FILE}).
|
||||
-- @treturn table A table with a collection of version information tables.
|
||||
function M.getAvailableVersions()
|
||||
-- The information is obtained from the either cached or downloaded image index file.
|
||||
local function fetchIndexTable(indexFile, cachePath)
|
||||
if not baseUrl then baseUrl = M.DEFAULT_BASE_URL end
|
||||
local indexFilename = M.CACHE_PATH .. '/' .. M.IMAGE_INDEX_FILE
|
||||
|
||||
local ccRv,ccMsg = createCacheDirectory()
|
||||
if not ccRv then return nil,ccMsg end
|
||||
local indexFilename = cachePath .. '/' .. indexFile
|
||||
|
||||
if not useCache or not exists(indexFilename) then
|
||||
local rv = downloadFile(baseUrl .. '/images/' .. M.IMAGE_INDEX_FILE, M.CACHE_PATH, M.IMAGE_INDEX_FILE)
|
||||
if rv ~= 0 then return nil,"could not download image index file (" .. wgetStatusToString(rv) .. ")" end
|
||||
local rv1,rv2 = downloadFile(baseUrl .. '/images/' .. indexFile, cachePath, indexFile)
|
||||
if not rv1 then return nil,"could not download image index file (" .. wgetStatusToString(rv2) .. ")" end
|
||||
end
|
||||
|
||||
local status,idxLines = pcall(io.lines, indexFilename)
|
||||
@ -505,7 +600,7 @@ function M.getAvailableVersions()
|
||||
for line in idxLines do
|
||||
local k,v = line:match('^(.-):(.*)$')
|
||||
k,v = trim(k), trim(v)
|
||||
if not log then D("#" .. lineno .. ": considering '" .. line .. "' (" .. (k or '<nil>') .. " / " .. (v or '<nil>') .. ")") end
|
||||
--if not log then D("#" .. lineno .. ": considering '" .. line .. "' (" .. (k or '<nil>') .. " / " .. (v or '<nil>') .. ")") end
|
||||
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
|
||||
@ -537,6 +632,13 @@ function M.getAvailableVersions()
|
||||
sSum,fSum = trim(sSum), trim(fSum)
|
||||
if sSum then entry.sysupgradeMD5 = sSum end
|
||||
if fSum then entry.factoryMD5 = fSum end
|
||||
elseif k == 'ReleaseDate' then
|
||||
local ts = M.parseDate(v)
|
||||
if not ts then
|
||||
P(0, "ignoring incorrectly formatted ReleaseDate field (line " .. lineno .. ")")
|
||||
else
|
||||
entry.timestamp = ts
|
||||
end
|
||||
else
|
||||
P(-1, "ignoring unrecognized field in index file '" .. k .. "' (line " .. lineno .. ")")
|
||||
end
|
||||
@ -553,6 +655,31 @@ function M.getAvailableVersions()
|
||||
return result
|
||||
end
|
||||
|
||||
--- Returns an indexed and sorted table containing version information tables.
|
||||
-- The information is obtained from the either cached or downloaded image index (@{IMAGE_STABLE_INDEX_FILE}).
|
||||
-- @tparam which[opt] Which type of versions to fetch, either 'stables' (default), 'betas' or both.
|
||||
-- @treturn table A table with a collection of version information tables.
|
||||
function M.getAvailableVersions(which)
|
||||
local ccRv,ccMsg = createCacheDirectory()
|
||||
if not ccRv then return nil,ccMsg end
|
||||
|
||||
local verTable, msg = {}, nil
|
||||
|
||||
if which == 'stables' or which == 'both' then
|
||||
verTable,msg = fetchIndexTable(M.IMAGE_STABLE_INDEX_FILE, cachePath)
|
||||
if not verTable then return nil,msg end
|
||||
end
|
||||
|
||||
if which == 'betas' or which == 'both' then
|
||||
local betas,msg = fetchIndexTable(M.IMAGE_BETA_INDEX_FILE, cachePath)
|
||||
if not betas then return nil,msg end
|
||||
|
||||
for k,v in pairs(betas) do verTable[k] = v end
|
||||
end
|
||||
|
||||
return verTable
|
||||
end
|
||||
|
||||
--- Attempts to download an image file with the requested properties.
|
||||
-- @tparam table versionEntry A version information table.
|
||||
-- @string[opt] devType Image device type, see @{constructImageFilename}.
|
||||
@ -577,7 +704,7 @@ function M.downloadImageFile(versionEntry, devType, isFactory)
|
||||
local rv = 0
|
||||
if doDownload then
|
||||
M.setState(M.STATE.DOWNLOADING, "Downloading image (" .. filename .. ")")
|
||||
rv = downloadFile(baseUrl .. '/images/' .. filename, M.CACHE_PATH, filename)
|
||||
rv = downloadFile(baseUrl .. '/images/' .. filename, cachePath, filename)
|
||||
end
|
||||
|
||||
if rv == 0 then
|
||||
@ -585,14 +712,14 @@ function M.downloadImageFile(versionEntry, devType, isFactory)
|
||||
M.setState(M.STATE.IMAGE_READY, "Image downloaded, ready to install (image name: " .. filename .. ")")
|
||||
return true
|
||||
else
|
||||
removeFile(M.CACHE_PATH .. '/' .. filename)
|
||||
removeFile(cachePath .. '/' .. filename)
|
||||
local ws = "Image download failed (invalid image)"
|
||||
M.setState(M.STATE.DOWNLOAD_FAILED, ws)
|
||||
return nil,ws
|
||||
end
|
||||
else
|
||||
local ws = wgetStatusToString(rv)
|
||||
removeFile(M.CACHE_PATH .. '/' .. filename)
|
||||
removeFile(cachePath .. '/' .. filename)
|
||||
M.setState(M.STATE.DOWNLOAD_FAILED, "Image download failed (wget error: " .. ws .. ")")
|
||||
return nil,ws
|
||||
end
|
||||
@ -611,7 +738,7 @@ function M.flashImageVersion(versionEntry, noRetain, devType, isFactory)
|
||||
log:info("flashImageVersion")
|
||||
local imgName = M.constructImageFilename(versionEntry.version, devType, isFactory)
|
||||
local cmd = noRetain and 'sysupgrade -n ' or 'sysupgrade '
|
||||
cmd = cmd .. M.CACHE_PATH .. '/' .. imgName
|
||||
cmd = cmd .. cachePath .. '/' .. imgName
|
||||
|
||||
local ccRv,ccMsg = createCacheDirectory()
|
||||
if not ccRv then return nil,ccMsg end
|
||||
@ -634,22 +761,22 @@ function M.flashImageVersion(versionEntry, noRetain, devType, isFactory)
|
||||
return (rv == 0) and true or nil,rv
|
||||
end
|
||||
|
||||
--- Clears '*.bin' in the @{CACHE_PATH} directory.
|
||||
--- Clears '*.bin' in the @{cachePath} directory.
|
||||
-- @treturn bool|nil True on success, or nil on error.
|
||||
-- @treturn ?string Descriptive message on error.
|
||||
function M.clear()
|
||||
local ccRv,ccMsg = createCacheDirectory()
|
||||
if not ccRv then return nil,ccMsg end
|
||||
|
||||
D("Removing " .. M.CACHE_PATH .. "/doodle3d-wifibox-*.bin")
|
||||
D("Removing " .. cachePath .. "/doodle3d-wifibox-*.bin")
|
||||
M.setState(M.STATE.NONE, "")
|
||||
local rv = os.execute('rm -f ' .. M.CACHE_PATH .. '/doodle3d-wifibox-*.bin')
|
||||
local rv = M.compatexecute('rm -f ' .. cachePath .. '/doodle3d-wifibox-*.bin')
|
||||
return (rv == 0) and true or nil,"could not remove image files"
|
||||
end
|
||||
|
||||
--- Set updater state.
|
||||
--
|
||||
-- NOTE: make sure the cache directory @{CACHE_PATH} exists before calling this function or it will fail.
|
||||
-- NOTE: make sure the cache directory @{cachePath} exists before calling this function or it will fail.
|
||||
--
|
||||
-- NOTE: this function _can_ fail but this is not expected to happen so the return value is mostly ignored for now.
|
||||
--
|
||||
@ -659,7 +786,7 @@ end
|
||||
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')
|
||||
local file,msg = io.open(cachePath .. '/' .. M.STATE_FILE, 'w')
|
||||
|
||||
if not file then
|
||||
E("error: could not open state file for writing (" .. msg .. ")")
|
||||
|
58
test/test-d3d-updater.lua
Normal file
58
test/test-d3d-updater.lua
Normal file
@ -0,0 +1,58 @@
|
||||
-- This script contains a number of impromptu tests to check version comparisons, kept around in case real unit tests will be created one day.
|
||||
argStash = arg
|
||||
arg = nil
|
||||
local upd = require('d3d-updater')
|
||||
arg = argStash
|
||||
|
||||
local function dump(o)
|
||||
if type(o) == 'table' then
|
||||
local s = '{ '
|
||||
for k,v in pairs(o) do
|
||||
if type(k) ~= 'number' then k = '"'..k..'"' end
|
||||
s = s .. '['..k..'] = ' .. dump(v) .. ','
|
||||
end
|
||||
return s .. '} '
|
||||
else
|
||||
return tostring(o)
|
||||
end
|
||||
end
|
||||
|
||||
local input = '19990213'
|
||||
local ts = upd.parseDate(input)
|
||||
print("parse " .. input .. ": " .. ts)
|
||||
print("format " .. ts .. ": " .. upd.formatDate(ts))
|
||||
|
||||
local vertex1 = '0.2.3'
|
||||
local vertab1 = upd.parseVersion(vertex1)
|
||||
print("parse " .. vertex1 .. ": " .. dump(vertab1))
|
||||
print("formatted: " .. upd.formatVersion(vertab1))
|
||||
|
||||
local vertex2 = '0.2.3-text'
|
||||
local vertab2 = upd.parseVersion(vertex2)
|
||||
print("parse " .. vertex2 .. ": " .. dump(vertab2))
|
||||
print("formatted: " .. upd.formatVersion(vertab2))
|
||||
|
||||
local vA, vB = upd.parseVersion("1.4.5-alpha"), upd.parseVersion("1.4.4-rc1")
|
||||
local tsA, tsB = 100000, 100005
|
||||
|
||||
local cmp1,cmp2 = upd.compareVersions(vA, vB)
|
||||
print("vA <=> vB: " .. cmp1 .. " / " .. dump(cmp2))
|
||||
cmp1,cmp2 = upd.compareVersions(vA, vB, tsA, tsB)
|
||||
print("vA/tsA <=> vB/tsB: " .. cmp1 .. " / " .. dump(cmp2))
|
||||
cmp1,cmp2 = upd.compareVersions(vB, vA, tsA, tsB)
|
||||
print("vB/tsA <=> vA/tsB: " .. cmp1 .. " / " .. dump(cmp2))
|
||||
cmp1,cmp2 = upd.compareVersions(vA, vB, tsB, tsA)
|
||||
print("vA/tsB <=> vB/tsA: " .. cmp1 .. " / " .. dump(cmp2))
|
||||
|
||||
local vWithout,vWith = upd.parseVersion('1.2.3'), upd.parseVersion('1.2.3-sfx')
|
||||
--print("vWithout: " .. dump(vWithout) .. "; vWith: " .. dump(vWith))
|
||||
cmp1,cmp2 = upd.compareVersions(vWithout, vWithout)
|
||||
print("1.2.3 <=> 1.2.3: " .. cmp1 .. " / " .. dump(cmp2))
|
||||
cmp1,cmp2 = upd.compareVersions(vWithout, vWith)
|
||||
print("1.2.3 <=> 1.2.3-sfx: " .. cmp1 .. " / " .. dump(cmp2))
|
||||
cmp1,cmp2 = upd.compareVersions(vWith, vWith)
|
||||
print("1.2.3-sfx <=> 1.2.3-sfx: " .. cmp1 .. " / " .. dump(cmp2))
|
||||
|
||||
print("nn equal? " .. dump(upd.versionsEqual(vWithout, vWithout)))
|
||||
print("ny equal? " .. dump(upd.versionsEqual(vWithout, vWith)))
|
||||
print("yy equal? " .. dump(upd.versionsEqual(vWith, vWith)))
|
@ -1,951 +0,0 @@
|
||||
#!/usr/bin/env lua
|
||||
--
|
||||
-- This file is part of the Doodle3D project (http://doodle3d.com).
|
||||
--
|
||||
-- @copyright 2013, Doodle3D
|
||||
-- @license This software is licensed under the terms of the GNU GPL v2 or later.
|
||||
-- See file LICENSE.txt or visit http://www.gnu.org/licenses/gpl.html for full license details.
|
||||
|
||||
|
||||
--- 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.
|
||||
|
||||
-- TODO/NOTES: (from old script)
|
||||
-- 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: (from old script)
|
||||
-- add API calls to retrieve a list of all versions with their info (i.e., the result of getAvailableVersions)
|
||||
-- wget: add provision (in verbose mode?) to use '-v' instead of '-q' and disable output redirection
|
||||
-- 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)
|
||||
-- note: take care not to print any text in module functions, as this breaks http responses
|
||||
-- change representation of sysupgrade/factory info in versionInfo? (and also in image index?) <- create api call to get all info on all versions?
|
||||
|
||||
local M = {}
|
||||
|
||||
--- 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.
|
||||
M.STATE_NAMES = {
|
||||
[M.STATE.NONE] = 'none', [M.STATE.DOWNLOADING] = 'downloading', [M.STATE.DOWNLOAD_FAILED] = 'download_failed', [M.STATE.IMAGE_READY] = 'image_ready',
|
||||
[M.STATE.INSTALLING] = 'installing', [M.STATE.INSTALLED] = 'installed', [M.STATE.INSTALL_FAILED] = 'install_failed'
|
||||
}
|
||||
|
||||
--- 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 files @{IMAGE_STABLE_INDEX_FILE} and @{IMAGE_BETA_INDEX_FILE} there.
|
||||
M.DEFAULT_BASE_URL = 'http://doodle3d.com/updates'
|
||||
--M.DEFAULT_BASE_URL = 'http://localhost/~USERNAME/wifibox/updates'
|
||||
|
||||
--- The index file containing metadata on stable update images.
|
||||
M.IMAGE_STABLE_INDEX_FILE = 'wifibox-image.index'
|
||||
|
||||
--- The index file containing metadata on beta update images.
|
||||
M.IMAGE_BETA_INDEX_FILE = 'wifibox-image.beta.index'
|
||||
|
||||
--- Path to the updater cache.
|
||||
M.DEFAULT_CACHE_PATH = '/tmp/d3d-updater'
|
||||
|
||||
--- Name of the file to store current state in, this file resides in @{cachePath}.
|
||||
M.STATE_FILE = 'update-state'
|
||||
|
||||
M.WGET_OPTIONS = "-q -t 1 -T 30"
|
||||
--M.WGET_OPTIONS = "-v -t 1 -T 30"
|
||||
|
||||
local verbosity = 0 -- set by parseCommandlineArguments() or @{setVerbosity}
|
||||
local log = nil -- wifibox API can use M.setLogger to enable this module to use its logger
|
||||
local useCache = true -- default, can be overwritten using @{setUseCache}
|
||||
local cachePath = M.DEFAULT_CACHE_PATH -- default, can be change using @{setCachePath}
|
||||
local baseUrl = M.DEFAULT_BASE_URL -- default, can be overwritten by M.setBaseUrl()
|
||||
|
||||
|
||||
|
||||
---------------------
|
||||
-- LOCAL FUNCTIONS --
|
||||
---------------------
|
||||
|
||||
--- 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.
|
||||
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
|
||||
|
||||
--- 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.
|
||||
local function D(msg) P(-1, (log and msg or "(DBG) " .. msg)) end
|
||||
|
||||
--- 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.
|
||||
local function E(msg)
|
||||
if log then log:error(msg)
|
||||
else io.stderr:write(msg .. '\n')
|
||||
end
|
||||
end
|
||||
|
||||
--- Splits the return status from `os.execute` (only Lua <= 5.1), 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.
|
||||
local function splitExitStatus(exitStatus)
|
||||
if exitStatus == -1 then return -1,-1 end
|
||||
local cmdStatus = math.floor(exitStatus / 256)
|
||||
local systemStatus = exitStatus - cmdStatus * 256
|
||||
return cmdStatus, systemStatus
|
||||
end
|
||||
|
||||
--- 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.
|
||||
local function wgetStatusToString(exitStatus)
|
||||
-- local wgetStatus,systemStatus = splitExitStatus(exitStatus)
|
||||
local wgetStatus = exitStatus
|
||||
|
||||
-- if systemStatus ~= 0 then
|
||||
-- return "interrupted: " .. systemStatus
|
||||
-- end
|
||||
|
||||
-- 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'
|
||||
}
|
||||
local result = statusTexts[tostring(wgetStatus)]
|
||||
|
||||
if result then return exitStatus .. ": " .. result
|
||||
else return exitStatus
|
||||
end
|
||||
end
|
||||
|
||||
--- Creates the updater cache directory.
|
||||
-- @return bool|nil True, or nil on error.
|
||||
-- @return ?string A message in case of error.
|
||||
local function createCacheDirectory()
|
||||
local _,rv = M.compatexecute('mkdir -p ' .. cachePath)
|
||||
if rv ~= 0 then
|
||||
return nil,"Error: could not create cache directory '" .. cachePath .. "'"
|
||||
end
|
||||
return true
|
||||
end
|
||||
|
||||
--- 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).
|
||||
local function getState()
|
||||
local file,msg = io.open(cachePath .. '/' .. 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
|
||||
|
||||
--- 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.
|
||||
local function trim(s)
|
||||
if type(s) ~= 'string' then return s end
|
||||
return (s:find('^%s*$') and '' or s:match('^%s*(.*%S)'))
|
||||
end
|
||||
|
||||
--- Read the contents of a file.
|
||||
--
|
||||
-- TODO: this file has been copied from @{util.utils}.lua and should be merged back.
|
||||
-- @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.
|
||||
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
|
||||
|
||||
--- Reports whether or not a file exists.
|
||||
--
|
||||
-- TODO: this file has been copied from @{util.utils}.lua and should be merged back.
|
||||
-- @string file The file to report about.
|
||||
-- @treturn bool True if the file exists, false otherwise.
|
||||
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
|
||||
|
||||
--- Reports the size of a file or file handle.
|
||||
--
|
||||
-- TODO: this file has been copied from @{util.utils}.lua and should be merged back.
|
||||
-- @param file A file path or open file handle.
|
||||
-- @treturn number Size of the file.
|
||||
local function fileSize(file)
|
||||
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
|
||||
|
||||
return size
|
||||
end
|
||||
|
||||
--- 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.
|
||||
local function runCommand(command, dryRun)
|
||||
--D("about to run: '" .. command .. "'")
|
||||
if dryRun then return -1 end
|
||||
return M.compatexecute(command)
|
||||
end
|
||||
|
||||
--- Removes a file.
|
||||
-- @string filePath The file to remove.
|
||||
local function removeFile(filePath)
|
||||
return runCommand('rm ' .. filePath)
|
||||
end
|
||||
|
||||
--- 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.
|
||||
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 ''
|
||||
D("Downloading file '" .. url .. "'")
|
||||
if filename:len() > 0 then
|
||||
return runCommand('wget ' .. M.WGET_OPTIONS .. ' -O ' .. saveDir .. '/' .. filename .. ' ' .. url .. ' 2> /dev/null')
|
||||
else
|
||||
return runCommand('wget ' .. M.WGET_OPTIONS .. ' -P ' .. saveDir .. ' ' .. url .. ' 2> /dev/null')
|
||||
end
|
||||
end
|
||||
|
||||
--- Parses command-line arguments and returns a table containing information distilled from them.
|
||||
-- @tparam table arglist A table in the same form as the [arg table](http://www.lua.org/pil/1.4.html) created by Lua.
|
||||
-- @treturn table|nil A table containing information on what to do, or nil if invalid arguments were specified.
|
||||
-- @treturn ?string Descriptive message on error.
|
||||
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'
|
||||
elseif argument == '-s' then result.action = 'showStatus'
|
||||
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
|
||||
elseif argument == '-r' then result.action = 'clear'
|
||||
else return nil,"unrecognized argument '" .. argument .. "'"
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
if result.version then
|
||||
result.version = M.parseVersion(result.version)
|
||||
if not result.version then
|
||||
return nil,"error parsing specified version"
|
||||
end
|
||||
end
|
||||
|
||||
if nextIsVersion then return nil, "missing required version argument" end
|
||||
if nextIsUrl then return nil, "missing required URL argument" end
|
||||
|
||||
return result
|
||||
end
|
||||
|
||||
--- Returns the [MD5](http://en.wikipedia.org/wiki/MD5) hash for a given file.
|
||||
--
|
||||
-- NOTE: this function is not implemented, and a better hash function should probably be chosen anyway.
|
||||
-- @string filepath The path of which to calculate the MD5-sum.
|
||||
-- @treturn nil
|
||||
local function md5sum(filepath)
|
||||
return nil
|
||||
-- TODO [osx: md5 -q <file>], [linux: ?]
|
||||
end
|
||||
|
||||
|
||||
|
||||
|
||||
----------------------
|
||||
-- MODULE FUNCTIONS --
|
||||
----------------------
|
||||
|
||||
local compatlua51 = _VERSION == 'Lua 5.1'
|
||||
|
||||
--- execute a shell command. Taken from penlight library.
|
||||
-- This is a compatibility function that returns the same for Lua 5.1 and Lua 5.2
|
||||
-- @param cmd a shell command
|
||||
-- @return true if successful
|
||||
-- @return actual return code
|
||||
function M.compatexecute (cmd)
|
||||
local res1,res2,res3 = os.execute(cmd)
|
||||
if compatlua51 then
|
||||
local cmd, sys = splitExitStatus(res1)
|
||||
return (res1 == 0) and true or nil, sys
|
||||
else
|
||||
return res1, res3
|
||||
end
|
||||
end
|
||||
|
||||
--- Set verbosity (log level) that determines which messages do get logged and which do not.
|
||||
-- @tparam number level The level to set, between -1 and 1.
|
||||
function M.setVerbosity(level)
|
||||
if level and level >= -1 and level <= 1 then
|
||||
verbosity = level
|
||||
end
|
||||
end
|
||||
|
||||
--- 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.
|
||||
function M.setLogger(logger)
|
||||
log = logger
|
||||
end
|
||||
|
||||
--- 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.
|
||||
function M.setUseCache(use)
|
||||
useCache = use
|
||||
end
|
||||
|
||||
--- Sets the base URL to use for finding update images, defaults to @{DEFAULT_BASE_URL}.
|
||||
-- @string url The new base URL to use.
|
||||
function M.setBaseUrl(url)
|
||||
baseUrl = url
|
||||
end
|
||||
|
||||
--- Sets the filesystem path to use as cache for downloaded index and image files.
|
||||
-- @string path The path to use, use nil to restore default @{DEFAULT_CACHE_PATH}.
|
||||
function M.setCachePath(path)
|
||||
cachePath = path or M.DEFAULT_CACHE_PATH
|
||||
end
|
||||
|
||||
--- 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.
|
||||
function M.getStatus(includeBetas)
|
||||
if not baseUrl then baseUrl = M.DEFAULT_BASE_URL end
|
||||
local unknownVersion = { major = 0, minor = 0, patch = 0 }
|
||||
local result = {}
|
||||
|
||||
result.currentVersion = M.getCurrentVersion()
|
||||
result.stateCode, result.stateText = getState()
|
||||
result.stateCode = tonumber(result.stateCode)
|
||||
|
||||
local verTable,msg = M.getAvailableVersions(includeBetas and 'both' or 'stables')
|
||||
if not verTable then
|
||||
D("error: could not obtain available versions (" .. msg .. ")")
|
||||
-- TODO: set an error state in result to signify we probably do not have internet access?
|
||||
return false, result, msg
|
||||
end
|
||||
|
||||
local newest = verTable and verTable[#verTable]
|
||||
result.newestVersion = newest and newest.version or unknownVersion
|
||||
result.newestReleaseTimestamp = newest and newest.timestamp
|
||||
|
||||
-- look up timestamp of current version
|
||||
local cEnt = M.findVersion(result.currentVersion, verTable)
|
||||
if cEnt then
|
||||
result.currentReleaseTimestamp = cEnt.timestamp
|
||||
else
|
||||
D("warning: could not find current wifibox version in release index, beta setting disabled after having beta installed?")
|
||||
end
|
||||
|
||||
if result.stateCode == M.STATE.DOWNLOADING then
|
||||
result.progress = fileSize(cachePath .. '/' .. newest.sysupgradeFilename)
|
||||
if not result.progress then result.progress = 0 end -- in case the file does not exist yet (which yields nil)
|
||||
result.imageSize = newest.sysupgradeFileSize
|
||||
end
|
||||
|
||||
return true, result
|
||||
end
|
||||
|
||||
--- Turns a plain-text version as returned by @{formatVersion} into a table.
|
||||
-- @tparam string|table versionText The version string to parse, if it is already a table, it is returned as-is.
|
||||
-- @treturn table A parsed version or nil on incorrect argument.
|
||||
function M.parseVersion(versionText)
|
||||
if not versionText then return nil end
|
||||
if type(versionText) == 'table' then return versionText end
|
||||
if not versionText or versionText:len() == 0 then return nil end
|
||||
|
||||
local major,minor,patch,suffix = versionText:match("^%s*(%d+)%.(%d+)%.(%d+)(-?%w*)%s*$")
|
||||
if not major or not minor or not patch then return nil end -- suffix not required
|
||||
|
||||
if type(suffix) == 'string' and suffix:len() > 0 then
|
||||
if suffix:sub(1, 1) ~= '-' then return nil end
|
||||
suffix = suffix:sub(2)
|
||||
else
|
||||
suffix = nil
|
||||
end
|
||||
|
||||
return { ['major'] = major, ['minor'] = minor, ['patch'] = patch, ['suffix'] = suffix }
|
||||
end
|
||||
|
||||
--- Formats a version as returned by @{parseVersion}.
|
||||
-- @tparam table|string version The version to format, if it is already a string, that will be returned unmodified.
|
||||
-- @treturn string A formatted version or nil on incorrect argument.
|
||||
function M.formatVersion(version)
|
||||
if not version then return nil end
|
||||
if type(version) == 'string' then return version end
|
||||
|
||||
local ver = version.major .. "." .. version.minor .. "." .. version.patch
|
||||
if version.suffix then ver = ver .. '-' .. version.suffix end
|
||||
|
||||
return ver
|
||||
end
|
||||
|
||||
--- Compares two versions. Note that the second return value must be used for equality testing.
|
||||
-- If given, the timestamps have higher priority than the versions. Suffixes are ignored.
|
||||
-- @tparam table versionA A version as returned by @{parseVersion}.
|
||||
-- @tparam table versionB A version as returned by @{parseVersion}.
|
||||
-- @param timestampA[opt] A timestamp as returned by @{parseDate}.
|
||||
-- @param timestampB[opt] A timestamp as returned by @{parseDate}.
|
||||
-- @treturn number -1 if versionA/timestampA is smaller/older than versionB/timestampB, 0 if versions are equal (or undecided) or 1 if A is larger/newer than B.
|
||||
-- @treturn bool True if versions are really equal (first return value can be 0 if everything but the suffix is equal)
|
||||
function M.compareVersions(versionA, versionB, timestampA, timestampB)
|
||||
if type(versionA) ~= 'table' or type(versionB) ~= 'table' then return nil end
|
||||
|
||||
local diff = 0
|
||||
if timestampA and timestampB then diff = timestampA - timestampB end
|
||||
if diff == 0 then
|
||||
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
|
||||
end
|
||||
|
||||
local result = diff > 0 and 1 or (diff < 0 and -1 or 0)
|
||||
local reallyEqual = (diff == 0) and (versionA.suffix == versionB.suffix)
|
||||
|
||||
return result, (reallyEqual and true or false)
|
||||
end
|
||||
|
||||
--- Checks if versions are exactly equal.
|
||||
-- It returns the second return value of @{compareVersions} and accepts the same arguments.
|
||||
-- @treturn bool True if versions are equal, false otherwise.
|
||||
function M.versionsEqual(versionA, versionB, timestampA, timestampB)
|
||||
return select(2, M.compareVersions(versionA, versionB, timestampA, timestampB))
|
||||
end
|
||||
|
||||
--- Returns information on a version if it can be found in a collection of versions as returned by @{getAvailableVersions}.
|
||||
-- @tparam table version The version to look for.
|
||||
-- @tparam table[opt] verTable A table containing a collection of versions, if not passed in, it will be obtained using @{getAvailableVersions}.
|
||||
-- @param timestamp[opt] Specific timestamp to look for.
|
||||
-- @treturn table|nil Version information table found in the collection, or nil on error or if not found.
|
||||
-- @treturn string Descriptive message in case of error or if the version could not be found.
|
||||
function M.findVersion(version, verTable, timestamp)
|
||||
local msg = nil
|
||||
version = M.parseVersion(version)
|
||||
if not verTable then verTable,msg = M.getAvailableVersions() end
|
||||
|
||||
if not verTable then return nil,msg end
|
||||
|
||||
for _,ent in pairs(verTable) do
|
||||
if M.versionsEqual(ent.version, version, ent.timestamp, timestamp) == true then return ent end
|
||||
end
|
||||
return nil,"no such version"
|
||||
end
|
||||
|
||||
--- Turns a date of the format 'yyyymmdd' into a timestamp as returned by os.time.
|
||||
-- @tparam string dateText The date to parse.
|
||||
-- @return A timestamp or nil if the argument does not have correct format.
|
||||
function M.parseDate(dateText)
|
||||
if type(dateText) ~= 'string' or dateText:len() ~= 8 or dateText:find('[^%d]') ~= nil then return nil end
|
||||
|
||||
return os.time({ year = dateText:sub(1, 4), month = dateText:sub(5, 6), day = dateText:sub(7,8) })
|
||||
end
|
||||
|
||||
--- Formats a timestamp as returned by os.time to a date of the form 'yyyymmdd'.
|
||||
-- @param timestamp The timestamp to format.
|
||||
-- @return A formatted date or nil if the argument is nil.
|
||||
function M.formatDate(timestamp)
|
||||
if not timestamp then return nil end
|
||||
return os.date('%Y%m%d', timestamp)
|
||||
end
|
||||
|
||||
--- Creates an image file name based on given properties.
|
||||
-- The generated name has the following form: `doodle3d-wifibox-<version>-<deviceType>-<'factory'|'sysupgrade'>.bin`.
|
||||
-- @tparam table|string version The version of the image.
|
||||
-- @string[opt] devType Openwrt device identifier (defaults to 'tl-mr3020').
|
||||
-- @bool[opt] isFactory Switches between factory or sysupgrade image name.
|
||||
-- @treturn string The constructed file name.
|
||||
function M.constructImageFilename(version, devType, isFactory)
|
||||
local sf = isFactory and 'factory' or 'sysupgrade'
|
||||
local v = M.formatVersion(version)
|
||||
local dt = devType and devType or 'tl-mr3020'
|
||||
return 'doodle3d-wifibox-' .. M.formatVersion(v) .. '-' .. dt .. '-' .. sf .. '.bin'
|
||||
end
|
||||
|
||||
--- Checks whether a valid image file is present in @{cachePath} for the given image properties.
|
||||
-- The versionEntry table will be augmented with an `isValid` key.
|
||||
--
|
||||
-- NOTE: currently, this function only checks the image exists and has the correct size.
|
||||
-- Sysupgrade will perform integrity checks, so this is not a major issue.
|
||||
--
|
||||
-- @tparam table versionEntry A version information table.
|
||||
-- @string[opt] devType Image device type, see @{constructImageFilename}.
|
||||
-- @bool[opt] isFactory Image type, see @{constructImageFilename}.
|
||||
-- @treturn bool True if a valid image is present, false otherwise.
|
||||
function M.checkValidImage(versionEntry, devType, isFactory)
|
||||
local filename = M.constructImageFilename(versionEntry.version, devType, isFactory)
|
||||
--return versionEntry.md5 == md5sum(cachePath .. '/' .. filename)
|
||||
local size = fileSize(cachePath .. '/' .. filename)
|
||||
versionEntry.isValid = versionEntry.sysupgradeFileSize == size
|
||||
return versionEntry.isValid
|
||||
end
|
||||
|
||||
--- Returns the current wifibox version text, extracted from `/etc/wifibox-version`.
|
||||
-- @treturn string Current version as plain-text.
|
||||
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 the current wifibox version as a table with major, minor and patch as keys.
|
||||
-- @treturn table Current version as version table.
|
||||
function M.getCurrentVersion()
|
||||
local vt,msg = M.getCurrentVersionText()
|
||||
return vt and M.parseVersion(vt) or nil,msg
|
||||
end
|
||||
|
||||
--- Returns an indexed and sorted table containing version information tables.
|
||||
-- The information is obtained from the either cached or downloaded image index file.
|
||||
local function fetchIndexTable(indexFile, cachePath)
|
||||
if not baseUrl then baseUrl = M.DEFAULT_BASE_URL end
|
||||
local indexFilename = cachePath .. '/' .. indexFile
|
||||
|
||||
if not useCache or not exists(indexFilename) then
|
||||
local rv1,rv2 = downloadFile(baseUrl .. '/images/' .. indexFile, cachePath, indexFile)
|
||||
if not rv1 then return nil,"could not download image index file (" .. wgetStatusToString(rv2) .. ")" end
|
||||
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)
|
||||
--if not log then D("#" .. lineno .. ": considering '" .. line .. "' (" .. (k or '<nil>') .. " / " .. (v or '<nil>') .. ")") end
|
||||
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)
|
||||
if sSize then entry.sysupgradeFileSize = tonumber(sSize) end
|
||||
if fSize then entry.factoryFileSize = tonumber(fSize) end
|
||||
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
|
||||
elseif k == 'ReleaseDate' then
|
||||
local ts = M.parseDate(v)
|
||||
if not ts then
|
||||
P(0, "ignoring incorrectly formatted ReleaseDate field (line " .. lineno .. ")")
|
||||
else
|
||||
entry.timestamp = ts
|
||||
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
|
||||
|
||||
--- Returns an indexed and sorted table containing version information tables.
|
||||
-- The information is obtained from the either cached or downloaded image index (@{IMAGE_STABLE_INDEX_FILE}).
|
||||
-- @tparam which[opt] Which type of versions to fetch, either 'stables' (default), 'betas' or both.
|
||||
-- @treturn table A table with a collection of version information tables.
|
||||
function M.getAvailableVersions(which)
|
||||
local ccRv,ccMsg = createCacheDirectory()
|
||||
if not ccRv then return nil,ccMsg end
|
||||
|
||||
local verTable, msg = {}, nil
|
||||
|
||||
if which == 'stables' or which == 'both' then
|
||||
verTable,msg = fetchIndexTable(M.IMAGE_STABLE_INDEX_FILE, cachePath)
|
||||
if not verTable then return nil,msg end
|
||||
end
|
||||
|
||||
if which == 'betas' or which == 'both' then
|
||||
local betas,msg = fetchIndexTable(M.IMAGE_BETA_INDEX_FILE, cachePath)
|
||||
if not betas then return nil,msg end
|
||||
|
||||
for k,v in pairs(betas) do verTable[k] = v end
|
||||
end
|
||||
|
||||
return verTable
|
||||
end
|
||||
|
||||
--- Attempts to download an image file with the requested properties.
|
||||
-- @tparam table versionEntry A version information table.
|
||||
-- @string[opt] devType Image device type, see @{constructImageFilename}.
|
||||
-- @bool[opt] isFactory Image type, see @{constructImageFilename}.
|
||||
-- @treturn bool|nil True if successful, or nil on error.
|
||||
-- @treturn ?string|number (optional) Descriptive message on general error, or wget exit status.
|
||||
function M.downloadImageFile(versionEntry, devType, isFactory)
|
||||
if not baseUrl then baseUrl = M.DEFAULT_BASE_URL end
|
||||
local filename = M.constructImageFilename(versionEntry.version, devType, isFactory)
|
||||
local doDownload = not useCache
|
||||
|
||||
local ccRv,ccMsg = createCacheDirectory()
|
||||
if not ccRv then return nil,ccMsg end
|
||||
|
||||
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
|
||||
|
||||
local rv = 0
|
||||
if doDownload then
|
||||
M.setState(M.STATE.DOWNLOADING, "Downloading image (" .. filename .. ")")
|
||||
rv = downloadFile(baseUrl .. '/images/' .. filename, cachePath, filename)
|
||||
end
|
||||
|
||||
if rv == 0 then
|
||||
if M.checkValidImage(versionEntry, devType, isFactory) then
|
||||
M.setState(M.STATE.IMAGE_READY, "Image downloaded, ready to install (image name: " .. filename .. ")")
|
||||
return true
|
||||
else
|
||||
removeFile(cachePath .. '/' .. filename)
|
||||
local ws = "Image download failed (invalid image)"
|
||||
M.setState(M.STATE.DOWNLOAD_FAILED, ws)
|
||||
return nil,ws
|
||||
end
|
||||
else
|
||||
local ws = wgetStatusToString(rv)
|
||||
removeFile(cachePath .. '/' .. filename)
|
||||
M.setState(M.STATE.DOWNLOAD_FAILED, "Image download failed (wget error: " .. ws .. ")")
|
||||
return nil,ws
|
||||
end
|
||||
end
|
||||
|
||||
--- Issues a [sysupgrade](http://wiki.openwrt.org/doc/howto/generic.sysupgrade) command with a wifibox image file.
|
||||
--
|
||||
-- This function will not return if it does its job successfully, the device will flash and reboot instead.
|
||||
-- @tparam table versionEntry A version information table.
|
||||
-- @bool[opt] noRetain If true, do not keep files in overlay filesystem (i.e., the '-n' switch in sysupgrade).
|
||||
-- @string[opt] devType Image device type, see @{constructImageFilename}.
|
||||
-- @bool[opt] isFactory Image type, see @{constructImageFilename}.
|
||||
-- @treturn bool|nil True on success (with the 'exception' as noted above) or nil on error.
|
||||
-- @treturn ?string|number (optional) Descriptive message or sysupgrade exit status on error.
|
||||
function M.flashImageVersion(versionEntry, noRetain, devType, isFactory)
|
||||
log:info("flashImageVersion")
|
||||
local imgName = M.constructImageFilename(versionEntry.version, devType, isFactory)
|
||||
local cmd = noRetain and 'sysupgrade -n ' or 'sysupgrade '
|
||||
cmd = cmd .. cachePath .. '/' .. imgName
|
||||
|
||||
local ccRv,ccMsg = createCacheDirectory()
|
||||
if not ccRv then return nil,ccMsg end
|
||||
|
||||
if not M.checkValidImage(versionEntry) then
|
||||
return nil,"no valid image for requested version present"
|
||||
end
|
||||
|
||||
M.setState(M.STATE.INSTALLING, "Installing new image (" .. imgName .. ")") -- yes this is rather pointless
|
||||
local rv = runCommand(cmd) -- if everything goes to plan, this will not return
|
||||
|
||||
if rv == 0 then
|
||||
M.setState(M.STATE.INSTALLED, "Image installed")
|
||||
else
|
||||
-- NOTE: if cmdrv == 127, this means the command was not found
|
||||
local cmdrv,sysrv = splitExitStatus(rv)
|
||||
M.setState(M.STATE.INSTALL_FAILED, "Image installation failed (sysupgrade returned " .. cmdrv .. ", execution status: " .. sysrv .. ")")
|
||||
end
|
||||
|
||||
return (rv == 0) and true or nil,rv
|
||||
end
|
||||
|
||||
--- Clears '*.bin' in the @{cachePath} directory.
|
||||
-- @treturn bool|nil True on success, or nil on error.
|
||||
-- @treturn ?string Descriptive message on error.
|
||||
function M.clear()
|
||||
local ccRv,ccMsg = createCacheDirectory()
|
||||
if not ccRv then return nil,ccMsg end
|
||||
|
||||
D("Removing " .. cachePath .. "/doodle3d-wifibox-*.bin")
|
||||
M.setState(M.STATE.NONE, "")
|
||||
local rv = M.compatexecute('rm -f ' .. cachePath .. '/doodle3d-wifibox-*.bin')
|
||||
return (rv == 0) and true or nil,"could not remove image files"
|
||||
end
|
||||
|
||||
--- Set updater state.
|
||||
--
|
||||
-- NOTE: make sure the cache directory @{cachePath} exists before calling this function or it will fail.
|
||||
--
|
||||
-- NOTE: this function _can_ fail but this is not expected to happen so the return value is mostly ignored for now.
|
||||
--
|
||||
-- @number code The @{STATE} code to set.
|
||||
-- @string msg The accompanying state message to set.
|
||||
-- @treturn bool True on success or false if the state file could not be opened for writing.
|
||||
function M.setState(code, msg)
|
||||
local s = code .. '|' .. msg
|
||||
D("set update state: " .. M.STATE_NAMES[code] .. " ('" .. s .. "')")
|
||||
local file,msg = io.open(cachePath .. '/' .. M.STATE_FILE, 'w')
|
||||
|
||||
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
|
||||
|
||||
|
||||
|
||||
----------
|
||||
-- MAIN --
|
||||
----------
|
||||
|
||||
--- 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.
|
||||
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
|
||||
|
||||
verbosity = argTable.verbosity
|
||||
if argTable.useCache ~= nil then useCache = argTable.useCache end
|
||||
|
||||
P(0, "Doodle3D Wifibox firmware updater")
|
||||
local cacheCreated,msg = createCacheDirectory()
|
||||
if not cacheCreated then
|
||||
E(msg)
|
||||
os.exit(1)
|
||||
end
|
||||
|
||||
if argTable.action == 'showHelp' then
|
||||
P(1, "\t-h\t\tShow this help message")
|
||||
P(1, "\t-q\t\tquiet mode")
|
||||
P(1, "\t-V\t\tverbose mode")
|
||||
P(1, "\t-c\t\tUse cache as much as possible")
|
||||
P(1, "\t-C\t\tDo not use the cache")
|
||||
P(1, "\t-u <base_url>\tUse specified base URL (default: " .. M.DEFAULT_BASE_URL .. ")")
|
||||
P(1, "\t-v\t\tShow current image version")
|
||||
P(1, "\t-s\t\tShow current update status")
|
||||
P(1, "\t-l\t\tShow list of available image versions (and which one has been downloaded, if any)")
|
||||
P(1, "\t-i <version>\tShow information (changelog) about the requested image version")
|
||||
P(1, "\t-d <version>\tDownload requested image version")
|
||||
P(1, "\t-f <version>\tFlash to requested image version (by means of sysupgrade)")
|
||||
P(1, "\t-r\t\tClear downloaded images and reset state")
|
||||
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))
|
||||
|
||||
elseif argTable.action == 'showStatus' then
|
||||
local status = M.getStatus()
|
||||
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
|
||||
|
||||
elseif argTable.action == 'showAvailableVersions' then
|
||||
local verTable,msg = M.getAvailableVersions()
|
||||
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
|
||||
local vEnt,msg = M.findVersion(argTable.version)
|
||||
|
||||
if vEnt then
|
||||
P(0, "Information on version:")
|
||||
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
|
||||
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)
|
||||
end
|
||||
|
||||
elseif argTable.action == 'imageDownload' then
|
||||
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)
|
||||
if not rv then E("could not download file (" .. msg .. ")")
|
||||
else P(1, "success")
|
||||
end
|
||||
|
||||
elseif argTable.action == 'clear' then
|
||||
local rv,msg = M.clear()
|
||||
if not rv then P(1, "error (" .. msg .. ")")
|
||||
else P(1, "success")
|
||||
end
|
||||
|
||||
elseif argTable.action == 'imageInstall' then
|
||||
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 .. ")")
|
||||
os.exit(3)
|
||||
|
||||
else
|
||||
P(0, "usage: d3d-updater [-hqVcCvslr] [-u base_url] [-i version] [-d version] [-f version]")
|
||||
end
|
||||
|
||||
os.exit(0)
|
||||
end
|
||||
|
||||
--- Only execute the main function if an arg table is present, this enables usage both as module and as standalone script.
|
||||
if arg ~= nil then main() end
|
||||
|
||||
return M
|
Loading…
Reference in New Issue
Block a user