Compare commits

...

39 Commits

Author SHA1 Message Date
Jonatan Nilsson 6c720f4c2d Fix git integration test
continuous-integration/appveyor/branch AppVeyor build succeeded Details
2024-02-16 06:52:50 +00:00
Jonatan Nilsson 14fead9c17 core: Update tests after moving version info
continuous-integration/appveyor/branch AppVeyor build failed Details
2024-02-16 06:50:34 +00:00
Jonatan Nilsson 6c115aa8b2 core: In application, move version into context
continuous-integration/appveyor/branch AppVeyor build failed Details
2024-02-16 06:48:29 +00:00
Jonatan Nilsson 5d39f776e1 git: Auto replace spaces with underscores in version output. Allows versions to have spaces in their name while keeping it unix friendly folder and file name.
continuous-integration/appveyor/branch AppVeyor build succeeded Details
2022-08-18 10:54:38 +00:00
Jonatan Nilsson 79f3203f70 Fix integration test based on latest changes in git config loading
continuous-integration/appveyor/branch AppVeyor build succeeded Details
2022-08-13 02:10:40 +00:00
Jonatan Nilsson e8a2ec52a6 git: Add support to filter based on specific prefix 2022-08-13 02:10:24 +00:00
Jonatan Nilsson 1995b38510 git: checkConfig no longer checks repo url. Git being down or latest version being unfetchable should not stop services from starting up.
continuous-integration/appveyor/branch AppVeyor build failed Details
Package: Remove beta flag, is stable enough for now
2022-08-13 01:48:05 +00:00
Jonatan Nilsson 63a06a2a34 git: checkConfig no longer checks repo url. Git being down or latest version being unfetchable should not stop services from starting up.
Package: Remove beta flag, is stable enough for now
2022-08-13 01:47:05 +00:00
Jonatan Nilsson cb5de72e13 db: Don't warn about clearing db when using in-memory db
continuous-integration/appveyor/branch AppVeyor build succeeded Details
2022-04-19 16:37:45 +00:00
Jonatan Nilsson 33b3d98a37 application: Expose app config in ctx.config
continuous-integration/appveyor/branch AppVeyor build succeeded Details
2022-04-02 20:10:09 +00:00
Jonatan Nilsson 618cfd0451 log: Workers in cluster will now properly notify master of newlog entries. Useful for sc-manager
continuous-integration/appveyor/branch AppVeyor build succeeded Details
2022-04-01 09:59:47 +00:00
Jonatan Nilsson b5359515b5 lib: Fix so it displays the restart message during restart
continuous-integration/appveyor/branch AppVeyor build succeeded Details
2022-03-30 08:19:49 +00:00
Jonatan Nilsson 23f9173720 test: Fix regex match error after some recent changes
continuous-integration/appveyor/branch AppVeyor build succeeded Details
2022-03-29 20:38:59 +00:00
Jonatan Nilsson d519996959 Application: Add flag in application to keep track if its running and which version.
continuous-integration/appveyor/branch AppVeyor build failed Details
Lib: Add restart support to shut down server.
Runner: Add better logging when shut down request is sent.
Many: Add bunch of event emitters for interested parties
2022-03-29 17:14:50 +00:00
Jonatan Nilsson 90d4e7ab81 application: Emit event when updating changes
continuous-integration/appveyor/branch AppVeyor build succeeded Details
2022-03-28 15:15:30 +00:00
Jonatan Nilsson a5239d0c27 db: Removed unused latestVersion
continuous-integration/appveyor/branch AppVeyor build succeeded Details
2022-03-28 07:48:30 +00:00
Jonatan Nilsson 46386a13b3 application: Add more event emits on specific events
continuous-integration/appveyor/branch AppVeyor build succeeded Details
2022-03-28 07:40:25 +00:00
Jonatan Nilsson 99d7a0655d lib: Now generates a valid config and enforces provider on lib to be static
continuous-integration/appveyor/branch AppVeyor build succeeded Details
2022-03-27 16:02:53 +00:00
Jonatan Nilsson 95d72fc404 Application: Expose some helper classes to application through ctx.sc
continuous-integration/appveyor/branch AppVeyor build succeeded Details
2022-03-27 15:12:04 +00:00
Jonatan Nilsson 2edcedb1b7 cli: Add basic install support for linux
continuous-integration/appveyor/branch AppVeyor build succeeded Details
http: Fix so listenAsync defaults to all hosts
package: Increment version
2022-03-13 01:22:45 +00:00
Jonatan Nilsson 4a508d20a4 lib: Fix test for the new port parameter support
continuous-integration/appveyor/branch AppVeyor build succeeded Details
2022-03-10 13:40:10 +00:00
Jonatan Nilsson 1d7b118229 lib: Add optional port parameter that is prefilled. Prevents crashes if no port is specified
continuous-integration/appveyor/branch AppVeyor build failed Details
package: Increment beta version
2022-03-10 13:38:35 +00:00
Jonatan Nilsson 1f70f36e8d package: Update dependencies and version
continuous-integration/appveyor/branch AppVeyor build succeeded Details
2022-03-10 13:35:34 +00:00
Jonatan Nilsson 73e9be2ff0 fix core integration test yet again
continuous-integration/appveyor/branch AppVeyor build succeeded Details
2022-03-10 13:11:23 +00:00
Jonatan Nilsson 87cc47f498 Make the integration test slightly more consistent
continuous-integration/appveyor/branch AppVeyor build failed Details
2022-03-10 13:10:02 +00:00
Jonatan Nilsson b8a0ec137c Increment version, create a new release
continuous-integration/appveyor/branch AppVeyor build failed Details
2022-03-10 13:08:29 +00:00
Jonatan Nilsson 67606b9b3b Fixed lib and finished implementing it
continuous-integration/appveyor/branch AppVeyor build succeeded Details
2022-03-10 13:08:02 +00:00
Jonatan Nilsson 4024f1269a remove lodash
continuous-integration/appveyor/branch AppVeyor build succeeded Details
2022-03-10 12:34:54 +00:00
Jonatan Nilsson 25cac907be Fix core integration test
continuous-integration/appveyor/branch AppVeyor build succeeded Details
2022-03-10 12:27:01 +00:00
Jonatan Nilsson 4d15623a23 Fix some test, update version
continuous-integration/appveyor/branch AppVeyor build failed Details
2022-03-10 12:24:57 +00:00
Jonatan Nilsson 746e0c3f46 Fix bug in client.mjs
continuous-integration/appveyor/branch AppVeyor build failed Details
2022-03-10 11:30:43 +00:00
Jonatan Nilsson 44f576c1d4 More appveyor stuff :(
continuous-integration/appveyor/branch AppVeyor build failed Details
2022-03-10 11:28:29 +00:00
Jonatan Nilsson dccad23890 Fix bug with missing log level
continuous-integration/appveyor/branch AppVeyor build failed Details
2022-03-10 11:15:02 +00:00
Jonatan Nilsson 5330ecfa5c More appveyor stuff :(
continuous-integration/appveyor/branch AppVeyor build failed Details
2022-03-10 11:13:35 +00:00
Jonatan Nilsson b152f18fc3 appveyor: Should now fail test if test fail
continuous-integration/appveyor/branch AppVeyor build failed Details
2022-03-10 11:11:17 +00:00
Jonatan Nilsson bc2d3d9a9b Increment version to beta 3
continuous-integration/appveyor/branch AppVeyor build cancelled Details
2022-03-10 11:07:01 +00:00
Jonatan Nilsson e0da74f0be Finished implementing cluster support 2022-03-10 11:06:17 +00:00
Jonatan Nilsson 47344c5e7a 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.
2022-02-18 13:32:44 +00:00
Jonatan Nilsson 57be8a144a Some cleanup on files. Remove skip tests 2022-02-18 08:16:36 +00:00
31 changed files with 2242 additions and 841 deletions

View File

@ -1,58 +0,0 @@
version: 2
jobs:
build:
docker:
- image: circleci/node:latest
resource_class: medium
working_directory: ~/app
steps:
- checkout
- run:
name: Install npm deployment app
command: sudo npm install -g github-release-cli @babel/runtime
- run:
name: Check if this is a new release
command: |
set +e
PACKAGE_VERSION=$(cat package.json | grep version | head -1 | awk -F: '{ print $2 }' | sed 's/[", ]//g')
github-release list --owner $CIRCLE_PROJECT_USERNAME --repo $CIRCLE_PROJECT_REPONAME | grep "tag_name=\"v${PACKAGE_VERSION}\""
retVal=$?
if [ $retVal -eq 0 ]; then
echo "Release already exists. Quitting early"
circleci step halt
fi
- run:
name: Get current git commit message
command: |
echo "export COMMIT_MESSAGE=\"$(git log --format=oneline -n 1 $CIRCLE_SHA1)\"" >> $BASH_ENV
source $BASH_ENV
- run:
name: Get 7zz
command: |
wget https://www.7-zip.org/a/7z2106-linux-x64.tar.xz
tar -xvf 7z2106-linux-x64.tar.xz
- deploy:
name: Create a release
command: |
PACKAGE_VERSION=$(cat package.json | grep version | head -1 | awk -F: '{ print $2 }' | sed 's/[", ]//g')
echo "Creating release v${PACKAGE_VERSION}"
./7zz a "${CIRCLE_PROJECT_REPONAME}_build-sc-core.7z" runner.mjs package.json "service/*" "core/**/*" "core/*" -mx9 -mfb273 -mmt2 -md16M -mqs -myx9
./7zz a "${CIRCLE_PROJECT_REPONAME}_app.zip" runner.mjs package.json "service/*" "core/**/*" "core/*" config.json install.bat npminstall.bat README.md uninstall.bat
echo "Creating release '${PACKAGE_VERSION}'"
github-release upload \
--commitish $CIRCLE_SHA1 \
--token $GITHUB_TOKEN \
--owner $CIRCLE_PROJECT_USERNAME \
--repo $CIRCLE_PROJECT_REPONAME \
--tag "v${PACKAGE_VERSION}" \
--release-name "v${PACKAGE_VERSION}" \
--body "Automatic CircleCI Build of v${PACKAGE_VERSION} from ${CIRCLE_SHA1}: ${COMMIT_MESSAGE}" \
"${CIRCLE_PROJECT_REPONAME}_build-sc-core.zip" "${CIRCLE_PROJECT_REPONAME}_app.zip"
workflows:
version: 2
build_deploy:
jobs:
- build:
context: github-thething

98
.gitignore vendored
View File

@ -1,111 +1,13 @@
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
lerna-debug.log*
# Diagnostic reports (https://nodejs.org/api/report.html)
report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
# Runtime data
pids
*.pid
*.seed
*.pid.lock
# Directory for instrumented libs generated by jscoverage/JSCover
lib-cov
# Coverage directory used by tools like istanbul
coverage
*.lcov
# nyc test coverage
.nyc_output
# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
.grunt
# Bower dependency directory (https://bower.io/)
bower_components
# node-waf configuration
.lock-wscript
# Compiled binary addons (https://nodejs.org/api/addons.html)
build/Release
# Dependency directories
node_modules/
jspm_packages/
# TypeScript v1 declaration files
typings/
# TypeScript cache
*.tsbuildinfo
# Optional npm cache directory
.npm
# Optional eslint cache
.eslintcache
# Microbundle cache
.rpt2_cache/
.rts2_cache_cjs/
.rts2_cache_es/
.rts2_cache_umd/
# Optional REPL history
.node_repl_history
# Output of 'npm pack'
*.tgz
# Yarn Integrity file
.yarn-integrity
# dotenv environment variables file
.env
.env.test
# parcel-bundler cache (https://parceljs.org/)
.cache
# Next.js build output
.next
# Nuxt.js build / generate output
.nuxt
dist
# Gatsby files
.cache/
# Comment in the public line in if your project uses Gatsby and *not* Next.js
# https://nextjs.org/blog/next-9-1#public-directory-support
# public
# vuepress build output
.vuepress/dist
# Serverless directories
.serverless/
# FuseBox cache
.fusebox/
# DynamoDB Local files
.dynamodb/
# TernJS port file
.tern-port
# Custom ignore
db.json
package-lock.json
daemon
dev/public/main.js

View File

@ -19,9 +19,7 @@ clone_depth: 1
build_cloud: Docker
environment:
APPVEYOR_SSH_KEY: ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIBRMxhawMlUlQ8l4pOaeHsZl8XDO54WQngkYM1U/XB4m samsyn\jonatan@JonatanAMD
docker_image: node:16-alpine
npm_config_cache: /appveyor/projects/cache
test_script:
- sh: |
@ -29,8 +27,14 @@ test_script:
ln -s /lib/libc.musl-x86_64.so.1 /lib64/ld-linux-x86-64.so.2
chmod -R 777 /appveyor/projects
npm install
npm test
npm run test:integration
npm run test -- --ignore-only
if [ $? -ne 0 ]; then
exit 1
fi
npm run test:integration -- --ignore-only
if [ $? -ne 0 ]; then
exit 1
fi
artifacts:
- path: ./*_sc-core.7z
@ -39,8 +43,16 @@ artifacts:
on_success:
- sh: |
apk add curl jq
if [ $? -eq 0 ]; then
echo "Finished installling curl and jq"
else
exit 1
fi
CURR_VER=$(cat package.json | jq -r .version)
if curl -s -X GET -H "Authorization: token $deploytoken" https://git.nfp.is/api/v1/repos/$APPVEYOR_REPO_NAME/releases | grep -o "\"name\"\:\"v${CURR_VER}\"" > /dev/null; then
echo "Checking https://git.nfp.is/api/v1/repos/$APPVEYOR_REPO_NAME/releases for version v${CURR_VER}"
curl -s -X GET -H "Authorization: token $deploytoken" https://git.nfp.is/api/v1/repos/$APPVEYOR_REPO_NAME/releases | grep -o "\"name\"\:\"v${CURR_VER}\"" > /dev/null
if [ $? -eq 0 ] ; then
echo "Release already exists, nothing to do.";
else
./test/7zas a -mx9 "${CURR_VER}_sc-core.7z" package.json index.mjs cli.mjs core bin
@ -50,7 +62,7 @@ on_success:
-H "Authorization: token $deploytoken" \
-H "Content-Type: application/json" \
https://git.nfp.is/api/v1/repos/$APPVEYOR_REPO_NAME/releases \
-d "{\"tag_name\":\"v${CURR_VER}\",\"name\":\"v${CURR_VER}\",\"body\":\"Automatic release from Appveyor from ${APPVEYOR_REPO_COMMIT}: ${APPVEYOR_REPO_COMMIT_MESSAGE}\"}")
-d "{\"tag_name\":\"v${CURR_VER}\",\"name\":\"v${CURR_VER}\",\"body\":\"Automatic release from Appveyor from ${APPVEYOR_REPO_COMMIT} :\n\n${APPVEYOR_REPO_COMMIT_MESSAGE}\"}")
RELEASE_ID=$(echo $RELEASE_RESULT | jq -r .id)
echo "Adding ${CURR_VER}_sc-core.7z to release ${RELEASE_ID}"
curl \
@ -62,16 +74,16 @@ on_success:
echo "Publishing new version to npm"
npm publish
fi
- sh: |
YELLOW='\033[0;33m'
NC='\033[0m'
LOCK_FILE="${HOME}/build.lock"
export APPVEYOR_SSH_BLOCK=true
touch "${LOCK_FILE}"
echo -e "Build paused. To resume it, open a bash session to run '${YELLOW}rm \"${LOCK_FILE}\"${NC}' command."
while [ -f "${LOCK_FILE}" ]; do
sleep 1
done
# - sh: |
# YELLOW='\033[0;33m'
# NC='\033[0m'
# LOCK_FILE="${HOME}/build.lock"
# export APPVEYOR_SSH_BLOCK=true
# touch "${LOCK_FILE}"
# echo -e "Build paused. To resume it, open a bash session to run '${YELLOW}rm \"${LOCK_FILE}\"${NC}' command."
# while [ -f "${LOCK_FILE}" ]; do
# sleep 1
# done
# on build failure
on_failure:

24
cli.mjs
View File

@ -82,9 +82,18 @@ if (args[0] === 'checkconfig') {
const runner = path.join(process.cwd(), './runner.mjs')
fs.stat(runner).catch(function() {
console.log('Creating runner.mjs')
return fs.writeFile(runner, basicRunnerTemplate)
}).then(function() {
console.log('Linking local service-core instance')
return util.runCommand('npm', ['link', 'service-core'], null, function(stream) {
console.log(stream.replace('\n', ''))
})
}).then(function() {
return Promise.all([
util.runCommand('npm', ['link', 'service-core'], null, function(stream) {
console.log(stream.replace('\n', ''))
}),
fs.readFile(configFile),
import('node-windows'),
])
@ -124,8 +133,19 @@ if (args[0] === 'checkconfig') {
svc.install();
})
} else {
console.log('non windows install targets are currently unsupported')
process.exit(2)
const runner = path.join(process.cwd(), './runner.mjs')
fs.stat(runner).catch(function() {
console.log('Creating runner.mjs')
return fs.writeFile(runner, basicRunnerTemplate)
}).then(function() {
console.log('Linking local service-core instance')
return util.runCommand('npm', ['link', 'service-core'], null, function(stream) {
console.log(stream.replace('\n', ''))
})
}).then(function() {
console.log('Runner is ready to be added to init.d')
})
}
} else if (args[0] === 'uninstall') {
if(os.platform() === 'win32') {

View File

@ -1,9 +1,16 @@
import { EventEmitter } from 'events'
import cluster from 'cluster'
import fs from 'fs/promises'
import { request } from './client.mjs'
import HttpServer from './http.mjs'
import { defaults } from './defaults.mjs'
import Util from './util.mjs'
import bunyan from 'bunyan-lite'
import getLog from './log.mjs'
export default class Application extends EventEmitter {
constructor(ctx, provider, name, opts = {}) {
super()
@ -13,6 +20,14 @@ export default class Application extends EventEmitter {
log: ctx.log,
core: ctx.core,
app: this,
sc: {
Util: Util,
bunyan: bunyan,
getLog: getLog,
HttpServer: HttpServer,
request: request,
},
version: '',
}
this.config = defaults({}, this.ctx.db.config[name])
this.provider = provider
@ -20,7 +35,7 @@ export default class Application extends EventEmitter {
this.updating = false
this.http = new HttpServer(this.config)
this.module = null
this.running = false
this.workers = {}
// Fresh is used to indicate that when we run the application and it fails,
// whether the environment we are in was fresh or not. An example would be
@ -38,18 +53,36 @@ export default class Application extends EventEmitter {
this.config.heartbeatAttempts = this.config.heartbeatAttempts || 5
this.config.heartbeatAttemptsWait = this.config.heartbeatAttemptsWait || (2 * 1000)
this.config.heartbeatPath = this.config.heartbeatPath || '/'
this.config.clusterWaitOnCrash = this.config.clusterWaitOnCrash || (1 * 1000)
this.ctx.config = this.config
Object.assign(this, {
setInterval: opts.setInterval || setInterval,
setTimeout: opts.setTimeout || setTimeout,
fs: opts.fs || fs,
cluster: opts.cluster || cluster
})
this.isSlave = this.cluster.isWorker
this.monitoringCluster = false
if (this.config.cluster && !this.isSlave) {
for (let i = 1; i <= this.config.cluster; i++) {
this.workers[i] = null
}
}
this.ctx.db.addApplication(name)
}
startAutoupdater() {
if (this.provider.static) return
if (this.config.updateEvery === 0) return
if (this.isSlave) {
this.ctx.log.warn('app.startAutoupdater was called from within a slave')
return
}
let timer = this.setInterval(() => {
this.update().then(
@ -64,30 +97,39 @@ export default class Application extends EventEmitter {
timer.unref()
}
updateLog(message) {
updateLog(message, level = 'info') {
this.ctx.db.data.core[this.name].updater += message
this.ctx.log.info(message)
this.ctx.log[level](message)
return message
}
msgStatic = 'Provider in question is static and so no update required, nothing to do.'
update() {
if (this.isSlave) {
this.ctx.log.warn('app.update was called from within a slave')
return null
}
if (this.provider.static) {
if (this.ctx.db.data.core[this.name].updater !== this.msgStatic) {
this.ctx.db.data.core[this.name].updater = ''
this.updateLog(this.msgStatic)
this.emit('updatelog', this.msgStatic)
return this.ctx.db.write().then(function() { return null })
}
return Promise.resolve(null)
}
if (this.updating) return null
this.updating = true
this.emit('updating', this.updating)
return this._update()
.then((result) => {
this.updating = false
this.emit('updating', this.updating)
return this.ctx.db.write()
.then(function() { return result })
})
@ -122,18 +164,21 @@ export default class Application extends EventEmitter {
let latest = null
try {
log += this.updateLog(`Checking for latest version at ${new Date().toISOString().replace('T', ' ').split('.')[0]}. `) + '\n'
log += this.updateLog(`Checking for latest version at ${new Date().toISOString().replace('T', ' ').split('.')[0]}. `, 'debug') + '\n'
this.emit('updatelog', log)
// Get the latest version from our provider
latest = await this.provider.getLatestVersion()
log += this.updateLog(`Found ${latest.version}. `) + '\n'
// If the versino matches the latest installed, then there's nothing to do
if (this.ctx.db.data.core[this.name].latestInstalled === latest.version) {
this.updateLog('Already up to date, nothing to do. ')
log += this.updateLog(`Found ${latest.version}. `, 'debug') + '\n'
log += this.updateLog('Already up to date, nothing to do. ', 'debug')
this.emit('updatelog', log)
return null
}
log += this.updateLog(`Found new update ${latest.version}. `) + '\n'
// Make the id for the vesion the version number. Allows for easy lookup
// among other nice and simple structure.
@ -144,7 +189,8 @@ export default class Application extends EventEmitter {
if (found) {
// Check if the existing version found was already installed.
if (found.installed) {
this.updateLog('Version was already installed, nothing to do. ')
log += this.updateLog('Version was already installed, nothing to do. ')
this.emit('updatelog', log)
return null
}
@ -160,12 +206,14 @@ export default class Application extends EventEmitter {
// if so, we should skip them and consider those versions as black
// listed and avoid at all cost.
if (latest.failtodownload && latest.failtodownload > 3) {
this.updateLog('Version failed to download too many times, skipping this version. ')
log += this.updateLog('Version failed to download too many times, skipping this version. ')
this.emit('updatelog', log)
return null
}
if (latest.failtoinstall && latest.failtoinstall > 3) {
this.updateLog('Version failed to install too many times, skipping this version. ')
log += this.updateLog('Version failed to install too many times, skipping this version. ')
this.emit('updatelog', log)
return null
}
@ -176,6 +224,8 @@ export default class Application extends EventEmitter {
latest.stable = 0
this.ctx.db.upsertFirst(this.ctx.db.data.core[this.name].versions, latest)
}
this.emit('updatelog', log)
this.emit('update', latest)
// The target file for the archive and the target folder for new our version
let target = this.ctx.util.getPathFromRoot(`./${this.name}/${latest.version}/file${this.ctx.util.getExtension(latest.filename)}`)
@ -185,6 +235,7 @@ export default class Application extends EventEmitter {
await this.fs.mkdir(folder, { recursive: true })
log += this.updateLog(`Downloading ${latest.link} to ${target}. `) + '\n'
this.emit('updatelog', log)
await this.ctx.db.write()
// Download the latest version using the provider in question.
@ -195,11 +246,13 @@ export default class Application extends EventEmitter {
})
log += '\n' + this.updateLog(`Extracting ${target}. `) + '\n'
this.emit('updatelog', log)
await this.ctx.db.write()
// Download was successful, extract the archived file that we downloaded
await this.ctx.util.extractFile(target, function(msg) {
await this.ctx.util.extractFile(target, (msg) => {
log += msg
this.emit('updatelog', log)
}).catch(function(err) {
latest.failtodownload = (latest.failtodownload || 0) + 1
return Promise.reject(err)
@ -219,6 +272,7 @@ export default class Application extends EventEmitter {
.catch((err) => {
latest.failtodownload = (latest.failtodownload || 0) + 1
log += this.updateLog('Version did not include or was missing index.mjs. ') + '\n'
this.emit('updatelog', log)
return Promise.reject(err)
})
@ -236,23 +290,29 @@ export default class Application extends EventEmitter {
if (packageStat) {
log += this.updateLog(`running npm install --production. `) + '\n'
this.emit('updatelog', log)
await this.ctx.db.write()
// For some weird reason, --loglevel=notice is required otherwise
// we get practically zero log output.
await this.ctx.util.runCommand(
'npm.cmd',
this.ctx.util.getNpmExecutable(),
['install', '--production', '--no-optional', '--no-package-lock', '--no-audit', '--loglevel=notice'],
folder,
function(msg) { log += msg }
(msg) => {
log += msg
this.emit('updatelog', log)
}
).catch(function(err) {
latest.failtoinstall = (latest.failtoinstall || 0) + 1
return Promise.reject(err)
})
log = this.logAddSeperator(log)
this.emit('updatelog', log)
} else {
log += this.updateLog('Release did not contain package.json, skipping npm install. ') + '\n'
this.emit('updatelog', log)
}
} catch (err) {
log += this.updateLog(`Error: ${err.message}. `) + '\n'
@ -268,12 +328,14 @@ export default class Application extends EventEmitter {
if (latest) {
latest.log = log
}
this.emit('updatelog', log)
return Promise.reject(err)
}
// If we reached here then everything went swimmingly. Mark the version
// as being installed and attach the install log to it.
log += this.updateLog(`Finished updating ${this.name} to version ${latest.version}.`) + '\n'
this.emit('updatelog', log)
this.ctx.db.data.core[this.name].latestInstalled = latest.version
latest.installed = true
latest.log = log
@ -291,56 +353,145 @@ export default class Application extends EventEmitter {
this.module = module
}
async runVersion(version) {
workerDied(worker) {
if (this.workers[worker.w_id] !== worker) {
return
}
this.workers[worker.w_id] = null
this.setTimeout(() => {
if (this.workers[worker.w_id]) return
this.startForkProcess(worker.w_id, this.ctx.db.data.core[this.name].active)
}, this.clusterWaitOnCrash)
}
startForkProcess(i, version) {
this.workers[i] = this.cluster.fork({
CLUSTER_APP_NAME: this.name,
CLUSTER_APP_VERSION: version,
CLUSTER_APP_INDEX: i,
})
let pid = this.workers[i].process.pid
this.ctx.log.info(`Creating worker ${i} (${pid})`)
// this.workers[i].once('listening', () => {
// this.ctx.log.info(`Fork ${i} is alive and listening`)
// })
this.workers[i].once('exit', (code, signal) => {
if (signal) {
// this.ctx.log.info(`Worker ${i} (${pid}) was killed by signal ${signal}`)
} else if (code !== 0) {
this.ctx.log.warn(`Worker ${i} (${pid}) exited with code ${code}`)
}
})
this.workers[i].w_id = i
this.workers[i].listening = false
this.workers[i].started = new Date()
}
runVersion(version) {
this.ctx.db.data.core[this.name].active = version
this.ctx.version = version
this.emit('running', this.ctx.version)
return this.ctx.db.write().then(() => {
return this._runVersion(version)
.catch((err) => {
this.ctx.version = ''
this.emit('running', this.ctx.version)
return Promise.reject(err)
})
})
}
async _runVersion(version) {
this.ctx.db.data.core[this.name].active = version
await this.ctx.db.write()
if (version !== 'static') {
let indexPath = this.ctx.util.getPathFromRoot(`./${this.name}/${version}/index.mjs`)
await this.fs.stat(indexPath).catch((err) => {
return Promise.reject(new Error(`Version was missing index.mjs: ${err.message}`))
if (this.config.cluster && !this.isSlave) {
for (let i = 1; i <= this.config.cluster; i++) {
this.startForkProcess(i, version)
}
} else {
if (version !== 'static') {
let indexPath = this.ctx.util.getPathFromRoot(`./${this.name}/${version}/index.mjs`)
await this.fs.stat(indexPath).catch((err) => {
return Promise.reject(new Error(`Version was missing index.mjs: ${err.message}`))
})
this.fresh = false
let module = await import(this.ctx.util.getUrlFromRoot(`./${this.name}/${version}/index.mjs`))
this.registerModule(module, version)
} else {
this.fresh = false
}
let errTimeout = new Error(`Version timed out (took over ${this.config.startWaitUntilFail}ms) while running start()`)
await new Promise((res, rej) => {
setTimeout(() => {
rej(errTimeout)
}, this.config.startWaitUntilFail)
let startRes = this.module.start(this.http, this.config.port || null, this.ctx)
if (startRes && startRes.then) {
return startRes.then(res, rej)
}
res()
})
this.fresh = false
let module = await import(this.ctx.util.getUrlFromRoot(`./${this.name}/${version}/index.mjs`))
this.registerModule(module, version)
} else {
this.fresh = false
if (!this.http.active) {
return Promise.reject(new Error(`Version did not call http.createServer()`))
}
}
let errTimeout = new Error(`Version timed out (took over ${this.config.startWaitUntilFail}ms) while running start()`)
await new Promise((res, rej) => {
setTimeout(() => {
rej(errTimeout)
}, this.config.startWaitUntilFail)
let startRes = this.module.start(this.http, this.config.port, this.ctx)
if (startRes && startRes.then) {
return startRes.then(res, rej)
}
res()
})
if (!this.http.active) {
return Promise.reject(new Error(`Version did not call http.createServer()`))
if (this.isSlave) {
return
}
let lastErr = null
for (let i = 0; i < this.config.heartbeatAttempts; i++) {
try {
await request({ timeout: this.config.heartbeatAttemptsWait }, `http://localhost:${this.config.port}` + this.config.heartbeatPath, null, 0, true)
return
await request({ timeout: this.config.heartbeatTimeout }, `http://localhost:${this.config.port}` + this.config.heartbeatPath, null, 0, true)
lastErr = null
break
} catch (err) {
lastErr = err
await new Promise((res) => {
setTimeout(res, this.config.heartbeatAttemptsWait)
})
// this.config.heartbeatAttemptsWait
}
}
return Promise.reject(new Error(`Version failed to start properly: ${lastErr.message}`))
if (lastErr) {
let err = new Error(`Version failed to start properly: ${lastErr.message}`)
err.stack = lastErr.stack
return Promise.reject(err)
}
if (this.config.cluster && !this.isSlave) {
this.__clusterWorkerDied = this.workerDied.bind(this)
this.cluster.on('exit', this.__clusterWorkerDied)
}
}
closeServer() {
this.ctx.version = ''
this.emit('running', this.ctx.version)
if (this.config.cluster && !this.isSlave) {
if (this.__clusterWorkerDied) {
this.cluster.off('exit', this.__clusterWorkerDied)
}
this.__clusterWorkerDied = null
for (let i = 1; i <= this.config.cluster; i++) {
if (this.workers[i]) {
this.workers[i].process.kill()
this.workers[i] = null
}
}
}
return this.http.closeServer()
}
}

View File

@ -2,6 +2,7 @@ import http from 'http'
import https from 'https'
import stream from 'stream/promises'
import fs from 'fs'
import Util from './util.mjs'
function resolveRelative(from, to) {
const resolvedUrl = new URL(to, new URL(from, 'resolve://'));
@ -19,13 +20,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
@ -35,7 +36,7 @@ export function request(config, path, filePath = null, redirects, fastRaw = fals
let req = null
return new Promise(function(resolve, reject) {
let orgErr = new Error(); return new Promise(function(resolve, reject) {
if (!path) {
return reject(new Error('Request path was empty'))
}
@ -52,7 +53,7 @@ export function request(config, path, filePath = null, redirects, fastRaw = fals
let timer = setTimeout(function() {
timedout = true
if (req) { req.destroy() }
reject(new Error(`Request ${path} timed out after ${timeout}ms`))
reject(Util.combineStack(new Error(`Request ${path} timed out after ${timeout}ms`), orgErr))
}, timeout)
req = h.request({
@ -81,7 +82,7 @@ export function request(config, path, filePath = null, redirects, fastRaw = fals
})
}, function(err) {
if (err.code === 'ABORT_ERR') return
reject(err)
reject(Util.combineStack(err, orgErr))
})
// let file = fs.createWriteStream(filePath)
// res.pipe(file)
@ -113,11 +114,11 @@ export function request(config, path, filePath = null, redirects, fastRaw = fals
if (err) {
ac.abort()
if (!filePath) return reject(err)
if (!filePath) return reject(Util.combineStack(err, orgErr))
// Do some cleanup in case we were in the middle of downloading file
return fs.rm(filePath, function() {
reject(err)
reject(Util.combineStack(err, orgErr))
})
}
// Let the pipeline do the resolving so it can finish flusing before calling resolve
@ -137,11 +138,11 @@ export function request(config, path, filePath = null, redirects, fastRaw = fals
if (timedout) return
let wrapped = new Error(`Error during request ${path}: ${err.message}`)
wrapped.code = err.code
reject(wrapped)
reject(Util.combineStack(wrapped, orgErr))
})
req.on('timeout', function(err) {
if (timedout) return
reject(err)
reject(Util.combineStack(err, orgErr))
})
req.end()
@ -155,9 +156,9 @@ export function request(config, path, filePath = null, redirects, fastRaw = fals
res.body = JSON.parse(res.body)
} catch(e) {
if (res.body.indexOf('<!DOCTYPE') < 100 || res.body.indexOf('<html') < 100) {
return Promise.reject(new Error('Error parsing body, expected JSON but got HTML instead: ' + res.body))
return Promise.reject(Util.combineStack(new Error('Error parsing body, expected JSON but got HTML instead: ' + res.body), orgErr))
}
return Promise.reject(new Error(`Error parsing body ${res.body}: ${e.message}`))
return Promise.reject(Util.combineStack(new Error(`Error parsing body ${res.body}: ${e.message}`), orgErr))
}
}
}

View File

@ -1,7 +1,9 @@
import cluster from 'cluster'
import { Low } from 'lowdb'
import Application from './application.mjs'
import Util from './util.mjs'
import getLog from './log.mjs'
import { Low } from 'lowdb'
import StaticProvider from './providers/static.mjs'
import GitProvider from './providers/git.mjs'
@ -22,7 +24,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,14 +36,17 @@ 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
this.isSlave = cluster.isWorker
}
getApplication(name) {
@ -56,26 +61,74 @@ export default class Core {
this.log.info(`Found applications: ${names.join(', ')}.`)
let hasCluster = false
for (let name of names) {
if (this.isSlave && process.env.CLUSTER_APP_NAME !== name) {
continue
}
try {
let provConstructor = Core.providers.get(this.db.config[name].provider)
let provider = new provConstructor(this.db.config[name])
await provider.checkConfig(this.db.config[name])
if (!this.isSlave) {
await provider.checkConfig(this.db.config[name])
}
let logName = name
if (this.isSlave && process.env.CLUSTER_APP_INDEX) {
logName += '-' + process.env.CLUSTER_APP_INDEX
}
let application = new Application({
db: this.db,
util: this.util,
log: getLog(name, this.db.config[name].log || null),
log: getLog(logName, this.db.config[name].log || null, { name: name }),
core: this,
}, provider, name)
this.applications.push(application)
this.applicationMap.set(name, application)
if (this.db.config[name].cluster) {
hasCluster = true
}
} catch (err) {
this.log.error(err, `Error creating application ${name} with provider ${this.db.config[name].provider}: ${err.message}`)
this.log.error(err, `Error creating application ${name} with provider ${this.db.config[name].provider} with config ${JSON.stringify(this.db.config[name])}: ${err.message}`)
}
}
if (hasCluster && !this.isSlave) {
cluster.on('message', (worker, message) => {
// Some sanity checking
if (!message
|| typeof(message) !== 'object'
|| typeof(message.apptarget) !== 'string'
|| typeof(message.type) !== 'string'
|| typeof(message.payload) !== 'object'
|| !message.payload
) {
return
}
let app = this.getApplication(message.apptarget)
let targetLog = null
if (app) {
targetLog = app.ctx.log
} else if (message.apptarget === this.db.config.name) {
targetLog = this.log
}
if (!targetLog) return
if (message.type === 'newlog') {
targetLog.emit('newlog', message.payload)
}
})
}
if (names.length && !this.applications.length) {
this.log.error('None of the application were successful in running')
return Promise.reject(new Error('None of the application were successful in running'))
}
}
@ -84,14 +137,27 @@ export default class Core {
if (this.running) return
this.running = true
this.log.info(`Running updater on ${this.applications.length} apps`)
await Promise.all(this.applications.map((app) => {
return app.update().catch(err => {
app.ctx.log.error(err, `Error updating: ${err.message}`)
})
}))
if (!this.isSlave) {
this.log.info(`Running updater on ${this.applications.length} apps`)
await Promise.all(this.applications.map((app) => {
return app.update().catch(err => {
app.ctx.log.error(err, `Error updating: ${err.message}`)
})
}))
}
let found = false
if (this.isSlave) {
let app = this.getApplication(process.env.CLUSTER_APP_NAME)
try {
await app.runVersion(process.env.CLUSTER_APP_VERSION)
} catch (err) {
app.ctx.log.fatal(err)
return Promise.reject(err)
}
return
}
for (let app of this.applications) {
app.startAutoupdater()
@ -130,27 +196,44 @@ 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)
found = true
version.stable = 1
await this.db.write()
application.ctx.log.info(`${version.version} is up and running`)
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

@ -89,7 +89,6 @@ export default function GetDB(config, log, orgFilename = 'db.json') {
defaults(db.data.core[name], {
active: '',
latestInstalled: '',
latestVersion: '',
updater: '',
versions: [],
})
@ -100,14 +99,15 @@ export default function GetDB(config, log, orgFilename = 'db.json') {
db.write = function() {
return this._write()
// Do couple of retries. Sometimes it fails randomly doing atomic writes.
.catch(() => { return setTimeout(20) })
.then(() => { return this._write() })
.catch(() => { return setTimeout(50) })
.then(() => { return this._write() })
.catch(() => { return setTimeout(100) })
.then(() => { return this._write() })
.catch(() => { return setTimeout(20).then(() => { return this._write() }) })
.catch(() => { return setTimeout(50).then(() => { return this._write() }) })
.catch(() => { return setTimeout(100).then(() => { return this._write() }) })
.catch((err) => {
this.log.error(err, 'Error saving to db')
try {
db.writeSync()
} catch {
this.log.error(err, 'Error saving to db')
}
})
}
@ -122,7 +122,9 @@ export default function GetDB(config, log, orgFilename = 'db.json') {
return db.read()
.then(function() {
if (!isObject(db.data)) {
db.log.warn(`File ${fullpath} was empty or not a json object, clearing it.`)
if (fullpath !== 'in-memory') {
db.log.warn(`File ${fullpath} was empty or not a json object, clearing it.`)
}
db.data = {}
}
defaults(db.data, { core: { version: 1 } })

View File

@ -28,7 +28,7 @@ export default class HttpServer {
return new Promise((res, rej) => {
server.once('error', rej)
server.listen(port, host || '0.0.0.0', () => {
server.listen(port, host || '::', () => {
server.off('error', rej)
res()
})

View File

@ -1,28 +1,47 @@
import Util from './util.mjs'
import getLog from './log.mjs'
import GetDB from './db.mjs'
import Application from './application.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') {
constructor(name, root_import_meta_url, port = 4000, 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.dbfilename = dbfilename
this.log = getLog(name)
this.name = name
this.config = {}
this.config = {
name: name,
title: 'Development Version of ' + name,
}
this.db = null
this.core = null
this.app = null
this.setConfig({
port: port,
})
}
setConfig(config) {
if (!config.provider) {
config.provider = 'static'
}
this.config[this.name] = config
}
async init(module = null) {
this.db = await GetDB(this.config, this.log, this.dbfilename)
this.core = new Core(this.db, this.util, this.log)
this.core = new Core(this.db, this.util, this.log, (msg) => {
let err = new Error('Got request to restart' + (msg ? ': ' + msg : ''))
this.log.fatal(err)
process.exit(0)
})
let provider = new StaticProvider()
this.app = new Application({
@ -32,6 +51,9 @@ export default class ServiceCore {
core: this.core,
}, provider, this.name)
this.app.registerModule(module)
this.core.applications.push(this.app)
this.core.applicationMap.set(this.name, this.app)
}
run() {

View File

@ -1,7 +1,24 @@
// import nodewindows from 'node-windows'
import cluster from 'cluster'
import bunyan from 'bunyan-lite'
import { setTimeout } from 'timers/promises'
export function getDefaultStreams() {
if (process.env.NODE_ENV === 'production') {
return [{
path: 'log.log',
level: 'info',
}
]
} else {
return [{
"stream": process.stdout,
"level": "debug"
}
]
}
}
export default function getLog(name, streams = null, opts = {}) {
let settings
let ringbuffer = new bunyan.RingBuffer({ limit: 100 })
@ -15,24 +32,9 @@ export default function getLog(name, streams = null, opts = {}) {
})
}
if (process.env.NODE_ENV === 'production') {
settings = {
"name": name,
"streams": streams || [{
path: 'log.log',
level: 'info',
}
]
}
} else {
settings = {
"name": name,
"streams": streams || [{
"stream": process.stdout,
"level": "debug"
}
]
}
settings = {
name: name,
streams: streams || getDefaultStreams()
}
let logger
@ -53,6 +55,14 @@ export default function getLog(name, streams = null, opts = {}) {
stream: {
write: function(record) {
logger.emit('newlog', record)
if (cluster.isWorker) {
process.send({
apptarget: opts.name || name,
type: 'newlog',
payload: record,
})
}
},
end: function() {},
destroy: function() {},
@ -122,6 +132,13 @@ export default function getLog(name, streams = null, opts = {}) {
error: function() { return Promise.resolve() },
}
}
logger.on('error', function(err) {
if (process.env.NODE_ENV === 'production') {
logger.event.warn(`Error in logger ${name}: ${err.message}`)
} else {
console.log(`Error in logger ${name}: ${err.message}`)
}
})
logger.ringbuffer = ringbuffer
logger.ringbufferwarn = ringbufferwarn

View File

@ -25,11 +25,11 @@ 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
if (this.config.git_required_prefix && !asset.name.startsWith(this.config.git_required_prefix)) continue
return {
version: item.name,
version: item.name.replace(/ /g, '_'),
link: asset.browser_download_url,
filename: asset.name,
description: item.body,
@ -56,10 +56,5 @@ export default class GitProvider {
if (typeof(this.config.url) !== 'string') return Promise.reject(new Error('url was not a valid url'))
try { new URL(this.config.url) }
catch (err) { return Promise.reject(new Error('url was not a valid url: ' + err.message)) }
return this.getLatestVersion()
.catch(function(err) {
return Promise.reject(new Error(`Error fetching latest release: ${err.message}`))
})
}
}

View File

@ -1,6 +1,7 @@
import cluster from 'cluster'
import Util from './util.mjs'
import fs from 'fs/promises'
import getLog from './log.mjs'
import getLog, { getDefaultStreams } from './log.mjs'
import GetDB from './db.mjs'
import Core from './core.mjs'
@ -21,12 +22,22 @@ export async function runner(root_import_meta_url, configname = 'config.json', d
throw new Error(`critical error opening ${fullpath}: ${err.message}`)
}
}
let streams = getDefaultStreams()
if (cluster.isWorker) {
streams[0].level = 'error'
}
const log = getLog(config.name)
const log = getLog(config.name, streams)
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) {
let err = new Error('Got request to restart' + (msg ? ': ' + msg : ''))
runner.log.fatal(err)
process.exit(0)
})
await core.init()
await core.run()

View File

@ -5,6 +5,11 @@ import { spawn, execSync } from 'child_process'
import { fileURLToPath, pathToFileURL } from 'url'
export default class Util {
static combineStack(err, appendErr) {
err.stack = err.stack + '\nFrom:\n' + appendErr.stack.split('\n').slice(1).join('\n') + '\n'
return err
}
constructor(root_import_meta_url) {
this._root_import_meta_url = root_import_meta_url
}
@ -79,6 +84,10 @@ export default class Util {
if (!valid)
continue
}
if (config[key].cluster != null && (typeof(config[key].cluster) !== 'number' || config[key].cluster > 100 || config[key].cluster < 0))
continue
if (config[key].clusterWaitOnCrash != null && (typeof(config[key].clusterWaitOnCrash) !== 'number' || config[key].clusterWaitOnCrash < 10))
continue
out.push(key)
}
@ -93,6 +102,13 @@ export default class Util {
return util.getPathFromRoot('../bin/7zdec')
}
getNpmExecutable() {
if (process.platform === 'win32') {
return 'npm.cmd'
}
return 'npm'
}
verifyConfig(config) {
if (!config.name) throw new Error('name is missing in config.json')
if (this.getAppNames(config).length === 0) throw new Error('no application was found in config')

View File

@ -1,6 +1,6 @@
import bunyan from 'bunyan-lite'
import { runner } from './core/runner.mjs'
import { ServiceCore } from './core/lib.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'

View File

@ -1,11 +1,11 @@
{
"name": "service-core",
"version": "3.0.0-beta.2",
"version": "3.0.2",
"description": "Core boiler plate code to install node server as windows service",
"main": "index.mjs",
"scripts": {
"dev": "nodemon --watch dev/api --watch core --watch runner.mjs --watch db.mjs --watch log.mjs runner.mjs | bunyan",
"test": "eltro \"test/**/*.test.mjs\" -r dot",
"test": "eltro -t 250 \"test/**/*.test.mjs\" -r dot",
"test:integration": "eltro \"test/**/*.test.integration.mjs\" -r list",
"test:test": "eltro \"test/application.test.integration.mjs\" -r list",
"test:spec": "eltro \"test/**/*.test.mjs\" -r list",
@ -25,6 +25,9 @@
"inherit": true
}
},
"bin": {
"sccli": "./cli.mjs"
},
"repository": {
"type": "git",
"url": "git+https://git.nfp.is/TheThing/service-core.git"
@ -44,11 +47,10 @@
"bin"
],
"dependencies": {
"bunyan-lite": "^1.0.1",
"lodash": "^4.17.20",
"bunyan-lite": "^1.2.0",
"lowdb": "^3.0.0"
},
"devDependencies": {
"eltro": "^1.3.0"
"eltro": "^1.3.1"
}
}

