0
0
mirror of https://github.com/Doodle3D/doodle3d-firmware.git synced 2024-06-28 20:31:21 +02:00

Add preliminary REST api for updating (WIP).

This commit is contained in:
Wouter R 2013-10-18 21:46:41 +02:00
parent b5d980c52e
commit e1dc7f47ed
4 changed files with 219 additions and 47 deletions

View File

@ -30,15 +30,6 @@ function M._global(request, response)
response:setSuccess() response:setSuccess()
end end
function M.firmware(request, response)
--response:setSuccess()
-- can return (essentially all wraps ipkg output):
-- available (list)
-- current
-- latest
-- upgradable
end
-- TODO: redirect stdout+stderr; handle errors -- TODO: redirect stdout+stderr; handle errors
function M.logfiles(request, response) function M.logfiles(request, response)
local rv,msg = lfs.mkdir(LOG_COLLECT_DIR) local rv,msg = lfs.mkdir(LOG_COLLECT_DIR)
@ -115,16 +106,16 @@ function M.access(request, response)
response:setSuccess() response:setSuccess()
response:addData('has_control', hasControl) response:addData('has_control', hasControl)
return true return true
end end
function M.status(request, response) function M.status(request, response)
local rv local rv
rv, state = printerAPI.state(request, response) rv, state = printerAPI.state(request, response)
if(rv == false) then return end if(rv == false) then return end
if(state ~= "disconnected") then if(state ~= "disconnected") then
rv = printerAPI.temperature(request, response) rv = printerAPI.temperature(request, response)
if(rv == false) then return end if(rv == false) then return end

103
src/rest/api/api_update.lua Normal file
View File

@ -0,0 +1,103 @@
-- NOTE: the module 'detects' command-line invocation by existence of 'arg', so we have to make sure it is not defined.
argStash = arg
arg = nil
local updater = require('script.d3d-updater')
arg = argStash
local log = require('util.logger')
local utils = require('util.utils')
local M = {
isApi = true
}
function M.status(request, response)
updater.setLogger(log)
local status,msg = updater.getStatus(nil, false)
if not status then
response:setFail(msg)
return
end
local canUpdate = updater.compareVersions(status.newestVersion, status.currentVersion) > 0
response:addData('current_version', updater.formatVersion(status.currentVersion))
response:addData('newest_version', updater.formatVersion(status.newestVersion))
--response:addData('current_version', status.currentVersion)
--response:addData('newest_version', status.newestVersion)
response:addData('can_update', canUpdate)
response:addData('state_code', status.stateCode)
response:addData('state_text', status.stateText)
if status.progress then response:addData('progress', status.progress) end
if status.imageSize then response:addData('image_size', status.imageSize) end
response:setSuccess()
end
-- requires: version(string) (major.minor.patch)
-- accepts: clear_gcode(bool, defaults to true) (this is to lower the chance on out-of-memory crashes, but still allows overriding this behaviour)
-- accepts: clear_images(bool, defaults to true) (same rationale as with clear_gcode)
-- note: call this with a long timeout - downloading may take a while (e.g. ~3.3MB with slow internet...)
function M.download_POST(request, response)
local argVersion = request:get("version")
local argClearGcode = utils.toboolean(request:get("clear_gcode"))
local argClearImages = utils.toboolean(request:get("clear_images"))
if argClearGcode == nil then argClearGcode = true end
if argClearImages == nil then argClearImages = true end
if not argVersion then
response:setError("missing version argument")
return
end
updater.setLogger(log)
local rv,msg
if argClearImages then
rv,msg = updater.clear()
if not rv then
response:setFail(msg)
return
end
end
if argClearGcode then
-- TODO
--[[ from api_printer.lua:
log:debug("clearing all gcode for " .. printer:getId())
response:addData('gcode_clear',true)
local rv,msg = printer:clearGcode()
if not rv then
response:setError(msg)
return
end
]]--
end
rv,msg = updater.downloadImageFile(nil, argVersion)
if not rv then
response:setFail(msg)
return
end
response:setSuccess()
end
-- if successful, this call won't return since the device will flash its memory and reboot
function M.install_POST(request, response)
updater.setLogger(log)
-- install
-- cross fingers
response:setSuccess()
end
function M.clear_POST(request, response)
updater.setLogger(log)
local rv,msg = updater.clear()
if rv then response:setSuccess()
else response:setFail(msg)
end
end
return M

View File

