diff --git a/src/cron.js b/src/cron.js index 9cf1a739e..dc9e0f843 100644 --- a/src/cron.js +++ b/src/cron.js @@ -190,14 +190,16 @@ function autoupdatePatternChanged(pattern, tz) { // do box before app updates. for the off chance that the box logic fixes some app update logic issue if (updateInfo.box && !updateInfo.box.unstable) { debug('Starting box autoupdate to %j', updateInfo.box); - await safe(updater.updateToLatest({ skipBackup: false }, auditSource.CRON)); + const [error] = await safe(updater.updateToLatest({ skipBackup: false }, auditSource.CRON)); + if (error) debug(`Failed to box autoupdate: ${error.message}`); return; } const appUpdateInfo = _.omit(updateInfo, 'box'); if (Object.keys(appUpdateInfo).length > 0) { debug('Starting app update to %j', appUpdateInfo); - await safe(apps.autoupdateApps(appUpdateInfo, auditSource.CRON)); + const [error] = await safe(apps.autoupdateApps(appUpdateInfo, auditSource.CRON)); + if (error) debug(`Failed to app autoupdate: ${error.message}`); } else { debug('No app auto updates available'); } diff --git a/src/shell.js b/src/shell.js index 265060fa6..2e5aa9094 100644 --- a/src/shell.js +++ b/src/shell.js @@ -30,11 +30,13 @@ function exec(tag, cmd, callback) { child_process.exec(cmd, function (error, stdout, stderr) { const stdoutResult = stdout.toString('utf8'); + const stderrResult = stderr.toString('utf8'); debug(`${tag} (stdout): %s`, stdoutResult); - debug(`${tag} (stderr): %s`, stderr.toString('utf8')); + debug(`${tag} (stderr): %s`, stderrResult); if (error) error.stdout = stdoutResult; // when promisified, this is the way to get stdout + if (error) error.stderr = stderrResult; // when promisified, this is the way to get stderr callback(error, stdoutResult); }); diff --git a/src/updater.js b/src/updater.js index 24f9def09..70b820b04 100644 --- a/src/updater.js +++ b/src/updater.js @@ -7,9 +7,7 @@ exports = module.exports = { const apps = require('./apps.js'), assert = require('assert'), - async = require('async'), BoxError = require('./boxerror.js'), - child_process = require('child_process'), backuptask = require('./backuptask.js'), constants = require('./constants.js'), crypto = require('crypto'), @@ -21,171 +19,146 @@ const apps = require('./apps.js'), os = require('os'), path = require('path'), paths = require('./paths.js'), + promiseRetry = require('./promise-retry.js'), safe = require('safetydance'), semver = require('semver'), settings = require('./settings.js'), shell = require('./shell.js'), tasks = require('./tasks.js'), - updateChecker = require('./updatechecker.js'); + updateChecker = require('./updatechecker.js'), + util = require('util'); const RELEASES_PUBLIC_KEY = path.join(__dirname, 'releases.gpg'); const UPDATE_CMD = path.join(__dirname, 'scripts/update.sh'); -function downloadUrl(url, file, callback) { +async function downloadUrl(url, file) { assert.strictEqual(typeof file, 'string'); - assert.strictEqual(typeof callback, 'function'); // do not assert since it comes from the appstore - if (typeof url !== 'string') return callback(new BoxError(BoxError.EXTERNAL_ERROR, `url cannot be download to ${file} as it is not a string`)); - - let retryCount = 0; + if (typeof url !== 'string') throw new BoxError(BoxError.EXTERNAL_ERROR, `url cannot be download to ${file} as it is not a string`); safe.fs.unlinkSync(file); - async.retry({ times: 10, interval: 5000 }, function (retryCallback) { + let retryCount = 0; + await promiseRetry({ times: 10, interval: 5000 }, async function () { debug(`Downloading ${url} to ${file}. Try ${++retryCount}`); const args = `-s --fail ${url} -o ${file}`; debug(`downloadUrl: curl ${args}`); - shell.spawn('downloadUrl', '/usr/bin/curl', args.split(' '), {}, function (error) { - if (error) return retryCallback(new BoxError(BoxError.NETWORK_ERROR, `Failed to download ${url}: ${error.message}`)); + const [error] = await safe(shell.promises.spawn('downloadUrl', '/usr/bin/curl', args.split(' '), {})); + if (error) throw new BoxError(BoxError.NETWORK_ERROR, `Failed to download ${url}: ${error.message}`); - debug(`downloadUrl: downloadUrl ${url} to ${file}`); - - retryCallback(); - }); - }, callback); + debug(`downloadUrl: downloaded ${url} to ${file}`); + }); } -function gpgVerify(file, sig, callback) { +async function gpgVerify(file, sig) { + assert.strictEqual(typeof file, 'string'); + assert.strictEqual(typeof sig, 'string'); + const cmd = `/usr/bin/gpg --status-fd 1 --no-default-keyring --keyring ${RELEASES_PUBLIC_KEY} --verify ${sig} ${file}`; debug(`gpgVerify: ${cmd}`); - child_process.exec(cmd, { encoding: 'utf8' }, function (error, stdout, stderr) { - if (error) { - debug(`gpgVerify: command failed. error: ${error}\n stdout: ${stdout}\n stderr: ${stderr}`); - return callback(new BoxError(BoxError.NOT_SIGNED, `The signature in ${path.basename(sig)} could not be verified (command failed)`)); - } + const [error, stdout] = await safe(shell.promises.exec('gpgVerify', cmd)); + if (error) { + debug(`gpgVerify: command failed. error: ${error}\n stdout: ${error.stdout}\n stderr: ${error.stderr}`); + throw new BoxError(BoxError.NOT_SIGNED, `The signature in ${path.basename(sig)} could not be verified (command failed)`); + } - if (stdout.indexOf('[GNUPG:] VALIDSIG 0EADB19CDDA23CD0FE71E3470A372F8703C493CC') !== -1) return callback(); + if (stdout.indexOf('[GNUPG:] VALIDSIG 0EADB19CDDA23CD0FE71E3470A372F8703C493CC') !== -1) return; // success - debug(`gpgVerify: verification of ${sig} failed: ${stdout}\n${stderr}`); + debug(`gpgVerify: verification of ${sig} failed: ${stdout}\n`); - return callback(new BoxError(BoxError.NOT_SIGNED, `The signature in ${path.basename(sig)} could not be verified (bad sig)`)); - }); + throw new BoxError(BoxError.NOT_SIGNED, `The signature in ${path.basename(sig)} could not be verified (bad sig)`); } -function extractTarball(tarball, dir, callback) { +async function extractTarball(tarball, dir) { + assert.strictEqual(typeof tarball, 'string'); + assert.strictEqual(typeof dir, 'string'); + const args = `-zxf ${tarball} -C ${dir}`; debug(`extractTarball: tar ${args}`); - shell.spawn('extractTarball', '/bin/tar', args.split(' '), {}, function (error) { - if (error) return callback(new BoxError(BoxError.FS_ERROR, `Failed to extract release package: ${error.message}`)); + const [error] = await safe(shell.promises.spawn('extractTarball', '/bin/tar', args.split(' '), {})); + if (error) throw new BoxError(BoxError.FS_ERROR, `Failed to extract release package: ${error.message}`); - safe.fs.unlinkSync(tarball); + safe.fs.unlinkSync(tarball); - debug(`extractTarball: extracted ${tarball} to ${dir}`); - - callback(); - }); + debug(`extractTarball: extracted ${tarball} to ${dir}`); } -function verifyUpdateInfo(versionsFile, updateInfo, callback) { +async function verifyUpdateInfo(versionsFile, updateInfo) { assert.strictEqual(typeof versionsFile, 'string'); assert.strictEqual(typeof updateInfo, 'object'); - assert.strictEqual(typeof callback, 'function'); const releases = safe.JSON.parse(safe.fs.readFileSync(versionsFile, 'utf8')) || {}; - if (!releases[constants.VERSION]) return callback(new BoxError(BoxError.EXTERNAL_ERROR, `No version info for ${constants.VERSION}`)); - if (!releases[constants.VERSION].next) return callback(new BoxError(BoxError.EXTERNAL_ERROR, `No next version info for ${constants.VERSION}`)); + if (!releases[constants.VERSION]) throw new BoxError(BoxError.EXTERNAL_ERROR, `No version info for ${constants.VERSION}`); + if (!releases[constants.VERSION].next) throw new BoxError(BoxError.EXTERNAL_ERROR, `No next version info for ${constants.VERSION}`); const nextVersion = releases[constants.VERSION].next; - if (typeof releases[nextVersion] !== 'object' || !releases[nextVersion]) return callback(new BoxError(BoxError.EXTERNAL_ERROR, 'No next version info')); - if (releases[nextVersion].sourceTarballUrl !== updateInfo.sourceTarballUrl) return callback(new BoxError(BoxError.EXTERNAL_ERROR, 'Version info mismatch')); - - callback(); + if (typeof releases[nextVersion] !== 'object' || !releases[nextVersion]) throw new BoxError(BoxError.EXTERNAL_ERROR, 'No next version info'); + if (releases[nextVersion].sourceTarballUrl !== updateInfo.sourceTarballUrl) throw new BoxError(BoxError.EXTERNAL_ERROR, 'Version info mismatch'); } -function downloadAndVerifyRelease(updateInfo, callback) { +async function downloadAndVerifyRelease(updateInfo) { assert.strictEqual(typeof updateInfo, 'object'); - assert.strictEqual(typeof callback, 'function'); - let newBoxSource = path.join(os.tmpdir(), 'box-' + crypto.randomBytes(4).readUInt32LE(0)); + const newBoxSource = path.join(os.tmpdir(), 'box-' + crypto.randomBytes(4).readUInt32LE(0)); - async.series([ - downloadUrl.bind(null, updateInfo.boxVersionsUrl, `${paths.UPDATE_DIR}/versions.json`), - downloadUrl.bind(null, updateInfo.boxVersionsSigUrl, `${paths.UPDATE_DIR}/versions.json.sig`), - gpgVerify.bind(null, `${paths.UPDATE_DIR}/versions.json`, `${paths.UPDATE_DIR}/versions.json.sig`), - verifyUpdateInfo.bind(null, `${paths.UPDATE_DIR}/versions.json`, updateInfo), - downloadUrl.bind(null, updateInfo.sourceTarballUrl, `${paths.UPDATE_DIR}/box.tar.gz`), - downloadUrl.bind(null, updateInfo.sourceTarballSigUrl, `${paths.UPDATE_DIR}/box.tar.gz.sig`), - gpgVerify.bind(null, `${paths.UPDATE_DIR}/box.tar.gz`, `${paths.UPDATE_DIR}/box.tar.gz.sig`), - fs.mkdir.bind(null, newBoxSource, { recursive: true }), - extractTarball.bind(null, `${paths.UPDATE_DIR}/box.tar.gz`, newBoxSource) - ], function (error) { - if (error) return callback(error); + await downloadUrl(updateInfo.boxVersionsUrl, `${paths.UPDATE_DIR}/versions.json`); + await downloadUrl(updateInfo.boxVersionsSigUrl, `${paths.UPDATE_DIR}/versions.json.sig`); + await gpgVerify(`${paths.UPDATE_DIR}/versions.json`, `${paths.UPDATE_DIR}/versions.json.sig`); + await verifyUpdateInfo(`${paths.UPDATE_DIR}/versions.json`, updateInfo); + await downloadUrl(updateInfo.sourceTarballUrl, `${paths.UPDATE_DIR}/box.tar.gz`); + await downloadUrl(updateInfo.sourceTarballSigUrl, `${paths.UPDATE_DIR}/box.tar.gz.sig`); + await gpgVerify(`${paths.UPDATE_DIR}/box.tar.gz`, `${paths.UPDATE_DIR}/box.tar.gz.sig`); + const [mkdirError] = await safe(fs.promises.mkdir(newBoxSource, { recursive: true })); + if (mkdirError) throw new BoxError(BoxError.FS_ERROR, `Failed to create directory ${newBoxSource}: ${mkdirError.message}`); + await extractTarball(`${paths.UPDATE_DIR}/box.tar.gz`, newBoxSource); - callback(null, { file: newBoxSource }); - }); + return { file: newBoxSource }; } -function checkFreeDiskSpace(neededSpace, callback) { +async function checkFreeDiskSpace(neededSpace) { assert.strictEqual(typeof neededSpace, 'number'); - assert.strictEqual(typeof callback, 'function'); // can probably be a bit more aggressive here since a new update can bring in new docker images - df.file('/').then(function (diskUsage) { - if (diskUsage.available < neededSpace) return callback(new BoxError(BoxError.FS_ERROR, 'Not enough disk space')); + const [error, diskUsage] = await safe(df.file('/')); + if (error) throw new BoxError(BoxError.FS_ERROR, error); - callback(null); - }).catch(function (error) { - callback(new BoxError(BoxError.FS_ERROR, error)); - }); + if (diskUsage.available < neededSpace) throw new BoxError(BoxError.FS_ERROR, 'Not enough disk space'); } -function update(boxUpdateInfo, options, progressCallback, callback) { +async function update(boxUpdateInfo, options, progressCallback) { assert(boxUpdateInfo && typeof boxUpdateInfo === 'object'); assert(options && typeof options === 'object'); assert.strictEqual(typeof progressCallback, 'function'); - assert.strictEqual(typeof callback, 'function'); progressCallback({ percent: 1, message: 'Checking disk space' }); - checkFreeDiskSpace(1024*1024*1024, function (error) { - if (error) return callback(error); + await checkFreeDiskSpace(1024*1024*1024); - progressCallback({ percent: 5, message: 'Downloading and verifying release' }); + progressCallback({ percent: 5, message: 'Downloading and verifying release' }); - downloadAndVerifyRelease(boxUpdateInfo, function (error, packageInfo) { - if (error) return callback(error); + const packageInfo = await downloadAndVerifyRelease(boxUpdateInfo); - function maybeBackup(next) { - if (options.skipBackup) return next(); + if (!options.skipBackup) { + progressCallback({ percent: 10, message: 'Backing up' }); - progressCallback({ percent: 10, message: 'Backing up' }); + await util.promisify(backuptask.backupBoxAndApps)({ preserveSecs: 3*7*24*60*60 }, (progress) => progressCallback({ percent: 10+progress.percent*70/100, message: progress.message })); + } - backuptask.backupBoxAndApps({ preserveSecs: 3*7*24*60*60 }, (progress) => progressCallback({ percent: 10+progress.percent*70/100, message: progress.message }), next); - } + debug('updating box %s', boxUpdateInfo.sourceTarballUrl); - maybeBackup(function (error) { - if (error) return callback(error); + progressCallback({ percent: 70, message: 'Installing update' }); - debug('updating box %s', boxUpdateInfo.sourceTarballUrl); + // run installer.sh from new box code as a separate service + await shell.promises.sudo('update', [ UPDATE_CMD, packageInfo.file ], {}); - progressCallback({ percent: 70, message: 'Installing update' }); - - // run installer.sh from new box code as a separate service - shell.sudo('update', [ UPDATE_CMD, packageInfo.file ], {}, function (error) { - if (error) return callback(error); - - // Do not add any code here. The installer script will stop the box code any instant - }); - }); - }); - }); + // Do not add any code here. The installer script will stop the box code any instant } async function canUpdate(boxUpdateInfo) {