Files
fb-admin/src/backend/FrontblockAdmin.ts
T
2019-08-03 13:50:46 +02:00

262 lines
7.5 KiB
TypeScript

'use strict'
import * as Logger from 'log4js'
import { Plugin, socketioRPC, ConfigLoader } from 'frontblock-generic/Plugin';
import { ErrorResponse, SuccessResponse } from 'frontblock-generic/Types';
import { PluginManagerPlugin } from './PluginManager/Plugin'
import express = require('express');
import http = require('http');
import bsock = require('bsock');
import { type } from 'os';
const kfs = require("key-file-storage").default('kfs')
Logger.configure({
appenders:
{
"frontblock-admin": { type: 'stdout' },
//app: { type: 'file', filename: 'application.log' }
},
categories:
{
default: { appenders: [ 'frontblock-admin' ], level: 'debug' }
}
})
const logger = Logger.getLogger("frontblock-admin")
type hookRPC =
{
type: 'hook',
generator: (socket) => Function,
unhook:(uid:string)=>Promise<ErrorResponse|SuccessResponse>
}
type unhookRPC =
{
type: 'unhook',
fn: Function
}
type callRPC =
{
type: 'call',
fn: Function
}
export type rpcInfo =
{
owner: string,
name: string,
args: string,
info: hookRPC | unhookRPC | callRPC
}
export type AdminConf = { httpPort: number}
/**
* FrontblockAdmin
*
* The customer-facing dynamic component of the customer backend
* Supports (un)loading plugins which will be communicated to its library component (See FrontblockLib.ts and the info() RPC)
*
* The list of available plugins is published via the frontblock API and downloaded via gitea-releases
*/
export class FrontblockAdmin extends ConfigLoader<AdminConf>{
private hookToUids:{[hookName:string]:string[]} = {}
private express
private httpServer
private io = bsock.createServer()
private wsServer = http.createServer()
private pm: PluginManagerPlugin
constructor(){
super("FrontblockAdmin")
this.pm = new PluginManagerPlugin()
this.initialize()
}
getDefaultConfig(): AdminConf {
return {httpPort: 8080}
}
private async initialize(){
this.startWebsocket()
this.startWebserver()
}
private initApis(socket){
//Declare own functions
const rpcInfos:rpcInfo[] = [
{
owner: 'Admin',
name: 'restartWebserver',
args: 'port',
info:{
type:'call',
fn: (port:number) => { this.restartWebserver(port) }
}
},{
owner: 'Admin',
name: 'info',
args: '',
info:{
type:'call',
fn: () => { return rpcInfos }
}
}
]
//translate RPCs to socket-bound function metadata
const loadedPlugins = this.pm.getLoadedPlugins()
for(const name in loadedPlugins){
loadedPlugins[name].exportRPCs().forEach(rpc => {
const info = this.rpcToRpcInfo(name, rpc)
rpcInfos.push(info)
})
}
//Hook up all the functions
for(const api of rpcInfos){
switch(api.info.type){
case 'call':
socket.hook(api.name, api.info.fn)
break
case 'hook':
const hook = api.info.generator(socket)
hook.bind(this)
socket.hook(api.name, hook)
break
case 'unhook':
socket.hook(api.name, api.info.fn)
break
}
}
//initialize the lists of open hooks
rpcInfos
.filter(rpc => rpc.info.type === "hook")
.forEach(hook => {
this.hookToUids[hook.name] = []
})
//On close, unhook open hooks
socket.on('close', () => {
logger.info("Client disconnected")
rpcInfos.forEach((rpc) => {
if(this.hookToUids[rpc.name] == null)
return
this.hookToUids[rpc.name].forEach((uid) => {
if(rpc.info.type === "hook"){
logger.info("Closing consumer `"+uid+"` owned by `"+rpc.name+"`")
rpc.info.unhook(uid)
}
})
})
})
}
private startWebserver(){
if(this.httpServer != null || this.express != null){
logger.warn("Webserver is already running")
return
}
let port:number = this.conf.httpPort
this.express = express()
this.express.use(express.static('static'))
this.httpServer = new http.Server(this.express)
this.httpServer.listen(port, () => {
logger.info('Admin panel listening for HTTP on *'+port)
})
}
private stopWebserver(){
if(this.httpServer == null || this.express == null){
logger.warn("Webserver is not running")
return
}
this.httpServer.close()
this.httpServer = null
this.express = null
logger.info("Webserver stopped")
}
private restartWebserver(port:number){
this.stopWebserver()
kfs[this.name+".conf"] = <AdminConf>{ httpPort: port }
this.startWebserver()
}
private startWebsocket(){
try{
this.io.attach(this.wsServer)
this.io.on('socket', (socket) => {
logger.info("New Websocket connection on port", socket.port)
const handleError = (e: any) => {
logger.info("Websocket closing", String(e))
socket.close()
}
socket.on('error', handleError)
this.initApis(socket)
})
logger.info('Admin websocket listening on *20000')
this.wsServer.listen(20000)
}catch(e){
logger.error(String(e))
}
}
private rpcToRpcInfo(owner:string, rpc:socketioRPC):rpcInfo{
switch(rpc.type){
case 'hook':
let f = this.hookGenerator(rpc)
return {owner: owner, name: rpc.name, args: this.extractArgs(f(null)), info: { type: 'hook', generator: f, unhook: rpc.unhook } }
case 'unhook':
return {owner: owner, name: rpc.name, args: this.extractArgs(rpc.rpc), info: { type: 'unhook', fn: rpc.rpc } }
case 'call':
return {owner: owner, name: rpc.name, args: this.extractArgs(rpc.rpc), info: { type: 'call', fn: rpc.rpc } }
}
}
/**
* Generates RPC hooks which support a callback.
*
* Note callbacks *need* to accept a singular argument and they have to be the last parameter!
*/
hookGenerator = (rpc:socketioRPC) => {
const argsArr = this.extractArgs(rpc.rpc).split(',')
argsArr.pop()
const args = argsArr.join(',')
return eval(`(socket) => async (`+args+`) => {
const res = await rpc.rpc(`+args+(args.length!==0?',':'')+` (x) => {
socket.call(res.uid, x).catch(e => {
logger.debug(String(e))
})
})
if(res.uid != null){
this.hookToUids[rpc.name].push(res.uid)
}
return res
}`)
}
private extractArgs(f:Function):string{
let fn = String(f)
let args = fn.substr(0, fn.indexOf(")"))
args = args.substr(fn.indexOf("(")+1)
return args
}
}
(async() => {
new FrontblockAdmin()
})()