From 47344c5e7a383e25b2a9c3a35c78c0f3a1a75f56 Mon Sep 17 00:00:00 2001 From: Jonatan Nilsson Date: Fri, 18 Feb 2022 13:31:56 +0000 Subject: [PATCH] Updated core logic and how stable is calculated. Fixed some minor bugs. Will now no longer travel through history but instead stop at last stable version. --- core/client.mjs | 4 +- core/core.mjs | 33 ++++-- core/providers/git.mjs | 3 +- core/runner.mjs | 9 +- test/application.test.integration.mjs | 1 + test/client.test.mjs | 17 ++-- test/core.test.integration.mjs | 73 +++++++------- test/core.test.mjs | 128 ++++++++++++++++++------ test/providers/git.test.integration.mjs | 2 +- test/providers/git.test.mjs | 10 +- 10 files changed, 185 insertions(+), 95 deletions(-) diff --git a/core/client.mjs b/core/client.mjs index 65384d8..468a63f 100644 --- a/core/client.mjs +++ b/core/client.mjs @@ -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 diff --git a/core/core.mjs b/core/core.mjs index 3587cca..f6e399b 100644 --- a/core/core.mjs +++ b/core/core.mjs @@ -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) } diff --git a/core/providers/git.mjs b/core/providers/git.mjs index 437a3fe..fa93ed4 100644 --- a/core/providers/git.mjs +++ b/core/providers/git.mjs @@ -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, diff --git a/core/runner.mjs b/core/runner.mjs index 2533570..57268df 100644 --- a/core/runner.mjs +++ b/core/runner.mjs @@ -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() diff --git a/test/application.test.integration.mjs b/test/application.test.integration.mjs index fdcdf9a..4cf1e59 100644 --- a/test/application.test.integration.mjs +++ b/test/application.test.integration.mjs @@ -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) diff --git a/test/client.test.mjs b/test/client.test.mjs index 916dce0..fb7b847 100644 --- a/test/client.test.mjs +++ b/test/client.test.mjs @@ -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')) }) }) diff --git a/test/core.test.integration.mjs b/test/core.test.integration.mjs index 45969ab..c34a8c7 100644 --- a/test/core.test.integration.mjs +++ b/test/core.test.integration.mjs @@ -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() diff --git a/test/core.test.mjs b/test/core.test.mjs index 591d1d1..71b6536 100644 --- a/test/core.test.mjs +++ b/test/core.test.mjs @@ -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() { diff --git a/test/providers/git.test.integration.mjs b/test/providers/git.test.integration.mjs index 8381440..2f965f5 100644 --- a/test/providers/git.test.integration.mjs +++ b/test/providers/git.test.integration.mjs @@ -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() }) diff --git a/test/providers/git.test.mjs b/test/providers/git.test.mjs index ba1ca51..2332477 100644 --- a/test/providers/git.test.mjs +++ b/test/providers/git.test.mjs @@ -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() {