Compare commits

..

No commits in common. "master" and "134cd3d6a4ae0acfa48e9be42c0fe621378b32b7" have entirely different histories.

59 changed files with 1084 additions and 7853 deletions

58
.circleci/config.yml Normal file
View file

@ -0,0 +1,58 @@
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

100
.gitignore vendored
View file

@ -1,13 +1,113 @@
# 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
app/*
manage/*
dev/public/main.js

View file

@ -1,8 +1,6 @@
# service-core
Service-Core is a project to faciliate running a node application in (semi-)production environment on a windows machine. Using Windows Services, Service-Core will register itself and autostart on startup and make sure the application is running. In addition it will take care of maintaining the application including auto updating it seamlessly.
Notice, linux support is coming soon.
# The Core
The core provides methods for updating applications as well as taking care of restarting and installing and everything needed to have a pleasent experience running a node application in Windows. It auto checks github for new releases based on the repository specified in `config.json`.

View file

@ -1,90 +0,0 @@
# version format
version: '{build}'
deploy: on
# branches to build
branches:
# whitelist
only:
- master
# Do not build on tags (GitHub, Bitbucket, GitLab, Gitea)
skip_tags: true
# Maximum number of concurrent jobs for the project
max_jobs: 1
clone_depth: 1
# Build worker image (VM template)
build_cloud: Docker
environment:
docker_image: node:16-alpine
test_script:
- sh: |
mkdir /lib64
ln -s /lib/libc.musl-x86_64.so.1 /lib64/ld-linux-x86-64.so.2
chmod -R 777 /appveyor/projects
npm install
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
# on successful build
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)
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
echo "Creating release on gitea"
RELEASE_RESULT=$(curl \
-X POST \
-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} :\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 \
-X POST \
-H "Authorization: token $deploytoken" \
-F "attachment=@${CURR_VER}_sc-core.7z" \
https://git.nfp.is/api/v1/repos/$APPVEYOR_REPO_NAME/releases/$RELEASE_ID/assets
echo '//registry.npmjs.org/:_authToken=${npmtoken}' > ~/.npmrc
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
# on build failure
on_failure:
- sh: echo on_failure

BIN
bin/7zdec

Binary file not shown.

Binary file not shown.

196
cli.mjs
View file

@ -1,196 +0,0 @@
#!/usr/bin/env node
// Get arguments
const [,, ...args] = process.argv
import os from 'os'
import fs from 'fs/promises'
import Util from './core/util.mjs'
import Core from './core/core.mjs'
import path from 'path'
if (!args.length) PrintHelp()
if (args[0] !== 'checkconfig' && args[0] !== 'install' && args[0] !== 'uninstall') PrintHelp
let configFile = 'config.json'
if (args[1]) {
configFile = args[1]
}
const basicRunnerTemplate = `
import fs from 'fs'
import { runner } from 'service-core'
runner(import.meta.url, '${configFile}', 'db.json')
.then(
function(core) {
core.log.info('core is running')
},
function(err) {
runner.log.error(err, 'Error starting runner')
process.exit(1)
}
)
`
const util = new Util(import.meta.url)
function PrintHelp() {
console.log('')
console.log('Usage: sccli <command> [config.json]')
console.log('')
console.log('<Commands>')
console.log(' checkconfig : Test local config.json for errors')
console.log(' install : Install local config.json as service')
console.log(' uninstall : Uninstall local config.json as a service')
console.log('')
process.exit(1)
}
if (args[0] === 'checkconfig') {
fs.readFile(configFile)
.then(async function(content) {
let config = JSON.parse(content)
util.verifyConfig(config)
let names = util.getAppNames(config)
if (!names.length) {
return Promise.reject(new Error('No application names were found'))
}
for (let name of names) {
let provConstructor = Core.providers.get(config[name].provider)
let provider = new provConstructor(config[name])
await provider.checkConfig(config[name])
}
console.log(`${configFile} is a valid config with ${names.length} application${names.length > 1 ? 's' : ''}:`)
for (let name of names) {
console.log(` * ${name} (${config[name].provider}${config[name].url ? ': ' + config[name].url : ' provider'})`)
}
})
.catch(function(err) {
console.log('Error checking config:', err)
process.exit(2)
})
.then(function() {
process.exit(0)
})
} else if (args[0] === 'install') {
if(os.platform() === 'win32') {
const runner = path.join(process.cwd(), './runner.mjs')
fs.stat(runner).catch(function() {
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'),
])
}).then(function([content, nodewindows]) {
let config = JSON.parse(content)
const Service = nodewindows.Service
let serviceConfig = {
name: config.title || config.name,
description: config.description,
script: runner,
env: {
name: 'NODE_ENV',
value: 'production',
},
wait: 0,
grow: .5,
maxRestarts: 10
//, workingDirectory: '...'
//, allowServiceLogon: true
}
console.log('Service', serviceConfig)
// Create a new service object
let svc = new Service(serviceConfig);
svc.on('install',function(){
svc.start();
process.exit(0)
});
svc.on('alreadyinstalled',function(){
svc.start();
process.exit(0)
});
svc.install();
})
} else {
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') {
const runner = path.join(process.cwd(), './runner.mjs')
fs.stat(runner).catch(function() {
return fs.writeFile(runner, basicRunnerTemplate)
}).then(function() {
return Promise.all([
fs.readFile(configFile),
import('node-windows'),
])
}).then(function([content, nodewindows]) {
let config = JSON.parse(content)
const Service = nodewindows.Service
let serviceConfig = {
name: config.title || config.name,
description: config.description,
script: runner,
env: {
name: 'NODE_ENV',
value: 'production',
},
wait: 0,
grow: .5,
maxRestarts: 10
//, workingDirectory: '...'
//, allowServiceLogon: true
}
console.log('Service', serviceConfig)
// Create a new service object
let svc = new Service(serviceConfig);
svc.on('uninstall',function(){
console.log('Uninstall complete.');
console.log('The service exists: ',svc.exists);
process.exit(0)
});
svc.uninstall();
})
} else {
console.log('non windows install targets are currently unsupported')
process.exit(2)
}
}

11
config.json Normal file
View file

@ -0,0 +1,11 @@
{
"name": "service-core",
"githubAuthToken": null,
"serviceName": "Service-Core Node",
"description": "NodeJS Test Service",
"port": 4270,
"managePort": 4271,
"devPort": 4269,
"appRepository": "thething/sc-helloworld",
"manageRepository": "TheThing/sc-manager"
}

View file

@ -1,497 +0,0 @@
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()
this.ctx = {
db: ctx.db,
util: ctx.util,
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
this.name = name
this.updating = false
this.http = new HttpServer(this.config)
this.module = null
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
// if we had previously run an older version. In that case, that older version
// might have dirtied the runtime or left a port open or other stuff.
// In which case, running the new version might fail even though it should
// normally be fine. As such we have this flag here. to indicate we might
// need a full restart before making another attempt.
this.fresh = true
// Apply defaults to config
this.config.updateEvery = this.config.updateEvery != null ? this.config.updateEvery : 180
this.config.startWaitUntilFail = this.config.startWaitUntilFail || (60 * 1000)
this.config.heartbeatTimeout = this.config.heartbeatTimeout || (3 * 1000)
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(
() => {
this.ctx.db.data.core[this.name].updater += 'Automatic update finished successfully. '
},
(err) => {
this.ctx.db.data.core[this.name].updater += 'Error while running automatic update: ' + err.message + '. '
}
)
}, this.config.updateEvery * 60 * 1000)
timer.unref()
}
updateLog(message, level = 'info') {
this.ctx.db.data.core[this.name].updater += 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 })
})
.then((result) => {
if (result) {
this.emit('updated', result)
}
return result
})
.catch((err) => {
this.updating = false
return this.ctx.db.write()
.then(function() { return Promise.reject(err) })
})
}
logAddSeperator(log) {
if (!log.endsWith('\n')) {
log += '\n'
}
if (!log.endsWith('\n\n')) {
log += '\n'
}
return log
}
async _update() {
this.ctx.db.data.core[this.name].updater = ''
let cleanup = true
let folder = ''
let log = ''
let latest = null
try {
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()
// If the versino matches the latest installed, then there's nothing to do
if (this.ctx.db.data.core[this.name].latestInstalled === latest.version) {
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.
latest.id = latest.version
// check to see if we already have this version in our database.
var found = this.ctx.db.get(this.ctx.db.data.core[this.name].versions, latest.id)
if (found) {
// Check if the existing version found was already installed.
if (found.installed) {
log += this.updateLog('Version was already installed, nothing to do. ')
this.emit('updatelog', log)
return null
}
// We found existing on, update the keys of the one in the databse with
// the latest data we got from getLatestVersion(). This ensures that info
// like link, filename and such get updated if these have changed since.
Object.keys(latest).forEach(function(key) {
found[key] = latest[key]
})
latest = found
// Check to see if the existing one has failed too many times and
// if so, we should skip them and consider those versions as black
// listed and avoid at all cost.
if (latest.failtodownload && latest.failtodownload > 3) {
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) {
log += this.updateLog('Version failed to install too many times, skipping this version. ')
this.emit('updatelog', log)
return null
}
// Combine the logs
log = latest.log + log
} else {
// This is a new version, mark it with stable tag of zero.
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)}`)
folder = this.ctx.util.getPathFromRoot(`./${this.name}/${latest.version}`)
// Create it in case it does not exist.
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.
await this.provider.downloadVersion(latest, target)
.catch(function(err) {
latest.failtodownload = (latest.failtodownload || 0) + 1
return Promise.reject(err)
})
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, (msg) => {
log += msg
this.emit('updatelog', log)
}).catch(function(err) {
latest.failtodownload = (latest.failtodownload || 0) + 1
return Promise.reject(err)
})
// Remove the archived file since we're done using it.
await this.fs.rm(target, { force: true }).catch(function() {})
// The extracting process might not leave enough newlines for our
// desired clean output for our logs so add them.
log = this.logAddSeperator(log)
// check if the version we downloaded had index.mjs. If this is
// missing then either the extracting or download failed without erroring
// or the archived is borked.
await this.fs.stat(this.ctx.util.getPathFromRoot(`./${this.name}/${latest.version}/index.mjs`))
.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)
})
// If we reach here, then we don't wanna cleanup or remove existing files
// in case more errors occured. The download was success and preliminary
// checks indicate the version is valid. As such, we are gonna skip
// clearing the folder even if something occurs later on.
cleanup = false
// Check if we have a package.json file. If we do, we need to run
// npm install. If we don't then this application either has all the
// required packages or it doesn't need them to run
let packageStat = await this.fs.stat(this.ctx.util.getPathFromRoot(`./${this.name}/${latest.version}/package.json`))
.catch(function() { return null })
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(
this.ctx.util.getNpmExecutable(),
['install', '--production', '--no-optional', '--no-package-lock', '--no-audit', '--loglevel=notice'],
folder,
(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'
// Check if we have a folder and we need to do some cleanups. We do
// this if the download process failed so we can have a fresh clean
// tree for the next time update is run
if (folder && cleanup) {
await this.fs.rm(folder, { force: true, recursive: true }).catch((err) => {
this.updateLog(`Error while cleaning up: ${err.message}. `)
})
}
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
return latest
}
registerModule(module, version = '') {
if (module && typeof(module) === 'function') {
return this.registerModule({ start: module })
}
if (!module || typeof(module) !== 'object' || typeof(module.start) !== 'function') {
throw new Error(`Application ${this.name}${version ? ' version ' + version : '' } registerModule was called with a non module missing start function`)
}
this.module = module
}
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 (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()
})
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.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
}
}
if (lastErr) {
let err = new Error(`Version failed to start properly: ${lastErr.message}`)
err.stack = lastErr.stack
return Promise.reject(err)
}
if (this.config.cluster && !this.isSlave) {
this.__clusterWorkerDied = this.workerDied.bind(this)
this.cluster.on('exit', this.__clusterWorkerDied)
}
}
closeServer() {
this.ctx.version = ''
this.emit('running', this.ctx.version)
if (this.config.cluster && !this.isSlave) {
if (this.__clusterWorkerDied) {
this.cluster.off('exit', this.__clusterWorkerDied)
}
this.__clusterWorkerDied = null
for (let i = 1; i <= this.config.cluster; i++) {
if (this.workers[i]) {
this.workers[i].process.kill()
this.workers[i] = null
}
}
}
return this.http.closeServer()
}
}

View file

@ -1,42 +1,26 @@
import http from 'http'
import https from 'https'
import stream from 'stream/promises'
import fs from 'fs'
import Util from './util.mjs'
import url from 'url'
function resolveRelative(from, to) {
const resolvedUrl = new URL(to, new URL(from, 'resolve://'));
if (resolvedUrl.protocol === 'resolve:') {
// `from` is a relative URL.
const { pathname, search, hash } = resolvedUrl;
return pathname + search + hash;
}
return resolvedUrl.toString();
}
export function request(config, path, filePath = null, redirects, fastRaw = false) {
if (!config || typeof(config) !== 'object' || Array.isArray(config)) {
export function request(config, path, filePath = null, redirects, returnText = false) {
if (!config || typeof(config) === 'string') {
return Promise.reject(new Error('Request must be called with config in first parameter'))
}
let newRedirects = (redirects || 0) + 1
if (!path || typeof(path) !== 'string' || !path.startsWith('http')) {
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: ' + path))
let newRedirects = redirects + 1
if (!path || !path.startsWith('http')) {
return Promise.reject(new Error('URL was empty or missing http in front'))
}
let parsed = new url.URL(path)
let h = http
let h
if (parsed.protocol === 'https:') {
h = https
} else {
h = http
}
let req = null
let orgErr = new Error(); return new Promise(function(resolve, reject) {
return new Promise(function(resolve, reject) {
if (!path) {
return reject(new Error('Request path was empty'))
}
@ -44,85 +28,42 @@ export function request(config, path, filePath = null, redirects, fastRaw = fals
'User-Agent': 'TheThing/service-core',
Accept: 'application/vnd.github.v3+json'
}
if (config.token) {
headers['Authorization'] = `token ${config.token}`
if (config.githubAuthToken && path.indexOf('api.github.com') >= 0) {
headers['Authorization'] = `token ${config.githubAuthToken}`
}
let timeout = config.timeout || 10000
let timedout = false
let timer = setTimeout(function() {
timedout = true
if (req) { req.destroy() }
reject(Util.combineStack(new Error(`Request ${path} timed out after ${timeout}ms`), orgErr))
}, timeout)
req = h.request({
let req = h.request({
path: parsed.pathname + parsed.search,
port: parsed.port,
method: 'GET',
headers: headers,
timeout: timeout,
timeout: returnText ? 5000 : 10000,
hostname: parsed.hostname
}, function(res) {
if (timedout) { return }
clearTimeout(timer)
const ac = new AbortController()
let output = ''
if (filePath) {
stream.pipeline(res, fs.createWriteStream(filePath), { signal: ac.signal })
.then(function() {
resolve({
statusCode: res.statusCode,
status: res.statusCode,
statusMessage: res.statusMessage,
headers: res.headers,
body: null
})
}, function(err) {
if (err.code === 'ABORT_ERR') return
reject(Util.combineStack(err, orgErr))
})
// let file = fs.createWriteStream(filePath)
// res.pipe(file)
let file = fs.createWriteStream(filePath)
res.pipe(file)
} else {
res.on('data', function(chunk) {
output += chunk
})
}
res.on('end', function() {
let err = null
if (res.statusCode >= 300 && res.statusCode < 400) {
if (newRedirects > 5) {
err = new Error(`Too many redirects (last one was ${res.headers.location})`)
return reject(new Error(`Too many redirects (last one was ${res.headers.location})`))
}
else if (!res.headers.location) {
err = new Error('Redirect returned no path in location header')
if (!res.headers.location) {
return reject(new Error('Redirect returned no path in location header'))
}
else if (res.headers.location.startsWith('http')) {
ac.abort()
return resolve(request(config, res.headers.location, filePath, newRedirects, fastRaw))
if (res.headers.location.startsWith('http')) {
return resolve(request(config, res.headers.location, filePath, newRedirects, returnText))
} else {
ac.abort()
return resolve(request(config, resolveRelative(path, res.headers.location), filePath, newRedirects, fastRaw))
return resolve(request(config, url.resolve(path, res.headers.location), filePath, newRedirects, returnText))
}
} else if (res.statusCode >= 400) {
err = new Error(`HTTP Error ${res.statusCode}: ${output}`)
return reject(new Error(`HTTP Error ${res.statusCode}: ${output}`))
}
if (err) {
ac.abort()
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(Util.combineStack(err, orgErr))
})
}
// Let the pipeline do the resolving so it can finish flusing before calling resolve
if (!filePath) {
resolve({
statusCode: res.statusCode,
status: res.statusCode,
@ -130,36 +71,19 @@ export function request(config, path, filePath = null, redirects, fastRaw = fals
headers: res.headers,
body: output
})
}
})
})
req.on('error', function(err) {
if (timedout) return
let wrapped = new Error(`Error during request ${path}: ${err.message}`)
wrapped.code = err.code
reject(Util.combineStack(wrapped, orgErr))
})
req.on('error', reject)
req.on('timeout', function(err) {
if (timedout) return
reject(Util.combineStack(err, orgErr))
reject(err)
})
})
req.end()
}).then(function(res) {
if (req) {
req.destroy()
}
if (!filePath && !fastRaw) {
if (typeof(res.body) === 'string') {
if (!filePath && !returnText) {
try {
res.body = JSON.parse(res.body)
} catch(e) {
if (res.body.indexOf('<!DOCTYPE') < 100 || res.body.indexOf('<html') < 100) {
return Promise.reject(Util.combineStack(new Error('Error parsing body, expected JSON but got HTML instead: ' + res.body), orgErr))
}
return Promise.reject(Util.combineStack(new Error(`Error parsing body ${res.body}: ${e.message}`), orgErr))
}
throw new Error(res.body)
}
}
return res

View file

@ -1,249 +1,499 @@
import cluster from 'cluster'
import fs from 'fs'
import { EventEmitter } from 'events'
import { request } from './client.mjs'
import HttpServer from './http.mjs'
import { Low } from 'lowdb'
import Application from './application.mjs'
import Util from './util.mjs'
import getLog from './log.mjs'
import StaticProvider from './providers/static.mjs'
import GitProvider from './providers/git.mjs'
const fsp = fs.promises
export default class Core {
static providers = new Map()
static addProvider(name, provider) {
if (!name || typeof(name) !== 'string')
throw new Error('addProvider name must be a string')
if (typeof(provider) !== 'function')
throw new Error(`addProvider ${name} provider must be a class`)
let test = new provider({})
if (typeof(test.checkConfig) !== 'function')
throw new Error(`addProvider ${name} provider class missing checkConfig`)
if (typeof(test.getLatestVersion) !== 'function')
throw new Error(`addProvider ${name} provider class missing getLatestVersion`)
Core.providers.set(name, provider)
}
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')
if (typeof(log.info) !== 'function'
|| typeof(log.warn) !== 'function'
|| typeof(log.error) !== 'function'
|| typeof(log.event.info) !== 'function'
|| typeof(log.event.warn) !== 'function'
|| 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
export default class Core extends EventEmitter{
constructor(util, config, db, log, closeCb) {
super()
process.stdin.resume()
this.http = new HttpServer()
this.util = util
this.config = config
this.db = db
this.log = log
this.restart = restart
this.applications = []
this.applicationMap = new Map()
this._applicationFatalCrash = null
this.isSlave = cluster.isWorker
this._close = closeCb
this._activeCrashHandler = null
this.appRunning = false
this.manageRunning = false
this.monitoring = false
this._appUpdating = {
fresh: true,
updating: false,
starting: false,
logs: '',
}
this._manageUpdating = {
fresh: true,
updating: false,
starting: false,
logs: '',
}
getApplication(name) {
return this.applicationMap.get(name)
this.db.set('core.manageActive', null)
.set('core.appActive', null)
.write().then()
}
async init() {
this.log.info(`Verifying config`)
startMonitor() {
if (this.monitoring) return
this.log.info('[Scheduler] Automatic updater has been turned on. Will check for updates every 3 hours')
let updating = false
this.util.verifyConfig(this.db.config)
let names = this.util.getAppNames(this.db.config)
this.monitoring = setInterval(async () => {
if (updating) return
updating = true
this.log.info('[Scheduler] Starting automatic check for latest version of app and manage')
this.log.info(`Found applications: ${names.join(', ')}.`)
try {
await this.installLatestVersion('app')
await this.installLatestVersion('manage')
} catch(err) {
this.log.error(err, 'Error checking for latest versions')
this.log.event.error('Error checking for latest versions: ' + err.message)
updating = false
return
}
let hasCluster = false
try {
if (this.hasNewVersionAvailable('app') || !this.appRunning) {
await this.tryStartProgram('app')
}
} catch(err) {
this.log.error(err, 'Unknown error occured attempting to app')
this.log.event.error('Unknown error starting app: ' + err.message)
}
try {
if (this.hasNewVersionAvailable('manage') || !this.manageRunning) {
await this.tryStartProgram('manage')
}
} catch(err) {
this.log.error(err, 'Unknown error occured attempting to start manage')
this.log.event.error('Unknown error starting manage: ' + err.message)
}
updating = false
}, 1000 * 60 * 60 * 3) // every 3 hours
}
for (let name of names) {
if (this.isSlave && process.env.CLUSTER_APP_NAME !== name) {
restart() {
this._close()
}
status() {
return {
app: this.appRunning,
manage: this.manageRunning,
appUpdating: this._appUpdating.updating,
manageUpdating: this._manageUpdating.updating,
appStarting: this._appUpdating.starting,
manageStarting: this._manageUpdating.starting,
}
}
async getLatestVersion(active, name) {
// Example: 'https://api.github.com/repos/thething/sc-helloworld/releases'
this.logActive(name, active, `Updater: Fetching release info from: https://api.github.com/repos/${this.config[name + 'Repository']}/releases\n`)
let result = await request(this.config, `https://api.github.com/repos/${this.config[name + 'Repository']}/releases`)
let items = result.body.filter(function(item) {
if (!item.assets.length) return false
for (let i = 0; i < item.assets.length; i++) {
if (item.assets[i].name.endsWith('-sc.zip')) return true
}
})
if (items && items.length) {
for (let x = 0; x < items.length; x++) {
let item = items[x]
for (let i = 0; i < item.assets.length; i++) {
if (item.assets[i].name.endsWith('-sc.zip')) {
if (this.db.get('core.' + name + 'LatestInstalled').value() === item.name) {
this.logActive(name, active, `Updater: Latest version already installed, exiting early\n`)
return null
}
this.logActive(name, active, `Updater: Found version ${item.name} with file ${item.assets[i].name}\n`)
await this.db.set(`core.${name}LatestVersion`, item.name)
.write()
this.emit('dbupdated', {})
return {
name: item.name,
filename: item.assets[i].name,
url: item.assets[i].browser_download_url,
description: item.body,
}
}
}
}
} else {
return null
}
}
logActive(name, active, logline, doNotPrint = false) {
if (!doNotPrint) {
this.log.info(`[${name}] ` + logline.replace(/\n/g, ''))
}
active.logs += logline
this.emit(name + 'log', active)
}
getProgramLogs(name) {
if (name === 'app' && this._appUpdating.logs) {
return this._appUpdating.logs
} else if (name === 'manage' && this._manageUpdating.logs) {
return this._manageUpdating.logs
}
let latestInstalled = this.db.get('core.' + name + 'LatestInstalled').value()
let latestVersion = this.db.get('core.' + name + 'LatestVersion').value()
if (latestVersion) {
let value = this.db.get(`core_${name}History`).getById(latestVersion).value()
if (value) return value.logs
}
if (latestInstalled) {
let value = this.db.get(`core_${name}History`).getById(latestInstalled).value()
if (value) return value.logs
}
return '< no logs found >'
}
async installVersion(name, active, version) {
if (fs.existsSync(this.util.getPathFromRoot(`./${name}/` + version.name))) {
await this.util.runCommand('rmdir', ['/S', '/Q', `"${this.util.getPathFromRoot(`./${name}/` + version.name)}"`])
}
if (!fs.existsSync(this.util.getPathFromRoot(`./${name}/`))) {
await fsp.mkdir(this.util.getPathFromRoot(`./${name}/`))
}
try {
await fsp.mkdir(this.util.getPathFromRoot(`./${name}/` + version.name))
} catch(err) {
if (err.code !== 'EEXIST') {
throw err
}
}
// await fsp.mkdir(this.util.getPathFromRoot(`./${name}/` + version.name + '/node_modules'))
this.logActive(name, active, `Installer: Downloading ${version.name} (${version.url}) to ${version.name + '/' + version.name + '.zip'}\n`)
let filePath = this.util.getPathFromRoot(`./${name}/` + version.name + '/' + version.name + '.zip')
await request(this.config, version.url, filePath)
this.logActive(name, active, `Installer: Downloading finished, starting extraction\n`)
await this.util.runCommand(
'"C:\\Program Files\\7-Zip\\7z.exe"',
['x', `"${filePath}"`],
this.util.getPathFromRoot(`./${name}/` + version.name + '/'),
this.logActive.bind(this, name, active)
)
if (!fs.existsSync(this.util.getPathFromRoot(`./${name}/` + version.name + '/index.mjs'))) {
this.logActive(name, active, `\nInstaller: ERROR: Missing index.mjs in the folder, exiting\n`)
throw new Error(`Missing index.mjs in ${this.util.getPathFromRoot(`./${name}/` + version.name + '/index.mjs')}`)
}
this.logActive(name, active, `\nInstaller: Starting npm install\n`)
await this.util.runCommand(
'npm.cmd',
['install', '--production', '--no-optional', '--no-package-lock', '--no-audit'],
this.util.getPathFromRoot(`./${name}/` + version.name + '/'),
this.logActive.bind(this, name, active)
)
await this.db.set(`core.${name}LatestInstalled`, version.name)
.write()
this.emit('dbupdated', {})
this.logActive(name, active, `\nInstaller: Successfully installed ${version.name}\n`)
}
getActive(name) {
if (name === 'app') {
return this._appUpdating
} else if (name === 'manage') {
return this._manageUpdating
} else {
throw new Error('Invalid name: ' + name)
}
}
async startModule(module, port) {
let out = await module.start(this.config, this.db, this.log, this, this.http, port)
if (out && out.then) {
await out
}
if (!this.http.getCurrentServer()) {
this.log.warn('Module did not call http.createServer')
}
}
hasNewVersionAvailable(name) {
let newestVersion = this.db.get(`core.${name}LatestInstalled`).value()
if (!newestVersion) return false
let history = this.db.get(`core_${name}History`).getById(newestVersion).value()
if (history.installed && history.stable === 0) {
return true
}
return false
}
async tryStartProgram(name) {
let active = this.getActive(name)
if (this[name + 'Running'] && !this.hasNewVersionAvailable(name)) {
this.log.event.warn('Attempting to start ' + name + ' which is already running')
this.log.warn('Attempting to start ' + name + ' which is already running')
this.logActive(name, active, `Runner: Attempting to start it but it is already running\n`, true)
return
}
active.starting = true
if (this[name + 'Running']) {
let success = await this.http.closeServer(name)
if (!success) {
if (process.env.NODE_ENV === 'production') {
this.logActive(name, active, `Runner: Found new version but server could not be shut down, restarting service core\n`)
await new Promise(() => {
this.log.event.warn('Found new version of ' + name + ' but server could not be shut down gracefully, restarting...', null, () => {
process.exit(100)
})
})
} else {
this.logActive(name, active, `Runner: Found new version but server could not be shut down\n`)
return
}
}
this[name + 'Running'] = false
this.emit('statusupdated', {})
}
let history = this.db.get(`core_${name}History`)
.filter('installed')
.orderBy('installed', 'desc')
.value()
this.logActive(name, active, `Runner: Finding available version\n`)
for (let i = 0; i < history.length; i++) {
if ((history[i].stable === -1 && !active.fresh)
|| (history[i].stable < -1)) {
this.logActive(name, active, `Runner: Skipping version ${history[i].name} due to marked as unstable\n`)
continue
}
await this.db.set(`core.${name}Active`, history[i].name)
.write()
this.emit('dbupdated', {})
let running = await this.tryStartProgramVersion(name, active, history[i].name)
if (running) {
history[i].stable = 1
} else {
if (active.fresh || history[i].stable === -1) {
history[i].stable = -2
} else {
history[i].stable = -1
}
await this.db.set(`core.${name}Active`, null)
.write()
this.emit('dbupdated', {})
}
active.fresh = false
await this.db.get(`core_${name}History`).updateById(history[i].id, history[i].stable).write()
if (history[i].stable > 0) break
}
if (!this.db.get(`core.${name}Active`).value()) {
this.logActive(name, active, `Runner: Could not find any available stable version of ${name}\n`)
this.log.error('Unable to start ' + name)
this.log.event.error('Unable to start ' + name)
}
active.starting = false
}
programCrashed(name, version, active, oldStable) {
let newStable = -2
console.log('EXITING:', oldStable, active)
if (oldStable === 0 && !active.fresh) {
newStable = -1
}
let temp = this.db.get(`core_${name}History`).getById(version).set('stable', newStable )
temp.value() // Trigger update on __wrapped__
fs.writeFileSync(this.db.adapterFilePath, JSON.stringify(temp.__wrapped__, null, 2))
}
async tryStartProgramVersion(name, active, version) {
if (!version) return false
this.logActive(name, active, `Runner: Attempting to start ${version}\n`)
let indexPath = this.util.getUrlFromRoot(`./${name}/` + version + '/index.mjs')
let module
try {
let provConstructor = Core.providers.get(this.db.config[name].provider)
let provider = new provConstructor(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(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
}
this.logActive(name, active, `Runner: Loading ${indexPath}\n`)
module = await import(indexPath)
} catch (err) {
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}`)
}
this.logActive(name, active, `Runner: Error importing module\n`, true)
this.logActive(name, active, `${err.stack}\n`, true)
this.log.error(err, `Failed to load ${indexPath}`)
return false
}
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 checkTimeout = null
let oldStable = this.db.get(`core_${name}History`).getById(version).value().stable
this._activeCrashHandler = this.programCrashed.bind(this, name, version, active, oldStable)
process.once('exit', this._activeCrashHandler)
try {
let port = name === 'app' ? this.config.port : this.config.managePort
await new Promise((res, rej) => {
checkTimeout = setTimeout(function() {
rej(new Error('Program took longer than 60 seconds to resolve promise'))
}, 60 * 1000)
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
}
this.logActive(name, active, `Runner: Starting module\n`)
if (!targetLog) return
if (message.type === 'newlog') {
targetLog.emit('newlog', message.payload)
try {
this.http.setContext(name)
this.startModule(module, port)
.then(res, rej)
} catch (err) {
rej(err)
}
})
}
clearTimeout(checkTimeout)
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'))
}
}
async run() {
if (this.running) return
this.running = true
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)
await this.checkProgramRunning(name, active, port)
process.off('exit', this._activeCrashHandler)
} catch (err) {
app.ctx.log.fatal(err)
return Promise.reject(err)
clearTimeout(checkTimeout)
process.off('exit', this._activeCrashHandler)
await this.http.closeServer(name)
this.logActive(name, active, `Runner: Error starting\n`, true)
this.logActive(name, active, `${err.stack}\n`, true)
this.log.error(err, `Failed to start ${name}`)
return false
}
return
this._activeCrashHandler = null
this.logActive(name, active, `Runner: Successfully started version ${version}\n`)
await this.db.set(`core.${name}Active`, version)
.write()
if (name === 'app') {
this.appRunning = true
} else {
this.manageRunning = true
}
this.emit('statusupdated', {})
this.logActive(name, active, `Runner: Module is running successfully\n`)
return true
}
for (let app of this.applications) {
app.startAutoupdater()
await this.runApplication(app).then(
() => {
found = true
},
err => {
app.ctx.log.error(err, `Error running: ${err.message}`)
}
)
app.on('updated', this.runApplication.bind(this, app))
}
if (!found) {
throw new Error('No stable application was found')
}
}
criticalError(application, version, errorCode) {
application.ctx.log.fatal(`Critical error ${errorCode} running ${version.version}`)
version.stable = -2
this.db.writeSync()
}
async runApplication(application) {
let name = application.name
let found = false
if (!this.db.data.core[name].versions.length) {
return Promise.reject(new Error(`No versions were found`))
}
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) {
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
async checkProgramRunning(name, active, port) {
this.logActive(name, active, `Checker: Testing out module port ${port}\n`)
let start = new Date()
let error = null
let success = false
while (new Date() - start < 10 * 1000) {
try {
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`)
let check = await request(this.config, `http://localhost:${port}`, null, 0, true)
success = true
break
} catch(err) {
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
this.logActive(name, active, `Checker: ${err.message}, retrying in 3 seconds\n`)
error = err
await new Promise(function(res) { setTimeout(res, 3000)})
}
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)
}
if (success) return true
throw error || new Error('Checking server failed')
}
if (!found) {
return Promise.reject(Error(`No stable versions were found`))
async installLatestVersion(name) {
if (!this.config[name + 'Repository']) {
if (name === 'app') {
this.log.error(name + ' Repository was missing from config')
this.log.event.error(name + ' Repository was missing from config')
} else {
this.log.warn(name + ' Repository was missing from config')
this.log.event.warn(name + ' Repository was missing from config')
}
return
}
let active = this.getActive(name)
let oldLogs = active.logs || ''
if (oldLogs) {
oldLogs += '\n'
}
active.logs = ''
active.updating = true
this.emit('statusupdated', {})
this.logActive(name, active, `Installer: Checking for updates at time: ${new Date().toISOString().replace('T', ' ').split('.')[0]}\n`)
let version = null
let installed = false
let found = false
try {
version = await this.getLatestVersion(active, name)
if (version) {
let core = this.db.get('core').value()
let fromDb = this.db.get(`core_${name}History`).getById(version.name).value()
if (!fromDb || !fromDb.installed) {
let oldVersion = core[name + 'Current'] || '<none>'
this.logActive(name, active, `Installer: Updating from ${oldVersion} to ${version.name}\n`)
await this.installVersion(name, active, version)
this.logActive(name, active, `Installer: Finished: ${new Date().toISOString().replace('T', ' ').split('.')[0]}\n`)
installed = new Date()
} else {
found = true
this.logActive(name, active, `Installer: Version ${version.name} already installed\n`)
}
}
} catch(err) {
this.logActive(name, active, '\n', true)
this.logActive(name, active, `Installer: Exception occured while updating ${name}\n`, true)
this.logActive(name, active, err.stack, true)
this.log.error('Error while updating ' + name, err)
}
active.updating = false
if (version && !found) {
await this.db.get(`core_${name}History`).upsert({
id: version.name,
name: version.name,
filename: version.filename,
url: version.url,
description: version.description,
logs: active.logs,
stable: 0,
installed: installed && installed.toISOString(),
}).write()
}
active.logs = oldLogs + active.logs
this.emit(name + 'log', active)
this.emit('statusupdated', {})
}
async start(name) {
var version = this.db.get('core.' + name + 'LatestInstalled').value()
if (version) {
await this.tryStartProgram(name)
}
await this.installLatestVersion(name)
if (version !== this.db.get('core.' + name + 'LatestInstalled').value()) {
if (!this[name + 'Running'] || this.hasNewVersionAvailable(name)) {
await this.tryStartProgram(name)
}
}
}
}
Core.addProvider('static', StaticProvider)
Core.addProvider('git', GitProvider)

View file

@ -1,142 +1,163 @@
import { setTimeout } from 'timers/promises'
import { Low, JSONFile, Memory } from 'lowdb'
import fs from 'fs'
import { defaults, isObject } from './defaults.mjs'
import lowdb from 'lowdb'
import FileAsync from 'lowdb/adapters/FileAsync.js'
export default function GetDB(config, log, orgFilename = 'db.json') {
let adapter = new Memory()
let fullpath = 'in-memory'
if (orgFilename) {
fullpath = orgFilename
adapter = new JSONFile(fullpath)
}
const db = new Low(adapter)
let lastId = -1
db.id = 'id'
db.filename = fullpath
db.config = config
// Take from https://github.com/typicode/lodash-id/blob/master/src/index.js
// from package lodash-id
const lodashId = {
// Empties properties
__empty: function (doc) {
this.forEach(doc, function (value, key) {
delete doc[key]
})
},
db.createId = function(collection) {
if (collection.length) {
return (collection[collection.length - 1].id || 0) + 1
}
return 1
}
// Copies properties from an object to another
__update: function (dest, src) {
this.forEach(src, function (value, key) {
dest[key] = value
})
},
db.get = function(collection, id, returnIndex = false) {
let col = db.getCollection(collection)
for (let i = col.length - 1; i >= 0; i--) {
if (col[i][db.id] === id) {
if (returnIndex) return i
return col[i]
}
}
return null
}
// Removes an item from an array
__remove: function (array, item) {
var index = this.indexOf(array, item)
if (index !== -1) array.splice(index, 1)
},
db.getCollection = function(collection) {
if (typeof(collection) === 'string') {
return db.data[collection]
}
return collection
}
__id: function () {
var id = this.id || 'id'
return id
},
db.upsert = function(collection, item) {
let col = db.getCollection(collection)
if (item[db.id]) {
let i = db.get(col, item[db.id], true)
if (i !== null) {
col[i] = item
return
getById: function (collection, id) {
var self = this
return this.find(collection, function (doc) {
if (self.has(doc, self.__id())) {
return doc[self.__id()] === id
}
})
},
createId: function (collection, doc) {
let next = new Date().getTime()
if (next <= lastId) {
next = lastId + 1
}
lastId = next
return next
},
insert: function (collection, doc) {
doc[this.__id()] = doc[this.__id()] || this.createId(collection, doc)
var d = this.getById(collection, doc[this.__id()])
if (d) throw new Error('Insert failed, duplicate id')
collection.push(doc)
return doc
},
upsert: function (collection, doc) {
if (doc[this.__id()]) {
// id is set
var d = this.getById(collection, doc[this.__id()])
if (d) {
// replace properties of existing object
this.__empty(d)
this.assign(d, doc)
} else {
// push new object
collection.push(doc)
}
} else {
item[db.id] = db.createId(col)
}
col.push(item)
// create id and push new object
doc[this.__id()] = this.createId(collection, doc)
collection.push(doc)
}
db.upsertFirst = function(collection, item) {
let col = db.getCollection(collection)
if (item[db.id]) {
let i = db.get(col, item[db.id], true)
if (i !== null) {
col[i] = item
return
}
} else {
item[db.id] = db.createId(col)
}
col.splice(0, 0, item)
return doc
},
updateById: function (collection, id, attrs) {
var doc = this.getById(collection, id)
if (doc) {
this.assign(doc, attrs, {id: doc.id})
}
db.remove = function(collection, itemOrId) {
let col = db.getCollection(collection)
let id = itemOrId
if (typeof(id) === 'object') {
id = id[db.id]
}
for (let i = col.length - 1; i >= 0; i--) {
if (col[i][db.id] === id) {
col.splice(i, 1)
return true
}
}
return false
}
return doc
},
db.addApplication = function(name) {
db.data.core[name] ||= {}
defaults(db.data.core[name], {
active: '',
latestInstalled: '',
updater: '',
versions: [],
updateWhere: function (collection, predicate, attrs) {
var self = this
var docs = this.filter(collection, predicate)
docs.forEach(function (doc) {
self.assign(doc, attrs, {id: doc.id})
})
return docs
},
replaceById: function (collection, id, attrs) {
var doc = this.getById(collection, id)
if (doc) {
var docId = doc.id
this.__empty(doc)
this.assign(doc, attrs, {id: docId})
}
db.log = log
db._write = db.write.bind(db)
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((err) => {
try {
db.writeSync()
} catch {
this.log.error(err, 'Error saving to db')
}
return doc
},
removeById: function (collection, id) {
var doc = this.getById(collection, id)
this.__remove(collection, doc)
return doc
},
removeWhere: function (collection, predicate) {
var self = this
var docs = this.filter(collection, predicate)
docs.forEach(function (doc) {
self.__remove(collection, doc)
})
}
db.writeSync = function() {
try {
fs.writeFileSync(db.filename, JSON.stringify(db.data))
} catch(err) {
db.log.error(err, `Error during writeSync to ${db.filename}`)
return docs
}
}
return db.read()
.then(function() {
if (!isObject(db.data)) {
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 } })
return db.write()
})
.then(
function() { return db },
function(err) {
let wrapped = new Error(`Error writing to ${fullpath}: ${err.message} (${err.code})`)
wrapped.code = err.code
throw wrapped
}
)
}
export default function GetDB(util, log) {
const adapter = new FileAsync(util.getPathFromRoot('./db.json'))
return lowdb(adapter)
.then(function(db) {
db._.mixin(lodashId)
db.adapterFilePath = util.getPathFromRoot('./db.json')
db.defaults({
core: {
"appActive": null, // Current active running
"appLatestInstalled": null, // Latest installed version
"appLatestVersion": null, // Newest version available
"manageActive": null,
"manageLatestInstalled": null,
"manageLatestVersion": null
},
core_appHistory: [],
core_manageHistory: [],
core_version: 1,
})
.write()
.then(
function() { },
function(e) { log.error(e, 'Error writing defaults to lowdb') }
)
return db
})
}

View file

@ -1,25 +0,0 @@
// taken from isobject npm library
export function isObject(val) {
return val != null && typeof val === 'object' && Array.isArray(val) === false
}
export function defaults(original, def) {
if (!isObject(original)) throw new Error('defaults called with non-object type')
Object.keys(original).forEach(key => {
if (isObject(original[key]) && def && isObject(def[key])) {
defaults(original[key], def[key])
}
})
if (def) {
Object.keys(def).forEach(function(key) {
if (typeof original[key] === 'undefined') {
original[key] = def[key]
}
})
}
return original
}

View file

@ -1,62 +1,73 @@
import http from 'http'
import https from 'https'
export default class HttpServer {
constructor(config) {
this.ishttps = false
this.active = null
this.sockets = new Set()
this.creator = http
if (config && config.https) {
this.creator = https
this.ishttps = true
this.active = {
app: false,
manage: false,
dev: false,
}
this.sockets = {
app: new Set(),
manage: new Set(),
dev: new Set(),
}
this._context = 'dev'
}
setContext(name) {
if (name !== 'app' && name !== 'manage' && name !== 'dev') {
throw new Error('Cannot call setContext with values other than app or manage')
}
this._context = name
}
createServer(opts, listener) {
let server = this.creator.createServer(opts, listener)
server.on('connection', (socket) => {
this.sockets.add(socket)
socket.once('close', () => {
this.sockets.delete(socket)
})
})
server.listenAsync = (port, host) => {
return new Promise((res, rej) => {
server.once('error', rej)
server.listen(port, host || '::', () => {
server.off('error', rej)
res()
})
})
return this._createServer(this._context, opts, listener)
}
this.active = server
_createServer(name, opts, listener) {
let server = http.createServer(opts, listener)
server.on('connection', (socket) => {
this.sockets[name].add(socket)
socket.once('close', () => {
this.sockets[name].delete(socket)
})
})
this.active[name] = server
return server
}
closeServer() {
if (!this.active) return Promise.resolve()
getServer(name) {
return this.active[name]
}
return new Promise((res, rej) => {
this.sockets.forEach(function(socket) {
async closeServer(name) {
if (!this.active[name]) return false
try {
return await new Promise((res, rej) => {
this.sockets[name].forEach(function(socket) {
socket.destroy()
})
this.sockets.clear()
this.sockets[name].clear()
this.active.close(err => {
if (err) {
if (err.code !== 'ERR_SERVER_NOT_RUNNING') return rej(err)
}
this.active = null
this.active[name].close(function(err) {
if (err) return rej(err)
// Waiting 1 second for it to close down
setTimeout(function() {res() }, 100)
setTimeout(function() { res(true) }, 1000)
})
})
} catch (err) {
throw new Error(`Error closing ${name}: ${err.message}`)
}
}
getCurrentServer() {
return this.active[this._context]
}
}

View file

@ -1,62 +0,0 @@
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, 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 = dbfilename
this.log = getLog(name)
this.name = name
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, (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({
db: this.db,
util: this.util,
log: this.log,
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() {
return this.app.runVersion('static')
}
}

View file

@ -1,40 +1,29 @@
// import nodewindows from 'node-windows'
import cluster from 'cluster'
import nodewindows from 'node-windows'
import bunyan from 'bunyan-lite'
import { setTimeout } from 'timers/promises'
export function getDefaultStreams() {
export default function getLog(name) {
let settings
let ringbuffer = new bunyan.RingBuffer({ limit: 100 })
let ringbufferwarn = new bunyan.RingBuffer({ limit: 100 })
if (process.env.NODE_ENV === 'production') {
return [{
settings = {
"name": "service-core",
"streams": [{
path: 'log.log',
level: 'info',
}
]
}
} else {
return [{
settings = {
"name": "service-core",
"streams": [{
"stream": process.stdout,
"level": "debug"
}
]
}
}
export default function getLog(name, streams = null, opts = {}) {
let settings
let ringbuffer = new bunyan.RingBuffer({ limit: 100 })
let ringbufferwarn = new bunyan.RingBuffer({ limit: 100 })
if (streams) {
streams.forEach(function(stream) {
if (stream.stream === 'process.stdout') {
stream.stream = process.stdout
}
})
}
settings = {
name: name,
streams: streams || getDefaultStreams()
}
let logger
@ -55,14 +44,6 @@ 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() {},
@ -72,73 +53,18 @@ export default function getLog(name, streams = null, opts = {}) {
level: 'info',
})
let eventManager = null
let eventManagerLoading = false
async function safeLoadEvent(level, message, code) {
if (eventManager === false) {
return Promise.resolve()
}
if (!eventManager) {
if (eventManagerLoading) {
for (let i = 0; i < 10 && eventManagerLoading; i++) {
await setTimeout(50)
}
if (eventManagerLoading) {
eventManager = false
}
return safeLoadEvent(level, message, code)
}
eventManagerLoading = true
let prom
if (opts.import) {
prom = opts.import('node-windows')
} else {
prom = import('node-windows')
}
await prom.then(
function(res) { eventManager = new res.default.EventLogger(name) },
function() { eventManager = false },
)
eventManagerLoading = false
return safeLoadEvent(level, message, code)
}
return new Promise(function(res) {
try {
eventManager[level](message, code, function() { res() })
} catch {
res()
}
})
}
// Create our logger.
logger = bunyan.createLogger(settings)
if (process.env.NODE_ENV === 'production') {
logger.event = {
info: safeLoadEvent.bind(this, 'info'),
warn: safeLoadEvent.bind(this, 'warn'),
error: safeLoadEvent.bind(this, 'error'),
}
logger.event = new nodewindows.EventLogger(name)
} else {
logger.event = {
info: function() { return Promise.resolve() },
warn: function() { return Promise.resolve() },
error: function() { return Promise.resolve() },
info: function() {},
warn: function() {},
error: function() {},
}
}
logger.on('error', function(err) {
if (process.env.NODE_ENV === 'production') {
logger.event.warn(`Error in logger ${name}: ${err.message}`)
} else {
console.log(`Error in logger ${name}: ${err.message}`)
}
})
logger.ringbuffer = ringbuffer
logger.ringbufferwarn = ringbufferwarn

View file

@ -1,60 +0,0 @@
import { request } from '../client.mjs'
export default class GitProvider {
constructor(config, requester = request) {
this.config = config
this.requestConfig = GitProvider.getRequestConfig(config)
this.requester = requester
}
async getLatestVersion() {
let res = await this.requester(this.requestConfig, this.config.url)
.catch(function(err) {
return Promise.reject(new Error('Failed to get release information, got ' + err.message))
})
if (!Array.isArray(res.body)) {
return Promise.reject(new Error('Body was not a valid git repository release data: ' + JSON.stringify(res.body)))
}
let checked = 0
for (let item of res.body) {
if (!Array.isArray(item.assets)) continue
checked++
for (let asset of item.assets) {
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.replace(/ /g, '_'),
link: asset.browser_download_url,
filename: asset.name,
description: item.body,
log: '',
}
}
}
return Promise.reject(new Error(`No valid service-core release was found, checked ${checked} releases.`))
}
downloadVersion(version, target){
return this.requester(this.requestConfig, version.link, target)
}
static getRequestConfig(config) {
if (config.token) {
return { token: config.token }
}
return {}
}
async checkConfig() {
if (!this.config.url) return Promise.reject(new Error('url was missing in git provider'))
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)) }
}
}

View file

@ -1,24 +0,0 @@
export default class StaticProvider {
constructor(config) {
this.config = config
this.static = true
}
getLatestVersion() {
return Promise.resolve({
version: 'static',
link: '',
filename: '',
description: '',
log: '',
})
}
downloadVersion(){
return Promise.reject(new Error('Static provider does not support downloading'))
}
checkConfig() {
return Promise.resolve()
}
}

View file

@ -1,47 +0,0 @@
import cluster from 'cluster'
import Util from './util.mjs'
import fs from 'fs/promises'
import getLog, { getDefaultStreams } from './log.mjs'
import GetDB from './db.mjs'
import Core from './core.mjs'
export async function runner(root_import_meta_url, configname = 'config.json', dbname = 'db.json') {
if (!root_import_meta_url) {
throw new Error('ServiceRunner must be called with the full string from "import.meta.url" from a file residing in the root directory')
}
const util = new Util(root_import_meta_url)
let config = configname
if (typeof(config) === 'string') {
let fullpath = util.getPathFromRoot('./' + config)
try {
config = JSON.parse(await fs.readFile(fullpath))
} catch (err) {
throw new Error(`critical error opening ${fullpath}: ${err.message}`)
}
}
let streams = getDefaultStreams()
if (cluster.isWorker) {
streams[0].level = 'error'
}
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, 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()
return core
}
runner.log = getLog('runner')

View file

@ -1,15 +1,8 @@
import os from 'os'
import path from 'path'
import fs from 'fs/promises'
import { spawn, execSync } from 'child_process'
import { spawn } 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
}
@ -23,193 +16,33 @@ export default class Util {
return path.join(this._root_import_meta_url,'../', add)
}
getExtension(filename) {
let extension = path.extname(filename)
if (filename.indexOf('.tar.') > 0) {
return '.tar' + extension
}
return extension
}
getAppNames(config) {
const validLevels = [
'fatal',
'error',
'warn',
'info',
'debug',
'trace',
]
let out = []
let keys = Object.keys(config)
for (let key of keys) {
if (typeof(config[key]) !== 'object' || config[key] == null)
continue
if (typeof(config[key].port) !== 'number' || !config[key].port)
continue
if (typeof(config[key].provider) !== 'string' || !config[key].provider)
continue
if (config[key].https != null && typeof(config[key].https) !== 'boolean')
continue
if (config[key].updateEvery != null && (typeof(config[key].updateEvery) !== 'number' || config[key].updateEvery < 0))
continue
if (config[key].startWaitUntilFail != null && (typeof(config[key].startWaitUntilFail) !== 'number' || config[key].startWaitUntilFail < 10))
continue
if (config[key].heartbeatTimeout != null && (typeof(config[key].heartbeatTimeout) !== 'number' || config[key].heartbeatTimeout < 10))
continue
if (config[key].heartbeatAttempts != null && (typeof(config[key].heartbeatAttempts) !== 'number' || config[key].heartbeatAttempts < 1))
continue
if (config[key].heartbeatAttemptsWait != null && (typeof(config[key].heartbeatAttemptsWait) !== 'number' || config[key].heartbeatAttemptsWait < 10))
continue
if (config[key].heartbeatPath != null && (typeof(config[key].heartbeatPath) !== 'string' || config[key].heartbeatPath[0] !== '/'))
continue
if (config[key].log != null) {
if (!Array.isArray(config[key].log))
continue
let valid = true
for (let stream of config[key].log) {
if (!stream || typeof(stream) !== 'object' || Array.isArray(stream)) {
valid = false
break
}
if (typeof(stream.level) !== 'string' || !stream.level || !validLevels.includes(stream.level)) {
valid = false
break
}
if ((typeof(stream.path) !== 'string' || !stream.path) && stream.stream !== 'process.stdout') {
valid = false
break
}
}
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)
}
return out
}
get7zipExecutable() {
const util = new Util(import.meta.url)
if (process.platform === 'win32') {
return util.getPathFromRoot('../bin/7zdec.exe')
}
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')
if (config.debugPort != null && (typeof(config.debugPort) !== 'number' || !config.debugPort)) throw new Error('debugPort in config not a valid number')
}
extractFile(file, stream = function() {}) {
let program = this.get7zipExecutable()
let args = ['x', file]
if (file.indexOf('.tar.') > 0) {
program = 'tar'
args = ['xvf', file]
}
return this.runCommand(program, args, path.dirname(file), stream)
}
runCommand(command, options = [], folder = null, stream = function() {}) {
let baseError = new Error('')
return new Promise(function(res, rej) {
let fullcommand = path.join(folder ? folder : '', command)
if (command.indexOf('/') >= 0 || command.indexOf('\\') >= 0) {
fullcommand = command
}
stream(`[Command] ${fullcommand} ${options.join(' ')}\n`)
stream(`[Command] ${folder ? folder : ''}${command} ${options.join(' ')}\n`)
let processor = spawn(command, options, {
shell: true,
cwd: folder,
})
let timeOuter = setInterval(function() {
try {
let timeOuter = setTimeout(function() {
processor.stdin.write('n\n')
} catch {}
}, 250)
processor.stdout.on('data', function(data) {
stream(data.toString().replace(/\r\n/g, '\n'))
stream(data.toString())
})
processor.stderr.on('data', function(data) {
stream(data.toString().replace(/\r\n/g, '\n'))
})
processor.stdin.on('error', function() {
clearInterval(timeOuter)
stream(data.toString())
})
processor.on('error', function(err) {
clearInterval(timeOuter)
baseError.message = err.message
rej(baseError)
rej(err)
})
processor.on('exit', function (code) {
clearInterval(timeOuter)
if (code !== 0) {
baseError.message = 'Program returned error code: ' + code
return rej(baseError)
return rej(new Error('Program returned error code: ' + code))
}
res(code)
})
})
}
runCommandBackground(command, options = [], folder = null, stream = function() {}) {
let fullcommand = path.join(folder ? folder : '', command)
if (command.indexOf('/') >= 0 || command.indexOf('\\') >= 0) {
fullcommand = command
}
stream(`[Command] ${fullcommand} ${options.join(' ')}\n`)
let processor = spawn(command, options, {
shell: true,
cwd: folder,
})
let timeOuter = setInterval(function() {
try {
processor.stdin.write('n\n')
} catch {}
}, 250)
processor.stdout.on('data', function(data) {
stream(data.toString().replace(/\r\n/g, '\n'))
})
processor.stderr.on('data', function(data) {
stream(data.toString().replace(/\r\n/g, '\n'))
})
processor.stdin.on('error', function() {
clearInterval(timeOuter)
})
processor.on('error', function(err) {
clearInterval(timeOuter)
})
processor.on('exit', function (code) {
clearInterval(timeOuter)
})
processor._kill = processor.kill
processor.kill = function() {
if(os.platform() === 'win32'){
try {
execSync('taskkill /pid ' + processor.pid + ' /T /F')
} catch {}
}else{
processor._kill();
}
}
return processor
}
}

View file

@ -1,24 +0,0 @@
{
"name": "service-core",
"title": "ExampleService",
"description": "Example config service running helloworld",
"helloworld": {
"provider": "git",
"url": "https://git.nfp.is/api/v1/repos/thething/sc-helloworld/releases",
"token": null,
"port": 8888,
"https": false,
"updateEvery": 180,
"startWaitUntilFail": 60000,
"heartbeatTimeout": 3000,
"heartbeatAttempts": 5,
"heartbeatAttemptsWait": 2000,
"heartbeatPath": "/",
"log": [
{
"level": "info",
"stream": "process.stdout"
}
]
}
}

View file

@ -1,21 +0,0 @@
import bunyan from 'bunyan-lite'
import { runner } from './core/runner.mjs'
import ServiceCore from './core/lib.mjs'
import Core from './core/core.mjs'
import Application from './core/application.mjs'
import Util from './core/util.mjs'
import HttpServer from './core/http.mjs'
import { request } from './core/client.mjs'
import getLog from './core/log.mjs'
export {
bunyan,
runner,
ServiceCore,
Core,
Application,
Util,
HttpServer,
request,
getLog,
}

2
install.bat Normal file
View file

@ -0,0 +1,2 @@
node service\install.mjs
PAUSE

52
lib.mjs Normal file
View file

@ -0,0 +1,52 @@
import Util from './core/util.mjs'
import { readFileSync } from 'fs'
import getLog from './core/log.mjs'
import lowdb from './core/db.mjs'
import Core from './core/core.mjs'
export default class ServiceCore {
constructor(name, root_import_meta_url) {
if (!root_import_meta_url) {
throw new Error('ServiceCore must be called with the full string from "import.meta.url" from a file residing in the root directory')
}
this._root_import_meta_url = root_import_meta_url
this.util = new Util(this._root_import_meta_url)
this.log = getLog(name)
this.db = null
this.config = null
this.core = null
}
close(err) {
if (err) {
this.log.fatal(err, 'App recorded a fatal error')
process.exit(4)
}
this.log.warn('App asked to be restarted')
process.exit(0)
}
async init(module = null) {
try {
this.config = JSON.parse(readFileSync(this.util.getPathFromRoot('./config.json')))
} catch (err) {
throw new Error('Unable to read config.json from root directory: ' + err)
}
try {
this.db = await lowdb(this.util, this.log)
} catch (err) {
throw new Error('Unable to read initialise lowdb: ' + err)
}
this.core = new Core(this.util, this.config, this.db, this.log, (err) => this.close(err))
if (module) {
return this.startModule(module)
}
}
startModule(module) {
return this.core.startModule(module, this.config.devPort)
}
}

2
npminstall.bat Normal file
View file

@ -0,0 +1,2 @@
npm install
PAUSE

View file

@ -1,56 +1,33 @@
{
"name": "service-core",
"version": "3.0.2",
"version": "2.0.2",
"description": "Core boiler plate code to install node server as windows service",
"main": "index.mjs",
"main": "lib.mjs",
"scripts": {
"dev": "nodemon --watch dev/api --watch core --watch runner.mjs --watch db.mjs --watch log.mjs runner.mjs | bunyan",
"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",
"test:watch": "npm-watch test"
},
"watch": {
"test": {
"patterns": [
"{core,test}/*"
],
"ignore": [
"test/testapp",
"test/testnoexisting"
],
"extensions": "js,mjs",
"quiet": true,
"inherit": true
}
},
"bin": {
"sccli": "./cli.mjs"
"test": "echo \"Error: no test specified\" && exit 1"
},
"repository": {
"type": "git",
"url": "git+https://git.nfp.is/TheThing/service-core.git"
"url": "git+https://github.com/TheThing/service-core.git"
},
"author": "Jonatan Nilsson",
"license": "WTFPL",
"bugs": {
"url": "https://git.nfp.is/TheThing/service-core/issues"
"url": "https://github.com/TheThing/service-core/issues"
},
"homepage": "https://git.nfp.is/TheThing/service-core#readme",
"homepage": "https://github.com/TheThing/service-core#readme",
"files": [
"index.mjs",
"cli.mjs",
"lib.mjs",
"package.json",
"README.md",
"core",
"bin"
"core"
],
"dependencies": {
"bunyan-lite": "^1.2.0",
"lowdb": "^3.0.0"
"bunyan-lite": "^1.0.1",
"lodash": "^4.17.20",
"lowdb": "^1.0.0"
},
"devDependencies": {
"eltro": "^1.3.1"
}
}

69
runner.mjs Normal file
View file

@ -0,0 +1,69 @@
import { readFileSync } from 'fs'
import getLog from './core/log.mjs'
import lowdb from './core/db.mjs'
import Core from './core/core.mjs'
import Util from './core/util.mjs'
let config
try {
config = JSON.parse(readFileSync('./config.json'))
} catch (err) {
let logger = getLog('critical-error')
logger.fatal('Error opening config file')
logger.fatal('Make sure it is valid JSON')
logger.fatal(err)
logger.event.error('Unable to start, error in config.json: ' + err.message)
process.exit(10)
}
const log = getLog(config.name)
const close = function(err) {
if (err) {
log.fatal(err, 'App recorded a fatal error')
log.event.error('App recorded a fatal error: ' + err.message, null, function() {
process.exit(4)
})
return
}
log.warn('App asked to be restarted')
log.event.warn('App requested to be restarted', null, function() {
process.exit(0)
})
}
const util = new Util(import.meta.url)
lowdb(util, log).then(async function(db) {
let core = new Core(util, config, db, log, close)
let errors = 0
try {
await core.start('app')
} catch (err) {
log.event.error('Unable to start app: ' + err.message)
log.error(err, 'Unable to start app')
errors++
}
try {
await core.start('manage')
} catch (err) {
log.event.error('Unable to start manage: ' + err.message)
log.error(err, 'Unable to start manage')
errors++
}
core.startMonitor()
if (errors === 2 || (!core.appRunning && !core.manageRunning)) {
throw new Error('Neither manage or app were started, exiting.')
}
}, function(err) {
log.fatal(err, 'Critical error opening database')
log.event.error('Critical error opening database: ' + err.message, null, function() {
process.exit(2)
})
}).catch(function(err) {
log.fatal(err, 'Unknown error occured opening app')
log.event.error('Unknown error occured opening app: ' + err.message, null, function() {
process.exit(3)
})
})

11
service/install.mjs Normal file
View file

@ -0,0 +1,11 @@
import svc from './service.mjs'
svc.on('install',function(){
svc.start();
});
svc.on('alreadyinstalled',function(){
svc.start();
});
svc.install();

35
service/service.mjs Normal file
View file

@ -0,0 +1,35 @@
import path from 'path'
import { readFileSync } from 'fs'
import { fileURLToPath } from 'url'
import nodewindows from 'node-windows'
function getPathFromRoot(add) {
const __dirname = path.dirname(fileURLToPath(import.meta.url));
return path.join(__dirname,'../', add)
}
let config = JSON.parse(readFileSync(getPathFromRoot('./config.json')))
const Service = nodewindows.Service
let serviceConfig = {
name: config.serviceName,
description: config.description,
script: getPathFromRoot('./runner.mjs'),
env: {
name: 'NODE_ENV',
value: 'production',
},
wait: 0,
grow: .5,
maxRestarts: 10
//, workingDirectory: '...'
//, allowServiceLogon: true
}
console.log('Service', serviceConfig)
// Create a new service object
let svc = new Service(serviceConfig);
export default svc

10
service/uninstall.mjs Normal file
View file

@ -0,0 +1,10 @@
import svc from './service.mjs'
// Listen for the "install" event, which indicates the
// process is available as a service.
svc.on('uninstall',function(){
console.log('Uninstall complete.');
console.log('The service exists: ',svc.exists);
});
svc.uninstall();

View file

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

View file

@ -1,314 +0,0 @@
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'
const util = new Util(import.meta.url)
t.describe('#runVersion("static")', function() {
const assertPort = 22345
let ctx
let app
const defaultHandler = function(orgHandler) {
let handler = orgHandler || function (req, res) {
res.writeHead(200); res.end(JSON.stringify({ a: 1 }))
}
return function(http, port, ctx) {
const server = http.createServer(handler)
return server.listenAsync(port)
}
}
t.beforeEach(function() {
return createFakeContext()
.then(function(res) {
ctx = res
let provider = new StaticProvider()
app = new Application(ctx, provider, 'testapp')
app.config.port = assertPort
app.registerModule(defaultHandler())
})
})
t.afterEach(function() {
return app.closeServer()
})
t.test('should throw if http is not called', async function() {
app.registerModule(function(checkHttp, checkPort, checkCtx) {
assert.strictEqual(checkHttp, app.http)
assert.strictEqual(checkPort, assertPort)
assert.strictEqual(checkCtx.db, ctx.db)
assert.strictEqual(checkCtx.log, ctx.log)
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)
assert.match(err.message, /call/i)
assert.strictEqual(ctx.db.data.core.testapp.active, 'static')
})
t.test('should throw if it timeouts waiting for promise to succeed', async 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)
assert.match(err.message, /50ms/)
assert.strictEqual(ctx.db.data.core.testapp.active, 'static')
})
t.test('should otherwise succeed if it finished within the time limit', async function() {
const handler = defaultHandler()
app.config.startWaitUntilFail = 250
app.registerModule(function(http, port, ctx) {
return new Promise(function(res) {
setTimeout(res, 25)
}).then(function() {
return handler(http, port, ctx)
})
})
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')
})
t.test('should fail if run succeeds but heartbeat errors', async function() {
let called = 0
const handler = function(req, res) {
called++
res.statusCode = 400
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)
assert.strictEqual(called, 3)
assert.strictEqual(ctx.db.data.core.testapp.active, 'static')
})
t.test('should fail if run succeeds but heartbeat times out', async function() {
let called = 0
const handler = function(req, res) {
called++
}
app.config.heartbeatAttempts = 2
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.heartbeatTimeout + app.config.heartbeatAttemptsWait))
assert.strictEqual(called, 2)
assert.strictEqual(ctx.db.data.core.testapp.active, 'static')
})
t.test('should call with correct path', async function() {
const assertPath = '/test/something'
const handler = function(req, res) {
if (req.url === assertPath) {
res.writeHead(204); res.end(JSON.stringify({ a: 1 }))
} else {
res.statusCode = 400
res.end(JSON.stringify({ a: 1 }))
}
}
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.closeServer()
app.registerModule(defaultHandler(handler))
app.config.heartbeatPath = assertPath
await app.runVersion('static')
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() {
const assertConfig = util.getPathFromRoot('./db_test_applicationrun.json')
const assertPort = 22345
let ctx
let app
t.after(function() {
return Promise.all([
fs.rm(util.getPathFromRoot('./testnoexisting/v100'), { force: true, recursive: true }),
fs.rm(util.getPathFromRoot('./testnoexisting/v99'), { force: true, recursive: true }),
fs.rm(util.getPathFromRoot('./testnoexisting/v98'), { force: true, recursive: true }),
fs.rm(util.getPathFromRoot('./testnoexisting/v97'), { force: true, recursive: true }),
])
})
t.beforeEach(function() {
return createFakeContext({ }, util, assertConfig)
.then(function(res) {
ctx = res
let provider = new StaticProvider()
app = new Application(ctx, provider, 'testnoexisting')
app.config.port = assertPort
return app.ctx.db.write()
})
})
t.afterEach(function() {
return Promise.all([
fs.rm(assertConfig),
app.closeServer(),
])
})
t.test('when version is specified, should check if index.mjs exists', async function() {
const assertNotError = new Error('AI DO')
const assertTarget = util.getPathFromRoot('./testnoexisting/v100/index.mjs')
let stubFsStat = stub()
let provider = new StaticProvider()
app = new Application(ctx, provider, 'testnoexisting', {
fs: { stat: stubFsStat }
})
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))
assert.match(err.message, /index\.mjs/i)
assert.match(err.message, /missing/i)
assert.strictEqual(stubFsStat.firstCall[0], assertTarget)
assert.strictEqual(app.ctx.db.data.core.testnoexisting.active, 'v100')
let checkDb = await lowdb({}, ctx.log, assertConfig)
assert.strictEqual(checkDb.data.core.testnoexisting.active, 'v100')
})
t.test('when version is specified and file exists, should attempt to load module', async function() {
const assertError = new Error('Parallel Days')
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)
assert.strictEqual(app.ctx.db.data.core.testnoexisting.active, 'v99')
let checkDb = await lowdb({}, ctx.log, assertConfig)
assert.strictEqual(checkDb.data.core.testnoexisting.active, 'v99')
})
t.test('when version is specified and file exists, should check if it has start', async 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)
assert.strictEqual(checkDb.data.core.testnoexisting.active, 'v98')
})
t.test('when version is specified and file exists and everything is okay, should work normally', async function() {
await fs.mkdir(util.getPathFromRoot('./testnoexisting/v97'), { recursive: true })
await fs.copyFile(
util.getPathFromRoot('./exampleindex.mjs'),
util.getPathFromRoot('./testnoexisting/v97/index.mjs')
)
app.ctx.log.info.reset()
app.ctx.log.event.info.reset()
assert.strictEqual(app.ctx.version, '')
assert.strictEqual(app.fresh, true)
await app.runVersion('v97')
assert.strictEqual(app.fresh, false)
assert.strictEqual(app.ctx.version, 'v97')
assert.ok(app.ctx.log.info.called)
assert.ok(app.ctx.log.event.info.called)
})
})

View file

@ -1,51 +0,0 @@
import { Eltro as t, assert, stub } from 'eltro'
import fs from 'fs/promises'
import Application from '../core/application.mjs'
import GitProvider from '../core/providers/git.mjs'
import Util from '../core/util.mjs'
import { createFakeContext } from './helpers.mjs'
const util = new Util(import.meta.url)
let ctx
let app
let provider
t.before(function() {
return createFakeContext({ }, util, util.getPathFromRoot('./db_test.json'))
.then(function(res) {
ctx = res
provider = new GitProvider({ url: 'https://git.nfp.is/api/v1/repos/thething/sc-helloworld/releases' })
app = new Application(ctx, provider, 'testapp')
return provider.getLatestVersion()
}).then(function(version) {
return fs.rm(`./test/testapp/${version.version}`, { force: true, recursive: true })
})
})
t.after(function() {
return fs.rm(util.getPathFromRoot('./db_test.json'))
.then(function() {
if (ctx.db.data.core.testapp.versions.length) {
return fs.rm(`./test/testapp/${ctx.db.data.core.testapp.versions[0].id}`, { force: true, recursive: true })
}
})
})
t.skip().timeout(10000).test('should run update and install correctly', async function(){
try {
await app.update()
} catch (err) {
console.log()
console.log(err)
if (ctx.db.data.core.testapp.versions.length) {
console.log(ctx.db.data.core.testapp.versions[0].log)
}
throw err
}
assert.ok(ctx.db.data.core.testapp.latestInstalled)
await fs.stat(util.getPathFromRoot(`./testapp/${ctx.db.data.core.testapp.latestInstalled}/index.mjs`))
await fs.stat(util.getPathFromRoot(`./testapp/${ctx.db.data.core.testapp.latestInstalled}/package.json`))
await fs.stat(util.getPathFromRoot(`./testapp/${ctx.db.data.core.testapp.latestInstalled}/node_modules`))
})

File diff suppressed because it is too large Load diff

View file

@ -1,284 +0,0 @@
import { Eltro as t, assert} from 'eltro'
import fs from 'fs/promises'
import http from 'http'
import Util from '../core/util.mjs'
import { request } from '../core/client.mjs'
const util = new Util(import.meta.url)
const testTargetFile = util.getPathFromRoot('./filetest.html')
const port = 61412
const defaultHandler = function(req, res) {
res.statusCode = 200
res.end('{"a":1}');
}
let server = null
let prefix = `http://localhost:${port}/`
let handler = defaultHandler
t.before(function(cb) {
server = http.createServer(function(req, res) {
req.on('error', function(err) {
console.log('error', err)
})
res.on('error', function(err) {
console.log('error', err)
})
handler(req, res)
})
server.listen(port, cb)
})
t.after(function() {
return fs.rm(testTargetFile)
.catch(function() { })
})
t.describe('Basics', function() {
t.beforeEach(function() {
handler = defaultHandler
})
t.test('should require valid config', async function() {
function checkError(err) {
assert.match(err.message, /config/i)
}
await assert.isRejected(request(prefix)).then(checkError)
await assert.isRejected(request('', prefix)).then(checkError)
await assert.isRejected(request([], prefix)).then(checkError)
await assert.isRejected(request(123, prefix)).then(checkError)
await assert.isRejected(request(0, prefix)).then(checkError)
})
t.test('should fail if url is invalid', async function() {
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.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'))
})
})
t.describe('Request', function() {
t.test('should work normally', async function() {
let res = await request({}, prefix)
assert.deepEqual(res.body, {a:1})
})
t.test('should follow redirects', async function() {
let counter = 0
handler = function(req, res) {
if (counter < 3) {
res.statusCode = 302
res.setHeader('Location', encodeURI(prefix))
res.end();
counter++
return
}
assert.strictEqual(req.url, '/')
res.statusCode = 200
res.end('{"a":1}');
return
}
let res = await request({}, prefix)
assert.deepEqual(res.body, {a:1})
assert.strictEqual(counter, 3)
})
t.test('should fail gracefully if HTML is received', async function() {
handler = function(req, res) {
res.statusCode = 200
res.end(`<!DOCTYPE html>
<html lang="en">
<head>
</head>
<body>
</body>
</html>`);
}
let err = await assert.isRejected(request({}, prefix))
assert.match(err.message, /html/i)
assert.notMatch(err.message, /Unexpected token/i)
handler = function(req, res) {
res.statusCode = 200
res.end(`<html>
<head>
</head>
<body>
</body>
</html>`);
}
err = await assert.isRejected(request({}, prefix))
assert.match(err.message, /html/i)
assert.notMatch(err.message, /Unexpected token/i)
})
t.test('should fail if infinite redirect', async function() {
const assertRelativeLocation = 'some/text/here'
const assertLocation = prefix + assertRelativeLocation
let counter = 0
handler = function(req, res) {
res.statusCode = 302
res.setHeader('Location', encodeURI(assertLocation))
if (counter === 0) {
assert.strictEqual(req.url, '/')
} else {
assert.strictEqual(req.url, '/' + assertRelativeLocation)
}
res.end();
counter++
}
let err = await assert.isRejected(request({}, prefix))
assert.strictEqual(counter, 6)
assert.match(err.message, /redirect/i)
assert.match(err.message, new RegExp(assertRelativeLocation))
})
t.test('should fail if redirect is missing location', async function() {
let counter = 0
handler = function(req, res) {
res.statusCode = 302
res.end();
counter++
}
let err = await assert.isRejected(request({}, prefix))
assert.strictEqual(counter, 1)
assert.match(err.message, /redirect/i)
assert.match(err.message, /location/i)
})
t.test('should follow relative redirects', async function() {
const assertUrl = 'asdf1234'
let counter = 0
let url = ''
handler = function(req, res) {
if (counter < 1) {
res.statusCode = 302
res.setHeader('Location', encodeURI(assertUrl))
res.end();
counter++
return
}
url = req.url
res.statusCode = 200
res.end('{"a":1}');
return
}
let res = await request({}, prefix)
assert.deepEqual(res.body, {a:1})
assert.strictEqual(counter, 1)
assert.strictEqual(url, '/' + assertUrl)
counter = 0
res = await request({}, prefix + 'some/url/here')
assert.deepEqual(res.body, {a:1})
assert.strictEqual(counter, 1)
assert.strictEqual(url, '/some/url/' + assertUrl)
})
t.timeout(30).test('should support timeout on invalid url', async function() {
// blocked off port, should time out
let err = await assert.isRejected(request({ timeout: 15 }, 'http://git.nfp.is:8080'))
assert.match(err.message, /timed out/i)
assert.match(err.message, /15/i)
})
})
t.describe('Request download', function() {
const assertBody = `<!DOCTYPE html>
<html lang="en">
<head>
</head>
<body>
</body>
</html>`
t.beforeEach(function() {
handler = defaultHandler
})
t.afterEach(function() {
return fs.rm(testTargetFile)
.catch(function() { })
})
t.test('should support downloading file successfully', async function() {
handler = function(req, res) {
res.statusCode = 200
res.end(assertBody);
}
await request({}, prefix, testTargetFile)
let stat = await fs.stat(testTargetFile)
assert.notStrictEqual(stat.size, 0)
assert.strictEqual(stat.size, assertBody.length)
let contents = await (await fs.readFile(testTargetFile)).toString()
assert.strictEqual(contents, assertBody)
})
t.test('should support downloading file after redirect successfully', async function() {
let counter = 0
handler = function(req, res) {
if (counter < 3) {
res.statusCode = 302
res.setHeader('Location', encodeURI(prefix))
res.end();
counter++
return
}
res.statusCode = 200
res.end(assertBody);
}
await request({}, prefix, testTargetFile)
assert.strictEqual(counter, 3)
let stat = await fs.stat(testTargetFile)
assert.notStrictEqual(stat.size, 0)
assert.strictEqual(stat.size, assertBody.length)
let contents = await (await fs.readFile(testTargetFile)).toString()
assert.strictEqual(contents, assertBody)
})
t.test('should not create file if download redirects too many times', async function() {
handler = function(req, res) {
res.statusCode = 302
res.setHeader('Location', encodeURI(prefix))
res.end(assertBody);
}
let err = await assert.isRejected(request({}, prefix, testTargetFile))
assert.match(err.message, /redirects/)
let errStat = await assert.isRejected(fs.stat(testTargetFile))
assert.strictEqual(errStat.code, 'ENOENT')
})
t.test('should not create file if download fails', async function() {
handler = function(req, res) {
res.statusCode = 404
res.end('{}');
}
let err = await assert.isRejected(request({}, prefix, testTargetFile))
assert.match(err.message, /HTTP/)
assert.match(err.message, /404/)
let errStat = await assert.isRejected(fs.stat(testTargetFile))
assert.strictEqual(errStat.code, 'ENOENT')
})
})
t.after(function(cb) {
server.close(cb)
})

View file

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

View file

@ -1,591 +0,0 @@
import { Eltro as t, assert} from 'eltro'
import fs from 'fs/promises'
import HttpServer from '../core/http.mjs'
import Util from '../core/util.mjs'
import { request } from '../core/client.mjs'
import { setTimeout } from 'timers/promises'
import { prettyPrintMessage } from './helpers.mjs'
import { pipeline } from 'stream'
import getLog from '../core/log.mjs'
const util = new Util(import.meta.url)
const port = 61412
const turnDebuggingOn = false
const runners = [
['runner.mjs', 'testapp'],
['runner_cluster.mjs', 'testappcluster'],
]
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.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]
}]
}
})
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()
return waitUntilClosed()
}
}).then(function() {
return http.closeServer()
})
})
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\`)
})
}
`
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\`)
})
}
`
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)
}
}
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[i]))
if (res) return true
}
else if (logs[i].match(regMatch)) {
return true
}
}
}
async function waitUntilListening() {
let listeningLine = null
while (processor.exitCode == null
&& !hasLogLine((rec) => { listeningLine = rec; return rec.listening && rec.port })) {
await catchupLog(10)
}
catchupLog()
if (listeningLine.listening && listeningLine.port) {
return listeningLine
} else {
return null
}
}
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)
}
}
log('-- core.test.integration.mjs crash here --')
log(lastErr.toString())
throw lastErr
}
async function waitUntilClosed(listening) {
while (true) {
catchupLog()
try {
await request({}, `http://localhost:${listening.port}/`)
} catch (err) {
break
}
await setTimeout(25)
}
catchupLog()
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') }
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) {
await catchupLog(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)
// Reset our log
logs.splice(0, logs.length); logIndex = 0; logWaitIndex = 0;
if (turnDebuggingOn) { console.log('\n-------\n') }
const assertNameVersion1 = 'v1_ok'
if (!turnDebuggingOn) { console.log(`Running update ${assertNameVersion1} test`) }
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)
catchupLog()
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)
})
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
}
}
assert.ok(foundCore)
assert.ok(foundWorker)
}
wasSuccessful = true
})
})
})

File diff suppressed because it is too large Load diff

View file

@ -1,334 +0,0 @@
import { Eltro as t, assert, stub } from 'eltro'
import fs from 'fs/promises'
import fsSync from 'fs'
import lowdb from '../core/db.mjs'
import Util from '../core/util.mjs'
var util = new Util(import.meta.url)
var logger = {
info: stub(),
warn: stub(),
error: stub(),
}
t.before(function() {
return Promise.all([
fs.rm('./test/db_test.json', { force: true }),
fs.rm('./test/null', { force: true }),
])
})
t.afterEach(function() {
return Promise.all([
fs.rm('./test/db_test.json', { force: true }),
fs.rm('./test/null', { force: true }),
])
})
t.test('Should auto create file with some defaults', async function() {
await assert.isRejected(fs.stat('./test/db_test.json'))
let db = await lowdb({}, logger, util.getPathFromRoot('./db_test.json'))
let stat = await fs.stat('./test/db_test.json')
assert.ok(stat.size > 0)
assert.ok(db.data.core)
assert.ok(db.data.core.version)
assert.notOk(db.data.core.app)
assert.notOk(db.data.core.manager)
assert.strictEqual(db.filename, util.getPathFromRoot('./db_test.json'))
})
t.test('#writeSync() Should support syncronous write', function() {
const filename = util.getPathFromRoot('./db_test.json')
const assertValue = 'Yume no Naka no Watashi no Yume'
return lowdb({}, logger, filename)
.then(function(db) {
db.data.songtest = assertValue
db.writeSync()
let content = JSON.parse(fsSync.readFileSync(filename))
assert.strictEqual(content.songtest, assertValue)
})
})
t.test('#writeSync() Should not throw', async function() {
let db = await lowdb({}, logger, util.getPathFromRoot('./db_test.json'))
db.filename = util.getPathFromRoot('../test')
assert.notOk(db.log.error.called)
db.writeSync()
assert.ok(db.log.error.called)
assert.match(db.log.error.firstCall[0].message, /directory/i)
assert.match(db.log.error.firstCall[1], /writ/)
assert.match(db.log.error.firstCall[1], new RegExp(db.filename.replace(/\\/g, '\\\\')))
})
t.test('Should support in-memory db', async function() {
await assert.isRejected(fs.stat('./test/db_test.json'))
await assert.isRejected(fs.stat('./test/null'))
let db = await lowdb({}, logger, null)
await assert.isRejected(fs.stat('./test/db_test.json'))
await assert.isRejected(fs.stat('./test/null'))
assert.ok(db.data.core)
assert.ok(db.data.core.version)
assert.notOk(db.data.core.app)
assert.notOk(db.data.core.manager)
})
t.test('Should map config to config', async function() {
const assertConfig = { a: 1 }
let db = await lowdb(assertConfig, logger, util.getPathFromRoot('./db_test.json'))
assert.strictEqual(db.config, assertConfig)
})
t.test('Should apply defaults to existing file', async function() {
await assert.isRejected(fs.stat('./test/db_test.json'))
await fs.writeFile('./test/db_test.json', '{"test":true}')
let db = await lowdb({}, logger, util.getPathFromRoot('./db_test.json'))
let stat = await fs.stat('./test/db_test.json')
assert.ok(stat.size > 0)
assert.ok(db.data.core)
assert.ok(db.data.core.version)
assert.strictEqual(db.data.test, true)
assert.notOk(db.data.core.app)
assert.notOk(db.data.core.manager)
})
t.test('Should not fail if db is invalid', async function() {
logger.warn.reset()
await assert.isRejected(fs.stat('./test/db_test.json'))
await fs.writeFile('./test/db_test.json', '[]')
let db = await lowdb({}, logger, util.getPathFromRoot('./db_test.json'))
let stat = await fs.stat('./test/db_test.json')
assert.ok(stat.size > 0)
assert.ok(db.data.core)
assert.ok(db.data.core.version)
assert.notOk(db.data.core.app)
assert.notOk(db.data.core.manager)
assert.ok(logger.warn.called)
assert.match(logger.warn.firstCall[0], /db_test.json/i)
assert.match(logger.warn.firstCall[0], /clear/i)
})
t.test('Should attach logger to log', async function() {
let db = await lowdb({}, logger, util.getPathFromRoot('./db_test.json'))
assert.strictEqual(db.log, logger)
})
t.test('Should support adding an application with defaults', async function() {
await assert.isRejected(fs.stat('./test/db_test.json'))
let db = await lowdb({}, logger, util.getPathFromRoot('./db_test.json'))
let stat = await fs.stat('./test/db_test.json')
assert.ok(stat.size > 0)
assert.ok(db.data.core)
assert.ok(db.data.core.version)
assert.notOk(db.data.core.app)
assert.notOk(db.data.core.manager)
db.addApplication('app')
assert.ok(db.data.core.app)
assert.ok(db.data.core.app.versions)
assert.strictEqual(db.data.core.app.active, '')
assert.strictEqual(db.data.core.app.latestInstalled, '')
assert.notOk(db.data.core.herpderp)
db.addApplication('herpderp')
assert.ok(db.data.core.herpderp)
assert.ok(db.data.core.herpderp.versions)
assert.strictEqual(db.data.core.herpderp.active, '')
assert.strictEqual(db.data.core.herpderp.latestInstalled, '')
})
t.test('Should support reading from db', async function() {
const assertValue = { a: 1 }
await assert.isRejected(fs.stat('./test/db_test.json'))
let db = await lowdb({}, logger, util.getPathFromRoot('./db_test.json'))
db.data.test = assertValue
await db.write()
assert.strictEqual(db.data.test, assertValue)
let dbSecondary = await lowdb({}, logger, util.getPathFromRoot('./db_test.json'))
assert.notStrictEqual(dbSecondary.data.test, assertValue)
assert.deepStrictEqual(dbSecondary.data.test, assertValue)
})
t.test('should throw if unable to write to file', function() {
return assert.isRejected(lowdb({}, logger, util.getPathFromRoot('../test')))
})
t.test('should have basic database-like functions defined', async function() {
const assertItem1 = { a: 1 }
const assertItem2 = { a: 2 }
const assertItem3 = { a: 3 }
let db = await lowdb({}, logger, util.getPathFromRoot('./db_test.json'))
assert.strictEqual(db.id, 'id')
db.data.myarr = []
db.upsert(db.data.myarr, assertItem1)
db.upsert(db.data.myarr, assertItem2)
assert.strictEqual(db.data.myarr.length, 2)
assert.ok(assertItem1.id)
assert.ok(assertItem2.id)
assert.notStrictEqual(assertItem1.id, assertItem2.id)
assert.strictEqual(db.get(db.data.myarr, assertItem1.id), assertItem1)
assert.strictEqual(db.get(db.data.myarr, assertItem2.id), assertItem2)
assert.strictEqual(db.get(db.data.myarr, assertItem2.id + 1), null)
await db.write()
let dbSec = await lowdb({}, logger, util.getPathFromRoot('./db_test.json'))
assert.strictEqual(dbSec.data.myarr.length, 2)
assert.notStrictEqual(dbSec.get(dbSec.data.myarr, assertItem1.id), assertItem1)
assert.notStrictEqual(dbSec.get(dbSec.data.myarr, assertItem2.id), assertItem2)
assert.deepEqual(dbSec.get(dbSec.data.myarr, assertItem1.id), assertItem1)
assert.deepEqual(dbSec.get(dbSec.data.myarr, assertItem2.id), assertItem2)
dbSec.upsert(dbSec.data.myarr, assertItem3)
assert.strictEqual(dbSec.data.myarr.length, 3)
assert.ok(assertItem3.id)
assert.strictEqual(dbSec.get(dbSec.data.myarr, assertItem3.id), assertItem3)
assert.strictEqual(assertItem2.id + 1, assertItem3.id)
const assertItem4 = { a: 4, id: assertItem2.id }
dbSec.upsert(dbSec.data.myarr, assertItem4)
assert.strictEqual(dbSec.data.myarr.length, 3)
assert.notDeepEqual(dbSec.get(dbSec.data.myarr, assertItem2.id), assertItem2)
assert.deepEqual(dbSec.get(dbSec.data.myarr, assertItem2.id), assertItem4)
const assertItem5 = { a: 5, id: assertItem1.id }
dbSec.upsert(dbSec.data.myarr, assertItem5)
assert.strictEqual(dbSec.data.myarr.length, 3)
assert.notDeepEqual(dbSec.get(dbSec.data.myarr, assertItem1.id), assertItem1)
assert.deepEqual(dbSec.get(dbSec.data.myarr, assertItem1.id), assertItem5)
dbSec.remove(dbSec.data.myarr, assertItem2.id)
assert.strictEqual(dbSec.data.myarr.length, 2)
assert.strictEqual(dbSec.get(dbSec.data.myarr, assertItem2.id), null)
dbSec.remove(dbSec.data.myarr, assertItem1.id)
assert.strictEqual(dbSec.data.myarr.length, 1)
assert.strictEqual(dbSec.get(dbSec.data.myarr, assertItem1.id), null)
})
t.test('should have basic database-like functions with string-like name of collection', async function() {
const assertItem1 = { a: 1 }
const assertItem2 = { a: 2 }
const assertItem3 = { a: 3 }
let db = await lowdb({}, logger, util.getPathFromRoot('./db_test.json'))
assert.strictEqual(db.id, 'id')
db.data.myarr = []
db.upsert('myarr', assertItem1)
db.upsert('myarr', assertItem2)
assert.strictEqual(db.data.myarr.length, 2)
assert.ok(assertItem1.id)
assert.ok(assertItem2.id)
assert.notStrictEqual(assertItem1.id, assertItem2.id)
assert.strictEqual(db.get('myarr', assertItem1.id), assertItem1)
assert.strictEqual(db.get('myarr', assertItem2.id), assertItem2)
assert.strictEqual(db.get('myarr', assertItem2.id + 1), null)
await db.write()
let dbSec = await lowdb({}, logger, util.getPathFromRoot('./db_test.json'))
assert.strictEqual(dbSec.data.myarr.length, 2)
assert.notStrictEqual(dbSec.get('myarr', assertItem1.id), assertItem1)
assert.notStrictEqual(dbSec.get('myarr', assertItem2.id), assertItem2)
assert.deepEqual(dbSec.get('myarr', assertItem1.id), assertItem1)
assert.deepEqual(dbSec.get('myarr', assertItem2.id), assertItem2)
dbSec.upsert('myarr', assertItem3)
assert.strictEqual(dbSec.data.myarr.length, 3)
assert.ok(assertItem3.id)
assert.strictEqual(dbSec.get('myarr', assertItem3.id), assertItem3)
assert.strictEqual(assertItem2.id + 1, assertItem3.id)
const assertItem4 = { a: 4, id: assertItem2.id }
dbSec.upsert('myarr', assertItem4)
assert.strictEqual(dbSec.data.myarr.length, 3)
assert.notDeepEqual(dbSec.get('myarr', assertItem2.id), assertItem2)
assert.deepEqual(dbSec.get('myarr', assertItem2.id), assertItem4)
const assertItem5 = { a: 5, id: assertItem1.id }
dbSec.upsert('myarr', assertItem5)
assert.strictEqual(dbSec.data.myarr.length, 3)
assert.notDeepEqual(dbSec.get('myarr', assertItem1.id), assertItem1)
assert.deepEqual(dbSec.get('myarr', assertItem1.id), assertItem5)
dbSec.remove('myarr', assertItem2.id)
assert.strictEqual(dbSec.data.myarr.length, 2)
assert.strictEqual(dbSec.get('myarr', assertItem2.id), null)
dbSec.remove('myarr', assertItem1.id)
assert.strictEqual(dbSec.data.myarr.length, 1)
assert.strictEqual(dbSec.get('myarr', assertItem1.id), null)
})
t.test('#upsert() should work properly', async function() {
let db = await lowdb({}, logger, null)
db.data.test = {
items: []
}
db.upsert(db.data.test.items, { id: '1234', text: '1' })
db.upsert(db.data.test.items, { id: '1234', text: '2' })
assert.strictEqual(db.data.test.items.length, 1)
assert.strictEqual(db.data.test.items[0].id, '1234')
assert.strictEqual(db.data.test.items[0].text, '2')
})
t.test('#upsertFirst() should work properly', async function() {
let db = await lowdb({}, logger, null)
db.data.test = {
items: []
}
db.upsertFirst(db.data.test.items, { id: '1234', text: '1' })
db.upsertFirst(db.data.test.items, { id: '1234', text: '2' })
db.upsertFirst(db.data.test.items, { id: '1', text: '3' })
db.upsertFirst(db.data.test.items, { id: '2', text: '4' })
assert.strictEqual(db.data.test.items.length, 3)
assert.strictEqual(db.data.test.items[2].id, '1234')
assert.strictEqual(db.data.test.items[2].text, '2')
assert.strictEqual(db.data.test.items[1].text, '3')
assert.strictEqual(db.data.test.items[0].text, '4')
})

View file

@ -1,64 +0,0 @@
import { Eltro as t, assert} from 'eltro'
import { isObject, defaults } from '../core/defaults.mjs'
t.describe('#isObject()', () => {
t.test('should return false for invalid objects', function() {
let tests = [
[],
'',
'asdf',
12341,
0,
]
tests.forEach(function(check) {
assert.strictEqual(isObject(check), false)
})
})
t.test('should return true for valid objects', function() {
let tests = [
{},
new Object(),
]
tests.forEach(function(check) {
assert.strictEqual(isObject(check), true)
})
})
})
t.describe('#defaults()', () => {
t.test('should throw if original is missing or invalid', () => {
assert.throws(function() { defaults() })
assert.throws(function() { defaults('asdf') })
assert.throws(function() { defaults('') })
assert.throws(function() { defaults(null) })
assert.throws(function() { defaults(1234) })
assert.throws(function() { defaults([]) })
})
t.test('should not override existing variables', () => {
let assertOutput = { a: 2 }
defaults(assertOutput, { a: 1 })
assert.deepStrictEqual(assertOutput, { a: 2 })
})
t.test('should allow nesting through objects', () => {
let def = { a: { b: 2 } }
let inside = { a: { c: 3} }
defaults(inside, def)
assert.deepStrictEqual(inside.a, {
b: 2,
c: 3,
})
})
t.test('should not return new reference', () => {
let assertInput = { a: 1 }
let existing = assertInput
defaults(assertInput, { b: 2 })
assert.strictEqual(assertInput, existing)
})
})

View file

@ -1,17 +0,0 @@
export function start(http, port, ctx) {
const server = http.createServer(function (req, res) {
res.writeHead(200);
res.end(JSON.stringify({ version: 'exampleindex' }))
})
return new Promise(function(res, rej) {
server.listen(port || 4000, '0.0.0.0', function(err) {
if (err) {
return rej(err)
}
ctx.log.event.info(`Server is listening on ${port} serving exampleindex`)
ctx.log.info(`Server is listening on ${port} serving exampleindex`)
res()
})
})
}

View file

@ -1,92 +0,0 @@
import { stub } from 'eltro'
import lowdb from '../core/db.mjs'
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(),
error: stub(),
}
}
}
export function createFakeContext(config = { }, util = new Util(import.meta.url), filename = null) {
const log = createFakeLog()
return lowdb(config, log, filename).then(function(res) {
return {
db: res,
util: util,
log: log,
core: { },
}
})
}
var colors = {
'bold' : [1, 22],
'italic' : [3, 23],
'underline' : [4, 24],
'inverse' : [7, 27],
'white' : [37, 39],
'grey' : [90, 39],
'black' : [30, 39],
'blue' : [34, 39],
'cyan' : [36, 39],
'green' : [32, 39],
'magenta' : [35, 39],
'red' : [31, 39],
'yellow' : [33, 39]
};
let levels = {
10: 'TRACE',
20: 'DEBUG',
30: 'INFO',
40: 'WARN',
50: 'ERROR',
60: 'FATAL',
}
var levelcolor = {
10: 'white', // TRACE
20: 'yellow', // DEBUG
30: 'cyan', // INFO
40: 'magenta', // WARN
50: 'red', // ERROR
60: 'inverse', // FATAL
};
function style(str, color) {
if (!str)
return '';
var codes = colors[color];
if (codes) {
return '\x1B[' + codes[0] + 'm' + str +
'\x1B[' + codes[1] + 'm';
} else {
return str;
}
}
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('(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 { }
}
console.log(line)
}

View file

@ -1,167 +0,0 @@
import { Eltro as t, assert, stub } from 'eltro'
import http from 'http'
import https from 'https'
import { setTimeout } from 'timers/promises'
import { request } from '../core/client.mjs'
import HttpServer from '../core/http.mjs'
const port = 61413
let prefix = `http://localhost:${port}/`
t.describe('config', function() {
t.test('should use https if https is true', function() {
let server = new HttpServer()
assert.strictEqual(server.creator, http)
assert.strictEqual(server.ishttps, false)
server = new HttpServer({ https: true })
assert.strictEqual(server.creator, https)
assert.strictEqual(server.ishttps, true)
})
})
t.describe('Sockets', function() {
let http = new HttpServer()
t.after(function() {
return http.closeServer()
})
t.test('should keep track of sockets through its lifetime', function(cb) {
let actives = []
let server = http.createServer(function(req, res) {
req.on('error', function(err) { cb(err) })
res.on('error', function(err) { cb(err) })
res.on('finish', function() { })
actives.push(res)
})
Promise.resolve()
.then(async function() {
await new Promise(function(res, rej) {
server.listen(port, function() { res()})
})
assert.strictEqual(actives.length, 0)
assert.strictEqual(http.sockets.size, 0)
request({}, prefix).then(function() {}, cb)
request({}, prefix).then(async function() {
while (http.sockets.size > 0) {
await setTimeout(10)
}
assert.strictEqual(http.sockets.size, 0)
cb()
}, cb)
while (actives.length < 2) {
await setTimeout(10)
}
assert.strictEqual(http.sockets.size, 2)
assert.ok(http.active)
actives[0].statusCode = 200
actives[0].end('{}')
actives[1].statusCode = 200
actives[1].end('{}')
}).catch(cb)
})
})
t.describe('closeServer()', function() {
let http = new HttpServer()
t.after(function() {
return http.closeServer()
})
t.test('should not fail if server is not listening', function() {
http.createServer(function() { })
return http.closeServer()
})
t.test('should support forcefully closing them on server close', function(cb) {
let requestErrors = []
let serverErrors = []
let server = http.createServer(function(req, res) {
req.on('error', function(err) { serverErrors.push(err) })
res.on('error', function(err) { serverErrors.push(err) })
res.on('finish', function() { })
})
Promise.resolve()
.then(async function() {
await new Promise(function(res, rej) {
server.listen(port, function() { res()})
})
assert.strictEqual(http.sockets.size, 0)
request({}, prefix).then(
function() { cb(new Error('first succeeded')) },
function(err) { requestErrors.push(err) }
)
request({}, prefix).then(
function() { cb(new Error('first succeeded')) },
function(err) { requestErrors.push(err) }
)
while (http.sockets.size < 2) {
await setTimeout(10)
}
assert.ok(http.active)
http.closeServer().then(function() { }, cb)
while (requestErrors.length < 2) {
await setTimeout(10)
}
assert.strictEqual(http.sockets.size, 0)
assert.strictEqual(requestErrors.length, 2)
assert.strictEqual(serverErrors.length, 2)
assert.strictEqual(serverErrors[0].code, 'ECONNRESET')
assert.strictEqual(serverErrors[1].code, 'ECONNRESET')
assert.strictEqual(requestErrors[0].code, 'ECONNRESET')
assert.strictEqual(requestErrors[1].code, 'ECONNRESET')
while (requestErrors.length < 2) {
await setTimeout(10)
}
while (http.active) {
await setTimeout(10)
}
})
.then(function() { cb()}, cb)
})
})
t.describe('listenAsync()', function() {
let httpFirst = new HttpServer()
let httpSecond = new HttpServer()
t.after(function() {
return Promise.all([
httpFirst.closeServer(),
httpSecond.closeServer(),
])
})
t.test('should reject successfully if port is busy', async function() {
let serverFirst = httpFirst.createServer(function() { })
let serverSecond = httpSecond.createServer(function() { })
await serverFirst.listenAsync(port)
await setTimeout(10)
let err = await assert.isRejected(serverSecond.listenAsync(port))
assert.strictEqual(err.code, 'EADDRINUSE')
assert.ok(serverFirst.listening)
assert.notOk(serverSecond.listening)
})
})

View file

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

View file

@ -1,294 +0,0 @@
import { Eltro as t, assert, stub } from 'eltro'
import fs from 'fs/promises'
import getLog from '../core/log.mjs'
t.describe('#constructor', function() {
t.afterEach(function() {
process.env.NODE_ENV = null
})
t.after(function() {
return Promise.all([
fs.rm('./log.log', { recursive: true, force: true }),
])
})
t.test('should add name', function() {
const assertName = 'Stray Cat'
let logger = getLog(assertName)
assert.strictEqual(logger.fields.name, assertName)
process.env.NODE_ENV = 'production'
logger = getLog(assertName)
assert.strictEqual(logger.fields.name, assertName)
})
t.test('should add default stdout streams in normal environment', function() {
let logger = getLog('app', null)
assert.strictEqual(logger.streams.length, 4)
assert.strictEqual(logger.streams[0].stream, process.stdout)
assert.strictEqual(logger.streams[0].level, 20)
})
t.test('should add default file log stream in production environment', function() {
process.env.NODE_ENV = 'production'
let logger = getLog('app', null)
assert.strictEqual(logger.streams.length, 4)
assert.strictEqual(logger.streams[0].path, 'log.log')
assert.strictEqual(logger.streams[0].level, 30)
})
t.test('should not add default stream if empty array', function() {
let logger = getLog('app', [])
assert.strictEqual(logger.streams.length, 3)
process.env.NODE_ENV = 'production'
logger = getLog('app', [])
assert.strictEqual(logger.streams.length, 3)
})
t.test('should replace process.stdout with actual process', function() {
let logger = getLog('app', [{ stream: 'process.stdout', level: 'info' }])
assert.strictEqual(logger.streams.length, 4)
assert.strictEqual(logger.streams[0].stream, process.stdout)
process.env.NODE_ENV = 'production'
logger = getLog('app', [{ stream: 'process.stdout', level: 'info' }])
assert.strictEqual(logger.streams.length, 4)
assert.strictEqual(logger.streams[0].stream, process.stdout)
})
})
t.describe('ringbuffer', function() {
let logger
t.beforeEach(function() {
logger = getLog('app', [])
})
t.test('should have ringbuffer for info', function() {
const assertMessage = 'Oitachi'
assert.strictEqual(logger.ringbuffer.records.length, 0)
logger.info(assertMessage)
assert.strictEqual(logger.ringbuffer.records.length, 1)
assert.strictEqual(logger.ringbuffer.records[0].level, 30)
assert.strictEqual(logger.ringbuffer.records[0].msg, assertMessage)
logger.debug(assertMessage)
assert.strictEqual(logger.ringbuffer.records.length, 1)
logger.warn(assertMessage)
assert.strictEqual(logger.ringbuffer.records.length, 2)
assert.strictEqual(logger.ringbuffer.records[1].level, 40)
assert.strictEqual(logger.ringbuffer.records[1].msg, assertMessage)
})
t.test('should keep it limited to max 100 records', function() {
const assertPrefix = 'In memory of Keiten'
for (let i = 1; i <= 101; i++) {
logger.info(assertPrefix + i)
}
assert.strictEqual(logger.ringbuffer.records.length, 100)
assert.strictEqual(logger.ringbuffer.records[0].msg, assertPrefix + '2')
logger.info(assertPrefix)
assert.strictEqual(logger.ringbuffer.records.length, 100)
assert.strictEqual(logger.ringbuffer.records[0].msg, assertPrefix + '3')
})
})
t.describe('ringbufferwarn', function() {
let logger
t.beforeEach(function() {
logger = getLog('app', [])
})
t.test('should have ringbufferwarn for info', function() {
const assertMessage = 'Oitachi'
assert.strictEqual(logger.ringbufferwarn.records.length, 0)
logger.warn(assertMessage)
assert.strictEqual(logger.ringbufferwarn.records.length, 1)
assert.strictEqual(logger.ringbufferwarn.records[0].level, 40)
assert.strictEqual(logger.ringbufferwarn.records[0].msg, assertMessage)
logger.info(assertMessage)
assert.strictEqual(logger.ringbufferwarn.records.length, 1)
logger.error(assertMessage)
assert.strictEqual(logger.ringbufferwarn.records.length, 2)
assert.strictEqual(logger.ringbufferwarn.records[1].level, 50)
assert.strictEqual(logger.ringbufferwarn.records[1].msg, assertMessage)
})
t.test('should keep it limited to max 100 records', function() {
const assertPrefix = 'In memory of Keiten'
for (let i = 1; i <= 101; i++) {
logger.warn(assertPrefix + i)
}
assert.strictEqual(logger.ringbufferwarn.records.length, 100)
assert.strictEqual(logger.ringbufferwarn.records[0].msg, assertPrefix + '2')
logger.warn(assertPrefix)
assert.strictEqual(logger.ringbufferwarn.records.length, 100)
assert.strictEqual(logger.ringbufferwarn.records[0].msg, assertPrefix + '3')
})
})
t.describe('event', function() {
t.test('should call import if not in production', async function() {
let stubImport = stub()
stubImport.rejects(new Error('should not be seen'))
let logger = getLog('app', [], { import: stubImport })
let first = new Promise(function(res, rej) {
setImmediate(function() { logger.event.warn('text message here').then(res, rej) })
})
let second = new Promise(function(res, rej) {
setImmediate(function() { logger.event.warn('new message here').then(res, rej) })
})
await Promise.all([
first, second,
])
assert.notOk(stubImport.called)
})
t.test('should call import correctly if in production and fail only once', async function() {
process.env.NODE_ENV = 'production'
let stubImport = stub()
stubImport.rejects(new Error('should not be seen'))
let logger = getLog('app', [], { import: stubImport })
let first = new Promise(function(res, rej) {
setImmediate(function() { try { logger.event.warn('first').then(res, rej) } catch (err) { rej(err) } })
})
let second = new Promise(function(res, rej) {
setImmediate(function() { try { logger.event.warn('second').then(res, rej) } catch (err) { rej(err) } })
})
await Promise.all([
first, second,
])
assert.ok(stubImport.called)
assert.strictEqual(stubImport.callCount, 1)
assert.strictEqual(stubImport.firstCall[0], 'node-windows')
await new Promise(function(res, rej) {
setImmediate(function() { try { logger.event.warn('third').then(res, rej) } catch (err) { rej(err) } })
})
})
t.test('should call event on imported object correctly', async function() {
const assertName = 'It is going to be The Special'
let checkName = ''
let stubInfo = stub().returnWith(function(msg, code, cb) { setTimeout(cb, 20) })
let stubWarn = stub().returnWith(function(msg, code, cb) { setTimeout(cb, 20) })
let stubError = stub().returnWith(function(msg, code, cb) { setTimeout(cb, 20) })
process.env.NODE_ENV = 'production'
let stubImport = stub().resolves({
default: {
EventLogger: function(name) {
checkName = name
this.info = stubInfo
this.warn = stubWarn
this.error = stubError
},
},
})
let logger = getLog(assertName, [], { import: stubImport })
let first = new Promise(function(res, rej) {
setImmediate(function() { try { logger.event.info('first', 1010).then(res, rej) } catch (err) { rej(err) } })
})
let second = new Promise(function(res, rej) {
setImmediate(function() { try { logger.event.warn('second', 1020).then(res, rej) } catch (err) { rej(err) } })
})
await Promise.all([
first, second,
])
assert.strictEqual(checkName, assertName)
assert.ok(stubInfo.called)
assert.strictEqual(stubInfo.firstCall[0], 'first')
assert.strictEqual(stubInfo.firstCall[1], 1010)
assert.ok(stubWarn.called)
assert.strictEqual(stubWarn.firstCall[0], 'second')
assert.strictEqual(stubWarn.firstCall[1], 1020)
assert.notOk(stubError.called)
await new Promise(function(res, rej) {
setImmediate(function() { try { logger.event.error('third', 1030).then(res, rej) } catch (err) { rej(err) } })
})
assert.ok(stubError.called)
assert.strictEqual(stubError.firstCall[0], 'third')
assert.strictEqual(stubError.firstCall[1], 1030)
})
t.test('should work even if it were to throw', async function() {
const assertName = 'It is going to be The Special'
let checkName = ''
let stubInfo = stub().returnWith(function() { throw new Error('not to be seen') })
let stubWarn = stub().returnWith(function() { throw new Error('not to be seen') })
let stubError = stub().returnWith(function() { throw new Error('not to be seen') })
process.env.NODE_ENV = 'production'
let stubImport = stub().resolves({
default: {
EventLogger: function(name) {
checkName = name
this.info = stubInfo
this.warn = stubWarn
this.error = stubError
},
},
})
let logger = getLog(assertName, [], { import: stubImport })
let first = new Promise(function(res, rej) {
setImmediate(function() { try { logger.event.info().then(res, rej) } catch (err) { rej(err) } })
})
let second = new Promise(function(res, rej) {
setImmediate(function() { try { logger.event.warn().then(res, rej) } catch (err) { rej(err) } })
})
await Promise.all([
first, second,
])
assert.strictEqual(checkName, assertName)
assert.ok(stubInfo.called)
assert.ok(stubWarn.called)
assert.notOk(stubError.called)
await new Promise(function(res, rej) {
setImmediate(function() { try { logger.event.error().then(res, rej) } catch (err) { rej(err) } })
})
assert.ok(stubError.called)
})
t.test('should work without stub', async function() {
let res = await import('node-windows').catch(function() {})
if (!res) { return }
process.env.NODE_ENV = 'production'
let logger = getLog('service-core-unit-test', [])
await logger.event.info('Hello from service-core log.event unit test')
})
})

View file

@ -1,96 +0,0 @@
import { Eltro as t, assert, stub } from 'eltro'
import Util from '../../core/util.mjs'
import fs from 'fs/promises'
import GitProvider from '../../core/providers/git.mjs'
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.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.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.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)
})
t.test('should otherwise succeed', function() {
return new GitProvider({ url: 'https://git.nfp.is/api/v1/repos/TheThing/sc-helloworld/releases' })
.getLatestVersion()
})
let test = t
if (!process.env.gittesttoken) {
console.log('Skipping "git.test.integration: #getLatestVersion() should succeed on private repo with token"')
test = test.skip()
}
test.test('should succeed on private repo with token', function() {
return new GitProvider({
token: process.env.gittesttoken.trim(),
url: 'https://git.nfp.is/api/v1/repos/TheThing/privateexample/releases',
}).getLatestVersion()
})
})
t.timeout(5000).describe('#downloadVersion()', function() {
const util = new Util(import.meta.url)
let test = t
if (!process.env.gittesttoken) {
console.log('Skipping "git.test.integration: #downloadVersion() should successfully download release"')
test = test.skip()
}
test.test('should successfully download release', async function() {
let provider = new GitProvider({
token: process.env.gittesttoken.trim(),
url: 'https://git.nfp.is/api/v1/repos/TheThing/privateexample/releases',
})
await provider.checkConfig()
let version = await provider.getLatestVersion()
assert.ok(version.version)
assert.ok(version.filename)
assert.ok(version.link)
let path = util.getPathFromRoot('./file.7z')
await provider.downloadVersion(version, path)
let stat = await fs.stat(path)
assert.ok(stat.size > 0)
stat = await fs.stat('./test/providers/file.7z')
assert.ok(stat.size > 0)
let output = ''
await util.runCommand(util.get7zipExecutable(), ['l', 'file.7z'], util.getPathFromRoot('./'), function(chunk) {
output += chunk + '\n'
})
assert.ok(output.indexOf('file1.txt'))
assert.ok(output.indexOf('file2.txt'))
})
})

View file

@ -1,347 +0,0 @@
import { Eltro as t, assert, stub } from 'eltro'
import GitProvider from '../../core/providers/git.mjs'
t.describe('#getLatestVersion()', function() {
t.test('should return correct name and link in result', async function() {
const assertName = 'Karen'
const assertLink = 'Over The Future'
const assertFilename = 'test-sc.7z'
let stubber = stub()
let provider = new GitProvider({}, 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 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'
let stubber = stub()
let provider = new GitProvider({}, 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 no assets', async function() {
const assertName = 'name1'
const assertLink = 'somelink'
const assertFilename = 'something-sc.7z'
let stubber = stub()
let provider = new GitProvider({}, stubber)
stubber.resolves({ body: [
{ name: 'test', assets: [] },
{ 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 filename', async function() {
const assertName = 'name2'
const assertLink = 'somelink2'
const assertFilename = 'something-sc.7z'
let stubber = stub()
let provider = new GitProvider({}, stubber)
stubber.resolves({ body: [
{ name: 'test', assets: [{ name: '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', async function() {
const assertName = 'name3'
const assertLink = 'somelink3'
const assertFilename = 'something-sc.7z'
let stubber = stub()
let provider = new GitProvider({}, stubber)
stubber.resolves({ body: [
{ name: 'test', assets: [{ name: 'nope.7z', browser_download_url: 'nope' }] },
{ name: assertName, assets: [
{ name: 'nope.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', 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 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())
provider.requester.resolves({ body: { } })
let err = await assert.isRejected(provider.getLatestVersion())
assert.match(err.message, /valid/)
assert.match(err.message, /repository/)
assert.match(err.message, /release/)
})
t.test('should wrap rejected errors', async function() {
const assertError = new Error('Oosegoto')
let provider = new GitProvider({}, stub())
provider.requester.rejects(assertError)
let err = await assert.isRejected(provider.getLatestVersion())
assert.notStrictEqual(err, assertError)
assert.match(err.message, /failed/i)
assert.match(err.message, /release/i)
assert.match(err.message, new RegExp(assertError.message))
})
})
t.describe('#downloadVersion()', function() {
t.test('should call requester with correct url and info', async function() {
const assertError = new Error('Moeyuru Monmon Mousou Zou')
const assertConfig = { a: 1 }
const assertUrl = 'http://test'
const assertTarget = '/some/path/here'
let provider = new GitProvider({}, stub())
provider.requestConfig = assertConfig
provider.requester.rejects(assertError)
let err = await assert.isRejected(provider.downloadVersion({
link: assertUrl,
}, assertTarget))
assert.strictEqual(err, assertError)
assert.strictEqual(provider.requester.firstCall[0], assertConfig)
assert.strictEqual(provider.requester.firstCall[1], assertUrl)
assert.strictEqual(provider.requester.firstCall[2], assertTarget)
})
})
t.describe('#getRequestConfig()', function() {
t.test('should default return empty object', function() {
assert.deepStrictEqual(GitProvider.getRequestConfig({}), {})
assert.deepStrictEqual(GitProvider.getRequestConfig({ asdf: '1234' }), {})
assert.deepStrictEqual(GitProvider.getRequestConfig({ url: 'http://test' }), {})
assert.deepStrictEqual(GitProvider.getRequestConfig({ a: 1 }), {})
})
t.test('should return token if token is defined', function() {
const assertToken = 'asdf1234'
assert.deepStrictEqual(GitProvider.getRequestConfig({ token: assertToken }), { token: assertToken })
assert.deepStrictEqual(GitProvider.getRequestConfig({ token: assertToken, asdf: '1234' }), { token: assertToken })
assert.deepStrictEqual(GitProvider.getRequestConfig({ token: assertToken, url: 'http://test' }), { token: assertToken })
assert.deepStrictEqual(GitProvider.getRequestConfig({ token: assertToken, a: 1 }), { token: assertToken })
})
})
t.describe('#checkConfig()', function() {
t.test('should fail if missing url in config', async function() {
let err = await assert.isRejected(new GitProvider({}).checkConfig())
assert.match(err.message, /url/i)
assert.match(err.message, /missing/i)
})
t.test('should fail if url is something else than an url', async function() {
let tests = [[], {}, 1234, 'asdf', 'http://']
let errors = await Promise.all(tests.map(function(check) {
return assert.isRejected(new GitProvider({ url: check }).checkConfig(), `Promise fulfilled with url = ${JSON.stringify(check)}`)
}))
for (let err of errors) {
assert.match(err.message, /url/i)
assert.match(err.message, /valid/i)
}
})
t.test('should not call requester with correct config and url', async function() {
const assertUrl = 'http://test'
let provider = new GitProvider({ url: assertUrl }, stub())
let err = await provider.checkConfig()
assert.notOk(provider.requester.called)
})
})

View file

@ -1,30 +0,0 @@
import { Eltro as t, assert, stub } from 'eltro'
import StaticProvider from '../../core/providers/static.mjs'
t.describe('#getLatestVersion()', function() {
t.test('should return static result', async function() {
let provider = new StaticProvider({})
let version = await provider.getLatestVersion()
assert.strictEqual(version.version, 'static')
assert.strictEqual(version.link, '')
assert.strictEqual(version.filename, '')
assert.strictEqual(version.log, '')
})
})
t.describe('#downloadVersion()', function() {
t.test('should return an error', async function() {
let provider = new StaticProvider({})
let err = await assert.isRejected(provider.downloadVersion({}))
assert.match(err.message, /static/i)
assert.match(err.message, /support/i)
})
})
t.describe('#checkConfig()', function() {
t.test('should always succeed', async function() {
await new StaticProvider({}).checkConfig()
})
})

View file

@ -1,28 +0,0 @@
import fs from 'fs'
import { runner } from '../core/runner.mjs'
try {
fs.rmSync(util.getPathFromRoot('./db.json'))
} catch {}
runner(import.meta.url, {
name: 'test-runner',
testapp: {
port: 31313,
provider: 'git',
url: 'http://localhost:61412/releases',
updateEvery: 0.014,
heartbeatTimeout: 500,
heartbeatAttempts: 2,
heartbeatAttemptsWait: 250,
}
}, 'db.json')
.then(
function(core) {
core.log.info('core is running')
},
function(err) {
runner.log.error(err, 'Error starting runner')
process.exit(1)
}
)

View file

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

View file

@ -1 +0,0 @@
export default { a: 1 }

Binary file not shown.

Binary file not shown.

View file

@ -1,462 +0,0 @@
import { Eltro as t, assert} from 'eltro'
import fs from 'fs/promises'
import Util from '../core/util.mjs'
import { defaults } from '../core/defaults.mjs'
const isWindows = process.platform === 'win32'
t.describe('#getPathFromRoot()', function() {
t.test('should return file relative to root', async function() {
var util = new Util(import.meta.url)
let path = util.getPathFromRoot('')
if (isWindows) {
assert.ok(path.endsWith('\\test\\'))
} else {
assert.ok(path.endsWith('/test/'))
}
path = util.getPathFromRoot('../core/http.mjs')
if (isWindows) {
assert.ok(path.endsWith('\\core\\http.mjs'))
} else {
assert.ok(path.endsWith('/core/http.mjs'))
}
let stat = await fs.stat(util.getPathFromRoot('../core/core.mjs'))
assert.ok(stat.size > 0)
})
})
t.describe('#getUrlFromRoot()', function() {
t.test('should return an import compatible path', async function() {
var util = new Util(import.meta.url)
let data = await import(util.getUrlFromRoot('template.mjs'))
assert.deepStrictEqual(data.default, { a: 1 })
})
})
t.describe('#get7zipExecutable()', function() {
var util = new Util(import.meta.url)
if (process.platform === 'win32') {
console.log('Adding 7zip windows exe path test')
t.test('should return windows executable path', function() {
assert.ok(util.get7zipExecutable().endsWith('\\service-core\\bin\\7zdec.exe'), `${util.get7zipExecutable()} should end with 7zdec.exe`)
})
} else {
console.log('Adding 7zip linux exe path test')
t.test('should return linux executable path', function() {
assert.ok(util.get7zipExecutable().endsWith('/service-core/bin/7zdec'), `${util.get7zipExecutable()} should end with 7zdec`)
})
}
})
t.describe('#getExtension()', function() {
var util = new Util(import.meta.url)
t.test('should return correct extension on basic extension types', function() {
assert.strictEqual(util.getExtension('file.7z'), '.7z')
assert.strictEqual(util.getExtension('file.zip'), '.zip')
assert.strictEqual(util.getExtension('file.tar'), '.tar')
assert.strictEqual(util.getExtension('file.doc'), '.doc')
assert.strictEqual(util.getExtension('file.rar'), '.rar')
assert.strictEqual(util.getExtension('file.test'), '.test')
})
t.test('should support that annoying tar extension', function() {
assert.strictEqual(util.getExtension('file.tar.test'), '.tar.test')
assert.strictEqual(util.getExtension('file.tar.gz'), '.tar.gz')
assert.strictEqual(util.getExtension('file.tar.xz'), '.tar.xz')
assert.strictEqual(util.getExtension('file.tar.bz2'), '.tar.bz2')
})
})
t.describe('#getApplications()', function() {
var util = new Util(import.meta.url)
t.test('should fail to find if not a valid object', function() {
assert.deepStrictEqual(util.getAppNames({ app: [] }), [])
assert.deepStrictEqual(util.getAppNames({ app: 1234 }), [])
assert.deepStrictEqual(util.getAppNames({ app: '124124' }), [])
assert.deepStrictEqual(util.getAppNames({ app: '' }), [])
assert.deepStrictEqual(util.getAppNames({ app: {} }), [])
assert.deepStrictEqual(util.getAppNames({ app: null }), [])
})
t.test('should return the name of the key if an object with port and provider is found', function() {
assert.deepStrictEqual(util.getAppNames({ app: { port: 1234, provider: 'asdf' } }), ['app'])
})
t.test('should fail to find if port is missing or port not a number', function() {
assert.deepStrictEqual(util.getAppNames({ app: { provider: 'asdf', } }), [])
assert.deepStrictEqual(util.getAppNames({ app: { provider: 'asdf', port: null } }), [])
assert.deepStrictEqual(util.getAppNames({ app: { provider: 'asdf', port: 'asdf' } }), [])
assert.deepStrictEqual(util.getAppNames({ app: { provider: 'asdf', port: '1234' } }), [])
assert.deepStrictEqual(util.getAppNames({ app: { provider: 'asdf', port: 0 } }), [])
assert.deepStrictEqual(util.getAppNames({ app: { provider: 'asdf', port: [] } }), [])
assert.deepStrictEqual(util.getAppNames({ app: { provider: 'asdf', port: {} } }), [])
})
t.test('should fail to find if provider is missing or not a string', function() {
assert.deepStrictEqual(util.getAppNames({ app: { port: 1234 } }), [])
assert.deepStrictEqual(util.getAppNames({ app: { provider: '', port: 1234 } }), [])
assert.deepStrictEqual(util.getAppNames({ app: { provider: null, port: 1234 } }), [])
assert.deepStrictEqual(util.getAppNames({ app: { provider: [], port: 1234 } }), [])
assert.deepStrictEqual(util.getAppNames({ app: { provider: {}, port: 1234 } }), [])
assert.deepStrictEqual(util.getAppNames({ app: { provider: 1234, port: 1234 } }), [])
})
function getBase(extra = {}) {
return defaults({ app: extra }, { app: { provider: 'asdf', port: 1234, } })
}
t.test('should fail to find if https is defined but not a boolean', function() {
assert.deepStrictEqual(util.getAppNames(getBase()), ['app'])
assert.deepStrictEqual(util.getAppNames(getBase({ https: null })), ['app'])
assert.deepStrictEqual(util.getAppNames(getBase({ https: false })), ['app'])
assert.deepStrictEqual(util.getAppNames(getBase({ https: true })), ['app'])
assert.deepStrictEqual(util.getAppNames(getBase({ https: 'asdf' })), [])
assert.deepStrictEqual(util.getAppNames(getBase({ https: '1234' })), [])
assert.deepStrictEqual(util.getAppNames(getBase({ https: 0 })), [])
assert.deepStrictEqual(util.getAppNames(getBase({ https: [] })), [])
assert.deepStrictEqual(util.getAppNames(getBase({ https: {} })), [])
})
t.test('should fail to find if updateEvery is defined but not a valid number', function() {
assert.deepStrictEqual(util.getAppNames(getBase()), ['app'])
assert.deepStrictEqual(util.getAppNames(getBase({ updateEvery: null })), ['app'])
assert.deepStrictEqual(util.getAppNames(getBase({ updateEvery: 5 })), ['app'])
assert.deepStrictEqual(util.getAppNames(getBase({ updateEvery: 1000 })), ['app'])
assert.deepStrictEqual(util.getAppNames(getBase({ updateEvery: 'asdf' })), [])
assert.deepStrictEqual(util.getAppNames(getBase({ updateEvery: '1234' })), [])
assert.deepStrictEqual(util.getAppNames(getBase({ updateEvery: 0 })), ['app'])
assert.deepStrictEqual(util.getAppNames(getBase({ updateEvery: -1 })), [])
assert.deepStrictEqual(util.getAppNames(getBase({ updateEvery: -5 })), [])
assert.deepStrictEqual(util.getAppNames(getBase({ updateEvery: [] })), [])
assert.deepStrictEqual(util.getAppNames(getBase({ updateEvery: {} })), [])
})
t.test('should fail to find if startWaitUntilFail is defined but not a valid number', function() {
assert.deepStrictEqual(util.getAppNames(getBase()), ['app'])
assert.deepStrictEqual(util.getAppNames(getBase({ startWaitUntilFail: null })), ['app'])
assert.deepStrictEqual(util.getAppNames(getBase({ startWaitUntilFail: 5 })), [])
assert.deepStrictEqual(util.getAppNames(getBase({ startWaitUntilFail: 15 })), ['app'])
assert.deepStrictEqual(util.getAppNames(getBase({ startWaitUntilFail: 1000 })), ['app'])
assert.deepStrictEqual(util.getAppNames(getBase({ startWaitUntilFail: 'asdf' })), [])
assert.deepStrictEqual(util.getAppNames(getBase({ startWaitUntilFail: '1234' })), [])
assert.deepStrictEqual(util.getAppNames(getBase({ startWaitUntilFail: 0 })), [])
assert.deepStrictEqual(util.getAppNames(getBase({ startWaitUntilFail: -5 })), [])
assert.deepStrictEqual(util.getAppNames(getBase({ startWaitUntilFail: [] })), [])
assert.deepStrictEqual(util.getAppNames(getBase({ startWaitUntilFail: {} })), [])
})
t.test('should fail to find if heartbeatTimeout is defined but not a valid number', function() {
assert.deepStrictEqual(util.getAppNames(getBase()), ['app'])
assert.deepStrictEqual(util.getAppNames(getBase({ heartbeatTimeout: null })), ['app'])
assert.deepStrictEqual(util.getAppNames(getBase({ heartbeatTimeout: 5 })), [])
assert.deepStrictEqual(util.getAppNames(getBase({ heartbeatTimeout: 15 })), ['app'])
assert.deepStrictEqual(util.getAppNames(getBase({ heartbeatTimeout: 1000 })), ['app'])
assert.deepStrictEqual(util.getAppNames(getBase({ heartbeatTimeout: 'asdf' })), [])
assert.deepStrictEqual(util.getAppNames(getBase({ heartbeatTimeout: '1234' })), [])
assert.deepStrictEqual(util.getAppNames(getBase({ heartbeatTimeout: 0 })), [])
assert.deepStrictEqual(util.getAppNames(getBase({ heartbeatTimeout: -5 })), [])
assert.deepStrictEqual(util.getAppNames(getBase({ heartbeatTimeout: [] })), [])
assert.deepStrictEqual(util.getAppNames(getBase({ heartbeatTimeout: {} })), [])
})
t.test('should fail to find if heartbeatAttempts is defined but not a valid number', function() {
assert.deepStrictEqual(util.getAppNames(getBase()), ['app'])
assert.deepStrictEqual(util.getAppNames(getBase({ heartbeatAttempts: null })), ['app'])
assert.deepStrictEqual(util.getAppNames(getBase({ heartbeatAttempts: 1 })), ['app'])
assert.deepStrictEqual(util.getAppNames(getBase({ heartbeatAttempts: 15 })), ['app'])
assert.deepStrictEqual(util.getAppNames(getBase({ heartbeatAttempts: 1000 })), ['app'])
assert.deepStrictEqual(util.getAppNames(getBase({ heartbeatAttempts: 'asdf' })), [])
assert.deepStrictEqual(util.getAppNames(getBase({ heartbeatAttempts: '1234' })), [])
assert.deepStrictEqual(util.getAppNames(getBase({ heartbeatAttempts: 0 })), [])
assert.deepStrictEqual(util.getAppNames(getBase({ heartbeatAttempts: -5 })), [])
assert.deepStrictEqual(util.getAppNames(getBase({ heartbeatAttempts: [] })), [])
assert.deepStrictEqual(util.getAppNames(getBase({ heartbeatAttempts: {} })), [])
})
t.test('should fail to find if heartbeatAttemptsWait is defined but not a valid number', function() {
assert.deepStrictEqual(util.getAppNames(getBase()), ['app'])
assert.deepStrictEqual(util.getAppNames(getBase({ heartbeatAttemptsWait: null })), ['app'])
assert.deepStrictEqual(util.getAppNames(getBase({ heartbeatAttemptsWait: 5 })), [])
assert.deepStrictEqual(util.getAppNames(getBase({ heartbeatAttemptsWait: 15 })), ['app'])
assert.deepStrictEqual(util.getAppNames(getBase({ heartbeatAttemptsWait: 1000 })), ['app'])
assert.deepStrictEqual(util.getAppNames(getBase({ heartbeatAttemptsWait: 'asdf' })), [])
assert.deepStrictEqual(util.getAppNames(getBase({ heartbeatAttemptsWait: '1234' })), [])
assert.deepStrictEqual(util.getAppNames(getBase({ heartbeatAttemptsWait: 0 })), [])
assert.deepStrictEqual(util.getAppNames(getBase({ heartbeatAttemptsWait: -5 })), [])
assert.deepStrictEqual(util.getAppNames(getBase({ heartbeatAttemptsWait: [] })), [])
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'])
assert.deepStrictEqual(util.getAppNames(getBase({ heartbeatPath: 5 })), [])
assert.deepStrictEqual(util.getAppNames(getBase({ heartbeatPath: 15 })), [])
assert.deepStrictEqual(util.getAppNames(getBase({ heartbeatPath: 1000 })), [])
assert.deepStrictEqual(util.getAppNames(getBase({ heartbeatPath: 'asdf' })), [])
assert.deepStrictEqual(util.getAppNames(getBase({ heartbeatPath: '1234' })), [])
assert.deepStrictEqual(util.getAppNames(getBase({ heartbeatPath: '/asdf' })), ['app'])
assert.deepStrictEqual(util.getAppNames(getBase({ heartbeatPath: '/1234' })), ['app'])
assert.deepStrictEqual(util.getAppNames(getBase({ heartbeatPath: 0 })), [])
assert.deepStrictEqual(util.getAppNames(getBase({ heartbeatPath: -5 })), [])
assert.deepStrictEqual(util.getAppNames(getBase({ heartbeatPath: [] })), [])
assert.deepStrictEqual(util.getAppNames(getBase({ heartbeatPath: {} })), [])
})
t.test('should fail to find if log is defined but not an array', function() {
assert.deepStrictEqual(util.getAppNames(getBase()), ['app'])
assert.deepStrictEqual(util.getAppNames(getBase({ log: null })), ['app'])
assert.deepStrictEqual(util.getAppNames(getBase({ log: 5 })), [])
assert.deepStrictEqual(util.getAppNames(getBase({ log: 'asdf' })), [])
assert.deepStrictEqual(util.getAppNames(getBase({ log: '1234' })), [])
assert.deepStrictEqual(util.getAppNames(getBase({ log: 0 })), [])
assert.deepStrictEqual(util.getAppNames(getBase({ log: [] })), ['app'])
assert.deepStrictEqual(util.getAppNames(getBase({ log: {} })), [])
assert.deepStrictEqual(util.getAppNames(getBase({ log: { length:1 } })), [])
})
t.test('should fail to find if log has an item but level and either stream or path ', function() {
assert.deepStrictEqual(util.getAppNames(getBase()), ['app'])
assert.deepStrictEqual(util.getAppNames(getBase({ log: null })), ['app'])
assert.deepStrictEqual(util.getAppNames(getBase({ log: [null] })), [])
assert.deepStrictEqual(util.getAppNames(getBase({ log: [5] })), [])
assert.deepStrictEqual(util.getAppNames(getBase({ log: [15] })), [])
assert.deepStrictEqual(util.getAppNames(getBase({ log: [1000] })), [])
assert.deepStrictEqual(util.getAppNames(getBase({ log: ['asdf'] })), [])
assert.deepStrictEqual(util.getAppNames(getBase({ log: ['1234'] })), [])
assert.deepStrictEqual(util.getAppNames(getBase({ log: ['/asdf'] })), [])
assert.deepStrictEqual(util.getAppNames(getBase({ log: ['/1234'] })), [])
assert.deepStrictEqual(util.getAppNames(getBase({ log: [0] })), [])
assert.deepStrictEqual(util.getAppNames(getBase({ log: [-5] })), [])
assert.deepStrictEqual(util.getAppNames(getBase({ log: [[]] })), [])
assert.deepStrictEqual(util.getAppNames(getBase({ log: [{}] })), [])
assert.deepStrictEqual(util.getAppNames(getBase({ log: [{ level: null, path: 'log' }] })), [])
assert.deepStrictEqual(util.getAppNames(getBase({ log: [{ level: 5, path: 'log' }] })), [])
assert.deepStrictEqual(util.getAppNames(getBase({ log: [{ level: 0, path: 'log' }] })), [])
assert.deepStrictEqual(util.getAppNames(getBase({ log: [{ level: [], path: 'log' }] })), [])
assert.deepStrictEqual(util.getAppNames(getBase({ log: [{ level: {}, path: 'log' }] })), [])
assert.deepStrictEqual(util.getAppNames(getBase({ log: [{ level: '', path: 'log' }] })), [])
assert.deepStrictEqual(util.getAppNames(getBase({ log: [{ level: 'asdf', path: 'log' }] })), [])
assert.deepStrictEqual(util.getAppNames(getBase({ log: [{ level: 'fatal', path: 'log' }] })), ['app'])
assert.deepStrictEqual(util.getAppNames(getBase({ log: [{ level: 'error', path: 'log' }] })), ['app'])
assert.deepStrictEqual(util.getAppNames(getBase({ log: [{ level: 'warn', path: 'log' }] })), ['app'])
assert.deepStrictEqual(util.getAppNames(getBase({ log: [{ level: 'info', path: 'log' }] })), ['app'])
assert.deepStrictEqual(util.getAppNames(getBase({ log: [{ level: 'debug', path: 'log' }] })), ['app'])
assert.deepStrictEqual(util.getAppNames(getBase({ log: [{ level: 'trace', path: 'log' }] })), ['app'])
assert.deepStrictEqual(util.getAppNames(getBase({ log: [{ level: 'info', path: '' }] })), [])
assert.deepStrictEqual(util.getAppNames(getBase({ log: [{ level: 'info', path: null }] })), [])
assert.deepStrictEqual(util.getAppNames(getBase({ log: [{ level: 'info', path: 5 }] })), [])
assert.deepStrictEqual(util.getAppNames(getBase({ log: [{ level: 'info', path: 0 }] })), [])
assert.deepStrictEqual(util.getAppNames(getBase({ log: [{ level: 'info', path: [] }] })), [])
assert.deepStrictEqual(util.getAppNames(getBase({ log: [{ level: 'info', path: {} }] })), [])
assert.deepStrictEqual(util.getAppNames(getBase({ log: [{ level: 'info', stream: '' }] })), [])
assert.deepStrictEqual(util.getAppNames(getBase({ log: [{ level: 'info', stream: null }] })), [])
assert.deepStrictEqual(util.getAppNames(getBase({ log: [{ level: 'info', stream: 5 }] })), [])
assert.deepStrictEqual(util.getAppNames(getBase({ log: [{ level: 'info', stream: 0 }] })), [])
assert.deepStrictEqual(util.getAppNames(getBase({ log: [{ level: 'info', stream: [] }] })), [])
assert.deepStrictEqual(util.getAppNames(getBase({ log: [{ level: 'info', stream: {} }] })), [])
assert.deepStrictEqual(util.getAppNames(getBase({ log: [{ level: 'info', stream: 'asdf' }] })), [])
assert.deepStrictEqual(util.getAppNames(getBase({ log: [{ level: 'info', stream: 'process.stdout' }] })), ['app'])
})
})
t.describe('#verifyConfig()', function() {
var util = new Util(import.meta.url)
let input
t.beforeEach(function() {
input = {
name: 'test',
title: 'Test',
description: 'Some description',
app: { port: 1234, provider: 'asdf' },
}
})
let testMissing = ['name']
testMissing.forEach(function(check) {
t.test(`should fail on missing ${check}`, function() {
delete input[check]
assert.throws(function() {
util.verifyConfig(input)
}, function(err) {
assert.match(err.message, new RegExp(check))
return true
})
})
})
let testOptional = ['title', 'description']
testOptional.forEach(function(check) {
t.test(`should succeed even if ${check} is missing`, function() {
delete input[check]
util.verifyConfig(input)
})
})
t.test('should succeed if debug port is specified or null', function() {
input.debugPort = 1234
util.verifyConfig(input)
input.debugPort = null
util.verifyConfig(input)
})
t.test('should fail if debug port is invalid', function() {
let checks = [
[],
{},
0,
'asdf',
'1234'
]
checks.forEach(function(check) {
input.debugPort = check
assert.throws(function() {
util.verifyConfig(input)
}, function(err) {
assert.match(err.message, /debugPort/)
assert.match(err.message, /number/)
return true
})
})
})
t.test('should fail if no objects in config', function() {
delete input.app
assert.throws(function() {
util.verifyConfig(input)
}, function(err) {
assert.match(err.message, /no/)
assert.match(err.message, /app/)
return true
})
})
})
t.describe('#extractFile()', function() {
var util = new Util(import.meta.url)
t.beforeEach(function() {
return Promise.all([
fs.rm('./test/testapp/example.tar', { force: true }),
fs.rm('./test/testapp/file1.txt', { force: true }),
fs.rm('./test/testapp/file2.txt', { force: true }),
])
})
t.afterEach(function() {
return Promise.all([
fs.rm('./test/testapp/example.tar', { force: true }),
fs.rm('./test/testapp/file1.txt', { force: true }),
fs.rm('./test/testapp/file2.txt', { force: true }),
])
})
t.test('should support extracting 7z file', async function() {
await Promise.all([
assert.isRejected(fs.stat(util.getPathFromRoot('./testapp/file1.txt'))),
assert.isRejected(fs.stat(util.getPathFromRoot('./testapp/file2.txt'))),
])
let log = ''
try {
await util.extractFile(util.getPathFromRoot('./testapp/example.7z'), function(msg) {
log += msg + '\n'
})
} catch (err) {
console.log(log)
console.log(err)
throw err
}
let stats = await Promise.all([
fs.stat(util.getPathFromRoot('./testapp/file1.txt')),
fs.stat(util.getPathFromRoot('./testapp/file2.txt')),
])
assert.strictEqual(stats[0].size, 5)
assert.strictEqual(stats[1].size, 5)
})
t.test('should support extracting .tar.gz file', async function() {
await Promise.all([
assert.isRejected(fs.stat(util.getPathFromRoot('./testapp/file1.txt'))),
assert.isRejected(fs.stat(util.getPathFromRoot('./testapp/file2.txt'))),
])
let log = ''
try {
await util.extractFile(util.getPathFromRoot('./testapp/example.tar.gz'), function(msg) {
log += msg + '\n'
})
} catch (err) {
console.log(log)
console.log(err)
throw err
}
let stats = await Promise.all([
fs.stat(util.getPathFromRoot('./testapp/file1.txt')),
fs.stat(util.getPathFromRoot('./testapp/file2.txt')),
])
assert.strictEqual(stats[0].size, 5)
assert.strictEqual(stats[1].size, 5)
await assert.isRejected(fs.stat(util.getPathFromRoot('./testapp/example.tar')))
})
t.test('should stream the process of extracting', async function() {
let output = ''
try {
await util.extractFile(util.getPathFromRoot('./testapp/example.tar.gz'), function(msg) { output += msg + '\n' })
} catch (err) {
console.log(output)
console.log(err)
throw err
}
assert.match(output, /file1.txt/)
assert.match(output, /file2.txt/)
assert.strictEqual(output.indexOf('\r\n'), -1)
})
})

5
uninstall.bat Normal file
View file

@ -0,0 +1,5 @@
@setlocal enableextensions
@cd /d "%~dp0"
node service\uninstall.mjs
PAUSE