Lots of dev
This commit is contained in:
parent
dc1bcf250d
commit
2616dc94a4
40 changed files with 1561 additions and 253 deletions
BIN
app/v1.0.1.5/v1.0.1.5.zip
Normal file
BIN
app/v1.0.1.5/v1.0.1.5.zip
Normal file
Binary file not shown.
|
@ -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
|
||||
}
|
69
core/client.mjs
Normal file
69
core/client.mjs
Normal file
|
@ -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
|
||||
})
|
||||
}
|
186
core/core.mjs
Normal file
186
core/core.mjs
Normal file
|
@ -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'] || '<none>'
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
7
core/test.mjs
Normal file
7
core/test.mjs
Normal file
|
@ -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))
|
28
core/util.mjs
Normal file
28
core/util.mjs
Normal file
|
@ -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)
|
||||
})
|
||||
})
|
||||
}
|
13
db.mjs
13
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()
|
||||
|
|
20
dev/api/core/coremonitor.mjs
Normal file
20
dev/api/core/coremonitor.mjs
Normal file
|
@ -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,
|
||||
})
|
||||
}))
|
||||
}
|
104
dev/api/core/ioroutes.mjs
Normal file
104
dev/api/core/ioroutes.mjs
Normal file
|
@ -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')
|
||||
}
|
|
@ -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) {
|
|
@ -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,
|
||||
}
|
||||
|
|
@ -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}`)
|
||||
})
|
||||
}
|
16
dev/api/util.mjs
Normal file
16
dev/api/util.mjs
Normal file
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
34
dev/app/defaults.js
Normal file
34
dev/app/defaults.js
Normal file
|
@ -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
|
||||
}
|
|
@ -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,
|
|
@ -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,
|
||||
})
|
|
@ -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
|
38
dev/app/module.js
Normal file
38
dev/app/module.js
Normal file
|
@ -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])
|
||||
}
|
||||
},
|
||||
})
|
||||
}
|
119
dev/app/status/status.js
Normal file
119
dev/app/status/status.js
Normal file
|
@ -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
|
161
dev/app/updater/updater.js
Normal file
161
dev/app/updater/updater.js
Normal file
|
@ -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
|
5
dev/index.mjs
Normal file
5
dev/index.mjs
Normal file
|
@ -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)
|
||||
})
|
||||
}
|
270
dev/public/main.css
Normal file
270
dev/public/main.css
Normal file
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
|
||||
}]));
|
|
@ -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')
|
||||
}
|
|
@ -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))
|
||||
})
|
||||
}
|
|
@ -1,11 +0,0 @@
|
|||
const m = require('mithril')
|
||||
|
||||
const Frontpage = {
|
||||
view: function() {
|
||||
return [
|
||||
m('h4.header', 'Frontpage'),
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = Frontpage
|
|
@ -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)
|
||||
})
|
||||
}
|
|
@ -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;
|
||||
}
|
5
index.mjs
Normal file
5
index.mjs
Normal file
|
@ -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)
|
||||
})
|
||||
}
|
18
log.mjs
18
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)
|
||||
|
||||
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
|
||||
}
|
||||
|
|
0
manage/.gitkeep
Normal file
0
manage/.gitkeep
Normal file
|
@ -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": {
|
||||
|
|
28
runner.mjs
28
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) {
|
||||
const close = function(err) {
|
||||
if (err) {
|
||||
log.fatal(err, 'App recorded a fatal error')
|
||||
log.event.error('App recorded a fatal error: ' + err.message)
|
||||
log.event.error('App recorded a fatal error: ' + err.message, null, function() {
|
||||
process.exit(4)
|
||||
})
|
||||
return
|
||||
}
|
||||
log.warn('App asked to be shut down')
|
||||
log.event.warn('App requested to be closed')
|
||||
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)
|
||||
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)
|
||||
log.event.error('Unknown error occured opening app: ' + err.message, null, function() {
|
||||
process.exit(3)
|
||||
})
|
||||
})
|
||||
|
|
|
@ -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
|
||||
|
|
Loading…
Reference in a new issue