Major development, finished refactoring for new service-core version 3

This commit is contained in:
Jonatan Nilsson 2022-03-29 10:25:25 +00:00
parent f64acc95eb
commit 6a2cfcfb26
19 changed files with 484 additions and 357 deletions

View file

@ -1,7 +1,46 @@
import { formatLog } from './loghelper.mjs' 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) { log.on('newlog', safeWrap(log, 'coremonitor.on.newlog', function(data) {
io.to('logger').emit('newlog', formatLog(data)) io.to('logger').emit('newlog', formatLog(data))
})) }))
@ -22,5 +61,5 @@ export default function coremonitor(io, config, db, log, core) {
name: 'manage', name: 'manage',
logs: manage.logs, logs: manage.logs,
}) })
})) }))*/
} }

View file

@ -1,23 +1,8 @@
import defaults from '../defaults.mjs' import defaults from '../defaults.mjs'
import { formatLog } from './loghelper.mjs' import { formatLog } from './loghelper.mjs'
import { getStatus } from '../util.mjs'
/* const stopSpam = {}
* Event: 'core.config'
*
* Get config
*/
export async function config(ctx, data, cb) {
let merge = {
applications: []
}
for (let app of ctx.core.applications) {
merge[app.name] = app.config
merge.applications.push(app.name)
}
let out = defaults(ctx.db.config, merge)
console.log(out)
cb(out)
}
/* /*
* Event: 'core.restart' * Event: 'core.restart'
@ -25,7 +10,12 @@ export async function config(ctx, data, cb) {
* Restart server * Restart server
*/ */
export async function restart(ctx, data, cb) { export async function restart(ctx, data, cb) {
if (ctx.db.config.allowRestart) {
ctx.core.restart() ctx.core.restart()
} else {
ctx.log.fatal('Invalid core restart command')
ctx.log.event.error('Invalid core restart command')
}
} }
/* /*
@ -34,7 +24,17 @@ export async function restart(ctx, data, cb) {
* Returns last few log messages from log * Returns last few log messages from log
*/ */
export async function getlastlogs(ctx, data, cb) { 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 = ctx.core.applicationMap.get(data.name)
if (!app) {
ctx.log.warn('Invalid getlastlogs command for app ' + data.name)
ctx.log.event.warn('Invalid getlastlogs command for app ' + data.name)
return
}
cb(app.ctx.log.ringbuffer.records.map(formatLog))
} }
/* /*
@ -42,8 +42,8 @@ export async function getlastlogs(ctx, data, cb) {
* *
* Start listening to new log lines * Start listening to new log lines
*/ */
export async function listenlogs(ctx) { export async function listenlogs(ctx, data) {
ctx.socket.join('logger') ctx.socket.join('logger.' + (data.name || 'service-core'))
} }
/* /*
@ -51,8 +51,8 @@ export async function listenlogs(ctx) {
* *
* Stop listening to new log lines * Stop listening to new log lines
*/ */
export async function unlistenlogs(ctx) { export async function unlistenlogs(ctx, data) {
ctx.socket.leave('logger') ctx.socket.leave('logger.' + (data.name || 'service-core'))
} }
/* /*
@ -61,12 +61,27 @@ export async function unlistenlogs(ctx) {
* Update specific software * Update specific software
*/ */
export async function update(ctx, data, cb) { export async function update(ctx, data, cb) {
if (data.name !== 'app' && data.name !== 'manage') { let app = ctx.core.applicationMap.get(data.name)
if (!app) {
ctx.log.warn('Invalid update command for app ' + data.name) ctx.log.warn('Invalid update command for app ' + data.name)
ctx.log.event.warn('Invalid update command for app ' + data.name) ctx.log.event.warn('Invalid update command for app ' + data.name)
return return
} }
await ctx.core.installLatestVersion(data.name)
let d = new Date()
if (stopSpam[app.name] && d - stopSpam[app.name] < 1000 * 60 * 5) {
ctx.log.warn('Update called too fast for app ' + data.name)
ctx.log.event.warn('Update called too fast for app ' + data.name)
return
}
stopSpam[app.name] = d
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.err(err, 'Error checking for updates on app ' + data.name)
})
} }
/* /*
@ -75,71 +90,52 @@ export async function update(ctx, data, cb) {
* Start specific software * Start specific software
*/ */
export async function start(ctx, data, cb) { export async function start(ctx, data, cb) {
if (data.name !== 'app' && data.name !== 'manage') { let app = ctx.core.applicationMap.get(data.name)
if (!app || (!app.config.scAllowStop && app.running)) {
ctx.log.warn('Invalid start command for app ' + data.name) ctx.log.warn('Invalid start command for app ' + data.name)
ctx.log.event.warn('Invalid start command for app ' + data.name) ctx.log.event.warn('Invalid start command for app ' + data.name)
return return
} }
await ctx.core.tryStartProgram(data.name)
}
/* let d = new Date()
* Event: 'core.updatestart' if (app.running && stopSpam[app.name] && d - stopSpam[app.name] < 1000 * 60 * 5) {
* ctx.log.warn('Update called too fast for app ' + data.name)
* Update and start specific software ctx.log.event.warn('Update called too fast for app ' + data.name)
*/
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 return
} }
await ctx.core.start(data.name) stopSpam[app.name] = d
}
/* ctx.log.info('Checking for updates on app ' + data.name)
* Event: 'core.listencore' ctx.core.runApplication(app).then(function(res) {
* ctx.log.info('Successfully started ' + data.name + ' running ' + app.running)
* Start listening to new log lines }, function(err) {
*/ ctx.log.err(err, 'Error starting app ' + data.name)
export async function listencore(ctx) { })
ctx.socket.join('core')
ctx.socket.emit('core.db', ctx.db.data.core)
ctx.socket.emit('core.status', {})
}
/*
* Event: 'core.unlistencore'
*
* Stop listening to new log lines
*/
export async function unlistencore(ctx) {
ctx.socket.leave('core')
} }
/* /*
* Event: 'core.listentoapp' * Event: 'core.listentoapp'
* *
* Start listening to changes in core app * Start listening to changes in core application name
*/ */
export async function listentoapp(ctx) { export async function listentoapp(ctx, data) {
ctx.socket.join('core.app') if (!data.name) {
ctx.socket.emit('core.program.log', { ctx.log.warn(`listento called with missing name`)
name: 'app', return
logs: ctx.core.getProgramLogs('app')
})
} }
/* let app = ctx.core.applicationMap.get(data.name)
* Event: 'core.listentomanage'
* if (!app) {
* Start listening to changes in core manage ctx.log.warn(`listento called on non-existing app ${data.name}`)
*/ return
export async function listentomanage(ctx) { }
ctx.socket.join('core.manage') ctx.socket.join('app.' + data.name)
ctx.socket.emit('core.program.log', { let version = ctx.db.get(ctx.db.data.core[app.name].versions, ctx.db.data.core[app.name].latestInstalled)
name: 'manage', ctx.socket.emit('app.updatelog', {
logs: ctx.core.getProgramLogs('manage') name: data.name,
log: version?.log || ctx.db.data.core[app.name].updater
}) })
} }
@ -148,15 +144,18 @@ export async function listentomanage(ctx) {
* *
* Stop listening to new log lines * Stop listening to new log lines
*/ */
export async function unlistentoapp(ctx) { export async function unlistentoapp(ctx, data) {
ctx.socket.leave('core.app') if (!data.name) {
ctx.log.warn(`unlistento called with missing name`)
return
} }
/* let app = ctx.core.applicationMap.get(data.name)
* Event: 'core.unlistentomanage'
* if (!app) {
* Stop listening to new log lines ctx.log.warn(`unlistento called on non-existing app ${data.name}`)
*/ return
export async function unlistentomanage(ctx) { }
ctx.socket.leave('core.manage')
ctx.socket.leave('app.' + data.name)
} }

