Compare commits

...

88 Commits

Author SHA1 Message Date
Jonatan Nilsson 6c720f4c2d Fix git integration test
continuous-integration/appveyor/branch AppVeyor build succeeded Details
2024-02-16 06:52:50 +00:00
Jonatan Nilsson 14fead9c17 core: Update tests after moving version info
continuous-integration/appveyor/branch AppVeyor build failed Details
2024-02-16 06:50:34 +00:00
Jonatan Nilsson 6c115aa8b2 core: In application, move version into context
continuous-integration/appveyor/branch AppVeyor build failed Details
2024-02-16 06:48:29 +00:00
Jonatan Nilsson 5d39f776e1 git: Auto replace spaces with underscores in version output. Allows versions to have spaces in their name while keeping it unix friendly folder and file name.
continuous-integration/appveyor/branch AppVeyor build succeeded Details
2022-08-18 10:54:38 +00:00
Jonatan Nilsson 79f3203f70 Fix integration test based on latest changes in git config loading
continuous-integration/appveyor/branch AppVeyor build succeeded Details
2022-08-13 02:10:40 +00:00
Jonatan Nilsson e8a2ec52a6 git: Add support to filter based on specific prefix 2022-08-13 02:10:24 +00:00
Jonatan Nilsson 1995b38510 git: checkConfig no longer checks repo url. Git being down or latest version being unfetchable should not stop services from starting up.
continuous-integration/appveyor/branch AppVeyor build failed Details
Package: Remove beta flag, is stable enough for now
2022-08-13 01:48:05 +00:00
Jonatan Nilsson 63a06a2a34 git: checkConfig no longer checks repo url. Git being down or latest version being unfetchable should not stop services from starting up.
Package: Remove beta flag, is stable enough for now
2022-08-13 01:47:05 +00:00
Jonatan Nilsson cb5de72e13 db: Don't warn about clearing db when using in-memory db
continuous-integration/appveyor/branch AppVeyor build succeeded Details
2022-04-19 16:37:45 +00:00
Jonatan Nilsson 33b3d98a37 application: Expose app config in ctx.config
continuous-integration/appveyor/branch AppVeyor build succeeded Details
2022-04-02 20:10:09 +00:00
Jonatan Nilsson 618cfd0451 log: Workers in cluster will now properly notify master of newlog entries. Useful for sc-manager
continuous-integration/appveyor/branch AppVeyor build succeeded Details
2022-04-01 09:59:47 +00:00
Jonatan Nilsson b5359515b5 lib: Fix so it displays the restart message during restart
continuous-integration/appveyor/branch AppVeyor build succeeded Details
2022-03-30 08:19:49 +00:00
Jonatan Nilsson 23f9173720 test: Fix regex match error after some recent changes
continuous-integration/appveyor/branch AppVeyor build succeeded Details
2022-03-29 20:38:59 +00:00
Jonatan Nilsson d519996959 Application: Add flag in application to keep track if its running and which version.
continuous-integration/appveyor/branch AppVeyor build failed Details
Lib: Add restart support to shut down server.
Runner: Add better logging when shut down request is sent.
Many: Add bunch of event emitters for interested parties
2022-03-29 17:14:50 +00:00
Jonatan Nilsson 90d4e7ab81 application: Emit event when updating changes
continuous-integration/appveyor/branch AppVeyor build succeeded Details
2022-03-28 15:15:30 +00:00
Jonatan Nilsson a5239d0c27 db: Removed unused latestVersion
continuous-integration/appveyor/branch AppVeyor build succeeded Details
2022-03-28 07:48:30 +00:00
Jonatan Nilsson 46386a13b3 application: Add more event emits on specific events
continuous-integration/appveyor/branch AppVeyor build succeeded Details
2022-03-28 07:40:25 +00:00
Jonatan Nilsson 99d7a0655d lib: Now generates a valid config and enforces provider on lib to be static
continuous-integration/appveyor/branch AppVeyor build succeeded Details
2022-03-27 16:02:53 +00:00
Jonatan Nilsson 95d72fc404 Application: Expose some helper classes to application through ctx.sc
continuous-integration/appveyor/branch AppVeyor build succeeded Details
2022-03-27 15:12:04 +00:00
Jonatan Nilsson 2edcedb1b7 cli: Add basic install support for linux
continuous-integration/appveyor/branch AppVeyor build succeeded Details
http: Fix so listenAsync defaults to all hosts
package: Increment version
2022-03-13 01:22:45 +00:00
Jonatan Nilsson 4a508d20a4 lib: Fix test for the new port parameter support
continuous-integration/appveyor/branch AppVeyor build succeeded Details
2022-03-10 13:40:10 +00:00
Jonatan Nilsson 1d7b118229 lib: Add optional port parameter that is prefilled. Prevents crashes if no port is specified
continuous-integration/appveyor/branch AppVeyor build failed Details
package: Increment beta version
2022-03-10 13:38:35 +00:00
Jonatan Nilsson 1f70f36e8d package: Update dependencies and version
continuous-integration/appveyor/branch AppVeyor build succeeded Details
2022-03-10 13:35:34 +00:00
Jonatan Nilsson 73e9be2ff0 fix core integration test yet again
continuous-integration/appveyor/branch AppVeyor build succeeded Details
2022-03-10 13:11:23 +00:00
Jonatan Nilsson 87cc47f498 Make the integration test slightly more consistent
continuous-integration/appveyor/branch AppVeyor build failed Details
2022-03-10 13:10:02 +00:00
Jonatan Nilsson b8a0ec137c Increment version, create a new release
continuous-integration/appveyor/branch AppVeyor build failed Details
2022-03-10 13:08:29 +00:00
Jonatan Nilsson 67606b9b3b Fixed lib and finished implementing it
continuous-integration/appveyor/branch AppVeyor build succeeded Details
2022-03-10 13:08:02 +00:00
Jonatan Nilsson 4024f1269a remove lodash
continuous-integration/appveyor/branch AppVeyor build succeeded Details
2022-03-10 12:34:54 +00:00
Jonatan Nilsson 25cac907be Fix core integration test
continuous-integration/appveyor/branch AppVeyor build succeeded Details
2022-03-10 12:27:01 +00:00
Jonatan Nilsson 4d15623a23 Fix some test, update version
continuous-integration/appveyor/branch AppVeyor build failed Details
2022-03-10 12:24:57 +00:00
Jonatan Nilsson 746e0c3f46 Fix bug in client.mjs
continuous-integration/appveyor/branch AppVeyor build failed Details
2022-03-10 11:30:43 +00:00
Jonatan Nilsson 44f576c1d4 More appveyor stuff :(
continuous-integration/appveyor/branch AppVeyor build failed Details
2022-03-10 11:28:29 +00:00
Jonatan Nilsson dccad23890 Fix bug with missing log level
continuous-integration/appveyor/branch AppVeyor build failed Details
2022-03-10 11:15:02 +00:00
Jonatan Nilsson 5330ecfa5c More appveyor stuff :(
continuous-integration/appveyor/branch AppVeyor build failed Details
2022-03-10 11:13:35 +00:00
Jonatan Nilsson b152f18fc3 appveyor: Should now fail test if test fail
continuous-integration/appveyor/branch AppVeyor build failed Details
2022-03-10 11:11:17 +00:00
Jonatan Nilsson bc2d3d9a9b Increment version to beta 3
continuous-integration/appveyor/branch AppVeyor build cancelled Details
2022-03-10 11:07:01 +00:00
Jonatan Nilsson e0da74f0be Finished implementing cluster support 2022-03-10 11:06:17 +00:00
Jonatan Nilsson 47344c5e7a Updated core logic and how stable is calculated.
continuous-integration/appveyor/branch AppVeyor build failed Details
Fixed some minor bugs.
Will now no longer travel through history but instead stop at last stable version.
2022-02-18 13:32:44 +00:00
Jonatan Nilsson 57be8a144a Some cleanup on files. Remove skip tests 2022-02-18 08:16:36 +00:00
Jonatan Nilsson 0f504eaf53 Clean up some of the test, finish implementing auto publish on both npm and git
continuous-integration/appveyor/branch AppVeyor build succeeded Details
2022-02-18 07:40:39 +00:00
Jonatan Nilsson ed73607e7b More appveyor stuff
continuous-integration/appveyor/branch AppVeyor build succeeded Details
2022-02-18 06:50:14 +00:00
Jonatan Nilsson 17b100bb07 more appveyor testing
continuous-integration/appveyor/branch AppVeyor build failed Details
2022-02-18 06:46:32 +00:00
Jonatan Nilsson 396ac97b8b Appveyor lots of debug code
continuous-integration/appveyor/branch AppVeyor build succeeded Details
2022-02-18 06:43:35 +00:00
Jonatan Nilsson 35b82934f1 Appveyor stuff
continuous-integration/appveyor/branch AppVeyor build succeeded Details
2022-02-18 06:34:24 +00:00
Jonatan Nilsson 5eb9fb9476 Even more appveyor stuff
continuous-integration/appveyor/branch AppVeyor build succeeded Details
2022-02-18 06:33:23 +00:00
Jonatan Nilsson b50ed47ec5 more appveyor stuff
continuous-integration/appveyor/branch AppVeyor build succeeded Details
2022-02-18 06:32:35 +00:00
Jonatan Nilsson 2dad21e8ab PRevent appveyor from finishing
continuous-integration/appveyor/branch AppVeyor build succeeded Details
2022-02-18 06:28:21 +00:00
Jonatan Nilsson bca4b665a6 Fix typo in appveyor
continuous-integration/appveyor/branch AppVeyor build succeeded Details
2022-02-18 06:26:28 +00:00
Jonatan Nilsson 39af3ca162 Update package to new git.nfp.is repo, update appveyor with more deployment scripts
continuous-integration/appveyor/branch Error parsing appveyor.yml Details
2022-02-18 06:24:52 +00:00
Jonatan Nilsson be6c24778d even more attempts to enable ssh
continuous-integration/appveyor/branch AppVeyor build failed Details
2022-02-16 12:19:35 +00:00
Jonatan Nilsson d434564217 more attempts to enable appveyor ssh
continuous-integration/appveyor/branch AppVeyor build failed Details
2022-02-16 12:17:41 +00:00
Jonatan Nilsson 10e45b33ee appveyor: Attempt to enable ssh for debugging
continuous-integration/appveyor/branch AppVeyor build failed Details
2022-02-16 12:16:38 +00:00
Jonatan Nilsson cbc0f7a37a Fix tests to use 7zdec correctly
continuous-integration/appveyor/branch AppVeyor build failed Details
2022-02-16 11:49:48 +00:00
Jonatan Nilsson dbcc431e70 Replace binaries in bin folder with pure 7zdec only for smaller release files
continuous-integration/appveyor/branch AppVeyor build failed Details
2022-02-15 16:32:43 +00:00
Jonatan Nilsson 5f3e688b8c Finished basic beta implementation of entire thing.
continuous-integration/appveyor/branch AppVeyor build failed Details
2022-02-15 11:28:30 +00:00
Jonatan Nilsson e540a54844 More core testing and some development and fixes
continuous-integration/appveyor/branch AppVeyor build succeeded Details
2022-02-14 08:15:50 +00:00
Jonatan Nilsson 4f4bc8cf6a More core development, full integration started
continuous-integration/appveyor/branch AppVeyor build succeeded Details
2022-02-11 13:59:10 +00:00
Jonatan Nilsson 3f8e3cdc16 More core development
continuous-integration/appveyor/branch AppVeyor build succeeded Details
2022-02-08 06:35:36 +00:00
Jonatan Nilsson c29e128814 Core development
continuous-integration/appveyor/branch AppVeyor build succeeded Details
2022-02-05 09:26:15 +00:00
Jonatan Nilsson ae4f0f141b Fix util unittest
continuous-integration/appveyor/branch AppVeyor build succeeded Details
2022-02-04 09:45:03 +00:00
Jonatan Nilsson f1710601fc Cleanup old app and manage folder, no longer needed
continuous-integration/appveyor/branch AppVeyor build failed Details
2022-02-04 09:34:47 +00:00
Jonatan Nilsson 758e61b8b1 Massive development. Application finished. Core started 2022-02-04 09:33:03 +00:00
Jonatan Nilsson df7e1e5509 Fix a bug in db test, finished application update. Fixed a few bugs and did some refactoring. Started on application.run() functionality
continuous-integration/appveyor/branch AppVeyor build succeeded Details
2022-01-28 11:47:55 +00:00
Jonatan Nilsson e026b9757d Fix few random bug occurance, make db more stable at writing. Add static provider and fix a few issues in application update.
continuous-integration/appveyor/branch AppVeyor build failed Details
2022-01-28 07:03:18 +00:00
Jonatan Nilsson 2d8e7fd8cc Another attempt to fix permission error in appveyor
continuous-integration/appveyor/branch AppVeyor build failed Details
2022-01-27 18:32:43 +00:00
Jonatan Nilsson 442b241aaf Fix appveyor permission error
continuous-integration/appveyor/branch AppVeyor build failed Details
2022-01-27 18:31:21 +00:00
Jonatan Nilsson 47b8ac2ebb Fix so util test on 7zip will log 7zip output on errors
continuous-integration/appveyor/branch AppVeyor build failed Details
2022-01-27 18:29:54 +00:00
Jonatan Nilsson f008af597d Application almost done
continuous-integration/appveyor/branch AppVeyor build failed Details
2022-01-27 18:25:33 +00:00
Jonatan Nilsson a16a04a91f More application development. Added 7zip extraction helper in util 2022-01-24 17:06:40 +00:00
Jonatan Nilsson edba2d72ab Development, started on application
continuous-integration/appveyor/branch AppVeyor build failed Details
2022-01-24 06:59:58 +00:00
Jonatan Nilsson f8adcbbc21 Fix unit test running in linux environment
continuous-integration/appveyor/branch AppVeyor build succeeded Details
2022-01-21 12:40:01 +00:00
Jonatan Nilsson 726ac81eb3 Lots more development, git provider finished implementing. Started on creating application
continuous-integration/appveyor/branch AppVeyor build succeeded Details
2022-01-21 02:43:48 +00:00
Jonatan Nilsson 0948885671 More development
continuous-integration/appveyor/branch AppVeyor build succeeded Details
2022-01-18 11:48:35 +00:00
Jonatan Nilsson 17830d7f8d Many test, many refactoring, only core, lib and runner left to test and fix
continuous-integration/appveyor/branch AppVeyor build succeeded Details
2022-01-13 17:02:55 +00:00
Jonatan Nilsson 04747bc1c6 Moved binaries to its own folder
continuous-integration/appveyor/branch AppVeyor build failed Details
2022-01-11 16:51:27 +00:00
Jonatan Nilsson d215329c2b Added some tests, fixed some bugs 2022-01-11 16:51:15 +00:00
Jonatan Nilsson 1be2c4ef07 Update Readme 2022-01-10 08:49:01 +00:00
Jonatan Nilsson 134cd3d6a4 Add local 7za 2022-01-09 16:31:52 +00:00
Jonatan Nilsson 1e82f47604 circleci: Fix bugs and other stuff, switch to 7zip for core 2021-12-02 12:26:36 +00:00
Jonatan Nilsson 3b8405021a try and add ssh keys 2021-12-02 11:19:55 +00:00
TheThing dc67a1bd5e Merge branch 'master' of https://github.com/TheThing/service-core 2021-12-02 11:05:38 +00:00
Jonatan Nilsson 40087a0130 Update package.json 2021-12-02 11:05:15 +00:00
TheThing 95392c80b2 Merge branch 'master' of https://github.com/TheThing/service-core 2021-12-02 11:01:49 +00:00
TheThing 0b4b5ebdd5 Add 7zip decompress binaries. Removed some windows service stuff. Fixed uninstall 2021-12-02 11:01:45 +00:00
Jonatan Nilsson 564cfe4263 Package: Increment version and create new build 2020-12-08 14:11:42 +00:00
Jonatan Nilsson 036a6b6eca Fix circleci build 2020-12-08 14:06:33 +00:00
Jonatan Nilsson 8a973cb1e5 build also app zip file for quick deploy, fix github release bugs 2020-12-08 14:04:29 +00:00
Jonatan Nilsson 37737a7ef0 Fix a bug in http server where it never gracefully migrates to new version without restart 2020-09-13 15:23:21 +00:00
62 changed files with 7856 additions and 1083 deletions

View File

@ -1,49 +0,0 @@
version: 2
jobs:
build:
docker:
- image: circleci/node:latest
working_directory: ~/app
steps:
- checkout
- run:
name: Install npm deployment app
command: sudo npm install -g github-release-cli
- run:
name: Check if this is a new release
command: |
set +e
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
- deploy:
name: Create a release
command: |
PACKAGE_VERSION=$(cat package.json | grep version | head -1 | awk -F: '{ print $2 }' | sed 's/[", ]//g')
echo "Packaging to ${CIRCLE_PROJECT_REPONAME}_build-sc.zip"
zip "${CIRCLE_PROJECT_REPONAME}_build-sc-core.zip" runner.mjs package.json service/* core/**/* core/*
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}" \
--name "v${PACKAGE_VERSION}" \
--body "Automatic CircleCI Build of v${PACKAGE_VERSION} from ${CIRCLE_SHA1}: ${COMMIT_MESSAGE}" \
"${CIRCLE_PROJECT_REPONAME}_build-sc-core.zip"
workflows:
version: 2
build_deploy:
jobs:
- build:
context: github-thething

100
.gitignore vendored
View File

@ -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

View File

@ -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
View 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

Binary file not shown.

BIN
bin/7zdec.exe Normal file

Binary file not shown.

196
cli.mjs Normal file
View 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)
}
}

View File

@ -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
View 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()
}
}

View File

@ -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

View File

@ -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)

View File

@ -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
View 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
}

View File

@ -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 {
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
View 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')
}
}

View File

@ -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
View 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
View 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
View 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')

View File

@ -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
View 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
View 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,
}

View File

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

52
lib.mjs
View File

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

View File

@ -1,35 +1,56 @@
{
"name": "service-core",
"version": "2.0.0",
"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",
"node-windows": "^1.0.0-beta.5"
"bunyan-lite": "^1.2.0",
"lowdb": "^3.0.0"
},
"devDependencies": {
"nodemon": "^2.0.4"
"eltro": "^1.3.1"
}
}

View File

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

View File

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

View File

@ -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

View File

@ -1,3 +0,0 @@
import svc from './service.mjs'
svc.start();

View File

@ -1,3 +0,0 @@
import svc from './service.mjs'
svc.stop();

View File

@ -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();

View File

@ -1 +0,0 @@
node service\start.mjs

View File

@ -1 +0,0 @@
node service\stop.mjs

BIN
test/7za.exe Normal file

Binary file not shown.

BIN
test/7zas Normal file

Binary file not shown.

View File

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

View File

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

View 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

File diff suppressed because it is too large Load Diff

284
test/client.test.mjs Normal file
View 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
View File

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

View File

@ -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

File diff suppressed because it is too large Load Diff

334
test/db.test.mjs Normal file
View 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
View 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
View 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
View 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
View 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
View File

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

294
test/log.test.mjs Normal file
View 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')
})
})

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

View 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
View 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
View File

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

1
test/template.mjs Normal file
View File

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

BIN
test/testapp/example.7z Normal file

Binary file not shown.

BIN
test/testapp/example.tar.gz Normal file

Binary file not shown.

462
test/util.test.mjs Normal file
View 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)
})
})

View File

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