Finished basic beta implementation of entire thing.
Some checks failed
continuous-integration/appveyor/branch AppVeyor build failed
Some checks failed
continuous-integration/appveyor/branch AppVeyor build failed
This commit is contained in:
parent
e540a54844
commit
5f3e688b8c
26 changed files with 555 additions and 754 deletions
|
@ -26,3 +26,4 @@ test_script:
|
|||
chmod -R 777 /appveyor/projects
|
||||
npm install
|
||||
npm test
|
||||
npm test:integration
|
||||
|
|
176
cli.mjs
Normal file
176
cli.mjs
Normal file
|
@ -0,0 +1,176 @@
|
|||
#!/usr/bin/env node
|
||||
|
||||
// Get arguments
|
||||
const [,, ...args] = process.argv
|
||||
|
||||
import os from 'os'
|
||||
import fs from 'fs/promises'
|
||||
import Util from './core/util.mjs'
|
||||
import Core from './core/core.mjs'
|
||||
import path from 'path'
|
||||
|
||||
if (!args.length) PrintHelp()
|
||||
if (args[0] !== 'checkconfig' && args[0] !== 'install' && args[0] !== 'uninstall') PrintHelp
|
||||
|
||||
let configFile = 'config.json'
|
||||
if (args[1]) {
|
||||
configFile = args[1]
|
||||
}
|
||||
|
||||
const basicRunnerTemplate = `
|
||||
import fs from 'fs'
|
||||
import { runner } from 'service-core'
|
||||
|
||||
runner(import.meta.url, '${configFile}', 'db.json')
|
||||
.then(
|
||||
function(core) {
|
||||
core.log.info('core is running')
|
||||
},
|
||||
function(err) {
|
||||
runner.log.error(err, 'Error starting runner')
|
||||
process.exit(1)
|
||||
}
|
||||
)
|
||||
`
|
||||
|
||||
const util = new Util(import.meta.url)
|
||||
|
||||
function PrintHelp() {
|
||||
console.log('')
|
||||
console.log('Usage: sccli <command> [config.json]')
|
||||
console.log('')
|
||||
console.log('<Commands>')
|
||||
console.log(' checkconfig : Test local config.json for errors')
|
||||
console.log(' install : Install local config.json as service')
|
||||
console.log(' uninstall : Uninstall local config.json as a service')
|
||||
console.log('')
|
||||
process.exit(1)
|
||||
}
|
||||
|
||||
if (args[0] === 'checkconfig') {
|
||||
fs.readFile(configFile)
|
||||
.then(async function(content) {
|
||||
let config = JSON.parse(content)
|
||||
util.verifyConfig(config)
|
||||
|
||||
let names = util.getAppNames(config)
|
||||
|
||||
if (!names.length) {
|
||||
return Promise.reject(new Error('No application names were found'))
|
||||
}
|
||||
|
||||
for (let name of names) {
|
||||
let provConstructor = Core.providers.get(config[name].provider)
|
||||
let provider = new provConstructor(config[name])
|
||||
await provider.checkConfig(config[name])
|
||||
}
|
||||
|
||||
console.log(`${configFile} is a valid config with ${names.length} application${names.length > 1 ? 's' : ''}:`)
|
||||
for (let name of names) {
|
||||
console.log(` * ${name} (${config[name].provider}${config[name].url ? ': ' + config[name].url : ' provider'})`)
|
||||
}
|
||||
})
|
||||
.catch(function(err) {
|
||||
console.log('Error checking config:', err)
|
||||
process.exit(2)
|
||||
})
|
||||
.then(function() {
|
||||
process.exit(0)
|
||||
})
|
||||
} else if (args[0] === 'install') {
|
||||
if(os.platform() === 'win32') {
|
||||
const runner = path.join(process.cwd(), './runner.mjs')
|
||||
|
||||
fs.stat(runner).catch(function() {
|
||||
return fs.writeFile(runner, basicRunnerTemplate)
|
||||
}).then(function() {
|
||||
return Promise.all([
|
||||
fs.readFile(configFile),
|
||||
import('node-windows'),
|
||||
])
|
||||
}).then(function([content, nodewindows]) {
|
||||
let config = JSON.parse(content)
|
||||
const Service = nodewindows.Service
|
||||
|
||||
let serviceConfig = {
|
||||
name: config.title || config.name,
|
||||
description: config.description,
|
||||
script: runner,
|
||||
env: {
|
||||
name: 'NODE_ENV',
|
||||
value: 'production',
|
||||
},
|
||||
wait: 0,
|
||||
grow: .5,
|
||||
maxRestarts: 10
|
||||
//, workingDirectory: '...'
|
||||
//, allowServiceLogon: true
|
||||
}
|
||||
|
||||
console.log('Service', serviceConfig)
|
||||
// Create a new service object
|
||||
let svc = new Service(serviceConfig);
|
||||
|
||||
svc.on('install',function(){
|
||||
svc.start();
|
||||
process.exit(0)
|
||||
});
|
||||
|
||||
svc.on('alreadyinstalled',function(){
|
||||
svc.start();
|
||||
process.exit(0)
|
||||
});
|
||||
|
||||
svc.install();
|
||||
})
|
||||
} else {
|
||||
console.log('non windows install targets are currently unsupported')
|
||||
process.exit(2)
|
||||
}
|
||||
} else if (args[0] === 'uninstall') {
|
||||
if(os.platform() === 'win32') {
|
||||
const runner = path.join(process.cwd(), './runner.mjs')
|
||||
|
||||
fs.stat(runner).catch(function() {
|
||||
return fs.writeFile(runner, basicRunnerTemplate)
|
||||
}).then(function() {
|
||||
return Promise.all([
|
||||
fs.readFile(configFile),
|
||||
import('node-windows'),
|
||||
])
|
||||
}).then(function([content, nodewindows]) {
|
||||
let config = JSON.parse(content)
|
||||
const Service = nodewindows.Service
|
||||
|
||||
let serviceConfig = {
|
||||
name: config.title || config.name,
|
||||
description: config.description,
|
||||
script: runner,
|
||||
env: {
|
||||
name: 'NODE_ENV',
|
||||
value: 'production',
|
||||
},
|
||||
wait: 0,
|
||||
grow: .5,
|
||||
maxRestarts: 10
|
||||
//, workingDirectory: '...'
|
||||
//, allowServiceLogon: true
|
||||
}
|
||||
|
||||
console.log('Service', serviceConfig)
|
||||
// Create a new service object
|
||||
let svc = new Service(serviceConfig);
|
||||
|
||||
svc.on('uninstall',function(){
|
||||
console.log('Uninstall complete.');
|
||||
console.log('The service exists: ',svc.exists);
|
||||
process.exit(0)
|
||||
});
|
||||
|
||||
svc.uninstall();
|
||||
})
|
||||
} else {
|
||||
console.log('non windows install targets are currently unsupported')
|
||||
process.exit(2)
|
||||
}
|
||||
}
|
11
config.json
11
config.json
|
@ -1,11 +0,0 @@
|
|||
{
|
||||
"name": "service-core",
|
||||
"githubAuthToken": null,
|
||||
"serviceName": "Service-Core Node",
|
||||
"description": "NodeJS Test Service",
|
||||
"port": 4270,
|
||||
"managePort": 4271,
|
||||
"devPort": 4269,
|
||||
"appRepository": "thething/sc-helloworld",
|
||||
"manageRepository": "TheThing/sc-manager"
|
||||
}
|
|
@ -32,7 +32,7 @@ export default class Application extends EventEmitter {
|
|||
this.fresh = true
|
||||
|
||||
// Apply defaults to config
|
||||
this.config.updateEvery = this.config.updateEvery || 180
|
||||
this.config.updateEvery = this.config.updateEvery != null ? this.config.updateEvery : 180
|
||||
this.config.startWaitUntilFail = this.config.startWaitUntilFail || (60 * 1000)
|
||||
this.config.heartbeatTimeout = this.config.heartbeatTimeout || (3 * 1000)
|
||||
this.config.heartbeatAttempts = this.config.heartbeatAttempts || 5
|
||||
|
@ -49,6 +49,7 @@ export default class Application extends EventEmitter {
|
|||
|
||||
startAutoupdater() {
|
||||
if (this.provider.static) return
|
||||
if (this.config.updateEvery === 0) return
|
||||
|
||||
let timer = this.setInterval(() => {
|
||||
this.update().then(
|
||||
|
|
0
core/cli.mjs
Normal file
0
core/cli.mjs
Normal file
|
@ -22,9 +22,8 @@ export default class Core {
|
|||
Core.providers.set(name, provider)
|
||||
}
|
||||
|
||||
constructor(db, util, log, restart) {
|
||||
constructor(db, util, log) {
|
||||
// some sanity checks
|
||||
if (typeof(restart) !== 'function') throw new Error('restart parameter was not a function')
|
||||
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'
|
||||
|
@ -40,9 +39,9 @@ export default class Core {
|
|||
this.db = db
|
||||
this.util = util
|
||||
this.log = log
|
||||
this.restart = restart
|
||||
this.applications = []
|
||||
this.applicationMap = new Map()
|
||||
this._applicationFatalCrash = null
|
||||
}
|
||||
|
||||
getApplication(name) {
|
||||
|
@ -114,6 +113,12 @@ export default class Core {
|
|||
}
|
||||
}
|
||||
|
||||
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
|
||||
|
@ -128,6 +133,9 @@ export default class Core {
|
|||
if (version.stable < 0 && !application.fresh) continue
|
||||
await application.closeServer()
|
||||
|
||||
this._applicationFatalCrash = this.criticalError.bind(this, application, version)
|
||||
process.once('exit', this._applicationFatalCrash)
|
||||
|
||||
try {
|
||||
application.ctx.log.info(`Attempting to run version ${version.version}`)
|
||||
await application.runVersion(version.version)
|
||||
|
@ -136,9 +144,15 @@ export default class Core {
|
|||
await this.db.write()
|
||||
break
|
||||
} catch(err) {
|
||||
version.stable = Math.min(version.stable, 0) - 1
|
||||
if (application.fresh) {
|
||||
version.stable = -2
|
||||
} else {
|
||||
version.stable = Math.min(version.stable, 0) - 1
|
||||
}
|
||||
await this.db.write()
|
||||
application.ctx.log.error(err, `Error starting ${version.version}: ${err.message}`)
|
||||
} finally {
|
||||
process.off('exit', this._applicationFatalCrash)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -1,499 +0,0 @@
|
|||
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()
|
||||
process.stdin.resume()
|
||||
this.http = new HttpServer()
|
||||
this.util = util
|
||||
this.config = config
|
||||
this.db = db
|
||||
this.log = log
|
||||
this._close = closeCb
|
||||
this._activeCrashHandler = null
|
||||
this.appRunning = false
|
||||
this.manageRunning = false
|
||||
this.monitoring = false
|
||||
this._appUpdating = {
|
||||
fresh: true,
|
||||
updating: false,
|
||||
starting: false,
|
||||
logs: '',
|
||||
}
|
||||
this._manageUpdating = {
|
||||
fresh: true,
|
||||
updating: false,
|
||||
starting: false,
|
||||
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 3 hours')
|
||||
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
|
||||
}
|
||||
|
||||
restart() {
|
||||
this._close()
|
||||
}
|
||||
|
||||
status() {
|
||||
return {
|
||||
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, `Updater: Fetching release info from: https://api.github.com/repos/${this.config[name + 'Repository']}/releases\n`)
|
||||
|
||||
let result = await request(this.config, `https://api.github.com/repos/${this.config[name + 'Repository']}/releases`)
|
||||
|
||||
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`)
|
||||
|
||||
await this.db.set(`core.${name}LatestVersion`, item.name)
|
||||
.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, ''))
|
||||
}
|
||||
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
|
||||
}
|
||||
|
||||
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()
|
||||
if (value) return value.logs
|
||||
}
|
||||
if (latestInstalled) {
|
||||
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}/`))) {
|
||||
await fsp.mkdir(this.util.getPathFromRoot(`./${name}/`))
|
||||
}
|
||||
try {
|
||||
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'))
|
||||
this.logActive(name, active, `Installer: Downloading ${version.name} (${version.url}) to ${version.name + '/' + version.name + '.zip'}\n`)
|
||||
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`)
|
||||
await this.util.runCommand(
|
||||
'"C:\\Program Files\\7-Zip\\7z.exe"',
|
||||
['x', `"${filePath}"`],
|
||||
this.util.getPathFromRoot(`./${name}/` + version.name + '/'),
|
||||
this.logActive.bind(this, name, active)
|
||||
)
|
||||
|
||||
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`)
|
||||
throw new Error(`Missing index.mjs in ${this.util.getPathFromRoot(`./${name}/` + version.name + '/index.mjs')}`)
|
||||
}
|
||||
|
||||
this.logActive(name, active, `\nInstaller: Starting npm install\n`)
|
||||
|
||||
await this.util.runCommand(
|
||||
'npm.cmd',
|
||||
['install', '--production', '--no-optional', '--no-package-lock', '--no-audit'],
|
||||
this.util.getPathFromRoot(`./${name}/` + version.name + '/'),
|
||||
this.logActive.bind(this, name, active)
|
||||
)
|
||||
|
||||
await this.db.set(`core.${name}LatestInstalled`, version.name)
|
||||
.write()
|
||||
this.emit('dbupdated', {})
|
||||
|
||||
this.logActive(name, active, `\nInstaller: Successfully installed ${version.name}\n`)
|
||||
}
|
||||
|
||||
getActive(name) {
|
||||
if (name === 'app') {
|
||||
return this._appUpdating
|
||||
} else if (name === 'manage') {
|
||||
return this._manageUpdating
|
||||
} else {
|
||||
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')
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
async tryStartProgram(name) {
|
||||
let active = this.getActive(name)
|
||||
|
||||
if (this[name + 'Running'] && !this.hasNewVersionAvailable(name)) {
|
||||
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)
|
||||
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', {})
|
||||
}
|
||||
|
||||
let history = this.db.get(`core_${name}History`)
|
||||
.filter('installed')
|
||||
.orderBy('installed', 'desc')
|
||||
.value()
|
||||
this.logActive(name, active, `Runner: Finding available version\n`)
|
||||
|
||||
for (let i = 0; i < history.length; i++) {
|
||||
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
|
||||
}
|
||||
|
||||
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 {
|
||||
if (active.fresh || history[i].stable === -1) {
|
||||
history[i].stable = -2
|
||||
} else {
|
||||
history[i].stable = -1
|
||||
}
|
||||
await this.db.set(`core.${name}Active`, null)
|
||||
.write()
|
||||
this.emit('dbupdated', {})
|
||||
}
|
||||
active.fresh = false
|
||||
|
||||
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`)
|
||||
this.log.error('Unable to start ' + name)
|
||||
this.log.event.error('Unable to start ' + name)
|
||||
}
|
||||
|
||||
active.starting = false
|
||||
}
|
||||
|
||||
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))
|
||||
}
|
||||
|
||||
async tryStartProgramVersion(name, active, version) {
|
||||
if (!version) return false
|
||||
this.logActive(name, active, `Runner: Attempting to start ${version}\n`)
|
||||
let indexPath = this.util.getUrlFromRoot(`./${name}/` + version + '/index.mjs')
|
||||
let module
|
||||
|
||||
try {
|
||||
this.logActive(name, active, `Runner: Loading ${indexPath}\n`)
|
||||
module = await import(indexPath)
|
||||
} catch (err) {
|
||||
this.logActive(name, active, `Runner: Error importing module\n`, true)
|
||||
this.logActive(name, active, `${err.stack}\n`, true)
|
||||
this.log.error(err, `Failed to load ${indexPath}`)
|
||||
return false
|
||||
}
|
||||
|
||||
let checkTimeout = null
|
||||
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)
|
||||
try {
|
||||
let port = name === 'app' ? this.config.port : this.config.managePort
|
||||
await new Promise((res, rej) => {
|
||||
checkTimeout = setTimeout(function() {
|
||||
rej(new Error('Program took longer than 60 seconds to resolve promise'))
|
||||
}, 60 * 1000)
|
||||
|
||||
this.logActive(name, active, `Runner: Starting module\n`)
|
||||
|
||||
try {
|
||||
this.http.setContext(name)
|
||||
this.startModule(module, port)
|
||||
.then(res, rej)
|
||||
} catch (err) {
|
||||
rej(err)
|
||||
}
|
||||
})
|
||||
clearTimeout(checkTimeout)
|
||||
|
||||
await this.checkProgramRunning(name, active, port)
|
||||
process.off('exit', this._activeCrashHandler)
|
||||
} catch (err) {
|
||||
clearTimeout(checkTimeout)
|
||||
process.off('exit', this._activeCrashHandler)
|
||||
await this.http.closeServer(name)
|
||||
|
||||
this.logActive(name, active, `Runner: Error starting\n`, true)
|
||||
this.logActive(name, active, `${err.stack}\n`, true)
|
||||
this.log.error(err, `Failed to start ${name}`)
|
||||
return false
|
||||
}
|
||||
this._activeCrashHandler = null
|
||||
|
||||
this.logActive(name, active, `Runner: Successfully started version ${version}\n`)
|
||||
await this.db.set(`core.${name}Active`, version)
|
||||
.write()
|
||||
|
||||
if (name === 'app') {
|
||||
this.appRunning = true
|
||||
} else {
|
||||
this.manageRunning = true
|
||||
}
|
||||
this.emit('statusupdated', {})
|
||||
|
||||
this.logActive(name, active, `Runner: Module is running successfully\n`)
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
async checkProgramRunning(name, active, port) {
|
||||
this.logActive(name, active, `Checker: Testing out module port ${port}\n`)
|
||||
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)
|
||||
success = true
|
||||
break
|
||||
} catch(err) {
|
||||
this.logActive(name, active, `Checker: ${err.message}, retrying in 3 seconds\n`)
|
||||
error = err
|
||||
await new Promise(function(res) { setTimeout(res, 3000)})
|
||||
}
|
||||
}
|
||||
if (success) return true
|
||||
throw error || new Error('Checking server failed')
|
||||
}
|
||||
|
||||
async installLatestVersion(name) {
|
||||
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')
|
||||
} else {
|
||||
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)
|
||||
let oldLogs = active.logs || ''
|
||||
if (oldLogs) {
|
||||
oldLogs += '\n'
|
||||
}
|
||||
active.logs = ''
|
||||
active.updating = true
|
||||
|
||||
this.emit('statusupdated', {})
|
||||
this.logActive(name, active, `Installer: Checking for updates at time: ${new Date().toISOString().replace('T', ' ').split('.')[0]}\n`)
|
||||
|
||||
let version = null
|
||||
let installed = false
|
||||
let found = false
|
||||
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`)
|
||||
}
|
||||
}
|
||||
} catch(err) {
|
||||
this.logActive(name, active, '\n', true)
|
||||
this.logActive(name, active, `Installer: Exception occured while updating ${name}\n`, true)
|
||||
this.logActive(name, active, err.stack, true)
|
||||
this.log.error('Error while updating ' + name, err)
|
||||
}
|
||||
active.updating = false
|
||||
if (version && !found) {
|
||||
await this.db.get(`core_${name}History`).upsert({
|
||||
id: version.name,
|
||||
name: version.name,
|
||||
filename: version.filename,
|
||||
url: version.url,
|
||||
description: version.description,
|
||||
logs: active.logs,
|
||||
stable: 0,
|
||||
installed: installed && installed.toISOString(),
|
||||
}).write()
|
||||
}
|
||||
active.logs = oldLogs + active.logs
|
||||
this.emit(name + 'log', active)
|
||||
this.emit('statusupdated', {})
|
||||
}
|
||||
|
||||
async start(name) {
|
||||
var version = this.db.get('core.' + name + 'LatestInstalled').value()
|
||||
if (version) {
|
||||
await this.tryStartProgram(name)
|
||||
}
|
||||
|
||||
await this.installLatestVersion(name)
|
||||
|
||||
if (version !== this.db.get('core.' + name + 'LatestInstalled').value()) {
|
||||
if (!this[name + 'Running'] || this.hasNewVersionAvailable(name)) {
|
||||
await this.tryStartProgram(name)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
11
core/db.mjs
11
core/db.mjs
|
@ -1,5 +1,6 @@
|
|||
import { setTimeout } from 'timers/promises'
|
||||
import { Low, JSONFile, Memory } from 'lowdb'
|
||||
import fs from 'fs'
|
||||
import { defaults, isObject } from './defaults.mjs'
|
||||
|
||||
export default function GetDB(config, log, orgFilename = 'db.json') {
|
||||
|
@ -12,7 +13,7 @@ export default function GetDB(config, log, orgFilename = 'db.json') {
|
|||
const db = new Low(adapter)
|
||||
|
||||
db.id = 'id'
|
||||
|
||||
db.filename = fullpath
|
||||
db.config = config
|
||||
|
||||
db.createId = function(collection) {
|
||||
|
@ -110,6 +111,14 @@ export default function GetDB(config, log, orgFilename = 'db.json') {
|
|||
})
|
||||
}
|
||||
|
||||
db.writeSync = function() {
|
||||
try {
|
||||
fs.writeFileSync(db.filename, JSON.stringify(db.data))
|
||||
} catch(err) {
|
||||
db.log.error(err, `Error during writeSync to ${db.filename}`)
|
||||
}
|
||||
}
|
||||
|
||||
return db.read()
|
||||
.then(function() {
|
||||
if (!isObject(db.data)) {
|
||||
|
|
40
core/lib.mjs
Normal file
40
core/lib.mjs
Normal file
|
@ -0,0 +1,40 @@
|
|||
import Util from './util.mjs'
|
||||
import getLog from './log.mjs'
|
||||
import GetDB from './db.mjs'
|
||||
import StaticProvider from './providers/static.mjs'
|
||||
import Core from './core.mjs'
|
||||
|
||||
export default class ServiceCore {
|
||||
constructor(name, root_import_meta_url, dbfilename = 'db.json') {
|
||||
if (!root_import_meta_url) {
|
||||
throw new Error('ServiceCore must be called with the full string from "import.meta.url" from a file residing in the root directory')
|
||||
}
|
||||
this._root_import_meta_url = root_import_meta_url
|
||||
this.util = new Util(this._root_import_meta_url)
|
||||
this.dbfilename = dbname
|
||||
this.log = getLog(name)
|
||||
this.name = name
|
||||
this.config = {}
|
||||
this.db = null
|
||||
this.core = null
|
||||
this.app = null
|
||||
}
|
||||
|
||||
async init(module = null) {
|
||||
this.db = await GetDB(this.config, this.log, this.dbfilename)
|
||||
this.core = new Core(this.db, this.util, this.log)
|
||||
|
||||
let provider = new StaticProvider()
|
||||
this.app = new Application({
|
||||
db: this.db,
|
||||
util: this.util,
|
||||
log: this.log,
|
||||
core: this.core,
|
||||
}, provider, this.name)
|
||||
this.app.registerModule(module)
|
||||
}
|
||||
|
||||
run() {
|
||||
return this.app.runVersion('static')
|
||||
}
|
||||
}
|
|
@ -26,7 +26,7 @@ export async function runner(root_import_meta_url, configname = 'config.json', d
|
|||
runner.log = log
|
||||
const db = await GetDB(config, log, util.getPathFromRoot('./' + dbname))
|
||||
|
||||
const core = new Core(db, util, log, function() {})
|
||||
const core = new Core(db, util, log)
|
||||
await core.init()
|
||||
await core.run()
|
||||
|
||||
|
|
|
@ -188,7 +188,9 @@ export default class Util {
|
|||
processor._kill = processor.kill
|
||||
processor.kill = function() {
|
||||
if(os.platform() === 'win32'){
|
||||
execSync('taskkill /pid ' + processor.pid + ' /T /F')
|
||||
try {
|
||||
execSync('taskkill /pid ' + processor.pid + ' /T /F')
|
||||
} catch {}
|
||||
}else{
|
||||
processor.kill();
|
||||
}
|
||||
|
|
24
exampleconfig.json
Normal file
24
exampleconfig.json
Normal file
|
@ -0,0 +1,24 @@
|
|||
{
|
||||
"name": "service-core",
|
||||
"title": "ExampleService",
|
||||
"description": "Example config service running helloworld",
|
||||
"helloworld": {
|
||||
"provider": "git",
|
||||
"url": "https://git.nfp.is/api/v1/repos/thething/sc-helloworld/releases",
|
||||
"token": null,
|
||||
"port": 8888,
|
||||
"https": false,
|
||||
"updateEvery": 180,
|
||||
"startWaitUntilFail": 60000,
|
||||
"heartbeatTimeout": 3000,
|
||||
"heartbeatAttempts": 5,
|
||||
"heartbeatAttemptsWait": 2000,
|
||||
"heartbeatPath": "/",
|
||||
"log": [
|
||||
{
|
||||
"level": "info",
|
||||
"stream": "process.stdout"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
21
index.mjs
Normal file
21
index.mjs
Normal file
|
@ -0,0 +1,21 @@
|
|||
import bunyan from 'bunyan-lite'
|
||||
import { runner } from './core/runner.mjs'
|
||||
import { ServiceCore } from './core/lib.mjs'
|
||||
import Core from './core/core.mjs'
|
||||
import Application from './core/application.mjs'
|
||||
import Util from './core/util.mjs'
|
||||
import HttpServer from './core/http.mjs'
|
||||
import { request } from './core/client.mjs'
|
||||
import getLog from './core/log.mjs'
|
||||
|
||||
export {
|
||||
bunyan,
|
||||
runner,
|
||||
ServiceCore,
|
||||
Core,
|
||||
Application,
|
||||
Util,
|
||||
HttpServer,
|
||||
request,
|
||||
getLog,
|
||||
}
|
|
@ -1,2 +0,0 @@
|
|||
node service\install.mjs
|
||||
PAUSE
|
52
lib.mjs
52
lib.mjs
|
@ -1,52 +0,0 @@
|
|||
import Util from './core/util.mjs'
|
||||
import { readFileSync } from 'fs'
|
||||
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) {
|
||||
if (!root_import_meta_url) {
|
||||
throw new Error('ServiceCore must be called with the full string from "import.meta.url" from a file residing in the root directory')
|
||||
}
|
||||
this._root_import_meta_url = root_import_meta_url
|
||||
this.util = new Util(this._root_import_meta_url)
|
||||
this.log = getLog(name)
|
||||
this.db = null
|
||||
this.config = null
|
||||
this.core = null
|
||||
}
|
||||
|
||||
close(err) {
|
||||
if (err) {
|
||||
this.log.fatal(err, 'App recorded a fatal error')
|
||||
process.exit(4)
|
||||
}
|
||||
this.log.warn('App asked to be restarted')
|
||||
process.exit(0)
|
||||
}
|
||||
|
||||
async init(module = null) {
|
||||
try {
|
||||
this.config = JSON.parse(readFileSync(this.util.getPathFromRoot('./config.json')))
|
||||
} catch (err) {
|
||||
throw new Error('Unable to read config.json from root directory: ' + err)
|
||||
}
|
||||
|
||||
try {
|
||||
this.db = await lowdb(this.util, this.log)
|
||||
} catch (err) {
|
||||
throw new Error('Unable to read initialise lowdb: ' + err)
|
||||
}
|
||||
|
||||
this.core = new Core(this.util, this.config, this.db, this.log, (err) => this.close(err))
|
||||
|
||||
if (module) {
|
||||
return this.startModule(module)
|
||||
}
|
||||
}
|
||||
|
||||
startModule(module) {
|
||||
return this.core.startModule(module, this.config.devPort)
|
||||
}
|
||||
}
|
|
@ -1,2 +0,0 @@
|
|||
npm install
|
||||
PAUSE
|
|
@ -36,10 +36,12 @@
|
|||
},
|
||||
"homepage": "https://github.com/TheThing/service-core#readme",
|
||||
"files": [
|
||||
"lib.mjs",
|
||||
"index.mjs",
|
||||
"cli.mjs",
|
||||
"package.json",
|
||||
"README.md",
|
||||
"core"
|
||||
"core",
|
||||
"bin"
|
||||
],
|
||||
"dependencies": {
|
||||
"bunyan-lite": "^1.0.1",
|
||||
|
|
69
runner.mjs
69
runner.mjs
|
@ -1,69 +0,0 @@
|
|||
import { readFileSync } from 'fs'
|
||||
import getLog from './core/log.mjs'
|
||||
import lowdb from './core/db.mjs'
|
||||
import Core from './core/core.mjs'
|
||||
import Util from './core/util.mjs'
|
||||
|
||||
let config
|
||||
|
||||
try {
|
||||
config = JSON.parse(readFileSync('./config.json'))
|
||||
} catch (err) {
|
||||
let logger = getLog('critical-error')
|
||||
logger.fatal('Error opening config file')
|
||||
logger.fatal('Make sure it is valid JSON')
|
||||
logger.fatal(err)
|
||||
logger.event.error('Unable to start, error in config.json: ' + err.message)
|
||||
process.exit(10)
|
||||
}
|
||||
|
||||
const log = getLog(config.name)
|
||||
|
||||
const close = function(err) {
|
||||
if (err) {
|
||||
log.fatal(err, 'App recorded a fatal error')
|
||||
log.event.error('App recorded a fatal error: ' + err.message, null, function() {
|
||||
process.exit(4)
|
||||
})
|
||||
return
|
||||
}
|
||||
log.warn('App asked to be restarted')
|
||||
log.event.warn('App requested to be restarted', null, function() {
|
||||
process.exit(0)
|
||||
})
|
||||
}
|
||||
|
||||
const util = new Util(import.meta.url)
|
||||
|
||||
lowdb(util, log).then(async function(db) {
|
||||
let core = new Core(util, config, db, log, close)
|
||||
let errors = 0
|
||||
try {
|
||||
await core.start('app')
|
||||
} catch (err) {
|
||||
log.event.error('Unable to start app: ' + err.message)
|
||||
log.error(err, 'Unable to start app')
|
||||
errors++
|
||||
}
|
||||
try {
|
||||
await core.start('manage')
|
||||
} catch (err) {
|
||||
log.event.error('Unable to start manage: ' + err.message)
|
||||
log.error(err, 'Unable to start manage')
|
||||
errors++
|
||||
}
|
||||
core.startMonitor()
|
||||
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')
|
||||
log.event.error('Critical error opening database: ' + err.message, null, function() {
|
||||
process.exit(2)
|
||||
})
|
||||
}).catch(function(err) {
|
||||
log.fatal(err, 'Unknown error occured opening app')
|
||||
log.event.error('Unknown error occured opening app: ' + err.message, null, function() {
|
||||
process.exit(3)
|
||||
})
|
||||
})
|
|
@ -1,11 +0,0 @@
|
|||
import svc from './service.mjs'
|
||||
|
||||
svc.on('install',function(){
|
||||
svc.start();
|
||||
});
|
||||
|
||||
svc.on('alreadyinstalled',function(){
|
||||
svc.start();
|
||||
});
|
||||
|
||||
svc.install();
|
|
@ -1,35 +0,0 @@
|
|||
import path from 'path'
|
||||
import { readFileSync } from 'fs'
|
||||
import { fileURLToPath } from 'url'
|
||||
import nodewindows from 'node-windows'
|
||||
|
||||
function getPathFromRoot(add) {
|
||||
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
||||
return path.join(__dirname,'../', add)
|
||||
}
|
||||
|
||||
let config = JSON.parse(readFileSync(getPathFromRoot('./config.json')))
|
||||
|
||||
const Service = nodewindows.Service
|
||||
|
||||
let serviceConfig = {
|
||||
name: config.serviceName,
|
||||
description: config.description,
|
||||
script: getPathFromRoot('./runner.mjs'),
|
||||
env: {
|
||||
name: 'NODE_ENV',
|
||||
value: 'production',
|
||||
},
|
||||
wait: 0,
|
||||
grow: .5,
|
||||
maxRestarts: 10
|
||||
//, workingDirectory: '...'
|
||||
//, allowServiceLogon: true
|
||||
}
|
||||
|
||||
console.log('Service', serviceConfig)
|
||||
|
||||
// Create a new service object
|
||||
let svc = new Service(serviceConfig);
|
||||
|
||||
export default svc
|
|
@ -1,10 +0,0 @@
|
|||
import svc from './service.mjs'
|
||||
|
||||
// Listen for the "install" event, which indicates the
|
||||
// process is available as a service.
|
||||
svc.on('uninstall',function(){
|
||||
console.log('Uninstall complete.');
|
||||
console.log('The service exists: ',svc.exists);
|
||||
});
|
||||
|
||||
svc.uninstall();
|
|
@ -70,6 +70,24 @@ t.describe('constructor()', function() {
|
|||
assert.strictEqual(typeof(app.http.closeServer), 'function')
|
||||
})
|
||||
|
||||
|
||||
|
||||
t.test('should not default updateEvery if its zero', function() {
|
||||
const assertTest = { a: 1, updateEvery: 0 }
|
||||
const assertName = 'test'
|
||||
ctx.db.config = {
|
||||
test: assertTest,
|
||||
app: { b: 2},
|
||||
manage: { c: 3 },
|
||||
}
|
||||
|
||||
let app = new Application(ctx, {}, assertName)
|
||||
assert.notStrictEqual(app.config, assertTest)
|
||||
assert.strictEqual(app.config.a, assertTest.a)
|
||||
assert.strictEqual(app.config.updateEvery, assertTest.updateEvery)
|
||||
assert.strictEqual(app.config.updateEvery, 0)
|
||||
})
|
||||
|
||||
t.test('should create http instance correctly', function() {
|
||||
ctx.db.config = {
|
||||
testapp: { a: 1, https: true },
|
||||
|
@ -105,6 +123,15 @@ t.timeout(250).describe('#startAutoupdater()', function() {
|
|||
|
||||
app.startAutoupdater()
|
||||
})
|
||||
|
||||
t.test('should do nothing if updateEvery is zero', async function() {
|
||||
const stubInterval = stub()
|
||||
stubInterval.throws(new Error('should not be seen'))
|
||||
ctx.db.config.test = { updateEvery: 0 }
|
||||
let app = new Application(ctx, { }, 'test', { setInterval: stubInterval })
|
||||
|
||||
app.startAutoupdater()
|
||||
})
|
||||
|
||||
t.test('should call setInterval correctly', function() {
|
||||
const assertTimeMinutes = 1440
|
||||
|
|
|
@ -6,31 +6,30 @@ import { request } from '../core/client.mjs'
|
|||
import { setTimeout } from 'timers/promises'
|
||||
import { prettyPrintMessage } from './helpers.mjs'
|
||||
import { pipeline } from 'stream'
|
||||
import getLog from '../core/log.mjs'
|
||||
|
||||
const util = new Util(import.meta.url)
|
||||
const port = 61412
|
||||
|
||||
t.timeout(5000).describe('', function() {
|
||||
t.timeout(10000).describe('', function() {
|
||||
let server = null
|
||||
let prefix = `http://localhost:${port}/`
|
||||
let files = []
|
||||
let logs = []
|
||||
let versions = []
|
||||
let requests = []
|
||||
let processor
|
||||
let integrationLog = getLog('test.integration', [])
|
||||
|
||||
t.before(function(cb) {
|
||||
server = http.createServer(function(req, res) {
|
||||
req.on('error', function(err) {
|
||||
console.log('error', err)
|
||||
integrationLog.error(err, 'error')
|
||||
})
|
||||
res.on('error', function(err) {
|
||||
console.log('error', err)
|
||||
integrationLog.error(err, 'error')
|
||||
})
|
||||
|
||||
requests.push(req.url)
|
||||
|
||||
console.log(req.url)
|
||||
integrationLog.info('[SERVER] got request ' + req.url)
|
||||
|
||||
if (req.url === '/releases') {
|
||||
res.statusCode = 200
|
||||
|
@ -78,7 +77,7 @@ t.timeout(5000).describe('', function() {
|
|||
return fs.rm(file, { force: true, recursive: true })
|
||||
}))
|
||||
.then(function() {
|
||||
if (processor) {
|
||||
if (processor && !processor.exitCode) {
|
||||
processor.kill()
|
||||
}
|
||||
})
|
||||
|
@ -105,6 +104,21 @@ t.timeout(5000).describe('', function() {
|
|||
|
||||
const version_3_crashing = `
|
||||
export function start(http, port, ctx) {
|
||||
process.exit(1)
|
||||
}
|
||||
`
|
||||
|
||||
const version_4_stable = `
|
||||
export function start(http, port, ctx) {
|
||||
const server = http.createServer(function (req, res) {
|
||||
res.writeHead(200);
|
||||
res.end(JSON.stringify({ version: 'v4' }))
|
||||
})
|
||||
|
||||
return server.listenAsync(port, '0.0.0.0')
|
||||
.then(() => {
|
||||
ctx.log.info({ port: port, listening: true }, \`Server is listening on \${port} serving v4\`)
|
||||
})
|
||||
}
|
||||
`
|
||||
|
||||
|
@ -141,6 +155,10 @@ t.timeout(5000).describe('', function() {
|
|||
}
|
||||
}
|
||||
|
||||
integrationLog.on('newlog', function(record) {
|
||||
prettyPrintMessage(JSON.stringify(record))
|
||||
})
|
||||
|
||||
let logWaitIndex = 0
|
||||
function hasLogLine(regMatch) {
|
||||
if (logs.length > logWaitIndex) {
|
||||
|
@ -241,9 +259,12 @@ t.timeout(5000).describe('', function() {
|
|||
assert.strictEqual(checkListening.body.version, 'v1')
|
||||
|
||||
while (!hasLogLine(/core is running/)) {
|
||||
catchupLog()
|
||||
await setTimeout(10)
|
||||
}
|
||||
|
||||
catchupLog()
|
||||
|
||||
let db = JSON.parse(await fs.readFile(util.getPathFromRoot('./db.json')))
|
||||
assert.strictEqual(db.core.testapp.active, assertNameVersion1)
|
||||
assert.strictEqual(db.core.testapp.versions.length, 1)
|
||||
|
@ -257,18 +278,11 @@ t.timeout(5000).describe('', function() {
|
|||
const assertNameVersion2 = 'v2_nolisten'
|
||||
file(`./testapp/${assertNameVersion2}`)
|
||||
versions.splice(0, 0, [assertNameVersion2, 'no listen version', 'v2-sc.7z'])
|
||||
requests.splice(0, requests.length)
|
||||
assert.strictEqual(requests.length, 0)
|
||||
|
||||
// wait a second for it to trigger an update
|
||||
|
||||
await setTimeout(500)
|
||||
|
||||
while (requests.length < 2) {
|
||||
catchupLog()
|
||||
await setTimeout(25)
|
||||
}
|
||||
|
||||
while (!hasLogLine(/Error starting v2_nolisten/)) {
|
||||
catchupLog()
|
||||
await setTimeout(10)
|
||||
|
@ -326,8 +340,8 @@ t.timeout(5000).describe('', function() {
|
|||
assert.ok(findInLogs(/Error starting v2_nolisten/))
|
||||
|
||||
processor.kill()
|
||||
await waitUntilClosed()
|
||||
|
||||
await waitUntilClosed()
|
||||
processor = startRunner()
|
||||
|
||||
listening = await waitUntilListening()
|
||||
|
@ -350,10 +364,85 @@ t.timeout(5000).describe('', function() {
|
|||
assert.notOk(findInLogs(/Attempting to run version v2_nolisten/))
|
||||
assert.notOk(findInLogs(/Error starting v2_nolisten/))
|
||||
|
||||
// Create our third version
|
||||
await fs.writeFile(index, version_3_crashing)
|
||||
await util.runCommand(util.get7zipExecutable(), ['a', file('./v3-sc.7z'), index], util.getPathFromRoot('./testapp'))
|
||||
|
||||
const assertNameVersion3 = 'v3_crash'
|
||||
file(`./testapp/${assertNameVersion3}`)
|
||||
versions.splice(0, 0, [assertNameVersion3, 'crash version', 'v3-sc.7z'])
|
||||
|
||||
// wait a second for it to trigger an update
|
||||
|
||||
await setTimeout(500)
|
||||
|
||||
while (!hasLogLine(/Attempting to run version v3_crash/)) {
|
||||
catchupLog()
|
||||
await setTimeout(10)
|
||||
}
|
||||
|
||||
while (processor.exitCode == null) {
|
||||
catchupLog()
|
||||
await setTimeout(10)
|
||||
}
|
||||
|
||||
db = JSON.parse(await fs.readFile(util.getPathFromRoot('./db.json')))
|
||||
assert.strictEqual(db.core.testapp.active, assertNameVersion3)
|
||||
assert.strictEqual(db.core.testapp.versions.length, 3)
|
||||
assert.strictEqual(db.core.testapp.versions[0].stable, -2)
|
||||
assert.strictEqual(db.core.testapp.versions[1].stable, -2)
|
||||
assert.strictEqual(db.core.testapp.versions[2].stable, 1)
|
||||
|
||||
catchupLog()
|
||||
|
||||
// Should recover afterwards
|
||||
await waitUntilClosed()
|
||||
processor = startRunner()
|
||||
|
||||
listening = await waitUntilListening()
|
||||
|
||||
assert.ok(listening)
|
||||
|
||||
checkListening = await request({}, `http://localhost:${listening.port}/`)
|
||||
assert.strictEqual(checkListening.body.version, 'v1')
|
||||
|
||||
while (!hasLogLine(/core is running/)) {
|
||||
await setTimeout(10)
|
||||
}
|
||||
|
||||
// Create our fourth version
|
||||
await fs.writeFile(index, version_4_stable)
|
||||
await util.runCommand(util.get7zipExecutable(), ['a', file('./v4-sc.7z'), index], util.getPathFromRoot('./testapp'))
|
||||
|
||||
const assertNameVersion4 = 'v4_stable'
|
||||
file(`./testapp/${assertNameVersion4}`)
|
||||
versions.splice(0, 0, [assertNameVersion4, 'no listen version', 'v4-sc.7z'])
|
||||
|
||||
// wait a second for it to trigger an update
|
||||
|
||||
await setTimeout(500)
|
||||
|
||||
while (!hasLogLine(/Attempting to run version v4_stable/)) {
|
||||
catchupLog()
|
||||
await setTimeout(10)
|
||||
}
|
||||
|
||||
while (!hasLogLine(/Server is listening on 31313 serving v4/)) {
|
||||
catchupLog()
|
||||
await setTimeout(10)
|
||||
}
|
||||
|
||||
catchupLog()
|
||||
|
||||
checkListening = await request({}, `http://localhost:${listening.port}/`)
|
||||
assert.strictEqual(checkListening.body.version, 'v4')
|
||||
|
||||
await setTimeout(10)
|
||||
|
||||
db = JSON.parse(await fs.readFile(util.getPathFromRoot('./db.json')))
|
||||
assert.strictEqual(db.core.testapp.active, assertNameVersion4)
|
||||
assert.strictEqual(db.core.testapp.versions.length, 4)
|
||||
assert.strictEqual(db.core.testapp.versions[0].stable, 1)
|
||||
assert.strictEqual(db.core.testapp.versions[1].stable, -2)
|
||||
})
|
||||
})
|
||||
|
|
|
@ -126,27 +126,6 @@ t.describe('Core.addProvider()', function() {
|
|||
})
|
||||
|
||||
t.describe('#constructor()', function() {
|
||||
t.test('should throw if close is not a function', function() {
|
||||
let tests = [
|
||||
[1, 'number'],
|
||||
[0, 'false number'],
|
||||
['asdf', 'string'],
|
||||
['', 'false string'],
|
||||
[[], 'array'],
|
||||
[{}, 'object'],
|
||||
]
|
||||
|
||||
tests.forEach(function(check) {
|
||||
assert.throws(function() {
|
||||
new Core(db, util, log, check[0])
|
||||
}, function(err) {
|
||||
assert.match(err.message, /restart/i)
|
||||
assert.match(err.message, /function/i)
|
||||
return true
|
||||
}, `throw if restart is ${check[1]}`)
|
||||
})
|
||||
})
|
||||
|
||||
t.test('should throw if util is not util', function() {
|
||||
let tests = [
|
||||
[1, 'number'],
|
||||
|
@ -160,7 +139,7 @@ t.describe('#constructor()', function() {
|
|||
|
||||
tests.forEach(function(check) {
|
||||
assert.throws(function() {
|
||||
new Core(db, check[0], log, function() {})
|
||||
new Core(db, check[0], log)
|
||||
}, function(err) {
|
||||
assert.match(err.message, /util/i)
|
||||
assert.match(err.message, /instance/i)
|
||||
|
@ -182,7 +161,7 @@ t.describe('#constructor()', function() {
|
|||
|
||||
tests.forEach(function(check) {
|
||||
assert.throws(function() {
|
||||
new Core(check[0], util, log, function() {})
|
||||
new Core(check[0], util, log)
|
||||
}, function(err) {
|
||||
assert.match(err.message, /db/i)
|
||||
assert.match(err.message, /instance/i)
|
||||
|
@ -230,13 +209,11 @@ t.describe('#constructor()', function() {
|
|||
|
||||
t.test('should accept log, util and close function', function() {
|
||||
const assertLog = log
|
||||
const assertClose = function() {}
|
||||
|
||||
let core = new Core(db, util, assertLog, assertClose)
|
||||
let core = new Core(db, util, assertLog)
|
||||
assert.strictEqual(core.db, db)
|
||||
assert.strictEqual(core.util, util)
|
||||
assert.strictEqual(core.log, assertLog)
|
||||
assert.strictEqual(core.restart, assertClose)
|
||||
assert.deepStrictEqual(core.applications, [])
|
||||
assert.ok(core.applicationMap)
|
||||
})
|
||||
|
@ -247,7 +224,7 @@ t.describe('#getApplication()', function() {
|
|||
t.test('should return application based on the name', function() {
|
||||
const assertName = 'Yami no Naka'
|
||||
const assertApplication = { a: 1 }
|
||||
let core = new Core(db, util, log, function() {})
|
||||
let core = new Core(db, util, log)
|
||||
core.applicationMap.set(assertName, assertApplication)
|
||||
assert.strictEqual(core.getApplication(assertName), assertApplication)
|
||||
})
|
||||
|
@ -268,7 +245,7 @@ t.describe('#init()', function() {
|
|||
|
||||
t.beforeEach(function() {
|
||||
log.error.reset()
|
||||
core = new Core(db, util, log, function() {})
|
||||
core = new Core(db, util, log)
|
||||
core.util = fakeUtil = {
|
||||
verifyConfig: stub(),
|
||||
getAppNames: stub().returns([]),
|
||||
|
@ -453,7 +430,7 @@ t.describe('#run()', function() {
|
|||
versions: []
|
||||
},
|
||||
}
|
||||
core = new Core(db, util, log, function() {})
|
||||
core = new Core(db, util, log)
|
||||
core.runApplication = stubRunApplication = stub().resolves()
|
||||
db.write = stubWrite = stub().resolves()
|
||||
log.info.reset()
|
||||
|
@ -642,7 +619,7 @@ t.describe('#runApplication()', function() {
|
|||
versions: []
|
||||
}
|
||||
}
|
||||
core = new Core(db, util, log, function() {})
|
||||
core = new Core(db, util, log)
|
||||
db.write = stubWrite = stub().resolves()
|
||||
log.info.reset()
|
||||
log.warn.reset()
|
||||
|
@ -770,7 +747,6 @@ t.describe('#runApplication()', function() {
|
|||
assert.strict(testApp.runVersion.firstCall[0], '32')
|
||||
})
|
||||
|
||||
|
||||
t.test('should skip version with stable of -1 if fresh is false', async function() {
|
||||
const assertError = new Error('Daikichi to Rin')
|
||||
testApp.runVersion.rejects(assertError)
|
||||
|
@ -841,7 +817,7 @@ t.describe('#runApplication()', function() {
|
|||
assert.match(testApp.ctx.log.error.secondCall[1], new RegExp('31'))
|
||||
assert.match(testApp.ctx.log.error.secondCall[1], new RegExp(assertError.message))
|
||||
|
||||
assert.strictEqual(db.data.core[testAppName].versions[0].stable, -1)
|
||||
assert.strictEqual(db.data.core[testAppName].versions[0].stable, -2)
|
||||
assert.strictEqual(db.data.core[testAppName].versions[1].stable, -2)
|
||||
|
||||
assert.ok(stubWrite.callCount, 2)
|
||||
|
@ -1003,3 +979,88 @@ t.describe('#runApplication()', function() {
|
|||
assert.ok(stubWrite.called)
|
||||
})
|
||||
})
|
||||
|
||||
t.describe('#criticalError()', function() {
|
||||
let core
|
||||
let testApp
|
||||
let testAppName
|
||||
let stubWrite
|
||||
|
||||
t.beforeEach(function() {
|
||||
testAppName = 'nano.RIPE'
|
||||
core = new Core(db, util, log)
|
||||
db.writeSync = stubWrite = stub()
|
||||
log.info.reset()
|
||||
log.warn.reset()
|
||||
log.error.reset()
|
||||
testApp = {
|
||||
name: testAppName,
|
||||
fresh: false,
|
||||
ctx: {
|
||||
log: {
|
||||
info: stub(),
|
||||
warn: stub(),
|
||||
error: stub(),
|
||||
fatal: stub(),
|
||||
},
|
||||
},
|
||||
closeServer: stub(),
|
||||
runVersion: stub(),
|
||||
}
|
||||
})
|
||||
|
||||
t.test('should log to fatal', function() {
|
||||
const assertVersion = {
|
||||
version: 'Dai kirai! Aishiteru',
|
||||
stable: 0,
|
||||
}
|
||||
assert.notOk(testApp.ctx.log.fatal.called)
|
||||
core.criticalError(testApp, assertVersion)
|
||||
assert.ok(testApp.ctx.log.fatal.called)
|
||||
assert.match(testApp.ctx.log.fatal.firstCall[0], /critical/i)
|
||||
assert.match(testApp.ctx.log.fatal.firstCall[0], new RegExp(assertVersion.version))
|
||||
})
|
||||
|
||||
t.test('should always change to stable -2 regardless of fresh', function() {
|
||||
const assertVersion = {
|
||||
version: 'Dai kirai! Aishiteru',
|
||||
stable: 0,
|
||||
}
|
||||
testApp.fresh = false
|
||||
|
||||
assertVersion.stable = 5
|
||||
core.criticalError(testApp, assertVersion)
|
||||
assert.strictEqual(assertVersion.stable, -2)
|
||||
|
||||
assertVersion.stable = -1
|
||||
core.criticalError(testApp, assertVersion)
|
||||
assert.strictEqual(assertVersion.stable, -2)
|
||||
|
||||
testApp.fresh = true
|
||||
|
||||
assertVersion.stable = 5
|
||||
core.criticalError(testApp, assertVersion)
|
||||
assert.strictEqual(assertVersion.stable, -2)
|
||||
|
||||
assertVersion.stable = -1
|
||||
core.criticalError(testApp, assertVersion)
|
||||
assert.strictEqual(assertVersion.stable, -2)
|
||||
})
|
||||
|
||||
t.test('should call db.writeSync afterwards', function() {
|
||||
let checkStable = 0
|
||||
const assertVersion = {
|
||||
version: 'Dai kirai! Aishiteru',
|
||||
stable: 0,
|
||||
}
|
||||
|
||||
stubWrite.returnWith(function() {
|
||||
checkStable = assertVersion.stable
|
||||
})
|
||||
|
||||
assert.notOk(stubWrite.called)
|
||||
core.criticalError(testApp, assertVersion)
|
||||
assert.ok(stubWrite.called)
|
||||
assert.strictEqual(checkStable, -2)
|
||||
})
|
||||
})
|
|
@ -1,5 +1,6 @@
|
|||
import { Eltro as t, assert, stub } from 'eltro'
|
||||
import fs from 'fs/promises'
|
||||
import fsSync from 'fs'
|
||||
import lowdb from '../core/db.mjs'
|
||||
import Util from '../core/util.mjs'
|
||||
|
||||
|
@ -36,6 +37,35 @@ t.test('Should auto create file with some defaults', async function() {
|
|||
assert.ok(db.data.core.version)
|
||||
assert.notOk(db.data.core.app)
|
||||
assert.notOk(db.data.core.manager)
|
||||
assert.strictEqual(db.filename, util.getPathFromRoot('./db_test.json'))
|
||||
})
|
||||
|
||||
t.test('#writeSync() Should support syncronous write', function() {
|
||||
const filename = util.getPathFromRoot('./db_test.json')
|
||||
const assertValue = 'Yume no Naka no Watashi no Yume'
|
||||
|
||||
return lowdb({}, logger, filename)
|
||||
.then(function(db) {
|
||||
db.data.songtest = assertValue
|
||||
|
||||
db.writeSync()
|
||||
|
||||
let content = JSON.parse(fsSync.readFileSync(filename))
|
||||
assert.strictEqual(content.songtest, assertValue)
|
||||
})
|
||||
})
|
||||
|
||||
t.test('#writeSync() Should not throw', async function() {
|
||||
let db = await lowdb({}, logger, util.getPathFromRoot('./db_test.json'))
|
||||
|
||||
db.filename = util.getPathFromRoot('../test')
|
||||
|
||||
assert.notOk(db.log.error.called)
|
||||
db.writeSync()
|
||||
assert.ok(db.log.error.called)
|
||||
assert.match(db.log.error.firstCall[0].message, /directory/i)
|
||||
assert.match(db.log.error.firstCall[1], /writ/)
|
||||
assert.match(db.log.error.firstCall[1], new RegExp(db.filename.replace(/\\/g, '\\\\')))
|
||||
})
|
||||
|
||||
t.test('Should support in-memory db', async function() {
|
||||
|
|
|
@ -1,5 +0,0 @@
|
|||
@setlocal enableextensions
|
||||
@cd /d "%~dp0"
|
||||
|
||||
node service\uninstall.mjs
|
||||
PAUSE
|
Loading…
Reference in a new issue