diff --git a/src/rest/api/api_info.lua b/src/rest/api/api_info.lua index 318799c..8dc8753 100644 --- a/src/rest/api/api_info.lua +++ b/src/rest/api/api_info.lua @@ -30,15 +30,6 @@ function M._global(request, response) response:setSuccess() 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 function M.logfiles(request, response) local rv,msg = lfs.mkdir(LOG_COLLECT_DIR) @@ -115,16 +106,16 @@ function M.access(request, response) response:setSuccess() response:addData('has_control', hasControl) - + return true end function M.status(request, response) - + local rv rv, state = printerAPI.state(request, response) if(rv == false) then return end - + if(state ~= "disconnected") then rv = printerAPI.temperature(request, response) if(rv == false) then return end diff --git a/src/rest/api/api_update.lua b/src/rest/api/api_update.lua new file mode 100644 index 0000000..c67e87a --- /dev/null +++ b/src/rest/api/api_update.lua @@ -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 diff --git a/src/rest/request.lua b/src/rest/request.lua index 34edb94..54f5cce 100644 --- a/src/rest/request.lua +++ b/src/rest/request.lua @@ -70,6 +70,7 @@ local function resolveApiModule(modname) local reqModName = 'rest.api.api_' .. modname 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) else ok, modObj = pcall(require, reqModName) end @@ -192,7 +193,6 @@ function M.new(environment, postData, debugEnabled) end 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 >= 2 then self.requestedApiFunction = self.pathArgs[2] end diff --git a/src/script/d3d-updater.lua b/src/script/d3d-updater.lua index ac8442b..7897470 100755 --- a/src/script/d3d-updater.lua +++ b/src/script/d3d-updater.lua @@ -1,6 +1,8 @@ #!/usr/bin/env lua -- 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 -- after download: (can use checkValidImage for this) -- - remove file on fail @@ -8,7 +10,6 @@ -- add to status: validImage: none| (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?) --- API calls to add: update/status, update/download, update/install, update/clear -- MAYBE/LATER: -- wget: add provision (in verbose mode?) to use -v instead of -q and disable output redirection @@ -22,9 +23,9 @@ local M = {} -- 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.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' } @@ -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 -local function P(lvl, msg) if (-lvl <= M.verbosity) then print(msg) end end -local function E(msg) io.stderr:write(msg .. '\n') end -local function D(msg) P(-1, "(DBG) " .. msg) end +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 + +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 file,msg = io.open(M.CACHE_PATH .. '/' .. M.STATE_FILE, 'r') @@ -61,12 +84,21 @@ local function getState() return code,msg 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 s = code .. '|' .. msg - if log then log:info("update state: " .. s) else D("update state: " .. s) end - local file = io.open(M.CACHE_PATH .. '/' .. M.STATE_FILE, 'w') + D("set update state: " .. M.STATE_NAMES[code] .. " ('" .. s .. "')") + 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:close() + return true end -- 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 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) 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 '' @@ -186,10 +219,14 @@ function M.setLogger(logger) log = logger end +-- baseUrl and useCache are optional function M.getStatus(baseUrl, useCache) + if not baseUrl then baseUrl = M.DEFAULT_BASE_URL end 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] result.currentVersion = M.getCurrentVersion() result.newestVersion = newest.version @@ -198,20 +235,33 @@ function M.getStatus(baseUrl, useCache) if result.stateCode == M.STATE.DOWNLOADING then 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 end return result 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) + 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 + return { ['major'] = major, ['minor'] = minor, ['patch'] = patch } 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() function M.compareVersions(versionA, versionB) @@ -230,9 +280,9 @@ function M.findVersion(verTable, version) end -- 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 v = (type(ver) == 'table') and ver or M.formatVersion(ver) + local v = M.formatVersion(version) local dt = devType and devType or 'tl-mr3020' return 'doodle3d-wifibox-' .. M.formatVersion(v) .. '-' .. dt .. '-' .. sf .. '.bin' end @@ -250,9 +300,14 @@ function M.getCurrentVersion() end -- 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 ccRv,ccMsg = createCacheDirectory() + if not ccRv then return nil,ccMsg end + 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" end @@ -267,7 +322,7 @@ function M.getAvailableVersions(baseUrl, useCache, version) for line in idxLines do local k,v = line:match('^(.-):(.*)$') k,v = trim(k), trim(v) - --P(1, "#" .. lineno .. ": considering '" .. line .. "' (" .. (k or '') .. " / " .. (v or '') .. ")") -- debug + D(1, "#" .. lineno .. ": considering '" .. line .. "' (" .. (k or '') .. " / " .. (v or '') .. ")") -- debug 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 @@ -308,7 +363,6 @@ function M.getAvailableVersions(baseUrl, useCache, version) if entry ~= nil then table.insert(result, entry) end - --sort table table.sort(result, function(a,b) return M.compareVersions(a.version,b.version) < 0 end) @@ -316,27 +370,45 @@ function M.getAvailableVersions(baseUrl, useCache, version) return result end --- devtype and isFactory are optional; returns a table with major, minor and patch as keys -function M.downloadImageFile(baseUrl, ver, forceDownload, devType, isFactory) - local filename = M.constructImageFilename(ver, devType, isFactory) +-- forceDownload, devtype and isFactory are optional +-- returns true or nil+msg or nil + return value from wget +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 ccRv,ccMsg = createCacheDirectory() + if not ccRv then return nil,ccMsg end + --TODO: call M.checkValidImage, set doDownload to true if not valid local rv = 0 if doDownload then 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 - 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 -- 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) local imgName = M.constructImageFilename(version, devType, isFactory) local cmd = noRetain and 'sysupgrade -n ' or 'sysupgrade ' 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 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 .. ")") end - return rv + return (rv == 0) and true or nil,rv end +--returns true on success, or nil+msg otherwise 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, "") - 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 @@ -370,12 +447,13 @@ local function main() os.exit(1) end - M.verbosity = argTable.verbosity + verbosity = argTable.verbosity if argTable.useCache ~= nil then useCache = argTable.useCache end P(0, "Doodle3D Wifibox firmware updater") - if os.execute('mkdir -p ' .. M.CACHE_PATH) ~= 0 then - E("Error: could not create cache directory '" .. M.CACHE_PATH .. "'") + local cacheCreated,msg = createCacheDirectory() + if not cacheCreated then + E(msg) os.exit(1) end @@ -459,20 +537,20 @@ local function main() elseif argTable.action == 'imageDownload' then --TODO: first check if version exists 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") end elseif argTable.action == 'clear' then - local rv = M.clear() - if rv ~= 0 then - P(1, "error (" .. rv .. ")") - else - P(1, "success") + local rv,msg = M.clear() + if not rv then P(1, "error (" .. msg .. ")") + else P(1, "success") end + elseif argTable.action == 'imageInstall' then local rv = M.flashImageVersion(argTable.version) E("error: flash function returned, the device should have been flashed and rebooted instead") os.exit(3) + else P(0, "usage: d3d-updater [-hqVcCvslr] [-u base_url] [-i version] [-d version] [-f version]") end