@ -70,6 +70,7 @@ local function resolveApiModule(modname)
local reqModName = 'rest.api.api_' .. modname local reqModName = 'rest.api.api_' .. modname
local ok, modObj local ok, modObj
-- NOTE: with some errors, execution just seems to stop in require() (nothing is logged anymore, not even errors)
if confDefaults.DEBUG_PCALLS then ok, modObj = true, require(reqModName) if confDefaults.DEBUG_PCALLS then ok, modObj = true, require(reqModName)
else ok, modObj = pcall(require, reqModName) else ok, modObj = pcall(require, reqModName)
end end
@ -192,7 +193,6 @@ function M.new(environment, postData, debugEnabled)
end end
table.remove(self.pathArgs, 1) --drop the first 'empty' field caused by the opening slash of the query string table.remove(self.pathArgs, 1) --drop the first 'empty' field caused by the opening slash of the query string
if #self.pathArgs >= 1 then self.requestedApiModule = self.pathArgs[1] end if #self.pathArgs >= 1 then self.requestedApiModule = self.pathArgs[1] end
if #self.pathArgs >= 2 then self.requestedApiFunction = self.pathArgs[2] end if #self.pathArgs >= 2 then self.requestedApiFunction = self.pathArgs[2] end

View File

@ -1,6 +1,8 @@
#!/usr/bin/env lua #!/usr/bin/env lua
-- TODO/NOTES: -- TODO/NOTES:
-- add function wgetErrorToString and use it everywhere to get clearer error feedback
-- make P/E/D choose between print and log, so it doesn't clutter functions anymore (esp. since accidentally printing while in cgi mode breaks cgi)
-- M.checkValidImage(verEnt) -> doet exists+fileSize/MD5 check -- M.checkValidImage(verEnt) -> doet exists+fileSize/MD5 check
-- after download: (can use checkValidImage for this) -- after download: (can use checkValidImage for this)
-- - remove file on fail -- - remove file on fail
@ -8,7 +10,6 @@
-- add to status: validImage: none|<version> (can use checkValidImage for this) -- add to status: validImage: none|<version> (can use checkValidImage for this)
-- any more TODO's across this file? -- any more TODO's across this file?
-- max 1 image tegelijk (moet api doen), en rekening houden met printbuffer (printen blokkeren?) -- max 1 image tegelijk (moet api doen), en rekening houden met printbuffer (printen blokkeren?)
-- API calls to add: update/status, update/download, update/install, update/clear
-- MAYBE/LATER: -- MAYBE/LATER:
-- wget: add provision (in verbose mode?) to use -v instead of -q and disable output redirection -- wget: add provision (in verbose mode?) to use -v instead of -q and disable output redirection
@ -22,9 +23,9 @@
local M = {} local M = {}
-- NOTE: 'INSTALLED' will never be returned (and probably neither will 'INSTALLING') since in that case the device is flashing or rebooting -- NOTE: 'INSTALLED' will never be returned (and probably neither will 'INSTALLING') since in that case the device is flashing or rebooting
M.STATE = { NONE = 1, DOWNLOADING = 2, IMAGE_READY = 3, INSTALLING = 4, INSTALLED = 5, INSTALL_FAILED = 6 } M.STATE = { NONE = 1, DOWNLOADING = 2, DOWNLOAD_FAILED = 3, IMAGE_READY = 4, INSTALLING = 5, INSTALLED = 6, INSTALL_FAILED = 7 }
M.STATE_NAMES = { M.STATE_NAMES = {
[M.STATE.NONE] = 'none', [M.STATE.DOWNLOADING] = 'downloading', [M.STATE.IMAGE_READY] = 'image_ready', [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' [M.STATE.INSTALLING] = 'installing', [M.STATE.INSTALLED] = 'installed', [M.STATE.INSTALL_FAILED] = 'install_failed'
} }
@ -47,9 +48,31 @@ local log = nil -- wifibox API can use M.setLogger to enable this module to use
--------------------- ---------------------
-- use level==1 for important messages, 0 for regular messages and -1 for less important messages -- use level==1 for important messages, 0 for regular messages and -1 for less important messages
local function P(lvl, msg) if (-lvl <= M.verbosity) then print(msg) end end local function P(lvl, msg)
local function E(msg) io.stderr:write(msg .. '\n') end if log then
local function D(msg) P(-1, "(DBG) " .. msg) end 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
local function D(msg) P(-1, (log and msg or "(DBG) " .. msg)) end
local function E(msg)
if log then log:error(msg)
else io.stderr:write(msg .. '\n')
end
end
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
local function getState() local function getState()
local file,msg = io.open(M.CACHE_PATH .. '/' .. M.STATE_FILE, 'r') local file,msg = io.open(M.CACHE_PATH .. '/' .. M.STATE_FILE, 'r')
@ -61,12 +84,21 @@ local function getState()
return code,msg return code,msg
end end
-- 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
local function setState(code, msg) local function setState(code, msg)
local s = code .. '|' .. msg local s = code .. '|' .. msg
if log then log:info("update state: " .. s) else D("update state: " .. s) end D("set update state: " .. M.STATE_NAMES[code] .. " ('" .. s .. "')")
local file = io.open(M.CACHE_PATH .. '/' .. M.STATE_FILE, 'w') local file,msg = io.open(M.CACHE_PATH .. '/' .. 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:write(s)
file:close() file:close()
return true
end end
-- trim whitespace from both ends of string (from http://snippets.luacode.org/?p=snippets/trim_whitespace_from_string_76) -- trim whitespace from both ends of string (from http://snippets.luacode.org/?p=snippets/trim_whitespace_from_string_76)
@ -124,7 +156,8 @@ end
-- returns return value of command -- returns return value of command
local function runCommand(command, dryRun) D("about to run: '" .. command .. "'"); return (not dryRun) and os.execute(command) or 0 end local function runCommand(command, dryRun) D("about to run: '" .. command .. "'"); return (not dryRun) and os.execute(command) or 0 end
-- returns return value of wget (or nil if saveDir is nil or empty) -- returns return value of wget (or nil if saveDir is nil or empty), filename is optional
-- NOTE: leaving out filename will cause issues with files not being overwritten but suffixed with '.1', '.2',etc instead
local function downloadFile(url, saveDir, filename) local function downloadFile(url, saveDir, filename)
if not saveDir or saveDir:len() == 0 then return nil, "saveDir must be non-empty" end 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 '' local outArg = (filename:len() > 0) and (' -O' .. filename) or ''
@ -186,10 +219,14 @@ function M.setLogger(logger)
log = logger log = logger
end end
-- baseUrl and useCache are optional
function M.getStatus(baseUrl, useCache) function M.getStatus(baseUrl, useCache)
if not baseUrl then baseUrl = M.DEFAULT_BASE_URL end
local result = {} local result = {}
local verTable = M.getAvailableVersions(baseUrl, useCache) local verTable,msg = M.getAvailableVersions(baseUrl, useCache)
if not verTable then return nil,msg end
local newest = verTable[#verTable] local newest = verTable[#verTable]
result.currentVersion = M.getCurrentVersion() result.currentVersion = M.getCurrentVersion()
result.newestVersion = newest.version result.newestVersion = newest.version
@ -198,20 +235,33 @@ function M.getStatus(baseUrl, useCache)
if result.stateCode == M.STATE.DOWNLOADING then if result.stateCode == M.STATE.DOWNLOADING then
result.progress = fileSize(M.CACHE_PATH .. '/' .. newest.sysupgradeFilename) result.progress = fileSize(M.CACHE_PATH .. '/' .. 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 result.imageSize = newest.sysupgradeFileSize
end end
return result return result
end end
-- 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.
function M.parseVersion(versionText) function M.parseVersion(versionText)
if type(versionText) == 'table' then return versionText end
if not versionText or versionText:len() == 0 then return nil end if not versionText or versionText:len() == 0 then return nil end
local major,minor,patch = versionText:match("^%s*(%d+)%.(%d+)%.(%d+)%s*$") local major,minor,patch = versionText:match("^%s*(%d+)%.(%d+)%.(%d+)%s*$")
if not major or not minor or not patch then return nil end if not major or not minor or not patch then return nil end
return { ['major'] = major, ['minor'] = minor, ['patch'] = patch } return { ['major'] = major, ['minor'] = minor, ['patch'] = patch }
end end
function M.formatVersion(version) return version.major .. "." .. version.minor .. "." .. version.patch end -- 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
-- expects two tables as created by M.parseVersion() -- expects two tables as created by M.parseVersion()
function M.compareVersions(versionA, versionB) function M.compareVersions(versionA, versionB)
@ -230,9 +280,9 @@ function M.findVersion(verTable, version)
end end
-- version may be a table or a string, devtype and isFactory are optional -- version may be a table or a string, devtype and isFactory are optional
function M.constructImageFilename(ver, devType, isFactory) function M.constructImageFilename(version, devType, isFactory)
local sf = isFactory and 'factory' or 'sysupgrade' local sf = isFactory and 'factory' or 'sysupgrade'
local v = (type(ver) == 'table') and ver or M.formatVersion(ver) local v = M.formatVersion(version)
local dt = devType and devType or 'tl-mr3020' local dt = devType and devType or 'tl-mr3020'
return 'doodle3d-wifibox-' .. M.formatVersion(v) .. '-' .. dt .. '-' .. sf .. '.bin' return 'doodle3d-wifibox-' .. M.formatVersion(v) .. '-' .. dt .. '-' .. sf .. '.bin'
end end
@ -250,9 +300,14 @@ function M.getCurrentVersion()
end end
-- requires url of image index file; returns an indexed (and sorted) table containing version tables -- requires url of image index file; returns an indexed (and sorted) table containing version tables
function M.getAvailableVersions(baseUrl, useCache, version) -- baseUrl and useCache are optional
function M.getAvailableVersions(baseUrl, useCache)
if not baseUrl then baseUrl = M.DEFAULT_BASE_URL end
local indexFilename = M.CACHE_PATH .. '/' .. M.IMAGE_INDEX_FILE local indexFilename = M.CACHE_PATH .. '/' .. M.IMAGE_INDEX_FILE
local ccRv,ccMsg = createCacheDirectory()
if not ccRv then return nil,ccMsg end
if not useCache or not exists(indexFilename) then if not useCache or not exists(indexFilename) then
local rv = downloadFile(baseUrl .. '/images/' .. M.IMAGE_INDEX_FILE, M.CACHE_PATH, M.IMAGE_INDEX_FILE) 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" end if rv ~= 0 then return nil,"could not download image index file" end
@ -267,7 +322,7 @@ function M.getAvailableVersions(baseUrl, useCache, version)
for line in idxLines do for line in idxLines do
local k,v = line:match('^(.-):(.*)$') local k,v = line:match('^(.-):(.*)$')
k,v = trim(k), trim(v) k,v = trim(k), trim(v)
--P(1, "#" .. lineno .. ": considering '" .. line .. "' (" .. (k or '<nil>') .. " / " .. (v or '<nil>') .. ")") -- debug D(1, "#" .. lineno .. ": considering '" .. line .. "' (" .. (k or '<nil>') .. " / " .. (v or '<nil>') .. ")") -- debug
if not changelogMode and (not k or not v) then return nil,"incorrectly formatted line in index file (line " .. lineno .. ")" 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 if k == 'ChangelogEnd' then
@ -308,7 +363,6 @@ function M.getAvailableVersions(baseUrl, useCache, version)
if entry ~= nil then table.insert(result, entry) end if entry ~= nil then table.insert(result, entry) end
--sort table
table.sort(result, function(a,b) table.sort(result, function(a,b)
return M.compareVersions(a.version,b.version) < 0 return M.compareVersions(a.version,b.version) < 0
end) end)
@ -316,27 +370,45 @@ function M.getAvailableVersions(baseUrl, useCache, version)
return result return result
end end
-- devtype and isFactory are optional; returns a table with major, minor and patch as keys -- forceDownload, devtype and isFactory are optional
function M.downloadImageFile(baseUrl, ver, forceDownload, devType, isFactory) -- returns true or nil+msg or nil + return value from wget
local filename = M.constructImageFilename(ver, devType, isFactory) function M.downloadImageFile(baseUrl, version, forceDownload, devType, isFactory)
if not baseUrl then baseUrl = M.DEFAULT_BASE_URL end
local filename = M.constructImageFilename(version, devType, isFactory)
local doDownload = (type(forceDownload) == 'boolean') and forceDownload or (not exists(M.CACHE_PATH .. '/' .. filename)) local doDownload = (type(forceDownload) == 'boolean') and forceDownload or (not exists(M.CACHE_PATH .. '/' .. filename))
local ccRv,ccMsg = createCacheDirectory()
if not ccRv then return nil,ccMsg end
--TODO: call M.checkValidImage, set doDownload to true if not valid --TODO: call M.checkValidImage, set doDownload to true if not valid
local rv = 0 local rv = 0
if doDownload then if doDownload then
setState(M.STATE.DOWNLOADING, "Downloading image (" .. filename .. ")") setState(M.STATE.DOWNLOADING, "Downloading image (" .. filename .. ")")
rv = downloadFile(baseUrl .. '/images/' .. filename, M.CACHE_PATH, filename) or 0 rv = downloadFile(baseUrl .. '/images/' .. filename, M.CACHE_PATH, filename)
end end
setState(M.STATE.IMAGE_READY, "Image downloaded, ready to install (image name: " .. filename .. ")")
return rv if rv == 0 then
--TODO: check if the downloaded file is complete and matches checksum
setState(M.STATE.IMAGE_READY, "Image downloaded, ready to install (image name: " .. filename .. ")")
else
setState(M.STATE.DOWNLOAD_FAILED, "Image download failed (wget return value: " .. rv .. ")")
end
return (rv == 0) and true or nil,rv
end end
-- this function will not return -- this function will not return
-- noRetain, devType and isFactory are optional
-- returns true or nil + wget return value
function M.flashImageVersion(version, noRetain, devType, isFactory) function M.flashImageVersion(version, noRetain, devType, isFactory)
local imgName = M.constructImageFilename(version, devType, isFactory) local imgName = M.constructImageFilename(version, devType, isFactory)
local cmd = noRetain and 'sysupgrade -n ' or 'sysupgrade ' local cmd = noRetain and 'sysupgrade -n ' or 'sysupgrade '
cmd = cmd .. M.CACHE_PATH .. '/' .. imgName cmd = cmd .. M.CACHE_PATH .. '/' .. imgName
local ccRv,ccMsg = createCacheDirectory()
if not ccRv then return nil,ccMsg end
setState(M.STATE, "Installing new image (" .. imgName .. ")") -- yes this is rather pointless setState(M.STATE, "Installing new image (" .. imgName .. ")") -- yes this is rather pointless
local rv = runCommand(cmd, true) -- if everything goes to plan, this will not return local rv = runCommand(cmd, true) -- if everything goes to plan, this will not return
@ -344,13 +416,18 @@ function M.flashImageVersion(version, noRetain, devType, isFactory)
else setState(M.STATE.INSTALL_FAILED, "Image installation failed (sysupgrade returned " .. rv .. ")") else setState(M.STATE.INSTALL_FAILED, "Image installation failed (sysupgrade returned " .. rv .. ")")
end end
return rv return (rv == 0) and true or nil,rv
end end
--returns true on success, or nil+msg otherwise
function M.clear() function M.clear()
P(0, "Removing " .. M.CACHE_PATH .. "/doodle3d-wifibox-*.bin") local ccRv,ccMsg = createCacheDirectory()
if not ccRv then return nil,ccMsg end
D(0, "Removing " .. M.CACHE_PATH .. "/doodle3d-wifibox-*.bin")
setState(M.STATE.NONE, "") setState(M.STATE.NONE, "")
return os.execute('rm -f ' .. M.CACHE_PATH .. '/doodle3d-wifibox-*.bin') local rv = os.execute('rm -f ' .. M.CACHE_PATH .. '/doodle3d-wifibox-*.bin')
return (rv == 0) and true or nil,"could not remove image files"
end end
@ -370,12 +447,13 @@ local function main()
os.exit(1) os.exit(1)
end end
M.verbosity = argTable.verbosity verbosity = argTable.verbosity
if argTable.useCache ~= nil then useCache = argTable.useCache end if argTable.useCache ~= nil then useCache = argTable.useCache end
P(0, "Doodle3D Wifibox firmware updater") P(0, "Doodle3D Wifibox firmware updater")
if os.execute('mkdir -p ' .. M.CACHE_PATH) ~= 0 then local cacheCreated,msg = createCacheDirectory()
E("Error: could not create cache directory '" .. M.CACHE_PATH .. "'") if not cacheCreated then
E(msg)
os.exit(1) os.exit(1)
end end
@ -459,20 +537,20 @@ local function main()
elseif argTable.action == 'imageDownload' then elseif argTable.action == 'imageDownload' then
--TODO: first check if version exists --TODO: first check if version exists
local rv,msg = M.downloadImageFile(argTable.baseUrl, argTable.version, not useCache) --TEMP local rv,msg = M.downloadImageFile(argTable.baseUrl, argTable.version, not useCache) --TEMP
if rv ~= 0 then E("could not download file (" .. rv .. ")") if not rv then E("could not download file (" .. msg .. ")")
else P(1, "success") else P(1, "success")
end end
elseif argTable.action == 'clear' then elseif argTable.action == 'clear' then
local rv = M.clear() local rv,msg = M.clear()
if rv ~= 0 then if not rv then P(1, "error (" .. msg .. ")")
P(1, "error (" .. rv .. ")") else P(1, "success")
else
P(1, "success")
end end
elseif argTable.action == 'imageInstall' then elseif argTable.action == 'imageInstall' then
local rv = M.flashImageVersion(argTable.version) local rv = M.flashImageVersion(argTable.version)
E("error: flash function returned, the device should have been flashed and rebooted instead") E("error: flash function returned, the device should have been flashed and rebooted instead")
os.exit(3) os.exit(3)
else else
P(0, "usage: d3d-updater [-hqVcCvslr] [-u base_url] [-i version] [-d version] [-f version]") P(0, "usage: d3d-updater [-hqVcCvslr] [-u base_url] [-i version] [-d version] [-f version]")
end end