Updated core logic and how stable is calculated.
continuous-integration/appveyor/branch AppVeyor build failed Details

Fixed some minor bugs.
Will now no longer travel through history but instead stop at last stable version.
master
Jonatan Nilsson 2022-02-18 13:31:56 +00:00
parent 57be8a144a
commit 47344c5e7a
10 changed files with 185 additions and 95 deletions

View File

@ -19,13 +19,13 @@ export function request(config, path, filePath = null, redirects, fastRaw = fals
}
let newRedirects = (redirects || 0) + 1
if (!path || typeof(path) !== 'string' || !path.startsWith('http')) {
return Promise.reject(new Error('URL was empty or invalid'))
return Promise.reject(new Error('URL was empty or invalid' + (typeof(path) === 'string' ? ': ' + path : '')))
}
let parsed
try {
parsed = new URL(path)
} catch {
return Promise.reject(new Error('URL was empty or invalid'))
return Promise.reject(new Error('URL was empty or invalid: ' + path))
}
let h = http

View File

@ -22,7 +22,7 @@ export default class Core {
Core.providers.set(name, provider)
}
constructor(db, util, log) {
constructor(db, util, log, restart = function() {}) {
// some sanity checks
if (!log || typeof(log) !== 'object') throw new Error('log parameter was invalid')
if (typeof(log.event) !== 'object') throw new Error('log parameter was invalid')
@ -34,11 +34,13 @@ export default class Core {
|| typeof(log.event.error) !== 'function') throw new Error('log parameter was invalid')
if (!util || !(util instanceof Util)) throw new Error('util not instance of Util')
if (!db || !(db instanceof Low)) throw new Error('db not instance of Low')
if (typeof(restart) !== 'function') throw new Error('restart was not a function')
this.running = false
this.db = db
this.util = util
this.log = log
this.restart = restart
this.applications = []
this.applicationMap = new Map()
this._applicationFatalCrash = null
@ -130,12 +132,17 @@ export default class Core {
for (let i = 0; i < this.db.data.core[name].versions.length; i++) {
let version = this.db.data.core[name].versions[i]
if (!version.installed || version.stable < -1) continue
if (version.stable < 0 && !application.fresh) continue
if (version.stable < 0 && !application.fresh) {
application.ctx.log.warn(`Restarting for ${version.version} due to last run failing while not being in fresh state`)
return this.restart(`Application ${name} has fresh false while attempting to run ${version.version} with stable -1`)
}
await application.closeServer()
this._applicationFatalCrash = this.criticalError.bind(this, application, version)
process.once('exit', this._applicationFatalCrash)
let wasFresh = application.fresh
try {
application.ctx.log.info(`Attempting to run version ${version.version}`)
await application.runVersion(version.version)
@ -144,13 +151,23 @@ export default class Core {
await this.db.write()
break
} catch(err) {
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}`)
process.off('exit', this._applicationFatalCrash)
if (version.stable < 1) {
if (wasFresh) {
version.stable = -2
} else {
version.stable = -1
}
await this.db.write()
if (version.stable === -1) {
return this.restart(`Application ${name} version ${version.version} failed to start but application was dirty, check if restarting fixes it`)
}
} else {
await this.db.write()
return this.restart(`Application ${name} version ${version.version} previously stable but now failing`)
}
} finally {
process.off('exit', this._applicationFatalCrash)
}

View File

@ -25,8 +25,7 @@ export default class GitProvider {
checked++
for (let asset of item.assets) {
if (!asset.name.endsWith('-sc.7z')
&& !asset.name.endsWith('-sc.zip')) continue
if (!asset.name.endsWith('-sc.7z')) continue
return {
version: item.name,

View File

@ -26,7 +26,14 @@ 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)
const core = new Core(db, util, log, function(msg) {
if (msg) {
runner.log.warn('Got a restart request: ' + msg)
} else {
runner.log.warn('Got a restart request with no message or reason')
}
process.exit(1)
})
await core.init()
await core.run()

View File

@ -36,6 +36,7 @@ t.timeout(10000).test('should run update and install correctly', async function(
try {
await app.update()
} catch (err) {
console.log()
console.log(err)
if (ctx.db.data.core.testapp.versions.length) {
console.log(ctx.db.data.core.testapp.versions[0].log)

View File

@ -51,17 +51,20 @@ t.describe('Basics', function() {
})
t.test('should fail if url is invalid', async function() {
function checkError(err) {
function checkError(check, err) {
assert.match(err.message, /invalid/i)
assert.match(err.message, /url/i)
if (check) {
assert.match(err.message, new RegExp(check))
}
}
await assert.isRejected(request({}, 123)).then(checkError)
await assert.isRejected(request({}, [])).then(checkError)
await assert.isRejected(request({}, {})).then(checkError)
await assert.isRejected(request({}, '')).then(checkError)
await assert.isRejected(request({}, 'asdf')).then(checkError)
await assert.isRejected(request({}, 'httpppp')).then(checkError)
await assert.isRejected(request({}, 123)).then(checkError.bind(this, null))
await assert.isRejected(request({}, [])).then(checkError.bind(this, null))
await assert.isRejected(request({}, {})).then(checkError.bind(this, null))
await assert.isRejected(request({}, '')).then(checkError.bind(this, null))
await assert.isRejected(request({}, 'asdf')).then(checkError.bind(this, 'asdf'))
await assert.isRejected(request({}, 'httpppp')).then(checkError.bind(this, 'httpppp'))
})
})

View File

@ -151,12 +151,15 @@ t.timeout(10000).describe('', function() {
let logIndex = 0
function catchupLog() {
function catchupLog(ms = 0) {
if (logs.length > logIndex) {
for (; logIndex < logs.length; logIndex++) {
prettyPrintMessage(logs[logIndex])
}
}
if (ms > 0) {
return setTimeout(ms)
}
}
integrationLog.on('newlog', function(record) {
@ -195,11 +198,14 @@ t.timeout(10000).describe('', function() {
let listeningLine = null
while (processor.exitCode == null
&& !hasLogLine((rec) => { listeningLine = rec; return rec.listening && rec.port })) {
catchupLog()
await setTimeout(10)
await catchupLog(10)
}
catchupLog()
return listeningLine
if (listeningLine.listening && listeningLine.port) {
return listeningLine
} else {
return null
}
}
async function waitUntilClosed(listening) {
@ -232,8 +238,7 @@ t.timeout(10000).describe('', function() {
while (processor.exitCode == null) {
catchupLog()
await setTimeout(10)
await catchupLog(10)
}
catchupLog()
@ -263,8 +268,7 @@ t.timeout(10000).describe('', function() {
assert.strictEqual(checkListening.body.version, 'v1')
while (!hasLogLine(/core is running/)) {
catchupLog()
await setTimeout(10)
await catchupLog(10)
}
catchupLog()
@ -288,48 +292,43 @@ t.timeout(10000).describe('', function() {
await setTimeout(500)
while (!hasLogLine(/Error starting v2_nolisten/)) {
catchupLog()
await setTimeout(10)
await catchupLog(10)
}
while (!hasLogLine(/Attempting to run version v1_ok/)) {
catchupLog()
await setTimeout(10)
while (!hasLogLine(/restart request.*v2_nolisten.*dirty/)) {
await catchupLog(10)
}
while (!hasLogLine(/Server is listening on 31313 serving v1/)) {
catchupLog()
await setTimeout(10)
while (processor.exitCode == null) {
await catchupLog(10)
}
catchupLog()
checkListening = await request({}, `http://localhost:${listening.port}/`)
assert.strictEqual(checkListening.body.version, 'v1')
await setTimeout(10)
db = JSON.parse(await fs.readFile(util.getPathFromRoot('./db.json')))
assert.strictEqual(db.core.testapp.active, assertNameVersion1)
assert.strictEqual(db.core.testapp.active, assertNameVersion2)
assert.strictEqual(db.core.testapp.versions.length, 2)
assert.strictEqual(db.core.testapp.versions[0].stable, -1)
assert.strictEqual(db.core.testapp.versions[0].installed, true)
assert.strictEqual(db.core.testapp.versions[1].stable, 1)
assert.strictEqual(db.core.testapp.versions[1].installed, true)
processor.kill()
// Wait for ports to be marked as closed
// Since application was in dirty state, on next attempt should attempt to
// run v2 again and then falling back to v1
await waitUntilClosed()
processor = startRunner()
await catchupLog(10)
while (!hasLogLine(/Attempting to run version v2_nolisten/)) {
await catchupLog(10)
}
while (!hasLogLine(/Attempting to run version v1_ok/)) {
await catchupLog(10)
}
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)
}
@ -381,13 +380,11 @@ t.timeout(10000).describe('', function() {
await setTimeout(500)
while (!hasLogLine(/Attempting to run version v3_crash/)) {
catchupLog()
await setTimeout(10)
await catchupLog(10)
}
while (processor.exitCode == null) {
catchupLog()
await setTimeout(10)
await catchupLog(10)
}
db = JSON.parse(await fs.readFile(util.getPathFromRoot('./db.json')))
@ -427,13 +424,11 @@ t.timeout(10000).describe('', function() {
await setTimeout(500)
while (!hasLogLine(/Attempting to run version v4_stable/)) {
catchupLog()
await setTimeout(10)
await catchupLog(10)
}
while (!hasLogLine(/Server is listening on 31313 serving v4/)) {
catchupLog()
await setTimeout(10)
await catchupLog(10)
}
catchupLog()

View File

@ -207,13 +207,36 @@ t.describe('#constructor()', function() {
})
})
t.test('should throw if restart 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 accept log, util and close function', function() {
const assertLog = log
const assertRestarter = function() { }
let core = new Core(db, util, assertLog)
let core = new Core(db, util, assertLog, assertRestarter)
assert.strictEqual(core.db, db)
assert.strictEqual(core.util, util)
assert.strictEqual(core.log, assertLog)
assert.strictEqual(core.restart, assertRestarter)
assert.deepStrictEqual(core.applications, [])
assert.ok(core.applicationMap)
})
@ -626,6 +649,7 @@ t.describe('#runApplication()', function() {
}
}
core = new Core(db, util, log)
core.restart = stub()
db.write = stubWrite = stub().resolves()
log.info.reset()
log.warn.reset()
@ -667,6 +691,7 @@ t.describe('#runApplication()', function() {
const assertVersion = 'v50'
const assertError = new Error('jellyfish')
testApp.runVersion.rejects(assertError)
testApp.fresh = true
db.data.core[testAppName].versions.push({
id: assertVersion,
version: assertVersion + 'asdf',
@ -690,6 +715,7 @@ t.describe('#runApplication()', function() {
t.test('should skip versions that have not been installed or high error', async function() {
const assertError = new Error('Daikichi to Rin')
testApp.runVersion.rejects(assertError)
testApp.fresh = true
db.data.core[testAppName].versions.push({
id: '40',
version: 'v40',
@ -721,7 +747,10 @@ t.describe('#runApplication()', function() {
t.test('should attempt to run version with stable of -1 if fresh is true', async function() {
const assertError = new Error('Daikichi to Rin')
testApp.runVersion.rejects(assertError)
testApp.runVersion.returnWith(function() {
testApp.fresh = false
return Promise.reject(assertError)
})
testApp.fresh = true
db.data.core[testAppName].versions.push({
id: '30',
@ -740,8 +769,7 @@ t.describe('#runApplication()', function() {
stable: 0,
}, )
let err = await assert.isRejected(core.runApplication(testApp))
assert.notStrictEqual(err, assertError)
await core.runApplication(testApp)
assert.notOk(log.error.called)
assert.ok(testApp.ctx.log.error.called)
@ -751,11 +779,15 @@ t.describe('#runApplication()', function() {
assert.match(testApp.ctx.log.error.firstCall[1], new RegExp(assertError.message))
assert.strict(testApp.runVersion.firstCall[0], '31')
assert.strict(testApp.runVersion.firstCall[0], '32')
assert.strictEqual(db.data.core[testAppName].versions[1].stable, -2)
assert.strictEqual(db.data.core[testAppName].versions[2].stable, -1)
})
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)
t.test('should call restart if program crashes and fresh is false', async function() {
const assertErrorMessage = new Error('Daikichi to Rin')
const assertError = new Error('Country Lane')
testApp.runVersion.rejects(assertErrorMessage)
core.restart.rejects(assertError)
testApp.fresh = false
db.data.core[testAppName].versions.push({
id: '30',
@ -775,22 +807,22 @@ t.describe('#runApplication()', function() {
}, )
let err = await assert.isRejected(core.runApplication(testApp))
assert.notStrictEqual(err, assertError)
assert.strictEqual(err, assertError)
assert.notOk(log.error.called)
assert.ok(testApp.ctx.log.error.called)
assert.strictEqual(testApp.ctx.log.error.callCount, 2)
assert.strictEqual(testApp.ctx.log.error.firstCall[0], assertError)
assert.strictEqual(testApp.ctx.log.error.callCount, 1)
assert.strictEqual(testApp.ctx.log.error.firstCall[0], assertErrorMessage)
assert.match(testApp.ctx.log.error.firstCall[1], new RegExp('30'))
assert.match(testApp.ctx.log.error.firstCall[1], new RegExp(assertError.message))
assert.strictEqual(testApp.ctx.log.error.secondCall[0], assertError)
assert.match(testApp.ctx.log.error.secondCall[1], new RegExp('32'))
assert.match(testApp.ctx.log.error.secondCall[1], new RegExp(assertError.message))
assert.strict(testApp.runVersion.firstCall[0], '30')
assert.strict(testApp.runVersion.firstCall[0], '32')
assert.match(testApp.ctx.log.error.firstCall[1], new RegExp(assertErrorMessage.message))
assert.strictEqual(db.data.core[testAppName].versions[0].stable, -1)
assert.ok(core.restart.called)
assert.match(core.restart.firstCall[0], new RegExp(testAppName))
assert.match(core.restart.firstCall[0], /v30/)
assert.match(core.restart.firstCall[0], /dirty/)
})
t.test('should change status accordingly when application is fresh', async function() {
t.test('should attempt next non-tested version if fresh is true', async function() {
const assertError = new Error('Daikichi to Rin')
testApp.runVersion.rejects(assertError)
testApp.fresh = true
@ -829,9 +861,49 @@ t.describe('#runApplication()', function() {
assert.ok(stubWrite.callCount, 2)
})
t.test('should always go to -1 minimum on crash', async function() {
t.test('should exit immediately if next version is -1 and fresh is false', async function() {
const assertError = new Error('Daikichi to Rin')
testApp.runVersion.rejects(assertError)
core.restart.rejects(assertError)
testApp.fresh = false
db.data.core[testAppName].versions.push({
id: '30',
version: 'v30',
installed: true,
stable: -1,
}, {
id: '31',
version: 'v31',
installed: true,
stable: 0,
}, {
id: '32',
version: 'v32',
installed: true,
stable: -2,
})
let err = await assert.isRejected(core.runApplication(testApp))
assert.strictEqual(err, assertError)
assert.notOk(log.error.called)
assert.ok(testApp.ctx.log.warn.called)
assert.strictEqual(testApp.ctx.log.warn.callCount, 1)
assert.match(testApp.ctx.log.warn.firstCall[0], /restart/i)
assert.match(testApp.ctx.log.warn.firstCall[0], /fresh/i)
assert.match(testApp.ctx.log.warn.firstCall[0], /v30/i)
assert.match(core.restart.firstCall[0], /v30/i)
assert.match(core.restart.firstCall[0], /fresh/i)
assert.match(core.restart.firstCall[0], /-1/i)
assert.match(core.restart.firstCall[0], new RegExp(testAppName))
assert.strictEqual(db.data.core[testAppName].versions[0].stable, -1)
assert.strictEqual(db.data.core[testAppName].versions[1].stable, 0)
})
t.test('should stop on first stable and call core.restart if crash occurs', async function() {
const assertError = new Error('Daikichi to Rin')
testApp.runVersion.rejects(new Error('empty message'))
core.restart.rejects(assertError)
testApp.fresh = false
db.data.core[testAppName].versions.push({
id: '28',
@ -856,18 +928,16 @@ t.describe('#runApplication()', function() {
})
let err = await assert.isRejected(core.runApplication(testApp))
assert.notStrictEqual(err, assertError)
assert.strictEqual(err, assertError)
assert.notOk(log.error.called)
assert.ok(testApp.ctx.log.error.called)
assert.strictEqual(testApp.ctx.log.error.callCount, 3)
assert.strictEqual(db.data.core[testAppName].versions[0].stable, -1)
assert.strictEqual(db.data.core[testAppName].versions[1].stable, -1)
assert.strictEqual(db.data.core[testAppName].versions[2].stable, -1)
assert.strictEqual(db.data.core[testAppName].versions[3].stable, -1)
assert.ok(stubWrite.callCount, 2)
assert.strictEqual(testApp.ctx.log.error.callCount, 1)
assert.strictEqual(db.data.core[testAppName].versions[0].stable, 5)
assert.strictEqual(stubWrite.callCount, 1)
assert.ok(core.restart.called)
assert.match(core.restart.firstCall[0], new RegExp(testAppName))
assert.match(core.restart.firstCall[0], /v28/)
assert.match(core.restart.firstCall[0], /stable/)
})
t.test('should throw if no stable version is found', async function() {

View File

@ -44,7 +44,7 @@ t.timeout(5000).describe('#checkConfig()', function() {
})
t.test('should otherwise succeed', function() {
return new GitProvider({ url: 'https://git.nfp.is/api/v1/repos/TheThing/sc-manager/releases' })
return new GitProvider({ url: 'https://git.nfp.is/api/v1/repos/TheThing/sc-helloworld/releases' })
.checkConfig()
})

View File

@ -20,7 +20,7 @@ t.describe('#getLatestVersion()', function() {
assert.strictEqual(version.log, '')
})
t.test('should support multiple extension', async function() {
t.test('should skip zip files for now', async function() {
const assertName = 'Karen'
const assertLink = 'My Wings'
const assertFilename = 'test-sc.zip'
@ -31,11 +31,9 @@ t.describe('#getLatestVersion()', function() {
{ name: assertName, assets: [{ name: assertFilename, browser_download_url: assertLink }] },
]})
let version = await provider.getLatestVersion()
assert.strictEqual(version.version, assertName)
assert.strictEqual(version.link, assertLink)
assert.strictEqual(version.filename, assertFilename)
assert.strictEqual(version.log, '')
let err = await assert.isRejected(provider.getLatestVersion())
assert.match(err.message, /release/)
assert.match(err.message, /found/)
})
t.test('should skip versions with no assets', async function() {