initial commit

This commit is contained in:
peter
2019-06-17 14:25:27 +02:00
commit 369e9171ad
22 changed files with 8538 additions and 0 deletions
+75
View File
@@ -0,0 +1,75 @@
import { parseSubResponse, parseResponse } from "frontblock-generic/Service";
var bsock = require('bsock')
/**
* Dynamic library to communicate with FrontblockService remotely
*
* This will be automatically injected into the webpages served by FrontblockService
* Will ask it's service for available RPCs and parse them into methods of this object
* for convenient access.
*/
export class FrontblockConfigLib{
private socket
constructor(){
this.socket = bsock.connect(20000, 'localhost', false/*tls*/)
this.init()
}
// need this-context for eval-magic below
// DO NOT REMOVE. They're not really unused
private parseSubResponse = parseSubResponse
private parseResponse = parseResponse
private async init(){
const info = await this.info()
for (const i of info) {
let f: any
switch (i.info.type) {
case 'call':
f = this.callGenerator(i.name, i.args)
break
case 'hook':
f = this.hookGenerator(i.name, i.args)
break
case 'unhook':
f = this.unhookGenerator(i.name, i.args)
break
}
this[i.name] = f
this[i.name].bind(this)
}
}
async info(){
return await this.socket.call('info')
}
private callGenerator(fnName, fnArgs): Function{
return eval( '( () => async ('+fnArgs+') => { return await this.socket.call("'+fnName+'", '+fnArgs+')} )()' )
}
private hookGenerator(fnName, fnArgs): Function{
return eval( `( () => async (`+fnArgs+(fnArgs.length!==0?",":"")+` callback) => {
const r = await this.socket.call("`+fnName+`", `+fnArgs+`)
const res = await this.parseSubResponse(r);
if(res.uid != null){
this.socket.hook(res.uid, callback)
}
return res
} )()` )
}
private unhookGenerator(fnName, fnArgs): Function{
return eval( `( () => async (`+fnArgs+`) => {
const r = await this.socket.call("`+fnName+`", `+fnArgs+`)
const res = await this.parseResponse(r)
if(res.uid != null)
this.socket.unhook(res.uid)
return res
} )()` )
}
}
window['fb'] = new FrontblockConfigLib()
+222
View File
@@ -0,0 +1,222 @@
'use strict'
import * as Logger from 'log4js'
import { Plugin, socketioRPC } from 'frontblock-generic/Plugin';
import { ErrorResponse, SuccessResponse } from 'frontblock-generic/Service';
const pluginList = [
'../../paymentmanager/static/Plugin',
'frontblock/FrontblockLib'
]
const express = require('express')
const http = require('http')
const bsock = require('bsock')
const kfs = require("key-file-storage")('conf') //'conf' is a directory that will be generated if it doesn't exist
const logger = Logger.getLogger() // logs to STDOUT
logger.level = 'debug'
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 = {
name: string,
args: string,
info: hookRpc | unhookRPC | callRPC
}
/**
* 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{
private plugins: Plugin[]
private express
private httpServer
private io = bsock.createServer()
private wsServer = http.createServer()
constructor(){
if(!('FrontblockService' in kfs)){
logger.warn('No config file found! Generating one')
kfs.FrontblockService = { httpPort: 8080 }
}
this.initialize()
}
private async initialize(){
await this.loadPlugins()
this.plugins.forEach(plugin => plugin.start())
this.startWebsocket()
this.startWebserver()
}
private async loadPlugins(){
const promises = pluginList.map(path => {
return import(path)
})
const pluginsClasses = await Promise.all(promises)
this.plugins = pluginsClasses.map(clazz => new clazz.default())
}
private initApis(socket){
const rpcInfos:rpcInfo[] = [
{
name: 'restartWebserver',
args: 'port',
info:{
type:'call',
fn: (port:number) => { this.restartWebserver(port) }
}
},{
name: 'info',
args: '',
info:{
type:'call',
fn: () => { return rpcInfos }
}
}
]
this.plugins.forEach(plugin => {
plugin.exportRPCs().forEach(rpc => rpcInfos.push(this.rpcToRpcInfo(rpc)))
})
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
}
}
socket.on('close', () => {
logger.info("Client disconnected")
rpcInfos.forEach(rpc => {
socket.unhook(rpc.name)
})
})
}
private startWebserver(){
if(this.httpServer != null || this.express != null){
logger.warn("Webserver is already running")
return
}
let port:number = kfs.FrontblockService.httpPort
this.express = express()
this.express.use(express.static('static'))
this.httpServer = http.Server(this.express)
this.httpServer.listen(port, () => {
logger.info('listening 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.FrontblockService = { 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('websocket listening on *20000')
this.wsServer.listen(20000)
}catch(e){
logger.error(String(e))
}
}
private rpcToRpcInfo(rpc:socketioRPC):rpcInfo{
switch(rpc.type){
case 'hook':
let f = this.hookGenerator(rpc)
return {name: rpc.name, args: this.extractArgs(f(null)), info: { type: 'hook', generator: f, unhook: rpc.unhook } }
case 'unhook':
return {name: rpc.name, args: this.extractArgs(rpc.rpc), info: { type: 'unhook', fn: rpc.rpc } }
case 'call':
return {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!
*
* TODO: Maybe kill open callbacks when socket closes? See rpc.info.unhook for the appropriate function to call.
*/
hookGenerator = (rpc) => {
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) => {
if(res.uid != null){
console.log("calling "+res.uid)
socket.call(res.uid, x).catch(e => {
rpc.unhook(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()
})()
+4
View File
@@ -0,0 +1,4 @@
import { Greeter } from '../index';
test('My Greeter', () => {
expect(Greeter('Carl')).toBe('Hello Carl');
});