View file

@ -16,6 +16,8 @@ export default function defaults(options, def) {
if (isObject(item)) return defaults(item) if (isObject(item)) return defaults(item)
return item return item
}) })
} else if (out[key] instanceof Date) {
out[key] = new Date(out[key])
} else if (out[key] && typeof out[key] === 'object') { } else if (out[key] && typeof out[key] === 'object') {
out[key] = defaults(options[key], def && def[key]) out[key] = defaults(options[key], def && def[key])
} }

View file

@ -1,4 +1,5 @@
import * as core from './core/ioroutes.mjs' import * as core from './core/ioroutes.mjs'
import { getConfig, getStatus } from './util.mjs'
function register(ctx, name, method) { function register(ctx, name, method) {
if (typeof(method) === 'object') { if (typeof(method) === 'object') {
@ -46,6 +47,9 @@ function onConnection(server, ctx, data) {
}) })
register(ioCtx, 'core', core) register(ioCtx, 'core', core)
ioCtx.socket.emit('core.config', getConfig(ioCtx))
ioCtx.socket.emit('core.status', getStatus(ioCtx))
} }
export default onConnection export default onConnection

View file

@ -1,6 +1,6 @@
import socket from 'socket.io-serveronly' import socket from 'socket.io-serveronly'
import nStatic from 'node-static' import nStatic from 'node-static'
// import coremonitor from './core/coremonitor.mjs' import coremonitor from './core/coremonitor.mjs'
import onConnection from './routerio.mjs' import onConnection from './routerio.mjs'
@ -66,7 +66,7 @@ export function run(http, port, ctx) {
const io = new socket(server) const io = new socket(server)
io.on('connection', onConnection.bind(this, io, ctx)) io.on('connection', onConnection.bind(this, io, ctx))
// coremonitor(io, config, db, log, core) coremonitor(io, ctx)
return server.listenAsync(port) return server.listenAsync(port)
.then(function() { .then(function() {

View file

@ -1,3 +1,5 @@
import defaults from './defaults.mjs'
export function safeWrap(log, name, fn) { export function safeWrap(log, name, fn) {
return function(data, cb) { return function(data, cb) {
try { try {
@ -14,3 +16,41 @@ export function safeWrap(log, name, fn) {
} }
} }
} }
export function getConfig(ctx) {
let merge = {
applications: []
}
for (let app of ctx.core.applications) {
merge[app.name] = app.config
merge.applications.push(app.name)
}
return defaults(ctx.db.config, merge)
}
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,
updating: app.updating,
}
}
}
return status
}

View file

@ -26,6 +26,7 @@ m.mount(document.getElementById('header'), Header)
m.route(document.getElementById('content'), '/', { m.route(document.getElementById('content'), '/', {
'/': Status, '/': Status,
'/log': Log, '/log': Log,
'/log/:id': Log,
'/updater': Updater, '/updater': Updater,
'/updater/:id': Updater, '/updater/:id': Updater,
}) })

View file

@ -3,7 +3,8 @@ const socket = require('../socket')
const Module = require('../module') const Module = require('../module')
const Log = Module({ const Log = Module({
init: function() { init: function(vnode) {
this.activeAppName = 'service-core'
this.connected = socket.connected this.connected = socket.connected
this.loglines = [] this.loglines = []
this.logUpdated = false this.logUpdated = false
@ -14,21 +15,39 @@ const Log = Module({
m.redraw() 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() { 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 = [] this.loglines = []
socket.emit('core.listenlogs', {}) socket.emit('core.listenlogs', { name: this.activeAppName })
socket.emit('core.getlastlogs', {}, (res) => { socket.emit('core.getlastlogs', { name: this.activeAppName }, (res) => {
this.loglines = res.map(this.formatLine) this.loglines = res.map(this.formatLine)
this.logUpdated = true this.logUpdated = true
m.redraw() m.redraw()
}) })
m.redraw()
}, },
formatLine: function(line) { formatLine: function(line) {
@ -45,7 +64,18 @@ const Log = Module({
view: function() { view: function() {
return [ 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', { m('div#logger', {
onupdate: (vnode) => { onupdate: (vnode) => {
if (this.logUpdated) { if (this.logUpdated) {

View file

@ -1,12 +1,44 @@
const m = require('mithril')
const defaults = require('./defaults') const defaults = require('./defaults')
const socket = require('./socket') const socket = require('./socket')
const core = {
name: '...loading...',
title: '',
apps: [],
db: {},
status: {},
}
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) { module.exports = function Module(module) {
return defaults(module, { return defaults(module, {
init: function() {}, init: function() {},
oninit: function(vnode) { oninit: function(vnode) {
this._listeners = [] this._listeners = []
this.core = core
this.init(vnode) this.init(vnode)
}, },
@ -21,6 +53,11 @@ module.exports = function Module(module) {
}, },
on: function(name, cb) { 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]) this._listeners.push([name, cb])
socket.on(name, cb) socket.on(name, cb)
}, },
@ -31,7 +68,7 @@ module.exports = function Module(module) {
this.remove() this.remove()
if (!this._listeners) return if (!this._listeners) return
for (let i = 0; i < this._listeners.length; i++) { 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])
} }
}, },
}) })

View file

@ -1,121 +1,67 @@
const m = require('mithril') const m = require('mithril')
const socket = require('../socket') const socket = require('../socket')
const Module = require('../module') const Module = require('../module')
const util = require('../util')
const Status = Module({ const Status = Module({
init: function() { init: function() {
this._name = '...loading...'
this._apps = []
this._socketOn(() => this.loadData())
},
loadData: function() {
socket.emit('core.config', {}, (res) => {
this._apps = []
console.log('config', res)
this._name = res.name
if (res.title) {
this._name += ' - ' + res.title
}
for (let appName of res.applications) {
this._apps.push({
name: appName,
config: res[appName],
})
}
console.log(this._apps)
m.redraw()
})
this.on('core.db', (res) => {
console.log('db', res)
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() { remove: function() {
socket.emit('core.unlistencore', {})
}, },
restartClicked: function() { restartClicked: function() {
socket.emit('core.restart', {}) socket.emit('core.restart', {})
}, },
start: function(name) { startSoftware: function(name) {
socket.emit('core.updatestart', { socket.emit('core.start', {
name: name, name: name,
}) })
}, },
getStatus: function(active) {
if (active.updating) {
return '< Updating >'
} else {
return '< Starting >'
}
},
view: function() { view: function() {
let loopOver = [ let name = this.core.name
['Management service', '_management'], if (this.core.title) {
['Application service', '_app'], name += ' - ' + this.core.title
] }
return m('div#status', [ return m('div#status', [
m('h1.header', this._name), m('h1.header', name),
m('div.split', [ m('div.split', [
this._apps.map((app) => { this.core.apps.map((app) => {
return m('div.item', [ let box = []
m('h4', app.name), let appStatus = this.core.status[app.name] || {}
m('p', app.config.port
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}` ? `Port: ${app.config.port}`
: ''), : ''))
m('p', app.config.repository box.push(m('p', util.getRepoMessage(app)))
? `${app.config.repository}`
: '< no repository >'), box.push(m('p', 'Running version: ' + util.getVersionSummary(appStatus.active, appStatus)))
m('p', app.config.active box.push(m('p', 'Latest installed: ' + util.getVersionSummary(appStatus.latestInstalled)))
? `Running version: ${app.config.active}`
: '< no running version >'), if (!this.core.status[app.name].running) {
m('p', app.config.latestInstalled box.push(m('button', {
? `Latest installed: ${app.config.latestInstalled}` onclick: () => this.startSoftware(app.name),
: '< no version installed >'), }, 'Start'))
m('p', app.config.latestVersion } else if (app.config.scAllowStop) {
? `Latest version: ${app.config.latestVersion}` box.push(m('button.fatal', {
: '< no version found >'), onclick: () => this.startSoftware(app.name),
app.config.running !== null && app.config.repository }, 'Restart'))
? m('p', }
{ class: app.config.running ? 'running' : 'notrunning' },
app.config.running ? 'Running' : 'Not Running' return m('div.item', box)
)
: null,
!app.config.running && (app.config.updating || app.config.starting)
? m('div.status', this.getStatus(app.config))
: null,
m('button', {
hidden: app.config.running || app.config.updating || app.config.starting || !app.config.repository,
onclick: () => this.start(app.config.name),
}, 'Update/Start')
])
}), }),
]), ]),
m('button', { m('button.fatal', {
hidden: !this.core.allowRestart,
onclick: () => this.restartClicked(), onclick: () => this.restartClicked(),
}, 'Restart service') }, 'Restart ' + this.core.name)
]) ])
} }
}) })

View file

@ -1,142 +1,123 @@
const m = require('mithril') const m = require('mithril')
const socket = require('../socket') const socket = require('../socket')
const Module = require('../module') const Module = require('../module')
const util = require('../util')
const Updater = Module({ const Updater = Module({
init: function(vnode) { init: function(vnode) {
this.activeApp = vnode.attrs.id || null this.activeApp = null
this.appRepository = null this.activeAppName = null
this.manageRepository = null
this.db = null
this.app = {}
this.status = {}
this.logUpdated = false this.logUpdated = false
this._socketOn(() => this.socketOpen()) this._socketOn(() => this.socketOpen())
this._active = null
if (this.activeApp && this.activeApp !== 'app'&& this.activeApp !== 'manage') { this.on('app.updatelog', (res) => {
return m.route('/updater') if (!this.activeApp || this.activeApp.name !== res.name) return
} this.activeApp.log = res.log.replace(/\. /g, '.\n')
},
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.logUpdated = true this.logUpdated = true
m.redraw() m.redraw()
}) })
if (this.core.apps.length && vnode.attrs.id) {
socket.emit('core.listencore', {}) this.updateActiveApp(vnode.attrs.id)
}, } else if (this.core.apps.length) {
let apps = this.core.apps.filter(function(app) {
updateActiveDb() { return Boolean(app.config.url)
if (this.db && this.activeApp) { })
this.app = { if (apps.length === 1) {
repository: this[this.activeApp + 'Repository'], return m.route.set('/updater/' + apps[0].name)
active: this.db[this.activeApp + 'Active'],
latestInstalled: this.db[this.activeApp + 'LatestInstalled'],
latestVersion: this.db[this.activeApp + 'LatestVersion'],
logs: '',
} }
} else {
this.app = {}
} }
}, },
loadAppData() { onupdate: function(vnode) {
this.updateActiveDb() if (this.core.apps.length) {
if (this.activeApp === 'app') { if (!vnode.attrs.id) {
socket.emit('core.unlistentomanage', {}) let apps = this.core.apps.filter(function(app) {
socket.emit('core.listentoapp', {}) return Boolean(app.config.url)
} else { })
socket.emit('core.unlistentoapp', {}) if (apps.length === 1) {
socket.emit('core.listentomanage', {}) return m.route.set('/updater/' + apps[0].name)
} }
/* request to listen to app updates */ }
this.updateActiveApp(vnode.attrs.id)
}
},
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() { remove: function() {
socket.emit('core.unlistencore', {}) if (this.activeApp) {
socket.emit('core.unlistentoapp', {}) socket.emit('core.unlistentoapp', { name: this.activeApp.name })
socket.emit('core.unlistentomanage', {}) }
}, },
startUpdate: function() { startUpdate: function() {
socket.emit('core.update', { socket.emit('core.update', {
name: this.activeApp, name: this.activeApp.name,
}) })
}, },
startSoftware: function() { startSoftware: function() {
socket.emit('core.start', { socket.emit('core.start', {
name: this.activeApp, name: this.activeApp.name,
}) })
}, },
view: function() { view: function() {
let apps = this.core.apps.filter(function(app) {
return Boolean(app.config.url)
})
return m('div#update', [ return m('div#update', [
m('div.actions', [ m('div.actions', [
m('h1.header', 'Updater'), apps.map((app) => {
m('div.filler'), return m(m.route.Link, {
m(m.route.Link, { class: 'button ' + (this.activeAppName === app.name ? 'active' : 'inactive'),
hidden: !this.appRepository, href: '/updater/' + app.name,
class: 'button ' + (this.activeApp === 'app' ? 'active' : 'inactive'), }, app.name)
href: '/updater/app', }),
}, 'Update App'),
m(m.route.Link, {
hidden: !this.manageRepository,
class: 'button ' + (this.activeApp === 'manage' ? 'active' : 'inactive'),
href: '/updater/manage',
}, 'Update Manager'),
]), ]),
this.activeApp && this.app ? [ this.activeApp ? (() => {
let appStatus = this.core.status[this.activeApp.name] || {}
console.log(appStatus)
return [
m('div.info', [ m('div.info', [
m('p', this.app.repository m('p', util.getRepoMessage(this.activeApp)),
? `Repository: ${this.app.repository}` m('p', 'Active version: ' + util.getVersionSummary(appStatus.active, appStatus)),
: '< no repository >'), m('p', appStatus.updating
m('p', this.app.latestInstalled ? 'Updating...'
? `Latest installed: ${this.app.latestInstalled}` : appStatus.running ? 'Running ' + appStatus.running : 'Not running'),
: '< no version installed >'), m('p', 'Latest installed: ' + util.getVersionSummary(appStatus.latestInstalled, appStatus)),
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', { m('div.console', {
onupdate: (vnode) => { onupdate: (vnode) => {
@ -146,26 +127,20 @@ const Updater = Module({
} }
} }
}, },
m('pre', this.app.logs && this.app.logs || '') m('pre', this.activeApp.log || '')
), ),
this.db m('div.actions', {
? m('div.actions', { hidden: appStatus.updating,
hidden: this.status[this.activeApp + 'Updating'],
}, [ }, [
m('button', { 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(), onclick: () => this.startSoftware(),
}, 'Start'), }, appStatus.running ? 'Restart' : 'Start'),
m('button', { m('button', {
hidden: !this.db[this.activeApp + 'LastActive'] onclick: () => this.startUpdate(),
|| this.db[this.activeApp + 'LastActive'] === this.db[this.activeApp + 'Active'] }, 'Check for updates'),
}, 'Use Last Version'), ]),
]) ]
: null, })() : null
] : null
]) ])
} }
}) })

42
client/util.js Normal file
View file

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

14
dev.mjs
View file

@ -7,15 +7,25 @@ const helloWorldConfig = {
"provider": "git", "provider": "git",
"url": "https://git.nfp.is/api/v1/repos/thething/sc-helloworld/releases", "url": "https://git.nfp.is/api/v1/repos/thething/sc-helloworld/releases",
"port": 8888, "port": 8888,
"scAllowStop": true,
} }
var core = new ServiceCore('sc-manager', import.meta.url) 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['hello-world'] = helloWorldConfig
core.config.allowRestart = true
core.setConfig({ core.setConfig({
port: 4271, port: 4271,
}) })
core.init(index).then(function() { 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 provConstructor = Core.providers.get(core.db.config['hello-world'].provider)
let provider = new provConstructor(core.db.config['hello-world']) let provider = new provConstructor(core.db.config['hello-world'])
@ -33,9 +43,9 @@ core.init(index).then(function() {
return core.run() return core.run()
}).then(function() { }).then(function() {
return core.core.applicationMap.get('hello-world').update() // return core.core.applicationMap.get('hello-world').update()
}).then(function() { }).then(function() {
return core.core.runApplication(core.core.applicationMap.get('hello-world')) // return core.core.runApplication(core.core.applicationMap.get('hello-world'))
}).then(function() { }).then(function() {
let helloApp = core.core.applicationMap.get('hello-world') let helloApp = core.core.applicationMap.get('hello-world')
helloApp.on('updated', core.core.runApplication.bind(core.core, helloApp)) helloApp.on('updated', core.core.runApplication.bind(core.core, helloApp))

View file

@ -1,5 +1,5 @@
{ {
"name": "app", "name": "sc-manager",
"version": "1.0.0", "version": "1.0.0",
"description": "", "description": "",
"main": "index.js", "main": "index.js",
@ -40,6 +40,6 @@
"devDependencies": { "devDependencies": {
"asbundle": "^2.6.1", "asbundle": "^2.6.1",
"mithril": "^2.0.4", "mithril": "^2.0.4",
"service-core": "^3.0.0-beta.11" "service-core": "^3.0.0-beta.13"
} }
} }

BIN
public/favicon-16x16.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 474 B

BIN
public/favicon-32x32.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 543 B

BIN
public/favicon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

View file

@ -1,6 +1,8 @@
<!DOCTYPE html> <!DOCTYPE html>
<html lang="en"> <html lang="en">
<head> <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="text/html;charset=utf-8" http-equiv="Content-Type">
<meta content="utf-8" http-equiv="encoding"> <meta content="utf-8" http-equiv="encoding">
<title>Service Core</title> <title>Service Core</title>

View file

@ -87,11 +87,33 @@ a.button {
text-align: center; text-align: center;
} }
button.fatal,
a.button.fatal {
background: hsl(0, 100%, 36%);
color: white;
border-color: hsl(0, 100%, 5%);
}
pre { pre {
margin: 0; margin: 0;
padding: 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 ********************/
#header { #header {
display: flex; display: flex;
@ -165,44 +187,38 @@ pre {
align-self: center; align-self: center;
width: calc(100% - 4rem); width: calc(100% - 4rem);
display: flex; display: flex;
flex-direction: column;
max-width: 700px; max-width: 700px;
border: 1px solid #999; border: 1px solid #999;
border-right: none;
} }
#status .item { #status .item {
flex: 2 1 50%; flex: 2 1 50%;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
border-right: 1px solid #999;
padding-bottom: 0.5rem; padding-bottom: 0.5rem;
} }
#status .item h4 { #status .item h4 {
margin-top: -1px;
font-size: 1.2rem; font-size: 1.2rem;
text-align: center; text-align: center;
padding: 1rem; padding: 1rem;
border-bottom: 1px solid #999; border-bottom: 1px solid #999;
border-top: 1px solid #999;
margin-bottom: 0.5rem; margin-bottom: 0.5rem;
} }
#status .item p { #status .item p {
padding: 0.25rem 1rem; padding: 0.25rem 1rem;
} }
#status .item p.running { #status .item .running {
color: hsl(118, 84%, 46.3%); color: hsl(118, 84%, 46.3%);
text-align: center;
} }
#status .item p.notrunning { #status .item .notrunning {
color: hsl(354.7, 85.8%, 67.6%); color: hsl(354.7, 85.8%, 67.6%);
text-align: center;
} }
#status button { #status button {
margin-top: 1rem; margin-top: 1rem;
} }
#status .status {
margin-top: 1rem;
align-self: center;
padding: 0.5rem;
color: #ccc;
}
/***************** Updater ********************/ /***************** Updater ********************/
#update { #update {
@ -212,22 +228,6 @@ pre {
align-self: center; align-self: center;
max-width: 700px; 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) { @media only screen and (max-device-width: 590px) {
#update .actions .filler { #update .actions .filler {
@ -273,7 +273,7 @@ pre {
@media only screen and (min-height: 650px) { @media only screen and (min-height: 650px) {
#update .console { #update .console {
height: calc(100vh - 300px); height: calc(100vh - 280px);
} }
} }