diff --git a/src/appdb.js b/src/appdb.js index 9dadd603c..698382bc9 100644 --- a/src/appdb.js +++ b/src/appdb.js @@ -21,8 +21,7 @@ exports = module.exports = { getAppIdByAddonConfigValue: getAppIdByAddonConfigValue, setHealth: setHealth, - setInstallationCommand: setInstallationCommand, - setRunCommand: setRunCommand, + setTask: setTask, getAppStoreIds: getAppStoreIds, // installation codes (keep in sync in UI) @@ -479,25 +478,12 @@ function setHealth(appId, health, healthTime, callback) { updateWithConstraints(appId, values, constraints, callback); } -function setInstallationCommand(appId, installationState, values, callback) { +function setTask(appId, values, callback) { assert.strictEqual(typeof appId, 'string'); - assert.strictEqual(typeof installationState, 'string'); assert.strictEqual(typeof values, 'object'); assert.strictEqual(typeof callback, 'function'); - values.installationState = installationState; - values.errorMessage = ''; - - updateWithConstraints(appId, values, `AND (installationState = "${exports.ISTATE_INSTALLED}" OR installationState = "${exports.ISTATE_ERROR}")`, callback); -} - -function setRunCommand(appId, runState, callback) { - assert.strictEqual(typeof appId, 'string'); - assert.strictEqual(typeof runState, 'string'); - assert.strictEqual(typeof callback, 'function'); - - var values = { runState: runState }; - updateWithConstraints(appId, values, `AND installationState = "${exports.ISTATE_INSTALLED}"`, callback); + updateWithConstraints(appId, values, 'AND taskId IS NULL', callback); } function getAppStoreIds(callback) { diff --git a/src/apps.js b/src/apps.js index b37f2d581..252c7f02b 100644 --- a/src/apps.js +++ b/src/apps.js @@ -322,17 +322,17 @@ function validateDataDir(dataDir) { return null; } -function getDuplicateErrorDetails(error, location, domainObject, portBindings, alternateDomains) { - assert.strictEqual(error.reason, DatabaseError.ALREADY_EXISTS); +function getDuplicateErrorDetails(errorMessage, location, domainObject, portBindings, alternateDomains) { + assert.strictEqual(typeof errorMessage, 'string'); assert.strictEqual(typeof location, 'string'); assert.strictEqual(typeof domainObject, 'object'); assert.strictEqual(typeof portBindings, 'object'); assert(Array.isArray(alternateDomains)); - var match = error.message.match(/ER_DUP_ENTRY: Duplicate entry '(.*)' for key '(.*)'/); + var match = errorMessage.match(/ER_DUP_ENTRY: Duplicate entry '(.*)' for key '(.*)'/); if (!match) { - debug('Unexpected SQL error message.', error); - return new AppsError(AppsError.INTERNAL_ERROR, error); + debug('Unexpected SQL error message.', errorMessage); + return new AppsError(AppsError.INTERNAL_ERROR, new Error(errorMessage)); } // check if the location or alternateDomains conflicts @@ -576,15 +576,23 @@ function mailboxNameForLocation(location, manifest) { return (location ? location : manifest.title.toLowerCase().replace(/[^a-zA-Z0-9]/g, '')) + '.app'; } -function scheduleTask(appId, args, callback) { +function scheduleTask(appId, args, values, callback) { assert.strictEqual(typeof appId, 'string'); assert.strictEqual(typeof args, 'object'); + assert.strictEqual(typeof values, 'object'); assert.strictEqual(typeof callback, 'function'); + assert(values.installationState || values.runState); + tasks.add(tasks.TASK_APP, [ appId, args ], function (error, taskId) { if (error) return callback(new AppsError(AppsError.INTERNAL_ERROR, error)); - appdb.update(appId, { taskId: taskId }, function (error) { + values.errorMessage = ''; + values.taskId = taskId; + + appdb.setTask(appId, values, function (error) { + if (error && error.reason === DatabaseError.ALREADY_EXISTS) return callback(new AppsError(AppsError.ALREADY_EXISTS, error.message)); + if (error && error.reason === DatabaseError.NOT_FOUND) return callback(new AppsError(AppsError.BAD_STATE)); if (error) return callback(new AppsError(AppsError.INTERNAL_ERROR, error)); appTaskManager.scheduleTask(appId, taskId, function (error) { @@ -713,7 +721,7 @@ function install(data, user, auditSource, callback) { }; appdb.add(appId, appStoreId, manifest, location, domain, translatePortBindings(portBindings, manifest), data, function (error) { - if (error && error.reason === DatabaseError.ALREADY_EXISTS) return callback(getDuplicateErrorDetails(error, location, domainObject, portBindings, data.alternateDomains)); + if (error && error.reason === DatabaseError.ALREADY_EXISTS) return callback(getDuplicateErrorDetails(error.message, location, domainObject, portBindings, data.alternateDomains)); if (error && error.reason === DatabaseError.NOT_FOUND) return callback(new AppsError(AppsError.NOT_FOUND, error.message)); if (error) return callback(new AppsError(AppsError.INTERNAL_ERROR, error)); @@ -728,7 +736,7 @@ function install(data, user, auditSource, callback) { const restoreConfig = backupId ? { backupId: backupId, backupFormat: backupFormat } : null; - scheduleTask(appId, { restoreConfig }, function (error, result) { + scheduleTask(appId, { restoreConfig }, { installationState: appdb.ISTATE_PENDING_INSTALL }, function (error, result) { if (error) return callback(error); eventlog.add(eventlog.ACTION_APP_INSTALL, auditSource, { appId: appId, app: result }); @@ -750,9 +758,10 @@ function configure(appId, data, user, auditSource, callback) { get(appId, function (error, app) { if (error) return callback(error); - if (app.installationState !== appdb.ISTATE_INSTALLED && app.installationState !== appdb.ISTATE_ERROR) return callback(new AppsError(AppsError.BAD_STATE, `Not allowed in this app state : ${app.installationState}`)); + if (app.taskId) return callback(new AppsError(AppsError.BAD_STATE, `Not allowed in this app state : ${app.installationState} / ${app.runState}`)); + + let domain, location, portBindings, values = { installationState: appdb.ISTATE_PENDING_CONFIGURE }; - let domain, location, portBindings, values = { }; if ('location' in data && 'domain' in data) { location = values.location = data.location.toLowerCase(); domain = values.domain = data.domain.toLowerCase(); @@ -870,18 +879,13 @@ function configure(appId, data, user, auditSource, callback) { debug(`configure: id:${appId}`); - appdb.setInstallationCommand(appId, appdb.ISTATE_PENDING_CONFIGURE, values, function (error) { - if (error && error.reason === DatabaseError.ALREADY_EXISTS) return callback(getDuplicateErrorDetails(error, location, domainObject, portBindings, data.alternateDomains)); - if (error && error.reason === DatabaseError.NOT_FOUND) return callback(new AppsError(AppsError.BAD_STATE)); - if (error) return callback(new AppsError(AppsError.INTERNAL_ERROR, error)); + scheduleTask(appId, { oldConfig: getAppConfig(app) }, values, function (error, result) { + if (error && error.reason === AppsError.ALREADY_EXISTS) error = getDuplicateErrorDetails(error.message, location, domainObject, portBindings, data.alternateDomains); + if (error) return callback(error); - scheduleTask(appId, { oldConfig: getAppConfig(app) }, function (error, result) { - if (error) return callback(error); + eventlog.add(eventlog.ACTION_APP_CONFIGURE, auditSource, { appId: appId, app: result }); - eventlog.add(eventlog.ACTION_APP_CONFIGURE, auditSource, { appId: appId, app: result }); - - callback(null, { taskId: result.taskId }); - }); + callback(null, { taskId: result.taskId }); }); }); }); @@ -897,7 +901,7 @@ function update(appId, data, auditSource, callback) { get(appId, function (error, app) { if (error) return callback(error); - if (app.installationState !== appdb.ISTATE_INSTALLED && app.installationState !== appdb.ISTATE_ERROR) return callback(new AppsError(AppsError.BAD_STATE, `Not allowed in this app state : ${app.installationState}`)); + if (app.taskId) return callback(new AppsError(AppsError.BAD_STATE, `Not allowed in this app state : ${app.installationState} / ${app.runState}`)); downloadManifest(data.appStoreId, data.manifest, function (error, appStoreId, manifest) { if (error) return callback(error); @@ -951,20 +955,15 @@ function update(appId, data, auditSource, callback) { updateConfig.memoryLimit = updateConfig.manifest.memoryLimit; } - appdb.setInstallationCommand(appId, appdb.ISTATE_PENDING_UPDATE, { }, function (error) { - if (error && error.reason === DatabaseError.NOT_FOUND) return callback(new AppsError(AppsError.BAD_STATE)); // might be a bad guess - if (error) return callback(new AppsError(AppsError.INTERNAL_ERROR, error)); + scheduleTask(appId, { updateConfig: updateConfig }, { installationState: appdb.ISTATE_PENDING_UPDATE }, function (error, result) { + if (error) return callback(error); - scheduleTask(appId, { updateConfig: updateConfig }, function (error, result) { - if (error) return callback(error); + eventlog.add(eventlog.ACTION_APP_UPDATE, auditSource, { appId: appId, toManifest: manifest, fromManifest: app.manifest, force: data.force, app: app }); - eventlog.add(eventlog.ACTION_APP_UPDATE, auditSource, { appId: appId, toManifest: manifest, fromManifest: app.manifest, force: data.force, app: app }); + // clear update indicator, if update fails, it will come back through the update checker + updateChecker.resetAppUpdateInfo(appId); - // clear update indicator, if update fails, it will come back through the update checker - updateChecker.resetAppUpdateInfo(appId); - - callback(null, { taskId: result.taskId }); - }); + callback(null, { taskId: result.taskId }); }); }); }); @@ -1034,7 +1033,7 @@ function restore(appId, data, auditSource, callback) { get(appId, function (error, app) { if (error) return callback(error); - if (app.installationState !== appdb.ISTATE_INSTALLED && app.installationState !== appdb.ISTATE_ERROR) return callback(new AppsError(AppsError.BAD_STATE, `Not allowed in this app state : ${app.installationState}`)); + if (app.taskId) return callback(new AppsError(AppsError.BAD_STATE, `Not allowed in this app state : ${app.installationState} / ${app.runState}`)); // for empty or null backupId, use existing manifest to mimic a reinstall var func = data.backupId ? backups.get.bind(null, data.backupId) : function (next) { return next(null, { manifest: app.manifest }); }; @@ -1050,22 +1049,18 @@ function restore(appId, data, auditSource, callback) { error = checkManifestConstraints(backupInfo.manifest); if (error) return callback(error); - var values = { + let values = { + installationState: appdb.ISTATE_PENDING_RESTORE, manifest: backupInfo.manifest }; - appdb.setInstallationCommand(appId, appdb.ISTATE_PENDING_RESTORE, values, function (error) { - if (error && error.reason === DatabaseError.NOT_FOUND) return callback(new AppsError(AppsError.BAD_STATE)); // might be a bad guess - if (error) return callback(new AppsError(AppsError.INTERNAL_ERROR, error)); + const restoreConfig = data.backupId ? { backupId: data.backupId, backupFormat: backupInfo.format, oldManifest: app.manifest } : null; // when null, apptask simply reinstalls + scheduleTask(appId, { restoreConfig }, values, function (error, result) { + if (error) return callback(error); - const restoreConfig = data.backupId ? { backupId: data.backupId, backupFormat: backupInfo.format, oldManifest: app.manifest } : null; // when null, apptask simply reinstalls - scheduleTask(appId, { restoreConfig }, function (error, result) { - if (error) return callback(error); + eventlog.add(eventlog.ACTION_APP_RESTORE, auditSource, { app: app, backupId: backupInfo.id, fromManifest: app.manifest, toManifest: backupInfo.manifest }); - eventlog.add(eventlog.ACTION_APP_RESTORE, auditSource, { app: app, backupId: backupInfo.id, fromManifest: app.manifest, toManifest: backupInfo.manifest }); - - callback(null, { taskId: result.taskId }); - }); + callback(null, { taskId: result.taskId }); }); }); }); @@ -1161,7 +1156,7 @@ function clone(appId, data, user, auditSource, callback) { }; appdb.add(newAppId, app.appStoreId, manifest, location, domain, translatePortBindings(portBindings, manifest), data, function (error) { - if (error && error.reason === DatabaseError.ALREADY_EXISTS) return callback(getDuplicateErrorDetails(error, location, domainObject, portBindings, [])); + if (error && error.reason === DatabaseError.ALREADY_EXISTS) return callback(getDuplicateErrorDetails(error.message, location, domainObject, portBindings, [])); if (error) return callback(new AppsError(AppsError.INTERNAL_ERROR, error)); purchaseApp({ appId: newAppId, appstoreId: app.appStoreId, manifestId: manifest.id }, function (error) { @@ -1169,7 +1164,7 @@ function clone(appId, data, user, auditSource, callback) { const restoreConfig = { backupId: backupId, backupFormat: backupInfo.format }; - scheduleTask(newAppId, { restoreConfig }, function (error, result) { + scheduleTask(newAppId, { restoreConfig }, { installationState: appdb.ISTATE_PENDING_CLONE }, function (error, result) { if (error) return callback(error); eventlog.add(eventlog.ACTION_APP_CLONE, auditSource, { appId: newAppId, oldAppId: appId, backupId: backupId, oldApp: app, newApp: result }); @@ -1192,7 +1187,7 @@ function uninstall(appId, auditSource, callback) { get(appId, function (error, app) { if (error) return callback(error); - if (app.installationState !== appdb.ISTATE_INSTALLED && app.installationState !== appdb.ISTATE_ERROR) return callback(new AppsError(AppsError.BAD_STATE, `Not allowed in this app state : ${app.installationState}`)); + if (app.taskId) return callback(new AppsError(AppsError.BAD_STATE, `Not allowed in this app state : ${app.installationState} / ${app.runState}`)); appstore.unpurchaseApp(appId, { appstoreId: app.appStoreId, manifestId: app.manifest.id }, function (error) { if (error && error.reason === AppstoreError.NOT_FOUND) return callback(new AppsError(AppsError.NOT_FOUND)); @@ -1201,17 +1196,12 @@ function uninstall(appId, auditSource, callback) { if (error && error.reason === AppstoreError.EXTERNAL_ERROR) return callback(new AppsError(AppsError.EXTERNAL_ERROR, error.message)); if (error) return callback(new AppsError(AppsError.INTERNAL_ERROR, error)); - appdb.update(appId, { installationState: appdb.ISTATE_PENDING_UNINSTALL }, function (error) { - if (error && error.reason === DatabaseError.NOT_FOUND) return callback(new AppsError(AppsError.NOT_FOUND, 'No such app')); - if (error) return callback(new AppsError(AppsError.INTERNAL_ERROR, error)); + scheduleTask(appId, { /* args */ }, { installationState: appdb.ISTATE_PENDING_UNINSTALL }, function (error, result) { + if (error) return callback(error); - scheduleTask(appId, {}, function (error, result) { - if (error) return callback(error); + eventlog.add(eventlog.ACTION_APP_UNINSTALL, auditSource, { appId: appId, app: result }); - eventlog.add(eventlog.ACTION_APP_UNINSTALL, auditSource, { appId: appId, app: result }); - - callback(null, { taskId: result.taskId }); - }); + callback(null, { taskId: result.taskId }); }); }); }); @@ -1225,15 +1215,9 @@ function start(appId, callback) { get(appId, function (error, app) { if (error) return callback(error); - if (app.installationState !== appdb.ISTATE_INSTALLED) return callback(new AppsError(AppsError.BAD_STATE, `Not allowed in this app state : ${app.installationState}`)); - if (app.runState !== appdb.RSTATE_STOPPED) return callback(new AppsError(AppsError.BAD_STATE, `Not allowed in this app state : ${app.installationState}`)); + if (app.taskId) return callback(new AppsError(AppsError.BAD_STATE, `Not allowed in this app state : ${app.installationState} / ${app.runState}`)); - appdb.setRunCommand(appId, appdb.RSTATE_PENDING_START, function (error) { - if (error && error.reason === DatabaseError.NOT_FOUND) return callback(new AppsError(AppsError.BAD_STATE)); // might be a bad guess - if (error) return callback(new AppsError(AppsError.INTERNAL_ERROR, error)); - - scheduleTask(appId, {}, callback); - }); + scheduleTask(appId, { /* args */ }, { runState: appdb.RSTATE_PENDING_START }, callback); }); } @@ -1245,15 +1229,9 @@ function stop(appId, callback) { get(appId, function (error, app) { if (error) return callback(error); - if (app.installationState !== appdb.ISTATE_INSTALLED) return callback(new AppsError(AppsError.BAD_STATE, `Not allowed in this app state : ${app.installationState}`)); - if (app.runState !== appdb.RSTATE_RUNNING) return callback(new AppsError(AppsError.BAD_STATE, `Not allowed in this app state : ${app.installationState}`)); + if (app.taskId) return callback(new AppsError(AppsError.BAD_STATE, `Not allowed in this app state : ${app.installationState} / ${app.runState}`)); - appdb.setRunCommand(appId, appdb.RSTATE_PENDING_STOP, function (error) { - if (error && error.reason === DatabaseError.NOT_FOUND) return callback(new AppsError(AppsError.BAD_STATE)); // might be a bad guess - if (error) return callback(new AppsError(AppsError.INTERNAL_ERROR, error)); - - scheduleTask(appId, {}, callback); - }); + scheduleTask(appId, { /* args */ }, { runState: appdb.RSTATE_PENDING_STOP }, callback); }); } @@ -1393,24 +1371,18 @@ function backup(appId, callback) { assert.strictEqual(typeof appId, 'string'); assert.strictEqual(typeof callback, 'function'); - appdb.exists(appId, function (error, exists) { - if (error) return callback(new AppsError(AppsError.INTERNAL_ERROR, error)); - if (!exists) return callback(new AppsError(AppsError.NOT_FOUND)); + get(appId, function (error, app) { + if (error) return callback(error); + if (app.taskId) return callback(new AppsError(AppsError.BAD_STATE, `Not allowed in this app state : ${app.installationState} / ${app.runState}`)); - appdb.setInstallationCommand(appId, appdb.ISTATE_PENDING_BACKUP, function (error) { - if (error && error.reason === DatabaseError.NOT_FOUND) return callback(new AppsError(AppsError.BAD_STATE)); // might be a bad guess - if (error) return callback(new AppsError(AppsError.INTERNAL_ERROR, error)); + scheduleTask(appId, { /* args */ }, { installationState: appdb.ISTATE_PENDING_BACKUP }, (error, result) => { + if (error) return callback(error); - scheduleTask(appId, { }, (error, result) => { - if (error) return callback(error); - - callback(null, { taskId: result.taskId }); - }); + callback(null, { taskId: result.taskId }); }); }); } - function listBackups(page, perPage, appId, callback) { assert(typeof page === 'number' && page > 0); assert(typeof perPage === 'number' && perPage > 0); @@ -1442,10 +1414,10 @@ function restoreInstalledApps(callback) { debug(`marking ${app.fqdn} for restore using restore config ${JSON.stringify(restoreConfig)}`); - appdb.setInstallationCommand(app.id, appdb.ISTATE_PENDING_RESTORE, { restoreConfig: restoreConfig }, function (error) { + appdb.update(app.id, { taskId: null }, function (error) { // clear any stale taskId if (error) debug(`Error marking ${app.fqdn} for restore: ${JSON.stringify(error)}`); - scheduleTask(app.id, { restoreConfig }, () => iteratorDone()); // always succeed + scheduleTask(app.id, { restoreConfig }, { installationState: appdb.ISTATE_PENDING_RESTORE }, () => iteratorDone()); // always succeed }); }); }, callback); @@ -1461,10 +1433,10 @@ function configureInstalledApps(callback) { async.map(apps, function (app, iteratorDone) { debug(`marking ${app.fqdn} for reconfigure`); - appdb.setInstallationCommand(app.id, appdb.ISTATE_PENDING_CONFIGURE, { }, function (error) { + appdb.update(app.id, { taskId: null }, function (error) { // clear any stale taskId if (error) debug(`Error marking ${app.fqdn} for reconfigure: ${JSON.stringify(error)}`); - scheduleTask(app.id, { oldConfig: getAppConfig(app) }, () => iteratorDone()); // always succeed + scheduleTask(app.id, { oldConfig: getAppConfig(app) }, { installationState: appdb.ISTATE_PENDING_CONFIGURE }, () => iteratorDone()); // always succeed }); }, callback); });