Compare commits
39 Commits
v3.0.0-bet
...
master
Author | SHA1 | Date |
---|---|---|
Jonatan Nilsson | 6c720f4c2d | |
Jonatan Nilsson | 14fead9c17 | |
Jonatan Nilsson | 6c115aa8b2 | |
Jonatan Nilsson | 5d39f776e1 | |
Jonatan Nilsson | 79f3203f70 | |
Jonatan Nilsson | e8a2ec52a6 | |
Jonatan Nilsson | 1995b38510 | |
Jonatan Nilsson | 63a06a2a34 | |
Jonatan Nilsson | cb5de72e13 | |
Jonatan Nilsson | 33b3d98a37 | |
Jonatan Nilsson | 618cfd0451 | |
Jonatan Nilsson | b5359515b5 | |
Jonatan Nilsson | 23f9173720 | |
Jonatan Nilsson | d519996959 | |
Jonatan Nilsson | 90d4e7ab81 | |
Jonatan Nilsson | a5239d0c27 | |
Jonatan Nilsson | 46386a13b3 | |
Jonatan Nilsson | 99d7a0655d | |
Jonatan Nilsson | 95d72fc404 | |
Jonatan Nilsson | 2edcedb1b7 | |
Jonatan Nilsson | 4a508d20a4 | |
Jonatan Nilsson | 1d7b118229 | |
Jonatan Nilsson | 1f70f36e8d | |
Jonatan Nilsson | 73e9be2ff0 | |
Jonatan Nilsson | 87cc47f498 | |
Jonatan Nilsson | b8a0ec137c | |
Jonatan Nilsson | 67606b9b3b | |
Jonatan Nilsson | 4024f1269a | |
Jonatan Nilsson | 25cac907be | |
Jonatan Nilsson | 4d15623a23 | |
Jonatan Nilsson | 746e0c3f46 | |
Jonatan Nilsson | 44f576c1d4 | |
Jonatan Nilsson | dccad23890 | |
Jonatan Nilsson | 5330ecfa5c | |
Jonatan Nilsson | b152f18fc3 | |
Jonatan Nilsson | bc2d3d9a9b | |
Jonatan Nilsson | e0da74f0be | |
Jonatan Nilsson | 47344c5e7a | |
Jonatan Nilsson | 57be8a144a |
|
@ -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
|
|
@ -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
|
||||
|
|
44
appveyor.yml
44
appveyor.yml
|
@ -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
24
cli.mjs
|
@ -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') {
|
||||
|
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
119
core/core.mjs
119
core/core.mjs
|
@ -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)
|
||||
}
|
||||
|
|
20
core/db.mjs
20
core/db.mjs
|
@ -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 } })
|
||||
|
|
|
@ -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()
|
||||
})
|
||||
|
|
30
core/lib.mjs
30
core/lib.mjs
|
@ -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() {
|
||||
|
|
53
core/log.mjs
53
core/log.mjs
|
@ -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
|
||||
|
||||
|
|
|
@ -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}`))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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()
|
||||
|
||||
|
|
|
@ -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')
|
||||
|
|
|
@ -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'
|
||||
|
|
12
package.json
12
package.json
|
@ -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"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
})
|
||||
})
|
|
@ -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)
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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'))
|
||||
})
|
||||
})
|
||||
|
||||
|
|
|
@ -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)
|
||||
})
|
||||
})
|
|
@ -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
|
||||
})
|
||||
})
|
||||
})
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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() {
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
})
|
||||
})
|
|
@ -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'))
|
||||
})
|
||||
})
|
||||
|
|
|
@ -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)
|
||||
})
|
||||
})
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
)
|
|
@ -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'])
|
||||
|
|
Loading…
Reference in New Issue