merge updatechecker into updater
This commit is contained in:
+146
-27
@@ -9,10 +9,18 @@ exports = module.exports = {
|
||||
|
||||
autoUpdate,
|
||||
|
||||
notifyBoxUpdate
|
||||
notifyBoxUpdate,
|
||||
|
||||
checkForUpdates,
|
||||
|
||||
getAppUpdateInfoSync,
|
||||
getUpdateInfoSync,
|
||||
|
||||
_checkAppUpdates: checkAppUpdates
|
||||
};
|
||||
|
||||
const apps = require('./apps.js'),
|
||||
appstore = require('./appstore.js'),
|
||||
assert = require('assert'),
|
||||
AuditSource = require('./auditsource.js'),
|
||||
BoxError = require('./boxerror.js'),
|
||||
@@ -37,7 +45,6 @@ const apps = require('./apps.js'),
|
||||
settings = require('./settings.js'),
|
||||
shell = require('./shell.js')('updater'),
|
||||
tasks = require('./tasks.js'),
|
||||
updateChecker = require('./updatechecker.js'),
|
||||
_ = require('./underscore.js');
|
||||
|
||||
const RELEASES_PUBLIC_KEY = path.join(__dirname, 'releases.gpg');
|
||||
@@ -60,7 +67,7 @@ async function getAutoupdatePattern() {
|
||||
return pattern || cron.DEFAULT_AUTOUPDATE_PATTERN;
|
||||
}
|
||||
|
||||
async function downloadUrl(url, file) {
|
||||
async function downloadBoxUrl(url, file) {
|
||||
assert.strictEqual(typeof file, 'string');
|
||||
|
||||
// do not assert since it comes from the appstore
|
||||
@@ -69,44 +76,44 @@ async function downloadUrl(url, file) {
|
||||
safe.fs.unlinkSync(file);
|
||||
|
||||
await promiseRetry({ times: 10, interval: 5000, debug }, async function () {
|
||||
debug(`downloadUrl: downloading ${url} to ${file}`);
|
||||
debug(`downloadBoxUrl: downloading ${url} to ${file}`);
|
||||
const [error] = await safe(shell.spawn('curl', ['-s', '--fail', url, '-o', file], {}));
|
||||
if (error) throw new BoxError(BoxError.NETWORK_ERROR, `Failed to download ${url}: ${error.message}`);
|
||||
debug('downloadUrl: done');
|
||||
debug('downloadBoxUrl: done');
|
||||
});
|
||||
}
|
||||
|
||||
async function gpgVerify(file, sig) {
|
||||
async function gpgVerifyBoxTarball(file, sig) {
|
||||
assert.strictEqual(typeof file, 'string');
|
||||
assert.strictEqual(typeof sig, 'string');
|
||||
|
||||
const [error, stdout] = await safe(shell.spawn('/usr/bin/gpg', ['--status-fd', '1', '--no-default-keyring', '--keyring', RELEASES_PUBLIC_KEY, '--verify', sig, file], { encoding: 'utf8' }));
|
||||
if (error) {
|
||||
debug(`gpgVerify: command failed. error: ${error}\n stdout: ${error.stdout}\n stderr: ${error.stderr}`);
|
||||
debug(`gpgVerifyBoxTarball: 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; // success
|
||||
|
||||
debug(`gpgVerify: verification of ${sig} failed: ${stdout}\n`);
|
||||
debug(`gpgVerifyBoxTarball: verification of ${sig} failed: ${stdout}\n`);
|
||||
|
||||
throw new BoxError(BoxError.NOT_SIGNED, `The signature in ${path.basename(sig)} could not be verified (bad sig)`);
|
||||
}
|
||||
|
||||
async function extractTarball(tarball, dir) {
|
||||
async function extractBoxTarball(tarball, dir) {
|
||||
assert.strictEqual(typeof tarball, 'string');
|
||||
assert.strictEqual(typeof dir, 'string');
|
||||
|
||||
debug(`extractTarball: extracting ${tarball} to ${dir}`);
|
||||
debug(`extractBoxTarball: extracting ${tarball} to ${dir}`);
|
||||
|
||||
const [error] = await safe(shell.spawn('tar', ['-zxf', tarball, '-C', dir], {}));
|
||||
if (error) throw new BoxError(BoxError.FS_ERROR, `Failed to extract release package: ${error.message}`);
|
||||
safe.fs.unlinkSync(tarball);
|
||||
|
||||
debug('extractTarball: extracted');
|
||||
debug('extractBoxTarball: extracted');
|
||||
}
|
||||
|
||||
async function verifyUpdateInfo(versionsFile, updateInfo) {
|
||||
async function verifyBoxUpdateInfo(versionsFile, updateInfo) {
|
||||
assert.strictEqual(typeof versionsFile, 'string');
|
||||
assert.strictEqual(typeof updateInfo, 'object');
|
||||
|
||||
@@ -118,29 +125,29 @@ async function verifyUpdateInfo(versionsFile, updateInfo) {
|
||||
if (releases[nextVersion].sourceTarballUrl !== updateInfo.sourceTarballUrl) throw new BoxError(BoxError.EXTERNAL_ERROR, 'Version info mismatch');
|
||||
}
|
||||
|
||||
async function downloadAndVerifyRelease(updateInfo) {
|
||||
async function downloadAndVerifyBoxRelease(updateInfo) {
|
||||
assert.strictEqual(typeof updateInfo, 'object');
|
||||
|
||||
const filenames = await fs.promises.readdir(os.tmpdir());
|
||||
const oldArtifactNames = filenames.filter(f => f.startsWith('box-'));
|
||||
for (const artifactName of oldArtifactNames) {
|
||||
const fullPath = path.join(os.tmpdir(), artifactName);
|
||||
debug(`downloadAndVerifyRelease: removing old artifact ${fullPath}`);
|
||||
debug(`downloadAndVerifyBoxRelease: removing old artifact ${fullPath}`);
|
||||
await fs.promises.rm(fullPath, { recursive: true, force: true });
|
||||
}
|
||||
|
||||
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`);
|
||||
await downloadBoxUrl(updateInfo.boxVersionsUrl, `${paths.UPDATE_DIR}/versions.json`);
|
||||
await downloadBoxUrl(updateInfo.boxVersionsSigUrl, `${paths.UPDATE_DIR}/versions.json.sig`);
|
||||
await gpgVerifyBoxTarball(`${paths.UPDATE_DIR}/versions.json`, `${paths.UPDATE_DIR}/versions.json.sig`);
|
||||
await verifyBoxUpdateInfo(`${paths.UPDATE_DIR}/versions.json`, updateInfo);
|
||||
await downloadBoxUrl(updateInfo.sourceTarballUrl, `${paths.UPDATE_DIR}/box.tar.gz`);
|
||||
await downloadBoxUrl(updateInfo.sourceTarballSigUrl, `${paths.UPDATE_DIR}/box.tar.gz.sig`);
|
||||
await gpgVerifyBoxTarball(`${paths.UPDATE_DIR}/box.tar.gz`, `${paths.UPDATE_DIR}/box.tar.gz.sig`);
|
||||
|
||||
const newBoxSource = path.join(os.tmpdir(), 'box-' + crypto.randomBytes(4).readUInt32LE(0));
|
||||
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);
|
||||
await extractBoxTarball(`${paths.UPDATE_DIR}/box.tar.gz`, newBoxSource);
|
||||
|
||||
return { file: newBoxSource };
|
||||
}
|
||||
@@ -166,7 +173,7 @@ async function updateBox(boxUpdateInfo, options, progressCallback) {
|
||||
|
||||
progressCallback({ percent: 5, message: 'Downloading and verifying release' });
|
||||
|
||||
const packageInfo = await downloadAndVerifyRelease(boxUpdateInfo);
|
||||
const packageInfo = await downloadAndVerifyBoxRelease(boxUpdateInfo);
|
||||
|
||||
if (!options.skipBackup) {
|
||||
progressCallback({ percent: 10, message: 'Backing up' });
|
||||
@@ -186,7 +193,7 @@ async function updateBox(boxUpdateInfo, options, progressCallback) {
|
||||
// Do not add any code here. The installer script will stop the box code any instant
|
||||
}
|
||||
|
||||
async function checkUpdateRequirements(boxUpdateInfo) {
|
||||
async function checkBoxUpdateRequirements(boxUpdateInfo) {
|
||||
assert.strictEqual(typeof boxUpdateInfo, 'object');
|
||||
|
||||
const result = await apps.list();
|
||||
@@ -203,12 +210,12 @@ async function startBoxUpdateTask(options, auditSource) {
|
||||
assert.strictEqual(typeof options, 'object');
|
||||
assert.strictEqual(typeof auditSource, 'object');
|
||||
|
||||
const boxUpdateInfo = updateChecker.getUpdateInfo().box;
|
||||
const boxUpdateInfo = getUpdateInfoSync().box;
|
||||
if (!boxUpdateInfo) throw new BoxError(BoxError.NOT_FOUND, 'No update available');
|
||||
if (!boxUpdateInfo.sourceTarballUrl) throw new BoxError(BoxError.BAD_STATE, 'No automatic update available');
|
||||
if (semver.gte(constants.VERSION, boxUpdateInfo.version)) throw new BoxError(BoxError.NOT_FOUND, 'No update available'); // can happen after update completed or hotfix
|
||||
|
||||
await checkUpdateRequirements(boxUpdateInfo);
|
||||
await checkBoxUpdateRequirements(boxUpdateInfo);
|
||||
|
||||
const [error] = await safe(locks.acquire(locks.TYPE_UPDATE_TASK));
|
||||
if (error) throw new BoxError(BoxError.BAD_STATE, `Another update task is in progress: ${error.message}`);
|
||||
@@ -254,7 +261,7 @@ async function notifyBoxUpdate() {
|
||||
async function autoUpdate(auditSource) {
|
||||
assert.strictEqual(typeof auditSource, 'object');
|
||||
|
||||
const updateInfo = updateChecker.getUpdateInfo();
|
||||
const updateInfo = getUpdateInfoSync();
|
||||
debug('autoUpdate: available updates: %j', Object.keys(updateInfo));
|
||||
|
||||
// do box before app updates. for the off chance that the box logic fixes some app update logic issue
|
||||
@@ -291,3 +298,115 @@ async function autoUpdate(auditSource) {
|
||||
if (updateError) debug(`autoUpdate: error autoupdating ${app.fqdn}: ${updateError.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
function setUpdateInfo(state) {
|
||||
// appid -> update info { creationDate, manifest }
|
||||
// box -> { version, changelog, upgrade, sourceTarballUrl }
|
||||
state.version = 2;
|
||||
if (!safe.fs.writeFileSync(paths.UPDATE_CHECKER_FILE, JSON.stringify(state, null, 4), 'utf8')) debug(`setUpdateInfo: Error writing to update checker file: ${safe.error.message}`);
|
||||
}
|
||||
|
||||
function getUpdateInfoSync() {
|
||||
const state = safe.JSON.parse(safe.fs.readFileSync(paths.UPDATE_CHECKER_FILE, 'utf8'));
|
||||
if (!state || state.version !== 2) return {};
|
||||
delete state.version;
|
||||
return state;
|
||||
}
|
||||
|
||||
function getAppUpdateInfoSync(appId) {
|
||||
assert.strictEqual(typeof appId, 'string');
|
||||
|
||||
return getUpdateInfoSync()[appId] || null;
|
||||
}
|
||||
|
||||
async function checkAppUpdates(options) {
|
||||
assert.strictEqual(typeof options, 'object');
|
||||
|
||||
debug('checkAppUpdates: checking for updates');
|
||||
|
||||
const state = getUpdateInfoSync();
|
||||
const newState = { }; // create new state so that old app ids are removed
|
||||
|
||||
const result = await apps.list();
|
||||
|
||||
for (const app of result) {
|
||||
if (app.appStoreId === '') continue; // appStoreId can be '' for dev apps
|
||||
|
||||
const [error, updateInfo] = await safe(appstore.getAppUpdate(app, options));
|
||||
if (error) {
|
||||
debug('checkAppUpdates: Error getting app update info for %s', app.id, error);
|
||||
continue; // continue to next
|
||||
}
|
||||
|
||||
if (!updateInfo) continue; // skip if no next version is found
|
||||
newState[app.id] = updateInfo;
|
||||
}
|
||||
|
||||
if ('box' in state) newState.box = state.box; // preserve the latest box state information
|
||||
setUpdateInfo(newState);
|
||||
}
|
||||
|
||||
async function checkBoxUpdates(options) {
|
||||
assert.strictEqual(typeof options, 'object');
|
||||
|
||||
debug('checkBoxUpdates: checking for updates');
|
||||
|
||||
const updateInfo = await appstore.getBoxUpdate(options);
|
||||
|
||||
const state = getUpdateInfoSync();
|
||||
|
||||
if (!updateInfo) { // no update
|
||||
if ('box' in state) {
|
||||
delete state.box;
|
||||
setUpdateInfo(state);
|
||||
}
|
||||
debug('checkBoxUpdates: no updates');
|
||||
return;
|
||||
}
|
||||
|
||||
debug(`checkBoxUpdates: ${updateInfo.version} is available`);
|
||||
|
||||
state.box = updateInfo;
|
||||
setUpdateInfo(state);
|
||||
}
|
||||
|
||||
async function raiseNotifications() {
|
||||
const state = getUpdateInfoSync();
|
||||
|
||||
const pattern = await getAutoupdatePattern();
|
||||
|
||||
if (pattern === constants.AUTOUPDATE_PATTERN_NEVER && state.box) {
|
||||
const updateInfo = state.box;
|
||||
const changelog = updateInfo.changelog.map((m) => `* ${m}\n`).join('');
|
||||
const message = `Changelog:\n${changelog}\n\nGo to the Settings view to update.\n\n`;
|
||||
|
||||
await notifications.pin(notifications.TYPE_BOX_UPDATE, `Cloudron v${updateInfo.version} is available`, message, { context: updateInfo.version });
|
||||
}
|
||||
|
||||
const result = await apps.list();
|
||||
for (const app of result) {
|
||||
// currently, we do not raise notifications when auto-update is disabled. separate notifications appears spammy when having many apps
|
||||
// in the future, we can maybe aggregate?
|
||||
if (app.updateInfo && !app.updateInfo.isAutoUpdatable) {
|
||||
debug(`autoUpdate: ${app.fqdn} cannot be autoupdated. skipping`);
|
||||
await notifications.pin(notifications.TYPE_MANUAL_APP_UPDATE_NEEDED, `${app.manifest.title} at ${app.fqdn} requires manual update to version ${app.updateInfo.manifest.version}`,
|
||||
`Changelog:\n${app.updateInfo.manifest.changelog}\n`, { context: app.id });
|
||||
continue;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function checkForUpdates(options) {
|
||||
assert.strictEqual(typeof options, 'object');
|
||||
|
||||
const [boxError] = await safe(checkBoxUpdates(options));
|
||||
if (boxError) debug('checkForUpdates: error checking for box updates: %o', boxError);
|
||||
|
||||
const [appError] = await safe(checkAppUpdates(options));
|
||||
if (appError) debug('checkForUpdates: error checking for app updates: %o', appError);
|
||||
|
||||
// raise notifications here because the updatechecker runs regardless of auto-updater cron job
|
||||
await raiseNotifications();
|
||||
|
||||
return getUpdateInfoSync();
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user