import cluster from 'cluster' import { Low } from 'lowdb' import Application from './application.mjs' import Util from './util.mjs' import getLog from './log.mjs' import StaticProvider from './providers/static.mjs' import GitProvider from './providers/git.mjs' export default class Core { static providers = new Map() static addProvider(name, provider) { if (!name || typeof(name) !== 'string') throw new Error('addProvider name must be a string') if (typeof(provider) !== 'function') throw new Error(`addProvider ${name} provider must be a class`) let test = new provider({}) if (typeof(test.checkConfig) !== 'function') throw new Error(`addProvider ${name} provider class missing checkConfig`) if (typeof(test.getLatestVersion) !== 'function') throw new Error(`addProvider ${name} provider class missing getLatestVersion`) Core.providers.set(name, provider) } constructor(db, util, log, restart = function() {}) { // some sanity checks if (!log || typeof(log) !== 'object') throw new Error('log parameter was invalid') if (typeof(log.event) !== 'object') throw new Error('log parameter was invalid') if (typeof(log.info) !== 'function' || typeof(log.warn) !== 'function' || typeof(log.error) !== 'function' || typeof(log.event.info) !== 'function' || typeof(log.event.warn) !== 'function' || typeof(log.event.error) !== 'function') throw new Error('log parameter was invalid') if (!util || !(util instanceof Util)) throw new Error('util not instance of Util') if (!db || !(db instanceof Low)) throw new Error('db not instance of Low') if (typeof(restart) !== 'function') throw new Error('restart was not a function') this.running = false this.db = db this.util = util this.log = log this.restart = restart this.applications = [] this.applicationMap = new Map() this._applicationFatalCrash = null this.isSlave = cluster.isWorker } getApplication(name) { return this.applicationMap.get(name) } async init() { this.log.info(`Verifying config`) this.util.verifyConfig(this.db.config) let names = this.util.getAppNames(this.db.config) this.log.info(`Found applications: ${names.join(', ')}.`) for (let name of names) { if (this.isSlave && process.env.CLUSTER_APP_NAME !== name) { continue } try { let provConstructor = Core.providers.get(this.db.config[name].provider) let provider = new provConstructor(this.db.config[name]) if (!this.isSlave) { await provider.checkConfig(this.db.config[name]) } let logName = name if (this.isSlave && process.env.CLUSTER_APP_INDEX) { logName += '-' + process.env.CLUSTER_APP_INDEX } let application = new Application({ db: this.db, util: this.util, log: getLog(logName, this.db.config[name].log || null), core: this, }, provider, name) this.applications.push(application) this.applicationMap.set(name, application) } catch (err) { this.log.error(err, `Error creating application ${name} with provider ${this.db.config[name].provider}: ${err.message}`) } } if (names.length && !this.applications.length) { return Promise.reject(new Error('None of the application were successful in running')) } } async run() { if (this.running) return this.running = true if (!this.isSlave) { this.log.info(`Running updater on ${this.applications.length} apps`) await Promise.all(this.applications.map((app) => { return app.update().catch(err => { app.ctx.log.error(err, `Error updating: ${err.message}`) }) })) } let found = false if (this.isSlave) { let app = this.getApplication(process.env.CLUSTER_APP_NAME) try { await app.runVersion(process.env.CLUSTER_APP_VERSION) } catch (err) { app.ctx.log.fatal(err) return Promise.reject(err) } return } for (let app of this.applications) { app.startAutoupdater() await this.runApplication(app).then( () => { found = true }, err => { app.ctx.log.error(err, `Error running: ${err.message}`) } ) app.on('updated', this.runApplication.bind(this, app)) } if (!found) { throw new Error('No stable application was found') } } criticalError(application, version, errorCode) { application.ctx.log.fatal(`Critical error ${errorCode} running ${version.version}`) version.stable = -2 this.db.writeSync() } async runApplication(application) { let name = application.name let found = false if (!this.db.data.core[name].versions.length) { return Promise.reject(new Error(`No versions were found`)) } for (let i = 0; i < this.db.data.core[name].versions.length; i++) { let version = this.db.data.core[name].versions[i] if (!version.installed || version.stable < -1) continue if (version.stable < 0 && !application.fresh) { application.ctx.log.warn(`Restarting for ${version.version} due to last run failing while not being in fresh state`) return this.restart(`Application ${name} has fresh false while attempting to run ${version.version} with stable -1`) } await application.closeServer() this._applicationFatalCrash = this.criticalError.bind(this, application, version) process.once('exit', this._applicationFatalCrash) let wasFresh = application.fresh try { application.ctx.log.info(`Attempting to run version ${version.version}`) await application.runVersion(version.version) found = true version.stable = 1 await this.db.write() application.ctx.log.info(`${version.version} is up and running`) break } catch(err) { application.ctx.log.error(err, `Error starting ${version.version}: ${err.message}`) process.off('exit', this._applicationFatalCrash) if (version.stable < 1) { if (wasFresh) { version.stable = -2 } else { version.stable = -1 } await this.db.write() if (version.stable === -1) { return this.restart(`Application ${name} version ${version.version} failed to start but application was dirty, check if restarting fixes it`) } } else { await this.db.write() return this.restart(`Application ${name} version ${version.version} previously stable but now failing`) } } finally { process.off('exit', this._applicationFatalCrash) } } if (!found) { return Promise.reject(Error(`No stable versions were found`)) } } } Core.addProvider('static', StaticProvider) Core.addProvider('git', GitProvider)