Compare commits
77 commits
134cd3d6a4
...
master
Author | SHA1 | Date | |
---|---|---|---|
6c720f4c2d | |||
14fead9c17 | |||
6c115aa8b2 | |||
5d39f776e1 | |||
79f3203f70 | |||
e8a2ec52a6 | |||
1995b38510 | |||
63a06a2a34 | |||
cb5de72e13 | |||
33b3d98a37 | |||
618cfd0451 | |||
b5359515b5 | |||
23f9173720 | |||
d519996959 | |||
90d4e7ab81 | |||
a5239d0c27 | |||
46386a13b3 | |||
99d7a0655d | |||
95d72fc404 | |||
2edcedb1b7 | |||
4a508d20a4 | |||
1d7b118229 | |||
1f70f36e8d | |||
73e9be2ff0 | |||
87cc47f498 | |||
b8a0ec137c | |||
67606b9b3b | |||
4024f1269a | |||
25cac907be | |||
4d15623a23 | |||
746e0c3f46 | |||
44f576c1d4 | |||
dccad23890 | |||
5330ecfa5c | |||
b152f18fc3 | |||
bc2d3d9a9b | |||
e0da74f0be | |||
47344c5e7a | |||
57be8a144a | |||
0f504eaf53 | |||
ed73607e7b | |||
17b100bb07 | |||
396ac97b8b | |||
35b82934f1 | |||
5eb9fb9476 | |||
b50ed47ec5 | |||
2dad21e8ab | |||
bca4b665a6 | |||
39af3ca162 | |||
be6c24778d | |||
d434564217 | |||
10e45b33ee | |||
cbc0f7a37a | |||
dbcc431e70 | |||
5f3e688b8c | |||
e540a54844 | |||
4f4bc8cf6a | |||
3f8e3cdc16 | |||
c29e128814 | |||
ae4f0f141b | |||
f1710601fc | |||
758e61b8b1 | |||
df7e1e5509 | |||
e026b9757d | |||
2d8e7fd8cc | |||
442b241aaf | |||
47b8ac2ebb | |||
f008af597d | |||
a16a04a91f | |||
edba2d72ab | |||
f8adcbbc21 | |||
726ac81eb3 | |||
0948885671 | |||
17830d7f8d | |||
04747bc1c6 | |||
d215329c2b | |||
1be2c4ef07 |
59 changed files with 7856 additions and 1087 deletions
|
@ -1,58 +0,0 @@
|
|||
version: 2
|
||||
jobs:
|
||||
build:
|
||||
docker:
|
||||
- image: circleci/node:latest
|
||||
resource_class: medium
|
||||
working_directory: ~/app
|
||||
steps:
|
||||
- checkout
|
||||
- run:
|
||||
name: Install npm deployment app
|
||||
command: sudo npm install -g github-release-cli @babel/runtime
|
||||
- run:
|
||||
name: Check if this is a new release
|
||||
command: |
|
||||
set +e
|
||||
PACKAGE_VERSION=$(cat package.json | grep version | head -1 | awk -F: '{ print $2 }' | sed 's/[", ]//g')
|
||||
github-release list --owner $CIRCLE_PROJECT_USERNAME --repo $CIRCLE_PROJECT_REPONAME | grep "tag_name=\"v${PACKAGE_VERSION}\""
|
||||
retVal=$?
|
||||
if [ $retVal -eq 0 ]; then
|
||||
echo "Release already exists. Quitting early"
|
||||
circleci step halt
|
||||
fi
|
||||
- run:
|
||||
name: Get current git commit message
|
||||
command: |
|
||||
echo "export COMMIT_MESSAGE=\"$(git log --format=oneline -n 1 $CIRCLE_SHA1)\"" >> $BASH_ENV
|
||||
source $BASH_ENV
|
||||
- run:
|
||||
name: Get 7zz
|
||||
command: |
|
||||
wget https://www.7-zip.org/a/7z2106-linux-x64.tar.xz
|
||||
tar -xvf 7z2106-linux-x64.tar.xz
|
||||
- deploy:
|
||||
name: Create a release
|
||||
command: |
|
||||
PACKAGE_VERSION=$(cat package.json | grep version | head -1 | awk -F: '{ print $2 }' | sed 's/[", ]//g')
|
||||
echo "Creating release v${PACKAGE_VERSION}"
|
||||
|
||||
./7zz a "${CIRCLE_PROJECT_REPONAME}_build-sc-core.7z" runner.mjs package.json "service/*" "core/**/*" "core/*" -mx9 -mfb273 -mmt2 -md16M -mqs -myx9
|
||||
./7zz a "${CIRCLE_PROJECT_REPONAME}_app.zip" runner.mjs package.json "service/*" "core/**/*" "core/*" config.json install.bat npminstall.bat README.md uninstall.bat
|
||||
echo "Creating release '${PACKAGE_VERSION}'"
|
||||
github-release upload \
|
||||
--commitish $CIRCLE_SHA1 \
|
||||
--token $GITHUB_TOKEN \
|
||||
--owner $CIRCLE_PROJECT_USERNAME \
|
||||
--repo $CIRCLE_PROJECT_REPONAME \
|
||||
--tag "v${PACKAGE_VERSION}" \
|
||||
--release-name "v${PACKAGE_VERSION}" \
|
||||
--body "Automatic CircleCI Build of v${PACKAGE_VERSION} from ${CIRCLE_SHA1}: ${COMMIT_MESSAGE}" \
|
||||
"${CIRCLE_PROJECT_REPONAME}_build-sc-core.zip" "${CIRCLE_PROJECT_REPONAME}_app.zip"
|
||||
|
||||
workflows:
|
||||
version: 2
|
||||
build_deploy:
|
||||
jobs:
|
||||
- build:
|
||||
context: github-thething
|
100
.gitignore
vendored
100
.gitignore
vendored
|
@ -1,113 +1,13 @@
|
|||
# Logs
|
||||
logs
|
||||
*.log
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
lerna-debug.log*
|
||||
|
||||
# Diagnostic reports (https://nodejs.org/api/report.html)
|
||||
report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
|
||||
|
||||
# Runtime data
|
||||
pids
|
||||
*.pid
|
||||
*.seed
|
||||
*.pid.lock
|
||||
|
||||
# Directory for instrumented libs generated by jscoverage/JSCover
|
||||
lib-cov
|
||||
|
||||
# Coverage directory used by tools like istanbul
|
||||
coverage
|
||||
*.lcov
|
||||
|
||||
# nyc test coverage
|
||||
.nyc_output
|
||||
|
||||
# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
|
||||
.grunt
|
||||
|
||||
# Bower dependency directory (https://bower.io/)
|
||||
bower_components
|
||||
|
||||
# node-waf configuration
|
||||
.lock-wscript
|
||||
|
||||
# Compiled binary addons (https://nodejs.org/api/addons.html)
|
||||
build/Release
|
||||
|
||||
# Dependency directories
|
||||
node_modules/
|
||||
jspm_packages/
|
||||
|
||||
# TypeScript v1 declaration files
|
||||
typings/
|
||||
|
||||
# TypeScript cache
|
||||
*.tsbuildinfo
|
||||
|
||||
# Optional npm cache directory
|
||||
.npm
|
||||
|
||||
# Optional eslint cache
|
||||
.eslintcache
|
||||
|
||||
# Microbundle cache
|
||||
.rpt2_cache/
|
||||
.rts2_cache_cjs/
|
||||
.rts2_cache_es/
|
||||
.rts2_cache_umd/
|
||||
|
||||
# Optional REPL history
|
||||
.node_repl_history
|
||||
|
||||
# Output of 'npm pack'
|
||||
*.tgz
|
||||
|
||||
# Yarn Integrity file
|
||||
.yarn-integrity
|
||||
|
||||
# dotenv environment variables file
|
||||
.env
|
||||
.env.test
|
||||
|
||||
# parcel-bundler cache (https://parceljs.org/)
|
||||
.cache
|
||||
|
||||
# Next.js build output
|
||||
.next
|
||||
|
||||
# Nuxt.js build / generate output
|
||||
.nuxt
|
||||
dist
|
||||
|
||||
# Gatsby files
|
||||
.cache/
|
||||
# Comment in the public line in if your project uses Gatsby and *not* Next.js
|
||||
# https://nextjs.org/blog/next-9-1#public-directory-support
|
||||
# public
|
||||
|
||||
# vuepress build output
|
||||
.vuepress/dist
|
||||
|
||||
# Serverless directories
|
||||
.serverless/
|
||||
|
||||
# FuseBox cache
|
||||
.fusebox/
|
||||
|
||||
# DynamoDB Local files
|
||||
.dynamodb/
|
||||
|
||||
# TernJS port file
|
||||
.tern-port
|
||||
|
||||
# Custom ignore
|
||||
db.json
|
||||
package-lock.json
|
||||
|
||||
daemon
|
||||
app/*
|
||||
manage/*
|
||||
dev/public/main.js
|
||||
|
|
|
@ -1,6 +1,8 @@
|
|||
# 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`.
|
||||
|
||||
|
|
90
appveyor.yml
Normal file
90
appveyor.yml
Normal file
|
@ -0,0 +1,90 @@
|
|||
# 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
Normal file
BIN
bin/7zdec
Normal file
Binary file not shown.
BIN
bin/7zdec.exe
Normal file
BIN
bin/7zdec.exe
Normal file
Binary file not shown.
196
cli.mjs
Normal file
196
cli.mjs
Normal file
|
@ -0,0 +1,196 @@
|
|||
#!/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
11
config.json
|
@ -1,11 +0,0 @@
|
|||
{
|
||||
"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"
|
||||
}
|
497
core/application.mjs
Normal file
497
core/application.mjs
Normal file
|
@ -0,0 +1,497 @@
|
|||
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()
|
||||
}
|
||||
}
|
156
core/client.mjs
156
core/client.mjs
|
@ -1,26 +1,42 @@
|
|||
import http from 'http'
|
||||
import https from 'https'
|
||||
import stream from 'stream/promises'
|
||||
import fs from 'fs'
|
||||
import url from 'url'
|
||||
import Util from './util.mjs'
|
||||
|
||||
export function request(config, path, filePath = null, redirects, returnText = false) {
|
||||
if (!config || typeof(config) === 'string') {
|
||||
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)) {
|
||||
return Promise.reject(new Error('Request must be called with config in first parameter'))
|
||||
}
|
||||
let newRedirects = redirects + 1
|
||||
if (!path || !path.startsWith('http')) {
|
||||
return Promise.reject(new Error('URL was empty or missing http in front'))
|
||||
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 parsed = new url.URL(path)
|
||||
|
||||
let h
|
||||
let h = http
|
||||
if (parsed.protocol === 'https:') {
|
||||
h = https
|
||||
} else {
|
||||
h = http
|
||||
}
|
||||
|
||||
return new Promise(function(resolve, reject) {
|
||||
let req = null
|
||||
|
||||
let orgErr = new Error(); return new Promise(function(resolve, reject) {
|
||||
if (!path) {
|
||||
return reject(new Error('Request path was empty'))
|
||||
}
|
||||
|
@ -28,62 +44,122 @@ export function request(config, path, filePath = null, redirects, returnText = f
|
|||
'User-Agent': 'TheThing/service-core',
|
||||
Accept: 'application/vnd.github.v3+json'
|
||||
}
|
||||
if (config.githubAuthToken && path.indexOf('api.github.com') >= 0) {
|
||||
headers['Authorization'] = `token ${config.githubAuthToken}`
|
||||
if (config.token) {
|
||||
headers['Authorization'] = `token ${config.token}`
|
||||
}
|
||||
let req = h.request({
|
||||
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({
|
||||
path: parsed.pathname + parsed.search,
|
||||
port: parsed.port,
|
||||
method: 'GET',
|
||||
headers: headers,
|
||||
timeout: returnText ? 5000 : 10000,
|
||||
timeout: timeout,
|
||||
hostname: parsed.hostname
|
||||
}, function(res) {
|
||||
if (timedout) { return }
|
||||
clearTimeout(timer)
|
||||
|
||||
const ac = new AbortController()
|
||||
|
||||
let output = ''
|
||||
if (filePath) {
|
||||
let file = fs.createWriteStream(filePath)
|
||||
res.pipe(file)
|
||||
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)
|
||||
} 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) {
|
||||
return reject(new Error(`Too many redirects (last one was ${res.headers.location})`))
|
||||
err = new Error(`Too many redirects (last one was ${res.headers.location})`)
|
||||
}
|
||||
if (!res.headers.location) {
|
||||
return reject(new Error('Redirect returned no path in location header'))
|
||||
else if (!res.headers.location) {
|
||||
err = new Error('Redirect returned no path in location header')
|
||||
}
|
||||
if (res.headers.location.startsWith('http')) {
|
||||
return resolve(request(config, res.headers.location, filePath, newRedirects, returnText))
|
||||
else if (res.headers.location.startsWith('http')) {
|
||||
ac.abort()
|
||||
return resolve(request(config, res.headers.location, filePath, newRedirects, fastRaw))
|
||||
} else {
|
||||
return resolve(request(config, url.resolve(path, res.headers.location), filePath, newRedirects, returnText))
|
||||
ac.abort()
|
||||
return resolve(request(config, resolveRelative(path, res.headers.location), filePath, newRedirects, fastRaw))
|
||||
}
|
||||
} else if (res.statusCode >= 400) {
|
||||
return reject(new Error(`HTTP Error ${res.statusCode}: ${output}`))
|
||||
err = 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,
|
||||
statusMessage: res.statusMessage,
|
||||
headers: res.headers,
|
||||
body: output
|
||||
})
|
||||
}
|
||||
resolve({
|
||||
statusCode: res.statusCode,
|
||||
status: res.statusCode,
|
||||
statusMessage: res.statusMessage,
|
||||
headers: res.headers,
|
||||
body: output
|
||||
})
|
||||
})
|
||||
req.on('error', reject)
|
||||
req.on('timeout', function(err) {
|
||||
reject(err)
|
||||
})
|
||||
})
|
||||
|
||||
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('timeout', function(err) {
|
||||
if (timedout) return
|
||||
reject(Util.combineStack(err, orgErr))
|
||||
})
|
||||
|
||||
req.end()
|
||||
}).then(function(res) {
|
||||
if (!filePath && !returnText) {
|
||||
try {
|
||||
res.body = JSON.parse(res.body)
|
||||
} catch(e) {
|
||||
throw new Error(res.body)
|
||||
if (req) {
|
||||
req.destroy()
|
||||
}
|
||||
if (!filePath && !fastRaw) {
|
||||
if (typeof(res.body) === 'string') {
|
||||
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))
|
||||
}
|
||||
}
|
||||
}
|
||||
return res
|
||||
|
|
650
core/core.mjs
650
core/core.mjs
|
@ -1,499 +1,249 @@
|
|||
import fs from 'fs'
|
||||
import { EventEmitter } from 'events'
|
||||
import { request } from './client.mjs'
|
||||
import HttpServer from './http.mjs'
|
||||
import cluster from 'cluster'
|
||||
|
||||
const fsp = fs.promises
|
||||
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'
|
||||
|
||||
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
|
||||
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
|
||||
this.util = util
|
||||
this.log = log
|
||||
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: '',
|
||||
}
|
||||
|
||||
this.db.set('core.manageActive', null)
|
||||
.set('core.appActive', null)
|
||||
.write().then()
|
||||
this.restart = restart
|
||||
this.applications = []
|
||||
this.applicationMap = new Map()
|
||||
this._applicationFatalCrash = null
|
||||
this.isSlave = cluster.isWorker
|
||||
}
|
||||
|
||||
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.monitoring = setInterval(async () => {
|
||||
if (updating) return
|
||||
updating = true
|
||||
this.log.info('[Scheduler] Starting automatic check for latest version of app and manage')
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
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
|
||||
getApplication(name) {
|
||||
return this.applicationMap.get(name)
|
||||
}
|
||||
|
||||
restart() {
|
||||
this._close()
|
||||
}
|
||||
async init() {
|
||||
this.log.info(`Verifying config`)
|
||||
|
||||
status() {
|
||||
return {
|
||||
app: this.appRunning,
|
||||
manage: this.manageRunning,
|
||||
appUpdating: this._appUpdating.updating,
|
||||
manageUpdating: this._manageUpdating.updating,
|
||||
appStarting: this._appUpdating.starting,
|
||||
manageStarting: this._manageUpdating.starting,
|
||||
}
|
||||
}
|
||||
this.util.verifyConfig(this.db.config)
|
||||
let names = this.util.getAppNames(this.db.config)
|
||||
|
||||
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`)
|
||||
this.log.info(`Found applications: ${names.join(', ')}.`)
|
||||
|
||||
let result = await request(this.config, `https://api.github.com/repos/${this.config[name + 'Repository']}/releases`)
|
||||
let hasCluster = false
|
||||
|
||||
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`)
|
||||
for (let name of names) {
|
||||
if (this.isSlave && process.env.CLUSTER_APP_NAME !== name) {
|
||||
continue
|
||||
}
|
||||
|
||||
await this.db.set(`core.${name}Active`, history[i].name)
|
||||
.write()
|
||||
this.emit('dbupdated', {})
|
||||
try {
|
||||
let provConstructor = Core.providers.get(this.db.config[name].provider)
|
||||
let provider = new provConstructor(this.db.config[name])
|
||||
|
||||
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
|
||||
if (!this.isSlave) {
|
||||
await provider.checkConfig(this.db.config[name])
|
||||
}
|
||||
await this.db.set(`core.${name}Active`, null)
|
||||
.write()
|
||||
this.emit('dbupdated', {})
|
||||
|
||||
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
|
||||
}
|
||||
} 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}`)
|
||||
}
|
||||
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)
|
||||
}
|
||||
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
|
||||
}
|
||||
|
||||
active.starting = false
|
||||
}
|
||||
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
|
||||
}
|
||||
|
||||
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))
|
||||
}
|
||||
if (!targetLog) return
|
||||
|
||||
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 {
|
||||
this.logActive(name, active, `Runner: Loading ${indexPath}\n`)
|
||||
module = await import(indexPath)
|
||||
} catch (err) {
|
||||
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
|
||||
}
|
||||
|
||||
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)
|
||||
|
||||
this.logActive(name, active, `Runner: Starting module\n`)
|
||||
|
||||
try {
|
||||
this.http.setContext(name)
|
||||
this.startModule(module, port)
|
||||
.then(res, rej)
|
||||
} catch (err) {
|
||||
rej(err)
|
||||
if (message.type === 'newlog') {
|
||||
targetLog.emit('newlog', message.payload)
|
||||
}
|
||||
})
|
||||
clearTimeout(checkTimeout)
|
||||
|
||||
await this.checkProgramRunning(name, active, port)
|
||||
process.off('exit', this._activeCrashHandler)
|
||||
} catch (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
|
||||
}
|
||||
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
|
||||
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'))
|
||||
}
|
||||
this.emit('statusupdated', {})
|
||||
|
||||
this.logActive(name, active, `Runner: Module is running successfully\n`)
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
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
|
||||
async run() {
|
||||
if (this.running) return
|
||||
this.running = true
|
||||
|
||||
while (new Date() - start < 10 * 1000) {
|
||||
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 {
|
||||
let check = await request(this.config, `http://localhost:${port}`, null, 0, true)
|
||||
success = true
|
||||
break
|
||||
} catch(err) {
|
||||
this.logActive(name, active, `Checker: ${err.message}, retrying in 3 seconds\n`)
|
||||
error = err
|
||||
await new Promise(function(res) { setTimeout(res, 3000)})
|
||||
}
|
||||
}
|
||||
if (success) return true
|
||||
throw error || new Error('Checking server failed')
|
||||
}
|
||||
|
||||
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')
|
||||
await app.runVersion(process.env.CLUSTER_APP_VERSION)
|
||||
} catch (err) {
|
||||
app.ctx.log.fatal(err)
|
||||
return Promise.reject(err)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
let active = this.getActive(name)
|
||||
let oldLogs = active.logs || ''
|
||||
if (oldLogs) {
|
||||
oldLogs += '\n'
|
||||
}
|
||||
active.logs = ''
|
||||
active.updating = true
|
||||
for (let app of this.applications) {
|
||||
app.startAutoupdater()
|
||||
|
||||
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 {
|
||||
await this.runApplication(app).then(
|
||||
() => {
|
||||
found = true
|
||||
this.logActive(name, active, `Installer: Version ${version.name} already installed\n`)
|
||||
},
|
||||
err => {
|
||||
app.ctx.log.error(err, `Error running: ${err.message}`)
|
||||
}
|
||||
}
|
||||
} 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)
|
||||
)
|
||||
|
||||
app.on('updated', this.runApplication.bind(this, app))
|
||||
}
|
||||
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()
|
||||
|
||||
if (!found) {
|
||||
throw new Error('No stable application was found')
|
||||
}
|
||||
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)
|
||||
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`))
|
||||
}
|
||||
|
||||
await this.installLatestVersion(name)
|
||||
|
||||
if (version !== this.db.get('core.' + name + 'LatestInstalled').value()) {
|
||||
if (!this[name + 'Running'] || this.hasNewVersionAvailable(name)) {
|
||||
await this.tryStartProgram(name)
|
||||
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
|
||||
|
||||
try {
|
||||
application.ctx.log.info(`Attempting to run version ${version.version}`)
|
||||
await application.runVersion(version.version)
|
||||
found = true
|
||||
version.stable = 1
|
||||
await this.db.write()
|
||||
application.ctx.log.info(`${version.version} is up and running`)
|
||||
break
|
||||
} catch(err) {
|
||||
application.ctx.log.error(err, `Error starting ${version.version}: ${err.message}`)
|
||||
process.off('exit', this._applicationFatalCrash)
|
||||
|
||||
if (version.stable < 1) {
|
||||
if (wasFresh) {
|
||||
version.stable = -2
|
||||
} else {
|
||||
version.stable = -1
|
||||
}
|
||||
await this.db.write()
|
||||
if (version.stable === -1) {
|
||||
return this.restart(`Application ${name} version ${version.version} failed to start but application was dirty, check if restarting fixes it`)
|
||||
}
|
||||
} else {
|
||||
await this.db.write()
|
||||
return this.restart(`Application ${name} version ${version.version} previously stable but now failing`)
|
||||
}
|
||||
} finally {
|
||||
process.off('exit', this._applicationFatalCrash)
|
||||
}
|
||||
}
|
||||
|
||||
if (!found) {
|
||||
return Promise.reject(Error(`No stable versions were found`))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Core.addProvider('static', StaticProvider)
|
||||
Core.addProvider('git', GitProvider)
|
||||
|
|
277
core/db.mjs
277
core/db.mjs
|
@ -1,163 +1,142 @@
|
|||
import lowdb from 'lowdb'
|
||||
import FileAsync from 'lowdb/adapters/FileAsync.js'
|
||||
import { setTimeout } from 'timers/promises'
|
||||
import { Low, JSONFile, Memory } from 'lowdb'
|
||||
import fs from 'fs'
|
||||
import { defaults, isObject } from './defaults.mjs'
|
||||
|
||||
let lastId = -1
|
||||
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)
|
||||
|
||||
// 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.id = 'id'
|
||||
db.filename = fullpath
|
||||
db.config = config
|
||||
|
||||
// Copies properties from an object to another
|
||||
__update: function (dest, src) {
|
||||
this.forEach(src, function (value, key) {
|
||||
dest[key] = value
|
||||
})
|
||||
},
|
||||
|
||||
// Removes an item from an array
|
||||
__remove: function (array, item) {
|
||||
var index = this.indexOf(array, item)
|
||||
if (index !== -1) array.splice(index, 1)
|
||||
},
|
||||
|
||||
__id: function () {
|
||||
var id = this.id || 'id'
|
||||
return id
|
||||
},
|
||||
|
||||
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
|
||||
db.createId = function(collection) {
|
||||
if (collection.length) {
|
||||
return (collection[collection.length - 1].id || 0) + 1
|
||||
}
|
||||
lastId = next
|
||||
return next
|
||||
},
|
||||
return 1
|
||||
}
|
||||
|
||||
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
|
||||
},
|
||||
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
|
||||
}
|
||||
|
||||
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)
|
||||
db.getCollection = function(collection) {
|
||||
if (typeof(collection) === 'string') {
|
||||
return db.data[collection]
|
||||
}
|
||||
return collection
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
} else {
|
||||
// create id and push new object
|
||||
doc[this.__id()] = this.createId(collection, doc)
|
||||
collection.push(doc)
|
||||
item[db.id] = db.createId(col)
|
||||
}
|
||||
|
||||
return doc
|
||||
},
|
||||
|
||||
updateById: function (collection, id, attrs) {
|
||||
var doc = this.getById(collection, id)
|
||||
|
||||
if (doc) {
|
||||
this.assign(doc, attrs, {id: doc.id})
|
||||
}
|
||||
|
||||
return doc
|
||||
},
|
||||
|
||||
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})
|
||||
}
|
||||
|
||||
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)
|
||||
})
|
||||
|
||||
return docs
|
||||
col.push(item)
|
||||
}
|
||||
}
|
||||
|
||||
export default function GetDB(util, log) {
|
||||
const adapter = new FileAsync(util.getPathFromRoot('./db.json'))
|
||||
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 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
|
||||
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
|
||||
}
|
||||
|
||||
db.addApplication = function(name) {
|
||||
db.data.core[name] ||= {}
|
||||
defaults(db.data.core[name], {
|
||||
active: '',
|
||||
latestInstalled: '',
|
||||
updater: '',
|
||||
versions: [],
|
||||
})
|
||||
}
|
||||
|
||||
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')
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
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 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
|
||||
}
|
||||
)
|
||||
}
|
||||
|
|
25
core/defaults.mjs
Normal file
25
core/defaults.mjs
Normal file
|
@ -0,0 +1,25 @@
|
|||
|
||||
// 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
|
||||
}
|
|
@ -1,73 +1,62 @@
|
|||
import http from 'http'
|
||||
import https from 'https'
|
||||
|
||||
export default class HttpServer {
|
||||
constructor(config) {
|
||||
this.active = {
|
||||
app: false,
|
||||
manage: false,
|
||||
dev: false,
|
||||
this.ishttps = false
|
||||
this.active = null
|
||||
this.sockets = new Set()
|
||||
this.creator = http
|
||||
if (config && config.https) {
|
||||
this.creator = https
|
||||
this.ishttps = true
|
||||
}
|
||||
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) {
|
||||
return this._createServer(this._context, opts, listener)
|
||||
}
|
||||
|
||||
_createServer(name, opts, listener) {
|
||||
let server = http.createServer(opts, listener)
|
||||
let server = this.creator.createServer(opts, listener)
|
||||
|
||||
server.on('connection', (socket) => {
|
||||
this.sockets[name].add(socket)
|
||||
this.sockets.add(socket)
|
||||
|
||||
socket.once('close', () => {
|
||||
this.sockets[name].delete(socket)
|
||||
this.sockets.delete(socket)
|
||||
})
|
||||
})
|
||||
|
||||
this.active[name] = server
|
||||
server.listenAsync = (port, host) => {
|
||||
return new Promise((res, rej) => {
|
||||
server.once('error', rej)
|
||||
|
||||
server.listen(port, host || '::', () => {
|
||||
server.off('error', rej)
|
||||
res()
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
this.active = server
|
||||
return server
|
||||
}
|
||||
|
||||
getServer(name) {
|
||||
return this.active[name]
|
||||
}
|
||||
closeServer() {
|
||||
if (!this.active) return Promise.resolve()
|
||||
|
||||
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[name].clear()
|
||||
|
||||
this.active[name].close(function(err) {
|
||||
if (err) return rej(err)
|
||||
|
||||
// Waiting 1 second for it to close down
|
||||
setTimeout(function() { res(true) }, 1000)
|
||||
})
|
||||
return new Promise((res, rej) => {
|
||||
this.sockets.forEach(function(socket) {
|
||||
socket.destroy()
|
||||
})
|
||||
} catch (err) {
|
||||
throw new Error(`Error closing ${name}: ${err.message}`)
|
||||
}
|
||||
}
|
||||
this.sockets.clear()
|
||||
|
||||
getCurrentServer() {
|
||||
return this.active[this._context]
|
||||
this.active.close(err => {
|
||||
if (err) {
|
||||
if (err.code !== 'ERR_SERVER_NOT_RUNNING') return rej(err)
|
||||
}
|
||||
this.active = null
|
||||
|
||||
// Waiting 1 second for it to close down
|
||||
setTimeout(function() {res() }, 100)
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
62
core/lib.mjs
Normal file
62
core/lib.mjs
Normal file
|
@ -0,0 +1,62 @@
|
|||
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')
|
||||
}
|
||||
}
|
122
core/log.mjs
122
core/log.mjs
|
@ -1,29 +1,40 @@
|
|||
import nodewindows from 'node-windows'
|
||||
// import nodewindows from 'node-windows'
|
||||
import cluster from 'cluster'
|
||||
import bunyan from 'bunyan-lite'
|
||||
import { setTimeout } from 'timers/promises'
|
||||
|
||||
export default function getLog(name) {
|
||||
export function getDefaultStreams() {
|
||||
if (process.env.NODE_ENV === 'production') {
|
||||
return [{
|
||||
path: 'log.log',
|
||||
level: 'info',
|
||||
}
|
||||
]
|
||||
} else {
|
||||
return [{
|
||||
"stream": process.stdout,
|
||||
"level": "debug"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
export default function getLog(name, streams = null, opts = {}) {
|
||||
let settings
|
||||
let ringbuffer = new bunyan.RingBuffer({ limit: 100 })
|
||||
let ringbufferwarn = new bunyan.RingBuffer({ limit: 100 })
|
||||
|
||||
if (process.env.NODE_ENV === 'production') {
|
||||
settings = {
|
||||
"name": "service-core",
|
||||
"streams": [{
|
||||
path: 'log.log',
|
||||
level: 'info',
|
||||
}
|
||||
]
|
||||
}
|
||||
} else {
|
||||
settings = {
|
||||
"name": "service-core",
|
||||
"streams": [{
|
||||
"stream": process.stdout,
|
||||
"level": "debug"
|
||||
}
|
||||
]
|
||||
}
|
||||
if (streams) {
|
||||
streams.forEach(function(stream) {
|
||||
if (stream.stream === 'process.stdout') {
|
||||
stream.stream = process.stdout
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
settings = {
|
||||
name: name,
|
||||
streams: streams || getDefaultStreams()
|
||||
}
|
||||
|
||||
let logger
|
||||
|
@ -44,6 +55,14 @@ export default function getLog(name) {
|
|||
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() {},
|
||||
|
@ -53,18 +72,73 @@ export default function getLog(name) {
|
|||
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 = new nodewindows.EventLogger(name)
|
||||
logger.event = {
|
||||
info: safeLoadEvent.bind(this, 'info'),
|
||||
warn: safeLoadEvent.bind(this, 'warn'),
|
||||
error: safeLoadEvent.bind(this, 'error'),
|
||||
}
|
||||
} else {
|
||||
logger.event = {
|
||||
info: function() {},
|
||||
warn: function() {},
|
||||
error: function() {},
|
||||
info: function() { return Promise.resolve() },
|
||||
warn: function() { return Promise.resolve() },
|
||||
error: function() { return Promise.resolve() },
|
||||
}
|
||||
}
|
||||
logger.on('error', function(err) {
|
||||
if (process.env.NODE_ENV === 'production') {
|
||||
logger.event.warn(`Error in logger ${name}: ${err.message}`)
|
||||
} else {
|
||||
console.log(`Error in logger ${name}: ${err.message}`)
|
||||
}
|
||||
})
|
||||
logger.ringbuffer = ringbuffer
|
||||
logger.ringbufferwarn = ringbufferwarn
|
||||
|
||||
|
|
60
core/providers/git.mjs
Normal file
60
core/providers/git.mjs
Normal file
|
@ -0,0 +1,60 @@
|
|||
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)) }
|
||||
}
|
||||
}
|
24
core/providers/static.mjs
Normal file
24
core/providers/static.mjs
Normal file
|
@ -0,0 +1,24 @@
|
|||
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()
|
||||
}
|
||||
}
|
47
core/runner.mjs
Normal file
47
core/runner.mjs
Normal file
|
@ -0,0 +1,47 @@
|
|||
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')
|
183
core/util.mjs
183
core/util.mjs
|
@ -1,8 +1,15 @@
|
|||
import os from 'os'
|
||||
import path from 'path'
|
||||
import { spawn } from 'child_process'
|
||||
import fs from 'fs/promises'
|
||||
import { spawn, execSync } from 'child_process'
|
||||
import { fileURLToPath, pathToFileURL } from 'url'
|
||||
|
||||
export default class Util {
|
||||
static combineStack(err, appendErr) {
|
||||
err.stack = err.stack + '\nFrom:\n' + appendErr.stack.split('\n').slice(1).join('\n') + '\n'
|
||||
return err
|
||||
}
|
||||
|
||||
constructor(root_import_meta_url) {
|
||||
this._root_import_meta_url = root_import_meta_url
|
||||
}
|
||||
|
@ -16,33 +23,193 @@ 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) {
|
||||
stream(`[Command] ${folder ? folder : ''}${command} ${options.join(' ')}\n`)
|
||||
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 = setTimeout(function() {
|
||||
processor.stdin.write('n\n')
|
||||
let timeOuter = setInterval(function() {
|
||||
try {
|
||||
processor.stdin.write('n\n')
|
||||
} catch {}
|
||||
}, 250)
|
||||
processor.stdout.on('data', function(data) {
|
||||
stream(data.toString())
|
||||
stream(data.toString().replace(/\r\n/g, '\n'))
|
||||
})
|
||||
processor.stderr.on('data', function(data) {
|
||||
stream(data.toString())
|
||||
stream(data.toString().replace(/\r\n/g, '\n'))
|
||||
})
|
||||
processor.stdin.on('error', function() {
|
||||
clearInterval(timeOuter)
|
||||
})
|
||||
processor.on('error', function(err) {
|
||||
clearInterval(timeOuter)
|
||||
rej(err)
|
||||
baseError.message = err.message
|
||||
rej(baseError)
|
||||
})
|
||||
processor.on('exit', function (code) {
|
||||
clearInterval(timeOuter)
|
||||
if (code !== 0) {
|
||||
return rej(new Error('Program returned error code: ' + code))
|
||||
baseError.message = 'Program returned error code: ' + code
|
||||
return rej(baseError)
|
||||
}
|
||||
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
|
||||
}
|
||||
}
|
||||
|
|
24
exampleconfig.json
Normal file
24
exampleconfig.json
Normal file
|
@ -0,0 +1,24 @@
|
|||
{
|
||||
"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"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
21
index.mjs
Normal file
21
index.mjs
Normal file
|
@ -0,0 +1,21 @@
|
|||
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,
|
||||
}
|
|
@ -1,2 +0,0 @@
|
|||
node service\install.mjs
|
||||
PAUSE
|
52
lib.mjs
52
lib.mjs
|
@ -1,52 +0,0 @@
|
|||
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)
|
||||
}
|
||||
}
|
|
@ -1,2 +0,0 @@
|
|||
npm install
|
||||
PAUSE
|
45
package.json
45
package.json
|
@ -1,33 +1,56 @@
|
|||
{
|
||||
"name": "service-core",
|
||||
"version": "2.0.2",
|
||||
"version": "3.0.2",
|
||||
"description": "Core boiler plate code to install node server as windows service",
|
||||
"main": "lib.mjs",
|
||||
"main": "index.mjs",
|
||||
"scripts": {
|
||||
"dev": "nodemon --watch dev/api --watch core --watch runner.mjs --watch db.mjs --watch log.mjs runner.mjs | bunyan",
|
||||
"test": "echo \"Error: no test specified\" && exit 1"
|
||||
"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"
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git+https://github.com/TheThing/service-core.git"
|
||||
"url": "git+https://git.nfp.is/TheThing/service-core.git"
|
||||
},
|
||||
"author": "Jonatan Nilsson",
|
||||
"license": "WTFPL",
|
||||
"bugs": {
|
||||
"url": "https://github.com/TheThing/service-core/issues"
|
||||
"url": "https://git.nfp.is/TheThing/service-core/issues"
|
||||
},
|
||||
"homepage": "https://github.com/TheThing/service-core#readme",
|
||||
"homepage": "https://git.nfp.is/TheThing/service-core#readme",
|
||||
"files": [
|
||||
"lib.mjs",
|
||||
"index.mjs",
|
||||
"cli.mjs",
|
||||
"package.json",
|
||||
"README.md",
|
||||
"core"
|
||||
"core",
|
||||
"bin"
|
||||
],
|
||||
"dependencies": {
|
||||
"bunyan-lite": "^1.0.1",
|
||||
"lodash": "^4.17.20",
|
||||
"lowdb": "^1.0.0"
|
||||
"bunyan-lite": "^1.2.0",
|
||||
"lowdb": "^3.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"eltro": "^1.3.1"
|
||||
}
|
||||
}
|
||||
|
|
69
runner.mjs
69
runner.mjs
|
@ -1,69 +0,0 @@
|
|||
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)
|
||||
})
|
||||
})
|
|
@ -1,11 +0,0 @@
|
|||
import svc from './service.mjs'
|
||||
|
||||
svc.on('install',function(){
|
||||
svc.start();
|
||||
});
|
||||
|
||||
svc.on('alreadyinstalled',function(){
|
||||
svc.start();
|
||||
});
|
||||
|
||||
svc.install();
|
|
@ -1,35 +0,0 @@
|
|||
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
|
|
@ -1,10 +0,0 @@
|
|||
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();
|
122
test/application.cluster.test.mjs
Normal file
122
test/application.cluster.test.mjs
Normal file
|
@ -0,0 +1,122 @@
|
|||
import { Eltro as t, assert, stub } from 'eltro'
|
||||
import fs from 'fs/promises'
|
||||
import Application from '../core/application.mjs'
|
||||
import Util from '../core/util.mjs'
|
||||
import lowdb from '../core/db.mjs'
|
||||
import StaticProvider from '../core/providers/static.mjs'
|
||||
import { createFakeContext } from './helpers.mjs'
|
||||
import HttpServer from '../core/http.mjs'
|
||||
|
||||
const util = new Util(import.meta.url)
|
||||
|
||||
|
||||
t.describe('#runVersion("version") cluster mode', function() {
|
||||
const assertPort = 22345
|
||||
const assertClusterSize = Math.floor(Math.random() * (8 - 2 + 1) + 2)
|
||||
const http = new HttpServer()
|
||||
let handler
|
||||
let cluster
|
||||
let workers
|
||||
let ctx
|
||||
let app
|
||||
|
||||
t.beforeEach(function() {
|
||||
return createFakeContext({ testnoexisting: { cluster: assertClusterSize } }, util, null)
|
||||
.then(function(res) {
|
||||
workers = []
|
||||
ctx = res
|
||||
let provider = new StaticProvider()
|
||||
cluster = {
|
||||
on: stub(),
|
||||
off: stub(),
|
||||
fork: stub().returnWith(function() {
|
||||
let worker = {
|
||||
on: stub(),
|
||||
once: stub(),
|
||||
process: {
|
||||
kill: stub(),
|
||||
}
|
||||
}
|
||||
workers.push(worker)
|
||||
return worker
|
||||
}),
|
||||
isWorker: false,
|
||||
}
|
||||
app = new Application(ctx, provider, 'testnoexisting', { cluster })
|
||||
app.config.port = assertPort
|
||||
app.workerDied = stub()
|
||||
return app.ctx.db.write()
|
||||
})
|
||||
})
|
||||
|
||||
t.afterEach(function() {
|
||||
return Promise.all([
|
||||
app.closeServer(),
|
||||
])
|
||||
})
|
||||
|
||||
t.before(function() {
|
||||
handler = function(req, res) {
|
||||
res.writeHead(200)
|
||||
res.end('{}')
|
||||
}
|
||||
let server = http.createServer(function(req, res) {
|
||||
req.on('error', function() { })
|
||||
res.on('error', function() { })
|
||||
res.on('finish', function() { })
|
||||
handler(req, res)
|
||||
})
|
||||
|
||||
return server.listenAsync(assertPort)
|
||||
})
|
||||
|
||||
t.after(function() {
|
||||
return http.closeServer()
|
||||
})
|
||||
|
||||
t.test('should call fork for entire cluster and attach handlers correctly', async function() {
|
||||
let lastErr = null
|
||||
for (let i = 1; i <= assertClusterSize; i++) {
|
||||
assert.strictEqual(app.workers[i], null)
|
||||
}
|
||||
handler = function(req, res) {
|
||||
try {
|
||||
assert.notOk(cluster.on.called)
|
||||
assert.notOk(app.__clusterWorkerDied)
|
||||
lastErr = false
|
||||
} catch (err) {
|
||||
lastErr = err
|
||||
}
|
||||
res.writeHead(200)
|
||||
res.end('{}')
|
||||
}
|
||||
const assertVersion = 'v11.22.33'
|
||||
|
||||
await app.runVersion(assertVersion)
|
||||
|
||||
assert.strictEqual(cluster.fork.callCount, assertClusterSize)
|
||||
|
||||
for (let i = 1; i <= assertClusterSize; i++) {
|
||||
assert.ok(app.workers[i])
|
||||
assert.strictEqual(app.workers[i], workers[i - 1])
|
||||
assert.strictEqual(cluster.fork.getCallN(i)[0].CLUSTER_APP_NAME, app.name)
|
||||
assert.strictEqual(cluster.fork.getCallN(i)[0].CLUSTER_APP_VERSION, assertVersion)
|
||||
assert.strictEqual(app.workers[i].w_id, i)
|
||||
assert.strictEqual(app.workers[i].listening, false)
|
||||
assert.ok(app.workers[i].started)
|
||||
assert.ok(new Date() - app.workers[i].started < 100 && new Date() - app.workers[i].started >= 0)
|
||||
}
|
||||
|
||||
assert.strictEqual(lastErr, false)
|
||||
assert.strictEqual(cluster.on.callCount, 1)
|
||||
assert.ok(app.__clusterWorkerDied)
|
||||
assert.strictEqual(cluster.on.firstCall[0], 'exit')
|
||||
assert.strictEqual(cluster.on.firstCall[1], app.__clusterWorkerDied)
|
||||
assert.notStrictEqual(app.workerDied, app.__clusterWorkerDied)
|
||||
assert.notOk(app.workerDied.called)
|
||||
|
||||
app.__clusterWorkerDied()
|
||||
|
||||
assert.ok(app.workerDied.called)
|
||||
})
|
||||
})
|
314
test/application.run.test.mjs
Normal file
314
test/application.run.test.mjs
Normal file
|
@ -0,0 +1,314 @@
|
|||
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)
|
||||
})
|
||||
})
|
51
test/application.test.integration.mjs
Normal file
51
test/application.test.integration.mjs
Normal file
|
@ -0,0 +1,51 @@
|
|||
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`))
|
||||
})
|
1109
test/application.test.mjs
Normal file
1109
test/application.test.mjs
Normal file
File diff suppressed because it is too large
Load diff
284
test/client.test.mjs
Normal file
284
test/client.test.mjs
Normal file
|
@ -0,0 +1,284 @@
|
|||
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)
|
||||
})
|
236
test/core.cluster.test.mjs
Normal file
236
test/core.cluster.test.mjs
Normal file
|
@ -0,0 +1,236 @@
|
|||
import { Eltro as t, assert, stub } from 'eltro'
|
||||
import fs from 'fs/promises'
|
||||
import Core from '../core/core.mjs'
|
||||
import Util from '../core/util.mjs'
|
||||
import { createFakeLog } from './helpers.mjs'
|
||||
import StaticProvider from '../core/providers/static.mjs'
|
||||
import lowdb from '../core/db.mjs'
|
||||
|
||||
const util = new Util(import.meta.url)
|
||||
const log = createFakeLog()
|
||||
let db
|
||||
|
||||
t.before(function() {
|
||||
return lowdb({}, log, null).then(function(res) {
|
||||
db = res
|
||||
})
|
||||
})
|
||||
|
||||
t.describe('#init()', function() {
|
||||
const assertProviderName = 'Kyousuu Gakku Gogyou Kikan'
|
||||
let core
|
||||
let fakeUtil
|
||||
let fakeProvider
|
||||
let fakeProviderConfig
|
||||
|
||||
function FakeProvider(config) {
|
||||
fakeProvider(config)
|
||||
this.static = true
|
||||
this.checkConfig = fakeProviderConfig
|
||||
}
|
||||
|
||||
t.beforeEach(function() {
|
||||
log.error.reset()
|
||||
core = new Core(db, util, log)
|
||||
core.util = fakeUtil = {
|
||||
verifyConfig: stub(),
|
||||
getAppNames: stub().returns([]),
|
||||
}
|
||||
fakeProvider = stub()
|
||||
fakeProviderConfig = stub()
|
||||
Core.providers.set(assertProviderName, FakeProvider)
|
||||
core.isSlave = true
|
||||
})
|
||||
|
||||
t.after(function() {
|
||||
return Promise.all([
|
||||
fs.rm('./log_test_1.log', { recursive: true, force: true }).catch(function() {}),
|
||||
])
|
||||
})
|
||||
|
||||
t.test('should not call provider checkConfig if isSlave', async function() {
|
||||
const assertAppName = 'Zetsubou'
|
||||
const assertConfig = {
|
||||
[assertAppName]: {
|
||||
provider: assertProviderName,
|
||||
}
|
||||
}
|
||||
db.config = assertConfig
|
||||
process.env.CLUSTER_APP_NAME = assertAppName
|
||||
|
||||
const assertError = new Error('Shousou')
|
||||
fakeProviderConfig.rejects(assertError)
|
||||
fakeUtil.getAppNames.returns([assertAppName])
|
||||
|
||||
await core.init()
|
||||
assert.notOk(fakeProviderConfig.called)
|
||||
})
|
||||
|
||||
t.test('should only create an application with the environment app name', async function() {
|
||||
const assertAppName = 'Yasashii Ketsumatsu'
|
||||
const assertAppSecondaryName = 'Big Time'
|
||||
const assertTestString = 'Serozore no Omoi'
|
||||
const assertConfig = {
|
||||
[assertAppName]: {
|
||||
provider: assertProviderName,
|
||||
teststring: assertTestString,
|
||||
},
|
||||
[assertAppSecondaryName]: {
|
||||
provider: assertProviderName,
|
||||
teststring: assertTestString,
|
||||
},
|
||||
}
|
||||
db.config = assertConfig
|
||||
process.env.CLUSTER_APP_NAME = assertAppName
|
||||
|
||||
fakeUtil.getAppNames.returns([assertAppSecondaryName, assertAppName])
|
||||
assert.strictEqual(core.applications.length, 0)
|
||||
|
||||
await core.init()
|
||||
|
||||
assert.strictEqual(core.applications.length, 1)
|
||||
assert.ok(core.getApplication(assertAppName))
|
||||
assert.notOk(core.getApplication(assertAppSecondaryName))
|
||||
|
||||
let application = core.getApplication(assertAppName)
|
||||
assert.strictEqual(core.applications[0], application)
|
||||
assert.strictEqual(application.name, assertAppName)
|
||||
assert.strictEqual(application.ctx.db, core.db)
|
||||
assert.strictEqual(application.ctx.util, core.util)
|
||||
assert.notStrictEqual(application.ctx.log, core.log)
|
||||
assert.strictEqual(application.ctx.core, core)
|
||||
assert.strictEqual(application.config.teststring, assertTestString)
|
||||
assert.ok(application.fresh)
|
||||
assert.ok(application.provider instanceof FakeProvider)
|
||||
})
|
||||
})
|
||||
|
||||
t.describe('#run()', function() {
|
||||
let core
|
||||
let testAppOneName
|
||||
let testAppTwoName
|
||||
let stubRunApplication
|
||||
|
||||
t.beforeEach(function() {
|
||||
testAppOneName = 'Tenshi'
|
||||
testAppTwoName = 'no CLOVER'
|
||||
db.data.core = {
|
||||
[testAppOneName]: {
|
||||
versions: []
|
||||
},
|
||||
[testAppTwoName]: {
|
||||
versions: []
|
||||
},
|
||||
}
|
||||
core = new Core(db, util, log)
|
||||
core.runApplication = stubRunApplication = stub().resolves()
|
||||
core.isSlave = true
|
||||
db.write = stub().resolves()
|
||||
log.info.reset()
|
||||
log.warn.reset()
|
||||
log.error.reset()
|
||||
for (let name of [testAppOneName, testAppTwoName]) {
|
||||
let onOrOnce = stub()
|
||||
let app = {
|
||||
name: name,
|
||||
fresh: false,
|
||||
on: onOrOnce,
|
||||
once: onOrOnce,
|
||||
ctx: {
|
||||
log: {
|
||||
info: stub(),
|
||||
warn: stub(),
|
||||
error: stub(),
|
||||
fatal: stub(),
|
||||
},
|
||||
},
|
||||
runVersion: stub().resolves(),
|
||||
update: stub().resolves(),
|
||||
startAutoupdater: stub(),
|
||||
}
|
||||
core.applicationMap.set(name, app)
|
||||
core.applications.push(app)
|
||||
}
|
||||
process.env.CLUSTER_APP_NAME = testAppTwoName
|
||||
})
|
||||
|
||||
t.test('should not call update', async function() {
|
||||
const assertFirstError = new Error('Manatsu')
|
||||
const assertSecondError = new Error('no Photograph')
|
||||
core.applicationMap.get(testAppOneName).update.rejects(assertFirstError)
|
||||
core.applicationMap.get(testAppTwoName).update.rejects(assertSecondError)
|
||||
|
||||
await core.run()
|
||||
|
||||
assert.notOk(core.applications[0].update.called)
|
||||
assert.notOk(core.applications[1].update.called)
|
||||
})
|
||||
|
||||
|
||||
t.test('app.on updated should not be hooked', async function() {
|
||||
assert.notOk(core.applicationMap.get(testAppOneName).once.called)
|
||||
assert.notOk(core.applicationMap.get(testAppTwoName).once.called)
|
||||
assert.notOk(core.applicationMap.get(testAppOneName).on.called)
|
||||
assert.notOk(core.applicationMap.get(testAppTwoName).on.called)
|
||||
|
||||
core.runApplication.returnWith(function(app) {
|
||||
assert.notOk(app.once.called)
|
||||
assert.notOk(app.on.called)
|
||||
return Promise.resolve()
|
||||
})
|
||||
|
||||
await core.run()
|
||||
|
||||
assert.notOk(core.applicationMap.get(testAppOneName).once.called)
|
||||
assert.notOk(core.applicationMap.get(testAppTwoName).once.called)
|
||||
assert.notOk(core.applicationMap.get(testAppOneName).on.called)
|
||||
assert.notOk(core.applicationMap.get(testAppTwoName).on.called)
|
||||
})
|
||||
|
||||
t.test('should not call startAutoupdater', async function() {
|
||||
stubRunApplication.rejects(new Error('not seen'))
|
||||
assert.notOk(core.applications[0].startAutoupdater.called)
|
||||
assert.notOk(core.applications[1].startAutoupdater.called)
|
||||
|
||||
await core.run()
|
||||
|
||||
assert.notOk(core.applications[0].startAutoupdater.called)
|
||||
assert.notOk(core.applications[1].startAutoupdater.called)
|
||||
})
|
||||
|
||||
t.test('should be safe to call multiple times', async function() {
|
||||
await core.run()
|
||||
|
||||
assert.strictEqual(core.applicationMap.get(testAppOneName).runVersion.callCount, 0)
|
||||
assert.strictEqual(core.applicationMap.get(testAppTwoName).runVersion.callCount, 1)
|
||||
|
||||
await core.run()
|
||||
await core.run()
|
||||
await core.run()
|
||||
await core.run()
|
||||
|
||||
assert.strictEqual(core.applicationMap.get(testAppOneName).runVersion.callCount, 0)
|
||||
assert.strictEqual(core.applicationMap.get(testAppTwoName).runVersion.callCount, 1)
|
||||
})
|
||||
|
||||
t.test('should call runVersion on selected application', async function() {
|
||||
const assertFirstError = new Error('Manatsu')
|
||||
const assertSecondError = new Error('no Photograph')
|
||||
const assertVersion = 'v11.22.33'
|
||||
core.applicationMap.get(testAppOneName).runVersion.rejects(assertFirstError)
|
||||
core.applicationMap.get(testAppTwoName).runVersion.rejects(assertSecondError)
|
||||
|
||||
process.env.CLUSTER_APP_NAME = testAppTwoName
|
||||
process.env.CLUSTER_APP_VERSION = assertVersion
|
||||
|
||||
let err = await assert.isRejected(core.run())
|
||||
|
||||
let app = core.applicationMap.get(testAppTwoName)
|
||||
assert.strictEqual(core.applicationMap.get(testAppOneName).runVersion.callCount, 0)
|
||||
assert.strictEqual(app.runVersion.callCount, 1)
|
||||
assert.strictEqual(app.runVersion.firstCall[0], assertVersion)
|
||||
assert.strictEqual(app.ctx.log.fatal.callCount, 1)
|
||||
assert.strictEqual(app.ctx.log.fatal.firstCall[0], assertSecondError)
|
||||
assert.strictEqual(err, assertSecondError)
|
||||
})
|
||||
})
|
591
test/core.test.integration.mjs
Normal file
591
test/core.test.integration.mjs
Normal file
|
@ -0,0 +1,591 @@
|
|||
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
|
||||
})
|
||||
})
|
||||
})
|
1244
test/core.test.mjs
Normal file
1244
test/core.test.mjs
Normal file
File diff suppressed because it is too large
Load diff
334
test/db.test.mjs
Normal file
334
test/db.test.mjs
Normal file
|
@ -0,0 +1,334 @@
|
|||
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')
|
||||
})
|
64
test/defaults.test.mjs
Normal file
64
test/defaults.test.mjs
Normal file
|
@ -0,0 +1,64 @@
|
|||
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)
|
||||
})
|
||||
})
|
17
test/exampleindex.mjs
Normal file
17
test/exampleindex.mjs
Normal file
|
@ -0,0 +1,17 @@
|
|||
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()
|
||||
})
|
||||
})
|
||||
}
|
92
test/helpers.mjs
Normal file
92
test/helpers.mjs
Normal file
|
@ -0,0 +1,92 @@
|
|||
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)
|
||||
}
|
167
test/http.test.mjs
Normal file
167
test/http.test.mjs
Normal file
|
@ -0,0 +1,167 @@
|
|||
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)
|
||||
})
|
||||
})
|
86
test/lib.test.mjs
Normal file
86
test/lib.test.mjs
Normal file
|
@ -0,0 +1,86 @@
|
|||
import { Eltro as t, assert, stub } from 'eltro'
|
||||
import * as sc from '../index.mjs'
|
||||
|
||||
t.describe('', function() {
|
||||
const module = {
|
||||
start: stub()
|
||||
}
|
||||
|
||||
t.beforeEach(function() {
|
||||
module.start.reset()
|
||||
})
|
||||
|
||||
t.test('should have ServiceCore defined', function() {
|
||||
assert.ok(sc.ServiceCore)
|
||||
})
|
||||
|
||||
t.test('constructor should work', function() {
|
||||
const assertAppName = 'Gondola'
|
||||
let core = new sc.ServiceCore(assertAppName, import.meta.url)
|
||||
assert.strictEqual(core.util._root_import_meta_url, import.meta.url)
|
||||
assert.strictEqual(core.name, assertAppName)
|
||||
})
|
||||
|
||||
t.test('should support proper init', async function() {
|
||||
const assertAppName = 'Hero Combat'
|
||||
let core = new sc.ServiceCore(assertAppName, import.meta.url)
|
||||
await core.init(module)
|
||||
|
||||
assert.strictEqual(core.core.applications.length, 1)
|
||||
assert.strictEqual(core.core.applications[0], core.app)
|
||||
assert.strictEqual(core.core.applicationMap.size, 1)
|
||||
assert.strictEqual(core.core.applicationMap.get(assertAppName), core.app)
|
||||
assert.strictEqual(core.app.name, assertAppName)
|
||||
assert.strictEqual(core.app.module, module)
|
||||
})
|
||||
|
||||
t.test('should call module start', async function() {
|
||||
const assertError = new Error('Inbo')
|
||||
module.start.rejects(assertError)
|
||||
|
||||
let core = new sc.ServiceCore('testapp', import.meta.url)
|
||||
await core.init(module)
|
||||
let err = await assert.isRejected(core.run())
|
||||
assert.strictEqual(err, assertError)
|
||||
assert.strictEqual(module.start.firstCall[0], core.app.http)
|
||||
assert.strictEqual(module.start.firstCall[1], 4000)
|
||||
assert.strictEqual(module.start.firstCall[2], core.app.ctx)
|
||||
})
|
||||
|
||||
t.test('should support overwriting port', async function() {
|
||||
const assertError = new Error('Inbo')
|
||||
const assertPort = 9382
|
||||
module.start.rejects(assertError)
|
||||
|
||||
let core = new sc.ServiceCore('testapp', import.meta.url, assertPort)
|
||||
|
||||
await core.init(module)
|
||||
|
||||
let err = await assert.isRejected(core.run())
|
||||
assert.strictEqual(err, assertError)
|
||||
assert.strictEqual(module.start.firstCall[0], core.app.http)
|
||||
assert.strictEqual(module.start.firstCall[1], assertPort)
|
||||
assert.strictEqual(module.start.firstCall[2], core.app.ctx)
|
||||
})
|
||||
|
||||
t.test('should support overwriting config', async function() {
|
||||
const assertError = new Error('Inbo')
|
||||
const assertPort = 9382
|
||||
module.start.rejects(assertError)
|
||||
|
||||
let core = new sc.ServiceCore('testapp', import.meta.url)
|
||||
|
||||
core.setConfig({
|
||||
port: assertPort
|
||||
})
|
||||
await core.init(module)
|
||||
|
||||
let err = await assert.isRejected(core.run())
|
||||
assert.strictEqual(err, assertError)
|
||||
assert.strictEqual(core.config['testapp'].port, assertPort)
|
||||
assert.strictEqual(core.config['testapp'].provider, 'static')
|
||||
assert.strictEqual(module.start.firstCall[0], core.app.http)
|
||||
assert.strictEqual(module.start.firstCall[1], assertPort)
|
||||
assert.strictEqual(module.start.firstCall[2], core.app.ctx)
|
||||
})
|
||||
})
|
294
test/log.test.mjs
Normal file
294
test/log.test.mjs
Normal file
|
@ -0,0 +1,294 @@
|
|||
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')
|
||||
})
|
||||
})
|
96
test/providers/git.test.integration.mjs
Normal file
96
test/providers/git.test.integration.mjs
Normal file
|
@ -0,0 +1,96 @@
|
|||
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'))
|
||||
})
|
||||
})
|
347
test/providers/git.test.mjs
Normal file
347
test/providers/git.test.mjs
Normal file
|
@ -0,0 +1,347 @@
|
|||
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)
|
||||
})
|
||||
})
|
30
test/providers/static.test.mjs
Normal file
30
test/providers/static.test.mjs
Normal file
|
@ -0,0 +1,30 @@
|
|||
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()
|
||||
})
|
||||
})
|
28
test/runner.mjs
Normal file
28
test/runner.mjs
Normal file
|
@ -0,0 +1,28 @@
|
|||
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)
|
||||
}
|
||||
)
|
49
test/runner_cluster.mjs
Normal file
49
test/runner_cluster.mjs
Normal file
|
@ -0,0 +1,49 @@
|
|||
import fs from 'fs'
|
||||
import cluster from 'cluster'
|
||||
import { runner } from '../core/runner.mjs'
|
||||
|
||||
if (cluster.isPrimary) {
|
||||
try {
|
||||
fs.rmSync(util.getPathFromRoot('./db.json'))
|
||||
} catch {}
|
||||
}
|
||||
|
||||
runner(import.meta.url, {
|
||||
name: 'test-runner-cluster',
|
||||
testappcluster: {
|
||||
cluster: 2,
|
||||
port: 31313,
|
||||
provider: 'git',
|
||||
url: 'http://localhost:61412/releases',
|
||||
updateEvery: 0.014,
|
||||
heartbeatTimeout: 100,
|
||||
heartbeatAttempts: 3,
|
||||
heartbeatAttemptsWait: 100,
|
||||
log: [{
|
||||
stream: 'process.stdout',
|
||||
level: 'info',
|
||||
}],
|
||||
}
|
||||
}, 'db.json')
|
||||
.then(
|
||||
function(core) {
|
||||
if (cluster.isPrimary) {
|
||||
let app = core.applications[0]
|
||||
app.ctx.log.on('newlog', function(record) {
|
||||
if (record.name !== app.name) {
|
||||
console.log(`[FROMWORKERAPP] ${record.name} (${record.pid}) ${record.msg}`)
|
||||
}
|
||||
})
|
||||
core.log.on('newlog', function(record) {
|
||||
if (record.pid !== process.pid) {
|
||||
console.log(`[FROMWORKERCORE] ${record.name} (${record.pid}) ${record.msg}`)
|
||||
}
|
||||
})
|
||||
}
|
||||
core.log.info('core is running')
|
||||
},
|
||||
function(err) {
|
||||
runner.log.error(err, 'Error starting runner')
|
||||
process.exit(1)
|
||||
}
|
||||
)
|
1
test/template.mjs
Normal file
1
test/template.mjs
Normal file
|
@ -0,0 +1 @@
|
|||
export default { a: 1 }
|
BIN
test/testapp/example.7z
Normal file
BIN
test/testapp/example.7z
Normal file
Binary file not shown.
BIN
test/testapp/example.tar.gz
Normal file
BIN
test/testapp/example.tar.gz
Normal file
Binary file not shown.
462
test/util.test.mjs
Normal file
462
test/util.test.mjs
Normal file
|
@ -0,0 +1,462 @@
|
|||
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)
|
||||
})
|
||||
})
|
|
@ -1,5 +0,0 @@
|
|||
@setlocal enableextensions
|
||||
@cd /d "%~dp0"
|
||||
|
||||
node service\uninstall.mjs
|
||||
PAUSE
|
Loading…
Reference in a new issue