diff --git a/src/backups.js b/src/backups.js index 50b65265e..9adbd6bb4 100644 --- a/src/backups.js +++ b/src/backups.js @@ -12,9 +12,6 @@ exports = module.exports = { ensureBackup: ensureBackup, - startBackupTask: startBackupTask, - stopBackupTask: stopBackupTask, - restore: restore, backupApp: backupApp, @@ -42,16 +39,12 @@ var addons = require('./addons.js'), async = require('async'), assert = require('assert'), backupdb = require('./backupdb.js'), - child_process = require('child_process'), config = require('./config.js'), crypto = require('crypto'), database = require('./database.js'), DatabaseError = require('./databaseerror.js'), debug = require('debug')('box:backups'), - eventlog = require('./eventlog.js'), fs = require('fs'), - locker = require('./locker.js'), - mailer = require('./mailer.js'), mkdirp = require('mkdirp'), once = require('once'), path = require('path'), @@ -67,11 +60,8 @@ var addons = require('./addons.js'), util = require('util'), zlib = require('zlib'); -var NOOP_CALLBACK = function (error) { if (error) debug(error); }; - -var BACKUP_UPLOAD_CMD = path.join(__dirname, 'backupupload.js'); - -let gBackupTask = null; +const NOOP_CALLBACK = function (error) { if (error) debug(error); }; +const BACKUP_UPLOAD_CMD = path.join(__dirname, 'backupupload.js'); function debugApp(app) { assert(typeof app === 'object'); @@ -928,62 +918,6 @@ function backupBoxAndApps(progressCallback, callback) { }); } -function startBackupTask(auditSource, callback) { - assert.strictEqual(typeof auditSource, 'object'); - assert.strictEqual(typeof callback, 'function'); - - let error = locker.lock(locker.OP_FULL_BACKUP); - if (error) return callback(new BackupsError(BackupsError.BAD_STATE, error.message)); - - tasks.clearProgress(tasks.TASK_BACKUP, NOOP_CALLBACK); - - let fd = safe.fs.openSync(paths.BACKUP_LOG_FILE, 'a'); // will autoclose - if (!fd) { - debug('startBackupTask: unable to get log filedescriptor %s', safe.error.message); - locker.unlock(locker.OP_FULL_BACKUP); - return callback(safe.error); - } - - debug(`starting backuptask. logs at ${paths.BACKUP_LOG_FILE}`); - - // when parent process dies, this process is killed because KillMode=control-group in systemd unit file - assert(!gBackupTask, 'Previous backup task already running!'); - eventlog.add(eventlog.ACTION_BACKUP_START, auditSource, { }); - - gBackupTask = child_process.fork(__dirname + '/tasks/backuptask.js', [ ], { stdio: [ 'pipe', fd, fd, 'ipc' ]}); - gBackupTask.once('exit', function (code, signal) { - debug(`startBackupTask: completed with code ${code} and signal ${signal}`); - - tasks.getProgress(tasks.TASK_BACKUP, function (error, progress) { - if (!error && progress.errorMessage) error = new Error(progress.errorMessage); - - eventlog.add(eventlog.ACTION_BACKUP_FINISH, auditSource, { errorMessage: error ? error.message : null, backupId: progress ? progress.result : null }); - - locker.unlock(locker.OP_FULL_BACKUP); - - if (error) mailer.backupFailed(error); - - gBackupTask = null; - - debug('startBackupTask: backup done'); - }); - }); - callback(null); -} - -function stopBackupTask(auditSource, callback) { - assert.strictEqual(typeof auditSource, 'object'); - assert.strictEqual(typeof callback, 'function'); - - if (!gBackupTask) return callback(new BackupsError(BackupsError.BAD_STATE, 'Backup task is not active')); - - debug('stopBackupTask: stopping backup process'); - - gBackupTask.kill('SIGTERM'); // this will end up calling the 'exit' signal handler - - callback(null); -} - function ensureBackup(auditSource, callback) { assert.strictEqual(typeof auditSource, 'object'); @@ -1003,7 +937,7 @@ function ensureBackup(auditSource, callback) { return callback(null); } - startBackupTask(auditSource, callback); + tasks.startTask(tasks.TASK_BACKUP, auditSource, callback); }); }); } diff --git a/src/routes/backups.js b/src/routes/backups.js index c39813619..4b9728662 100644 --- a/src/routes/backups.js +++ b/src/routes/backups.js @@ -9,7 +9,9 @@ var backupdb = require('../backupdb.js'), backups = require('../backups.js'), BackupsError = require('../backups.js').BackupsError, HttpError = require('connect-lastmile').HttpError, - HttpSuccess = require('connect-lastmile').HttpSuccess; + HttpSuccess = require('connect-lastmile').HttpSuccess, + tasks = require('../tasks.js'), + TasksError = require('../tasks.js').TasksError; function auditSource(req) { var ip = req.headers['x-forwarded-for'] || req.connection.remoteAddress || null; @@ -34,8 +36,8 @@ function list(req, res, next) { function startBackup(req, res, next) { // note that cloudron.backup only waits for backup initiation and not for backup to complete // backup progress can be checked up ny polling the progress api call - backups.startBackupTask(auditSource(req), function (error) { - if (error && error.reason === BackupsError.BAD_STATE) return next(new HttpError(409, error.message)); + tasks.startTask(tasks.TASK_BACKUP, auditSource(req), function (error) { + if (error && error.reason === TasksError.BAD_STATE) return next(new HttpError(409, error.message)); if (error) return next(new HttpError(500, error)); next(new HttpSuccess(202, {})); diff --git a/src/routes/sysadmin.js b/src/routes/sysadmin.js index 89de0700e..74d228709 100644 --- a/src/routes/sysadmin.js +++ b/src/routes/sysadmin.js @@ -11,12 +11,12 @@ exports = module.exports = { var apps = require('../apps.js'), AppsError = apps.AppsError, addons = require('../addons.js'), - backups = require('../backups.js'), - BackupsError = require('../backups.js').BackupsError, cloudron = require('../cloudron.js'), debug = require('debug')('box:routes/sysadmin'), HttpError = require('connect-lastmile').HttpError, HttpSuccess = require('connect-lastmile').HttpSuccess, + tasks = require('../tasks.js'), + TasksError = require('../tasks.js').TasksError, updater = require('../updater.js'), UpdaterError = require('../updater.js').UpdaterError; @@ -26,8 +26,8 @@ function backup(req, res, next) { // note that cloudron.backup only waits for backup initiation and not for backup to complete // backup progress can be checked up ny polling the progress api call var auditSource = { userId: null, username: 'sysadmin' }; - backups.startBackupTask(auditSource, function (error) { - if (error && error.reason === BackupsError.BAD_STATE) return next(new HttpError(409, error.message)); + tasks.startTask(tasks.TASK_BACKUP, auditSource, function (error) { + if (error && error.reason === TasksError.BAD_STATE) return next(new HttpError(409, error.message)); if (error) return next(new HttpError(500, error)); next(new HttpSuccess(202, {})); diff --git a/src/tasks.js b/src/tasks.js index 2b0f4b065..ef35dd883 100644 --- a/src/tasks.js +++ b/src/tasks.js @@ -5,6 +5,7 @@ exports = module.exports = { getProgress: getProgress, clearProgress: clearProgress, + startTask: startTask, stopTask: stopTask, TaskError: TaskError, @@ -15,13 +16,32 @@ exports = module.exports = { }; let assert = require('assert'), - BackupsError = require('./backups.js').BackupsError, - backups = require('./backups.js'), + child_process = require('child_process'), DatabaseError = require('./databaseerror.js'), debug = require('debug')('box:tasks'), + eventlog = require('./eventlog.js'), + locker = require('./locker.js'), + mailer = require('./mailer.js'), + paths = require('./paths.js'), + safe = require('safetydance'), taskdb = require('./taskdb.js'), util = require('util'); +const NOOP_CALLBACK = function (error) { if (error) debug(error); }; + +const TASKS = { + '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 + } +}; + +let gTasks = {}; + function TaskError(reason, errorOrMessage) { assert.strictEqual(typeof reason, 'string'); assert(errorOrMessage instanceof Error || typeof errorOrMessage === 'string' || typeof errorOrMessage === 'undefined'); @@ -78,22 +98,64 @@ function clearProgress(id, callback) { setProgress(id, { percent: 0, message: 'Starting', result: '', errorMessage: '' }, callback); } +function startTask(id, auditSource, callback) { + assert.strictEqual(typeof id, 'string'); + assert.strictEqual(typeof auditSource, 'object'); + assert.strictEqual(typeof callback, 'function'); + + let taskInfo = TASKS[id]; + if (!taskInfo) return callback(new TaskError(TaskError.NOT_FOUND, 'No such task')); + + let error = locker.lock(taskInfo.lock); + if (error) return callback(new TaskError(TaskError.BAD_STATE, error.message)); + + let fd = safe.fs.openSync(taskInfo.logFile, 'a'); // will autoclose + if (!fd) { + debug(`startTask: unable to get log filedescriptor ${safe.error.message}`); + locker.unlock(taskInfo.lock); + return callback(new TaskError(TaskError.INTERNAL_ERROR, error.message)); + } + + debug(`startTask - starting task ${id}. logs at ${taskInfo.logFile}`); + + // when parent process dies, this process is killed because KillMode=control-group in systemd unit file + assert(!gTasks[id], 'Task is already running'); + + clearProgress(id, NOOP_CALLBACK); + eventlog.add(taskInfo.startEventId, auditSource, { }); + + gTasks[id] = child_process.fork(taskInfo.program, [ ], { stdio: [ 'pipe', fd, fd, 'ipc' ]}); + gTasks[id].once('exit', function (code, signal) { + debug(`startTask: ${id} completed with code ${code} and signal ${signal}`); + + getProgress(id, function (error, progress) { + if (!error && progress.errorMessage) error = new Error(progress.errorMessage); + + eventlog.add(taskInfo.finishEventId, auditSource, { errorMessage: error ? error.message : null, backupId: progress ? progress.result : null }); + + locker.unlock(taskInfo.lock); + + if (error) taskInfo.onFailure(error); + + gTasks[id] = null; + + debug(`startTask: ${id} done`); + }); + }); + + callback(null); +} + function stopTask(id, auditSource, callback) { assert.strictEqual(typeof id, 'string'); assert.strictEqual(typeof auditSource, 'object'); assert.strictEqual(typeof callback, 'function'); - switch (id) { - case exports.TASK_BACKUP: - backups.stopBackupTask(auditSource, function (error) { - if (error && error.reason === BackupsError.BAD_STATE) return callback(new TaskError(TaskError.NOT_FOUND)); - if (error) return callback(new TaskError(TaskError.INTERNAL_ERROR, error)); + if (!gTasks[id]) return callback(new TaskError(TaskError.BAD_STATE, 'task is not active')); - callback(null); - }); - break; + debug(`stopTask: stopping task ${id}`); - default: - return callback(new TaskError(TaskError.NOT_FOUND)); - } + gTasks[id].kill('SIGTERM'); // this will end up calling the 'exit' signal handler + + callback(null); } diff --git a/src/test/backups-test.js b/src/test/backups-test.js index 47d0cd469..aef78751e 100644 --- a/src/test/backups-test.js +++ b/src/test/backups-test.js @@ -23,7 +23,7 @@ var async = require('async'), tasks = require('../tasks.js'); function createBackup(callback) { - backups.startBackupTask({ username: 'test' }, function (error) { // this call does not wait for the backup! + tasks.startTask(tasks.TASK_BACKUP, { username: 'test' }, function (error) { // this call does not wait for the backup! if (error) return callback(error); function waitForBackup() {