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
|
chmod -R 777 /appveyor/projects
|
||||||
npm install
|
npm install
|
||||||
npm test
|
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
|
this.fresh = true
|
||||||
|
|
||||||
// Apply defaults to config
|
// 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.startWaitUntilFail = this.config.startWaitUntilFail || (60 * 1000)
|
||||||
this.config.heartbeatTimeout = this.config.heartbeatTimeout || (3 * 1000)
|
this.config.heartbeatTimeout = this.config.heartbeatTimeout || (3 * 1000)
|
||||||
this.config.heartbeatAttempts = this.config.heartbeatAttempts || 5
|
this.config.heartbeatAttempts = this.config.heartbeatAttempts || 5
|
||||||
|
@ -49,6 +49,7 @@ export default class Application extends EventEmitter {
|
||||||
|
|
||||||
startAutoupdater() {
|
startAutoupdater() {
|
||||||
if (this.provider.static) return
|
if (this.provider.static) return
|
||||||
|
if (this.config.updateEvery === 0) return
|
||||||
|
|
||||||
let timer = this.setInterval(() => {
|
let timer = this.setInterval(() => {
|
||||||
this.update().then(
|
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)
|
Core.providers.set(name, provider)
|
||||||
}
|
}
|
||||||
|
|
||||||
constructor(db, util, log, restart) {
|
constructor(db, util, log) {
|
||||||
// some sanity checks
|
// 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 (!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.event) !== 'object') throw new Error('log parameter was invalid')
|
||||||
if (typeof(log.info) !== 'function'
|
if (typeof(log.info) !== 'function'
|
||||||
|
@ -40,9 +39,9 @@ export default class Core {
|
||||||
this.db = db
|
this.db = db
|
||||||
this.util = util
|
this.util = util
|
||||||
this.log = log
|
this.log = log
|
||||||
this.restart = restart
|
|
||||||
this.applications = []
|
this.applications = []
|
||||||
this.applicationMap = new Map()
|
this.applicationMap = new Map()
|
||||||
|
this._applicationFatalCrash = null
|
||||||
}
|
}
|
||||||
|
|
||||||
getApplication(name) {
|
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) {
|
async runApplication(application) {
|
||||||
let name = application.name
|
let name = application.name
|
||||||
let found = false
|
let found = false
|
||||||
|
@ -128,6 +133,9 @@ export default class Core {
|
||||||
if (version.stable < 0 && !application.fresh) continue
|
if (version.stable < 0 && !application.fresh) continue
|
||||||
await application.closeServer()
|
await application.closeServer()
|
||||||
|
|
||||||
|
this._applicationFatalCrash = this.criticalError.bind(this, application, version)
|
||||||
|
process.once('exit', this._applicationFatalCrash)
|
||||||
|
|
||||||
try {
|
try {
|
||||||
application.ctx.log.info(`Attempting to run version ${version.version}`)
|
application.ctx.log.info(`Attempting to run version ${version.version}`)
|
||||||
await application.runVersion(version.version)
|
await application.runVersion(version.version)
|
||||||
|
@ -136,9 +144,15 @@ export default class Core {
|
||||||
await this.db.write()
|
await this.db.write()
|
||||||
break
|
break
|
||||||
} catch(err) {
|
} 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()
|
await this.db.write()
|
||||||
application.ctx.log.error(err, `Error starting ${version.version}: ${err.message}`)
|
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 { setTimeout } from 'timers/promises'
|
||||||
import { Low, JSONFile, Memory } from 'lowdb'
|
import { Low, JSONFile, Memory } from 'lowdb'
|
||||||
|
import fs from 'fs'
|
||||||
import { defaults, isObject } from './defaults.mjs'
|
import { defaults, isObject } from './defaults.mjs'
|
||||||
|
|
||||||
export default function GetDB(config, log, orgFilename = 'db.json') {
|
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)
|
const db = new Low(adapter)
|
||||||
|
|
||||||
db.id = 'id'
|
db.id = 'id'
|
||||||
|
db.filename = fullpath
|
||||||
db.config = config
|
db.config = config
|
||||||
|
|
||||||
db.createId = function(collection) {
|
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()
|
return db.read()
|
||||||
.then(function() {
|
.then(function() {
|
||||||
if (!isObject(db.data)) {
|
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
|
runner.log = log
|
||||||
const db = await GetDB(config, log, util.getPathFromRoot('./' + dbname))
|
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.init()
|
||||||
await core.run()
|
await core.run()
|
||||||
|
|
||||||
|
|
|
@ -188,7 +188,9 @@ export default class Util {
|
||||||
processor._kill = processor.kill
|
processor._kill = processor.kill
|
||||||
processor.kill = function() {
|
processor.kill = function() {
|
||||||
if(os.platform() === 'win32'){
|
if(os.platform() === 'win32'){
|
||||||
execSync('taskkill /pid ' + processor.pid + ' /T /F')
|
try {
|
||||||
|
execSync('taskkill /pid ' + processor.pid + ' /T /F')
|
||||||
|
} catch {}
|
||||||
}else{
|
}else{
|
||||||
processor.kill();
|
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",
|
"homepage": "https://github.com/TheThing/service-core#readme",
|
||||||
"files": [
|
"files": [
|
||||||
"lib.mjs",
|
"index.mjs",
|
||||||
|
"cli.mjs",
|
||||||
"package.json",
|
"package.json",
|
||||||
"README.md",
|
"README.md",
|
||||||
"core"
|
"core",
|
||||||
|
"bin"
|
||||||
],
|
],
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"bunyan-lite": "^1.0.1",
|
"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')
|
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() {
|
t.test('should create http instance correctly', function() {
|
||||||
ctx.db.config = {
|
ctx.db.config = {
|
||||||
testapp: { a: 1, https: true },
|
testapp: { a: 1, https: true },
|
||||||
|
@ -106,6 +124,15 @@ t.timeout(250).describe('#startAutoupdater()', function() {
|
||||||
app.startAutoupdater()
|
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() {
|
t.test('should call setInterval correctly', function() {
|
||||||
const assertTimeMinutes = 1440
|
const assertTimeMinutes = 1440
|
||||||
const stubInterval = stub()
|
const stubInterval = stub()
|
||||||
|
|
|
@ -6,31 +6,30 @@ import { request } from '../core/client.mjs'
|
||||||
import { setTimeout } from 'timers/promises'
|
import { setTimeout } from 'timers/promises'
|
||||||
import { prettyPrintMessage } from './helpers.mjs'
|
import { prettyPrintMessage } from './helpers.mjs'
|
||||||
import { pipeline } from 'stream'
|
import { pipeline } from 'stream'
|
||||||
|
import getLog from '../core/log.mjs'
|
||||||
|
|
||||||
const util = new Util(import.meta.url)
|
const util = new Util(import.meta.url)
|
||||||
const port = 61412
|
const port = 61412
|
||||||
|
|
||||||
t.timeout(5000).describe('', function() {
|
t.timeout(10000).describe('', function() {
|
||||||
let server = null
|
let server = null
|
||||||
let prefix = `http://localhost:${port}/`
|
let prefix = `http://localhost:${port}/`
|
||||||
let files = []
|
let files = []
|
||||||
let logs = []
|
let logs = []
|
||||||
let versions = []
|
let versions = []
|
||||||
let requests = []
|
|
||||||
let processor
|
let processor
|
||||||
|
let integrationLog = getLog('test.integration', [])
|
||||||
|
|
||||||
t.before(function(cb) {
|
t.before(function(cb) {
|
||||||
server = http.createServer(function(req, res) {
|
server = http.createServer(function(req, res) {
|
||||||
req.on('error', function(err) {
|
req.on('error', function(err) {
|
||||||
console.log('error', err)
|
integrationLog.error(err, 'error')
|
||||||
})
|
})
|
||||||
res.on('error', function(err) {
|
res.on('error', function(err) {
|
||||||
console.log('error', err)
|
integrationLog.error(err, 'error')
|
||||||
})
|
})
|
||||||
|
|
||||||
requests.push(req.url)
|
integrationLog.info('[SERVER] got request ' + req.url)
|
||||||
|
|
||||||
console.log(req.url)
|
|
||||||
|
|
||||||
if (req.url === '/releases') {
|
if (req.url === '/releases') {
|
||||||
res.statusCode = 200
|
res.statusCode = 200
|
||||||
|
@ -78,7 +77,7 @@ t.timeout(5000).describe('', function() {
|
||||||
return fs.rm(file, { force: true, recursive: true })
|
return fs.rm(file, { force: true, recursive: true })
|
||||||
}))
|
}))
|
||||||
.then(function() {
|
.then(function() {
|
||||||
if (processor) {
|
if (processor && !processor.exitCode) {
|
||||||
processor.kill()
|
processor.kill()
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
@ -105,6 +104,21 @@ t.timeout(5000).describe('', function() {
|
||||||
|
|
||||||
const version_3_crashing = `
|
const version_3_crashing = `
|
||||||
export function start(http, port, ctx) {
|
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
|
let logWaitIndex = 0
|
||||||
function hasLogLine(regMatch) {
|
function hasLogLine(regMatch) {
|
||||||
if (logs.length > logWaitIndex) {
|
if (logs.length > logWaitIndex) {
|
||||||
|
@ -241,9 +259,12 @@ t.timeout(5000).describe('', function() {
|
||||||
assert.strictEqual(checkListening.body.version, 'v1')
|
assert.strictEqual(checkListening.body.version, 'v1')
|
||||||
|
|
||||||
while (!hasLogLine(/core is running/)) {
|
while (!hasLogLine(/core is running/)) {
|
||||||
|
catchupLog()
|
||||||
await setTimeout(10)
|
await setTimeout(10)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
catchupLog()
|
||||||
|
|
||||||
let db = JSON.parse(await fs.readFile(util.getPathFromRoot('./db.json')))
|
let db = JSON.parse(await fs.readFile(util.getPathFromRoot('./db.json')))
|
||||||
assert.strictEqual(db.core.testapp.active, assertNameVersion1)
|
assert.strictEqual(db.core.testapp.active, assertNameVersion1)
|
||||||
assert.strictEqual(db.core.testapp.versions.length, 1)
|
assert.strictEqual(db.core.testapp.versions.length, 1)
|
||||||
|
@ -257,18 +278,11 @@ t.timeout(5000).describe('', function() {
|
||||||
const assertNameVersion2 = 'v2_nolisten'
|
const assertNameVersion2 = 'v2_nolisten'
|
||||||
file(`./testapp/${assertNameVersion2}`)
|
file(`./testapp/${assertNameVersion2}`)
|
||||||
versions.splice(0, 0, [assertNameVersion2, 'no listen version', 'v2-sc.7z'])
|
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
|
// wait a second for it to trigger an update
|
||||||
|
|
||||||
await setTimeout(500)
|
await setTimeout(500)
|
||||||
|
|
||||||
while (requests.length < 2) {
|
|
||||||
catchupLog()
|
|
||||||
await setTimeout(25)
|
|
||||||
}
|
|
||||||
|
|
||||||
while (!hasLogLine(/Error starting v2_nolisten/)) {
|
while (!hasLogLine(/Error starting v2_nolisten/)) {
|
||||||
catchupLog()
|
catchupLog()
|
||||||
await setTimeout(10)
|
await setTimeout(10)
|
||||||
|
@ -326,8 +340,8 @@ t.timeout(5000).describe('', function() {
|
||||||
assert.ok(findInLogs(/Error starting v2_nolisten/))
|
assert.ok(findInLogs(/Error starting v2_nolisten/))
|
||||||
|
|
||||||
processor.kill()
|
processor.kill()
|
||||||
await waitUntilClosed()
|
|
||||||
|
|
||||||
|
await waitUntilClosed()
|
||||||
processor = startRunner()
|
processor = startRunner()
|
||||||
|
|
||||||
listening = await waitUntilListening()
|
listening = await waitUntilListening()
|
||||||
|
@ -350,10 +364,85 @@ t.timeout(5000).describe('', function() {
|
||||||
assert.notOk(findInLogs(/Attempting to run version v2_nolisten/))
|
assert.notOk(findInLogs(/Attempting to run version v2_nolisten/))
|
||||||
assert.notOk(findInLogs(/Error starting 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) {
|
while (processor.exitCode == null) {
|
||||||
catchupLog()
|
catchupLog()
|
||||||
await setTimeout(10)
|
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()
|
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.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() {
|
t.test('should throw if util is not util', function() {
|
||||||
let tests = [
|
let tests = [
|
||||||
[1, 'number'],
|
[1, 'number'],
|
||||||
|
@ -160,7 +139,7 @@ t.describe('#constructor()', function() {
|
||||||
|
|
||||||
tests.forEach(function(check) {
|
tests.forEach(function(check) {
|
||||||
assert.throws(function() {
|
assert.throws(function() {
|
||||||
new Core(db, check[0], log, function() {})
|
new Core(db, check[0], log)
|
||||||
}, function(err) {
|
}, function(err) {
|
||||||
assert.match(err.message, /util/i)
|
assert.match(err.message, /util/i)
|
||||||
assert.match(err.message, /instance/i)
|
assert.match(err.message, /instance/i)
|
||||||
|
@ -182,7 +161,7 @@ t.describe('#constructor()', function() {
|
||||||
|
|
||||||
tests.forEach(function(check) {
|
tests.forEach(function(check) {
|
||||||
assert.throws(function() {
|
assert.throws(function() {
|
||||||
new Core(check[0], util, log, function() {})
|
new Core(check[0], util, log)
|
||||||
}, function(err) {
|
}, function(err) {
|
||||||
assert.match(err.message, /db/i)
|
assert.match(err.message, /db/i)
|
||||||
assert.match(err.message, /instance/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() {
|
t.test('should accept log, util and close function', function() {
|
||||||
const assertLog = log
|
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.db, db)
|
||||||
assert.strictEqual(core.util, util)
|
assert.strictEqual(core.util, util)
|
||||||
assert.strictEqual(core.log, assertLog)
|
assert.strictEqual(core.log, assertLog)
|
||||||
assert.strictEqual(core.restart, assertClose)
|
|
||||||
assert.deepStrictEqual(core.applications, [])
|
assert.deepStrictEqual(core.applications, [])
|
||||||
assert.ok(core.applicationMap)
|
assert.ok(core.applicationMap)
|
||||||
})
|
})
|
||||||
|
@ -247,7 +224,7 @@ t.describe('#getApplication()', function() {
|
||||||
t.test('should return application based on the name', function() {
|
t.test('should return application based on the name', function() {
|
||||||
const assertName = 'Yami no Naka'
|
const assertName = 'Yami no Naka'
|
||||||
const assertApplication = { a: 1 }
|
const assertApplication = { a: 1 }
|
||||||
let core = new Core(db, util, log, function() {})
|
let core = new Core(db, util, log)
|
||||||
core.applicationMap.set(assertName, assertApplication)
|
core.applicationMap.set(assertName, assertApplication)
|
||||||
assert.strictEqual(core.getApplication(assertName), assertApplication)
|
assert.strictEqual(core.getApplication(assertName), assertApplication)
|
||||||
})
|
})
|
||||||
|
@ -268,7 +245,7 @@ t.describe('#init()', function() {
|
||||||
|
|
||||||
t.beforeEach(function() {
|
t.beforeEach(function() {
|
||||||
log.error.reset()
|
log.error.reset()
|
||||||
core = new Core(db, util, log, function() {})
|
core = new Core(db, util, log)
|
||||||
core.util = fakeUtil = {
|
core.util = fakeUtil = {
|
||||||
verifyConfig: stub(),
|
verifyConfig: stub(),
|
||||||
getAppNames: stub().returns([]),
|
getAppNames: stub().returns([]),
|
||||||
|
@ -453,7 +430,7 @@ t.describe('#run()', function() {
|
||||||
versions: []
|
versions: []
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
core = new Core(db, util, log, function() {})
|
core = new Core(db, util, log)
|
||||||
core.runApplication = stubRunApplication = stub().resolves()
|
core.runApplication = stubRunApplication = stub().resolves()
|
||||||
db.write = stubWrite = stub().resolves()
|
db.write = stubWrite = stub().resolves()
|
||||||
log.info.reset()
|
log.info.reset()
|
||||||
|
@ -642,7 +619,7 @@ t.describe('#runApplication()', function() {
|
||||||
versions: []
|
versions: []
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
core = new Core(db, util, log, function() {})
|
core = new Core(db, util, log)
|
||||||
db.write = stubWrite = stub().resolves()
|
db.write = stubWrite = stub().resolves()
|
||||||
log.info.reset()
|
log.info.reset()
|
||||||
log.warn.reset()
|
log.warn.reset()
|
||||||
|
@ -770,7 +747,6 @@ t.describe('#runApplication()', function() {
|
||||||
assert.strict(testApp.runVersion.firstCall[0], '32')
|
assert.strict(testApp.runVersion.firstCall[0], '32')
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
||||||
t.test('should skip version with stable of -1 if fresh is false', async function() {
|
t.test('should skip version with stable of -1 if fresh is false', async function() {
|
||||||
const assertError = new Error('Daikichi to Rin')
|
const assertError = new Error('Daikichi to Rin')
|
||||||
testApp.runVersion.rejects(assertError)
|
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('31'))
|
||||||
assert.match(testApp.ctx.log.error.secondCall[1], new RegExp(assertError.message))
|
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.strictEqual(db.data.core[testAppName].versions[1].stable, -2)
|
||||||
|
|
||||||
assert.ok(stubWrite.callCount, 2)
|
assert.ok(stubWrite.callCount, 2)
|
||||||
|
@ -1003,3 +979,88 @@ t.describe('#runApplication()', function() {
|
||||||
assert.ok(stubWrite.called)
|
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 { Eltro as t, assert, stub } from 'eltro'
|
||||||
import fs from 'fs/promises'
|
import fs from 'fs/promises'
|
||||||
|
import fsSync from 'fs'
|
||||||
import lowdb from '../core/db.mjs'
|
import lowdb from '../core/db.mjs'
|
||||||
import Util from '../core/util.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.ok(db.data.core.version)
|
||||||
assert.notOk(db.data.core.app)
|
assert.notOk(db.data.core.app)
|
||||||
assert.notOk(db.data.core.manager)
|
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() {
|
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