View File

@ -0,0 +1,122 @@
import { Eltro as t, assert, stub } from 'eltro'
import fs from 'fs/promises'
import Application from '../core/application.mjs'
import Util from '../core/util.mjs'
import lowdb from '../core/db.mjs'
import StaticProvider from '../core/providers/static.mjs'
import { createFakeContext } from './helpers.mjs'
import HttpServer from '../core/http.mjs'
const util = new Util(import.meta.url)
t.describe('#runVersion("version") cluster mode', function() {
const assertPort = 22345
const assertClusterSize = Math.floor(Math.random() * (8 - 2 + 1) + 2)
const http = new HttpServer()
let handler
let cluster
let workers
let ctx
let app
t.beforeEach(function() {
return createFakeContext({ testnoexisting: { cluster: assertClusterSize } }, util, null)
.then(function(res) {
workers = []
ctx = res
let provider = new StaticProvider()
cluster = {
on: stub(),
off: stub(),
fork: stub().returnWith(function() {
let worker = {
on: stub(),
once: stub(),
process: {
kill: stub(),
}
}
workers.push(worker)
return worker
}),
isWorker: false,
}
app = new Application(ctx, provider, 'testnoexisting', { cluster })
app.config.port = assertPort
app.workerDied = stub()
return app.ctx.db.write()
})
})
t.afterEach(function() {
return Promise.all([
app.closeServer(),
])
})
t.before(function() {
handler = function(req, res) {
res.writeHead(200)
res.end('{}')
}
let server = http.createServer(function(req, res) {
req.on('error', function() { })
res.on('error', function() { })
res.on('finish', function() { })
handler(req, res)
})
return server.listenAsync(assertPort)
})
t.after(function() {
return http.closeServer()
})
t.test('should call fork for entire cluster and attach handlers correctly', async function() {
let lastErr = null
for (let i = 1; i <= assertClusterSize; i++) {
assert.strictEqual(app.workers[i], null)
}
handler = function(req, res) {
try {
assert.notOk(cluster.on.called)
assert.notOk(app.__clusterWorkerDied)
lastErr = false
} catch (err) {
lastErr = err
}
res.writeHead(200)
res.end('{}')
}
const assertVersion = 'v11.22.33'
await app.runVersion(assertVersion)
assert.strictEqual(cluster.fork.callCount, assertClusterSize)
for (let i = 1; i <= assertClusterSize; i++) {
assert.ok(app.workers[i])
assert.strictEqual(app.workers[i], workers[i - 1])
assert.strictEqual(cluster.fork.getCallN(i)[0].CLUSTER_APP_NAME, app.name)
assert.strictEqual(cluster.fork.getCallN(i)[0].CLUSTER_APP_VERSION, assertVersion)
assert.strictEqual(app.workers[i].w_id, i)
assert.strictEqual(app.workers[i].listening, false)
assert.ok(app.workers[i].started)
assert.ok(new Date() - app.workers[i].started < 100 && new Date() - app.workers[i].started >= 0)
}
assert.strictEqual(lastErr, false)
assert.strictEqual(cluster.on.callCount, 1)
assert.ok(app.__clusterWorkerDied)
assert.strictEqual(cluster.on.firstCall[0], 'exit')
assert.strictEqual(cluster.on.firstCall[1], app.__clusterWorkerDied)
assert.notStrictEqual(app.workerDied, app.__clusterWorkerDied)
assert.notOk(app.workerDied.called)
app.__clusterWorkerDied()
assert.ok(app.workerDied.called)
})
})

