Implemented basic service core support

This commit is contained in:
Jonatan Nilsson 2020-09-09 15:41:05 +00:00
parent 9f0dddc5ad
commit 4b274445ca
7 changed files with 211 additions and 95 deletions

View file

@ -3,7 +3,8 @@
"serviceName": "Service-Core Node", "serviceName": "Service-Core Node",
"description": "NodeJS Test Service", "description": "NodeJS Test Service",
"port": 4270, "port": 4270,
"managePort": 4269, "managePort": 4271,
"devPort": 4269,
"appRepository": "thething/sc-helloworld", "appRepository": "thething/sc-helloworld",
"manageRepository": null "manageRepository": null
} }

View file

@ -1,26 +1,28 @@
import fs from 'fs' import fs from 'fs'
import { EventEmitter } from 'events' import { EventEmitter } from 'events'
import { request } from './client.mjs' import { request } from './client.mjs'
import HttpServer from './http.mjs'
const fsp = fs.promises const fsp = fs.promises
export default class Core extends EventEmitter{ export default class Core extends EventEmitter{
constructor(util, config, db, log, closeCb) { constructor(util, config, db, log, closeCb) {
super() super()
this._util = util this.http = new HttpServer()
this._config = config this.util = util
this._db = db this.config = config
this._log = log this.db = db
this.log = log
this._close = closeCb this._close = closeCb
this._appRunning = false this.appRunning = false
this._manageRunning = false this.manageRunning = false
this._appUpdating = { this._appUpdating = {
status: false, updating: false,
starting: false, starting: false,
logs: '', logs: '',
} }
this._manageUpdating = { this._manageUpdating = {
status: false, updating: false,
starting: false, starting: false,
logs: '', logs: '',
} }
@ -32,19 +34,21 @@ export default class Core extends EventEmitter{
status() { status() {
return { return {
app: this._appRunning, app: this.appRunning,
manage: this._manageRunning, manage: this.manageRunning,
appUpdating: this._appUpdating.status, appUpdating: this._appUpdating.updating,
manageUpdating: this._manageUpdating.status, manageUpdating: this._manageUpdating.updating,
appStarting: this._appUpdating.starting,
manageStarting: this._manageUpdating.starting,
} }
} }
async getLatestVersion(active, name) { async getLatestVersion(active, name) {
// Example: 'https://api.github.com/repos/thething/sc-helloworld/releases' // 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) { let items = result.body.filter(function(item) {
if (!item.assets.length) return false if (!item.assets.length) return false
@ -60,7 +64,7 @@ export default class Core extends EventEmitter{
if (item.assets[i].name.endsWith('-sc.zip')) { if (item.assets[i].name.endsWith('-sc.zip')) {
this.logActive(name, active, `[Core] Found version ${item.name} with file ${item.assets[i].name}\n`) 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() .write()
this.emit('dbupdated', {}) this.emit('dbupdated', {})
@ -80,7 +84,7 @@ export default class Core extends EventEmitter{
logActive(name, active, logline, doNotPrint = false) { logActive(name, active, logline, doNotPrint = false) {
if (!doNotPrint) { if (!doNotPrint) {
this._log.info(`Log ${name}: ` + logline.replace(/\n/g, '')) this.log.info(`Log ${name}: ` + logline.replace(/\n/g, ''))
} }
active.logs += logline active.logs += logline
this.emit(name + 'log', active) this.emit(name + 'log', active)
@ -93,57 +97,57 @@ export default class Core extends EventEmitter{
return this._manageUpdating.logs return this._manageUpdating.logs
} }
let latestInstalled = this._db.get('core.' + name + 'LatestInstalled').value() let latestInstalled = this.db.get('core.' + name + 'LatestInstalled').value()
let latestVersion = this._db.get('core.' + name + 'LatestVersion').value() let latestVersion = this.db.get('core.' + name + 'LatestVersion').value()
if (latestVersion) { 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 (value) return value.logs
} }
if (latestInstalled) { 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 if (value) return value.logs
} }
return '< no logs found >' return '< no logs found >'
} }
async installVersion(name, active, version) { async installVersion(name, active, version) {
if (fs.existsSync(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)}"`]) await this.util.runCommand('rmdir', ['/S', '/Q', `"${this.util.getPathFromRoot(`./${name}/` + version.name)}"`])
} }
try { try {
await fsp.mkdir(this._util.getPathFromRoot(`./${name}/` + version.name)) await fsp.mkdir(this.util.getPathFromRoot(`./${name}/` + version.name))
} catch(err) { } catch(err) {
if (err.code !== 'EEXIST') { if (err.code !== 'EEXIST') {
throw err 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`) 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) await request(version.url, filePath)
this.logActive(name, active, `[Core] Downloading finished, starting extraction\n`) 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"', '"C:\\Program Files\\7-Zip\\7z.exe"',
['x', `"${filePath}"`], ['x', `"${filePath}"`],
this._util.getPathFromRoot(`./${name}/` + version.name + '/'), this.util.getPathFromRoot(`./${name}/` + version.name + '/'),
this.logActive.bind(this, name, active) 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`) 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`) this.logActive(name, active, `\n[Core] Starting npm install\n`)
await this._util.runCommand( await this.util.runCommand(
'npm.cmd', 'npm.cmd',
['install', '--production', '--no-optional', '--no-package-lock', '--no-audit'], ['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) 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() .write()
this.emit('dbupdated', {}) this.emit('dbupdated', {})
@ -160,35 +164,67 @@ export default class Core extends EventEmitter{
} }
} }
async startProgram(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 tryStartProgram(name) {
let active = this.getActive(name) let active = this.getActive(name)
if ((name === 'app' && this._appRunning) if ((name === 'app' && this.appRunning)
|| (name === 'manage' && this._manageRunning) || (name === 'manage' && this.manageRunning)
|| active.starting) { || active.starting) {
this._log.event.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.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) this.logActive(name, active, `[${name}] Attempting to start it but it is already running\n`, true)
return return
} }
active.starting = true active.starting = true
let core = this._db.get('core').value() let history = this.db.get(`core_${name}History`)
let version = core[name + 'LatestInstalled'] .filter('installed')
if (await this.tryStartProgram(name, active, version)) return .orderBy('installed', 'desc')
version = core[name + 'LastActive'] .value()
if (await this.tryStartProgram(name, active,version)) return
this._log.error('Unable to start ' + name) for (let i = 0; i < history.length; i++) {
this._log.event.error('Unable to start ' + name) 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 active.starting = false
} }
async tryStartProgram(name, active, version) { async tryStartProgramVersion(name, active, version) {
if (!version) return false if (!version) return false
this.logActive(name, active, `[${name}] Attempting to start ${version}\n`) 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 let module
try { try {
@ -197,49 +233,50 @@ export default class Core extends EventEmitter{
} catch (err) { } catch (err) {
this.logActive(name, active, `[${name}] Error importing module\n`, true) this.logActive(name, active, `[${name}] Error importing module\n`, true)
this.logActive(name, active, `[${name}] ${err.stack}\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 return false
} }
let checkTimeout = null let checkTimeout = null
try { try {
await new Promise((res, rej) => { await new Promise((res, rej) => {
try {
let checkTimeout = setTimeout(function() { let checkTimeout = setTimeout(function() {
rej(new Error('Program took longer than 60 seconds to resolve promise')) rej(new Error('Program took longer than 60 seconds to resolve promise'))
}, 60 * 1000) }, 60 * 1000)
this.logActive(name, active, `[${name}] Starting module\n`) this.logActive(name, active, `[${name}] Starting module\n`)
let out = module.start(this._config, this._db, this._log, this)
if (out.then) { try {
return out.then(res, rej) this.http.setContext(name)
} else { this.startModule(module, name === 'app' ? this.config.port : this.config.managePort)
res() .then(res, rej)
}
} catch (err) { } catch (err) {
rej(err) rej(err)
} }
}) })
} catch (err) { } catch (err) {
clearTimeout(checkTimeout) clearTimeout(checkTimeout)
await this.http.closeServer(name)
this.logActive(name, active, `[${name}] Error starting\n`, true) this.logActive(name, active, `[${name}] Error starting\n`, true)
this.logActive(name, active, `[${name}] ${err.stack}\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 return false
} }
clearTimeout(checkTimeout) clearTimeout(checkTimeout)
this.logActive(name, active, `[${name}] Successfully started version ${version}\n`) 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() .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`) this.logActive(name, active, `[${name}] Checking if listening to port ${port}\n`)
if (name === 'app') { if (name === 'app') {
this._appRunning = true this.appRunning = true
} else { } else {
this._manageRunning = true this.manageRunning = true
} }
this.emit('statusupdated', {})
this.logActive(name, active, `[${name}] Module is running successfully\n`) this.logActive(name, active, `[${name}] Module is running successfully\n`)
@ -247,20 +284,19 @@ export default class Core extends EventEmitter{
} }
async updateProgram(name) { async updateProgram(name) {
if (!this._config[name + 'Repository']) { if (!this.config[name + 'Repository']) {
if (name === 'app') { if (name === 'app') {
this._log.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') this.log.event.error(name + 'Repository was missing from config')
} else { } else {
this._log.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') this.log.event.warn(name + 'Repository was missing from config')
} }
return return
} }
let active = this.getActive(name) let active = this.getActive(name)
active.status = true active.updating = true
active.logs = ''
this.emit('statusupdated', {}) this.emit('statusupdated', {})
this.logActive(name, active, `[Core] Time: ${new Date().toISOString().replace('T', ' ').split('.')[0]}\n`) 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 let found = false
try { try {
version = await this.getLatestVersion(active, name) version = await this.getLatestVersion(active, name)
let core = this._db.get('core').value() let core = this.db.get('core').value()
let fromDb = this._db.get(`core_${name}History`).getById(version.name).value() let fromDb = this.db.get(`core_${name}History`).getById(version.name).value()
console.log(fromDb)
if (!fromDb || !fromDb.installed) { if (!fromDb || !fromDb.installed) {
let oldVersion = core[name + 'Current'] || '<none>' let oldVersion = core[name + 'Current'] || '<none>'
this.logActive(name, active, `[Core] Updating from ${oldVersion} to ${version.name}\n`) 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() installed = new Date()
} else { } else {
found = true 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) { } catch(err) {
this.logActive(name, active, '\n', true) this.logActive(name, active, '\n', true)
this.logActive(name, active, `[Error] Exception occured while updating ${name}\n`, true) this.logActive(name, active, `[Error] Exception occured while updating ${name}\n`, true)
this.logActive(name, active, err.stack, 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) { if (version && !found) {
await this._db.get(`core_${name}History`).upsert({ await this.db.get(`core_${name}History`).upsert({
id: version.name, id: version.name,
name: version.name, name: version.name,
filename: version.filename, filename: version.filename,
@ -308,8 +343,9 @@ export default class Core extends EventEmitter{
async start(name) { async start(name) {
await this.updateProgram(name) await this.updateProgram(name)
if (core[name + 'CurrentVersion']) { var version = this.db.get('core.' + name + 'LatestVersion').value()
await this.startProgram(name) if (version) {
await this.tryStartProgram(name)
} }
} }
} }

View file

@ -141,11 +141,9 @@ export default function GetDB(util, log) {
db.defaults({ db.defaults({
core: { core: {
"appActive": null, // Current active running "appActive": null, // Current active running
"appLastActive": null, // Last active stable running
"appLatestInstalled": null, // Latest installed version "appLatestInstalled": null, // Latest installed version
"appLatestVersion": null, // Newest version available "appLatestVersion": null, // Newest version available
"manageActive": null, "manageActive": null,
"manageLastActive": null,
"manageLatestInstalled": null, "manageLatestInstalled": null,
"manageLatestVersion": null "manageLatestVersion": null
}, },

66
core/http.mjs Normal file
View file

@ -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]
}
}

17
lib.mjs
View file

@ -1,7 +1,8 @@
import Util from './core/util.mjs' import Util from './core/util.mjs'
import { readFileSync } from 'fs' import { readFileSync } from 'fs'
import { getLog } from './core/log.mjs' import getLog from './core/log.mjs'
import lowdb from './core/db.mjs' import lowdb from './core/db.mjs'
import Core from './core/core.mjs'
export default class ServiceCore { export default class ServiceCore {
constructor(name, root_import_meta_url) { constructor(name, root_import_meta_url) {
@ -18,14 +19,14 @@ export default class ServiceCore {
close(err) { close(err) {
if (err) { if (err) {
log.fatal(err, 'App recorded a fatal error') this.log.fatal(err, 'App recorded a fatal error')
process.exit(4) process.exit(4)
} }
log.warn('App asked to be restarted') this.log.warn('App asked to be restarted')
process.exit(0) process.exit(0)
} }
async init() { async init(module = null) {
try { try {
this.config = JSON.parse(readFileSync(this.util.getPathFromRoot('./config.json'))) this.config = JSON.parse(readFileSync(this.util.getPathFromRoot('./config.json')))
} catch (err) { } 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)) 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) { startModule(module) {
return module.start(config, db, log, core) return this.core.startModule(module, this.config.devPort)
} }
} }

View file

@ -2,7 +2,7 @@
"name": "service-core", "name": "service-core",
"version": "1.0.0", "version": "1.0.0",
"description": "Core boiler plate code to install node server as windows service", "description": "Core boiler plate code to install node server as windows service",
"main": "lib.js", "main": "lib.mjs",
"scripts": { "scripts": {
"dev": "nodemon --watch dev/api --watch core --watch runner.mjs --watch db.mjs --watch log.mjs runner.mjs | bunyan", "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" "test": "echo \"Error: no test specified\" && exit 1"

View file

@ -35,13 +35,23 @@ const close = function(err) {
const util = new Util(import.meta.url) 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) let core = new Core(util, config, db, log, close)
let errors = 0
if (config.useDev) { try {
return import('./dev/index.mjs').then(function(module) { await core.start('app')
return module.start(config, db, log, core) } 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) { }, function(err) {
log.fatal(err, 'Critical error opening database') log.fatal(err, 'Critical error opening database')