diff --git a/apps/.gitkeep b/app/.gitkeep similarity index 100% rename from apps/.gitkeep rename to app/.gitkeep diff --git a/app/v1.0.1.5/v1.0.1.5.zip b/app/v1.0.1.5/v1.0.1.5.zip new file mode 100644 index 0000000..760124a Binary files /dev/null and b/app/v1.0.1.5/v1.0.1.5.zip differ diff --git a/config.json b/config.json index 49fe93e..4b0ca1f 100644 --- a/config.json +++ b/config.json @@ -2,5 +2,10 @@ "name": "service-core", "serviceName": "Service-Core Node", "description": "NodeJS Test Service", - "port": 4269 + "port": 4270, + "managePort": 4269, + "hasManage": true, + "appRepository": "thething/sc-helloworld", + "manageRepository": null, + "useDev": true } \ No newline at end of file diff --git a/core/client.mjs b/core/client.mjs new file mode 100644 index 0000000..55329ed --- /dev/null +++ b/core/client.mjs @@ -0,0 +1,69 @@ +import http from 'http' +import https from 'https' +import fs from 'fs' +import { URL } from 'url' + +export function request(path, filePath = null, redirects = 0) { + let parsed = new URL(path) + + let h + if (parsed.protocol === 'https:') { + h = https + } else { + h = http + } + + return new Promise(function(resolve, reject) { + if (!path) { + return reject(new Error('Request path was empty')) + } + let req = h.request({ + path: parsed.pathname + parsed.search, + port: parsed.port, + method: 'GET', + headers: { + 'User-Agent': 'TheThing/service-core', + Accept: 'application/vnd.github.v3+json' + }, + hostname: parsed.hostname + }, function(res) { + let output = '' + if (filePath) { + let file = fs.createWriteStream(filePath) + res.pipe(file) + } else { + res.on('data', function(chunk) { + output += chunk + }) + } + res.on('end', function() { + if (res.statusCode >= 300 && res.statusCode < 400) { + if (redirects > 5) { + return reject(new Error(`Too many redirects (last one was ${res.headers.location})`)) + } + return resolve(request(res.headers.location, filePath, redirects + 1)) + } else if (res.statusCode >= 400) { + return reject(new Error(`HTTP Error ${statusCode}: ${output}`)) + } + resolve({ + statusCode: res.statusCode, + status: res.statusCode, + statusMessage: res.statusMessage, + headers: res.headers, + body: output + }) + }) + req.on('error', reject) + }) + req.end() + }).then(function(res) { + if (!filePath) { + try { + res.body = JSON.parse(res.body) + } catch(e) { + throw new Error(res.body) + } + } + return res + }) +} \ No newline at end of file diff --git a/core/core.mjs b/core/core.mjs new file mode 100644 index 0000000..f5427e9 --- /dev/null +++ b/core/core.mjs @@ -0,0 +1,186 @@ +import fs from 'fs' +import { EventEmitter } from 'events' +import { request } from './client.mjs' +import { getPathFromRoot, runCommand } from './util.mjs' + +const fsp = fs.promises + +export default class Core extends EventEmitter{ + constructor(config, db, log, closeCb) { + super() + this._config = config + this._db = db + this._log = log + this._close = closeCb + this._appRunning = false + this._manageRunning = false + this._appUpdating = { + status: false, + logs: '', + } + this._manageUpdating = { + status: false, + logs: '', + } + } + + restart() { + this._close() + } + + status() { + return { + app: this._appRunning, + manage: this._manageRunning, + appUpdating: this._appUpdating.status, + manageUpdating: this._manageUpdating.status, + } + } + + async getLatestVersion(active, name) { + // Example: 'https://api.github.com/repos/thething/sc-helloworld/releases' + this.logActive(name, active, `Fetching release info from: https://api.github.com/repos/${this._config[name + 'Repository']}/releases\n`) + + + let result = await request(`https://api.github.com/repos/${this._config[name + 'Repository']}/releases`) + + let items = result.body.filter(function(item) { + if (!item.assets.length) return false + for (let i = 0; i < item.assets.length; i++) { + if (item.assets[i].name.endsWith('-sc.zip')) return true + } + }) + + if (items && items.length) { + for (let x = 0; x < items.length; x++) { + let item = items[x] + for (let i = 0; i < item.assets.length; i++) { + if (item.assets[i].name.endsWith('-sc.zip')) { + this.logActive(name, active, `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 (!logline.replace) { + console.log(logline) + } + if (!doNotPrint) { + this._log.info(`Log ${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(getPathFromRoot('./app/' + version.name))) { + await runCommand('rmdir', ['/S', '/Q', `"${getPathFromRoot('./app/' + version.name)}"`]) + } + await fsp.mkdir(getPathFromRoot('./app/' + version.name)) + this.logActive(name, active, `Downloading ${version.name} (${version.url}) to ${version.name + '/' + version.name + '.zip'}\n`) + let filePath = getPathFromRoot('./app/' + version.name + '/' + version.name + '.zip') + await request(version.url, filePath) + this.logActive(name, active, `Downloading finished, starting extraction\n`) + await runCommand('"C:\\Program Files\\7-Zip\\7z.exe"', ['e', `"${filePath}"`], this.logActive.bind(this, name, active)) + // await request(version) + // request(config[name + 'Repository']) + } + + async startProgram(name) { + } + + async updateProgram(name) { + if (!this._config[name + 'Repository']) { + if (name === 'app') { + this._log.error(name + 'Repository was missing from config') + this._log.event.error(name + 'Repository was missing from config') + } else { + this._log.warn(name + 'Repository was missing from config') + this._log.event.warn(name + 'Repository was missing from config') + } + return + } + + let active = null + if (name === 'app') { + active = this._appUpdating + } else { + active = this._manageUpdating + } + active.status = true + active.logs = '' + + this.emit('statusupdated', {}) + this.logActive(name, active, 'Checking for updates...\n') + + let version = null + try { + version = await this.getLatestVersion(active, name) + let core = this._db.get('core').value() + if (!core[name + 'Current'] || (core[name + 'Current'] !== version.name && core[name + 'CurrentVersion'] !== version)) { + let oldVersion = core[name + 'Current'] || '' + this.logActive(name, active, `Updating from ${oldVersion} to ${version.name}\n`) + await this.installVersion(name, active, version) + } + } catch(err) { + this.logActive(name, active, '\n', true) + this.logActive(name, active, `Exception occured while updating ${name}\n`, true) + this.logActive(name, active, err.stack, true) + this._log.error(err, 'Error while updating ' + name) + } + active.status = false + if (version) { + 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, + }).write() + } + this.emit('statusupdated', {}) + } + + async start(name) { + await this.updateProgram(name) + if (core[name + 'CurrentVersion']) { + await this.startProgram(name) + } + } +} \ No newline at end of file diff --git a/core/test.mjs b/core/test.mjs new file mode 100644 index 0000000..659accf --- /dev/null +++ b/core/test.mjs @@ -0,0 +1,7 @@ +import * as client from './client.mjs' + +client.request('https://api.github.com/repos/thething/sc-helloworld/releases') + .then( + a => console.log('res:', a), + err => console.error('err', err) + ).then(() => process.exit(0)) diff --git a/core/util.mjs b/core/util.mjs new file mode 100644 index 0000000..5e1c870 --- /dev/null +++ b/core/util.mjs @@ -0,0 +1,28 @@ +import path from 'path' +import { spawn } from 'child_process' +import { fileURLToPath } from 'url' + +export function getPathFromRoot(add) { + const __dirname = path.dirname(fileURLToPath(import.meta.url)); + return path.join(__dirname,'../', add) +} + +export function runCommand(command, options = [], stream = function() {}) { + return new Promise(function(res, rej) { + let processor = spawn(command, options, {shell: true}) + processor.stdout.on('data', function(data) { + stream(data.toString()) + processor.stdin.write('n') + }) + processor.stderr.on('data', function(data) { + stream(data.toString()) + processor.stdin.write('n') + }) + processor.on('error', function(err) { + rej(err) + }) + processor.on('exit', function (code) { + res(code) + }) + }) +} diff --git a/db.mjs b/db.mjs index 902865a..240a4a0 100644 --- a/db.mjs +++ b/db.mjs @@ -139,7 +139,18 @@ export default function GetDB(log) { db._.mixin(lodashId) db.defaults({ - lastActiveVersion: null, + core: { + "appActive": null, // Current active running + "appLastActive": null, // Last active stable running + "appLatestInstalled": null, // Latest installed version + "appLatestVersion": null, // Newest version available + "manageActive": null, + "manageLastActive": null, + "manageLatestInstalled": null, + "manageLatestVersion": null + }, + core_appHistory: [], + core_manageHistory: [], version: 1, }) .write() diff --git a/dev/api/core/coremonitor.mjs b/dev/api/core/coremonitor.mjs new file mode 100644 index 0000000..86f5c88 --- /dev/null +++ b/dev/api/core/coremonitor.mjs @@ -0,0 +1,20 @@ +import { formatLog } from './loghelper.mjs' +import { safeWrap } from '../util.mjs' + +export default function coremonitor(io, config, db, log, core) { + log.on('newlog', safeWrap(log, 'coremonitor.on.newlog', function(data) { + io.to('logger').emit('newlog', formatLog(data)) + })) + core.on('dbupdated', safeWrap(log, 'coremonitor.on.dbupdated', function() { + io.to('core').emit('core.db', db.get('core').value()) + })) + core.on('statusupdated', safeWrap(log, 'coremonitor.on.statusupdated', function() { + io.to('core').emit('core.status', core.status()) + })) + core.on('applog', safeWrap(log, 'coremonitor.on.applog', function(app) { + io.to('core.app').emit('core.program.log', { + name: 'app', + logs: app.logs, + }) + })) +} \ No newline at end of file diff --git a/dev/api/core/ioroutes.mjs b/dev/api/core/ioroutes.mjs new file mode 100644 index 0000000..0703049 --- /dev/null +++ b/dev/api/core/ioroutes.mjs @@ -0,0 +1,104 @@ +import { formatLog } from './loghelper.mjs' + +/* + * Event: 'core.config' + * + * Get config + */ +export async function config(ctx, data, cb) { + cb(ctx.config) +} + +/* + * Event: 'core.restart' + * + * Restart server + */ +export async function restart(ctx, data, cb) { + ctx.core.restart() +} + +/* + * Event: 'core.getlastlogs' + * + * Returns last few log messages from log + */ +export async function getlastlogs(ctx, data, cb) { + cb(ctx.logroot.ringbuffer.records.map(formatLog)) +} + +/* + * Event: 'core.listenlogs' + * + * Start listening to new log lines + */ +export async function listenlogs(ctx) { + ctx.socket.join('logger') +} + +/* + * Event: 'core.unlistenlogs' + * + * Stop listening to new log lines + */ +export async function unlistenlogs(ctx) { + ctx.socket.leave('logger') +} + +/* + * Event: 'core.update' + * + * Update specific software + */ +export async function update(ctx, data, cb) { + if (data.name === 'app') { + await ctx.core.updateProgram('app') + } else if (data.name === 'manage') { + await ctx.core.updateProgram('manage') + } else { + ctx.log.warn('Invalid update command for app ' + data.name) + ctx.log.event.warn('Invalid update command for app ' + 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') +} + +/* + * Event: 'core.listentoapp' + * + * Start listening to changes in core app + */ +export async function listentoapp(ctx) { + ctx.socket.join('core.app') + ctx.socket.emit('core.program.log', { + name: 'app', + logs: ctx.core.getProgramLogs('app') + }) +} + +/* + * Event: 'core.unlistentoapp' + * + * Stop listening to new log lines + */ +export async function unlistentoapp(ctx) { + ctx.socket.leave('core.app') +} diff --git a/example/api/core/loghelper.mjs b/dev/api/core/loghelper.mjs similarity index 96% rename from example/api/core/loghelper.mjs rename to dev/api/core/loghelper.mjs index c648fb0..59e878f 100644 --- a/example/api/core/loghelper.mjs +++ b/dev/api/core/loghelper.mjs @@ -59,6 +59,10 @@ function stylize(str, color) { } } +function indent(s) { + return ' ' + s.split(/\r?\n/).join('\n '); +} + export function formatLog(data) { let rec = _.cloneDeep(data) @@ -151,6 +155,9 @@ export function formatLog(data) { var stringified = false; if (typeof (value) !== 'string') { value = JSON.stringify(value, null, 2); + if (typeof (value) !== 'string') { + value = 'null' + } stringified = true; } if (value.indexOf('\n') !== -1 || value.length > 50) { diff --git a/example/api/routerio.mjs b/dev/api/routerio.mjs similarity index 86% rename from example/api/routerio.mjs rename to dev/api/routerio.mjs index 87c82a9..8a2c3cd 100644 --- a/example/api/routerio.mjs +++ b/dev/api/routerio.mjs @@ -9,7 +9,7 @@ function register(ctx, name, method) { } ctx.socket.on(name, async function(data, cb) { - ctx.log.debug('Got event', name) + ctx.log.debug('SocketIO: ' + name) try { await method(ctx, data, cb) @@ -21,7 +21,7 @@ function register(ctx, name, method) { } -function onConnection(server, config, db, log, data) { +function onConnection(server, config, db, log, coreService, data) { const io = server const socket = data @@ -37,6 +37,7 @@ function onConnection(server, config, db, log, data) { socket, log: child, db, + core: coreService, logroot: log, } diff --git a/example/api/server.mjs b/dev/api/server.mjs similarity index 79% rename from example/api/server.mjs rename to dev/api/server.mjs index 8ff551a..adaa063 100644 --- a/example/api/server.mjs +++ b/dev/api/server.mjs @@ -3,11 +3,11 @@ import path from 'path' import { fileURLToPath } from 'url' import socket from 'socket.io-serveronly' import nStatic from 'node-static' -import logmonitor from './core/logmonitor.mjs' +import coremonitor from './core/coremonitor.mjs' import onConnection from './routerio.mjs' -export function run(config, db, log, next) { +export function run(config, db, log, core) { const __dirname = path.dirname(fileURLToPath(import.meta.url)) const staticRoot = path.join(__dirname,'../public') @@ -66,15 +66,17 @@ export function run(config, db, log, next) { }) const io = new socket(server) - io.on('connection', onConnection.bind(this, io, config, db, log)) + io.on('connection', onConnection.bind(this, io, config, db, log, core)) - logmonitor(io, config, db, log) + coremonitor(io, config, db, log, core) - server.listen(config.port, '0.0.0.0', function(err) { + server.listen(config.managePort, '0.0.0.0', function(err) { if (err) { log.fatal(err) + log.event.error('Error starting server: ' + err.message) return process.exit(2) } - log.info(`Server is listening on ${config.port} serving files on ${staticRoot}`) + log.event.info(`Server is listening on ${config.managePort} serving files on ${staticRoot}`) + log.info(`Server is listening on ${config.managePort} serving files on ${staticRoot}`) }) } \ No newline at end of file diff --git a/dev/api/util.mjs b/dev/api/util.mjs new file mode 100644 index 0000000..aff5296 --- /dev/null +++ b/dev/api/util.mjs @@ -0,0 +1,16 @@ +export function safeWrap(log, name, fn) { + return function(data, cb) { + try { + let out = fn(data, cb) + if (out && out.then) { + out.then(function() {}, function(err) { + log.error(err, 'Unknown error in ' + name) + log.event.error('Unknown error occured in ' + name + ': ' + err.message) + }) + } + } catch (err) { + log.error(err, 'Unknown error in ' + name) + log.event.error('Unknown error occured in ' + name + ': ' + err.message) + } + } +} \ No newline at end of file diff --git a/example/app/client.js b/dev/app/client.js similarity index 100% rename from example/app/client.js rename to dev/app/client.js diff --git a/dev/app/defaults.js b/dev/app/defaults.js new file mode 100644 index 0000000..01f6560 --- /dev/null +++ b/dev/app/defaults.js @@ -0,0 +1,34 @@ + +// taken from isobject npm library +function isObject(val) { + return val != null && typeof val === 'object' && Array.isArray(val) === false +} + +module.exports = 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] && 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 +} diff --git a/example/app/header.js b/dev/app/header.js similarity index 73% rename from example/app/header.js rename to dev/app/header.js index 4f065d8..a5001b2 100644 --- a/example/app/header.js +++ b/dev/app/header.js @@ -15,20 +15,25 @@ const Header = { }) }, view: function() { - let path = m.route.get() + let path = m.route.get() || '' return [ m('div.seperator'), m(m.route.Link, { href: '/', - class: path === '/' ? 'active' : '', - }, 'Frontpage'), + class: path === '/' || path === '' ? 'active' : '', + }, 'Status'), m('div.seperator'), m(m.route.Link, { href: '/log', class: path === '/log' ? 'active' : '', }, 'Log'), m('div.seperator'), + m(m.route.Link, { + href: '/updater', + class: path.startsWith('/updater') ? 'active' : '', + }, 'Updater'), + m('div.seperator'), !this.connected && m('div.disconnected', ` Lost connection with server, Attempting to reconnect `) || null, diff --git a/example/app/index.js b/dev/app/index.js similarity index 78% rename from example/app/index.js rename to dev/app/index.js index c65fd63..f63e6c5 100644 --- a/example/app/index.js +++ b/dev/app/index.js @@ -17,12 +17,15 @@ require('./socket') const m = require('mithril') const Header = require('./header') -const Frontpage = require('./frontpage/frontpage') +const Status = require('./status/status') const Log = require('./log/log') +const Updater = require('./updater/updater') m.mount(document.getElementById('header'), Header) m.route(document.getElementById('content'), '/', { - '/': Frontpage, + '/': Status, '/log': Log, + '/updater': Updater, + '/updater/:id': Updater, }) diff --git a/example/app/log/log.js b/dev/app/log/log.js similarity index 81% rename from example/app/log/log.js rename to dev/app/log/log.js index 7a7bb82..bf9c903 100644 --- a/example/app/log/log.js +++ b/dev/app/log/log.js @@ -1,27 +1,27 @@ const m = require('mithril') const socket = require('../socket') +const Module = require('../module') -const Log = { - oninit: function() { +const Log = Module({ + init: function() { this.connected = socket.connected this.loglines = [] - socket.on('newlog', data => { + this.on('newlog', data => { this.loglines.push(this.formatLine(data)) m.redraw() }) - socket.on('connect', () => { - this.loglines = [] - this.loadData() - socket.emit('core.listenlogs', {}) - m.redraw() - }) + this._socketOn(() => this.loadData()) + }, - this.loadData() + remove: function() { + socket.emit('core.unlistenlogs', {}) }, loadData: function() { + this.loglines = [] + socket.emit('core.listenlogs', {}) socket.emit('core.getlastlogs', {}, (res) => { this.loglines = res.map(this.formatLine) m.redraw() @@ -51,6 +51,6 @@ const Log = { ]), ] } -} +}) module.exports = Log diff --git a/dev/app/module.js b/dev/app/module.js new file mode 100644 index 0000000..5f4b6f9 --- /dev/null +++ b/dev/app/module.js @@ -0,0 +1,38 @@ +const defaults = require('./defaults') +const socket = require('./socket') + +module.exports = function Module(module) { + return defaults(module, { + init: function() {}, + + oninit: function(vnode) { + this._listeners = [] + this.init(vnode) + }, + + _listeners: null, + + _socketOn: function(cb) { + socket.on('connect', () => cb()) + + if (socket.connected) { + cb() + } + }, + + on: function(name, cb) { + this._listeners.push([name, cb]) + socket.on(name, cb) + }, + + remove: function() {}, + + onremove: function() { + this.remove() + if (!this._listeners) return + for (let i = 0; i < this._listeners.length; i++) { + socket.removeListener(this._listeners[0], this._listeners[1]) + } + }, + }) +} diff --git a/example/app/socket.js b/dev/app/socket.js similarity index 100% rename from example/app/socket.js rename to dev/app/socket.js diff --git a/dev/app/status/status.js b/dev/app/status/status.js new file mode 100644 index 0000000..8278bdd --- /dev/null +++ b/dev/app/status/status.js @@ -0,0 +1,119 @@ +const m = require('mithril') +const socket = require('../socket') +const Module = require('../module') + +const Status = Module({ + init: function() { + this._name = '...loading...' + this._management = { + port: null, + repository: null, + active: null, + latestInstalled: null, + lastActive: null, + latestVersion: null, + running: null, + } + this._app = { + port: null, + repository: null, + active: null, + latestInstalled: null, + lastActive: null, + latestVersion: null, + running: 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.lastActive = res.manageLastActive + this._management.latestVersion = res.manageLatestVersion + this._app.active = res.appActive + this._app.latestInstalled = res.appLatestInstalled + this._app.lastActive = res.appLastActive + this._app.latestVersion = res.appLatestVersion + + m.redraw() + }) + + this.on('core.status', (res) => { + this._management.running = res.manage + this._app.running = res.app + + m.redraw() + }) + + socket.emit('core.listencore', {}) + }, + + remove: function() { + socket.emit('core.unlistencore', {}) + }, + + restartClicked: function() { + socket.emit('core.restart', {}) + }, + + view: function() { + let loopOver = [ + ['Management service', '_management'], + ['Application service', '_app'], + ] + return m('div#status', [ + m('h1.header', this._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]].lastActive + ? `Last stable version: ${this[group[1]].lastActive}` + : '< no last stable version >'), + m('p', this[group[1]].latestVersion + ? `Latest version: ${this[group[1]].latestVersion}` + : '< no version found >'), + this[group[1]].running !== null + ? m('p', + { class: group[1].running ? 'running' : 'notrunning' }, + group[1].running ? 'Running' : 'Not Running' + ) + : null, + m('button', { + + }, 'Update/Start') + ]) + }), + ]), + m('button', { + onclick: () => this.restartClicked(), + }, 'Restart service') + ]) + } +}) + +module.exports = Status diff --git a/dev/app/updater/updater.js b/dev/app/updater/updater.js new file mode 100644 index 0000000..bea05bf --- /dev/null +++ b/dev/app/updater/updater.js @@ -0,0 +1,161 @@ +const m = require('mithril') +const socket = require('../socket') +const Module = require('../module') + +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.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.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'], + lastActive: this.db[this.activeApp + 'LastActive'], + latestInstalled: this.db[this.activeApp + 'LatestInstalled'], + latestVersion: this.db[this.activeApp + 'LatestVersion'], + logs: '', + } + } else { + this.app = {} + } + }, + + loadAppData() { + this.updateActiveDb() + if (this.activeApp === 'app') { + socket.emit('core.listentoapp', {}) + } + /* request to listen to app updates */ + }, + + remove: function() { + socket.emit('core.unlistencore', {}) + socket.emit('core.unlistentoapp', {}) + }, + + startUpdate: function() { + socket.emit('core.update', { + name: this.activeApp, + }) + }, + + view: function() { + 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' : ''), + href: '/updater/app', + }, 'Update App'), + m(m.route.Link, { + hidden: this.manageRepository, + class: 'button' + (this.activeApp === 'manage' ? ' active' : ''), + href: '/updater/manage', + }, 'Update Manager'), + ]), + this.activeApp && this.app ? [ + m('h4', this.app.repository + ? `${this.app.repository}` + : '< no repository >'), + m('div.info', [ + m('p', this.app.active + ? `Running version: ${this.app.active}` + : '< no running version >'), + m('p', this.app.latestInstalled + ? `Latest installed: ${this.app.latestInstalled}` + : '< no version installed >'), + m('p', this.app.lastActive + ? `Last stable version: ${this.app.lastActive}` + : '< no last stable 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 + } + } + }, + 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.db[this.activeApp + 'LastActive'] + || this.db[this.activeApp + 'LastActive'] === this.db[this.activeApp + 'Active'] + }, 'Use Last Version'), + ]) + : null, + ] : null + ]) + } +}) + +module.exports = Updater diff --git a/example/build.bat b/dev/build.bat similarity index 100% rename from example/build.bat rename to dev/build.bat diff --git a/dev/index.mjs b/dev/index.mjs new file mode 100644 index 0000000..9e27ec0 --- /dev/null +++ b/dev/index.mjs @@ -0,0 +1,5 @@ +export function start(config, db, log, core) { + return import('./api/server.mjs').then(function(module) { + return module.run(config, db, log, core) + }) +} \ No newline at end of file diff --git a/example/package.json b/dev/package.json similarity index 100% rename from example/package.json rename to dev/package.json diff --git a/example/public/index.html b/dev/public/index.html similarity index 100% rename from example/public/index.html rename to dev/public/index.html diff --git a/dev/public/main.css b/dev/public/main.css new file mode 100644 index 0000000..8a12380 --- /dev/null +++ b/dev/public/main.css @@ -0,0 +1,270 @@ +html { + box-sizing: border-box; + font-size: 16px; + height: 100%; +} + +*, *:before, *:after { + box-sizing: inherit; +} + +body, h1, h2, h3, h4, h5, h6, p { + margin: 0; + padding: 0; + font-weight: normal; +} + +body { + background: #3d3d3d; + color: #f1f1f1; + display: flex; + font-size: 16px; + min-height: 100vh; + flex-direction: column; + font-family: Helvetica, sans-serif, Arial; +} + +a, a:visited { + color: #eee; + text-decoration: underline; +} + +/* Container */ +.container { + display: flex; + align-items: stretch; + flex-direction: column; + flex-grow: 2; +} + +.disconnected { + position: fixed; + top: 0; + left: 0; + right: 0; + bottom: 0; + background: rgba(0,0,0,0.8); + color: #fff; + font-size: 1em; + display: flex; + justify-content: center; + align-items: center; +} + +[hidden] { display: none !important; } + +main { + display: flex; + flex-direction: column; + align-items: stretch; + flex-grow: 2; + width: 100%; +} + +main h1 { + text-align: center; + font-size: 1.6rem; + line-height: 3.6rem; +} + +form.login { + align-self: center; + display: flex; + flex-direction: column; +} + +button, +a.button { + align-self: center; + min-width: 200px; + background: #ffb843; + color: black; + padding: 0.5rem; + border: none; + cursor: pointer; + text-decoration: none; + border: 1px solid hsl(37.3, 80%, 37%); + text-align: center; +} + +pre { + margin: 0; + padding: 0; +} + +/***************** Header ********************/ +#header { + display: flex; + justify-content: center; + width: 100%; + background: #292929; + align-items: center; +} + +#header a { + width: 150px; + text-align: center; + padding: 1rem; + color: #ffb843; +} + +#header a.active { + color: #999; + text-decoration: none; +} + +#header .seperator { + border-right: 1px solid #999; + height: 1rem; +} + +/***************** Log ********************/ +#logger { + margin: 0 2rem; + display: flex; + flex-direction: column; + border: 1px solid #999; + padding: 0.5rem; + overflow-y: scroll; + height: calc(100vh - 160px); + background: #0c0c0c; + color: #ccc; +} + +#logger div { + margin-left: 1rem; + text-indent: -1rem; + line-height: 1.4rem; + margin-bottom: 0.1rem; +} + +#logger span.white { color: rgb(242,242,242); } +#logger span.yellow { color: rgb(193,156,0); } +#logger span.cyan { color: rgb(58,150,221); } +#logger span.magenta { color: rgb(136,23,152); } +#logger span.red { color: rgb(197,15,31); } +#logger span.green { color: rgb(19,161,14); } +#logger span.inverse { + color: #0c0c0c; + background: white; + display: inline-block; +} + +#logger .padder { + height: 0.5rem; + flex: 0 0 auto; +} + +/***************** Status ********************/ +#status { + display: flex; + flex-direction: column; +} +#status .split { + margin-top: 2rem; + align-self: center; + width: calc(100% - 4rem); + display: flex; + 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 { + font-size: 1.2rem; + text-align: center; + padding: 1rem; + border-bottom: 1px solid #999; + margin-bottom: 0.5rem; +} +#status .item p { + padding: 0.25rem 1rem; +} +#status .item p.running { + color: rgb(19,161,14); + text-align: center; +} +#status .item p.notrunning { + color: rgb(197,15,31); + text-align: center; +} +#status button { + margin-top: 1rem; +} + +/***************** Updater ********************/ +#update { + display: flex; + flex-direction: column; + width: calc(100vw - 4rem); + 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.active { + background: transparent; + color: #ffb843; +} + +@media only screen and (max-device-width: 590px) { + #update .actions .filler { + flex: 2 1 100%; + } + #update .actions .button { + flex: 2 1 50%; + } +} + +#update h4 { + text-align: center; + padding: 0 1rem 1rem; + font-size: 1.2rem; +} +#update .info { + margin-top: 0rem; + display: flex; + border: 1px solid #999; + border-right: none; + border-bottom: none; + flex-wrap: wrap; +} +#update .info p { + flex: 2 1 50%; + border-right: 1px solid #999; + border-bottom: 1px solid #999; + padding: 0.5rem; +} + +#update .console { + font-family: "Lucida Console", Monaco, monospace; + margin: 1rem 0 0; + display: flex; + flex-direction: column; + border: 1px solid #999; + padding: 0.5rem; + overflow-y: scroll; + height: calc(100vh - 160px); + background: #0c0c0c; + color: #ccc; +} + +@media only screen and (min-height: 650px) { + #update .console { + height: calc(100vh - 340px); + } +} + diff --git a/example/public/main.js b/dev/public/main.js similarity index 93% rename from example/public/main.js rename to dev/public/main.js index 5252fe6..586f1a5 100644 --- a/example/public/main.js +++ b/dev/public/main.js @@ -28,14 +28,17 @@ require(1) const m = require(3) const Header = require(26) -const Frontpage = require(27) -const Log = require(28) +const Status = require(27) +const Log = require(30) +const Updater = require(31) m.mount(document.getElementById('header'), Header) m.route(document.getElementById('content'), '/', { - '/': Frontpage, + '/': Status, '/log': Log, + '/updater': Updater, + '/updater/:id': Updater, }) },function (global, require, module, exports) { @@ -2196,20 +2199,25 @@ const Header = { }) }, view: function() { - let path = m.route.get() + let path = m.route.get() || '' return [ m('div.seperator'), m(m.route.Link, { href: '/', - class: path === '/' ? 'active' : '', - }, 'Frontpage'), + class: path === '/' || path === '' ? 'active' : '', + }, 'Status'), m('div.seperator'), m(m.route.Link, { href: '/log', class: path === '/log' ? 'active' : '', }, 'Log'), m('div.seperator'), + m(m.route.Link, { + href: '/updater', + class: path.startsWith('/updater') ? 'active' : '', + }, 'Updater'), + m('div.seperator'), !this.connected && m('div.disconnected', ` Lost connection with server, Attempting to reconnect `) || null, @@ -2220,45 +2228,231 @@ const Header = { module.exports = Header },function (global, require, module, exports) { -// frontpage\frontpage.js +// status\status.js const m = require(3) +const socket = require(1) +const Module = require(28) + +const Status = Module({ + init: function() { + this._name = '...loading...' + this._management = { + port: null, + repository: null, + active: null, + latestInstalled: null, + lastActive: null, + latestVersion: null, + running: null, + } + this._app = { + port: null, + repository: null, + active: null, + latestInstalled: null, + lastActive: null, + latestVersion: null, + running: 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.lastActive = res.manageLastActive + this._management.latestVersion = res.manageLatestVersion + this._app.active = res.appActive + this._app.latestInstalled = res.appLatestInstalled + this._app.lastActive = res.appLastActive + this._app.latestVersion = res.appLatestVersion + + m.redraw() + }) + + this.on('core.status', (res) => { + this._management.running = res.manage + this._app.running = res.app + + m.redraw() + }) + + socket.emit('core.listencore', {}) + }, + + remove: function() { + socket.emit('core.unlistencore', {}) + }, + + restartClicked: function() { + socket.emit('core.restart', {}) + }, -const Frontpage = { view: function() { - return [ - m('h4.header', 'Frontpage'), + let loopOver = [ + ['Management service', '_management'], + ['Application service', '_app'], ] + return m('div#status', [ + m('h1.header', this._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]].lastActive + ? `Last stable version: ${this[group[1]].lastActive}` + : '< no last stable version >'), + m('p', this[group[1]].latestVersion + ? `Latest version: ${this[group[1]].latestVersion}` + : '< no version found >'), + this[group[1]].running !== null + ? m('p', + { class: group[1].running ? 'running' : 'notrunning' }, + group[1].running ? 'Running' : 'Not Running' + ) + : null, + m('button', { + + }, 'Update/Start') + ]) + }), + ]), + m('button', { + onclick: () => this.restartClicked(), + }, 'Restart service') + ]) } +}) + +module.exports = Status + +},function (global, require, module, exports) { +// module.js +const defaults = require(29) +const socket = require(1) + +module.exports = function Module(module) { + return defaults(module, { + init: function() {}, + + oninit: function(vnode) { + this._listeners = [] + this.init(vnode) + }, + + _listeners: null, + + _socketOn: function(cb) { + socket.on('connect', () => cb()) + + if (socket.connected) { + cb() + } + }, + + on: function(name, cb) { + this._listeners.push([name, cb]) + socket.on(name, cb) + }, + + remove: function() {}, + + onremove: function() { + this.remove() + if (!this._listeners) return + for (let i = 0; i < this._listeners.length; i++) { + socket.removeListener(this._listeners[0], this._listeners[1]) + } + }, + }) } -module.exports = Frontpage +},function (global, require, module, exports) { +// defaults.js + +// taken from isobject npm library +function isObject(val) { + return val != null && typeof val === 'object' && Array.isArray(val) === false +} + +module.exports = 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] && 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 +} },function (global, require, module, exports) { // log\log.js const m = require(3) const socket = require(1) +const Module = require(28) -const Log = { - oninit: function() { +const Log = Module({ + init: function() { this.connected = socket.connected this.loglines = [] - socket.on('newlog', data => { + this.on('newlog', data => { this.loglines.push(this.formatLine(data)) m.redraw() }) - socket.on('connect', () => { - this.loglines = [] - this.loadData() - socket.emit('core.listenlogs', {}) - m.redraw() - }) + this._socketOn(() => this.loadData()) + }, - this.loadData() + remove: function() { + socket.emit('core.unlistenlogs', {}) }, loadData: function() { + this.loglines = [] + socket.emit('core.listenlogs', {}) socket.emit('core.getlastlogs', {}, (res) => { this.loglines = res.map(this.formatLine) m.redraw() @@ -2288,8 +2482,172 @@ const Log = { ]), ] } -} +}) module.exports = Log +},function (global, require, module, exports) { +// updater\updater.js +const m = require(3) +const socket = require(1) +const Module = require(28) + +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.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.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'], + lastActive: this.db[this.activeApp + 'LastActive'], + latestInstalled: this.db[this.activeApp + 'LatestInstalled'], + latestVersion: this.db[this.activeApp + 'LatestVersion'], + logs: '', + } + } else { + this.app = {} + } + }, + + loadAppData() { + this.updateActiveDb() + if (this.activeApp === 'app') { + socket.emit('core.listentoapp', {}) + } + /* request to listen to app updates */ + }, + + remove: function() { + socket.emit('core.unlistencore', {}) + socket.emit('core.unlistentoapp', {}) + }, + + startUpdate: function() { + socket.emit('core.update', { + name: this.activeApp, + }) + }, + + view: function() { + 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' : ''), + href: '/updater/app', + }, 'Update App'), + m(m.route.Link, { + hidden: this.manageRepository, + class: 'button' + (this.activeApp === 'manage' ? ' active' : ''), + href: '/updater/manage', + }, 'Update Manager'), + ]), + this.activeApp && this.app ? [ + m('h4', this.app.repository + ? `${this.app.repository}` + : '< no repository >'), + m('div.info', [ + m('p', this.app.active + ? `Running version: ${this.app.active}` + : '< no running version >'), + m('p', this.app.latestInstalled + ? `Latest installed: ${this.app.latestInstalled}` + : '< no version installed >'), + m('p', this.app.lastActive + ? `Last stable version: ${this.app.lastActive}` + : '< no last stable 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 + } + } + }, + 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.db[this.activeApp + 'LastActive'] + || this.db[this.activeApp + 'LastActive'] === this.db[this.activeApp + 'Active'] + }, 'Use Last Version'), + ]) + : null, + ] : null + ]) + } +}) + +module.exports = Updater + }])); \ No newline at end of file diff --git a/example/api/core/ioroutes.mjs b/example/api/core/ioroutes.mjs deleted file mode 100644 index e47e7ac..0000000 --- a/example/api/core/ioroutes.mjs +++ /dev/null @@ -1,28 +0,0 @@ -import { formatLog } from './loghelper.mjs' - -/* - * Event: 'core.config' - * - * Get config - */ -export async function config(ctx) { - ctx.socket.emit('core.config', ctx.config) -} - -/* - * Event: 'core.getlastlogs' - * - * Returns last few log messages from log - */ -export async function getlastlogs(ctx, data, cb) { - cb(ctx.logroot.ringbuffer.records.map(formatLog)) -} - -/* - * Event: 'core.listenlogs' - * - * Start listening to new log lines - */ -export async function listenlogs(ctx) { - ctx.socket.join('logger') -} diff --git a/example/api/core/logmonitor.mjs b/example/api/core/logmonitor.mjs deleted file mode 100644 index aa02e9c..0000000 --- a/example/api/core/logmonitor.mjs +++ /dev/null @@ -1,7 +0,0 @@ -import { formatLog } from './loghelper.mjs' - -export default function logmonitor(io, config, db, log) { - log.on('newlog', function(data) { - io.to('logger').emit('newlog', formatLog(data)) - }) -} \ No newline at end of file diff --git a/example/app/frontpage/frontpage.js b/example/app/frontpage/frontpage.js deleted file mode 100644 index 1cfb7bd..0000000 --- a/example/app/frontpage/frontpage.js +++ /dev/null @@ -1,11 +0,0 @@ -const m = require('mithril') - -const Frontpage = { - view: function() { - return [ - m('h4.header', 'Frontpage'), - ] - } -} - -module.exports = Frontpage diff --git a/example/index.mjs b/example/index.mjs deleted file mode 100644 index 8e20f73..0000000 --- a/example/index.mjs +++ /dev/null @@ -1,5 +0,0 @@ -export function start(config, db, log, next) { - return import('./api/server.mjs').then(function(module) { - return module.run(config, db, log, next) - }) -} \ No newline at end of file diff --git a/example/public/main.css b/example/public/main.css deleted file mode 100644 index ebcc272..0000000 --- a/example/public/main.css +++ /dev/null @@ -1,129 +0,0 @@ -html { - box-sizing: border-box; - font-size: 16px; - height: 100%; -} - -*, *:before, *:after { - box-sizing: inherit; -} - -body, h1, h2, h3, h4, h5, h6, p { - margin: 0; - padding: 0; - font-weight: normal; -} - -body { - background: #3d3d3d; - color: #f1f1f1; - display: flex; - font-size: 16px; - min-height: 100vh; - flex-direction: column; - font-family: Helvetica, sans-serif, Arial; -} - -a, a:visited { - color: #eee; - text-decoration: underline; -} - -#header { - display: flex; - justify-content: center; - width: 100%; - background: #292929; - align-items: center; -} - -#header a { - width: 150px; - text-align: center; - padding: 1rem; -} - -#header .seperator { - border-right: 1px solid #999; - height: 1rem; -} - -/* Container */ -.container { - display: flex; - align-items: stretch; - flex-direction: column; - flex-grow: 2; -} - -.disconnected { - position: fixed; - top: 0; - left: 0; - right: 0; - bottom: 0; - background: rgba(0,0,0,0.8); - color: #fff; - font-size: 1em; - display: flex; - justify-content: center; - align-items: center; -} - -main { - display: flex; - flex-direction: column; - align-items: stretch; - flex-grow: 2; - width: 100%; -} - -main h1 { - text-align: center; - font-size: 1.6rem; - line-height: 3.6rem; -} - -form.login { - align-self: center; - display: flex; - flex-direction: column; -} - - -/***************** Log ********************/ -#logger { - margin: 0 2rem; - display: flex; - flex-direction: column; - border: 1px solid #999; - padding: 0.5rem; - overflow-y: scroll; - height: calc(100vh - 160px); - background: #0c0c0c; -} - -#logger div { - color: #ccc; - margin-left: 1rem; - text-indent: -1rem; - line-height: 1.4rem; - margin-bottom: 0.1rem; -} - -#logger span.white { color: rgb(242,242,242); } -#logger span.yellow { color: rgb(193,156,0); } -#logger span.cyan { color: rgb(58,150,221); } -#logger span.magenta { color: rgb(136,23,152); } -#logger span.red { color: rgb(197,15,31); } -#logger span.green { color: rgb(19,161,14); } -#logger span.inverse { - color: #0c0c0c; - background: white; - display: inline-block; -} - -#logger .padder { - height: 0.5rem; - flex: 0 0 auto; -} diff --git a/index.mjs b/index.mjs new file mode 100644 index 0000000..9e27ec0 --- /dev/null +++ b/index.mjs @@ -0,0 +1,5 @@ +export function start(config, db, log, core) { + return import('./api/server.mjs').then(function(module) { + return module.run(config, db, log, core) + }) +} \ No newline at end of file diff --git a/log.mjs b/log.mjs index c5288d5..3a0b978 100644 --- a/log.mjs +++ b/log.mjs @@ -4,7 +4,8 @@ import lowdb from './db.mjs' export default function getLog(name) { let settings - let ringbuffer = new bunyan.RingBuffer({ limit: 10 }) + let ringbuffer = new bunyan.RingBuffer({ limit: 20 }) + let ringbufferwarn = new bunyan.RingBuffer({ limit: 20 }) if (process.env.NODE_ENV === 'production') { settings = { @@ -34,6 +35,12 @@ export default function getLog(name) { level: 'info', }) + settings.streams.push({ + stream: ringbufferwarn, + type: 'raw', + level: 'warn', + }) + settings.streams.push({ stream: { write: function(record) { @@ -50,8 +57,17 @@ export default function getLog(name) { // Create our logger. logger = bunyan.createLogger(settings) - logger.event = new nodewindows.EventLogger(name) + if (process.env.NODE_ENV === 'production') { + logger.event = new nodewindows.EventLogger(name) + } else { + logger.event = { + info: function() {}, + warn: function() {}, + error: function() {}, + } + } logger.ringbuffer = ringbuffer + logger.ringbufferwarn = ringbufferwarn return logger } diff --git a/manage/.gitkeep b/manage/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/package.json b/package.json index 34d710a..1e8aaa4 100644 --- a/package.json +++ b/package.json @@ -4,7 +4,7 @@ "description": "Core boiler plate code to install node server as windows service", "main": "index.js", "scripts": { - "dev": "nodemon --watch example/api --watch runner.mjs --watch db.mjs --watch log.mjs runner.mjs | bunyan", + "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" }, "repository": { diff --git a/runner.mjs b/runner.mjs index e754994..c5a8914 100644 --- a/runner.mjs +++ b/runner.mjs @@ -1,6 +1,7 @@ import { readFileSync } from 'fs' import getLog from './log.mjs' import lowdb from './db.mjs' +import Core from './core/core.mjs' let config @@ -17,25 +18,36 @@ try { const log = getLog(config.name) -lowdb(log).then(function(db) { - return import('./example/index.mjs').then(function(module) { - return module.start(config, db, log, function(err) { - if (err) { - log.fatal(err, 'App recorded a fatal error') - log.event.error('App recorded a fatal error: ' + err.message) - process.exit(4) - } - log.warn('App asked to be shut down') - log.event.warn('App requested to be closed') - process.exit(0) +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) }) +} + +lowdb(log).then(function(db) { + let core = new Core(config, db, log, close) + + if (config.useDev) { + return import('./dev/index.mjs').then(function(module) { + return module.start(config, db, log, core) + }) + } }, function(err) { log.fatal(err, 'Critical error opening database') - log.event.error('Critical error opening database: ' + err.message) - process.exit(2) + 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) - process.exit(3) + log.event.error('Unknown error occured opening app: ' + err.message, null, function() { + process.exit(3) + }) }) diff --git a/service/service.mjs b/service/service.mjs index ae0028f..d445c68 100644 --- a/service/service.mjs +++ b/service/service.mjs @@ -1,24 +1,30 @@ import path from 'path' import { readFileSync } from 'fs' -import { fileURLToPath } from 'url' +import { getPathFromRoot } from '../core/util.mjs' import nodewindows from 'node-windows' -const __dirname = path.dirname(fileURLToPath(import.meta.url)); -let config = JSON.parse(readFileSync(path.join(__dirname,'../config.json'))) +let config = JSON.parse(readFileSync(getPathFromRoot('./config.json'))) const Service = nodewindows.Service -// Create a new service object -var svc = new Service({ +let serviceConfig = { name: config.serviceName, description: config.description, - script: path.join(__dirname,'../runner.mjs'), + 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