RPC update

This commit is contained in:
peter
2019-08-26 00:04:32 +02:00
parent fda4fec201
commit 6d707e7572
5 changed files with 338 additions and 345 deletions
+273 -161
View File
File diff suppressed because it is too large Load Diff
+1 -1
View File
@@ -26,7 +26,7 @@
"child-process-promise": "^2.2.1", "child-process-promise": "^2.2.1",
"express": "^4.16.4", "express": "^4.16.4",
"frontblock": "^0.9.9", "frontblock": "^0.9.9",
"frontblock-generic": "latest", "frontblock-generic": "^0.28.4",
"git-cherrypicker": "0.0.3", "git-cherrypicker": "0.0.3",
"git-describe": "^4.0.4", "git-describe": "^4.0.4",
"http": "0.0.0", "http": "0.0.0",
+23 -156
View File
@@ -1,14 +1,13 @@
'use strict' 'use strict'
import * as Logger from 'log4js' import * as Logger from 'log4js'
import { socketioRPC, ConfigLoader, Plugin } from 'frontblock-generic/Plugin'; import { ConfigLoader, Plugin } from 'frontblock-generic/Plugin';
import { socketioRPC, rpcHooker, ExtendedRpcInfo } from 'frontblock-generic/RPC';
import { ErrorResponse, SuccessResponse, SubscriptionResponse } from 'frontblock-generic/Types'; import { ErrorResponse, SuccessResponse, SubscriptionResponse } from 'frontblock-generic/Types';
import express = require('express'); import express = require('express');
import http = require('http'); import http = require('http');
import bsock = require('bsock'); import bsock = require('bsock');
import { FrontblockCherryPicker } from "git-cherrypicker"
import { promises as fs } from "fs" import { promises as fs } from "fs"
import { GitUpdater, RepoFolderStatus, } from "./GitUpdater";
import * as path from "path" import * as path from "path"
import { UpdateManager } from './UpdateManger'; import { UpdateManager } from './UpdateManger';
var exec = require('child-process-promise').exec; var exec = require('child-process-promise').exec;
@@ -26,32 +25,6 @@ Logger.configure({
}) })
const logger = Logger.getLogger("admin") 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} export type AdminConf = { httpPort: number}
@@ -63,9 +36,17 @@ export type AdminConf = { httpPort: number}
* *
* The list of available plugins is published via the frontblock API and downloaded via gitea-releases * The list of available plugins is published via the frontblock API and downloaded via gitea-releases
*/ */
export class FrontblockAdmin extends ConfigLoader<AdminConf>{ export class FrontblockAdmin extends ConfigLoader<AdminConf> implements Plugin<AdminConf>{
exportRPCs(): socketioRPC[] {
throw new Error("Method not implemented.");
}
start(): void | Promise<void> {
throw new Error("Method not implemented.");
}
stop(): void | Promise<void> {
throw new Error("Method not implemented.");
}
private hookToUids:{[hookName:string]:string[]} = {}
private express private express
private httpServer private httpServer
private io = bsock.createServer() private io = bsock.createServer()
@@ -93,88 +74,22 @@ export class FrontblockAdmin extends ConfigLoader<AdminConf>{
} }
private initApis(socket){ private initApis(socket){
//Declare own functions
const rpcInfos:rpcInfo[] = [ const adminRPCs:socketioRPC[] = [{
{
owner: 'Admin',
name: 'restartWebserver',
args: 'port',
info:{
type:'call',
fn: (port:number) => { this.restartWebserver(port) }
}
},{
owner: 'Admin',
name: 'info', name: 'info',
args: '', type: 'call',
info:{ visibility: 'private',
type:'call', func: async () => rpcInfos
fn: () => { return rpcInfos } }]
}
} const rpcInfos:ExtendedRpcInfo[] = [
...rpcHooker(socket, "Admin", adminRPCs, false),
...rpcHooker(socket, "UpdateManager", this.updateManager.exportRPCs()),
...this.updateManager.getLoadedPlugins().flatMap(plugin => rpcHooker(socket, plugin.name, plugin.exportRPCs()))
] ]
//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', () => { socket.on('close', () => {
logger.info("Client disconnected") 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)
}
})
})
}) })
} }
@@ -238,12 +153,6 @@ export class FrontblockAdmin extends ConfigLoader<AdminConf>{
logger.info("Webserver stopped") logger.info("Webserver stopped")
} }
private restartWebserver(port:number){
this.stopWebserver()
this.setConfigKey("httpPort", port)
this.startWebserver()
}
private startWebsocket(){ private startWebsocket(){
try{ try{
this.io.attach(this.wsServer) this.io.attach(this.wsServer)
@@ -264,48 +173,6 @@ export class FrontblockAdmin extends ConfigLoader<AdminConf>{
logger.error(String(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() new FrontblockAdmin()
+32 -19
View File
@@ -1,6 +1,12 @@
import { parseSubResponse, parseResponse } from "frontblock-generic/Types"; import { parseSubResponse, parseResponse } from "frontblock-generic/Types";
import { ExtendedRpcInfo, UnhookFunction, callbackFunction, AsyncFunction } from "frontblock-generic/RPC";
var bsock = require('bsock') var bsock = require('bsock')
//fix args with defaults like "force = true" -> "force"
function stripAfterEquals(str:string){
return str.split("=")[0]
}
/** /**
* Dynamic library to communicate with FrontblockService remotely * Dynamic library to communicate with FrontblockService remotely
* *
@@ -8,7 +14,7 @@ var bsock = require('bsock')
* Will ask it's service for available RPCs and parse them into methods of this object * Will ask it's service for available RPCs and parse them into methods of this object
* for convenient access. * for convenient access.
*/ */
class FrontblockConfigLib{ export class FrontblockConfigLib{
private socket private socket
constructor(){ constructor(){
@@ -22,38 +28,42 @@ class FrontblockConfigLib{
private parseResponse = parseResponse private parseResponse = parseResponse
private async init(){ private async init(){
const info = await this.info() const info:ExtendedRpcInfo[] = await this.info()
for (const i of info) { info.forEach(i => {
let f: any let f: any
switch (i.info.type) { switch (i.type) {
case 'call': case 'call':
f = this.callGenerator(i.name, i.args) f = this.callGenerator(i.uniqueName, i.argNames)
break break
case 'hook': case 'hook':
f = this.hookGenerator(i.name, i.args) f = this.hookGenerator(i.uniqueName, i.argNames)
break break
case 'unhook': case 'unhook':
f = this.unhookGenerator(i.name, i.args) f = this.unhookGenerator(i.uniqueName, i.argNames)
break break
} }
if(this[i.owner] == null) if(this[i.owner] == null)
this[i.owner] = {} this[i.owner] = {}
this[i.owner][i.name] = f this[i.owner][i.name] = f
this[i.owner][i.name].bind(this) this[i.owner][i.name].bind(this)
} })
} }
async info(){ async info(){
return await this.socket.call('info') return await this.socket.call('info')
} }
private callGenerator(fnName, fnArgs): Function{ private callGenerator(fnName, fnArgs:string[]): AsyncFunction{
return eval( '( () => async ('+fnArgs+') => { return await this.socket.call("'+fnName+'", '+fnArgs+')} )()' ) const headerArgs = fnArgs.join(",")
const argParams = fnArgs.map(stripAfterEquals).join(",")
return eval( '( () => async ('+headerArgs+') => { return await this.socket.call("'+fnName+'", '+argParams+')} )()' )
} }
private hookGenerator(fnName, fnArgs): Function{ private hookGenerator(fnName, fnArgs:string[]): callbackFunction{
return eval( `( () => async (`+fnArgs+(fnArgs.length!==0?",":"")+` callback) => { const headerArgs = fnArgs.join(",")
const r = await this.socket.call("`+fnName+`", `+fnArgs+`) const argParams = fnArgs.map(stripAfterEquals).join(",")
return eval( `( () => async (`+headerArgs+(headerArgs.length!==0?",":"")+` callback) => {
const r = await this.socket.call("`+fnName+`", `+argParams+`)
const res = await this.parseSubResponse(r); const res = await this.parseSubResponse(r);
if(res.uid != null){ if(res.uid != null){
this.socket.hook(res.uid, callback) this.socket.hook(res.uid, callback)
@@ -62,13 +72,16 @@ class FrontblockConfigLib{
} )()` ) } )()` )
} }
private unhookGenerator(fnName, fnArgs): Function{ private unhookGenerator(fnName, fnArgs:string[]): UnhookFunction{
return eval( `( () => async (`+fnArgs+`) => { const headerArgs = fnArgs.join(",")
const r = await this.socket.call("`+fnName+`", `+fnArgs+`) const argParams = fnArgs.map(stripAfterEquals).join(",")
if(fnArgs.length != 1)
console.error("UnhookFunction", fnName, "specified more than one argument: ("+headerArgs+")")
return eval( `( () => async (`+headerArgs+`) => {
const r = await this.socket.call("`+fnName+`", `+argParams+`)
const res = await this.parseResponse(r) const res = await this.parseResponse(r)
console.log(res) this.socket.unhook(`+argParams+`)
if(res.uid != null)
this.socket.unhook(res.uid)
return res return res
} )()` ) } )()` )
} }
+8 -7
View File
@@ -1,4 +1,5 @@
import { Plugin, socketioRPC } from "frontblock-generic/Plugin"; import { Plugin } from "frontblock-generic/Plugin";
import { socketioRPC } from "frontblock-generic/RPC";
import { GitUpdater, RepoFolderStatus } from "./GitUpdater"; import { GitUpdater, RepoFolderStatus } from "./GitUpdater";
import { FrontblockCherryPicker } from "git-cherrypicker"; import { FrontblockCherryPicker } from "git-cherrypicker";
import * as Logger from 'log4js' import * as Logger from 'log4js'
@@ -33,32 +34,32 @@ export class UpdateManager implements Plugin{
exportRPCs(): socketioRPC[] { exportRPCs(): socketioRPC[] {
return [{ return [{
name: 'installPlugin', name: 'installPlugin',
rpc: async (name:string, force = false) => {return await this.installPlugin(name, force)}, func: async (name:string, force = false) => {return await this.installPlugin(name, force)},
type: 'call', type: 'call',
visibility: 'private' visibility: 'private'
},{ },{
name: 'startPlugin', name: 'startPlugin',
rpc: async (name:string) => {return await this.startPlugin(name)}, func: async (name:string) => {return await this.startPlugin(name)},
type: 'call', type: 'call',
visibility: 'private' visibility: 'private'
},{ },{
name: 'updatePlugin', name: 'updatePlugin',
rpc: async (name) => {return await this.updatePlugin(name)}, func: async (name) => {return await this.updatePlugin(name)},
type: 'call', type: 'call',
visibility: 'private' visibility: 'private'
},{ },{
name: 'setPluginVersion', name: 'setPluginVersion',
rpc: async (name, tag) => {return await this.setPluginVersion(name, tag)}, func: async (name, tag) => {return await this.setPluginVersion(name, tag)},
type: 'call', type: 'call',
visibility: 'private' visibility: 'private'
},{ },{
name: 'updateDashboard', name: 'updateDashboard',
rpc: async () => {return await this.updateDashboard()}, func: async () => {return await this.updateDashboard()},
type: 'call', type: 'call',
visibility: 'private' visibility: 'private'
},{ },{
name: 'getLoadedPluginNames', name: 'getLoadedPluginNames',
rpc: async () => {return await this.getLoadedPluginNames()}, func: async () => {return await this.getLoadedPluginNames()},
type: 'call', type: 'call',
visibility: 'private' visibility: 'private'
}] }]