diff --git a/config.json b/config.json index 9a09038..c32ba6c 100644 --- a/config.json +++ b/config.json @@ -3,7 +3,8 @@ "serviceName": "Service-Core Node", "description": "NodeJS Test Service", "port": 4270, - "managePort": 4269, + "managePort": 4271, + "devPort": 4269, "appRepository": "thething/sc-helloworld", "manageRepository": null } \ No newline at end of file diff --git a/core/core.mjs b/core/core.mjs index 3eaa36a..d570a36 100644 --- a/core/core.mjs +++ b/core/core.mjs @@ -1,26 +1,28 @@ import fs from 'fs' import { EventEmitter } from 'events' import { request } from './client.mjs' +import HttpServer from './http.mjs' const fsp = fs.promises export default class Core extends EventEmitter{ constructor(util, config, db, log, closeCb) { super() - this._util = util - this._config = config - this._db = db - this._log = log + this.http = new HttpServer() + this.util = util + this.config = config + this.db = db + this.log = log this._close = closeCb - this._appRunning = false - this._manageRunning = false + this.appRunning = false + this.manageRunning = false this._appUpdating = { - status: false, + updating: false, starting: false, logs: '', } this._manageUpdating = { - status: false, + updating: false, starting: false, logs: '', } @@ -32,19 +34,21 @@ export default class Core extends EventEmitter{ status() { return { - app: this._appRunning, - manage: this._manageRunning, - appUpdating: this._appUpdating.status, - manageUpdating: this._manageUpdating.status, + app: this.appRunning, + manage: this.manageRunning, + appUpdating: this._appUpdating.updating, + manageUpdating: this._manageUpdating.updating, + appStarting: this._appUpdating.starting, + manageStarting: this._manageUpdating.starting, } } async getLatestVersion(active, name) { // Example: 'https://api.github.com/repos/thething/sc-helloworld/releases' - this.logActive(name, active, `[Core] Fetching release info from: https://api.github.com/repos/${this._config[name + 'Repository']}/releases\n`) + this.logActive(name, active, `[Core] Fetching release info from: https://api.github.com/repos/${this.config[name + 'Repository']}/releases\n`) - let result = await request(`https://api.github.com/repos/${this._config[name + 'Repository']}/releases`) + let result = await request(`https://api.github.com/repos/${this.config[name + 'Repository']}/releases`) let items = result.body.filter(function(item) { if (!item.assets.length) return false @@ -60,7 +64,7 @@ export default class Core extends EventEmitter{ if (item.assets[i].name.endsWith('-sc.zip')) { this.logActive(name, active, `[Core] Found version ${item.name} with file ${item.assets[i].name}\n`) - await this._db.set(`core.${name}LatestVersion`, item.name) + await this.db.set(`core.${name}LatestVersion`, item.name) .write() this.emit('dbupdated', {}) @@ -80,7 +84,7 @@ export default class Core extends EventEmitter{ logActive(name, active, logline, doNotPrint = false) { if (!doNotPrint) { - this._log.info(`Log ${name}: ` + logline.replace(/\n/g, '')) + this.log.info(`Log ${name}: ` + logline.replace(/\n/g, '')) } active.logs += logline this.emit(name + 'log', active) @@ -93,57 +97,57 @@ export default class Core extends EventEmitter{ return this._manageUpdating.logs } - let latestInstalled = this._db.get('core.' + name + 'LatestInstalled').value() - let latestVersion = this._db.get('core.' + name + 'LatestVersion').value() + let latestInstalled = this.db.get('core.' + name + 'LatestInstalled').value() + let latestVersion = this.db.get('core.' + name + 'LatestVersion').value() if (latestVersion) { - let value = this._db.get(`core_${name}History`).getById(latestVersion).value() + let value = this.db.get(`core_${name}History`).getById(latestVersion).value() if (value) return value.logs } if (latestInstalled) { - let value = this._db.get(`core_${name}History`).getById(latestInstalled).value() + let value = this.db.get(`core_${name}History`).getById(latestInstalled).value() if (value) return value.logs } return '< no logs found >' } async installVersion(name, active, version) { - if (fs.existsSync(this._util.getPathFromRoot(`./${name}/` + version.name))) { - await this._util.runCommand('rmdir', ['/S', '/Q', `"${this._util.getPathFromRoot(`./${name}/` + version.name)}"`]) + if (fs.existsSync(this.util.getPathFromRoot(`./${name}/` + version.name))) { + await this.util.runCommand('rmdir', ['/S', '/Q', `"${this.util.getPathFromRoot(`./${name}/` + version.name)}"`]) } try { - await fsp.mkdir(this._util.getPathFromRoot(`./${name}/` + version.name)) + await fsp.mkdir(this.util.getPathFromRoot(`./${name}/` + version.name)) } catch(err) { if (err.code !== 'EEXIST') { throw err } } - // await fsp.mkdir(this._util.getPathFromRoot(`./${name}/` + version.name + '/node_modules')) + // await fsp.mkdir(this.util.getPathFromRoot(`./${name}/` + version.name + '/node_modules')) this.logActive(name, active, `[Core] Downloading ${version.name} (${version.url}) to ${version.name + '/' + version.name + '.zip'}\n`) - let filePath = this._util.getPathFromRoot(`./${name}/` + version.name + '/' + version.name + '.zip') + let filePath = this.util.getPathFromRoot(`./${name}/` + version.name + '/' + version.name + '.zip') await request(version.url, filePath) this.logActive(name, active, `[Core] Downloading finished, starting extraction\n`) - await this._util.runCommand( + await this.util.runCommand( '"C:\\Program Files\\7-Zip\\7z.exe"', ['x', `"${filePath}"`], - this._util.getPathFromRoot(`./${name}/` + version.name + '/'), + this.util.getPathFromRoot(`./${name}/` + version.name + '/'), this.logActive.bind(this, name, active) ) - if (!fs.existsSync(this._util.getPathFromRoot(`./${name}/` + version.name + '/index.mjs'))) { + if (!fs.existsSync(this.util.getPathFromRoot(`./${name}/` + version.name + '/index.mjs'))) { this.logActive(name, active, `\n[Core] ERROR: Missing index.mjs in the folder, exiting\n`) - throw new Error(`Missing index.mjs in ${this._util.getPathFromRoot(`./${name}/` + version.name + '/index.mjs')}`) + throw new Error(`Missing index.mjs in ${this.util.getPathFromRoot(`./${name}/` + version.name + '/index.mjs')}`) } this.logActive(name, active, `\n[Core] Starting npm install\n`) - await this._util.runCommand( + await this.util.runCommand( 'npm.cmd', ['install', '--production', '--no-optional', '--no-package-lock', '--no-audit'], - this._util.getPathFromRoot(`./${name}/` + version.name + '/'), + this.util.getPathFromRoot(`./${name}/` + version.name + '/'), this.logActive.bind(this, name, active) ) - await this._db.set(`core.${name}LatestInstalled`, version.name) + await this.db.set(`core.${name}LatestInstalled`, version.name) .write() this.emit('dbupdated', {}) @@ -159,36 +163,68 @@ export default class Core extends EventEmitter{ throw new Error('Invalid name: ' + name) } } + + async startModule(module, port) { + let out = await module.start(this.config, this.db, this.log, this, this.http, port) + if (out && out.then) { + await out + } + if (!this.http.getCurrentServer()) { + this.log.warn('Module did not call http.createServer') + } + } - async startProgram(name) { + async tryStartProgram(name) { let active = this.getActive(name) - if ((name === 'app' && this._appRunning) - || (name === 'manage' && this._manageRunning) + if ((name === 'app' && this.appRunning) + || (name === 'manage' && this.manageRunning) || active.starting) { - this._log.event.warn('Attempting to start ' + name + ' which is already running') - this._log.warn('Attempting to start ' + name + ' which is already running') + this.log.event.warn('Attempting to start ' + name + ' which is already running') + this.log.warn('Attempting to start ' + name + ' which is already running') this.logActive(name, active, `[${name}] Attempting to start it but it is already running\n`, true) return } active.starting = true - let core = this._db.get('core').value() - let version = core[name + 'LatestInstalled'] - if (await this.tryStartProgram(name, active, version)) return - version = core[name + 'LastActive'] - if (await this.tryStartProgram(name, active,version)) return + let history = this.db.get(`core_${name}History`) + .filter('installed') + .orderBy('installed', 'desc') + .value() - this._log.error('Unable to start ' + name) - this._log.event.error('Unable to start ' + name) + for (let i = 0; i < history.length; i++) { + if (history[i].stable < 0) continue + + await this.db.set(`core.${name}Active`, history[i].name) + .write() + this.emit('dbupdated', {}) + + let running = await this.tryStartProgramVersion(name, active, history[i].name) + if (running) { + history[i].stable = 1 + } else { + history[i].stable = -1 + await this.db.set(`core.${name}Active`, null) + .write() + this.emit('dbupdated', {}) + } + + await this.db.get(`core_${name}History`).updateById(history[i].id, history[i].stable).write() + if (history[i].stable > 0) break + } + + if (!this.db.get(`core.${name}Active`).value()) { + this.log.error('Unable to start ' + name) + this.log.event.error('Unable to start ' + name) + } active.starting = false } - async tryStartProgram(name, active, version) { + async tryStartProgramVersion(name, active, version) { if (!version) return false this.logActive(name, active, `[${name}] Attempting to start ${version}\n`) - let indexPath = this._util.getUrlFromRoot(`./${name}/` + version + '/index.mjs') + let indexPath = this.util.getUrlFromRoot(`./${name}/` + version + '/index.mjs') let module try { @@ -197,49 +233,50 @@ export default class Core extends EventEmitter{ } catch (err) { this.logActive(name, active, `[${name}] Error importing module\n`, true) this.logActive(name, active, `[${name}] ${err.stack}\n`, true) - this._log.error(err, `Failed to load ${indexPath}`) + this.log.error(err, `Failed to load ${indexPath}`) return false } let checkTimeout = null try { await new Promise((res, rej) => { - try { - let checkTimeout = setTimeout(function() { - rej(new Error('Program took longer than 60 seconds to resolve promise')) - }, 60 * 1000) + let checkTimeout = setTimeout(function() { + rej(new Error('Program took longer than 60 seconds to resolve promise')) + }, 60 * 1000) - this.logActive(name, active, `[${name}] Starting module\n`) - let out = module.start(this._config, this._db, this._log, this) - if (out.then) { - return out.then(res, rej) - } else { - res() - } + this.logActive(name, active, `[${name}] Starting module\n`) + + try { + this.http.setContext(name) + this.startModule(module, name === 'app' ? this.config.port : this.config.managePort) + .then(res, rej) } catch (err) { rej(err) } }) } catch (err) { clearTimeout(checkTimeout) + await this.http.closeServer(name) + this.logActive(name, active, `[${name}] Error starting\n`, true) this.logActive(name, active, `[${name}] ${err.stack}\n`, true) - this._log.error(err, `Failed to start ${name}`) + this.log.error(err, `Failed to start ${name}`) return false } clearTimeout(checkTimeout) this.logActive(name, active, `[${name}] Successfully started version ${version}\n`) - await this._db.set(`core.${name}Active`, version) + await this.db.set(`core.${name}Active`, version) .write() - let port = name === 'app' ? this._config.port : this._config.managePort + let port = name === 'app' ? this.config.port : this.config.managePort this.logActive(name, active, `[${name}] Checking if listening to port ${port}\n`) if (name === 'app') { - this._appRunning = true + this.appRunning = true } else { - this._manageRunning = true + this.manageRunning = true } + this.emit('statusupdated', {}) this.logActive(name, active, `[${name}] Module is running successfully\n`) @@ -247,20 +284,19 @@ export default class Core extends EventEmitter{ } async updateProgram(name) { - if (!this._config[name + 'Repository']) { + if (!this.config[name + 'Repository']) { if (name === 'app') { - this._log.error(name + 'Repository was missing from config') - this._log.event.error(name + 'Repository was missing from config') + this.log.error(name + 'Repository was missing from config') + this.log.event.error(name + 'Repository was missing from config') } else { - this._log.warn(name + 'Repository was missing from config') - this._log.event.warn(name + 'Repository was missing from config') + this.log.warn(name + 'Repository was missing from config') + this.log.event.warn(name + 'Repository was missing from config') } return } let active = this.getActive(name) - active.status = true - active.logs = '' + active.updating = true this.emit('statusupdated', {}) this.logActive(name, active, `[Core] Time: ${new Date().toISOString().replace('T', ' ').split('.')[0]}\n`) @@ -271,9 +307,8 @@ export default class Core extends EventEmitter{ let found = false try { version = await this.getLatestVersion(active, name) - let core = this._db.get('core').value() - let fromDb = this._db.get(`core_${name}History`).getById(version.name).value() - console.log(fromDb) + let core = this.db.get('core').value() + let fromDb = this.db.get(`core_${name}History`).getById(version.name).value() if (!fromDb || !fromDb.installed) { let oldVersion = core[name + 'Current'] || '' this.logActive(name, active, `[Core] Updating from ${oldVersion} to ${version.name}\n`) @@ -282,17 +317,17 @@ export default class Core extends EventEmitter{ installed = new Date() } else { found = true - this.logActive(name, active, `[Core] Version ${version.name} already installed\n\n[Core] Logs from previous install:\n----------------------------------\n\n${fromDb.logs}\n----------------------------------\n[Core] Old logs finished`) + this.logActive(name, active, `[Core] Version ${version.name} already installed\n`) } } catch(err) { this.logActive(name, active, '\n', true) this.logActive(name, active, `[Error] Exception occured while updating ${name}\n`, true) this.logActive(name, active, err.stack, true) - this._log.error(err, 'Error while updating ' + name) + this.log.error(err, 'Error while updating ' + name) } - active.status = false + active.updating = false if (version && !found) { - await this._db.get(`core_${name}History`).upsert({ + await this.db.get(`core_${name}History`).upsert({ id: version.name, name: version.name, filename: version.filename, @@ -308,8 +343,9 @@ export default class Core extends EventEmitter{ async start(name) { await this.updateProgram(name) - if (core[name + 'CurrentVersion']) { - await this.startProgram(name) + var version = this.db.get('core.' + name + 'LatestVersion').value() + if (version) { + await this.tryStartProgram(name) } } } \ No newline at end of file diff --git a/core/db.mjs b/core/db.mjs index 14add32..970a58f 100644 --- a/core/db.mjs +++ b/core/db.mjs @@ -141,11 +141,9 @@ export default function GetDB(util, log) { db.defaults({ core: { "appActive": null, // Current active running - "appLastActive": null, // Last active stable running "appLatestInstalled": null, // Latest installed version "appLatestVersion": null, // Newest version available "manageActive": null, - "manageLastActive": null, "manageLatestInstalled": null, "manageLatestVersion": null }, diff --git a/core/http.mjs b/core/http.mjs new file mode 100644 index 0000000..22e0206 --- /dev/null +++ b/core/http.mjs @@ -0,0 +1,66 @@ +import http from 'http' + +export default class HttpServer { + constructor() { + this.active = { + app: false, + manage: false, + dev: false, + } + this.sockets = { + app: new Set(), + manage: new Set(), + dev: new Set(), + } + this._context = 'dev' + } + + setContext(name) { + if (name !== 'app' && name !== 'manage' && name !== 'dev') { + throw new Error('Cannot call setContext with values other than app or manage') + } + } + + createServer(opts, listener) { + return this._createServer(this._context, opts, listener) + } + + _createServer(name, opts, listener) { + let server = http.createServer(opts, listener) + + server.on('connection', (socket) => { + this.sockets[name].add(socket) + + socket.once('close', () => { + this.sockets[name].delete(socket) + }) + }) + + this.active[name] = server + return server + } + + getServer(name) { + return this.active[name] + } + + closeServer(name) { + if (!this.active[name]) return + + return new Promise((res, rej) => { + this.sockets[name].forEach(function(socket) { + socket.destroy() + }) + this.sockets[name].clear() + + this.active[name].close(function(err) { + if (err) return rej(err) + res() + }) + }) + } + + getCurrentServer() { + return this.active[this._context] + } +} diff --git a/lib.mjs b/lib.mjs index 582c943..b7f43e5 100644 --- a/lib.mjs +++ b/lib.mjs @@ -1,7 +1,8 @@ import Util from './core/util.mjs' import { readFileSync } from 'fs' -import { getLog } from './core/log.mjs' +import getLog from './core/log.mjs' import lowdb from './core/db.mjs' +import Core from './core/core.mjs' export default class ServiceCore { constructor(name, root_import_meta_url) { @@ -18,14 +19,14 @@ export default class ServiceCore { close(err) { if (err) { - log.fatal(err, 'App recorded a fatal error') + this.log.fatal(err, 'App recorded a fatal error') process.exit(4) } - log.warn('App asked to be restarted') + this.log.warn('App asked to be restarted') process.exit(0) } - async init() { + async init(module = null) { try { this.config = JSON.parse(readFileSync(this.util.getPathFromRoot('./config.json'))) } catch (err) { @@ -39,9 +40,13 @@ export default class ServiceCore { } this.core = new Core(this.util, this.config, this.db, this.log, (err) => this.close(err)) + + if (module) { + return this.startModule(module) + } } - async startModule(module) { - return module.start(config, db, log, core) + startModule(module) { + return this.core.startModule(module, this.config.devPort) } } diff --git a/package.json b/package.json index e2eb67b..d1d7f56 100644 --- a/package.json +++ b/package.json @@ -2,7 +2,7 @@ "name": "service-core", "version": "1.0.0", "description": "Core boiler plate code to install node server as windows service", - "main": "lib.js", + "main": "lib.mjs", "scripts": { "dev": "nodemon --watch dev/api --watch core --watch runner.mjs --watch db.mjs --watch log.mjs runner.mjs | bunyan", "test": "echo \"Error: no test specified\" && exit 1" diff --git a/runner.mjs b/runner.mjs index fbd5fb4..5d1109d 100644 --- a/runner.mjs +++ b/runner.mjs @@ -35,13 +35,23 @@ const close = function(err) { const util = new Util(import.meta.url) -lowdb(util, log).then(function(db) { +lowdb(util, log).then(async function(db) { let core = new Core(util, config, db, log, close) - - if (config.useDev) { - return import('./dev/index.mjs').then(function(module) { - return module.start(config, db, log, core) - }) + let errors = 0 + try { + await core.start('app') + } catch (err) { + log.error(err, 'Unable to start app') + errors++ + } + try { + await core.start('manage') + } catch (err) { + log.error(err, 'Unable to start manage') + errors++ + } + if (errors === 2 || (!core.appRunning && !core.manageRunning)) { + throw new Error('Neither manage or app were started, exiting.') } }, function(err) { log.fatal(err, 'Critical error opening database')