diff --git a/setup/start.sh b/setup/start.sh index 0797d8b80..8c5b7bd36 100755 --- a/setup/start.sh +++ b/setup/start.sh @@ -58,7 +58,7 @@ mkdir -p "${PLATFORM_DATA_DIR}/collectd/collectd.conf.d" mkdir -p "${PLATFORM_DATA_DIR}/logrotate.d" mkdir -p "${PLATFORM_DATA_DIR}/acme" mkdir -p "${PLATFORM_DATA_DIR}/backup" -mkdir -p "${PLATFORM_DATA_DIR}/logs/backup" +mkdir -p "${PLATFORM_DATA_DIR}/logs/backup" "${PLATFORM_DATA_DIR}/logs/updater" mkdir -p "${PLATFORM_DATA_DIR}/update" mkdir -p "${BOX_DATA_DIR}/appicons" diff --git a/src/paths.js b/src/paths.js index c5784deae..4bd39a148 100644 --- a/src/paths.js +++ b/src/paths.js @@ -35,4 +35,5 @@ exports = module.exports = { LOG_DIR: path.join(config.baseDir(), 'platformdata/logs'), // this pattern is for the cloudron logs API route to work BACKUP_LOG_FILE: path.join(config.baseDir(), 'platformdata/logs/backup/app.log'), + UPDATER_LOG_FILE: path.join(config.baseDir(), 'platformdata/logs/updater/app.log') }; diff --git a/src/scripts/update.sh b/src/scripts/update.sh index 2aab12b90..963daa3f9 100755 --- a/src/scripts/update.sh +++ b/src/scripts/update.sh @@ -9,7 +9,7 @@ fi readonly UPDATER_SERVICE="cloudron-updater" readonly DATETIME=`date '+%Y-%m-%d_%H-%M-%S'` -readonly LOG_FILE="/var/log/cloudron-updater-${DATETIME}.log" +readonly LOG_FILE="/home/yellowtent/platformdata/logs/updater/cloudron-updater-${DATETIME}.log" if [[ $# == 1 && "$1" == "--check" ]]; then echo "OK" diff --git a/src/tasks.js b/src/tasks.js index 079d4fb53..ef7637f00 100644 --- a/src/tasks.js +++ b/src/tasks.js @@ -3,7 +3,6 @@ exports = module.exports = { update: update, get: get, - clear: clear, startTask: startTask, stopTask: stopTask, @@ -25,18 +24,27 @@ let assert = require('assert'), paths = require('./paths.js'), safe = require('safetydance'), taskdb = require('./taskdb.js'), - util = require('util'); + util = require('util'), + _ = require('underscore'); const NOOP_CALLBACK = function (error) { if (error) debug(error); }; const TASKS = { - 'backup': { + backup: { lock: locker.OP_FULL_BACKUP, logFile: paths.BACKUP_LOG_FILE, program: __dirname + '/tasks/backuptask.js', onFailure: mailer.backupFailed, startEventId: eventlog.ACTION_BACKUP_START, finishEventId: eventlog.ACTION_BACKUP_FINISH + }, + update: { + lock: locker.OP_BOX_UPDATE, + logFile: paths.UPDATER_LOG_FILE, + program: __dirname + '/tasks/updatertask.js', + onFailure: NOOP_CALLBACK, + startEventId: eventlog.ACTION_UPDATE, + finishEventId: eventlog.ACTION_UPDATE } }; @@ -93,11 +101,12 @@ function get(id, callback) { }); } -function clear(id, callback) { +function clear(id, args, callback) { assert.strictEqual(typeof id, 'string'); + assert(args && typeof args === 'object'); assert.strictEqual(typeof callback, 'function'); - update(id, { percent: 0, message: 'Starting', result: '', errorMessage: '', args: {} }, callback); + update(id, { percent: 0, message: 'Starting', result: '', errorMessage: '', args: args }, callback); } function startTask(id, args, auditSource, callback) { @@ -124,17 +133,22 @@ function startTask(id, args, auditSource, callback) { // when parent process dies, this process is killed because KillMode=control-group in systemd unit file assert(!gTasks[id], 'Task is already running'); - clear(id, NOOP_CALLBACK); + clear(id, args, NOOP_CALLBACK); eventlog.add(taskInfo.startEventId, auditSource, args); - gTasks[id] = child_process.fork(taskInfo.program, [], { stdio: [ 'pipe', fd, fd, 'ipc' ]}); + gTasks[id] = child_process.fork(taskInfo.program, [], { stdio: [ 'pipe', fd, fd, 'ipc' ]}); // fork requires ipc gTasks[id].once('exit', function (code, signal) { debug(`startTask: ${id} completed with code ${code} and signal ${signal}`); get(id, function (error, progress) { - if (!error && progress.errorMessage) error = new Error(progress.errorMessage); + if (!error && progress.percent !== 100) { // task crashed or was killed by us (code 50) + error = code === 0 ? new Error(`${id} task stopped`) : new Error(`${id} task crashed with code ${code} and signal ${signal}`); + update(id, { percent: 100, errorMessage: error.message }, NOOP_CALLBACK); + } else if (!error && progress.errorMessage) { + error = new Error(progress.errorMessage); + } - eventlog.add(taskInfo.finishEventId, auditSource, { errorMessage: error ? error.message : null, backupId: progress ? progress.result : null }); + eventlog.add(taskInfo.finishEventId, auditSource, _.extend({ errorMessage: error ? error.message : null }, progress ? progress.result : {})); locker.unlock(taskInfo.lock); diff --git a/src/tasks/updatertask.js b/src/tasks/updatertask.js new file mode 100755 index 000000000..9004e14d6 --- /dev/null +++ b/src/tasks/updatertask.js @@ -0,0 +1,38 @@ +'use strict'; + +require('supererror')({ splatchError: true }); + +let database = require('../database.js'), + debug = require('debug')('box:updatertask'), + tasks = require('../tasks.js'), + updater = require('../updater.js'); + +const NOOP_CALLBACK = function (error) { if (error) debug(error); }; + +process.on('SIGTERM', function () { + process.exit(0); +}); + +function exit(error) { + if (!error) process.exit(0); + + debug(error); + process.exit(50); +} + +// Main process starts here +debug('Staring update'); +database.initialize(function (error) { + if (error) return exit(error); + + tasks.get(tasks.TASK_UPDATE, function (error, result) { + if (error) return exit(error); + if (!result.args.boxUpdateInfo) return exit(new Error('Invalid args:' + JSON.stringify(result))); + + updater.update(result.args.boxUpdateInfo, (progress) => tasks.update(tasks.TASK_UPDATE, progress, NOOP_CALLBACK), function (updateError) { + const progress = { percent: 100, errorMessage: updateError ? updateError.message : '' }; + + tasks.update(tasks.TASK_UPDATE, progress, () => exit(updateError)); + }); + }); +}); diff --git a/src/updater.js b/src/updater.js index 92c3d8420..a9ec21ade 100644 --- a/src/updater.js +++ b/src/updater.js @@ -2,6 +2,7 @@ exports = module.exports = { updateToLatest: updateToLatest, + update: update, UpdaterError: UpdaterError }; @@ -13,8 +14,6 @@ var assert = require('assert'), config = require('./config.js'), crypto = require('crypto'), debug = require('debug')('box:updater'), - eventlog = require('./eventlog.js'), - locker = require('./locker.js'), mkdirp = require('mkdirp'), os = require('os'), path = require('path'), @@ -27,7 +26,6 @@ var assert = require('assert'), const RELEASES_PUBLIC_KEY = path.join(__dirname, 'releases.gpg'); const UPDATE_CMD = path.join(__dirname, 'scripts/update.sh'); -const NOOP_CALLBACK = function (error) { if (error) debug(error); }; function UpdaterError(reason, errorOrMessage) { assert.strictEqual(typeof reason, 'string'); @@ -152,30 +150,28 @@ function downloadAndVerifyRelease(updateInfo, callback) { }); } -function doUpdate(boxUpdateInfo, callback) { +function update(boxUpdateInfo, progressCallback, callback) { assert(boxUpdateInfo && typeof boxUpdateInfo === 'object'); + assert.strictEqual(typeof progressCallback, 'function'); + assert.strictEqual(typeof callback, 'function'); - function updateError(e) { - tasks.update(tasks.TASK_UPDATE, { percent: -1, errorMessage: e.message }, NOOP_CALLBACK); - callback(e); - } - - tasks.update(tasks.TASK_UPDATE, { percent: 5, message: 'Downloading and verifying release' }, NOOP_CALLBACK); + progressCallback({ percent: 5, message: 'Downloading and verifying release' }); downloadAndVerifyRelease(boxUpdateInfo, function (error, packageInfo) { - if (error) return updateError(error); + if (error) return callback(error); - tasks.update(tasks.TASK_UPDATE, { percent: 10, message: 'Backing up' }, NOOP_CALLBACK); + progressCallback({ percent: 10, message: 'Backing up' }); - backups.backupBoxAndApps((progress) => tasks.update(tasks.TASK_MIGRATE, { percent: 10+progress.percent*70/100, message: progress.message }, NOOP_CALLBACK), function (error) { - if (error) return updateError(error); + backups.backupBoxAndApps((progress) => progressCallback({ percent: 10+progress.percent*70/100, message: progress.message }), function (error) { + if (error) return callback(error); debug('updating box %s', boxUpdateInfo.sourceTarballUrl); - tasks.update(tasks.TASK_UPDATE, { percent: 70, message: 'Installing update' }, NOOP_CALLBACK); + 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 updateError(error); + if (error) return callback(error); // Do not add any code here. The installer script will stop the box code any instant }); @@ -183,30 +179,6 @@ function doUpdate(boxUpdateInfo, callback) { }); } -function update(boxUpdateInfo, auditSource, callback) { - assert(boxUpdateInfo && typeof boxUpdateInfo === 'object'); - assert.strictEqual(typeof auditSource, 'object'); - assert.strictEqual(typeof callback, 'function'); - - var error = locker.lock(locker.OP_BOX_UPDATE); - if (error) return callback(new UpdaterError(UpdaterError.BAD_STATE, error.message)); - - eventlog.add(eventlog.ACTION_UPDATE, auditSource, { boxUpdateInfo: boxUpdateInfo }); - - // ensure tools can 'wait' on progress - tasks.update(tasks.TASK_UPDATE, { percent: 0, message: 'Starting' }, NOOP_CALLBACK); - - debug('Starting update'); - doUpdate(boxUpdateInfo, function (error) { - if (error) { - debug('Update failed with error:', error); - locker.unlock(locker.OP_BOX_UPDATE); - } - }); - - callback(null); -} - function updateToLatest(auditSource, callback) { assert.strictEqual(typeof auditSource, 'object'); assert.strictEqual(typeof callback, 'function'); @@ -215,5 +187,5 @@ function updateToLatest(auditSource, callback) { if (!boxUpdateInfo) return callback(new UpdaterError(UpdaterError.ALREADY_UPTODATE, 'No update available')); if (!boxUpdateInfo.sourceTarballUrl) return callback(new UpdaterError(UpdaterError.BAD_STATE, 'No automatic update available')); - update(boxUpdateInfo, auditSource, callback); + tasks.startTask(tasks.TASK_UPDATE, { boxUpdateInfo }, auditSource, callback); }