View File

@ -15,7 +15,7 @@ t.describe('#runVersion("static")', function() {
const defaultHandler = function(orgHandler) {
let handler = orgHandler || function (req, res) {
res.writeHead(204); res.end(JSON.stringify({ a: 1 }))
res.writeHead(200); res.end(JSON.stringify({ a: 1 }))
}
return function(http, port, ctx) {
const server = http.createServer(handler)
@ -36,7 +36,7 @@ t.describe('#runVersion("static")', function() {
})
t.afterEach(function() {
return app.http.closeServer()
return app.closeServer()
})
t.test('should throw if http is not called', async function() {
@ -48,9 +48,11 @@ t.describe('#runVersion("static")', function() {
assert.strictEqual(checkCtx.app, app)
})
assert.strictEqual(app.ctx.version, '')
assert.strictEqual(app.fresh, true)
let err = await assert.isRejected(app.runVersion('static'))
assert.strictEqual(app.fresh, false)
assert.strictEqual(app.ctx.version, '')
assert.match(err.message, /http/i)
assert.match(err.message, /createServer/i)
@ -62,9 +64,11 @@ t.describe('#runVersion("static")', function() {
app.config.startWaitUntilFail = 50
app.registerModule(function() { return new Promise(function() {}) })
assert.strictEqual(app.ctx.version, '')
assert.strictEqual(app.fresh, true)
let err = await assert.isRejected(app.runVersion('static'))
assert.strictEqual(app.fresh, false)
assert.strictEqual(app.ctx.version, '')
assert.match(err.message, /time/i)
assert.match(err.message, /out/i)
@ -83,9 +87,11 @@ t.describe('#runVersion("static")', function() {
})
})
assert.strictEqual(app.ctx.version, '')
assert.strictEqual(app.fresh, true)
await app.runVersion('static')
assert.strictEqual(app.fresh, false)
assert.strictEqual(app.ctx.version, 'static')
assert.strictEqual(ctx.db.data.core.testapp.active, 'static')
})
@ -98,11 +104,14 @@ t.describe('#runVersion("static")', function() {
res.end(JSON.stringify({ a: 1 }))
}
app.config.heartbeatAttempts = 3
app.config.heartbeatAttemptsWait = 5
app.registerModule(defaultHandler(handler))
assert.strictEqual(app.ctx.version, '')
assert.strictEqual(app.fresh, true)
let err = await assert.isRejected(app.runVersion('static'))
assert.strictEqual(app.fresh, false)
assert.strictEqual(app.ctx.version, '')
assert.match(err.message, /failed/i)
assert.match(err.message, /400/i)
@ -116,20 +125,23 @@ t.describe('#runVersion("static")', function() {
called++
}
app.config.heartbeatAttempts = 2
app.config.heartbeatAttemptsWait = 30
app.config.heartbeatTimeout = 30
app.config.heartbeatAttemptsWait = 10
app.registerModule(defaultHandler(handler))
assert.strictEqual(app.ctx.version, '')
assert.strictEqual(app.fresh, true)
let start = performance.now()
let err = await assert.isRejected(app.runVersion('static'))
let end = performance.now()
assert.strictEqual(app.fresh, false)
assert.strictEqual(app.ctx.version, '')
assert.match(err.message, /failed/i)
assert.match(err.message, /time/i)
assert.match(err.message, /out/i)
assert.match(err.message, /30ms/i)
assert.ok(end - start > app.config.heartbeatAttempts * app.config.heartbeatAttemptsWait)
assert.ok(end - start > app.config.heartbeatAttempts * (app.config.heartbeatTimeout + app.config.heartbeatAttemptsWait))
assert.strictEqual(called, 2)
assert.strictEqual(ctx.db.data.core.testapp.active, 'static')
})
@ -145,13 +157,16 @@ t.describe('#runVersion("static")', function() {
}
}
app.config.heartbeatAttempts = 3
app.config.heartbeatAttemptsWait = 5
app.registerModule(defaultHandler(handler))
assert.strictEqual(app.ctx.version, '')
let err = await assert.isRejected(app.runVersion('static'))
assert.match(err.message, /failed/i)
assert.match(err.message, /400/i)
assert.strictEqual(app.ctx.version, '')
await app.http.closeServer()
await app.closeServer()
app.registerModule(defaultHandler(handler))
app.config.heartbeatPath = assertPath
@ -159,6 +174,26 @@ t.describe('#runVersion("static")', function() {
assert.strictEqual(ctx.db.data.core.testapp.active, 'static')
})
t.test('should not check heartbeat if slave', async function() {
let called = 0
const handler = function(req, res) {
called++
res.statusCode = 400
res.end(JSON.stringify({ a: 1 }))
}
app.registerModule(defaultHandler(handler))
app.isSlave = true
assert.strictEqual(app.ctx.version, '')
assert.strictEqual(app.fresh, true)
await app.runVersion('static')
assert.strictEqual(app.fresh, false)
assert.strictEqual(app.ctx.version, 'static')
assert.strictEqual(called, 0)
assert.strictEqual(ctx.db.data.core.testapp.active, 'static')
})
})
t.describe('#runVersion("version")', function() {
@ -190,7 +225,7 @@ t.describe('#runVersion("version")', function() {
t.afterEach(function() {
return Promise.all([
fs.rm(assertConfig),
app.http.closeServer(),
app.closeServer(),
])
})
@ -205,9 +240,11 @@ t.describe('#runVersion("version")', function() {
app.config.port = assertPort
stubFsStat.rejects(assertNotError)
assert.strictEqual(app.ctx.version, '')
assert.strictEqual(app.fresh, true)
let err = await assert.isRejected(app.runVersion('v100'))
assert.strictEqual(app.fresh, true)
assert.strictEqual(app.ctx.version, '')
assert.notStrictEqual(err, assertNotError)
assert.match(err.message, new RegExp(assertNotError.message))
@ -225,9 +262,11 @@ t.describe('#runVersion("version")', function() {
await fs.mkdir(util.getPathFromRoot('./testnoexisting/v99'), { recursive: true })
await fs.writeFile(util.getPathFromRoot('./testnoexisting/v99/index.mjs'), `throw new Error('${assertError.message}')`)
assert.strictEqual(app.ctx.version, '')
assert.strictEqual(app.fresh, true)
let err = await assert.isRejected(app.runVersion('v99'))
assert.strictEqual(app.fresh, false)
assert.strictEqual(app.ctx.version, '')
assert.notStrictEqual(err, assertError)
assert.strictEqual(err.message, assertError.message)
@ -241,10 +280,12 @@ t.describe('#runVersion("version")', function() {
await fs.mkdir(util.getPathFromRoot('./testnoexisting/v98'), { recursive: true })
await fs.writeFile(util.getPathFromRoot('./testnoexisting/v98/index.mjs'), ``)
assert.strictEqual(app.ctx.version, '')
assert.strictEqual(app.fresh, true)
let err = await assert.isRejected(app.runVersion('v98'))
assert.strictEqual(app.fresh, false)
assert.match(err.message, /start/i)
assert.strictEqual(app.ctx.version, '')
assert.strictEqual(app.ctx.db.data.core.testnoexisting.active, 'v98')
let checkDb = await lowdb({}, ctx.log, assertConfig)
@ -261,9 +302,11 @@ t.describe('#runVersion("version")', function() {
app.ctx.log.info.reset()
app.ctx.log.event.info.reset()
assert.strictEqual(app.ctx.version, '')
assert.strictEqual(app.fresh, true)
await app.runVersion('v97')
assert.strictEqual(app.fresh, false)
assert.strictEqual(app.ctx.version, 'v97')
assert.ok(app.ctx.log.info.called)
assert.ok(app.ctx.log.event.info.called)

View File

@ -36,6 +36,7 @@ t.skip().timeout(10000).test('should run update and install correctly', async fu
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

@ -6,6 +6,11 @@ import Util from '../core/util.mjs'
import StaticProvider from '../core/providers/static.mjs'
import { createFakeContext } from './helpers.mjs'
import bunyan from 'bunyan-lite'
import HttpServer from '../core/http.mjs'
import { request } from '../core/client.mjs'
import getLog from '../core/log.mjs'
const util = new Util(import.meta.url)
const logger = {
@ -37,7 +42,6 @@ t.describe('constructor()', function() {
assert.ok(ctx.db.data.core.test.versions)
assert.strictEqual(ctx.db.data.core.test.active, '')
assert.strictEqual(ctx.db.data.core.test.latestInstalled, '')
assert.strictEqual(ctx.db.data.core.test.latestVersion, '')
})
t.test('should keep config and other of itself', function() {
@ -58,19 +62,90 @@ t.describe('constructor()', function() {
assert.strictEqual(app.config.heartbeatAttempts, 5)
assert.strictEqual(app.config.heartbeatAttemptsWait, 2 * 1000)
assert.strictEqual(app.config.heartbeatPath, '/')
assert.strictEqual(app.config.clusterWaitOnCrash, 1 * 1000)
assert.strictEqual(app.ctx.db, ctx.db)
assert.strictEqual(app.ctx.app, app)
assert.strictEqual(app.ctx.config, app.config)
assert.strictEqual(app.ctx.util, ctx.util)
assert.strictEqual(app.ctx.sc.Util, Util)
assert.strictEqual(app.ctx.sc.bunyan, bunyan)
assert.strictEqual(app.ctx.sc.HttpServer, HttpServer)
assert.strictEqual(app.ctx.sc.request, request)
assert.strictEqual(app.ctx.sc.getLog, getLog)
assert.strictEqual(app.name, assertName)
assert.strictEqual(app.fresh, true)
assert.strictEqual(app.running, false)
assert.strictEqual(app.ctx.version, '')
assert.strictEqual(app.monitoringCluster, false)
assert.deepStrictEqual(app.workers, {})
assert.strictEqual(app.isSlave, false)
assert.ok(app.http)
assert.ok(app.http.sockets)
assert.strictEqual(typeof(app.http.createServer), 'function')
assert.strictEqual(typeof(app.http.closeServer), 'function')
})
t.test('should support overriding defaults', function() {
const assertTest = {
a: 1,
updateEvery: 10,
startWaitUntilFail: 10,
heartbeatTimeout: 10,
heartbeatAttempts: 10,
heartbeatAttemptsWait: 10,
heartbeatPath: '/asdf',
clusterWaitOnCrash: 10,
}
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, 10)
assert.strictEqual(app.config.startWaitUntilFail, 10)
assert.strictEqual(app.config.heartbeatTimeout, 10)
assert.strictEqual(app.config.heartbeatAttempts, 10)
assert.strictEqual(app.config.heartbeatAttemptsWait, 10)
assert.strictEqual(app.config.heartbeatPath, '/asdf')
assert.strictEqual(app.config.clusterWaitOnCrash, 10)
})
t.test('should fill out workers indexes if is master', function() {
const assertTest = { a: 1, cluster: 2 }
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.deepStrictEqual(app.workers, {
1: null,
2: null,
})
})
t.test('should leave workers empty if not master', function() {
const assertTest = { a: 1, cluster: 2 }
const assertName = 'test'
ctx.db.config = {
test: assertTest,
app: { b: 2},
manage: { c: 3 },
}
let app = new Application(ctx, {}, assertName, { cluster: { isWorker: true } })
assert.notStrictEqual(app.config, assertTest)
assert.strictEqual(app.config.a, assertTest.a)
assert.deepStrictEqual(app.workers, {})
})
t.test('should not default updateEvery if its zero', function() {
const assertTest = { a: 1, updateEvery: 0 }
@ -107,7 +182,7 @@ t.describe('constructor()', function() {
})
})
t.timeout(250).describe('#startAutoupdater()', function() {
t.describe('#startAutoupdater()', function() {
let ctx
t.beforeEach(function() {
@ -124,6 +199,20 @@ t.timeout(250).describe('#startAutoupdater()', function() {
app.startAutoupdater()
})
t.test('should do nothing if isSlave but should warn', async function() {
const stubInterval = stub()
stubInterval.throws(new Error('should not be seen'))
let app = new Application(ctx, { }, 'teststatic', { setInterval: stubInterval })
app.ctx.log.warn.reset()
assert.notOk(app.ctx.log.warn.called)
app.isSlave = true
app.startAutoupdater()
assert.ok(app.ctx.log.warn.called)
assert.match(app.ctx.log.warn.firstCall[0], /slave/i)
})
t.test('should do nothing if updateEvery is zero', async function() {
const stubInterval = stub()
stubInterval.throws(new Error('should not be seen'))
@ -222,34 +311,138 @@ t.timeout(250).describe('#startAutoupdater()', function() {
})
})
t.timeout(250).describe('#closeServer()', function() {
t.describe('#closeServer()', function() {
let app
let cluster
let stubCloseServer
t.beforeEach(function() {
cluster = {
off: stub(),
}
return createFakeContext()
.then(function(res) {
let provider = createProvider()
app = new Application(res, provider, 'testapp')
app = new Application(res, provider, 'testapp', { cluster: cluster })
app.http.closeServer = stubCloseServer = stub().resolves()
})
})
t.test('should call closeServer correctly', async function() {
const assertNotVersion = 'v1521'
const assertError = new Error('Moonlight Fiesta')
stubCloseServer.rejects(assertError)
app.ctx.version = assertNotVersion
let err = await assert.isRejected(app.closeServer())
assert.strictEqual(app.ctx.version, '')
assert.strictEqual(err, assertError)
})
t.test('should kill entire cluster if cluster master', async function() {
let workers = {}
let handle = app.__clusterWorkerDied = function() {}
app.config.cluster = Math.floor(Math.random() * (10 - 4 + 1) + 4)
for (let i = 1; i <= app.config.cluster; i++) {
if (i % 2 === 0) {
workers[i] = app.workers[i] = {
process: {
kill: stub().returnWith(function() {
assert.strictEqual(app.__clusterWorkerDied, null)
assert.strictEqual(cluster.off.callCount, 1)
assert.strictEqual(cluster.off.firstCall[0], 'exit')
assert.strictEqual(cluster.off.firstCall[1], handle)
})
}
}
} else {
workers[i] = app.workers[i] = null
}
}
await app.closeServer()
for (let i = 1; i <= app.config.cluster; i++) {
if (workers[i]) {
assert.ok(workers[i].process.kill.called)
assert.strictEqual(app.__clusterWorkerDied, null)
}
}
})
t.test('otherwise should work fine', async function() {
await app.closeServer()
})
})
t.timeout(250).describe('#update()', function() {
t.describe('#workerDied()', function() {
let app
let stubTimeout
let stubFork
t.beforeEach(function() {
stubTimeout = stub()
return createFakeContext()
.then(function(res) {
let provider = createProvider()
app = new Application(res, provider, 'testapp', { setTimeout: stubTimeout })
app.startForkProcess = stubFork = stub()
})
})
t.test('should do nothing if worker is not found at index', function() {
const assertWorkerId = 2
const assertWorker = { a: 1, w_id: assertWorkerId }
stubTimeout.throws(new Error('should not be seen'))
stubFork.throws(new Error('should not be seen'))
app.workers[1] = assertWorker
app.workerDied(assertWorker)
})
t.test('should mark worker as null and call setTimeout', function() {
const assertWorkerId = 2
const assertTimeoutDuration = 12421
const assertVersion = 'v11.22.33'
const assertWorker = { a: 1, w_id: assertWorkerId }
app.ctx.db.data.core[app.name].active = assertVersion
app.workers[assertWorkerId] = assertWorker
app.clusterWaitOnCrash = assertTimeoutDuration
app.workerDied(assertWorker)
assert.strictEqual(app.workers[assertWorkerId], null)
assert.ok(stubTimeout.called)
assert.strictEqual(typeof(stubTimeout.firstCall[0]), 'function')
assert.strictEqual(stubTimeout.firstCall[1], assertTimeoutDuration)
assert.notOk(stubFork.called)
stubTimeout.firstCall[0]()
assert.ok(stubFork.called)
assert.ok(stubFork.firstCall[0], assertWorkerId)
assert.ok(stubFork.firstCall[1], assertVersion)
})
t.test('setTimeout should not call startForkProcess if one was already started', function() {
const assertWorkerId = 2
const assertWorker = { a: 1, w_id: assertWorkerId }
app.workers[assertWorkerId] = assertWorker
app.workerDied(assertWorker)
assert.strictEqual(app.workers[assertWorkerId], null)
app.workers[assertWorkerId] = { a : 2 }
stubTimeout.firstCall[0]()
assert.notOk(stubFork.called)
})
})
t.describe('#update()', function() {
let ctx
let app
let provider
@ -321,6 +514,23 @@ t.timeout(250).describe('#update()', function() {
assert.strictEqual(stubWrite.callCount, 2)
})
t.test('should do nothing if slave but warn', async function() {
stubWrite.reset().resolves()
app.isSlave = true
app.ctx.log.warn.reset()
assert.notOk(app.ctx.log.warn.called)
let result = await app.update()
assert.strictEqual(result, null)
assert.notOk(ctx.db.data.core.teststatic)
assert.notOk(stubWrite.called)
assert.ok(app.ctx.log.warn.called)
assert.match(app.ctx.log.warn.firstCall[0], /slave/i)
})
t.test('multiple calls should be safe', async function() {
ctx.db.data.core.testapp.updater = ''
@ -638,7 +848,7 @@ t.timeout(250).describe('#update()', function() {
assert.notOk(stubUpdated.called)
assert.strictEqual(app.updating, false)
assert.strictEqual(err, assertError)
assert.strictEqual(stubRunCommand.firstCall[0], 'npm.cmd')
assert.strictEqual(stubRunCommand.firstCall[0], util.getNpmExecutable())
assert.ok(stubRunCommand.firstCall[1])
assert.strictEqual(stubRunCommand.firstCall[1][0], 'install')
assert.ok(stubRunCommand.firstCall[1].includes('--production'), 'should have --production')
@ -845,7 +1055,7 @@ t.timeout(250).describe('#update()', function() {
})
})
t.timeout(250).describe('#registerModule()', function() {
t.describe('#registerModule()', function() {
const assertAppName = 'testappregister'
let ctx
let app

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'))
})
})

236
test/core.cluster.test.mjs Normal file
View File

@ -0,0 +1,236 @@
import { Eltro as t, assert, stub } from 'eltro'
import fs from 'fs/promises'
import Core from '../core/core.mjs'
import Util from '../core/util.mjs'
import { createFakeLog } from './helpers.mjs'
import StaticProvider from '../core/providers/static.mjs'
import lowdb from '../core/db.mjs'
const util = new Util(import.meta.url)
const log = createFakeLog()
let db
t.before(function() {
return lowdb({}, log, null).then(function(res) {
db = res
})
})
t.describe('#init()', function() {
const assertProviderName = 'Kyousuu Gakku Gogyou Kikan'
let core
let fakeUtil
let fakeProvider
let fakeProviderConfig
function FakeProvider(config) {
fakeProvider(config)
this.static = true
this.checkConfig = fakeProviderConfig
}
t.beforeEach(function() {
log.error.reset()
core = new Core(db, util, log)
core.util = fakeUtil = {
verifyConfig: stub(),
getAppNames: stub().returns([]),
}
fakeProvider = stub()
fakeProviderConfig = stub()
Core.providers.set(assertProviderName, FakeProvider)
core.isSlave = true
})
t.after(function() {
return Promise.all([
fs.rm('./log_test_1.log', { recursive: true, force: true }).catch(function() {}),
])
})
t.test('should not call provider checkConfig if isSlave', async function() {
const assertAppName = 'Zetsubou'
const assertConfig = {
[assertAppName]: {
provider: assertProviderName,
}
}
db.config = assertConfig
process.env.CLUSTER_APP_NAME = assertAppName
const assertError = new Error('Shousou')
fakeProviderConfig.rejects(assertError)
fakeUtil.getAppNames.returns([assertAppName])
await core.init()
assert.notOk(fakeProviderConfig.called)
})
t.test('should only create an application with the environment app name', async function() {
const assertAppName = 'Yasashii Ketsumatsu'
const assertAppSecondaryName = 'Big Time'
const assertTestString = 'Serozore no Omoi'
const assertConfig = {
[assertAppName]: {
provider: assertProviderName,
teststring: assertTestString,
},
[assertAppSecondaryName]: {
provider: assertProviderName,
teststring: assertTestString,
},
}
db.config = assertConfig
process.env.CLUSTER_APP_NAME = assertAppName
fakeUtil.getAppNames.returns([assertAppSecondaryName, assertAppName])
assert.strictEqual(core.applications.length, 0)
await core.init()
assert.strictEqual(core.applications.length, 1)
assert.ok(core.getApplication(assertAppName))
assert.notOk(core.getApplication(assertAppSecondaryName))
let application = core.getApplication(assertAppName)
assert.strictEqual(core.applications[0], application)
assert.strictEqual(application.name, assertAppName)
assert.strictEqual(application.ctx.db, core.db)
assert.strictEqual(application.ctx.util, core.util)
assert.notStrictEqual(application.ctx.log, core.log)
assert.strictEqual(application.ctx.core, core)
assert.strictEqual(application.config.teststring, assertTestString)
assert.ok(application.fresh)
assert.ok(application.provider instanceof FakeProvider)
})
})
t.describe('#run()', function() {
let core
let testAppOneName
let testAppTwoName
let stubRunApplication
t.beforeEach(function() {
testAppOneName = 'Tenshi'
testAppTwoName = 'no CLOVER'
db.data.core = {
[testAppOneName]: {
versions: []
},
[testAppTwoName]: {
versions: []
},
}
core = new Core(db, util, log)
core.runApplication = stubRunApplication = stub().resolves()
core.isSlave = true
db.write = stub().resolves()
log.info.reset()
log.warn.reset()
log.error.reset()
for (let name of [testAppOneName, testAppTwoName]) {
let onOrOnce = stub()
let app = {
name: name,
fresh: false,
on: onOrOnce,
once: onOrOnce,
ctx: {
log: {
info: stub(),
warn: stub(),
error: stub(),
fatal: stub(),
},
},
runVersion: stub().resolves(),
update: stub().resolves(),
startAutoupdater: stub(),
}
core.applicationMap.set(name, app)
core.applications.push(app)
}
process.env.CLUSTER_APP_NAME = testAppTwoName
})
t.test('should not call update', async function() {
const assertFirstError = new Error('Manatsu')
const assertSecondError = new Error('no Photograph')
core.applicationMap.get(testAppOneName).update.rejects(assertFirstError)
core.applicationMap.get(testAppTwoName).update.rejects(assertSecondError)
await core.run()
assert.notOk(core.applications[0].update.called)
assert.notOk(core.applications[1].update.called)
})
t.test('app.on updated should not be hooked', async function() {
assert.notOk(core.applicationMap.get(testAppOneName).once.called)
assert.notOk(core.applicationMap.get(testAppTwoName).once.called)
assert.notOk(core.applicationMap.get(testAppOneName).on.called)
assert.notOk(core.applicationMap.get(testAppTwoName).on.called)
core.runApplication.returnWith(function(app) {
assert.notOk(app.once.called)
assert.notOk(app.on.called)
return Promise.resolve()
})
await core.run()
assert.notOk(core.applicationMap.get(testAppOneName).once.called)
assert.notOk(core.applicationMap.get(testAppTwoName).once.called)
assert.notOk(core.applicationMap.get(testAppOneName).on.called)
assert.notOk(core.applicationMap.get(testAppTwoName).on.called)
})
t.test('should not call startAutoupdater', async function() {
stubRunApplication.rejects(new Error('not seen'))
assert.notOk(core.applications[0].startAutoupdater.called)
assert.notOk(core.applications[1].startAutoupdater.called)
await core.run()
assert.notOk(core.applications[0].startAutoupdater.called)
assert.notOk(core.applications[1].startAutoupdater.called)
})
t.test('should be safe to call multiple times', async function() {
await core.run()
assert.strictEqual(core.applicationMap.get(testAppOneName).runVersion.callCount, 0)
assert.strictEqual(core.applicationMap.get(testAppTwoName).runVersion.callCount, 1)
await core.run()
await core.run()
await core.run()
await core.run()
assert.strictEqual(core.applicationMap.get(testAppOneName).runVersion.callCount, 0)
assert.strictEqual(core.applicationMap.get(testAppTwoName).runVersion.callCount, 1)
})
t.test('should call runVersion on selected application', async function() {
const assertFirstError = new Error('Manatsu')
const assertSecondError = new Error('no Photograph')
const assertVersion = 'v11.22.33'
core.applicationMap.get(testAppOneName).runVersion.rejects(assertFirstError)
core.applicationMap.get(testAppTwoName).runVersion.rejects(assertSecondError)
process.env.CLUSTER_APP_NAME = testAppTwoName
process.env.CLUSTER_APP_VERSION = assertVersion
let err = await assert.isRejected(core.run())
let app = core.applicationMap.get(testAppTwoName)
assert.strictEqual(core.applicationMap.get(testAppOneName).runVersion.callCount, 0)
assert.strictEqual(app.runVersion.callCount, 1)
assert.strictEqual(app.runVersion.firstCall[0], assertVersion)
assert.strictEqual(app.ctx.log.fatal.callCount, 1)
assert.strictEqual(app.ctx.log.fatal.firstCall[0], assertSecondError)
assert.strictEqual(err, assertSecondError)
})
})

View File

@ -1,6 +1,6 @@
import { Eltro as t, assert} from 'eltro'
import fs from 'fs/promises'
import http from 'http'
import HttpServer from '../core/http.mjs'
import Util from '../core/util.mjs'
import { request } from '../core/client.mjs'
import { setTimeout } from 'timers/promises'
@ -10,443 +10,582 @@ import getLog from '../core/log.mjs'
const util = new Util(import.meta.url)
const port = 61412
const turnDebuggingOn = false
t.timeout(10000).describe('', function() {
let server = null
let prefix = `http://localhost:${port}/`
let files = []
let logs = []
let versions = []
let processor
let integrationLog = getLog('test.integration', [])
let compressorPath = util.getPathFromRoot('./7za.exe')
if (process.platform !== 'win32') {
compressorPath = util.getPathFromRoot('./7zas')
}
const runners = [
['runner.mjs', 'testapp'],
['runner_cluster.mjs', 'testappcluster'],
]
t.before(function(cb) {
server = http.createServer(function(req, res) {
req.on('error', function(err) {
integrationLog.error(err, 'error')
})
res.on('error', function(err) {
integrationLog.error(err, 'error')
})
integrationLog.info('[SERVER] got request ' + req.url)
if (req.url === '/releases') {
res.statusCode = 200
let output = versions.map(x => {
return {
name: x[0],
body: x[1],
assets: [{
name: x[2],
browser_download_url: prefix + 'files/' + x[2]
}]
}
runners.forEach(function([runnerName, appname]) {
t.timeout(10000).describe(runnerName, function() {
let wasSuccessful = false
let http = null
let server = null
let prefix = `http://localhost:${port}/`
let files = [util.getPathFromRoot('./testappcluster')]
let logs = []
let allLogs = []
let versions = []
let processor
let integrationLog = getLog('test.integration', [])
let compressorPath = util.getPathFromRoot('./7za.exe')
if (process.platform !== 'win32') {
compressorPath = util.getPathFromRoot('./7zas')
}
t.before(function() {
http = new HttpServer()
server = http.createServer(function(req, res) {
req.on('error', function(err) {
integrationLog.error(err, 'error')
})
res.end(JSON.stringify(output));
return
} else if (req.url.startsWith('/files')) {
let filename = req.url.substring(req.url.lastIndexOf('/') + 1)
return fs.open(util.getPathFromRoot('./' + filename))
.then(function(file) {
pipeline(file.createReadStream(), res, function(err) {
if (err) {
console.log(err)
res.statusCode = 404
res.end(JSON.stringify({ error: 'unknown url' }))
res.on('error', function(err) {
integrationLog.error(err, 'error')
})
integrationLog.info('[SERVER] got request ' + req.url)
if (req.url === '/releases') {
res.statusCode = 200
let output = versions.map(x => {
return {
name: x[0],
body: x[1],
assets: [{
name: x[2],
browser_download_url: prefix + 'files/' + x[2]
}]
}
})
}).catch(function(err) {
console.log(err)
res.statusCode = 404
res.end(JSON.stringify({ error: 'unknown url' }))
})
res.end(JSON.stringify(output));
return
} else if (req.url.startsWith('/files')) {
let filename = req.url.substring(req.url.lastIndexOf('/') + 1)
return fs.open(util.getPathFromRoot('./' + filename))
.then(function(file) {
pipeline(file.createReadStream(), res, function(err) {
if (err) {
console.log(err)
res.statusCode = 404
res.end(JSON.stringify({ error: 'unknown url' }))
}
})
}).catch(function(err) {
console.log(err)
res.statusCode = 404
res.end(JSON.stringify({ error: 'unknown url' }))
})
}
res.statusCode = 404
res.end(JSON.stringify({ error: 'unknown url' }))
})
return fs.rm(util.getPathFromRoot('./db.json'), { force: true })
.then(function() {
return server.listenAsync(port)
})
})
t.after(function() {
if (!turnDebuggingOn && !wasSuccessful) {
for (let i = 0; i < allLogs.length; i++) {
prettyPrintMessage(allLogs[i])
}
}
return Promise.all(files.map(function(file) {
return fs.rm(file, { force: true, recursive: true })
}))
.then(function() {
if (processor && !processor.exitCode) {
processor.kill()
res.statusCode = 404
res.end(JSON.stringify({ error: 'unknown url' }))
})
fs.rm(util.getPathFromRoot('./db.json'), { force: true }).then(function() {
server.listen(port, cb)
}, cb)
})
t.after(function() {
return Promise.all(files.map(function(file) {
return fs.rm(file, { force: true, recursive: true })
}))
.then(function() {
if (processor && !processor.exitCode) {
processor.kill()
}
})
})
const version_1_stable = `
export function start(http, port, ctx) {
const server = http.createServer(function (req, res) {
res.writeHead(200);
res.end(JSON.stringify({ version: 'v1' }))
})
return server.listenAsync(port, '0.0.0.0')
.then(() => {
ctx.log.info({ port: port, listening: true }, \`Server is listening on \${port} serving v1\`)
return waitUntilClosed()
}
}).then(function() {
return http.closeServer()
})
}
`
const version_2_nolisten = `
export function start(http, port, ctx) {
}
`
const version_3_crashing = `
export function start(http, port, ctx) {
process.exit(1)
}
`
const version_4_stable = `
export function start(http, port, ctx) {
const server = http.createServer(function (req, res) {
res.writeHead(200);
res.end(JSON.stringify({ version: 'v4' }))
})
return server.listenAsync(port, '0.0.0.0')
.then(() => {
ctx.log.info({ port: port, listening: true }, \`Server is listening on \${port} serving v4\`)
const version_1_stable = `
export function start(http, port, ctx) {
const server = http.createServer(function (req, res) {
res.writeHead(200);
res.end(JSON.stringify({ version: 'v1' }))
})
}
`
function file(relative) {
let file = util.getPathFromRoot(relative)
files.push(file)
return file
}
function log(message) {
let lines = message.split('\n')
for (let line of lines) {
if (!line.trim()) continue
logs.push(line)
return server.listenAsync(port, '0.0.0.0')
.then(() => {
ctx.log.info({ port: port, listening: true }, \`Server is listening on \${port} serving v1\`)
})
}
}
function parseLine(line) {
if (line[0] === '{') {
return JSON.parse(line)
`
const version_2_nolisten = `
export function start(http, port, ctx) {
}
return {
msg: line
`
const version_3_crashing = `
export function start(http, port, ctx) {
process.exit(1)
}
}
let logIndex = 0
function catchupLog() {
if (logs.length > logIndex) {
for (; logIndex < logs.length; logIndex++) {
prettyPrintMessage(logs[logIndex])
`
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\`)
})
}
`
function file(relative) {
let file = util.getPathFromRoot(relative)
files.push(file)
return file
}
function log(message) {
let lines = message.split('\n')
for (let line of lines) {
if (!line.trim()) continue
logs.push(line)
allLogs.push(line)
}
}
function parseLine(line) {
if (line[0] === '{') {
try {
return JSON.parse(line)
} catch {}
}
return {
msg: line
}
}
let logIndex = 0
function catchupLog(ms = 0) {
if (logs.length > logIndex) {
for (; logIndex < logs.length; logIndex++) {
if (turnDebuggingOn) {
prettyPrintMessage(logs[logIndex])
}
}
}
if (ms > 0) {
return setTimeout(ms)
}
}
}
integrationLog.on('newlog', function(record) {
prettyPrintMessage(JSON.stringify(record))
})
let logWaitIndex = 0
function hasLogLine(regMatch) {
if (logs.length > logWaitIndex) {
for (; logWaitIndex < logs.length; logWaitIndex++) {
async function safeTry(func) {
let lastException = null
for (let i = 0; i < 3; i++) {
if (i > 0) {
allLogs.push('[safeTry] Failed with error ' + lastException.message + ', trying agian')
await setTimeout(500)
}
try {
await func()
return
}
catch (err) {
lastException = err
}
}
throw lastException
}
integrationLog.on('newlog', function(record) {
allLogs.push(JSON.stringify(record))
if (turnDebuggingOn) {
prettyPrintMessage(JSON.stringify(record))
}
})
let logWaitIndex = 0
function hasLogLine(regMatch) {
if (logs.length > logWaitIndex) {
for (; logWaitIndex < logs.length; logWaitIndex++) {
if (typeof(regMatch) === 'function') {
let res = regMatch(parseLine(logs[logWaitIndex]))
if (res) return true
}
else if (logs[logWaitIndex].match(regMatch)) {
return true
}
}
}
return false
}
function findInLogs(regMatch) {
for (let i = 0; i < logs.length; i++) {
if (typeof(regMatch) === 'function') {
let res = regMatch(parseLine(logs[logWaitIndex]))
let res = regMatch(parseLine(logs[i]))
if (res) return true
}
else if (logs[logWaitIndex].match(regMatch)) {
else if (logs[i].match(regMatch)) {
return true
}
}
}
return false
}
function findInLogs(regMatch) {
for (let i = 0; i < logs.length; i++) {
if (typeof(regMatch) === 'function') {
let res = regMatch(parseLine(logs[i]))
if (res) return true
async function waitUntilListening() {
let listeningLine = null
while (processor.exitCode == null
&& !hasLogLine((rec) => { listeningLine = rec; return rec.listening && rec.port })) {
await catchupLog(10)
}
else if (logs[i].match(regMatch)) {
return true
catchupLog()
if (listeningLine.listening && listeningLine.port) {
return listeningLine
} else {
return null
}
}
}
async function waitUntilListening() {
let listeningLine = null
while (processor.exitCode == null
&& !hasLogLine((rec) => { listeningLine = rec; return rec.listening && rec.port })) {
catchupLog()
await setTimeout(10)
}
catchupLog()
return listeningLine
}
async function waitUntilClosed(listening) {
while (true) {
catchupLog()
try {
await request({}, `http://localhost:${listening.port}/`)
} catch (err) {
break
async function sendRequestToApplication(listening) {
let lastErr = null
for (let i = 0; i < 4; i++) {
try {
let checkListening = await request({}, `http://localhost:${listening.port}/`)
return checkListening
} catch (err) {
lastErr = err
integrationLog.info(`Request http://localhost:${listening.port}/ failed with ${err.message} trying again in 250ms`)
await setTimeout(250)
}
}
await setTimeout(25)
log('-- core.test.integration.mjs crash here --')
log(lastErr.toString())
throw lastErr
}
catchupLog()
logs.splice(0, logs.length); logIndex = 0; logWaitIndex = 0; console.log('\n-------\n')
}
function startRunner() {
return util.runCommandBackground('node', ['runner.mjs'], util.getPathFromRoot('./'), log)
}
t.test('should be fully operational', async function() {
console.log()
let index = file('./index.mjs')
await fs.writeFile(index, version_1_stable)
await util.runCommand(compressorPath, ['a', file('./v1-sc.7z'), index], util.getPathFromRoot('./testapp'))
processor = startRunner()
while (processor.exitCode == null) {
async function waitUntilClosed(listening) {
while (true) {
catchupLog()
try {
await request({}, `http://localhost:${listening.port}/`)
} catch (err) {
break
}
await setTimeout(25)
}
catchupLog()
await setTimeout(10)
logs.splice(0, logs.length); logIndex = 0; logWaitIndex = 0;
if (turnDebuggingOn) { console.log('\n-------\n') }
}
function startRunner() {
return util.runCommandBackground('node', [runnerName], util.getPathFromRoot('./'), log)
}
t.test('should be fully operational', async function() {
let db;
console.log()
if (!turnDebuggingOn) { console.log('Running empty test') }
catchupLog()
let index = file('./index.mjs')
await fs.writeFile(index, version_1_stable)
await util.runCommand(compressorPath, ['a', file('./v1-sc.7z'), index], util.getPathFromRoot('./testapp'))
let secondLast = parseLine(logs[logs.length - 2])
let last = parseLine(logs[logs.length - 1])
assert.match(secondLast.msg, /creating/i)
assert.match(secondLast.msg, /application/i)
assert.match(secondLast.msg, /testapp/i)
assert.match(secondLast.msg, /0 releases/i)
assert.match(last.err.message, /none/i)
assert.match(last.err.message, /successful/i)
processor = startRunner()
// Reset our log
logs.splice(0, logs.length); logIndex = 0; logWaitIndex = 0; console.log('\n-------\n')
const assertNameVersion1 = 'v1_ok'
while (processor.exitCode == null) {
await catchupLog(10)
}
file(`./testapp/${assertNameVersion1}`)
versions.splice(0, 0, [assertNameVersion1, 'ok version', 'v1-sc.7z'])
processor = startRunner()
let listening = await waitUntilListening()
let checkListening = await request({}, `http://localhost:${listening.port}/`)
assert.strictEqual(checkListening.body.version, 'v1')
while (!hasLogLine(/core is running/)) {
catchupLog()
await setTimeout(10)
}
catchupLog()
let secondLast = parseLine(logs[logs.length - 2])
let last = parseLine(logs[logs.length - 1])
assert.match(secondLast.msg, /No/i)
assert.match(secondLast.msg, /versions/i)
assert.match(secondLast.msg, /found/i)
assert.match(last.msg, /starting/i)
assert.match(last.msg, /runner/i)
assert.match(last.err.message, /stable/i)
assert.match(last.err.message, /application/i)
let db = JSON.parse(await fs.readFile(util.getPathFromRoot('./db.json')))
assert.strictEqual(db.core.testapp.active, assertNameVersion1)
assert.strictEqual(db.core.testapp.versions.length, 1)
assert.strictEqual(db.core.testapp.versions[0].stable, 1)
assert.strictEqual(db.core.testapp.versions[0].installed, true)
// Reset our log
logs.splice(0, logs.length); logIndex = 0; logWaitIndex = 0;
if (turnDebuggingOn) { console.log('\n-------\n') }
// Create our second version
await fs.writeFile(index, version_2_nolisten)
await util.runCommand(compressorPath, ['a', file('./v2-sc.7z'), index], util.getPathFromRoot('./testapp'))
const assertNameVersion2 = 'v2_nolisten'
file(`./testapp/${assertNameVersion2}`)
versions.splice(0, 0, [assertNameVersion2, 'no listen version', 'v2-sc.7z'])
const assertNameVersion1 = 'v1_ok'
// wait a second for it to trigger an update
if (!turnDebuggingOn) { console.log(`Running update ${assertNameVersion1} test`) }
await setTimeout(500)
file(`./testapp/${assertNameVersion1}`)
versions.splice(0, 0, [assertNameVersion1, 'ok version', 'v1-sc.7z'])
processor = startRunner()
let listening = await waitUntilListening()
let checkListening = await sendRequestToApplication(listening)
assert.strictEqual(checkListening.body.version, 'v1')
while (!hasLogLine(/is up and running/)) {
await setTimeout(10)
if (processor.exitCode !== null) {
throw new Error('Process exited with ' + processor.exitCode)
}
}
await setTimeout(50)
while (!hasLogLine(/Error starting v2_nolisten/)) {
catchupLog()
await setTimeout(10)
}
while (!hasLogLine(/Attempting to run version v1_ok/)) {
await safeTry(async function() {
db = JSON.parse(await fs.readFile(util.getPathFromRoot('./db.json')))
assert.strictEqual(db.core[appname].active, assertNameVersion1)
assert.strictEqual(db.core[appname].versions.length, 1)
assert.strictEqual(db.core[appname].versions[0].stable, 1)
assert.strictEqual(db.core[appname].versions[0].installed, true)
})
// Create our second version
await fs.writeFile(index, version_2_nolisten)
await util.runCommand(compressorPath, ['a', file('./v2-sc.7z'), index], util.getPathFromRoot('./testapp'))
const assertNameVersion2 = 'v2_nolisten'
if (!turnDebuggingOn) { console.log(`Running update ${assertNameVersion2} test`) }
file(`./testapp/${assertNameVersion2}`)
versions.splice(0, 0, [assertNameVersion2, 'no listen version', 'v2-sc.7z'])
// wait a second for it to trigger an update
await setTimeout(500)
while (!hasLogLine(/Error starting v2_nolisten/)) {
await catchupLog(10)
}
if (appname !== 'testappcluster') {
while (!hasLogLine(/restart.*v2_nolisten.*dirty/)) {
await catchupLog(10)
}
while (processor.exitCode == null) {
await catchupLog(10)
}
catchupLog()
await safeTry(async function() {
db = JSON.parse(await fs.readFile(util.getPathFromRoot('./db.json')))
assert.strictEqual(db.core[appname].active, assertNameVersion2)
assert.strictEqual(db.core[appname].versions.length, 2)
assert.strictEqual(db.core[appname].versions[0].stable, -1)
assert.strictEqual(db.core[appname].versions[0].installed, true)
assert.strictEqual(db.core[appname].versions[1].stable, 1)
assert.strictEqual(db.core[appname].versions[1].installed, true)
})
// Since application was in dirty state, on next attempt should attempt to
// run v2 again and then falling back to v1
await waitUntilClosed()
if (!turnDebuggingOn) { console.log(`Running fresh ${assertNameVersion2} test`) }
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 sendRequestToApplication(listening)
assert.strictEqual(checkListening.body.version, 'v1')
while (!hasLogLine(/is up and running/)) {
await setTimeout(10)
}
await safeTry(async function() {
db = JSON.parse(await fs.readFile(util.getPathFromRoot('./db.json')))
assert.strictEqual(db.core[appname].active, assertNameVersion1)
assert.strictEqual(db.core[appname].versions.length, 2)
assert.strictEqual(db.core[appname].versions[0].stable, -2)
assert.strictEqual(db.core[appname].versions[1].stable, 1)
})
assert.ok(findInLogs(/Attempting to run version v2_nolisten/))
assert.ok(findInLogs(/Error starting v2_nolisten/))
processor.kill()
if (!turnDebuggingOn) { console.log(`Running version stability check test`) }
await waitUntilClosed()
processor = startRunner()
listening = await waitUntilListening()
assert.ok(listening)
checkListening = await sendRequestToApplication(listening)
assert.strictEqual(checkListening.body.version, 'v1')
while (!hasLogLine(/is up and running/)) {
await setTimeout(10)
}
await safeTry(async function() {
db = JSON.parse(await fs.readFile(util.getPathFromRoot('./db.json')))
assert.strictEqual(db.core[appname].active, assertNameVersion1)
assert.strictEqual(db.core[appname].versions.length, 2)
assert.strictEqual(db.core[appname].versions[0].stable, -2)
assert.strictEqual(db.core[appname].versions[1].stable, 1)
})
assert.notOk(findInLogs(/Attempting to run version v2_nolisten/))
assert.notOk(findInLogs(/Error starting v2_nolisten/))
// Create our third version
await fs.writeFile(index, version_3_crashing)
await util.runCommand(compressorPath, ['a', file('./v3-sc.7z'), index], util.getPathFromRoot('./testapp'))
const assertNameVersion3 = 'v3_crash'
if (!turnDebuggingOn) { console.log(`Running update ${assertNameVersion3} test`) }
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/)) {
await catchupLog(10)
}
if (appname !== 'testappcluster') {
while (processor.exitCode == null) {
await catchupLog(10)
}
await safeTry(async function() {
db = JSON.parse(await fs.readFile(util.getPathFromRoot('./db.json')))
assert.strictEqual(db.core[appname].active, assertNameVersion3)
assert.strictEqual(db.core[appname].versions.length, 3)
assert.strictEqual(db.core[appname].versions[0].stable, -2)
assert.strictEqual(db.core[appname].versions[1].stable, -2)
assert.strictEqual(db.core[appname].versions[2].stable, 1)
})
catchupLog()
// Should recover afterwards
await waitUntilClosed()
processor = startRunner()
listening = await waitUntilListening()
assert.ok(listening)
checkListening = await sendRequestToApplication(listening)
assert.strictEqual(checkListening.body.version, 'v1')
while (!hasLogLine(/core is running/)) {
await setTimeout(10)
}
} else {
while (!hasLogLine(/Attempting to run version v1_ok/)) {
await catchupLog(10)
}
while (!hasLogLine(/is up and running/)) {
await setTimeout(10)
}
await safeTry(async function() {
db = JSON.parse(await fs.readFile(util.getPathFromRoot('./db.json')))
assert.strictEqual(db.core[appname].active, assertNameVersion1)
assert.strictEqual(db.core[appname].versions.length, 3)
assert.strictEqual(db.core[appname].versions[0].stable, -2)
assert.strictEqual(db.core[appname].versions[1].stable, -2)
assert.strictEqual(db.core[appname].versions[2].stable, 1)
})
}
// Create our fourth version
await fs.writeFile(index, version_4_stable)
await util.runCommand(compressorPath, ['a', file('./v4-sc.7z'), index], util.getPathFromRoot('./testapp'))
const assertNameVersion4 = 'v4_stable'
if (!turnDebuggingOn) { console.log(`Running update ${assertNameVersion4} test`) }
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/)) {
await catchupLog(10)
}
while (!hasLogLine(/Server is listening on 31313 serving v4/)) {
await catchupLog(10)
}
while (!hasLogLine(/is up and running/)) {
await setTimeout(10)
}
catchupLog()
checkListening = await sendRequestToApplication(listening)
assert.strictEqual(checkListening.body.version, 'v4')
await setTimeout(10)
}
await safeTry(async function() {
db = JSON.parse(await fs.readFile(util.getPathFromRoot('./db.json')))
assert.strictEqual(db.core[appname].active, assertNameVersion4)
assert.strictEqual(db.core[appname].versions.length, 4)
assert.strictEqual(db.core[appname].versions[0].stable, 1)
assert.strictEqual(db.core[appname].versions[1].stable, -2)
})
while (!hasLogLine(/Server is listening on 31313 serving v1/)) {
catchupLog()
await setTimeout(10)
}
if (appname === 'testappcluster') {
let foundCore = false
let foundWorker = false
for (let line of allLogs) {
if (line.startsWith('[FROMWORKERCORE] test-runner-cluster')) {
foundCore = true
}
else if (line.startsWith('[FROMWORKERAPP] testappcluster-1')) {
foundWorker = true
}
if (foundCore && foundWorker) {
break
}
}
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.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
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)
}
db = JSON.parse(await fs.readFile(util.getPathFromRoot('./db.json')))
assert.strictEqual(db.core.testapp.active, assertNameVersion1)
assert.strictEqual(db.core.testapp.versions.length, 2)
assert.strictEqual(db.core.testapp.versions[0].stable, -2)
assert.strictEqual(db.core.testapp.versions[1].stable, 1)
assert.ok(findInLogs(/Attempting to run version v2_nolisten/))
assert.ok(findInLogs(/Error starting v2_nolisten/))
processor.kill()
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)
}
db = JSON.parse(await fs.readFile(util.getPathFromRoot('./db.json')))
assert.strictEqual(db.core.testapp.active, assertNameVersion1)
assert.strictEqual(db.core.testapp.versions.length, 2)
assert.strictEqual(db.core.testapp.versions[0].stable, -2)
assert.strictEqual(db.core.testapp.versions[1].stable, 1)
assert.notOk(findInLogs(/Attempting to run version v2_nolisten/))
assert.notOk(findInLogs(/Error starting v2_nolisten/))
// Create our third version
await fs.writeFile(index, version_3_crashing)
await util.runCommand(compressorPath, ['a', file('./v3-sc.7z'), index], util.getPathFromRoot('./testapp'))
const assertNameVersion3 = 'v3_crash'
file(`./testapp/${assertNameVersion3}`)
versions.splice(0, 0, [assertNameVersion3, 'crash version', 'v3-sc.7z'])
// wait a second for it to trigger an update
await setTimeout(500)
while (!hasLogLine(/Attempting to run version v3_crash/)) {
catchupLog()
await setTimeout(10)
}
while (processor.exitCode == null) {
catchupLog()
await setTimeout(10)
}
db = JSON.parse(await fs.readFile(util.getPathFromRoot('./db.json')))
assert.strictEqual(db.core.testapp.active, assertNameVersion3)
assert.strictEqual(db.core.testapp.versions.length, 3)
assert.strictEqual(db.core.testapp.versions[0].stable, -2)
assert.strictEqual(db.core.testapp.versions[1].stable, -2)
assert.strictEqual(db.core.testapp.versions[2].stable, 1)
catchupLog()
// Should recover afterwards
await waitUntilClosed()
processor = startRunner()
listening = await waitUntilListening()
assert.ok(listening)
checkListening = await request({}, `http://localhost:${listening.port}/`)
assert.strictEqual(checkListening.body.version, 'v1')
while (!hasLogLine(/core is running/)) {
await setTimeout(10)
}
// Create our fourth version
await fs.writeFile(index, version_4_stable)
await util.runCommand(compressorPath, ['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)
assert.ok(foundCore)
assert.ok(foundWorker)
}
wasSuccessful = true
})
})
})

View File

@ -1,3 +1,4 @@
import cluster from 'cluster'
import { Eltro as t, assert, stub } from 'eltro'
import fs from 'fs/promises'
import Core from '../core/core.mjs'
@ -207,15 +208,39 @@ 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)
assert.strictEqual(core.isSlave, false)
})
})
@ -415,6 +440,108 @@ t.describe('#init()', function() {
assert.strictEqual(application.ctx.log.streams[1].level, 40)
assert.strictEqual(application.ctx.log.streams[1].type, 'stream')
})
t.test('should listen on cluster messages if one or more are cluster on', async function() {
const assertAppName1 = 'Dai Sagara Yoshiharu'
const assertAppName2 = 'Kuryo'
const assertCoreName = 'Ichuu'
const assertPayload1 = { a: 1 }
const assertPayload2 = { b: 2 }
const assertConfig = {
name: assertCoreName,
[assertAppName1]: {
provider: assertProviderName,
cluster: 2,
},
[assertAppName2]: {
provider: assertProviderName,
cluster: 1,
},
}
db.config = assertConfig
fakeUtil.getAppNames.returns([assertAppName1, assertAppName2])
assert.strictEqual(core.applications.length, 0)
await core.init()
core.log.emit = stub()
assert.strictEqual(core.applications.length, 2)
let app1 = core.getApplication(assertAppName1)
let app2 = core.getApplication(assertAppName2)
app1.ctx.log.emit = stub()
app2.ctx.log.emit = stub()
cluster.emit('message', null, {
apptarget: app1.name,
type: 'newlog',
payload: assertPayload1
})
assert.notOk(core.log.emit.called)
assert.ok(app1.ctx.log.emit.called)
assert.ok(app1.ctx.log.emit.firstCall[0], 'newlog')
assert.ok(app1.ctx.log.emit.firstCall[1], assertPayload1)
assert.notOk(app2.ctx.log.emit.called)
app1.ctx.log.emit.reset()
cluster.emit('message', null, {
apptarget: app2.name,
type: 'newlog',
payload: assertPayload2
})
assert.notOk(core.log.emit.called)
assert.notOk(app1.ctx.log.emit.called)
assert.ok(app2.ctx.log.emit.called)
assert.ok(app2.ctx.log.emit.firstCall[0], 'newlog')
assert.ok(app2.ctx.log.emit.firstCall[1], assertPayload2)
app2.ctx.log.emit.reset()
let tests = [
null,
undefined,
12412,
'asdfag',
{},
{ apptarget: 12421, type: 'newlog', payload: {}},
{ apptarget: {}, type: 'newlog', payload: {}},
{ apptarget: null, type: 'newlog', payload: {}},
{ type: 'newlog', payload: {}},
{ apptarget: app1.name, type: 12421, payload: {}},
{ apptarget: app1.name, type: {}, payload: {}},
{ apptarget: app1.name, type: null, payload: {}},
{ apptarget: app1.name, payload: {}},
{ apptarget: app1.name, type: 'newlog', payload: 12421},
{ apptarget: app1.name, type: 'newlog', payload: null},
{ apptarget: app1.name, type: 'newlog', payload: 'test'},
]
tests.forEach(function(test) {
cluster.emit('message', null, test)
assert.notOk(core.log.emit.called)
assert.notOk(app1.ctx.log.emit.called)
assert.notOk(app2.ctx.log.emit.called)
})
cluster.emit('message', null, {
apptarget: assertCoreName,
type: 'newlog',
payload: assertPayload1
})
assert.notOk(app1.ctx.log.emit.called)
assert.notOk(app2.ctx.log.emit.called)
assert.ok(core.log.emit.called)
assert.ok(core.log.emit.called)
assert.ok(core.log.emit.firstCall[0], 'newlog')
assert.ok(core.log.emit.firstCall[1], assertPayload1)
})
})
t.describe('#run()', function() {
@ -626,6 +753,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 +795,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 +819,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 +851,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 +873,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 +883,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 +911,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 +965,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 +1032,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() {
@ -895,7 +1069,6 @@ t.describe('#runApplication()', function() {
assert.notStrictEqual(err, assertError)
assert.match(err.message, /found/)
})
t.test('should throw if no version are found', async function() {
const assertError = new Error('Daikichi to Rin')
@ -946,7 +1119,6 @@ t.describe('#runApplication()', function() {
assert.ok(stubWrite.called)
})
t.test('should succeed if running a minus one on fresh', async function() {
const assertError = new Error('Daikichi to Rin')
let count = 0

View File

@ -150,7 +150,6 @@ t.test('Should support adding an application with defaults', async function() {
assert.ok(db.data.core.app.versions)
assert.strictEqual(db.data.core.app.active, '')
assert.strictEqual(db.data.core.app.latestInstalled, '')
assert.strictEqual(db.data.core.app.latestVersion, '')
assert.notOk(db.data.core.herpderp)
@ -160,7 +159,6 @@ t.test('Should support adding an application with defaults', async function() {
assert.ok(db.data.core.herpderp.versions)
assert.strictEqual(db.data.core.herpderp.active, '')
assert.strictEqual(db.data.core.herpderp.latestInstalled, '')
assert.strictEqual(db.data.core.herpderp.latestVersion, '')
})
t.test('Should support reading from db', async function() {

View File

@ -4,9 +4,11 @@ import Util from '../core/util.mjs'
export function createFakeLog() {
return {
debug: stub(),
info: stub(),
warn: stub(),
error: stub(),
fatal: stub(),
event: {
info: stub(),
warn: stub(),
@ -77,14 +79,14 @@ export function prettyPrintMessage(line) {
if (line[0] === '{') {
try {
let rec = JSON.parse(line)
console.log(`[${rec.time.substr(11).replace('Z', '')}] ${style(levels[rec.level], levelcolor[rec.level])}: ${rec.name}: ${style(rec.msg, 'cyan')}`)
console.log(`[${rec.time.substr(11).replace('Z', '')}] ${style(levels[rec.level], levelcolor[rec.level])}: ${rec.name} ${style('(pid: ' + rec.pid + ')', 'grey')}: ${style(rec.msg, 'cyan')}`)
if (rec.err && rec.err.message && rec.err.stack) {
let err = new Error(rec.err.message)
err.stack = rec.err.stack
console.log(err)
}
return
} catch (err){ console.log(err)}
} catch { }
}
console.log(line)
}

86
test/lib.test.mjs Normal file
View File

@ -0,0 +1,86 @@
import { Eltro as t, assert, stub } from 'eltro'
import * as sc from '../index.mjs'
t.describe('', function() {
const module = {
start: stub()
}
t.beforeEach(function() {
module.start.reset()
})
t.test('should have ServiceCore defined', function() {
assert.ok(sc.ServiceCore)
})
t.test('constructor should work', function() {
const assertAppName = 'Gondola'
let core = new sc.ServiceCore(assertAppName, import.meta.url)
assert.strictEqual(core.util._root_import_meta_url, import.meta.url)
assert.strictEqual(core.name, assertAppName)
})
t.test('should support proper init', async function() {
const assertAppName = 'Hero Combat'
let core = new sc.ServiceCore(assertAppName, import.meta.url)
await core.init(module)
assert.strictEqual(core.core.applications.length, 1)
assert.strictEqual(core.core.applications[0], core.app)
assert.strictEqual(core.core.applicationMap.size, 1)
assert.strictEqual(core.core.applicationMap.get(assertAppName), core.app)
assert.strictEqual(core.app.name, assertAppName)
assert.strictEqual(core.app.module, module)
})
t.test('should call module start', async function() {
const assertError = new Error('Inbo')
module.start.rejects(assertError)
let core = new sc.ServiceCore('testapp', import.meta.url)
await core.init(module)
let err = await assert.isRejected(core.run())
assert.strictEqual(err, assertError)
assert.strictEqual(module.start.firstCall[0], core.app.http)
assert.strictEqual(module.start.firstCall[1], 4000)
assert.strictEqual(module.start.firstCall[2], core.app.ctx)
})
t.test('should support overwriting port', async function() {
const assertError = new Error('Inbo')
const assertPort = 9382
module.start.rejects(assertError)
let core = new sc.ServiceCore('testapp', import.meta.url, assertPort)
await core.init(module)
let err = await assert.isRejected(core.run())
assert.strictEqual(err, assertError)
assert.strictEqual(module.start.firstCall[0], core.app.http)
assert.strictEqual(module.start.firstCall[1], assertPort)
assert.strictEqual(module.start.firstCall[2], core.app.ctx)
})
t.test('should support overwriting config', async function() {
const assertError = new Error('Inbo')
const assertPort = 9382
module.start.rejects(assertError)
let core = new sc.ServiceCore('testapp', import.meta.url)
core.setConfig({
port: assertPort
})
await core.init(module)
let err = await assert.isRejected(core.run())
assert.strictEqual(err, assertError)
assert.strictEqual(core.config['testapp'].port, assertPort)
assert.strictEqual(core.config['testapp'].provider, 'static')
assert.strictEqual(module.start.firstCall[0], core.app.http)
assert.strictEqual(module.start.firstCall[1], assertPort)
assert.strictEqual(module.start.firstCall[2], core.app.ctx)
})
})

View File

@ -3,96 +3,94 @@ import Util from '../../core/util.mjs'
import fs from 'fs/promises'
import GitProvider from '../../core/providers/git.mjs'
t.skip().describe('test', function() {
t.after(function() {
return fs.rm('./test/providers/file.7z')
.catch(function() { })
t.after(function() {
return fs.rm('./test/providers/file.7z')
.catch(function() { })
})
t.timeout(5000).describe('#getLatestVersion()', function() {
t.test('should return latest version in a valid repository', async function() {
let provider = new GitProvider({ url: 'https://git.nfp.is/api/v1/repos/thething/sc-helloworld/releases' })
let version = await provider.getLatestVersion()
assert.ok(version)
assert.ok(version.version)
assert.ok(version.description)
assert.ok(version.link)
assert.match(version.link, /\/download\//)
})
t.timeout(5000).describe('#getLatestVersion()', function() {
t.test('should return latest version in a valid repository', async function() {
let provider = new GitProvider({ url: 'https://git.nfp.is/api/v1/repos/thething/sc-helloworld/releases' })
let version = await provider.getLatestVersion()
assert.ok(version)
assert.ok(version.version)
assert.ok(version.description)
assert.ok(version.link)
assert.match(version.link, /\/attachments\//)
})
t.test('should fail if link does not return json repository object', async function() {
let err = await assert.isRejected(new GitProvider({ url: 'http://git.nfp.is/api/v1/repos/thething/ProgramQueuer' }).getLatestVersion())
assert.match(err.message, /valid/i)
assert.match(err.message, /repository/i)
err = await assert.isRejected(new GitProvider({ url: 'http://git.nfp.is/api/v1/orgs/nfp/repos' }).getLatestVersion())
assert.match(err.message, /service-core/i)
assert.match(err.message, /release/i)
})
t.timeout(5000).describe('#checkConfig()', function() {
t.test('should fail if link does not return json repository object', async function() {
let err = await assert.isRejected(new GitProvider({ url: 'http://git.nfp.is/api/v1/repos/thething/ProgramQueuer' }).checkConfig())
assert.match(err.message, /valid/i)
assert.match(err.message, /repository/i)
err = await assert.isRejected(new GitProvider({ url: 'http://git.nfp.is/api/v1/orgs/nfp/repos' }).checkConfig())
assert.match(err.message, /service-core/i)
assert.match(err.message, /release/i)
})
t.test('should fail if no active release repository with assets', async function() {
let err = await assert.isRejected(new GitProvider({ url: 'https://git.nfp.is/api/v1/repos/thething/eltro/releases' }).checkConfig())
assert.match(err.message, /service-core/i)
assert.match(err.message, /release/i)
})
t.test('should fail on private repositories', async function() {
let err = await assert.isRejected(new GitProvider({ url: 'https://git.nfp.is/api/v1/repos/TheThing/privateexample/releases' }).checkConfig())
assert.match(err.message, /fail/i)
assert.match(err.message, /404/i)
assert.match(err.message, /release/i)
})
t.test('should otherwise succeed', function() {
return new GitProvider({ url: 'https://git.nfp.is/api/v1/repos/TheThing/sc-manager/releases' })
.checkConfig()
})
let test = t
if (!process.env.gittesttoken) {
test = test.skip()
}
test.test('should succeed on private repo with token', function() {
return new GitProvider({
token: process.env.gittesttoken.trim(),
url: 'https://git.nfp.is/api/v1/repos/TheThing/privateexample/releases',
}).checkConfig()
})
t.test('should fail if no active release repository with assets', async function() {
let err = await assert.isRejected(new GitProvider({ url: 'https://git.nfp.is/api/v1/repos/thething/eltro/releases' }).getLatestVersion())
assert.match(err.message, /service-core/i)
assert.match(err.message, /release/i)
})
t.timeout(5000).describe('#downloadVersion()', function() {
const util = new Util(import.meta.url)
t.test('should fail on private repositories', async function() {
let err = await assert.isRejected(new GitProvider({ url: 'https://git.nfp.is/api/v1/repos/TheThing/privateexample/releases' }).getLatestVersion())
assert.match(err.message, /fail/i)
assert.match(err.message, /404/i)
assert.match(err.message, /release/i)
})
let test = t
if (!process.env.gittesttoken) {
test = test.skip()
}
test.test('should successfully download release', async function() {
let provider = new GitProvider({
token: process.env.gittesttoken.trim(),
url: 'https://git.nfp.is/api/v1/repos/TheThing/privateexample/releases',
})
await provider.checkConfig()
let version = await provider.getLatestVersion()
assert.ok(version.version)
assert.ok(version.filename)
assert.ok(version.link)
t.test('should otherwise succeed', function() {
return new GitProvider({ url: 'https://git.nfp.is/api/v1/repos/TheThing/sc-helloworld/releases' })
.getLatestVersion()
})
let path = util.getPathFromRoot('./file.7z')
await provider.downloadVersion(version, path)
let stat = await fs.stat(path)
assert.ok(stat.size > 0)
stat = await fs.stat('./test/providers/file.7z')
assert.ok(stat.size > 0)
let output = ''
await util.runCommand(util.get7zipExecutable(), ['l', 'file.7z'], util.getPathFromRoot('./'), function(chunk) {
output += chunk + '\n'
})
assert.ok(output.indexOf('file1.txt'))
assert.ok(output.indexOf('file2.txt'))
})
let test = t
if (!process.env.gittesttoken) {
console.log('Skipping "git.test.integration: #getLatestVersion() should succeed on private repo with token"')
test = test.skip()
}
test.test('should succeed on private repo with token', function() {
return new GitProvider({
token: process.env.gittesttoken.trim(),
url: 'https://git.nfp.is/api/v1/repos/TheThing/privateexample/releases',
}).getLatestVersion()
})
})
t.timeout(5000).describe('#downloadVersion()', function() {
const util = new Util(import.meta.url)
let test = t
if (!process.env.gittesttoken) {
console.log('Skipping "git.test.integration: #downloadVersion() should successfully download release"')
test = test.skip()
}
test.test('should successfully download release', async function() {
let provider = new GitProvider({
token: process.env.gittesttoken.trim(),
url: 'https://git.nfp.is/api/v1/repos/TheThing/privateexample/releases',
})
await provider.checkConfig()
let version = await provider.getLatestVersion()
assert.ok(version.version)
assert.ok(version.filename)
assert.ok(version.link)
let path = util.getPathFromRoot('./file.7z')
await provider.downloadVersion(version, path)
let stat = await fs.stat(path)
assert.ok(stat.size > 0)
stat = await fs.stat('./test/providers/file.7z')
assert.ok(stat.size > 0)
let output = ''
await util.runCommand(util.get7zipExecutable(), ['l', 'file.7z'], util.getPathFromRoot('./'), function(chunk) {
output += chunk + '\n'
})
assert.ok(output.indexOf('file1.txt'))
assert.ok(output.indexOf('file2.txt'))
})
})

View File

@ -20,7 +20,26 @@ t.describe('#getLatestVersion()', function() {
assert.strictEqual(version.log, '')
})
t.test('should support multiple extension', async function() {
t.test('should auto replace spaces with underscores result', async function() {
const assertOriginalName = 'The Smell Of Sea'
const assertCorrectName = 'The_Smell_Of_Sea'
const assertLink = 'Over The Future'
const assertFilename = 'test-sc.7z'
let stubber = stub()
let provider = new GitProvider({}, stubber)
stubber.resolves({ body: [
{ name: assertOriginalName, assets: [{ name: assertFilename, browser_download_url: assertLink }] },
]})
let version = await provider.getLatestVersion()
assert.strictEqual(version.version, assertCorrectName)
assert.strictEqual(version.link, assertLink)
assert.strictEqual(version.filename, assertFilename)
assert.strictEqual(version.log, '')
})
t.test('should skip zip files for now', async function() {
const assertName = 'Karen'
const assertLink = 'My Wings'
const assertFilename = 'test-sc.zip'
@ -31,11 +50,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() {
@ -110,6 +127,135 @@ t.describe('#getLatestVersion()', function() {
assert.match(err.message, /found/)
})
// --
t.test('should return correct name and link in result when git_required_prefix is specified', async function() {
const assertName = 'Karen'
const assertLink = 'Over The Future'
const assertFilename = 'gittest_test-sc.7z'
let stubber = stub()
let provider = new GitProvider({ git_required_prefix: 'gittest' }, stubber)
stubber.resolves({ body: [
{ 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, '')
})
t.test('should skip zip files for now even when git_required_prefix is specified', async function() {
const assertName = 'Karen'
const assertLink = 'My Wings'
const assertFilename = 'gittest_test-sc.zip'
let stubber = stub()
let provider = new GitProvider({ git_required_prefix: 'gittest' }, stubber)
stubber.resolves({ body: [
{ name: assertName, assets: [{ name: assertFilename, browser_download_url: assertLink }] },
]})
let err = await assert.isRejected(provider.getLatestVersion())
assert.match(err.message, /release/)
assert.match(err.message, /found/)
})
t.test('should skip versions with missing git_required_prefix prefix', async function() {
const assertName = 'name1'
const assertLink = 'somelink'
const assertFilename = 'gittest_something-sc.7z'
let stubber = stub()
let provider = new GitProvider({ git_required_prefix: 'gittest' }, stubber)
stubber.resolves({ body: [
{ name: 'test', assets: [] },
{ name: assertName, assets: [{ name: 'something-sc.7z', browser_download_url: 'nope' }] },
{ 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)
})
t.test('should skip versions with non-sc, non-git_required_prefix filename', async function() {
const assertName = 'name2'
const assertLink = 'somelink2'
const assertFilename = 'gittest_something-sc.7z'
let stubber = stub()
let provider = new GitProvider({ git_required_prefix: 'gittest' }, stubber)
stubber.resolves({ body: [
{ name: 'test', assets: [{ name: 'nope.7z', browser_download_url: 'nope' }] },
{ name: 'test', assets: [{ name: 'gittest_nope.7z', browser_download_url: 'nope' }] },
{ 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)
})
t.test('should skip assets with non-sc filename and non-git_required_prefix', async function() {
const assertName = 'name3'
const assertLink = 'somelink3'
const assertFilename = 'gittest_something-sc.7z'
let stubber = stub()
let provider = new GitProvider({ git_required_prefix: 'gittest' }, stubber)
stubber.resolves({ body: [
{ name: 'test', assets: [{ name: 'gittest_nope.7z', browser_download_url: 'nope' }] },
{ name: assertName, assets: [
{ name: 'nope.7z', browser_download_url: 'nope' },
{ name: 'gittest_nope.7z', browser_download_url: 'nope' },
{ name: 'something-sc.7z', browser_download_url: 'nope' },
{ 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)
})
t.test('should otherwise reject non-git_required_prefix files', async function() {
let stubber = stub()
let provider = new GitProvider({ git_required_prefix: 'gittest' }, stubber)
stubber.resolves({ body: [
{ name: 'test', assets: [{ name: 'nope.7z', browser_download_url: 'nope' }] },
{ name: 'test2', assets: [{ name: 'nope2.7z', browser_download_url: 'nope' },] },
{ name: 'test3', assets: [{ name: 'nope2.7z', browser_download_url: 'nope' },] },
{ name: 'test3', assets: [{ name: 'something-sc.7z', browser_download_url: 'nope' },] },
]})
let err = await assert.isRejected(provider.getLatestVersion())
assert.match(err.message, /release/)
assert.match(err.message, /found/)
})
t.test('should otherwise reject', async function() {
let stubber = stub()
let provider = new GitProvider({}, stubber)
stubber.resolves({ body: [
{ name: 'test', assets: [{ name: 'nope.7z', browser_download_url: 'nope' }] },
{ name: 'test2', assets: [{ name: 'nope2.7z', browser_download_url: 'nope' },] },
{ name: 'test3', assets: [{ name: 'nope2.7z', browser_download_url: 'nope' },] },
]})
let err = await assert.isRejected(provider.getLatestVersion())
assert.match(err.message, /release/)
assert.match(err.message, /found/)
})
t.test('should reject if not valid body', async function() {
let provider = new GitProvider({}, stub())
@ -192,19 +338,10 @@ t.describe('#checkConfig()', function() {
}
})
t.test('should call requester with correct config and url', async function() {
const assertError = new Error('Toki wo Koeta Yoru')
const assertRequestConfig = { a: 1 }
t.test('should not call requester with correct config and url', async function() {
const assertUrl = 'http://test'
let provider = new GitProvider({ url: assertUrl }, stub())
provider.requestConfig = assertRequestConfig
provider.requester.rejects(assertError)
let err = await assert.isRejected(provider.checkConfig())
assert.notStrictEqual(err, assertError)
assert.match(err.message, new RegExp(assertError.message))
assert.strict(provider.requester.firstCall[0], assertRequestConfig)
assert.strict(provider.requester.firstCall[1], assertUrl)
let err = await provider.checkConfig()
assert.notOk(provider.requester.called)
})
})

49
test/runner_cluster.mjs Normal file
View File

@ -0,0 +1,49 @@
import fs from 'fs'
import cluster from 'cluster'
import { runner } from '../core/runner.mjs'
if (cluster.isPrimary) {
try {
fs.rmSync(util.getPathFromRoot('./db.json'))
} catch {}
}
runner(import.meta.url, {
name: 'test-runner-cluster',
testappcluster: {
cluster: 2,
port: 31313,
provider: 'git',
url: 'http://localhost:61412/releases',
updateEvery: 0.014,
heartbeatTimeout: 100,
heartbeatAttempts: 3,
heartbeatAttemptsWait: 100,
log: [{
stream: 'process.stdout',
level: 'info',
}],
}
}, 'db.json')
.then(
function(core) {
if (cluster.isPrimary) {
let app = core.applications[0]
app.ctx.log.on('newlog', function(record) {
if (record.name !== app.name) {
console.log(`[FROMWORKERAPP] ${record.name} (${record.pid}) ${record.msg}`)
}
})
core.log.on('newlog', function(record) {
if (record.pid !== process.pid) {
console.log(`[FROMWORKERCORE] ${record.name} (${record.pid}) ${record.msg}`)
}
})
}
core.log.info('core is running')
},
function(err) {
runner.log.error(err, 'Error starting runner')
process.exit(1)
}
)

View File

@ -195,6 +195,35 @@ t.describe('#getApplications()', function() {
assert.deepStrictEqual(util.getAppNames(getBase({ heartbeatAttemptsWait: {} })), [])
})
t.test('should fail to find if clusterWaitOnCrash is defined but not a valid number', function() {
assert.deepStrictEqual(util.getAppNames(getBase()), ['app'])
assert.deepStrictEqual(util.getAppNames(getBase({ clusterWaitOnCrash: null })), ['app'])
assert.deepStrictEqual(util.getAppNames(getBase({ clusterWaitOnCrash: 5 })), [])
assert.deepStrictEqual(util.getAppNames(getBase({ clusterWaitOnCrash: 15 })), ['app'])
assert.deepStrictEqual(util.getAppNames(getBase({ clusterWaitOnCrash: 1000 })), ['app'])
assert.deepStrictEqual(util.getAppNames(getBase({ clusterWaitOnCrash: 'asdf' })), [])
assert.deepStrictEqual(util.getAppNames(getBase({ clusterWaitOnCrash: '1234' })), [])
assert.deepStrictEqual(util.getAppNames(getBase({ clusterWaitOnCrash: 0 })), [])
assert.deepStrictEqual(util.getAppNames(getBase({ clusterWaitOnCrash: -5 })), [])
assert.deepStrictEqual(util.getAppNames(getBase({ clusterWaitOnCrash: [] })), [])
assert.deepStrictEqual(util.getAppNames(getBase({ clusterWaitOnCrash: {} })), [])
})
t.test('should fail to find if cluster is defined but not a valid number', function() {
assert.deepStrictEqual(util.getAppNames(getBase()), ['app'])
assert.deepStrictEqual(util.getAppNames(getBase({ cluster: null })), ['app'])
assert.deepStrictEqual(util.getAppNames(getBase({ cluster: 1 })), ['app'])
assert.deepStrictEqual(util.getAppNames(getBase({ cluster: 5 })), ['app'])
assert.deepStrictEqual(util.getAppNames(getBase({ cluster: 15 })), ['app'])
assert.deepStrictEqual(util.getAppNames(getBase({ cluster: 1000 })), [])
assert.deepStrictEqual(util.getAppNames(getBase({ cluster: 'asdf' })), [])
assert.deepStrictEqual(util.getAppNames(getBase({ cluster: '1234' })), [])
assert.deepStrictEqual(util.getAppNames(getBase({ cluster: 0 })), ['app'])
assert.deepStrictEqual(util.getAppNames(getBase({ cluster: -5 })), [])
assert.deepStrictEqual(util.getAppNames(getBase({ cluster: [] })), [])
assert.deepStrictEqual(util.getAppNames(getBase({ cluster: {} })), [])
})
t.test('should fail to find if heartbeatPath is defined but not a valid string', function() {
assert.deepStrictEqual(util.getAppNames(getBase()), ['app'])
assert.deepStrictEqual(util.getAppNames(getBase({ heartbeatPath: null })), ['app'])