service-core/core/core.mjs

500 lines
17 KiB
JavaScript
Raw Normal View History

2020-09-07 00:47:53 +00:00
import fs from 'fs'
import { EventEmitter } from 'events'
import { request } from './client.mjs'
2020-09-09 15:41:05 +00:00
import HttpServer from './http.mjs'
2020-09-07 00:47:53 +00:00
const fsp = fs.promises
export default class Core extends EventEmitter{
2020-09-08 08:11:42 +00:00
constructor(util, config, db, log, closeCb) {
2020-09-07 00:47:53 +00:00
super()
2020-09-12 20:31:36 +00:00
process.stdin.resume()
2020-09-09 15:41:05 +00:00
this.http = new HttpServer()
this.util = util
this.config = config
this.db = db
this.log = log
2020-09-07 00:47:53 +00:00
this._close = closeCb
2020-09-12 20:31:36 +00:00
this._activeCrashHandler = null
2020-09-09 15:41:05 +00:00
this.appRunning = false
this.manageRunning = false
this.monitoring = false
2020-09-07 00:47:53 +00:00
this._appUpdating = {
2020-09-12 20:31:36 +00:00
fresh: true,
2020-09-09 15:41:05 +00:00
updating: false,
2020-09-07 18:15:11 +00:00
starting: false,
2020-09-07 00:47:53 +00:00
logs: '',
}
this._manageUpdating = {
2020-09-12 20:31:36 +00:00
fresh: true,
2020-09-09 15:41:05 +00:00
updating: false,
2020-09-07 18:15:11 +00:00
starting: false,
2020-09-07 00:47:53 +00:00
logs: '',
}
this.db.set('core.manageActive', null)
.set('core.appActive', null)
.write().then()
}
startMonitor() {
if (this.monitoring) return
this.log.info('[Scheduler] Automatic updater has been turned on. Will check for updates every 5 seconds')
let updating = false
this.monitoring = setInterval(async () => {
if (updating) return
updating = true
this.log.info('[Scheduler] Starting automatic check for latest version of app and manage')
try {
await this.installLatestVersion('app')
await this.installLatestVersion('manage')
} catch(err) {
this.log.error(err, 'Error checking for latest versions')
this.log.event.error('Error checking for latest versions: ' + err.message)
updating = false
return
}
try {
if (this.hasNewVersionAvailable('app') || !this.appRunning) {
await this.tryStartProgram('app')
}
} catch(err) {
this.log.error(err, 'Unknown error occured attempting to app')
this.log.event.error('Unknown error starting app: ' + err.message)
}
try {
if (this.hasNewVersionAvailable('manage') || !this.manageRunning) {
await this.tryStartProgram('manage')
}
} catch(err) {
this.log.error(err, 'Unknown error occured attempting to start manage')
this.log.event.error('Unknown error starting manage: ' + err.message)
}
updating = false
}, 1000 * 60 * 60 * 3) // every 3 hours
2020-09-07 00:47:53 +00:00
}
restart() {
this._close()
}
status() {
return {
2020-09-09 15:41:05 +00:00
app: this.appRunning,
manage: this.manageRunning,
appUpdating: this._appUpdating.updating,
manageUpdating: this._manageUpdating.updating,
appStarting: this._appUpdating.starting,
manageStarting: this._manageUpdating.starting,
2020-09-07 00:47:53 +00:00
}
}
async getLatestVersion(active, name) {
// Example: 'https://api.github.com/repos/thething/sc-helloworld/releases'
this.logActive(name, active, `Updater: Fetching release info from: https://api.github.com/repos/${this.config[name + 'Repository']}/releases\n`)
2020-09-07 00:47:53 +00:00
let result = await request(this.config, `https://api.github.com/repos/${this.config[name + 'Repository']}/releases`)
2020-09-07 00:47:53 +00:00
let items = result.body.filter(function(item) {
if (!item.assets.length) return false
for (let i = 0; i < item.assets.length; i++) {
if (item.assets[i].name.endsWith('-sc.zip')) return true
}
})
if (items && items.length) {
for (let x = 0; x < items.length; x++) {
let item = items[x]
for (let i = 0; i < item.assets.length; i++) {
if (item.assets[i].name.endsWith('-sc.zip')) {
if (this.db.get('core.' + name + 'LatestInstalled').value() === item.name) {
this.logActive(name, active, `Updater: Latest version already installed, exiting early\n`)
return null
}
this.logActive(name, active, `Updater: Found version ${item.name} with file ${item.assets[i].name}\n`)
2020-09-07 00:47:53 +00:00
2020-09-09 15:41:05 +00:00
await this.db.set(`core.${name}LatestVersion`, item.name)
2020-09-07 00:47:53 +00:00
.write()
this.emit('dbupdated', {})
return {
name: item.name,
filename: item.assets[i].name,
url: item.assets[i].browser_download_url,
description: item.body,
}
}
}
}
} else {
return null
}
}
logActive(name, active, logline, doNotPrint = false) {
if (!doNotPrint) {
this.log.info(`[${name}] ` + logline.replace(/\n/g, ''))
2020-09-07 00:47:53 +00:00
}
active.logs += logline
this.emit(name + 'log', active)
}
getProgramLogs(name) {
if (name === 'app' && this._appUpdating.logs) {
return this._appUpdating.logs
} else if (name === 'manage' && this._manageUpdating.logs) {
return this._manageUpdating.logs
}
2020-09-09 15:41:05 +00:00
let latestInstalled = this.db.get('core.' + name + 'LatestInstalled').value()
let latestVersion = this.db.get('core.' + name + 'LatestVersion').value()
2020-09-07 00:47:53 +00:00
if (latestVersion) {
2020-09-09 15:41:05 +00:00
let value = this.db.get(`core_${name}History`).getById(latestVersion).value()
2020-09-07 00:47:53 +00:00
if (value) return value.logs
}
if (latestInstalled) {
2020-09-09 15:41:05 +00:00
let value = this.db.get(`core_${name}History`).getById(latestInstalled).value()
2020-09-07 00:47:53 +00:00
if (value) return value.logs
}
return '< no logs found >'
}
async installVersion(name, active, version) {
2020-09-09 15:41:05 +00:00
if (fs.existsSync(this.util.getPathFromRoot(`./${name}/` + version.name))) {
await this.util.runCommand('rmdir', ['/S', '/Q', `"${this.util.getPathFromRoot(`./${name}/` + version.name)}"`])
2020-09-07 00:47:53 +00:00
}
2020-09-12 20:31:36 +00:00
if (!fs.existsSync(this.util.getPathFromRoot(`./${name}/`))) {
await fsp.mkdir(this.util.getPathFromRoot(`./${name}/`))
}
2020-09-07 01:25:03 +00:00
try {
2020-09-09 15:41:05 +00:00
await fsp.mkdir(this.util.getPathFromRoot(`./${name}/` + version.name))
2020-09-07 01:25:03 +00:00
} catch(err) {
if (err.code !== 'EEXIST') {
throw err
}
}
2020-09-09 15:41:05 +00:00
// await fsp.mkdir(this.util.getPathFromRoot(`./${name}/` + version.name + '/node_modules'))
this.logActive(name, active, `Installer: Downloading ${version.name} (${version.url}) to ${version.name + '/' + version.name + '.zip'}\n`)
2020-09-09 15:41:05 +00:00
let filePath = this.util.getPathFromRoot(`./${name}/` + version.name + '/' + version.name + '.zip')
await request(this.config, version.url, filePath)
this.logActive(name, active, `Installer: Downloading finished, starting extraction\n`)
2020-09-09 15:41:05 +00:00
await this.util.runCommand(
2020-09-07 01:25:03 +00:00
'"C:\\Program Files\\7-Zip\\7z.exe"',
['x', `"${filePath}"`],
2020-09-09 15:41:05 +00:00
this.util.getPathFromRoot(`./${name}/` + version.name + '/'),
2020-09-07 01:25:03 +00:00
this.logActive.bind(this, name, active)
)
2020-09-09 15:41:05 +00:00
if (!fs.existsSync(this.util.getPathFromRoot(`./${name}/` + version.name + '/index.mjs'))) {
this.logActive(name, active, `\nInstaller: ERROR: Missing index.mjs in the folder, exiting\n`)
2020-09-09 15:41:05 +00:00
throw new Error(`Missing index.mjs in ${this.util.getPathFromRoot(`./${name}/` + version.name + '/index.mjs')}`)
2020-09-07 01:25:03 +00:00
}
this.logActive(name, active, `\nInstaller: Starting npm install\n`)
2020-09-07 01:25:03 +00:00
2020-09-09 15:41:05 +00:00
await this.util.runCommand(
2020-09-07 01:25:03 +00:00
'npm.cmd',
['install', '--production', '--no-optional', '--no-package-lock', '--no-audit'],
2020-09-09 15:41:05 +00:00
this.util.getPathFromRoot(`./${name}/` + version.name + '/'),
2020-09-07 01:25:03 +00:00
this.logActive.bind(this, name, active)
)
2020-09-09 15:41:05 +00:00
await this.db.set(`core.${name}LatestInstalled`, version.name)
2020-09-07 01:25:03 +00:00
.write()
this.emit('dbupdated', {})
this.logActive(name, active, `\nInstaller: Successfully installed ${version.name}\n`)
2020-09-07 18:15:11 +00:00
}
getActive(name) {
if (name === 'app') {
return this._appUpdating
} else if (name === 'manage') {
return this._manageUpdating
} else {
throw new Error('Invalid name: ' + name)
}
2020-09-07 00:47:53 +00:00
}
2020-09-09 15:41:05 +00:00
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')
}
}
hasNewVersionAvailable(name) {
let newestVersion = this.db.get(`core.${name}LatestInstalled`).value()
if (!newestVersion) return false
let history = this.db.get(`core_${name}History`).getById(newestVersion).value()
if (history.installed && history.stable === 0) {
return true
}
return false
}
2020-09-07 00:47:53 +00:00
2020-09-09 15:41:05 +00:00
async tryStartProgram(name) {
2020-09-07 18:15:11 +00:00
let active = this.getActive(name)
if (this[name + 'Running'] && !this.hasNewVersionAvailable(name)) {
2020-09-09 15:41:05 +00:00
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, `Runner: Attempting to start it but it is already running\n`, true)
2020-09-07 18:15:11 +00:00
return
}
active.starting = true
if (this[name + 'Running']) {
let success = await this.http.closeServer(name)
if (!success) {
if (process.env.NODE_ENV === 'production') {
this.logActive(name, active, `Runner: Found new version but server could not be shut down, restarting service core\n`)
await new Promise(() => {
this.log.event.warn('Found new version of ' + name + ' but server could not be shut down gracefully, restarting...', null, () => {
process.exit(100)
})
})
} else {
this.logActive(name, active, `Runner: Found new version but server could not be shut down\n`)
return
}
}
this[name + 'Running'] = false
this.emit('statusupdated', {})
}
2020-09-09 15:41:05 +00:00
let history = this.db.get(`core_${name}History`)
.filter('installed')
.orderBy('installed', 'desc')
.value()
this.logActive(name, active, `Runner: Finding available version\n`)
2020-09-09 15:41:05 +00:00
for (let i = 0; i < history.length; i++) {
2020-09-12 20:31:36 +00:00
if ((history[i].stable === -1 && !active.fresh)
|| (history[i].stable < -1)) {
this.logActive(name, active, `Runner: Skipping version ${history[i].name} due to marked as unstable\n`)
continue
}
2020-09-07 18:15:11 +00:00
2020-09-09 15:41:05 +00:00
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 {
2020-09-12 20:31:36 +00:00
if (active.fresh || history[i].stable === -1) {
history[i].stable = -2
} else {
history[i].stable = -1
}
2020-09-09 15:41:05 +00:00
await this.db.set(`core.${name}Active`, null)
.write()
this.emit('dbupdated', {})
}
2020-09-12 20:31:36 +00:00
active.fresh = false
2020-09-09 15:41:05 +00:00
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.logActive(name, active, `Runner: Could not find any available stable version of ${name}\n`)
2020-09-09 15:41:05 +00:00
this.log.error('Unable to start ' + name)
this.log.event.error('Unable to start ' + name)
}
2020-09-07 18:15:11 +00:00
active.starting = false
}
2020-09-12 20:31:36 +00:00
programCrashed(name, version, active, oldStable) {
let newStable = -2
console.log('EXITING:', oldStable, active)
if (oldStable === 0 && !active.fresh) {
newStable = -1
}
let temp = this.db.get(`core_${name}History`).getById(version).set('stable', newStable )
temp.value() // Trigger update on __wrapped__
fs.writeFileSync(this.db.adapterFilePath, JSON.stringify(temp.__wrapped__, null, 2))
}
2020-09-09 15:41:05 +00:00
async tryStartProgramVersion(name, active, version) {
2020-09-07 18:15:11 +00:00
if (!version) return false
this.logActive(name, active, `Runner: Attempting to start ${version}\n`)
2020-09-09 15:41:05 +00:00
let indexPath = this.util.getUrlFromRoot(`./${name}/` + version + '/index.mjs')
2020-09-07 18:15:11 +00:00
let module
try {
this.logActive(name, active, `Runner: Loading ${indexPath}\n`)
2020-09-07 18:15:11 +00:00
module = await import(indexPath)
} catch (err) {
this.logActive(name, active, `Runner: Error importing module\n`, true)
this.logActive(name, active, `${err.stack}\n`, true)
2020-09-09 15:41:05 +00:00
this.log.error(err, `Failed to load ${indexPath}`)
2020-09-07 18:15:11 +00:00
return false
}
2020-09-12 20:31:36 +00:00
2020-09-07 18:15:11 +00:00
let checkTimeout = null
2020-09-12 20:31:36 +00:00
let oldStable = this.db.get(`core_${name}History`).getById(version).value().stable
this._activeCrashHandler = this.programCrashed.bind(this, name, version, active, oldStable)
process.once('exit', this._activeCrashHandler)
2020-09-07 18:15:11 +00:00
try {
2020-09-12 20:31:36 +00:00
let port = name === 'app' ? this.config.port : this.config.managePort
2020-09-07 18:15:11 +00:00
await new Promise((res, rej) => {
2020-09-12 20:31:36 +00:00
checkTimeout = setTimeout(function() {
2020-09-09 15:41:05 +00:00
rej(new Error('Program took longer than 60 seconds to resolve promise'))
}, 60 * 1000)
this.logActive(name, active, `Runner: Starting module\n`)
2020-09-09 15:41:05 +00:00
2020-09-07 18:15:11 +00:00
try {
2020-09-09 15:41:05 +00:00
this.http.setContext(name)
2020-09-12 20:31:36 +00:00
this.startModule(module, port)
2020-09-09 15:41:05 +00:00
.then(res, rej)
2020-09-07 18:15:11 +00:00
} catch (err) {
rej(err)
}
})
2020-09-12 20:31:36 +00:00
clearTimeout(checkTimeout)
await this.checkProgramRunning(name, active, port)
process.off('exit', this._activeCrashHandler)
2020-09-07 18:15:11 +00:00
} catch (err) {
clearTimeout(checkTimeout)
2020-09-12 20:31:36 +00:00
process.off('exit', this._activeCrashHandler)
2020-09-09 15:41:05 +00:00
await this.http.closeServer(name)
this.logActive(name, active, `Runner: Error starting\n`, true)
this.logActive(name, active, `${err.stack}\n`, true)
2020-09-09 15:41:05 +00:00
this.log.error(err, `Failed to start ${name}`)
2020-09-07 18:15:11 +00:00
return false
}
2020-09-12 20:31:36 +00:00
this._activeCrashHandler = null
2020-09-07 18:15:11 +00:00
this.logActive(name, active, `Runner: Successfully started version ${version}\n`)
2020-09-09 15:41:05 +00:00
await this.db.set(`core.${name}Active`, version)
2020-09-07 18:15:11 +00:00
.write()
if (name === 'app') {
2020-09-09 15:41:05 +00:00
this.appRunning = true
2020-09-07 18:15:11 +00:00
} else {
2020-09-09 15:41:05 +00:00
this.manageRunning = true
2020-09-07 18:15:11 +00:00
}
2020-09-09 15:41:05 +00:00
this.emit('statusupdated', {})
2020-09-07 18:15:11 +00:00
this.logActive(name, active, `Runner: Module is running successfully\n`)
2020-09-07 18:15:11 +00:00
return true
2020-09-07 00:47:53 +00:00
}
2020-09-12 20:31:36 +00:00
async checkProgramRunning(name, active, port) {
this.logActive(name, active, `Checker: Testing out module port ${port}\n`)
2020-09-12 20:31:36 +00:00
let start = new Date()
let error = null
let success = false
while (new Date() - start < 10 * 1000) {
try {
let check = await request(this.config, `http://localhost:${port}`, null, 0, true)
2020-09-12 20:31:36 +00:00
success = true
break
} catch(err) {
this.logActive(name, active, `Checker: ${err.message}, retrying in 3 seconds\n`)
2020-09-12 20:31:36 +00:00
error = err
await new Promise(function(res) { setTimeout(res, 3000)})
}
}
if (success) return true
throw error || new Error('Checking server failed')
}
async installLatestVersion(name) {
2020-09-09 15:41:05 +00:00
if (!this.config[name + 'Repository']) {
2020-09-07 00:47:53 +00:00
if (name === 'app') {
this.log.error(name + ' Repository was missing from config')
this.log.event.error(name + ' Repository was missing from config')
2020-09-07 00:47:53 +00:00
} else {
this.log.warn(name + ' Repository was missing from config')
this.log.event.warn(name + ' Repository was missing from config')
2020-09-07 00:47:53 +00:00
}
return
}
2020-09-07 18:15:11 +00:00
let active = this.getActive(name)
2020-09-12 20:31:36 +00:00
let oldLogs = active.logs || ''
if (oldLogs) {
oldLogs += '\n'
}
active.logs = ''
2020-09-09 15:41:05 +00:00
active.updating = true
2020-09-07 00:47:53 +00:00
this.emit('statusupdated', {})
this.logActive(name, active, `Installer: Checking for updates at time: ${new Date().toISOString().replace('T', ' ').split('.')[0]}\n`)
2020-09-07 00:47:53 +00:00
let version = null
2020-09-07 18:15:11 +00:00
let installed = false
let found = false
2020-09-07 00:47:53 +00:00
try {
version = await this.getLatestVersion(active, name)
if (version) {
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'] || '<none>'
this.logActive(name, active, `Installer: Updating from ${oldVersion} to ${version.name}\n`)
await this.installVersion(name, active, version)
this.logActive(name, active, `Installer: Finished: ${new Date().toISOString().replace('T', ' ').split('.')[0]}\n`)
installed = new Date()
} else {
found = true
this.logActive(name, active, `Installer: Version ${version.name} already installed\n`)
}
2020-09-07 00:47:53 +00:00
}
} catch(err) {
this.logActive(name, active, '\n', true)
this.logActive(name, active, `Installer: Exception occured while updating ${name}\n`, true)
2020-09-07 00:47:53 +00:00
this.logActive(name, active, err.stack, true)
2020-09-12 20:31:36 +00:00
this.log.error('Error while updating ' + name, err)
2020-09-07 00:47:53 +00:00
}
2020-09-09 15:41:05 +00:00
active.updating = false
2020-09-07 18:15:11 +00:00
if (version && !found) {
2020-09-09 15:41:05 +00:00
await this.db.get(`core_${name}History`).upsert({
2020-09-07 00:47:53 +00:00
id: version.name,
name: version.name,
filename: version.filename,
url: version.url,
description: version.description,
logs: active.logs,
2020-09-07 18:15:11 +00:00
stable: 0,
2020-09-12 20:31:36 +00:00
installed: installed && installed.toISOString(),
2020-09-07 00:47:53 +00:00
}).write()
}
2020-09-12 20:31:36 +00:00
active.logs = oldLogs + active.logs
this.emit(name + 'log', active)
2020-09-07 00:47:53 +00:00
this.emit('statusupdated', {})
}
async start(name) {
var version = this.db.get('core.' + name + 'LatestInstalled').value()
2020-09-09 15:41:05 +00:00
if (version) {
await this.tryStartProgram(name)
2020-09-07 00:47:53 +00:00
}
await this.installLatestVersion(name)
if (version !== this.db.get('core.' + name + 'LatestInstalled').value()) {
if (!this[name + 'Running'] || this.hasNewVersionAvailable(name)) {
await this.tryStartProgram(name)
}
}
2020-09-07 00:47:53 +00:00
}
}