Compare commits
12 Commits
Author | SHA1 | Date |
---|---|---|
TheThing | f01fc2ac02 | |
Jonatan Nilsson | d175782f73 | |
Jonatan Nilsson | bdaf58b7b0 | |
Jonatan Nilsson | 7f6d1546f2 | |
Jonatan Nilsson | e853474cdd | |
Jonatan Nilsson | 455f667e88 | |
Jonatan Nilsson | 3d3a4c553b | |
Jonatan Nilsson | a68a73438c | |
Jonatan Nilsson | 24bcea69a2 | |
Jonatan Nilsson | 6a2cfcfb26 | |
Jonatan Nilsson | f64acc95eb | |
Jonatan Nilsson | 7e127cd58f |
|
@ -1,44 +0,0 @@
|
|||
version: 2
|
||||
jobs:
|
||||
build:
|
||||
docker:
|
||||
- image: circleci/node:latest
|
||||
working_directory: ~/app
|
||||
steps:
|
||||
- checkout
|
||||
- run:
|
||||
name: Get current git commit message
|
||||
command: |
|
||||
echo "export COMMIT_MESSAGE=\"$(git log --format=oneline -n 1 $CIRCLE_SHA1)\"" >> $BASH_ENV
|
||||
source $BASH_ENV
|
||||
- run:
|
||||
name: Install npm deployment app
|
||||
command: sudo npm install -g github-release-cli
|
||||
- run:
|
||||
name: Build client javascript
|
||||
command: |
|
||||
npm install
|
||||
npm run build
|
||||
- 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.zip" index.mjs package.json public/* api/**/* api/*
|
||||
echo "Creating release '${PACKAGE_VERSION}.${CIRCLE_BUILD_NUM}'"
|
||||
github-release upload \
|
||||
--commitish $CIRCLE_SHA1 \
|
||||
--token $GITHUB_TOKEN \
|
||||
--owner $CIRCLE_PROJECT_USERNAME \
|
||||
--repo $CIRCLE_PROJECT_REPONAME \
|
||||
--tag "v${PACKAGE_VERSION}.${CIRCLE_BUILD_NUM}" \
|
||||
--name "v${PACKAGE_VERSION}.${CIRCLE_BUILD_NUM}" \
|
||||
--body "Automatic CircleCI Build of v${PACKAGE_VERSION}.${CIRCLE_BUILD_NUM} from ${CIRCLE_SHA1}: ${COMMIT_MESSAGE}" \
|
||||
"${CIRCLE_PROJECT_REPONAME}_build-sc.zip"
|
||||
|
||||
workflows:
|
||||
version: 2
|
||||
build_deploy:
|
||||
jobs:
|
||||
- build:
|
||||
context: github-thething
|
|
@ -104,6 +104,5 @@ dist
|
|||
.tern-port
|
||||
|
||||
db.json
|
||||
app/*
|
||||
manage/*
|
||||
hello-world
|
||||
public/main.js
|
||||
|
|
|
@ -1,7 +1,46 @@
|
|||
import { formatLog } from './loghelper.mjs'
|
||||
import { safeWrap } from '../util.mjs'
|
||||
import { getStatus, safeWrap } from '../util.mjs'
|
||||
|
||||
export default function coremonitor(io, config, db, log, core) {
|
||||
export default function coremonitor(io, ctx) {
|
||||
|
||||
ctx.core.applications.forEach(function(app) {
|
||||
app.on('updating', safeWrap(ctx.log, `${app.name}.on('updating')`, function() {
|
||||
io.emit('core.status', getStatus(ctx))
|
||||
}))
|
||||
app.on('running', safeWrap(ctx.log, `${app.name}.on('updating')`, function() {
|
||||
io.emit('core.status', getStatus(ctx))
|
||||
}))
|
||||
|
||||
app.on('updatelog', safeWrap(ctx.log, `${app.name}.on('updatelog')`, function(loglines) {
|
||||
io.to(`app.${app.name}`).emit('app.updatelog', {
|
||||
name: app.name,
|
||||
log: loglines,
|
||||
})
|
||||
}))
|
||||
|
||||
if (app.name !== ctx.app.name) {
|
||||
app.ctx.log.on('newlog', safeWrap(ctx.log, `${app.name}.log.on('newlog')`, function(data) {
|
||||
io.to('logger.' + app.name).emit('newlog', formatLog(data))
|
||||
}))
|
||||
}
|
||||
})
|
||||
|
||||
ctx.core.log.on('newlog', safeWrap(ctx.log, `core.log.on('newlog')`, function(data) {
|
||||
io.to('logger.service-core').emit('newlog', formatLog(data))
|
||||
}))
|
||||
|
||||
ctx.log.on('newlog', safeWrap(ctx.log, 'coremonitor.on.newlog', function(data) {
|
||||
// Stop infinite regression
|
||||
if (data?.err?.signal === 'newlogerror') return
|
||||
try {
|
||||
io.to('logger.' + ctx.app.name).emit('newlog', formatLog(data))
|
||||
} catch (err) {
|
||||
err.signal = 'newlogerror'
|
||||
throw err
|
||||
}
|
||||
}))
|
||||
|
||||
/*
|
||||
log.on('newlog', safeWrap(log, 'coremonitor.on.newlog', function(data) {
|
||||
io.to('logger').emit('newlog', formatLog(data))
|
||||
}))
|
||||
|
@ -22,5 +61,5 @@ export default function coremonitor(io, config, db, log, core) {
|
|||
name: 'manage',
|
||||
logs: manage.logs,
|
||||
})
|
||||
}))
|
||||
}
|
||||
}))*/
|
||||
}
|
||||
|
|
|
@ -1,13 +1,6 @@
|
|||
import defaults from '../defaults.mjs'
|
||||
import { formatLog } from './loghelper.mjs'
|
||||
|
||||
/*
|
||||
* Event: 'core.config'
|
||||
*
|
||||
* Get config
|
||||
*/
|
||||
export async function config(ctx, data, cb) {
|
||||
cb(ctx.config)
|
||||
}
|
||||
import { getApp, stopSpam } from '../util.mjs'
|
||||
|
||||
/*
|
||||
* Event: 'core.restart'
|
||||
|
@ -15,7 +8,12 @@ export async function config(ctx, data, cb) {
|
|||
* Restart server
|
||||
*/
|
||||
export async function restart(ctx, data, cb) {
|
||||
ctx.core.restart()
|
||||
if (ctx.db.config.allowRestart) {
|
||||
ctx.core.restart()
|
||||
} else {
|
||||
ctx.log.fatal('Invalid core restart command')
|
||||
ctx.log.event.error('Invalid core restart command')
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
|
@ -24,7 +22,13 @@ export async function restart(ctx, data, cb) {
|
|||
* Returns last few log messages from log
|
||||
*/
|
||||
export async function getlastlogs(ctx, data, cb) {
|
||||
cb(ctx.logroot.ringbuffer.records.map(formatLog))
|
||||
if (data.name === 'service-core') {
|
||||
return cb(ctx.core.log.ringbuffer.records.map(formatLog))
|
||||
}
|
||||
let app = getApp(ctx, data.name, 'getlastlogs')
|
||||
if (!app) return
|
||||
|
||||
cb(app.ctx.log.ringbuffer.records.map(formatLog))
|
||||
}
|
||||
|
||||
/*
|
||||
|
@ -32,8 +36,8 @@ export async function getlastlogs(ctx, data, cb) {
|
|||
*
|
||||
* Start listening to new log lines
|
||||
*/
|
||||
export async function listenlogs(ctx) {
|
||||
ctx.socket.join('logger')
|
||||
export async function listenlogs(ctx, data) {
|
||||
ctx.socket.join('logger.' + (data.name || 'service-core'))
|
||||
}
|
||||
|
||||
/*
|
||||
|
@ -41,8 +45,8 @@ export async function listenlogs(ctx) {
|
|||
*
|
||||
* Stop listening to new log lines
|
||||
*/
|
||||
export async function unlistenlogs(ctx) {
|
||||
ctx.socket.leave('logger')
|
||||
export async function unlistenlogs(ctx, data) {
|
||||
ctx.socket.leave('logger.' + (data.name || 'service-core'))
|
||||
}
|
||||
|
||||
/*
|
||||
|
@ -51,12 +55,17 @@ export async function unlistenlogs(ctx) {
|
|||
* Update specific software
|
||||
*/
|
||||
export async function update(ctx, data, cb) {
|
||||
if (data.name !== 'app' && data.name !== 'manage') {
|
||||
ctx.log.warn('Invalid update command for app ' + data.name)
|
||||
ctx.log.event.warn('Invalid update command for app ' + data.name)
|
||||
return
|
||||
}
|
||||
await ctx.core.installLatestVersion(data.name)
|
||||
let app = getApp(ctx, data.name, 'update')
|
||||
if (!app) return
|
||||
|
||||
if (stopSpam(ctx, 'update', data.name)) return
|
||||
|
||||
ctx.log.info('Checking for updates on app ' + data.name)
|
||||
app.update().then(function(res) {
|
||||
ctx.log.info(res, 'Update completed on app ' + data.name)
|
||||
}, function(err) {
|
||||
ctx.log.error(err, 'Error checking for updates on app ' + data.name)
|
||||
})
|
||||
}
|
||||
|
||||
/*
|
||||
|
@ -65,71 +74,38 @@ export async function update(ctx, data, cb) {
|
|||
* Start specific software
|
||||
*/
|
||||
export async function start(ctx, data, cb) {
|
||||
if (data.name !== 'app' && data.name !== 'manage') {
|
||||
ctx.log.warn('Invalid start command for app ' + data.name)
|
||||
ctx.log.event.warn('Invalid start command for app ' + data.name)
|
||||
return
|
||||
}
|
||||
await ctx.core.tryStartProgram(data.name)
|
||||
}
|
||||
let app = getApp(ctx, data.name, 'start')
|
||||
if (!app) return
|
||||
|
||||
if (stopSpam(ctx, 'start', data.name)) return
|
||||
|
||||
/*
|
||||
* Event: 'core.updatestart'
|
||||
*
|
||||
* Update and start specific software
|
||||
*/
|
||||
export async function updatestart(ctx, data, cb) {
|
||||
if (data.name !== 'app' && data.name !== 'manage') {
|
||||
ctx.log.warn('Invalid updatestart command for app ' + data.name)
|
||||
ctx.log.event.warn('Invalid updatestart command for app ' + data.name)
|
||||
return
|
||||
}
|
||||
await ctx.core.start(data.name)
|
||||
}
|
||||
|
||||
/*
|
||||
* Event: 'core.listencore'
|
||||
*
|
||||
* Start listening to new log lines
|
||||
*/
|
||||
export async function listencore(ctx) {
|
||||
ctx.socket.join('core')
|
||||
ctx.socket.emit('core.db', ctx.db.get('core').value())
|
||||
ctx.socket.emit('core.status', ctx.core.status())
|
||||
}
|
||||
|
||||
/*
|
||||
* Event: 'core.unlistencore'
|
||||
*
|
||||
* Stop listening to new log lines
|
||||
*/
|
||||
export async function unlistencore(ctx) {
|
||||
ctx.socket.leave('core')
|
||||
ctx.log.info('Checking for updates on app ' + data.name)
|
||||
ctx.core.runApplication(app).then(function(res) {
|
||||
ctx.log.info('Successfully started ' + data.name + ' running ' + app.running)
|
||||
}, function(err) {
|
||||
ctx.log.error(err, 'Error starting app ' + data.name)
|
||||
})
|
||||
}
|
||||
|
||||
/*
|
||||
* Event: 'core.listentoapp'
|
||||
*
|
||||
* Start listening to changes in core app
|
||||
* Start listening to changes in core application name
|
||||
*/
|
||||
export async function listentoapp(ctx) {
|
||||
ctx.socket.join('core.app')
|
||||
ctx.socket.emit('core.program.log', {
|
||||
name: 'app',
|
||||
logs: ctx.core.getProgramLogs('app')
|
||||
})
|
||||
}
|
||||
export async function listentoapp(ctx, data) {
|
||||
if (!data.name) {
|
||||
ctx.log.warn(`listento called with missing name`)
|
||||
return
|
||||
}
|
||||
|
||||
/*
|
||||
* Event: 'core.listentomanage'
|
||||
*
|
||||
* Start listening to changes in core manage
|
||||
*/
|
||||
export async function listentomanage(ctx) {
|
||||
ctx.socket.join('core.manage')
|
||||
ctx.socket.emit('core.program.log', {
|
||||
name: 'manage',
|
||||
logs: ctx.core.getProgramLogs('manage')
|
||||
let app = getApp(ctx, data.name, 'listentoapp')
|
||||
if (!app) return
|
||||
|
||||
ctx.socket.join('app.' + data.name)
|
||||
let version = ctx.db.data.core[app.name].versions[0]
|
||||
ctx.socket.emit('app.updatelog', {
|
||||
name: data.name,
|
||||
log: version?.log || ctx.db.data.core[app.name].updater
|
||||
})
|
||||
}
|
||||
|
||||
|
@ -138,15 +114,14 @@ export async function listentomanage(ctx) {
|
|||
*
|
||||
* Stop listening to new log lines
|
||||
*/
|
||||
export async function unlistentoapp(ctx) {
|
||||
ctx.socket.leave('core.app')
|
||||
}
|
||||
export async function unlistentoapp(ctx, data) {
|
||||
if (!data.name) {
|
||||
ctx.log.warn(`unlistento called with missing name`)
|
||||
return
|
||||
}
|
||||
|
||||
let app = getApp(ctx, data.name, 'unlistentoapp')
|
||||
if (!app) return
|
||||
|
||||
/*
|
||||
* Event: 'core.unlistentomanage'
|
||||
*
|
||||
* Stop listening to new log lines
|
||||
*/
|
||||
export async function unlistentomanage(ctx) {
|
||||
ctx.socket.leave('core.manage')
|
||||
ctx.socket.leave('app.' + data.name)
|
||||
}
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import _ from 'lodash'
|
||||
import defaults from '../defaults.mjs'
|
||||
import { format } from 'util'
|
||||
|
||||
// http://en.wikipedia.org/wiki/ANSI_escape_code#graphics
|
||||
|
@ -64,12 +64,14 @@ function indent(s) {
|
|||
}
|
||||
|
||||
export function formatLog(data) {
|
||||
let rec = _.cloneDeep(data)
|
||||
let rec = defaults(data)
|
||||
|
||||
delete rec.v;
|
||||
|
||||
let d = new Date(rec.time)
|
||||
|
||||
// Time.
|
||||
var time = '[' + rec.time.toISOString().replace('T', ' ').replace('Z', '') + ']'
|
||||
var time = '[' + d.toISOString().replace('T', ' ').replace('Z', '') + ']'
|
||||
time = stylize(time, 'none')
|
||||
|
||||
delete rec.time;
|
||||
|
|
|
@ -0,0 +1,50 @@
|
|||
import { HttpError } from 'flaska'
|
||||
import { getApp, stopSpam } from '../util.mjs'
|
||||
|
||||
/*
|
||||
* POST: /update/:name
|
||||
*
|
||||
*/
|
||||
export async function updateApp(ctx) {
|
||||
let app = getApp(ctx, ctx.params.name, 'POST.update')
|
||||
if (!app) {
|
||||
ctx.status = 404
|
||||
return ctx.body = {
|
||||
status: 404,
|
||||
message: `Application ${ctx.params.name} was not found`
|
||||
}
|
||||
}
|
||||
|
||||
let t = stopSpam(ctx, 'update', ctx.params.name)
|
||||
if (t) {
|
||||
let d2 = new Date()
|
||||
let timeLeft =
|
||||
Math.round((new Date(t).setMinutes(t.getMinutes() + 1) - d2) / 1000)
|
||||
ctx.headers['Retry-After'] = timeLeft
|
||||
|
||||
ctx.status = 503
|
||||
return ctx.body = {
|
||||
status: 503,
|
||||
message: `Update request too fast for ${ctx.params.name}. Try again in ${timeLeft} seconds.`
|
||||
}
|
||||
}
|
||||
|
||||
return app.update()
|
||||
.then(function(res) {
|
||||
if (res) {
|
||||
ctx.body = res
|
||||
} else {
|
||||
ctx.body = {
|
||||
message: 'No update was found',
|
||||
}
|
||||
}
|
||||
}, function(err) {
|
||||
ctx.log.error(err)
|
||||
|
||||
ctx.status = 500
|
||||
return ctx.body = {
|
||||
status: 500,
|
||||
message: `Application ${ctx.params.name} failed to update: ${err.message}`,
|
||||
}
|
||||
})
|
||||
}
|
|
@ -0,0 +1,36 @@
|
|||
|
||||
// taken from isobject npm library
|
||||
function isObject(val) {
|
||||
return val != null && typeof val === 'object' && Array.isArray(val) === false
|
||||
}
|
||||
|
||||
export default function defaults(options, def) {
|
||||
let out = { }
|
||||
|
||||
if (options) {
|
||||
Object.keys(options || {}).forEach(key => {
|
||||
out[key] = options[key]
|
||||
|
||||
if (Array.isArray(out[key])) {
|
||||
out[key] = out[key].map(item => {
|
||||
if (isObject(item)) return defaults(item)
|
||||
return item
|
||||
})
|
||||
} else if (out[key] instanceof Date) {
|
||||
out[key] = new Date(out[key])
|
||||
} else if (out[key] && typeof out[key] === 'object') {
|
||||
out[key] = defaults(options[key], def && def[key])
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
if (def) {
|
||||
Object.keys(def).forEach(function(key) {
|
||||
if (typeof out[key] === 'undefined') {
|
||||
out[key] = def[key]
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
return out
|
||||
}
|
|
@ -1,4 +1,5 @@
|
|||
import * as core from './core/ioroutes.mjs'
|
||||
import { getConfig, getStatus } from './util.mjs'
|
||||
|
||||
function register(ctx, name, method) {
|
||||
if (typeof(method) === 'object') {
|
||||
|
@ -21,32 +22,34 @@ function register(ctx, name, method) {
|
|||
}
|
||||
|
||||
|
||||
function onConnection(server, config, db, log, coreService, data) {
|
||||
function onConnection(server, ctx, data) {
|
||||
const io = server
|
||||
const socket = data
|
||||
|
||||
const child = log.child({
|
||||
const child = ctx.log.child({
|
||||
id: socket.id,
|
||||
})
|
||||
child.event = log.event
|
||||
child.event = ctx.log.event
|
||||
|
||||
child.info('Got new socket connection')
|
||||
|
||||
let ctx = {
|
||||
config,
|
||||
io,
|
||||
socket,
|
||||
let ioCtx = {
|
||||
io: io,
|
||||
socket: socket,
|
||||
log: child,
|
||||
db,
|
||||
core: coreService,
|
||||
logroot: log,
|
||||
db: ctx.db,
|
||||
core: ctx.core,
|
||||
logroot: ctx.log,
|
||||
}
|
||||
|
||||
ctx.socket.on('disconnect', function() {
|
||||
ioCtx.socket.on('disconnect', function() {
|
||||
child.info('Closed connection')
|
||||
})
|
||||
|
||||
register(ctx, 'core', core)
|
||||
register(ioCtx, 'core', core)
|
||||
|
||||
ioCtx.socket.emit('core.config', getConfig(ioCtx))
|
||||
ioCtx.socket.emit('core.status', getStatus(ioCtx))
|
||||
}
|
||||
|
||||
export default onConnection
|
||||
|
|
132
api/server.mjs
132
api/server.mjs
|
@ -1,84 +1,88 @@
|
|||
import path from 'path'
|
||||
import { fileURLToPath } from 'url'
|
||||
import fs from 'fs/promises'
|
||||
import socket from 'socket.io-serveronly'
|
||||
import nStatic from 'node-static'
|
||||
import { Flaska, FileResponse } from 'flaska'
|
||||
import coremonitor from './core/coremonitor.mjs'
|
||||
import * as routes from './core/routes.mjs'
|
||||
|
||||
import onConnection from './routerio.mjs'
|
||||
|
||||
export function run(config, db, log, core, http, port) {
|
||||
return new Promise(function(resolve, reject) {
|
||||
core.startMonitor()
|
||||
export function run(http, port, orgCtx) {
|
||||
let localUtil = new orgCtx.sc.Util(import.meta.url)
|
||||
const staticRoot = localUtil.getPathFromRoot('../public')
|
||||
|
||||
const __dirname = path.dirname(fileURLToPath(import.meta.url))
|
||||
const staticRoot = path.join(__dirname,'../public')
|
||||
const flaska = new Flaska({
|
||||
log: orgCtx.log,
|
||||
}, http)
|
||||
|
||||
const fileServer = new nStatic.Server(staticRoot)
|
||||
const server = http.createServer(function (req, res) {
|
||||
const child = log.child({})
|
||||
flaska.before(function(ctx) {
|
||||
ctx.core = orgCtx.core
|
||||
ctx.state.started = new Date().getTime()
|
||||
})
|
||||
|
||||
const d1 = new Date().getTime()
|
||||
flaska.onreqerror(function(err, ctx) {
|
||||
if (ctx.aborted) {
|
||||
flaska.log.info('Request aborted')
|
||||
} else {
|
||||
flaska.log.error(err)
|
||||
}
|
||||
})
|
||||
|
||||
let finishedRequest = false
|
||||
var done = function () {
|
||||
if (finishedRequest) return
|
||||
finishedRequest = true
|
||||
if (req.url === '/main.css.map') return
|
||||
var requestTime = new Date().getTime() - d1
|
||||
flaska.after(function(ctx) {
|
||||
let ended = new Date().getTime()
|
||||
var requestTime = ended - ctx.state.started
|
||||
|
||||
let level = 'debug'
|
||||
if (res.statusCode >= 400) {
|
||||
level = 'warn'
|
||||
}
|
||||
if (res.statusCode >= 500) {
|
||||
level = 'error'
|
||||
}
|
||||
let status = ''
|
||||
let level = 'debug'
|
||||
if (ctx.status >= 400) {
|
||||
status = ctx.status + ' '
|
||||
level = 'warn'
|
||||
}
|
||||
if (ctx.status >= 500) {
|
||||
level = 'error'
|
||||
}
|
||||
|
||||
let status = ''
|
||||
if (res.statusCode >= 400) {
|
||||
status = res.statusCode + ' '
|
||||
}
|
||||
ctx.log[level]({
|
||||
duration: requestTime,
|
||||
status: ctx.status,
|
||||
}, `<-- ${status}${ctx.method} ${ctx.url}`)
|
||||
})
|
||||
|
||||
child[level]({
|
||||
duration: requestTime,
|
||||
status: res.statusCode,
|
||||
}, `<-- ${status}${req.method} ${req.url}`)
|
||||
flaska.on404(function(ctx) {
|
||||
if (ctx.method !== 'GET' && ctx.method !== 'HEAD') {
|
||||
ctx.status = 404
|
||||
ctx.body = {
|
||||
status: 404,
|
||||
message: 'Not Found',
|
||||
}
|
||||
|
||||
res.addListener('finish', done);
|
||||
res.addListener('close', done);
|
||||
return
|
||||
}
|
||||
|
||||
req.addListener('end', function () {
|
||||
if (req.url === '/') {
|
||||
res.writeHead(302, { Location: '/index.html' })
|
||||
return res.end()
|
||||
}
|
||||
let file = path.resolve(path.join(staticRoot, ctx.url === '/' ? 'index.html' : ctx.url))
|
||||
if (!file.startsWith(staticRoot)) {
|
||||
ctx.status = 404
|
||||
ctx.body = 'HTTP 404 Error'
|
||||
return
|
||||
}
|
||||
|
||||
fileServer.serve(req, res, function (err) {
|
||||
if (err) {
|
||||
if (err.status !== 404) {
|
||||
log.error(err, req.url);
|
||||
}
|
||||
|
||||
res.writeHead(err.status, err.headers);
|
||||
res.end(err.message);
|
||||
}
|
||||
});
|
||||
}).resume()
|
||||
})
|
||||
|
||||
const io = new socket(server)
|
||||
io.on('connection', onConnection.bind(this, io, config, db, log, core))
|
||||
|
||||
coremonitor(io, config, db, log, core)
|
||||
|
||||
server.listen(port, '0.0.0.0', function(err) {
|
||||
if (err) {
|
||||
return reject(err)
|
||||
return fs.stat(file).then(function(stat) {
|
||||
if (!stat) {
|
||||
ctx.status = 404
|
||||
ctx.body = 'HTTP 404 Error'
|
||||
return
|
||||
}
|
||||
log.event.info(`Server is listening on ${port} serving files on ${staticRoot}`)
|
||||
log.info(`Server is listening on ${port} serving files on ${staticRoot}`)
|
||||
resolve()
|
||||
ctx.body = new FileResponse(file, stat)
|
||||
})
|
||||
})
|
||||
|
||||
flaska.post('/update/:name', routes.updateApp)
|
||||
|
||||
return flaska.listenAsync(port).then(function() {
|
||||
orgCtx.log.info('Server is listening on port ' + port)
|
||||
|
||||
const io = new socket(flaska.server)
|
||||
io.on('connection', onConnection.bind(this, io, orgCtx))
|
||||
|
||||
coremonitor(io, orgCtx)
|
||||
})
|
||||
}
|
85
api/util.mjs
85
api/util.mjs
|
@ -1,3 +1,5 @@
|
|||
import defaults from './defaults.mjs'
|
||||
|
||||
export function safeWrap(log, name, fn) {
|
||||
return function(data, cb) {
|
||||
try {
|
||||
|
@ -13,4 +15,85 @@ export function safeWrap(log, name, fn) {
|
|||
log.event.error('Unknown error occured in ' + name + ': ' + err.message)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function getConfig(ctx) {
|
||||
let merge = {
|
||||
applications: [],
|
||||
name: ctx.db.config.name || '',
|
||||
title: ctx.db.config.title || '',
|
||||
}
|
||||
for (let app of ctx.core.applications) {
|
||||
merge[app.name] = {
|
||||
provider: app.config.provider || null,
|
||||
url: app.config.url || null,
|
||||
port: app.config.port || null,
|
||||
scAllowStop: app.config.scAllowStop || null,
|
||||
}
|
||||
merge.applications.push(app.name)
|
||||
}
|
||||
return merge
|
||||
}
|
||||
|
||||
export function getApp(ctx, name, action) {
|
||||
let app = ctx.core.applicationMap.get(name)
|
||||
|
||||
if (!app) {
|
||||
ctx.log.warn(`Invalid action ${action} on non-existing app ${name}`)
|
||||
ctx.log.event.warn(`Invalid action ${action} on non-existing app ${name}`)
|
||||
return null
|
||||
}
|
||||
if (action === 'start' && !app.config.scAllowStop && app.running) {
|
||||
ctx.log.warn(`Invalid action ${action} on existing app ${name}`)
|
||||
ctx.log.event.warn(`Invalid action ${action} on existing app ${name}`)
|
||||
return null
|
||||
}
|
||||
return app
|
||||
}
|
||||
|
||||
const lastAction = {
|
||||
|
||||
}
|
||||
|
||||
export function stopSpam(ctx, action, name) {
|
||||
let key = name + '.' + action
|
||||
var d = new Date()
|
||||
if (!lastAction[key]) {
|
||||
lastAction[key] = d
|
||||
return false
|
||||
}
|
||||
if (d - lastAction[key] < 1000 * 60 * 1) {
|
||||
ctx.log.warn(`${action} called too fast on ${name}`)
|
||||
ctx.log.event.warn(`${action} called too fast on ${name}`)
|
||||
return lastAction[key]
|
||||
}
|
||||
lastAction[key] = d
|
||||
return false
|
||||
}
|
||||
|
||||
export function getStatus(ctx) {
|
||||
let status = {}
|
||||
for (let app of ctx.core.applications) {
|
||||
if (app.provider.static) {
|
||||
status[app.name] = {
|
||||
active: 'static',
|
||||
latestInstalled: 'static',
|
||||
updated: '',
|
||||
running: app.running,
|
||||
updating: false,
|
||||
}
|
||||
} else {
|
||||
let appDb = ctx.db.data.core[app.name]
|
||||
let active = ctx.db.get(appDb.versions, appDb.active)
|
||||
let installed = ctx.db.get(appDb.versions, appDb.latestInstalled)
|
||||
status[app.name] = {
|
||||
active: active,
|
||||
latestInstalled: installed,
|
||||
updated: appDb.updater,
|
||||
running: app.running || app.ctx.version,
|
||||
updating: app.updating,
|
||||
}
|
||||
}
|
||||
}
|
||||
return status
|
||||
}
|
||||
|
|
|
@ -0,0 +1,70 @@
|
|||
# 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
|
||||
|
||||
build_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 build
|
||||
|
||||
# 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
|
||||
./7zas a -mx9 "${CURR_VER}_build-sc.7z" package.json index.mjs api public
|
||||
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}_build-sc.7z to release ${RELEASE_ID}"
|
||||
curl \
|
||||
-X POST \
|
||||
-H "Authorization: token $deploytoken" \
|
||||
-F "attachment=@${CURR_VER}_build-sc.7z" \
|
||||
https://git.nfp.is/api/v1/repos/$APPVEYOR_REPO_NAME/releases/$RELEASE_ID/assets
|
||||
|
||||
echo "Deplying to production"
|
||||
curl -X POST http://192.168.93.51:8881/update/manager | jq
|
||||
fi
|
||||
|
||||
# on build failure
|
||||
on_failure:
|
||||
- sh: echo on_failure
|
|
@ -26,7 +26,7 @@ const Header = {
|
|||
m('div.seperator'),
|
||||
m(m.route.Link, {
|
||||
href: '/log',
|
||||
class: path === '/log' ? 'active' : '',
|
||||
class: path.startsWith('/log') ? 'active' : '',
|
||||
}, 'Log'),
|
||||
m('div.seperator'),
|
||||
m(m.route.Link, {
|
||||
|
|
|
@ -26,6 +26,7 @@ m.mount(document.getElementById('header'), Header)
|
|||
m.route(document.getElementById('content'), '/', {
|
||||
'/': Status,
|
||||
'/log': Log,
|
||||
'/log/:id': Log,
|
||||
'/updater': Updater,
|
||||
'/updater/:id': Updater,
|
||||
})
|
||||
|
|
|
@ -3,7 +3,8 @@ const socket = require('../socket')
|
|||
const Module = require('../module')
|
||||
|
||||
const Log = Module({
|
||||
init: function() {
|
||||
init: function(vnode) {
|
||||
this.activeAppName = 'service-core'
|
||||
this.connected = socket.connected
|
||||
this.loglines = []
|
||||
this.logUpdated = false
|
||||
|
@ -14,21 +15,39 @@ const Log = Module({
|
|||
m.redraw()
|
||||
})
|
||||
|
||||
this._socketOn(() => this.loadData())
|
||||
this.updateActiveApp(vnode.attrs.id)
|
||||
this._socketOn(() => this.socketOpen())
|
||||
},
|
||||
|
||||
onupdate: function(vnode) {
|
||||
this.updateActiveApp(vnode.attrs.id)
|
||||
},
|
||||
|
||||
remove: function() {
|
||||
socket.emit('core.unlistenlogs', {})
|
||||
socket.emit('core.unlistenlogs', { name: this.activeAppName })
|
||||
},
|
||||
|
||||
loadData: function() {
|
||||
updateActiveApp(name) {
|
||||
if (!name) {
|
||||
name = 'service-core'
|
||||
}
|
||||
if (this.activeAppName === name) return
|
||||
if (this.activeAppName !== name) {
|
||||
socket.emit('core.unlistenlogs', { name: this.activeAppName })
|
||||
}
|
||||
this.activeAppName = name
|
||||
this.socketOpen()
|
||||
},
|
||||
|
||||
socketOpen: function() {
|
||||
this.loglines = []
|
||||
socket.emit('core.listenlogs', {})
|
||||
socket.emit('core.getlastlogs', {}, (res) => {
|
||||
socket.emit('core.listenlogs', { name: this.activeAppName })
|
||||
socket.emit('core.getlastlogs', { name: this.activeAppName }, (res) => {
|
||||
this.loglines = res.map(this.formatLine)
|
||||
this.logUpdated = true
|
||||
m.redraw()
|
||||
})
|
||||
m.redraw()
|
||||
},
|
||||
|
||||
formatLine: function(line) {
|
||||
|
@ -45,7 +64,18 @@ const Log = Module({
|
|||
|
||||
view: function() {
|
||||
return [
|
||||
m('h1.header', 'Log'),
|
||||
m('div.actions', [
|
||||
m(m.route.Link, {
|
||||
class: 'button ' + (this.activeAppName === 'service-core' ? 'active' : 'inactive'),
|
||||
href: '/log',
|
||||
}, 'service-core'),
|
||||
this.core.apps.map((app) => {
|
||||
return m(m.route.Link, {
|
||||
class: 'button ' + (this.activeAppName === app.name ? 'active' : 'inactive'),
|
||||
href: '/log/' + app.name,
|
||||
}, app.name)
|
||||
}),
|
||||
]),
|
||||
m('div#logger', {
|
||||
onupdate: (vnode) => {
|
||||
if (this.logUpdated) {
|
||||
|
|
|
@ -1,12 +1,46 @@
|
|||
const m = require('mithril')
|
||||
const defaults = require('./defaults')
|
||||
const socket = require('./socket')
|
||||
|
||||
const core = {
|
||||
name: '...loading...',
|
||||
title: '',
|
||||
apps: [],
|
||||
db: {},
|
||||
status: {},
|
||||
}
|
||||
|
||||
window.core = core
|
||||
|
||||
socket.on('core.config', function(res) {
|
||||
core.apps = []
|
||||
let keys = Object.keys(res)
|
||||
for (let key of keys) {
|
||||
if (typeof(res[key]) !== 'object') {
|
||||
core[key] = res[key]
|
||||
}
|
||||
}
|
||||
for (let appName of res.applications) {
|
||||
core.apps.push({
|
||||
name: appName,
|
||||
config: res[appName],
|
||||
})
|
||||
}
|
||||
m.redraw()
|
||||
})
|
||||
|
||||
socket.on('core.status', function(res) {
|
||||
core.status = res
|
||||
m.redraw()
|
||||
})
|
||||
|
||||
module.exports = function Module(module) {
|
||||
return defaults(module, {
|
||||
init: function() {},
|
||||
|
||||
oninit: function(vnode) {
|
||||
this._listeners = []
|
||||
this.core = core
|
||||
this.init(vnode)
|
||||
},
|
||||
|
||||
|
@ -21,6 +55,11 @@ module.exports = function Module(module) {
|
|||
},
|
||||
|
||||
on: function(name, cb) {
|
||||
for (let i = 0; i < this._listeners.length; i++) {
|
||||
if (this._listeners[i][0] === name) {
|
||||
return
|
||||
}
|
||||
}
|
||||
this._listeners.push([name, cb])
|
||||
socket.on(name, cb)
|
||||
},
|
||||
|
@ -31,7 +70,7 @@ module.exports = function Module(module) {
|
|||
this.remove()
|
||||
if (!this._listeners) return
|
||||
for (let i = 0; i < this._listeners.length; i++) {
|
||||
socket.removeListener(this._listeners[0], this._listeners[1])
|
||||
socket.off(this._listeners[i][0], this._listeners[i][1])
|
||||
}
|
||||
},
|
||||
})
|
||||
|
|
|
@ -1,139 +1,67 @@
|
|||
const m = require('mithril')
|
||||
const socket = require('../socket')
|
||||
const Module = require('../module')
|
||||
const util = require('../util')
|
||||
|
||||
const Status = Module({
|
||||
init: function() {
|
||||
this._name = '...loading...'
|
||||
this._management = {
|
||||
name: 'manage',
|
||||
port: null,
|
||||
repository: null,
|
||||
active: null,
|
||||
latestInstalled: null,
|
||||
latestVersion: null,
|
||||
running: null,
|
||||
updating: null,
|
||||
starting: null,
|
||||
}
|
||||
this._app = {
|
||||
name: 'app',
|
||||
port: null,
|
||||
repository: null,
|
||||
active: null,
|
||||
latestInstalled: null,
|
||||
latestVersion: null,
|
||||
running: null,
|
||||
updating: null,
|
||||
starting: null,
|
||||
}
|
||||
|
||||
this._socketOn(() => this.loadData())
|
||||
},
|
||||
|
||||
loadData: function() {
|
||||
socket.emit('core.config', {}, (res) => {
|
||||
this._name = res.name + ' - ' + res.serviceName
|
||||
this._app.port = res.port
|
||||
this._app.repository = res.appRepository
|
||||
this._management.port = res.managePort
|
||||
this._management.repository = res.manageRepository
|
||||
m.redraw()
|
||||
})
|
||||
|
||||
this.on('core.db', (res) => {
|
||||
this._management.active = res.manageActive
|
||||
this._management.latestInstalled = res.manageLatestInstalled
|
||||
this._management.latestVersion = res.manageLatestVersion
|
||||
this._app.active = res.appActive
|
||||
this._app.latestInstalled = res.appLatestInstalled
|
||||
this._app.latestVersion = res.appLatestVersion
|
||||
|
||||
m.redraw()
|
||||
})
|
||||
|
||||
this.on('core.status', (res) => {
|
||||
console.log(res)
|
||||
this._management.running = res.manage
|
||||
this._management.updating = res.manageUpdating
|
||||
this._management.starting = res.manageStarting
|
||||
this._app.running = res.app
|
||||
this._app.updating = res.appUpdating
|
||||
this._app.starting = res.appStarting
|
||||
|
||||
m.redraw()
|
||||
})
|
||||
|
||||
socket.emit('core.listencore', {})
|
||||
},
|
||||
|
||||
remove: function() {
|
||||
socket.emit('core.unlistencore', {})
|
||||
},
|
||||
|
||||
restartClicked: function() {
|
||||
socket.emit('core.restart', {})
|
||||
},
|
||||
|
||||
start: function(name) {
|
||||
socket.emit('core.updatestart', {
|
||||
startSoftware: function(name) {
|
||||
socket.emit('core.start', {
|
||||
name: name,
|
||||
})
|
||||
},
|
||||
|
||||
getStatus: function(active) {
|
||||
if (active.updating) {
|
||||
return '< Updating >'
|
||||
} else {
|
||||
return '< Starting >'
|
||||
}
|
||||
},
|
||||
|
||||
view: function() {
|
||||
let loopOver = [
|
||||
['Management service', '_management'],
|
||||
['Application service', '_app'],
|
||||
]
|
||||
let name = this.core.name
|
||||
if (this.core.title) {
|
||||
name += ' - ' + this.core.title
|
||||
}
|
||||
return m('div#status', [
|
||||
m('h1.header', this._name),
|
||||
m('h1.header', name),
|
||||
m('div.split', [
|
||||
loopOver.map((group) => {
|
||||
return m('div.item', [
|
||||
m('h4', group[0]),
|
||||
m('p', this[group[1]].port
|
||||
? `Port: ${this[group[1]].port}`
|
||||
: ''),
|
||||
m('p', this[group[1]].repository
|
||||
? `${this[group[1]].repository}`
|
||||
: '< no repository >'),
|
||||
m('p', this[group[1]].active
|
||||
? `Running version: ${this[group[1]].active}`
|
||||
: '< no running version >'),
|
||||
m('p', this[group[1]].latestInstalled
|
||||
? `Latest installed: ${this[group[1]].latestInstalled}`
|
||||
: '< no version installed >'),
|
||||
m('p', this[group[1]].latestVersion
|
||||
? `Latest version: ${this[group[1]].latestVersion}`
|
||||
: '< no version found >'),
|
||||
this[group[1]].running !== null && this[group[1]].repository
|
||||
? m('p',
|
||||
{ class: this[group[1]].running ? 'running' : 'notrunning' },
|
||||
this[group[1]].running ? 'Running' : 'Not Running'
|
||||
)
|
||||
: null,
|
||||
!this[group[1]].running && (this[group[1]].updating || this[group[1]].starting)
|
||||
? m('div.status', this.getStatus(this[group[1]]))
|
||||
: null,
|
||||
m('button', {
|
||||
hidden: this[group[1]].running || this[group[1]].updating || this[group[1]].starting || !this[group[1]].repository,
|
||||
onclick: () => this.start(this[group[1]].name),
|
||||
}, 'Update/Start')
|
||||
])
|
||||
this.core.apps.map((app) => {
|
||||
let box = []
|
||||
let appStatus = this.core.status[app.name] || {}
|
||||
|
||||
box.push(m('h4', [
|
||||
app.name + ' (',
|
||||
m('span', { class: appStatus.running ? 'running' : 'notrunning' }, appStatus.running ? 'Running ' + appStatus.running : 'Not running'),
|
||||
')'
|
||||
]))
|
||||
box.push(m('p', app.config.port
|
||||
? `Port: ${app.config.port}`
|
||||
: ''))
|
||||
box.push(m('p', util.getRepoMessage(app)))
|
||||
|
||||
box.push(m('p', 'Running version: ' + util.getVersionSummary(appStatus.active, appStatus)))
|
||||
box.push(m('p', 'Latest installed: ' + util.getVersionSummary(appStatus.latestInstalled)))
|
||||
|
||||
if (!this.core.status[app.name].running) {
|
||||
box.push(m('button', {
|
||||
onclick: () => this.startSoftware(app.name),
|
||||
}, 'Start'))
|
||||
} else if (app.config.scAllowStop) {
|
||||
box.push(m('button.fatal', {
|
||||
onclick: () => this.startSoftware(app.name),
|
||||
}, 'Restart'))
|
||||
}
|
||||
|
||||
return m('div.item', box)
|
||||
}),
|
||||
]),
|
||||
m('button', {
|
||||
m('button.fatal', {
|
||||
hidden: !this.core.allowRestart,
|
||||
onclick: () => this.restartClicked(),
|
||||
}, 'Restart service')
|
||||
}, 'Restart ' + this.core.name)
|
||||
])
|
||||
}
|
||||
})
|
||||
|
|
|
@ -1,171 +1,146 @@
|
|||
const m = require('mithril')
|
||||
const socket = require('../socket')
|
||||
const Module = require('../module')
|
||||
const util = require('../util')
|
||||
|
||||
const Updater = Module({
|
||||
init: function(vnode) {
|
||||
this.activeApp = vnode.attrs.id || null
|
||||
this.appRepository = null
|
||||
this.manageRepository = null
|
||||
this.db = null
|
||||
this.app = {}
|
||||
this.status = {}
|
||||
this.activeApp = null
|
||||
this.activeAppName = null
|
||||
this.logUpdated = false
|
||||
this._socketOn(() => this.socketOpen())
|
||||
this._active = null
|
||||
|
||||
if (this.activeApp && this.activeApp !== 'app'&& this.activeApp !== 'manage') {
|
||||
return m.route('/updater')
|
||||
}
|
||||
},
|
||||
|
||||
onupdate: function(vnode) {
|
||||
if (this.activeApp === vnode.attrs.id) return
|
||||
|
||||
this.activeApp = vnode.attrs.id || null
|
||||
if (this.activeApp && this.activeApp !== 'app'&& this.activeApp !== 'manage') {
|
||||
return m.route.set('/updater')
|
||||
}
|
||||
if (this.activeApp && (this.appRepository || this.manageRepository)) {
|
||||
this.loadAppData()
|
||||
}
|
||||
m.redraw()
|
||||
},
|
||||
|
||||
socketOpen: function() {
|
||||
socket.emit('core.config', {}, (res) => {
|
||||
this.appRepository = res.appRepository
|
||||
this.manageRepository = res.manageRepository
|
||||
if (this.activeApp) {
|
||||
this.loadAppData()
|
||||
}
|
||||
m.redraw()
|
||||
})
|
||||
|
||||
socket.on('core.status', (res) => {
|
||||
this.status = res
|
||||
m.redraw()
|
||||
})
|
||||
|
||||
this.on('core.db', (res) => {
|
||||
this.db = res
|
||||
this.updateActiveDb()
|
||||
m.redraw()
|
||||
})
|
||||
|
||||
this.on('core.program.log', (res) => {
|
||||
this.app.logs = res.logs
|
||||
this.on('app.updatelog', (res) => {
|
||||
if (!this.activeApp || this.activeApp.name !== res.name) return
|
||||
this.activeApp.log = res.log.replace(/\. /g, '.\n')
|
||||
this.logUpdated = true
|
||||
m.redraw()
|
||||
})
|
||||
|
||||
|
||||
socket.emit('core.listencore', {})
|
||||
},
|
||||
|
||||
updateActiveDb() {
|
||||
if (this.db && this.activeApp) {
|
||||
this.app = {
|
||||
repository: this[this.activeApp + 'Repository'],
|
||||
active: this.db[this.activeApp + 'Active'],
|
||||
latestInstalled: this.db[this.activeApp + 'LatestInstalled'],
|
||||
latestVersion: this.db[this.activeApp + 'LatestVersion'],
|
||||
logs: '',
|
||||
if (this.core.apps.length && vnode.attrs.id) {
|
||||
this.updateActiveApp(vnode.attrs.id)
|
||||
} else if (this.core.apps.length) {
|
||||
let apps = this.core.apps.filter(function(app) {
|
||||
return Boolean(app.config.url)
|
||||
})
|
||||
if (apps.length === 1) {
|
||||
return m.route.set('/updater/' + apps[0].name)
|
||||
}
|
||||
} else {
|
||||
this.app = {}
|
||||
}
|
||||
},
|
||||
|
||||
loadAppData() {
|
||||
this.updateActiveDb()
|
||||
if (this.activeApp === 'app') {
|
||||
socket.emit('core.unlistentomanage', {})
|
||||
socket.emit('core.listentoapp', {})
|
||||
} else {
|
||||
socket.emit('core.unlistentoapp', {})
|
||||
socket.emit('core.listentomanage', {})
|
||||
onupdate: function(vnode) {
|
||||
if (this.core.apps.length) {
|
||||
if (!vnode.attrs.id) {
|
||||
let apps = this.core.apps.filter(function(app) {
|
||||
return Boolean(app.config.url)
|
||||
})
|
||||
if (apps.length === 1) {
|
||||
return m.route.set('/updater/' + apps[0].name)
|
||||
}
|
||||
}
|
||||
this.updateActiveApp(vnode.attrs.id)
|
||||
}
|
||||
/* request to listen to app updates */
|
||||
},
|
||||
|
||||
socketOpen: function() {
|
||||
if (this.activeApp) {
|
||||
socket.emit('core.listentoapp', { name: this.activeApp.name })
|
||||
}
|
||||
},
|
||||
|
||||
updateActiveApp(orgName) {
|
||||
let name = orgName || null
|
||||
if (this.activeAppName === name && (!name || this.activeApp)) return
|
||||
if (this.activeAppName !== name && this.activeAppName) {
|
||||
socket.emit('core.unlistentoapp', { name: this.activeAppName })
|
||||
}
|
||||
|
||||
this.activeAppName = name
|
||||
this.activeApp = null
|
||||
|
||||
if (!name) {
|
||||
return
|
||||
}
|
||||
|
||||
for (let app of this.core.apps) {
|
||||
if (app.name === this.activeAppName) {
|
||||
this.activeApp = app
|
||||
break
|
||||
}
|
||||
}
|
||||
if (!this.activeApp) {
|
||||
return
|
||||
}
|
||||
socket.emit('core.listentoapp', { name: this.activeApp.name })
|
||||
},
|
||||
|
||||
remove: function() {
|
||||
socket.emit('core.unlistencore', {})
|
||||
socket.emit('core.unlistentoapp', {})
|
||||
socket.emit('core.unlistentomanage', {})
|
||||
if (this.activeApp) {
|
||||
socket.emit('core.unlistentoapp', { name: this.activeApp.name })
|
||||
}
|
||||
},
|
||||
|
||||
startUpdate: function() {
|
||||
socket.emit('core.update', {
|
||||
name: this.activeApp,
|
||||
name: this.activeApp.name,
|
||||
})
|
||||
},
|
||||
|
||||
startSoftware: function() {
|
||||
socket.emit('core.start', {
|
||||
name: this.activeApp,
|
||||
name: this.activeApp.name,
|
||||
})
|
||||
},
|
||||
|
||||
view: function() {
|
||||
let apps = this.core.apps.filter(function(app) {
|
||||
return Boolean(app.config.url)
|
||||
})
|
||||
return m('div#update', [
|
||||
m('div.actions', [
|
||||
m('h1.header', 'Updater'),
|
||||
m('div.filler'),
|
||||
m(m.route.Link, {
|
||||
hidden: !this.appRepository,
|
||||
class: 'button ' + (this.activeApp === 'app' ? 'active' : 'inactive'),
|
||||
href: '/updater/app',
|
||||
}, 'Update App'),
|
||||
m(m.route.Link, {
|
||||
hidden: !this.manageRepository,
|
||||
class: 'button ' + (this.activeApp === 'manage' ? 'active' : 'inactive'),
|
||||
href: '/updater/manage',
|
||||
}, 'Update Manager'),
|
||||
apps.map((app) => {
|
||||
return m(m.route.Link, {
|
||||
class: 'button ' + (this.activeAppName === app.name ? 'active' : 'inactive'),
|
||||
href: '/updater/' + app.name,
|
||||
}, app.name)
|
||||
}),
|
||||
]),
|
||||
this.activeApp && this.app ? [
|
||||
m('div.info', [
|
||||
m('p', this.app.repository
|
||||
? `Repository: ${this.app.repository}`
|
||||
: '< no repository >'),
|
||||
m('p', this.app.latestInstalled
|
||||
? `Latest installed: ${this.app.latestInstalled}`
|
||||
: '< no version installed >'),
|
||||
m('p', this.app.active
|
||||
? `Running version: ${this.app.active}`
|
||||
: '< no running version >'),
|
||||
m('p', this.app.latestVersion
|
||||
? `Latest version: ${this.app.latestVersion}`
|
||||
: '< no version found >'),
|
||||
]),
|
||||
m('div.console', {
|
||||
onupdate: (vnode) => {
|
||||
if (this.logUpdated) {
|
||||
vnode.dom.scrollTop = vnode.dom.scrollHeight
|
||||
this.logUpdated = false
|
||||
this.activeApp ? (() => {
|
||||
let appStatus = this.core.status[this.activeApp.name] || {}
|
||||
console.log(appStatus)
|
||||
|
||||
return [
|
||||
m('div.info', [
|
||||
m('p', util.getRepoMessage(this.activeApp)),
|
||||
m('p', 'Active version: ' + util.getVersionSummary(appStatus.active, appStatus)),
|
||||
m('p', appStatus.updating
|
||||
? 'Updating...'
|
||||
: appStatus.running ? 'Running ' + appStatus.running : 'Not running'),
|
||||
m('p', 'Latest installed: ' + util.getVersionSummary(appStatus.latestInstalled, appStatus)),
|
||||
]),
|
||||
m('div.console', {
|
||||
onupdate: (vnode) => {
|
||||
if (this.logUpdated) {
|
||||
vnode.dom.scrollTop = vnode.dom.scrollHeight
|
||||
this.logUpdated = false
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
m('pre', this.app.logs && this.app.logs || '')
|
||||
),
|
||||
this.db
|
||||
? m('div.actions', {
|
||||
hidden: this.status[this.activeApp + 'Updating'],
|
||||
}, [
|
||||
m('button', {
|
||||
onclick: () => this.startUpdate(),
|
||||
}, 'Update & Install'),
|
||||
m('button', {
|
||||
hidden: this.status[this.activeApp] || !(this.db[this.activeApp + 'LastActive'] || this.db[this.activeApp + 'LatestInstalled']),
|
||||
onclick: () => this.startSoftware(),
|
||||
}, 'Start'),
|
||||
m('button', {
|
||||
hidden: !this.db[this.activeApp + 'LastActive']
|
||||
|| this.db[this.activeApp + 'LastActive'] === this.db[this.activeApp + 'Active']
|
||||
}, 'Use Last Version'),
|
||||
])
|
||||
: null,
|
||||
] : null
|
||||
},
|
||||
m('pre', this.activeApp.log || '')
|
||||
),
|
||||
m('div.actions', {
|
||||
hidden: appStatus.updating,
|
||||
}, [
|
||||
m('button', {
|
||||
onclick: () => this.startSoftware(),
|
||||
}, appStatus.running ? 'Restart' : 'Start'),
|
||||
m('button', {
|
||||
onclick: () => this.startUpdate(),
|
||||
}, 'Check for updates'),
|
||||
]),
|
||||
]
|
||||
})() : null
|
||||
])
|
||||
}
|
||||
})
|
||||
|
|
|
@ -0,0 +1,42 @@
|
|||
const m = require('mithril')
|
||||
|
||||
const regexRepoName = /([^\/]+\/[^\/]+)\/releases/
|
||||
|
||||
module.exports.getRepoMessage = function(app) {
|
||||
let prefix = app.config.provider[0].toUpperCase() + app.config.provider.slice(1)
|
||||
if (!app.config.url) {
|
||||
return [prefix, null]
|
||||
}
|
||||
let url = app.config.url.replace('/api/v1/repos', '').replace('/releases', '')
|
||||
let name = app.config.url
|
||||
let temp = regexRepoName.exec(app.config.url)
|
||||
if (temp) {
|
||||
name = temp[1]
|
||||
}
|
||||
|
||||
return [
|
||||
prefix + ': ',
|
||||
m('a', {
|
||||
target: '_blank',
|
||||
href: url,
|
||||
}, name),
|
||||
]
|
||||
}
|
||||
|
||||
module.exports.getVersionSummary = function(version, status) {
|
||||
if (!version) {
|
||||
if (status && status.running) {
|
||||
return '< unknown version >'
|
||||
}
|
||||
return '< no version >'
|
||||
}
|
||||
if (typeof(version) === 'string') {
|
||||
return version
|
||||
}
|
||||
return `${version.version} (${
|
||||
version.stable > 0
|
||||
? 'Stable'
|
||||
: version.stable === 0
|
||||
? 'Not tested'
|
||||
: 'Unstable'}) ${version.installed ? '' : '(Not installed)'}`
|
||||
}
|
|
@ -0,0 +1,52 @@
|
|||
import { ServiceCore, Application, getLog, Util, Core } from 'service-core'
|
||||
import * as index from './index.mjs'
|
||||
|
||||
let util = new Util(import.meta.url)
|
||||
|
||||
const helloWorldConfig = {
|
||||
"provider": "git",
|
||||
"url": "https://git.nfp.is/api/v1/repos/thething/sc-helloworld/releases",
|
||||
"port": 8888,
|
||||
"scAllowStop": true,
|
||||
}
|
||||
|
||||
var core = new ServiceCore('sc-manager', import.meta.url)
|
||||
core.log = getLog('sc-manager', [{
|
||||
"stream": process.stdout,
|
||||
"level": "info"
|
||||
}])
|
||||
core.config['hello-world'] = helloWorldConfig
|
||||
core.config.allowRestart = true
|
||||
core.setConfig({
|
||||
port: 4271,
|
||||
})
|
||||
|
||||
core.init(index).then(function() {
|
||||
let coreLogger = getLog('service-core')
|
||||
core.core.log = coreLogger
|
||||
core.core.db.log = coreLogger
|
||||
|
||||
let provConstructor = Core.providers.get(core.db.config['hello-world'].provider)
|
||||
let provider = new provConstructor(core.db.config['hello-world'])
|
||||
|
||||
let helloWorld = new Application({
|
||||
db: core.db,
|
||||
util: util,
|
||||
log: getLog('hello-world'),
|
||||
core: core.core,
|
||||
}, provider, 'hello-world')
|
||||
|
||||
core.core.applications.push(helloWorld)
|
||||
core.core.applicationMap.set('hello-world', helloWorld)
|
||||
|
||||
helloWorld.startAutoupdater()
|
||||
|
||||
return core.run()
|
||||
}).then(function() {
|
||||
// return core.core.applicationMap.get('hello-world').update()
|
||||
}).then(function() {
|
||||
// return core.core.runApplication(core.core.applicationMap.get('hello-world'))
|
||||
}).then(function() {
|
||||
let helloApp = core.core.applicationMap.get('hello-world')
|
||||
helloApp.on('updated', core.core.runApplication.bind(core.core, helloApp))
|
||||
})
|
10
index.mjs
10
index.mjs
|
@ -1,5 +1,5 @@
|
|||
export function start(config, db, log, core, http, port) {
|
||||
return import('./api/server.mjs').then(function(module) {
|
||||
return module.run(config, db, log, core, http, port)
|
||||
})
|
||||
}
|
||||
import { run } from "./api/server.mjs"
|
||||
|
||||
export function start(http, port, ctx) {
|
||||
return run(http, port, ctx)
|
||||
}
|
||||
|
|
39
package.json
39
package.json
|
@ -1,27 +1,44 @@
|
|||
{
|
||||
"name": "app",
|
||||
"version": "1.0.0",
|
||||
"name": "sc-manager",
|
||||
"version": "2.0.6",
|
||||
"description": "",
|
||||
"main": "index.js",
|
||||
"scripts": {
|
||||
"js:build:main": "asbundle client/index.js public/main.js",
|
||||
"dev:server": "nodemon --watch runner.mjs --watch api runner.mjs | bunyan",
|
||||
"dev:client": "nodemon --watch client --exec \"npm run js:build:main\"",
|
||||
"build": "npm run js:build:main",
|
||||
"build:main": "asbundle client/index.js public/main.js",
|
||||
"build:main:watch": "npm-watch build:main",
|
||||
"dev:server": "node dev.mjs | bunyan",
|
||||
"dev:server:watch": "npm-watch dev:server",
|
||||
"dev": "npm run dev:server && npm run dev:client",
|
||||
"build": "npm run build:main",
|
||||
"test": "echo \"Error: no test specified\" && exit 1"
|
||||
},
|
||||
"watch": {
|
||||
"dev:server": {
|
||||
"patterns": [
|
||||
"api/*"
|
||||
],
|
||||
"extensions": "js,mjs",
|
||||
"quiet": true,
|
||||
"inherit": true
|
||||
},
|
||||
"build:main": {
|
||||
"patterns": [
|
||||
"client/*"
|
||||
],
|
||||
"extensions": "js,mjs",
|
||||
"quiet": true,
|
||||
"inherit": true
|
||||
}
|
||||
},
|
||||
"author": "",
|
||||
"license": "WTFPL",
|
||||
"dependencies": {
|
||||
"lodash": "^4.17.20",
|
||||
"node-static": "^0.7.11",
|
||||
"flaska": "^1.2.1",
|
||||
"socket.io-serveronly": "^2.3.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"asbundle": "^2.6.1",
|
||||
"bunyan-lite": "^1.0.1",
|
||||
"mithril": "^2.0.4",
|
||||
"nodemon": "^2.0.4",
|
||||
"service-core": "^2.0.0"
|
||||
"service-core": "^3.0.0-beta.13"
|
||||
}
|
||||
}
|
||||
|
|
Binary file not shown.
After Width: | Height: | Size: 474 B |
Binary file not shown.
After Width: | Height: | Size: 543 B |
Binary file not shown.
After Width: | Height: | Size: 15 KiB |
|
@ -1,6 +1,8 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<link rel="icon" type="image/png" sizes="32x32" href="/favicon-32x32.png">
|
||||
<link rel="icon" type="image/png" sizes="16x16" href="/favicon-16x16.png">
|
||||
<meta content="text/html;charset=utf-8" http-equiv="Content-Type">
|
||||
<meta content="utf-8" http-equiv="encoding">
|
||||
<title>Service Core</title>
|
||||
|
|
|
@ -87,11 +87,33 @@ a.button {
|
|||
text-align: center;
|
||||
}
|
||||
|
||||
button.fatal,
|
||||
a.button.fatal {
|
||||
background: hsl(0, 100%, 36%);
|
||||
color: white;
|
||||
border-color: hsl(0, 100%, 5%);
|
||||
}
|
||||
|
||||
pre {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
|
||||
.actions {
|
||||
margin: 1rem 0;
|
||||
flex-wrap: wrap;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.actions .button.inactive {
|
||||
background: transparent;
|
||||
color: #ffb843;
|
||||
}
|
||||
.actions .button.active {
|
||||
}
|
||||
|
||||
/***************** Header ********************/
|
||||
#header {
|
||||
display: flex;
|
||||
|
@ -165,44 +187,38 @@ pre {
|
|||
align-self: center;
|
||||
width: calc(100% - 4rem);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
max-width: 700px;
|
||||
border: 1px solid #999;
|
||||
border-right: none;
|
||||
}
|
||||
#status .item {
|
||||
flex: 2 1 50%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
border-right: 1px solid #999;
|
||||
padding-bottom: 0.5rem;
|
||||
}
|
||||
#status .item h4 {
|
||||
margin-top: -1px;
|
||||
font-size: 1.2rem;
|
||||
text-align: center;
|
||||
padding: 1rem;
|
||||
border-bottom: 1px solid #999;
|
||||
border-top: 1px solid #999;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
#status .item p {
|
||||
padding: 0.25rem 1rem;
|
||||
}
|
||||
#status .item p.running {
|
||||
#status .item .running {
|
||||
color: hsl(118, 84%, 46.3%);
|
||||
text-align: center;
|
||||
}
|
||||
#status .item p.notrunning {
|
||||
#status .item .notrunning {
|
||||
color: hsl(354.7, 85.8%, 67.6%);
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
#status button {
|
||||
margin-top: 1rem;
|
||||
}
|
||||
#status .status {
|
||||
margin-top: 1rem;
|
||||
align-self: center;
|
||||
padding: 0.5rem;
|
||||
color: #ccc;
|
||||
}
|
||||
|
||||
/***************** Updater ********************/
|
||||
#update {
|
||||
|
@ -212,22 +228,6 @@ pre {
|
|||
align-self: center;
|
||||
max-width: 700px;
|
||||
}
|
||||
#update .actions {
|
||||
margin: 1rem 0;
|
||||
flex-wrap: wrap;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
}
|
||||
#update .actions .filler {
|
||||
flex-grow: 2;
|
||||
}
|
||||
|
||||
#update .actions .button.inactive {
|
||||
background: transparent;
|
||||
color: #ffb843;
|
||||
}
|
||||
#update .actions .button.active {
|
||||
}
|
||||
|
||||
@media only screen and (max-device-width: 590px) {
|
||||
#update .actions .filler {
|
||||
|
@ -273,7 +273,7 @@ pre {
|
|||
|
||||
@media only screen and (min-height: 650px) {
|
||||
#update .console {
|
||||
height: calc(100vh - 300px);
|
||||
height: calc(100vh - 280px);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
Loading…
Reference in New Issue