318 lines
9.5 KiB
TypeScript
318 lines
9.5 KiB
TypeScript
'use strict'
|
|
|
|
import * as Logger from 'log4js'
|
|
import { socketioRPC, ConfigLoader, Plugin } from 'frontblock-generic/Plugin';
|
|
import { ErrorResponse, SuccessResponse, SubscriptionResponse } from 'frontblock-generic/Types';
|
|
import express = require('express');
|
|
import http = require('http');
|
|
import bsock = require('bsock');
|
|
import { FrontblockCherryPicker } from "git-cherrypicker"
|
|
import { promises as fs } from "fs"
|
|
import { GitUpdater, RepoFolderStatus, } from "./GitUpdater";
|
|
import * as path from "path"
|
|
import { UpdateManager } from './UpdateManger';
|
|
|
|
const kfs = require("key-file-storage").default('kfs')
|
|
|
|
Logger.configure({
|
|
appenders:
|
|
{
|
|
"admin": { type: 'stdout' },
|
|
//app: { type: 'file', filename: 'application.log' }
|
|
},
|
|
categories:
|
|
{
|
|
default: { appenders: [ 'admin' ], level: 'debug' }
|
|
}
|
|
})
|
|
const logger = Logger.getLogger("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 updateManager:UpdateManager = new UpdateManager()
|
|
|
|
constructor(){
|
|
super("FrontblockAdmin")
|
|
|
|
this.initialize()
|
|
}
|
|
|
|
getDefaultConfig(): AdminConf {
|
|
return {httpPort: 8080}
|
|
}
|
|
|
|
private async initialize(){
|
|
await this.updateManager.updateAdmin()
|
|
await this.updateManager.updateDashboard()
|
|
await this.updateManager.updateFrontblockLib()
|
|
|
|
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:Plugin[] = [this.updateManager, ...this.updateManager.getLoadedPlugins()]
|
|
loadedPlugins.forEach(plugin => plugin.exportRPCs().forEach(rpc => {
|
|
const info = this.rpcToRpcInfo(plugin.name, rpc)
|
|
rpcInfos.push(info)
|
|
}))
|
|
|
|
//Hook up all the functions
|
|
for(const api of rpcInfos){
|
|
switch(api.info.type){
|
|
case 'call':
|
|
try{
|
|
socket.unhook(api.name)
|
|
}catch(e){
|
|
|
|
}
|
|
socket.hook(api.name, api.info.fn)
|
|
break
|
|
case 'hook':
|
|
const hook = api.info.generator(socket)
|
|
hook.bind(this)
|
|
try{
|
|
socket.unhook(api.name)
|
|
}catch(e){
|
|
|
|
}
|
|
socket.hook(api.name, hook)
|
|
break
|
|
case 'unhook':
|
|
try{
|
|
socket.unhook(api.name)
|
|
}catch(e){
|
|
|
|
}
|
|
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'))
|
|
|
|
/**
|
|
* get the compiled FrontendPlugins.js
|
|
*/
|
|
this.express.get('/plugins/:id'+".js", async (request, response) => {
|
|
const pth = path.resolve("plugins/"+request.params.id, "FrontendPlugin.js");
|
|
logger.info("Loading frontend plugin from fs", pth)
|
|
const file = await fs.readFile(pth)
|
|
const frontend = file.toString()
|
|
|
|
response.status(200)
|
|
response.set('Content-Type', 'application/javascript')
|
|
response.send(frontend)
|
|
})
|
|
|
|
/**
|
|
* serve the index.html from the static folder
|
|
*/
|
|
this.express.get("/", (request, response) => {
|
|
response.status(200)
|
|
response.sendFile('index.html');
|
|
})
|
|
|
|
/**
|
|
* redirect all the other traffic to the single
|
|
* page app to the main entry point from where
|
|
* a webpacked and rolled up index.html is serverd
|
|
* and angular takes over routing
|
|
*/
|
|
this.express.get("*", (request, response) => {
|
|
response.status(301)
|
|
response.redirect('/')
|
|
})
|
|
|
|
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): (socket)=>(...args:any[])=>Promise<SubscriptionResponse> => {
|
|
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
|
|
}
|
|
}
|
|
|
|
new FrontblockAdmin()
|
|
|
|
|
|
process.on( 'SIGINT', function() {
|
|
logger.info( "Gracefully shutting down from SIGINT (Ctrl-C)" );
|
|
// some other closing procedures go here
|
|
process.exit( );
|
|
}) |