Mode development. Boilerplate finished
This commit is contained in:
parent
b096179dc0
commit
dc1bcf250d
34 changed files with 3302 additions and 0 deletions
4
.gitignore
vendored
4
.gitignore
vendored
|
@ -102,3 +102,7 @@ dist
|
||||||
|
|
||||||
# TernJS port file
|
# TernJS port file
|
||||||
.tern-port
|
.tern-port
|
||||||
|
|
||||||
|
# Custom ignore
|
||||||
|
db.json
|
||||||
|
package-lock.json
|
||||||
|
|
1
.npmrc
Normal file
1
.npmrc
Normal file
|
@ -0,0 +1 @@
|
||||||
|
package-lock=false
|
0
apps/.gitkeep
Normal file
0
apps/.gitkeep
Normal file
6
config.json
Normal file
6
config.json
Normal file
|
@ -0,0 +1,6 @@
|
||||||
|
{
|
||||||
|
"name": "service-core",
|
||||||
|
"serviceName": "Service-Core Node",
|
||||||
|
"description": "NodeJS Test Service",
|
||||||
|
"port": 4269
|
||||||
|
}
|
153
db.mjs
Normal file
153
db.mjs
Normal file
|
@ -0,0 +1,153 @@
|
||||||
|
import lowdb from 'lowdb'
|
||||||
|
import FileAsync from 'lowdb/adapters/FileAsync.js'
|
||||||
|
|
||||||
|
let lastId = -1
|
||||||
|
|
||||||
|
// Take from https://github.com/typicode/lodash-id/blob/master/src/index.js
|
||||||
|
// from package lodash-id
|
||||||
|
const lodashId = {
|
||||||
|
// Empties properties
|
||||||
|
__empty: function (doc) {
|
||||||
|
this.forEach(doc, function (value, key) {
|
||||||
|
delete doc[key]
|
||||||
|
})
|
||||||
|
},
|
||||||
|
|
||||||
|
// Copies properties from an object to another
|
||||||
|
__update: function (dest, src) {
|
||||||
|
this.forEach(src, function (value, key) {
|
||||||
|
dest[key] = value
|
||||||
|
})
|
||||||
|
},
|
||||||
|
|
||||||
|
// Removes an item from an array
|
||||||
|
__remove: function (array, item) {
|
||||||
|
var index = this.indexOf(array, item)
|
||||||
|
if (index !== -1) array.splice(index, 1)
|
||||||
|
},
|
||||||
|
|
||||||
|
__id: function () {
|
||||||
|
var id = this.id || 'id'
|
||||||
|
return id
|
||||||
|
},
|
||||||
|
|
||||||
|
getById: function (collection, id) {
|
||||||
|
var self = this
|
||||||
|
return this.find(collection, function (doc) {
|
||||||
|
if (self.has(doc, self.__id())) {
|
||||||
|
return doc[self.__id()] === id
|
||||||
|
}
|
||||||
|
})
|
||||||
|
},
|
||||||
|
|
||||||
|
createId: function (collection, doc) {
|
||||||
|
let next = new Date().getTime()
|
||||||
|
if (next <= lastId) {
|
||||||
|
next = lastId + 1
|
||||||
|
}
|
||||||
|
lastId = next
|
||||||
|
return next
|
||||||
|
},
|
||||||
|
|
||||||
|
insert: function (collection, doc) {
|
||||||
|
doc[this.__id()] = doc[this.__id()] || this.createId(collection, doc)
|
||||||
|
var d = this.getById(collection, doc[this.__id()])
|
||||||
|
if (d) throw new Error('Insert failed, duplicate id')
|
||||||
|
collection.push(doc)
|
||||||
|
return doc
|
||||||
|
},
|
||||||
|
|
||||||
|
upsert: function (collection, doc) {
|
||||||
|
if (doc[this.__id()]) {
|
||||||
|
// id is set
|
||||||
|
var d = this.getById(collection, doc[this.__id()])
|
||||||
|
if (d) {
|
||||||
|
// replace properties of existing object
|
||||||
|
this.__empty(d)
|
||||||
|
this.assign(d, doc)
|
||||||
|
} else {
|
||||||
|
// push new object
|
||||||
|
collection.push(doc)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// create id and push new object
|
||||||
|
doc[this.__id()] = this.createId(collection, doc)
|
||||||
|
collection.push(doc)
|
||||||
|
}
|
||||||
|
|
||||||
|
return doc
|
||||||
|
},
|
||||||
|
|
||||||
|
updateById: function (collection, id, attrs) {
|
||||||
|
var doc = this.getById(collection, id)
|
||||||
|
|
||||||
|
if (doc) {
|
||||||
|
this.assign(doc, attrs, {id: doc.id})
|
||||||
|
}
|
||||||
|
|
||||||
|
return doc
|
||||||
|
},
|
||||||
|
|
||||||
|
updateWhere: function (collection, predicate, attrs) {
|
||||||
|
var self = this
|
||||||
|
var docs = this.filter(collection, predicate)
|
||||||
|
|
||||||
|
docs.forEach(function (doc) {
|
||||||
|
self.assign(doc, attrs, {id: doc.id})
|
||||||
|
})
|
||||||
|
|
||||||
|
return docs
|
||||||
|
},
|
||||||
|
|
||||||
|
replaceById: function (collection, id, attrs) {
|
||||||
|
var doc = this.getById(collection, id)
|
||||||
|
|
||||||
|
if (doc) {
|
||||||
|
var docId = doc.id
|
||||||
|
this.__empty(doc)
|
||||||
|
this.assign(doc, attrs, {id: docId})
|
||||||
|
}
|
||||||
|
|
||||||
|
return doc
|
||||||
|
},
|
||||||
|
|
||||||
|
removeById: function (collection, id) {
|
||||||
|
var doc = this.getById(collection, id)
|
||||||
|
|
||||||
|
this.__remove(collection, doc)
|
||||||
|
|
||||||
|
return doc
|
||||||
|
},
|
||||||
|
|
||||||
|
removeWhere: function (collection, predicate) {
|
||||||
|
var self = this
|
||||||
|
var docs = this.filter(collection, predicate)
|
||||||
|
|
||||||
|
docs.forEach(function (doc) {
|
||||||
|
self.__remove(collection, doc)
|
||||||
|
})
|
||||||
|
|
||||||
|
return docs
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const adapter = new FileAsync('db.json')
|
||||||
|
|
||||||
|
export default function GetDB(log) {
|
||||||
|
return lowdb(adapter)
|
||||||
|
.then(function(db) {
|
||||||
|
db._.mixin(lodashId)
|
||||||
|
|
||||||
|
db.defaults({
|
||||||
|
lastActiveVersion: null,
|
||||||
|
version: 1,
|
||||||
|
})
|
||||||
|
.write()
|
||||||
|
.then(
|
||||||
|
function() { },
|
||||||
|
function(e) { log.error(e, 'Error writing defaults to lowdb') }
|
||||||
|
)
|
||||||
|
|
||||||
|
return db
|
||||||
|
})
|
||||||
|
}
|
28
example/api/core/ioroutes.mjs
Normal file
28
example/api/core/ioroutes.mjs
Normal file
|
@ -0,0 +1,28 @@
|
||||||
|
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')
|
||||||
|
}
|
181
example/api/core/loghelper.mjs
Normal file
181
example/api/core/loghelper.mjs
Normal file
|
@ -0,0 +1,181 @@
|
||||||
|
import _ from 'lodash'
|
||||||
|
import { format } from 'util'
|
||||||
|
|
||||||
|
// http://en.wikipedia.org/wiki/ANSI_escape_code#graphics
|
||||||
|
// Suggested colors (some are unreadable in common cases):
|
||||||
|
// - Good: cyan, yellow (limited use), bold, green, magenta, red
|
||||||
|
// - Bad: blue (not visible on cmd.exe), grey (same color as background on
|
||||||
|
// Solarized Dark theme from <https://github.com/altercation/solarized>, see
|
||||||
|
// issue #160)
|
||||||
|
var colors = {
|
||||||
|
'bold' : [1, 22],
|
||||||
|
'italic' : [3, 23],
|
||||||
|
'underline' : [4, 24],
|
||||||
|
'inverse' : [7, 27],
|
||||||
|
'white' : [37, 39],
|
||||||
|
'grey' : [90, 39],
|
||||||
|
'black' : [30, 39],
|
||||||
|
'blue' : [34, 39],
|
||||||
|
'cyan' : [36, 39],
|
||||||
|
'green' : [32, 39],
|
||||||
|
'magenta' : [35, 39],
|
||||||
|
'red' : [31, 39],
|
||||||
|
'yellow' : [33, 39]
|
||||||
|
};
|
||||||
|
|
||||||
|
// Levels
|
||||||
|
var TRACE = 10;
|
||||||
|
var DEBUG = 20;
|
||||||
|
var INFO = 30;
|
||||||
|
var WARN = 40;
|
||||||
|
var ERROR = 50;
|
||||||
|
var FATAL = 60;
|
||||||
|
|
||||||
|
var levelFromName = {
|
||||||
|
'trace': TRACE,
|
||||||
|
'debug': DEBUG,
|
||||||
|
'info': INFO,
|
||||||
|
'warn': WARN,
|
||||||
|
'error': ERROR,
|
||||||
|
'fatal': FATAL
|
||||||
|
};
|
||||||
|
|
||||||
|
var upperPaddedNameFromLevel = {};
|
||||||
|
Object.keys(levelFromName).forEach(function (name) {
|
||||||
|
var lvl = levelFromName[name];
|
||||||
|
upperPaddedNameFromLevel[lvl] = (
|
||||||
|
name.length === 4 ? ' ' : '') + name.toUpperCase();
|
||||||
|
});
|
||||||
|
|
||||||
|
function stylize(str, color) {
|
||||||
|
if (!str)
|
||||||
|
return '';
|
||||||
|
var codes = colors[color];
|
||||||
|
if (codes) {
|
||||||
|
return '\\033[' + codes[0] + 'm' + str +
|
||||||
|
'\\033[' + codes[1] + 'm';
|
||||||
|
} else {
|
||||||
|
return str;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function formatLog(data) {
|
||||||
|
let rec = _.cloneDeep(data)
|
||||||
|
|
||||||
|
delete rec.v;
|
||||||
|
|
||||||
|
// Time.
|
||||||
|
var time = '[' + rec.time.toISOString().replace('T', ' ').replace('Z', '') + ']'
|
||||||
|
time = stylize(time, 'none')
|
||||||
|
|
||||||
|
delete rec.time;
|
||||||
|
|
||||||
|
var nameStr = rec.name;
|
||||||
|
delete rec.name;
|
||||||
|
|
||||||
|
if (rec.component) {
|
||||||
|
nameStr += '/' + rec.component;
|
||||||
|
}
|
||||||
|
delete rec.component;
|
||||||
|
|
||||||
|
nameStr += '/' + rec.pid;
|
||||||
|
delete rec.pid;
|
||||||
|
|
||||||
|
var level = (upperPaddedNameFromLevel[rec.level] || 'LVL' + rec.level);
|
||||||
|
var colorFromLevel = {
|
||||||
|
10: 'white', // TRACE
|
||||||
|
20: 'yellow', // DEBUG
|
||||||
|
30: 'cyan', // INFO
|
||||||
|
40: 'magenta', // WARN
|
||||||
|
50: 'red', // ERROR
|
||||||
|
60: 'inverse', // FATAL
|
||||||
|
};
|
||||||
|
level = stylize(level, colorFromLevel[rec.level]);
|
||||||
|
delete rec.level;
|
||||||
|
|
||||||
|
var src = '';
|
||||||
|
if (rec.src && rec.src.file) {
|
||||||
|
var s = rec.src;
|
||||||
|
if (s.func) {
|
||||||
|
src = format(' (%s:%d in %s)', s.file, s.line, s.func);
|
||||||
|
} else {
|
||||||
|
src = format(' (%s:%d)', s.file, s.line);
|
||||||
|
}
|
||||||
|
src = stylize(src, 'green');
|
||||||
|
}
|
||||||
|
delete rec.src;
|
||||||
|
|
||||||
|
var hostname = rec.hostname;
|
||||||
|
delete rec.hostname;
|
||||||
|
|
||||||
|
var extras = [];
|
||||||
|
var details = [];
|
||||||
|
|
||||||
|
if (rec.req_id) {
|
||||||
|
extras.push('req_id=' + rec.req_id);
|
||||||
|
}
|
||||||
|
delete rec.req_id;
|
||||||
|
|
||||||
|
var onelineMsg;
|
||||||
|
if (rec.msg.indexOf('\n') !== -1) {
|
||||||
|
onelineMsg = '';
|
||||||
|
details.push(indent(stylize(rec.msg, 'cyan')));
|
||||||
|
} else {
|
||||||
|
onelineMsg = ' ' + stylize(rec.msg, 'cyan');
|
||||||
|
}
|
||||||
|
delete rec.msg;
|
||||||
|
|
||||||
|
if (rec.err && rec.err.stack) {
|
||||||
|
var err = rec.err
|
||||||
|
if (typeof (err.stack) !== 'string') {
|
||||||
|
details.push(indent(err.stack.toString()));
|
||||||
|
} else {
|
||||||
|
details.push(indent(err.stack));
|
||||||
|
}
|
||||||
|
delete err.message;
|
||||||
|
delete err.name;
|
||||||
|
delete err.stack;
|
||||||
|
// E.g. for extra 'foo' field on 'err', add 'err.foo' at
|
||||||
|
// top-level. This *does* have the potential to stomp on a
|
||||||
|
// literal 'err.foo' key.
|
||||||
|
Object.keys(err).forEach(function (k) {
|
||||||
|
rec['err.' + k] = err[k];
|
||||||
|
})
|
||||||
|
delete rec.err;
|
||||||
|
}
|
||||||
|
|
||||||
|
var leftover = Object.keys(rec);
|
||||||
|
for (var i = 0; i < leftover.length; i++) {
|
||||||
|
var key = leftover[i];
|
||||||
|
var value = rec[key];
|
||||||
|
var stringified = false;
|
||||||
|
if (typeof (value) !== 'string') {
|
||||||
|
value = JSON.stringify(value, null, 2);
|
||||||
|
stringified = true;
|
||||||
|
}
|
||||||
|
if (value.indexOf('\n') !== -1 || value.length > 50) {
|
||||||
|
details.push(indent(key + ': ' + value));
|
||||||
|
} else if (!stringified && (value.indexOf(' ') != -1 ||
|
||||||
|
value.length === 0))
|
||||||
|
{
|
||||||
|
extras.push(key + '=' + JSON.stringify(value));
|
||||||
|
} else {
|
||||||
|
extras.push(key + '=' + value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extras = stylize(
|
||||||
|
(extras.length ? ' (' + extras.join(', ') + ')' : ''), 'none');
|
||||||
|
details = stylize(
|
||||||
|
(details.length ? details.join('\n --\n') + '\n' : ''), 'none');
|
||||||
|
|
||||||
|
return format('%s %s: %s on %s%s:%s%s\n%s',
|
||||||
|
time,
|
||||||
|
level,
|
||||||
|
nameStr,
|
||||||
|
hostname || '<no-hostname>',
|
||||||
|
src,
|
||||||
|
onelineMsg,
|
||||||
|
extras,
|
||||||
|
details)
|
||||||
|
}
|
7
example/api/core/logmonitor.mjs
Normal file
7
example/api/core/logmonitor.mjs
Normal file
|
@ -0,0 +1,7 @@
|
||||||
|
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))
|
||||||
|
})
|
||||||
|
}
|
50
example/api/routerio.mjs
Normal file
50
example/api/routerio.mjs
Normal file
|
@ -0,0 +1,50 @@
|
||||||
|
import * as core from './core/ioroutes.mjs'
|
||||||
|
|
||||||
|
function register(ctx, name, method) {
|
||||||
|
if (typeof(method) === 'object') {
|
||||||
|
Object.keys(method).forEach(key => {
|
||||||
|
register(ctx, [name, key].join('.'), method[key])
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx.socket.on(name, async function(data, cb) {
|
||||||
|
ctx.log.debug('Got event', name)
|
||||||
|
|
||||||
|
try {
|
||||||
|
await method(ctx, data, cb)
|
||||||
|
}
|
||||||
|
catch (error) {
|
||||||
|
ctx.log.error(error, `Error processing ${name}`)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
function onConnection(server, config, db, log, data) {
|
||||||
|
const io = server
|
||||||
|
const socket = data
|
||||||
|
|
||||||
|
const child = log.child({
|
||||||
|
id: socket.id,
|
||||||
|
})
|
||||||
|
|
||||||
|
child.info('Got new socket connection')
|
||||||
|
|
||||||
|
let ctx = {
|
||||||
|
config,
|
||||||
|
io,
|
||||||
|
socket,
|
||||||
|
log: child,
|
||||||
|
db,
|
||||||
|
logroot: log,
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx.socket.on('disconnect', function() {
|
||||||
|
child.info('Closed connection')
|
||||||
|
})
|
||||||
|
|
||||||
|
register(ctx, 'core', core)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default onConnection
|
80
example/api/server.mjs
Normal file
80
example/api/server.mjs
Normal file
|
@ -0,0 +1,80 @@
|
||||||
|
import http from 'http'
|
||||||
|
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 onConnection from './routerio.mjs'
|
||||||
|
|
||||||
|
export function run(config, db, log, next) {
|
||||||
|
const __dirname = path.dirname(fileURLToPath(import.meta.url))
|
||||||
|
const staticRoot = path.join(__dirname,'../public')
|
||||||
|
|
||||||
|
const fileServer = new nStatic.Server(staticRoot)
|
||||||
|
const server = http.createServer(function (req, res) {
|
||||||
|
const child = log.child({})
|
||||||
|
|
||||||
|
const d1 = new Date().getTime()
|
||||||
|
|
||||||
|
let finishedRequest = false
|
||||||
|
var done = function () {
|
||||||
|
if (finishedRequest) return
|
||||||
|
finishedRequest = true
|
||||||
|
if (req.url === '/main.css.map') return
|
||||||
|
var requestTime = new Date().getTime() - d1
|
||||||
|
|
||||||
|
let level = 'debug'
|
||||||
|
if (res.statusCode >= 400) {
|
||||||
|
level = 'warn'
|
||||||
|
}
|
||||||
|
if (res.statusCode >= 500) {
|
||||||
|
level = 'error'
|
||||||
|
}
|
||||||
|
|
||||||
|
let status = ''
|
||||||
|
if (res.statusCode >= 400) {
|
||||||
|
status = res.statusCode + ' '
|
||||||
|
}
|
||||||
|
|
||||||
|
child[level]({
|
||||||
|
duration: requestTime,
|
||||||
|
status: res.statusCode,
|
||||||
|
}, `<-- ${status}${req.method} ${req.url}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
res.addListener('finish', done);
|
||||||
|
res.addListener('close', done);
|
||||||
|
|
||||||
|
req.addListener('end', function () {
|
||||||
|
if (req.url === '/') {
|
||||||
|
res.writeHead(302, { Location: '/index.html' })
|
||||||
|
return res.end()
|
||||||
|
}
|
||||||
|
|
||||||
|
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))
|
||||||
|
|
||||||
|
logmonitor(io, config, db, log)
|
||||||
|
|
||||||
|
server.listen(config.port, '0.0.0.0', function(err) {
|
||||||
|
if (err) {
|
||||||
|
log.fatal(err)
|
||||||
|
return process.exit(2)
|
||||||
|
}
|
||||||
|
log.info(`Server is listening on ${config.port} serving files on ${staticRoot}`)
|
||||||
|
})
|
||||||
|
}
|
8
example/app/client.js
Normal file
8
example/app/client.js
Normal file
File diff suppressed because one or more lines are too long
11
example/app/frontpage/frontpage.js
Normal file
11
example/app/frontpage/frontpage.js
Normal file
|
@ -0,0 +1,11 @@
|
||||||
|
const m = require('mithril')
|
||||||
|
|
||||||
|
const Frontpage = {
|
||||||
|
view: function() {
|
||||||
|
return [
|
||||||
|
m('h4.header', 'Frontpage'),
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = Frontpage
|
39
example/app/header.js
Normal file
39
example/app/header.js
Normal file
|
@ -0,0 +1,39 @@
|
||||||
|
const m = require('mithril')
|
||||||
|
const socket = require('./socket')
|
||||||
|
|
||||||
|
const Header = {
|
||||||
|
oninit: function() {
|
||||||
|
this.connected = socket.connected
|
||||||
|
|
||||||
|
socket.on('connect', () => {
|
||||||
|
this.connected = true
|
||||||
|
m.redraw()
|
||||||
|
})
|
||||||
|
socket.on('disconnect', () => {
|
||||||
|
this.connected = false
|
||||||
|
m.redraw()
|
||||||
|
})
|
||||||
|
},
|
||||||
|
view: function() {
|
||||||
|
let path = m.route.get()
|
||||||
|
|
||||||
|
return [
|
||||||
|
m('div.seperator'),
|
||||||
|
m(m.route.Link, {
|
||||||
|
href: '/',
|
||||||
|
class: path === '/' ? 'active' : '',
|
||||||
|
}, 'Frontpage'),
|
||||||
|
m('div.seperator'),
|
||||||
|
m(m.route.Link, {
|
||||||
|
href: '/log',
|
||||||
|
class: path === '/log' ? 'active' : '',
|
||||||
|
}, 'Log'),
|
||||||
|
m('div.seperator'),
|
||||||
|
!this.connected && m('div.disconnected', `
|
||||||
|
Lost connection with server, Attempting to reconnect
|
||||||
|
`) || null,
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = Header
|
28
example/app/index.js
Normal file
28
example/app/index.js
Normal file
|
@ -0,0 +1,28 @@
|
||||||
|
/**
|
||||||
|
* @license
|
||||||
|
* stream-manager <https://filadelfia.is>
|
||||||
|
* Copyright 2015 Jonatan Nilsson <http://jonatan.nilsson.is/>
|
||||||
|
*
|
||||||
|
* Available under WTFPL License (http://www.wtfpl.net/txt/copying/)
|
||||||
|
*/
|
||||||
|
|
||||||
|
'use strict'
|
||||||
|
|
||||||
|
//Add debug components to window. Allows us to play with controls
|
||||||
|
//in the console.
|
||||||
|
window.components = {}
|
||||||
|
|
||||||
|
require('./socket')
|
||||||
|
|
||||||
|
const m = require('mithril')
|
||||||
|
const Header = require('./header')
|
||||||
|
|
||||||
|
const Frontpage = require('./frontpage/frontpage')
|
||||||
|
const Log = require('./log/log')
|
||||||
|
|
||||||
|
m.mount(document.getElementById('header'), Header)
|
||||||
|
|
||||||
|
m.route(document.getElementById('content'), '/', {
|
||||||
|
'/': Frontpage,
|
||||||
|
'/log': Log,
|
||||||
|
})
|
56
example/app/log/log.js
Normal file
56
example/app/log/log.js
Normal file
|
@ -0,0 +1,56 @@
|
||||||
|
const m = require('mithril')
|
||||||
|
const socket = require('../socket')
|
||||||
|
|
||||||
|
const Log = {
|
||||||
|
oninit: function() {
|
||||||
|
this.connected = socket.connected
|
||||||
|
this.loglines = []
|
||||||
|
|
||||||
|
socket.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.loadData()
|
||||||
|
},
|
||||||
|
|
||||||
|
loadData: function() {
|
||||||
|
socket.emit('core.getlastlogs', {}, (res) => {
|
||||||
|
this.loglines = res.map(this.formatLine)
|
||||||
|
m.redraw()
|
||||||
|
})
|
||||||
|
},
|
||||||
|
|
||||||
|
formatLine: function(line) {
|
||||||
|
return m.trust(line.replace(/\\033\[37m/g, '<span class="white">')
|
||||||
|
.replace(/\\033\[33m/g, '<span class="yellow">')
|
||||||
|
.replace(/\\033\[36m/g, '<span class="cyan">')
|
||||||
|
.replace(/\\033\[35m/g, '<span class="magenta">')
|
||||||
|
.replace(/\\033\[31m/g, '<span class="red">')
|
||||||
|
.replace(/\\033\[7m/g, '<span class="inverse">')
|
||||||
|
.replace(/\\033\[32m/g, '<span class="green">')
|
||||||
|
.replace(/\\033\[27m/g, '</span>')
|
||||||
|
.replace(/\\033\[39m/g, '</span>'))
|
||||||
|
},
|
||||||
|
|
||||||
|
view: function() {
|
||||||
|
return [
|
||||||
|
m('h1.header', 'Log'),
|
||||||
|
m('div#logger', [
|
||||||
|
this.loglines.map((line, i) => {
|
||||||
|
return m('div', { key: i }, line)
|
||||||
|
}),
|
||||||
|
m('div.padder'),
|
||||||
|
]),
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = Log
|
4
example/app/socket.js
Normal file
4
example/app/socket.js
Normal file
|
@ -0,0 +1,4 @@
|
||||||
|
var io = require('./client')
|
||||||
|
var socket = io()
|
||||||
|
|
||||||
|
module.exports = socket
|
1
example/build.bat
Normal file
1
example/build.bat
Normal file
|
@ -0,0 +1 @@
|
||||||
|
@ECHO off
|
5
example/index.mjs
Normal file
5
example/index.mjs
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
export function start(config, db, log, next) {
|
||||||
|
return import('./api/server.mjs').then(function(module) {
|
||||||
|
return module.run(config, db, log, next)
|
||||||
|
})
|
||||||
|
}
|
20
example/package.json
Normal file
20
example/package.json
Normal file
|
@ -0,0 +1,20 @@
|
||||||
|
{
|
||||||
|
"name": "app",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"description": "",
|
||||||
|
"main": "index.js",
|
||||||
|
"scripts": {
|
||||||
|
"js:build:main": "asbundle app/index.js public/main.js",
|
||||||
|
"dev": "nodemon --watch app --exec \"npm run js:build:main\"",
|
||||||
|
"test": "echo \"Error: no test specified\" && exit 1"
|
||||||
|
},
|
||||||
|
"author": "",
|
||||||
|
"license": "ISC",
|
||||||
|
"dependencies": {
|
||||||
|
"asbundle": "^2.6.1",
|
||||||
|
"mithril": "^2.0.4",
|
||||||
|
"node-static": "^0.7.11",
|
||||||
|
"nodemon": "^2.0.4",
|
||||||
|
"socket.io-serveronly": "^2.3.0"
|
||||||
|
}
|
||||||
|
}
|
14
example/public/index.html
Normal file
14
example/public/index.html
Normal file
|
@ -0,0 +1,14 @@
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta content="text/html;charset=utf-8" http-equiv="Content-Type">
|
||||||
|
<meta content="utf-8" http-equiv="encoding">
|
||||||
|
<title>Service Core</title>
|
||||||
|
<link href="/main.css" rel="stylesheet" />
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<section id="header"></section>
|
||||||
|
<main id="content"></main>
|
||||||
|
<script src="main.js"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
129
example/public/main.css
Normal file
129
example/public/main.css
Normal file
|
@ -0,0 +1,129 @@
|
||||||
|
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;
|
||||||
|
}
|
2295
example/public/main.js
Normal file
2295
example/public/main.js
Normal file
File diff suppressed because one or more lines are too long
1
install.bat
Normal file
1
install.bat
Normal file
|
@ -0,0 +1 @@
|
||||||
|
node service\install.mjs
|
57
log.mjs
Normal file
57
log.mjs
Normal file
|
@ -0,0 +1,57 @@
|
||||||
|
import nodewindows from 'node-windows'
|
||||||
|
import bunyan from 'bunyan-lite'
|
||||||
|
import lowdb from './db.mjs'
|
||||||
|
|
||||||
|
export default function getLog(name) {
|
||||||
|
let settings
|
||||||
|
let ringbuffer = new bunyan.RingBuffer({ limit: 10 })
|
||||||
|
|
||||||
|
if (process.env.NODE_ENV === 'production') {
|
||||||
|
settings = {
|
||||||
|
"name": "service-core",
|
||||||
|
"streams": [{
|
||||||
|
path: 'log.log',
|
||||||
|
level: 'info',
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
settings = {
|
||||||
|
"name": "service-core",
|
||||||
|
"streams": [{
|
||||||
|
"stream": process.stdout,
|
||||||
|
"level": "debug"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let logger
|
||||||
|
|
||||||
|
settings.streams.push({
|
||||||
|
stream: ringbuffer,
|
||||||
|
type: 'raw',
|
||||||
|
level: 'info',
|
||||||
|
})
|
||||||
|
|
||||||
|
settings.streams.push({
|
||||||
|
stream: {
|
||||||
|
write: function(record) {
|
||||||
|
logger.emit('newlog', record)
|
||||||
|
},
|
||||||
|
end: function() {},
|
||||||
|
destroy: function() {},
|
||||||
|
destroySoon: function() {},
|
||||||
|
},
|
||||||
|
type: 'raw',
|
||||||
|
level: 'info',
|
||||||
|
})
|
||||||
|
|
||||||
|
// Create our logger.
|
||||||
|
logger = bunyan.createLogger(settings)
|
||||||
|
|
||||||
|
logger.event = new nodewindows.EventLogger(name)
|
||||||
|
logger.ringbuffer = ringbuffer
|
||||||
|
|
||||||
|
return logger
|
||||||
|
}
|
29
package.json
Normal file
29
package.json
Normal file
|
@ -0,0 +1,29 @@
|
||||||
|
{
|
||||||
|
"name": "service-core",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"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",
|
||||||
|
"test": "echo \"Error: no test specified\" && exit 1"
|
||||||
|
},
|
||||||
|
"repository": {
|
||||||
|
"type": "git",
|
||||||
|
"url": "git+https://github.com/TheThing/service-core.git"
|
||||||
|
},
|
||||||
|
"author": "Jonatan Nilsson",
|
||||||
|
"license": "WTFPL",
|
||||||
|
"bugs": {
|
||||||
|
"url": "https://github.com/TheThing/service-core/issues"
|
||||||
|
},
|
||||||
|
"homepage": "https://github.com/TheThing/service-core#readme",
|
||||||
|
"dependencies": {
|
||||||
|
"bunyan-lite": "^1.0.1",
|
||||||
|
"lodash": "^4.17.20",
|
||||||
|
"lowdb": "^1.0.0",
|
||||||
|
"node-windows": "^1.0.0-beta.5"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"nodemon": "^2.0.4"
|
||||||
|
}
|
||||||
|
}
|
41
runner.mjs
Normal file
41
runner.mjs
Normal file
|
@ -0,0 +1,41 @@
|
||||||
|
import { readFileSync } from 'fs'
|
||||||
|
import getLog from './log.mjs'
|
||||||
|
import lowdb from './db.mjs'
|
||||||
|
|
||||||
|
let config
|
||||||
|
|
||||||
|
try {
|
||||||
|
config = JSON.parse(readFileSync('./config.json'))
|
||||||
|
} catch (err) {
|
||||||
|
let logger = getLog('critical-error')
|
||||||
|
logger.fatal('Error opening config file')
|
||||||
|
logger.fatal('Make sure it is valid JSON')
|
||||||
|
logger.fatal(err)
|
||||||
|
logger.event.error('Unable to start, error in config.json: ' + err.message)
|
||||||
|
process.exit(10)
|
||||||
|
}
|
||||||
|
|
||||||
|
const log = getLog(config.name)
|
||||||
|
|
||||||
|
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)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}, function(err) {
|
||||||
|
log.fatal(err, 'Critical error opening database')
|
||||||
|
log.event.error('Critical error opening database: ' + err.message)
|
||||||
|
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)
|
||||||
|
})
|
11
service/install.mjs
Normal file
11
service/install.mjs
Normal file
|
@ -0,0 +1,11 @@
|
||||||
|
import svc from './service.mjs'
|
||||||
|
|
||||||
|
svc.on('install',function(){
|
||||||
|
svc.start();
|
||||||
|
});
|
||||||
|
|
||||||
|
svc.on('alreadyinstalled',function(){
|
||||||
|
svc.start();
|
||||||
|
});
|
||||||
|
|
||||||
|
svc.install();
|
24
service/service.mjs
Normal file
24
service/service.mjs
Normal file
|
@ -0,0 +1,24 @@
|
||||||
|
import path from 'path'
|
||||||
|
import { readFileSync } from 'fs'
|
||||||
|
import { fileURLToPath } from 'url'
|
||||||
|
import nodewindows from 'node-windows'
|
||||||
|
|
||||||
|
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
||||||
|
let config = JSON.parse(readFileSync(path.join(__dirname,'../config.json')))
|
||||||
|
|
||||||
|
const Service = nodewindows.Service
|
||||||
|
|
||||||
|
// Create a new service object
|
||||||
|
var svc = new Service({
|
||||||
|
name: config.serviceName,
|
||||||
|
description: config.description,
|
||||||
|
script: path.join(__dirname,'../runner.mjs'),
|
||||||
|
env: {
|
||||||
|
name: 'NODE_ENV',
|
||||||
|
value: 'production',
|
||||||
|
},
|
||||||
|
//, workingDirectory: '...'
|
||||||
|
//, allowServiceLogon: true
|
||||||
|
});
|
||||||
|
|
||||||
|
export default svc
|
3
service/start.mjs
Normal file
3
service/start.mjs
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
import svc from './service.mjs'
|
||||||
|
|
||||||
|
svc.start();
|
3
service/stop.mjs
Normal file
3
service/stop.mjs
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
import svc from './service.mjs'
|
||||||
|
|
||||||
|
svc.stop();
|
10
service/uninstall.mjs
Normal file
10
service/uninstall.mjs
Normal file
|
@ -0,0 +1,10 @@
|
||||||
|
import svc from './service.mjs'
|
||||||
|
|
||||||
|
// Listen for the "install" event, which indicates the
|
||||||
|
// process is available as a service.
|
||||||
|
svc.on('uninstall',function(){
|
||||||
|
console.log('Uninstall complete.');
|
||||||
|
console.log('The service exists: ',svc.exists);
|
||||||
|
});
|
||||||
|
|
||||||
|
svc.uninstall();
|
1
start.bat
Normal file
1
start.bat
Normal file
|
@ -0,0 +1 @@
|
||||||
|
node service\start.mjs
|
1
stop.bat
Normal file
1
stop.bat
Normal file
|
@ -0,0 +1 @@
|
||||||
|
node service\stop.mjs
|
1
uninstall.bat
Normal file
1
uninstall.bat
Normal file
|
@ -0,0 +1 @@
|
||||||
|
node service\uninstall.mjs
|
Loading…
Reference in a new issue